diff --git a/.github/workflows/build-staging-render.yml b/.github/workflows/build-staging-render.yml
deleted file mode 100644
index 7e65a518a9..0000000000
--- a/.github/workflows/build-staging-render.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-name: _STAGING RENDER
-
-on:
- workflow_dispatch:
- schedule:
- - cron: '36 5-20 * * 1-5'
-
-jobs:
- build-bundle:
- uses: ./.github/workflows/build-bundle.yml
- secrets: inherit
- with:
- gh_ref: "staging-render"
- build_wasm: "yes"
- build_storybook: "yes"
diff --git a/.gitignore b/.gitignore
index 8586839ba0..dc4861f51f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
+/frontend/playwright/ui/visual-specs/
/frontend/text-editor/src/wasm/
/frontend/dist/
/frontend/npm-debug.log
@@ -84,3 +85,4 @@
/**/node_modules
/**/.yarn/*
/.pnpm-store
+/.vscode
diff --git a/CHANGES.md b/CHANGES.md
index 380a52feeb..149fc49f05 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,15 @@
# CHANGELOG
+## 2.17.0 (Unreleased)
+
+### :boom: Breaking changes & Deprecations
+
+### :rocket: Epics and highlights
+
+### :sparkles: New features & Enhancements
+
+### :bug: Bugs fixed
+
## 2.16.0 (Unreleased)
### :boom: Breaking changes & Deprecations
@@ -9,6 +19,54 @@
### :sparkles: New features & Enhancements
- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714)
+- Add "Delete group" option to the assets panel context menu for components, colors and typographies (by @FairyPigDev) [Github #9141](https://github.com/penpot/penpot/issues/9141)
+- Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree in the Layers sidebar (by @MilosM348) [Github #9179](https://github.com/penpot/penpot/pull/9179)
+- Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328)
+- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987)
+- Add loader feedback while importing and exporting files (by @moorsecopers99) [Github #9024](https://github.com/penpot/penpot/pull/9024)
+- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912)
+- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
+- Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391)
+- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
+- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
+- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474)
+- Copy and paste entire rows in existing table (by @bittoby) [Github #8498](https://github.com/penpot/penpot/pull/8498)
+- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
+- Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653)
+- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568)
+- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713)
+- Add drag-to-change for numeric inputs in workspace sidebar (by @RenzoMXD) [Github #8536](https://github.com/penpot/penpot/pull/8536)
+- Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790)
+- Save and restore selection state in undo/redo (by @eureka0928) [Github #6007](https://github.com/penpot/penpot/issues/6007)
+- Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790)
+- Add per-group add button for typographies (by @eureka0928) [Github #5275](https://github.com/penpot/penpot/issues/5275)
+- Add Find & Replace for text content and layer names (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108)
+- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773)
+- Make links in comments clickable (by @eureka0928) [Github #1602](https://github.com/penpot/penpot/issues/1602)
+- Add visibility toggle for strokes (by @eureka0928) [Github #7438](https://github.com/penpot/penpot/issues/7438)
+- Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [Github #2572](https://github.com/penpot/penpot/issues/2572)
+- Add Paste to replace (Cmd+Shift+V) to replace the selected shape with clipboard contents (by @eureka0928) [Github #4240](https://github.com/penpot/penpot/issues/4240)
+- Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794)
+- Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358)
+- Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647)
+- Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910)
+- Add customizable colors for ruler guides (by @Dexterity104) [Github #5199](https://github.com/penpot/penpot/issues/5199)
+- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913)
+- Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270)
+- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311)
+- Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653)
+- Add a search bar to filter board size presets (by @eureka0928) [Github #4658](https://github.com/penpot/penpot/issues/4658)
+- Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027)
+- Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806)
+- Preserve vector content when pasting SVG from external tools such as Inkscape (by @RenzoMXD) [Github #9182](https://github.com/penpot/penpot/pull/9182)
+- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts (by @RenzoMXD) [Github #9063](https://github.com/penpot/penpot/pull/9063)
+- Add pixel grid color picker in viewport settings (by @Yakehira) [Github #7750](https://github.com/penpot/penpot/issues/7750)
+- Add HEX, HSB and HSL support to the color picker with a model switcher that persists across sessions (by @edwin-rivera-dev) [Github #9133](https://github.com/penpot/penpot/issues/9133)
+- Show specific invitation-link error messages for expired, email-mismatch and invalid token cases [Github #9220](https://github.com/penpot/penpot/issues/9220)
+- Show detailed messages on file import errors to help diagnose why a file could not be imported (by @jsdevninja) [Github #9004](https://github.com/penpot/penpot/issues/9004)
+- Add read-only preview mode for saved versions — click a version name to open a dedicated preview view (by @wdeveloper16) [Github #8976](https://github.com/penpot/penpot/issues/8976)
+- Add clipboard read/write permissions to the plugin system (by @wdeveloper16) [Github #9053](https://github.com/penpot/penpot/issues/9053)
+- Add new numeric inputs for token management on the right sidebar [Taiga #12109](https://tree.taiga.io/project/penpot/us/12109?milestone=513226)
### :bug: Bugs fixed
@@ -23,7 +81,56 @@
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
- Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035)
- Fix themes modal height [Taiga #14046](https://tree.taiga.io/project/penpot/issue/14046)
-
+- Fix layers panel rename input showing the default type name instead of the saved layer name (by @jack-stormentswe) [Github #9231](https://github.com/penpot/penpot/pull/9231)
+- Suppress browser context menu on right-click in workspace sidebars while preserving it on text inputs (by @sujyotraut) [Github #5127](https://github.com/penpot/penpot/issues/5127)
+- Fix release notes modal appearing behind the dashboard sidebar (by @ciaokitty) [Github #8296](https://github.com/penpot/penpot/issues/8296)
+- Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure (by @thomascolden585-svg) [Github #9092](https://github.com/penpot/penpot/issues/9092)
+- Fix imported stroke-only SVG paths losing their rounded join when split into adjacent subpaths (by @Chrissi2812) [Github #5283](https://github.com/penpot/penpot/issues/5283)
+- Fix plugin API `library.connectLibrary()` not returning a Promise when the plugin lacks `library:write` permission (by @boskodev790) [Github #9158](https://github.com/penpot/penpot/pull/9158)
+- Fix LDAP provider schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration (by @boskodev790) [Github #9165](https://github.com/penpot/penpot/pull/9165)
+- Fix `login-with-ldap` silently dropping the error message when LDAP is not initialized (typo `:hide` → `:hint`) (by @boskodev790) [Github #9159](https://github.com/penpot/penpot/pull/9159)
+- Fix plugin API `applyToken()` / `applyToShapes()` / `applyToSelected()` rejecting JS-array attribute lists (by @brunopbezerra) [Github #9162](https://github.com/penpot/penpot/issues/9162)
+- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored in the OIDC callback (by @GeekClassy) [Github #9108](https://github.com/penpot/penpot/issues/9108)
+- Fix crash in share-link viewer when a team member's email is missing `@` or has no domain TLD (by @boskodev790) [Github #9120](https://github.com/penpot/penpot/pull/9120)
+- Fix crash when pasting a component with variants from an external shared library into a file that uses that library (by @FairyPigDev) [Github #8144](https://github.com/penpot/penpot/issues/8144)
+- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled (by @TheAifam5) [Github #8877](https://github.com/penpot/penpot/issues/8877)
+- Fix Copy as SVG to produce a valid document for multi-shape selections and use `image/svg+xml` MIME type (by @RenzoMXD) [Github #9066](https://github.com/penpot/penpot/pull/9066)
+- Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947)
+- Preserve OpenType variant name table for custom fonts in the dashboard (by @rutherfordcraze) [Github #8924](https://github.com/penpot/penpot/issues/8924)
+- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582)
+- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526)
+- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
+- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
+- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639)
+- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786)
+- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841)
+- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
+- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
+- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
+- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
+- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
+- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
+- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris099) [Github #8577](https://github.com/penpot/penpot/issues/8577)
+- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628)
+- Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901)
+- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959)
+- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960)
+- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984)
+- Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977)
+- Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979)
+- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990)
+- Allow deleting the profile avatar after uploading (by @moorsecopers99) [Github #9067](https://github.com/penpot/penpot/issues/9067)
+- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516)
+- Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090)
+- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137)
+- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` (by @axelseis) [Github #8409](https://github.com/penpot/penpot/issues/8409)
+- Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479)
+- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057)
+- Fix restore-deleted-team-files failing due to a typo in the reduce accumulator (by @Dexterity104) [Github #9241](https://github.com/penpot/penpot/issues/9241)
+- Fix internal error on layer prev/next sibling selection (by @jsdevninja) [Github #9003](https://github.com/penpot/penpot/issues/9003)
+- Fix tooltip appearing two times when nested elements [Github #9031](https://github.com/penpot/penpot/issues/9031)
+- Fix broken update library notification link in the UI [Github #9070](https://github.com/penpot/penpot/issues/9070)
+- Fix plugin API `ShapeBase.component()` returning the outermost component instead of the immediate component in case of nested component instances [Github #9183](https://github.com/penpot/penpot/issues/9183)
## 2.15.0 (Unreleased)
@@ -39,7 +146,6 @@
- Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041)
- Fix Plugin API token methods rejecting JS array of strings [Github #9162](https://github.com/penpot/penpot/issues/9162)
-
## 2.14.4
### :bug: Bugs fixed
@@ -48,7 +154,6 @@
- Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122)
- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927)
-
## 2.14.3
### :sparkles: New features & Enhancements
@@ -79,7 +184,6 @@
- Fix typo `:podition` in swap-shapes grid cell
- Fix multiple selection on shapes with token applied to stroke color
-
## 2.14.2
### :sparkles: New features & Enhancements
@@ -102,7 +206,6 @@
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
-
## 2.14.1
### :sparkles: New features & Enhancements
@@ -126,7 +229,6 @@
- Ensure path content is always PathData when saving
- Fix error when get-parent-with-data encounters non-Element nodes
-
## 2.14.0
### :boom: Breaking changes & Deprecations
@@ -196,6 +298,7 @@
## 2.13.0
### :heart: Community contributions (Thank you!)
+
- Add 'page' special shapeId to MCP export_shape tool for full-page snapshots [Github #8689](https://github.com/penpot/penpot/issues/8689)
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d733ea5c7a..532413194d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -63,12 +63,11 @@ Advisories](https://github.com/penpot/penpot/security/advisories)
1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco)
below. All code patches must include a `Signed-off-by` line.
2. **Discuss before building** — open a [GitHub
- Issue](https://github.com/penpot/penpot/issues) or start a [GitHub
- Discussion](https://github.com/penpot/penpot/discussions) before starting
- work on a new feature or significant change. For planned features on the
- roadmap, reference the corresponding Taiga story. No PR will be accepted
- without prior discussion, whether it is a new feature, a planned one, or a
- quick win.
+ Issue](https://github.com/penpot/penpot/issues) before starting work on
+ a new feature or significant change. For planned features on the roadmap,
+ reference the corresponding Taiga story. Do not expect your contribution
+ to be accepted if you submit it without prior discussion — this applies
+ to new features, planned features, and quick wins alike.
3. **Bug fixes** — you may submit a PR directly, but we still recommend
filing an issue first so we can track it independently of your fix.
4. **Format and lint** — run the checks described in
@@ -136,7 +135,11 @@ refactor/layout-sizing
### Review process
-- Maintainers review PRs when time permits. Please be patient.
+- We are a small team and maintainers juggle reviews alongside other
+ tasks. Please do not expect your code to be reviewed instantly.
+- Reviews are handled in dedicated blocks of time, usually in the order
+ PRs arrive. It may take a few days to get a first review, especially
+ when urgent tasks come up.
- Address review feedback by **pushing new commits** — do not
force-push during review, as it breaks comment threads.
- PRs require at least **one approval** before merge.
diff --git a/README.md b/README.md
index 07190bcb29..fd681f9afc 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,56 @@
+
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
- Website • - User Guide • - Learning Center • - Community + Website • + User Guide • + Learning Center • + Community
- Youtube • - Peertube • - Linkedin • - Instagram • - Mastodon • - Bluesky • - X - + Youtube • + Peertube • + Linkedin • + Instagram • + Mastodon • + Bluesky • + X
-
-
-
-
-
-
-
-
-
|
+
+
+
+
+
|
+
|
+
+
+
+
+
|
+
manifest.js
{
"name": "Plugin name",
"description": "Plugin description",
- "code": "/plugin.js",
- "icon": "/icon.png",
+ "version": 2,
+ "code": "plugin.js",
+ "icon": "icon.png",
"permissions": [
"content:read",
"content:write",
@@ -234,6 +235,13 @@ Now that everything is in place you need a manifest.js
}
```
+
+Use "version": 2 when your
+code and icon values
+are relative paths. Version 2 resolves these assets from the manifest location.
+If omitted, Penpot treats the manifest as version 1.
+
+
### Icon
The plugin icon must be an image file. All image formats are valid, so you can use whichever format works best for your needs. Although there is no specific size requirement, it is recommended that the icon be 56x56 pixels in order to ensure its optimal appearance across all devices.
diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md
index abfbc508b1..ea993640a9 100644
--- a/docs/plugins/getting-started.md
+++ b/docs/plugins/getting-started.md
@@ -131,6 +131,7 @@ The manifest.json file contains the basic infor
{
"name": "Your plugin name",
"description": "Your plugin description",
+ "version": 2,
"code": "plugin.js",
"icon": "Your icon",
"permissions": [
@@ -147,6 +148,12 @@ The manifest.json file contains the basic infor
}
```
+
+Set "version": 2 in your
+manifest.json if you use relative paths for
+code or icon.
+
+
#### Properties
- **Name and description**: your plugin's basic information, which will be displayed in the plugin manager modal.
diff --git a/docs/technical-guide/configuration.md b/docs/technical-guide/configuration.md
index ad4579fcde..4c70936dc7 100644
--- a/docs/technical-guide/configuration.md
+++ b/docs/technical-guide/configuration.md
@@ -242,6 +242,16 @@ register with another method.
PENPOT_FLAGS: [...] enable-oidc-registration
```
+__Since version 2.16.0__
+
+Allows customising the label shown on the OIDC login button (defaults to "OpenID").
+
+```bash
+# Frontend
+PENPOT_OIDC_NAME:
+```
+
+
#### Azure Active Directory using OpenID Connect
Allows integrating with Azure Active Directory as authentication provider:
diff --git a/docs/technical-guide/developer/devenv.md b/docs/technical-guide/developer/devenv.md
index facb810dd8..0443466e03 100644
--- a/docs/technical-guide/developer/devenv.md
+++ b/docs/technical-guide/developer/devenv.md
@@ -161,6 +161,59 @@ If an exception is raised or an error occurs when code is reloaded, just use
(repl/refresh-all) to finish loading the code correctly and then use
(restart) again.
+
+### MCP Server
+
+To set up the MCP server local development environment it's needed some additional steps.
+
+### Activate the MCP features variables
+
+Create or modify the file `frontend/resources/public/js/config.js` and add (or modify) the `penpotFlags` to add the following:
+
+```javascript
+var penpotFlags = "enable-mcp enable-access-tokens"
+```
+
+This will enable the MCP in the workspace and in the user settings profile.
+
+### Start the DEVENV
+
+Start as usual the development environment
+
+```
+./manage.sh start-devenv
+```
+
+Once the TMUX is showing, create a new tmux tab (Ctrl+b c). And in the new tab run:
+
+```bash
+cd mcp
+pnpm run bootstrap:multi-user
+```
+
+This will start the MCP server and the multi-user plugin that will be loaded automaticaly by Penpot.
+
+There is a NGINX proxy that makes a proxy-pass from outside the docker container so you don't need to remember the ports it's using.
+
+### Configure the MCP in your tool
+
+You can use the instructions in [/mcp/#remote-mcp-in-5-steps](/mcp/#remote-mcp-in-5-steps) to setup the server.
+
+Warning: by default Cursor won't support HTTPS with a self-signed certificate. In order to work around this issue please use the port `3450` that uses an standard `http` protocol
+
+An example of your cursor configuration can be:
+
+```javascript
+{
+ "mcpServers": {
+ "penpot-devenv": {
+ "url": "http://localhost:3450/mcp/stream?userToken=TOKEN",
+ "type": "http"
+ }
+ }
+}
+```
+
## Email
To test email sending, the devenv includes [MailCatcher](https://mailcatcher.me/),
diff --git a/docs/technical-guide/developer/ui.md b/docs/technical-guide/developer/ui.md
index 2a6fb81102..a9bdfaa6f3 100644
--- a/docs/technical-guide/developer/ui.md
+++ b/docs/technical-guide/developer/ui.md
@@ -199,6 +199,7 @@ Remember that nesting selector increases specificity, and it's usually not neede
fill: var(--icon-color);
}
```
+
Note: Thanks to CSS Modules, identical class names defined in different files are scoped locally and do not cause naming collisions.
### Use CSS logical properties
@@ -228,17 +229,21 @@ Note: Although `width` and `height` are physical properties, their use is allowe
Avoid hardcoded values like `px`, `rem`, or raw SASS variables `($s-*)`. Use semantic, named variables provided by the Design System to ensure consistency and scalability.
#### Spacing (margins, paddings, gaps...)
+
Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefined and approved by the design team — **do not add or modify values without design approval**.
#### Fixed dimensions
+
For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them:
```scss
@use "ds/_sizes.scss" as *;
```
+
Note: Since these values haven't been semantically defined yet, we’re temporarily using SASS variables instead of named CSS custom properties.
#### Border Widths
+
Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import:
```scss
@@ -288,16 +293,16 @@ Replace plain text tags with `text*` or `heading*` components from the Design Sy
```clojure
...
[app.main.ui.ds.foundations.typography :as t]
- [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
+ [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
...
[:> heading* {:level 2
:typography t/headline-medium
- :class (stl/css :modal-title)}
+ :class (stl/css :modal-title)}
title]
- [:> text* {:as "div"
- :typography t/body-medium
+ [:> text* {:as "div"
+ :typography t/body-medium
:class (stl/css :modal-content)}
"Content"]
```
@@ -308,11 +313,12 @@ When applying typography in SCSS, use the proper mixin from the Design System.
```scss
.class {
- @include headlineLargeTypography;
+ @include headline-large-typography;
}
```
✅ **DO: Use the DS mixin**
+
```scss
@use "ds/typography.scss" as t;
@@ -320,10 +326,10 @@ When applying typography in SCSS, use the proper mixin from the Design System.
@include t.use-typography("body-small");
}
```
+
You can find the full list of available typography tokens in [Storybook](https://design.penpot.app/storybook/?path=/docs/foundations-typography--docs).
If the design you are implementing doesn't match any of them, ask a designer.
-
### Use custom properties within components
Reduce the need for one-off SASS variables by leveraging [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) in your component styles. This keeps component theming flexible and composable.
@@ -664,7 +670,6 @@ We use three **levels of tokens**:
We can leverage component tokens to easily implement variants as explained [here](/technical-guide/developer/ui/#use-custom-properties-within-components).
-
### Using icons and SVG assets
Please refer to the Storybook [documentation for icons](https://hourly.penpot.dev/storybook/?path=/docs/foundations-assets-icon--docs) and other [SVG assets](https://hourly.penpot.dev/storybook/?path=/docs/foundations-assets-rawsvg--docs) (logos, illustrations, etc.).
diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs
index 526ae77380..798a9c3f44 100644
--- a/exporter/src/app/browser.cljs
+++ b/exporter/src/app/browser.cljs
@@ -47,6 +47,19 @@
[page ms]
(.waitForTimeout ^js page ms))
+(defn wait-for-fonts
+ "Wait until the browser has finished loading all fonts"
+ ([page] (wait-for-fonts page nil))
+ ([page {:keys [timeout] :or {timeout 15000}}]
+ (-> (.waitForFunction ^js page
+ "() => document.fonts && document.fonts.status === 'loaded'"
+ nil
+ #js {:timeout timeout})
+ (p/catch (fn [cause]
+ (l/warn :hint "wait-for-fonts timed out; continuing anyway"
+ :cause (ex-message cause))
+ (p/resolved nil))))))
+
(defn wait-for
([locator] (wait-for locator nil))
([locator {:keys [state timeout] :or {state "visible" timeout 10000}}]
diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs
index f0f655c498..e981856da4 100644
--- a/exporter/src/app/handlers/resources.cljs
+++ b/exporter/src/app/handlers/resources.cljs
@@ -36,7 +36,7 @@
{:path path
:mtype (mime/get type)
:name name
- :filename (str/concat (str/slug name) (mime/get-extension type))
+ :filename (str/concat (str/replace name #"[\\/:*?\"<>|]" "_") (mime/get-extension type))
:id task-id}))
(defn create-zip
diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs
index 6b9dbcb4b9..63276ee48a 100644
--- a/exporter/src/app/renderer/bitmap.cljs
+++ b/exporter/src/app/renderer/bitmap.cljs
@@ -47,6 +47,7 @@
;; navigate to the page and perform basic setup
(bw/nav! page (str uri))
(bw/sleep page 1000) ; the good old fix with sleep
+ (bw/wait-for-fonts page)
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
;; take the screnshot of requested objects, one by one
diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs
index edfdcda1b1..bdfd8c6dc5 100644
--- a/exporter/src/app/renderer/pdf.cljs
+++ b/exporter/src/app/renderer/pdf.cljs
@@ -66,6 +66,7 @@
(sync-page-size! dom)
(bw/screenshot dom {:full-page? true})
(bw/sleep page 2000) ; the good old fix with sleep
+ (bw/wait-for-fonts page)
(bw/pdf page {:path path})
path)))
diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs
index 71da424fb3..135edee8d0 100644
--- a/exporter/src/app/renderer/svg.cljs
+++ b/exporter/src/app/renderer/svg.cljs
@@ -338,6 +338,7 @@
;; navigate to the page and perform basic setup
(bw/nav! page (str uri))
(bw/sleep page 1000) ; the good old fix with sleep
+ (bw/wait-for-fonts page)
;; take the screnshot of requested objects, one by one
(p/run (partial render-object page) objects)
diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md
index 0e33a32f30..681528b4f3 100644
--- a/frontend/AGENTS.md
+++ b/frontend/AGENTS.md
@@ -329,6 +329,31 @@ CSS modules pattern):
- [ ] Selectors are flat (no deep nesting).
+### Translations (`tr`) and Memoization
+
+`(tr "some.key")` resolves the translation string from the **currently active
+locale at call time**. This has two consequences:
+
+- **Never call `(tr ...)` at namespace level** (inside a `def` or `defonce`).
+ Doing so would freeze the label to the locale active at module load time and
+ break runtime language switching.
+- **Always call `(tr ...)` at render time** — either directly in the component
+ body or inside a `mf/with-memo` / `mf/use-memo` block.
+
+When a component renders a **static list of options** whose labels come from
+`(tr ...)` (e.g. radio button options, select options), wrap the vector in
+`mf/with-memo []` with no dependencies. This ensures the vector and its
+`(tr ...)` calls are evaluated once per component mount instead of on every
+render, while still respecting the render-time requirement:
+
+```clojure
+(let [options (mf/with-memo []
+ [{:value "top" :label (tr "some.key.top")}
+ {:value "center" :label (tr "some.key.center")}
+ {:value "bottom" :label (tr "some.key.bottom")}])]
+ ...)
+```
+
### Performance Macros (`app.common.data.macros`)
Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript:
diff --git a/frontend/package.json b/frontend/package.json
index 564f2bf0ca..82047594c1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -31,7 +31,7 @@
"fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w",
"lint:clj": "clj-kondo --parallel --lint ../common/src src/",
"lint:js": "exit 0",
- "lint:scss": "exit 0",
+ "lint:scss": "pnpx stylelint '{src,resources}/**/*.scss'",
"build:test": "clojure -M:dev:shadow-cljs compile test",
"test": "pnpm run build:wasm && pnpm run build:test && node target/tests/test.js",
"test:storybook": "vitest run --project=storybook",
@@ -94,6 +94,7 @@
"postcss": "^8.5.8",
"postcss-clean": "^1.2.2",
"postcss-modules": "^6.0.1",
+ "postcss-scss": "^4.0.9",
"prettier": "3.8.1",
"pretty-time": "^1.1.0",
"prop-types": "^15.8.1",
@@ -111,6 +112,10 @@
"source-map-support": "^0.5.21",
"storybook": "10.3.5",
"style-dictionary": "5.0.0-rc.1",
+ "stylelint": "^17.4.0",
+ "stylelint-config-standard-scss": "^17.0.0",
+ "stylelint-scss": "^7.0.0",
+ "stylelint-use-logical-spec": "^5.0.1",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.2",
"tinycolor2": "^1.6.0",
diff --git a/frontend/packages/mousetrap/index.js b/frontend/packages/mousetrap/index.js
index 5a0bc3e0bc..12bcbab1b9 100644
--- a/frontend/packages/mousetrap/index.js
+++ b/frontend/packages/mousetrap/index.js
@@ -187,6 +187,14 @@ function _addEvent(object, type, callback) {
*/
function _characterFromEvent(e) {
+ // Numpad digits as "num0".."num9" — keeps them separate from main-row bindings across NumLock states and event types.
+ if (e.code && e.code.indexOf('Numpad') === 0) {
+ var suffix = e.code.substring(6);
+ if (suffix.length === 1 && suffix >= '0' && suffix <= '9') {
+ return 'num' + suffix;
+ }
+ }
+
// for keypress events we should return the character as is
if (e.type == 'keypress') {
var character = String.fromCharCode(e.which);
diff --git a/frontend/playwright/data/workspace/get-file-fragment-tokens.json b/frontend/playwright/data/workspace/get-file-fragment-tokens.json
index 128f45d28d..69061c79eb 100644
--- a/frontend/playwright/data/workspace/get-file-fragment-tokens.json
+++ b/frontend/playwright/data/workspace/get-file-fragment-tokens.json
@@ -186,7 +186,8 @@
},
"~:fills": [
{
- "~:fill-color": "#7f9cf5"
+ "~:fill-color": "#7f9cf5",
+ "~:fill-opacity": 1
}
],
"~:flip-x": null,
@@ -235,7 +236,8 @@
"~:letter-spacing": "0",
"~:fills": [
{
- "~:fill-color": "#ffffff"
+ "~:fill-color": "#ffffff",
+ "~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
@@ -257,7 +259,8 @@
"~:letter-spacing": "0",
"~:fills": [
{
- "~:fill-color": "#ffffff"
+ "~:fill-color": "#ffffff",
+ "~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
@@ -328,7 +331,8 @@
"~:y2": 37.33333456516266,
"~:fills": [
{
- "~:fill-color": "#ffffff"
+ "~:fill-color": "#ffffff",
+ "~:fill-opacity": 1
}
],
"~:x2": 86.60417175292969,
@@ -445,7 +449,8 @@
},
"~:fills": [
{
- "~:fill-color": "#ffffff"
+ "~:fill-color": "#ffffff",
+ "~:fill-opacity": 1
}
],
"~:flip-x": null,
diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js
index f7e4df2582..4dee04f18e 100644
--- a/frontend/playwright/ui/pages/DashboardPage.js
+++ b/frontend/playwright/ui/pages/DashboardPage.js
@@ -147,6 +147,7 @@ export class DashboardPage extends BaseWebSocketPage {
"get-projects?team-id=*",
"dashboard/get-projects-full.json",
);
+
await this.mockRPC(
"get-project-files?project-id=*",
"dashboard/get-project-files.json",
diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js
index f114d8abda..ec963f718a 100644
--- a/frontend/playwright/ui/pages/WorkspacePage.js
+++ b/frontend/playwright/ui/pages/WorkspacePage.js
@@ -191,6 +191,7 @@ export class WorkspacePage extends BaseWebSocketPage {
this.tokensUpdateCreateModal = page.getByTestId(
"token-update-create-modal",
);
+ this.tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
this.tokenThemeUpdateCreateModal = page.getByTestId(
"token-theme-update-create-modal",
);
@@ -311,7 +312,7 @@ export class WorkspacePage extends BaseWebSocketPage {
async clickWithDragViewportAt(x, y, width, height) {
await this.page.waitForTimeout(100);
const box = await this.viewport.boundingBox();
- if (!box) throw new Error('Viewport not visible');
+ if (!box) throw new Error("Viewport not visible");
const startX = box.x + x;
const startY = box.y + y;
@@ -364,7 +365,9 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait);
- const layersCountBefore = await this.layers.getByTestId("layer-row").count();
+ const layersCountBefore = await this.layers
+ .getByTestId("layer-row")
+ .count();
await this.clickAndMove(x1, y1, x2, y2);
if (initialText) {
@@ -387,10 +390,13 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.keyboard.press("ControlOrMeta+C");
}
// wait for the clipboard to be updated
- await this.page.waitForFunction(async () => {
- const content = await navigator.clipboard.readText()
- return content !== "";
- }, { timeout: 1000 });
+ await this.page.waitForFunction(
+ async () => {
+ const content = await navigator.clipboard.readText();
+ return content !== "";
+ },
+ { timeout: 1000 },
+ );
}
async cut(kind = "keyboard", locator = undefined) {
@@ -401,13 +407,15 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.keyboard.press("ControlOrMeta+X");
}
// wait for the clipboard to be updated
- await this.page.waitForFunction(async () => {
- const content = await navigator.clipboard.readText()
- return content !== "";
- }, { timeout: 1000 });
+ await this.page.waitForFunction(
+ async () => {
+ const content = await navigator.clipboard.readText();
+ return content !== "";
+ },
+ { timeout: 1000 },
+ );
await this.page.waitForTimeout(3000);
-
}
/**
diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js
index a0e28eea07..75dad9c97f 100644
--- a/frontend/playwright/ui/specs/colorpicker.spec.js
+++ b/frontend/playwright/ui/specs/colorpicker.spec.js
@@ -85,14 +85,6 @@ test("Create a LINEAR gradient", async ({ page }) => {
.last();
await inputOpacity2.fill("40");
- const inputOpacityGlobal = workspacePage.colorpicker.getByTestId(
- "opacity-global-input",
- );
- await inputOpacityGlobal.fill("50");
- await inputOpacityGlobal.press("Enter");
- await expect(inputOpacityGlobal).toHaveValue("50");
- await expect(inputOpacityGlobal).toBeVisible();
-
await expect(
workspacePage.page.getByText("Linear gradient")
).toBeVisible();
@@ -169,14 +161,6 @@ test("Create a RADIAL gradient", async ({ page }) => {
.last();
await inputOpacity2.fill("100");
- const inputOpacityGlobal = workspacePage.colorpicker.getByTestId(
- "opacity-global-input",
- );
- await inputOpacityGlobal.fill("50");
- await inputOpacityGlobal.press("Enter");
- await expect(inputOpacityGlobal).toHaveValue("50");
- await expect(inputOpacityGlobal).toBeVisible();
-
await expect(
workspacePage.page.getByText("Radial gradient")
).toBeVisible();
@@ -212,7 +196,7 @@ test("Gradient stops limit", async ({ page }) => {
});
// Fix for https://tree.taiga.io/project/penpot/issue/9900
-test("Bug 9900 - Color picker has no inputs for HSV values", async ({
+test("Bug 9900 - Color picker has no inputs for HSB values", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
@@ -223,12 +207,12 @@ test("Bug 9900 - Color picker has no inputs for HSV values", async ({
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
await swatch.click();
- const HSVA = await workspacePage.page.getByLabel("HSVA");
- await HSVA.click();
+ const HSBA = await workspacePage.page.getByLabel("HSBA");
+ await HSBA.click();
await workspacePage.page.getByLabel("H", { exact: true }).isVisible();
await workspacePage.page.getByLabel("S", { exact: true }).isVisible();
- await workspacePage.page.getByLabel("V", { exact: true }).isVisible();
+ await workspacePage.page.getByLabel("B(V)", { exact: true }).isVisible();
});
test("Bug 10089 - Cannot change alpha", async ({ page }) => {
diff --git a/frontend/playwright/ui/specs/components.spec.js b/frontend/playwright/ui/specs/components.spec.js
index 50adc17eae..9661ba9c88 100644
--- a/frontend/playwright/ui/specs/components.spec.js
+++ b/frontend/playwright/ui/specs/components.spec.js
@@ -3,9 +3,12 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
+ await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]);
});
-test("BUG 13267 - Component instance is not synced with parent for geometry changes", async ({ page }) => {
+test("BUG 13267 - Component instance is not synced with parent for geometry changes", async ({
+ page,
+}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("components/get-file-13267.json");
@@ -21,7 +24,9 @@ test("BUG 13267 - Component instance is not synced with parent for geometry chan
// Select the main component
await workspacePage.clickLeafLayer("A Component", {}, 1);
- const rotationInput = workspacePage.rightSidebar.getByTestId("rotation").getByRole("textbox");
+ const rotationInput = workspacePage.rightSidebar.getByRole("textbox", {
+ name: "Rotation",
+ });
await rotationInput.fill("45");
await rotationInput.press("Enter");
@@ -30,4 +35,4 @@ test("BUG 13267 - Component instance is not synced with parent for geometry chan
await workspacePage.clickLeafLayer("Rectangle");
await expect(rotationInput).toHaveValue("45");
-});
\ No newline at end of file
+});
diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js
index 8fe67b9d3a..0cf953a302 100644
--- a/frontend/playwright/ui/specs/design-tab.spec.js
+++ b/frontend/playwright/ui/specs/design-tab.spec.js
@@ -1,8 +1,11 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
+const tokenInputFlag = "enable-feature-token-input";
+
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
+ await WasmWorkspacePage.mockConfigFlags(page, [tokenInputFlag]);
});
const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`;
@@ -71,7 +74,10 @@ test.describe("Shape attributes", () => {
page,
}) => {
const workspace = new WasmWorkspacePage(page);
- await workspace.mockConfigFlags(["enable-feature-render-wasm"]);
+ await workspace.mockConfigFlags([
+ "enable-feature-render-wasm",
+ tokenInputFlag,
+ ]);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "design/get-file-fills-limit.json");
@@ -95,7 +101,10 @@ test.describe("Shape attributes", () => {
page,
}) => {
const workspace = new WasmWorkspacePage(page);
- await workspace.mockConfigFlags(["enable-feature-render-wasm"]);
+ await workspace.mockConfigFlags([
+ "enable-feature-render-wasm",
+ tokenInputFlag,
+ ]);
await workspace.setupEmptyFile();
await workspace.mockRPC(
/get\-file\?/,
@@ -236,7 +245,7 @@ test.describe("Background blur", () => {
page,
}) => {
const workspace = new WasmWorkspacePage(page);
- await workspace.mockConfigFlags(["enable-background-blur"]);
+ await workspace.mockConfigFlags(["enable-background-blur", tokenInputFlag]);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-background-blur.json");
@@ -260,7 +269,7 @@ test.describe("Background blur", () => {
page,
}) => {
const workspace = new WasmWorkspacePage(page);
- await workspace.mockConfigFlags(["enable-background-blur"]);
+ await workspace.mockConfigFlags(["enable-background-blur", tokenInputFlag]);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-background-blur.json");
@@ -319,6 +328,7 @@ test("BUG 9543 - Layout padding inputs not showing 'mixed' when needed", async (
page,
}) => {
const workspace = new WasmWorkspacePage(page);
+
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "design/get-file-9543.json");
await workspace.mockRPC(
@@ -338,14 +348,18 @@ test("BUG 9543 - Layout padding inputs not showing 'mixed' when needed", async (
});
await toggle.click();
- await workspace.page.getByLabel("Top padding").fill("10");
+ const topPaddingInput = workspace.page.getByRole("textbox", {
+ name: "Top padding",
+ });
+ await topPaddingInput.fill("10");
+ await topPaddingInput.press("Enter");
await toggle.click();
- await expect(workspace.page.getByLabel("Vertical padding")).toHaveValue("");
- await expect(workspace.page.getByLabel("Vertical padding")).toHaveAttribute(
- "placeholder",
- "Mixed",
- );
+ const verticalPaddingInput = await workspace.page.getByRole("textbox", {
+ name: "Vertical padding",
+ });
+ await expect(verticalPaddingInput).toHaveValue("");
+ await expect(verticalPaddingInput).toHaveAttribute("placeholder", "Mixed");
});
test("BUG 11177 - Font size input not showing 'mixed' when needed", async ({
diff --git a/frontend/playwright/ui/specs/multiseleccion.spec.js b/frontend/playwright/ui/specs/multiseleccion.spec.js
index 1b4be19e4c..5d7ee1c92c 100644
--- a/frontend/playwright/ui/specs/multiseleccion.spec.js
+++ b/frontend/playwright/ui/specs/multiseleccion.spec.js
@@ -3,6 +3,7 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
+ await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]);
});
test("Multiselection - check multiple values in measures", async ({ page }) => {
@@ -27,37 +28,53 @@ test("Multiselection - check multiple values in measures", async ({ page }) => {
await workspacePage.layers.getByTestId("layer-row").nth(0).click();
// === CHECK SINGLE SELECTION - ALL MEASURE FIELDS ===
- const measuresSection = workspacePage.rightSidebar.getByRole('region', { name: 'shape-measures-section' });
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
await expect(measuresSection).toBeVisible();
// Width
- const widthInput = measuresSection.getByTitle('Width', { exact: true }).getByRole('textbox');
+ const widthInput = measuresSection.getByRole("textbox", {
+ name: "Width",
+ exact: true,
+ });
await expect(widthInput).toHaveValue("360");
// Height
- const heightInput = measuresSection.getByTitle('Height', { exact: true }).getByRole('textbox');
+ const heightInput = measuresSection.getByRole("textbox", {
+ name: "Height",
+ exact: true,
+ });
await expect(heightInput).toHaveValue("53");
// X Position (using "X axis" title)
- const xPosInput = measuresSection.getByTitle('X axis', { exact: true }).getByRole('textbox');
+ const xPosInput = measuresSection.getByRole("textbox", {
+ name: "X axis",
+ exact: true,
+ });
await expect(xPosInput).toHaveValue("1094");
// Y Position (using "Y axis" title)
- const yPosInput = measuresSection.getByTitle('Y axis', { exact: true }).getByRole('textbox');
+ const yPosInput = measuresSection.getByRole("textbox", {
+ name: "Y axis",
+ exact: true,
+ });
await expect(yPosInput).toHaveValue("856");
// === CHECK MULTI-SELECTION - MIXED VALUES ===
// Shift+click to add second layer to selection
- await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] });
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(1)
+ .click({ modifiers: ["Shift"] });
// All measure fields should show "Mixed" placeholder when values differ
- await expect(widthInput).toHaveAttribute('placeholder', 'Mixed');
- await expect(heightInput).toHaveAttribute('placeholder', 'Mixed');
- await expect(xPosInput).toHaveAttribute('placeholder', 'Mixed');
- await expect(yPosInput).toHaveAttribute('placeholder', 'Mixed');
+ await expect(widthInput).toHaveAttribute("placeholder", "Mixed");
+ await expect(heightInput).toHaveAttribute("placeholder", "Mixed");
+ await expect(xPosInput).toHaveAttribute("placeholder", "Mixed");
+ await expect(yPosInput).toHaveAttribute("placeholder", "Mixed");
});
-
test("Multiselection - check fill multiple values", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
@@ -79,17 +96,22 @@ test("Multiselection - check fill multiple values", async ({ page }) => {
await workspacePage.layers.getByTestId("layer-row").nth(0).click();
// Fill section
- const fillSection = workspacePage.rightSidebar.getByRole('region', { name: "Fill section" });
+ const fillSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "Fill section",
+ });
await expect(fillSection).toBeVisible();
// Single selection - fill color should be visible (not "Mixed")
await expect(fillSection.getByText(/Mixed/i)).not.toBeVisible();
// Multi-selection with Shift+click
- await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] });
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(1)
+ .click({ modifiers: ["Shift"] });
// Should show "Mixed" for fills when shapes have different fill colors
- await expect(fillSection.getByText('Mixed')).toBeVisible();
+ await expect(fillSection.getByText("Mixed")).toBeVisible();
});
test("Multiselection - check stroke multiple values", async ({ page }) => {
@@ -113,17 +135,22 @@ test("Multiselection - check stroke multiple values", async ({ page }) => {
await workspacePage.layers.getByTestId("layer-row").nth(0).click();
// Stroke section
- const strokeSection = workspacePage.rightSidebar.getByRole('region', { name: "Stroke section" });
+ const strokeSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "Stroke section",
+ });
await expect(strokeSection).toBeVisible();
// Single selection - stroke should be visible (not "Mixed")
await expect(strokeSection.getByText(/Mixed/i)).not.toBeVisible();
// Multi-selection
- await workspacePage.layers.getByTestId("layer-row").nth(1).click({ modifiers: ['Shift'] });
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(1)
+ .click({ modifiers: ["Shift"] });
// Should show "Mixed" for strokes when shapes have different stroke colors
- await expect(strokeSection.getByText('Mixed')).toBeVisible();
+ await expect(strokeSection.getByText("Mixed")).toBeVisible();
});
test("Multiselection - check rotation multiple values", async ({ page }) => {
@@ -147,26 +174,33 @@ test("Multiselection - check rotation multiple values", async ({ page }) => {
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
// Measures section contains rotation
- const measuresSection = workspacePage.rightSidebar.getByRole('region', { name: 'shape-measures-section' });
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
await expect(measuresSection).toBeVisible();
// Rotation field exists
- const rotationInput = measuresSection.getByTitle('Rotation', { exact: true }).getByRole('textbox');
+ const rotationInput = measuresSection.getByRole("textbox", {
+ name: "Rotation",
+ exact: true,
+ });
await expect(rotationInput).toBeVisible();
// Rotate that shape
await rotationInput.fill("45");
- await page.keyboard.press('Enter');
+ await page.keyboard.press("Enter");
await expect(rotationInput).toHaveValue("45"); // Rotation should be 45
// Multi-selection
- await workspacePage.layers.getByTestId("layer-row").nth(0).click({ modifiers: ['Shift'] });
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(0)
+ .click({ modifiers: ["Shift"] });
// Rotation should show "Mixed" placeholder
- await expect(rotationInput).toHaveAttribute('placeholder', 'Mixed');
+ await expect(rotationInput).toHaveAttribute("placeholder", "Mixed");
});
-
test("Multiselection of text and typographies", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
@@ -181,29 +215,45 @@ test("Multiselection of text and typographies", async ({ page }) => {
});
const plainTextLayer = workspacePage.layers.getByTestId("layer-row").nth(5);
- const plainTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(2);
- const typographyTextLayerOne = workspacePage.layers.getByTestId("layer-row").nth(7);
- const typographyTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(4);
- const tokenTypographyTextLayerOne = workspacePage.layers.getByTestId("layer-row").nth(6);
- const tokenTypographyTextLayerTwo = workspacePage.layers.getByTestId("layer-row").nth(3);
+ const plainTextLayerTwo = workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(2);
+ const typographyTextLayerOne = workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(7);
+ const typographyTextLayerTwo = workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(4);
+ const tokenTypographyTextLayerOne = workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(6);
+ const tokenTypographyTextLayerTwo = workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(3);
const rectangleLayer = workspacePage.layers.getByTestId("layer-row").nth(1);
const elipseLayer = workspacePage.layers.getByTestId("layer-row").nth(0);
- const textSection = workspacePage.rightSidebar.getByRole('region', { name: "Text section" });
+ const textSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "Text section",
+ });
// Select rectangle and elipse together
await rectangleLayer.click();
- await elipseLayer.click({ modifiers: ['Control'] });
+ await elipseLayer.click({ modifiers: ["Control"] });
await expect(textSection).not.toBeVisible();
-
+
// Select plain text layer
await plainTextLayer.click();
await expect(textSection).toBeVisible();
- await expect(textSection.getByText("Multiple typographies")).not.toBeVisible();
+ await expect(
+ textSection.getByText("Multiple typographies"),
+ ).not.toBeVisible();
// Select two plain text layer with different font family
- await plainTextLayerTwo.click({ modifiers: ['Control'] });
+ await plainTextLayerTwo.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
- await expect(textSection.getByTitle("Font family").getByText("--")).toBeVisible();
+ await expect(
+ textSection.getByTitle("Font family").getByText("--"),
+ ).toBeVisible();
// Select typography text layer
await typographyTextLayerOne.click();
@@ -211,48 +261,50 @@ test("Multiselection of text and typographies", async ({ page }) => {
await expect(textSection.getByText("Typography one")).toBeVisible();
// Select two typography text layer with different typography
- await typographyTextLayerTwo.click({ modifiers: ['Control'] });
+ await typographyTextLayerTwo.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
await expect(textSection.getByText("Multiple typographies")).toBeVisible();
- // Select token typography text layer
+ // Select token typography text layer
// TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY
await tokenTypographyTextLayerOne.click();
await expect(textSection).toBeVisible();
- await expect(textSection.getByText('Metrophobic')).toBeVisible();
+ await expect(textSection.getByText("Metrophobic")).toBeVisible();
// Select two token typography text layer with different token typography
- // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY
- await tokenTypographyTextLayerTwo.click({ modifiers: ['Control'] });
+ // TODO: CHANGE WHEN TOKEN TYPOGRAPHY ROW IS READY
+ await tokenTypographyTextLayerTwo.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
- await expect(textSection.getByTitle("Font family").getByText("--")).toBeVisible();
+ await expect(
+ textSection.getByTitle("Font family").getByText("--"),
+ ).toBeVisible();
//Select plain text layer and typography text layer together
await plainTextLayer.click();
- await typographyTextLayerOne.click({ modifiers: ['Control'] });
+ await typographyTextLayerOne.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
await expect(textSection.getByText("Multiple typographies")).toBeVisible();
//Select plain text layer and typography text layer together on reverse order
await typographyTextLayerOne.click();
- await plainTextLayer.click({ modifiers: ['Control'] });
+ await plainTextLayer.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
await expect(textSection.getByText("Multiple typographies")).toBeVisible();
//Selen token typography text layer and typography text layer together
await tokenTypographyTextLayerOne.click();
- await typographyTextLayerOne.click({ modifiers: ['Control'] });
+ await typographyTextLayerOne.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
await expect(textSection.getByText("Multiple typographies")).toBeVisible();
//Select token typography text layer and typography text layer together on reverse order
await typographyTextLayerOne.click();
- await tokenTypographyTextLayerOne.click({ modifiers: ['Control'] });
+ await tokenTypographyTextLayerOne.click({ modifiers: ["Control"] });
await expect(textSection).toBeVisible();
await expect(textSection.getByText("Multiple typographies")).toBeVisible();
// Select rectangle and elipse together
await rectangleLayer.click();
- await elipseLayer.click({ modifiers: ['Control'] });
+ await elipseLayer.click({ modifiers: ["Control"] });
await expect(textSection).not.toBeVisible();
-});
\ No newline at end of file
+});
diff --git a/frontend/playwright/ui/specs/profile-menu.spec.js b/frontend/playwright/ui/specs/profile-menu.spec.js
index e86a79a826..71bdbb4199 100644
--- a/frontend/playwright/ui/specs/profile-menu.spec.js
+++ b/frontend/playwright/ui/specs/profile-menu.spec.js
@@ -10,12 +10,16 @@ test("Navigate to penpot changelog from profile menu", async ({ page }) => {
await dashboardPage.goToDashboard();
await dashboardPage.openProfileMenu();
- await dashboardPage.clickProfileMenuItem("About Penpot");
+ const aboutPenpotItem = page.getByText("About Penpot");
+ await aboutPenpotItem.hover();
+
+ const changelogSubmenuItem = page.getByText("Penpot Changelog");
+ await expect(changelogSubmenuItem).toBeVisible();
// Listen for the new page (tab) that opens when clicking "Penpot Changelog"
const [newPage] = await Promise.all([
page.context().waitForEvent("page"),
- dashboardPage.clickProfileMenuItem("Penpot Changelog"),
+ changelogSubmenuItem.click(),
]);
await newPage.waitForLoadState();
diff --git a/frontend/playwright/ui/specs/tokens/apply.spec.js b/frontend/playwright/ui/specs/tokens/apply.spec.js
index 23a25f3669..64f49733ce 100644
--- a/frontend/playwright/ui/specs/tokens/apply.spec.js
+++ b/frontend/playwright/ui/specs/tokens/apply.spec.js
@@ -4,7 +4,8 @@ import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import {
setupTokensFileRender,
setupTypographyTokensFileRender,
- unfoldTokenTree,
+ unfoldTokenType,
+ createToken,
} from "./helpers";
test.beforeEach(async ({ page }) => {
@@ -24,10 +25,9 @@ test.describe("Tokens: Apply token", () => {
.filter({ hasText: "Button" })
.click();
- const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
- await tokensTabButton.click();
+ await page.getByRole("tab", { name: "Tokens" }).click();
- unfoldTokenTree(tokensSidebar, "color", "colors.black");
+ await unfoldTokenType(tokensSidebar, "color");
await tokensSidebar
.getByRole("button", { name: "black" })
@@ -52,17 +52,15 @@ test.describe("Tokens: Apply token", () => {
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 border radius tokens
- await page.getByRole("button", { name: "Border Radius 3" }).click();
+ await page.getByRole("tab", { name: "Tokens" }).click();
+
+ await unfoldTokenType(tokensSidebar, "border radius");
await expect(
- tokensSidebar.getByRole("button", { name: "borderRadius" }),
- ).toBeVisible();
- await tokensSidebar.getByRole("button", { name: "borderRadius" }).click();
- await expect(
- tokensSidebar.getByRole("button", { name: "borderRadius.sm" }),
+ tokensSidebar.getByRole("button", {
+ name: "borderRadius.sm",
+ exact: true,
+ }),
).toBeVisible();
// Apply border radius token from token panels
@@ -72,7 +70,7 @@ test.describe("Tokens: Apply token", () => {
// Check if border radius sections is visible on right sidebar
const borderRadiusSection = page.getByRole("region", {
- name: "border-radius-section",
+ name: "Border radius section",
});
await expect(borderRadiusSection).toBeVisible();
@@ -84,8 +82,9 @@ test.describe("Tokens: Apply token", () => {
await brTokenPillSM.click();
// Change token from dropdown
- const brTokenOptionXl = borderRadiusSection
- .getByRole("option", { name: "borderRadius.xl" });
+ const brTokenOptionXl = borderRadiusSection.getByRole("option", {
+ name: "borderRadius.xl",
+ });
await expect(brTokenOptionXl).toBeVisible();
await brTokenOptionXl.click();
@@ -118,13 +117,7 @@ test.describe("Tokens: Apply token", () => {
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 unfoldTokenType(tokensSidebar, "opacity");
await expect(
tokensSidebar.getByRole("button", { name: "opacity.high" }),
).toBeVisible();
@@ -134,7 +127,7 @@ test.describe("Tokens: Apply token", () => {
// Check if opacity sections is visible on right sidebar
const layerMenuSection = page.getByRole("region", {
- name: "layer-menu-section",
+ name: "Layer menu section",
});
await expect(layerMenuSection).toBeVisible();
@@ -151,7 +144,9 @@ test.describe("Tokens: Apply token", () => {
await detachButton.click();
// Open dropdown from input
- const dropdownBtn = layerMenuSection.getByRole('button', { name: 'Open token list' })
+ const dropdownBtn = layerMenuSection.getByRole("button", {
+ name: "Open token list",
+ });
await expect(dropdownBtn).toBeVisible();
await dropdownBtn.click();
@@ -200,12 +195,8 @@ test.describe("Tokens: Apply token", () => {
test("User adds shadow token with multiple shadows and applies it to shape", async ({
page,
}) => {
- const {
- tokensUpdateCreateModal,
- tokensSidebar,
- workspacePage,
- tokenContextMenuForToken,
- } = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
+ const { tokensUpdateCreateModal, tokensSidebar, workspacePage } =
+ await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -227,8 +218,12 @@ test.describe("Tokens: Apply token", () => {
await expect(firstShadowFields).toBeVisible();
// Fill in the shadow values
- const offsetXInput = firstShadowFields.getByRole('textbox', { name: 'X' });
- const offsetYInput = firstShadowFields.getByRole('textbox', { name: 'Y' });
+ const offsetXInput = firstShadowFields.getByRole("textbox", {
+ name: "X",
+ });
+ const offsetYInput = firstShadowFields.getByRole("textbox", {
+ name: "Y",
+ });
const blurInput = firstShadowFields.getByRole("textbox", {
name: "Blur",
});
@@ -301,8 +296,12 @@ test.describe("Tokens: Apply token", () => {
await expect(thirdShadowFields).toBeVisible();
// User adds values for the third shadow
- const thirdOffsetXInput = thirdShadowFields.getByRole('textbox', { name: 'X' });
- const thirdOffsetYInput = thirdShadowFields.getByRole('textbox', { name: 'Y' });
+ const thirdOffsetXInput = thirdShadowFields.getByRole("textbox", {
+ name: "X",
+ });
+ const thirdOffsetYInput = thirdShadowFields.getByRole("textbox", {
+ name: "Y",
+ });
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
name: "Blur",
});
@@ -330,10 +329,10 @@ test.describe("Tokens: Apply token", () => {
// Verify that the first shadow kept its values
const firstOffsetXValue = await firstShadowFields
- .getByRole('textbox', { name: 'X' })
+ .getByRole("textbox", { name: "X" })
.inputValue();
const firstOffsetYValue = await firstShadowFields
- .getByRole('textbox', { name: 'Y' })
+ .getByRole("textbox", { name: "Y" })
.inputValue();
const firstBlurValue = await firstShadowFields
.getByRole("textbox", { name: "Blur" })
@@ -359,10 +358,10 @@ test.describe("Tokens: Apply token", () => {
await expect(newSecondShadowFields).toBeVisible();
const secondOffsetXValue = await newSecondShadowFields
- .getByRole('textbox', { name: 'X' })
+ .getByRole("textbox", { name: "X" })
.inputValue();
const secondOffsetYValue = await newSecondShadowFields
- .getByRole('textbox', { name: 'Y' })
+ .getByRole("textbox", { name: "Y" })
.inputValue();
const secondBlurValue = await newSecondShadowFields
.getByRole("textbox", { name: "Blur" })
@@ -412,10 +411,10 @@ test.describe("Tokens: Apply token", () => {
// Verify first shadow values are still there
const restoredFirstOffsetX = await firstShadowFields
- .getByRole('textbox', { name: 'X' })
+ .getByRole("textbox", { name: "X" })
.inputValue();
const restoredFirstOffsetY = await firstShadowFields
- .getByRole('textbox', { name: 'Y' })
+ .getByRole("textbox", { name: "Y" })
.inputValue();
const restoredFirstBlur = await firstShadowFields
.getByRole("textbox", { name: "Blur" })
@@ -435,10 +434,10 @@ test.describe("Tokens: Apply token", () => {
// Verify second shadow values are still there
const restoredSecondOffsetX = await newSecondShadowFields
- .getByRole('textbox', { name: 'X' })
+ .getByRole("textbox", { name: "X" })
.inputValue();
const restoredSecondOffsetY = await newSecondShadowFields
- .getByRole('textbox', { name: 'Y' })
+ .getByRole("textbox", { name: "Y" })
.inputValue();
const restoredSecondBlur = await newSecondShadowFields
.getByRole("textbox", { name: "Blur" })
@@ -465,8 +464,6 @@ test.describe("Tokens: Apply token", () => {
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
- unfoldTokenTree(tokensSidebar, "shadow", "primary");
-
// Verify token appears in sidebar
const shadowToken = tokensSidebar.getByRole("button", {
name: "primary",
@@ -501,7 +498,7 @@ test.describe("Tokens: Apply token", () => {
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
- unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
+ await unfoldTokenType(tokensSidebar, "dimensions");
// Apply token to width and height token from token panel
await tokensSidebar.getByRole("button", { name: "dimension.sm" }).click();
@@ -554,7 +551,7 @@ test.describe("Tokens: Apply token", () => {
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
- unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
+ await unfoldTokenType(tokensSidebar, "dimensions");
// Apply token to width and height token from token panel
await tokensSidebar
@@ -610,13 +607,13 @@ test.describe("Tokens: Apply token", () => {
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
- unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
+ await unfoldTokenType(tokensSidebar, "dimensions");
// 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();
+ await tokenContextMenuForToken.getByText("Y", { exact: true }).click();
// Check if measures sections is visible on right sidebar
const measuresSection = page.getByRole("region", {
@@ -666,7 +663,7 @@ test.describe("Tokens: Apply token", () => {
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
- unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.xs");
+ await unfoldTokenType(tokensSidebar, "dimensions");
// Apply token to width and height token from token panel
await tokensSidebar
@@ -677,7 +674,7 @@ test.describe("Tokens: Apply token", () => {
// Check if border radius sections is visible on right sidebar
const borderRadiusSection = page.getByRole("region", {
- name: "border-radius-section",
+ name: "Border radius section",
});
await expect(borderRadiusSection).toBeVisible();
@@ -798,8 +795,7 @@ test.describe("Tokens: Apply token", () => {
const tokensTab = page.getByRole("tab", { name: "Tokens" });
await expect(tokensTab).toBeVisible();
await tokensTab.click();
- await page.getByRole("button", { name: "Dimensions 4" }).click();
- await page.getByRole("button", { name: "dim", exact: true }).click();
+ await unfoldTokenType(workspace.tokensSidebar, "dimensions");
const tokensSidebar = workspace.tokensSidebar;
await expect(
tokensSidebar.getByRole("button", { name: "dim.md" }),
@@ -818,7 +814,7 @@ test.describe("Tokens: Apply token", () => {
// Check if token pill is visible on right sidebar
const layoutItemSectionSidebar = rightSidebar.getByRole("region", {
- name: "layout item menu",
+ name: "Layout item section",
});
await expect(layoutItemSectionSidebar).toBeVisible();
const marginPillMd = layoutItemSectionSidebar.getByRole("button", {
@@ -870,11 +866,7 @@ test.describe("Tokens: Detach token", () => {
await tokensTabButton.click();
// Unfold border radius tokens
- await page.getByRole("button", { name: "Border Radius 3" }).click();
- await expect(
- tokensSidebar.getByRole("button", { name: "borderRadius" }),
- ).toBeVisible();
- await tokensSidebar.getByRole("button", { name: "borderRadius" }).click();
+ await unfoldTokenType(tokensSidebar, "Border Radius");
await expect(
tokensSidebar.getByRole("button", { name: "borderRadius.sm" }),
).toBeVisible();
@@ -886,7 +878,7 @@ test.describe("Tokens: Detach token", () => {
// Check if border radius sections is visible on right sidebar
const borderRadiusSection = page.getByRole("region", {
- name: "border-radius-section",
+ name: "Border radius section",
});
await expect(borderRadiusSection).toBeVisible();
@@ -936,3 +928,497 @@ test.describe("Tokens: Detach token", () => {
await expect(brokenPill).not.toBeVisible();
});
});
+
+test("Bug: 13959, User select shapes with different hidden state.", async ({
+ page,
+}) => {
+ const { workspacePage } = await setupTokensFileRender(page);
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+ const layerMenuSection = page.getByRole("region", {
+ name: "Layer menu section",
+ });
+ await expect(layerMenuSection).toBeVisible();
+ await layerMenuSection
+ .getByRole("button", { name: "Toggle layer visibility" })
+ .click();
+ await expect(layerMenuSection).toBeVisible();
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(0)
+ .click({ modifiers: ["Shift"] });
+ await expect(layerMenuSection).toBeVisible();
+});
+
+test("Bug: 13960, User select shapes with different opacity and input show mixed state.", async ({
+ page,
+}) => {
+ const { workspacePage } = await setupTokensFileRender(page);
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+ const layerMenuSection = page.getByRole("region", {
+ name: "Layer menu section",
+ });
+ await expect(layerMenuSection).toBeVisible();
+ await layerMenuSection.getByRole("textbox", { name: "Opacity" }).fill("50");
+ await expect(layerMenuSection).toBeVisible();
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(0)
+ .click({ modifiers: ["Shift"] });
+ await expect(
+ layerMenuSection.getByRole("textbox", { name: "Opacity" }),
+ ).toBeVisible();
+ await expect(
+ layerMenuSection.getByRole("textbox", { name: "Opacity" }),
+ ).toBeVisible();
+
+ await expect(
+ layerMenuSection.getByRole("textbox", { name: "Opacity" }),
+ ).toHaveAttribute("placeholder", "Mixed");
+});
+
+test("BUG: 13930, Token colors are shown on selected colors section", async ({
+ page,
+}) => {
+ const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
+ await setupTokensFileRender(page);
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .filter({ hasText: "Button" })
+ .click();
+
+ await page.getByRole("tab", { name: "Tokens" }).click();
+
+ await unfoldTokenType(tokensSidebar, "color");
+
+ await tokensSidebar
+ .getByRole("button", { name: "black" })
+ .click({ button: "right" });
+ await tokenContextMenuForToken.getByText("Fill").click();
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .filter({ hasText: "Rectangle" })
+ .first()
+ .click({ modifiers: ["Shift"] });
+
+ await expect(
+ workspacePage.page.getByRole("region", { name: "Color selection section" }),
+ ).toBeVisible();
+
+ await workspacePage.page
+ .getByRole("button", { name: "Resolved value: #7f9cf5" })
+ .click();
+ await expect(
+ workspacePage.page.getByRole("region", { name: "Color selection section" }),
+ ).toBeVisible();
+
+ await expect(
+ workspacePage.page
+ .getByTestId("colorpicker")
+ .getByRole("button", { name: "colors.black" }),
+ ).toBeVisible();
+});
+
+test.describe("Numeric Input and Token Integration Tests", () => {
+ test("Token pill persists after blur in gap inputs", async ({ page }) => {
+ // Setup the workspace with token features enabled
+ const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
+ await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+
+ // Transform a rectangle into a flex container to expose gap properties
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+
+ const layoutSection =
+ workspacePage.rightSidebar.getByTestId("inspect-layout");
+
+ const addLayoutButton = layoutSection
+ .getByRole("button", { name: "Add layout" })
+ .first();
+ await addLayoutButton.click();
+ await page.getByText("Flex layout").click();
+
+ // Apply a spacing token to the Column gap property
+ const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
+ await tokensTabButton.click();
+ await unfoldTokenType(tokensSidebar, "spacing");
+
+ await tokensSidebar
+ .getByRole("button", { name: "spacing.lg" })
+ .click({ button: "right" });
+
+ await tokenContextMenuForToken.getByText("Column gap").click();
+
+ // Verify that the token pill appears in the layout section, check after blur
+ await expect(
+ page
+ .getByTestId("inspect-layout")
+ .getByRole("button", { name: "spacing.lg" }),
+ ).toBeVisible();
+
+ await page
+ .getByTestId("inspect-layout")
+ .getByRole("textbox", { name: "Vertical padding" })
+ .click();
+
+ await expect(
+ page
+ .getByTestId("inspect-layout")
+ .getByRole("button", { name: "spacing.lg" }),
+ ).toBeVisible();
+ });
+
+ test("Padding tokens are applied to both vertical or horizontal properties", async ({
+ page,
+ }) => {
+ // Setup the workspace with token features enabled
+ const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
+ await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+
+ // Transform a rectangle into a flex container to expose gap properties
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+
+ const layoutSection =
+ workspacePage.rightSidebar.getByTestId("inspect-layout");
+
+ const addLayoutButton = layoutSection
+ .getByRole("button", { name: "Add layout" })
+ .first();
+ await addLayoutButton.click();
+ await page.getByText("Flex layout").click();
+
+ // Apply a spacing token to the Column gap property
+ const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
+ await tokensTabButton.click();
+ await unfoldTokenType(tokensSidebar, "spacing");
+
+ await tokensSidebar
+ .getByRole("button", { name: "spacing.lg" })
+ .click({ button: "right" });
+
+ await tokenContextMenuForToken.getByText("Horizontal").click();
+
+ // Verify that the token pill appears in the layout section, check after blur
+ await expect(
+ page
+ .getByTestId("inspect-layout")
+ .getByRole("button", { name: "spacing.lg" }),
+ ).toBeVisible();
+
+ await layoutSection
+ .getByRole("button", { name: "Show 4 sided padding options" })
+ .click();
+
+ await expect(
+ page
+ .getByTestId("inspect-layout")
+ .getByRole("button", { name: "spacing.lg" }),
+ ).toHaveCount(2);
+
+ await layoutSection
+ .getByRole("button", { name: "Show 4 sided padding options" })
+ .click();
+
+ await expect(
+ page
+ .getByTestId("inspect-layout")
+ .getByRole("button", { name: "spacing.lg" }),
+ ).toBeVisible();
+ });
+
+ test("Token pill persists after blur in min/max width inputs", async ({
+ page,
+ }) => {
+ // Setup the workspace with token features enabled
+ const { workspacePage } = await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+
+ // Create a flex container to expose min/max width properties
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(2).click();
+
+ const layoutSection =
+ workspacePage.rightSidebar.getByTestId("inspect-layout");
+
+ const addLayoutButton = layoutSection
+ .getByRole("button", { name: "Add layout" })
+ .first();
+ await addLayoutButton.click();
+ await page.getByText("Flex layout").click();
+
+ // Verify that the flex container (Flex board) is created
+ await expect(
+ page.getByRole("button", { name: "Flex board" }),
+ ).toBeVisible();
+
+ // Select element inside flex container to access to layout constrains inputs
+ // Apply token to min width property
+ await workspacePage.layers
+ .getByTestId("layer-row")
+ .nth(2)
+ .getByTestId("toggle-content")
+ .click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(3).click();
+
+ const layoutItemSection = page.getByRole("region", {
+ name: "Layout item section",
+ });
+
+ await layoutItemSection.getByTestId("behaviour-h-fill").click();
+
+ const constraintsSection = layoutItemSection.getByRole("region", {
+ name: "layout item size constraints",
+ });
+ await expect(constraintsSection).toBeVisible();
+
+ await constraintsSection
+ .getByRole("button", { name: "Open token list" })
+ .nth(0)
+ .click();
+
+ await expect(
+ page.getByRole("option", { name: "dimension.md" }),
+ ).toBeVisible();
+ await page.getByRole("option", { name: "dimension.md" }).click();
+
+ await expect(
+ constraintsSection.getByRole("button", { name: "dimension.md" }),
+ ).toBeVisible();
+
+ // Focus another input (Max width) to trigger blur and check if token pill persists
+ await constraintsSection
+ .getByRole("textbox", { name: "Max width" })
+ .click();
+
+ await expect(
+ constraintsSection.getByRole("button", { name: "dimension.md" }),
+ ).toBeVisible();
+ });
+
+ test("Invalid formula reverts to previous value in padding inputs", async ({
+ page,
+ }) => {
+ const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
+ await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+
+ const layoutSection =
+ workspacePage.rightSidebar.getByTestId("inspect-layout");
+
+ const addLayoutButton = layoutSection
+ .getByRole("button", { name: "Add layout" })
+ .first();
+
+ await addLayoutButton.click();
+
+ await page.getByText("Flex layout").click();
+
+ // Apply a spacing token to the Column gap property
+ const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
+ await tokensTabButton.click();
+ await unfoldTokenType(tokensSidebar, "spacing");
+
+ await tokensSidebar
+ .getByRole("button", { name: "spacing.lg" })
+ .click({ button: "right" });
+
+ await tokenContextMenuForToken.getByText("Column gap").click();
+
+ const verticalPaddingInput = layoutSection.getByRole("textbox", {
+ name: "Vertical padding",
+ });
+
+ // Enter a valid value first
+ await verticalPaddingInput.fill("23");
+ await verticalPaddingInput.press("Enter");
+ // Wait for potential error handling
+ await page.waitForTimeout(500);
+
+ expect(await verticalPaddingInput.inputValue()).toMatch("23");
+
+ // Enter invalid expression
+ await verticalPaddingInput.fill("abc+1");
+ await verticalPaddingInput.press("Enter");
+
+ // Wait for potential error handling
+ await page.waitForTimeout(500);
+
+ // Value should revert to previous valid value
+ expect(await verticalPaddingInput.inputValue()).toMatch("23");
+
+ // Should NOT contain invalid characters
+ expect(await verticalPaddingInput.inputValue()).not.toContain("abc");
+ });
+
+ test("Division by zero reverts to previous value", async ({ page }) => {
+ const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
+ await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+
+ const layoutSection =
+ workspacePage.rightSidebar.getByTestId("inspect-layout");
+
+ const addLayoutButton = layoutSection
+ .getByRole("button", { name: "Add layout" })
+ .first();
+
+ await addLayoutButton.click();
+
+ await page.getByText("Flex layout").click();
+
+ // Apply a spacing token to the Column gap property
+ const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
+ await tokensTabButton.click();
+ await unfoldTokenType(tokensSidebar, "spacing");
+
+ await tokensSidebar
+ .getByRole("button", { name: "spacing.lg" })
+ .click({ button: "right" });
+
+ await tokenContextMenuForToken.getByText("Column gap").click();
+
+ const verticalPaddingInput = layoutSection.getByRole("textbox", {
+ name: "Vertical padding",
+ });
+
+ // Enter a valid value first
+ await verticalPaddingInput.fill("23");
+ await verticalPaddingInput.press("Enter");
+ // Wait for potential error handling
+ await page.waitForTimeout(500);
+
+ expect(await verticalPaddingInput.inputValue()).toMatch("23");
+
+ // Enter invalid expression
+ await verticalPaddingInput.fill("10/0");
+ await verticalPaddingInput.press("Enter");
+
+ // Wait for potential error handling
+ await page.waitForTimeout(500);
+
+ // Value should revert to previous valid value
+ expect(await verticalPaddingInput.inputValue()).toMatch("23");
+
+ // Should NOT contain invalid characters
+ expect(await verticalPaddingInput.inputValue()).not.toContain("10/0");
+
+ // Value should revert
+ expect(await verticalPaddingInput.inputValue()).toMatch(/^(\d+|--)$/);
+ expect(await verticalPaddingInput.inputValue()).not.toBe("Infinity");
+ });
+
+ test("Negative expression result handled correctly", async ({ page }) => {
+ const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
+ await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+
+ await page.getByRole("tab", { name: "Layers" }).click();
+
+ await workspacePage.layers.getByTestId("layer-row").nth(1).click();
+ const widthInput = workspacePage.rightSidebar.getByRole("textbox", {
+ name: "Width",
+ });
+ await expect(widthInput).toBeVisible();
+
+ // Enter a valid value first
+ await widthInput.fill("23");
+ await widthInput.press("Enter");
+
+ // Wait for potential error handling
+ await page.waitForTimeout(500);
+ expect(await widthInput.inputValue()).toMatch("23");
+
+ // Enter a negative expression
+ await widthInput.fill("10-50");
+ await widthInput.press("Enter");
+
+ // Wait for potential error handling
+ await page.waitForTimeout(500);
+
+ expect(await widthInput.inputValue()).toMatch("0.01");
+
+ // Should NOT negative values
+ expect(await widthInput.inputValue()).not.toContain("-40");
+ });
+
+ test("Token pill show broken reference when set is not activated", async ({
+ page,
+ }) => {
+ // Setup the workspace with token features enabled
+ const {
+ workspacePage,
+ tokensSidebar,
+ tokenContextMenuForToken,
+ tokenThemesSetsSidebar,
+ } = await setupTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
+ // Create a token with a reference value in other set.
+ await createToken(page, "Dimensions", "reference-token", "Value", "{card.padding}");
+
+
+ // Apply this token to a shape
+ 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();
+ await unfoldTokenType(tokensSidebar, "dimensions");
+
+ await tokensSidebar
+ .getByRole("button", { name: "reference-token" })
+ .click({ button: "right" });
+
+ await tokenContextMenuForToken.getByText("X", { exact: true }).click();
+
+ //Check if token is applied and visible on right sidebar
+ const measuresSection = page.getByRole("region", {
+ name: "shape-measures-section",
+ });
+ await expect(measuresSection).toBeVisible();
+
+ await expect(measuresSection.getByRole('button', { name: 'reference-token' })).toBeVisible();
+
+ // Deactivate token set where reference token exist to make token broken
+ await tokenThemesSetsSidebar.getByRole('button', { name: 'theme' }).getByRole('checkbox').click();
+
+ // Check if token pill show broken reference state
+ const brokenPill = measuresSection.getByRole("button", {
+ name: "is not in any active set",
+ });
+ await expect(brokenPill).toHaveCount(2);
+ });
+});
diff --git a/frontend/playwright/ui/specs/tokens/crud.spec.js b/frontend/playwright/ui/specs/tokens/crud.spec.js
index ac72a2cc7c..4bfd1c6a4b 100644
--- a/frontend/playwright/ui/specs/tokens/crud.spec.js
+++ b/frontend/playwright/ui/specs/tokens/crud.spec.js
@@ -6,7 +6,8 @@ import {
setupTokensFileRender,
setupTypographyTokensFileRender,
testTokenCreationFlow,
- unfoldTokenTree,
+ unfoldTokenType,
+ createToken,
} from "./helpers";
test.beforeEach(async ({ page }) => {
@@ -31,15 +32,9 @@ test.describe("Tokens - creation", () => {
});
test("User creates border radius token with combobox", async ({ page }) => {
- const invalidValueError = "Invalid token value";
- const emptyNameError = "Name should be at least 1 character";
- const selfReferenceError = "Token has self reference";
- const missingReferenceError = "Missing token references";
-
- const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
- await setupEmptyTokensFileRender(page, {
- flags: ["enable-token-combobox", "enable-feature-token-input"],
- });
+ const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page, {
+ flags: ["enable-token-combobox", "enable-feature-token-input"],
+ });
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -83,8 +78,10 @@ test.describe("Tokens - creation", () => {
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "border radius");
+
await expect(
- tokensTabPanel.getByRole('button', { name: 'my-token' }),
+ tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
// Create second token referencing the first one using the combobox options
@@ -310,7 +307,7 @@ test.describe("Tokens - creation", () => {
await expect(submitButton).toBeEnabled();
await submitButton.click();
- await unfoldTokenTree(tokensSidebar, "color", "color.primary");
+ await unfoldTokenType(tokensSidebar, "color");
// Create token referencing the previous one with keyboard
@@ -477,6 +474,8 @@ test.describe("Tokens - creation", () => {
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "font family");
+
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
@@ -631,6 +630,8 @@ test.describe("Tokens - creation", () => {
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "font weight");
+
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
@@ -767,6 +768,8 @@ test.describe("Tokens - creation", () => {
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "text case");
+
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
@@ -885,6 +888,8 @@ test.describe("Tokens - creation", () => {
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "text decoration");
+
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
@@ -914,7 +919,9 @@ test.describe("Tokens - creation", () => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
- await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
+ await setupEmptyTokensFileRender(page, {
+ flags: ["enable-token-shadow"],
+ });
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1049,6 +1056,8 @@ test.describe("Tokens - creation", () => {
await expect(submitButton).toBeEnabled();
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "shadow");
+
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
@@ -1086,6 +1095,8 @@ test.describe("Tokens - creation", () => {
await expect(submitButton).toBeEnabled();
await submitButton.click();
+
+ await unfoldTokenType(tokensTabPanel, "shadow");
await expect(
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
).toBeEnabled();
@@ -1107,7 +1118,9 @@ test.describe("Tokens - creation", () => {
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("typography.empty");
- const valueField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Size" });
+ const valueField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Font Size",
+ });
// Insert a value and then delete it
await valueField.fill("1");
@@ -1130,7 +1143,9 @@ test.describe("Tokens - creation", () => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
- await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
+ await setupEmptyTokensFileRender(page, {
+ flags: ["enable-token-shadow"],
+ });
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1270,6 +1285,8 @@ test.describe("Tokens - creation", () => {
await expect(submitButton).toBeEnabled();
await submitButton.click();
+ await unfoldTokenType(tokensTabPanel, "shadow");
+
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
@@ -1576,7 +1593,8 @@ test.describe("Tokens - creation", () => {
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newTokenTitle);
- const referenceTabButton = tokensUpdateCreateModal.getByTestId("reference-opt");
+ const referenceTabButton =
+ tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceTabButton.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
@@ -1637,7 +1655,7 @@ test.describe("Tokens - creation", () => {
await expect(submitButton).toBeEnabled();
await submitButton.click();
- await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
+ await unfoldTokenType(tokensSidebar, "color");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
@@ -1676,10 +1694,10 @@ test.describe("Tokens - creation", () => {
await expect(tokensSidebar).toBeVisible();
- unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
+ await unfoldTokenType(tokensSidebar, "color");
const colorToken = tokensSidebar.getByRole("button", {
- name: "100",
+ name: "colors.blue.100",
});
await colorToken.click({ button: "right" });
@@ -1719,7 +1737,7 @@ test("User creates grouped color token", async ({ page }) => {
await expect(submitButton).toBeEnabled();
await submitButton.click();
- await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
+ await unfoldTokenType(tokensSidebar, "color");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
@@ -1756,10 +1774,10 @@ test("User duplicate color token", async ({ page }) => {
await expect(tokensSidebar).toBeVisible();
- unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
+ await unfoldTokenType(tokensSidebar, "color");
const colorToken = tokensSidebar.getByRole("button", {
- name: "100",
+ name: "colors.blue.100",
});
await colorToken.click({ button: "right" });
@@ -1773,6 +1791,27 @@ test("User duplicate color token", async ({ page }) => {
).toBeVisible();
});
+test("User disables the current set but token still have resolved values shown in the sidebar", async ({
+ page,
+}) => {
+ const { tokenThemesSetsSidebar, tokensSidebar } = await setupEmptyTokensFileRender(page);
+
+ // Create color token
+ await createToken(page, "Color", "color.primary", "Value", "#ff0000");
+ await unfoldTokenType(tokensSidebar, "color");
+
+ // Deactivate current set
+ await tokenThemesSetsSidebar
+ .getByRole("checkbox")
+ .click();
+
+ // Tokens tab panel should have a token with the color #ff0000 and correct resolved value in the tooltip
+ const colorTokenPill = tokensSidebar.getByRole("button", { name: "#ff0000 color.primary" });
+ await expect(colorTokenPill).toHaveCount(1);
+ await colorTokenPill.hover(); // Force title attribute to be attached to the button
+ await expect(colorTokenPill).toHaveAttribute("title", /Resolved value: #ff0000/);
+});
+
test.describe("Tokens tab - edition", () => {
test("User edits typography token and all fields are valid", async ({
page,
@@ -1804,7 +1843,9 @@ test.describe("Tokens tab - edition", () => {
await fontFamilyField.fill("OneWord");
// Invalidate incorrect values for font size
- const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Size" });
+ const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Font Size",
+ });
await fontSizeField.fill("invalid");
await expect(
tokensUpdateCreateModal.getByText(/Invalid token value:/),
@@ -1819,13 +1860,21 @@ test.describe("Tokens tab - edition", () => {
await fontSizeField.fill("16");
await expect(saveButton).toBeEnabled();
- const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", { name: "Font Weight" });
- const letterSpacingField =
- tokensUpdateCreateModal.getByRole("textbox", { name: "Letter Spacing" });
- const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", { name: "Line Height" });
- const textCaseField = tokensUpdateCreateModal.getByRole("textbox", { name: "Text Case" });
- const textDecorationField =
- tokensUpdateCreateModal.getByRole("textbox", { name: "Text Decoration" });
+ const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Font Weight",
+ });
+ const letterSpacingField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Letter Spacing",
+ });
+ const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Line Height",
+ });
+ const textCaseField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Text Case",
+ });
+ const textDecorationField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: "Text Decoration",
+ });
// Capture all values before switching tabs
const originalValues = {
@@ -1878,10 +1927,10 @@ test.describe("Tokens tab - edition", () => {
await expect(tokensSidebar).toBeVisible();
- await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
+ await unfoldTokenType(tokensSidebar, "color");
const colorToken = tokensSidebar.getByRole("button", {
- name: "100",
+ name: "colors.blue.100",
});
await expect(colorToken).toBeVisible();
@@ -1899,7 +1948,7 @@ test.describe("Tokens tab - edition", () => {
await expect(tokensUpdateCreateModal).not.toBeVisible();
- await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100.changed");
+ await unfoldTokenType(tokensSidebar, "color");
const colorTokenChanged = tokensSidebar.getByRole("button", {
name: "changed",
@@ -1970,10 +2019,10 @@ test.describe("Tokens tab - delete", () => {
await expect(tokensSidebar).toBeVisible();
- unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
+ await unfoldTokenType(tokensSidebar, "color");
const colorToken = tokensSidebar.getByRole("button", {
- name: "100",
+ name: "colors.blue.100",
});
await expect(colorToken).toBeVisible();
await colorToken.click({ button: "right" });
@@ -1984,48 +2033,4 @@ test.describe("Tokens tab - delete", () => {
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(colorToken).not.toBeVisible();
});
-
- test("User removes node and all child tokens", async ({ page }) => {
- const { tokensSidebar } = await setupTokensFileRender(page);
-
- await expect(tokensSidebar).toBeVisible();
-
- // Expand color tokens
- unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
-
- // Verify that the node and child token are visible before deletion
- const colorNode = tokensSidebar.getByRole("button", {
- name: "colors",
- exact: true,
- });
- const colorNodeToken = tokensSidebar.getByRole("button", {
- name: "100",
- });
-
- // Select a node and right click on it to open context menu
- await expect(colorNode).toBeVisible();
- await expect(colorNodeToken).toBeVisible();
- await colorNode.click({ button: "right" });
-
- // select "Delete" from the context menu
- const deleteNodeButton = page.getByRole("button", {
- name: "Delete",
- exact: true,
- });
- await expect(deleteNodeButton).toBeVisible();
- await deleteNodeButton.click();
-
- // Verify that the node is removed
- await expect(colorNode).not.toBeVisible();
- // Verify that child token is also removed
- await expect(colorNodeToken).not.toBeVisible();
-
- // Save the type button to verify that expands/folds
- const tokenTypeButton = await tokensSidebar.getByRole("button", {
- name: "Color",
- exact: true,
- });
-
- await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
- });
});
diff --git a/frontend/playwright/ui/specs/tokens/helpers.js b/frontend/playwright/ui/specs/tokens/helpers.js
index 63c54af0f9..937a268242 100644
--- a/frontend/playwright/ui/specs/tokens/helpers.js
+++ b/frontend/playwright/ui/specs/tokens/helpers.js
@@ -161,6 +161,7 @@ const setupTokensFileRender = async (page, options = {}) => {
workspacePage,
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
+ tokensRenameNodeModal: workspacePage.tokensRenameNodeModal,
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
tokenSetItems: workspacePage.tokenSetItems,
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
@@ -206,7 +207,7 @@ const testTokenCreationFlow = async (
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
- const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
+ const { tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFileRender(page);
// Open modal
@@ -312,12 +313,11 @@ const testTokenCreationFlow = async (
).toBeEnabled();
};
-const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
- const tokenSegments = tokenName.split(".");
- const tokenFolderTree = tokenSegments.slice(0, -1);
- const tokenLeafName = tokenSegments.pop();
-
- const typeParentWrapper = tokensTabPanel.getByTestId(`section-${type}`);
+const unfoldTokenType = async (tokensTabPanel, type) => {
+ const kebabClaseType = type.toLocaleLowerCase().replace(/\s/g, "-");
+ const typeParentWrapper = tokensTabPanel.getByTestId(
+ `section-${kebabClaseType}`,
+ );
const typeSectionButton = typeParentWrapper
.getByRole("button", {
name: type,
@@ -330,24 +330,34 @@ const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
if (isSectionExpanded === "false") {
await typeSectionButton.click();
}
+};
- for (const segment of tokenFolderTree) {
- const segmentButton = typeParentWrapper
- .getByRole("listitem")
- .getByRole("button", { name: segment })
- .first();
+const createToken = async (page, type, name, textFieldName, value) => {
+ const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
- const isExpanded = await segmentButton.getAttribute("aria-expanded");
- if (isExpanded === "false") {
- await segmentButton.click();
- }
- }
+ const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
+ flags: ["enable-token-shadow"],
+ });
- await expect(
- typeParentWrapper.getByRole("button", {
- name: tokenLeafName,
- }),
- ).toBeEnabled();
+ // Create base token
+ await tokensTabPanel
+ .getByRole("button", { name: `Add Token: ${type}` })
+ .click();
+ await expect(tokensUpdateCreateModal).toBeVisible();
+
+ const nameField = tokensUpdateCreateModal.getByLabel("Name");
+ await nameField.fill(name);
+
+ const colorField = tokensUpdateCreateModal.getByRole("textbox", {
+ name: textFieldName,
+ });
+ await colorField.fill(value);
+
+ const submitButton = tokensUpdateCreateModal.getByRole("button", {
+ name: "Save",
+ });
+ await submitButton.click();
+ await expect(tokensUpdateCreateModal).not.toBeVisible();
};
export {
@@ -358,5 +368,6 @@ export {
setupTypographyTokensFile,
setupTypographyTokensFileRender,
testTokenCreationFlow,
- unfoldTokenTree,
+ unfoldTokenType,
+ createToken,
};
diff --git a/frontend/playwright/ui/specs/tokens/remapping.spec.js b/frontend/playwright/ui/specs/tokens/remapping.spec.js
index 4563b491b3..55472cb4a1 100644
--- a/frontend/playwright/ui/specs/tokens/remapping.spec.js
+++ b/frontend/playwright/ui/specs/tokens/remapping.spec.js
@@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import {
+ createToken,
setupTokensFileRender,
setupTypographyTokensFileRender,
} from "./helpers";
@@ -14,34 +15,6 @@ test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
-const createToken = async (page, type, name, textFieldName, value) => {
- const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
-
- const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
- flags: ["enable-token-shadow"],
- });
-
- // Create base token
- await tokensTabPanel
- .getByRole("button", { name: `Add Token: ${type}` })
- .click();
- await expect(tokensUpdateCreateModal).toBeVisible();
-
- const nameField = tokensUpdateCreateModal.getByLabel("Name");
- await nameField.fill(name);
-
- const colorField = tokensUpdateCreateModal.getByRole("textbox", {
- name: textFieldName,
- });
- await colorField.fill(value);
-
- const submitButton = tokensUpdateCreateModal.getByRole("button", {
- name: "Save",
- });
- await submitButton.click();
- await expect(tokensUpdateCreateModal).not.toBeVisible();
-};
-
const createTokenCombobox = async (page, type, name, textFieldName, value) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -123,7 +96,7 @@ const createCompositeDerivedToken = async (page, type, name, reference) => {
await expect(tokensUpdateCreateModal).not.toBeVisible();
};
-test.describe("Remapping Tokens", () => {
+test.describe("Remapping a single token", () => {
test.describe("Box Shadow Token Remapping", () => {
test("User renames box shadow token with alias references", async ({
page,
@@ -634,3 +607,92 @@ test.describe("Remapping Tokens", () => {
});
});
});
+
+test.describe("Remapping group of tokens", () => {
+ test("User renames a group - and remaps", async ({ page }) => {
+ const { tokensSidebar } = await setupTokensFileRender(page);
+ const workspacePage = new WasmWorkspacePage(page);
+ const rightSidebar = workspacePage.rightSidebar;
+
+ // Create multiple tokens in a group
+ await createToken(page, "Color", "light.primary", "Value", "#FFFFFF");
+ await createToken(page, "Color", "light.secondary", "Value", "#EEEEEE");
+
+ // Verify that the node and child token are visible before deletion
+ const lightNode = tokensSidebar.getByRole("button", {
+ name: "light",
+ exact: true,
+ });
+ const lightNodeToken = tokensSidebar.getByRole("button", {
+ name: "primary",
+ });
+
+ // Select a node and right click on it to open context menu
+ await expect(lightNode).toBeVisible();
+ await expect(lightNodeToken).toBeVisible();
+
+ // Apply token to a shape to ensure remapping modal appears with applied token reference
+ await page.getByRole("tab", { name: "Layers" }).click();
+ await page
+ .getByTestId("layer-row")
+ .filter({ hasText: "Rectangle" })
+ .first()
+ .click();
+
+ await page.getByRole("tab", { name: "Tokens" }).click();
+ const lightPrimaryToken = tokensSidebar.getByRole("button", {
+ name: "primary",
+ });
+ await lightPrimaryToken.click();
+
+ // Right click on the node to rename
+
+ await lightNode.click({ button: "right" });
+ const renameNodeButton = page.getByRole("button", {
+ name: "Rename",
+ exact: true,
+ });
+ await expect(renameNodeButton).toBeVisible();
+ await renameNodeButton.click();
+
+ // Expect the rename modal to be visible, fill in the new name and submit
+ const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
+ await expect(tokenRenameNodeModal).toBeVisible();
+
+ const nameField = tokenRenameNodeModal.getByRole("textbox", {
+ name: "Name",
+ });
+ await nameField.fill("lighter");
+
+ const submitButton = tokenRenameNodeModal.getByRole("button", {
+ name: "Rename",
+ });
+ await submitButton.click();
+
+ // Ensure that the remapping modal appears and confirm remap
+ const remappingModal = page.getByTestId("token-remapping-modal");
+ await expect(remappingModal).toBeVisible({ timeout: 5000 });
+
+ const confirmButton = remappingModal.getByRole("button", {
+ name: "remap tokens",
+ });
+ await confirmButton.click();
+
+ // Verify that the node has been renamed and tokens are still visible
+ const lighterNode = tokensSidebar.getByRole("button", {
+ name: "lighter",
+ exact: true,
+ });
+
+ await expect(lighterNode).toBeVisible();
+
+ // Verify that the applied token reference has been updated in the right sidebar for the selected shape
+ const fillSection = rightSidebar.getByRole("region", { name: "Fill section" });
+ await expect(fillSection).toBeVisible();
+
+ const tokenReference = fillSection.getByLabel("lighter.primary", {
+ exact: true,
+ });
+ await expect(tokenReference).toBeVisible();
+ });
+});
diff --git a/frontend/playwright/ui/specs/tokens/tree.spec.js b/frontend/playwright/ui/specs/tokens/tree.spec.js
index ae43197acc..243a539432 100644
--- a/frontend/playwright/ui/specs/tokens/tree.spec.js
+++ b/frontend/playwright/ui/specs/tokens/tree.spec.js
@@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
-import { setupTokensFileRender, unfoldTokenTree } from "./helpers";
+import { createToken, setupTokensFileRender, unfoldTokenType } from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
@@ -20,13 +20,168 @@ test.describe("Tokens - node tree", () => {
await expect(tokensColorGroup).toBeVisible();
await tokensColorGroup.click();
- await unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
-
const colorToken = tokensSidebar.getByRole("button", {
- name: "100",
+ name: "colors.blue.100",
});
await expect(colorToken).toBeVisible();
await tokensColorGroup.click();
await expect(colorToken).not.toBeVisible();
});
+
+ test("User renames a group", async ({ page }) => {
+ const { tokensSidebar } = await setupTokensFileRender(page);
+
+ // Create multiple tokens in a group
+ await createToken(page, "Color", "dark.primary", "Value", "#000000");
+ await createToken(page, "Color", "dark.secondary", "Value", "#111111");
+
+ // Verify that the node and child token are visible before deletion
+ const darkNode = tokensSidebar.getByRole("button", {
+ name: "dark",
+ exact: true,
+ });
+ const darkNodeToken = tokensSidebar.getByRole("button", {
+ name: "primary",
+ });
+
+ // Select a node and right click on it to open context menu
+ await expect(darkNode).toBeVisible();
+ await expect(darkNodeToken).toBeVisible();
+ await darkNode.click({ button: "right" });
+
+ // select "Rename" from the context menu
+ const renameNodeButton = page.getByRole("button", {
+ name: "Rename",
+ exact: true,
+ });
+ await expect(renameNodeButton).toBeVisible();
+ await renameNodeButton.click();
+
+ // Expect the rename modal to be visible, fill in the new name and submit
+ const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
+ await expect(tokenRenameNodeModal).toBeVisible();
+
+ const nameField = tokenRenameNodeModal.getByRole("textbox", {
+ name: "Name",
+ });
+ await nameField.fill("darker");
+
+ const submitButton = tokenRenameNodeModal.getByRole("button", {
+ name: "Rename",
+ });
+ await submitButton.click();
+
+ // Ensure that the remapping modal does not appear
+ const remappingModal = page.getByTestId("token-remapping-modal");
+ await expect(remappingModal).not.toBeVisible();
+
+ // Verify that the node has been renamed and tokens are still visible
+ const darkerNode = tokensSidebar.getByRole("button", {
+ name: "darker",
+ exact: true,
+ });
+
+ await expect(darkerNode).toBeVisible();
+ });
+
+ test("User duplicates a group", async ({ page }) => {
+ const { tokensSidebar } = await setupTokensFileRender(page);
+
+ // Create multiple tokens in a group
+ await createToken(page, "Color", "dark.primary", "Value", "#000000");
+ await createToken(page, "Color", "dark.secondary", "Value", "#111111");
+
+ // Verify that the node and child token are visible before deletion
+ const darkNode = tokensSidebar.getByRole("button", {
+ name: "dark",
+ exact: true,
+ });
+ const darkNodeToken = tokensSidebar.getByRole("button", {
+ name: "primary",
+ });
+
+ // Select a node and right click on it to open context menu
+ await expect(darkNode).toBeVisible();
+ await expect(darkNodeToken).toBeVisible();
+ await darkNode.click({ button: "right" });
+
+ // select "Duplicate" from the context menu
+ const duplicateNodeButton = page.getByRole("button", {
+ name: "Duplicate",
+ exact: true,
+ });
+ await expect(duplicateNodeButton).toBeVisible();
+ await duplicateNodeButton.click();
+
+ // Expect the duplicate modal to be visible, fill in the new name and submit
+ const tokenDuplicateNodeModal = page.getByTestId("token-rename-node-modal");
+ await expect(tokenDuplicateNodeModal).toBeVisible();
+
+ const nameField = tokenDuplicateNodeModal.getByRole("textbox", {
+ name: "Name",
+ });
+ await nameField.fill("darker");
+
+ const submitButton = tokenDuplicateNodeModal.getByRole("button", {
+ name: "Duplicate",
+ });
+ await submitButton.click();
+
+ // Verify that the node has been duplicated and tokens are visible
+ const darkerNode = tokensSidebar.getByRole("button", {
+ name: "darker",
+ exact: true,
+ });
+
+ const darkerNodeToken = tokensSidebar.getByRole("button", {
+ name: "darker.primary",
+ });
+
+ await expect(darkerNode).toBeVisible();
+ await expect(darkerNodeToken).toBeVisible();
+ });
+
+ test("User removes node and all child tokens", async ({ page }) => {
+ const { tokensSidebar } = await setupTokensFileRender(page);
+
+ await expect(tokensSidebar).toBeVisible();
+
+ // Expand color tokens
+ await unfoldTokenType(tokensSidebar, "color");
+
+ // Verify that the node and child token are visible before deletion
+ const colorNode = tokensSidebar.getByRole("button", {
+ name: "colors",
+ exact: true,
+ });
+ const colorNodeToken = tokensSidebar.getByRole("button", {
+ name: "colors.blue.100",
+ });
+
+ // Select a node and right click on it to open context menu
+ await expect(colorNode).toBeVisible();
+ await expect(colorNodeToken).toBeVisible();
+ await colorNode.click({ button: "right" });
+
+ // select "Delete" from the context menu
+ const deleteNodeButton = page.getByRole("button", {
+ name: "Delete",
+ exact: true,
+ });
+ await expect(deleteNodeButton).toBeVisible();
+ await deleteNodeButton.click();
+
+ // Verify that the node is removed
+ await expect(colorNode).not.toBeVisible();
+ // Verify that child token is also removed
+ await expect(colorNodeToken).not.toBeVisible();
+
+ // Save the type button to verify that expands/folds
+ const tokenTypeButton = await tokensSidebar.getByRole("button", {
+ name: "Color",
+ exact: true,
+ });
+
+ await expect(tokenTypeButton).toHaveAttribute("aria-expanded", "false");
+ });
});
diff --git a/frontend/playwright/ui/specs/workspace-modifers.spec.js b/frontend/playwright/ui/specs/workspace-modifers.spec.js
index 8e5f871fd8..bbea6199f8 100644
--- a/frontend/playwright/ui/specs/workspace-modifers.spec.js
+++ b/frontend/playwright/ui/specs/workspace-modifers.spec.js
@@ -3,13 +3,17 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
+ await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]);
});
test("BUG 13305 - Fix resize board to fit content", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13305.json");
- await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-13305.json");
+ await workspacePage.mockRPC(
+ "update-file?id=*",
+ "workspace/update-file-13305.json",
+ );
await workspacePage.goToWorkspace({
fileId: "9666e946-78e8-8111-8007-8fe5f0f454bf",
@@ -17,12 +21,42 @@ test("BUG 13305 - Fix resize board to fit content", async ({ page }) => {
});
await workspacePage.clickLeafLayer("Board");
- await workspacePage.rightSidebar.getByRole("button", { name: "Resize board to fit content" }).click();
+ await workspacePage.rightSidebar
+ .getByRole("button", { name: "Resize board to fit content" })
+ .click();
- await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("630");
- await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("630");
- await expect(workspacePage.rightSidebar.getByTitle("X axis").getByRole("textbox")).toHaveValue("110");
- await expect(workspacePage.rightSidebar.getByTitle("Y axis").getByRole("textbox")).toHaveValue("110");
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
+ await expect(measuresSection).toBeVisible();
+
+ // Width
+ const widthInput = measuresSection.getByRole("textbox", {
+ name: "Width",
+ exact: true,
+ });
+ await expect(widthInput).toHaveValue("630");
+
+ // Height
+ const heightInput = measuresSection.getByRole("textbox", {
+ name: "Height",
+ exact: true,
+ });
+ await expect(heightInput).toHaveValue("630");
+
+ // X Position (using "X axis" title)
+ const xPosInput = measuresSection.getByRole("textbox", {
+ name: "X axis",
+ exact: true,
+ });
+ await expect(xPosInput).toHaveValue("110");
+
+ // Y Position (using "Y axis" title)
+ const yPosInput = measuresSection.getByRole("textbox", {
+ name: "Y axis",
+ exact: true,
+ });
+ await expect(yPosInput).toHaveValue("110");
});
test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
@@ -35,7 +69,10 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
"workspace/get-file-13382-fragment.json",
);
- await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
+ await workspacePage.mockRPC(
+ "update-file?id=*",
+ "workspace/update-file-empty.json",
+ );
await workspacePage.goToWorkspace({
fileId: "52c4e771-3853-8190-8007-9506c70e8100",
@@ -47,13 +84,26 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
await workspacePage.clickToggableLayer("C");
await workspacePage.clickLeafLayer("R2");
- const heightText = workspacePage.rightSidebar.getByTitle("Height").getByPlaceholder('--');
- await heightText.fill("200");
- await heightText.press("Enter");
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
+ await expect(measuresSection).toBeVisible();
+
+ const heightInput = measuresSection.getByRole("textbox", {
+ name: "Height",
+ exact: true,
+ });
+ await heightInput.fill("200");
+ await heightInput.press("Enter");
await workspacePage.clickLeafLayer("B");
- await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340");
+ // Width
+ const widthInput = measuresSection.getByRole("textbox", {
+ name: "Width",
+ exact: true,
+ });
+ await expect(widthInput).toHaveValue("393");
});
test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => {
@@ -66,7 +116,10 @@ test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => {
"workspace/get-file-13468-fragment.json",
);
- await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
+ await workspacePage.mockRPC(
+ "update-file?id=*",
+ "workspace/update-file-empty.json",
+ );
await workspacePage.goToWorkspace({
fileId: "3a4d7ec7-c391-8146-8007-9a05c41da6b9",
@@ -76,10 +129,21 @@ test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => {
await workspacePage.clickToggableLayer("Parent");
await workspacePage.clickToggableLayer("Container");
- await workspacePage.sidebar.getByRole('button', { name: 'Show' }).click();
+ await workspacePage.sidebar.getByRole("button", { name: "Show" }).click();
await workspacePage.clickLeafLayer("Container");
- await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("76");
+
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
+ await expect(measuresSection).toBeVisible();
+
+ const heightInput = measuresSection.getByRole("textbox", {
+ name: "Height",
+ exact: true,
+ });
+
+ await expect(heightInput).toHaveValue("76");
});
test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => {
@@ -92,7 +156,10 @@ test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => {
"workspace/get-file-13272-fragment.json",
);
- await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
+ await workspacePage.mockRPC(
+ "update-file?id=*",
+ "workspace/update-file-empty.json",
+ );
await workspacePage.goToWorkspace({
fileId: "3b9773cc-d4f1-81e1-8007-b3f8dcaba770",
@@ -102,15 +169,31 @@ test("BUG 13272 - Fix problem with snap to pixel", async ({ page }) => {
await workspacePage.clickToggableLayer("Group");
await workspacePage.clickLeafLayer("Group");
- await workspacePage.page.locator('g:nth-child(11) > .cursor-resize-nesw-0').hover();
+ await workspacePage.page
+ .locator("g:nth-child(11) > .cursor-resize-nesw-0")
+ .hover();
await workspacePage.page.mouse.down();
await workspacePage.page.mouse.move(1200, 800);
await workspacePage.page.mouse.up();
await workspacePage.clickLeafLayer("Rectangle");
- await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("197.5");
- await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("128.28");
+
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
+ await expect(measuresSection).toBeVisible();
+
+ const heightInput = measuresSection.getByRole("textbox", {
+ name: "Height",
+ exact: true,
+ });
+ const widthInput = measuresSection.getByRole("textbox", {
+ name: "Width",
+ exact: true,
+ });
+ await expect(widthInput).toHaveValue("197.5");
+ await expect(heightInput).toHaveValue("128.28");
});
test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => {
@@ -123,7 +206,10 @@ test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => {
"workspace/get-file-13755-fragment.json",
);
- await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
+ await workspacePage.mockRPC(
+ "update-file?id=*",
+ "workspace/update-file-empty.json",
+ );
await workspacePage.goToWorkspace({
fileId: "7fd33337-c651-80ae-8007-c357213f876e",
@@ -133,9 +219,19 @@ test("BUG 13755 - Fix problem with text change modiifers", async ({ page }) => {
await workspacePage.clickToggableLayer("Board");
await workspacePage.clickLeafLayer("uno dos tres cuatro");
- await workspacePage.page.keyboard.press('Enter');
- await workspacePage.page.keyboard.type('test');
+ await workspacePage.page.keyboard.press("Enter");
+ await workspacePage.page.keyboard.type("test");
await workspacePage.clickToggableLayer("Board");
- await expect(workspacePage.rightSidebar.getByTitle("Width").getByRole("textbox")).toHaveValue("23");
+
+ const measuresSection = workspacePage.rightSidebar.getByRole("region", {
+ name: "shape-measures-section",
+ });
+ await expect(measuresSection).toBeVisible();
+
+ const widthInput = measuresSection.getByRole("textbox", {
+ name: "Width",
+ exact: true,
+ });
+ await expect(widthInput).toHaveValue("23");
});
diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js
index 5e3f1a5eff..9a61a8ae97 100644
--- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js
+++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js
@@ -9,6 +9,7 @@ test("User goes to an empty dashboard", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.goToDashboard();
+ await expect(dashboardPage.page).toHaveURL(/dashboard/);
await expect(dashboardPage.mainHeading).toBeVisible();
await expect(dashboardPage.page).toHaveScreenshot();
@@ -122,9 +123,7 @@ test("User goes to a full search page", async ({ page }) => {
await dashboardPage.searchInput.fill("3");
await expect(dashboardPage.mainHeading).toHaveText("Search results");
- await expect(
- dashboardPage.page.getByRole("button", { name: "New File 3" }),
- ).toBeVisible();
+ await expect(page.getByRole("button", { name: "New File 3" })).toBeVisible();
await expect(dashboardPage.page).toHaveScreenshot();
});
@@ -202,6 +201,10 @@ test("User opens teams selector with more than one team", async ({ page }) => {
test("User goes to second team", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
+ await dashboardPage.mockRPC(
+ `get-projects?team-id=${DashboardPage.secondTeamId}`,
+ "dashboard/get-projects-second-team.json",
+ );
await dashboardPage.goToDashboard();
await dashboardPage.teamDropdown.click();
@@ -216,6 +219,10 @@ test("User goes to second team", async ({ page }) => {
test("User opens team management dropdown", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
+ await dashboardPage.mockRPC(
+ `get-projects?team-id=${DashboardPage.secondTeamId}`,
+ "dashboard/get-projects-second-team.json",
+ );
await dashboardPage.goToSecondTeamDashboard();
await expect(page.getByText("Team Up")).toBeVisible();
diff --git a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js
index 977eb57fcb..8361c263fb 100644
--- a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js
+++ b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js
@@ -103,7 +103,7 @@ test("User goes to the Viewer Inspect code", async ({ page }) => {
await expect(
viewerPage.page.getByRole("button", {
- name: "Toggle panel Size & Position",
+ name: "Toggle panel Size and position",
}),
).toBeVisible();
diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js
index c1cc4ecbcc..df766736a0 100644
--- a/frontend/playwright/ui/visual-specs/workspace.spec.js
+++ b/frontend/playwright/ui/visual-specs/workspace.spec.js
@@ -158,7 +158,9 @@ test.describe("Palette", () => {
.getByRole("button", { name: "Color Palette" })
.click();
await expect(
- workspace.palette.getByRole("button", { name: "#7798ff" }),
+ workspace.palette.getByText(
+ "There are no color styles in your library yet",
+ ),
).toBeVisible();
});
});
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index fdd69fb27a..5fa2751411 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -148,6 +148,9 @@ importers:
postcss-modules:
specifier: ^6.0.1
version: 6.0.1(postcss@8.5.8)
+ postcss-scss:
+ specifier: ^4.0.9
+ version: 4.0.9(postcss@8.5.8)
prettier:
specifier: 3.8.1
version: 3.8.1
@@ -199,6 +202,18 @@ importers:
style-dictionary:
specifier: 5.0.0-rc.1
version: 5.0.0-rc.1(tslib@2.8.1)
+ stylelint:
+ specifier: ^17.4.0
+ version: 17.4.0(typescript@6.0.2)
+ stylelint-config-standard-scss:
+ specifier: ^17.0.0
+ version: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@6.0.2))
+ stylelint-scss:
+ specifier: ^7.0.0
+ version: 7.0.0(stylelint@17.4.0(typescript@6.0.2))
+ stylelint-use-logical-spec:
+ specifier: ^5.0.1
+ version: 5.0.1(stylelint@17.4.0(typescript@6.0.2))
svg-sprite:
specifier: ^2.0.4
version: 2.0.4
@@ -555,6 +570,12 @@ packages:
'@bundled-es-modules/postcss-calc-ast-parser@0.1.6':
resolution: {integrity: sha512-y65TM5zF+uaxo9OeekJ3rxwTINlQvrkbZLogYvQYVoLtxm4xEiHfZ7e/MyiWbStYyWZVZkVqsaVU6F4SUK5XUA==}
+ '@cacheable/memory@2.0.8':
+ resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==}
+
+ '@cacheable/utils@2.4.0':
+ resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==}
+
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
@@ -604,6 +625,9 @@ packages:
'@csstools/css-syntax-patches-for-csstree@1.0.26':
resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==}
+ '@csstools/css-syntax-patches-for-csstree@1.1.0':
+ resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==}
+
'@csstools/css-syntax-patches-for-csstree@1.1.2':
resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==}
peerDependencies:
@@ -616,6 +640,25 @@ packages:
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
+ '@csstools/media-query-list-parser@5.0.0':
+ resolution: {integrity: sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/selector-resolve-nested@4.0.0':
+ resolution: {integrity: sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ postcss-selector-parser: ^7.1.1
+
+ '@csstools/selector-specificity@6.0.0':
+ resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ postcss-selector-parser: ^7.1.1
+
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
@@ -1491,6 +1534,15 @@ packages:
peerDependencies:
tslib: '2'
+ '@keyv/bigmap@1.3.1':
+ resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ keyv: ^5.6.0
+
+ '@keyv/serialize@1.1.1':
+ resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
+
'@mdx-js/react@3.1.1':
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
peerDependencies:
@@ -2003,6 +2055,10 @@ packages:
'@sinclair/typebox@0.27.10':
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
+ '@sindresorhus/merge-streams@4.0.0':
+ resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+ engines: {node: '>=18'}
+
'@so-ric/colorspace@1.1.6':
resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==}
@@ -2526,6 +2582,10 @@ packages:
ast-v8-to-istanbul@1.0.0:
resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==}
+ astral-regex@2.0.0:
+ resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
+ engines: {node: '>=8'}
+
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
@@ -2659,6 +2719,9 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
+ cacheable@2.3.3:
+ resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==}
+
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -2797,6 +2860,9 @@ packages:
resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==}
engines: {node: '>=18'}
+ colord@2.9.3:
+ resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+
colorjs.io@0.4.5:
resolution: {integrity: sha512-yCtUNCmge7llyfd/Wou19PMAcf5yC3XXhgFoAh6zsO2pGswhUPBaaUh8jzgHnXtXuZyFKzXZNAnyF5i+apICow==}
@@ -2880,6 +2946,15 @@ packages:
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+ cosmiconfig@9.0.1:
+ resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
cross-fetch@3.2.0:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
@@ -2891,6 +2966,10 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ css-functions-list@3.3.3:
+ resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==}
+ engines: {node: '>=12'}
+
css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
@@ -3182,6 +3261,10 @@ packages:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
@@ -3411,6 +3494,10 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+ fastest-levenshtein@1.0.16:
+ resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
+ engines: {node: '>= 4.9.1'}
+
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -3435,6 +3522,9 @@ packages:
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+ file-entry-cache@11.1.2:
+ resolution: {integrity: sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==}
+
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -3458,6 +3548,9 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
+ flat-cache@6.1.20:
+ resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==}
+
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
@@ -3552,6 +3645,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
+ get-east-asian-width@1.5.0:
+ resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
+ engines: {node: '>=18'}
+
get-func-name@2.0.2:
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
@@ -3608,6 +3705,14 @@ packages:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ global-modules@2.0.0:
+ resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
+ engines: {node: '>=6'}
+
+ global-prefix@3.0.0:
+ resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==}
+ engines: {node: '>=6'}
+
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -3616,6 +3721,13 @@ packages:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
+ globby@16.1.1:
+ resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==}
+ engines: {node: '>=20'}
+
+ globjoin@0.1.4:
+ resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -3635,6 +3747,10 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-flag@5.0.1:
+ resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==}
+ engines: {node: '>=12'}
+
has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
@@ -3650,6 +3766,10 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
+ hashery@1.5.0:
+ resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==}
+ engines: {node: '>=20'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -3668,6 +3788,9 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
+ hookified@1.15.1:
+ resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==}
+
hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@@ -3678,6 +3801,10 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ html-tags@5.1.0:
+ resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
+ engines: {node: '>=20.10'}
+
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -3722,6 +3849,10 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
immutable@3.7.6:
resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==}
engines: {node: '>=0.8.0'}
@@ -3740,6 +3871,9 @@ packages:
resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
engines: {node: '>=8'}
+ import-meta-resolve@4.2.0:
+ resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
+
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
@@ -3870,10 +4004,18 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-path-inside@4.0.0:
+ resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==}
+ engines: {node: '>=12'}
+
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
+ is-plain-object@5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@@ -4025,6 +4167,9 @@ packages:
json-parse-better-errors@1.0.2:
resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -4060,9 +4205,19 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ keyv@5.6.0:
+ resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
+
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
klaw-sync@6.0.0:
resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==}
+ known-css-properties@0.37.0:
+ resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
+
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
@@ -4154,6 +4309,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
load-json-file@4.0.0:
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
engines: {node: '>=4'}
@@ -4186,6 +4344,9 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ lodash.truncate@4.4.2:
+ resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
+
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
@@ -4257,6 +4418,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
+ mathml-tag-names@4.0.0:
+ resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==}
+
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
@@ -4282,6 +4446,10 @@ packages:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
+ meow@14.1.0:
+ resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==}
+ engines: {node: '>=20'}
+
merge-descriptors@2.0.0:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
@@ -4585,6 +4753,10 @@ packages:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'}
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
parse5@8.0.0:
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
@@ -4730,6 +4902,9 @@ packages:
resolution: {integrity: sha512-DpuMWW19Dd2K9KY4wknMz3khq9q2yZYa2U37bnhzdtBdBv0ggIfUj5T2XD3ir6gKVlDkb5OtOqw1iQJWq6qvpw==}
engines: {node: '>=4.0.0'}
+ postcss-media-query-parser@0.2.3:
+ resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
+
postcss-modules-extract-imports@3.1.0:
resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
engines: {node: ^10 || ^12 || >= 14}
@@ -4759,6 +4934,21 @@ packages:
peerDependencies:
postcss: ^8.0.0
+ postcss-resolve-nested-selector@0.1.6:
+ resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
+
+ postcss-safe-parser@7.0.1:
+ resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
+ engines: {node: '>=18.0'}
+ peerDependencies:
+ postcss: ^8.4.31
+
+ postcss-scss@4.0.9:
+ resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.4.29
+
postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'}
@@ -4780,6 +4970,7 @@ packages:
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
+ deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
prelude-ls@1.2.1:
@@ -4849,6 +5040,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ qified@0.6.0:
+ resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==}
+ engines: {node: '>=20'}
+
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
@@ -5305,6 +5500,14 @@ packages:
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
engines: {node: '>=6'}
+ slash@5.1.0:
+ resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
+ engines: {node: '>=14.16'}
+
+ slice-ansi@4.0.0:
+ resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
+ engines: {node: '>=10'}
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -5382,6 +5585,10 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
+ string-width@8.2.0:
+ resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==}
+ engines: {node: '>=20'}
+
string.prototype.codepointat@0.2.1:
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
@@ -5461,6 +5668,59 @@ packages:
engines: {node: '>=22.0.0'}
hasBin: true
+ stylelint-config-recommended-scss@17.0.0:
+ resolution: {integrity: sha512-VkVD9r7jfUT/dq3mA3/I1WXXk2U71rO5wvU2yIil9PW5o1g3UM7Xc82vHmuVJHV7Y8ok5K137fmW5u3HbhtTOA==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ postcss: ^8.3.3
+ stylelint: ^17.0.0
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+
+ stylelint-config-recommended@18.0.0:
+ resolution: {integrity: sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ stylelint: ^17.0.0
+
+ stylelint-config-standard-scss@17.0.0:
+ resolution: {integrity: sha512-uLJS6xgOCBw5EMsDW7Ukji8l28qRoMnkRch15s0qwZpskXvWt9oPzMmcYM307m9GN4MxuWLsQh4I6hU9yI53cQ==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ postcss: ^8.3.3
+ stylelint: ^17.0.0
+ peerDependenciesMeta:
+ postcss:
+ optional: true
+
+ stylelint-config-standard@40.0.0:
+ resolution: {integrity: sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ stylelint: ^17.0.0
+
+ stylelint-scss@7.0.0:
+ resolution: {integrity: sha512-H88kCC+6Vtzj76NsC8rv6x/LW8slBzIbyeSjsKVlS+4qaEJoDrcJR4L+8JdrR2ORdTscrBzYWiiT2jq6leYR1Q==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ stylelint: ^16.8.2 || ^17.0.0
+
+ stylelint-use-logical-spec@5.0.1:
+ resolution: {integrity: sha512-UfLB4LW6iG4r3cXxjxkiHQrFyhWFqt8FpNNngD+TyvgMWSokk5TYwTvBHS3atUvZhOogllTOe/PUrGE+4z84AA==}
+ engines: {node: '>=8.0.0'}
+ peerDependencies:
+ stylelint: '>=11 < 17'
+
+ stylelint@17.4.0:
+ resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==}
+ engines: {node: '>=20.19.0'}
+ hasBin: true
+
+ supports-color@10.2.2:
+ resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
+ engines: {node: '>=18'}
+
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -5473,6 +5733,10 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
+ supports-hyperlinks@4.4.0:
+ resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==}
+ engines: {node: '>=20'}
+
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
@@ -5482,6 +5746,9 @@ packages:
engines: {node: '>=12'}
hasBin: true
+ svg-tags@1.0.0:
+ resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
+
svgo@2.8.0:
resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==}
engines: {node: '>=10.13.0'}
@@ -5503,6 +5770,10 @@ packages:
resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==}
engines: {node: '>=16.0.0'}
+ table@6.9.0:
+ resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
+ engines: {node: '>=10.0.0'}
+
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
@@ -5726,6 +5997,10 @@ packages:
resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==}
engines: {node: '>=20.18.1'}
+ unicorn-magic@0.4.0:
+ resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==}
+ engines: {node: '>=20'}
+
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -6037,6 +6312,10 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ write-file-atomic@7.0.1:
+ resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
@@ -6379,6 +6658,18 @@ snapshots:
dependencies:
postcss-calc-ast-parser: 0.1.4
+ '@cacheable/memory@2.0.8':
+ dependencies:
+ '@cacheable/utils': 2.4.0
+ '@keyv/bigmap': 1.3.1(keyv@5.6.0)
+ hookified: 1.15.1
+ keyv: 5.6.0
+
+ '@cacheable/utils@2.4.0':
+ dependencies:
+ hashery: 1.5.0
+ keyv: 5.6.0
+
'@colors/colors@1.6.0': {}
'@csstools/color-helpers@6.0.1': {}
@@ -6415,12 +6706,27 @@ snapshots:
'@csstools/css-syntax-patches-for-csstree@1.0.26': {}
+ '@csstools/css-syntax-patches-for-csstree@1.1.0': {}
+
'@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
'@csstools/css-tokenizer@4.0.0': {}
+ '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)':
+ dependencies:
+ postcss-selector-parser: 7.1.1
+
+ '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)':
+ dependencies:
+ postcss-selector-parser: 7.1.1
+
'@dabh/diagnostics@2.0.8':
dependencies:
'@so-ric/colorspace': 1.1.6
@@ -6998,6 +7304,14 @@ snapshots:
'@jsonjoy.com/codegen': 17.65.0(tslib@2.8.1)
tslib: 2.8.1
+ '@keyv/bigmap@1.3.1(keyv@5.6.0)':
+ dependencies:
+ hashery: 1.5.0
+ hookified: 1.15.1
+ keyv: 5.6.0
+
+ '@keyv/serialize@1.1.1': {}
+
'@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@types/mdx': 2.0.13
@@ -7367,6 +7681,8 @@ snapshots:
'@sinclair/typebox@0.27.10': {}
+ '@sindresorhus/merge-streams@4.0.0': {}
+
'@so-ric/colorspace@1.1.6':
dependencies:
color: 5.0.3
@@ -8030,6 +8346,8 @@ snapshots:
estree-walker: 3.0.3
js-tokens: 10.0.0
+ astral-regex@2.0.0: {}
+
async-function@1.0.0: {}
async@3.2.6: {}
@@ -8179,6 +8497,14 @@ snapshots:
cac@6.7.14: {}
+ cacheable@2.3.3:
+ dependencies:
+ '@cacheable/memory': 2.0.8
+ '@cacheable/utils': 2.4.0
+ hookified: 1.15.1
+ keyv: 5.6.0
+ qified: 0.6.0
+
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -8323,6 +8649,8 @@ snapshots:
color-convert: 3.1.3
color-string: 2.1.4
+ colord@2.9.3: {}
+
colorjs.io@0.4.5: {}
colorjs.io@0.5.2: {}
@@ -8393,6 +8721,15 @@ snapshots:
core-util-is@1.0.3: {}
+ cosmiconfig@9.0.1(typescript@6.0.2):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 6.0.2
+
cross-fetch@3.2.0(encoding@0.1.13):
dependencies:
node-fetch: 2.7.0(encoding@0.1.13)
@@ -8413,6 +8750,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ css-functions-list@3.3.3: {}
+
css-select@4.3.0:
dependencies:
boolbase: 1.0.0
@@ -8689,6 +9028,8 @@ snapshots:
entities@7.0.1: {}
+ env-paths@2.2.1: {}
+
error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
@@ -9165,6 +9506,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
+ fastest-levenshtein@1.0.16: {}
+
fastq@1.20.1:
dependencies:
reusify: 1.1.0
@@ -9191,6 +9534,10 @@ snapshots:
fflate@0.8.2: {}
+ file-entry-cache@11.1.2:
+ dependencies:
+ flat-cache: 6.1.20
+
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -9224,6 +9571,12 @@ snapshots:
flatted: 3.4.2
keyv: 4.5.4
+ flat-cache@6.1.20:
+ dependencies:
+ cacheable: 2.3.3
+ flatted: 3.3.3
+ hookified: 1.15.1
+
flatted@3.3.3: {}
flatted@3.4.2: {}
@@ -9304,6 +9657,8 @@ snapshots:
get-caller-file@2.0.5: {}
+ get-east-asian-width@1.5.0: {}
+
get-func-name@2.0.2: {}
get-intrinsic@1.3.0:
@@ -9377,6 +9732,16 @@ snapshots:
once: 1.4.0
path-is-absolute: 1.0.1
+ global-modules@2.0.0:
+ dependencies:
+ global-prefix: 3.0.0
+
+ global-prefix@3.0.0:
+ dependencies:
+ ini: 1.3.8
+ kind-of: 6.0.3
+ which: 1.3.1
+
globals@14.0.0: {}
globalthis@1.0.4:
@@ -9384,6 +9749,17 @@ snapshots:
define-properties: 1.2.1
gopd: 1.2.0
+ globby@16.1.1:
+ dependencies:
+ '@sindresorhus/merge-streams': 4.0.0
+ fast-glob: 3.3.3
+ ignore: 7.0.5
+ is-path-inside: 4.0.0
+ slash: 5.1.0
+ unicorn-magic: 0.4.0
+
+ globjoin@0.1.4: {}
+
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@@ -9394,6 +9770,8 @@ snapshots:
has-flag@4.0.0: {}
+ has-flag@5.0.1: {}
+
has-property-descriptors@1.0.2:
dependencies:
es-define-property: 1.0.1
@@ -9408,6 +9786,10 @@ snapshots:
dependencies:
has-symbols: 1.1.0
+ hashery@1.5.0:
+ dependencies:
+ hookified: 1.15.1
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -9422,6 +9804,8 @@ snapshots:
highlight.js@11.11.1: {}
+ hookified@1.15.1: {}
+
hosted-git-info@2.8.9: {}
html-encoding-sniffer@6.0.0:
@@ -9432,6 +9816,8 @@ snapshots:
html-escaper@2.0.2: {}
+ html-tags@5.1.0: {}
+
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -9476,6 +9862,8 @@ snapshots:
ignore@5.3.2: {}
+ ignore@7.0.5: {}
+
immutable@3.7.6: {}
immutable@5.1.4: {}
@@ -9489,6 +9877,8 @@ snapshots:
import-lazy@4.0.0: {}
+ import-meta-resolve@4.2.0: {}
+
imurmurhash@0.1.4: {}
indent-string@4.0.0: {}
@@ -9609,8 +9999,12 @@ snapshots:
is-number@7.0.0: {}
+ is-path-inside@4.0.0: {}
+
is-plain-obj@4.1.0: {}
+ is-plain-object@5.0.0: {}
+
is-potential-custom-element-name@1.0.1: {}
is-promise@4.0.0: {}
@@ -9806,6 +10200,8 @@ snapshots:
json-parse-better-errors@1.0.2: {}
+ json-parse-even-better-errors@2.3.1: {}
+
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -9845,10 +10241,18 @@ snapshots:
dependencies:
json-buffer: 3.0.1
+ keyv@5.6.0:
+ dependencies:
+ '@keyv/serialize': 1.1.1
+
+ kind-of@6.0.3: {}
+
klaw-sync@6.0.0:
dependencies:
graceful-fs: 4.2.11
+ known-css-properties@0.37.0: {}
+
kolorist@1.8.0: {}
kuler@2.0.0: {}
@@ -9913,6 +10317,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ lines-and-columns@1.2.4: {}
+
load-json-file@4.0.0:
dependencies:
graceful-fs: 4.2.11
@@ -9945,6 +10351,8 @@ snapshots:
lodash.merge@4.6.2: {}
+ lodash.truncate@4.4.2: {}
+
lodash@4.17.23: {}
lodash@4.18.1: {}
@@ -10012,6 +10420,8 @@ snapshots:
math-intrinsics@1.1.0: {}
+ mathml-tag-names@4.0.0: {}
+
mdn-data@2.0.14: {}
mdn-data@2.0.28: {}
@@ -10041,6 +10451,8 @@ snapshots:
memorystream@0.3.1: {}
+ meow@14.1.0: {}
+
merge-descriptors@2.0.0: {}
merge-stream@2.0.0: {}
@@ -10341,6 +10753,13 @@ snapshots:
error-ex: 1.3.4
json-parse-better-errors: 1.0.2
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
parse5@8.0.0:
dependencies:
entities: 6.0.1
@@ -10466,6 +10885,8 @@ snapshots:
clean-css: 4.2.4
postcss: 6.0.23
+ postcss-media-query-parser@0.2.3: {}
+
postcss-modules-extract-imports@3.1.0(postcss@8.5.8):
dependencies:
postcss: 8.5.8
@@ -10499,6 +10920,16 @@ snapshots:
postcss-modules-values: 4.0.0(postcss@8.5.8)
string-hash: 1.1.3
+ postcss-resolve-nested-selector@0.1.6: {}
+
+ postcss-safe-parser@7.0.1(postcss@8.5.8):
+ dependencies:
+ postcss: 8.5.8
+
+ postcss-scss@4.0.9(postcss@8.5.8):
+ dependencies:
+ postcss: 8.5.8
+
postcss-selector-parser@7.1.1:
dependencies:
cssesc: 3.0.0
@@ -10595,6 +11026,10 @@ snapshots:
punycode@2.3.1: {}
+ qified@0.6.0:
+ dependencies:
+ hookified: 1.15.1
+
qs@6.14.1:
dependencies:
side-channel: 1.1.0
@@ -11117,6 +11552,14 @@ snapshots:
slash@2.0.0: {}
+ slash@5.1.0: {}
+
+ slice-ansi@4.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -11203,6 +11646,11 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.2
+ string-width@8.2.0:
+ dependencies:
+ get-east-asian-width: 1.5.0
+ strip-ansi: 7.1.2
+
string.prototype.codepointat@0.2.1: {}
string.prototype.includes@2.0.1:
@@ -11317,6 +11765,93 @@ snapshots:
transitivePeerDependencies:
- tslib
+ stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@6.0.2)):
+ dependencies:
+ postcss-scss: 4.0.9(postcss@8.5.8)
+ stylelint: 17.4.0(typescript@6.0.2)
+ stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@6.0.2))
+ stylelint-scss: 7.0.0(stylelint@17.4.0(typescript@6.0.2))
+ optionalDependencies:
+ postcss: 8.5.8
+
+ stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@6.0.2)):
+ dependencies:
+ stylelint: 17.4.0(typescript@6.0.2)
+
+ stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@6.0.2)):
+ dependencies:
+ stylelint: 17.4.0(typescript@6.0.2)
+ stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@6.0.2))
+ stylelint-config-standard: 40.0.0(stylelint@17.4.0(typescript@6.0.2))
+ optionalDependencies:
+ postcss: 8.5.8
+
+ stylelint-config-standard@40.0.0(stylelint@17.4.0(typescript@6.0.2)):
+ dependencies:
+ stylelint: 17.4.0(typescript@6.0.2)
+ stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@6.0.2))
+
+ stylelint-scss@7.0.0(stylelint@17.4.0(typescript@6.0.2)):
+ dependencies:
+ css-tree: 3.1.0
+ is-plain-object: 5.0.0
+ known-css-properties: 0.37.0
+ mdn-data: 2.27.1
+ postcss-media-query-parser: 0.2.3
+ postcss-resolve-nested-selector: 0.1.6
+ postcss-selector-parser: 7.1.1
+ postcss-value-parser: 4.2.0
+ stylelint: 17.4.0(typescript@6.0.2)
+
+ stylelint-use-logical-spec@5.0.1(stylelint@17.4.0(typescript@6.0.2)):
+ dependencies:
+ stylelint: 17.4.0(typescript@6.0.2)
+
+ stylelint@17.4.0(typescript@6.0.2):
+ dependencies:
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-syntax-patches-for-csstree': 1.1.0
+ '@csstools/css-tokenizer': 4.0.0
+ '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1)
+ '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1)
+ colord: 2.9.3
+ cosmiconfig: 9.0.1(typescript@6.0.2)
+ css-functions-list: 3.3.3
+ css-tree: 3.1.0
+ debug: 4.4.3(supports-color@5.5.0)
+ fast-glob: 3.3.3
+ fastest-levenshtein: 1.0.16
+ file-entry-cache: 11.1.2
+ global-modules: 2.0.0
+ globby: 16.1.1
+ globjoin: 0.1.4
+ html-tags: 5.1.0
+ ignore: 7.0.5
+ import-meta-resolve: 4.2.0
+ imurmurhash: 0.1.4
+ is-plain-object: 5.0.0
+ mathml-tag-names: 4.0.0
+ meow: 14.1.0
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.5.8
+ postcss-safe-parser: 7.0.1(postcss@8.5.8)
+ postcss-selector-parser: 7.1.1
+ postcss-value-parser: 4.2.0
+ string-width: 8.2.0
+ supports-hyperlinks: 4.4.0
+ svg-tags: 1.0.0
+ table: 6.9.0
+ write-file-atomic: 7.0.1
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ supports-color@10.2.2: {}
+
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
@@ -11329,6 +11864,11 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ supports-hyperlinks@4.4.0:
+ dependencies:
+ has-flag: 5.0.1
+ supports-color: 10.2.2
+
supports-preserve-symlinks-flag@1.0.0: {}
svg-sprite@2.0.4:
@@ -11351,6 +11891,8 @@ snapshots:
xpath: 0.0.34
yargs: 17.7.2
+ svg-tags@1.0.0: {}
+
svgo@2.8.0:
dependencies:
'@trysound/sax': 0.2.0
@@ -11377,6 +11919,14 @@ snapshots:
sync-message-port@1.2.0: {}
+ table@6.9.0:
+ dependencies:
+ ajv: 8.13.0
+ lodash.truncate: 4.4.2
+ slice-ansi: 4.0.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
@@ -11591,6 +12141,8 @@ snapshots:
undici@7.24.7: {}
+ unicorn-magic@0.4.0: {}
+
universalify@2.0.1: {}
unpipe@1.0.0: {}
@@ -11938,6 +12490,10 @@ snapshots:
wrappy@1.0.2: {}
+ write-file-atomic@7.0.1:
+ dependencies:
+ signal-exit: 4.1.0
+
ws@8.19.0: {}
ws@8.20.0: {}
diff --git a/frontend/resources/images/assets/nitrate-welcome.svg b/frontend/resources/images/assets/nitrate-welcome.svg
new file mode 100644
index 0000000000..18ced86fa1
--- /dev/null
+++ b/frontend/resources/images/assets/nitrate-welcome.svg
@@ -0,0 +1,52 @@
+
diff --git a/frontend/resources/images/icons/stroke-center.svg b/frontend/resources/images/icons/stroke-center.svg
new file mode 100644
index 0000000000..a00cdf58df
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-center.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/stroke-dashed.svg b/frontend/resources/images/icons/stroke-dashed.svg
new file mode 100644
index 0000000000..40c3bdcae1
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-dashed.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/stroke-dotted.svg b/frontend/resources/images/icons/stroke-dotted.svg
new file mode 100644
index 0000000000..8b3c1940e3
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-dotted.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/stroke-inside.svg b/frontend/resources/images/icons/stroke-inside.svg
new file mode 100644
index 0000000000..21f2eb1c52
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-inside.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/stroke-mixed.svg b/frontend/resources/images/icons/stroke-mixed.svg
new file mode 100644
index 0000000000..56070d56ee
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-mixed.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/stroke-outside.svg b/frontend/resources/images/icons/stroke-outside.svg
new file mode 100644
index 0000000000..0f4dec0924
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-outside.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/stroke-solid.svg b/frontend/resources/images/icons/stroke-solid.svg
new file mode 100644
index 0000000000..a9bba0e9b9
--- /dev/null
+++ b/frontend/resources/images/icons/stroke-solid.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/newsletter-notification.svg b/frontend/resources/images/newsletter-notification.svg
new file mode 100644
index 0000000000..395e291284
--- /dev/null
+++ b/frontend/resources/images/newsletter-notification.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss
index 0d61ab7ebb..37df67b3e8 100644
--- a/frontend/resources/styles/common/base.scss
+++ b/frontend/resources/styles/common/base.scss
@@ -27,6 +27,14 @@ body {
width: 100vw;
height: 100vh;
overflow: hidden;
+
+ &.cursor-drag-scrub {
+ cursor: ew-resize !important;
+
+ * {
+ cursor: ew-resize !important;
+ }
+ }
}
#app {
@@ -113,16 +121,15 @@ hr {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
- -webkit-appearance: none;
+ appearance: none;
margin: 0;
}
input[type="number"] {
- -moz-appearance: textfield;
+ appearance: textfield;
}
[contenteditable] {
- -webkit-user-select: text;
user-select: text;
}
@@ -132,15 +139,12 @@ select {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs14;
margin-bottom: $size-4;
- -webkit-appearance: none;
- -moz-appearance: none;
+ appearance: none;
}
[draggable] {
- -moz-user-select: none;
- -khtml-user-select: none;
- -webkit-user-select: none;
user-select: none;
+
/* Required to make elements draggable in old WebKit */
-khtml-user-drag: element;
-webkit-user-drag: element;
diff --git a/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss b/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss
index ddfa3a09e7..4654c54c94 100644
--- a/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss
+++ b/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss
@@ -82,7 +82,7 @@
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #316dca;
- font-weight: bold;
+ font-weight: 700;
}
.hljs-bullet {
@@ -99,7 +99,7 @@
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #adbac7;
- font-weight: bold;
+ font-weight: 700;
}
.hljs-addition {
diff --git a/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss b/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss
index ea2d601f76..78397f2cf4 100644
--- a/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss
+++ b/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss
@@ -11,7 +11,7 @@
.hljs {
color: #24292e;
- background: #ffffff;
+ background: #fff;
}
.hljs-doctag,
@@ -83,7 +83,7 @@
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #005cc5;
- font-weight: bold;
+ font-weight: 700;
}
.hljs-bullet {
@@ -100,7 +100,7 @@
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #24292e;
- font-weight: bold;
+ font-weight: 700;
}
.hljs-addition {
diff --git a/frontend/resources/styles/common/dependencies/animations.scss b/frontend/resources/styles/common/dependencies/animations.scss
index ea30c21e10..ac8b99d7d9 100644
--- a/frontend/resources/styles/common/dependencies/animations.scss
+++ b/frontend/resources/styles/common/dependencies/animations.scss
@@ -7,13 +7,11 @@
*/
.animated {
- -webkit-animation-duration: 1s;
animation-duration: 1s;
- -webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
-@-webkit-keyframes fadeIn {
+@keyframes fade-in {
0% {
opacity: 0;
}
@@ -23,79 +21,22 @@
}
}
-@keyframes fadeIn {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
+.fade-in {
+ animation-name: fade-in;
}
-.fadeIn {
- -webkit-animation-name: fadeIn;
- animation-name: fadeIn;
-}
-
-@-webkit-keyframes fadeInDown {
+@keyframes fade-in-down {
0% {
opacity: 0;
- -webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
100% {
opacity: 1;
- -webkit-transform: none;
transform: none;
}
}
-@keyframes fadeInDown {
- 0% {
- opacity: 0;
- -webkit-transform: translate3d(0, -100%, 0);
- transform: translate3d(0, -100%, 0);
- }
-
- 100% {
- opacity: 1;
- -webkit-transform: none;
- transform: none;
- }
-}
-
-.fadeInDown {
- -webkit-animation-name: fadeInDown;
- animation-name: fadeInDown;
-}
-
-@keyframes loaderColor {
- 0% {
- fill: #513b56;
- }
-
- 33% {
- fill: #348aa7;
- }
-
- 66% {
- fill: #5dd39e;
- }
-
- 100% {
- fill: #513b56;
- }
-}
-
-//pencil loader animation
-@keyframes linePencil {
- 0% {
- transform: translateY(0);
- }
-
- 100% {
- transform: translateY(-150px);
- }
+.fade-in-down {
+ animation-name: fade-in-down;
}
diff --git a/frontend/resources/styles/common/dependencies/fonts.scss b/frontend/resources/styles/common/dependencies/fonts.scss
index 146f7099fd..ed4e821f7e 100644
--- a/frontend/resources/styles/common/dependencies/fonts.scss
+++ b/frontend/resources/styles/common/dependencies/fonts.scss
@@ -10,7 +10,7 @@
$style-name,
$file,
$unicode-range,
- $weight: unquote("normal"),
+ $weight: string.unquote("normal"),
$style: string.unquote("normal")
) {
$filepath: "../fonts/" + $file;
@@ -22,6 +22,7 @@
url($filepath + ".ttf") format("truetype");
font-weight: string.unquote($weight);
font-style: string.unquote($style);
+
@if $unicode-range {
unicode-range: $unicode-range;
}
diff --git a/frontend/resources/styles/common/dependencies/highlight.scss b/frontend/resources/styles/common/dependencies/highlight.scss
index 9d53084cb7..a7bebe984c 100644
--- a/frontend/resources/styles/common/dependencies/highlight.scss
+++ b/frontend/resources/styles/common/dependencies/highlight.scss
@@ -7,9 +7,9 @@
@use "sass:meta";
:root {
- @include meta.load-css("./_hljs-dark-theme.scss");
+ @include meta.load-css("./_hljs-dark-theme");
}
.light {
- @include meta.load-css("./_hljs-light-theme.scss");
+ @include meta.load-css("./_hljs-light-theme");
}
diff --git a/frontend/resources/styles/common/dependencies/reset.scss b/frontend/resources/styles/common/dependencies/reset.scss
index 39e198d8d7..d86c883697 100644
--- a/frontend/resources/styles/common/dependencies/reset.scss
+++ b/frontend/resources/styles/common/dependencies/reset.scss
@@ -11,12 +11,13 @@ License: none (public domain)
div {
vertical-align: top;
}
+
img {
display: block;
}
// #Reset & Basics (Inspired by E. Meyers)
-//==================================================
+// ==================================================
a,
abbr,
acronym,
@@ -100,7 +101,9 @@ var,
video {
border: 0;
font: inherit;
+ /* stylelint-disable-next-line declaration-property-unit-allowed-list */
font-size: 100%;
+
// TODO: Changing line-height to 1 (as it should be) makes the visual tests
// fail with a max pixel diff ratio of 0.005.
// We should tackle this later.
@@ -124,6 +127,7 @@ nav,
section {
display: block;
}
+
body {
line-height: 1;
}
@@ -138,10 +142,10 @@ q {
quotes: none;
}
-blockquote:before,
-blockquote:after,
-q:before,
-q:after {
+blockquote::before,
+blockquote::after,
+q::before,
+q::after {
content: "";
}
@@ -151,5 +155,5 @@ table {
}
select {
- -webkit-appearance: none;
+ appearance: none;
}
diff --git a/frontend/resources/styles/common/refactor/animations.scss b/frontend/resources/styles/common/refactor/animations.scss
index 44cdf2ee65..a35832ba1a 100644
--- a/frontend/resources/styles/common/refactor/animations.scss
+++ b/frontend/resources/styles/common/refactor/animations.scss
@@ -5,16 +5,6 @@
// Copyright (c) KALEIDOS INC
@mixin animation($delay, $duration, $animation) {
- -webkit-animation-delay: $delay;
- -webkit-animation-duration: $duration;
- -webkit-animation-name: $animation;
- -webkit-animation-fill-mode: both;
-
- -moz-animation-delay: $delay;
- -moz-animation-duration: $duration;
- -moz-animation-name: $animation;
- -moz-animation-fill-mode: both;
-
animation-delay: $delay;
animation-duration: $duration;
animation-name: $animation;
diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss
index 91068275cc..efac327f36 100644
--- a/frontend/resources/styles/common/refactor/basic-rules.scss
+++ b/frontend/resources/styles/common/refactor/basic-rules.scss
@@ -12,11 +12,12 @@
@use "./z-index.scss" as *;
// SCROLLBAR
-.new-scrollbar {
+%new-scrollbar {
scrollbar-width: thin;
- scrollbar-color: rgba(170, 181, 186, 0.3) transparent;
+ scrollbar-color: rgb(170 181 186 / 0.3) transparent;
+
&:hover {
- scrollbar-color: rgba(170, 181, 186, 0.7) transparent;
+ scrollbar-color: rgb(170 181 186 / 0.7) transparent;
}
// These rules do not apply in chrome - 121 or higher
@@ -27,18 +28,20 @@
height: $s-12;
width: $s-12;
}
+
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
- background-color: rgba(170, 181, 186, 0.3);
+ background-color: rgb(170 181 186 / 0.3);
background-clip: content-box;
border: $s-2 solid transparent;
border-radius: $br-8;
+
&:hover {
- background-color: rgba(170, 181, 186, 0.7);
+ background-color: rgb(170 181 186 / 0.7);
outline: none;
}
}
@@ -48,48 +51,53 @@
color: var(--text-editor-selection-foreground-color);
}
- ::placeholder,
- ::-webkit-input-placeholder {
- @include bodySmallTypography;
+ ::placeholder {
+ @include body-small-typography;
+
color: var(--input-placeholder-color);
}
}
// BUTTONS
-.button-primary {
- @include buttonStyle;
- @include flexCenter;
- @include headlineSmallTypography;
+%button-primary {
+ @include button-style;
+ @include flex-center;
+ @include headline-small-typography;
+
background-color: var(--button-primary-background-color-rest);
border: $s-1 solid var(--button-primary-border-color-rest);
color: var(--button-primary-foreground-color-rest);
border-radius: $br-8;
min-height: $s-32;
- svg,
- span svg {
+
+ svg {
stroke: var(--button-primary-foreground-color-rest);
}
- @include focusPrimary;
+
+ @include focus-primary;
+
&:hover {
background-color: var(--button-primary-background-color-hover);
border: $s-1 solid var(--button-primary-border-color-hover);
color: var(--button-primary-foreground-color-hover);
text-decoration: none;
- svg,
- span svg {
+
+ svg {
stroke: var(--button-primary-foreground-color-hover);
}
}
+
&:active {
background-color: var(--button-primary-background-color-active);
border: $s-1 solid var(--button-primary-border-color-active);
color: var(--button-primary-foreground-color-active);
outline: none;
- svg,
- span svg {
+
+ svg {
stroke: var(--button-primary-foreground-color-active);
}
}
+
&:global(.disabled),
&[disabled],
&:disabled {
@@ -100,38 +108,43 @@
}
}
-.button-secondary {
- @include buttonStyle;
- @include flexCenter;
+%button-secondary {
+ @include button-style;
+ @include flex-center;
+
border-radius: $br-8;
background-color: var(--button-secondary-background-color-rest);
border: $s-1 solid var(--button-secondary-border-color-rest);
color: var(--button-secondary-foreground-color-rest);
- svg,
- span svg {
+
+ svg {
stroke: var(--button-secondary-foreground-color-rest);
}
- @include focusSecondary;
+
+ @include focus-secondary;
+
&:hover {
background-color: var(--button-secondary-background-color-hover);
border: $s-1 solid var(--button-secondary-border-color-hover);
color: var(--button-secondary-foreground-color-hover);
text-decoration: none;
- svg,
- span svg {
+
+ svg {
stroke: var(--button-secondary-foreground-color-hover);
}
}
+
&:active {
outline: none;
background-color: var(--button-secondary-background-color-active);
border: $s-1 solid var(--button-secondary-border-color-active);
color: var(--button-secondary-foreground-color-active);
- svg,
- span svg {
+
+ svg {
stroke: var(--button-secondary-foreground-color-active);
}
}
+
&:global(.disabled),
&[disabled],
&:disabled {
@@ -142,37 +155,42 @@
}
}
-.button-tertiary {
- @include buttonStyle;
- @include flexCenter;
+%button-tertiary {
+ @include button-style;
+ @include flex-center;
+
--button-tertiary-border-width: #{$s-2};
+
border-radius: $br-8;
color: var(--button-tertiary-foreground-color-rest);
background-color: transparent;
border: var(--button-tertiary-border-width) solid transparent;
display: grid;
place-content: center;
- svg,
- span svg {
+
+ svg {
stroke: var(--button-tertiary-foreground-color-rest);
}
- @include focusTertiary;
+
+ @include focus-tertiary;
+
&:hover {
background-color: var(--button-tertiary-background-color-hover);
color: var(--button-tertiary-foreground-color-hover);
border-color: var(--button-secondary-border-color-hover);
- svg,
- span svg {
+
+ svg {
stroke: var(--button-tertiary-foreground-color-hover);
}
}
+
&:active {
outline: none;
border-color: transparent;
background-color: var(--button-tertiary-background-color-active);
color: var(--button-tertiary-foreground-color-active);
- svg,
- span svg {
+
+ svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
@@ -184,89 +202,98 @@
cursor: unset;
pointer-events: none;
- svg,
- span svg {
+ svg {
stroke: var(--button-foreground-color-disabled);
}
}
}
-.button-icon-selected {
+%button-icon-selected {
outline: none;
border-color: var(--button-icon-border-color-selected);
background-color: var(--button-icon-background-color-selected);
color: var(--button-icon-foreground-color-selected);
+
svg {
stroke: var(--button-icon-foreground-color-selected);
}
}
.button-radio {
- @include buttonStyle;
- @include flexCenter;
+ @include button-style;
+ @include flex-center;
+
border-radius: $br-8;
color: var(--button-radio-foreground-color-rest);
border-color: $s-1 solid var(--button-radio-background-color-rest);
- svg,
- span svg {
+
+ svg {
stroke: var(--button-radio-foreground-color-rest);
}
- @include focusRadio;
+
+ @include focus-radio;
+
&:hover {
background-color: var(--button-radio-background-color-rest);
color: var(--button-radio-foreground-color-hover);
border: $s-1 solid transparent;
- svg,
- span svg {
+
+ svg {
stroke: var(--button-radio-foreground-color-hover);
}
}
+
&:active {
outline: none;
border: $s-1 solid transparent;
background-color: var(--button-radio-background-color-active);
color: var(--button-radio-foreground-color-active);
- svg,
- span svg {
+
+ svg {
stroke: var(--button-radio-foreground-color-active);
}
}
}
.button-warning {
- @include buttonStyle;
- @include flexCenter;
+ @include button-style;
+ @include flex-center;
+
background-color: var(--button-warning-background-color-rest);
border: $s-1 solid var(--button-warning-border-color-rest);
color: var(--button-warning-foreground-color-rest);
}
-.button-disabled {
- @include buttonStyle;
- @include flexCenter;
+%button-disabled {
+ @include button-style;
+ @include flex-center;
+
background-color: var(--button-background-color-disabled);
border: $s-1 solid var(--button-border-color-disabled);
color: var(--button-foreground-color-disabled);
cursor: unset;
}
-.button-tag {
- @include buttonStyle;
- @include flexCenter;
+%button-tag {
+ @include button-style;
+ @include flex-center;
@include focus;
+
&:hover {
svg {
stroke: var(--title-foreground-color-hover);
}
}
+
&:active {
border: none;
background-color: transparent;
}
}
-.button-icon {
- @include flexCenter;
+%button-icon {
+ @include flex-center;
+
height: $s-16;
width: $s-16;
color: transparent;
@@ -274,21 +301,24 @@
stroke-width: 1px;
}
-.button-icon-small {
- @extend .button-icon;
+%button-icon-small {
+ @extend %button-icon;
+
height: $s-12;
width: $s-12;
stroke-width: 1.33px;
}
.button-constraint {
- @include buttonStyle;
+ @include button-style;
+
width: $s-32;
height: $s-4;
border-radius: $br-8;
background-color: var(--button-constraint-background-color-rest);
padding: 0;
margin: 0;
+
&:hover {
outline: $s-4 solid var(--button-constraint-border-color-hover);
background-color: var(--button-constraint-background-color-hover);
@@ -296,9 +326,10 @@
}
// INPUTS
-.input-base {
- @include removeInputStyle;
- @include textEllipsis;
+%input-base {
+ @include remove-input-style;
+ @include text-ellipsis;
+
height: $s-28;
width: 100%;
flex-grow: 1;
@@ -306,6 +337,7 @@
padding: 0 0 0 $s-6;
border-radius: $br-8;
color: var(--input-foreground-color-active);
+
&[disabled] {
opacity: 0.5;
pointer-events: none;
@@ -313,24 +345,31 @@
}
.input-icon {
- @include flexCenter;
+ @include flex-center;
+
min-width: $s-12;
height: $s-32;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
}
}
-.input-label {
- @include headlineSmallTypography;
- @include flexCenter;
+%input-label {
+ @include headline-small-typography;
+ @include flex-center;
+
width: $s-20;
padding-left: $s-8;
height: $s-32;
color: var(--input-foreground-color);
}
-.input-element {
+.input-label {
+ @extend %input-label;
+}
+
+%input-element {
display: flex;
align-items: center;
height: $s-32;
@@ -338,60 +377,83 @@
background-color: var(--input-background-color);
border: $s-1 solid var(--input-border-color);
color: var(--input-foreground-color);
+
+ &:not(:focus-within) {
+ cursor: ew-resize;
+
+ input {
+ cursor: ew-resize;
+ }
+ }
+
span,
label {
- @extend .input-label;
+ @extend %input-label;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-foreground-color);
}
}
input {
- @extend .input-base;
+ @extend %input-base;
}
::placeholder {
color: var(--input-placeholder-color);
}
- @include focusInput;
+ @include focus-input;
+
&:hover {
border: $s-1 solid var(--input-border-color-hover);
background-color: var(--input-background-color-hover);
+
span {
color: var(--input-foreground-color-hover);
}
+
input {
color: var(--input-foreground-color-hover);
}
}
+
&:active {
border: $s-1 solid var(--input-border-color-active);
background-color: var(--input-background-color-active);
+
span {
color: var(--input-foreground-color-active);
}
+
input {
color: var(--input-foreground-color-active);
}
}
+
&:focus,
&:focus-within {
border: $s-1 solid var(--input-border-color-focus);
background-color: var(--input-background-color-focus);
+
span {
color: var(--input-foreground-color-focus);
}
+
input {
color: var(--input-foreground-color-focus);
}
+
&:hover {
border: $s-1 solid var(--input-border-color-focus);
background-color: var(--input-background-color-focus);
+
span {
color: var(--input-foreground-color-focus);
}
+
input {
color: var(--input-foreground-color-focus);
}
@@ -399,13 +461,16 @@
}
}
-.input-element-label {
- @include bodySmallTypography;
+%input-element-label {
+ @include body-small-typography;
+
display: flex;
align-items: flex-start;
padding: 0;
+
input {
- @extend .input-base;
+ @extend %input-base;
+
padding-left: $s-8;
display: flex;
align-items: flex-start;
@@ -418,10 +483,13 @@
color: var(--input-foreground-color-active);
background-color: var(--input-background-color);
}
+
::placeholder {
- @include bodySmallTypography;
+ @include body-small-typography;
+
color: var(--input-placeholder-color);
}
+
&:hover {
input {
color: var(--input-foreground-color-active);
@@ -439,22 +507,25 @@
}
}
-.disabled-input {
+%disabled-input {
background-color: var(--input-background-color-disabled);
border: $s-1 solid var(--input-border-color-disabled);
color: var(--input-foreground-color-disabled);
+
input {
pointer-events: none;
cursor: default;
color: var(--input-foreground-color-disabled);
}
- span svg {
+
+ svg {
stroke: var(--input-foreground-color-disabled);
}
}
-.checkbox-icon {
- @include flexCenter;
+%checkbox-icon {
+ @include flex-center;
+
width: $s-16;
height: $s-16;
min-width: $s-16;
@@ -462,15 +533,18 @@
background-color: var(--input-checkbox-background-color-rest);
border: $s-1 solid var(--input-checkbox-border-color-rest);
border-radius: $br-4;
+
svg {
width: $s-16;
height: $s-16;
display: none;
stroke: var(--input-checkbox-inactive-foreground-color);
}
+
&:hover {
border-color: var(--input-checkbox-border-color-hover);
}
+
&:focus {
border-color: var(--input-checkbox-border-color-focus);
}
@@ -478,8 +552,10 @@
&:global(.checked) {
border-color: var(--input-checkbox-border-color-active);
background-color: var(--input-checkbox-background-color-active);
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-checkbox-foreground-color-active);
}
}
@@ -487,8 +563,10 @@
&:global(.intermediate) {
background-color: var(--input-checkbox-background-color-intermediate);
border-color: var(--input-checkbox-border-color-intermediate);
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-checkbox-foreground-color-intermediate);
}
}
@@ -496,28 +574,34 @@
&:global(.unchecked) {
background-color: var(--input-checkbox-background-color-rest);
border: $s-1 solid var(--input-checkbox-background-color-rest);
+
svg {
display: none;
}
}
}
-.input-checkbox {
+%input-checkbox {
display: flex;
align-items: center;
+
label {
- @include bodySmallTypography;
+ @include body-small-typography;
+
display: flex;
align-items: center;
gap: $s-6;
cursor: pointer;
color: var(--input-checkbox-text-foreground-color);
+
span {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
+
input {
margin: 0;
}
+
&:hover {
span {
border-color: var(--input-checkbox-border-color-hover);
@@ -533,11 +617,13 @@
}
}
-.input-with-label {
+%input-with-label {
display: flex;
flex-direction: column;
+
label {
- @include bodySmallTypography;
+ @include body-small-typography;
+
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -546,8 +632,9 @@
}
input {
- @extend .input-base;
- @include bodySmallTypography;
+ @extend %input-base;
+ @include body-small-typography;
+
border-radius: $br-8;
height: $s-32;
min-height: $s-32;
@@ -555,17 +642,20 @@
background-color: var(--input-background-color);
border: $s-1 solid var(--input-border-color);
color: var(--input-foreground-color-active);
+
&:focus-within,
&:active {
input {
color: var(--input-foreground-color-active);
}
+
background-color: var(--input-background-color-active);
border: $s-1 solid var(--input-border-color-active);
}
}
+
&:global(.disabled) {
- @extend .disabled-input;
+ @extend %disabled-input;
}
&:global(.invalid) {
@@ -575,9 +665,10 @@
}
}
-//MODALS
-.modal-background {
- @include menuShadow;
+// MODALS
+%modal-background {
+ @include menu-shadow;
+
position: absolute;
display: flex;
flex-direction: column;
@@ -588,8 +679,9 @@
background-color: var(--modal-background-color);
}
-.modal-overlay-base {
- @include flexCenter;
+%modal-overlay-base {
+ @include flex-center;
+
position: fixed;
left: 0;
top: 0;
@@ -599,7 +691,7 @@
background-color: var(--overlay-color);
}
-.modal-container-base {
+%modal-container-base {
position: relative;
padding: $s-32;
border-radius: $br-8;
@@ -611,52 +703,58 @@
max-height: $s-512;
}
-.modal-close-btn-base {
- @extend .button-tertiary;
+%modal-close-btn-base {
+ @extend %button-tertiary;
+
position: absolute;
top: $s-8;
right: $s-6;
height: $s-32;
width: $s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
.modal-hint-base {
- @include bodySmallTypography;
+ @include body-small-typography;
+
color: var(--modal-title-foreground-color);
border-top: $s-1 solid var(--modal-hint-border-color);
border-bottom: $s-1 solid var(--modal-hint-border-color);
}
-.modal-action-btns {
+%modal-action-btns {
display: flex;
justify-content: flex-end;
gap: $s-16;
}
-.modal-cancel-btn {
- @extend .button-secondary;
- @include uppercaseTitleTipography;
+%modal-cancel-btn {
+ @extend %button-secondary;
+ @include uppercase-title-typography;
+
padding: $s-8 $s-24;
border-radius: $br-8;
height: $s-32;
margin: 0;
}
-.modal-accept-btn {
- @extend .button-primary;
- @include uppercaseTitleTipography;
+%modal-accept-btn {
+ @extend %button-primary;
+ @include uppercase-title-typography;
+
padding: $s-8 $s-24;
border-radius: $br-8;
height: $s-32;
margin: 0;
}
-.modal-danger-btn {
- @extend .button-primary;
- @include uppercaseTitleTipography;
+%modal-danger-btn {
+ @extend %button-primary;
+ @include uppercase-title-typography;
+
padding: $s-8 $s-24;
border-radius: $br-8;
height: $s-32;
@@ -670,8 +768,9 @@
// FIXME: This is used multiple times accross the app. We should design this in
// the DS and create a proper component for it.
-.asset-element {
- @include bodySmallTypography;
+%asset-element {
+ @include body-small-typography;
+
display: flex;
align-items: center;
height: $s-32;
@@ -679,29 +778,33 @@
padding: $s-8 $s-12;
background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover);
+
&:hover {
background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
}
}
-.shortcut-base {
- @include flexCenter;
+%shortcut-base {
+ @include flex-center;
+
gap: $s-2;
color: var(--menu-shortcut-foreground-color);
}
-.shortcut-key-base {
- @include bodySmallTypography;
- @include flexCenter;
+%shortcut-key-base {
+ @include body-small-typography;
+ @include flex-center;
+
height: $s-20;
padding: $s-2 $s-6;
border-radius: $br-6;
background-color: var(--menu-shortcut-background-color);
}
-.mixed-bar {
- @include bodySmallTypography;
+%mixed-bar {
+ @include body-small-typography;
+
display: flex;
align-items: center;
flex-grow: 1;
@@ -712,7 +815,7 @@
color: var(--input-foreground-color-active);
}
-.link {
+%link {
background: unset;
border: none;
color: var(--link-foreground-color);
@@ -720,7 +823,7 @@
text-decoration: none;
}
-.colorpicker-handler {
+%colorpicker-handler {
position: absolute;
left: 50%;
top: 50%;
@@ -730,25 +833,31 @@
border-radius: $br-circle;
transform: translate(calc(-1 * $s-12), calc(-1 * $s-12));
z-index: $z-index-1;
+
&:hover,
&:active {
border-color: var(--colorpicker-details-color-selected);
}
}
-.attr-title {
+%attr-title {
div {
margin-left: 0;
color: var(--entry-foreground-color-hover);
}
+
button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
display: none;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&:hover {
button {
display: flex;
@@ -756,15 +865,17 @@
}
}
-.attr-row {
+%attr-row {
display: grid;
grid-template-areas: "name content";
grid-template-columns: 1fr 3fr;
gap: $s-4;
height: $s-32;
+
:global(.attr-label) {
- @include bodySmallTypography;
- @include twoLineTextEllipsis;
+ @include body-small-typography;
+ @include two-line-text-ellipsis;
+
width: $s-92;
margin: auto 0;
color: var(--entry-foreground-color);
@@ -775,17 +886,20 @@
grid-area: content;
display: flex;
color: var(--entry-foreground-color-hover);
- @include bodySmallTypography;
+
+ @include body-small-typography;
}
}
-.copy-button-children {
- @include bodySmallTypography;
+%copy-button-children {
+ @include body-small-typography;
+
color: var(--color-foreground-primary);
text-align: left;
margin: 0;
padding: 0;
height: fit-content;
+
&:hover {
div {
color: var(--entry-foreground-color-hover);
@@ -794,9 +908,10 @@
}
// SELECTS AND DROPDOWNS
-.menu-dropdown {
- @include menuShadow;
- @include flexColumn;
+%menu-dropdown {
+ @include menu-shadow;
+ @include flex-column;
+
position: absolute;
padding: $s-4;
border-radius: $br-8;
@@ -807,8 +922,9 @@
margin: 0;
}
-.menu-item-base {
- @include bodySmallTypography;
+%menu-item-base {
+ @include body-small-typography;
+
display: flex;
align-items: center;
justify-content: space-between;
@@ -817,13 +933,15 @@
padding: $s-6;
border-radius: $br-8;
cursor: pointer;
+
&:hover {
background-color: var(--menu-background-color-hover);
}
}
-.dropdown-element-base {
- @include bodySmallTypography;
+%dropdown-element-base {
+ @include body-small-typography;
+
display: flex;
align-items: center;
gap: $s-8;
@@ -834,24 +952,29 @@
color: var(--menu-foreground-color-rest);
span {
- @include flexCenter;
- @include textEllipsis;
+ @include flex-center;
+ @include text-ellipsis;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&:hover {
background-color: var(--menu-background-color-hover);
color: var(--menu-foreground-color);
- span svg {
+
+ svg {
stroke: var(--menu-foreground-color-hover);
}
}
}
-.dropdown-wrapper {
- @include menuShadow;
+%dropdown-wrapper {
+ @include menu-shadow;
+
position: absolute;
top: $s-32;
left: 0;
@@ -862,15 +985,15 @@
margin-top: $s-1;
border-radius: $br-8;
z-index: $z-index-4;
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
background-color: var(--menu-background-color);
color: var(--menu-foreground-color);
border: $s-2 solid var(--panel-border-color);
}
-.select-wrapper {
- @include bodySmallTypography;
+%select-wrapper {
+ @include body-small-typography;
+
position: relative;
display: flex;
align-items: center;
diff --git a/frontend/resources/styles/common/refactor/color-defs.scss b/frontend/resources/styles/common/refactor/color-defs.scss
index f3f1df5e20..47f933d991 100644
--- a/frontend/resources/styles/common/refactor/color-defs.scss
+++ b/frontend/resources/styles/common/refactor/color-defs.scss
@@ -11,53 +11,47 @@
// Dark background
--db-primary-60: #{color.change(#18181a, $alpha: 0.6)}; // used on overlay dark mode
- //Dark foreground
+ // Dark foreground
--df-secondary: #8f9da3; // Used on button disabled background dark mode, grid metadata and some svg
--df-secondary-40: #{color.change(#8f9da3, $alpha: 0.4)}; // Used on button disabled foreground dark mode
- //Dark accent
+ // Dark accent
--da-tertiary-10: #{color.change(#00d1b8, $alpha: 0.1)}; // selection rect dark mode
--da-tertiary-70: #{color.change(#00d1b8, $alpha: 0.7)}; // selection rect background dark mode
// LIGHT
// Light background
- --lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)}; // overlay color light mode
+ --lb-primary-60: #{color.change(#fff, $alpha: 0.6)}; // overlay color light mode
--lb-quaternary: #eef0f2; // background disabled token
- //Light foreground
+ // Light foreground
--lf-secondary-40: #{color.change(#495e74, $alpha: 0.4)}; // foreground disabled token
- //Light accent
+ // Light accent
--la-tertiary-10: #{color.change(#8c33eb, $alpha: 0.1)}; // selection rect light mode
--la-tertiary-70: #{color.change(#8c33eb, $alpha: 0.7)}; // selection rect background light mode
// STATUS COLOR
--status-color-success-200: #a7e8d9; // Used on Register confirmation text
--status-color-success-500: #2d9f8f; // Used on accept icon, and status widget
-
--status-color-warning-500: #f5a91b; // Used on status widget, some buttons and warnings icons and elements
-
--status-color-error-500: #ff3277; // Used on discard icon, some borders and svg, and on status widget
-
--status-color-info-500: #0e9be9; // used on pixel grid and status widget
// APP COLORS
- --app-white: #ffffff; // Used in several places
+ --app-white: #fff; // Used in several places
--app-black: #000; // Used on interactions, measurements and editor files
// SOCIAL LOGIN BUTTONS
--google-login-background: #4285f4;
--google-login-background-hover: #{color.adjust(#4285f4, $lightness: -15%)};
--google-login-foreground: var(--app-white);
-
--github-login-background: #4c4c4c;
--github-login-background-hover: #{color.adjust(#4c4c4c, $lightness: -15%)};
--github-login-foreground: var(--app-white);
-
--oidc-login-background: #b3b3b3;
--oidc-login-background-hover: #{color.adjust(#b3b3b3, $lightness: -15%)};
--oidc-login-foreground: var(--app-white);
-
--gitlab-login-background: #fc6d26;
--gitlab-login-background-hover: #{color.adjust(#fc6d26, $lightness: -15%)};
--gitlab-login-foreground: var(--app-white);
diff --git a/frontend/resources/styles/common/refactor/common-dashboard.scss b/frontend/resources/styles/common/refactor/common-dashboard.scss
index ed30f20a2e..75f52ad936 100644
--- a/frontend/resources/styles/common/refactor/common-dashboard.scss
+++ b/frontend/resources/styles/common/refactor/common-dashboard.scss
@@ -29,6 +29,7 @@
.btn-secondary {
flex-shrink: 0;
height: $s-32;
+
svg {
height: $s-16;
width: $s-16;
@@ -57,6 +58,7 @@
height: $s-40;
padding: $s-4 $s-24;
font-weight: $fw400;
+
&:hover {
color: var(--color-background-secondary);
text-decoration: none;
@@ -124,10 +126,12 @@
font-size: $s-16;
color: var(--color-foreground-secondary);
border-color: transparent;
+
&:hover {
color: var(--color-foreground-primary);
}
}
+
&.active {
a {
color: var(--color-foreground-primary);
@@ -138,14 +142,16 @@
}
.btn-primary {
- @extend .button-primary;
+ @extend %button-primary;
+
text-transform: uppercase;
font-size: $fs-14;
font-weight: $fw400;
}
.btn-secondary {
- @extend .button-secondary;
+ @extend %button-secondary;
+
color: var(--color-foreground-primary);
font-size: $fs-12;
text-transform: uppercase;
diff --git a/frontend/resources/styles/common/refactor/common-refactor.scss b/frontend/resources/styles/common/refactor/common-refactor.scss
index a6098ee978..173fd6c7da 100644
--- a/frontend/resources/styles/common/refactor/common-refactor.scss
+++ b/frontend/resources/styles/common/refactor/common-refactor.scss
@@ -4,17 +4,17 @@
//
// Copyright (c) KALEIDOS INC
-//#################################################
+// #################################################
// MAIN STYLES
-//#################################################
+// #################################################
-@forward "./fonts.scss";
-@forward "./spacing.scss";
-@forward "./borders.scss";
-@forward "./opacity.scss";
-@forward "./shadows.scss";
-@forward "./z-index.scss";
-@forward "./mixins.scss";
-@forward "./focus.scss";
-@forward "./animations.scss";
-@forward "./basic-rules.scss";
+@forward "./fonts";
+@forward "./spacing";
+@forward "./borders";
+@forward "./opacity";
+@forward "./shadows";
+@forward "./z-index";
+@forward "./mixins";
+@forward "./focus";
+@forward "./animations";
+@forward "./basic-rules";
diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss
index 2acb81398a..9d894e65a0 100644
--- a/frontend/resources/styles/common/refactor/design-tokens.scss
+++ b/frontend/resources/styles/common/refactor/design-tokens.scss
@@ -10,11 +10,9 @@
// BASE COLORS
--canvas-background-color: var(--color-background-primary);
--canvas-fill-color: var(--color-canvas);
-
--scrollbar-background-color: var(--color-foreground-secondary);
--panel-background-color: var(--color-background-primary);
--panel-border-color: var(--color-background-quaternary);
-
--app-background: var(--color-background-primary);
--loader-background: var(--color-background-primary);
@@ -26,7 +24,6 @@
--button-foreground-color-disabled: var(--color-foreground-disabled);
--button-background-color-disabled: var(--color-background-quaternary);
--button-border-color-disabled: var(--color-background-quaternary);
-
--button-primary-background-color-rest: var(--color-accent-primary);
--button-primary-border-color-rest: var(--color-accent-primary);
--button-primary-foreground-color-rest: var(--color-background-secondary);
@@ -39,7 +36,6 @@
--button-primary-background-color-focus: var(--color-background-tertiary);
--button-primary-border-color-focus: var(--color-accent-primary);
--button-primary-foreground-color-focus: var(--color-foreground-secondary);
-
--button-secondary-background-color-rest: var(--color-background-tertiary);
--button-secondary-border-color-rest: var(--color-background-tertiary);
--button-secondary-foreground-color-rest: var(--color-foreground-secondary);
@@ -52,7 +48,6 @@
--button-secondary-background-color-focus: var(--color-background-tertiary);
--button-secondary-border-color-focus: var(--color-accent-primary);
--button-secondary-foreground-color-focus: var(--color-foreground-secondary);
-
--button-tertiary-foreground-color-rest: var(--color-foreground-secondary);
--button-tertiary-background-color-hover: var(--color-background-quaternary);
--button-tertiary-border-color-hover: var(--color-background-quaternary);
@@ -63,16 +58,13 @@
--button-tertiary-background-color-focus: var(--color-background-tertiary);
--button-tertiary-border-color-focus: var(--color-accent-primary);
--button-tertiary-foreground-color-focus: var(--color-foreground-primary);
-
--expand-button-icon-border-width: 0;
--expand-button-icon-border-width-selected: 0;
-
--button-icon-foreground-color: var(--color-foreground-secondary);
--button-icon-foreground-color-hover: var(--color-foreground-secondary);
--button-icon-background-color-selected: var(--color-background-quaternary);
--button-icon-foreground-color-selected: var(--color-accent-primary);
--button-icon-border-color-selected: var(--color-background-quaternary);
-
--button-radio-background-color-rest: var(--color-background-tertiary);
--button-radio-border-color-rest: var(--color-background-tertiary);
--button-radio-foreground-color-rest: var(--color-foreground-secondary);
@@ -84,20 +76,16 @@
--button-radio-background-color-focus: var(--color-background-tertiary);
--button-radio-border-color-focus: var(--color-accent-primary);
--button-radio-foreground-color-focus: var(--color-foreground-secondary);
-
--button-warning-background-color-rest: var(--status-color-warning-500);
--button-warning-border-color-rest: var(--status-color-warning-500);
--button-warning-foreground-color-rest: var(--color-background-secondary);
-
--button-disabled-background-color-rest: var(--color-background-disabled);
--button-disabled-border-color-rest: var(--color-background-disabled);
--button-disabled-foreground-color-rest: var(--color-foreground-disabled);
-
--button-constraint-background-color-rest: var(--color-foreground-secondary);
--button-constraint-border-color-rest: var(--color-background-tertiary);
--button-constraint-border-color-hover: var(--color-accent-primary-muted);
--button-constraint-background-color-hover: var(--color-accent-primary);
-
--constraint-widget-background-color: var(--color-background-tertiary);
--constraint-center-area-background-color: var(--color-background-primary);
@@ -144,7 +132,6 @@
--palette-button-shadow-initial: var(--color-background-primary);
--palette-button-shadow-final: transparent;
--palette-handler-background-color: var(--color-background-quaternary);
-
--color-bullet-background-color: var(--app-white); // We don't want this color to change with palette
--color-bullet-border-color: var(--color-background-quaternary);
--color-bullet-border-color-selected: var(--color-accent-primary);
@@ -183,7 +170,6 @@
--input-border-color-error: var(--status-color-error-500);
--input-border-color-success: var(--color-accent-primary);
--input-details-color: var(--color-background-primary);
-
--input-checkbox-background-color-rest: var(--color-background-quaternary);
--input-checkbox-border-color-rest: var(--color-foreground-secondary);
--input-checkbox-border-color-active: var(--color-background-quaternary);
@@ -200,7 +186,6 @@
--input-checkbox-background-color-active: var(--color-accent-primary);
--input-checkbox-foreground-color-active: var(--color-background-primary);
--input-checkbox-text-foreground-color: var(--color-foreground-secondary);
-
--menu-background-color: var(--color-background-tertiary);
--menu-foreground-color: var(--color-foreground-primary);
--menu-icon-foreground-color: var(--color-foreground-secondary);
@@ -219,7 +204,6 @@
--menu-background-color-disabled: var(--color-background-primary);
--menu-foreground-color-disabled: var(--color-foreground-secondary);
--menu-border-color-disabled: var(--color-background-quaternary);
-
--context-menu-background-color: var(--color-background-tertiary);
--context-menu-foreground-color: var(--color-foreground-secondary);
--context-menu-background-color-selected: var(--color-background-quaternary);
@@ -243,34 +227,27 @@
--assets-component-border-selected: var(--color-accent-tertiary);
--assets-component-second-border-selected: var(--color-background-primary);
--assets-component-hightlight: var(--color-accent-secondary);
-
--radio-btns-background-color: var(--color-background-tertiary);
--radio-btn-background-color-selected: var(--color-background-quaternary);
--radio-btn-foreground-color: var(--color-foreground-secondary);
--radio-btn-foreground-color-selected: var(--color-accent-primary);
--radio-btn-border-color: var(--color-background-tertiary);
--radio-btn-border-color-selected: var(--color-background-quaternary);
-
--library-name-foreground-color: var(--color-foreground-primary);
--library-content-foreground-color: var(--color-foreground-secondary);
-
--dropdown-background-color: var(--color-background-tertiary);
--dropdown-separator-color: var(--color-background-primary);
--profile-drowpdown-background-color: var(--color-background-primary);
-
--not-found-background-color: var(--color-background-tertiary);
--not-found-foreground-color: var(--color-foreground-secondary);
-
--entry-foreground-color: var(--color-foreground-secondary);
--entry-background-color: var(--color-background-tertiary);
--entry-background-color-disabled: var(--color-background-primary);
--entry-border-color-disabled: var(--color-background-quaternary);
--entry-foreground-color-hover: var(--color-foreground-primary);
--entry-background-color-hover: var(--color-background-quaternary);
-
--empty-message-background-color: var(--color-background-tertiary);
--empty-message-foreground-color: var(--color-foreground-secondary);
-
--user-count-background-color: var(--color-accent-primary);
--user-count-foreground-color: var(--color-background-secondary);
@@ -323,32 +300,25 @@
--alert-text-foreground-color-default: var(--color-foreground-primary);
--alert-icon-foreground-color-default: var(--color-foreground-primary);
--alert-border-color-default: var(--color-background-quaternary);
-
--alert-background-color-success: var(--color-background-success);
--alert-text-foreground-color-success: var(--color-foreground-primary);
--alert-icon-foreground-color-success: var(--color-accent-success);
--alert-border-color-success: var(--color-accent-success);
-
--alert-background-color-warning: var(--color-background-warning);
--alert-text-foreground-color-warning: var(--color-foreground-primary);
--alert-icon-foreground-color-warning: var(--color-accent-warning);
--alert-border-color-warning: var(--color-accent-warning);
-
--alert-background-color-error: var(--color-background-error);
--alert-text-foreground-color-error: var(--color-foreground-primary);
--alert-icon-foreground-color-error: var(--color-accent-error);
--alert-border-color-error: var(--color-accent-error);
-
--alert-background-color-info: var(--color-background-info);
--alert-text-foreground-color-info: var(--color-foreground-primary);
--alert-icon-foreground-color-info: var(--color-accent-info);
--alert-border-color-info: var(--color-accent-info);
-
--alert-text-foreground-color-focus: var(--color-accent-primary);
--alert-border-color-focus: var(--color-accent-primary);
-
--notification-foreground-color-default: var(--color-foreground-secondary);
-
--element-foreground-warning: var(--status-color-warning-500);
--element-foreground-error: var(--status-color-error-500);
@@ -368,21 +338,16 @@
--search-bar-foreground-color: var(--color-foreground-primary);
--search-bar-icon-foreground-color: var(--color-foreground-secondary);
--search-bar-icon-foreground-color-hover: var(--color-accent-primary);
-
--pill-background-color: var(--color-background-tertiary);
--pill-foreground-color: var(--color-foreground-primary);
-
--link-foreground-color: var(--color-accent-primary);
- --register-confirmation-color: var(--status-color-success-200); //TODO: review this color
-
+ --register-confirmation-color: var(--status-color-success-200); // TODO: review this color
--resize-area-background-color: var(--color-background-primary);
--resize-area-border-color: var(--color-background-quaternary);
-
--profile-section-background-color: var(--color-background-tertiary);
--dashboard-list-background-color: var(--color-background-tertiary);
--dashboard-list-foreground-color: var(--color-foreground-primary);
--dashboard-list-text-foreground-color: var(--color-foreground-secondary);
-
--communication-tag-background-color: var(--color-foreground-primary);
--communication-tag-foreground-color: var(--color-background-tertiary);
@@ -404,7 +369,7 @@
// TODO: we should not put these functional tokens here, but rather in the components they belong to
--new-team-button-background-color: var(--color-background-primary);
- //DASHBOARD
+ // DASHBOARD
--sidebar-element-foreground-color: var(--color-foreground-secondary);
--sidebar-element-background-color-hover: var(--color-background-secondary);
--sidebar-element-foreground-color-hover: var(--color-accent-primary);
@@ -422,21 +387,16 @@
--tab-background-color-selected: var(--color-background-primary);
--tab-border-color: var(--color-background-tertiary);
--tab-border-color-selected: var(--color-background-secondary);
-
--radio-btns-background-color: var(--color-background-tertiary);
--radio-btn-background-color-selected: var(--color-background-primary);
--radio-btn-foreground-color: var(--color-foreground-secondary);
--radio-btn-foreground-color-selected: var(--color-accent-primary);
--radio-btn-border-color: var(--color-background-tertiary);
--radio-btn-border-color-selected: var(--color-background-secondary);
-
--button-icon-background-color-selected: var(--color-background-primary);
--button-icon-border-color-selected: var(--color-background-secondary);
-
--assets-item-name-foreground-color: var(--color-foreground-primary);
-
--text-editor-selection-background-color: var(--la-tertiary-70);
--expand-button-icon-border-width-selected: 2px;
-
--colorpicker-background-color: var(--color-background-primary);
}
diff --git a/frontend/resources/styles/common/refactor/focus.scss b/frontend/resources/styles/common/refactor/focus.scss
index 0ac2dde780..8e01cab247 100644
--- a/frontend/resources/styles/common/refactor/focus.scss
+++ b/frontend/resources/styles/common/refactor/focus.scss
@@ -6,44 +6,46 @@
@use "./spacing.scss" as *;
-@mixin focusType($type) {
- $realType: "";
+@mixin focus-type($type) {
+ $real-type: "";
+
@if $type {
- $realType: $type + "-";
+ $real-type: $type + "-";
}
+
&:focus-visible {
outline: none;
- background-color: var(--button-#{$realType}background-color-focus);
- border: $s-1 solid var(--button-#{$realType}border-color-focus);
- color: var(--button-#{$realType}foreground-color-focus);
- svg,
- span svg {
- stroke: var(--button-#{$realType}foreground-color-focus);
+ background-color: var(--button-#{$real-type}background-color-focus);
+ border: $s-1 solid var(--button-#{$real-type}border-color-focus);
+ color: var(--button-#{$real-type}foreground-color-focus);
+
+ svg {
+ stroke: var(--button-#{$real-type}foreground-color-focus);
}
}
}
-@mixin focusPrimary {
- @include focusType(primary);
+@mixin focus-primary {
+ @include focus-type(primary);
}
-@mixin focusSecondary {
- @include focusType(secondary);
+@mixin focus-secondary {
+ @include focus-type(secondary);
}
-@mixin focusTertiary {
- @include focusType(tertiary);
+@mixin focus-tertiary {
+ @include focus-type(tertiary);
}
-@mixin focusRadio {
- @include focusType(radio);
+@mixin focus-radio {
+ @include focus-type(radio);
}
@mixin focus {
- @include focusType(null);
+ @include focus-type(null);
}
-@mixin focusInput {
+@mixin focus-input {
&:focus-within {
color: var(--input-foreground-color-active);
background-color: var(--input-background-color-active);
diff --git a/frontend/resources/styles/common/refactor/fonts.scss b/frontend/resources/styles/common/refactor/fonts.scss
index 015555225a..86f95cc303 100644
--- a/frontend/resources/styles/common/refactor/fonts.scss
+++ b/frontend/resources/styles/common/refactor/fonts.scss
@@ -8,7 +8,6 @@
// Typography scale
$fs-base: 16;
-
$fs-10: math.div(10, $fs-base) + rem;
$fs-11: 0.688rem;
$fs-12: math.div(12, $fs-base) + rem;
diff --git a/frontend/resources/styles/common/refactor/mixins.scss b/frontend/resources/styles/common/refactor/mixins.scss
index c4d07d09e2..9ec8d1996b 100644
--- a/frontend/resources/styles/common/refactor/mixins.scss
+++ b/frontend/resources/styles/common/refactor/mixins.scss
@@ -7,37 +7,37 @@
@use "./fonts.scss" as *;
@use "./spacing.scss" as *;
-@mixin flexCenter {
+@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
-@mixin flexColumn($gap: $s-4) {
+@mixin flex-column($gap: $s-4) {
display: flex;
flex-direction: column;
gap: #{$gap};
}
-@mixin flexRow {
+@mixin flex-row {
display: flex;
align-items: center;
gap: $s-4;
}
-@mixin buttonStyle {
+@mixin button-style {
border: none;
background: none;
cursor: pointer;
}
-@mixin removeInputStyle {
+@mixin remove-input-style {
border: none;
background: none;
outline: none;
}
-@mixin uppercaseTitleTipography {
+@mixin uppercase-title-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-11;
font-weight: $fw500;
@@ -45,28 +45,28 @@
text-transform: uppercase;
}
-@mixin bigTitleTipography {
+@mixin big-title-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-24;
font-weight: $fw400;
line-height: 1.2;
}
-@mixin medTitleTipography {
+@mixin med-title-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-20;
font-weight: $fw400;
line-height: 1.2;
}
-@mixin smallTitleTipography {
+@mixin small-title-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-14;
font-weight: $fw400;
line-height: 1.2;
}
-@mixin headlineLargeTypography {
+@mixin headline-large-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-18;
line-height: 1.2;
@@ -74,7 +74,7 @@
font-weight: $fw400;
}
-@mixin headlineMediumTypography {
+@mixin headline-medium-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-16;
line-height: 1.4;
@@ -82,7 +82,7 @@
font-weight: $fw400;
}
-@mixin headlineSmallTypography {
+@mixin headline-small-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-12;
line-height: 1.2;
@@ -90,35 +90,35 @@
font-weight: $fw500;
}
-@mixin bodyLargeTypography {
+@mixin body-large-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-16;
line-height: 1.5;
font-weight: $fw400;
}
-@mixin bodyMediumTypography {
+@mixin body-medium-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-14;
line-height: 1.4;
font-weight: $fw400;
}
-@mixin bodySmallTypography {
+@mixin body-small-typography {
font-family: "worksans", "vazirmatn", sans-serif;
font-size: $fs-12;
font-weight: $fw400;
line-height: 1.4;
}
-@mixin codeTypography {
+@mixin code-typography {
font-family: "robotomono", monospace;
font-size: $fs-12;
font-weight: $fw400;
line-height: 1.2;
}
-@mixin textEllipsis {
+@mixin text-ellipsis {
display: block;
max-width: 99%;
overflow: hidden;
@@ -126,7 +126,7 @@
white-space: nowrap;
}
-@mixin twoLineTextEllipsis {
+@mixin two-line-text-ellipsis {
max-width: 99%;
overflow: hidden;
text-overflow: ellipsis;
@@ -135,8 +135,9 @@
-webkit-box-orient: vertical;
}
-@mixin inspectValue {
- @include bodySmallTypography;
+@mixin inspect-value {
+ @include body-small-typography;
+
display: inline-block;
width: fit-content;
padding: 0;
@@ -145,7 +146,7 @@
color: var(--menu-foreground-color);
}
-@mixin copyWrapperBase {
+@mixin copy-wrapper-base {
position: relative;
min-height: $s-32;
width: $s-144;
@@ -154,7 +155,7 @@
box-sizing: border-box;
}
-@mixin hiddenElement {
+@mixin hidden-element {
cursor: default;
pointer-events: none;
box-sizing: border-box;
@@ -167,6 +168,7 @@
0% {
transform: rotate(0deg);
}
+
100% {
transform: rotate(359deg);
}
diff --git a/frontend/resources/styles/common/refactor/shadows.scss b/frontend/resources/styles/common/refactor/shadows.scss
index c936ca115d..ee825fa4c5 100644
--- a/frontend/resources/styles/common/refactor/shadows.scss
+++ b/frontend/resources/styles/common/refactor/shadows.scss
@@ -6,10 +6,6 @@
@use "./spacing.scss" as *;
-@mixin menuShadow {
- box-shadow: 0px 0px $s-12 0px var(--menu-shadow-color);
-}
-
-@mixin alertShadow {
- box-shadow: 0px $s-4 $s-4 var(--menu-shadow-color);
+@mixin menu-shadow {
+ box-shadow: 0 0 $s-12 0 var(--menu-shadow-color);
}
diff --git a/frontend/resources/styles/common/refactor/themes.scss b/frontend/resources/styles/common/refactor/themes.scss
index 9a5a9a1e64..cb4ab93a0f 100644
--- a/frontend/resources/styles/common/refactor/themes.scss
+++ b/frontend/resources/styles/common/refactor/themes.scss
@@ -4,5 +4,5 @@
//
// Copyright (c) KALEIDOS INC
-@forward "./themes/default-theme.scss";
-@forward "./themes/light-theme.scss";
+@forward "./themes/default-theme";
+@forward "./themes/light-theme";
diff --git a/frontend/resources/styles/common/refactor/themes/default-theme.scss b/frontend/resources/styles/common/refactor/themes/default-theme.scss
index f7d092338a..11ac2c8e89 100644
--- a/frontend/resources/styles/common/refactor/themes/default-theme.scss
+++ b/frontend/resources/styles/common/refactor/themes/default-theme.scss
@@ -10,6 +10,5 @@
--color-background-disabled: var(--df-secondary);
--color-foreground-disabled: var(--df-secondary-40);
--color-accent-tertiary-muted: var(--da-tertiary-10); // selection rect
-
--overlay-color: var(--db-primary-60);
}
diff --git a/frontend/resources/styles/common/refactor/themes/light-theme.scss b/frontend/resources/styles/common/refactor/themes/light-theme.scss
index 8baec1aa94..69e6259a0a 100644
--- a/frontend/resources/styles/common/refactor/themes/light-theme.scss
+++ b/frontend/resources/styles/common/refactor/themes/light-theme.scss
@@ -10,6 +10,5 @@
--color-background-disabled: var(--lb-quaternary);
--color-foreground-disabled: var(--lf-secondary-40);
--color-accent-tertiary-muted: var(--la-tertiary-10);
-
--overlay-color: var(--lb-primary-60);
}
diff --git a/frontend/resources/styles/debug.scss b/frontend/resources/styles/debug.scss
index be3edc5228..227b18941f 100644
--- a/frontend/resources/styles/debug.scss
+++ b/frontend/resources/styles/debug.scss
@@ -10,9 +10,9 @@
// debugging.
body {
- color: yellow;
+ color: rgb(255 255 0);
}
.deprecated-icon {
- fill: red !important;
+ fill: rgb(255 0 0) !important;
}
diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss
index 5b6c1cb247..d9048c610c 100644
--- a/frontend/resources/styles/main-default.scss
+++ b/frontend/resources/styles/main-default.scss
@@ -4,29 +4,28 @@
//
// Copyright (c) KALEIDOS INC
-//#################################################
+// #################################################
// MAIN STYLES
-//#################################################
+// #################################################
@forward "common/dependencies/reset";
-@forward "common/refactor/color-defs.scss";
+@forward "common/refactor/color-defs";
@forward "common/dependencies/fonts";
@forward "common/dependencies/animations";
-@forward "common/dependencies/highlight.scss";
-@forward "common/dependencies/storybook.scss";
+@forward "common/dependencies/highlight";
+@forward "common/dependencies/storybook";
+@forward "common/refactor/themes";
+@forward "common/refactor/design-tokens";
-@forward "common/refactor/themes.scss";
-@forward "common/refactor/design-tokens.scss";
-
-//#################################################
+// #################################################
// Layouts
-//#################################################
+// #################################################
@forward "common/base";
-//#################################################
+// #################################################
// Commons
-//#################################################
+// #################################################
// TODO: remove this stylesheet once the new text editor is in place
// https: //tree.taiga.io/project/penpot/us/8165
diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss
index aab38a4966..ad945dc69b 100644
--- a/frontend/resources/styles/main/partials/texts.scss
+++ b/frontend/resources/styles/main/partials/texts.scss
@@ -2,7 +2,7 @@
.rich-text {
color: var(--app-black);
height: 100%;
- font-family: sourcesanspro;
+ font-family: sans-serif, "sourcesanspro";
div {
line-height: inherit;
diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs
index 058e265bd2..79487fbfc6 100644
--- a/frontend/src/app/config.cljs
+++ b/frontend/src/app/config.cljs
@@ -157,6 +157,7 @@
true))))
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
+(def oidc-name (obj/get global "penpotOIDCName"))
(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 grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs
index a2d493f1b8..e91fcf0f4e 100644
--- a/frontend/src/app/main/data/changes.cljs
+++ b/frontend/src/app/main/data/changes.cljs
@@ -23,10 +23,11 @@
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module
-(log/set-level! :info)
+(log/set-level! :warn)
(def page-change?
#{:add-page :mod-page :del-page :mov-page})
+
(def update-layout-attr?
#{:hidden})
@@ -123,7 +124,7 @@
"Create a commit event instance"
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?
- translation?]}]
+ selected-before translation?]}]
(assert (cpc/check-changes redo-changes)
"expect valid vector of changes for redo-changes")
@@ -150,6 +151,7 @@
:tags tags
:stack-undo? stack-undo?
:ignore-wasm? ignore-wasm?
+ :selected-before selected-before
:translation? translation?}]
(ptk/reify ::commit
@@ -208,16 +210,19 @@
;; Prevent commit changes by a viewer team member (it really should never happen)
(when (:can-edit permissions)
- (rx/of (-> params
- (assoc :undo-group undo-group)
- (assoc :features features)
- (assoc :tags tags)
- (assoc :stack-undo? stack-undo?)
- (assoc :save-undo? save-undo?)
- (assoc :file-id file-id)
- (assoc :file-revn (resolve-file-revn state file-id))
- (assoc :file-vern (resolve-file-vern state file-id))
- (assoc :undo-changes uchg)
- (assoc :redo-changes rchg)
- (assoc :translation? translation?)
- (commit))))))))
+ (log/trace :hint "commit-changes" :redo-changes redo-changes)
+ (let [selected (dm/get-in state [:workspace-local :selected])]
+ (rx/of (-> params
+ (assoc :undo-group undo-group)
+ (assoc :features features)
+ (assoc :tags tags)
+ (assoc :stack-undo? stack-undo?)
+ (assoc :save-undo? save-undo?)
+ (assoc :file-id file-id)
+ (assoc :file-revn (resolve-file-revn state file-id))
+ (assoc :file-vern (resolve-file-vern state file-id))
+ (assoc :undo-changes uchg)
+ (assoc :redo-changes rchg)
+ (assoc :selected-before selected)
+ (assoc :translation? translation?)
+ (commit)))))))))
diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs
index fb55df73de..3ac4f1eee6 100644
--- a/frontend/src/app/main/data/common.cljs
+++ b/frontend/src/app/main/data/common.cljs
@@ -199,8 +199,10 @@
(ptk/reify ::change-team-role
ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (ntf/info (get-change-role-msg role))))
+ (watch [_ state _]
+ (let [current-team-id (:current-team-id state)]
+ (when (= team-id current-team-id)
+ (rx/of (ntf/info (get-change-role-msg role))))))
ptk/UpdateEvent
(update [_ state]
@@ -459,6 +461,17 @@
(let [page-id (or page-id (:current-page-id state))
file-id (or file-id (:current-file-id state))
section (or section :interactions)
+ selected (get-in state [:workspace-local :selected])
+ objects (dsh/lookup-page-objects state file-id page-id)
+ frame-id (or frame-id
+ (reduce
+ (fn [_ id]
+ (let [obj (get objects id)]
+ (when (and obj
+ (= :frame (:type obj)))
+ (reduced (:id obj)))))
+ nil
+ selected))
params {:file-id file-id
:page-id page-id
:section section
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index a5ce2cd2c3..7810d15a95 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -13,6 +13,7 @@
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.time :as ct]
+ [app.common.types.organization :as co]
[app.common.types.project :refer [valid-project?]]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -23,6 +24,7 @@
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
+ [app.main.data.team :as dtm]
[app.main.data.websocket :as dws]
[app.main.repo :as rp]
[app.main.store :as st]
@@ -685,16 +687,71 @@
(modal/hide)))))
(defn handle-change-team-org
- [{:keys [team-id organization-id organization-name]}]
+ [{:keys [team notification]}]
(ptk/reify ::handle-change-team-org
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [current-team-id (:current-team-id state)
+ organization (:organization team)]
+ (when (and (contains? cf/flags :nitrate)
+ notification
+ (= (:id team) current-team-id))
+ (rx/of (ntf/show {:content (tr notification (:name organization))
+ :type :toast
+ :level :info
+ :timeout nil})))))
ptk/UpdateEvent
(update [_ state]
(if (contains? cf/flags :nitrate)
- (d/update-in-when state [:teams team-id] assoc
- :organization-id organization-id
- :organization-name organization-name)
+ (let [team-id (:id team)
+ team-name (:name team)
+ organization (:organization team)]
+ (d/update-in-when state [:teams team-id]
+ (fn [team]
+ (cond-> (co/apply-organization team organization)
+ team-name (assoc :name team-name)))))
state))))
+(defn- handle-user-org-change
+ [{:keys [organization-id organization-name notification]}]
+ (ptk/reify ::handle-user-org-change
+ ptk/WatchEvent
+ (watch [_ state _]
+ (when (and notification (contains? cf/flags :nitrate))
+ (let [team-id (:current-team-id state)
+ team (dm/get-in state [:teams team-id])]
+ (rx/of (ntf/show {:content (tr notification organization-name)
+ :type :toast
+ :level :info
+ :timeout nil})
+ (dtm/fetch-teams)
+ ;; When the user is currently on a team of the org
+ (when (= organization-id (:organization-id team))
+ (dcm/go-to-dashboard-recent {:team-id :default}))))))))
+
+
+(defn- handle-organization-deleted
+ [{:keys [organization-name teams deleted-teams]}]
+ (ptk/reify ::handle-organization-deleted
+ ptk/WatchEvent
+ (watch [_ state _]
+ (when (contains? cf/flags :nitrate)
+ (let [team-id (:current-team-id state)
+ teams-set (set teams)
+ notify? (contains? teams-set team-id)
+ fetch? (some (:teams state) teams)
+ go-to-default? (some #{team-id} deleted-teams)]
+ (rx/concat
+ (when go-to-default? ;; If the user is currently on one of the deleted teams
+ (rx/of (dcm/go-to-dashboard-recent {:team-id :default})))
+
+ (when notify? ;; If the user is currently on one of the org teams
+ (rx/of (ntf/show {:content (tr "dashboard.org-deleted" organization-name)
+ :type :toast
+ :level :info
+ :timeout nil})))
+ (when fetch? ;; If the user belonged to the org
+ (rx/of (dtm/fetch-teams)))))))))
(defn- process-message
[{:keys [type] :as msg}]
@@ -703,6 +760,8 @@
:team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg)
:team-org-change (handle-change-team-org msg)
+ :user-org-change (handle-user-org-change msg)
+ :organization-deleted (handle-organization-deleted msg)
nil))
diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs
index 8ab85b5228..143dec67d4 100644
--- a/frontend/src/app/main/data/exports/assets.cljs
+++ b/frontend/src/app/main/data/exports/assets.cljs
@@ -65,6 +65,9 @@
(dsh/lookup-shapes state selected)
(reverse (dsh/filter-shapes state #(pos? (count (:exports %))))))
+ page (dsh/lookup-page state)
+ page-name (:name page)
+
exports (for [shape shapes
export (:exports shape)]
(-> export
@@ -76,10 +79,12 @@
(assoc :name (:name shape))))]
(rx/of (modal/show :export-shapes
- {:exports (vec exports) :origin origin}))))))
+ {:exports (vec exports)
+ :origin origin
+ :name page-name}))))))
(defn show-viewer-export-dialog
- [{:keys [shapes page-id file-id share-id exports]}]
+ [{:keys [shapes page-id file-id share-id exports name]}]
(ptk/reify ::show-viewer-export-dialog
ptk/WatchEvent
(watch [_ _ _]
@@ -93,27 +98,32 @@
(assoc :shape (dissoc shape :exports))
(assoc :name (:name shape))
(cond-> share-id (assoc :share-id share-id))))]
- (rx/of (modal/show :export-shapes {:exports (vec exports) :origin "viewer"})))))) #_TODO
+ (rx/of (modal/show :export-shapes {:exports (vec exports)
+ :origin "viewer"
+ :name name})))))) #_TODO
(defn show-workspace-export-frames-dialog
[frames]
(ptk/reify ::show-workspace-export-frames-dialog
ptk/WatchEvent
(watch [_ state _]
- (let [file-id (:current-file-id state)
- page-id (:current-page-id state)
- exports (mapv (fn [frame]
- {:enabled true
- :page-id page-id
- :file-id file-id
- :object-id (:id frame)
- :shape frame
- :name (:name frame)})
- frames)]
+ (let [file-id (:current-file-id state)
+ page-id (:current-page-id state)
+ page (dsh/lookup-page state)
+ page-name (:name page)
+ exports (mapv (fn [frame]
+ {:enabled true
+ :page-id page-id
+ :file-id file-id
+ :object-id (:id frame)
+ :shape frame
+ :name (:name frame)})
+ frames)]
(rx/of (modal/show :export-frames
{:exports exports
- :origin "workspace:menu"}))))))
+ :origin "workspace:menu"
+ :name page-name}))))))
(defn- initialize-export-status
[exports cmd resource]
@@ -197,7 +207,7 @@
(rx/throw cause)))))))))))
(defn request-multiple-export
- [{:keys [exports cmd]
+ [{:keys [exports cmd name]
:or {cmd :export-shapes}
:as params}]
(ptk/reify ::request-multiple-export
@@ -206,14 +216,17 @@
(let [resource-id (volatile! nil)
profile-id (:profile-id state)
ws-conn (:ws-conn state)
- params {:exports exports
- :cmd cmd
- :profile-id profile-id
- :force-multiple true
- :is-wasm
- (and
- (features/active-feature? state "render-wasm/v1")
- (contains? cf/flags :wasm-export))}
+ params (cond->
+ {:exports exports
+ :cmd cmd
+ :profile-id profile-id
+ :force-multiple true
+ :is-wasm
+ (and
+ (features/active-feature? state "render-wasm/v1")
+ (contains? cf/flags :wasm-export))}
+ (some? name)
+ (assoc :name name))
progress-stream
(->> (ws/get-rcv-stream ws-conn)
diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs
index d72cde8436..9a49711e85 100644
--- a/frontend/src/app/main/data/fonts.cljs
+++ b/frontend/src/app/main/data/fonts.cljs
@@ -60,9 +60,9 @@
(prepare-font-variant [item]
{:id (str (:font-style item) "-" (:font-weight item))
- :name (str (cm/font-weight->name (:font-weight item))
- (when (not= "normal" (:font-style item))
- (str " " (str/capital (:font-style item)))))
+ :name (cm/font-display-variant (:variant-name item)
+ (:font-weight item)
+ (:font-style item))
:style (:font-style item)
:weight (str (:font-weight item))
::fonts/woff1-file-id (:woff1-file-id item)
@@ -140,6 +140,7 @@
:font-family (or family "")
:font-weight (cm/parse-font-weight variant)
:font-style (cm/parse-font-style variant)
+ :variant-name variant
:height-warning? height-warning?})
;; Font could not be parsed (woff2), extract metadata from filename
(let [base-name (str/replace name #"\.[^.]+$" "")
diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs
index d9743c3543..77374a0a0f 100644
--- a/frontend/src/app/main/data/nitrate.cljs
+++ b/frontend/src/app/main/data/nitrate.cljs
@@ -3,35 +3,76 @@
[app.common.data.macros :as dm]
[app.common.uri :as u]
[app.config :as cf]
+ [app.main.data.common :as dcm]
[app.main.data.modal :as modal]
+ [app.main.data.notifications :as ntf]
+ [app.main.data.team :as dt]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
+ [app.util.i18n :refer [tr]]
+ [app.util.storage :as storage]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
+(def ^:private nitrate-entry-active-key ::nitrate-entry-active)
+(def ^:private nitrate-entry-pending-popup-key ::nitrate-entry-pending-popup)
+
+(defn activate-nitrate-entry-popup!
+ []
+ (binding [storage/*sync* true]
+ (swap! storage/storage assoc
+ nitrate-entry-active-key true
+ nitrate-entry-pending-popup-key true)))
+
+(defn nitrate-entry-active?
+ []
+ (true? (get storage/storage nitrate-entry-active-key)))
+
+(defn nitrate-entry-popup-pending?
+ []
+ (true? (get storage/storage nitrate-entry-pending-popup-key)))
+
+(defn consume-nitrate-entry-popup!
+ []
+ (binding [storage/*sync* true]
+ (swap! storage/storage dissoc
+ nitrate-entry-active-key
+ nitrate-entry-pending-popup-key)))
+
(defn show-nitrate-popup
- [popup-type]
- (ptk/reify ::show-nitrate-popup
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (rp/cmd! ::get-nitrate-connectivity {})
- (rx/map (fn [connectivity]
- (modal/show popup-type (or connectivity {}))))))))
+ ([popup-type] (show-nitrate-popup popup-type {}))
+ ([popup-type extra-props]
+ (ptk/reify ::show-nitrate-popup
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! ::get-nitrate-connectivity {})
+ (rx/map (fn [connectivity]
+ (modal/show popup-type (merge (or connectivity {}) extra-props)))))))))
(defn go-to-nitrate-cc
([]
(st/emit! (rt/nav-raw :href "/control-center/")))
([{:keys [organization-id organization-slug]}]
- (let [href (dm/str "/control-center/org/"
- (u/percent-encode organization-slug)
- "/"
- (u/percent-encode (str organization-id)))]
- (st/emit! (rt/nav-raw :href href)))))
+ (if (and organization-id organization-slug)
+ (let [href (dm/str "/control-center/org/"
+ (u/percent-encode organization-slug)
+ "/"
+ (u/percent-encode (str organization-id))
+ "/people/")]
+ (st/emit! (rt/nav-raw :href href)))
+ (st/emit! (rt/nav-raw :href "/control-center/")))))
+
+(defn go-to-nitrate-cc-create-org
+ []
+ (st/emit! (rt/nav-raw :href "/control-center/?action=create-org")))
+
+(def go-to-subscription-url (u/join cf/public-uri "#/settings/subscriptions"))
(defn go-to-nitrate-billing
[]
- (st/emit! (rt/nav-raw :href "/control-center/licenses/billing")))
+ (let [href (dm/str "/control-center/licenses/billing?callback=" (js/encodeURIComponent go-to-subscription-url))]
+ (st/emit! (rt/nav-raw :href href))))
(defn go-to-buy-nitrate-license
([subscription]
@@ -42,8 +83,6 @@
href (dm/str "/control-center/licenses/start?" (u/map->query-string params))]
(st/emit! (rt/nav-raw :href href)))))
-(def go-to-subscription-url (u/join cf/public-uri "#/settings/subscriptions"))
-
(defn is-valid-license?
[profile]
(and (contains? cf/flags :nitrate)
@@ -51,4 +90,47 @@
(contains? #{"active" "past_due" "trialing"}
(dm/get-in profile [:subscription :status]))))
+(defn leave-org
+ [{:keys [id name default-team-id teams-to-delete teams-to-leave on-error] :as params}]
+ (ptk/reify ::leave-org
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [profile-team-id (dm/get-in state [:profile :default-team-id])]
+ (->> (rp/cmd! ::leave-org {:id id
+ :name name
+ :default-team-id default-team-id
+ :teams-to-delete teams-to-delete
+ :teams-to-leave teams-to-leave})
+ (rx/mapcat
+ (fn [_]
+ (rx/of
+ (dt/fetch-teams)
+ (dcm/go-to-dashboard-recent :team-id profile-team-id)
+ (modal/hide)
+ (ntf/show {:content (tr "dasboard.leave-org.toast" name)
+ :type :toast
+ :level :success}))))
+ (rx/catch on-error))))))
+
+
+(defn remove-team-from-org
+ [{:keys [team-id organization-id organization-name] :as params}]
+ (ptk/reify ::remove-team-from-org
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! ::remove-team-from-org {:team-id team-id :organization-id organization-id :organization-name organization-name})
+ (rx/mapcat
+ (fn [_]
+ (rx/of (modal/hide))))))))
+
+
+(defn add-team-to-org
+ [{:keys [team-id organization-id] :as params}]
+ (ptk/reify ::add-team-to-org
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! ::add-team-to-organization {:team-id team-id :organization-id organization-id})
+ (rx/mapcat
+ (fn [_]
+ (rx/of (modal/hide))))))))
diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs
index adcc70cbb3..c90c423f96 100644
--- a/frontend/src/app/main/data/persistence.cljs
+++ b/frontend/src/app/main/data/persistence.cljs
@@ -121,8 +121,10 @@
:features features}
permissions (:permissions state)]
- ;; Prevent commit changes by a team member without edition permission
- (when (:can-edit permissions)
+ ;; Prevent saving changes when in version preview (read-only) mode
+ ;; or when the user does not have edition permission.
+ (when (and (:can-edit permissions)
+ (not (get-in state [:workspace-global :read-only?])))
(->> (rp/cmd! :update-file params)
(rx/mapcat (fn [{:keys [revn lagged] :as response}]
(log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged))
diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs
index 4233e8a826..29e96585a8 100644
--- a/frontend/src/app/main/data/profile.cljs
+++ b/frontend/src/app/main/data/profile.cljs
@@ -354,6 +354,23 @@
(rx/map (constantly (refresh-profile)))
(rx/catch on-error))))))
+(def delete-photo
+ (ptk/reify ::delete-photo
+ ev/Event
+ (-data [_] {})
+
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc-in state [:profile :photo-id] nil))
+
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (rp/cmd! :delete-profile-photo {})
+ (rx/map (constantly (refresh-profile)))
+ (rx/catch (fn [cause]
+ (js/console.error "delete-photo failed" cause)
+ (rx/of (refresh-profile))))))))
+
(defn fetch-file-comments-users
[{:keys [team-id]}]
(assert (uuid? team-id) "expected a valid uuid for `team-id`")
diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs
index 60846d88bd..f3bb5207bf 100644
--- a/frontend/src/app/main/data/team.cljs
+++ b/frontend/src/app/main/data/team.cljs
@@ -41,10 +41,19 @@
ptk/UpdateEvent
(update [_ state]
- (reduce (fn [state {:keys [id] :as team}]
- (update-in state [:teams id] merge team))
- state
- teams))))
+ (let [team-ids (map :id teams)
+ ;; Delete old teams from state
+ state (update state :teams #(select-keys % team-ids))]
+ (reduce (fn [state {:keys [id organization-id] :as team}]
+ (let [team-updated (cond-> (merge (dm/get-in state [:teams id]) team)
+ (not organization-id) (dissoc :organization-id
+ :organization-name
+ :organization-slug
+ :organization-owner-id
+ :organization-avatar-bg-url))]
+ (update state :teams assoc id team-updated)))
+ state
+ teams)))))
(defn fetch-teams
[]
@@ -255,7 +264,7 @@
(-deref [_] team)))
(defn create-team
- [{:keys [name] :as params}]
+ [{:keys [name organization-id] :as params}]
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
@@ -264,7 +273,8 @@
:or {on-success identity
on-error rx/throw}} (meta params)
features features/global-enabled-features
- params {:name name :features features}]
+ params (cond-> {:name name :features features}
+ organization-id (assoc :organization-id organization-id))]
(->> (rp/cmd! :create-team (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
@@ -581,3 +591,12 @@
(rx/map shared-files-fetched)))))))
+(defn team->organization [team]
+ {:id (:organization-id team)
+ :slug (:organization-slug team)
+ :owner-id (:organization-owner-id team)
+ :avatar-bg-url (:organization-avatar-bg-url team)
+ :custom-photo (:organization-custom-photo team)
+ :name (:organization-name team)
+ :default-team-id (:id team)})
+
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index faccfb5750..4bc744174a 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -483,12 +483,13 @@
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat
- (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
+ (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
- :tags tags}]
+ :tags tags
+ :selected-before selected-before}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))))
@@ -515,7 +516,8 @@
:workspace-persistence
:workspace-presence
:workspace-tokens
- :workspace-undo)
+ :workspace-undo
+ :workspace-versions)
(update :workspace-global dissoc :read-only?)
(assoc-in [:workspace-global :options-mode] :design)
(update :files d/update-vals #(dissoc % :data))))
@@ -1206,6 +1208,16 @@
(-> params (assoc :kind :grid-cells
:grid grid
:cells cells))))))))
+(defn show-guide-context-menu
+ [{:keys [position guide] :as params}]
+ (dm/assert! (gpt/point? position))
+ (ptk/reify ::show-guide-context-menu
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of (show-context-menu
+ (-> params (assoc :kind :guide
+ :guide guide)))))))
+
(def hide-context-menu
(ptk/reify ::hide-context-menu
ptk/UpdateEvent
@@ -1249,6 +1261,24 @@
(pcb/mod-page {:background (:color color)}))]
(rx/of (dch/commit-changes changes)))))))
+(defn change-pixel-grid-color
+ "Update the pixel grid color (and optional alpha) for the given page.
+ Mirrors `change-canvas-color` — stored on the page so the choice
+ travels with the file and persists across sessions."
+ ([color]
+ (change-pixel-grid-color nil color))
+ ([page-id color]
+ (ptk/reify ::change-pixel-grid-color
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [page-id (or page-id (:current-page-id state))
+ page (dsh/lookup-page state page-id)
+ changes (-> (pcb/empty-changes it)
+ (pcb/with-page page)
+ (pcb/mod-page {:pixel-grid-color (:color color)
+ :pixel-grid-opacity (:opacity color)}))]
+ (rx/of (dch/commit-changes changes)))))))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1431,6 +1461,19 @@
(update [_ state]
(assoc-in state [:workspace-global :clipboard-style] style))))
+(defn open-layers-search
+ [mode]
+ (ptk/reify ::open-layers-search
+ ptk/UpdateEvent
+ (update [_ state]
+ (assoc-in state [:workspace-local :layers-panel-search] mode))))
+
+(def clear-layers-search
+ (ptk/reify ::clear-layers-search
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :workspace-local dissoc :layers-panel-search))))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs
index 4c3e60f7d4..e5d6dbd19b 100644
--- a/frontend/src/app/main/data/workspace/clipboard.cljs
+++ b/frontend/src/app/main/data/workspace/clipboard.cljs
@@ -18,6 +18,7 @@
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.logic.libraries :as cll]
+ [app.common.logic.shapes :as cls]
[app.common.schema :as sm]
[app.common.transit :as t]
[app.common.types.component :as ctc]
@@ -260,7 +261,7 @@
:allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
(defn- create-paste-from-blob
- [in-viewport?]
+ [in-viewport? replace?]
(fn [blob]
(let [type (.-type blob)]
(cond
@@ -281,7 +282,9 @@
(rx/filter map?)
(rx/map
(fn [pdata]
- (assoc pdata :in-viewport in-viewport?)))
+ (-> pdata
+ (assoc :in-viewport in-viewport?)
+ (assoc :replace replace?))))
(rx/mapcat
(fn [pdata]
(case (:type pdata)
@@ -293,8 +296,6 @@
(->> (rx/from (.text blob))
(rx/map paste-text))))))
-(def default-paste-from-blob (create-paste-from-blob false))
-
(defn- clipboard-permission-error?
"Check if the given error is a clipboard permission error
(NotAllowedError DOMException)."
@@ -313,14 +314,15 @@
(defn paste-from-clipboard
"Perform a `paste` operation using the Clipboard API."
- []
- (ptk/reify ::paste-from-clipboard
- ptk/WatchEvent
- (watch [_ _ _]
- (->> (clipboard/from-navigator default-options)
- (rx/mapcat default-paste-from-blob)
- (rx/take 1)
- (rx/catch on-clipboard-permission-error)))))
+ ([] (paste-from-clipboard nil))
+ ([{:keys [replace?]}]
+ (ptk/reify ::paste-from-clipboard
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (->> (clipboard/from-navigator default-options)
+ (rx/mapcat (create-paste-from-blob false (boolean replace?)))
+ (rx/take 1)
+ (rx/catch on-clipboard-permission-error))))))
(defn paste-from-event
"Perform a `paste` operation from user emmited event."
@@ -337,7 +339,7 @@
(if is-editing?
(rx/empty)
(->> (clipboard/from-synthetic-clipboard-event event default-options)
- (rx/mapcat (create-paste-from-blob in-viewport?))))))))
+ (rx/mapcat (create-paste-from-blob in-viewport? false))))))))
(defn copy-selected-svg
[]
@@ -356,7 +358,9 @@
shapes (mapv maybe-translate selected)
svg-formatted (svg/generate-formatted-markup objects shapes)]
- (clipboard/to-clipboard svg-formatted)))))
+ (clipboard/to-clipboard-multi
+ {"image/svg+xml" svg-formatted
+ "text/plain" svg-formatted})))))
(defn copy-selected-css
[]
@@ -543,8 +547,8 @@
(defn- frame-same-size?
[paste-obj frame-obj]
(and
- (= (:heigth (:selrect (first (vals paste-obj))))
- (:heigth (:selrect frame-obj)))
+ (= (:height (:selrect (first (vals paste-obj))))
+ (:height (:selrect frame-obj)))
(= (:width (:selrect (first (vals paste-obj))))
(:width (:selrect frame-obj)))))
@@ -722,7 +726,7 @@
(update change :obj process-rchange-shape media-idx)
change))
- (calculate-paste-position [state pobjects selected position]
+ (calculate-paste-position [state pobjects selected position replace-id]
(let [page-objects (dsh/lookup-page-objects state)
selected-objs (map (d/getf pobjects) selected)
first-selected-obj (first selected-objs)
@@ -736,9 +740,20 @@
tree-root (get-tree-root-shapes pobjects)
only-one-root-shape? (and
(< 1 (count pobjects))
- (= 1 (count tree-root)))]
+ (= 1 (count tree-root)))
+ replaced (some->> replace-id (get page-objects))]
(cond
+ ;; Paste in place: center pasted content on the replaced shape and
+ ;; reparent to its container. The replaced shape is deleted below
+ ;; so the new content takes its z-index slot.
+ (some? replaced)
+ (let [delta (gpt/subtract (gsh/shape->center replaced)
+ (grc/rect->center wrapper))
+ parent-id (:parent-id replaced)
+ target-index (cfh/get-position-on-parent page-objects replace-id)]
+ [parent-id delta target-index])
+
;; Paste next to selected frame, if selected is itself or of the same size as the copied
(and (selected-frame? state)
(or (any-same-frame-from-selected? state (keys pobjects))
@@ -854,10 +869,17 @@
position (deref ms/mouse-position)
+ ;; Replace mode is only valid with a single selected shape.
+ ;; In that case we drop the pasted content at its position and
+ ;; delete it in the same transaction.
+ page-selected (dsh/lookup-selected state)
+ replace-id (when (and (:replace pdata) (= 1 (count page-selected)))
+ (first page-selected))
+
;; Calculate position for the pasted elements
[candidate-parent-id
delta
- index] (calculate-paste-position state objects selected position)
+ index] (calculate-paste-position state objects selected position replace-id)
page-objects (:objects page)
@@ -899,6 +921,10 @@
(map :id)
(pcb/resize-parents changes))
+ changes (if (some? replace-id)
+ (second (cls/generate-delete-shapes changes #{replace-id} {}))
+ changes)
+
orig-shapes (map (d/getf all-objects) selected)
children-after (-> (pcb/get-objects changes)
diff --git a/frontend/src/app/main/data/workspace/collapse.cljs b/frontend/src/app/main/data/workspace/collapse.cljs
index 1143a6f4d8..b5b4998c6b 100644
--- a/frontend/src/app/main/data/workspace/collapse.cljs
+++ b/frontend/src/app/main/data/workspace/collapse.cljs
@@ -49,3 +49,19 @@
(update [_ state]
(update state :workspace-local dissoc :expanded))))
+(defn expand-subtree
+ "Recursively expand the layer subtree rooted at `id`, marking the shape
+ and all of its descendants as expanded in the Layers sidebar.
+
+ Closes the gap with `collapse-all`: there was no symmetric way to
+ open every nested level of a single subtree, so unfolding a deep
+ shape required clicking each disclosure indicator one by one
+ (O(siblings × depth) clicks)."
+ [id objects]
+ (ptk/reify ::expand-subtree
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [ids (cfh/get-children-ids-with-self objects id)
+ expansions (into {} (map (fn [descendant-id] [descendant-id true])) ids)]
+ (update-in state [:workspace-local :expanded] merge expansions)))))
+
diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs
index 4ef75ee613..3946e3efbe 100644
--- a/frontend/src/app/main/data/workspace/guides.cljs
+++ b/frontend/src/app/main/data/workspace/guides.cljs
@@ -6,6 +6,7 @@
(ns app.main.data.workspace.guides
(:require
+ [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.geom.point :as gpt]
@@ -77,6 +78,36 @@
guides (-> (select-keys guides ids) (vals))]
(rx/from (mapv remove-guide guides))))))
+(defn remove-frame-guides
+ [frame-ids]
+
+ (assert (every? uuid? frame-ids) "expected a coll of uuids")
+
+ (ptk/reify ::remove-frame-guides
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [{:keys [guides]} (dsh/lookup-page state)
+ frame-ids-set (set frame-ids)
+ guide-ids (into #{}
+ (comp (filter #(contains? frame-ids-set (:frame-id %)))
+ d/xf:map-id)
+ (vals guides))]
+ (update-in state [:workspace-guides :hover]
+ (fn [hover] (reduce disj (or hover #{}) guide-ids)))))
+
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [{:keys [guides] :as page} (dsh/lookup-page state)
+ frame-ids-set (set frame-ids)
+ to-remove (filter #(contains? frame-ids-set (:frame-id %)) (vals guides))
+ changes (reduce
+ (fn [acc {:keys [id]}]
+ (pcb/set-guide acc id nil))
+ (-> (pcb/empty-changes it)
+ (pcb/with-page page))
+ to-remove)]
+ (rx/of (dwc/commit-changes changes))))))
+
(defmethod ptk/resolve ::move-frame-guides
[_ args]
(dm/assert!
@@ -121,6 +152,23 @@
(map build-move-event)
(rx/from))))))
+(defn update-guide-color
+ [guide-id color]
+ (ptk/reify ::update-guide-color
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [{:keys [guides] :as page} (dsh/lookup-page state)
+ guide (get guides guide-id)]
+ (when (some? guide)
+ (let [updated-guide (if (some? color)
+ (assoc guide :color color)
+ (dissoc guide :color))
+ changes
+ (-> (pcb/empty-changes it)
+ (pcb/with-page page)
+ (pcb/set-guide guide-id updated-guide))]
+ (rx/of (dwc/commit-changes changes))))))))
+
(defn set-hover-guide
[id hover?]
(ptk/reify ::set-hover-guide
diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs
index 44cd36e5ce..fad7a91802 100644
--- a/frontend/src/app/main/data/workspace/layout.cljs
+++ b/frontend/src/app/main/data/workspace/layout.cljs
@@ -25,6 +25,7 @@
:element-options
:rulers
:display-guides
+ :lock-guides
:snap-guides
:scale-text
:dynamic-alignment
diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs
index 5865cb969d..222a4a5c0e 100644
--- a/frontend/src/app/main/data/workspace/pages.cljs
+++ b/frontend/src/app/main/data/workspace/pages.cljs
@@ -328,11 +328,24 @@
(ptk/reify ::rename-page
ptk/WatchEvent
(watch [it state _]
- (let [page (dsh/lookup-page state id)
- changes (-> (pcb/empty-changes it)
- (pcb/with-page page)
- (pcb/mod-page page {:name name}))]
- (rx/of (dch/commit-changes changes))))))
+ (let [page (dsh/lookup-page state id)
+ changes (-> (pcb/empty-changes it)
+ (pcb/with-page page)
+ (pcb/mod-page page {:name name}))
+ pages (-> (dsh/lookup-file-data state) :pages)
+ index (d/index-of pages id)
+ prev-id (when (and (some? index) (pos? index))
+ (nth pages (dec index) nil))
+ next-id (when (some? index)
+ (nth pages (inc index) nil))
+ fallback-page-id (or prev-id next-id)
+ separator? (= "---" (str/trim name))]
+ (rx/concat
+ (rx/of (dch/commit-changes changes))
+ (when (and separator?
+ (= id (:current-page-id state))
+ (some? fallback-page-id))
+ (rx/of (dcm/go-to-workspace :page-id fallback-page-id))))))))
(defn- delete-page-components
[changes page]
diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs
index 45c323a860..d0ce0c5c2f 100644
--- a/frontend/src/app/main/data/workspace/selection.cljs
+++ b/frontend/src/app/main/data/workspace/selection.cljs
@@ -173,13 +173,17 @@
current (get objects first-selected)
parent (get objects (:parent-id current))
sibling-ids (:shapes parent)
- current-index (d/index-of sibling-ids first-selected)
- sibling (if (= (dec (count sibling-ids)) current-index)
- (first sibling-ids)
- (nth sibling-ids (inc current-index)))]
+ ;; `index-of` is nil when the shape is not listed under the parent (stale
+ ;; selection or inconsistent tree). Do not call `nth` with `(dec nil)` — in
+ ;; ClojureScript that is -1 and throws (see penpot#7064).
+ current-index (some-> sibling-ids (d/index-of first-selected))
+ sibling (when (some? current-index)
+ (if (= (dec (count sibling-ids)) current-index)
+ (first sibling-ids)
+ (nth sibling-ids (inc current-index) nil)))]
(cond
- (= 1 count-selected)
+ (and (= 1 count-selected) (some? sibling))
(rx/of (select-shape sibling))
(> count-selected 1)
@@ -198,12 +202,13 @@
current (get objects first-selected)
parent (get objects (:parent-id current))
sibling-ids (:shapes parent)
- current-index (d/index-of sibling-ids first-selected)
- sibling (if (= 0 current-index)
- (last sibling-ids)
- (nth sibling-ids (dec current-index)))]
+ current-index (some-> sibling-ids (d/index-of first-selected))
+ sibling (when (some? current-index)
+ (if (= 0 current-index)
+ (last sibling-ids)
+ (nth sibling-ids (dec current-index) nil)))]
(cond
- (= 1 count-selected)
+ (and (= 1 count-selected) (some? sibling))
(rx/of (select-shape sibling))
(> count-selected 1)
diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs
index 163195f11f..bbff39663a 100644
--- a/frontend/src/app/main/data/workspace/shape_layout.cljs
+++ b/frontend/src/app/main/data/workspace/shape_layout.cljs
@@ -787,3 +787,135 @@
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [layout-id]})
(dwu/commit-undo-transaction undo-id))))))
+
+(defn complete-rows?
+ "Check if the selected cells cover complete row(s) — all columns must be included."
+ [grid cells]
+ (let [{:keys [first-column last-column]} (ctl/cells-coordinates cells)
+ num-columns (count (:layout-grid-columns grid))]
+ (and (= first-column 1)
+ (= last-column num-columns))))
+
+(defn complete-columns?
+ "Check if the selected cells cover complete column(s) — all rows must be included."
+ [grid cells]
+ (let [{:keys [first-row last-row]} (ctl/cells-coordinates cells)
+ num-rows (count (:layout-grid-rows grid))]
+ (and (= first-row 1)
+ (= last-row num-rows))))
+
+(defn copy-grid-tracks
+ "Store the selected track indices for later paste. Works for both
+ complete rows and complete columns."
+ [grid-id type]
+ (assert (#{:row :column} type))
+ (ptk/reify ::copy-grid-tracks
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [objects (dsh/lookup-page-objects state)
+ grid (get objects grid-id)
+ selected (get-in state [:workspace-grid-edition grid-id :selected])
+ cells (->> selected (map #(get-in grid [:layout-grid-cells %])))
+ {:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells)
+ ;; Convert 1-indexed cell positions to 0-indexed track indices
+ track-indices (if (= type :row)
+ (vec (range (dec first-row) last-row))
+ (vec (range (dec first-column) last-column)))]
+ (assoc-in state [:workspace-grid-edition grid-id :copied-tracks]
+ {:track-indices track-indices
+ :type type
+ :grid-id grid-id})))))
+
+(defn paste-grid-tracks
+ "Paste previously copied tracks at the end of the grid.
+ Each source track is duplicated and appended after the last
+ existing track. All operations are grouped in a single undo
+ transaction. Follows the same pattern as `duplicate-layout-track`."
+ [grid-id]
+ (ptk/reify ::paste-grid-tracks
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [file-id (:current-file-id state)
+ page (dsh/lookup-page state)
+ objects (:objects page)
+ libraries (dsh/lookup-libraries state)
+ library-data (dsh/lookup-file state file-id)
+ grid (get objects grid-id)
+
+ copied (get-in state [:workspace-grid-edition grid-id :copied-tracks])
+ track-indices (:track-indices copied)
+ type (:type copied)
+ undo-id (js/Symbol)]
+
+ (when (and (seq track-indices) (some? type))
+ (let [shapes-by-track-fn
+ (if (= type :row)
+ ctl/shapes-by-row
+ ctl/shapes-by-column)
+
+ ;; Collect shapes from all source tracks
+ all-shapes
+ (->> track-indices
+ (mapcat #(shapes-by-track-fn grid % false))
+ (set))
+
+ ;; Generate duplication changes for all shapes at once
+ changes
+ (-> (pcb/empty-changes it)
+ (cll/generate-duplicate-changes objects page all-shapes (gpt/point 0 0) libraries library-data file-id)
+ (cll/generate-duplicate-changes-update-indices objects all-shapes))
+
+ ;; Build ids-map: old-shape-id -> new-shape-id
+ ids-map
+ (->> changes
+ :redo-changes
+ (filter #(= (:type %) :add-obj))
+ (filter #(all-shapes (:old-id %)))
+ (map #(vector (:old-id %) (get-in % [:obj :id])))
+ (into {}))
+
+ duplicate-at-fn
+ (if (= type :row)
+ ctl/duplicate-row-at
+ ctl/duplicate-column-at)
+
+ tracks-prop
+ (if (= type :row)
+ :layout-grid-rows
+ :layout-grid-columns)
+
+ ;; Sort source indices ascending — we'll append each
+ ;; copy at the end in order, preserving the original
+ ;; track ordering in the appended block.
+ sorted-indices (vec (sort track-indices))
+
+ changes
+ (-> changes
+ (pcb/update-shapes
+ [grid-id]
+ (fn [shape objects]
+ ;; Restore grid structure (duplication may have altered it)
+ (let [shape (merge shape (select-keys grid [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
+ ;; Append each source track at the end.
+ ;; Process in ascending order so the copies
+ ;; appear in the same order as the originals.
+ ;; Each insertion adds one track, so both the
+ ;; target index and the source index (if it
+ ;; comes after the target) shift by 1.
+ (reduce
+ (fn [s [offset src-idx]]
+ (let [;; Source tracks don't shift because we
+ ;; append after them (target > source).
+ actual-src src-idx
+ ;; Append at the end (which grows by
+ ;; one with each iteration).
+ target-idx (+ (count (get grid tracks-prop)) offset)]
+ (duplicate-at-fn s objects actual-src target-idx ids-map)))
+ shape
+ (map-indexed vector sorted-indices))))
+ {:with-objects? true}))]
+
+ (rx/of (dwu/start-undo-transaction undo-id)
+ (dch/commit-changes changes)
+ (ptk/data-event :layout/update {:ids [grid-id]})
+ (dwu/commit-undo-transaction undo-id))))))))
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index 4f4d9296cc..e7ff9a99ed 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -104,6 +104,11 @@
:subsections [:edit]
:fn (constantly nil)}
+ :paste-replace {:tooltip (ds/meta (ds/shift "V"))
+ :command (ds/c-mod "shift+v")
+ :subsections [:edit]
+ :fn #(emit-when-no-readonly (dw/paste-from-clipboard {:replace? true}))}
+
:copy-props {:tooltip (ds/meta (ds/alt "c"))
:command (ds/c-mod "alt+c")
:subsections [:edit]
@@ -146,6 +151,11 @@
:subsections [:edit]
:fn #(st/emit! esc-pressed)}
+ :find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit]
+ :fn #(st/emit! (dw/open-layers-search :find))}
+ :find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit]
+ :fn #(st/emit! (dw/open-layers-search :find-and-replace))}
+
;; MODIFY LAYERS
:rename {:tooltip (ds/alt "N")
@@ -504,17 +514,17 @@
:fn #(st/emit! (dw/decrease-zoom))}
:reset-zoom {:tooltip (ds/shift "0")
- :command "shift+0"
+ :command ["shift+0" "shift+num0"]
:subsections [:zoom-workspace]
:fn #(st/emit! dw/reset-zoom)}
:fit-all {:tooltip (ds/shift "1")
- :command "shift+1"
+ :command ["shift+1" "shift+num1"]
:subsections [:zoom-workspace]
:fn #(st/emit! dw/zoom-to-fit-all)}
:zoom-selected {:tooltip (ds/shift "2")
- :command ["shift+2" "@" "\""]
+ :command ["shift+2" "shift+num2" "@" "\""]
:subsections [:zoom-workspace]
:fn #(st/emit! dw/zoom-to-selected-shape)}
@@ -616,7 +626,7 @@
(range 10)
(map (fn [n] [(keyword (str "opacity-" n))
{:tooltip (str n)
- :command (str n)
+ :command [(str n) (str "num" n)]
:subsections [:modify-layers]
:fn #(emit-when-no-readonly (dwly/pressed-opacity n))}])))))
diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs
index fe12f88804..bb604b3f2f 100644
--- a/frontend/src/app/main/data/workspace/texts.cljs
+++ b/frontend/src/app/main/data/workspace/texts.cljs
@@ -905,50 +905,61 @@
"A higher level version of dwl/add-typography, and has mainly two
responsabilities: add the typography to the library and apply it to
the currently selected text shapes (being aware of the open text
- editors."
- [file-id]
- (ptk/reify ::add-typography
- ptk/WatchEvent
- (watch [_ state _]
- (let [selected (dsh/lookup-selected state)
- objects (dsh/lookup-page-objects state)
+ editors.
+ Optionally accepts a group-path to place the new typography inside
+ a specific group."
+ ([file-id] (add-typography file-id nil))
+ ([file-id group-path]
+ (ptk/reify ::add-typography
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [selected (dsh/lookup-selected state)
+ objects (dsh/lookup-page-objects state)
- xform (comp (keep (d/getf objects))
- (filter cfh/text-shape?))
- shapes (into [] xform selected)
- shape (first shapes)
+ xform (comp (keep (d/getf objects))
+ (filter cfh/text-shape?))
+ shapes (into [] xform selected)
+ shape (first shapes)
- values (current-text-values
- {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)])
- :shape shape
- :attrs txt/text-node-attrs})
+ values (current-text-values
+ {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)])
+ :shape shape
+ :attrs txt/text-node-attrs})
- multiple? (or (> 1 (count shapes))
- (d/seek (partial = :multiple)
- (vals values)))
+ multiple? (or (> 1 (count shapes))
+ (d/seek (partial = :multiple)
+ (vals values)))
- values (-> (d/without-nils values)
- (select-keys
- (d/concat-vec txt/text-font-attrs
- txt/text-spacing-attrs
- txt/text-transform-attrs)))
+ values (-> (d/without-nils values)
+ (select-keys
+ (d/concat-vec txt/text-font-attrs
+ txt/text-spacing-attrs
+ txt/text-transform-attrs)))
+ values (cond-> values
+ (number? (:line-height values))
+ (update :line-height str)
- typ-id (uuid/next)
- typ (-> (if multiple?
- txt/default-typography
- (merge txt/default-typography values))
- (generate-typography-name)
- (assoc :id typ-id))]
+ (number? (:letter-spacing values))
+ (update :letter-spacing str))
- (rx/concat
- (rx/of (dwl/add-typography typ)
- (ptk/event ::ev/event {::ev/name "add-asset-to-library"
- :asset-type "typography"}))
+ typ-id (uuid/next)
+ typ (-> (if multiple?
+ txt/default-typography
+ (merge txt/default-typography values))
+ (generate-typography-name)
+ (assoc :id typ-id)
+ (cond-> (string? group-path)
+ (update :name #(str group-path " / " %))))]
- (when (not multiple?)
- (rx/of (update-attrs (:id shape)
- {:typography-ref-id typ-id
- :typography-ref-file file-id}))))))))
+ (rx/concat
+ (rx/of (dwl/add-typography typ)
+ (ptk/event ::ev/event {::ev/name "add-asset-to-library"
+ :asset-type "typography"}))
+
+ (when (not multiple?)
+ (rx/of (update-attrs (:id shape)
+ {:typography-ref-id typ-id
+ :typography-ref-file file-id})))))))))
;; -- Text Editor v2
@@ -1095,16 +1106,15 @@
content-has-text?
has-prev-content?)
(dissoc :prev-content))
+
(cond-> (and (not new-shape?)
prev-content-has-text?
(not content-has-text?)
(not finalize?))
(assoc :prev-content prev-content))
+
(cond-> (and update-name? (some? name))
- (assoc :name name))
- (cond-> (some? new-size)
- (gsh/transform-shape
- (ctm/change-size shape (:width new-size) (:height new-size))))))
+ (assoc :name name))))
{:save-undo? finalize-save-undo-first?
:stack-undo? effective-stack-undo?
:undo-group (when new-shape? id)})
@@ -1167,6 +1177,35 @@
(gsh/transform-shape (ctm/change-size shape width height))))))
{:undo-group (when new-shape? id)})))))))
+(defn replace-layer-names-in-shapes
+ [ids search replacement]
+ (ptk/reify ::replace-layer-names-in-shapes
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [undo-group (uuid/next)]
+ (rx/of
+ (dwsh/update-shapes
+ ids
+ (fn [shape] (update shape :name txt/replace-all-case-insensitive search replacement))
+ {:attrs #{:name} :undo-group undo-group}))))))
+
+(defn replace-text-in-shapes
+ [ids search replacement]
+ (ptk/reify ::replace-text-in-shapes
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [undo-group (uuid/next)]
+ (rx/of
+ (dwsh/update-shapes
+ ids
+ (fn [shape]
+ (if (and (= :text (:type shape)) (some? (:content shape)))
+ (let [new-content (txt/replace-text-in-content (:content shape) search replacement)
+ new-name (txt/generate-shape-name (txt/content->text new-content))]
+ (-> shape (assoc :content new-content) (assoc :name new-name)))
+ shape))
+ {:attrs #{:content :name} :undo-group undo-group}))))))
+
;; -- Text Editor v3
;; @see texts_v3.cljs
diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs
index 3ee7758284..89cccdd869 100644
--- a/frontend/src/app/main/data/workspace/tokens/application.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/application.cljs
@@ -656,6 +656,7 @@
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
+ (assert (ctob/token? token) "apply-token event requires a valid token")
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
@@ -667,9 +668,10 @@
text-editing? (and (some? edition)
(= :text (:type (get objects edition))))]
(if (and (empty? (get state :workspace-editor-state))
+ (some? token)
(not text-editing?))
(let [attributes-to-remove
- ;; Remove atomic typography tokens when applying composite and vice-verca
+ ;; Remove atomic typography tokens when applying composite and vice-versa
(cond
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
@@ -696,7 +698,7 @@
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
- resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
+ resolved-value (get-in resolved-tokens [(:name token) :resolved-value])
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)
@@ -822,9 +824,50 @@
:shape-ids shape-ids
:on-update-shape on-update-shape}))))))))
-(defn apply-token-on-selected
+(defn apply-token-from-input
+ [{:keys [token attrs shape-ids expand-with-children]}]
+ (ptk/reify ::apply-token-from-input
+ 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 _ on-update-shape]}
+ (get token-properties (:type token))
+
+ on-update-shape
+ (if (seq attrs)
+ (or (get attr->shape-update (first attrs)) on-update-shape)
+ on-update-shape)]
+
+ (rx/of
+ (cond
+ (and (= (:type token) :spacing)
+ (nil? attrs))
+ (apply-spacing-token-separated {:token token
+ :attr attrs
+ :shapes shapes})
+
+ :else
+ (apply-token {:attributes (if (empty? attrs) attributes attrs)
+ :token token
+ :shape-ids shape-ids
+ :on-update-shape on-update-shape})))))))
+
+
+(defn apply-token-on-color-selected
[color-operations token]
- (ptk/reify ::apply-token-on-selected
+ (ptk/reify ::apply-token-on-color-selected
ptk/WatchEvent
(watch [_ _ _]
(let [undo-id (js/Symbol)]
diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs
index 30ab2e30b9..7338663b2d 100644
--- a/frontend/src/app/main/data/workspace/tokens/errors.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs
@@ -12,109 +12,109 @@
(def error-codes
{:error.import/json-parse-error
{:error/code :error.import/json-parse-error
- :error/fn #(tr "workspace.tokens.error-parse")}
+ :error/fn #(tr "errors.tokens.error-parse")}
:error.import/no-token-files-found
{:error/code :error.import/no-token-files-found
- :error/fn #(tr "workspace.tokens.no-token-files-found")}
+ :error/fn #(tr "errors.tokens.no-token-files-found")}
:error.import/invalid-json-data
{:error/code :error.import/invalid-json-data
- :error/fn #(tr "workspace.tokens.invalid-json")}
+ :error/fn #(tr "errors.tokens.invalid-json")}
:error.import/invalid-token-name
{:error/code :error.import/invalid-token-name
- :error/fn #(tr "workspace.tokens.invalid-json-token-name")
- :error/detail #(tr "workspace.tokens.invalid-json-token-name-detail" %)}
+ :error/fn #(tr "errors.tokens.invalid-json-token-name")
+ :error/detail #(tr "errors.tokens.invalid-json-token-name-detail" %)}
:error.import/style-dictionary-reference-errors
{:error/code :error.import/style-dictionary-reference-errors
- :error/fn #(str (tr "workspace.tokens.import-error") "\n\n" (first %))
+ :error/fn #(str (tr "errors.tokens.import-error") "\n\n" (first %))
:error/detail #(str/join "\n\n" (rest %))}
:error.import/style-dictionary-unknown-error
{:error/code :error.import/style-dictionary-reference-errors
- :error/fn #(tr "workspace.tokens.import-error")}
+ :error/fn #(tr "errors.tokens.import-error")}
:error.token/empty-input
{:error/code :error.token/empty-input
- :error/fn #(tr "workspace.tokens.empty-input")}
+ :error/fn #(tr "errors.tokens.empty-input")}
:error.token/direct-self-reference
{:error/code :error.token/direct-self-reference
- :error/fn #(tr "workspace.tokens.self-reference")}
+ :error/fn #(tr "errors.tokens.self-reference")}
:error.token/invalid-color
{:error/code :error.token/invalid-color
- :error/fn #(str (tr "workspace.tokens.invalid-color" %))}
+ :error/fn #(str (tr "errors.tokens.invalid-color" %))}
:error.token/number-too-large
{:error/code :error.token/number-too-large
- :error/fn #(str (tr "workspace.tokens.number-too-large" %))}
+ :error/fn #(str (tr "errors.tokens.number-too-large" %))}
:error.style-dictionary/missing-reference
{:error/code :error.style-dictionary/missing-reference
- :error/fn #(str (tr "workspace.tokens.missing-references") (str/join " " %))}
+ :error/fn #(str (tr "errors.tokens.missing-references") (str/join " " %))}
:error.style-dictionary/invalid-token-value
{:error/code :error.style-dictionary/invalid-token-value
- :error/fn #(str (tr "workspace.tokens.invalid-value" %))}
+ :error/fn #(str (tr "errors.tokens.invalid-value" %))}
:error.style-dictionary/value-with-units
{:error/code :error.style-dictionary/value-with-units
- :error/fn #(str (tr "workspace.tokens.value-with-units"))}
+ :error/fn #(str (tr "errors.tokens.value-with-units"))}
:error.style-dictionary/value-with-percent
{:error/code :error.style-dictionary/value-with-percent
- :error/fn #(str (tr "workspace.tokens.value-with-percent"))}
+ :error/fn #(str (tr "errors.tokens.value-with-percent"))}
:error.style-dictionary/invalid-token-value-opacity
{:error/code :error.style-dictionary/invalid-token-value-opacity
- :error/fn #(str/join "\n" [(str (tr "workspace.tokens.invalid-value" %) ".") (tr "workspace.tokens.opacity-range")])}
+ :error/fn #(str/join "\n" [(str (tr "errors.tokens.invalid-value" %) ".") (tr "errors.tokens.opacity-range")])}
:error.style-dictionary/invalid-token-value-stroke-width
{:error/code :error.style-dictionary/invalid-token-value-stroke-width
- :error/fn #(str/join "\n" [(str (tr "workspace.tokens.invalid-value" %) ".") (tr "workspace.tokens.stroke-width-range")])}
+ :error/fn #(str/join "\n" [(str (tr "errors.tokens.invalid-value" %) ".") (tr "errors.tokens.stroke-width-range")])}
:error.style-dictionary/invalid-token-value-text-case
{:error/code :error.style-dictionary/invalid-token-value-text-case
- :error/fn #(tr "workspace.tokens.invalid-text-case-token-value" %)}
+ :error/fn #(tr "errors.tokens.invalid-text-case-token-value" %)}
:error.style-dictionary/invalid-token-value-text-decoration
{:error/code :error.style-dictionary/invalid-token-value-text-decoration
- :error/fn #(tr "workspace.tokens.invalid-text-decoration-token-value" %)}
+ :error/fn #(tr "errors.tokens.invalid-text-decoration-token-value" %)}
:error.style-dictionary/invalid-token-value-font-weight
{:error/code :error.style-dictionary/invalid-token-value-font-weight
- :error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)}
+ :error/fn #(tr "errors.tokens.invalid-font-weight-token-value" %)}
:error.style-dictionary/invalid-token-value-font-family
{:error/code :error.style-dictionary/invalid-token-value-font-family
- :error/fn #(tr "workspace.tokens.invalid-font-family-token-value" %)}
+ :error/fn #(tr "errors.tokens.invalid-font-family-token-value" %)}
:error.style-dictionary/invalid-token-value-typography
{:error/code :error.style-dictionary/invalid-token-value-typography
- :error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)}
+ :error/fn #(tr "errors.tokens.invalid-token-value-typography" %)}
:error.style-dictionary/composite-line-height-needs-font-size
{:error/code :error.style-dictionary/composite-line-height-needs-font-size
- :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size" %)}
+ :error/fn #(tr "errors.tokens.composite-line-height-needs-font-size" %)}
:error.style-dictionary/invalid-token-value-shadow-type
{:error/code :error.style-dictionary/invalid-token-value-shadow-type
- :error/fn #(tr "workspace.tokens.invalid-shadow-type-token-value" %)}
+ :error/fn #(tr "errors.tokens.invalid-shadow-type-token-value" %)}
:error.style-dictionary/invalid-token-value-shadow-blur
{:error/code :error.style-dictionary/invalid-token-value-shadow-blur
- :error/fn #(tr "workspace.tokens.shadow-blur-range")}
+ :error/fn #(tr "errors.tokens.shadow-blur-range")}
:error.style-dictionary/invalid-token-value-shadow-spread
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
- :error/fn #(tr "workspace.tokens.shadow-spread-range")}
+ :error/fn #(tr "errors.tokens.shadow-spread-range")}
:error.style-dictionary/invalid-token-value-shadow
{:error/code :error.style-dictionary/invalid-token-value-shadow
- :error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
+ :error/fn #(tr "errors.tokens.invalid-token-value-shadow" %)}
:error/unknown
{:error/code :error/unknown
diff --git a/frontend/src/app/main/data/workspace/tokens/import_export.cljs b/frontend/src/app/main/data/workspace/tokens/import_export.cljs
index 32ba61fd70..f9793d38c8 100644
--- a/frontend/src/app/main/data/workspace/tokens/import_export.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/import_export.cljs
@@ -7,6 +7,7 @@
(ns app.main.data.workspace.tokens.import-export
(:require
[app.common.json :as json]
+ [app.common.logging :as l]
[app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
@@ -15,7 +16,7 @@
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.store :as st]
- [app.util.i18n :refer [tr]]
+ [app.util.i18n :as i18n]
[beicon.v2.core :as rx]
[cuerdas.core :as str]))
@@ -44,10 +45,20 @@
(defn- show-unknown-types-warning [unknown-tokens]
(let [type->tokens (group-by-value unknown-tokens)]
- (ntf/show {:content (tr "workspace.tokens.unknown-token-type-message")
- :detail (->> (for [[token-type tokens] type->tokens]
- (tr "workspace.tokens.unknown-token-type-section" token-type (count tokens)))
- (str/join "
"))
+ (l/wrn :hint "unsupported token types found during import"
+ :tokens (str/join ", " (map (fn [[path type]] (str path " (" type ")")) unknown-tokens)))
+ (ntf/show {:content (i18n/tr "workspace.tokens.unknown-token-type-message")
+ :detail (->> (for [[token-type token-paths] type->tokens]
+ (str (i18n/tr "workspace.tokens.unknown-token-type-section"
+ token-type
+ (i18n/tr "labels.warning-count" (i18n/c (count token-paths))))
+ ""
+ (->> token-paths
+ (sort)
+ (map #(str "- " % "
"))
+ (str/join ""))
+ "
"))
+ (str/join ""))
:type :toast
:level :info})))
diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs
index 4daccd05b8..4de3430986 100644
--- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs
@@ -12,6 +12,7 @@
[app.common.geom.point :as gpt]
[app.common.logic.tokens :as clt]
[app.common.path-names :as cpn]
+ [app.common.test-helpers.ids-map :as cthi]
[app.common.types.shape :as cts]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
@@ -22,6 +23,7 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.util.i18n :refer [tr]]
+ [app.util.storage :as storage]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
@@ -62,52 +64,147 @@
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))))
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Toggle tree nodes
+;; TOKENS TREE - Type folders
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defn- remove-paths-recursively
+;; Helper functions for localStorage persistence
+(defn- get-unfolded-token-types-from-storage
+ [file-id set-id]
+ (get-in storage/user [:app.main.ui.workspace.tokens/unfolded-token-types file-id set-id] #{}))
+
+(defn- save-unfolded-token-types-in-storage
+ [file-id set-id types]
+ (swap! storage/user update :app.main.ui.workspace.tokens/unfolded-token-types
+ assoc-in [file-id set-id] (vec types)))
+
+;; Helper functions for app state persistence
+(defn- make-unfolded-token-types-state
+ [file-id set-id types]
+ {:file-id file-id
+ :set-id set-id
+ :types (set (or types #{}))})
+
+(defn- get-unfolded-token-types-from-state
+ [state]
+ (let [value (get-in state [:workspace-tokens :unfolded-token-types])]
+ (or (:types value) #{})))
+
+(defn restore-unfolded-token-types
+ "Loads unfolded token types from localStorage for the current file and set"
+ []
+ (ptk/reify ::restore-unfolded-token-types
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [file-id (:current-file-id state)
+ set-id (get-in state [:workspace-tokens :selected-token-set-id])
+ stored (get-unfolded-token-types-from-storage file-id set-id)]
+ (assoc-in state
+ [:workspace-tokens :unfolded-token-types]
+ (make-unfolded-token-types-state file-id set-id stored))))))
+
+(defn open-token-type
+ ([types type]
+ (conj (or types #{}) type))
+ ([type]
+ (ptk/reify ::open-token-type
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [file-id (:current-file-id state)
+ set-id (get-in state [:workspace-tokens :selected-token-set-id])
+ types (get-unfolded-token-types-from-state state)
+ new-types (open-token-type types type)
+ new-state (assoc-in state
+ [:workspace-tokens :unfolded-token-types]
+ (make-unfolded-token-types-state file-id set-id new-types))]
+ (save-unfolded-token-types-in-storage file-id set-id
+ new-types)
+ new-state)))))
+
+(defn close-token-type
+ ([types type]
+ (disj (or types #{}) type))
+ ([type]
+ (ptk/reify ::close-token-type
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [file-id (:current-file-id state)
+ set-id (get-in state [:workspace-tokens :selected-token-set-id])
+ types (get-unfolded-token-types-from-state state)
+ new-types (close-token-type types type)
+ new-state (assoc-in state
+ [:workspace-tokens :unfolded-token-types]
+ (make-unfolded-token-types-state file-id set-id new-types))]
+ (save-unfolded-token-types-in-storage file-id set-id
+ new-types)
+ new-state)))))
+
+(defn
+ toggle-token-type
+ [type]
+ (ptk/reify ::toggle-token-type
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [file-id (:current-file-id state)
+ set-id (get-in state [:workspace-tokens :selected-token-set-id])
+ types (get-unfolded-token-types-from-state state)
+ new-types (if (contains? types type)
+ (close-token-type types type)
+ (open-token-type types type))
+ new-state (assoc-in state
+ [:workspace-tokens :unfolded-token-types]
+ (make-unfolded-token-types-state file-id set-id new-types))]
+ (save-unfolded-token-types-in-storage file-id set-id
+ new-types)
+ new-state))))
+
+(defn clear-tokens-types
+ []
+ (ptk/reify ::clear-tokens-types
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [file-id (:current-file-id state)
+ set-id (get-in state [:workspace-tokens :selected-token-set-id])]
+ (save-unfolded-token-types-in-storage file-id set-id #{})
+ (assoc-in state
+ [:workspace-tokens :unfolded-token-types]
+ (make-unfolded-token-types-state file-id set-id #{}))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; TOKENS TREE - Toggle tree nodes
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- remove-path
[path paths]
(->> paths
- (remove #(str/starts-with? % (str path)))
+ (remove #(= % path))
vec))
(defn add-path
[path paths]
- (let [split-path (cpn/split-path path :separator ".")
- partial-paths (->> split-path
- (reduce
- (fn [acc segment]
- (let [new-acc (if (empty? acc)
- segment
- (str (last acc) "." segment))]
- (conj acc new-acc)))
- []))]
- (->> paths
- (into partial-paths)
- distinct
- vec)))
+ (vec (conj paths path)))
(defn clear-tokens-paths
[]
(ptk/reify ::clear-tokens-paths
ptk/UpdateEvent
(update [_ state]
- (assoc-in state [:workspace-tokens :unfolded-token-paths] []))))
+ (assoc-in state [:workspace-tokens :folded-token-paths] []))))
(defn toggle-token-path
[path]
(ptk/reify ::toggle-token-path
ptk/UpdateEvent
(update [_ state]
- (update-in state [:workspace-tokens :unfolded-token-paths]
+ (update-in state [:workspace-tokens :folded-token-paths]
(fn [paths]
(let [paths (or paths [])]
(if (some #(= % path) paths)
- (remove-paths-recursively path paths)
+ (remove-path path paths)
(add-path path paths))))))))
+
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -430,6 +527,37 @@
(rx/of (create-token-with-set token)))))))
+(defn bulk-create-tokens
+ [set-id token-ids type node new-node-name]
+ (assert (uuid? set-id) "expected uuid for `set-id`")
+ (assert (every? uuid? token-ids) "expected a collection of uuids for `token-ids`")
+ (assert (keyword? type) "expected keyword for `type`")
+ (assert (string? new-node-name) "expected string for `new-node-name`")
+
+ (ptk/reify ::bulk-create-tokens
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [token-set (lookup-token-set state set-id)
+ data (dsh/lookup-file-data state)
+ changes (reduce (fn [changes token-id]
+ (let [token (-> (get-tokens-lib state)
+ (ctob/get-token (ctob/get-id token-set) token-id))
+ new-name (->
+ (cpn/split-path (:name token) :separator ".")
+ (assoc (:depth node) new-node-name)
+ (cpn/join-path :separator "." :with-spaces? false))
+ token' (->> (merge token {:name new-name
+ :id (cthi/new-id! (:name new-name))})
+ (into {})
+ (ctob/make-token))]
+ (pcb/set-token changes (ctob/get-id token-set) (:id token') token')))
+ (-> (pcb/empty-changes it)
+ (pcb/with-library-data data))
+ token-ids)]
+ (rx/of
+ (dch/commit-changes changes)
+ (ptk/data-event ::ev/event {::ev/name "bulk-create-tokens" :type type}))))))
+
(defn update-token
([id params] (update-token nil id params))
([set-id id params]
@@ -456,6 +584,34 @@
(rx/of (dch/commit-changes changes)
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type})))))))
+(defn bulk-update-tokens
+ [set-id token-ids type old-path new-path]
+ (dm/assert! (uuid? set-id))
+ (dm/assert! (every? uuid? token-ids))
+ (ptk/reify ::bulk-update-tokens
+ ptk/WatchEvent
+ (watch [it state _]
+ (let [token-set (if set-id
+ (lookup-token-set state set-id)
+ (lookup-token-set state))
+ data (dsh/lookup-file-data state)
+ changes (reduce (fn [changes token-id]
+ (let [token (-> (get-tokens-lib state)
+ (ctob/get-token (ctob/get-id token-set) token-id))
+ new-name (str/replace (:name token) old-path new-path)
+ token' (->> (merge token {:name new-name})
+ (into {})
+ (ctob/make-token))]
+ (pcb/set-token changes (ctob/get-id token-set) token-id token')))
+ (-> (pcb/empty-changes it)
+ (pcb/with-library-data data))
+
+ token-ids)]
+ (toggle-token-path (str (name type) "." old-path))
+ (toggle-token-path (str (name type) "." new-path))
+ (rx/of (dch/commit-changes changes)
+ (ptk/data-event ::ev/event {::ev/name "bulk-update-tokens" :type type}))))))
+
(defn delete-token
[set-id token-id]
(dm/assert! (uuid? set-id))
@@ -566,7 +722,12 @@
(ptk/reify ::set-selected-token-set-id
ptk/UpdateEvent
(update [_ state]
- (update state :workspace-tokens assoc :selected-token-set-id id))))
+ (let [file-id (:current-file-id state)
+ stored (get-unfolded-token-types-from-storage file-id id)]
+ (-> state
+ (update :workspace-tokens assoc :selected-token-set-id id)
+ (assoc-in [:workspace-tokens :unfolded-token-types]
+ (make-unfolded-token-types-state file-id id stored)))))))
(defn start-token-set-edition
[edition-id]
diff --git a/frontend/src/app/main/data/workspace/tokens/remapping.cljs b/frontend/src/app/main/data/workspace/tokens/remapping.cljs
index fac4eeb40e..0992501f4c 100644
--- a/frontend/src/app/main/data/workspace/tokens/remapping.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/remapping.cljs
@@ -150,6 +150,18 @@
(rx/of (dch/commit-changes token-changes))))))
+(defn bulk-remap-tokens
+ "Helper function to remap a batch of tokens, used for node renaming"
+ [tokens-in-path new-tokens]
+ (ptk/reify ::bulk-remap-tokens
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/concat
+ (map (fn [old-token new-token]
+ (remap-tokens (:name old-token) (:name new-token)))
+ tokens-in-path
+ new-tokens)))))
+
(defn validate-token-remapping
"Validate that a token remapping operation is safe to perform"
[old-name new-name]
diff --git a/frontend/src/app/main/data/workspace/tokens/warnings.cljs b/frontend/src/app/main/data/workspace/tokens/warnings.cljs
index f59047e600..6f3d4161aa 100644
--- a/frontend/src/app/main/data/workspace/tokens/warnings.cljs
+++ b/frontend/src/app/main/data/workspace/tokens/warnings.cljs
@@ -12,11 +12,11 @@
(def warning-codes
{:warning.style-dictionary/invalid-referenced-token-value-opacity
{:warning/code :warning.style-dictionary/invalid-referenced-token-value-opacity
- :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "workspace.tokens.opacity-range")]))}
+ :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "errors.tokens.opacity-range")]))}
:warning.style-dictionary/invalid-referenced-token-value-stroke-width
{:warning/code :warning.style-dictionary/invalid-referenced-token-value-stroke-width
- :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "workspace.tokens.stroke-width-range")]))}
+ :warning/fn (fn [value] (str/join "\n" [(str (tr "workspace.tokens.resolved-value" value) ".") (tr "errors.tokens.stroke-width-range")]))}
:warning/unknown
{:warning/code :warning/unknown
diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs
index 2b2c6f048b..2296aed447 100644
--- a/frontend/src/app/main/data/workspace/undo.cljs
+++ b/frontend/src/app/main/data/workspace/undo.cljs
@@ -60,7 +60,9 @@
[:undo-changes [:vector cpc/schema:change]]
[:redo-changes [:vector cpc/schema:change]]
[:undo-group ::sm/uuid]
- [:tags [:set :keyword]]])
+ [:tags [:set :keyword]]
+ [:selected-before {:optional true} [:maybe [:set ::sm/uuid]]]
+ [:selected-after {:optional true} [:maybe [:set ::sm/uuid]]]])
(def check-undo-entry
(sm/check-fn schema:undo-entry))
@@ -103,24 +105,28 @@
(defn- stack-undo-entry
"Extends the current undo entry in the workspace with new changes if it
exists, or creates a new entry if it doesn't."
- [state {:keys [undo-changes redo-changes] :as entry}]
+ [state {:keys [undo-changes redo-changes selected-after] :as entry}]
(let [index (get-in state [:workspace-undo :index] -1)]
(if (>= index 0)
(update-in state [:workspace-undo :items index]
(fn [item]
(-> item
(update :undo-changes #(into undo-changes %))
- (update :redo-changes #(into % redo-changes)))))
+ (update :redo-changes #(into % redo-changes))
+ (assoc :selected-after selected-after))))
(add-undo-entry state entry))))
(defn- accumulate-undo-entry
"Extends the current undo transaction with new changes."
- [state {:keys [undo-changes redo-changes undo-group tags]}]
+ [state {:keys [undo-changes redo-changes undo-group tags selected-before selected-after]}]
(-> state
(update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %))
(update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes))
(cond-> (nil? (get-in state [:workspace-undo :transaction :undo-group]))
(assoc-in [:workspace-undo :transaction :undo-group] undo-group))
+ (cond-> (nil? (get-in state [:workspace-undo :transaction :selected-before]))
+ (assoc-in [:workspace-undo :transaction :selected-before] selected-before))
+ (assoc-in [:workspace-undo :transaction :selected-after] selected-after)
(assoc-in [:workspace-undo :transaction :tags] tags)))
(defn append-undo
@@ -137,18 +143,20 @@
(ptk/reify ::append-undo
ptk/UpdateEvent
(update [_ state]
- (cond
- (and (get-in state [:workspace-undo :transaction])
- (or (not stack?)
- (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
- (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
- (accumulate-undo-entry state entry)
+ (let [selected-after (dm/get-in state [:workspace-local :selected])
+ entry (assoc entry :selected-after selected-after)]
+ (cond
+ (and (get-in state [:workspace-undo :transaction])
+ (or (not stack?)
+ (d/not-empty? (get-in state [:workspace-undo :transaction :undo-changes]))
+ (d/not-empty? (get-in state [:workspace-undo :transaction :redo-changes]))))
+ (accumulate-undo-entry state entry)
- stack?
- (stack-undo-entry state entry)
+ stack?
+ (stack-undo-entry state entry)
- :else
- (add-undo-entry state entry)))))
+ :else
+ (add-undo-entry state entry))))))
(def empty-tx
{:undo-changes [] :redo-changes []})
@@ -234,6 +242,16 @@
(rx/map first)
(rx/map commit-undo-transaction))))))
+(defn- restore-selection
+ "Restores the selection state from an undo entry."
+ [selected-ids]
+ (ptk/reify ::restore-selection
+ ptk/UpdateEvent
+ (update [_ state]
+ (if (some? selected-ids)
+ (assoc-in state [:workspace-local :selected] selected-ids)
+ state))))
+
(defn undo-to-index
"Repeat undoing or redoing until dest-index is reached."
[dest-index]
@@ -302,12 +320,15 @@
(find-first-group-idx index))]
(if undo-group
- (rx/of (undo-to-index (dec undo-group-index)))
+ (let [first-item (get items undo-group-index)]
+ (rx/of (undo-to-index (dec undo-group-index))
+ (restore-selection (:selected-before first-item))))
(rx/of (materialize-undo changes (dec index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:save-undo? false
:origin it})
+ (restore-selection (:selected-before item))
(assure-valid-current-page)))))))))))
(def redo
@@ -337,12 +358,15 @@
redo-group-index (when undo-group
(find-last-group-idx (inc index)))]
(if undo-group
- (rx/of (undo-to-index redo-group-index))
+ (let [last-item (get items redo-group-index)]
+ (rx/of (undo-to-index redo-group-index)
+ (restore-selection (:selected-after last-item))))
(rx/of (materialize-undo changes (inc index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
- :save-undo? false})))))))))))
+ :save-undo? false})
+ (restore-selection (:selected-after item))))))))))))
(defn- assure-valid-current-page
[]
diff --git a/frontend/src/app/main/data/workspace/versions.cljs b/frontend/src/app/main/data/workspace/versions.cljs
index 85630cfccb..b942add4d8 100644
--- a/frontend/src/app/main/data/workspace/versions.cljs
+++ b/frontend/src/app/main/data/workspace/versions.cljs
@@ -8,23 +8,28 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
+ [app.common.logging :as log]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.main.data.event :as ev]
+ [app.main.data.helpers :as dsh]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.thumbnails :as th]
+ [app.main.features :as features]
[app.main.refs :as refs]
[app.main.repo :as rp]
+ [app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defonce default-state
{:status :loading
:data nil
- :editing nil})
+ :editing nil
+ :preview-id nil})
(declare fetch-versions)
@@ -122,32 +127,6 @@
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id}))))
-(defn restore-version
- [id origin]
- (assert (uuid? id) "expected valid uuid for `id`")
- (ptk/reify ::restore-version
- ptk/WatchEvent
- (watch [_ state _]
- (let [file-id (:current-file-id state)
- team-id (:current-team-id state)
- event-name (case origin
- :version "restore-pin-version"
- :snapshot "restore-autosave"
- :plugin "restore-version-plugin")]
-
- (rx/concat
- (rx/of ::dwp/force-persist
- (dw/remove-layout-flag :document-history))
-
- (->> (wait-for-persistence file-id id)
- (rx/map #(initialize-version)))
-
- (if event-name
- (rx/of (ev/event {::ev/name event-name
- :file-id file-id
- :team-id team-id}))
- (rx/empty)))))))
-
(defn delete-version
[id]
(assert (uuid? id) "expected valid uuid for `id`")
@@ -193,6 +172,145 @@
(->> (rp/cmd! :unlock-file-snapshot {:id id})
(rx/map fetch-versions)))))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; RESTORE VERSION EVENTS
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- restore-version
+ [id]
+ (assert (uuid? id) "expected valid uuid for `id`")
+ (ptk/reify ::restore-version
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)]
+ (rx/concat
+ (rx/of ::dwp/force-persist
+ (dw/remove-layout-flag :document-history))
+
+ (->> (wait-for-persistence file-id id)
+ (rx/map #(initialize-version))))))))
+
+(defn enter-restore
+ [id]
+ (assert (uuid? id) "expected valid uuid for `id`")
+ (ptk/reify ::enter-restore
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [output-s (rx/subject)]
+ (rx/merge
+ output-s
+ (rx/of (ntf/dialog
+ :content (tr "workspace.versions.restore-warning")
+ :controls :inline-actions
+ :cancel {:label (tr "workspace.updates.dismiss")
+ :callback #(do
+ (rx/push! output-s (ntf/hide :tag :restore-dialog))
+ (rx/end! output-s))}
+ :accept {:label (tr "labels.restore")
+ :callback #(do
+ (rx/push! output-s (restore-version id))
+ (rx/end! output-s))}
+ :tag :restore-dialog)))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; PREVIEW VERSION EVENTS
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- apply-snapshot
+ "Swap the file data in app state with the provided snapshot-file
+ response. Used by the version preview feature to show historical
+ file content without modifying the database"
+ [{:keys [id] :as snapshot}]
+ (ptk/reify ::apply-snapshot-data
+ ptk/UpdateEvent
+ (update [_ state]
+ (update state :files assoc id snapshot))))
+
+(defn exit-preview
+ "Exit from preview mode and reload the live file data"
+ []
+ (ptk/reify ::exit-preview
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [backup (dm/get-in state [:workspace-versions :backup])]
+ (-> state
+ (update :workspace-versions dissoc :backup)
+ (update :workspace-global dissoc :read-only? :preview-id)
+ (update :files assoc (:id backup) backup))))
+
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)
+ page-id (:current-page-id state)]
+
+ (rx/of (dwpg/initialize-page file-id page-id))))))
+
+(defn enter-preview
+ "Load a snapshot into the workspace for read-only preview without
+ modifying any database state. Sets a read-only flag so no changes
+ are persisted while previewing and enter on the preview mode"
+ [id]
+ (assert (uuid? id) "expected valid uuid for `id`")
+
+ (ptk/reify ::enter-preview
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [file (dsh/lookup-file state)]
+ (-> state
+ (update :workspace-versions assoc :backup file)
+ (update :workspace-global assoc :read-only? true :preview-id id))))
+
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [file-id (:current-file-id state)
+ page-id (:current-page-id state)
+ team-id (:current-team-id state)
+ features (features/get-enabled-features state team-id)
+ snapshot (->> (dm/get-in state [:workspace-versions :data])
+ (d/seek #(= id (:id %))))
+ label (or (:label snapshot)
+ (tr "workspace.versions.preview.unnamed"))
+ output-s (rx/subject)]
+ (rx/merge
+ output-s
+
+ (rx/of (ntf/dialog
+ :content (tr "workspace.versions.preview-banner-title" label)
+ :controls :inline-actions
+ :cancel {:label (tr "labels.exit")
+ :callback #(do
+ (rx/push! output-s (ntf/hide))
+ (rx/push! output-s (exit-preview))
+ (rx/end! output-s))}
+ :accept {:label (tr "labels.restore")
+ :callback #(do
+ (rx/push! output-s (ntf/hide))
+ (rx/push! output-s (restore-version id))
+ (rx/end! output-s))}
+ :tag :preview-dialog))
+
+ (->> (rp/cmd! :get-file-snapshot
+ {:file-id file-id
+ :id id
+ :features features})
+ (rx/mapcat
+ (fn [snapshot]
+ (rx/of
+ ;; Swap the file data in state with snapshot content.
+ ;; Passing id sets workspace-file-version-id, which
+ ;; causes the WASM viewport to reload its shape buffer.
+ (apply-snapshot snapshot)
+ ;; Re-initialize the page to rebuild its search index
+ ;; and page-local state with the new snapshot
+ ;; objects.
+ (dwpg/initialize-page file-id page-id))))
+
+ (rx/catch (fn [err]
+ ;; On error roll back the read-only flag so the
+ ;; user is not stuck in a broken preview state.
+ (log/error :hint "failed to load snapshot" :cause err :file-id file-id :snapshot-id id)
+ (rx/of (exit-preview))))))))))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PLUGINS SPECIFIC EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -241,25 +359,28 @@
(rx/empty))))))))
(defn restore-version-from-plugin
- [file-id id resolve _reject]
+ [file-id id resolve reject]
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::restore-version-from-plugins
ptk/WatchEvent
- (watch [_ state _]
- (let [team-id (:current-team-id state)]
- (rx/concat
- (rx/of (ev/event {::ev/name "restore-version-plugin"
- :file-id file-id
- :team-id team-id})
- ::dwp/force-persist)
+ (watch [_ _ _]
+ (->> (rx/concat
+ (rx/of (ev/event {::ev/name "restore-version"
+ ::ev/origin "plugins"})
+ ::dwp/force-persist)
- (->> (wait-for-persistence file-id id)
- (rx/map #(initialize-version)))
+ (->> (wait-for-persistence file-id id)
+ (rx/map #(initialize-version)))
- (->> (rx/of 1)
- (rx/tap resolve)
- (rx/ignore)))))))
+ (->> (rx/of 1)
+ (rx/tap resolve)
+ (rx/ignore)))
+
+ ;; On error reject the promise and empty the stream
+ (rx/catch (fn [error]
+ (reject error)
+ (rx/empty)))))))
diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs
index 85f1334cc9..eaaebedff2 100644
--- a/frontend/src/app/main/errors.cljs
+++ b/frontend/src/app/main/errors.cljs
@@ -481,7 +481,6 @@
(and (= (.-name ^js cause) "NotFoundError")
(str/includes? message "removeChild")))))
-
(defn- from-plugin?
"Check if the error is marked as originating from plugin code. The
plugin runtime tracks plugin errors in a WeakMap, which works even
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index c61307cf93..870e659746 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -303,6 +303,9 @@
(def workspace-page-flows
(l/derived #(-> % :flows not-empty) workspace-page))
+(def workspace-page-guides
+ (l/derived :guides workspace-page))
+
(defn workspace-page-object-by-id
[page-id shape-id]
(l/derived #(dsh/lookup-shape % page-id shape-id) st/state =))
diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs
index d60366592c..a53971c452 100644
--- a/frontend/src/app/main/render.cljs
+++ b/frontend/src/app/main/render.cljs
@@ -484,6 +484,46 @@
[:& ff/fontfaces-style {:fonts fonts}]
[:& shape-wrapper {:shape object}]]]]))
+(mf/defc objects-svg
+ {::mf/wrap [mf/memo]}
+ [{:keys [objects object-ids embed] :or {embed false} :as props}]
+ (let [shapes
+ (->> object-ids
+ (keep #(get objects %))
+ (mapv (fn [object]
+ (cond-> object
+ (:hide-fill-on-export object)
+ (assoc :fills [])))))
+
+ bounds
+ (->> shapes
+ (map #(gsb/get-object-bounds objects % {:ignore-margin? false}))
+ (grc/join-rects))
+
+ {:keys [width height]} bounds
+ vbox (format-viewbox bounds)
+ fonts (->> shapes
+ (mapcat #(ff/shape->fonts % objects))
+ (distinct))
+
+ shape-wrapper
+ (mf/with-memo [objects]
+ (shape-wrapper-factory objects))]
+
+ [:& (mf/provider export/include-metadata-ctx) {:value false}
+ [:& (mf/provider embed/context) {:value embed}
+ [:svg {:view-box vbox
+ :width (ust/format-precision width viewbox-decimal-precision)
+ :height (ust/format-precision height viewbox-decimal-precision)
+ :version "1.1"
+ :xmlns "http://www.w3.org/2000/svg"
+ :xmlnsXlink "http://www.w3.org/1999/xlink"
+ :style {:-webkit-print-color-adjust :exact}
+ :fill "none"}
+ [:& ff/fontfaces-style {:fonts fonts}]
+ (for [shape shapes]
+ [:& shape-wrapper {:key (dm/str (:id shape)) :shape shape}])]]]))
+
(defn render-to-canvas
[objects canvas bounds scale object-id on-render]
(let [width (.-width canvas)
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 67795c2ff3..39ff4493dd 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -10,6 +10,7 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.common :as dcm]
+ [app.main.data.nitrate :as dnt]
[app.main.data.team :as dtm]
[app.main.errors :as errors]
[app.main.refs :as refs]
@@ -23,6 +24,7 @@
[app.main.ui.error-boundary :refer [error-boundary*]]
[app.main.ui.exports.files]
[app.main.ui.frame-preview :as frame-preview]
+ [app.main.ui.nitrate.entry :as nitrate-entry]
[app.main.ui.notifications :as notifications]
[app.main.ui.onboarding.questions :refer [questions-modal]]
[app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]]
@@ -152,21 +154,25 @@
props (get profile :props)
section (get data :name)
team (mf/deref refs/team)
+ nitrate-entry-active? (dnt/nitrate-entry-active?)
show-question-modal?
(and (contains? cf/flags :onboarding)
+ (not nitrate-entry-active?)
(not (:onboarding-viewed props))
(not (contains? props :onboarding-questions)))
show-team-modal?
(and (contains? cf/flags :onboarding)
+ (not nitrate-entry-active?)
(not (:onboarding-viewed props))
(not (contains? props :onboarding-team-id))
(:is-default team))
show-release-modal?
(and (contains? cf/flags :onboarding)
+ (not nitrate-entry-active?)
(not (contains? cf/flags :hide-release-modal))
(:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version))
@@ -185,6 +191,9 @@
:auth-verify-token
[:? [:& verify-token-page* {:route route}]]
+ :nitrate-entry
+ [:> nitrate-entry/nitrate-entry-page* {:profile profile}]
+
(:settings-profile
:settings-password
:settings-options
diff --git a/frontend/src/app/main/ui/alert.scss b/frontend/src/app/main/ui/alert.scss
index f50ee50d41..b3d0144fc1 100644
--- a/frontend/src/app/main/ui/alert.scss
+++ b/frontend/src/app/main/ui/alert.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
&.transparent {
background-color: transparent;
@@ -15,7 +15,7 @@
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
}
.modal-header {
@@ -23,39 +23,42 @@
}
.modal-title {
- @include deprecated.headlineMediumTypography;
+ @include deprecated.headline-medium-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
margin-bottom: deprecated.$s-24;
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
.modal-scd-msg,
.modal-subtitle,
.modal-msg {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
line-height: 1.5;
}
diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss
index 62ba1a1830..40f2076fec 100644
--- a/frontend/src/app/main/ui/auth.scss
+++ b/frontend/src/app/main/ui/auth.scss
@@ -18,7 +18,7 @@
width: 100%;
overflow: auto;
- @media (max-width: 992px) {
+ @media (width <= 992px) {
display: flex;
justify-content: center;
}
@@ -53,7 +53,7 @@
height: auto;
justify-self: center;
- @media (max-width: 992px) {
+ @media (width <= 992px) {
display: none;
}
}
diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss
index eedfa34da1..dc438a1d97 100644
--- a/frontend/src/app/main/ui/auth/common.scss
+++ b/frontend/src/app/main/ui/auth/common.scss
@@ -11,6 +11,7 @@
padding-block-end: 0;
display: grid;
gap: deprecated.$s-12;
+
form {
display: flex;
flex-direction: column;
@@ -32,17 +33,20 @@
}
.auth-title {
- @include deprecated.bigTitleTipography;
+ @include deprecated.big-title-typography;
+
color: var(--title-foreground-color-hover);
}
.auth-subtitle {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
color: var(--title-foreground-color);
}
.auth-tagline {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
margin: 0;
color: var(--title-foreground-color);
}
@@ -60,8 +64,9 @@
.login-button,
.login-ldap-button {
- @extend .button-primary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-primary;
+ @include deprecated.uppercase-title-typography;
+
height: deprecated.$s-40;
width: 100%;
}
@@ -75,8 +80,9 @@
}
.go-back-link {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
height: deprecated.$s-40;
}
@@ -99,7 +105,8 @@
.account-text,
.recovery-text,
.demo-account-text {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
text-align: right;
color: var(--title-foreground-color);
}
@@ -109,7 +116,8 @@
.recovery-link,
.forgot-pass-link,
.demo-account-link {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
text-align: left;
background-color: transparent;
border: none;
@@ -129,14 +137,16 @@
.submit-btn,
.register-btn,
.recover-btn {
- @extend .button-primary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-primary;
+ @include deprecated.uppercase-title-typography;
+
height: deprecated.$s-40;
width: 100%;
}
.login-btn {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
display: flex;
align-items: center;
gap: deprecated.$s-6;
@@ -144,6 +154,7 @@
border-radius: deprecated.$br-8;
background-color: var(--button-secondary-background-color-rest);
color: var(--button-foreground-color-focus);
+
span {
padding-block-start: deprecated.$s-2;
}
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 51c59f0c71..4382f95327 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -44,12 +44,12 @@
(st/emit! (da/create-demo-profile)))
(defn- store-login-redirect
- []
+ [callback-url]
(binding [s/*sync* true]
;; Save the current login raw uri for later redirect user back to
;; the same page, we need it to be synchronous because the user is
;; going to be redirected instantly to the oidc provider uri
- (swap! s/session assoc :login-redirect (rt/get-current-href))))
+ (swap! s/session assoc :login-redirect (or callback-url (rt/get-current-href)))))
(defn- clear-login-redirect
[]
@@ -74,6 +74,7 @@
error (mf/use-state false)
form (fm/use-form :schema schema:login-form
:initial initial)
+ callback-url (:callback-url params)
on-error
(fn [cause]
(let [cause (ex-data cause)]
@@ -156,9 +157,9 @@
#(st/emit! (rt/nav :auth-recovery-request)))]
- (mf/with-effect [handle-redirect]
- (if handle-redirect
- (store-login-redirect)
+ (mf/with-effect [handle-redirect callback-url]
+ (if (or handle-redirect callback-url)
+ (store-login-redirect callback-url)
(clear-login-redirect)))
[:*
@@ -238,7 +239,7 @@
(when (contains? cf/flags :login-with-oidc)
[:& bl/button-link {:on-click login-with-oidc
:icon deprecated-icon/brand-openid
- :label (tr "auth.login-with-oidc-submit")
+ :label (or (not-empty cf/oidc-name) (tr "auth.login-with-oidc-submit"))
:class (stl/css :login-btn :btn-oidc-auth)}])]))
(mf/defc login-dialog*
diff --git a/frontend/src/app/main/ui/auth/login.scss b/frontend/src/app/main/ui/auth/login.scss
index b0002114f9..4f4aa3dd9d 100644
--- a/frontend/src/app/main/ui/auth/login.scss
+++ b/frontend/src/app/main/ui/auth/login.scss
@@ -4,4 +4,4 @@
//
// Copyright (c) KALEIDOS INC
-@use "./common.scss";
+@use "./common";
diff --git a/frontend/src/app/main/ui/auth/recovery.scss b/frontend/src/app/main/ui/auth/recovery.scss
index a89055b061..6da351a238 100644
--- a/frontend/src/app/main/ui/auth/recovery.scss
+++ b/frontend/src/app/main/ui/auth/recovery.scss
@@ -5,7 +5,7 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
-@use "./common.scss";
+@use "./common";
.submit-btn {
margin-top: deprecated.$s-16;
diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss
index c774a575a3..b4c053b104 100644
--- a/frontend/src/app/main/ui/auth/recovery_request.scss
+++ b/frontend/src/app/main/ui/auth/recovery_request.scss
@@ -5,14 +5,15 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
-@use "./common.scss";
+@use "./common";
.fields-row {
margin-bottom: deprecated.$s-8;
}
.notification-text-email {
- @include deprecated.medTitleTipography;
+ @include deprecated.med-title-typography;
+
font-size: deprecated.$fs-20;
color: var(--register-confirmation-color);
margin-inline: deprecated.$s-36;
diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs
index 917b272dd9..60fd3e0167 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -221,6 +221,7 @@
:class (stl/css :demo-account-link)}
(tr "auth.create-demo-account")]]])]])
+
;; --- PAGE: register success page
(mf/defc register-success-page*
diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss
index 182dfddbaa..47445a3633 100644
--- a/frontend/src/app/main/ui/auth/register.scss
+++ b/frontend/src/app/main/ui/auth/register.scss
@@ -5,7 +5,7 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
-@use "./common.scss";
+@use "./common";
.accept-terms-and-privacy-wrapper {
:global(a) {
@@ -25,8 +25,9 @@
.register-success {
gap: deprecated.$s-24;
+
.auth-title {
- @include deprecated.medTitleTipography;
+ @include deprecated.med-title-typography;
}
}
@@ -35,6 +36,7 @@
display: flex;
justify-content: center;
margin-bottom: deprecated.$s-32;
+
svg {
width: deprecated.$s-92;
height: deprecated.$s-92;
@@ -42,12 +44,14 @@
}
.notification-text {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--title-foreground-color);
}
.notification-text-email {
- @include deprecated.medTitleTipography;
+ @include deprecated.med-title-typography;
+
font-size: deprecated.$fs-20;
color: var(--register-confirmation-color);
margin-inline: deprecated.$s-36;
@@ -55,6 +59,7 @@
.logo-btn {
height: deprecated.$s-40;
+
svg {
width: deprecated.$s-120;
height: deprecated.$s-40;
@@ -70,7 +75,8 @@
}
.terms-register {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
gap: deprecated.$s-4;
justify-content: center;
@@ -84,6 +90,7 @@
.auth-link {
color: var(--link-foreground-color);
+
&:hover {
text-decoration: underline;
}
diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs
index 16e818e4b2..a93954ace3 100644
--- a/frontend/src/app/main/ui/auth/verify_token.cljs
+++ b/frontend/src/app/main/ui/auth/verify_token.cljs
@@ -43,19 +43,22 @@
(st/emit! (da/login-from-token tdata)))
(defmethod handle-token :team-invitation
- [tdata]
- (case (:state tdata)
+ [{:keys [state team-id org-team-id organization-name invitation-token] :as tdata}]
+ (case state
:created
- (let [team-id (:team-id tdata)]
+ (if org-team-id
(st/emit!
- (ntf/success (tr "auth.notifications.team-invitation-accepted"))
(du/refresh-profile)
- (dcm/go-to-dashboard-recent :team-id team-id)))
+ (dcm/go-to-dashboard-recent :team-id org-team-id)
+ (ntf/success (tr "auth.notifications.org-invitation-accepted" organization-name)))
+ (st/emit!
+ (du/refresh-profile)
+ (dcm/go-to-dashboard-recent :team-id team-id)
+ (ntf/success (tr "auth.notifications.team-invitation-accepted"))))
:pending
- (let [token (:invitation-token tdata)
- route-id (:redirect-to tdata :auth-register)]
- (st/emit! (rt/nav route-id {:invitation-token token})))))
+ (let [route-id (:redirect-to tdata :auth-register)]
+ (st/emit! (rt/nav route-id {:invitation-token invitation-token})))))
(defmethod handle-token :default
[_tdata]
@@ -65,8 +68,15 @@
(mf/defc verify-token*
[{:keys [route]}]
- (let [token (get-in route [:query-params :token])
- bad-token (mf/use-state false)]
+ (let [token (get-in route [:query-params :token])
+ ;; Holds the specific failure reason when the token fails, or
+ ;; nil while still loading / on success. Any non-nil keyword is
+ ;; truthy, so this single state replaces the previous pair of
+ ;; (bad-token? + bad-token-reason) hooks. Reasons:
+ ;; :token-expired -> JWT past its :exp
+ ;; :email-mismatch -> invitation email != logged-in email
+ ;; :invalid-token -> corrupted / unknown / fallback
+ bad-token-reason (mf/use-state nil)]
(mf/with-effect []
(dom/set-html-title (tr "title.default"))
@@ -75,12 +85,25 @@
(fn [tdata]
(handle-token tdata))
(fn [cause]
- (let [{:keys [type code] :as error} (ex-data cause)]
+ (let [{:keys [type code team-id reason] :as error} (ex-data cause)]
(cond
+ (= :invalid-token-already-member code)
+ (st/emit!
+ (rt/nav :dashboard-recent {:team-id team-id}))
+
+ (= :org-not-found code)
+ (st/emit!
+ (rt/nav :dashboard-recent {:team-id team-id})
+ (ntf/error (tr "errors.org-not-found")))
+
(or (= :validation type)
(= :invalid-token code)
- (= :token-expired (:reason error)))
- (reset! bad-token true)
+ (= :token-expired reason))
+ (reset! bad-token-reason
+ (cond
+ (= :token-expired reason) :token-expired
+ (= :email-mismatch reason) :email-mismatch
+ :else :invalid-token))
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
@@ -97,8 +120,8 @@
(ts/schedule 100 #(st/emit! (ntf/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
- (if @bad-token
- [:> static/invalid-token {}]
+ (if @bad-token-reason
+ [:> static/invalid-token {:reason @bad-token-reason}]
[:> loader* {:title (tr "labels.loading")
:overlay true}])))
diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs
index cc12398c62..3093a11287 100644
--- a/frontend/src/app/main/ui/comments.cljs
+++ b/frontend/src/app/main/ui/comments.cljs
@@ -45,20 +45,34 @@
(def mentions-context (mf/create-context nil))
(def r-mentions-split #"@\[[^\]]*\]\([^\)]*\)")
(def r-mentions #"@\[([^\]]*)\]\(([^\)]*)\)")
+(def r-url-split #"https?://[^\s\)\]]+[^\s\)\]\.,;:!?]")
(def zero-width-space \u200B)
-(defn- parse-comment
- "Parse a comment into its elements (texts and mentions)"
- [comment]
- (d/interleave-all
- (->> (str/split comment r-mentions-split)
- (map #(hash-map :type :text :content %)))
+(defn- parse-urls
+ "Split a text element into text and url sub-elements"
+ [element]
+ (if (= (:type element) :text)
+ (let [text (:content element)
+ parts (str/split text r-url-split)
+ urls (re-seq r-url-split text)]
+ (d/interleave-all
+ (map #(hash-map :type :text :content %) parts)
+ (map #(hash-map :type :url :content %) urls)))
+ [element]))
- (->> (re-seq r-mentions comment)
- (map (fn [[_ user id]]
- {:type :mention
- :content user
- :data {:id id}})))))
+(defn- parse-comment
+ "Parse a comment into its elements (texts, mentions and urls)"
+ [comment]
+ (->> (d/interleave-all
+ (->> (str/split comment r-mentions-split)
+ (map #(hash-map :type :text :content %)))
+
+ (->> (re-seq r-mentions comment)
+ (map (fn [[_ user id]]
+ {:type :mention
+ :content user
+ :data {:id id}}))))
+ (mapcat parse-urls)))
(defn- parse-nodes
"Parse the nodes to format a comment"
@@ -146,7 +160,13 @@
[{:keys [content]}]
(let [comment-elements (mf/use-memo (mf/deps content) #(parse-comment content))]
(for [[idx {:keys [type content]}] (d/enumerate comment-elements)]
- (case type
+ (if (= type :url)
+ [:a {:key idx
+ :href content
+ :target "_blank"
+ :rel "noopener noreferrer"
+ :class (stl/css :comment-link)}
+ content]
[:span
{:key idx
:class (stl/css-case
@@ -177,6 +197,7 @@
(doseq [{:keys [type content data]} (parse-comment value)]
(case type
:text (dom/append-child! node (create-text-node content))
+ :url (dom/append-child! node (create-text-node content))
:mention (dom/append-child! node (create-mention-node (:id data) content))
nil)))))
diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss
index 9da4eef616..9da2078d38 100644
--- a/frontend/src/app/main/ui/comments.scss
+++ b/frontend/src/app/main/ui/comments.scss
@@ -23,7 +23,8 @@
}
.error-text {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--color-foreground-error);
}
@@ -39,11 +40,12 @@
}
.location-text {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
}
.author {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
gap: deprecated.$s-8;
@@ -54,12 +56,14 @@
}
.author-fullname {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
color: var(--comment-title-color);
}
.author-timeago {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
color: var(--comment-subtitle-color);
}
@@ -112,11 +116,12 @@
}
.avatar-darken {
- background: rgba(0, 0, 0, 0.5);
+ background: rgb(0 0 0 / 0.5);
}
.cover {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
cursor: pointer;
display: flex;
flex-direction: column;
@@ -126,16 +131,17 @@
}
.item {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--color-foreground-primary);
- word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
white-space: pre-wrap;
}
.replies {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
gap: deprecated.$s-8;
}
@@ -143,6 +149,7 @@
.replies-total {
color: var(--color-foreground-secondary);
}
+
.replies-unread {
color: var(--color-accent-primary);
}
@@ -168,15 +175,18 @@
--translate-x: 0%;
--translate-y: 0%;
+
transform: translate(var(--translate-x), var(--translate-y));
&.left {
--translate-x: -100%;
+
flex-direction: row-reverse;
}
&.top {
--translate-y: -100%;
+
align-items: flex-end;
}
}
@@ -214,10 +224,13 @@
--translate-x: 0%;
--translate-y: 0%;
+
transform: translate(var(--translate-x), var(--translate-y));
+
&.left {
--translate-x: -100%;
}
+
&.top {
--translate-y: -100%;
}
@@ -232,7 +245,8 @@
}
.floating-thread-header-left {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--color-foreground-primary);
}
@@ -257,22 +271,25 @@
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
- @include deprecated.bodySmallTypography;
+
+ @include deprecated.body-small-typography;
}
.checkbox-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-16;
height: deprecated.$s-24;
margin-right: deprecated.$s-8;
}
.checkbox {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
.dropdown-menu {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
position: absolute;
width: fit-content;
max-width: deprecated.$s-200;
@@ -282,7 +299,7 @@
}
.dropdown-menu-option {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
}
.form {
@@ -364,8 +381,8 @@
}
.comment-input {
- @include deprecated.bodySmallTypography;
- white-space: pre-line;
+ @include deprecated.body-small-typography;
+
background: var(--input-background-color);
border-radius: deprecated.$br-8;
border: deprecated.$s-1 solid var(--input-border-color);
@@ -401,6 +418,12 @@
color: var(--color-accent-primary);
}
+.comment-link {
+ color: var(--color-accent-primary);
+ text-decoration: underline;
+ cursor: pointer;
+}
+
.comments-mentions-empty {
font-size: deprecated.$fs-12;
color: var(--color-foreground-secondary);
diff --git a/frontend/src/app/main/ui/components/button_link.scss b/frontend/src/app/main/ui/components/button_link.scss
index bb58dcc4a5..b3693cbdb0 100644
--- a/frontend/src/app/main/ui/components/button_link.scss
+++ b/frontend/src/app/main/ui/components/button_link.scss
@@ -18,7 +18,6 @@
padding: 0 1rem;
transition: all 0.4s;
text-decoration: none !important;
-
height: 40px;
svg {
diff --git a/frontend/src/app/main/ui/components/code_block.scss b/frontend/src/app/main/ui/components/code_block.scss
index 69b4658f0f..dd8d79680e 100644
--- a/frontend/src/app/main/ui/components/code_block.scss
+++ b/frontend/src/app/main/ui/components/code_block.scss
@@ -9,6 +9,7 @@
.code-display {
@include t.use-typography("code-font");
+
user-select: text;
border-radius: $br-8;
margin-top: var(--sp-s);
diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs
index d94938d147..55d9e4c57c 100644
--- a/frontend/src/app/main/ui/components/color_bullet.cljs
+++ b/frontend/src/app/main/ui/components/color_bullet.cljs
@@ -7,24 +7,33 @@
(ns app.main.ui.components.color-bullet
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.math :as mth]
[app.config :as cfg]
[app.util.color :as uc]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
+(defn- format-color-with-alpha
+ [color opacity]
+ (if (and (number? opacity) (< opacity 1))
+ (str color " " (mth/round (* opacity 100)) "%")
+ color))
+
(defn- color-title
[color-item]
(let [{:keys [name path]} (meta color-item)
path-and-name (if path (str path " / " name) name)
gradient (:gradient color-item)
image (:image color-item)
- color (:color color-item)]
+ opacity (:opacity color-item)
+ color (:color color-item)
+ color-str (when color (format-color-with-alpha color opacity))]
(if (some? name)
(cond
(some? color)
- (str/ffmt "% (%)" path-and-name color)
+ (str/ffmt "% (%)" path-and-name color-str)
(some? gradient)
(str/ffmt "% (%)" path-and-name (uc/gradient-type->string (:type gradient)))
@@ -37,7 +46,7 @@
(cond
(some? color)
- color
+ color-str
(some? gradient)
(uc/gradient-type->string (:type gradient))
diff --git a/frontend/src/app/main/ui/components/color_bullet.scss b/frontend/src/app/main/ui/components/color_bullet.scss
index 52fc242fac..7972780483 100644
--- a/frontend/src/app/main/ui/components/color_bullet.scss
+++ b/frontend/src/app/main/ui/components/color_bullet.scss
@@ -16,9 +16,11 @@
min-height: var(--bullet-size, deprecated.$s-24);
border: deprecated.$s-2 solid var(--color-bullet-border-color);
border-radius: deprecated.$br-circle;
+
&.grid-area {
grid-area: color;
}
+
&.mini {
width: var(--bullet-size, deprecated.$s-16);
height: var(--bullet-size, deprecated.$s-16);
@@ -31,24 +33,29 @@
&.is-not-library-color {
overflow: hidden;
border-radius: deprecated.$br-8;
+
& .color-bullet-wrapper {
clip-path: none;
}
+
&.mini {
border-radius: deprecated.$br-4;
}
}
+
&.is-gradient {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=")
left center;
background-color: var(--color-bullet-background-color);
transform: rotate(-90deg);
}
+
&.is-transparent {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=")
left center;
background-color: var(--color-bullet-background-color);
}
+
.color-bullet-wrapper {
display: flex;
flex-direction: row;
@@ -59,33 +66,39 @@
background-repeat: no-repeat;
background-position: center;
}
+
.color-bullet-wrapper > * {
width: 100%;
height: 100%;
background-color: var(--color-bullet-background-color);
}
+
&:hover:not(.read-only) {
border: deprecated.$s-2 solid var(--color-bullet-border-color-selected);
}
}
.color-text {
- @include deprecated.twoLineTextEllipsis;
- @include deprecated.bodySmallTypography;
+ @include deprecated.two-line-text-ellipsis;
+ @include deprecated.body-small-typography;
+
width: deprecated.$s-80;
text-align: center;
margin-top: deprecated.$s-2;
max-height: deprecated.$s-28;
color: var(--palette-text-color);
+
&.small-text {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
max-height: deprecated.$s-16;
}
}
.big-text {
- @include deprecated.inspectValue;
- @include deprecated.twoLineTextEllipsis;
+ @include deprecated.inspect-value;
+ @include deprecated.two-line-text-ellipsis;
+
line-height: 1;
color: var(--palette-text-color);
text-align: center;
diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss
index 787941b595..5297f75422 100644
--- a/frontend/src/app/main/ui/components/context_menu_a11y.scss
+++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss
@@ -25,7 +25,8 @@
}
.context-menu-items {
- @include deprecated.menuShadow;
+ @include deprecated.menu-shadow;
+
position: absolute;
top: deprecated.$s-12;
left: calc(-1 * deprecated.$s-6);
@@ -50,7 +51,8 @@
display: flex;
.context-menu-action {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
justify-content: flex-start;
@@ -70,7 +72,8 @@
margin-left: 0.5rem;
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--menu-foreground-color);
}
}
@@ -85,7 +88,8 @@
cursor: pointer;
.submenu-icon-back svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--menu-foreground-color);
transform: rotate(180deg);
}
@@ -141,12 +145,14 @@
}
.selected-icon {
- @extend .button-tag;
+ @extend %button-tag;
+
border-radius: deprecated.$br-8;
height: 100%;
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--menu-foreground-color-focus);
}
}
@@ -155,7 +161,7 @@
.is-selected .context-menu-action {
padding-left: deprecated.$s-28;
- background-image: url(/images/icons/tick.svg);
+ background-image: url("/images/icons/tick.svg");
background-repeat: no-repeat;
background-position: 5% 48%;
background-size: deprecated.$s-12;
diff --git a/frontend/src/app/main/ui/components/copy_button.scss b/frontend/src/app/main/ui/components/copy_button.scss
index 0239900938..63f2f5069b 100644
--- a/frontend/src/app/main/ui/components/copy_button.scss
+++ b/frontend/src/app/main/ui/components/copy_button.scss
@@ -7,20 +7,25 @@
@use "refactor/common-refactor.scss" as deprecated;
.copy-button {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
width: 100%;
height: deprecated.$s-32;
border: deprecated.$s-1 solid transparent;
border-radius: deprecated.$br-8;
background-color: transparent;
box-sizing: border-box;
+
.icon-btn {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
min-width: deprecated.$s-28;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
@@ -29,18 +34,21 @@
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
border: deprecated.$s-1 solid var(--color-background-tertiary);
+
.icon-btn {
svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
}
+
&:focus,
&:focus-visible {
outline: none;
border: deprecated.$s-1 solid var(--button-tertiary-border-color-focus);
background-color: transparent;
color: var(--button-tertiary-foreground-color-focus);
+
.icon-btn svg {
stroke: var(--button-tertiary-foreground-color-active);
}
@@ -48,29 +56,36 @@
}
.copy-wrapper {
- @include deprecated.buttonStyle;
- @include deprecated.copyWrapperBase;
+ @include deprecated.button-style;
+ @include deprecated.copy-wrapper-base;
+
width: 100%;
height: fit-content;
text-align: left;
border: deprecated.$s-1 solid transparent;
+
.icon-btn {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
position: absolute;
top: 0;
right: 0;
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--button-tertiary-foreground-color-focus);
display: none;
}
}
+
&:hover {
background-color: var(--button-tertiary-background-color-focus);
color: var(--button-tertiary-foreground-color-focus);
border: deprecated.$s-1 solid var(--button-tertiary-background-color-focus);
+
.icon-btn svg {
display: flex;
}
diff --git a/frontend/src/app/main/ui/components/editable_label.scss b/frontend/src/app/main/ui/components/editable_label.scss
index 29a57d551d..46ff586f95 100644
--- a/frontend/src/app/main/ui/components/editable_label.scss
+++ b/frontend/src/app/main/ui/components/editable_label.scss
@@ -11,6 +11,7 @@
.editable-label-input {
@include t.use-typography("body-small");
+
outline: none;
width: 100%;
height: 100%;
@@ -23,6 +24,7 @@
.editable-label-text {
@include t.use-typography("body-small");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
diff --git a/frontend/src/app/main/ui/components/editable_select.scss b/frontend/src/app/main/ui/components/editable_select.scss
index ef874df8c0..07a1272095 100644
--- a/frontend/src/app/main/ui/components/editable_select.scss
+++ b/frontend/src/app/main/ui/components/editable_select.scss
@@ -4,18 +4,17 @@
//
// Copyright (c) KALEIDOS INC
-// FIXME: we need this import for .asset-element
+// FIXME: we need this import for %asset-element
@use "refactor/basic-rules.scss" as deprecated;
-
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/spacing.scss" as *;
.editable-select {
- @extend .asset-element;
+ @extend %asset-element;
+
margin: 0;
- padding: 0;
border: $b-1 solid var(--input-border-color);
position: relative;
display: flex;
@@ -24,27 +23,34 @@
padding: var(--sp-s);
border-radius: $br-8;
cursor: pointer;
+
.dropdown-button {
display: flex;
place-content: center;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
}
.custom-select-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
width: fit-content;
max-height: px2rem(320); // TODO: when this gets addressed in the DS, use a token
.separator {
margin: 0;
height: $sz-12;
}
+
.dropdown-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
color: var(--menu-foreground-color-rest);
+
.label {
flex-grow: 1;
width: 100%;
@@ -53,8 +59,10 @@
.check-icon {
display: flex;
place-content: center;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
visibility: hidden;
stroke: var(--icon-foreground);
}
@@ -62,14 +70,17 @@
&.is-selected {
color: var(--menu-foreground-color);
+
.check-icon svg {
stroke: var(--menu-foreground-color);
visibility: visible;
}
}
+
&:hover {
background-color: var(--menu-background-color-hover);
color: var(--menu-foreground-color-hover);
+
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs
index e2bb4cd5cf..17a1708196 100644
--- a/frontend/src/app/main/ui/components/forms.cljs
+++ b/frontend/src/app/main/ui/components/forms.cljs
@@ -92,6 +92,15 @@
(when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true)))
+ on-clear
+ (fn [event]
+ (dom/prevent-default event)
+ (swap! form (fn [state]
+ (-> state
+ (assoc-in [:data input-name] "")
+ (assoc-in [:touched input-name] false))))
+ (some-> (mf/ref-val input-ref) (dom/focus!)))
+
on-key-press
(mf/use-fn
(mf/deps input-ref)
@@ -158,7 +167,10 @@
deprecated-icon/tick])
(when show-invalid?
- [:span {:class (stl/css :invalid-icon)}
+ [:button {:class (stl/css :invalid-icon)
+ :type "button"
+ :tab-index "-1"
+ :on-click on-clear}
deprecated-icon/close])])]
(some? children)
diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss
index 6139098e5f..a8705671c6 100644
--- a/frontend/src/app/main/ui/components/forms.scss
+++ b/frontend/src/app/main/ui/components/forms.scss
@@ -5,64 +5,82 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
+@use "ds/typography.scss" as t;
+@use "ds/_borders.scss" as *;
+@use "ds/spacing.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/_utils.scss" as *;
+@use "ds/z-index.scss" as *;
+@use "ds/mixins.scss" as *;
// INPUT
.input-wrapper {
--input-icon-padding: var(--sp-l);
+
display: flex;
flex-direction: column;
align-items: center;
position: relative;
+
&.valid {
input {
- border: deprecated.$s-1 solid var(--input-border-color-success);
- @extend .disabled-input;
+ @extend %disabled-input;
+
+ border: $b-1 solid var(--input-border-color-success);
+
&:hover,
&:focus {
- border: deprecated.$s-1 solid var(--input-border-color-success);
+ border: $b-1 solid var(--input-border-color-success);
}
}
}
+
&.invalid {
input {
- border: deprecated.$s-1 solid var(--input-border-color-error);
- @extend .disabled-input;
+ @extend %disabled-input;
+
+ border: $b-1 solid var(--input-border-color-error);
+
&:hover,
&:focus {
- border: deprecated.$s-1 solid var(--input-border-color-error);
+ border: $b-1 solid var(--input-border-color-error);
}
}
}
+
&.valid .help-icon,
&.invalid .help-icon {
- right: deprecated.$s-40;
+ inset-inline-end: $sz-40;
}
}
.input-with-label-form {
- @include deprecated.flexColumn;
- gap: deprecated.$s-8;
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-s);
justify-content: flex-start;
align-items: flex-start;
- height: 100%;
- width: 100%;
+ block-size: 100%;
+ inline-size: 100%;
padding: 0;
cursor: pointer;
color: var(--modal-title-foreground-color);
text-transform: uppercase;
+
input {
- @extend .input-element;
+ @extend %input-element;
+
color: var(--input-foreground-color-active);
- margin-top: 0;
- width: 100%;
- max-width: 100%;
- height: 100%;
- padding: 0 deprecated.$s-8;
+ margin-block-start: 0;
+ inline-size: 100%;
+ max-inline-size: 100%;
+ block-size: 100%;
+ padding: 0 var(--sp-s);
&:focus {
outline: none;
- border: deprecated.$s-1 solid var(--input-border-color-focus);
- border-radius: deprecated.$br-8;
+ border: $b-1 solid var(--input-border-color-focus);
+ border-radius: var(--sp-s);
}
}
@@ -72,9 +90,9 @@
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-text-fill-color: var(--input-foreground-color-active);
- -webkit-box-shadow: inset 0 0 20px 20px var(--input-background-color);
- border: deprecated.$s-1 solid var(--input-border-color);
- -webkit-background-clip: text;
+ box-shadow: inset 0 0 20px 20px var(--input-background-color);
+ border: $b-1 solid var(--input-border-color);
+ background-clip: text;
transition: background-color 5000s ease-in-out 0s;
caret-color: var(--input-foreground-color-active);
}
@@ -82,56 +100,63 @@
.input-and-icon {
position: relative;
- width: var(--input-width, calc(100% - deprecated.$s-1));
- min-width: var(--input-min-width);
- height: var(--input-height, deprecated.$s-32);
+ inline-size: var(--input-width, calc(100% - deprecated.$s-1));
+ min-inline-size: var(--input-min-width);
+ block-size: var(--input-height, $sz-32);
}
.help-icon {
cursor: pointer;
position: absolute;
- right: deprecated.$s-16;
- top: calc(50% - deprecated.$s-8);
+ inset-inline-end: var(--sp-l);
+ inset-block-start: calc(50% - var(--sp-s));
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--color-foreground-secondary);
- width: deprecated.$s-16;
- height: deprecated.$s-16;
+ inline-size: $sz-16;
+ block-size: $sz-16;
}
}
.invalid-icon {
- width: deprecated.$s-16;
- height: deprecated.$s-16;
+ inline-size: $sz-16;
+ block-size: $sz-16;
+ padding: 0;
+ border: none;
background: var(--input-border-color-error);
- border-radius: 50%;
+ border-radius: $br-circle;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
- right: var(--input-icon-padding);
- top: calc(50% - deprecated.$s-8);
+ inset-inline-end: var(--input-icon-padding);
+ inset-block-start: calc(50% - var(--sp-s));
+ cursor: pointer;
+
svg {
- width: deprecated.$s-12;
- height: deprecated.$s-12;
+ inline-size: $sz-12;
+ block-size: $sz-12;
stroke: var(--input-background-color);
}
}
.valid-icon {
- width: deprecated.$s-16;
- height: deprecated.$s-16;
+ inline-size: $sz-16;
+ block-size: $sz-16;
background: var(--input-border-color-success);
- border-radius: 50%;
+ border-radius: $br-circle;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
- right: deprecated.$s-16;
- top: calc(50% - deprecated.$s-8);
+ inset-inline-end: var(--sp-l);
+ inset-block-start: calc(50% - var(--sp-s));
+
svg {
- width: deprecated.$s-12;
- height: deprecated.$s-12;
+ inline-size: $sz-12;
+ block-size: $sz-12;
fill: var(--input-border-color-success);
stroke: var(--input-background-color);
}
@@ -139,38 +164,45 @@
.error {
color: var(--input-border-color-error);
- width: 100%;
+ inline-size: 100%;
font-size: deprecated.$fs-14;
}
.hint {
- @include deprecated.bodySmallTypography;
- width: 99%;
- margin-block-start: deprecated.$s-8;
+ @include t.use-typography("body-small");
+
+ inline-size: 99%;
+ margin-block-start: var(--sp-s);
color: var(--modal-text-foreground-color);
}
.checkbox {
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
.checkbox-label {
- @include deprecated.bodySmallTypography;
+ @include t.use-typography("body-small");
+
display: flex;
align-items: center;
flex-direction: row-reverse;
- gap: deprecated.$s-6;
- min-height: deprecated.$s-32;
+ gap: px2rem(6);
+ min-block-size: var(--sp-xxxl);
cursor: pointer;
+
span {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
+
input {
display: none !important;
}
+
&:hover {
span {
border-color: var(--input-checkbox-border-color-hover);
}
}
+
a {
// Need for terms and conditions links on register checkbox
color: var(--link-foreground-color);
@@ -180,43 +212,60 @@
// SELECT
.custom-select {
- @extend .select-wrapper;
- height: deprecated.$s-32;
+ @extend %select-wrapper;
+
+ block-size: $sz-32;
+
.input-container {
- @include deprecated.flexRow;
- height: deprecated.$s-32;
- width: 100%;
- border-radius: deprecated.$br-8;
- border: deprecated.$s-1 solid var(--input-border-color);
+ display: flex;
+ align-items: center;
+ gap: var(--sp-xs);
+ block-size: $sz-32;
+ inline-size: 100%;
+ border-radius: var(--sp-s);
+ border: $b-1 solid var(--input-border-color);
+
+ @extend %select-wrapper;
+
color: var(--input-foreground-color-active);
background-color: var(--input-background-color);
+
.main-content {
- @include deprecated.flexColumn;
- @include deprecated.bodySmallTypography;
+ @include t.use-typography("body-small");
+
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
position: relative;
justify-content: center;
flex-grow: 1;
- height: 100%;
- padding: deprecated.$s-8;
+ block-size: 100%;
+ padding: var(--sp-s);
.label {
color: var(--input-foreground-color);
}
+
.value {
- width: 100%;
- padding: 0px;
- margin: 0px;
- border: 0px;
+ inline-size: 100%;
+ padding: 0;
+ margin: 0;
+ border: 0;
color: var(--input-foreground-color-active);
}
}
+
.icon {
- @include deprecated.flexCenter;
- height: deprecated.$s-32;
- width: deprecated.$s-24;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ block-size: $sz-32;
+ inline-size: $sz-24;
pointer-events: none;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
transform: rotate(90deg);
}
@@ -224,50 +273,56 @@
&.disabled {
background-color: var(--input-background-color-disabled);
- border: deprecated.$s-1 solid var(--input-border-color-disabled);
+ border: $b-1 solid var(--input-border-color-disabled);
color: var(--input-foreground-color-disabled);
}
+
&.focus {
outline: none;
color: var(--input-foreground-color-active);
background-color: var(--input-background-color-active);
- border: deprecated.$s-1 solid var(--input-border-color-active);
+ border: $b-1 solid var(--input-border-color-active);
}
}
select {
- @extend .menu-dropdown;
- @include deprecated.bodySmallTypography;
+ @extend %menu-dropdown;
+ @include t.use-typography("body-small");
+
box-sizing: border-box;
position: absolute;
- top: 0;
- left: 0;
- min-height: deprecated.$s-32;
- height: auto;
- width: calc(100% - 1px);
- padding: 0 deprecated.$s-12;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ min-block-size: $sz-32;
+ block-size: auto;
+ inline-size: calc(100% - 1px);
+ padding: 0 var(--sp-m);
margin: 0;
border: none;
opacity: 0;
- z-index: deprecated.$z-index-10;
+ z-index: var(--z-index-dropdown);
background-color: transparent;
cursor: pointer;
+
option {
- @include deprecated.bodySmallTypography;
+ @include t.use-typography("body-small");
+
color: var(--title-foreground-color-hover);
background-color: var(--menu-background-color);
appearance: none;
- height: deprecated.$s-32;
+ block-size: $sz-32;
}
}
}
// SUBMIT-BUTTON
.button-submit {
- @extend .button-primary;
+ @extend %button-primary;
+
&:disabled {
- @extend .button-disabled;
- min-height: deprecated.$s-32;
+ @extend %button-disabled;
+
+ min-block-size: $sz-32;
}
}
@@ -276,78 +331,98 @@
display: flex;
flex-direction: column;
position: relative;
- min-height: deprecated.$s-40;
- max-height: deprecated.$s-180;
- width: 100%;
+ min-block-size: $sz-40;
+ max-block-size: px2rem(180);
+ inline-size: 100%;
overflow-y: hidden;
+
.inside-input {
- @include deprecated.removeInputStyle;
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
- width: 100%;
- max-width: calc(100% - deprecated.$s-1);
- min-height: deprecated.$s-32;
- padding-top: 0;
- height: deprecated.$s-32;
- padding: deprecated.$s-8;
+ @include deprecated.remove-input-style;
+ @include t.use-typography("body-small");
+ @include text-ellipsis;
+
+ inline-size: 100%;
+ max-inline-size: calc(100% - deprecated.$s-1);
+ min-block-size: $sz-32;
+ padding-block-start: 0;
+ block-size: $sz-32;
+ padding: var(--sp-s);
margin: 0;
- border-radius: deprecated.$br-8;
+ border-radius: var(--sp-s);
color: var(--input-foreground-color-active);
background-color: var(--input-background-color);
+
&:focus {
outline: none;
- border: deprecated.$s-1 solid var(--input-border-color-focus);
+ border: $b-1 solid var(--input-border-color-focus);
}
+
&.invalid {
- border: deprecated.$s-1 solid var(--input-border-color-error);
+ border: $b-1 solid var(--input-border-color-error);
+
&:hover,
&:focus {
- border: deprecated.$s-1 solid var(--input-border-color-error);
+ border: $b-1 solid var(--input-border-color-error);
}
}
}
+
label {
display: none;
}
+
.selected-items {
display: flex;
flex-wrap: wrap;
- gap: deprecated.$s-4;
- max-height: deprecated.$s-136;
- padding: deprecated.$s-4 0;
+ gap: var(--sp-xs);
+ max-block-size: px2rem(136);
+ padding: var(--sp-xs) 0;
overflow-y: auto;
.selected-item {
.around {
- @include deprecated.flexRow;
- height: deprecated.$s-24;
- width: fit-content;
- padding-left: deprecated.$s-6;
- border-radius: deprecated.$br-6;
+ display: flex;
+ align-items: center;
+ gap: var(--sp-xs);
+ block-size: $sz-24;
+ inline-size: fit-content;
+ padding-inline-start: px2rem(6);
+ border-radius: $br-6;
background-color: var(--pill-background-color);
- border: deprecated.$s-1 solid var(--pill-background-color);
+ border: $b-1 solid var(--pill-background-color);
box-sizing: border-box;
+
.text {
- @include deprecated.bodySmallTypography;
- padding-right: deprecated.$s-8;
+ @include t.use-typography("body-small");
+
+ padding-inline-end: var(--sp-s);
color: var(--pill-foreground-color);
}
.icon {
- @include deprecated.flexCenter;
- @include deprecated.buttonStyle;
- height: deprecated.$s-32;
- width: deprecated.$s-24;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: none;
+ background: none;
+ cursor: pointer;
+ block-size: $sz-32;
+ inline-size: $sz-24;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&.invalid {
background-color: var(--status-widget-background-color-error);
+
.text {
color: var(--alert-text-foreground-color-error);
}
+
.icon svg {
stroke: var(--alert-text-foreground-color-error);
}
@@ -361,97 +436,107 @@
.custom-radio {
display: grid;
grid-template-columns: repeat(3, 1fr);
- gap: deprecated.$s-16;
+ gap: var(--sp-l);
}
.radio-label {
- @include deprecated.bodySmallTypography;
- @include deprecated.flexRow;
+ @include t.use-typography("body-small");
+
+ display: flex;
+ align-items: center;
align-items: flex-start;
- gap: deprecated.$s-8;
- min-height: deprecated.$s-32;
- height: fit-content;
- border-radius: deprecated.$br-8;
- padding: deprecated.$s-8;
+ gap: var(--sp-s);
+ min-block-size: $sz-32;
+ block-size: fit-content;
+ border-radius: var(--sp-s);
+ padding: var(--sp-s);
color: var(--input-foreground-color-rest);
- border: deprecated.$s-1 solid transparent;
- &:focus,
- &:focus-within {
+ border: $b-1 solid transparent;
+
+ &:has(:focus-visible) {
outline: none;
- border: deprecated.$s-1 solid var(--input-border-color-active);
+ border: $b-1 solid var(--input-border-color-active);
}
}
.radio-dot {
- height: deprecated.$s-8;
- width: deprecated.$s-8;
- border-radius: deprecated.$br-circle;
+ block-size: var(--sp-s);
+ inline-size: var(--sp-s);
+ border-radius: $br-circle;
background-color: var(--color-background-tertiary);
}
.radio-input {
- width: 0;
+ inline-size: 0;
margin: 0;
}
.radio-icon {
- @extend .checkbox-icon;
- border-radius: deprecated.$br-circle;
+ @extend %checkbox-icon;
+
+ border-radius: $br-circle;
}
.radio-label-image {
- @include deprecated.smallTitleTipography;
+ @include t.use-typography("body-medium");
+
display: grid;
- grid-template-rows: auto auto 0px;
+ grid-template-rows: auto auto 0;
justify-items: center;
gap: 0;
- border-radius: deprecated.$br-8;
+ border-radius: var(--sp-s);
margin: 0;
- border: 1px solid var(--color-background-tertiary);
+ border: $b-1 solid var(--color-background-tertiary);
cursor: pointer;
+
&:global(.checked) {
- border: 1px solid var(--color-accent-primary);
+ border: $b-1 solid var(--color-accent-primary);
}
+
&:focus,
&:focus-within {
outline: none;
- border: deprecated.$s-1 solid var(--input-border-color-active);
+ border: $b-1 solid var(--input-border-color-active);
}
+
.image-text {
color: var(--input-foreground-color-rest);
display: grid;
align-self: center;
- margin-bottom: deprecated.$s-16;
- padding-inline: deprecated.$s-8;
+ margin-block-end: var(--sp-l);
+ padding-inline: var(--sp-s);
text-align: center;
}
}
.image-inside {
- margin: deprecated.$s-16;
+ margin: var(--sp-l);
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.icon-inside {
- margin: deprecated.$s-16;
- @include deprecated.flexCenter;
+ margin: var(--sp-l);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
svg {
- width: 40px;
- height: 60px;
+ inline-size: 40px;
+ block-size: 60px;
stroke: var(--icon-foreground);
fill: none;
}
}
-//TEXTAREA
+// TEXTAREA
.textarea-label {
- @include deprecated.uppercaseTitleTipography;
+ @include t.use-typography("headline-small");
+
color: var(--modal-title-foreground-color);
- text-transform: uppercase;
- margin-bottom: deprecated.$s-8;
+ margin-block-end: var(--sp-s);
}
.textarea-wrapper {
diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs
index 3d7a2b3e46..bec4efd044 100644
--- a/frontend/src/app/main/ui/components/numeric_input.cljs
+++ b/frontend/src/app/main/ui/components/numeric_input.cljs
@@ -63,6 +63,11 @@
;; Last value input by the user we need to store to save on unmount
last-value* (mf/use-var value)
+ ;; Drag scrubbing state
+ drag-state* (mf/use-ref :idle)
+ drag-start-x* (mf/use-ref 0)
+ drag-start-val* (mf/use-ref 0)
+
parse-value
(mf/use-fn
(mf/deps min-value max-value value nillable? default integer?)
@@ -217,16 +222,80 @@
(mf/use-callback
(mf/deps on-focus select-on-focus?)
(fn [event]
- (reset! last-value* (parse-value))
- (let [target (dom/get-target event)]
- (when on-focus
- (mf/set-ref-val! dirty-ref true)
- (on-focus event))
+ (when-not (= :dragging (mf/ref-val drag-state*))
+ (reset! last-value* (parse-value))
+ (let [target (dom/get-target event)]
+ (when on-focus
+ (mf/set-ref-val! dirty-ref true)
+ (on-focus event))
- (when select-on-focus?
- (dom/select-text! target)
- ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
- (.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
+ (when select-on-focus?
+ (dom/select-text! target)
+ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
+ (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))))
+
+ on-scrub-pointer-down
+ (mf/use-fn
+ (mf/deps value value-str min-value max-value default)
+ (fn [event]
+ (let [disabled? (unchecked-get props "disabled")
+ node (mf/ref-val ref)
+ is-focused (and (some? node) (dom/active? node))]
+ (when-not (or disabled? is-focused (= :multiple value-str))
+ (let [client-x (.-clientX event)
+ start-val (or value default 0)]
+ (mf/set-ref-val! drag-state* :maybe-dragging)
+ (mf/set-ref-val! drag-start-x* client-x)
+ (mf/set-ref-val! drag-start-val* start-val)
+ (dom/capture-pointer event))))))
+
+ on-scrub-pointer-move
+ (mf/use-fn
+ (mf/deps apply-value update-input step-value min-value max-value)
+ (fn [event]
+ (let [state (mf/ref-val drag-state*)]
+ (when (or (= state :maybe-dragging) (= state :dragging))
+ (let [client-x (.-clientX event)
+ start-x (mf/ref-val drag-start-x*)
+ delta-x (- client-x start-x)]
+ (when (and (= state :maybe-dragging)
+ (>= (js/Math.abs delta-x) 3))
+ (mf/set-ref-val! drag-state* :dragging)
+ (dom/add-class! (dom/get-body) "cursor-drag-scrub"))
+ (when (= (mf/ref-val drag-state*) :dragging)
+ (let [effective-step (cond
+ (.-shiftKey event) (* step-value 10)
+ (.-ctrlKey event) (* step-value 0.1)
+ :else step-value)
+ steps (js/Math.round (/ delta-x 1))
+ new-val (+ (mf/ref-val drag-start-val*)
+ (* steps effective-step))
+ new-val (cond-> new-val
+ (d/num? min-value) (mth/max min-value)
+ (d/num? max-value) (mth/min max-value))]
+ (update-input new-val)
+ (apply-value event new-val))))))))
+
+ on-scrub-pointer-up
+ (mf/use-fn
+ (mf/deps ref)
+ (fn [event]
+ (let [state (mf/ref-val drag-state*)]
+ (when (= state :maybe-dragging)
+ (mf/set-ref-val! drag-state* :idle)
+ (dom/release-pointer event)
+ (when-let [node (mf/ref-val ref)]
+ (dom/focus! node)))
+ (when (= state :dragging)
+ (mf/set-ref-val! drag-state* :idle)
+ (dom/remove-class! (dom/get-body) "cursor-drag-scrub")
+ (dom/release-pointer event)))))
+
+ on-scrub-lost-pointer-capture
+ (mf/use-fn
+ (fn [_event]
+ (mf/set-ref-val! drag-state* :idle)
+ (dom/remove-class! (dom/get-body) "cursor-drag-scrub")))
props (-> (obj/clone props)
(obj/unset! "selectOnFocus")
@@ -241,7 +310,11 @@
(obj/set! "title" title)
(obj/set! "onKeyDown" handle-key-down)
(obj/set! "onBlur" handle-blur)
- (obj/set! "onFocus" handle-focus))]
+ (obj/set! "onFocus" handle-focus)
+ (obj/set! "onPointerDown" on-scrub-pointer-down)
+ (obj/set! "onPointerMove" on-scrub-pointer-move)
+ (obj/set! "onPointerUp" on-scrub-pointer-up)
+ (obj/set! "onLostPointerCapture" on-scrub-lost-pointer-capture))]
(mf/with-effect [value]
(when-let [input-node (mf/ref-val ref)]
diff --git a/frontend/src/app/main/ui/components/org_avatar.cljs b/frontend/src/app/main/ui/components/org_avatar.cljs
new file mode 100644
index 0000000000..43521a1dd7
--- /dev/null
+++ b/frontend/src/app/main/ui/components/org_avatar.cljs
@@ -0,0 +1,42 @@
+;; 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.components.org-avatar
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.data :as d]
+ [rumext.v2 :as mf]))
+
+(mf/defc org-avatar*
+ {::mf/props :obj}
+ [{:keys [org size]}]
+ (let [name (:name org)
+ custom-photo (:custom-photo org)
+ avatar-bg (:avatar-bg-url org)
+ initials (d/get-initials name)]
+
+ (if custom-photo
+ [:img {:src custom-photo
+ :class (stl/css-case :org-avatar true
+ :org-avatar-custom true
+ :org-avatar-xxxl (= size "xxxl")
+ :org-avatar-xxl (= size "xxl")
+ :org-avatar-xl (= size "xl"))
+ :alt name}]
+ [:div {:class (stl/css-case :org-avatar true
+ :org-avatar-xxxl (= size "xxxl")
+ :org-avatar-xxl (= size "xxl")
+ :org-avatar-xl (= size "xl"))
+ :aria-hidden "true"}
+ [:img {:src avatar-bg
+ :class (stl/css :org-avatar-bg)
+ :alt ""}]
+ (when (seq initials)
+ [:span {:class (stl/css-case :org-avatar-initials true
+ :size-initials-xxxl (= size "xxxl")
+ :size-initials-xxl (= size "xxl")
+ :size-initials-xxl (= size "xl"))} ;; Keep the initials as xxl to make them legible
+ initials])])))
diff --git a/frontend/src/app/main/ui/components/org_avatar.scss b/frontend/src/app/main/ui/components/org_avatar.scss
new file mode 100644
index 0000000000..b72591568b
--- /dev/null
+++ b/frontend/src/app/main/ui/components/org_avatar.scss
@@ -0,0 +1,63 @@
+// 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/typography.scss" as t;
+@use "ds/colors.scss" as *;
+
+.org-avatar {
+ position: relative;
+ border-radius: 50%;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.org-avatar-custom {
+ object-fit: cover;
+}
+
+.org-avatar-xxxl {
+ width: var(--sp-xxxl);
+ height: var(--sp-xxxl);
+}
+
+.org-avatar-xxl {
+ width: var(--sp-xxl);
+ height: var(--sp-xxl);
+}
+
+.org-avatar-xl {
+ width: var(--sp-xl);
+ height: var(--sp-xl);
+}
+
+.org-avatar-bg {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.org-avatar-initials {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ inset: 0;
+ color: #{$gray-950};
+}
+
+.size-initials-xxl {
+ @include t.use-typography("headline-small");
+
+ font-weight: 600;
+}
+
+.size-initials-xxxl {
+ @include t.use-typography("headline-medium");
+
+ font-weight: 600;
+}
diff --git a/frontend/src/app/main/ui/components/progress.scss b/frontend/src/app/main/ui/components/progress.scss
index 0ef02d0f17..646571f7f8 100644
--- a/frontend/src/app/main/ui/components/progress.scss
+++ b/frontend/src/app/main/ui/components/progress.scss
@@ -8,7 +8,8 @@
// PROGRESS WIDGET
.progress-widget {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-28;
height: deprecated.$s-28;
}
@@ -19,6 +20,7 @@
--export-modal-fg-color: var(--alert-text-foreground-color-default);
--export-modal-icon-color: var(--alert-icon-foreground-color-default);
--export-modal-border-color: var(--alert-border-color-default);
+
position: absolute;
right: deprecated.$s-16;
top: deprecated.$s-48;
@@ -41,13 +43,15 @@
--export-modal-fg-color: var(--alert-text-foreground-color-error);
--export-modal-icon-color: var(--alert-icon-foreground-color-error);
--export-modal-border-color: var(--alert-border-color-error);
+
grid-template-areas: "icon text close";
gap: deprecated.$s-8;
padding-block: deprecated.$s-8;
}
.icon {
- @extend .button-icon;
+ @extend %button-icon;
+
grid-area: icon;
align-self: center;
margin-inline-start: deprecated.$s-8;
@@ -55,7 +59,8 @@
}
.title {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
display: grid;
grid-template-columns: auto 1fr;
gap: deprecated.$s-8;
@@ -67,7 +72,8 @@
}
.progress {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
padding-left: deprecated.$s-8;
margin: 0;
align-self: center;
@@ -75,8 +81,9 @@
}
.retry-btn {
- @include deprecated.buttonStyle;
- @include deprecated.bodySmallTypography;
+ @include deprecated.button-style;
+ @include deprecated.body-small-typography;
+
display: inline;
text-align: left;
color: var(--modal-link-foreground-color);
@@ -85,13 +92,15 @@
}
.progress-close-button {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
padding: 0;
margin-inline-end: deprecated.$s-8;
}
.close-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--export-modal-icon-color);
}
diff --git a/frontend/src/app/main/ui/components/radio_buttons.scss b/frontend/src/app/main/ui/components/radio_buttons.scss
index 6ef73339ad..f8c06c4715 100644
--- a/frontend/src/app/main/ui/components/radio_buttons.scss
+++ b/frontend/src/app/main/ui/components/radio_buttons.scss
@@ -7,7 +7,8 @@
@use "refactor/common-refactor.scss" as deprecated;
.radio-btn-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
border-radius: deprecated.$br-8;
height: deprecated.$s-32;
background-color: var(--input-background-color);
@@ -17,9 +18,10 @@
.radio-icon {
--radio-icon-border-color: var(--radio-btn-border-color);
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
- @include deprecated.focusRadio;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+ @include deprecated.focus-radio;
+
height: deprecated.$s-32;
flex-grow: 1;
border-radius: deprecated.$s-8;
@@ -29,14 +31,19 @@
input {
display: none;
}
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--radio-btn-foreground-color);
}
+
.title-name {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--radio-btn-foreground-color);
}
+
&:hover {
svg {
stroke: var(--radio-btn-foreground-color-selected);
@@ -48,9 +55,11 @@
--radio-icon-border-color: var(--radio-btn-border-color-selected);
background-color: var(--radio-btn-background-color-selected);
+
svg {
stroke: var(--radio-btn-foreground-color-selected);
}
+
.title-name {
color: var(--radio-btn-foreground-color-selected);
}
@@ -60,18 +69,23 @@
cursor: default;
background-color: transparent;
border: deprecated.$s-2 solid transparent;
+
svg {
stroke: var(--button-foreground-color-disabled);
}
+
.title-name {
color: var(--button-foreground-color-disabled);
}
+
&:hover {
background-color: transparent;
border: deprecated.$s-2 solid transparent;
+
svg {
stroke: var(--button-foreground-color-disabled);
}
+
.title-name {
color: var(--button-foreground-color-disabled);
}
diff --git a/frontend/src/app/main/ui/components/reorder_handler.scss b/frontend/src/app/main/ui/components/reorder_handler.scss
index 499ff56ad5..8991efb661 100644
--- a/frontend/src/app/main/ui/components/reorder_handler.scss
+++ b/frontend/src/app/main/ui/components/reorder_handler.scss
@@ -20,6 +20,7 @@
block-size: var(--sp-l);
pointer-events: none;
visibility: var(--reorder-icon-visibility, hidden);
+
--icon-stroke-color: var(--color-foreground-secondary);
}
diff --git a/frontend/src/app/main/ui/components/search_bar.scss b/frontend/src/app/main/ui/components/search_bar.scss
index 96855005a1..22294f1d94 100644
--- a/frontend/src/app/main/ui/components/search_bar.scss
+++ b/frontend/src/app/main/ui/components/search_bar.scss
@@ -22,7 +22,8 @@
}
.search-input-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: 100%;
border: deprecated.$s-1 solid var(--search-bar-input-border-color);
@@ -32,6 +33,7 @@
&:hover {
border: deprecated.$s-1 solid var(--input-border-color-hover);
background-color: var(--input-background-color-hover);
+
.search-input {
background-color: var(--input-background-color-hover);
}
@@ -41,6 +43,7 @@
background-color: var(--input-background-color-active);
color: var(--input-foreground-color-active);
border: deprecated.$s-1 solid var(--input-border-color-focus);
+
.search-input {
background-color: var(--input-background-color-active);
}
@@ -56,13 +59,15 @@
font-size: deprecated.$fs-12;
color: var(--input-foreground-color);
border-radius: deprecated.$br-8;
+
&:focus {
outline: none;
}
}
.clear-icon {
- @extend .button-tag;
+ @extend %button-tag;
+
flex: 0 0 deprecated.$s-32;
height: 100%;
color: var(--color-icon-default);
diff --git a/frontend/src/app/main/ui/components/select.scss b/frontend/src/app/main/ui/components/select.scss
index ba01e42e08..e75034f6f3 100644
--- a/frontend/src/app/main/ui/components/select.scss
+++ b/frontend/src/app/main/ui/components/select.scss
@@ -11,8 +11,10 @@
--bg-color: var(--menu-background-color);
--icon-color: var(--icon-foreground);
--text-color: var(--menu-foreground-color);
- @extend .new-scrollbar;
- @include deprecated.bodySmallTypography;
+
+ @extend %new-scrollbar;
+ @include deprecated.body-small-typography;
+
position: relative;
display: grid;
grid-template-columns: 1fr auto;
@@ -48,32 +50,40 @@
--border-color: var(--menu-border-color-disabled);
--icon-color: var(--menu-foreground-color-disabled);
--text-color: var(--menu-foreground-color-disabled);
+
pointer-events: none;
cursor: default;
}
.dropdown-button {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
margin-inline-end: var(--sp-xxs);
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
transform: rotate(90deg);
stroke: var(--icon-color);
}
}
.current-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-24;
padding-right: deprecated.$s-4;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
.custom-select-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
.separator {
margin: 0;
height: deprecated.$s-12;
@@ -87,14 +97,18 @@
}
.checked-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
padding-right: deprecated.$s-4;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
@@ -105,9 +119,11 @@
}
.check-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
visibility: hidden;
stroke: var(--icon-foreground);
}
@@ -115,16 +131,18 @@
&.is-selected {
color: var(--menu-foreground-color);
+
.check-icon svg {
stroke: var(--menu-foreground-color);
visibility: visible;
}
}
+
&.disabled {
display: none;
}
}
.current-label {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
}
diff --git a/frontend/src/app/main/ui/components/tab_container.scss b/frontend/src/app/main/ui/components/tab_container.scss
index aab1d5ffdd..89c5692c55 100644
--- a/frontend/src/app/main/ui/components/tab_container.scss
+++ b/frontend/src/app/main/ui/components/tab_container.scss
@@ -31,7 +31,8 @@
}
.tab-container-tab-title {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: 100%;
padding: 0 deprecated.$s-8;
@@ -43,12 +44,14 @@
min-width: 0;
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--tab-foreground-color);
}
.content {
- @include deprecated.headlineSmallTypography;
+ @include deprecated.headline-small-typography;
+
text-align: center;
white-space: nowrap;
overflow: hidden;
@@ -76,8 +79,9 @@
}
.collapse-sidebar {
- @include deprecated.flexCenter;
- @include deprecated.buttonStyle;
+ @include deprecated.flex-center;
+ @include deprecated.button-style;
+
height: 100%;
width: deprecated.$s-24;
min-width: deprecated.$s-24;
@@ -85,7 +89,8 @@
border-radius: deprecated.$br-5;
svg {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
stroke: var(--icon-foreground);
@@ -109,13 +114,12 @@
}
.tab-container-content {
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
display: flex;
flex-direction: column;
}
-//Firefox doesn't respect scrollbar-gutter
+// Firefox doesn't respect scrollbar-gutter
@supports (-moz-appearance: none) {
.tab-container-content {
padding-right: deprecated.$s-8;
diff --git a/frontend/src/app/main/ui/components/title_bar.cljs b/frontend/src/app/main/ui/components/title_bar.cljs
index 432936b0b3..56c74696c3 100644
--- a/frontend/src/app/main/ui/components/title_bar.cljs
+++ b/frontend/src/app/main/ui/components/title_bar.cljs
@@ -13,30 +13,21 @@
(mf/defc title-bar*
[{:keys [class collapsable collapsed title children
- btn-icon btn-title all-clickable add-icon-gap
+ btn-icon btn-title add-icon-gap
title-class on-collapsed on-btn-click]}]
- [:div {:class [(stl/css-case :title-bar true
- :all-clickable all-clickable)
+ [:div {:class [(stl/css :title-bar)
class]}
(if ^boolean collapsable
[:div {:class [(stl/css :title-wrapper) title-class]}
(let [icon-id (if collapsed "arrow-right" "arrow-down")]
- (if ^boolean all-clickable
- [:button {:class (stl/css :icon-text-btn)
- :on-click on-collapsed}
- [:> icon* {:icon-id icon-id
- :size "s"
- :class (stl/css :icon)}]
- [:div {:class (stl/css :title)} title]]
- [:*
- [:button {:class (stl/css :icon-btn)
- :on-click on-collapsed}
- [:> icon* {:icon-id icon-id
- :size "s"
- :class (stl/css :icon)}]]
- [:div {:class (stl/css :title)} title]]))]
+ [:button {:class (stl/css :icon-text-btn)
+ :on-click on-collapsed}
+ [:> icon* {:icon-id icon-id
+ :size "s"
+ :class (stl/css :icon)}]
+ [:div {:class (stl/css :title)} title]])]
[:div {:class [(stl/css-case :title-only true
:title-only-icon-gap add-icon-gap)
diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss
index b4b5b84554..de605369bc 100644
--- a/frontend/src/app/main/ui/components/title_bar.scss
+++ b/frontend/src/app/main/ui/components/title_bar.scss
@@ -14,6 +14,7 @@
height: deprecated.$s-32;
width: 100%;
min-height: deprecated.$s-32;
+
--arrow-icon-color: var(--icon-foreground);
--title-color: var(--title-foreground-color);
}
@@ -32,12 +33,15 @@
.title {
@include t.use-typography("headline-small");
+
color: var(--title-color);
}
.title-only {
@include t.use-typography("headline-small");
+
--title-bar-title-margin: #{deprecated.$s-8};
+
color: var(--title-color);
margin-inline-start: var(--title-bar-title-margin);
}
@@ -63,7 +67,8 @@
}
.icon-text-btn {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
align-items: center;
flex-grow: 1;
@@ -75,12 +80,3 @@
--title-color: var(--title-foreground-color-hover);
}
}
-
-.icon-btn {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
-
- &:hover {
- --arrow-icon-color: var(--icon-foreground-hover);
- }
-}
diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs
index d2c068ebf2..522641e93c 100644
--- a/frontend/src/app/main/ui/confirm.cljs
+++ b/frontend/src/app/main/ui/confirm.cljs
@@ -30,10 +30,12 @@
on-accept
on-cancel
hint
+ error-msg
items
cancel-label
accept-label
- accept-style] :as props}]
+ accept-style
+ hint-level] :as props}]
(let [on-accept (or on-accept identity)
on-cancel (or on-cancel identity)
message (or message (tr "ds.confirm-title"))
@@ -83,9 +85,12 @@
(when (and (string? scd-message) (not= scd-message ""))
[:h3 {:class (stl/css :modal-scd-msg)} scd-message])
(when (string? hint)
- [:> context-notification* {:level :info
+ [:> context-notification* {:level (or hint-level :info)
:appearance :ghost}
hint])
+ (when (string? error-msg)
+ [:> context-notification* {:level :error :class (stl/css :modal-error-msg)}
+ error-msg])
(when (> (count items) 0)
[:*
[:p {:class (stl/css :modal-subtitle)}
diff --git a/frontend/src/app/main/ui/confirm.scss b/frontend/src/app/main/ui/confirm.scss
index 09b23426f3..0f14a6e305 100644
--- a/frontend/src/app/main/ui/confirm.scss
+++ b/frontend/src/app/main/ui/confirm.scss
@@ -7,21 +7,24 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
&.transparent {
background-color: transparent;
}
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
display: flex;
flex-direction: column;
gap: var(--sp-xxl);
}
.modal-title {
- @include deprecated.headlineMediumTypography;
+ @include deprecated.headline-medium-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -32,30 +35,37 @@
}
.modal-content {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
}
.modal-item-element {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
.modal-component-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
color: var(--color-foreground-secondary);
}
.modal-component-name {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--color-foreground-secondary);
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.modal-scd-msg,
.modal-subtitle,
.modal-msg {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
}
+
+.modal-error-msg {
+ margin: var(--sp-xxl) 0;
+}
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index 5962ecacf3..2625105d94 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -13,8 +13,10 @@
[app.main.data.dashboard.shortcuts :as sc]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
+ [app.main.data.nitrate :as dnt]
[app.main.data.notifications :as notif]
[app.main.data.plugins :as dp]
+ [app.main.data.profile :as dprof]
[app.main.data.project :as dpj]
[app.main.refs :as refs]
[app.main.router :as rt]
@@ -261,6 +263,14 @@
(binding [storage/*sync* true]
(swap! storage/session dissoc :template))))))
+(defn- use-nitrate-entry-popup
+ []
+ (mf/with-effect []
+ (when (dnt/nitrate-entry-popup-pending?)
+ (dnt/consume-nitrate-entry-popup!)
+ (st/emit! (dprof/update-profile-props {:onboarding-viewed true})
+ (dnt/show-nitrate-popup :nitrate-form)))))
+
(mf/defc dashboard*
[{:keys [profile project-id team-id search-term plugin-url template section]}]
(let [team (mf/deref refs/team)
@@ -299,6 +309,7 @@
(use-plugin-register plugin-url team-id (:id default-project))
(use-templates-import can-edit? template default-project)
+ (use-nitrate-entry-popup)
[:& (mf/provider ctx/current-project-id) {:value project-id}
[:> modal-container*]
diff --git a/frontend/src/app/main/ui/dashboard.scss b/frontend/src/app/main/ui/dashboard.scss
index 994f56a723..045d817316 100644
--- a/frontend/src/app/main/ui/dashboard.scss
+++ b/frontend/src/app/main/ui/dashboard.scss
@@ -7,7 +7,8 @@
@use "refactor/common-refactor.scss" as deprecated;
.dashboard {
- @extend .new-scrollbar;
+ @extend %new-scrollbar;
+
background-color: var(--app-background);
display: grid;
grid-template-columns: deprecated.$s-40 deprecated.$s-256 1fr;
diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs
index fc4ae33cc9..31bb3b3b0a 100644
--- a/frontend/src/app/main/ui/dashboard/change_owner.cljs
+++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs
@@ -7,11 +7,14 @@
(ns app.main.ui.dashboard.change-owner
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.common.schema :as sm]
+ [app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as deprecated-icon]
[app.util.i18n :as i18n :refer [tr]]
+ [cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private schema:leave-modal-form
@@ -72,3 +75,110 @@
:disabled (not (:valid @form))
:value (tr "modals.leave-and-reassign.promote-and-leave")
:on-click on-accept}]]]]]))
+
+
+
+(mf/defc ^:private team-member-select*
+ [{:keys [team profile form field-name default-member-id]}]
+ (let [members (get team :members)
+ filtered-members (->> members
+ (filter #(not= (:email %) (:email profile))))
+ options (->> filtered-members
+ (map #(hash-map :label (:name %) :value (str (:id %)))))]
+ [:div {:class (stl/css :team-select-container)}
+ [:div {:class (stl/css :team-name)} (:name team)]
+ (if (empty? filtered-members)
+ [:p {:class (stl/css :modal-msg)}
+ (tr "modals.leave-and-reassign.forbidden")]
+ [:& fm/select {:name field-name
+ :select-class (stl/css :team-member)
+ :dropdown-class (stl/css :team-member)
+ :options options
+ :form form
+ :default default-member-id}])]))
+
+(defn- make-leave-org-modal-form-schema [teams]
+ (into
+ [:map {:title "LeaveOrgModalForm"}]
+ (for [team teams]
+ [(keyword (str "member-id-" (:id team))) ::sm/text])))
+
+
+(mf/defc leave-and-reassign-org-modal
+ {::mf/register modal/components
+ ::mf/register-as :leave-and-reassign-org
+ ::mf/wrap [mf/memo]}
+ [{:keys [profile teams-to-transfer num-teams-to-delete accept] :as props}]
+ (let [schema (mf/with-memo [teams-to-transfer]
+ (make-leave-org-modal-form-schema teams-to-transfer))
+ ;; Compute initial values for each team select
+ team-fields (mf/with-memo [teams-to-transfer]
+ (for [team teams-to-transfer]
+ (let [members (get team :members)
+ filtered-members (filter #(not= (:email %) (:email profile)) members)
+ first-admin (first (filter :is-admin filtered-members))
+ first-member (first filtered-members)
+ default-member-id (cond
+ first-admin (str (:id first-admin))
+ first-member (str (:id first-member))
+ :else "")
+ field-name (keyword (str "member-id-" (:id team)))]
+ {:team team
+ :field-name field-name
+ :default-member-id default-member-id})))
+
+ initial-values (mf/with-memo [team-fields]
+ (d/index-by :field-name :default-member-id team-fields))
+
+ form (fm/use-form :schema schema :initial initial-values)
+
+ all-valid? (every?
+ (fn [{:keys [field-name]}]
+ (let [val (get-in @form [:clean-data field-name])]
+ (not (str/blank? val))))
+ team-fields)
+
+ on-accept (fn [_]
+ (let [teams-to-transfer (mapv (fn [{:keys [team field-name]}]
+ (let [val (get-in @form [:clean-data field-name])]
+ {:id (:id team)
+ :reassign-to (uuid/parse val)}))
+ team-fields)]
+ (accept {:teams-to-transfer teams-to-transfer})))]
+ [:div {:class (stl/css :modal-overlay)}
+ [:div {:class (stl/css :modal-org-container)}
+ [:div {:class (stl/css :modal-header)}
+ [:h2 {:class (stl/css :modal-org-title)} (tr "modals.before-leave-org.title")]
+ [:button {:class (stl/css :modal-close-btn)
+ :on-click modal/hide!} deprecated-icon/close]]
+
+ [:div {:class (stl/css :modal-content)}
+ (if (zero? num-teams-to-delete)
+ [:p {:class (stl/css :modal-org-msg)}
+ (tr "modals.leave-org-and-reassign.hint")]
+ [:*
+ [:p {:class (stl/css :modal-org-msg)}
+ (tr "modals.leave-org-and-reassign.hint-delete")]
+ [:p {:class (stl/css :modal-org-msg)}
+ (tr "modals.leave-org-and-reassign.hint-promote")]])
+ [:& fm/form {:form form}
+ [:div {:class (stl/css :teams-container)}
+ (for [{:keys [team field-name default-member-id]} team-fields]
+ ^{:key (:id team)}
+ [:> team-member-select* {:team team :profile profile :form form :field-name field-name :default-member-id default-member-id}])]]]
+
+ [:div {:class (stl/css :modal-footer)}
+ [:div {:class (stl/css :action-buttons)}
+ [:input {:class (stl/css :cancel-button)
+ :type "button"
+ :value (tr "labels.cancel")
+ :on-click modal/hide!}]
+
+ [:input.accept-button
+ {:type "button"
+ :class (stl/css-case :accept-btn true
+ :danger all-valid?
+ :global/disabled (not all-valid?))
+ :disabled (not all-valid?)
+ :value (tr "modals.leave-and-reassign.promote-and-leave")
+ :on-click on-accept}]]]]]))
diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss
index 40e8387274..13f5968eb8 100644
--- a/frontend/src/app/main/ui/dashboard/change_owner.scss
+++ b/frontend/src/app/main/ui/dashboard/change_owner.scss
@@ -5,13 +5,18 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
+@use "ds/typography.scss" as t;
+@use "ds/_sizes.scss" as *;
+@use "ds/z-index.scss" as *;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
+ z-index: var(--z-index-notifications);
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
}
.modal-header {
@@ -19,39 +24,85 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-bottom: deprecated.$s-24;
}
.input-wrapper {
- @extend .input-with-label;
- @include deprecated.bodySmallTypography;
+ @extend %input-with-label;
+ @include deprecated.body-small-typography;
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
.modal-msg {
color: var(--modal-text-foreground-color);
}
+
+.teams-container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-s);
+ margin: var(--sp-xxxl) 0;
+}
+
+.team-select-container {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ align-items: center;
+ width: 100%;
+}
+
+.modal-org-container {
+ @extend %modal-container-base;
+
+ overflow-y: auto;
+ max-height: $sz-512;
+}
+
+.modal-org-title {
+ @include t.use-typography("headline-large");
+
+ color: var(--modal-title-foreground-color);
+}
+
+.modal-org-msg {
+ @include t.use-typography("body-large");
+
+ color: var(--modal-text-foreground-color);
+}
+
+.team-name {
+ @include t.use-typography("body-medium");
+
+ color: var(--modal-text-foreground-color);
+}
+
+.team-member {
+ @include t.use-typography("body-medium");
+}
diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss
index c81fd1ee71..2d5948199c 100644
--- a/frontend/src/app/main/ui/dashboard/comments.scss
+++ b/frontend/src/app/main/ui/dashboard/comments.scss
@@ -7,7 +7,8 @@
@use "refactor/common-refactor.scss" as deprecated;
.dashboard-comments-section {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
position: relative;
border-radius: deprecated.$br-8;
}
@@ -35,7 +36,8 @@
}
.comments-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
height: deprecated.$s-24;
width: deprecated.$s-24;
@@ -44,25 +46,28 @@
.comment-button {
position: relative;
+
.unread {
position: absolute;
width: deprecated.$s-8;
height: deprecated.$s-8;
border: deprecated.$s-2 solid var(--color-background-tertiary);
border-radius: 50%;
- background: red;
+ background: var(--color-foreground-error);
top: deprecated.$s-6;
right: deprecated.$s-6;
}
}
.comments-icon-small {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--comment-icon-small-foreground-color);
}
.dropdown {
- @include deprecated.menuShadow;
+ @include deprecated.menu-shadow;
+
background-color: var(--color-background-tertiary);
border-radius: deprecated.$br-8;
border: deprecated.$s-1 solid transparent;
@@ -101,6 +106,7 @@
&:hover {
cursor: pointer;
}
+
&.mark-all-as-read-button {
border-radius: deprecated.$s-8;
border: deprecated.$s-1 solid;
diff --git a/frontend/src/app/main/ui/dashboard/deleted.scss b/frontend/src/app/main/ui/dashboard/deleted.scss
index 7187633722..69eeb9585d 100644
--- a/frontend/src/app/main/ui/dashboard/deleted.scss
+++ b/frontend/src/app/main/ui/dashboard/deleted.scss
@@ -29,9 +29,10 @@
.deleted-info {
display: block;
- height: fit-content;
color: var(--color-foreground-secondary);
+
@include t.use-typography("body-large");
+
line-height: 0.8;
height: var(--sp-xl);
}
@@ -64,7 +65,6 @@
.nav-option {
color: var(--color-foreground-secondary);
padding: 0.5rem;
-
display: flex;
align-items: center;
justify-content: center;
@@ -101,6 +101,7 @@
.project-name {
@include t.use-typography("body-large");
+
width: fit-content;
margin-inline-end: var(--sp-m);
line-height: 0.8;
@@ -116,7 +117,8 @@
.add-file-btn,
.options-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
@@ -131,6 +133,7 @@
.add-icon,
.menu-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss
index 838f8ea78c..39cfe4958b 100644
--- a/frontend/src/app/main/ui/dashboard/files.scss
+++ b/frontend/src/app/main/ui/dashboard/files.scss
@@ -20,6 +20,7 @@
&.dashboard-projects {
user-select: none;
}
+
&.dashboard-shared {
width: calc(100vw - deprecated.$s-320);
margin-right: deprecated.$s-52;
@@ -35,7 +36,8 @@
}
.menu-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs
index 72c57856b9..eaefe3925f 100644
--- a/frontend/src/app/main/ui/dashboard/fonts.cljs
+++ b/frontend/src/app/main/ui/dashboard/fonts.cljs
@@ -65,10 +65,9 @@
(mf/defc font-variant-display-name*
{::mf/private true}
[{:keys [variant]}]
- [:*
- [:span (cm/font-weight->name (:font-weight variant))]
- (when (not= "normal" (:font-style variant))
- [:span " " (str/capital (:font-style variant))])])
+ [:span (cm/font-display-variant (:variant-name variant)
+ (:font-weight variant)
+ (:font-style variant))])
(mf/defc uploaded-fonts*
{::mf/private true}
diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss
index f3d195b35b..f277fbe517 100644
--- a/frontend/src/app/main/ui/dashboard/fonts.scss
+++ b/frontend/src/app/main/ui/dashboard/fonts.scss
@@ -5,7 +5,6 @@
// Copyright (c) KALEIDOS INC
@use "common/refactor/common-dashboard";
-
@use "ds/_utils.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@@ -37,6 +36,7 @@
h3 {
@include t.use-typography("title-small");
+
color: var(--color-foreground-secondary);
margin: var(--sp-xs);
}
@@ -48,6 +48,7 @@
.installed-fonts-header {
@include t.use-typography("headline-small");
+
align-items: center;
color: var(--color-foreground-secondary);
display: flex;
@@ -55,7 +56,8 @@
padding-left: var(--sp-xxl);
> .family {
- @include twoLineTextEllipsis;
+ @include two-line-text-ellipsis;
+
min-width: $sz-200;
width: $sz-200;
}
@@ -72,8 +74,8 @@
input {
@include t.use-typography("body-medium");
+
background-color: var(--color-background-tertiary);
- border-color: transparent;
border-radius: $br-8;
border: $b-1 solid transparent;
color: var(--color-foreground-primary);
@@ -85,6 +87,7 @@
&:focus {
outline: $b-1 solid var(--color-accent-primary);
}
+
&::placeholder {
color: var(--color-foreground-secondary);
}
@@ -93,6 +96,7 @@
.font-item {
@include t.use-typography("body-medium");
+
align-items: center;
background-color: var(--color-background-tertiary);
border-radius: $br-4;
@@ -106,11 +110,11 @@
input {
@include t.use-typography("body-medium");
- @include textEllipsis;
+ @include text-ellipsis;
+
border: $b-1 solid transparent;
margin: 0;
padding: var(--sp-s);
-
background-color: var(--color-background-tertiary);
border-radius: $br-8;
color: var(--color-foreground-primary);
@@ -123,21 +127,25 @@
}
> .family {
- @include twoLineTextEllipsis;
+ @include two-line-text-ellipsis;
+
min-width: $sz-200;
width: $sz-200;
+
&.is-edition {
overflow: visible;
}
}
> .filenames {
- @include textEllipsis;
+ @include text-ellipsis;
+
min-width: $sz-200;
}
> .variants {
@include t.use-typography("body-medium");
+
display: flex;
flex-wrap: wrap;
flex-grow: 1;
@@ -151,12 +159,14 @@
padding: var(--sp-s) var(--sp-m);
cursor: pointer;
gap: var(--sp-xs);
+
.icon {
display: flex;
align-items: center;
justify-content: center;
height: $sz-16;
width: $sz-16;
+
svg {
fill: none;
width: $sz-12;
@@ -171,6 +181,7 @@
}
}
}
+
.inhert-variant {
cursor: default;
}
@@ -178,6 +189,7 @@
.table-field {
color: var(--color-foreground-primary);
+
.variant {
background-color: var(--color-background-quaternary);
border-radius: $br-8;
@@ -186,7 +198,8 @@
.filenames {
@include t.use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
min-width: $sz-400;
padding-left: var(--sp-xxxl);
}
@@ -203,6 +216,7 @@
margin-left: var(--sp-m);
justify-content: center;
align-items: center;
+
svg {
width: $sz-16;
height: $sz-16;
@@ -212,6 +226,7 @@
&.failure {
margin-right: var(--sp-m);
+
svg {
stroke: var(--element-foreground-warning);
}
@@ -220,6 +235,7 @@
&.close {
background: none;
border: none;
+
svg {
stroke: var(--color-foreground-secondary);
}
@@ -245,6 +261,7 @@
.dashboard-fonts-hero {
@include t.use-typography("body-medium");
+
padding: var(--sp-xxxl) 0;
margin-top: px2rem(80);
display: flex;
@@ -269,6 +286,7 @@
p {
@include t.use-typography("body-large");
+
color: var(--color-foreground-secondary);
}
}
@@ -299,6 +317,7 @@
.label {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-secondary);
}
}
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index 1396986e06..c1a813adc4 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -411,6 +411,7 @@
:ref node-ref
:role "button"
:title (:name file)
+ :aria-label (:name file)
:draggable (dm/str can-edit)
:on-click on-select
:on-key-down on-key-down
diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss
index e1aaef396a..394979b7e7 100644
--- a/frontend/src/app/main/ui/dashboard/grid.scss
+++ b/frontend/src/app/main/ui/dashboard/grid.scss
@@ -8,15 +8,13 @@
// TODO: Legacy sass variables. We should remove them in favor of DS tokens.
$bp-max-1366: "(max-width: 1366px)";
-
$thumbnail-default-width: deprecated.$s-252; // Default width
$thumbnail-default-height: deprecated.$s-168; // Default width
.dashboard-grid {
font-size: deprecated.$fs-14;
height: 100%;
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
padding: 0 var(--sp-l) deprecated.$s-16;
}
@@ -42,6 +40,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
width: 100%;
font-weight: deprecated.$fw400;
}
+
button {
background-color: transparent;
border: none;
@@ -108,7 +107,6 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
line-height: 1.92;
max-width: deprecated.$s-260;
overflow: hidden;
- padding-right: deprecated.$s-8;
padding: 0;
text-overflow: ellipsis;
white-space: nowrap;
@@ -126,9 +124,11 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
width: 100%;
white-space: nowrap;
max-width: deprecated.$s-260;
+
&::first-letter {
text-transform: capitalize;
}
+
@media #{$bp-max-1366} {
max-width: deprecated.$s-232;
}
@@ -198,9 +198,11 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
&:focus,
&:focus-within {
background-color: var(--color-background-tertiary);
+
.project-th-actions {
opacity: 1;
}
+
a {
text-decoration: none;
}
@@ -243,6 +245,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
margin-right: 0;
margin-top: deprecated.$s-20;
width: 100%;
+
--menu-icon-color: var(--button-tertiary-foreground-color-rest);
&:hover,
diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs
index 5f5dd533b8..7bb0f0b60f 100644
--- a/frontend/src/app/main/ui/dashboard/import.cljs
+++ b/frontend/src/app/main/ui/dashboard/import.cljs
@@ -194,13 +194,14 @@
(mf/defc import-entry*
{::mf/memo true
::mf/private true}
- [{:keys [entries entry edition can-be-deleted on-edit on-change on-delete]}]
+ [{:keys [entries entry edition can-be-deleted importing? on-edit on-change on-delete]}]
(let [status (:status entry)
;; FIXME: rename to format
format (:type entry)
loading? (or (= :analyze status)
- (= :import-progress status))
+ (= :import-progress status)
+ (and importing? (= :import-ready status)))
analyze-error? (= :analyze-error status)
import-success? (= :import-success status)
import-error? (= :import-error status)
@@ -293,7 +294,9 @@
import-error?
[:div {:class (stl/css :error-message)}
- (tr "labels.error")]
+ (if (some? (:error entry))
+ (tr (:error entry))
+ (tr "labels.error"))]
(and (not import-success?) (some? progress))
[:div {:class (stl/css :progress-message)} (parse-progress-message progress)])
@@ -489,7 +492,12 @@
[:ul {:class (stl/css :import-error-list)}
(for [entry entries]
(when (contains? #{:import-error :analyze-error} (:status entry))
- [:li {:class (stl/css :import-error-list-enry)} (:name entry)]))]
+ [:li {:class (stl/css :import-error-list-enry)
+ :key (dm/str (or (:file-id entry) (:uri entry) (:name entry)))}
+ [:div (:name entry)]
+ (when-let [err (:error entry)]
+ [:div {:class (stl/css :import-error-detail)}
+ (tr err)])]))]
[:div (tr "dashboard.import.import-error.message2")]]
(for [entry entries]
@@ -497,6 +505,7 @@
:key (dm/str (:uri entry) "/" (:file-id entry))
:entry entry
:entries entries
+ :importing? (= :import-progress status)
:on-edit on-edit
:on-change on-entry-change
:on-delete on-entry-delete
@@ -504,7 +513,13 @@
(when (some? template)
[:> import-entry* {:entry (assoc template :status status)
- :can-be-deleted false}])]
+ :can-be-deleted false}])
+
+ (when (= :import-progress status)
+ [:div {:class (stl/css :status-message)
+ :role "status"
+ :aria-live "polite"}
+ (tr "labels.uploading-file")])]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss
index cf7cea13ae..2d3cb22e67 100644
--- a/frontend/src/app/main/ui/dashboard/import.scss
+++ b/frontend/src/app/main/ui/dashboard/import.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
display: flex;
flex-direction: column;
}
@@ -21,19 +22,20 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
display: grid;
grid-template-columns: 1fr;
gap: deprecated.$s-16;
@@ -41,99 +43,140 @@
min-height: 40px;
}
+.status-message {
+ @include deprecated.body-small-typography;
+
+ color: var(--modal-title-foreground-color);
+ font-style: italic;
+}
+
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
+
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
.modal-scd-msg,
.modal-subtitle,
.modal-msg {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--modal-text-foreground-color);
line-height: 1.5;
}
.file-entry {
display: flex;
+
.file-name {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
.file-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
+
&.icon-fill svg {
fill: var(--icon-foreground);
}
}
+
.file-name-edit {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
flex-grow: 1;
}
+
.file-name-label {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
gap: deprecated.$s-12;
flex-grow: 1;
+
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
}
+
.edit-entry-buttons {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
width: deprecated.$s-28;
height: deprecated.$s-32;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
}
}
+
.error-message,
.progress-message {
display: flex;
align-items: center;
- height: deprecated.$s-32;
+ min-height: deprecated.$s-32;
color: var(--modal-text-foreground-color);
}
+ .error-message {
+ align-items: flex-start;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ }
+
.linked-library {
display: flex;
align-items: center;
gap: deprecated.$s-12;
color: var(--modal-text-foreground-color);
+
.linked-library-tag {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
+
&.error {
svg {
stroke: var(--element-foreground-error);
@@ -147,45 +190,57 @@
color: var(--modal-text-foreground-color);
}
}
+
&.warning {
.file-name {
color: var(--element-foreground-warning);
+
.file-icon svg {
stroke: var(--element-foreground-warning);
}
+
.file-icon.icon-fill svg {
fill: var(--element-foreground-warning);
}
}
}
+
&.success {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
+
.file-icon.icon-fill svg {
fill: var(--modal-text-foreground-color);
}
}
}
+
&.error {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
+
.file-icon.icon-fill svg {
fill: var(--modal-text-foreground-color);
}
}
}
+
&.editable {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
+
.file-icon.icon-fill svg {
fill: var(--modal-text-foreground-color);
}
@@ -209,3 +264,12 @@
.import-error-list-enry {
padding: var(--sp-xs) 0;
}
+
+.import-error-detail {
+ @include deprecated.body-small-typography;
+
+ margin-top: var(--sp-xs);
+ color: var(--modal-text-foreground-color);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.scss b/frontend/src/app/main/ui/dashboard/inline_edition.scss
index 4f62033011..d07e701fee 100644
--- a/frontend/src/app/main/ui/dashboard/inline_edition.scss
+++ b/frontend/src/app/main/ui/dashboard/inline_edition.scss
@@ -34,7 +34,6 @@ input.element-title {
.close {
cursor: pointer;
position: absolute;
-
top: deprecated.$s-1;
right: calc(-1 * deprecated.$s-8);
@@ -45,6 +44,7 @@ input.element-title {
width: deprecated.$s-16;
margin: 0;
}
+
&:hover {
svg {
fill: var(--element-foreground-warning);
diff --git a/frontend/src/app/main/ui/dashboard/placeholder.scss b/frontend/src/app/main/ui/dashboard/placeholder.scss
index ca47399436..6254535b58 100644
--- a/frontend/src/app/main/ui/dashboard/placeholder.scss
+++ b/frontend/src/app/main/ui/dashboard/placeholder.scss
@@ -14,7 +14,7 @@
padding: deprecated.$s-12 0;
&.libs {
- background-image: url(/images/ph-left.svg), url(/images/ph-right.svg);
+ background-image: url("/images/ph-left.svg"), url("/images/ph-right.svg");
background-position:
15% bottom,
85% top;
@@ -47,7 +47,6 @@
border-radius: deprecated.$br-8;
color: var(--color-foreground-primary);
cursor: pointer;
- height: deprecated.$s-160;
margin: deprecated.$s-8;
border: deprecated.$s-2 solid transparent;
width: var(--th-width, #{g.$thumbnail-default-width});
@@ -113,9 +112,11 @@
.empty-project-card {
@include t.use-typography("body-small");
+
--color-card-background: var(--color-background-tertiary);
--color-card-title: var(--color-foreground-primary);
--color-card-subtitle: var(--color-foreground-secondary);
+
display: flex;
flex-direction: column;
justify-content: center;
@@ -129,6 +130,7 @@
--color-card-background: var(--color-accent-primary);
--color-card-title: var(--color-background-secondary);
--color-card-subtitle: var(--color-background-secondary);
+
cursor: pointer;
.empty-project-card-title {
diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss
index a37575c38e..2616edcd49 100644
--- a/frontend/src/app/main/ui/dashboard/projects.scss
+++ b/frontend/src/app/main/ui/dashboard/projects.scss
@@ -41,6 +41,7 @@
.dashboard-project-row {
--actions-opacity: 0;
+
margin-block-end: var(--sp-xxl);
position: relative;
@@ -83,8 +84,9 @@
}
.project-name {
- @include textEllipsis;
+ @include text-ellipsis;
@include t.use-typography("body-large");
+
color: var(--title-foreground-color-hover);
cursor: pointer;
block-size: $sz-16;
@@ -101,8 +103,10 @@
.info,
.recent-files-row-title-info {
@include t.use-typography("body-medium");
+
color: var(--title-foreground-color);
- @media (max-width: 760px) {
+
+ @media (width <= 760px) {
display: none;
}
}
@@ -115,7 +119,8 @@
.add-file-btn,
.options-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
block-size: $sz-32;
inline-size: $sz-32;
margin: 0 var(--sp-s);
@@ -124,7 +129,8 @@
.add-icon,
.menu-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
@@ -139,7 +145,9 @@
.show-more {
--show-more-color: var(--button-secondary-foreground-color-rest);
+
@include t.use-typography("body-medium");
+
border: none;
background: none;
cursor: pointer;
@@ -178,7 +186,7 @@
border-radius: $br-4;
inline-size: auto;
- @media (max-width: 1200px) {
+ @media (width <= 1200px) {
display: none;
inline-size: 0;
}
@@ -201,18 +209,22 @@
.info {
flex: 1;
font-size: $sz-16;
+
span {
color: var(--color-foreground-secondary);
display: block;
}
+
a {
color: var(--color-accent-primary);
}
+
padding: var(--sp-s) 0;
}
.close {
--close-icon-foreground-color: var(--icon-foreground);
+
position: absolute;
top: var(--sp-xl);
inset-inline-end: var(--sp-xxl);
@@ -220,13 +232,15 @@
background-color: transparent;
border: none;
cursor: pointer;
+
&:hover {
--close-icon-foreground-color: var(--button-icon-foreground-color-selected);
}
}
.close-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--close-icon-foreground-color);
}
@@ -243,7 +257,8 @@
block-size: var(--sp-xl) 0;
overflow: hidden;
border-radius: $br-4;
- @media (max-width: 1200px) {
+
+ @media (width <= 1200px) {
display: none;
inline-size: 0;
}
diff --git a/frontend/src/app/main/ui/dashboard/search.scss b/frontend/src/app/main/ui/dashboard/search.scss
index c360ac61ac..f514fb4ba0 100644
--- a/frontend/src/app/main/ui/dashboard/search.scss
+++ b/frontend/src/app/main/ui/dashboard/search.scss
@@ -6,7 +6,7 @@
@use "refactor/common-refactor.scss" as deprecated;
@use "common/refactor/common-dashboard";
-@use "./placeholder.scss";
+@use "./placeholder";
.dashboard-container {
flex: 1 0 0;
@@ -18,6 +18,7 @@
&.dashboard-projects {
user-select: none;
}
+
&.dashboard-shared {
width: calc(100vw - deprecated.$s-320);
margin-right: deprecated.$s-52;
@@ -41,6 +42,7 @@
.text {
color: var(--color-foreground-primary);
}
+
.icon svg {
stroke: var(--color-foreground-secondary);
width: deprecated.$s-32;
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs
index 66604f3f73..84509976a4 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.cljs
+++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs
@@ -25,12 +25,14 @@
[app.main.ui.components.dropdown-menu :refer [dropdown-menu*
dropdown-menu-item*]]
[app.main.ui.components.link :refer [link]]
+ [app.main.ui.components.org-avatar :refer [org-avatar*]]
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.dashboard.subscription :refer [dashboard-cta*
get-subscription-type
menu-team-icon*
+ nitrate-current-plan*
nitrate-sidebar*
show-subscription-dashboard-banner?
subscription-sidebar*]]
@@ -72,6 +74,12 @@
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
+(def ^:private org-menu-icon
+ (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon)))
+
+(def ^:private org-menu-icon-open
+ (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon-open)))
+
(def ^:private pin-icon
(deprecated-icon/icon-xref :pin (stl/css :pin-icon)))
@@ -307,46 +315,47 @@
(mf/deps profile)
(fn []
(if (dnt/is-valid-license? profile)
- (dnt/go-to-nitrate-cc)
+ (dnt/go-to-nitrate-cc-create-org)
(st/emit! (dnt/show-nitrate-popup :nitrate-form)))))
on-go-to-cc-click
(mf/use-fn
- (mf/deps organization)
+ (mf/deps organization profile)
(fn []
- (dnt/go-to-nitrate-cc organization)))
+ ;; Navigate to active org if user owns it, otherwise to last visited org
+ (if (and (:id organization)
+ (= (:id profile) (:owner-id organization)))
+ (dnt/go-to-nitrate-cc organization)
+ (dnt/go-to-nitrate-cc))))
- default-team-id (or (->> organizations
- vals
- (filter :is-default)
- first
- :id)
+ empty-org (d/seek #(nil? (:id %)) organizations)
+ default-team-id (or (:default-team-id empty-org)
(:default-team-id profile))
- organizations (dissoc organizations default-team-id)]
+
+ organizations (filter :id organizations)
+
+ is-valid-license? (dnt/is-valid-license? profile)]
[:> dropdown-menu* props
[:> dropdown-menu-item* {:on-click on-org-click
:data-value default-team-id
:class (stl/css :org-dropdown-item)}
- [:span {:class (stl/css :nitrate-org-icon)}
+ [:span {:class (stl/css :org-icon)}
[:> raw-svg* {:id penpot-logo-icon}]]
"Penpot"
- (when (= default-team-id (:id organization))
+ (when (= default-team-id (:default-team-id organization))
tick-icon)]
- (for [org-item (remove :is-default (vals organizations))]
+ (for [org-item organizations]
[:> dropdown-menu-item* {:on-click on-org-click
- :data-value (:id org-item)
+ :data-value (:default-team-id org-item)
:class (stl/css :org-dropdown-item)
- :key (str (:id org-item))}
- ;; TODO org pictures
- [:img {:src (cf/resolve-team-photo-url org-item)
- :class (stl/css :team-picture)
- :alt (:name org-item)}]
+ :key (str (:default-team-id org-item))}
+ [:> org-avatar* {:org org-item :size "xxl"}]
[:span {:class (stl/css :team-text)
:title (:name org-item)} (:name org-item)]
- (when (= (:id org-item) (:id organization))
+ (when (= (:default-team-id org-item) (:default-team-id organization))
tick-icon)])
[:hr {:role "separator" :class (stl/css :team-separator)}]
@@ -354,10 +363,11 @@
:class (stl/css :org-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-org-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]]
- [:> dropdown-menu-item* {:on-click on-go-to-cc-click
- :class (stl/css :org-dropdown-item :action)}
- [:span {:class (stl/css :icon-wrapper)} arrow-up-right-icon]
- [:span {:class (stl/css :team-text)} (tr "dashboard.go-to-control-center")]]]))
+ (when is-valid-license?
+ [:> dropdown-menu-item* {:on-click on-go-to-cc-click
+ :class (stl/css :org-dropdown-item :action)}
+ [:span {:class (stl/css :icon-wrapper)} arrow-up-right-icon]
+ [:span {:class (stl/css :team-text)} (tr "dashboard.go-to-control-center")]])]))
(mf/defc teams-selector-dropdown*
{::mf/private true}
@@ -371,7 +381,13 @@
teams (dissoc teams default-team-id)
on-create-team-click
- (mf/use-fn #(st/emit! (modal/show :team-form {})))
+ (mf/use-fn
+ (mf/deps team)
+ (fn []
+ (let [params (if (and (contains? cf/flags :nitrate) (:organization-id team))
+ {:organization-id (:organization-id team)}
+ {})]
+ (st/emit! (modal/show :team-form params)))))
on-team-click
(mf/use-fn
@@ -383,12 +399,12 @@
[:> dropdown-menu* props
[:> dropdown-menu-item* {:on-click on-team-click
- :data-value (:default-team-id profile)
+ :data-value default-team-id
:class (stl/css :team-dropdown-item)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
- (when (= (:default-team-id profile) (:id team))
+ (when (= default-team-id (:id team))
tick-icon)]
(for [team-item (remove :is-default (vals teams))]
@@ -438,18 +454,22 @@
(modal/hide))))
on-error
- (fn [{:keys [code] :as error}]
- (condp = code
- :no-enough-members-for-leave
- (rx/of (ntf/error (tr "errors.team-leave.insufficient-members")))
+ (fn [error]
+ (let [code (-> error ex-data :code)]
+ (condp = code
+ :only-owner-can-delete-team
+ (rx/of (ntf/error (tr "errors.team-leave.only-owner-can-delete")))
- :member-does-not-exist
- (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists")))
+ :no-enough-members-for-leave
+ (rx/of (ntf/error (tr "errors.team-leave.insufficient-members")))
- :owner-cant-leave-team
- (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave")))
+ :member-does-not-exist
+ (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists")))
- (rx/throw error)))
+ :owner-cant-leave-team
+ (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave")))
+
+ (rx/throw error))))
leave-fn
(mf/use-fn
@@ -505,14 +525,19 @@
on-delete-clicked
(mf/use-fn
- (mf/deps delete-fn)
- #(st/emit!
- (modal/show
- {:type :confirm
- :title (tr "modals.delete-team-confirm.title")
- :message (tr "modals.delete-team-confirm.message")
- :accept-label (tr "modals.delete-team-confirm.accept")
- :on-accept delete-fn})))]
+ (mf/deps team delete-fn)
+ (fn []
+ (let [is-org-team? (some? (:organization-id team))
+ message (if is-org-team?
+ (tr "modals.delete-org-team-confirm.message" (:organization-name team))
+ (tr "modals.delete-team-confirm.message"))]
+ (st/emit!
+ (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-team-confirm.title")
+ :message message
+ :accept-label (tr "modals.delete-team-confirm.accept")
+ :on-accept delete-fn})))))]
[:> dropdown-menu* props
[:> dropdown-menu-item* {:on-click go-members
@@ -565,10 +590,108 @@
:data-testid "delete-team"}
(tr "dashboard.delete-team")])]))
+(mf/defc org-options-dropdown*
+ {::mf/private true}
+ [{:keys [organization profile teams] :rest props}]
+ (let [default-team-id (mf/with-memo [teams]
+ (->> teams
+ (filter :is-default)
+ first
+ :id))
+ non-default-teams (mf/with-memo [teams]
+ (remove :is-default teams))
+ owned-teams (mf/with-memo [non-default-teams]
+ (filter #(dm/get-in % [:permissions :is-owner]) non-default-teams))
+ not-owned-teams (mf/with-memo [non-default-teams]
+ (remove #(dm/get-in % [:permissions :is-owner]) non-default-teams))
+ teams-to-delete (mf/with-memo [owned-teams]
+ (filter #(= (count (:members %)) 1) owned-teams))
+ teams-to-transfer (mf/with-memo [owned-teams]
+ (filter #(> (count (:members %)) 1) owned-teams))
+ num-teams-to-leave (+ (count teams-to-transfer) (count not-owned-teams))
+ num-teams-to-delete (count teams-to-delete)
+ num-teams-to-transfer (count teams-to-transfer)
+
+ on-error
+ (mf/use-fn
+ (fn [error]
+ (let [code (-> error ex-data :code)
+ ;; Map error codes to their translation keys
+ error-map {:not-valid-teams "errors.org-leave.no-valid-teams"
+ :org-owner-cannot-leave "errors.org-leave.org-owner-cannot-leave"
+ :only-owner-can-delete-team "errors.team-leave.only-owner-can-delete"
+ :no-enough-members-for-leave "errors.team-leave.insufficient-members"
+ :member-does-not-exist "errors.team-leave.member-does-not-exists"
+ :owner-cant-leave-team "errors.team-leave.owner-cant-leave"}]
+
+ (if-let [tr-key (get error-map code)]
+ (rx/of (dtm/fetch-teams)
+ (modal/hide)
+ (ntf/error (tr tr-key)))
+ (rx/throw error)))))
+
+ leave-fn
+ (mf/use-fn
+ (mf/deps on-error organization default-team-id not-owned-teams teams-to-delete)
+ (fn [{:keys [teams-to-transfer]}]
+ (let [teams-to-leave (cond->> not-owned-teams
+ :always
+ (map #(select-keys % [:id]))
+ (seq teams-to-transfer)
+ (concat teams-to-transfer))
+ teams-to-delete (map :id teams-to-delete)]
+
+
+ (st/emit! (dnt/leave-org {:id (:id organization)
+ :name (:name organization)
+ :default-team-id default-team-id
+ :teams-to-delete teams-to-delete
+ :teams-to-leave teams-to-leave
+ :on-error on-error})))))
+
+ on-leave-clicked
+ (mf/use-fn
+ (mf/deps leave-fn profile organization teams-to-transfer num-teams-to-leave num-teams-to-delete num-teams-to-transfer)
+ (fn []
+ (cond
+ (and (pos? num-teams-to-delete)
+ (zero? num-teams-to-transfer))
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "modals.before-leave-org.title" (:name organization))
+ :message (tr "modals.before-leave-org.message")
+ :accept-label (tr "modals.leave-org-confirm.accept")
+ :on-accept leave-fn
+ :error-msg (tr "modals.before-leave-org.warning")}))
+ (pos? num-teams-to-transfer)
+ (st/emit!
+ (modal/show
+ {:type :leave-and-reassign-org
+ :profile profile
+ :teams-to-transfer teams-to-transfer
+ :num-teams-to-delete num-teams-to-delete
+ :accept leave-fn}))
+
+ :else
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "modals.leave-org-confirm.title" (:name organization))
+ :message (tr "modals.leave-org-confirm.message")
+ :accept-label (tr "modals.leave-org-confirm.accept")
+ :on-accept leave-fn})))))]
+ (mf/use-effect
+ (fn []
+ ;; We need all the team members of the owned teams
+ ;; TODO this will re-render once for each owned team, not very performance-wise
+ (do
+ (doseq [team owned-teams]
+ (st/emit! (dtm/fetch-members (:id team)))))))
+ [:> dropdown-menu* props
+
+ [:> dropdown-menu-item* {:on-click on-leave-clicked
+ :class (stl/css :team-options-item)}
+ (tr "dashboard.leave-org")]]))
-(defn- team->org [team]
- (assoc (dm/select-keys team [:id :organization-id :organization-slug])
- :name (:organization-name team)))
(mf/defc sidebar-org-switch*
[{:keys [team profile]}]
@@ -579,14 +702,23 @@
(->> teams
vals
(filter :is-default)
- (map team->org)
+ (map dtm/team->organization)
(d/index-by :id)))
- no-orgs? (= (count orgs) 0)
+ show-dropdown? (or (dnt/is-valid-license? profile)
+ (> (count orgs) 1))
- current-org (team->org team)
+ current-org (dtm/team->organization team)
- default-org? (= (:default-team-id profile) (:id current-org))
+ org-teams (mf/with-memo [teams current-org]
+ (->> teams
+ vals
+ (filter #(= (:organization-id %) (:id current-org)))))
+
+ default-org? (nil? (:id current-org))
+
+ show-options? (and (not default-org?)
+ (not= (:id profile) (:owner-id current-org)))
show-orgs-menu*
(mf/use-state false)
@@ -594,6 +726,21 @@
show-orgs-menu?
(deref show-orgs-menu*)
+ show-org-options-menu*
+ (mf/use-state false)
+
+ show-org-options-menu?
+ (deref show-org-options-menu*)
+
+ on-show-options-click
+ (mf/use-fn
+ (fn [event]
+ (dom/stop-propagation event)
+ (swap! show-org-options-menu* not)))
+
+ close-org-options-menu
+ (mf/use-fn #(reset! show-org-options-menu* false))
+
on-show-orgs-click
(mf/use-fn
(fn [event]
@@ -617,41 +764,35 @@
(mf/deps profile)
(fn []
(if (dnt/is-valid-license? profile)
- (dnt/go-to-nitrate-cc)
+ (dnt/go-to-nitrate-cc-create-org)
(st/emit! (dnt/show-nitrate-popup :nitrate-form)))))]
- (if no-orgs?
- [:div {:class (stl/css :nitrate-selected-org)}
- [:span {:class (stl/css :nitrate-penpot-icon)}
- [:> raw-svg* {:id penpot-logo-icon}]]
- "Penpot"
- [:> button* {:variant "ghost"
- :type "button"
- :class (stl/css :nitrate-create-org)
- :on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]]
-
+ (if show-dropdown?
[:div {:class (stl/css :sidebar-org-switch)}
+ [:div {:class (stl/css :org-switch-content)}
+ [:button {:class (stl/css-case :current-org true :current-org-no-options (not show-options?))
+ :on-click on-show-orgs-click
+ :on-key-down on-show-orgs-keydown
+ :aria-expanded show-orgs-menu?
+ :aria-haspopup "menu"}
+ [:div {:class (stl/css :team-name)}
+ (if default-org?
+ [:*
+ [:span {:class (stl/css :org-penpot-icon)}
+ [:> raw-svg* {:id penpot-logo-icon}]]
+ [:span {:class (stl/css :team-text)}
+ "Penpot"]]
+ [:*
+ [:> org-avatar* {:org current-org :size "xxxl"}]
+ [:span {:class (stl/css :team-text)}
+ (:name current-org)]])]
+ arrow-icon]
+ (when show-options?
+ [:> button* {:variant "ghost"
+ :type "button"
+ :class (stl/css :org-options-btn)
+ :on-click on-show-options-click}
+ (if show-org-options-menu? org-menu-icon-open org-menu-icon)])]
- [:button {:class (stl/css :current-org)
- :on-click on-show-orgs-click
- :on-key-down on-show-orgs-keydown
- :aria-expanded show-orgs-menu?
- :aria-haspopup "menu"}
- [:div {:class (stl/css :team-name)}
- (if default-org?
- [:*
- [:span {:class (stl/css :nitrate-penpot-icon)}
- [:> raw-svg* {:id penpot-logo-icon}]]
- [:span {:class (stl/css :team-text)}
- "Penpot"]]
- [:*
- [:span {:class (stl/css :nitrate-penpot-icon)}
- ;; TODO org pictures
- [:img {:src (cf/resolve-team-photo-url current-org)
- :class (stl/css :team-picture)
- :alt (:name current-org)}]]
- [:span {:class (stl/css :team-text)}
- (:name current-org)]])]
- arrow-icon]
;; Orgs Dropdown
[:> organizations-selector-dropdown* {:show show-orgs-menu?
@@ -660,15 +801,31 @@
:class (stl/css :dropdown :teams-dropdown)
:organization current-org
:profile profile
- :organizations orgs}]])))
+ :organizations (vals orgs)}]
+ ;; Orgs options
+ [:> org-options-dropdown* {:show show-org-options-menu?
+ :on-close close-org-options-menu
+ :id "team-options"
+ :class (stl/css :dropdown :options-dropdown)
+ :organization current-org
+ :profile profile
+ :teams org-teams}]]
+ [:div {:class (stl/css :selected-org)}
+ [:span {:class (stl/css :org-penpot-icon)}
+ [:> raw-svg* {:id penpot-logo-icon}]]
+ "Penpot"
+ [:> button* {:variant "ghost"
+ :type "button"
+ :class (stl/css :create-org)
+ :on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]])))
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
(let [nitrate? (contains? cf/flags :nitrate)
- org-id (when nitrate? (:organization-id team))
+ organization-id (when nitrate? (:organization-id team))
teams (cond->> (mf/deref refs/teams)
nitrate?
- (filter #(= (-> % val :organization-id) org-id))
+ (filter #(= (-> % val :organization-id) organization-id))
nitrate?
(into {}))
@@ -900,11 +1057,12 @@
(reset! overflow* (> scroll-height client-height))))
[:*
- [:div {:ref container}
+ [:div {:class (stl/css :sidebar-content-wrapper)}
(when nitrate?
- [:div {:class (stl/css :nitrate-orgs-container)}
+ [:div {:class (stl/css :orgs-container)}
[:> sidebar-org-switch* {:team team :profile profile}]])
- [:div {:class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)}
+ [:div {:ref container
+ :class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)}
[:> sidebar-team-switch* {:team team :profile profile}]
[:> sidebar-search* {:search-term search-term
@@ -1161,14 +1319,21 @@
(st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" ::ev/origin "dashboard" :section "sidebar"}))
(dom/open-new-window "https://penpot.app/pricing")))]
+ (mf/with-effect [show-profile-menu?]
+ (when-not show-profile-menu?
+ (reset! sub-menu* nil)))
+
[:*
(if (contains? cf/flags :nitrate)
- [:> nitrate-sidebar* {:profile profile :teams teams}]
+ [:*
+ [:> nitrate-sidebar* {:profile profile :teams teams}]
+ [:> nitrate-current-plan* {:profile profile}]]
(when (contains? cf/flags :subscriptions)
(if (show-subscription-dashboard-banner? profile)
[:> dashboard-cta* {:profile profile}]
[:> subscription-sidebar* {:profile profile}])))
+
;; TODO remove this block when subscriptions is full implemented
(when (contains? cf/flags :subscriptions-old)
[:button {:class (stl/css :upgrade-plan-section)
diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss
index 7b83394abe..51b3b929a2 100644
--- a/frontend/src/app/main/ui/dashboard/sidebar.scss
+++ b/frontend/src/app/main/ui/dashboard/sidebar.scss
@@ -28,15 +28,23 @@
background-color: var(--panel-background-color);
}
-//SIDEBAR CONTENT COMPONENT
+// SIDEBAR CONTENT COMPONENT
+.sidebar-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ height: 100%;
+ overflow: hidden;
+}
+
.sidebar-content {
display: grid;
grid-template-rows: auto auto auto auto 1fr;
gap: var(--sp-xxl);
- height: 100%;
+ flex: 1;
+ min-height: 0;
padding: 0;
- overflow-x: hidden;
- overflow-y: auto;
+ overflow: hidden auto;
}
.sidebar-content-nitrate {
@@ -56,6 +64,7 @@
.sidebar-section-title {
@include t.use-typography("headline-small");
+
padding: 0 var(--sp-s) var(--sp-s) var(--sp-xxl);
color: var(--color-foreground-secondary);
}
@@ -78,7 +87,8 @@
}
.current-team {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
@@ -96,8 +106,9 @@
}
.team-text {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
@include t.use-typography("title-small");
+
width: auto;
text-align: left;
color: var(--menu-foreground-color-hover);
@@ -112,7 +123,7 @@
// This icon still use the old svg
.penpot-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
svg {
fill: var(--icon-foreground);
@@ -122,21 +133,24 @@
}
.team-picture {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
border-radius: 50%;
height: var(--sp-xxl);
width: var(--sp-xxl);
}
.arrow-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
.switch-options {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
max-width: var(--sp-xxl);
min-width: deprecated.$s-28;
height: 100%;
@@ -145,26 +159,28 @@
}
.menu-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
// DROPDOWNS
.teams-dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
left: 0;
top: deprecated.$s-52;
height: fit-content;
max-height: $sz-480;
min-width: deprecated.$s-248;
width: 100%;
- overflow-x: hidden;
- overflow-y: auto;
+ overflow: hidden auto;
}
.team-dropdown-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
+
display: grid;
grid-template-columns: var(--sp-xxl) 1fr auto;
gap: var(--sp-s);
@@ -172,7 +188,8 @@
}
.org-dropdown-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
+
display: grid;
grid-template-columns: var(--sp-xxxl) 1fr auto;
gap: var(--sp-s);
@@ -190,7 +207,8 @@
}
.icon-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: var(--sp-xxl);
height: var(--sp-xxl);
margin-right: var(--sp-m);
@@ -199,7 +217,8 @@
}
.add-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
width: var(--sp-xxl);
height: var(--sp-xxl);
stroke: var(--sidebar-action-icon-color);
@@ -211,12 +230,14 @@
}
.tick-icon {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
.options-dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
right: var(--sp-xxs);
top: deprecated.$s-52;
max-height: $sz-480;
@@ -227,7 +248,8 @@
}
.team-options-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
+
height: $sz-40;
}
@@ -241,7 +263,6 @@
.sidebar-nav {
margin: 0;
user-select: none;
- overflow: none;
}
.pinned-projects {
@@ -288,7 +309,8 @@
}
.element-title {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
width: deprecated.$s-256;
color: var(--color-foreground-primary);
font-size: deprecated.$fs-14;
@@ -304,7 +326,8 @@
}
.pin-icon {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
margin: 0 var(--sp-m);
}
@@ -328,6 +351,7 @@
.input-text {
@include t.use-typography("title-small");
+
height: $sz-40;
width: 100%;
padding: $sz-6 var(--sp-m);
@@ -350,8 +374,9 @@
}
.search-btn {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
position: absolute;
right: 0;
height: var(--sp-xxl);
@@ -361,8 +386,10 @@
.search-icon,
.clear-search-btn {
- @extend .button-icon;
+ @extend %button-icon;
+
--sidebar-search-foreground-color: var(--search-bar-icon-foreground-color);
+
stroke: var(--sidebar-search-foreground-color);
}
@@ -382,7 +409,8 @@
}
.profile {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: grid;
grid-template-columns: auto 1fr;
gap: var(--sp-s);
@@ -392,7 +420,8 @@
.profile-fullname {
@include t.use-typography("title-small");
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
align-self: center;
max-width: var(--sp-l) 0;
color: var(--profile-foreground-color);
@@ -405,16 +434,19 @@
}
.profile-dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
inset-inline-start: var(--sp-s);
inset-block-end: px2rem(72); // 72 is the height of the profile button
min-width: calc(100% - var(--sp-s));
+
// TODO ADD animation fadeInUp
}
.profile-dropdown-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
@include t.use-typography("body-medium");
+
block-size: $sz-40;
margin-block-end: var(--sp-xs);
padding: var(--sp-s);
@@ -430,22 +462,29 @@
}
.profile-dropdown-item .open-arrow {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
}
.profile-dropdown-item .open-arrow svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
.sub-menu {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
inset-inline-start: calc(deprecated.$s-292 + var(--sp-s));
min-width: deprecated.$s-192;
}
+// Each submenu is positioned via its bottom edge; the visual top lands
+// at `inset-block-end + submenu_height`. Help & Learning (3 items,
+// taller) needs the same inset as Community (2 items, shorter) so that
+// its top edge sits one row above Community — aligning with the
+// "Help & Learning" trigger row in the parent menu.
.sub-menu.help-learning {
- inset-block-end: deprecated.$s-72;
+ inset-block-end: deprecated.$s-120;
}
.sub-menu.community {
@@ -457,8 +496,9 @@
}
.submenu-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
@include t.use-typography("body-medium");
+
block-size: $sz-40;
margin-block-end: var(--sp-xs);
padding-block: var(--sp-s);
@@ -477,7 +517,8 @@
.menu-version {
@include t.use-typography("code-font");
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
color: var(--color-foreground-secondary);
margin-inline-start: var(--sp-s);
text-transform: uppercase;
@@ -495,26 +536,30 @@
}
.exit-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
.add-org-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
width: var(--sp-l);
height: var(--sp-l);
stroke: var(--sidebar-action-icon-color);
}
.arrow-up-right-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
width: var(--sp-m);
height: var(--sp-m);
stroke: var(--sidebar-action-icon-color);
}
.upgrade-plan-section {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
justify-content: space-between;
border: $b-1 solid var(--color-background-quaternary);
@@ -527,6 +572,7 @@
.penpot-free {
@include t.use-typography("body-medium");
+
display: flex;
flex-direction: column;
text-align: left;
@@ -538,35 +584,36 @@
.power-up {
@include t.use-typography("body-small");
+
color: var(--color-accent-tertiary);
}
-.nitrate-orgs-container {
+.orgs-container {
align-items: center;
display: flex;
height: calc(2 * var(--sp-xxxl));
max-height: calc(2 * var(--sp-xxxl));
justify-content: space-between;
- padding: var(--sp-xs) var(--sp-l) var(--sp-xs) var(--sp-s);
- // border-block-end: $b-1 solid var(--color-background-quaternary);
+ padding: 0 var(--sp-xl);
}
-.nitrate-selected-org {
+.selected-org {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-primary);
width: 100%;
- margin: var(--sp-xs) 0 var(--sp-xs) var(--sp-l);
+ padding-inline-start: var(--sp-s);
display: flex;
align-items: center;
gap: var(--sp-s);
}
-.nitrate-create-org {
+.create-org {
margin-inline-start: auto;
text-transform: uppercase;
}
-.nitrate-penpot-icon {
+.org-penpot-icon {
display: flex;
justify-content: center;
align-items: center;
@@ -582,7 +629,7 @@
}
}
-.nitrate-org-icon {
+.org-icon {
display: flex;
justify-content: center;
align-items: center;
@@ -604,12 +651,64 @@
}
.current-org {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
+ text-transform: none;
display: grid;
align-items: center;
- grid-template-columns: 1fr auto;
+ grid-template-columns: 1fr auto auto;
gap: var(--sp-s);
height: 100%;
width: 100%;
- padding: 0 var(--sp-m);
+}
+
+.current-org-no-options {
+ gap: 0;
+}
+
+.current-org .arrow-icon {
+ margin-inline-end: var(--sp-xs);
+}
+
+.org-options {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ max-width: var(--sp-xxl);
+ min-width: $sz-28;
+ height: 100%;
+}
+
+.org-switch-content {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ height: $sz-48;
+ width: 100%;
+}
+
+.org-options-btn {
+ --icon-stroke: var(--icon-foreground);
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: $sz-32;
+ height: $sz-32;
+
+ &:hover {
+ --icon-stroke: var(--color-accent-primary);
+ }
+}
+
+.org-menu-icon {
+ @extend %button-icon;
+
+ stroke: var(--icon-stroke);
+}
+
+.org-menu-icon-open {
+ @extend %button-icon;
+
+ stroke: var(--color-accent-primary);
}
diff --git a/frontend/src/app/main/ui/dashboard/subscription.cljs b/frontend/src/app/main/ui/dashboard/subscription.cljs
index a07dbe0649..86dcba36fd 100644
--- a/frontend/src/app/main/ui/dashboard/subscription.cljs
+++ b/frontend/src/app/main/ui/dashboard/subscription.cljs
@@ -120,6 +120,8 @@
(mf/defc nitrate-sidebar*
[{:keys [profile teams]}]
(let [nitrate? (dnt/is-valid-license? profile)
+ nitrate-license (:subscription profile)
+ subscription-type (if nitrate? (:type nitrate-license) (get-subscription-type (-> profile :props :subscription)))
orgs (mf/with-memo [teams]
(let [orgs (->> teams
vals
@@ -133,8 +135,14 @@
handle-click
(mf/use-fn
+ (mf/deps nitrate-license subscription-type)
(fn []
- (st/emit! (dnt/show-nitrate-popup :nitrate-form))))]
+ (if (= subscription-type "unlimited")
+ (st/emit! (dnt/show-nitrate-popup :nitrate-dialog {:nitrate-license nitrate-license :show-contact-sales-option true}))
+ (st/emit! (dnt/show-nitrate-popup :nitrate-form)))))
+
+ handle-go-to-cc
+ (mf/use-fn dnt/go-to-nitrate-cc-create-org)]
;; TODO add translations for this texts when we have the definitive ones
(if (and nitrate? no-orgs-created?)
@@ -147,7 +155,7 @@
[:> button* {:variant "primary"
:type "button"
:class (stl/css :nitrate-bottom-button)
- :on-click dnt/go-to-nitrate-cc} "CREATE ORGANIZATION"]]]
+ :on-click handle-go-to-cc} "CREATE ORGANIZATION"]]]
;; Banner for users without nitrate license
(when (not nitrate?)
@@ -159,7 +167,30 @@
[:> button* {:variant "primary"
:type "button"
:class (stl/css :nitrate-bottom-button)
- :on-click handle-click} "UPGRADE TO NITRATE"]]]))))
+ :on-click handle-click} (if (:subscription profile)
+ "UPGRADE TO NITRATE"
+ "Try 14 days for free")]]]))))
+
+(mf/defc nitrate-current-plan*
+ [{:keys [profile]}]
+ (let [nitrate? (dnt/is-valid-license? profile)
+ nitrate-license (:subscription profile)
+ subscription (-> profile :props :subscription)
+ subscription-type (if nitrate? (:type nitrate-license) (get-subscription-type subscription))
+ subscription-is-trial (= "trialing" (:status (if nitrate? nitrate-license subscription)))]
+ [:div {:class (stl/css :nitrate-current-plan)}
+ [:div {:class (stl/css :nitrate-current-plan-label)}
+ (tr "subscription.current-plan.title")]
+ [:div {:class (stl/css :nitrate-current-plan-text)}
+ (case subscription-type
+ "professional" (tr "subscription.current-plan.professional")
+ "unlimited" (if subscription-is-trial
+ (tr "subscription.current-plan.unlimited-trial")
+ (tr "subscription.current-plan.unlimited"))
+ "nitrate" (if subscription-is-trial
+ (tr "subscription.current-plan.nitrate-trial")
+ (tr "subscription.current-plan.nitrate"))
+ "enterprise" (tr "subscription.current-plan.enterprise"))]]))
(mf/defc team*
[{:keys [is-owner team]}]
diff --git a/frontend/src/app/main/ui/dashboard/subscription.scss b/frontend/src/app/main/ui/dashboard/subscription.scss
index e9439558f1..b37cff4658 100644
--- a/frontend/src/app/main/ui/dashboard/subscription.scss
+++ b/frontend/src/app/main/ui/dashboard/subscription.scss
@@ -26,7 +26,8 @@
}
.cta-top-section {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: grid;
color: var(--color-foreground-secondary);
grid-template-columns: 1fr auto;
@@ -43,13 +44,15 @@
}
.icon-dropdown {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: var(--sp-l);
}
.icon-dropdown svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
transform: rotate(90deg);
}
@@ -67,7 +70,8 @@
.cta-bottom-section .content {
@include t.use-typography("body-medium");
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
color: var(--color-foreground-secondary);
display: inline-block;
text-align: left;
@@ -88,11 +92,13 @@
.cta-title {
@include t.use-typography("body-small");
+
margin-block-end: var(--sp-xs);
}
.highlighted .cta-title {
@include t.use-typography("body-medium");
+
margin-block-end: 0;
}
@@ -102,17 +108,20 @@
.highlighted .cta-text {
@include t.use-typography("body-large");
+
color: var(--color-foreground-primary);
}
.cta-bottom-section .content a {
@include t.use-typography("body-medium");
+
color: var(--color-accent-tertiary);
margin-inline-start: var(--sp-xs);
}
.cta-link {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
align-self: end;
margin-inline-start: var(--sp-xs);
}
@@ -127,17 +136,20 @@
.team-label {
@include t.use-typography("headline-small");
+
color: var(--title-foreground-color);
}
.team-text {
@include t.use-typography("title-medium");
+
color: var(--color-foreground-primary);
}
.manage-subscription-link {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
@include t.use-typography("body-medium");
+
color: var(--color-accent-tertiary);
display: flex;
margin-block-start: -8px;
@@ -161,7 +173,8 @@
}
.menu-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
+
cursor: pointer;
&:hover {
@@ -197,6 +210,7 @@
.cta-message {
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
line-height: 1;
@@ -210,7 +224,7 @@
display: flex;
border-radius: var(--sp-s);
flex-direction: column;
- margin: var(--sp-m);
+ margin: var(--sp-m) var(--sp-m) 0;
background: var(--color-background-quaternary);
border: $b-1 solid var(--color-accent-primary-muted);
padding: var(--sp-l);
@@ -218,11 +232,13 @@
.nitrate-title {
@include t.use-typography("body-large");
+
color: var(--color-foreground-primary);
}
.nitrate-info {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-secondary);
margin-block: var(--sp-s) var(--sp-xxl);
}
@@ -235,3 +251,24 @@
.nitrate-bottom-button {
width: fit-content;
}
+
+.nitrate-current-plan {
+ border-radius: var(--sp-s);
+ margin: var(--sp-m);
+ background: var(--color-background-tertiary);
+ border: $b-1 solid var(--color-background-quaternary);
+ padding: var(--sp-m) var(--sp-l);
+}
+
+.nitrate-current-plan-label {
+ @include t.use-typography("body-small");
+
+ padding-block-end: var(--sp-xs);
+ color: var(--color-foreground-secondary);
+}
+
+.nitrate-current-plan-text {
+ @include t.use-typography("body-medium");
+
+ color: var(--color-foreground-primary);
+}
diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs
index e492b9a20d..dd3bbfb2de 100644
--- a/frontend/src/app/main/ui/dashboard/team.cljs
+++ b/frontend/src/app/main/ui/dashboard/team.cljs
@@ -14,6 +14,7 @@
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
+ [app.main.data.nitrate :as dnt]
[app.main.data.notifications :as ntf]
[app.main.data.team :as dtm]
[app.main.refs :as refs]
@@ -21,6 +22,7 @@
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
+ [app.main.ui.components.org-avatar :refer [org-avatar*]]
[app.main.ui.dashboard.change-owner]
[app.main.ui.dashboard.subscription :refer [members-cta*
show-subscription-members-banner?
@@ -28,12 +30,15 @@
[app.main.ui.dashboard.team-form]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.badge :refer [badge-notification]]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.dom :as dom]
+ [app.util.forms :as uforms]
[app.util.i18n :as i18n :refer [tr]]
+ [app.util.timers :as tm]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -44,6 +49,9 @@
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
+(def ^:private org-menu-icon
+ (deprecated-icon/icon-xref :menu (stl/css :org-menu-icon)))
+
(def ^:private warning-icon
(deprecated-icon/icon-xref :msg-warning (stl/css :warning-icon)))
@@ -539,7 +547,7 @@
(tr "dashboard.your-penpot")
(:name team)))))
- (mf/with-effect []
+ (mf/with-effect [team]
(st/emit! (dtm/fetch-members)))
[:*
@@ -606,7 +614,10 @@
(mf/defc invitation-actions*
{::mf/private true}
[{:keys [invitation team-id]}]
- (let [email (:email invitation)
+ (let [email (:email invitation)
+ copied* (mf/use-state false)
+ copied? (deref copied*)
+
on-error
(mf/use-fn
(mf/deps email)
@@ -632,6 +643,8 @@
on-copy-success
(mf/use-fn
(fn []
+ (reset! copied* true)
+ (tm/schedule 1000 #(reset! copied* false))
(st/emit! (ntf/success (tr "notifications.invitation-link-copied"))
(modal/hide))))
@@ -649,7 +662,7 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.copy-invitation-link")
:on-click on-copy
- :icon "clipboard"}]))
+ :icon (if copied? "tick" "clipboard")}]))
(mf/defc invitation-row*
{::mf/wrap [mf/memo]
@@ -785,6 +798,83 @@
(tr "labels.continue")
(tr "labels.resend"))]]]]])
+
+(def schema:organization-form [:map {:title "SelectOrgForm"}
+ [:selected-id ::sm/uuid]])
+
+(mf/defc render-org-combobox-avatar*
+ [{:keys [avatar]}]
+ [:> org-avatar* {:org (:organization avatar)
+ :size (:size avatar)}])
+
+(mf/defc select-organization-modal
+ {::mf/register modal/components
+ ::mf/register-as :select-organization-modal}
+ [{:keys [organizations current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key]}]
+ (let [valid-organizations (mf/with-memo [organizations]
+ (remove #(= (:id %) current-organization-id) organizations))
+ options (mf/with-memo [valid-organizations]
+ (mapv (fn [organization]
+ {:id (str (:id organization))
+ :label (:name organization)
+ :avatar {:render-fn render-org-combobox-avatar*
+ :organization organization
+ :size "xl"}})
+ valid-organizations))
+
+ form (fm/use-form :schema schema:organization-form :initial {})
+
+ on-change
+ (mf/use-fn
+ (mf/deps form)
+ (fn [id]
+ (uforms/on-input-change form :selected-id id)))
+
+ on-confirm'
+ (mf/use-fn
+ (mf/deps on-confirm form)
+ (fn []
+ (on-confirm (dm/get-in @form [:clean-data :selected-id]))))]
+ [:div {:class (stl/css :modal-overlay)}
+ [:div {:class (stl/css :modal-select-org-container :modal-container)}
+ [:div {:class (stl/css :modal-header)}
+ [:h2 {:class (stl/css :modal-select-org-title)}
+ (tr title-key)]
+
+ [:button {:class (stl/css :modal-close-btn)
+ :on-click modal/hide!} deprecated-icon/close]]
+
+ (when text-key
+ [:div {:class (stl/css :modal-content :modal-select-org-text)} (tr text-key)])
+
+ [:div
+ [:div {:class (stl/css :modal-select-org-content)}
+ (tr choose-key)]
+ [:> combobox* {:id "selected-id"
+ :class (stl/css :team-member)
+ :options options
+ :select-only true
+ :default-selected (or (some-> (get-in @form [:data :selected-id]) str) "")
+ :placeholder (tr placeholder-key)
+ :on-change on-change}]]
+
+ [:div {:class (stl/css :modal-footer)}
+ [:div {:class (stl/css :action-buttons :modal-invitation-action-buttons)}
+
+ [:> button*
+ {:class (stl/css :cancel-button)
+ :variant "secondary"
+ :type "button"
+ :on-click modal/hide!}
+ (tr cancel-key)]
+ [:> button*
+ {:class (stl/css :accept-btn)
+ :variant "primary"
+ :type "button"
+ :disabled (not (:valid @form))
+ :on-click on-confirm'}
+ (tr accept-key)]]]]]))
+
(mf/defc invitation-section*
{::mf/private true}
[{:keys [team]}]
@@ -979,7 +1069,7 @@
(tr "dashboard.your-penpot")
(:name team)))))
- (mf/with-effect []
+ (mf/with-effect [(:id team) (:members team)]
(st/emit! (dtm/fetch-invitations)))
[:*
@@ -1264,7 +1354,8 @@
(mf/defc team-settings-page*
[{:keys [team]}]
- (let [finput (mf/use-ref)
+ (let [nitrate? (contains? cfg/flags :nitrate)
+ finput (mf/use-ref)
members (get team :members)
stats (get team :stats)
@@ -1275,12 +1366,99 @@
can-edit (or (:is-owner permissions)
(:is-admin permissions))
+ organizations (mf/deref refs/teams)
+ organizations (mf/with-memo [organizations]
+ (->> (vals organizations)
+ (filter :is-default)
+ (filter :organization-id)
+ (map dtm/team->organization)))
+
+ can-change-organization? (mf/with-memo [organizations]
+ (> (count organizations) 1))
+
+ can-add-to-organization? (mf/with-memo [organizations]
+ (and (pos? (count organizations))
+ (not (:is-default team))))
+
+ show-org-options-menu*
+ (mf/use-state false)
+
+ show-org-options-menu?
+ (deref show-org-options-menu*)
+
+ on-show-options-click
+ (mf/use-fn
+ (fn [event]
+ (dom/stop-propagation event)
+ (swap! show-org-options-menu* not)))
+
+ close-org-options-menu
+ (mf/use-fn #(reset! show-org-options-menu* false))
+
on-image-click
(mf/use-fn #(dom/click (mf/ref-val finput)))
on-file-selected
(fn [file]
- (st/emit! (dtm/update-team-photo file)))]
+ (st/emit! (dtm/update-team-photo file)))
+
+ remove-team-from-org-fn
+ (mf/use-fn
+ (mf/deps team)
+ (fn []
+ (st/emit! (dnt/remove-team-from-org {:team-id (:id team)
+ :organization-id (:organization-id team)
+ :organization-name (:organization-name team)}))))
+
+ on-remove-team-from-org
+ (mf/use-fn
+ (mf/deps team)
+ (fn []
+ (let [params {:type :confirm
+ :title (tr "modals.remove-team-org.title")
+ :message (tr "modals.remove-team-org.text" (:name team) (:organization-name team))
+ :hint (tr "modals.remove-team-org.info")
+ :hint-level :default
+ :accept-label (tr "modals.remove-team-org.accept")
+ :on-accept remove-team-from-org-fn
+ :accept-style :danger}]
+ (st/emit! (modal/show params)))))
+
+ on-add-team-to-org-confirm
+ (mf/use-fn
+ (mf/deps team)
+ (fn [organization-id]
+ (let [organization (d/seek #(= organization-id (:id %)) organizations)]
+ (when organization
+ (st/emit! (dnt/add-team-to-org {:team-id (:id team)
+ :organization-id organization-id}))))))
+
+ on-add-team-to-org
+ (mf/use-fn
+ (mf/deps organizations on-add-team-to-org-confirm)
+ (fn []
+ (st/emit! (modal/show :select-organization-modal {:organizations organizations
+ :current-organization-id (:organization-id team)
+ :on-confirm on-add-team-to-org-confirm
+ :title-key "dashboard.select-org-modal.title"
+ :choose-key "dashboard.select-org-modal.choose"
+ :placeholder-key "dashboard.select-org-modal.select"
+ :accept-key "dashboard.select-org-modal.accept"
+ :cancel-key "labels.cancel"}))))
+
+ on-change-team-org
+ (mf/use-fn
+ (mf/deps organizations on-add-team-to-org-confirm)
+ (fn []
+ (st/emit! (modal/show :select-organization-modal {:organizations organizations
+ :current-organization-id (:organization-id team)
+ :on-confirm on-add-team-to-org-confirm
+ :title-key "dashboard.change-org-modal.title"
+ :text-key "dashboard.change-org-modal.text"
+ :choose-key "dashboard.change-org-modal.choose"
+ :placeholder-key "dashboard.change-org-modal.select"
+ :accept-key "dashboard.change-org-modal.accept"
+ :cancel-key "labels.cancel"}))))]
(mf/with-effect [team]
(dom/set-html-title (tr "title.team-settings"
@@ -1314,6 +1492,44 @@
[:div {:class (stl/css :block-text)}
(:name team)]]
+ (when nitrate?
+ [:div {:class (stl/css :block)}
+ [:div {:class (stl/css :block-label)}
+ (tr "dashboard.team-organization")]
+ (if (:organization-id team)
+ [:div {:class (stl/css :block-content)}
+ [:div {:class (stl/css :org-block-content)}
+ [:> org-avatar* {:org (dtm/team->organization team) :size "xxxl"}]
+ [:span {:class (stl/css :block-text)}
+ (:organization-name team)]
+
+ (when (and (:is-owner permissions) (not (:is-default team)))
+ [:*
+ [:> button* {:variant "ghost"
+ :type "button"
+ :class (stl/css-case :org-options-btn (not show-org-options-menu?) :org-options-btn-open show-org-options-menu?)
+ :on-click on-show-options-click}
+ org-menu-icon
+
+ [:& dropdown {:show show-org-options-menu? :on-close close-org-options-menu :dropdown-id "org-options"}
+ [:ul {:class (stl/css :org-dropdown)
+ :role "listbox"}
+ (when can-change-organization?
+ [:li {:on-click on-change-team-org
+ :class (stl/css :org-dropdown-item)}
+ (tr "dashboard.team-organization.change")])
+ [:li {:on-click on-remove-team-from-org
+ :class (stl/css :org-dropdown-item)}
+ (tr "dashboard.team-organization.remove")]]]]])]]
+ [:*
+ [:div {:class (stl/css :block-content)}
+ [:span {:class (stl/css :block-text)}
+ (tr "dashboard.team-organization.none")]]
+ (when can-add-to-organization?
+ [:div {:class (stl/css :block-content)}
+ [:span {:class (stl/css :block-text)}
+ [:a {:on-click on-add-team-to-org} (tr "dashboard.team-organization.add")]]])])])
+
[:div {:class (stl/css :block)}
[:div {:class (stl/css :block-label)}
(tr "dashboard.team-members")]
diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss
index 259fdeb565..d68bbd26c9 100644
--- a/frontend/src/app/main/ui/dashboard/team.scss
+++ b/frontend/src/app/main/ui/dashboard/team.scss
@@ -45,11 +45,13 @@
.block-label {
@include t.use-typography("headline-small");
+
color: var(--color-foreground-secondary);
}
.block-text {
color: var(--color-foreground-primary);
+ text-wrap: nowrap;
}
.block-content {
@@ -82,6 +84,7 @@
.team-icon {
--update-button-opacity: 0;
+
position: relative;
height: $sz-120;
width: $sz-120;
@@ -162,6 +165,7 @@
.table-header {
@include t.use-typography("headline-small");
+
display: grid;
align-items: center;
grid-template-columns: 43% 1fr px2rem(108) var(--sp-m);
@@ -245,12 +249,13 @@
.member-name,
.member-email {
- @include textEllipsis;
+ @include text-ellipsis;
@include t.use-typography("body-large");
}
.member-email {
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
}
@@ -262,6 +267,7 @@
// ROL INFO
.rol-selector {
@include t.use-typography("body-medium");
+
position: relative;
display: grid;
grid-template-columns: 1fr auto;
@@ -303,6 +309,7 @@
.rol-dropdown-item {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
justify-content: space-between;
@@ -311,6 +318,7 @@
padding: px2rem(6);
border-radius: $br-8;
cursor: pointer;
+
&:hover {
background-color: var(--color-background-quaternary);
}
@@ -337,7 +345,8 @@
.input-checkbox {
// TODO: remove this extended class.
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
cursor: pointer;
}
@@ -363,6 +372,7 @@
.action-dropdown-item {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
justify-content: space-between;
@@ -371,6 +381,7 @@
padding: px2rem(6);
border-radius: $br-8;
cursor: pointer;
+
&:hover {
background-color: var(--color-background-quaternary);
}
@@ -399,6 +410,7 @@
.invitations-actions {
@include t.use-typography("body-medium");
+
display: flex;
justify-content: end;
align-items: center;
@@ -432,7 +444,8 @@
.btn-empty-invitations {
// TODO: Remove this extend add DS component
- @extend .button-primary;
+ @extend %button-primary;
+
margin-block-start: var(--sp-l);
padding-inline: var(--sp-m);
}
@@ -451,8 +464,9 @@
}
.field-email {
- @include textEllipsis;
+ @include text-ellipsis;
@include t.use-typography("body-large");
+
display: flex;
gap: var(--sp-l);
align-items: center;
@@ -499,10 +513,10 @@
.webhooks-hero {
@include t.use-typography("body-medium");
+
display: grid;
grid-template-rows: auto 1fr auto;
gap: var(--sp-xxxl);
- margin-top: var(--sp-xxxl);
margin: 0;
padding: var(--sp-xxxl);
padding: 0;
@@ -511,19 +525,22 @@
.hero-title {
@include t.use-typography("title-large");
+
color: var(--color-foreground-primary);
}
.hero-desc {
@include t.use-typography("body-large");
+
color: var(--color-foreground-secondary);
margin-bottom: 0;
max-width: $sz-512;
}
.hero-btn {
- //TODO: Remove this extended class using a DS component
- @extend .button-primary;
+ // TODO: Remove this extended class using a DS component
+ @extend %button-primary;
+
height: $sz-32;
max-width: $sz-512;
}
@@ -572,6 +589,7 @@
.webhook-dropdown-item {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
justify-content: space-between;
@@ -580,6 +598,7 @@
padding: px2rem(6);
border-radius: $br-8;
cursor: pointer;
+
&:hover {
background-color: var(--color-background-quaternary);
}
@@ -611,10 +630,7 @@
// INVITE MEMBERS MODAL
.modal-team-container {
- position: relative;
- padding: var(--sp-xxxl);
border-radius: $br-8;
- background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
min-width: $sz-364;
min-height: $sz-192;
@@ -643,6 +659,7 @@
.modal-title {
@include t.use-typography("headline-medium");
+
height: $sz-32;
color: var(--color-foreground-primary);
}
@@ -669,12 +686,14 @@
.invite-team-member-text {
@include t.use-typography("body-large");
+
margin: 0 0 var(--sp-l) 0;
color: var(--color-foreground-primary);
}
.role-title {
@include t.use-typography("body-large");
+
margin: 0;
color: var(--color-foreground-primary);
}
@@ -691,7 +710,7 @@
.accept-btn {
// TODO: remove this extend class creating a modal component
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
}
// WEBHOOKS MODAL
@@ -727,16 +746,18 @@
.modal-title {
@include t.use-typography("title-small");
+
color: var(--color-foreground-primary);
}
.modal-close-btn {
// TODO remove extended class creating a modal component
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
@include t.use-typography("body-small");
+
display: flex;
flex-direction: column;
gap: var(--sp-xxl);
@@ -751,6 +772,7 @@
.select-title {
@include t.use-typography("body-small");
+
color: var(--color-foreground-primary);
}
@@ -764,28 +786,31 @@
// TODO: Remove this extended classes creating a modal component
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
button {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
}
// TODO: Remove this extended class using input component
.email-input {
@include t.use-typography("body-small");
- @extend .input-base;
+ @extend %input-base;
+
height: auto;
}
+
// FIXME: This does not conform to our CSS Guidelines. Need to unnest and to use
// custom properties to handle state changes.
.input-wrapper {
display: flex;
align-items: center;
+
@include t.use-typography("body-large");
label {
@@ -801,6 +826,7 @@
border-color: var(--color-accent-primary);
}
}
+
&:hover {
span {
border-color: var(--color-accent-primary-muted);
@@ -809,13 +835,17 @@
}
span {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
}
+
input {
margin: 0;
+
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
}
}
@@ -840,3 +870,117 @@
margin-block-start: var(--sp-xxxl);
gap: var(--sp-s);
}
+
+// SELECT ORGANIZATION MODAL
+
+.modal-select-org-container {
+ display: flex;
+ flex-direction: column;
+ width: $sz-512;
+}
+
+.modal-select-org-content {
+ @include t.use-typography("body-large");
+
+ color: var(--color-foreground-secondary);
+ overflow: auto;
+ margin-block-end: var(--sp-s);
+}
+
+.modal-select-org-title {
+ @include t.use-typography("title-medium");
+
+ color: var(--color-foreground-primary);
+ text-transform: uppercase;
+ height: $sz-40;
+}
+
+.modal-select-org-text {
+ @include t.use-typography("body-large");
+
+ color: var(--color-foreground-secondary);
+}
+
+// ORGANIZATIONS SETTINGS
+
+.org-block-content {
+ display: grid;
+ grid-template-columns: var(--sp-xxxl) 1fr var(--sp-xxxl);
+ align-items: center;
+ gap: var(--sp-m);
+ width: max-content;
+}
+
+.org-options-btn {
+ padding: 0;
+ justify-content: center;
+
+ --stroke-color: var(--color-foreground-primary);
+
+ &:hover {
+ --stroke-color: var(--color-accent-primary);
+ }
+}
+
+.org-options-btn-open {
+ padding: 0;
+ justify-content: center;
+
+ --stroke-color: var(--color-accent-primary);
+
+ background-color: var(--color-background-tertiary);
+ position: relative;
+}
+
+.org-menu-icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: $sz-16;
+ width: $sz-16;
+ color: transparent;
+ fill: none;
+ stroke-width: $b-1;
+ stroke: var(--stroke-color);
+}
+
+.org-dropdown {
+ box-shadow: var(--el-shadow-dark);
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+ position: absolute;
+ padding: var(--sp-xs);
+ border-radius: $br-8;
+ z-index: var(--z-index-dropdown);
+ color: var(--color-foreground-primary);
+ background-color: var(--color-background-tertiary);
+ border: $b-2 solid var(--color-background-quaternary);
+ margin: 0;
+ top: var(--sp-xxxl);
+ width: fit-content;
+ min-width: $sz-160;
+}
+
+.org-dropdown-item {
+ @include t.use-typography("body-small");
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: $sz-28;
+ width: 100%;
+ padding: px2rem(6);
+ border-radius: $br-8;
+ cursor: pointer;
+ text-transform: none;
+ white-space: nowrap;
+
+ &:hover {
+ background-color: var(--color-background-quaternary);
+ }
+}
+
+a {
+ color: var(--modal-link-foreground-color);
+}
diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs
index a2ef4d1490..9a6240ce8d 100644
--- a/frontend/src/app/main/ui/dashboard/team_form.cljs
+++ b/frontend/src/app/main/ui/dashboard/team_form.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.dashboard.team-form
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.schema :as sm]
[app.common.types.team :as ctt]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
@@ -24,7 +25,8 @@
(def ^:private schema:team-form
[:map {:title "TeamForm"}
- [:name ctt/schema:team-name]])
+ [:name ctt/schema:team-name]
+ [:organization-id {:optional true} [:maybe ::sm/uuid]]])
(defn- on-create-success
[_form response]
@@ -50,7 +52,9 @@
[form]
(let [mdata {:on-success (partial on-create-success form)
:on-error (partial on-error form)}
- params {:name (get-in @form [:clean-data :name])}]
+ data (:clean-data @form)
+ params (cond-> {:name (:name data)}
+ (:organization-id data) (assoc :organization-id (:organization-id data)))]
(st/emit! (-> (dtm/create-team (with-meta params mdata))
(with-meta {::ev/origin :dashboard})))))
@@ -58,7 +62,8 @@
[form]
(let [mdata {:on-success (partial on-update-success form)
:on-error (partial on-error form)}
- team (get @form :clean-data)]
+ data (:clean-data @form)
+ team (select-keys data [:id :name])] ;; Only send name and id for updates
(st/emit! (dtm/update-team (with-meta team mdata))
(modal/hide))))
@@ -72,10 +77,16 @@
(mf/defc team-form-modal
{::mf/register modal/components
::mf/register-as :team-form}
- [{:keys [team] :as props}]
- (let [initial (mf/use-memo (fn []
- (or (some-> team (select-keys [:name :id]))
- {})))
+ [{:keys [team organization-id] :as props}]
+ (let [initial (mf/use-memo
+ (mf/deps team organization-id)
+ (fn []
+ (if team
+ ;; For existing teams, only include name and id (no organization changes)
+ (select-keys team [:name :id])
+ ;; For new teams, include organization-id if provided
+ (cond-> {}
+ organization-id (assoc :organization-id organization-id)))))
form (fm/use-form :schema schema:team-form
:initial initial)
handle-keydown
diff --git a/frontend/src/app/main/ui/dashboard/team_form.scss b/frontend/src/app/main/ui/dashboard/team_form.scss
index eba9c361d0..592ca7a94d 100644
--- a/frontend/src/app/main/ui/dashboard/team_form.scss
+++ b/frontend/src/app/main/ui/dashboard/team_form.scss
@@ -7,11 +7,11 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
}
.modal-header {
@@ -19,12 +19,13 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
@@ -36,12 +37,15 @@
}
.group-name-input {
- @extend .input-element-label;
- @include deprecated.bodySmallTypography;
+ @extend %input-element-label;
+ @include deprecated.body-small-typography;
+
margin-bottom: deprecated.$s-8;
+
label {
- @include deprecated.flexColumn;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-small-typography;
+
align-items: flex-start;
width: 100%;
border: none;
@@ -49,21 +53,23 @@
height: 100%;
input {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
}
}
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
+
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss
index f3323c58f2..6b4b44600d 100644
--- a/frontend/src/app/main/ui/dashboard/templates.scss
+++ b/frontend/src/app/main/ui/dashboard/templates.scss
@@ -20,11 +20,10 @@
flex-direction: column;
height: px2rem(244);
justify-content: flex-end;
- margin-inline-start: px2rem(6);
- margin-inline-end: px2rem(6);
- margin-block-end: px2rem(6);
+ margin-inline: var(--sp-s);
+ margin-block-end: var(--sp-xs);
position: absolute;
- transition: bottom 300ms;
+ transition: inset-block-end 300ms;
width: calc(100% - $sz-12);
pointer-events: none;
z-index: var(--z-index-panels);
@@ -32,11 +31,13 @@
&.collapsed {
inset-block-end: calc(-1 * px2rem(228));
background-color: transparent;
- transition: bottom 300ms;
+ transition: inset-block-end 300ms;
+
.title-btn {
border-end-end-radius: $br-8;
border-end-start-radius: $br-8;
}
+
.content,
.content-description {
visibility: hidden;
@@ -69,26 +70,24 @@
.title-text {
@include t.use-typography("body-large");
+
display: inline-block;
vertical-align: middle;
- margin-inline-start: var(--sp-m);
- margin-inline-end: var(--sp-s);
+ margin-inline: var(--sp-m) var(--sp-s);
color: var(--color-foreground-primary);
}
.title-icon-container {
display: inline-block;
vertical-align: middle;
- margin-inline-start: auto;
- margin-inline-end: var(--sp-s);
+ margin-inline: auto var(--sp-s);
color: var(--color-foreground-primary);
}
.title-icon {
display: inline-block;
vertical-align: middle;
- margin-inline-start: auto;
- margin-inline-end: var(--sp-s);
+ margin-inline: auto var(--sp-s);
transform: rotate(90deg);
}
@@ -130,6 +129,7 @@
&:hover {
border: $b-2 solid var(--color-background-tertiary);
background-color: var(--color-accent-primary);
+
.arrow-icon {
stroke: var(--color-background-tertiary);
}
@@ -149,9 +149,9 @@
.content-description {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-primary);
- margin-block-end: calc(-1 * var(--sp-s));
- margin-block-start: var(--sp-l);
+ margin-block: var(--sp-l) calc(-1 * var(--sp-s));
margin-inline-start: var(--sp-l);
visibility: visible;
}
@@ -182,6 +182,7 @@
.template-card {
@include t.use-typography("body-large");
+
display: inline-block;
width: px2rem(256);
cursor: pointer;
@@ -189,12 +190,15 @@
padding: 0 var(--sp-xs) var(--sp-s) var(--sp-xs);
border-radius: $br-8;
border: $b-2 solid transparent;
+
&:hover {
text-decoration: none;
border-color: var(--color-accent-primary);
+
.download-icon {
stroke: var(--color-accent-primary);
}
+
.card-text {
color: var(--color-accent-primary);
}
@@ -205,7 +209,7 @@
width: 100%;
height: px2rem(136);
margin-block-end: var(--sp-s);
- border-radius: px2rem(5);
+ border-radius: $br-6;
display: flex;
justify-content: center;
flex-direction: column;
@@ -216,7 +220,7 @@
}
.card-name {
- padding: 0 px2rem(6);
+ padding: 0 var(--sp-s);
display: flex;
justify-content: space-between;
height: $sz-24;
@@ -225,6 +229,7 @@
.card-text {
@include t.use-typography("body-large");
+
white-space: nowrap;
overflow: hidden;
width: 90%;
@@ -252,11 +257,13 @@
.template-link-title {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-primary);
}
.template-link-text {
@include t.use-typography("body-small");
+
margin-block-start: var(--sp-s);
color: var(--color-foreground-secondary);
}
diff --git a/frontend/src/app/main/ui/debug/icons_preview.scss b/frontend/src/app/main/ui/debug/icons_preview.scss
index a8493ed42b..a5a83fdf11 100644
--- a/frontend/src/app/main/ui/debug/icons_preview.scss
+++ b/frontend/src/app/main/ui/debug/icons_preview.scss
@@ -9,7 +9,8 @@
}
.title {
- @include deprecated.bigTitleTipography;
+ @include deprecated.big-title-typography;
+
color: var(--color-foreground-primary);
}
@@ -28,10 +29,10 @@
row-gap: 0.5rem;
grid-template-rows: var(--cell-size) 1fr;
padding: 0.5rem;
-
color: var(--color-foreground-primary);
- word-break: break-word;
- @include deprecated.bodySmallTypography;
+ overflow-wrap: break-word;
+
+ @include deprecated.body-small-typography;
svg {
width: var(--cell-size);
diff --git a/frontend/src/app/main/ui/debug/playground.scss b/frontend/src/app/main/ui/debug/playground.scss
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frontend/src/app/main/ui/delete_shared.scss b/frontend/src/app/main/ui/delete_shared.scss
index d0f08a50e8..c3487c3aa2 100644
--- a/frontend/src/app/main/ui/delete_shared.scss
+++ b/frontend/src/app/main/ui/delete_shared.scss
@@ -8,14 +8,16 @@
@use "ds/typography.scss" as t;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
&.transparent {
background-color: transparent;
}
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
display: grid;
gap: var(--sp-xxl);
grid-template-rows: auto minmax(0, 1fr) auto;
@@ -29,38 +31,42 @@
.modal-title {
@include t.use-typography("headline-medium");
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
@include t.use-typography("body-small");
+
display: grid;
gap: var(--sp-s);
}
.element-list {
@include t.use-typography("body-large");
+
color: var(--modal-text-foreground-color);
overflow-y: auto;
margin-block: 0;
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
@@ -72,6 +78,7 @@
.modal-subtitle,
.modal-msg {
@include t.use-typography("body-large");
+
color: var(--modal-text-foreground-color);
line-height: 1.5;
}
diff --git a/frontend/src/app/main/ui/ds/_borders.scss b/frontend/src/app/main/ui/ds/_borders.scss
index 9fff3615ac..070f0b9abb 100644
--- a/frontend/src/app/main/ui/ds/_borders.scss
+++ b/frontend/src/app/main/ui/ds/_borders.scss
@@ -12,6 +12,5 @@ $br-6: px2rem(6);
$br-8: px2rem(8);
$br-12: px2rem(12);
$br-circle: 50%;
-
$b-1: px2rem(1);
$b-2: px2rem(2);
diff --git a/frontend/src/app/main/ui/ds/_utils.scss b/frontend/src/app/main/ui/ds/_utils.scss
index 248d43d002..a455cb0318 100644
--- a/frontend/src/app/main/ui/ds/_utils.scss
+++ b/frontend/src/app/main/ui/ds/_utils.scss
@@ -7,6 +7,7 @@
@use "sass:math";
@function px2rem($value) {
- $remValue: math.div($value, 16) * 1rem;
- @return $remValue;
+ $rem-value: math.div($value, 16) * 1rem;
+
+ @return $rem-value;
}
diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss
index 433495c300..41c9474839 100644
--- a/frontend/src/app/main/ui/ds/buttons/_buttons.scss
+++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss
@@ -11,29 +11,21 @@
%base-button {
--button-bg-color: initial;
--button-fg-color: initial;
-
--button-hover-bg-color: initial;
--button-hover-fg-color: initial;
-
--button-active-bg-color: initial;
--button-active-fg-color: initial;
-
--button-disabled-bg-color: initial;
--button-disabled-fg-color: initial;
-
--button-border-color: var(--button-bg-color);
-
--button-focus-inner-ring-color: initial;
--button-focus-outer-ring-color: initial;
-
--button-width: initial;
--button-height: #{$sz-32};
appearance: none;
-
width: var(--button-width);
height: var(--button-height);
-
background: var(--button-bg-color);
color: var(--button-fg-color);
border: $b-1 solid var(--button-border-color);
@@ -53,6 +45,7 @@
&:focus-visible {
outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)};
outline-offset: -#{px2rem(3)};
+
--button-border-color: var(--button-focus-outer-ring-color);
--button-fg-color: var(--button-focus-fg-color);
}
@@ -66,16 +59,12 @@
%base-button-primary {
--button-bg-color: var(--color-accent-primary);
--button-fg-color: var(--color-background-secondary);
-
--button-hover-bg-color: var(--color-accent-tertiary);
--button-hover-fg-color: var(--color-background-secondary);
-
--button-active-bg-color: var(--color-accent-tertiary);
--button-active-fg-color: var(--color-background-secondary);
-
--button-disabled-bg-color: var(--color-accent-primary-muted);
--button-disabled-fg-color: var(--color-background-secondary);
-
--button-focus-bg-color: var(--color-accent-primary);
--button-focus-fg-color: var(--color-background-secondary);
--button-focus-inner-ring-color: var(--color-background-secondary);
@@ -83,23 +72,19 @@
&:active,
&[aria-pressed="true"] {
- box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
+ box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 0.2);
}
}
%base-button-secondary {
--button-bg-color: var(--color-background-tertiary);
--button-fg-color: var(--color-foreground-secondary);
-
--button-hover-bg-color: var(--color-background-tertiary);
--button-hover-fg-color: var(--color-accent-primary);
-
--button-active-bg-color: var(--color-background-quaternary);
--button-active-fg-color: var(--color-accent-primary);
-
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-foreground-secondary);
-
--button-focus-bg-color: var(--color-background-tertiary);
--button-focus-fg-color: var(--color-foreground-primary);
--button-focus-inner-ring-color: var(--color-background-secondary);
@@ -109,16 +94,12 @@
%base-button-ghost {
--button-bg-color: transparent;
--button-fg-color: var(--color-foreground-secondary);
-
--button-hover-bg-color: var(--color-background-tertiary);
--button-hover-fg-color: var(--color-accent-primary);
-
--button-active-bg-color: var(--color-background-quaternary);
--button-active-fg-color: var(--color-accent-primary);
-
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-accent-primary-muted);
-
--button-focus-bg-color: transparent;
--button-focus-fg-color: var(--color-foreground-secondary);
--button-focus-inner-ring-color: transparent;
@@ -128,16 +109,12 @@
%base-button-destructive {
--button-bg-color: var(--color-accent-error);
--button-fg-color: var(--color-foreground-primary);
-
--button-hover-bg-color: var(--color-background-error);
--button-hover-fg-color: var(--color-foreground-primary);
-
--button-active-bg-color: var(--color-accent-error);
--button-active-fg-color: var(--color-foreground-primary);
-
--button-disabled-bg-color: var(--color-background-error);
--button-disabled-fg-color: var(--color-accent-error);
-
--button-focus-bg-color: var(--color-accent-error);
--button-focus-fg-color: var(--color-foreground-primary);
--button-focus-inner-ring-color: var(--color-background-primary);
@@ -145,6 +122,6 @@
&:active,
&[aria-pressed="true"] {
- box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
+ box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 0.2);
}
}
diff --git a/frontend/src/app/main/ui/ds/buttons/button.scss b/frontend/src/app/main/ui/ds/buttons/button.scss
index dd8c720559..5885f881c8 100644
--- a/frontend/src/app/main/ui/ds/buttons/button.scss
+++ b/frontend/src/app/main/ui/ds/buttons/button.scss
@@ -9,10 +9,9 @@
.button {
@extend %base-button;
-
@include use-typography("headline-small");
- padding: 0 var(--sp-m);
+ padding: 0 var(--sp-m);
display: inline-flex;
align-items: center;
column-gap: var(--sp-xs);
diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
index bcfd24240e..45b0b7b1b7 100644
--- a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
+++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
@@ -19,6 +19,7 @@
[:tooltip-class {:optional true} [:maybe :string]]
[:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]]
[:icon-class {:optional true} :string]
+ [:icon-size {:optional true} [:maybe [:enum "s" "m" "l"]]]
[:icon
[:and :string [:fn #(contains? icon-list %)]]]
[:aria-label :string]
@@ -30,7 +31,7 @@
(mf/defc icon-button*
{::mf/schema schema:icon-button
::mf/memo true}
- [{:keys [class icon icon-class variant aria-label children tooltip-placement tooltip-class type] :rest props}]
+ [{:keys [class icon icon-class icon-size variant aria-label children tooltip-placement tooltip-class type] :rest props}]
(let [variant
(d/nilv variant "primary")
@@ -60,5 +61,5 @@
:placement tooltip-placement
:id tooltip-id}
[:> :button props
- [:> icon* {:icon-id icon :aria-hidden true :class icon-class}]
+ [:> icon* {:icon-id icon :aria-hidden true :class icon-class :size icon-size}]
children]]))
diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.scss b/frontend/src/app/main/ui/ds/buttons/icon_button.scss
index 26c8692558..40b422168a 100644
--- a/frontend/src/app/main/ui/ds/buttons/icon_button.scss
+++ b/frontend/src/app/main/ui/ds/buttons/icon_button.scss
@@ -37,20 +37,15 @@
.icon-button-action {
--button-bg-color: transparent;
--button-fg-color: var(--color-foreground-secondary);
-
--button-hover-bg-color: transparent;
--button-hover-fg-color: var(--color-accent-primary);
-
--button-active-bg-color: var(--color-background-quaternary);
-
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-accent-primary-muted);
-
--button-focus-bg-color: transparent;
--button-focus-fg-color: var(--color-accent-primary);
--button-focus-inner-ring-color: transparent;
--button-focus-outer-ring-color: var(--color-accent-primary);
-
--button-width: #{$sz-24};
--button-height: #{$sz-24};
}
diff --git a/frontend/src/app/main/ui/ds/colors.scss b/frontend/src/app/main/ui/ds/colors.scss
index e5c1525e10..67358d3096 100644
--- a/frontend/src/app/main/ui/ds/colors.scss
+++ b/frontend/src/app/main/ui/ds/colors.scss
@@ -12,22 +12,17 @@ $mint-700: #426158;
$mint-150-60: #7efff599;
$mint-250-10: #00d1b81a;
$mint-250-70: #00d1b8b3;
-
$green-200: #a7e8d9;
$green-500: #2d9f8f;
$green-950: #0a2927;
-
$orange-200: #fedeac;
$orange-500: #fe9c07;
$orange-950: #3d2501;
-
$red-200: #ffcada;
$red-400: #c80857;
$red-500: #ff3277;
$red-950: #500124;
-
$pink-400: #ff6fe0;
-
$purple-200: #e1d2f5;
$purple-400: #bb97d8;
$purple-500: #a977d1;
@@ -36,23 +31,18 @@ $purple-700: #6911d4;
$purple-600-10: #8c33eb1a;
$purple-600-70: #8c33ebb3;
$purple-700-60: #6911d499;
-
$aqua-200: #ddf7ff;
$aqua-400: #77e1f3;
$aqua-600: #59acbb;
$aqua-800: #1d4464;
-
$violet-300: #a7a9ff;
$violet-600: #6c6dad;
$violet-700: #484c74;
$violet-800: #272941;
-
$blue-200: #bae3fd;
$blue-500: #0e9be9;
$blue-950: #082c49;
-
$cobalt-700: #1345aa;
-
$black: #000;
$gray-950: #18181a;
$gray-950-60: #18181a99;
@@ -63,12 +53,10 @@ $gray-200: #e8eaee;
$gray-100: #eef0f2;
$gray-50: #f3f4f6;
$white: #fff;
-$white-60: #ffffff99;
+$white-60: #fff9;
$white-90: #ffffffe6;
-
$blue-teal-700: #495e74;
$grayish-blue-500: #8f9da3;
-
$grayish-red: #bfbfbf;
:global(.light) {
@@ -83,7 +71,6 @@ $grayish-red: #bfbfbf;
--color-accent-action: #{$purple-400};
--color-accent-action-hover: #{$purple-500};
--color-accent-off: #{$gray-50};
-
--color-accent-success: #{$green-500};
--color-background-success: #{$green-200};
--color-accent-warning: #{$orange-500};
@@ -97,29 +84,23 @@ $grayish-red: #bfbfbf;
--color-accent-default: #{$gray-100};
--color-icon-default: #{$blue-teal-700};
--color-background-disabled: #{$gray-200};
-
--color-background-primary: #{$white};
--color-background-secondary: #{$gray-200};
--color-background-tertiary: #{$gray-50};
--color-background-quaternary: #{$gray-100};
-
--color-foreground-primary: #{$black};
--color-foreground-secondary: #{$blue-teal-700};
-
--color-static-white: #{$white};
--color-static-black: #{$black};
-
--color-shadow-dark: #{color.change($gray-200, $alpha: 0.6)};
--color-shadow-light: #{color.change($black, $alpha: 0.3)};
--color-overlay-default: #{$white-60};
--color-overlay-onboarding: #{$white-90};
--color-canvas: #{$grayish-red};
-
--color-token-background: #{$aqua-200};
--color-token-border: #{$aqua-400};
--color-token-accent: #{$aqua-600};
--color-token-foreground: #{$aqua-800};
-
--color-badge-premium: #{$orange-500};
}
@@ -135,7 +116,6 @@ $grayish-red: #bfbfbf;
--color-accent-action: #{$purple-400};
--color-accent-action-hover: #{$purple-500};
--color-accent-off: #{$gray-50};
-
--color-accent-success: #{$green-500};
--color-background-success: #{$green-950};
--color-accent-warning: #{$orange-500};
@@ -149,28 +129,22 @@ $grayish-red: #bfbfbf;
--color-accent-default: #{$gray-800};
--color-icon-default: #{$grayish-blue-500};
--color-background-disabled: #{$gray-800};
-
--color-background-primary: #{$gray-950};
--color-background-secondary: #{$black};
--color-background-tertiary: #{$gray-900};
--color-background-quaternary: #{$gray-800};
-
--color-foreground-primary: #{$white};
--color-foreground-secondary: #{$grayish-blue-500};
-
--color-static-white: #{$white};
--color-static-black: #{$black};
-
--color-shadow-dark: #{color.change($black, $alpha: 0.6)};
--color-shadow-light: #{color.change($black, $alpha: 0.3)};
--color-overlay-default: #{$gray-950-60};
--color-overlay-onboarding: #{$gray-950-90};
--color-canvas: #{$grayish-red};
-
--color-token-background: #{$violet-800};
--color-token-border: #{$violet-700};
--color-token-accent: #{$violet-600};
--color-token-foreground: #{$violet-300};
-
--color-badge-premium: #{$orange-200};
}
diff --git a/frontend/src/app/main/ui/ds/controls/checkbox.scss b/frontend/src/app/main/ui/ds/controls/checkbox.scss
index 81eda1fd47..de272a654f 100644
--- a/frontend/src/app/main/ui/ds/controls/checkbox.scss
+++ b/frontend/src/app/main/ui/ds/controls/checkbox.scss
@@ -14,14 +14,11 @@
--input-checkbox-border-color-hover: var(--color-accent-primary-muted);
--input-checkbox-foreground-color: var(--color-foreground-primary);
--input-checkbox-background-color: var(--color-background-quaternary);
-
--input-checkbox-border-color-checked: var(--color-background-quaternary);
--input-checkbox-foreground-color-checked: var(--color-background-primary);
--input-checkbox-background-color-checked: var(--color-accent-primary);
-
--input-checkbox-foreground-color-disabled: var(--color-background-primary);
--input-checkbox-background-color-disabled: var(--color-foreground-secondary);
-
--input-checkbox-text-color: var(--color-foreground-secondary);
}
@@ -73,6 +70,7 @@
.checkbox-text {
@include use-typography("body-small");
+
padding-inline-start: var(--sp-s);
color: var(--input-checkbox-text-color);
}
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.cljs b/frontend/src/app/main/ui/ds/controls/combobox.cljs
index f8fcc566b6..14ade592a2 100644
--- a/frontend/src/app/main/ui/ds/controls/combobox.cljs
+++ b/frontend/src/app/main/ui/ds/controls/combobox.cljs
@@ -10,7 +10,7 @@
(:require
[app.common.data :as d]
[app.main.constants :refer [max-input-length]]
- [app.main.ui.ds.controls.select :refer [get-option handle-focus-change]]
+ [app.main.ui.ds.controls.select :refer [handle-focus-change]]
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.util.dom :as dom]
@@ -29,19 +29,21 @@
[:placeholder {:optional true} :string]
[:disabled {:optional true} :boolean]
[:default-selected {:optional true} :string]
+ [:select-only {:optional true} :boolean]
[:on-change {:optional true} fn?]
[:empty-to-end {:optional true} [:maybe :boolean]]
[:has-error {:optional true} :boolean]])
(mf/defc combobox*
{::mf/schema schema:combobox}
- [{:keys [id options class placeholder disabled has-error default-selected max-length empty-to-end on-change] :rest props}]
+ [{:keys [id options class placeholder disabled has-error default-selected select-only max-length empty-to-end on-change] :rest props}]
(let [;; NOTE: we use mfu/bean here for transparently handle
;; options provide as clojure data structures or javascript
;; plain objects and lists.
options (if (array? options)
(mfu/bean options)
options)
+ select-only (d/nilv select-only false)
empty-to-end (d/nilv empty-to-end false)
is-open* (mf/use-state false)
@@ -64,13 +66,15 @@
value-ref (mf/use-ref nil)
dropdown-options
- (mf/with-memo [options filter-id]
- (->> options
- (filterv (fn [option]
- (let [option (str/lower (get option :id))
- filter (str/lower filter-id)]
- (str/includes? option filter))))
- (not-empty)))
+ (mf/with-memo [options filter-id select-only]
+ (if select-only
+ (not-empty options)
+ (->> options
+ (filterv (fn [option]
+ (let [option (str/lower (get option :label))
+ filter (str/lower filter-id)]
+ (str/includes? option filter))))
+ (not-empty))))
set-option-ref
(mf/use-fn
@@ -113,7 +117,7 @@
on-blur
(mf/use-fn
- (mf/deps on-change)
+ (mf/deps on-change options selected-id select-only)
(fn [event]
(dom/stop-propagation event)
(let [target (dom/get-related-target event)
@@ -123,7 +127,14 @@
(reset! focused-id* nil)
(when (fn? on-change)
(when-let [input-node (mf/ref-val input-ref)]
- (on-change (dom/get-input-value input-node))))))))
+ (let [input-value (dom/get-input-value input-node)
+ selected-option (d/seek #(= selected-id (get % :id)) options)
+ value (if select-only
+ selected-id
+ (if (some? selected-option)
+ selected-id
+ input-value))]
+ (on-change value))))))))
on-input-click
(mf/use-fn
@@ -144,10 +155,18 @@
on-input-key-down
(mf/use-fn
- (mf/deps is-open focused-id disabled)
+ (mf/deps is-open focused-id disabled select-only)
(fn [event]
(dom/stop-propagation event)
(when-not disabled
+ (when (and select-only
+ (not (kbd/down-arrow? event))
+ (not (kbd/up-arrow? event))
+ (not (kbd/home? event))
+ (not (kbd/enter? event))
+ (not (kbd/esc? event))
+ (not (kbd/tab? event)))
+ (dom/prevent-default event))
(let [options (mf/ref-val options-ref)
len (count options)
index (d/index-of-pred options #(= focused-id (get % :id)))
@@ -196,24 +215,34 @@
on-input-change
(mf/use-fn
+ (mf/deps select-only)
(fn [event]
(dom/stop-propagation event)
- (let [value (-> event
- dom/get-target
- dom/get-value)]
- (mf/set-ref-val! value-ref value)
- (reset! selected-id* value)
- (reset! filter-id* value)
- (reset! focused-id* nil))))
+ (when-not select-only
+ (let [value (-> event
+ dom/get-target
+ dom/get-value)]
+ (mf/set-ref-val! value-ref value)
+ (reset! selected-id* value)
+ (reset! filter-id* value)
+ (reset! focused-id* nil)))))
selected-option
(mf/with-memo [options selected-id]
(when (d/not-empty? options)
- (get-option options selected-id)))
+ (d/seek #(= selected-id (get % :id)) options)))
icon
(when selected-option
- (get selected-option :icon))]
+ (get selected-option :icon))
+
+ avatar
+ (when selected-option
+ (get selected-option :avatar))
+
+ render-avatar-fn
+ (when avatar
+ (get avatar :render-fn))]
(mf/with-effect [dropdown-options]
(mf/set-ref-val! options-ref dropdown-options))
@@ -241,25 +270,33 @@
:on-click on-click}
[:span {:class (stl/css-case :header true
- :header-icon (some? icon))}
+ :header-icon (some? icon)
+ :header-avatar (fn? render-avatar-fn))}
(when icon
[:> icon* {:icon-id icon
:size "s"
:aria-hidden true}])
+ (when (fn? render-avatar-fn)
+ [:> render-avatar-fn {:avatar avatar}])
[:input {:id id
:ref input-ref
:type "text"
:role "combobox"
:class (stl/css :input)
:auto-complete "off"
- :aria-autocomplete "both"
+ :aria-autocomplete (if select-only "none" "both")
:aria-expanded is-open
:aria-controls listbox-id
:aria-activedescendant focused-id
:data-testid "combobox-input"
:max-length (d/nilv max-length max-input-length)
:disabled disabled
- :value (d/nilv selected-id "")
+ :read-only select-only
+ :value (if select-only
+ (d/nilv (:label selected-option) "")
+ (if (str/empty? (:id selected-option))
+ (d/nilv selected-id "")
+ (d/nilv (:label selected-option) "")))
:placeholder placeholder
:on-change on-input-change
:on-click on-input-click
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.mdx b/frontend/src/app/main/ui/ds/controls/combobox.mdx
index eff4b6b18d..58075b8aed 100644
--- a/frontend/src/app/main/ui/ds/controls/combobox.mdx
+++ b/frontend/src/app/main/ui/ds/controls/combobox.mdx
@@ -53,6 +53,39 @@ These are available in the `app.main.ds.foundations.assets.icon` namespace.
]}]
```
+
+### Avatars
+
+Each option of `combobox*` also accepts an optional `avatar` map.
+Avatar rendering is defined per option with `:render-fn`, so each avatar type can provide its own UI. The renderer should be a component function that receives the full `avatar` map.
+
+```clj
+;; Example renderer for organization avatars
+(mf/defc render-org-avatar*
+ [{:keys [avatar]}]
+ (when (= :organization (:type avatar))
+ [:> org-avatar* {:org (:organization avatar)
+ :size (:size avatar)}]))
+
+[:> combobox*
+ {:options [{:label "Design Team"
+ :id "org-design"
+ :avatar {:render-fn render-org-avatar*
+ :size "s"
+ :organization {:name "Design Team"
+ :organization-avatar-bg-url "https://example.com/avatar-bg.svg"
+ :organization-custom-photo nil}}}
+ {:label "Engineering"
+ :id "org-engineering"
+ :avatar {:render-fn render-org-avatar*
+ :size "s"
+ :organization {:name "Engineering"
+ :organization-avatar-bg-url nil
+ :organization-custom-photo "https://example.com/custom-photo.png"}}}]}]
+```
+
+The same pattern can be used later for other avatar kinds, for example `:team`, by adding a different `:render-fn` in those options.
+
## Usage guidelines (design)
### Where to Use
diff --git a/frontend/src/app/main/ui/ds/controls/combobox.scss b/frontend/src/app/main/ui/ds/controls/combobox.scss
index 3df8586715..519243a8fb 100644
--- a/frontend/src/app/main/ui/ds/controls/combobox.scss
+++ b/frontend/src/app/main/ui/ds/controls/combobox.scss
@@ -26,6 +26,7 @@
}
@include use-typography("body-small");
+
position: relative;
display: grid;
grid-template-rows: auto;
@@ -40,7 +41,6 @@
height: $sz-32;
width: 100%;
padding: var(--sp-s);
- border: none;
border-radius: $br-8;
outline: $b-1 solid var(--combobox-outline-color);
border: $b-1 solid var(--combobox-border-color);
@@ -64,10 +64,16 @@
color: var(--combobox-icon-color);
}
+.header-avatar {
+ grid-template-columns: auto 1fr;
+ gap: var(--sp-s);
+}
+
.input {
all: unset;
@include use-typography("body-small");
+
background-color: transparent;
overflow: hidden;
text-align: left;
@@ -88,6 +94,7 @@
.disabled {
cursor: default;
+
--combobox-background-color: var(--color-background-primary);
--combobox-border-color: var(--color-background-quaternary);
--combobox-text-color: var(--color-foreground-secondary);
diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs
index bd18d6dcdf..54da21ed03 100644
--- a/frontend/src/app/main/ui/ds/controls/numeric_input.cljs
+++ b/frontend/src/app/main/ui/ds/controls/numeric_input.cljs
@@ -19,6 +19,7 @@
[app.main.ui.ds.controls.utilities.token-field :refer [token-field*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
[app.main.ui.formats :as fmt]
+ [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
@@ -83,48 +84,6 @@
(str/replace #"^\{" "")
(str/replace #"\}$" "")))
-(defn- token->dropdown-option
- [token]
- {:id (str (get token :id))
- :type :token
- :resolved-value (get token :resolved-value)
- :name (get token :name)})
-
-(defn- generate-dropdown-options
- [tokens no-sets]
- (if (empty? tokens)
- [{:type :empty
- :label (if no-sets
- (tr "ds.inputs.numeric-input.no-applicable-tokens")
- (tr "ds.inputs.numeric-input.no-matches"))}]
- (->> tokens
- (map (fn [[type items]]
- (cons {:group true
- :type :group
- :id (dm/str "group-" (name type))
- :name (name type)}
- (map token->dropdown-option items))))
- (interpose [{:separator true
- :id "separator"
- :type :separator}])
- (apply concat)
- (vec)
- (not-empty))))
-
-(defn- extract-partial-brace-text
- [s]
- (when-let [start (str/last-index-of s "{")]
- (subs s (inc start))))
-
-(defn- filter-token-groups-by-name
- [tokens filter-text]
- (let [lc-filter (str/lower filter-text)]
- (into {}
- (keep (fn [[group tokens]]
- (let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)]
- (when (seq filtered)
- [group filtered]))))
- tokens)))
(defn- focusable-option?
[option]
@@ -150,31 +109,12 @@
j)))
indices)))
-(defn- sort-groups-and-tokens
- "Sorts both the groups and the tokens inside them alphabetically.
+(defn- find-token-by-name
+ [data name]
+ (some (fn [tokens-data]
+ (some #(when (= (:name %) name) %) tokens-data))
+ (vals data)))
- Input:
- A map where:
- - keys are groups (keywords or strings, e.g. :dimensions, :colors)
- - values are vectors of token maps, each containing at least a :name key
-
- Example input:
- {:dimensions [{:name \"tres\"} {:name \"quini\"}]
- :colors [{:name \"azul\"} {:name \"rojo\"}]}
-
- Output:
- A sorted map where:
- - groups are ordered alphabetically by key
- - tokens inside each group are sorted alphabetically by :name
-
- Example output:
- {:colors [{:name \"azul\"} {:name \"rojo\"}]
- :dimensions [{:name \"quini\"} {:name \"tres\"}]}"
-
- [groups->tokens]
- (into (sorted-map) ;; ensure groups are ordered alphabetically by their key
- (for [[group tokens] groups->tokens]
- [group (sort-by :name tokens)])))
(def ^:private schema:icon
[:and :string [:fn #(contains? icon-list %)]])
@@ -203,10 +143,14 @@
[:applied-token {:optional true} [:maybe [:or :string [:= :multiple]]]]
[:empty-to-end {:optional true} :boolean]
[:on-change {:optional true} fn?]
+ [:on-change-start {:optional true} fn?]
+ [:on-change-end {:optional true} fn?]
[:on-blur {:optional true} fn?]
[:on-focus {:optional true} fn?]
[:on-detach {:optional true} fn?]
[:property {:optional true} :string]
+ [:tooltip-placement {:optional true}
+ [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]
[:align {:optional true} [:maybe [:enum :left :right]]]])
(mf/defc numeric-input*
@@ -215,10 +159,11 @@
icon disabled inner-class
min max max-length step
is-selected-on-focus nillable
- tokens applied-token empty-to-end
- on-change on-blur on-focus on-detach
+ tokens applied-token-name empty-to-end
+ on-change on-change-start on-change-end
+ on-blur on-focus on-detach
property align ref name
- text-icon]
+ tooltip-placement text-icon]
:rest props}]
(let [;; NOTE: we use mfu/bean here for transparently handle
@@ -227,9 +172,16 @@
tokens (if (object? tokens)
(mfu/bean tokens)
tokens)
- value (if (= :multiple applied-token)
+
+ value (if (= :multiple applied-token-name)
:multiple
value)
+
+ token-applied (mf/with-memo [tokens applied-token-name]
+ (find-token-by-name tokens applied-token-name))
+
+ token-has-errors? (-> token-applied :errors seq boolean)
+
is-multiple? (= :multiple value)
value (cond
is-multiple? nil
@@ -264,8 +216,8 @@
is-open* (mf/use-state false)
is-open (deref is-open*)
- token-applied* (mf/use-state applied-token)
- token-applied (deref token-applied*)
+ token-applied-name* (mf/use-state applied-token-name)
+ token-applied-name (deref token-applied-name*)
focused-id* (mf/use-state nil)
focused-id (deref focused-id*)
@@ -276,6 +228,10 @@
raw-value* (mf/use-ref nil)
last-value* (mf/use-ref nil)
+ ;; Flag to prevent effect from overwriting token during selection
+ ;; This prevents race condition between blur and token selection
+ token-selection-in-progress* (mf/use-ref false)
+
;; Refs
wrapper-ref (mf/use-ref nil)
nodes-ref (mf/use-ref nil)
@@ -287,23 +243,19 @@
open-dropdown-ref (mf/use-ref nil)
token-detach-btn-ref (mf/use-ref nil)
+ ;; Drag scrubbing state
+ drag-state* (mf/use-ref :idle)
+ drag-start-x* (mf/use-ref 0)
+ drag-start-val* (mf/use-ref 0)
+
dropdown-options
(mf/with-memo [tokens filter-id]
- (delay
- (let [tokens (if (delay? tokens) @tokens tokens)
-
- sorted-tokens (sort-groups-and-tokens tokens)
- partial (extract-partial-brace-text filter-id)
- options (if (seq partial)
- (filter-token-groups-by-name sorted-tokens partial)
- sorted-tokens)
- no-sets? (nil? sorted-tokens)]
- (generate-dropdown-options options no-sets?))))
+ (csu/get-token-dropdown-options tokens filter-id))
selected-id*
(mf/use-state (fn []
- (if applied-token
- (:id (get-option-by-name dropdown-options applied-token))
+ (if applied-token-name
+ (:id (get-option-by-name dropdown-options applied-token-name))
nil)))
selected-id
(deref selected-id*)
@@ -337,7 +289,7 @@
(if-let [parsed (parse-value raw-value (mf/ref-val last-value*) min max nillable)]
(when-not (= parsed (mf/ref-val last-value*))
(mf/set-ref-val! last-value* parsed)
- (reset! token-applied* nil)
+ (reset! token-applied-name* nil)
(when (fn? on-change)
(on-change parsed))
@@ -348,7 +300,7 @@
(do
(mf/set-ref-val! last-value* nil)
(mf/set-ref-val! raw-value* "")
- (reset! token-applied* nil)
+ (reset! token-applied-name* nil)
(update-input "")
(when (fn? on-change)
(on-change nil)))
@@ -356,7 +308,7 @@
(let [fallback-value (or (mf/ref-val last-value*) default)]
(mf/set-ref-val! raw-value* fallback-value)
(mf/set-ref-val! last-value* fallback-value)
- (reset! token-applied* nil)
+ (reset! token-applied-name* nil)
(update-input (fmt/format-number fallback-value))
(when (and (fn? on-change) (not= fallback-value (str value)))
@@ -383,13 +335,15 @@
(mf/use-fn
(mf/deps apply-token)
(fn [id value name]
+ (mf/set-ref-val! token-selection-in-progress* true)
(reset! selected-id* id)
(reset! focused-id* nil)
(reset! is-open* false)
- (reset! token-applied* name)
+ (reset! token-applied-name* name)
(apply-token value name)
(ts/schedule-on-idle
(fn []
+ (mf/set-ref-val! token-selection-in-progress* false)
(when token-wrapper-ref
(dom/focus! (mf/ref-val token-wrapper-ref)))))))
@@ -419,7 +373,7 @@
(on-token-apply focused-id value name)
(reset! filter-id* ""))))
- on-blur
+ handle-blur
(mf/use-fn
(mf/deps apply-value on-blur)
(fn [event]
@@ -434,7 +388,8 @@
(when (mf/ref-val dirty-ref)
(apply-value (mf/ref-val raw-value*)))
(when (fn? on-blur)
- (on-blur event))))
+ (on-blur event))
+ (dom/blur! (mf/ref-val ref))))
on-key-down
(mf/use-fn
@@ -474,8 +429,9 @@
value (get option :resolved-value)
name (get option :name)]
(on-token-apply option-id value name)
- (reset! filter-id* ""))))
- (on-blur event))
+ (reset! filter-id* "")
+ (handle-blur event))))
+ (handle-blur event))
esc?
(do
@@ -516,13 +472,14 @@
(mf/use-fn
(mf/deps on-focus select-on-focus)
(fn [event]
- (when (fn? on-focus)
- (on-focus event))
- (let [target (dom/get-target event)]
- (when select-on-focus
- (dom/select-text! target)
- ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
- (.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
+ (when-not (= :dragging (mf/ref-val drag-state*))
+ (when (fn? on-focus)
+ (on-focus event))
+ (let [target (dom/get-target event)]
+ (when select-on-focus
+ (dom/select-text! target)
+ ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
+ (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))))
on-mouse-wheel
(mf/use-fn
@@ -542,6 +499,77 @@
(dom/stop-propagation event)
(apply-value (dm/str new-val)))))))
+ on-scrub-pointer-down
+ (mf/use-fn
+ (mf/deps disabled is-open is-multiple? ref min max nillable default)
+ (fn [event]
+ (when-not (or disabled is-open is-multiple?)
+ (let [node (mf/ref-val ref)
+ is-focused (and (some? node) (dom/active? node))
+ has-token (some? (deref token-applied-name*))]
+ (when-not (or is-focused has-token)
+ (let [client-x (.-clientX event)
+ parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable)
+ start-val (or parsed default 0)]
+ (mf/set-ref-val! drag-state* :maybe-dragging)
+ (mf/set-ref-val! drag-start-x* client-x)
+ (mf/set-ref-val! drag-start-val* start-val)
+ (dom/capture-pointer event)))))))
+
+ on-scrub-pointer-move
+ (mf/use-fn
+ (mf/deps apply-value update-input step min max on-change-start)
+ (fn [event]
+ (let [state (mf/ref-val drag-state*)]
+ (when (or (= state :maybe-dragging) (= state :dragging))
+ (let [client-x (.-clientX event)
+ start-x (mf/ref-val drag-start-x*)
+ delta-x (- client-x start-x)]
+ (when (and (= state :maybe-dragging)
+ (>= (js/Math.abs delta-x) 3))
+ (mf/set-ref-val! drag-state* :dragging)
+ (dom/add-class! (dom/get-body) "cursor-drag-scrub")
+ (when (fn? on-change-start)
+ (on-change-start)))
+ (when (= (mf/ref-val drag-state*) :dragging)
+ (let [effective-step (cond
+ (.-shiftKey event) (* step 10)
+ (.-ctrlKey event) (* step 0.1)
+ :else step)
+ steps (js/Math.round (/ delta-x 1))
+ new-val (mth/clamp (+ (mf/ref-val drag-start-val*)
+ (* steps effective-step))
+ min max)]
+ (update-input (fmt/format-number new-val))
+ (apply-value (dm/str new-val)))))))))
+
+ on-scrub-pointer-up
+ (mf/use-fn
+ (mf/deps ref on-change-end)
+ (fn [event]
+ (let [state (mf/ref-val drag-state*)]
+ (when (= state :maybe-dragging)
+ (mf/set-ref-val! drag-state* :idle)
+ (dom/release-pointer event)
+ (when-let [node (mf/ref-val ref)]
+ (dom/focus! node)))
+ (when (= state :dragging)
+ (mf/set-ref-val! drag-state* :idle)
+ (dom/remove-class! (dom/get-body) "cursor-drag-scrub")
+ (dom/release-pointer event)
+ (when (fn? on-change-end)
+ (on-change-end))))))
+
+ on-scrub-lost-pointer-capture
+ (mf/use-fn
+ (mf/deps on-change-end)
+ (fn [_event]
+ (let [was-dragging (= :dragging (mf/ref-val drag-state*))]
+ (mf/set-ref-val! drag-state* :idle)
+ (dom/remove-class! (dom/get-body) "cursor-drag-scrub")
+ (when (and was-dragging (fn? on-change-end))
+ (on-change-end)))))
+
open-dropdown
(mf/use-fn
(mf/deps disabled ref)
@@ -562,16 +590,16 @@
detach-token
(mf/use-fn
- (mf/deps on-detach tokens disabled token-applied)
+ (mf/deps on-detach tokens disabled token-applied-name)
(fn [event]
(when-not disabled
(dom/prevent-default event)
(dom/stop-propagation event)
- (reset! token-applied* nil)
+ (reset! token-applied-name* nil)
(reset! selected-id* nil)
(reset! focused-id* nil)
(when on-detach
- (on-detach token-applied))
+ (on-detach token-applied-name))
(ts/schedule-on-idle
(fn []
(dom/focus! (mf/ref-val ref)))))))
@@ -633,7 +661,7 @@
(tr "labels.mixed-values")
placeholder)
:default-value (or (mf/ref-val last-value*) (fmt/format-number value))
- :on-blur on-blur
+ :on-blur handle-blur
:on-key-down on-key-down
:on-focus on-focus
:on-change store-raw-value
@@ -653,14 +681,15 @@
:class (stl/css :invisible-button)
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
:ref open-dropdown-ref
+ :tooltip-placement tooltip-placement
:on-click open-dropdown}])))
:max-length max-length})
token-props
- (when (and token-applied (not= :multiple token-applied))
- (let [token (get-option-by-name dropdown-options token-applied)
+ (when (and token-applied-name (not= :multiple token-applied-name))
+ (let [token (get-option-by-name dropdown-options token-applied-name)
id (get token :id)
- label (or (get token :name) applied-token)
+ label (or (get token :name) applied-token-name)
token-value (or (get token :resolved-value)
(or (mf/ref-val last-value*)
(fmt/format-number value)))
@@ -675,9 +704,12 @@
:on-focus on-focus
:on-token-key-down on-token-key-down
:disabled disabled
- :on-blur on-blur
+ :on-blur handle-blur
+ :token-has-errors token-has-errors?
:class inner-class
:property property
+ :is-open is-open
+ :tooltip-placement tooltip-placement
:slot-start (when (or icon text-icon)
(mf/html
(cond
@@ -694,7 +726,7 @@
:token-detach-btn-ref token-detach-btn-ref
:detach-token detach-token})))]
- (mf/with-effect [value default applied-token]
+ (mf/with-effect [value default applied-token-name]
(let [value' (cond
is-multiple?
""
@@ -704,18 +736,28 @@
:else
(fmt/format-number (d/parse-double value default)))]
-
(mf/set-ref-val! raw-value* value')
(mf/set-ref-val! last-value* value')
- (reset! token-applied* applied-token)
- (if applied-token
- (let [token-id (:id (get-option-by-name dropdown-options applied-token))]
- (reset! selected-id* token-id))
- (reset! selected-id* nil))
+
+ ;; Only sync token state if not in the middle of a selection
+ ;; This prevents race condition between blur and token selection
+ (when-not (mf/ref-val token-selection-in-progress*)
+ (reset! token-applied-name* applied-token-name)
+ (if applied-token-name
+ (let [token-id (:id (get-option-by-name dropdown-options applied-token-name))]
+ (reset! selected-id* token-id))
+ (reset! selected-id* nil)))
(when-let [node (mf/ref-val ref)]
(dom/set-value! node value'))))
+ (mf/with-effect [applied-token-name]
+ (when (nil? applied-token-name)
+ ;; Only clear if not in the middle of a selection
+ (when-not (mf/ref-val token-selection-in-progress*)
+ (reset! token-applied-name* nil)
+ (reset! selected-id* nil))))
+
(mf/with-layout-effect [on-mouse-wheel]
(when-let [node (mf/ref-val ref)]
(let [key (events/listen node "wheel" on-mouse-wheel #js {:passive false})]
@@ -725,10 +767,14 @@
(mf/set-ref-val! options-ref dropdown-options))
[:div {:class [class (stl/css :input-wrapper)]
- :ref wrapper-ref}
+ :ref wrapper-ref
+ :on-pointer-down on-scrub-pointer-down
+ :on-pointer-move on-scrub-pointer-move
+ :on-pointer-up on-scrub-pointer-up
+ :on-lost-pointer-capture on-scrub-lost-pointer-capture}
- (if (and (some? token-applied)
- (not= :multiple token-applied))
+ (if (and (some? token-applied-name)
+ (not= :multiple token-applied-name))
[:> token-field* token-props]
[:> input-field* input-props])
diff --git a/frontend/src/app/main/ui/ds/controls/numeric_input.scss b/frontend/src/app/main/ui/ds/controls/numeric_input.scss
index 0b3ee3795f..60741e7d0b 100644
--- a/frontend/src/app/main/ui/ds/controls/numeric_input.scss
+++ b/frontend/src/app/main/ui/ds/controls/numeric_input.scss
@@ -13,16 +13,23 @@
.input-wrapper {
--input-padding-size: var(--sp-xs);
--opacity-button: 0;
+
@include t.use-typography("code-font");
+
display: flex;
flex-direction: column;
gap: var(--sp-xs);
inline-size: 100%;
position: relative;
+ &:not(:focus-within) {
+ cursor: ew-resize;
+ }
+
&:hover {
--opacity-button: 1;
}
+
&:focus-within {
--opacity-button: 1;
}
@@ -35,9 +42,12 @@
.text-icon {
color: var(--color-foreground-secondary);
- @include t.use-typography("code-font");
+
+ @include t.use-typography("body-small");
+
inline-size: fit-content;
- min-inline-size: px2rem(40);
+ min-inline-size: px2rem(46);
+ padding-inline-start: var(--sp-xs);
}
.invisible-button {
@@ -46,12 +56,16 @@
inset-block-start: 0;
opacity: var(--opacity-button);
background-color: var(--color-background-quaternary);
+
&:hover {
background-color: var(--color-background-quaternary);
+
--opacity-button: 1;
}
+
&:focus {
background-color: var(--color-background-quaternary);
+
--opacity-button: 1;
}
}
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs
index ea9dd6fff3..93837196d0 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs
@@ -24,7 +24,7 @@
[:and :string [:fn #(contains? icon-list %)]]]
[:label :string]
[:value [:or :keyword :string]]
- [:disabled {:optional true} :boolean]])
+ [:disabled {:optional true} [:maybe :boolean]]])
(def ^:private schema:radio-buttons
[:map
@@ -35,46 +35,58 @@
[:name {:optional true} :string]
[:selected {:optional true}
[:maybe [:or :keyword :string]]]
- [:allow-empty {:optional true} :boolean]
+ [:allow-empty {:optional true} [:maybe :boolean]]
+ [:disabled {:optional true} [:maybe :boolean]]
[:options [:vector {:min 1} schema:radio-button]]
[:on-change {:optional true} fn?]])
(mf/defc radio-buttons*
{::mf/schema schema:radio-buttons}
- [{:keys [class variant extended name selected allow-empty options on-change] :rest props}]
+ [{:keys [class variant extended name selected allow-empty options on-change disabled] :rest props}]
(let [options (if (array? options)
(mfu/bean options)
options)
- type (if allow-empty "checkbox" "radio")
- variant (d/nilv variant "secondary")
+ type (if allow-empty "checkbox" "radio")
+ variant (d/nilv variant "secondary")
+ wrapper-disabled (d/nilv disabled false)
handle-click
(mf/use-fn
+ (mf/deps selected on-change allow-empty)
(fn [event]
(let [target (dom/get-target event)
- label (dom/get-parent-with-data target "label")]
- (dom/prevent-default event)
- (dom/stop-propagation event)
- (dom/click label))))
+ label (dom/get-parent-with-data target "label")
+ input (dom/query label "input")
+ disabled? (dom/get-attribute target "disabled")]
+ (when-not disabled?
+ (dom/click input)))))
handle-change
(mf/use-fn
- (mf/deps selected on-change)
+ (mf/deps selected on-change allow-empty)
(fn [event]
- (let [input (dom/get-target event)
- value (dom/get-target-val event)]
+ (let [input (dom/get-target event)
+ value (dom/get-target-val event)
+ selected-str (when selected (d/name selected))
+ new-value (if (and allow-empty (= value selected-str))
+ nil
+ value)]
(when (fn? on-change)
- (on-change value event))
+ (on-change new-value event))
(dom/blur! input))))
props
(mf/spread-props props {:key (dm/str name "-" selected)
:class [class (stl/css-case :wrapper true
+ :disabled disabled
:extended extended)]})]
[:> :div props
(for [[idx {:keys [id class value label icon disabled]}] (d/enumerate options)]
- (let [checked? (= selected value)]
+ (let [value-str (d/name value)
+ selected-str (when selected (d/name selected))
+ checked? (= selected-str value-str)
+ disabled (d/nilv disabled false)]
[:label {:key idx
:html-for id
:data-label true
@@ -88,13 +100,13 @@
:aria-pressed checked?
:aria-label label
:icon icon
- :disabled disabled}]
+ :disabled (or disabled wrapper-disabled)}]
[:> button* {:variant variant
:on-click handle-click
:aria-pressed checked?
:class (stl/css-case :button true
:extended extended)
- :disabled disabled}
+ :disabled (or disabled wrapper-disabled)}
label])
[:input {:id id
@@ -102,6 +114,6 @@
:on-change handle-change
:type type
:name name
- :disabled disabled
+ :disabled (or disabled wrapper-disabled)
:value value
- :default-checked checked?}]]))]))
+ :checked checked?}]]))]))
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx b/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx
index 226319286a..5346d8751c 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx
@@ -11,11 +11,17 @@ import * as RadioButtons from "./radio_buttons.stories";
# Radio Buttons
-The `radio-buttons*` component allows users to switch between two or more options that are mutually exclusive.
+The `radio-buttons*` component lets users select a single option from a set of mutually exclusive choices.
+
+It is designed for immediate selection changes, without requiring a confirmation step.
+
+---
## Variants
-Radio buttons with text only. The label will be the text of the button.
+### Text only
+
+Radio buttons using text labels. The label is displayed directly on each option.
@@ -34,12 +40,14 @@ Radio buttons with text only. The label will be the text of the button.
{:id "align-right"
:label "Right"
:value "right"}]}]
+
+ Icon only
```
-Radio buttons with icons only. In this case, the label will act as the tooltip of each button.
+### Icons only
+Radio buttons using icons instead of text labels. The label is used as tooltip and accessibility text.
-
```clj
(ns app.main.ui.foo
(:require
@@ -63,35 +71,58 @@ Radio buttons with icons only. In this case, the label will act as the tooltip o
:label "Right align"
:value "right"}]}]
```
+### Anatomy
-## Anatomy
+Each option is composed of:
-Under the hood, each option is represented by
-- a button, which is the visible and clickable element. It may be either an icon button or a text button.
-- a radio input, which is not visible but retains the current state of the option.
+A visible control (button or icon button)
+A hidden native input (radio or checkbox) that stores the state
-A radio group is defined by giving each of radio buttons in the group the same name. Once a radio group is established,
-selecting any radio button in that group automatically deselects any currently-selected radio button in the same group.
+All options share the same name, forming a radio group. Selecting one option automatically deselects the previously selected one.
-The `selected` parameter should be set to the value of the option that is to be active. Otherwise, no option will be selected.
+## Behavior
-If the parameter `allow-empty` is enabled, then the component will work with checkboxes instead of radio buttons,
-and therefore the selected option can be deselected. However, it will still only be possible to select one option.
+### Selection
+The selected prop controls the active option
+It must match the value of one of the provided options
+If selected is nil, no option is selected
-The `extended` parameter allows the component to use all the available space from the parent and distribute it equally
-among all elements.
+### Allow empty
-Any option can be individually disabled using the `disabled` parameter.
+When allow-empty is enabled:
+
+The selected option can be deselected
+Only one option can still be active at a time
+This introduces toggle-like behavior over a single selection group
+
+### Extended
+
+When extended is enabled:
+
+The component expands to fill the width of its container
+Options are evenly distributed across available space
+
+### Disabled state
+The entire group can be disabled using the `:disabled` prop
+Individual options can also be disabled using `:disabled` inside each option
+Disabled options cannot be interacted with.
## Usage Guidelines
-### When to Use
+### When to use
+For settings where users must choose exactly one option
+For preference or configuration panels
+When changes should take effect immediately
-- For multiple choice settings that take effect immediately.
-- In preference panels and configuration screens.
+### When not to use
-### When Not to Use
+For boolean toggles → use a switch or checkbox
+For multiple selection → use checkboxes
+For actions requiring confirmation → use buttons or dialogs
+For workflows that require an explicit “Apply” step
-- For boolean settings (use switch or checkbox instead).
-- For actions that require confirmation (use buttons instead).
-- For temporary states that need explicit "Apply" action.
+### Notes
+
+This component is controlled: state must be managed externally via selected
+It does not manage internal state
+The on-change handler is called with the new value whenever selection changes
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.scss b/frontend/src/app/main/ui/ds/controls/radio_buttons.scss
index 05957025dc..56e53fae7e 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.scss
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.scss
@@ -20,6 +20,11 @@
width: 100%;
display: flex;
}
+
+ &.disabled {
+ outline: $b-1 solid var(--color-background-quaternary);
+ background-color: transparent;
+ }
}
.label {
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx
index 7133a1b961..157f83e465 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx
@@ -15,6 +15,12 @@ const options = [
{ id: "right", label: "Right", value: "right" },
];
+const optionsDisabled = [
+ { id: "left", label: "Left", value: "left" },
+ { id: "center", label: "Center", value: "center", disabled: true },
+ { id: "right", label: "Right", value: "right" },
+];
+
const optionsIcon = [
{ id: "left", label: "Left align", value: "left", icon: "text-align-left" },
{
@@ -68,9 +74,24 @@ export default {
parameters: {
controls: {
exclude: ["options", "on-change"],
+ disabled: {
+ control: { type: "boolean" },
+ },
},
},
- render: ({ ...args }) => ,
+ render: (args) => {
+ const [selected, setSelected] = React.useState(args.selected);
+
+ return (
+ {
+ setSelected(value);
+ }}
+ />
+ );
+ },
};
export const Default = {};
@@ -80,3 +101,9 @@ export const WithIcons = {
options: optionsIcon,
},
};
+
+export const WithOptionDisabled = {
+ args: {
+ options: optionsDisabled,
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/controls/select.cljs b/frontend/src/app/main/ui/ds/controls/select.cljs
index d40d7275b8..c31fb45264 100644
--- a/frontend/src/app/main/ui/ds/controls/select.cljs
+++ b/frontend/src/app/main/ui/ds/controls/select.cljs
@@ -9,8 +9,10 @@
[app.main.style :as stl])
(:require
[app.common.data :as d]
+ [app.common.data.macros :as dm]
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
+ [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
@@ -50,15 +52,17 @@
[:map
[:options [:vector {:min 1} schema:option]]
[:class {:optional true} :string]
+ [:wrapper-class {:optional true} :string]
[:disabled {:optional true} :boolean]
[:default-selected {:optional true} :string]
[:empty-to-end {:optional true} [:maybe :boolean]]
[:on-change {:optional true} fn?]
- [:variant {:optional true} [:maybe [:enum "default" "ghost"]]]])
+ [:dropdown-alignment {:optional true} [:maybe [:enum :left :right]]]
+ [:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]])
(mf/defc select*
{::mf/schema schema:select}
- [{:keys [options class disabled default-selected empty-to-end on-change variant] :rest props}]
+ [{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment] :rest props}]
(let [;; NOTE: we use mfu/bean here for transparently handle
;; options provide as clojure data structures or javascript
;; plain objects and lists.
@@ -192,26 +196,40 @@
(some? icon)
dimmed?
- (:dimmed selected-option)]
+ (:dimmed selected-option)
+
+ icon-ref (mf/use-ref nil)
+ icon-id (mf/use-id)]
(mf/with-effect [options]
(mf/set-ref-val! options-ref options))
- [:div {:class (stl/css :select-wrapper)
+ [:div {:class [wrapper-class (stl/css :select-wrapper)]
:on-click on-click
:ref select-ref
:on-blur on-blur}
[:> :button props
[:span {:class (stl/css-case :select-header true
- :header-icon has-icon?)}
+ :header-icon has-icon?
+ :header-icon-only (= variant "icon-only"))}
(when ^boolean has-icon?
- [:> icon* {:icon-id icon
- :size "s"
- :aria-hidden true}])
- [:span {:class (stl/css-case :header-label true
- :header-label-dimmed (or empty-selected-id? dimmed?))}
- (if ^boolean empty-selected-id? "--" label)]]
+ (if (= variant "icon-only")
+ [:> tooltip* {:content label
+ :trigger-ref icon-ref
+ :id (dm/str icon-id "-name")
+ :class (stl/css :option-text)}
+ [:> icon* {:icon-id icon
+ :ref icon-ref
+ :aria-labelledby (dm/str icon-id "-name")}]]
+ [:> icon* {:icon-id icon
+ :size "s"
+ :aria-hidden true}]))
+
+ (when-not ^boolean (= variant "icon-only")
+ [:span {:class (stl/css-case :header-label true
+ :header-label-dimmed (or empty-selected-id? dimmed?))}
+ (if ^boolean empty-selected-id? "--" label)])]
[:> icon* {:icon-id i/arrow-down
:class (stl/css :arrow)
@@ -224,5 +242,6 @@
:options options
:selected selected-id
:focused focused-id
+ :align dropdown-alignment
:empty-to-end empty-to-end
:ref set-option-ref}])]))
diff --git a/frontend/src/app/main/ui/ds/controls/select.scss b/frontend/src/app/main/ui/ds/controls/select.scss
index d52be44549..0cb48866ca 100644
--- a/frontend/src/app/main/ui/ds/controls/select.scss
+++ b/frontend/src/app/main/ui/ds/controls/select.scss
@@ -22,6 +22,7 @@
}
@include use-typography("body-small");
+
position: relative;
display: grid;
grid-template-rows: auto;
@@ -47,7 +48,6 @@
block-size: $sz-32;
inline-size: 100%;
padding: var(--sp-s);
- border: none;
border-radius: $br-8;
outline: $b-1 solid var(--select-outline-color);
border: $b-1 solid var(--select-border-color);
@@ -91,6 +91,7 @@
.header-label {
@include use-typography("body-small");
+
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -109,3 +110,8 @@
grid-template-columns: auto 1fr;
color: var(--select-icon-color);
}
+
+.header-icon-only {
+ grid-template-columns: 1fr;
+ color: var(--select-icon-color);
+}
diff --git a/frontend/src/app/main/ui/ds/controls/select.stories.jsx b/frontend/src/app/main/ui/ds/controls/select.stories.jsx
index 3cf750d5d7..8a2005cd32 100644
--- a/frontend/src/app/main/ui/ds/controls/select.stories.jsx
+++ b/frontend/src/app/main/ui/ds/controls/select.stories.jsx
@@ -9,7 +9,7 @@ import Components from "@target/components";
const { Select } = Components;
-const variants = ["default", "ghost"];
+const variants = ["default", "ghost", "icon-only"];
const options = [
{ id: "option-code", label: "Code" },
@@ -75,3 +75,10 @@ export const EmptyToEnd = {
emptyToEnd: true,
},
};
+
+export const OnlyWithIcons = {
+ args: {
+ options: optionsWithIcons,
+ variant: variants[2],
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs b/frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs
new file mode 100644
index 0000000000..3f9fc2fa8b
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/dropdown_navigation.cljs
@@ -0,0 +1,89 @@
+;; 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.controls.shared.dropdown-navigation
+ (:require
+ [app.util.dom :as dom]
+ [app.util.keyboard :as kbd]
+ [app.util.object :as obj]
+ [rumext.v2 :as mf]))
+
+(defn use-dropdown-navigation
+ "Hook for keyboard navigation in dropdowns.
+
+ Options:
+ - focusable-ids: vector of focusable ids (already filtered)
+ - nodes-ref: ref to a JS object mapping id -> DOM node
+ - on-enter: fn called with focused-id when Enter is pressed
+ - searchable: when true, nil focused-id means search input is focused
+ - search-input-ref: ref to the search input DOM node
+ - on-close: optional fn called when Esc/Tab is pressed"
+
+ [{:keys [focusable-ids nodes-ref on-enter searchable search-input-ref on-close]}]
+ (let [focused-id* (mf/use-state nil)
+ focused-id (deref focused-id*)
+
+ focus-input!
+ (mf/use-fn
+ (mf/deps search-input-ref)
+ (fn []
+ (reset! focused-id* nil)
+ (when-let [input (mf/ref-val search-input-ref)]
+ (dom/focus! input))))
+
+ on-key-down
+ (mf/use-fn
+ (mf/deps focused-id focusable-ids searchable)
+ (fn [event]
+ (cond
+ (kbd/down-arrow? event)
+ (do
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (if (nil? focused-id)
+ (reset! focused-id* (first focusable-ids))
+ (let [idx (or (first (keep-indexed #(when (= %2 focused-id) %1) focusable-ids)) -1)
+ next-idx (mod (inc idx) (count focusable-ids))
+ wrap-to-input? (and ^boolean searchable
+ (= next-idx 0)
+ (= idx (dec (count focusable-ids))))]
+ (if wrap-to-input?
+ (focus-input!)
+ (reset! focused-id* (nth focusable-ids next-idx nil))))))
+
+ (kbd/up-arrow? event)
+ (do
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (if (nil? focused-id)
+ (reset! focused-id* (last focusable-ids))
+ (let [idx (or (first (keep-indexed #(when (= %2 focused-id) %1) focusable-ids)) 0)
+ prev-idx (dec idx)
+ wrap-to-input? (and ^boolean searchable (= prev-idx -1))]
+ (if wrap-to-input?
+ (focus-input!)
+ (reset! focused-id* (nth focusable-ids (mod prev-idx (count focusable-ids)) nil))))))
+
+ (kbd/enter? event)
+ (when focused-id
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (on-enter focused-id))
+
+ (or (kbd/esc? event) (kbd/tab? event))
+ (do
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (reset! focused-id* nil)
+ (when on-close (on-close))))))]
+
+ (mf/with-effect [focused-id]
+ (when (some? focused-id)
+ (when-let [node (obj/get (mf/ref-val nodes-ref) focused-id)]
+ (dom/scroll-into-view-if-needed! node {:block "nearest" :inline "nearest"}))))
+
+ {:focused-id focused-id
+ :focused-id* focused-id*
+ :on-key-down on-key-down}))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.cljs b/frontend/src/app/main/ui/ds/controls/shared/option.cljs
index 0542268bc1..b313bf4263 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/option.cljs
+++ b/frontend/src/app/main/ui/ds/controls/shared/option.cljs
@@ -22,6 +22,12 @@
[:focused {:optional true} :boolean]
[:dimmed {:optional true} :boolean]
[:label {:optional true} :string]
+ [:avatar {:optional true}
+ [:maybe
+ [:map
+ [:size {:optional true} :string]
+ [:organization {:optional true} :any]
+ [:render-fn {:optional true} fn?]]]]
[:aria-label {:optional true} [:maybe :string]]
[:on-click {:optional true} fn?]]
[:fn {:error/message "invalid data: missing required props"}
@@ -33,9 +39,13 @@
(mf/defc option*
{::mf/schema schema:option}
- [{:keys [id ref label icon aria-label on-click selected focused dimmed] :rest props}]
- (let [class (stl/css-case :option true
+ [{:keys [id ref label icon avatar aria-label on-click selected focused dimmed] :rest props}]
+ (let [render-avatar-fn (when avatar
+ (get avatar :render-fn))
+
+ class (stl/css-case :option true
:option-with-icon (some? icon)
+ :option-with-avatar (fn? render-avatar-fn)
:option-selected selected
:option-current focused)]
@@ -57,6 +67,9 @@
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
+ (when (fn? render-avatar-fn)
+ [:> render-avatar-fn {:avatar avatar}])
+
[:span {:class (stl/css-case :option-text true
:option-text-dimmed dimmed)}
label]
diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss
index 0c2462206b..978110d1f3 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/option.scss
+++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss
@@ -14,8 +14,7 @@
--options-empty: var(--color-canvas);
display: grid;
- align-items: center;
- justify-items: start;
+ place-items: center start;
grid-template-columns: 1fr auto;
gap: var(--sp-xs);
width: 100%;
@@ -26,6 +25,7 @@
outline-offset: calc(-1 * $b-1);
background-color: var(--options-bg-color);
color: var(--options-fg-color);
+ cursor: default;
&:hover,
&[aria-selected="true"] {
@@ -37,6 +37,11 @@
grid-template-columns: auto 1fr auto;
}
+.option-with-avatar {
+ grid-template-columns: auto 1fr auto;
+ gap: var(--sp-s);
+}
+
.option-text {
white-space: nowrap;
overflow: hidden;
@@ -56,6 +61,7 @@
.option-current {
--options-outline-color: var(--color-accent-primary);
+
outline: $b-1 solid var(--options-outline-color);
}
@@ -63,3 +69,8 @@
--options-fg-color: var(--color-accent-primary);
--options-icon-fg-color: var(--color-accent-primary);
}
+
+.option-check {
+ color: var(--token-options-icon-fg-color);
+ min-width: var(--sp-l);
+}
diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs
index 0191891398..3ed3d6b3a3 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs
+++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs
@@ -9,10 +9,8 @@
[app.main.style :as stl])
(:require
[app.common.data :as d]
- [app.common.weak :refer [weak-key]]
- [app.main.ui.ds.controls.shared.option :refer [option*]]
- [app.main.ui.ds.controls.shared.token-option :refer [token-option*]]
- [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
+ [app.main.ui.ds.controls.shared.render-option :refer [render-option]]
+ [app.main.ui.ds.foundations.assets.icon :as i]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -20,16 +18,30 @@
[:and :string
[:fn {:error/message "invalid data: invalid icon"} #(contains? i/icon-list %)]])
+(def ^:private
+ xf:filter-blank-id
+ (filter #(str/blank? (get % :id))))
+
+(def ^:private
+ xf:filter-non-blank-id
+ (remove #(str/blank? (get % :id))))
+
(def schema:option
"A schema for the option data structure expected to receive on props
for the `options-dropdown*` component."
[:map
[:id {:optional true} :string]
[:resolved-value {:optional true}
- [:or :int :string :float]]
+ [:maybe [:or :int :string :float]]]
[:name {:optional true} :string]
+ [:value {:optional true} :keyword]
[:icon {:optional true} schema:icon-list]
[:label {:optional true} :string]
+ [:avatar {:optional true}
+ [:map
+ [:size {:optional true} :string]
+ [:organization {:optional true} :any]
+ [:render-fn {:optional true} fn?]]]
[:aria-label {:optional true} :string]])
(def ^:private schema:options-dropdown
@@ -44,66 +56,6 @@
[:empty-to-end {:optional true} [:maybe :boolean]]
[:align {:optional true} [:maybe [:enum :left :right]]]])
-(def ^:private
- xf:filter-blank-id
- (filter #(str/blank? (get % :id))))
-
-(def ^:private
- xf:filter-non-blank-id
- (remove #(str/blank? (get % :id))))
-
-(defn- render-option
- [option ref on-click selected focused]
- (let [id (get option :id)
- name (get option :name)
- type (get option :type)]
-
- (mf/html
- (case type
- :group
- [:li {:class (stl/css :group-option)
- :role "presentation"
- :key (weak-key option)}
- [:> icon*
- {:icon-id i/arrow-down
- :size "m"
- :class (stl/css :option-check)
- :aria-hidden (when name true)}]
- (d/name name)]
-
- :separator
- [:hr {:key (weak-key option) :class (stl/css :option-separator)}]
-
- :empty
- [:li {:key (weak-key option) :class (stl/css :option-empty) :role "presentation"}
- (get option :label)]
-
- ;; Token option
- :token
- [:> token-option* {:selected (= id selected)
- :key (weak-key option)
- :id id
- :name name
- :resolved (get option :resolved-value)
- :ref ref
- :role "option"
- :focused (= id focused)
- :on-click on-click}]
-
- ;; Normal option
- [:> option* {:selected (= id selected)
- :key (weak-key option)
- :id id
- :label (get option :label)
- :aria-label (get option :aria-label)
- :icon (get option :icon)
- :ref ref
- :role "option"
- :focused (= id focused)
- :dimmed (true? (:dimmed option))
- :on-click on-click}]))))
-
-
(mf/defc options-dropdown*
{::mf/schema schema:options-dropdown}
[{:keys [ref on-click options selected focused empty-to-end align wrapper-ref class] :rest props}]
@@ -140,4 +92,4 @@
[:hr {:class (stl/css :option-separator)}])
(for [option options-blank]
- (render-option option ref on-click selected focused))])]))
+ (render-option option ref on-click selected focused))])]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
index b7c3d2e40a..0041dc1a9c 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
+++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss
@@ -25,9 +25,13 @@
padding-block: var(--sp-xs);
margin-block-end: 0;
max-block-size: $sz-400;
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
z-index: var(--z-index-dropdown);
+ box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark);
+
+ &:focus {
+ outline: none;
+ }
}
.left-align {
@@ -40,23 +44,5 @@
.option-separator {
border: $b-1 solid var(--options-dropdown-border-color);
- margin-block-start: var(--sp-xs);
- margin-block-end: var(--sp-xs);
-}
-
-.group-option,
-.option-empty {
- @include use-typography("body-small");
- display: flex;
- align-items: center;
- gap: var(--sp-xs);
- color: var(--color-foreground-secondary);
- padding-inline: var(--sp-s);
- block-size: var(--sp-xxxl);
-}
-
-.option-empty {
- justify-content: center;
- text-align: center;
- padding: 0 px2rem(40);
+ margin-block: var(--sp-xs) var(--sp-xs);
}
diff --git a/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs
new file mode 100644
index 0000000000..ef015467be
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/render_option.cljs
@@ -0,0 +1,69 @@
+;; 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.controls.shared.render-option
+ (:require-macros
+ [app.main.style :as stl])
+ (:require
+ [app.common.data :as d]
+ [app.common.weak :refer [weak-key]]
+ [app.main.ui.ds.controls.shared.option :refer [option*]]
+ [app.main.ui.ds.controls.shared.token-option :refer [token-option*]]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
+ [rumext.v2 :as mf]))
+
+(defn render-option
+ [option ref on-click selected focused]
+ (let [id (get option :id)
+ name (get option :name)
+ type (get option :type)]
+
+ (mf/html
+ (case type
+ :group
+ [:li {:class (stl/css :group-option)
+ :role "presentation"
+ :key (weak-key option)}
+ [:> icon*
+ {:icon-id i/arrow-down
+ :size "m"
+ :class (stl/css :option-check)
+ :aria-hidden (when name true)}]
+ (d/name name)]
+
+ :separator
+ [:hr {:key (weak-key option) :class (stl/css :option-separator)}]
+
+ :empty
+ [:li {:key (weak-key option) :class (stl/css :option-empty) :role "presentation"}
+ (get option :label)]
+
+ ;; Token option
+ :token
+ [:> token-option* {:selected (= id selected)
+ :key (weak-key option)
+ :id id
+ :name name
+ :resolved (get option :resolved-value)
+ :value (get option :value)
+ :ref ref
+ :role "option"
+ :focused (= id focused)
+ :on-click on-click}]
+
+ ;; Normal option
+ [:> option* {:selected (= id selected)
+ :key (weak-key option)
+ :id id
+ :label (get option :label)
+ :aria-label (get option :aria-label)
+ :icon (get option :icon)
+ :avatar (get option :avatar)
+ :ref ref
+ :role "option"
+ :focused (= id focused)
+ :dimmed (true? (:dimmed option))
+ :on-click on-click}]))))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/controls/shared/render_option.scss b/frontend/src/app/main/ui/ds/controls/shared/render_option.scss
new file mode 100644
index 0000000000..232efb42a5
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/render_option.scss
@@ -0,0 +1,40 @@
+// 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/_borders.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/typography.scss" as *;
+@use "ds/_utils.scss" as *;
+
+.left-align {
+ inset-inline-start: var(--dropdown-offset, 0);
+}
+
+.right-align {
+ inset-inline-end: var(--dropdown-offset, 0);
+}
+
+.option-separator {
+ border: $b-1 solid var(--options-dropdown-border-color);
+ margin-block: var(--sp-xs) var(--sp-xs);
+}
+
+.group-option,
+.option-empty {
+ @include use-typography("body-small");
+
+ display: flex;
+ align-items: center;
+ gap: var(--sp-xs);
+ color: var(--color-foreground-secondary);
+ padding-inline: var(--sp-s);
+ block-size: var(--sp-xxxl);
+}
+
+.option-check {
+ color: var(--token-options-icon-fg-color);
+ min-width: var(--sp-l);
+}
diff --git a/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs
new file mode 100644
index 0000000000..6e5d8e7d51
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.cljs
@@ -0,0 +1,149 @@
+;; 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.controls.shared.searchable-options-dropdown
+ (:require-macros
+ [app.main.style :as stl])
+ (:require
+ [app.common.data :as d]
+ [app.main.ui.ds.controls.input :as ds]
+ [app.main.ui.ds.controls.shared.dropdown-navigation :refer [use-dropdown-navigation]]
+ [app.main.ui.ds.controls.shared.render-option :refer [render-option]]
+ [app.main.ui.ds.foundations.assets.icon :as i]
+ [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
+ [app.util.dom :as dom]
+ [app.util.i18n :as i18n :refer [tr]]
+ [app.util.object :as obj]
+ [app.util.timers :as ts]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(def ^:private schema:icon-list
+ [:and :string
+ [:fn {:error/message "invalid data: invalid icon"} #(contains? i/icon-list %)]])
+
+(def schema:option
+ "A schema for the option data structure expected to receive on props
+ for the `options-dropdown*` component."
+ [:map
+ [:id {:optional true} :string]
+ [:resolved-value {:optional true}
+ [:or :int :string :float :map]]
+ [:name {:optional true} :string]
+ [:value {:optional true} :keyword]
+ [:icon {:optional true} schema:icon-list]
+ [:label {:optional true} :string]
+ [:aria-label {:optional true} :string]])
+
+(def ^:private schema:options-dropdown
+ [:map
+ [:ref {:optional true} fn?]
+ [:class {:optional true} :string]
+ [:wrapper-ref {:optional true} :any]
+ [:placeholder {:optional true} :string]
+ [:on-click fn?]
+ [:options [:vector schema:option]]
+ [:selected {:optional true} :any]
+ [:align {:optional true} [:maybe [:enum :left :right]]]])
+
+(mf/defc searchable-options-dropdown*
+ {::mf/schema schema:options-dropdown}
+ [{:keys [on-click options selected align class placeholder] :rest props}]
+ (let [align (d/nilv align :left)
+
+ search* (mf/use-state "")
+ search (deref search*)
+ search-input-ref (mf/use-ref nil)
+
+ list-ref (mf/use-ref nil)
+ nodes-ref (mf/use-ref nil)
+
+ filtered-options
+ (mf/with-memo [options search]
+ (if (seq search)
+ (filterv (fn [opt]
+ (or (not= :token (:type opt))
+ (str/includes? (str/lower (:name opt ""))
+ (str/lower search))))
+ options)
+ options))
+
+ focusable-ids
+ (mf/with-memo [filtered-options]
+ (mapv :id (csu/focusable-options filtered-options)))
+
+ on-search-change
+ (mf/use-fn
+ (fn [event]
+ (reset! search* (dom/get-target-val event))))
+
+ set-option-ref
+ (mf/use-fn
+ (fn [node]
+ (when node
+ (let [state (d/nilv (mf/ref-val nodes-ref) #js {})
+ id (dom/get-data node "id")]
+ (mf/set-ref-val! nodes-ref (obj/set! state id node))
+ (fn []
+ (let [state (d/nilv (mf/ref-val nodes-ref) #js {})]
+ (mf/set-ref-val! nodes-ref (obj/unset! state id))))))))
+
+ {:keys [focused-id focused-id* on-key-down]}
+ (use-dropdown-navigation
+ {:focusable-ids focusable-ids
+ :nodes-ref nodes-ref
+ :on-enter (fn [id]
+ (when-let [node (obj/get (mf/ref-val nodes-ref) id)]
+ (.click node)))
+ :searchable true
+ :search-input-ref search-input-ref
+ :on-close nil})
+
+ on-click-inner
+ (mf/use-fn
+ (mf/deps on-click)
+ (fn [event]
+ (dom/stop-propagation event)
+ (on-click event)))
+
+ list-props
+ (mf/spread-props props
+ {:class [class (stl/css-case :option-list true
+ :left-align (= align :left)
+ :right-align (= align :right))]
+ :ref list-ref
+ :tab-index "-1"
+ :role "listbox"
+ :on-key-down on-key-down})]
+
+ (mf/with-effect []
+ (ts/schedule 0
+ #(if (mf/ref-val search-input-ref)
+ (dom/focus! (mf/ref-val search-input-ref))
+ (when-let [list (mf/ref-val list-ref)]
+ (dom/focus! list)))))
+
+ (mf/with-effect [focused-id]
+ (when (some? focused-id)
+ (when-let [list (mf/ref-val list-ref)]
+ (when-not (dom/active? list)
+ (dom/focus! list)))
+ (when-let [node (obj/get (mf/ref-val nodes-ref) focused-id)]
+ (dom/scroll-into-view-if-needed! node {:block "nearest" :inline "nearest"}))))
+
+ [:> :ul list-props
+ [:li {:class (stl/css :option-search)
+ :role "presentation"}
+ [:> ds/input* {:placeholder (or placeholder (tr "dashboard.search-placeholder"))
+ :value search
+ :ref search-input-ref
+ :variant "comfortable"
+ :on-change on-search-change
+ :on-click #(reset! focused-id* nil)
+ :on-key-down on-key-down}]]
+
+ (for [option filtered-options]
+ (render-option option set-option-ref on-click-inner selected focused-id))]))
diff --git a/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss
new file mode 100644
index 0000000000..cbeae912d8
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/shared/searchable_options_dropdown.scss
@@ -0,0 +1,47 @@
+// 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/_borders.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/typography.scss" as *;
+@use "ds/_utils.scss" as *;
+
+.option-list {
+ --options-dropdown-icon-fg-color: var(--color-foreground-secondary);
+ --options-dropdown-bg-color: var(--color-background-tertiary);
+ --options-dropdown-outline-color: none;
+ --options-dropdown-border-color: var(--color-background-quaternary);
+
+ position: absolute;
+ inset-block-start: $sz-36;
+ inline-size: var(--dropdown-width, 100%);
+ transform: translateX(var(--dropdown-translate-distance, 0));
+ background-color: var(--options-dropdown-bg-color);
+ border-radius: $br-8;
+ border: $b-1 solid var(--options-dropdown-border-color);
+ padding-block: var(--sp-xs);
+ margin-block-end: 0;
+ max-block-size: $sz-400;
+ overflow: hidden auto;
+ z-index: var(--z-index-dropdown);
+ box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark);
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.left-align {
+ inset-inline-start: var(--dropdown-offset, 0);
+}
+
+.right-align {
+ inset-inline-end: var(--dropdown-offset, 0);
+}
+
+.option-search {
+ padding: var(--sp-xs);
+}
diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs
index 11667ba8f8..2d989bca02 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs
+++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.cljs
@@ -18,7 +18,8 @@
[:map
[:id {:optiona true} :string]
[:ref some?]
- [:resolved {:optional true} [:or :int :string :float]]
+ [:resolved {:optional true} [:maybe [:or :int :string :float :map]]]
+ [:value {:optional true} [:maybe [:or :int :string :float :map]]]
[:name {:optional true} :string]
[:on-click {:optional true} fn?]
[:selected {:optional true} :boolean]
@@ -26,7 +27,7 @@
(mf/defc token-option*
{::mf/schema schema:token-option}
- [{:keys [id name on-click selected ref focused resolved] :rest props}]
+ [{:keys [id name on-click selected ref focused resolved value] :rest props}]
(let [internal-id (mf/use-id)
id (d/nilv id internal-id)
element-ref (mf/use-ref nil)]
@@ -55,10 +56,14 @@
:trigger-ref element-ref
:id (dm/str id "-name")
:class (stl/css :option-text)}
- ;; Add ellipsis
+
[:span {:aria-labelledby (dm/str id "-name")
+ :class (stl/css :option-name)
:ref element-ref}
name]]
- (when resolved
- [:> :span {:class (stl/css :option-pill)}
- resolved])]))
+ (when (and resolved (not (map? resolved)))
+ [:span {:class (stl/css :option-pill)}
+ resolved])
+ (when (and (nil? resolved) value)
+ [:span {:class (stl/css :option-pill)}
+ "--"])]))
diff --git a/frontend/src/app/main/ui/ds/controls/shared/token_option.scss b/frontend/src/app/main/ui/ds/controls/shared/token_option.scss
index 884ddfea54..14f4c6b3a8 100644
--- a/frontend/src/app/main/ui/ds/controls/shared/token_option.scss
+++ b/frontend/src/app/main/ui/ds/controls/shared/token_option.scss
@@ -7,15 +7,17 @@
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as *;
+@use "ds/mixins.scss" as *;
.token-option {
--token-options-fg-color: var(--color-foreground-primary);
--token-options-bg-color: unset;
--token-options-empty: var(--color-canvas);
+
@include use-typography("body-small");
+
display: grid;
- align-items: center;
- justify-items: start;
+ place-items: center start;
grid-template-columns: 1fr auto;
gap: $sz-6;
width: 100%;
@@ -26,10 +28,10 @@
outline-offset: calc(-1 * $b-1);
background-color: var(--token-options-bg-color);
color: var(--token-options-fg-color);
- overflow: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+
&:hover,
&[aria-selected="true"] {
--token-options-bg-color: var(--color-background-quaternary);
@@ -51,11 +53,13 @@
.option-current {
--token-options-outline-color: var(--color-accent-primary);
+
outline: $b-1 solid var(--token-options-outline-color);
}
.option-pill {
@include use-typography("code-font");
+
color: var(--color-foreground-secondary);
background-color: var(--color-background-primary);
border-radius: $br-6;
@@ -75,3 +79,7 @@
color: var(--token-options-icon-fg-color);
min-width: var(--sp-l);
}
+
+.option-name {
+ @include text-ellipsis;
+}
diff --git a/frontend/src/app/main/ui/ds/controls/switch.scss b/frontend/src/app/main/ui/ds/controls/switch.scss
index c5abbf3a00..01eae97f00 100644
--- a/frontend/src/app/main/ui/ds/controls/switch.scss
+++ b/frontend/src/app/main/ui/ds/controls/switch.scss
@@ -13,10 +13,8 @@
.switch {
--switch-label-foreground-color: var(--color-foreground-primary);
-
--switch-track-outline-color: none;
--switch-track-shadow: inset 0 1px 2px var(--color-shadow-light);
-
--switch-thumb-shadow: 0 1px 2px var(--color-shadow-light);
display: grid;
@@ -29,7 +27,6 @@
&.off {
--switch-track-justify-content: start;
--switch-track-background-color: var(--color-foreground-secondary);
-
--switch-thumb-width: #{px2rem(14)};
--switch-thumb-height: #{px2rem(14)};
--switch-thumb-background-color: var(--color-accent-off);
@@ -39,7 +36,6 @@
&.neutral {
--switch-track-justify-content: center;
--switch-track-background-color: var(--color-accent-tertiary);
-
--switch-thumb-width: #{px2rem(14)};
--switch-thumb-height: #{px2rem(4)};
--switch-thumb-background-color: var(--color-accent-off);
@@ -49,7 +45,6 @@
&.on {
--switch-track-justify-content: end;
--switch-track-background-color: var(--color-accent-tertiary);
-
--switch-thumb-width: #{px2rem(14)};
--switch-thumb-height: #{px2rem(14)};
--switch-thumb-background-color: var(--color-accent-off);
@@ -58,24 +53,21 @@
&[disabled] {
pointer-events: none;
+
--switch-label-foreground-color: var(--color-foreground-secondary);
-
--switch-track-shadow: none;
-
--switch-thumb-shadow: none;
}
&.off[disabled] {
--switch-track-background-color: var(--color-background-primary);
--switch-track-border-color: var(--color-background-disabled);
-
--switch-thumb-background-color: var(--color-background-disabled);
}
&.on[disabled],
&.neutral[disabled] {
--switch-track-background-color: var(--color-background-disabled);
-
--switch-thumb-background-color: var(--color-background-primary);
}
@@ -90,6 +82,7 @@
.switch-label {
@include t.use-typography("body-small");
+
color: var(--switch-label-foreground-color);
user-select: none;
}
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss
index 1112a3f816..97f1a12fda 100644
--- a/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss
+++ b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss
@@ -11,6 +11,7 @@
--hint-color: var(--color-foreground-secondary);
@include use-typography("body-small");
+
color: var(--hint-color);
}
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs
index 58a3202c80..c0ee6f245e 100644
--- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs
+++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs
@@ -37,6 +37,8 @@
has-hint hint-type
max-length variant
slot-start slot-end
+ data-option-focused
+ input-wrapper-ref
aria-label] :rest props} ref]
(let [input-ref (mf/use-ref)
type (d/nilv type "text")
@@ -74,7 +76,9 @@
(dom/select-node input-node)
(dom/focus! input-node))))]
- [:div {:class [inside-class class]}
+ [:div {:class [inside-class class]
+ :ref input-wrapper-ref
+ :data-option-focused data-option-focused}
(when (some? slot-start)
slot-start)
(when (some? icon)
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
index 80068f0c2b..adc4f5a301 100644
--- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
+++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
@@ -22,7 +22,6 @@
align-items: center;
position: relative;
inline-size: 100%;
-
background: var(--input-bg-color);
border-radius: $br-8;
padding: 0 var(--input-padding-size, var(--sp-s));
@@ -41,6 +40,11 @@
--input-bg-color: var(--color-background-primary);
--input-outline-color: var(--color-background-quaternary);
}
+
+ &[data-option-focused="true"]:has(*:focus-visible) {
+ --input-bg-color: var(--color-background-tertiary);
+ --input-outline-color: none;
+ }
}
.variant-dense,
@@ -80,12 +84,10 @@
border: none;
background: none;
inline-size: 100%;
-
font-family: inherit;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
-
color: var(--input-fg-color);
&:focus-visible {
@@ -102,7 +104,6 @@
&:is(:autofill, :autofill:hover, :autofill:focus, :autofill:active) {
-webkit-text-fill-color: var(--input-fg-color);
- -webkit-background-clip: text;
background-clip: text;
caret-color: var(--input-bg-color);
}
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/label.scss b/frontend/src/app/main/ui/ds/controls/utilities/label.scss
index 405beb6c6f..4ba6a988dc 100644
--- a/frontend/src/app/main/ui/ds/controls/utilities/label.scss
+++ b/frontend/src/app/main/ui/ds/controls/utilities/label.scss
@@ -13,6 +13,7 @@
--label-optional-color: var(--color-foreground-secondary);
@include use-typography("body-small");
+
color: var(--label-color);
display: flex;
gap: var(--sp-xs);
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs
index 7af90350e4..86146fe36b 100644
--- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs
+++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.cljs
@@ -25,22 +25,29 @@
[:property {:optional true} [:maybe :string]]
[:value :any]
[:disabled {:optional true} :boolean]
+ [:is-open {:optional true} :boolean]
[:slot-start {:optional true} [:maybe some?]]
[:on-click {:optional true} fn?]
[:on-token-key-down fn?]
[:on-blur {:optional true} fn?]
[:on-focus {:optional true} fn?]
+ [:tooltip-placement {:optional true}
+ [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]
[:detach-token fn?]])
(mf/defc token-field*
{::mf/schema schema:token-field}
[{:keys [id label value slot-start disabled class
- on-click on-token-key-down on-blur detach-token
- token-wrapper-ref token-detach-btn-ref on-focus property]}]
+ on-click on-token-key-down on-blur detach-token tooltip-placement
+ token-wrapper-ref token-detach-btn-ref on-focus property is-open
+ token-has-errors]}]
(let [set-active? (some? id)
- content (if set-active?
- label
- (tr "ds.inputs.token-field.no-active-token-option" label))
+
+ content (cond
+ token-has-errors (tr "workspace.tokens.ref-not-valid")
+ (not set-active?) (tr "ds.inputs.token-field.no-active-token-option" label)
+ :else label)
+
default-id (mf/use-id)
id (d/nilv id default-id)
pill-ref (mf/use-ref nil)
@@ -77,20 +84,24 @@
[:button {:on-click on-click
:ref pill-ref
:class (stl/css-case :pill true
- :no-set-pill (not set-active?)
+ :no-set-pill (or (not set-active?)
+ token-has-errors)
:pill-disabled disabled)
:disabled disabled
:aria-labelledby (dm/str id "-pill")
:on-key-down on-token-key-down}
value
- (when-not set-active?
+ (when (or (not set-active?)
+ token-has-errors)
[:div {:class (stl/css :pill-dot)}])]]]
(when-not ^boolean disabled
[:> icon-button* {:variant "ghost"
- :class (stl/css :invisible-button)
+ :class (stl/css-case :invisible-button true
+ :invisible-btn-dropdown-open is-open)
:tooltip-class (stl/css :button-tooltip)
+ :tooltip-placement tooltip-placement
:icon i/broken-link
:ref token-detach-btn-ref
- :aria-label (tr "ds.inputs.token-field.detach-token")
+ :aria-label (tr "token-actions.detach-token")
:on-click detach-token}])]]))
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss
index e96c5b583e..4cb3a61a0d 100644
--- a/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss
+++ b/frontend/src/app/main/ui/ds/controls/utilities/token_field.scss
@@ -18,10 +18,10 @@
--token-field-outline-color: none;
--token-field-height: var(--sp-xxxl);
--token-field-margin: unset;
+
display: inline-flex;
column-gap: var(--sp-xs);
align-items: center;
- position: relative;
inline-size: 100%;
background: var(--token-field-bg-color);
border-radius: $br-8;
@@ -38,6 +38,7 @@
--token-field-outline-color: var(--color-accent-primary);
}
}
+
.token-field-wrapper {
inline-size: 100%;
}
@@ -48,8 +49,10 @@
.token-field-disabled {
user-select: none;
+
--token-field-bg-color: var(--color-background-primary);
--token-field-outline-color: var(--color-background-quaternary);
+
&:hover {
--token-field-bg-color: var(--color-background-primary);
--token-field-outline-color: var(--color-background-quaternary);
@@ -60,8 +63,10 @@
--pill-border-color: var(--color-token-border);
--pill-bg-color: var(--color-background-tertiary);
--pill-fg-color: var(--color-token-foreground);
+
@include t.use-typography("code-font");
- @include textEllipsis;
+ @include text-ellipsis;
+
display: block;
block-size: var(--sp-xxl);
inline-size: fit-content;
@@ -72,24 +77,29 @@
border-radius: $br-6;
padding-inline: $sz-6;
max-inline-size: 100%;
+
&:hover {
--pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary);
--pill-border-color: var(--color-token-foreground);
}
+
&:focus-visible {
--pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary);
--pill-border-color: var(--color-accent-primary);
+
outline: none;
}
}
.pill-disabled {
user-select: none;
+
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
--pill-border-color: var(--color-token-border);
+
&:hover {
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
@@ -101,7 +111,9 @@
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
--pill-border-color: var(--color-token-border);
+
position: relative;
+
&:hover {
--pill-bg-color: none;
--pill-fg-color: var(--color-foreground-secondary);
@@ -127,16 +139,24 @@
inset-block-start: 0;
opacity: var(--opacity-button);
background-color: var(--color-background-quaternary);
+
&:hover {
background-color: var(--color-background-quaternary);
+
--opacity-button: 1;
}
+
&:focus {
background-color: var(--color-background-quaternary);
+
--opacity-button: 1;
}
}
+.invisible-btn-dropdown-open {
+ --opacity-button: 0;
+}
+
.content-wrapper {
inline-size: 100%;
}
diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
index 0e83173bee..f2ccc02d4a 100644
--- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
+++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
@@ -245,12 +245,19 @@
(def ^:icon-id status-update "status-update")
(def ^:icon-id status-wrong "status-wrong")
(def ^:icon-id stroke-arrow "stroke-arrow")
+(def ^:icon-id stroke-center "stroke-center")
(def ^:icon-id stroke-circle "stroke-circle")
+(def ^:icon-id stroke-dashed "stroke-dashed")
(def ^:icon-id stroke-diamond "stroke-diamond")
+(def ^:icon-id stroke-dotted "stroke-dotted")
+(def ^:icon-id stroke-inside "stroke-inside")
+(def ^:icon-id stroke-mixed "stroke-mixed")
+(def ^:icon-id stroke-outside "stroke-outside")
(def ^:icon-id stroke-rectangle "stroke-rectangle")
(def ^:icon-id stroke-rounded "stroke-rounded")
(def ^:icon-id stroke-size "stroke-size")
(def ^:icon-id stroke-squared "stroke-squared")
+(def ^:icon-id stroke-solid "stroke-solid")
(def ^:icon-id stroke-triangle "stroke-triangle")
(def ^:icon-id svg "svg")
(def ^:icon-id swatches "swatches")
@@ -315,22 +322,16 @@
(mf/defc icon*
{::mf/schema schema:icon}
[{:keys [icon-id size class] :rest props}]
- (let [props (mf/spread-props props
- {:class [class (stl/css :icon)]
- :width icon-size-m
- :height icon-size-m})
-
- size-px (cond (= size "l") icon-size-l
+ (let [size-px (cond (= size "l") icon-size-l
(= size "s") icon-size-s
:else icon-size-m)
- offset (if (or (= size "s") (= size "m"))
- (/ (- icon-size-m size-px) 2)
- 0)]
+ props (mf/spread-props props
+ {:class [class (stl/css :icon)]
+ :width size-px
+ :height size-px})]
[:> :svg props
[:use {:href (dm/str "#icon-" icon-id)
:width size-px
- :height size-px
- :x offset
- :y offset}]]))
+ :height size-px}]]))
diff --git a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs
index 428d582e97..56f5b076a5 100644
--- a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs
+++ b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs
@@ -20,6 +20,7 @@
(def ^:svg-id logo-error-screen "logo-error-screen")
(def ^:svg-id logo-subscription "logo-subscription")
(def ^:svg-id logo-subscription-light "logo-subscription-light")
+(def ^:svg-id nitrate-welcome "nitrate-welcome")
(def ^:svg-id marketing-arrows "marketing-arrows")
(def ^:svg-id marketing-exchange "marketing-exchange")
(def ^:svg-id marketing-file "marketing-file")
@@ -38,3 +39,4 @@
(assert (contains? raw-svg-list id) "invalid raw svg id")
[:> "svg" props
[:use {:href (dm/str "#asset-" id)}]])
+
diff --git a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss
index 207b2236af..a459eb5e74 100644
--- a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss
+++ b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss
@@ -5,6 +5,6 @@
// Copyright (c) KALEIDOS INC
.token-icon {
- fill: currentColor;
+ fill: currentcolor;
stroke: none;
}
diff --git a/frontend/src/app/main/ui/ds/layers/layer_button.cljs b/frontend/src/app/main/ui/ds/layers/layer_button.cljs
index 315ad56e88..268e33f961 100644
--- a/frontend/src/app/main/ui/ds/layers/layer_button.cljs
+++ b/frontend/src/app/main/ui/ds/layers/layer_button.cljs
@@ -27,8 +27,7 @@
[{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}]
(let [button-props (mf/spread-props props
{:class [class (stl/css-case :layer-button true
- :layer-button--expandable is-expandable
- :layer-button--expanded expanded)]
+ :layer-button-expanded expanded)]
:type "button"
:on-click on-toggle-expand
:on-context-menu on-context-menu})]
diff --git a/frontend/src/app/main/ui/ds/layers/layer_button.scss b/frontend/src/app/main/ui/ds/layers/layer_button.scss
index 56e59e8acf..2850af269f 100644
--- a/frontend/src/app/main/ui/ds/layers/layer_button.scss
+++ b/frontend/src/app/main/ui/ds/layers/layer_button.scss
@@ -16,9 +16,7 @@
display: flex;
justify-content: space-between;
-
block-size: var(--layer-button-block-size);
-
background: var(--layer-button-background);
color: var(--layer-button-text);
}
@@ -27,17 +25,15 @@
@include use-typography("body-small");
appearance: none;
-
flex: 1;
display: flex;
align-items: center;
-
border: none;
background: none;
color: inherit;
}
-.layer-button--expanded {
+.layer-button-expanded {
& .layer-button-name {
color: var(--color-foreground-primary);
}
diff --git a/frontend/src/app/main/ui/ds/layout/tab_switcher.scss b/frontend/src/app/main/ui/ds/layout/tab_switcher.scss
index 90af8cf778..b8e7dd8f8d 100644
--- a/frontend/src/app/main/ui/ds/layout/tab_switcher.scss
+++ b/frontend/src/app/main/ui/ds/layout/tab_switcher.scss
@@ -10,15 +10,14 @@
.tabs {
--tabs-bg-color: var(--color-background-secondary);
+
display: grid;
grid-template-rows: auto 1fr;
}
.padding-wrapper {
- padding-inline-start: var(--tabs-nav-padding-inline-start, 0);
- padding-inline-end: var(--tabs-nav-padding-inline-end, 0);
- padding-block-start: var(--tabs-nav-padding-block-start, 0);
- padding-block-end: var(--tabs-nav-padding-block-end, 0);
+ padding-inline: var(--tabs-nav-padding-inline-start, 0) var(--tabs-nav-padding-inline-end, 0);
+ padding-block: var(--tabs-nav-padding-block-start, 0) var(--tabs-nav-padding-block-end, 0);
}
// TAB NAV
@@ -44,6 +43,7 @@
grid-auto-flow: column;
gap: var(--sp-xxs);
width: 100%;
+
// Removing margin bottom from default ul
margin-block-end: 0;
border-radius: $br-8;
@@ -68,7 +68,6 @@
height: $sz-32;
border: none;
border-radius: $br-8;
- padding: 0 var(--sp-s);
outline: $b-1 solid var(--tabs-item-outline-color);
display: grid;
grid-auto-flow: column;
@@ -89,6 +88,7 @@
.tab-text {
@include use-typography("headline-small");
+
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -102,6 +102,7 @@
.tab-panel {
--tab-panel-outline-color: none;
+
&:focus {
outline: none;
}
diff --git a/frontend/src/app/main/ui/ds/mixins.scss b/frontend/src/app/main/ui/ds/mixins.scss
index 32e2dce255..f43b690c02 100644
--- a/frontend/src/app/main/ui/ds/mixins.scss
+++ b/frontend/src/app/main/ui/ds/mixins.scss
@@ -8,7 +8,7 @@
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
-@mixin textEllipsis {
+@mixin text-ellipsis {
display: block;
max-width: 99%;
overflow: hidden;
@@ -16,7 +16,7 @@
white-space: nowrap;
}
-@mixin twoLineTextEllipsis {
+@mixin two-line-text-ellipsis {
max-width: 99%;
overflow: hidden;
text-overflow: ellipsis;
@@ -33,6 +33,7 @@
/// @param {Length} $border - Inner transparent border size
/// @param {Bool} $include-selection - Include ::selection styles
/// @param {Bool} $include-placeholder - Include placeholder styles
+
@mixin custom-scrollbar(
$thumb-color: #aab5ba4d,
$thumb-hover-color: #aab5bab3,
@@ -84,12 +85,7 @@
@if $include-placeholder {
&::placeholder {
@include t.use-typography("body-small");
- color: var(--color-foreground-secondary);
- }
- // Legacy webkit
- &::-webkit-input-placeholder {
- @include t.use-typography("body-small");
color: var(--color-foreground-secondary);
}
}
diff --git a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs
index efa97a9247..a8fbe76c0e 100644
--- a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs
+++ b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.cljs
@@ -9,7 +9,6 @@
[app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
- [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -29,13 +28,11 @@
[:level [:enum :default :info :warning :error :success]]
[:type [:enum :toast :context]]
[:appearance {:optional true} [:enum :neutral :ghost]]
- [:is-html {:optional true} :boolean]
- [:show-detail {:optional true} [:maybe :boolean]]
- [:on-toggle-detail {:optional true} [:maybe fn?]]])
+ [:is-html {:optional true} :boolean]])
(mf/defc notification-pill*
{::mf/schema schema:notification-pill}
- [{:keys [level type is-html appearance detail children show-detail on-toggle-detail]}]
+ [{:keys [level type is-html appearance detail children]}]
(let [class (stl/css-case :appearance-neutral (= appearance :neutral)
:appearance-ghost (= appearance :ghost)
:with-detail detail
@@ -60,16 +57,7 @@
children)]
(when detail
- [:div {:class (stl/css :error-detail)}
- [:div {:class (stl/css :error-detail-title)}
- [:> icon-button*
- {:icon (if show-detail "arrow-down" "arrow")
- :aria-label (tr "workspace.notification-pill.detail")
- :icon-class (stl/css :expand-icon)
- :variant "action"
- :on-click on-toggle-detail}]
- [:div {:on-click on-toggle-detail}
- (tr "workspace.notification-pill.detail")]]
- (when show-detail
- [:div {:class (stl/css :error-detail-content)
- :dangerouslySetInnerHTML #js {:__html detail}}])])]))
+ [:details {:class (stl/css :error-detail)}
+ [:summary {:class (stl/css :error-detail-summary)} (tr "workspace.notification-pill.detail")]
+ [:div {:class (stl/css :error-detail-content)
+ :dangerouslySetInnerHTML #js {:__html detail}}]])]))
diff --git a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss
index 3cf7b1bd2c..0124798e98 100644
--- a/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss
+++ b/frontend/src/app/main/ui/ds/notifications/shared/notification_pill.scss
@@ -22,10 +22,8 @@
border: $b-1 solid var(--notification-border-color);
border-radius: $br-8;
padding: var(--notification-padding);
-
display: flex;
gap: var(--sp-s);
-
color: var(--notification-fg-color);
// Targets the potential links included by the creator in the children props.
@@ -100,20 +98,38 @@
}
.error-detail {
- overflow: auto;
+ list-style: none;
+ padding-inline-start: var(--sp-xxl);
}
-.error-detail-title {
- display: flex;
- align-items: center;
+.error-detail-summary {
+ list-style: none;
cursor: pointer;
-}
+ position: relative;
-.expand-icon {
- --icon-fill-color: var(--color-foreground-primary);
- --icon-stroke-color: var(--color-foreground-primary);
+ &::marker {
+ display: none;
+ }
+
+ &::before {
+ content: "‣";
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: -1.5rem;
+ inline-size: $sz-16;
+ text-box: trim-start cap alphabetic;
+ text-align: end;
+ font-size: 1lh;
+ line-height: 1;
+ font-weight: 700;
+ color: currentcolor;
+ }
}
.error-detail-content {
- padding-left: var(--sp-xxxl);
+ padding-block-start: var(--sp-s);
+
+ & ul {
+ list-style: disc inside;
+ }
}
diff --git a/frontend/src/app/main/ui/ds/notifications/toast.cljs b/frontend/src/app/main/ui/ds/notifications/toast.cljs
index f83dbd5fd6..c00827eb89 100644
--- a/frontend/src/app/main/ui/ds/notifications/toast.cljs
+++ b/frontend/src/app/main/ui/ds/notifications/toast.cljs
@@ -21,13 +21,11 @@
[:level {:optional true} [:maybe [:enum :default :info :warning :error :success]]]
[:appearance {:optional true} [:enum :neutral :ghost]]
[:is-html {:optional true} :boolean]
- [:show-detail {:optional true} [:maybe :boolean]]
- [:on-close {:optional true} fn?]
- [:on-toggle-detail {:optional true} [:maybe fn?]]])
+ [:on-close {:optional true} fn?]])
(mf/defc toast*
{::mf/schema schema:toast}
- [{:keys [class level appearance type is-html children detail show-detail on-close on-toggle-detail] :rest props}]
+ [{:keys [class level appearance type is-html children detail on-close] :rest props}]
(let [class (dm/str class " " (stl/css :toast))
level (if (string? level)
(keyword level)
@@ -47,9 +45,7 @@
:type type
:is-html is-html
:appearance appearance
- :detail detail
- :show-detail show-detail
- :on-toggle-detail on-toggle-detail} children]
+ :detail detail} children]
;; TODO: this should be a buttom from the DS, but this variant is not designed yet.
diff --git a/frontend/src/app/main/ui/ds/notifications/toast.scss b/frontend/src/app/main/ui/ds/notifications/toast.scss
index c09629bfdd..9a7728d75c 100644
--- a/frontend/src/app/main/ui/ds/notifications/toast.scss
+++ b/frontend/src/app/main/ui/ds/notifications/toast.scss
@@ -18,7 +18,6 @@
min-inline-size: $sz-224;
max-inline-size: $sz-480;
-
display: block;
position: fixed;
inset-block-start: var(--toast-inset-block-start-position);
diff --git a/frontend/src/app/main/ui/ds/product/avatar.scss b/frontend/src/app/main/ui/ds/product/avatar.scss
index 36952c13d7..a84777a71f 100644
--- a/frontend/src/app/main/ui/ds/product/avatar.scss
+++ b/frontend/src/app/main/ui/ds/product/avatar.scss
@@ -36,6 +36,7 @@
.is-selected {
--border-color: var(--color-accent-primary);
+
padding: var(--sp-xxs);
}
diff --git a/frontend/src/app/main/ui/ds/product/empty_placeholder.scss b/frontend/src/app/main/ui/ds/product/empty_placeholder.scss
index 2850b67eb5..d0e2a8dfa1 100644
--- a/frontend/src/app/main/ui/ds/product/empty_placeholder.scss
+++ b/frontend/src/app/main/ui/ds/product/empty_placeholder.scss
@@ -22,8 +22,7 @@
.text-wrapper {
display: grid;
grid-auto-rows: auto;
- align-self: center;
- justify-self: center;
+ place-self: center center;
max-width: $sz-400;
}
diff --git a/frontend/src/app/main/ui/ds/product/empty_state.scss b/frontend/src/app/main/ui/ds/product/empty_state.scss
index b0612ecec0..60f05e85b9 100644
--- a/frontend/src/app/main/ui/ds/product/empty_state.scss
+++ b/frontend/src/app/main/ui/ds/product/empty_state.scss
@@ -31,6 +31,7 @@
.text {
@include t.use-typography("body-small");
+
text-align: center;
color: var(--color-foreground-secondary);
}
diff --git a/frontend/src/app/main/ui/ds/product/input_with_meta.scss b/frontend/src/app/main/ui/ds/product/input_with_meta.scss
index a01190d120..17b7e8c65a 100644
--- a/frontend/src/app/main/ui/ds/product/input_with_meta.scss
+++ b/frontend/src/app/main/ui/ds/product/input_with_meta.scss
@@ -15,6 +15,7 @@
--input-meta-background: var(--color-background-tertiary);
@include t.use-typography("body-small");
+
border-radius: $br-8;
background-color: var(--input-meta-background);
padding: var(--sp-s);
@@ -28,6 +29,7 @@
&:hover {
--input-meta-background: var(--color-background-quaternary);
+
cursor: text;
}
}
diff --git a/frontend/src/app/main/ui/ds/product/loader.scss b/frontend/src/app/main/ui/ds/product/loader.scss
index 772049d573..f552bad7be 100644
--- a/frontend/src/app/main/ui/ds/product/loader.scss
+++ b/frontend/src/app/main/ui/ds/product/loader.scss
@@ -78,7 +78,7 @@
}
.loader {
- fill: currentColor;
+ fill: currentcolor;
width: var(--icon-width);
}
diff --git a/frontend/src/app/main/ui/ds/product/milestone.scss b/frontend/src/app/main/ui/ds/product/milestone.scss
index 5e276d23a0..6a60a52804 100644
--- a/frontend/src/app/main/ui/ds/product/milestone.scss
+++ b/frontend/src/app/main/ui/ds/product/milestone.scss
@@ -11,19 +11,13 @@
.milestone {
border: $b-1 solid var(--border-color, transparent);
border-radius: $br-8;
-
background: var(--color-background-primary);
-
display: grid;
- grid-template-areas:
- "avatar name button"
- "avatar content button";
- grid-template-rows: auto 1fr;
- grid-template-columns: calc(var(--sp-xxl) + var(--sp-l)) 1fr auto;
-
+ grid-template:
+ "avatar name button" auto "avatar content button" 1fr / calc(var(--sp-xxl) + var(--sp-l))
+ 1fr auto;
padding: var(--sp-s) 0;
align-items: center;
-
column-gap: var(--sp-s);
&.is-selected,
@@ -60,6 +54,7 @@
.date {
@include t.use-typography("body-small");
+
grid-area: content;
color: var(--color-foreground-secondary);
}
diff --git a/frontend/src/app/main/ui/ds/product/milestone_group.scss b/frontend/src/app/main/ui/ds/product/milestone_group.scss
index 43c71ce334..0903a24921 100644
--- a/frontend/src/app/main/ui/ds/product/milestone_group.scss
+++ b/frontend/src/app/main/ui/ds/product/milestone_group.scss
@@ -11,19 +11,13 @@
.milestone {
border: $b-1 solid var(--border-color, transparent);
border-radius: $br-8;
-
background: var(--color-background-primary);
-
display: grid;
- grid-template-areas:
- "avatar name button"
- "avatar content button";
- grid-template-rows: auto 1fr;
- grid-template-columns: calc(var(--sp-xxl) + var(--sp-l)) 1fr auto;
-
+ grid-template:
+ "avatar name button" auto "avatar content button" 1fr / calc(var(--sp-xxl) + var(--sp-l))
+ 1fr auto;
padding: var(--sp-s) 0;
align-items: center;
-
column-gap: var(--sp-s);
&.is-selected,
@@ -39,12 +33,14 @@
.name {
@include t.use-typography("body-small");
+
grid-area: name;
color: var(--color-foreground-primary);
}
.toggle-message {
@include t.use-typography("body-small");
+
grid-area: name;
}
@@ -95,6 +91,7 @@
&:hover {
color: var(--color-accent-primary);
+
--icon-stroke-color: var(--color-accent-primary);
}
}
diff --git a/frontend/src/app/main/ui/ds/product/panel_title.scss b/frontend/src/app/main/ui/ds/product/panel_title.scss
index e0419221e4..fcb59ea43b 100644
--- a/frontend/src/app/main/ui/ds/product/panel_title.scss
+++ b/frontend/src/app/main/ui/ds/product/panel_title.scss
@@ -19,6 +19,7 @@
.panel-title-text {
@include t.use-typography("headline-small");
+
flex-grow: 1;
text-align: center;
color: var(--color-foreground-primary);
diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs
index 5dca183533..10e9638cf2 100644
--- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs
+++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs
@@ -17,8 +17,52 @@
(def ^:private ^:const overlay-offset 32)
+;; Global state for tooltip coordination
(defonce active-tooltip (atom nil))
+;; Registry of visible tooltips to detect nested tooltips
+;; Map: tooltip-id -> trigger-element
+(defonce ^:private tooltip-registry (atom {}))
+
+;; Track tooltips that are "about to show" - used to prevent race conditions
+;; when both parent and child schedule their show at the same time.
+;; Map: tooltip-id -> trigger-element
+(defonce ^:private pending-tooltips (atom {}))
+
+(defn- mark-pending
+ "Mark a tooltip as pending (scheduled to show soon).
+ Used to detect potential nested tooltips during race condition window."
+ [tooltip-id trigger-el]
+ (swap! pending-tooltips assoc tooltip-id trigger-el))
+
+(defn- clear-pending
+ "Clear the pending state (tooltip showed or cancelled)."
+ [tooltip-id]
+ (swap! pending-tooltips dissoc tooltip-id))
+
+(defn- register-tooltip
+ "Register this tooltip in the global registry when it becomes visible.
+ Used to detect nested tooltips."
+ [tooltip-id trigger-el]
+ (swap! tooltip-registry assoc tooltip-id trigger-el)
+ (clear-pending tooltip-id))
+
+(defn- unregister-tooltip
+ "Unregister this tooltip from the global registry when it hides."
+ [tooltip-id]
+ (swap! tooltip-registry dissoc tooltip-id)
+ (clear-pending tooltip-id))
+
+(defn- has-descendant-tooltip?
+ "Check if there's a registered or pending tooltip that is a descendant of trigger-el.
+ If so, we should NOT show the parent tooltip."
+ [trigger-el]
+ (let [all-tooltips (merge @tooltip-registry @pending-tooltips)]
+ (some (fn [[_ entry-el]]
+ (when (some? entry-el)
+ (dom/child? entry-el trigger-el)))
+ all-tooltips)))
+
(defn- clear-schedule
[ref]
(when-let [schedule (mf/ref-val ref)]
@@ -175,7 +219,7 @@
(deref placement*)
delay
- (d/nilv delay 300)
+ (d/nilv delay 700)
schedule-ref
(mf/use-ref nil)
@@ -191,15 +235,27 @@
(when-not (.-hidden js/document)
(let [trigger-el (mf/ref-val trigger-ref)]
(clear-schedule schedule-ref)
- (add-schedule schedule-ref (d/nilv delay 300)
- (fn []
- (when-let [active @active-tooltip]
- (when (not= (:id active) tooltip-id)
- (when-let [tooltip-el (dom/get-element (:id active))]
- (dom/set-css-property! tooltip-el "display" "none"))
- (reset! active-tooltip nil)))
- (reset! active-tooltip {:id tooltip-id :trigger trigger-el})
- (reset! visible* true)))))))
+
+ ;; Check if there's a registered or pending tooltip that is a descendant of our trigger.
+ ;; If so, skip showing this tooltip and let the innermost one show instead.
+ (when-not (has-descendant-tooltip? trigger-el)
+ ;; Mark as pending BEFORE scheduling (helps prevent race conditions)
+ (mark-pending tooltip-id trigger-el)
+
+ (add-schedule schedule-ref (d/nilv delay 300)
+ (fn []
+ ;; Double-check: don't show if another tooltip is now visible
+ (when-let [active @active-tooltip]
+ (when (not= (:id active) tooltip-id)
+ (when-let [tooltip-el (dom/get-element (:id active))]
+ (dom/set-css-property! tooltip-el "display" "none"))
+ (reset! active-tooltip nil)))
+
+ ;; Register this tooltip as visible
+ (register-tooltip tooltip-id trigger-el)
+
+ (reset! active-tooltip {:id tooltip-id :trigger trigger-el})
+ (reset! visible* true))))))))
on-show-focus
(mf/use-fn
@@ -215,6 +271,10 @@
(fn []
(clear-schedule schedule-ref)
(reset! visible* false)
+
+ ;; Unregister from the global registry
+ (unregister-tooltip tooltip-id)
+
(when (= (:id @active-tooltip) tooltip-id)
(reset! active-tooltip nil))))
diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss
index 79fe80f774..dcb01cb95a 100644
--- a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss
+++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss
@@ -55,6 +55,7 @@ $arrow-side: 12px;
"arrow"
"content";
}
+
.tooltip-bottom .tooltip-arrow {
justify-self: center;
border-radius: var(--sp-xs) 0;
@@ -111,7 +112,7 @@ $arrow-side: 12px;
}
.tooltip-bottom-right .tooltip-arrow {
- margin: 0px var(--sp-s);
+ margin: 0 var(--sp-s);
transform: rotate(45deg) translateX(var(--sp-s));
border-radius: var(--sp-xs) 0;
border-block-start: $b-1 solid var(--color-accent-primary-muted);
@@ -123,6 +124,7 @@ $arrow-side: 12px;
"arrow"
"content";
}
+
.tooltip-bottom-left .tooltip-arrow {
justify-self: end;
margin: 0 var(--sp-s);
@@ -137,6 +139,7 @@ $arrow-side: 12px;
"content"
"arrow";
}
+
.tooltip-top-left .tooltip-arrow {
margin: 0 var(--sp-s);
justify-self: end;
@@ -148,6 +151,7 @@ $arrow-side: 12px;
.tooltip-content {
@include t.use-typography("body-small");
+
background-color: var(--color-background-primary);
color: var(--color-foreground-secondary);
border-radius: var(--sp-xs);
diff --git a/frontend/src/app/main/ui/ds/typography.scss b/frontend/src/app/main/ui/ds/typography.scss
index 6ca2fd6670..36b4086fba 100644
--- a/frontend/src/app/main/ui/ds/typography.scss
+++ b/frontend/src/app/main/ui/ds/typography.scss
@@ -8,11 +8,9 @@
$_font-weight-regular: 400;
$_font-weight-medium: 500;
-
$_font-lineheight-dense: 1.2;
$_font-lineheight-compact: 1.3;
$_font-lineheight-normal: 1.4;
-
$_fs-12: px2rem(12);
$_fs-14: px2rem(14);
$_fs-16: px2rem(16);
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss
index a9eb3b6936..3052f5f6c3 100644
--- a/frontend/src/app/main/ui/ds/utilities/swatch.scss
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss
@@ -11,7 +11,7 @@
@property --solid-color-overlay {
syntax: "";
inherits: false;
- initial-value: rgba(0, 0, 0, 0);
+ initial-value: rgb(0 0 0 / 0);
}
.swatch {
@@ -19,13 +19,13 @@
--border-radius: #{$br-4};
--border-color-active: var(--color-foreground-primary);
--border-color-active-inset: var(--color-background-primary);
-
- --checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%);
+ --checkerboard-background: repeating-conic-gradient(rgb(212 212 212) 0% 25%, rgb(255 255 255) 0% 50%);
--checkerboard-size: 0.5rem 0.5rem;
border: $b-1 solid var(--border-color);
border-radius: var(--border-radius);
overflow: hidden;
+
&:focus-visible {
--border-color: var(--color-accent-primary);
}
@@ -80,6 +80,7 @@
&:hover {
--border-color: var(--color-accent-primary-muted);
+
border-width: $b-2;
}
}
@@ -114,7 +115,6 @@
/* solid‑colour overlay */
/* checkerboard pattern */
linear-gradient(var(--solid-color-overlay), var(--solid-color-overlay)), var(--checkerboard-background);
-
background-size: cover, var(--checkerboard-size);
background-position: center, center;
background-repeat: no-repeat, repeat;
diff --git a/frontend/src/app/main/ui/exports/assets.cljs b/frontend/src/app/main/ui/exports/assets.cljs
index feb7f52906..a32a2ca5f6 100644
--- a/frontend/src/app/main/ui/exports/assets.cljs
+++ b/frontend/src/app/main/ui/exports/assets.cljs
@@ -36,7 +36,7 @@
(mf/defc export-multiple-dialog*
{::mf/private true}
- [{:keys [exports title cmd no-selection origin]}]
+ [{:keys [exports title cmd no-selection origin name]}]
(let [lstate (mf/deref refs/export)
in-progress? (:in-progress lstate)
exports (mf/use-state exports)
@@ -59,7 +59,7 @@
(fn [event]
(dom/prevent-default event)
(st/emit! (modal/hide)
- (de/request-multiple-export {:exports enabled-exports :cmd cmd})
+ (de/request-multiple-export {:exports enabled-exports :cmd cmd :name name})
(de/export-shapes-event enabled-exports origin)))
on-toggle-enabled
@@ -185,25 +185,27 @@
(mf/defc export-shapes-dialog
{::mf/register modal/components
::mf/register-as :export-shapes}
- [{:keys [exports origin]}]
+ [{:keys [exports origin name]}]
(let [title (tr "dashboard.export-shapes.title")]
[:> export-multiple-dialog*
{:exports exports
:title title
:cmd :export-shapes
:no-selection shapes-no-selection
- :origin origin}]))
+ :origin origin
+ :name name}]))
(mf/defc export-frames
{::mf/register modal/components
::mf/register-as :export-frames}
- [{:keys [exports origin]}]
+ [{:keys [exports origin name]}]
(let [title (tr "dashboard.export-frames.title")]
[:> export-multiple-dialog*
{:exports exports
:title title
:cmd :export-frames
- :origin origin}]))
+ :origin origin
+ :name name}]))
;; FIXME: deprecated, should be refactored in two components and use
;; the generic progress reporter
diff --git a/frontend/src/app/main/ui/exports/assets.scss b/frontend/src/app/main/ui/exports/assets.scss
index 8bc4737a20..cc690c5d3b 100644
--- a/frontend/src/app/main/ui/exports/assets.scss
+++ b/frontend/src/app/main/ui/exports/assets.scss
@@ -8,7 +8,8 @@
// PROGRESS WIDGET
.export-progress-widget {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-28;
height: deprecated.$s-28;
}
@@ -19,6 +20,7 @@
--export-modal-fg-color: var(--alert-text-foreground-color-default);
--export-modal-icon-color: var(--alert-icon-foreground-color-default);
--export-modal-border-color: var(--alert-border-color-default);
+
position: absolute;
right: deprecated.$s-16;
top: deprecated.$s-48;
@@ -41,13 +43,15 @@
--export-modal-fg-color: var(--alert-text-foreground-color-error);
--export-modal-icon-color: var(--alert-icon-foreground-color-error);
--export-modal-border-color: var(--alert-border-color-error);
+
grid-template-areas: "icon text close";
gap: deprecated.$s-8;
padding-block: deprecated.$s-8;
}
.icon {
- @extend .button-icon;
+ @extend %button-icon;
+
grid-area: icon;
align-self: center;
margin-inline-start: deprecated.$s-8;
@@ -55,7 +59,8 @@
}
.export-progress-title {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
display: grid;
grid-template-columns: auto 1fr;
gap: deprecated.$s-8;
@@ -67,7 +72,8 @@
}
.progress {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
padding-left: deprecated.$s-8;
margin: 0;
align-self: center;
@@ -75,8 +81,9 @@
}
.retry-btn {
- @include deprecated.buttonStyle;
- @include deprecated.bodySmallTypography;
+ @include deprecated.button-style;
+ @include deprecated.body-small-typography;
+
display: inline;
text-align: left;
color: var(--modal-link-foreground-color);
@@ -85,13 +92,15 @@
}
.progress-close-button {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
padding: 0;
margin-inline-end: deprecated.$s-8;
}
.close-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--export-modal-icon-color);
}
@@ -102,14 +111,16 @@
// EXPORT MODAL
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
&.transparent {
background-color: transparent;
}
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
max-height: calc(10 * deprecated.$s-80);
}
@@ -118,76 +129,96 @@
}
.modal-title {
- @include deprecated.headlineMediumTypography;
+ @include deprecated.headline-medium-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content,
.no-selection {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-bottom: deprecated.$s-24;
+
.modal-link {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
text-decoration: none;
cursor: pointer;
color: var(--modal-link-foreground-color);
}
+
.selection-header {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
height: deprecated.$s-32;
margin-bottom: deprecated.$s-4;
+
.selection-btn {
- @include deprecated.buttonStyle;
- @extend .input-checkbox;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @extend %input-checkbox;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
padding: 0;
margin-left: deprecated.$s-16;
+
span {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
}
+
.selection-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
}
}
+
.selection-wrapper {
position: relative;
width: 100%;
height: fit-content;
}
+
.selection-shadow {
width: 100%;
height: 100%;
- &:after {
+
+ &::after {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
- background: linear-gradient(to top, rgba(24, 24, 26, 1) 0%, rgba(24, 24, 26, 0) 100%);
+ background: linear-gradient(to top, rgb(24 24 26 / 1) 0%, rgb(24 24 26 / 0) 100%);
content: "";
pointer-events: none;
}
}
+
.selection-list {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
max-height: deprecated.$s-400;
overflow-y: auto;
padding-bottom: deprecated.$s-12;
+
.selection-row {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
background-color: var(--entry-background-color);
min-height: deprecated.$s-40;
border-radius: deprecated.$br-8;
+
.selection-btn {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: grid;
grid-template-columns: min-content auto 1fr auto auto;
align-items: center;
@@ -195,45 +226,57 @@
height: 10%;
gap: deprecated.$s-8;
padding: 0 deprecated.$s-16;
+
.checkbox-wrapper {
- @extend .input-checkbox;
- @include deprecated.flexCenter;
+ @extend %input-checkbox;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
padding: 0;
+
.checkobox-tick {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
}
+
.selection-name {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
+
flex-grow: 1;
color: var(--modal-text-foreground-color);
text-align: start;
}
+
.selection-scale {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
+
min-width: deprecated.$s-108;
padding: deprecated.$s-12;
color: var(--modal-text-foreground-color);
}
+
.selection-extension {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
+
min-width: deprecated.$s-72;
padding: deprecated.$s-12;
color: var(--modal-text-foreground-color);
}
}
+
.image-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
min-height: deprecated.$s-32;
min-width: deprecated.$s-32;
background-color: var(--app-white);
border-radius: deprecated.$br-6;
margin: auto 0;
+
img,
svg {
object-fit: contain;
@@ -245,80 +288,98 @@
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
+
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
+
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
.modal-scd-msg,
.modal-subtitle,
.modal-msg {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
}
.export-option {
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
width: 100%;
align-items: flex-start;
+
label {
align-items: flex-start;
+
.modal-subtitle {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
}
+
span {
margin-top: deprecated.$s-8;
}
}
.option-content {
- @include deprecated.flexColumn;
- @include deprecated.bodyLargeTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-large-typography;
}
.file-entry {
.file-name {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
.file-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-foreground);
}
}
+
.file-name-label {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
}
}
+
&.loading {
.file-name {
color: var(--modal-text-foreground-color);
}
}
+
&.error {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
}
}
+
&.success {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
diff --git a/frontend/src/app/main/ui/exports/files.cljs b/frontend/src/app/main/ui/exports/files.cljs
index 9103852613..13652480c5 100644
--- a/frontend/src/app/main/ui/exports/files.cljs
+++ b/frontend/src/app/main/ui/exports/files.cljs
@@ -173,15 +173,22 @@
:on-click on-accept}]]]]
(= status :exporting)
- [:*
- [:div {:class (stl/css :modal-content)}
- (for [file (:files state)]
- [:> export-entry* {:file file :key (dm/str (:id file))}])]
+ (let [in-progress? (->> state :files (some :loading))]
+ [:*
+ [:div {:class (stl/css :modal-content)}
+ (for [file (:files state)]
+ [:> export-entry* {:file file :key (dm/str (:id file))}])
- [:div {:class (stl/css :modal-footer)}
- [:div {:class (stl/css :action-buttons)}
- [:input {:class (stl/css :accept-btn)
- :type "button"
- :value (tr "labels.close")
- :disabled (->> state :files (some :loading))
- :on-click on-cancel}]]]])]]))
+ (when in-progress?
+ [:div {:class (stl/css :status-message)
+ :role "status"
+ :aria-live "polite"}
+ (tr "labels.downloading-file")])]
+
+ [:div {:class (stl/css :modal-footer)}
+ [:div {:class (stl/css :action-buttons)}
+ [:input {:class (stl/css :accept-btn)
+ :type "button"
+ :value (tr "labels.close")
+ :disabled in-progress?
+ :on-click on-cancel}]]]]))]]))
diff --git a/frontend/src/app/main/ui/exports/files.scss b/frontend/src/app/main/ui/exports/files.scss
index d6055ed184..e395c9c509 100644
--- a/frontend/src/app/main/ui/exports/files.scss
+++ b/frontend/src/app/main/ui/exports/files.scss
@@ -8,14 +8,16 @@
// EXPORT MODAL
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
&.transparent {
background-color: transparent;
}
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
max-height: calc(10 * deprecated.$s-80);
}
@@ -24,75 +26,95 @@
}
.modal-title {
- @include deprecated.headlineMediumTypography;
+ @include deprecated.headline-medium-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-bottom: deprecated.$s-24;
+
.modal-link {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
text-decoration: none;
cursor: pointer;
color: var(--modal-link-foreground-color);
}
+
.selection-header {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
height: deprecated.$s-32;
margin-bottom: deprecated.$s-4;
+
.selection-btn {
- @include deprecated.buttonStyle;
- @extend .input-checkbox;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @extend %input-checkbox;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
padding: 0;
margin-left: deprecated.$s-16;
+
span {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
}
+
.selection-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
}
}
+
.selection-wrapper {
position: relative;
width: 100%;
height: fit-content;
}
+
.selection-shadow {
width: 100%;
height: 100%;
- &:after {
+
+ &::after {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
- background: linear-gradient(to top, rgba(24, 24, 26, 1) 0%, rgba(24, 24, 26, 0) 100%);
+ background: linear-gradient(to top, rgb(24 24 26 / 1) 0%, rgb(24 24 26 / 0) 100%);
content: "";
pointer-events: none;
}
}
+
.selection-list {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
max-height: deprecated.$s-400;
overflow-y: auto;
padding-bottom: deprecated.$s-12;
+
.selection-row {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
background-color: var(--entry-background-color);
min-height: deprecated.$s-40;
border-radius: deprecated.$br-8;
+
.selection-btn {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: grid;
grid-template-columns: min-content auto 1fr auto auto;
align-items: center;
@@ -100,45 +122,57 @@
height: 10%;
gap: deprecated.$s-8;
padding: 0 deprecated.$s-16;
+
.checkbox-wrapper {
- @extend .input-checkbox;
- @include deprecated.flexCenter;
+ @extend %input-checkbox;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
padding: 0;
+
.checkobox-tick {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
}
}
+
.selection-name {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
+
flex-grow: 1;
color: var(--modal-text-foreground-color);
text-align: start;
}
+
.selection-scale {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
+
min-width: deprecated.$s-108;
padding: deprecated.$s-12;
color: var(--modal-text-foreground-color);
}
+
.selection-extension {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
+
min-width: deprecated.$s-72;
padding: deprecated.$s-12;
color: var(--modal-text-foreground-color);
}
}
+
.image-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
min-height: deprecated.$s-32;
min-width: deprecated.$s-32;
background-color: var(--app-white);
border-radius: deprecated.$br-6;
margin: auto 0;
+
img,
svg {
object-fit: contain;
@@ -149,83 +183,107 @@
}
}
+.status-message {
+ @include deprecated.body-small-typography;
+
+ color: var(--modal-title-foreground-color);
+ font-style: italic;
+}
+
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
+
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
+
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
.modal-scd-msg,
.modal-subtitle,
.modal-msg {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
}
.export-option {
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
width: 100%;
align-items: flex-start;
+
label {
align-items: flex-start;
+
.modal-subtitle {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
padding: 0.25rem 0;
}
}
+
span {
margin-top: deprecated.$s-8;
}
}
.option-content {
- @include deprecated.flexColumn;
- @include deprecated.bodyLargeTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-large-typography;
}
.file-entry {
.file-name {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
.file-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-foreground);
}
}
+
.file-name-label {
- @include deprecated.bodyLargeTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-large-typography;
+ @include deprecated.text-ellipsis;
}
}
+
&.loading {
.file-name {
color: var(--modal-text-foreground-color);
}
}
+
&.error {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
}
}
+
&.success {
.file-name {
color: var(--modal-text-foreground-color);
+
.file-icon svg {
stroke: var(--modal-text-foreground-color);
}
diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs
index 9aede980cf..c0426dcfaa 100644
--- a/frontend/src/app/main/ui/forms.cljs
+++ b/frontend/src/app/main/ui/forms.cljs
@@ -67,24 +67,38 @@
(mf/defc form-submit*
[{:keys [disabled on-submit] :rest props}]
+
(let [form (mf/use-ctx context)
- disabled? (or (and (some? form)
- (or (not (:valid @form))
- (seq (:async-errors @form))
- (seq (:extra-errors @form))))
- (true? disabled))
+ form-state (when form @form)
+
+ disabled? (mf/use-memo
+ (mf/deps form form-state disabled)
+ (fn []
+ (boolean
+ (or (nil? form)
+ (true? disabled)
+ (not (:valid form-state))
+ (seq (:async-errors form-state))
+ (seq (:extra-errors form-state))))))
+
handle-key-down-save
(mf/use-fn
- (mf/deps on-submit form)
+ (mf/deps on-submit form disabled?)
(fn [e]
- (when (or (k/enter? e) (k/space? e))
+ (when (and (or (k/enter? e) (k/space? e)) (not disabled?))
(dom/prevent-default e)
(on-submit form e))))
props
- (mf/spread-props props {:disabled disabled?
- :on-key-down handle-key-down-save
- :type "submit"})]
+ (mf/spread-props props {:on-key-down handle-key-down-save
+ :type "submit"})
+
+ props
+ (if disabled?
+ (mf/spread-props props {:disabled true
+ :on-key-down handle-key-down-save
+ :type "submit"})
+ props)]
[:> button* props]))
diff --git a/frontend/src/app/main/ui/frame_preview.cljs b/frontend/src/app/main/ui/frame_preview.cljs
index bff3da3abd..de15702d97 100644
--- a/frontend/src/app/main/ui/frame_preview.cljs
+++ b/frontend/src/app/main/ui/frame_preview.cljs
@@ -37,7 +37,6 @@
load-ref
(mf/use-callback
(fn [iframe-dom]
- (.log js/console "load-ref" iframe-dom)
(mf/set-ref-val! iframe-ref iframe-dom)
(when (and iframe-dom @last-data*)
(-> iframe-dom .-contentWindow .-document .open)
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index f5ca0d9117..1cc3569881 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -19,6 +19,7 @@
(def ^:icon logo-error-screen (icon-xref :logo-error-screen))
(def ^:icon logo-subscription (icon-xref :logo-subscription))
(def ^:icon logo-subscription-light (icon-xref :logo-subscription-light))
+(def ^:icon nitrate-welcome (icon-xref :nitrate-welcome))
(def ^:icon brand-openid (icon-xref :brand-openid))
(def ^:icon brand-github (icon-xref :brand-github))
diff --git a/frontend/src/app/main/ui/inspect/annotation.scss b/frontend/src/app/main/ui/inspect/annotation.scss
index 431754d330..75054e7e3a 100644
--- a/frontend/src/app/main/ui/inspect/annotation.scss
+++ b/frontend/src/app/main/ui/inspect/annotation.scss
@@ -7,15 +7,16 @@
@use "refactor/common-refactor.scss" as deprecated;
.attributes-block {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
}
.title-spacing-annotation {
- @extend .attr-title;
+ @extend %attr-title;
}
.annotation-content {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--entry-foreground-color);
}
diff --git a/frontend/src/app/main/ui/inspect/attributes.scss b/frontend/src/app/main/ui/inspect/attributes.scss
index 4eafa389eb..2fbcbd3e06 100644
--- a/frontend/src/app/main/ui/inspect/attributes.scss
+++ b/frontend/src/app/main/ui/inspect/attributes.scss
@@ -15,8 +15,7 @@
max-height: calc(100vh - px2rem(128)); // TODO: Fix this hardcoded value
padding-top: var(--sp-s);
padding-inline: var(--sp-m);
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
scrollbar-gutter: stable;
background-color: var(--low-emphasis-background);
}
diff --git a/frontend/src/app/main/ui/inspect/attributes/blur.scss b/frontend/src/app/main/ui/inspect/attributes/blur.scss
index 9ae8c464eb..240b8f8c20 100644
--- a/frontend/src/app/main/ui/inspect/attributes/blur.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/blur.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,12 +26,13 @@
}
.blur-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.copy-btn-title {
diff --git a/frontend/src/app/main/ui/inspect/attributes/common.scss b/frontend/src/app/main/ui/inspect/attributes/common.scss
index 79d7b9a410..569af36772 100644
--- a/frontend/src/app/main/ui/inspect/attributes/common.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/common.scss
@@ -5,7 +5,6 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
-
@use "ds/_utils.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@@ -24,7 +23,7 @@
}
.attributes-color-row {
- @extend .attr-row;
+ @extend %attr-row;
}
.bullet-wrapper {
@@ -41,6 +40,7 @@
.image-format {
@include use-typography("headline-small");
+
block-size: $sz-32;
padding: var(--sp-s) 0;
color: var(--color-foreground-secondary);
@@ -56,6 +56,7 @@
.format-info {
@include use-typography("body-small");
+
padding-left: var(--sp-xxs);
color: var(--color-foreground-secondary);
}
@@ -66,10 +67,12 @@
gap: var(--sp-xs);
flex-grow: 1;
max-inline-size: px2rem(144);
+
button {
visibility: hidden;
min-inline-size: px2rem(28);
}
+
&:hover button {
visibility: visible;
}
@@ -87,6 +90,7 @@
.color-name-wrapper {
@include use-typography("body-small");
+
display: flex;
flex-direction: column;
gap: var(--sp-xs);
@@ -109,7 +113,8 @@
.color-value-wrapper {
@include use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
color: var(--menu-foreground-color);
text-transform: uppercase;
}
@@ -120,6 +125,7 @@
.opacity-info {
@include use-typography("body-small");
+
color: var(--menu-foreground-color);
text-transform: uppercase;
inline-size: 100%;
@@ -127,7 +133,6 @@
.second-row {
min-block-size: $sz-16;
- padding-right: var(--sp-s);
inline-size: 100%;
text-align: left;
margin: 0;
@@ -136,8 +141,9 @@
.color-name-library {
@include use-typography("body-small");
+
color: var(--color-foreground-secondary);
- word-break: break-word;
+ overflow-wrap: break-word;
}
.image-download {
diff --git a/frontend/src/app/main/ui/inspect/attributes/fill.scss b/frontend/src/app/main/ui/inspect/attributes/fill.scss
index 3cede83d81..bb6fa49a90 100644
--- a/frontend/src/app/main/ui/inspect/attributes/fill.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/fill.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
diff --git a/frontend/src/app/main/ui/inspect/attributes/geometry.scss b/frontend/src/app/main/ui/inspect/attributes/geometry.scss
index f1a90db1e3..40777a76d7 100644
--- a/frontend/src/app/main/ui/inspect/attributes/geometry.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/geometry.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,12 +26,13 @@
}
.geometry-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.copy-btn-title {
diff --git a/frontend/src/app/main/ui/inspect/attributes/layout.scss b/frontend/src/app/main/ui/inspect/attributes/layout.scss
index 2164e152fc..4918983aba 100644
--- a/frontend/src/app/main/ui/inspect/attributes/layout.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/layout.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,12 +26,13 @@
}
.layout-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.copy-btn-title {
diff --git a/frontend/src/app/main/ui/inspect/attributes/layout_element.scss b/frontend/src/app/main/ui/inspect/attributes/layout_element.scss
index a51009ab53..0e4cc28285 100644
--- a/frontend/src/app/main/ui/inspect/attributes/layout_element.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/layout_element.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,15 +26,15 @@
}
.layout-element-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.copy-btn-title {
max-inline-size: $sz-28;
- max-inline-size: $sz-28;
}
diff --git a/frontend/src/app/main/ui/inspect/attributes/shadow.scss b/frontend/src/app/main/ui/inspect/attributes/shadow.scss
index 8cfb86f0c3..d4e04e5b1b 100644
--- a/frontend/src/app/main/ui/inspect/attributes/shadow.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/shadow.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,10 +26,11 @@
}
.shadow-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
diff --git a/frontend/src/app/main/ui/inspect/attributes/stroke.scss b/frontend/src/app/main/ui/inspect/attributes/stroke.scss
index dd5bf8d4b4..70bd2a5ef5 100644
--- a/frontend/src/app/main/ui/inspect/attributes/stroke.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/stroke.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -31,12 +32,13 @@
}
.stroke-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.attributes-content {
diff --git a/frontend/src/app/main/ui/inspect/attributes/svg.scss b/frontend/src/app/main/ui/inspect/attributes/svg.scss
index 1b7495e61d..dd50046496 100644
--- a/frontend/src/app/main/ui/inspect/attributes/svg.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/svg.scss
@@ -11,6 +11,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -26,24 +27,28 @@
}
.svg-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.attributes-subtitle {
@include use-typography("headline-small");
+
display: flex;
justify-content: space-between;
block-size: $sz-32;
+
span {
block-size: $sz-32;
display: flex;
align-items: center;
}
+
button {
display: none;
}
diff --git a/frontend/src/app/main/ui/inspect/attributes/text.scss b/frontend/src/app/main/ui/inspect/attributes/text.scss
index 9f3ecf1808..6a81b7bcd0 100644
--- a/frontend/src/app/main/ui/inspect/attributes/text.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/text.scss
@@ -12,6 +12,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -33,16 +34,18 @@
}
.text-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: unset;
min-block-size: $sz-36;
+
:global(.attr-value) {
align-items: center;
}
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.attributes-content-row {
@@ -51,8 +54,10 @@
border-radius: $br-8;
border: $b-1 solid var(--menu-border-color-disabled);
margin-block-start: var(--sp-xs);
+
.content {
@include use-typography("body-small");
+
width: 100%;
padding: var(--sp-xs) 0;
color: var(--color-foreground-secondary);
@@ -61,6 +66,7 @@
&:hover {
border: $b-1 solid var(--color-background-tertiary);
background-color: var(--menu-background-color);
+
.content {
color: var(--menu-foreground-color-hover);
}
diff --git a/frontend/src/app/main/ui/inspect/attributes/variant.scss b/frontend/src/app/main/ui/inspect/attributes/variant.scss
index 3d0df70402..050826a5db 100644
--- a/frontend/src/app/main/ui/inspect/attributes/variant.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/variant.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,12 +26,14 @@
}
.variant-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: fit-content;
min-block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
- word-break: break-word;
+ @extend %copy-button-children;
+
+ overflow-wrap: break-word;
}
diff --git a/frontend/src/app/main/ui/inspect/attributes/visibility.scss b/frontend/src/app/main/ui/inspect/attributes/visibility.scss
index c888735ff1..a4b20d8700 100644
--- a/frontend/src/app/main/ui/inspect/attributes/visibility.scss
+++ b/frontend/src/app/main/ui/inspect/attributes/visibility.scss
@@ -10,6 +10,7 @@
.attributes-block {
--box-border-color: var(--color-background-primary);
+
display: flex;
flex-direction: column;
border-block-end: $b-2 solid var(--box-border-color);
@@ -25,12 +26,13 @@
}
.visibility-row {
- @extend .attr-row;
+ @extend %attr-row;
+
block-size: $sz-36;
}
.button-children {
- @extend .copy-button-children;
+ @extend %copy-button-children;
}
.copy-btn-title {
diff --git a/frontend/src/app/main/ui/inspect/code.scss b/frontend/src/app/main/ui/inspect/code.scss
index 7f88871edc..4f455da3a2 100644
--- a/frontend/src/app/main/ui/inspect/code.scss
+++ b/frontend/src/app/main/ui/inspect/code.scss
@@ -13,7 +13,6 @@
overflow: hidden;
padding-bottom: deprecated.$s-16;
overflow-y: auto;
- overflow-x: hidden;
padding-inline: var(--sp-m);
}
@@ -22,15 +21,17 @@
}
.download-button {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
height: deprecated.$s-32;
width: 100%;
margin: deprecated.$s-8 0;
}
.code-block {
- @include deprecated.codeTypography;
+ @include deprecated.code-typography;
+
display: flex;
flex-direction: column;
height: 100%;
@@ -62,7 +63,8 @@
}
.code-lang {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
display: flex;
align-items: center;
}
@@ -76,11 +78,14 @@
.expand-button,
.css-copy-btn,
.html-copy-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
@@ -88,15 +93,19 @@
.code-lang-options {
max-width: deprecated.$s-108;
}
+
.code-lang-select {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
width: deprecated.$s-72;
border: deprecated.$s-1 solid transparent;
background-color: transparent;
color: var(--menu-foreground-color-disabled);
}
+
.code-lang-option {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
width: deprecated.$s-72;
height: deprecated.$s-32;
padding: deprecated.$s-8;
@@ -111,32 +120,41 @@
}
.toggle-btn {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
align-items: center;
padding: 0;
color: var(--title-foreground-color);
stroke: var(--title-foreground-color);
+
.collapsabled-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
border-radius: deprecated.$br-8;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
+
&.rotated svg {
transform: rotate(0deg);
}
}
+
&:hover {
color: var(--title-foreground-color-hover);
stroke: var(--title-foreground-color-hover);
+
.title {
color: var(--title-foreground-color-hover);
stroke: var(--title-foreground-color-hover);
}
+
.collapsabled-icon svg {
stroke: var(--title-foreground-color-hover);
}
diff --git a/frontend/src/app/main/ui/inspect/exports.cljs b/frontend/src/app/main/ui/inspect/exports.cljs
index 7240ad59ea..04cd8260ac 100644
--- a/frontend/src/app/main/ui/inspect/exports.cljs
+++ b/frontend/src/app/main/ui/inspect/exports.cljs
@@ -47,7 +47,7 @@
(if (= :multiple type)
(st/emit! (de/show-viewer-export-dialog {:shapes shapes
:exports @exports
- :filename filename
+ :name filename
:page-id page-id
:file-id file-id
:share-id share-id}))
diff --git a/frontend/src/app/main/ui/inspect/exports.scss b/frontend/src/app/main/ui/inspect/exports.scss
index 4ca98720a8..690e83d06d 100644
--- a/frontend/src/app/main/ui/inspect/exports.scss
+++ b/frontend/src/app/main/ui/inspect/exports.scss
@@ -23,43 +23,51 @@
}
.add-export {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
.element-set-content {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
margin-bottom: deprecated.$s-4;
}
.multiple-exports {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
grid-column: 1 / span 9;
}
.label {
- @extend .mixed-bar;
+ @extend %mixed-bar;
}
.actions {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
.element-group {
display: grid;
grid-template-columns: repeat(9, 1fr);
column-gap: deprecated.$s-4;
+
.action-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
}
@@ -84,6 +92,7 @@
.size-select {
grid-column: span 2;
padding: 0;
+
.dropdown-upwards {
bottom: deprecated.$s-36;
top: unset;
@@ -92,14 +101,16 @@
}
.suffix-input {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
grid-column: span 3;
}
.export-btn {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
height: deprecated.$s-32;
width: 100%;
}
diff --git a/frontend/src/app/main/ui/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/inspect/right_sidebar.cljs
index 5e205b502a..9d4dfa0a1b 100644
--- a/frontend/src/app/main/ui/inspect/right_sidebar.cljs
+++ b/frontend/src/app/main/ui/inspect/right_sidebar.cljs
@@ -188,7 +188,9 @@
:shapes shapes
:from from
:libraries libraries
- :file-id file-id}]
+ :page-id page-id
+ :file-id file-id
+ :share-id share-id}]
:computed
[:> attributes* {:color-space color-space
:page-id page-id
diff --git a/frontend/src/app/main/ui/inspect/right_sidebar.scss b/frontend/src/app/main/ui/inspect/right_sidebar.scss
index ca57b53f1c..1bc6a19a52 100644
--- a/frontend/src/app/main/ui/inspect/right_sidebar.scss
+++ b/frontend/src/app/main/ui/inspect/right_sidebar.scss
@@ -56,8 +56,9 @@
}
.layer-title {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.text-ellipsis;
+
block-size: $sz-32;
padding: var(--sp-s) 0;
color: var(--color-foreground-primary);
@@ -69,8 +70,9 @@
}
.layer-subtitle {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.text-ellipsis;
+
color: var(--assets-item-name-foreground-color-rest);
}
@@ -97,6 +99,7 @@
.inspect-tab-switcher-label {
@include use-typography("body-medium");
+
color: var(--color-foreground-primary);
flex: 0 1 40%;
}
diff --git a/frontend/src/app/main/ui/inspect/styles.cljs b/frontend/src/app/main/ui/inspect/styles.cljs
index 3794ba61c7..21bc681ec5 100644
--- a/frontend/src/app/main/ui/inspect/styles.cljs
+++ b/frontend/src/app/main/ui/inspect/styles.cljs
@@ -15,6 +15,7 @@
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.refs :as refs]
+ [app.main.ui.inspect.exports :as exports]
[app.main.ui.inspect.styles.panels.blur :refer [blur-panel*]]
[app.main.ui.inspect.styles.panels.fill :refer [fill-panel*]]
[app.main.ui.inspect.styles.panels.geometry :refer [geometry-panel*]]
@@ -89,8 +90,20 @@
(:type first-shape))
:multiple))
+(def ^:private schema:styles-tab
+ [:map
+ [:color-space {:optional true} :string] ;; color format, e.g., "hex", "rgba", etc.
+ [:shapes :any]
+ [:libraries :map]
+ [:objects :map]
+ [:file-id :uuid]
+ [:page-id :uuid]
+ [:share-id {:optional true} [:maybe :uuid]]
+ [:from {:optional true} [:enum :workspace :viewer]]])
+
(mf/defc styles-tab*
- [{:keys [color-space shapes libraries objects file-id from]}]
+ {::mf/schema schema:styles-tab}
+ [{:keys [color-space shapes libraries objects file-id page-id share-id from]}]
(let [data (dm/get-in libraries [file-id :data])
first-shape (first shapes)
first-component (ctkl/get-component data (:component-id first-shape))
@@ -131,130 +144,139 @@
(mf/deps shorthands*)
(fn [shorthand]
(swap! shorthands* assoc (:panel shorthand) (:property shorthand))))]
- [:ol {:class (stl/css-case :styles-tab true
- :styles-tab-workspace (= from :workspace)) :aria-label (tr "labels.styles")}
- ;; TOKENS PANEL
- (when (or (seq active-themes) (seq active-sets))
- [:li
- [:> style-box* {:panel :token}
- [:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]])
- (for [panel panels]
- [:li {:key (d/name panel)}
- (case panel
- ;; VARIANTS PANEL
- :variant
- [:> style-box* {:panel :variant}
- [:> variants-panel* {:component first-component
- :objects objects
- :shape first-shape
- :data data}]]
- ;; GEOMETRY PANEL
- :geometry
- [:> style-box* {:panel :geometry
- :shorthand (:geometry shorthands)}
- [:> geometry-panel* {:shapes shapes
- :objects objects
- :resolved-tokens resolved-active-tokens
- :on-geometry-shorthand set-shorthands}]]
- ;; LAYOUT PANEL
- :layout
- (let [layout-shapes (->> shapes (filter ctl/any-layout?))]
- (when (seq layout-shapes)
- [:> style-box* {:panel :layout
- :shorthand (:layout shorthands)}
- [:> layout-panel* {:shapes layout-shapes
- :objects objects
- :resolved-tokens resolved-active-tokens
- :on-layout-shorthand set-shorthands}]]))
- ;; LAYOUT ELEMENT PANEL
- :layout-element
- (let [shapes (->> shapes (filter #(ctl/any-layout-immediate-child? objects %)))
- some-layout-prop? (->> shapes
- (mapcat (fn [shape]
- (keep #(css/get-css-value objects shape %) layout-element-properties)))
- (seq))]
- (when some-layout-prop?
- (let [only-flex? (every? #(ctl/flex-layout-immediate-child? objects %) shapes)
- only-grid? (every? #(ctl/grid-layout-immediate-child? objects %) shapes)
- panel (if only-flex?
- :flex-element
- (if only-grid?
- :grid-element
- :layout-element))]
- [:> style-box* {:panel panel
- :shorthand (:layout-element shorthands)}
- [:> layout-element-panel* {:shapes shapes
- :objects objects
- :resolved-tokens resolved-active-tokens
- :layout-element-properties layout-element-properties
- :on-layout-element-shorthand set-shorthands}]])))
- ;; FILL PANEL
- :fill
- (let [shapes (filter has-fill? shapes)]
- (when (seq shapes)
- [:> style-box* {:panel :fill
- :shorthand (:fill shorthands)}
- [:> fill-panel* {:color-space color-space
- :shapes shapes
- :resolved-tokens resolved-active-tokens
- :on-fill-shorthand set-shorthands}]]))
+ [:section {:class (stl/css-case :styles-tab true
+ :styles-tab-workspace (= from :workspace))
+ :aria-label (tr "labels.styles")}
+ [:ol
+ ;; TOKENS PANEL
+ (when (or (seq active-themes) (seq active-sets))
+ [:li
+ [:> style-box* {:panel :token}
+ [:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]])
+ (for [panel panels]
+ [:li {:key (d/name panel)}
+ (case panel
+ ;; VARIANTS PANEL
+ :variant
+ [:> style-box* {:panel :variant}
+ [:> variants-panel* {:component first-component
+ :objects objects
+ :shape first-shape
+ :data data}]]
+ ;; GEOMETRY PANEL
+ :geometry
+ [:> style-box* {:panel :geometry
+ :shorthand (:geometry shorthands)}
+ [:> geometry-panel* {:shapes shapes
+ :objects objects
+ :resolved-tokens resolved-active-tokens
+ :on-geometry-shorthand set-shorthands}]]
+ ;; LAYOUT PANEL
+ :layout
+ (let [layout-shapes (->> shapes (filter ctl/any-layout?))]
+ (when (seq layout-shapes)
+ [:> style-box* {:panel :layout
+ :shorthand (:layout shorthands)}
+ [:> layout-panel* {:shapes layout-shapes
+ :objects objects
+ :resolved-tokens resolved-active-tokens
+ :on-layout-shorthand set-shorthands}]]))
+ ;; LAYOUT ELEMENT PANEL
+ :layout-element
+ (let [shapes (->> shapes (filter #(ctl/any-layout-immediate-child? objects %)))
+ some-layout-prop? (->> shapes
+ (mapcat (fn [shape]
+ (keep #(css/get-css-value objects shape %) layout-element-properties)))
+ (seq))]
+ (when some-layout-prop?
+ (let [only-flex? (every? #(ctl/flex-layout-immediate-child? objects %) shapes)
+ only-grid? (every? #(ctl/grid-layout-immediate-child? objects %) shapes)
+ panel (if only-flex?
+ :flex-element
+ (if only-grid?
+ :grid-element
+ :layout-element))]
+ [:> style-box* {:panel panel
+ :shorthand (:layout-element shorthands)}
+ [:> layout-element-panel* {:shapes shapes
+ :objects objects
+ :resolved-tokens resolved-active-tokens
+ :layout-element-properties layout-element-properties
+ :on-layout-element-shorthand set-shorthands}]])))
+ ;; FILL PANEL
+ :fill
+ (let [shapes (filter has-fill? shapes)]
+ (when (seq shapes)
+ [:> style-box* {:panel :fill
+ :shorthand (:fill shorthands)}
+ [:> fill-panel* {:color-space color-space
+ :shapes shapes
+ :resolved-tokens resolved-active-tokens
+ :on-fill-shorthand set-shorthands}]]))
- ;; STROKE PANEL
- :stroke
- (let [shapes (filter has-stroke? shapes)]
- (when (seq shapes)
- [:> style-box* {:panel :stroke
- :shorthand (:stroke shorthands)}
- [:> stroke-panel* {:color-space color-space
- :shapes shapes
- :objects objects
- :resolved-tokens resolved-active-tokens
- :on-stroke-shorthand set-shorthands}]]))
+ ;; STROKE PANEL
+ :stroke
+ (let [shapes (filter has-stroke? shapes)]
+ (when (seq shapes)
+ [:> style-box* {:panel :stroke
+ :shorthand (:stroke shorthands)}
+ [:> stroke-panel* {:color-space color-space
+ :shapes shapes
+ :objects objects
+ :resolved-tokens resolved-active-tokens
+ :on-stroke-shorthand set-shorthands}]]))
- ;; VISIBILITY PANEL
- :visibility
- (let [shapes (filter has-visibility-props? shapes)]
- (when (seq shapes)
- [:> style-box* {:panel :visibility}
- [:> visibility-panel* {:shapes shapes
- :objects objects
- :resolved-tokens resolved-active-tokens}]]))
- ;; SVG PANEL
- :svg
- (let [shape (first shapes)]
- (when (seq (:svg-attrs shape))
- [:> style-box* {:panel :svg}
- [:> svg-panel* {:shape shape
- :objects objects}]]))
- ;; BLUR PANEL
- :blur
- (let [shapes (->> shapes (filter has-blur?))]
- (when (seq shapes)
- [:> style-box* {:panel :blur}
- [:> blur-panel* {:shapes shapes
+ ;; VISIBILITY PANEL
+ :visibility
+ (let [shapes (filter has-visibility-props? shapes)]
+ (when (seq shapes)
+ [:> style-box* {:panel :visibility}
+ [:> visibility-panel* {:shapes shapes
+ :objects objects
+ :resolved-tokens resolved-active-tokens}]]))
+ ;; SVG PANEL
+ :svg
+ (let [shape (first shapes)]
+ (when (seq (:svg-attrs shape))
+ [:> style-box* {:panel :svg}
+ [:> svg-panel* {:shape shape
:objects objects}]]))
- ;; TEXT PANEL
- :text
- (let [shapes (filter has-text? shapes)]
- (when (seq shapes)
- [:> style-box* {:panel :text
- :shorthand (:text shorthands)}
- [:> text-panel* {:shapes shapes
- :color-space color-space
- :resolved-tokens resolved-active-tokens
- :on-font-shorthand set-shorthands}]]))
+ ;; BLUR PANEL
+ :blur
+ (let [shapes (->> shapes (filter has-blur?))]
+ (when (seq shapes)
+ [:> style-box* {:panel :blur}
+ [:> blur-panel* {:shapes shapes
+ :objects objects}]]))
+ ;; TEXT PANEL
+ :text
+ (let [shapes (filter has-text? shapes)]
+ (when (seq shapes)
+ [:> style-box* {:panel :text
+ :shorthand (:text shorthands)}
+ [:> text-panel* {:shapes shapes
+ :color-space color-space
+ :resolved-tokens resolved-active-tokens
+ :on-font-shorthand set-shorthands}]]))
- ;; SHADOW PANEL
- :shadow
- (let [shapes (filter has-shadow? shapes)]
- (when (seq shapes)
- [:> style-box* {:panel :shadow
- :shorthand (:shadow shorthands)}
- [:> shadow-panel* {:shapes shapes
- :resolved-tokens resolved-active-tokens
- :color-space color-space
- :on-shadow-shorthand set-shorthands}]]))
+ ;; SHADOW PANEL
+ :shadow
+ (let [shapes (filter has-shadow? shapes)]
+ (when (seq shapes)
+ [:> style-box* {:panel :shadow
+ :shorthand (:shadow shorthands)}
+ [:> shadow-panel* {:shapes shapes
+ :resolved-tokens resolved-active-tokens
+ :color-space color-space
+ :on-shadow-shorthand set-shorthands}]]))
- ;; DEFAULT WIP
- [:> style-box* {:panel panel}
- [:div color-space]])])]))
+ ;; DEFAULT WIP
+ [:> style-box* {:panel panel}
+ [:div color-space]])])]
+ [:div {:class (stl/css :exports-wrapper)}
+ [:& exports/exports
+ {:shapes shapes
+ :type type
+ :page-id page-id
+ :file-id file-id
+ :share-id share-id}]]]))
diff --git a/frontend/src/app/main/ui/inspect/styles.scss b/frontend/src/app/main/ui/inspect/styles.scss
index 0680351132..d78617bb6b 100644
--- a/frontend/src/app/main/ui/inspect/styles.scss
+++ b/frontend/src/app/main/ui/inspect/styles.scss
@@ -13,3 +13,8 @@
.styles-tab-workspace {
block-size: calc(100vh - px2rem(180)); // TODO: Fix this hardcoded value
}
+
+.exports-wrapper {
+ padding-block: var(--sp-s);
+ padding-inline: var(--sp-m);
+}
diff --git a/frontend/src/app/main/ui/inspect/styles/panels/text.scss b/frontend/src/app/main/ui/inspect/styles/panels/text.scss
index 0b1bbdd05c..3c68ea52c9 100644
--- a/frontend/src/app/main/ui/inspect/styles/panels/text.scss
+++ b/frontend/src/app/main/ui/inspect/styles/panels/text.scss
@@ -8,7 +8,7 @@
.text-content-wrapper {
--border-color: var(--color-background-quaternary);
- --border-radius: ${$br-8};
+ --border-radius: #{$br-8};
border: $b-1 solid var(--border-color);
border-radius: var(--border-radius);
@@ -16,5 +16,6 @@
.text-content {
--detail-color: var(--color-foreground-secondary);
+
color: var(--detail-color);
}
diff --git a/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss b/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss
index c1ddecdf4e..23c7fd9b52 100644
--- a/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss
+++ b/frontend/src/app/main/ui/inspect/styles/property_detail_copiable.scss
@@ -46,6 +46,7 @@
.property-detail-copied {
--button-border-active: var(--color-accent-tertiary);
+
border: $b-1 solid var(--button-border-active);
}
@@ -61,11 +62,13 @@
.property-detail-text {
@include use-typography("body-small");
+
color: var(--detail-color);
}
.property-detail-text-token {
@include use-typography("code-font");
+
--detail-color: var(--color-token-foreground);
line-height: 1.4;
diff --git a/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss b/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss
index d5b8497c5c..44d90d99b1 100644
--- a/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss
+++ b/frontend/src/app/main/ui/inspect/styles/rows/color_properties_row.scss
@@ -50,6 +50,7 @@
.color-image-preview-wrapper {
--image-background: var(--color-background-secondary);
+
background: var(--image-background);
}
@@ -69,11 +70,13 @@
.tooltip-token-title {
@include use-typography("body-small");
+
color: var(--title-color);
}
.tooltip-token-value {
@include use-typography("body-small");
+
color: var(--title-value);
}
diff --git a/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss b/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss
index 19287bc219..0dace4fcc7 100644
--- a/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss
+++ b/frontend/src/app/main/ui/inspect/styles/rows/properties_row.scss
@@ -45,11 +45,13 @@
.tooltip-token-title {
@include use-typography("body-small");
+
color: var(--title-color);
}
.tooltip-token-value {
@include use-typography("body-small");
+
color: var(--title-value);
}
diff --git a/frontend/src/app/main/ui/inspect/styles/style_box.scss b/frontend/src/app/main/ui/inspect/styles/style_box.scss
index a55a6b5fc4..7965049e59 100644
--- a/frontend/src/app/main/ui/inspect/styles/style_box.scss
+++ b/frontend/src/app/main/ui/inspect/styles/style_box.scss
@@ -25,7 +25,6 @@
padding-block: var(--sp-s);
padding-inline: var(--sp-m);
background-color: var(--low-emphasis-background);
-
border-block-end: 2px solid var(--box-border-color);
}
@@ -39,7 +38,6 @@
display: grid;
place-items: center;
color: var(--arrow-color);
-
appearance: none;
background: none;
padding: 0;
@@ -49,6 +47,7 @@
.panel-title {
@include use-typography("headline-small");
+
flex: 1;
color: var(--title-color);
padding-block: var(--title-padding);
diff --git a/frontend/src/app/main/ui/modal.scss b/frontend/src/app/main/ui/modal.scss
index b78ff64bf4..9068cc6eda 100644
--- a/frontend/src/app/main/ui/modal.scss
+++ b/frontend/src/app/main/ui/modal.scss
@@ -11,5 +11,5 @@
}
.modal-wrapper {
- @extend .new-scrollbar;
+ @extend %new-scrollbar;
}
diff --git a/frontend/src/app/main/ui/nitrate/entry.cljs b/frontend/src/app/main/ui/nitrate/entry.cljs
new file mode 100644
index 0000000000..4bcadf3216
--- /dev/null
+++ b/frontend/src/app/main/ui/nitrate/entry.cljs
@@ -0,0 +1,31 @@
+;; 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.nitrate.entry
+ (:require
+ [app.main.data.auth :as da]
+ [app.main.data.nitrate :as dnt]
+ [app.main.router :as rt]
+ [app.main.store :as st]
+ [app.main.ui.ds.product.loader :refer [loader*]]
+ [app.util.i18n :refer [tr]]
+ [rumext.v2 :as mf]))
+
+(mf/defc nitrate-entry*
+ {::mf/private true}
+ [{:keys [profile]}]
+ (mf/with-effect [profile]
+ (dnt/activate-nitrate-entry-popup!)
+ (if (da/is-authenticated? profile)
+ (st/emit! (rt/nav :dashboard-recent {:team-id (:default-team-id profile)}))
+ (st/emit! (rt/nav :auth-register))))
+
+ [:> loader* {:title (tr "labels.loading")
+ :overlay true}])
+
+(mf/defc nitrate-entry-page*
+ [props]
+ [:> nitrate-entry* props])
diff --git a/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs
new file mode 100644
index 0000000000..0f68a5b5f7
--- /dev/null
+++ b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.cljs
@@ -0,0 +1,67 @@
+;; 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.nitrate.nitrate-activation-success-modal
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.data.macros :as dm]
+ [app.common.time :as ct]
+ [app.main.data.modal :as modal]
+ [app.main.data.nitrate :as dnt]
+ [app.main.refs :as refs]
+ [app.main.ui.ds.buttons.button :refer [button*]]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
+ [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
+ [app.util.i18n :refer [tr]]
+ [rumext.v2 :as mf]))
+
+(mf/defc nitrate-activation-success-modal*
+ {::mf/register modal/components
+ ::mf/register-as :nitrate-activation-success
+ ::mf/wrap-props true}
+ [props]
+
+ (let [profile (mf/deref refs/profile)
+ light? (= "light" (:theme profile))
+ svg-id (if light? "logo-subscription-light" "logo-subscription")
+
+ cancel-at (dm/get-in props [:subscription :cancel-at])
+ date-str (when cancel-at
+ (ct/format-inst cancel-at "d MMMM, yyyy"))
+
+ on-create-org
+ (mf/use-fn
+ (fn []
+ (modal/hide!)
+ (dnt/go-to-nitrate-cc-create-org)))]
+
+ [:div {:class (stl/css :modal-overlay)}
+ [:div {:class (stl/css :modal-dialog)}
+ [:button {:class (stl/css :close-btn) :on-click modal/hide!}
+ [:> icon* {:icon-id "close"
+ :size "m"}]]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css :modal-start)}
+ [:> raw-svg* {:id svg-id}]]
+
+ [:div {:class (stl/css :modal-end)}
+ [:div {:class (stl/css :modal-title)}
+ (tr "nitrate.activation-success.title")]
+
+ [:p {:class (stl/css :modal-text-primary)}
+ (tr "nitrate.activation-success.active-until" date-str)]
+
+ [:p {:class (stl/css :modal-text)}
+ (tr "nitrate.activation-success.manage-info")]
+
+ [:p {:class (stl/css :modal-text)}
+ (tr "nitrate.activation-success.enjoy")]
+
+ [:> button* {:variant "primary"
+ :on-click on-create-org
+ :class (stl/css :modal-button)}
+ (tr "nitrate.activation-success.create-org")]]]]]))
diff --git a/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss
new file mode 100644
index 0000000000..5f1e8dd483
--- /dev/null
+++ b/frontend/src/app/main/ui/nitrate/nitrate_activation_success_modal.scss
@@ -0,0 +1,79 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "refactor/common-refactor.scss" as deprecated;
+@use "ds/typography.scss" as t;
+@use "ds/_borders.scss" as *;
+@use "ds/spacing.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/_utils.scss" as *;
+
+.modal-overlay {
+ @extend %modal-overlay-base;
+
+ z-index: var(--z-index-notifications);
+}
+
+.modal-dialog {
+ @extend %modal-container-base;
+
+ max-block-size: initial;
+ min-inline-size: px2rem(608);
+ max-inline-size: px2rem(608);
+ padding: var(--sp-xxxl);
+}
+
+.close-btn {
+ @extend %modal-close-btn-base;
+}
+
+.modal-content {
+ display: flex;
+ gap: $sz-40;
+}
+
+.modal-start {
+ display: flex;
+ justify-content: center;
+ min-inline-size: $sz-224;
+
+ @media (width <= 640px) {
+ display: none;
+ }
+}
+
+.modal-start svg {
+ inline-size: 100%;
+ block-size: auto;
+}
+
+.modal-end {
+ color: var(--color-foreground-secondary);
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-m);
+}
+
+.modal-title {
+ @include t.use-typography("title-large");
+
+ color: var(--modal-title-foreground-color);
+}
+
+.modal-text-primary {
+ @include t.use-typography("body-large");
+
+ color: var(--color-foreground-primary);
+}
+
+.modal-text {
+ @include t.use-typography("body-large");
+}
+
+.modal-button {
+ margin-block-start: var(--sp-s);
+ align-self: flex-start;
+}
diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs
new file mode 100644
index 0000000000..131dfc257c
--- /dev/null
+++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.cljs
@@ -0,0 +1,98 @@
+;; 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.nitrate.nitrate-code-activation-modal
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.main.data.modal :as modal]
+ [app.main.data.profile :as dprof]
+ [app.main.repo :as rp]
+ [app.main.store :as st]
+ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.foundations.assets.icon :as i]
+ [app.main.ui.nitrate.nitrate-activation-success-modal]
+ [app.util.dom :as dom]
+ [app.util.i18n :refer [tr]]
+ [beicon.v2.core :as rx]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(mf/defc nitrate-code-activation-modal*
+ {::mf/register modal/components
+ ::mf/register-as :nitrate-code-activation}
+ [_props]
+ (let [value* (mf/use-state "")
+ error* (mf/use-state nil)
+
+ on-change
+ (mf/use-fn
+ (fn [event]
+ (reset! error* nil)
+ (reset! value* (dom/get-target-val event))))
+
+ on-accept
+ (mf/use-fn
+ (mf/deps value*)
+ (fn [_]
+ (let [code (str/trim @value*)]
+ (when (seq code)
+ (->> (rp/cmd! ::redeem-nitrate-activation-code {:activation-code code})
+ (rx/subs!
+ (fn [result]
+ (modal/hide!)
+ (st/emit!
+ (modal/show {:type :nitrate-activation-success :subscription result})
+ (dprof/refresh-profile)))
+ (fn [error]
+ ;; TODO: "Already used" is not yet detectable (CC upserts on reuse).
+ (let [code (-> error ex-data :code)]
+ (reset! error* (case code
+ :expired-activation-code (tr "nitrate.activation-code.expired-error")
+ (tr "nitrate.activation-code.invalid-error")))))))))))
+
+ on-key-down
+ (mf/use-fn
+ (mf/deps on-accept)
+ (fn [event]
+ (when (and (= "Enter" (.-key event)) (.-ctrlKey event))
+ (on-accept event))))]
+
+ [:div {:class (stl/css :modal-overlay)}
+ [:div {:class (stl/css :modal-dialog)}
+ [:> icon-button* {:variant "ghost"
+ :class (stl/css :close-btn)
+ :aria-label (tr "labels.close")
+ :on-click modal/hide!
+ :icon i/close}]
+
+ [:div {:class (stl/css :modal-header)}
+ [:h2 {:class (stl/css :modal-title)} (tr "nitrate.code-activation.title")]]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css-case :code-field true :invalid (some? @error*))}
+ [:label {:class (stl/css :code-label)}
+ (tr "nitrate.code-activation.input-label")]
+ [:textarea {:class (stl/css :code-textarea)
+ :auto-focus true
+ :value @value*
+ :placeholder (tr "nitrate.code-activation.placeholder")
+ :on-change on-change
+ :on-key-down on-key-down}]
+ (when @error*
+ [:span {:class (stl/css :error-msg)} @error*])]
+
+ [:input
+ {:type "button"
+ :class (stl/css-case :accept-btn true
+ :global/disabled (empty? (str/trim @value*)))
+ :disabled (empty? (str/trim @value*))
+ :value (tr "nitrate.code-activation.submit")
+ :on-click on-accept}]
+ [:div {:class (stl/css :footer-text)}
+ (tr "nitrate.code-activation.footer") " "
+ [:a {:class (stl/css :link)
+ :href "mailto:sales@nitrate.com"}
+ "sales@nitrate.com"]]]]]))
diff --git a/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss
new file mode 100644
index 0000000000..d241c38332
--- /dev/null
+++ b/frontend/src/app/main/ui/nitrate/nitrate_code_activation_modal.scss
@@ -0,0 +1,107 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) KALEIDOS INC
+
+@use "refactor/common-refactor.scss" as deprecated;
+@use "ds/typography.scss" as t;
+@use "ds/spacing.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/_borders.scss" as *;
+
+.close-btn {
+ @extend %modal-close-btn-base;
+}
+
+.modal-overlay {
+ @extend %modal-overlay-base;
+
+ z-index: var(--z-index-notifications);
+}
+
+.modal-dialog {
+ @extend %modal-container-base;
+
+ inline-size: $sz-480;
+ max-inline-size: $sz-480;
+ max-block-size: none;
+ max-height: none;
+ padding: var(--sp-xxxl);
+}
+
+.modal-title {
+ @include t.use-typography("title-large");
+
+ color: var(--modal-title-foreground-color);
+ margin-block-end: var(--sp-xxxl);
+}
+
+.modal-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-m);
+ color: var(--color-foreground-secondary);
+}
+
+.accept-btn {
+ @extend %modal-accept-btn;
+
+ inline-size: 100%;
+}
+
+.code-field {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+}
+
+.code-label {
+ @include t.use-typography("body-medium");
+
+ color: var(--color-foreground-secondary);
+}
+
+.code-textarea {
+ @include t.use-typography("body-medium");
+
+ block-size: $sz-200;
+ resize: vertical;
+ font-family: monospace;
+ word-break: break-all;
+ padding: var(--sp-s);
+ border-radius: $br-8;
+ border: $b-1 solid var(--input-border-color);
+ background-color: var(--input-background-color);
+ color: var(--color-foreground-primary);
+ outline: none;
+}
+
+.code-textarea:focus {
+ border-color: var(--color-accent-primary);
+}
+
+.invalid .code-textarea {
+ border-color: var(--input-border-color-error);
+}
+
+.invalid .code-textarea:focus {
+ border-color: var(--input-border-color-error);
+}
+
+.error-msg {
+ @include t.use-typography("body-small");
+
+ color: var(--element-foreground-error);
+}
+
+.footer-text {
+ @include t.use-typography("body-medium");
+
+ color: var(--color-foreground-secondary);
+ margin-block-start: var(--sp-xxxl);
+}
+
+.link {
+ color: var(--color-accent-primary);
+}
diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs
index de55959c6f..13143d5337 100644
--- a/frontend/src/app/main/ui/nitrate/nitrate_form.cljs
+++ b/frontend/src/app/main/ui/nitrate/nitrate_form.cljs
@@ -10,10 +10,14 @@
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.data.nitrate :as dnt]
+ [app.main.refs :as refs]
+ [app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
+ [app.main.ui.nitrate.nitrate-code-activation-modal]
+ [app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private schema:nitrate-form
@@ -27,6 +31,7 @@
[connectivity]
(let [online? (:licenses connectivity)
+ profile (mf/deref refs/profile)
initial (mf/with-memo []
{:subscription "yearly"})
form (fm/use-form :schema schema:nitrate-form
@@ -35,7 +40,12 @@
(mf/use-fn
(mf/deps form)
(fn []
- (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name))))]
+ (dnt/go-to-buy-nitrate-license (-> @form :clean-data :subscription name))))
+
+ on-activate-click
+ (mf/use-fn
+ (fn []
+ (st/emit! (modal/show {:type :nitrate-code-activation}))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :subscription-success)}
@@ -45,11 +55,11 @@
[:div {:class (stl/css :modal-success-content)}
[:div {:class (stl/css :modal-start)}
;; TODO this svg is a placeholder. Use the proper one when created
- [:> raw-svg* {:id "logo-subscription"}]]
+ [:> raw-svg* {:id "nitrate-welcome"}]]
[:div {:class (stl/css :modal-end)}
[:div {:class (stl/css :modal-title)}
- "Unlock Nitrate Features"]
+ (tr "nitrate.form.title")]
[:p {:class (stl/css :modal-text-large)}
"Prow scuttle parrel provost."]
@@ -62,8 +72,8 @@
[:p {:class (stl/css :modal-text-large)}
[:& fm/radio-buttons
- {:options [{:label "Price Tag Montly" :value "monthly"}
- {:label "Price Tag Yearly (Discount)" :value "yearly"}]
+ {:options [{:label (tr "nitrate.form.billing-monthly") :value "monthly"}
+ {:label (tr "nitrate.form.billing-yearly") :value "yearly"}]
:name :subscription
:class (stl/css :radio-btns)}]]
@@ -72,20 +82,35 @@
[:> button* {:variant "primary"
:on-click on-click
:class (stl/css :modal-button)}
- "UPGRADE TO NITRATE"]
+ (if (:subscription profile)
+ (tr "nitrate.form.upgrade")
+ (tr "nitrate.form.try-free"))]
[:div {:class (stl/css :modal-text-small :modal-info)}
- "Cancel anytime before your next billing cycle."]]]
+ (tr "nitrate.form.cancel-anytime")]]]
+ [:p {:class (stl/css :modal-text-medium)}
+ (tr "nitrate.form.have-code") " " [:a {:class (stl/css :link)
+ :on-click on-activate-click}
+ (tr "nitrate.form.enter-code")]]
[:p {:class (stl/css :modal-text-medium)}
[:a {:class (stl/css :link) :href dnt/go-to-subscription-url}
- "See my current plan"]]]
+ (tr "nitrate.form.see-plan")]]]
[:div {:class (stl/css :contact)}
[:p {:class (stl/css :modal-text-large)}
- "Contact us to upgrade to Nitrate:"]
+ (if (:subscription profile)
+ (tr "nitrate.form.contact-upgrade")
+ (tr "nitrate.form.contact-trial"))]
[:p {:class (stl/css :modal-text-large)}
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
- "sales@penpot.app"]]])]]]]))
+ "sales@penpot.app"]]
+ [:div {:class (stl/css :activation-code)}
+ [:p {:class (stl/css :modal-text-large)}
+ (tr "nitrate.form.have-code")]
+ [:p {:class (stl/css :modal-text-large)}
+ [:a {:class (stl/css :link)
+ :on-click on-activate-click}
+ (tr "nitrate.form.enter-code")]]]])]]]]))
diff --git a/frontend/src/app/main/ui/nitrate/nitrate_form.scss b/frontend/src/app/main/ui/nitrate/nitrate_form.scss
index bc48fe7a6d..76942a6f7a 100644
--- a/frontend/src/app/main/ui/nitrate/nitrate_form.scss
+++ b/frontend/src/app/main/ui/nitrate/nitrate_form.scss
@@ -12,22 +12,31 @@
@use "ds/_utils.scss" as *;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
z-index: var(--z-index-notifications);
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
max-block-size: initial;
- min-inline-size: px2rem(648);
+ min-inline-size: px2rem(1021);
+ padding: px2rem(80);
+
+ @media (width <= 1024px) {
+ min-inline-size: px2rem(712);
+ padding: var(--sp-xxxl);
+ }
}
.close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-title {
@include t.use-typography("title-large");
+
margin-block-end: var(--sp-xxxl);
color: var(--modal-title-foreground-color);
display: flex;
@@ -66,33 +75,42 @@
.modal-start {
display: flex;
justify-content: center;
- max-inline-size: $sz-224;
+ min-inline-size: $sz-284;
- svg {
- inline-size: 100%;
- block-size: auto;
- }
-
- @media (max-inline-size: 992px) {
+ @media (width <= 992px) {
display: none;
}
}
-.radio-btns {
- label {
- @include t.use-typography("body-large");
- padding: 0;
- display: flex;
- align-items: center;
- }
+.modal-start svg {
+ inline-size: 100%;
+ block-size: auto;
+}
+.radio-btns {
display: flex;
flex-direction: column;
padding: var(--sp-l) 0 0 0;
gap: 0;
}
+.radio-btns label {
+ @include t.use-typography("body-large");
+
+ padding: 0;
+ display: flex;
+ align-items: center;
+}
+
.contact {
margin-block-start: $sz-96;
color: var(--color-foreground-primary);
}
+
+.activation-code {
+ margin-block-start: var(--sp-xxxl);
+}
+
+.link {
+ color: var(--color-accent-primary);
+}
diff --git a/frontend/src/app/main/ui/notifications.cljs b/frontend/src/app/main/ui/notifications.cljs
index de7161db99..e318946b6a 100644
--- a/frontend/src/app/main/ui/notifications.cljs
+++ b/frontend/src/app/main/ui/notifications.cljs
@@ -27,14 +27,7 @@
(= :floating (:position notification)))
toast? (or (= :toast (:type notification))
(some? (:timeout notification)))
- content (or (:content notification) "")
-
- show-detail* (mf/use-state false)
-
- handle-toggle-detail
- (mf/use-fn
- (fn []
- (swap! show-detail* not)))]
+ content (or (:content notification) "")]
(when notification
(cond
@@ -43,9 +36,8 @@
{:level (or (:level notification) :info)
:type (:type notification)
:detail (:detail notification)
- :on-close on-close
- :show-detail @show-detail*
- :on-toggle-detail handle-toggle-detail} content]
+ :on-close on-close}
+ content]
inline?
[:& inline-notification
diff --git a/frontend/src/app/main/ui/notifications/badge.scss b/frontend/src/app/main/ui/notifications/badge.scss
index 99941b8fb4..54f46964ce 100644
--- a/frontend/src/app/main/ui/notifications/badge.scss
+++ b/frontend/src/app/main/ui/notifications/badge.scss
@@ -7,10 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.badge-notification {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
--badge-notification-bg-color: var(--alert-background-color-default);
--badge-notification-fg-color: var(--alert-text-foreground-color-default);
--badge-notification-border-color: var(--alert-border-color-default);
+
box-sizing: border-box;
display: grid;
place-items: center;
@@ -29,7 +31,8 @@
}
.small {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
min-height: deprecated.$s-20;
border-radius: deprecated.$br-6;
}
diff --git a/frontend/src/app/main/ui/notifications/context_notification.scss b/frontend/src/app/main/ui/notifications/context_notification.scss
index 1b14e33cea..aa38cea54a 100644
--- a/frontend/src/app/main/ui/notifications/context_notification.scss
+++ b/frontend/src/app/main/ui/notifications/context_notification.scss
@@ -11,6 +11,7 @@
--context-notification-fg-color: var(--alert-text-foreground-color-default);
--context-notification-icon-color: var(--alert-icon-foreground-color-default);
--context-notification-border-color: var(--alert-border-color-default);
+
box-sizing: border-box;
display: grid;
grid-template-columns: deprecated.$s-16 1fr;
@@ -60,13 +61,15 @@
}
.icon {
- @extend .button-icon;
+ @extend %button-icon;
+
align-self: self-start;
stroke: var(--context-notification-icon-color);
}
.context-text {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
align-self: center;
color: var(--context-notification-fg-color);
margin: auto 0;
@@ -78,7 +81,8 @@
.link,
.contain-html .context-text a {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
align-self: center;
display: inline;
text-align: left;
diff --git a/frontend/src/app/main/ui/notifications/inline_notification.scss b/frontend/src/app/main/ui/notifications/inline_notification.scss
index ee71bc5c3d..4679db4026 100644
--- a/frontend/src/app/main/ui/notifications/inline_notification.scss
+++ b/frontend/src/app/main/ui/notifications/inline_notification.scss
@@ -20,5 +20,7 @@
}
.link {
+ @extend %link;
+
margin: 0;
}
diff --git a/frontend/src/app/main/ui/onboarding/questions.scss b/frontend/src/app/main/ui/onboarding/questions.scss
index 94444a493b..24ca56f535 100644
--- a/frontend/src/app/main/ui/onboarding/questions.scss
+++ b/frontend/src/app/main/ui/onboarding/questions.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -15,8 +15,7 @@
max-height: fit-content;
width: fit-content;
padding-inline: deprecated.$s-100;
- padding-block-start: deprecated.$s-40;
- padding-block-end: deprecated.$s-72;
+ padding-block: deprecated.$s-40 deprecated.$s-72;
border-radius: deprecated.$br-8;
border: deprecated.$s-2 solid var(--modal-border-color);
background-color: var(--modal-background-color);
@@ -30,26 +29,28 @@
// STEP CONTAINER
.paginator {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
height: deprecated.$s-20;
text-align: right;
color: var(--modal-text-foreground-color);
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
+
.next-button {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
}
.prev-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.radio-btns label,
.select-class span {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
}
// STEP 1
@@ -61,21 +62,24 @@
}
.modal-title {
- @include deprecated.bigTitleTipography;
+ @include deprecated.big-title-typography;
+
color: var(--modal-title-foreground-color);
min-height: deprecated.$s-32;
margin-block: auto;
}
.modal-subtitle {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
margin: 0;
padding: 0;
}
.modal-text {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
margin: 0;
}
@@ -88,6 +92,7 @@
max-width: deprecated.$s-540;
width: deprecated.$s-540;
}
+
.step-2 {
grid-template-rows: deprecated.$s-20 auto auto deprecated.$s-32;
}
@@ -121,8 +126,7 @@
display: grid;
grid-template-rows: 1fr 1fr;
grid-template-columns: deprecated.$s-92 deprecated.$s-92 deprecated.$s-92;
- row-gap: deprecated.$s-16;
- column-gap: deprecated.$s-24;
+ gap: deprecated.$s-16 deprecated.$s-24;
justify-content: center;
}
@@ -133,7 +137,7 @@
}
.input-spacing input {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
}
// STEP-4
diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs
index 0163de6c3d..49dd1cb4e8 100644
--- a/frontend/src/app/main/ui/onboarding/team_choice.cljs
+++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs
@@ -237,7 +237,7 @@
[:div {:class (stl/css-case
:modal-overlay true)}
- [:div.animated.fadeIn {:class (stl/css :modal-container)}
+ [:div.animated.fade-in {:class (stl/css :modal-container)}
[:h1 {:class (stl/css :modal-title)}
(tr "onboarding-v2.welcome.title")]
[:div {:class (stl/css :modal-sections)}
diff --git a/frontend/src/app/main/ui/onboarding/team_choice.scss b/frontend/src/app/main/ui/onboarding/team_choice.scss
index 067a1f1346..8b6487e53d 100644
--- a/frontend/src/app/main/ui/onboarding/team_choice.scss
+++ b/frontend/src/app/main/ui/onboarding/team_choice.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -16,8 +16,7 @@
max-height: deprecated.$s-800;
height: 100%;
padding-inline: deprecated.$s-100;
- padding-block-start: deprecated.$s-40;
- padding-block-end: deprecated.$s-40;
+ padding-block: deprecated.$s-40 deprecated.$s-40;
border-radius: deprecated.$br-8;
background-color: var(--modal-background-color);
border: deprecated.$s-2 solid var(--modal-border-color);
@@ -35,7 +34,8 @@
}
.paginator {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
position: absolute;
top: deprecated.$s-40;
right: deprecated.$s-100;
@@ -54,12 +54,14 @@
}
.modal-title {
- @include deprecated.bigTitleTipography;
+ @include deprecated.big-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-subtitle {
- @include deprecated.medTitleTipography;
+ @include deprecated.med-title-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -68,51 +70,59 @@
}
.modal-text {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
margin: 0;
}
.modal-desc {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
margin: 0;
color: var(--modal-title-foreground-color);
}
.team-features {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
gap: deprecated.$s-16;
margin: 0;
}
.feature {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
gap: deprecated.$s-16;
}
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
border-radius: deprecated.$br-circle;
border: deprecated.$s-1 solid var(--color-accent-primary);
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-accent-primary);
}
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
+
justify-content: flex-end;
}
.accept-button {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
}
.back-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
// SEPARATOR
@@ -120,7 +130,7 @@
width: deprecated.$s-8;
height: 100%;
border-radius: deprecated.$br-8;
- opacity: 42%;
+ opacity: 0.42;
background-color: var(--modal-separator-background-color);
}
@@ -140,7 +150,8 @@
.first-block,
.second-block {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
gap: deprecated.$s-16;
}
@@ -151,10 +162,12 @@
}
.team-name-input {
- @extend .input-element-label;
+ @extend %input-element-label;
+
label {
- @include deprecated.flexColumn;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-small-typography;
+
align-items: flex-start;
width: 100%;
border: none;
@@ -162,7 +175,8 @@
height: 100%;
input {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-top: deprecated.$s-8;
}
}
@@ -187,7 +201,8 @@
}
.role-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
margin-block-end: deprecated.$s-8;
color: var(--modal-title-foreground-color);
}
@@ -198,7 +213,8 @@
}
.modal-hint {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--modal-text-foreground-color);
text-align: right;
}
diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs
index 7919fc045b..7776e783a9 100644
--- a/frontend/src/app/main/ui/releases.cljs
+++ b/frontend/src/app/main/ui/releases.cljs
@@ -54,7 +54,7 @@
(let [slide* (mf/use-state :start)
slide (deref slide*)
- klass* (mf/use-state "fadeInDown")
+ klass* (mf/use-state "fade-in-down")
klass (deref klass*)
navigate
@@ -79,7 +79,7 @@
(mf/with-effect [slide]
(when (not= :start slide)
- (reset! klass* "fadeIn"))
+ (reset! klass* "fade-in"))
(let [sem (tm/schedule 300 #(reset! klass* nil))]
(fn []
(reset! klass* nil)
diff --git a/frontend/src/app/main/ui/releases.scss b/frontend/src/app/main/ui/releases.scss
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frontend/src/app/main/ui/releases/common.cljs b/frontend/src/app/main/ui/releases/common.cljs
index 4e3ce7cc5e..3da4516e0c 100644
--- a/frontend/src/app/main/ui/releases/common.cljs
+++ b/frontend/src/app/main/ui/releases/common.cljs
@@ -11,7 +11,7 @@
(defmulti render-release-notes :version)
-(mf/defc navigation-bullets
+(mf/defc navigation-bullets*
[{:keys [slide navigate total]}]
[:ul {:class (stl/css :step-dots)}
(for [i (range total)]
diff --git a/frontend/src/app/main/ui/releases/common.scss b/frontend/src/app/main/ui/releases/common.scss
index 977411aec5..e3ab396ec1 100644
--- a/frontend/src/app/main/ui/releases/common.scss
+++ b/frontend/src/app/main/ui/releases/common.scss
@@ -15,8 +15,7 @@
width: fit-content;
margin: 0;
padding: 0;
- align-self: center;
- justify-self: flex-start;
+ place-self: center flex-start;
}
.dot {
diff --git a/frontend/src/app/main/ui/releases/v1_11.cljs b/frontend/src/app/main/ui/releases/v1_11.cljs
index 395cd72ee7..7542f9339b 100644
--- a/frontend/src/app/main/ui/releases/v1_11.cljs
+++ b/frontend/src/app/main/ui/releases/v1_11.cljs
@@ -45,7 +45,7 @@
[:p "Use dissolve, slide and push animations to fade screens and imitate gestures like swipe."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]]]]]]
@@ -64,7 +64,7 @@
[:p "Now you can decide to include their backgrounds on your exports or leave them out."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]]]]]]
@@ -83,7 +83,7 @@
[:p "We’ve also added two new options to scale your designs at the view mode that might help you to make your presentations look better."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_12.cljs b/frontend/src/app/main/ui/releases/v1_12.cljs
index 65d7e2a41d..b38d7b12a1 100644
--- a/frontend/src/app/main/ui/releases/v1_12.cljs
+++ b/frontend/src/app/main/ui/releases/v1_12.cljs
@@ -45,7 +45,7 @@
[:p "Along with a better organization of panels (say hello to typography toolbar!) and new shortcuts that will speed your workflow."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "And they don’t come alone, but with some nice improvements to the rulers."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -82,7 +82,7 @@
[:p "Scrollbars at the design workspace will make it more obvious how to navigate it and easier for some users, for instance those who love using graphic tablets, from now on, will feel just as comfortable as those who use a mouseAnd they don’t come alone, but with some nice improvements to the rulers."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -101,7 +101,7 @@
[:p "This is a must if you’re working with grids (if you’re not, you should ;)), being able to adjust the movement to your baseline grid (8px? 5px?) is a huge timesaver that will improve your quality of life while designing."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_13.cljs b/frontend/src/app/main/ui/releases/v1_13.cljs
index 39ad2c79ac..9d3c0e6b0a 100644
--- a/frontend/src/app/main/ui/releases/v1_13.cljs
+++ b/frontend/src/app/main/ui/releases/v1_13.cljs
@@ -45,7 +45,7 @@
[:p "Use the export window to manage your multiple exports and be informed about the download progress. Big exports will happen in the background so you can keep designing in the meantime ;)"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "This opens endless graphic possibilities such as combining gradients and blending modes in the same element to create sophisticated visual effects."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "A refreshed interface and two new features! The Invitations section allows you to check the status of current team invites plus you now have the ability to invite multiple members at the same time."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "As a side effect, this can give you a performance boost in massive designs."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_14.cljs b/frontend/src/app/main/ui/releases/v1_14.cljs
index 334e993f62..106ffaaf49 100644
--- a/frontend/src/app/main/ui/releases/v1_14.cljs
+++ b/frontend/src/app/main/ui/releases/v1_14.cljs
@@ -45,7 +45,7 @@
[:p "Categories and filters will help you to find the shortcut you need. One of the most requested features by the community!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Play with the colors of a group without the hassles of individual selection!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "Ideal for prototyping fixed headers, navbars and floating buttons."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "Until now you could only do it by renaming the groups, now with drag & drop it is much more user friendly."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_15.cljs b/frontend/src/app/main/ui/releases/v1_15.cljs
index 9cdb26b079..0d04a305e4 100644
--- a/frontend/src/app/main/ui/releases/v1_15.cljs
+++ b/frontend/src/app/main/ui/releases/v1_15.cljs
@@ -45,7 +45,7 @@
[:p "Say goodbye to Artboards and hello to Boards!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Now you can thanks to new permissions that allow you to decide who can comment and/or inspect the code at a shared prototype link."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "Also, comments inside boards will be associated with it, so that if you move a board its comments will maintain its place inside it."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "We’ve also made some adjustments to ensure the access to the options from small screens."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_16.cljs b/frontend/src/app/main/ui/releases/v1_16.cljs
index 537507abb6..26db75b099 100644
--- a/frontend/src/app/main/ui/releases/v1_16.cljs
+++ b/frontend/src/app/main/ui/releases/v1_16.cljs
@@ -45,7 +45,7 @@
[:p "We heard the users before refreshing the interface, simplifying it to give prominence to the content. And yes, now that you ask, the dark theme is coming soon."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "You no longer need to to download most of them to the computer before importing."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "More relevant info and better explanations, a refined new team and invitation flow, a beginners tutorial and a walkthrough file that will help newcomers learn how to use and start designing with Penpot faster."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "This was a contribution by our community member @andrewzhurov <3"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_17.cljs b/frontend/src/app/main/ui/releases/v1_17.cljs
index 1965748c6a..668f879f0d 100644
--- a/frontend/src/app/main/ui/releases/v1_17.cljs
+++ b/frontend/src/app/main/ui/releases/v1_17.cljs
@@ -45,7 +45,7 @@
[:p "Penpot brings a layout system like no other. As described by one of our beta testers: 'I love the fact that Penpot is following the CSS FlexBox, which is making UI Design a step closer to the logic and behavior behind how things will be actually built after design.'"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Also, inspect mode provides a safer view-only mode and other improvements."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "While we are still working on a plugin system, this is a great and simple way to create integrations with other services."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "This release comes with improvements on color contrasts, alt texts, semantic labels, focusable items and keyboard navigation at login and dashboard, but more will come."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_18.cljs b/frontend/src/app/main/ui/releases/v1_18.cljs
index cb6d73458c..ff1c06d179 100644
--- a/frontend/src/app/main/ui/releases/v1_18.cljs
+++ b/frontend/src/app/main/ui/releases/v1_18.cljs
@@ -45,7 +45,7 @@
[:p "And not only that, when creating Flex layouts, the spacing is predicted, helping you to maintain your design composition."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Now you can exclude elements from the Flex layout flow using absolute position."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "This is another capability that brings Penpot Flex layout even closer to the power of CSS standards."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "Activate the scale tool by pressing K and scale your elements, maintaining their visual aspect."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_19.cljs b/frontend/src/app/main/ui/releases/v1_19.cljs
index 8543a0c45b..6b51d8faaf 100644
--- a/frontend/src/app/main/ui/releases/v1_19.cljs
+++ b/frontend/src/app/main/ui/releases/v1_19.cljs
@@ -72,7 +72,7 @@
" in particular and the Penpot community as a whole!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]]]]]]
@@ -99,7 +99,7 @@
"to the Penpot’s plugins system."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_4.cljs b/frontend/src/app/main/ui/releases/v1_4.cljs
index bc80923258..37ecf7be42 100644
--- a/frontend/src/app/main/ui/releases/v1_4.cljs
+++ b/frontend/src/app/main/ui/releases/v1_4.cljs
@@ -45,7 +45,7 @@
[:p "To open a file you just have to double click it. You can also open a file in a new tab with right click."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Also, now you have an easy way to manage files and projects between teams."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "If you write in arabic, hebrew or other RTL language text direction will be automatically detected in text layers."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "This is why the standard blend modes and opacity level are now available for each element."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_5.cljs b/frontend/src/app/main/ui/releases/v1_5.cljs
index 8d962515d7..d183bd7976 100644
--- a/frontend/src/app/main/ui/releases/v1_5.cljs
+++ b/frontend/src/app/main/ui/releases/v1_5.cljs
@@ -45,7 +45,7 @@
[:p "The usability and performance of the paths tool has been improved too."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]]]]]]
@@ -64,7 +64,7 @@
[:p "It is time to have all the libraries well organized and work more efficiently."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]]]]]]
@@ -83,7 +83,7 @@
[:p "It's easier to specify by how much you want to change a value and work with measures and distances."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_6.cljs b/frontend/src/app/main/ui/releases/v1_6.cljs
index c6636c4550..cf1c96bbe9 100644
--- a/frontend/src/app/main/ui/releases/v1_6.cljs
+++ b/frontend/src/app/main/ui/releases/v1_6.cljs
@@ -45,7 +45,7 @@
[:p "We hope you enjoy having more typography options and our brand new font selector."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Disabled by default, this tool is disabled back after being used."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "You should have the feeling that files and layers show up a bit faster :)"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "An easy way to increase speed by working with vectors!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_7.cljs b/frontend/src/app/main/ui/releases/v1_7.cljs
index 32666d5158..3d0c2db7da 100644
--- a/frontend/src/app/main/ui/releases/v1_7.cljs
+++ b/frontend/src/app/main/ui/releases/v1_7.cljs
@@ -48,7 +48,7 @@
suits you better!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -70,7 +70,7 @@
components."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -90,7 +90,7 @@
[:p "Easily " [:strong "rename and ungroup"] " asset groups."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -108,7 +108,7 @@
[:p "Do you sometimes copy and paste component copies that belong to a library already shared by the original and destination files? From now on, those component copies are aware of this and will retain their linkage to the library."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_8.cljs b/frontend/src/app/main/ui/releases/v1_8.cljs
index dfff4bd9f2..fcf214cae9 100644
--- a/frontend/src/app/main/ui/releases/v1_8.cljs
+++ b/frontend/src/app/main/ui/releases/v1_8.cljs
@@ -45,7 +45,7 @@
[:p "You can also create a shareable link deciding which pages will be available for the visitors. Sharing is caring!"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "You can select different styles for each end of an open path: arrows, square, circle, diamond or just a round ending are the available options."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "Quick and easy :)"]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p "Now you can easily export all the artboards of a page to a single pdf file."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v1_9.cljs b/frontend/src/app/main/ui/releases/v1_9.cljs
index 6a8ddfba80..d359063a6e 100644
--- a/frontend/src/app/main/ui/releases/v1_9.cljs
+++ b/frontend/src/app/main/ui/releases/v1_9.cljs
@@ -45,7 +45,7 @@
[:p "Create overlays, back buttons or links to URLs to mimic the behavior of the product you’re designing."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -64,7 +64,7 @@
[:p "Flows allow you to define multiple starting points within the same page so you can better organize and present your prototypes."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -83,7 +83,7 @@
[:p "Using boolean operations will lead to countless graphic possibilities for your designs."]]
[:div.modal-navigation
[:button.btn-secondary {:on-click next} "Continue"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]]
@@ -102,7 +102,7 @@
[:p [:a {:alt "Explore libraries & templates" :target "_blank" :href "https://penpot.app/libraries-templates"} "Explore libraries & templates"]]]
[:div.modal-navigation
[:button.btn-secondary {:on-click finish} "Start!"]
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]]]]]])))
diff --git a/frontend/src/app/main/ui/releases/v2_0.cljs b/frontend/src/app/main/ui/releases/v2_0.cljs
index 57f2b0847b..fd1299d0d7 100644
--- a/frontend/src/app/main/ui/releases/v2_0.cljs
+++ b/frontend/src/app/main/ui/releases/v2_0.cljs
@@ -92,7 +92,7 @@
" up the design as code to take it from there."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -126,7 +126,7 @@
" and adherence to other best practices."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -161,7 +161,7 @@
"that will help you to better manage your design systems."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -193,7 +193,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
diff --git a/frontend/src/app/main/ui/releases/v2_0.scss b/frontend/src/app/main/ui/releases/v2_0.scss
index 0d5bc38d2e..f47c7c9043 100644
--- a/frontend/src/app/main/ui/releases/v2_0.scss
+++ b/frontend/src/app/main/ui/releases/v2_0.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -38,8 +38,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -48,7 +49,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -66,18 +68,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -91,7 +96,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_1.scss b/frontend/src/app/main/ui/releases/v2_1.scss
index 7b2559bc96..4b9913e040 100644
--- a/frontend/src/app/main/ui/releases/v2_1.scss
+++ b/frontend/src/app/main/ui/releases/v2_1.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -38,8 +38,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -48,7 +49,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -60,7 +62,8 @@
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
@@ -72,7 +75,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_10.cljs b/frontend/src/app/main/ui/releases/v2_10.cljs
index fcefb326c5..297c1e77d6 100644
--- a/frontend/src/app/main/ui/releases/v2_10.cljs
+++ b/frontend/src/app/main/ui/releases/v2_10.cljs
@@ -74,7 +74,7 @@
"This release has been shaped by our amazing community. A huge thank-you to everyone who shared ideas, feedback, and insights to make Penpot Variants possible <3"]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -113,7 +113,7 @@
" now to join us 8-10 October, in Madrid!"]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -143,7 +143,7 @@
"This latest update brings—no more no less than—six new token types, significantly boosting your ability to manage design decisions, particularly in typography: Font Family, Font Weight, Text Case, Text Decoration, Letter Spacing token, and Number token (for unitless values)."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -174,7 +174,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
diff --git a/frontend/src/app/main/ui/releases/v2_10.scss b/frontend/src/app/main/ui/releases/v2_10.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_10.scss
+++ b/frontend/src/app/main/ui/releases/v2_10.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_11.cljs b/frontend/src/app/main/ui/releases/v2_11.cljs
index a4b330f8bc..529a6cb0a7 100644
--- a/frontend/src/app/main/ui/releases/v2_11.cljs
+++ b/frontend/src/app/main/ui/releases/v2_11.cljs
@@ -74,7 +74,7 @@
"The Typography token also marks a big step forward for Penpot: it’s our first composite token! Composite tokens are special because they can hold multiple properties within one token. Shadow token will be the next composite token coming your way."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -110,7 +110,7 @@
"- Reorder your component properties by drag & drop: Because organization matters, now you can arrange your properties however makes the most sense to you, so you can keep the ones you use most often right where you want them."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -148,7 +148,7 @@
"Invited users will also get clearer emails, including a reminder sent one day before the invite expires (after seven days). Simple, clean, and much more efficient."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -179,7 +179,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
diff --git a/frontend/src/app/main/ui/releases/v2_11.scss b/frontend/src/app/main/ui/releases/v2_11.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_11.scss
+++ b/frontend/src/app/main/ui/releases/v2_11.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_12.cljs b/frontend/src/app/main/ui/releases/v2_12.cljs
index 43ac723024..342f92100c 100644
--- a/frontend/src/app/main/ui/releases/v2_12.cljs
+++ b/frontend/src/app/main/ui/releases/v2_12.cljs
@@ -80,7 +80,7 @@
"Developers now get a clearer context during handoff. The Inspect panel shows the actual token used in your design, in a similar way to how styles are displayed. This small detail reduces ambiguity, aligns everyone on the same language, and strengthens collaboration across the team."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -116,7 +116,7 @@
"It’s a subtle improvement, but it removes friction you feel hundreds of times a week, and makes component work flow more naturally."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -152,7 +152,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
diff --git a/frontend/src/app/main/ui/releases/v2_12.scss b/frontend/src/app/main/ui/releases/v2_12.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_12.scss
+++ b/frontend/src/app/main/ui/releases/v2_12.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_13.cljs b/frontend/src/app/main/ui/releases/v2_13.cljs
index 149d914c61..54a0badaee 100644
--- a/frontend/src/app/main/ui/releases/v2_13.cljs
+++ b/frontend/src/app/main/ui/releases/v2_13.cljs
@@ -74,7 +74,7 @@
"Highly requested, long overdue, and now officially here."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -108,7 +108,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]
diff --git a/frontend/src/app/main/ui/releases/v2_13.scss b/frontend/src/app/main/ui/releases/v2_13.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_13.scss
+++ b/frontend/src/app/main/ui/releases/v2_13.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_14.cljs b/frontend/src/app/main/ui/releases/v2_14.cljs
index b424d4bfa8..9dd3013274 100644
--- a/frontend/src/app/main/ui/releases/v2_14.cljs
+++ b/frontend/src/app/main/ui/releases/v2_14.cljs
@@ -74,7 +74,7 @@
"One extra detail: if you edit the path and change group segments, the token is moved to its new group (creating it if needed), and empty groups are automatically cleaned up."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -104,7 +104,7 @@
"If you’ve been waiting to generate tokens, sync them, or manipulate them from your own tools, this is the missing piece. And yes, this one has been requested a lot."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -134,7 +134,7 @@
"Remapping is always optional, because sometimes you don’t want to keep the current connections. When enabled, it affects all tokens in the file and also takes libraries into account, so main components can propagate changes to child components, and applied tokens update on the elements using them."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -168,7 +168,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
diff --git a/frontend/src/app/main/ui/releases/v2_14.scss b/frontend/src/app/main/ui/releases/v2_14.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_14.scss
+++ b/frontend/src/app/main/ui/releases/v2_14.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_15.cljs b/frontend/src/app/main/ui/releases/v2_15.cljs
index 8c2f61580f..76f6527f02 100644
--- a/frontend/src/app/main/ui/releases/v2_15.cljs
+++ b/frontend/src/app/main/ui/releases/v2_15.cljs
@@ -74,7 +74,7 @@
"You can run MCP in two ways. Remote MCP is hosted and simpler to set up. Local MCP runs on your machine and gives advanced teams extra control. Same vision, different operating model."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -115,7 +115,7 @@
"This is where MCP becomes workflow infrastructure. Less manual glue work, fewer handoff gaps, and faster iterations between designers and developers."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -149,7 +149,7 @@
"]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
diff --git a/frontend/src/app/main/ui/releases/v2_15.scss b/frontend/src/app/main/ui/releases/v2_15.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_15.scss
+++ b/frontend/src/app/main/ui/releases/v2_15.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_2.scss b/frontend/src/app/main/ui/releases/v2_2.scss
index 34d030466f..beb1bdf674 100644
--- a/frontend/src/app/main/ui/releases/v2_2.scss
+++ b/frontend/src/app/main/ui/releases/v2_2.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -38,8 +38,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -48,7 +49,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -60,7 +62,8 @@
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
@@ -72,7 +75,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_3.cljs b/frontend/src/app/main/ui/releases/v2_3.cljs
index 8b3040b8f4..6063642485 100644
--- a/frontend/src/app/main/ui/releases/v2_3.cljs
+++ b/frontend/src/app/main/ui/releases/v2_3.cljs
@@ -72,7 +72,7 @@
"Find everything you need in our full comprehensive documentation to start building your plugins now!"]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]
@@ -105,7 +105,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]
diff --git a/frontend/src/app/main/ui/releases/v2_3.scss b/frontend/src/app/main/ui/releases/v2_3.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_3.scss
+++ b/frontend/src/app/main/ui/releases/v2_3.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_4.cljs b/frontend/src/app/main/ui/releases/v2_4.cljs
index 1559911a4d..67a3985127 100644
--- a/frontend/src/app/main/ui/releases/v2_4.cljs
+++ b/frontend/src/app/main/ui/releases/v2_4.cljs
@@ -72,7 +72,7 @@
"Now, you can invite members to your teams who only need to view and comment on files. Team members, stakeholders, developers… pick your case. Anyone who doesn't need to edit can participate confidently."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -102,7 +102,7 @@
"Some versions are saved automatically, serving as an invaluable emergency backup. Additionally, you can manually save versions, giving you full control over the timeline associated with a file. This way, you can always restore specific versions that you've intentionally saved."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -131,7 +131,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
diff --git a/frontend/src/app/main/ui/releases/v2_4.scss b/frontend/src/app/main/ui/releases/v2_4.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_4.scss
+++ b/frontend/src/app/main/ui/releases/v2_4.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_5.cljs b/frontend/src/app/main/ui/releases/v2_5.cljs
index c39c4aebba..cce9c83d70 100644
--- a/frontend/src/app/main/ui/releases/v2_5.cljs
+++ b/frontend/src/app/main/ui/releases/v2_5.cljs
@@ -72,7 +72,7 @@
"And that’s not all. We’ve also added quick actions to flip and rotate gradients, plus now you can adjust the radius for radial gradients. More control, more flexibility, more fun."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -102,7 +102,7 @@
"We’ve also added a new section in your profile where you can customize your notifications, choosing what to receive on your dashboard and via email. On top of that, comments got a UI refresh, making everything clearer and better organized. And this is just the first batch of improvements—expect even more comment-related upgrades in the next Penpot release."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -136,7 +136,7 @@
"Less manual work for a faster workflow. We hope you find it as useful as we do."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -165,7 +165,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
diff --git a/frontend/src/app/main/ui/releases/v2_5.scss b/frontend/src/app/main/ui/releases/v2_5.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_5.scss
+++ b/frontend/src/app/main/ui/releases/v2_5.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_6.cljs b/frontend/src/app/main/ui/releases/v2_6.cljs
index 9d47c870f7..92b4109fd7 100644
--- a/frontend/src/app/main/ui/releases/v2_6.cljs
+++ b/frontend/src/app/main/ui/releases/v2_6.cljs
@@ -84,7 +84,7 @@
your product needs."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -120,7 +120,7 @@
interoperability by design through Open Source."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -159,7 +159,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
diff --git a/frontend/src/app/main/ui/releases/v2_6.scss b/frontend/src/app/main/ui/releases/v2_6.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_6.scss
+++ b/frontend/src/app/main/ui/releases/v2_6.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_7.cljs b/frontend/src/app/main/ui/releases/v2_7.cljs
index 1a5c562e25..0f6c51abc7 100644
--- a/frontend/src/app/main/ui/releases/v2_7.cljs
+++ b/frontend/src/app/main/ui/releases/v2_7.cljs
@@ -71,7 +71,7 @@
"The highlight: you can now duplicate token sets directly from a menu item. A huge time-saver, especially when working from existing sets. We’ve also made it easier to create themes by letting you select their set right away, and we’ve polished some info indicators to make everything a bit clearer. Plus, we’ve fixed a bunch of early-stage bugs to keep things running smoothly."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -101,7 +101,7 @@
"This update gives editors and viewers the same ability to configure, create, copy, and delete sharing links. A capability that, until now, was limited to owners and admins."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
@@ -132,7 +132,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 3}]
diff --git a/frontend/src/app/main/ui/releases/v2_7.scss b/frontend/src/app/main/ui/releases/v2_7.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_7.scss
+++ b/frontend/src/app/main/ui/releases/v2_7.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_8.cljs b/frontend/src/app/main/ui/releases/v2_8.cljs
index fbdce6ee04..8eae8ff74e 100644
--- a/frontend/src/app/main/ui/releases/v2_8.cljs
+++ b/frontend/src/app/main/ui/releases/v2_8.cljs
@@ -83,7 +83,7 @@
"- And we’ve a new language! Hi Serbians!"]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -116,7 +116,7 @@
"This is just one more step in the evolution of Design Tokens in Penpot. And there's more to come: typography tokens are already in the works!"]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -149,7 +149,7 @@
"- We have integrated AI-powered help, which is trained on Penpot documentation, directly into the design workspace. Get assistance without switching context, so you can stay in the flow."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
@@ -186,7 +186,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
diff --git a/frontend/src/app/main/ui/releases/v2_8.scss b/frontend/src/app/main/ui/releases/v2_8.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_8.scss
+++ b/frontend/src/app/main/ui/releases/v2_8.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/releases/v2_9.cljs b/frontend/src/app/main/ui/releases/v2_9.cljs
index 600df72665..bd71956516 100644
--- a/frontend/src/app/main/ui/releases/v2_9.cljs
+++ b/frontend/src/app/main/ui/releases/v2_9.cljs
@@ -71,7 +71,7 @@
"And there’s more progress on Tokens, including support for importing multiple token files via .zip, and smarter token visibility, only showing the relevant tokens for each layer type."]]
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]
@@ -102,7 +102,7 @@
[:div {:class (stl/css :navigation)}
- [:& c/navigation-bullets
+ [:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 2}]
diff --git a/frontend/src/app/main/ui/releases/v2_9.scss b/frontend/src/app/main/ui/releases/v2_9.scss
index e5d13841eb..40c8f5316f 100644
--- a/frontend/src/app/main/ui/releases/v2_9.scss
+++ b/frontend/src/app/main/ui/releases/v2_9.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
@@ -42,8 +42,9 @@
}
.version-tag {
- @include deprecated.flexCenter;
- @include deprecated.headlineSmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.headline-small-typography;
+
height: deprecated.$s-32;
width: deprecated.$s-96;
background-color: var(--communication-tag-background-color);
@@ -52,7 +53,8 @@
}
.modal-title {
- @include deprecated.headlineLargeTypography;
+ @include deprecated.headline-large-typography;
+
color: var(--modal-title-foreground-color);
}
@@ -70,18 +72,21 @@
}
.feature-title {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-title-foreground-color);
}
.feature-content {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
margin: 0;
color: var(--modal-text-foreground-color);
}
.feature-list {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--modal-text-foreground-color);
list-style: disc;
display: grid;
@@ -95,7 +100,8 @@
}
.next-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
width: deprecated.$s-100;
justify-self: flex-end;
grid-area: button;
diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs
index ca45bc5133..920a79605f 100644
--- a/frontend/src/app/main/ui/routes.cljs
+++ b/frontend/src/app/main/ui/routes.cljs
@@ -30,6 +30,9 @@
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]]
+ (when (contains? cf/flags :nitrate)
+ ["/subscribe-nitrate" :nitrate-entry])
+
["/settings"
["/profile" :settings-profile]
["/password" :settings-password]
diff --git a/frontend/src/app/main/ui/settings.scss b/frontend/src/app/main/ui/settings.scss
index 2963138812..91cbc781a5 100644
--- a/frontend/src/app/main/ui/settings.scss
+++ b/frontend/src/app/main/ui/settings.scss
@@ -33,6 +33,7 @@
&.dashboard-projects {
user-select: none;
}
+
&.dashboard-shared {
width: calc(100vw - deprecated.$s-320);
margin-right: deprecated.$s-52;
@@ -48,13 +49,13 @@
width: 100%;
justify-content: center;
align-items: center;
+
a {
color: var(--color-foreground-secondary);
}
}
.form-container {
- width: deprecated.$s-800;
margin: deprecated.$s-48 auto deprecated.$s-32 deprecated.$s-120;
display: flex;
max-width: deprecated.$s-368;
@@ -76,6 +77,7 @@
.custom-input,
.custom-select {
flex-direction: column-reverse;
+
label {
position: relative;
text-transform: uppercase;
@@ -84,6 +86,7 @@
margin-bottom: deprecated.$s-12;
margin-left: calc(-1 * deprecated.$s-4);
}
+
input,
select {
background-color: var(--color-background-tertiary);
@@ -91,20 +94,25 @@
border-color: transparent;
color: var(--color-foreground-primary);
padding: 0 deprecated.$s-16;
+
&:focus {
outline: deprecated.$s-1 solid var(--color-accent-primary);
}
+
::placeholder {
color: var(--color-foreground-secondary);
}
}
+
.help-icon {
bottom: deprecated.$s-12;
top: auto;
+
svg {
fill: var(--color-foreground-secondary);
}
}
+
&.disabled {
input {
background-color: var(--input-background-color-disabled);
@@ -112,30 +120,36 @@
color: var(--color-foreground-secondary);
}
}
+
.input-container {
background-color: var(--color-background-tertiary);
border-radius: deprecated.$s-8;
border-color: transparent;
margin-top: deprecated.$s-24;
+
.main-content {
label {
position: absolute;
top: calc(-1 * deprecated.$s-24);
}
+
span {
color: var(--color-foreground-primary);
}
}
+
&:focus {
border: deprecated.$s-1 solid var(--color-accent-primary);
}
}
+
textarea {
border-radius: deprecated.$s-8;
padding: deprecated.$s-12 deprecated.$s-16;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
border: none;
+
&:focus {
outline: deprecated.$s-1 solid var(--color-accent-primary);
}
@@ -145,6 +159,7 @@
.field-title {
color: var(--color-foreground-primary);
}
+
.field-title:not(:first-child) {
margin-top: deprecated.$s-64;
}
@@ -152,6 +167,7 @@
.field-text {
color: var(--color-foreground-secondary);
}
+
button,
.btn-secondary {
width: 100%;
@@ -159,15 +175,18 @@
text-transform: uppercase;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
+
&:hover {
color: var(--color-accent-primary);
background-color: var(--color-background-quaternary);
}
}
+
hr {
display: none;
}
}
+
.links {
margin-top: deprecated.$s-12;
}
diff --git a/frontend/src/app/main/ui/settings/change_email.scss b/frontend/src/app/main/ui/settings/change_email.scss
index 71900cf9e4..60044a05a4 100644
--- a/frontend/src/app/main/ui/settings/change_email.scss
+++ b/frontend/src/app/main/ui/settings/change_email.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
min-width: deprecated.$s-408;
}
@@ -20,37 +21,41 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.flexColumn;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-small-typography;
+
gap: deprecated.$s-24;
margin-bottom: deprecated.$s-24;
}
.fields-row {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
}
.select-title {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--modal-title-foreground-color);
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
+
button {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
}
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
diff --git a/frontend/src/app/main/ui/settings/delete_account.scss b/frontend/src/app/main/ui/settings/delete_account.scss
index c69d17de53..4b0b6408c9 100644
--- a/frontend/src/app/main/ui/settings/delete_account.scss
+++ b/frontend/src/app/main/ui/settings/delete_account.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
min-width: deprecated.$s-408;
}
@@ -20,41 +21,45 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.flexColumn;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-small-typography;
+
gap: deprecated.$s-24;
margin-bottom: deprecated.$s-24;
}
.fields-row {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
}
.select-title {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--modal-title-foreground-color);
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.accept-button {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
diff --git a/frontend/src/app/main/ui/settings/feedback.scss b/frontend/src/app/main/ui/settings/feedback.scss
index ee91182b80..99e449267d 100644
--- a/frontend/src/app/main/ui/settings/feedback.scss
+++ b/frontend/src/app/main/ui/settings/feedback.scss
@@ -19,6 +19,7 @@
.feedback-description {
@include t.use-typography("body-medium");
+
border-radius: b.$br-8;
padding: var(--sp-m);
background-color: var(--color-background-tertiary);
@@ -28,6 +29,7 @@
::placeholder {
color: var(--input-placeholder-color);
}
+
&:focus {
outline: b.$b-1 solid var(--color-accent-primary);
}
@@ -35,13 +37,15 @@
.field-label {
@include t.use-typography("headline-small");
+
block-size: $sz-32;
color: var(--color-foreground-primary);
margin-block-end: var(--sp-l);
}
.feedback-button-link {
- @extend .button-primary;
+ @extend %button-primary;
+
margin-block-end: px2rem(72);
}
@@ -59,12 +63,14 @@
.link {
@include t.use-typography("headline-small");
+
color: var(--color-accent-tertiary);
margin-block-end: var(--sp-s);
}
.download-button {
@include t.use-typography("body-small");
+
color: var(--color-foreground-primary);
text-transform: lowercase;
border: b.$b-1 solid var(--color-background-quaternary);
diff --git a/frontend/src/app/main/ui/settings/integrations.scss b/frontend/src/app/main/ui/settings/integrations.scss
index d7be475bb4..e1833c1c6e 100644
--- a/frontend/src/app/main/ui/settings/integrations.scss
+++ b/frontend/src/app/main/ui/settings/integrations.scss
@@ -5,7 +5,6 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
-
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/mixins.scss" as *;
@@ -44,11 +43,12 @@
}
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
inline-size: $sz-400;
max-block-size: fit-content;
position: relative;
@@ -187,7 +187,8 @@
}
.item-title {
- @include textEllipsis;
+ @include text-ellipsis;
+
align-content: center;
block-size: $sz-64;
padding: 0 var(--sp-l);
@@ -222,6 +223,7 @@
.textarea {
@include t.use-typography("body-small");
+
border-radius: $br-8;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-secondary);
diff --git a/frontend/src/app/main/ui/settings/notifications.cljs b/frontend/src/app/main/ui/settings/notifications.cljs
index 5779474c70..d9347b5ee9 100644
--- a/frontend/src/app/main/ui/settings/notifications.cljs
+++ b/frontend/src/app/main/ui/settings/notifications.cljs
@@ -82,6 +82,7 @@
[:> fm/submit-button*
{:label (tr "dashboard.settings.notifications.submit")
+ :disabled (= (:data @form) (:initial @form))
:data-testid "submit-settings"
:class (stl/css :update-btn)}]]]]))
diff --git a/frontend/src/app/main/ui/settings/notifications.scss b/frontend/src/app/main/ui/settings/notifications.scss
index 27a2273536..4aaf1ac096 100644
--- a/frontend/src/app/main/ui/settings/notifications.scss
+++ b/frontend/src/app/main/ui/settings/notifications.scss
@@ -9,7 +9,9 @@
.update-btn {
margin-top: deprecated.$s-16;
- @extend .button-primary;
+
+ @extend %button-primary;
+
height: deprecated.$s-36;
}
diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs
index aa3ad3e064..7a2fc59413 100644
--- a/frontend/src/app/main/ui/settings/options.cljs
+++ b/frontend/src/app/main/ui/settings/options.cljs
@@ -80,6 +80,7 @@
[:> fm/submit-button*
{:label (tr "dashboard.update-settings")
+ :disabled (= (:data @form) (:initial @form))
:data-testid "submit-lang-change"
:class (stl/css :btn-primary)}]]))
diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss
index 2df1d9235f..abe949897f 100644
--- a/frontend/src/app/main/ui/settings/options.scss
+++ b/frontend/src/app/main/ui/settings/options.scss
@@ -25,9 +25,8 @@
grid-auto-rows: auto;
gap: var(--sp-s);
width: $sz-500;
- margin-block-start: var(--sp-xxxl);
+ margin-block: var(--sp-xxxl) $sz-120; /* FIXME: this should be a proper token */
padding-block-start: var(--sp-xxxl);
- margin-block-end: $sz-120; /* FIXME: this should be a proper token */
border-block-start: $b-1 solid var(--color-background-quaternary);
color: var(--color-foreground-primary);
}
diff --git a/frontend/src/app/main/ui/settings/password.scss b/frontend/src/app/main/ui/settings/password.scss
index 5a0551333e..504a6da2e5 100644
--- a/frontend/src/app/main/ui/settings/password.scss
+++ b/frontend/src/app/main/ui/settings/password.scss
@@ -9,6 +9,8 @@
.update-btn {
margin-top: deprecated.$s-16;
- @extend .button-primary;
+
+ @extend %button-primary;
+
height: deprecated.$s-36;
}
diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs
index 763ee3c836..b8903d4027 100644
--- a/frontend/src/app/main/ui/settings/profile.cljs
+++ b/frontend/src/app/main/ui/settings/profile.cljs
@@ -16,6 +16,7 @@
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
+ [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -25,13 +26,12 @@
[:fullname [::sm/text {:max 250}]]
[:email ::sm/email]])
-(defn- on-success
- [_]
- (st/emit! (ntf/success (tr "notifications.profile-saved"))))
-
(defn- on-submit
[form _event]
- (let [data (:clean-data @form)]
+ (let [data (:clean-data @form)
+ on-success (fn [_]
+ (swap! form assoc :touched {})
+ (st/emit! (ntf/success (tr "notifications.profile-saved"))))]
(st/emit! (du/update-profile data)
(du/persist-profile {:on-success on-success}))))
@@ -92,6 +92,7 @@
[]
(let [input-ref (mf/use-ref nil)
profile (mf/deref refs/profile)
+ has-photo? (some? (:photo-id profile))
photo
(mf/with-memo [profile]
@@ -103,13 +104,32 @@
on-file-selected
(fn [file]
- (st/emit! (du/update-photo file)))]
+ (st/emit! (du/update-photo file)))
+
+ on-delete-click
+ (mf/use-fn
+ (fn [event]
+ (dom/stop-propagation event)
+ (st/emit! (modal/show
+ {:type :confirm
+ :title (tr "labels.delete-profile-photo.title")
+ :message (tr "labels.delete-profile-photo.message")
+ :accept-label (tr "labels.delete")
+ :on-accept (fn [_] (st/emit! du/delete-photo))}))))]
[:form {:class (stl/css :avatar-form)}
[:div {:class (stl/css :image-change-field)}
[:span {:class (stl/css :update-overlay)
:on-click on-image-click} (tr "labels.update")]
[:img {:src photo}]
+ (when has-photo?
+ [:button {:type "button"
+ :class (stl/css :delete-overlay)
+ :title (tr "labels.delete")
+ :aria-label (tr "labels.delete")
+ :on-click on-delete-click
+ :data-testid "profile-image-delete"}
+ [:> icon* {:icon-id i/delete :size "m"}]])
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:ref input-ref
diff --git a/frontend/src/app/main/ui/settings/profile.scss b/frontend/src/app/main/ui/settings/profile.scss
index 4e8473b6e8..ce9d3b3b0f 100644
--- a/frontend/src/app/main/ui/settings/profile.scss
+++ b/frontend/src/app/main/ui/settings/profile.scss
@@ -11,17 +11,16 @@
width: 100%;
justify-content: center;
align-items: center;
- a:not(.button-primary):not(.link) {
+
+ a:not(.button-primary, .link) {
color: var(--color-foreground-secondary);
}
}
.form-container {
display: flex;
- justify-content: center;
flex-direction: column;
max-width: $s-500;
- margin-bottom: $s-32;
width: $s-580;
margin: $s-80 auto $s-120 auto;
justify-content: center;
@@ -36,11 +35,13 @@
text-transform: uppercase;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
+
&:hover {
color: var(--color-accent-primary);
background-color: var(--color-background-quaternary);
}
}
+
hr {
display: none;
}
@@ -48,6 +49,7 @@
.fields-row {
--input-height: #{$s-40};
+
margin-bottom: $s-20;
flex-direction: column;
@@ -78,6 +80,7 @@
.custom-input,
.custom-select {
flex-direction: column-reverse;
+
label {
position: relative;
text-transform: uppercase;
@@ -86,6 +89,7 @@
margin-bottom: $s-12;
margin-left: calc(-1 * $s-4);
}
+
input,
select {
background-color: var(--color-background-tertiary);
@@ -93,20 +97,25 @@
border-color: transparent;
color: var(--color-foreground-primary);
padding: 0 $s-16;
+
&:focus {
outline: $s-1 solid var(--color-accent-primary);
}
+
::placeholder {
color: var(--color-foreground-secondary);
}
}
+
.help-icon {
bottom: $s-12;
top: auto;
+
svg {
fill: var(--color-foreground-secondary);
}
}
+
&.disabled {
input {
background-color: var(--input-background-color-disabled);
@@ -114,30 +123,36 @@
color: var(--color-foreground-secondary);
}
}
+
.input-container {
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border-color: transparent;
margin-top: $s-24;
+
.main-content {
label {
position: absolute;
top: calc(-1 * $s-24);
}
+
span {
color: var(--color-foreground-primary);
}
}
+
&:focus {
border: $s-1 solid var(--color-accent-primary);
}
}
+
textarea {
border-radius: $br-8;
padding: $s-12 $s-16;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
border: none;
+
&:focus {
outline: $s-1 solid var(--color-accent-primary);
}
@@ -265,6 +280,31 @@ form.avatar-form {
z-index: $z-index-modal;
}
+ .delete-overlay {
+ position: absolute;
+ top: $s-4;
+ inset-inline-end: $s-4;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: $s-32;
+ height: $s-32;
+ padding: 0;
+ border: none;
+ border-radius: 50%;
+ background: var(--color-background-primary);
+ color: var(--color-foreground-primary);
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s ease-in-out;
+ z-index: calc(#{$z-index-modal} + 1);
+
+ &:hover {
+ background: var(--color-background-quaternary);
+ color: var(--color-accent-primary);
+ }
+ }
+
input[type="file"] {
width: 100%;
height: 100%;
@@ -279,6 +319,10 @@ form.avatar-form {
.update-overlay {
opacity: 0.8;
}
+
+ .delete-overlay {
+ opacity: 1;
+ }
}
}
@@ -321,11 +365,13 @@ form.avatar-form {
}
.btn-secondary {
- @extend .button-secondary;
+ @extend %button-secondary;
+
height: $s-32;
}
.btn-primary {
- @extend .button-primary;
+ @extend %button-primary;
+
height: $s-32;
}
diff --git a/frontend/src/app/main/ui/settings/sidebar.scss b/frontend/src/app/main/ui/settings/sidebar.scss
index a072c59b8f..e9571a7ab4 100644
--- a/frontend/src/app/main/ui/settings/sidebar.scss
+++ b/frontend/src/app/main/ui/settings/sidebar.scss
@@ -42,6 +42,7 @@
.settings-item {
--settings-foreground-color: var(--menu-foreground-color-rest);
--settings-background-color: transparent;
+
display: flex;
align-items: center;
padding: deprecated.$s-8 deprecated.$s-8 deprecated.$s-8 deprecated.$s-24;
@@ -61,18 +62,20 @@
}
.feedback-icon {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--settings-foreground-color);
margin-right: deprecated.$s-8;
}
.element-title {
- @include deprecated.textEllipsis;
- @include deprecated.bodyMediumTypography;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-medium-typography;
}
.back-to-dashboard {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
align-items: center;
padding: deprecated.$s-12 deprecated.$s-16;
@@ -84,7 +87,8 @@
}
.arrow-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
transform: rotate(180deg);
margin-right: deprecated.$s-12;
diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs
index f9441aca1c..9e85635f3b 100644
--- a/frontend/src/app/main/ui/settings/subscription.cljs
+++ b/frontend/src/app/main/ui/settings/subscription.cljs
@@ -18,6 +18,7 @@
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
+ [app.main.ui.nitrate.nitrate-activation-success-modal]
[app.main.ui.notifications.badge :refer [badge-notification]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr c]]
@@ -28,6 +29,7 @@
[{:keys [card-title
card-title-icon
price-value price-period
+ cancel-at
benefits-title benefits
cta-text
cta-link
@@ -35,6 +37,7 @@
cta-link-trial
cta-text-with-icon
cta-link-with-icon
+ show-activation-by-code
editors
recommended
show-button-cta]}]
@@ -56,11 +59,22 @@
(when (and price-value price-period)
[:div {:class (stl/css :plan-price)}
[:span {:class (stl/css :plan-price-value)} price-value]
- [:span {:class (stl/css :plan-price-period)} " / " price-period]])]
+ [:span {:class (stl/css :plan-price-period)} " / " price-period]])
+ (when cancel-at
+ [:div {:class (stl/css :plan-cancel)}
+ [:span {:class (stl/css :plan-cancel-date)} cancel-at]])]
(when benefits-title [:h5 {:class (stl/css :benefits-title)} benefits-title])
[:ul {:class (stl/css :benefits-list)}
(for [benefit benefits]
[:li {:key (dm/str benefit) :class (stl/css :benefit)} "- " benefit])]
+ (when (and cta-link cta-text show-button-cta)
+ [:> button* {:variant "primary"
+ :type "button"
+ :class (stl/css-case :bottom-button (not (and cta-link-trial cta-text-trial)))
+ :on-click cta-link} cta-text])
+ (when (and cta-link-trial cta-text-trial)
+ [:button {:class (stl/css :cta-button :bottom-link)
+ :on-click cta-link-trial} cta-text-trial])
(when (and cta-link-with-icon cta-text-with-icon)
[:button {:class (stl/css :cta-button :more-info)
:on-click cta-link-with-icon} cta-text-with-icon
@@ -70,14 +84,10 @@
[:button {:class (stl/css-case :cta-button true
:bottom-link (not (and cta-link-trial cta-text-trial)))
:on-click cta-link} cta-text])
- (when (and cta-link cta-text show-button-cta)
- [:> button* {:variant "primary"
- :type "button"
- :class (stl/css-case :bottom-button (not (and cta-link-trial cta-text-trial)))
- :on-click cta-link} cta-text])
- (when (and cta-link-trial cta-text-trial)
- [:button {:class (stl/css :cta-button :bottom-link)
- :on-click cta-link-trial} cta-text-trial])])
+ (when show-activation-by-code
+ [:button {:class (stl/css :cta-button :activate-by-code)
+ :on-click #(st/emit! (modal/show {:type :nitrate-code-activation}))}
+ (tr "subscription.settings.activate-by-code")])])
(defn- make-management-form-schema [min-editors]
[:map {:title "SeatsForm"}
@@ -339,14 +349,14 @@
[:div {:class (stl/css :modal-end)}
[:div {:class (stl/css :modal-title)}
- (tr "subscription.settings.sucess.dialog.title" subscription-name)]
+ (tr "subscription.settings.success.dialog.title" subscription-name)]
(when (not= subscription-name "professional")
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.success.dialog.thanks" subscription-name)])
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.success.dialog.description")]
[:p {:class (stl/css :modal-text-large)}
- (tr "subscription.settings.sucess.dialog.footer")]
+ (tr "subscription.settings.success.dialog.footer")]
[:div {:class (stl/css :success-action-buttons)}
[:input
@@ -355,37 +365,6 @@
:value (tr "labels.close")
:on-click handle-close-dialog}]]]]]]))
-(mf/defc nitrate-success-dialog
- {::mf/register modal/components
- ::mf/register-as :nitrate-success}
- []
- ;; TODO add translations for this texts when we have the definitive ones
- (let [profile (mf/deref refs/profile)]
-
- [:div {:class (stl/css :modal-overlay)}
- [:div {:class (stl/css :modal-dialog :subscription-success)}
- [:button {:class (stl/css :close-btn) :on-click modal/hide!}
- [:> icon* {:icon-id "close"
- :size "m"}]]
- [:div {:class (stl/css :modal-success-content)}
- [:div {:class (stl/css :modal-start)}
- [:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]]
-
- [:div {:class (stl/css :modal-end)}
- [:div {:class (stl/css :modal-title)}
- "You are Business Nitrate!"]
- [:p {:class (stl/css :modal-text-large)}
- (tr "subscription.settings.success.dialog.description")]
- [:p {:class (stl/css :modal-text-large)}
- (tr "subscription.settings.sucess.dialog.footer")]
-
- [:div {:class (stl/css :success-action-buttons)}
- [:input
- {:class (stl/css :primary-button)
- :type "button"
- :value "CREATE ORGANIZATION"
- :on-click dnt/go-to-nitrate-cc}]]]]]]))
-
(mf/defc subscription-page*
[{:keys [profile]}]
(let [route (mf/deref refs/route)
@@ -415,7 +394,7 @@
(-> profile :props :subscription)
subscription-type
- (get-subscription-type subscription)
+ (if (and (contains? cf/flags :nitrate) nitrate?) (:type nitrate-license) (get-subscription-type subscription))
subscription-is-trial?
(= (:status subscription) "trialing")
@@ -449,17 +428,25 @@
open-subscription-modal
(mf/use-fn
- (mf/deps subscription-editors)
+ (mf/deps subscription-editors nitrate-license)
(fn [subscription-type current-subscription]
(st/emit! (ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:in-app"}))
(if (= subscription-type "nitrate")
- (st/emit! (dnt/show-nitrate-popup :nitrate-dialog))
+ (st/emit! (dnt/show-nitrate-popup :nitrate-dialog {:nitrate-license nitrate-license}))
(st/emit!
(modal/show :management-dialog
{:subscription-type subscription-type
:current-subscription current-subscription
- :editors subscription-editors :subscribe-to-trial (not (:type subscription))})))))]
+ :editors subscription-editors :subscribe-to-trial (not (:type subscription))})))))
+
+ open-contact-sales-modal
+ (mf/use-fn
+ (mf/deps nitrate-license)
+ (fn [current-subscription subscription-type]
+ (if (= current-subscription "unlimited")
+ (st/emit! (dnt/show-nitrate-popup :nitrate-dialog {:nitrate-license nitrate-license :show-contact-sales-option true}))
+ (st/emit! (modal/show :nitrate-contact-sales-dialog {:subscription-type subscription-type})))))]
(mf/with-effect []
(dom/set-html-title (tr "subscription.labels")))
@@ -488,7 +475,7 @@
^boolean show-subscription-success-modal?
(st/emit!
(if (= params-subscription "subscribed-to-penpot-nitrate")
- (modal/show :nitrate-success {})
+ (modal/show :nitrate-activation-success {})
(modal/show :subscription-success
{:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited")
(if (= success-modal-is-trial? "true")
@@ -510,6 +497,8 @@
;; TODO add translations for this texts when we have the definitive ones
[:> plan-card* {:card-title "Business Nitrate"
:card-title-icon i/character-b
+ :cancel-at (when (:cancel-at nitrate-license)
+ (tr "nitrate.subscription.active-until" (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy")))
:benefits-title "Loren ipsum",
:benefits ["Loren ipsum",
"Loren ipsum",
@@ -611,7 +600,7 @@
(tr "subscription.settings.unlimited.autosave-benefit"),
(tr "subscription.settings.unlimited.bill")]
:cta-text (if (:type subscription) (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
- :cta-link #(open-subscription-modal "unlimited" subscription)
+ :cta-link (if (and (contains? cf/flags :nitrate) nitrate?) #(open-contact-sales-modal subscription-type "Unlimited") #(open-subscription-modal "unlimited" subscription))
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page
:recommended (= subscription-type "professional")
@@ -637,15 +626,17 @@
[:> plan-card* {:card-title "Business Nitrate"
:card-title-icon i/character-n
:price-value "$25"
- :price-period "org member"
+ :price-period (tr "subscription.settings.organization-member-month")
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
:benefits ["Crea organizaciones y añade personas, que usarán Penpot con las reglas que configures."
"Acceso exclusivo al Control Center"
"Lorem ipsum"]
- :cta-text (tr "subscription.settings.subscribe")
- :cta-link #(open-subscription-modal "nitrate" subscription)
+ :cta-text (if nitrate-license (tr "subscription.settings.subscribe") "Try 14 days for free")
+ :cta-link (if (= subscription-type "unlimited") #(open-contact-sales-modal subscription-type "Nitrate") #(open-subscription-modal "nitrate" subscription))
:cta-text-with-icon (tr "subscription.settings.more-information")
- :cta-link-with-icon go-to-pricing-page}])]]]))
+ :cta-link-with-icon go-to-pricing-page
+ :show-activation-by-code true
+ :show-button-cta (not nitrate-license)}])]]]))
(def ^:private schema:nitrate-form
@@ -655,7 +646,7 @@
(mf/defc subscribe-nitrate-dialog
{::mf/register modal/components
::mf/register-as :nitrate-dialog}
- [connectivity]
+ [{:keys [nitrate-license show-contact-sales-option] :as connectivity}]
;; TODO add translations for this texts when we have the definitive ones
(let [online? (:licenses connectivity)
initial (mf/with-memo []
@@ -688,7 +679,7 @@
[:div {:class (stl/css :modal-title :subscription-title)}
"Subcribe to the Business Nitrate plan"]
- (if online?
+ (if (and online? (not show-contact-sales-option))
[:div {:class (stl/css :modal-content)}
@@ -723,16 +714,50 @@
:on-click handle-close-dialog}]
[:> fm/submit-button*
- {:label "TRY 14 DAYS FOR FREE"
+ {:label (if nitrate-license (tr "subscription.settings.subscribe") "TRY 14 DAYS FOR FREE")
:class (stl/css :primary-button)}]]]]]]
[:div {:class (stl/css :modal-content :modal-contact-content)}
[:div {:class (stl/css :modal-text)}
"Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum"]
[:div {:class (stl/css :modal-text)}
- "Contact us to upgrade to Nitrate:"]
+ (if nitrate-license "Contact us to upgrade to Nitrate:" "Contact us to try Nitrate for 14 days:")]
[:div {:class (stl/css :modal-text)}
- [:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
+ [:a {:class (stl/css :cta-button) :href "mailto:sales@penpot.app"}
"sales@penpot.app"]]])]]))
+(mf/defc nitrate-contact-sales-dialog
+ {::mf/register modal/components
+ ::mf/register-as :nitrate-contact-sales-dialog}
+ [{:keys [subscription-type]}]
+ (let [handle-close-dialog
+ (mf/use-fn
+ (fn []
+ (modal/hide!)))]
+ [:div {:class (stl/css :modal-overlay)}
+ [:div {:class (stl/css :modal-dialog)}
+ [:button {:class (stl/css :close-btn) :on-click handle-close-dialog}
+ [:> icon* {:icon-id "close"
+ :size "m"}]]
+ [:div {:class (stl/css :modal-title :subscription-title)}
+ (str "Switch to " subscription-type " plan?")]
+
+ [:div {:class (stl/css :modal-content)}
+ [:div {:class (stl/css :modal-text-medium)}
+ "When you downgrade:"]
+ [:ul {:class (stl/css :downgrade-list)}
+ [:li {:class (stl/css :downgrade-item)} "Your organization will be deleted."]
+ [:li {:class (stl/css :downgrade-item)} "The teams, projects and files will no longer be part of any organization but they will remain available."]
+ [:li {:class (stl/css :downgrade-item)} "Your total storage, auto-version history, and file recovery period will be limited."]]
+
+ [:div {:class (stl/css :downgrade-warning)}
+ "To switch to this plan, please contact our sales team.
+We’ll help you update your subscription and ensure everything is set up correctly."]
+ [:div {:class (stl/css :action-buttons)}
+ [:> button* {:variant "secondary"
+ :type "button"
+ :on-click handle-close-dialog} (tr "ds.confirm-cancel")]
+ [:> button* {:variant "primary"
+ :type "button"
+ :on-click #(dom/open-new-window "mailto:sales@penpot.app?subject=Switch%20to%20the%20Unlimited%20plan")} "Contact sales"]]]]]))
diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss
index f98c1caef3..213624d901 100644
--- a/frontend/src/app/main/ui/settings/subscription.scss
+++ b/frontend/src/app/main/ui/settings/subscription.scss
@@ -20,12 +20,11 @@
.dashboard-content {
display: flex;
- justify-content: center;
flex-direction: column;
max-inline-size: $sz-500;
margin-block-end: var(--sp-xxxl);
inline-size: px2rem(580);
- margin: px2rem(92) auto px2rem(120) auto;
+ margin: px2rem(92) auto px2rem(120);
justify-content: center;
}
@@ -45,13 +44,14 @@
.membership-date {
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
margin-inline-start: var(--sp-s);
}
.subscription-member,
.penpot-member {
- @extend .button-icon;
+ @extend %button-icon;
}
.penpot-member {
@@ -64,12 +64,14 @@
.title-section {
@include t.use-typography("title-large");
+
color: var(--color-foreground-primary);
margin-block-end: var(--sp-xxl);
}
.plan-section-title {
@include t.use-typography("headline-small");
+
color: var(--color-foreground-primary);
}
@@ -98,14 +100,15 @@
}
.plan-title-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-foreground-primary);
block-size: var(--sp-xl);
inline-size: var(--sp-xl);
border-radius: 6px;
border: 1.75px solid var(--color-foreground-primary);
stroke-width: 2.25px;
- padding: deprecated.$s-1;
+ padding: px2rem(3);
svg {
block-size: var(--sp-m);
@@ -116,11 +119,13 @@
.plan-card-title,
.plan-price-value {
@include t.use-typography("title-medium");
+
color: var(--color-foreground-primary);
}
.plan-editors {
@include t.use-typography("body-medium");
+
align-self: end;
color: var(--color-foreground-primary);
margin-block-end: 2px;
@@ -128,6 +133,21 @@
.plan-price-period {
@include t.use-typography("body-small");
+
+ color: var(--color-foreground-primary);
+}
+
+.plan-cancel {
+ align-items: center;
+ background-color: var(--color-background-secondary);
+ border-radius: var(--sp-xs);
+ display: flex;
+ padding-inline: var(--sp-s);
+}
+
+.plan-cancel-date {
+ @include t.use-typography("body-medium");
+
color: var(--color-foreground-primary);
}
@@ -138,6 +158,7 @@
.benefits-title,
.benefit {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-secondary);
}
@@ -147,7 +168,8 @@
.cta-button {
@include t.use-typography("body-medium");
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
align-items: center;
color: var(--color-accent-primary);
display: flex;
@@ -156,7 +178,8 @@
}
.cta-button svg {
- @extend .button-icon;
+ @extend %button-icon;
+
block-size: var(--sp-l);
inline-size: var(--sp-l);
stroke: var(--color-accent-primary);
@@ -176,11 +199,12 @@
}
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
max-block-size: initial;
min-inline-size: px2rem(548);
}
@@ -190,11 +214,12 @@
}
.close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-title {
@include t.use-typography("title-large");
+
margin-block-end: var(--sp-xxxl);
color: var(--modal-title-foreground-color);
display: flex;
@@ -240,7 +265,7 @@
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.success-action-buttons {
@@ -248,13 +273,15 @@
}
.primary-button {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
+
min-block-size: $sz-32;
block-size: auto;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
+
min-block-size: $sz-32;
white-space: break-spaces;
block-size: auto;
@@ -270,13 +297,14 @@
block-size: auto;
}
- @media (max-inline-size: 992px) {
+ @media (width <= 992px) {
display: none;
}
}
.editors-text {
@include t.use-typography("body-medium");
+
margin: 0;
}
@@ -287,6 +315,7 @@
.editors-list {
@include t.use-typography("body-medium");
+
list-style-position: inside;
list-style-type: none;
margin-inline-start: var(--sp-xl);
@@ -296,11 +325,13 @@
.input-field {
--input-icon-padding: var(--sp-s);
+
inline-size: px2rem(80);
}
.error-message {
@include t.use-typography("body-small");
+
color: var(--color-foreground-error);
margin-block-start: var(--sp-s);
}
@@ -319,6 +350,7 @@
.unlimited-capped-warning {
@include t.use-typography("body-small");
+
background-color: var(--color-background-tertiary);
border-radius: var(--sp-s);
margin-block-start: $sz-40;
@@ -333,6 +365,7 @@
.radio-btns {
label {
@include t.use-typography("body-large");
+
padding: 0;
display: flex;
align-items: center;
@@ -348,3 +381,26 @@
.modal-contact-content {
gap: var(--sp-xl);
}
+
+.downgrade-warning {
+ @include t.use-typography("body-medium");
+
+ background-color: var(--color-background-tertiary);
+ border-radius: var(--sp-s);
+ padding-block: var(--sp-s);
+ padding-inline: var(--sp-m);
+ margin-block: var(--sp-m) var(--sp-xxxl);
+}
+
+.downgrade-list {
+ list-style-position: outside;
+ list-style-type: disc;
+ margin-block: var(--sp-l) 0;
+ padding-inline-start: var(--sp-l);
+}
+
+.downgrade-item {
+ @include t.use-typography("body-medium");
+
+ margin-block-end: var(--sp-l);
+}
diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs
index 02c3b5d07e..01d5c64c5b 100644
--- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs
+++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs
@@ -509,7 +509,8 @@
(when (some? shape-strokes)
[:> :g props
- (for [[index value] (reverse (d/enumerate shape-strokes))]
+ (for [[index value] (reverse (d/enumerate shape-strokes))
+ :when (not (:hidden value))]
[:& shape-custom-stroke {:shape shape
:stroke value
:index index
diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs
index a13d6f76d8..f426ae0874 100644
--- a/frontend/src/app/main/ui/static.cljs
+++ b/frontend/src/app/main/ui/static.cljs
@@ -67,10 +67,26 @@
[:span (tr "not-found.made-with-love")]]]))
(mf/defc invalid-token
- []
+ [{:keys [reason]}]
+ ;; Map the specific failure reason to actionable copy. Falls back to
+ ;; the generic invitation-invalid message when the reason is missing
+ ;; or unknown so the UX never regresses for unhandled cases.
+ ;;
+ ;; The branches use `tr` with literal keys (instead of `(tr key-var)`)
+ ;; so the i18n usage scanner can statically track every key.
[:> error-container* {}
- [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
- [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
+ (case reason
+ :email-mismatch
+ [:*
+ [:div {:class (stl/css :main-message)} (tr "errors.invite-email-mismatch")]]
+
+ :token-expired
+ [:*
+ [:div {:class (stl/css :main-message)} (tr "errors.invite-expired")]]
+
+ [:*
+ [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
+ [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])])
(mf/defc login-modal*
{::mf/private true}
diff --git a/frontend/src/app/main/ui/static.scss b/frontend/src/app/main/ui/static.scss
index cf8cb1ec9a..32c80dce5e 100644
--- a/frontend/src/app/main/ui/static.scss
+++ b/frontend/src/app/main/ui/static.scss
@@ -106,7 +106,8 @@
}
.login-header {
- @extend .button-primary;
+ @extend %button-primary;
+
padding: deprecated.$s-8 deprecated.$s-16;
font-size: deprecated.$fs-11;
position: fixed;
@@ -135,22 +136,26 @@
.main-message {
@include t.use-typography("title-large");
+
color: var(--color-foreground-primary);
}
.desc-message {
@include t.use-typography("title-large");
+
color: var(--color-foreground-secondary);
}
.desc-text {
@include t.use-typography("title-large");
+
color: var(--color-foreground-secondary);
margin-block-end: 0;
}
.download-link {
@include t.use-typography("code-font");
+
color: var(--color-foreground-primary);
text-transform: lowercase;
}
@@ -159,7 +164,8 @@
text-align: center;
button {
- @extend .button-primary;
+ @extend %button-primary;
+
text-transform: uppercase;
padding: deprecated.$s-8 deprecated.$s-16;
font-size: deprecated.$fs-11;
@@ -196,12 +202,14 @@
}
.project-name {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--title-foreground-color);
}
.file-name {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
text-transform: none;
color: var(--title-foreground-color-hover);
}
@@ -230,7 +238,7 @@
top: 0;
left: 0;
z-index: 100;
- background-color: rgba(0, 0, 0, 0.65);
+ background-color: rgb(0 0 0 / 0.65);
display: flex;
justify-content: center;
align-items: center;
@@ -274,14 +282,16 @@
margin-top: deprecated.$s-32;
button {
- @extend .button-primary;
+ @extend %button-primary;
+
text-transform: uppercase;
padding: deprecated.$s-8 deprecated.$s-16;
font-size: deprecated.$fs-11;
}
.cancel-button {
- @extend .button-secondary;
+ @extend %button-secondary;
+
text-transform: uppercase;
padding: deprecated.$s-8 deprecated.$s-16;
font-size: deprecated.$fs-11;
@@ -338,8 +348,10 @@
margin: deprecated.$s-20 0;
}
- form div {
- margin-bottom: deprecated.$s-8;
+ form {
+ div {
+ margin-bottom: deprecated.$s-8;
+ }
}
}
}
diff --git a/frontend/src/app/main/ui/viewer.scss b/frontend/src/app/main/ui/viewer.scss
index 6b46b2d5f4..6fbf27ce92 100644
--- a/frontend/src/app/main/ui/viewer.scss
+++ b/frontend/src/app/main/ui/viewer.scss
@@ -24,7 +24,8 @@
}
.empty-state {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--empty-message-foreground-color);
display: grid;
place-items: center;
@@ -46,7 +47,8 @@
}
.thumbnails-close {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
grid-row: 1 / span 2;
grid-column: 1 / span 1;
z-index: deprecated.$z-index-10;
@@ -58,14 +60,14 @@
}
.viewer-section {
- @extend .new-scrollbar;
+ @extend %new-scrollbar;
+
grid-row: 1 / span 2;
grid-column: 1 / span 1;
display: flex;
align-items: center;
- flex-wrap: nowrap;
+ flex-wrap: wrap;
height: calc(100vh - deprecated.$s-48);
- flex-flow: wrap;
overflow: auto;
}
@@ -78,8 +80,9 @@
.viewer-go-prev,
.viewer-go-next {
- @extend .button-secondary;
- @include deprecated.flexCenter;
+ @extend %button-secondary;
+ @include deprecated.flex-center;
+
position: absolute;
right: deprecated.$s-8;
height: deprecated.$s-64;
@@ -88,8 +91,10 @@
z-index: deprecated.$z-index-2;
background-color: var(--viewer-controls-background-color);
transition: transform 400ms ease 300ms;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
@@ -101,6 +106,7 @@
.viewer-go-prev {
left: deprecated.$s-8;
right: unset;
+
svg {
transform: rotate(180deg);
}
@@ -121,22 +127,26 @@
}
.reset-button {
- @extend .button-secondary;
- @include deprecated.flexCenter;
+ @extend %button-secondary;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
margin-left: deprecated.$s-8;
background-color: var(--viewer-controls-background-color);
pointer-events: all;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
.counter {
- @include deprecated.flexCenter;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.body-small-typography;
+
border-radius: deprecated.$br-8;
width: deprecated.$s-64;
height: deprecated.$s-32;
@@ -153,8 +163,7 @@
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr;
- justify-items: center;
- align-items: center;
+ place-items: center center;
overflow: hidden;
}
@@ -164,7 +173,7 @@
left: 0;
&.visible {
- background-color: rgb(0, 0, 0, 0.2);
+ background-color: rgb(0 0 0 / 0.2);
}
}
diff --git a/frontend/src/app/main/ui/viewer/comments.scss b/frontend/src/app/main/ui/viewer/comments.scss
index fa9ef1daf8..a6b2882ad2 100644
--- a/frontend/src/app/main/ui/viewer/comments.scss
+++ b/frontend/src/app/main/ui/viewer/comments.scss
@@ -8,7 +8,8 @@
// COMMENT DROPDOWN ON HEADER
.view-options {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
position: relative;
@@ -21,7 +22,8 @@
}
.dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
right: deprecated.$s-2;
top: calc(deprecated.$s-2 + deprecated.$s-48);
width: deprecated.$s-272;
@@ -29,7 +31,8 @@
}
.dropdown-title {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
flex-grow: 1;
color: var(--input-foreground-color-active);
}
@@ -41,11 +44,14 @@
.icon,
.icon-dropdown {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
@@ -55,16 +61,21 @@
}
.dropdown-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&:hover .label {
color: var(--input-foreground-color-active);
}
@@ -74,6 +85,7 @@
.label {
color: var(--input-foreground-color-active);
}
+
.icon svg {
stroke: var(--input-foreground-color);
}
@@ -86,8 +98,8 @@
// FLOATING COMMENT
.viewer-comments-container {
position: absolute;
- top: 0px;
- left: 0px;
+ top: 0;
+ left: 0;
width: 100%;
height: 100%;
z-index: deprecated.$z-index-1;
@@ -95,11 +107,11 @@
.threads {
position: absolute;
- top: 0px;
- left: 0px;
+ top: 0;
+ left: 0;
}
-//COMMENT SIDEBAR
+// COMMENT SIDEBAR
.comments-sidebar {
position: absolute;
right: 0;
diff --git a/frontend/src/app/main/ui/viewer/header.scss b/frontend/src/app/main/ui/viewer/header.scss
index f9814d3a44..c80da08171 100644
--- a/frontend/src/app/main/ui/viewer/header.scss
+++ b/frontend/src/app/main/ui/viewer/header.scss
@@ -45,39 +45,46 @@
}
.sitemap-zone {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
position: relative;
width: 100%;
}
.project-name {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--title-foreground-color);
}
.sitemap-text {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
.breadcrumb {
- @include deprecated.bodySmallTypography;
- @include deprecated.flexRow;
+ @include deprecated.body-small-typography;
+ @include deprecated.flex-row;
+
color: var(--title-foreground-color);
cursor: pointer;
}
.breadcrumb-text {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
max-width: 12vw; // This is a fallback
max-width: 12cqw; // This is a unit refered to container
}
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
@@ -88,7 +95,8 @@
}
.dropdown-sitemap {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
left: 0;
top: calc(deprecated.$s-2 + deprecated.$s-48);
width: deprecated.$s-272;
@@ -96,62 +104,74 @@
}
.dropdown-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
.icon-check {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&:hover .label {
color: var(--input-foreground-color-active);
}
}
.current-frame {
- @include deprecated.bodySmallTypography;
- @include deprecated.flexRow;
+ @include deprecated.body-small-typography;
+ @include deprecated.flex-row;
+
flex-grow: 1;
color: var(--title-foreground-color-hover);
cursor: pointer;
+
.icon svg {
stroke: var(--title-foreground-color-hover);
}
}
.frame-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
max-width: 17vw; // This is a fallback
max-width: 17cqw; // This is a unit refered to container
}
// SECTION BUTTONS
.mode-zone {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
height: 100%;
}
.mode-zone-btn {
- @extend .button-tertiary;
- @include deprecated.flexCenter;
+ @extend %button-tertiary;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
padding: 0;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
// OPTION AREA
.options-zone {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
position: relative;
justify-content: flex-end;
gap: deprecated.$s-8;
@@ -166,37 +186,45 @@
}
.fullscreen-btn {
- @extend .button-tertiary;
- @include deprecated.flexCenter;
+ @extend %button-tertiary;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
.share-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
height: deprecated.$s-32;
min-width: deprecated.$s-72;
margin-left: deprecated.$s-4;
}
.edit-btn {
- @extend .button-tertiary;
- @include deprecated.flexCenter;
+ @extend %button-tertiary;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
.go-log-btn {
- @extend .button-tertiary;
- @include deprecated.bodySmallTypography;
+ @extend %button-tertiary;
+ @include deprecated.body-small-typography;
+
height: deprecated.$s-32;
padding: 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
@@ -205,13 +233,16 @@
// ZOOM WIDGET
.zoom-widget {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
height: deprecated.$s-28;
min-width: deprecated.$s-64;
border-radius: deprecated.$br-8;
+
.label {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--button-tertiary-foreground-color-rest);
}
@@ -220,6 +251,7 @@
color: var(--button-tertiary-foreground-color-focus);
}
}
+
&.selected {
.label {
color: var(--button-tertiary-foreground-color-focus);
@@ -228,7 +260,8 @@
}
.dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
right: deprecated.$s-2;
top: calc(deprecated.$s-2 + deprecated.$s-48);
width: deprecated.$s-272;
@@ -246,19 +279,25 @@
}
.zoom-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-28;
width: deprecated.$s-28;
border-radius: deprecated.$br-8;
+
.zoom-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-24;
height: deprecated.$s-32;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
+
&:hover {
.zoom-icon svg {
stroke: var(--button-tertiary-foreground-color-hover);
@@ -267,7 +306,8 @@
}
.zoom-text {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
min-width: deprecated.$s-64;
padding: 0;
@@ -276,22 +316,27 @@
}
.reset-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
color: var(--button-tertiary-foreground-color-hover);
height: deprecated.$s-28;
border-radius: deprecated.$br-8;
}
.zoom-option {
- @extend .menu-item-base;
+ @extend %menu-item-base;
+
.shortcuts {
- @extend .shortcut-base;
+ @extend %shortcut-base;
+
.shortcut-key {
- @extend .shortcut-key-base;
+ @extend %shortcut-key-base;
}
}
+
&:hover {
color: var(--menu-foreground-color-hover);
+
.shortcuts {
.shortcut-key {
color: var(--menu-foreground-color-hover);
diff --git a/frontend/src/app/main/ui/viewer/inspect.scss b/frontend/src/app/main/ui/viewer/inspect.scss
index 0ed6152256..171340752b 100644
--- a/frontend/src/app/main/ui/viewer/inspect.scss
+++ b/frontend/src/app/main/ui/viewer/inspect.scss
@@ -7,7 +7,8 @@
@use "refactor/common-refactor.scss" as deprecated;
.inspect-svg-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
position: relative;
flex-direction: column;
flex: 1;
@@ -30,7 +31,6 @@
position: relative;
align-self: flex-start;
width: var(--right-sidebar-width);
-
background-color: var(--panel-background-color);
border-top: deprecated.$s-1 solid var(--search-bar-input-border-color);
}
diff --git a/frontend/src/app/main/ui/viewer/interactions.scss b/frontend/src/app/main/ui/viewer/interactions.scss
index 8e7d03cab1..d52fb6d933 100644
--- a/frontend/src/app/main/ui/viewer/interactions.scss
+++ b/frontend/src/app/main/ui/viewer/interactions.scss
@@ -7,7 +7,8 @@
@use "refactor/common-refactor.scss" as deprecated;
.view-options {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
position: relative;
@@ -18,8 +19,10 @@
padding: deprecated.$s-8;
cursor: pointer;
}
+
.dropdown-title {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
flex-grow: 1;
color: var(--input-foreground-color-active);
}
@@ -30,7 +33,8 @@
}
.dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
right: deprecated.$s-2;
top: calc(deprecated.$s-2 + deprecated.$s-48);
width: deprecated.$s-272;
@@ -40,17 +44,23 @@
}
.dropdown-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
min-height: deprecated.$s-32;
+
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&:hover .label {
color: var(--input-foreground-color-active);
}
@@ -60,6 +70,7 @@
.label {
color: var(--input-foreground-color-active);
}
+
.icon svg {
stroke: var(--input-foreground-color);
}
@@ -67,11 +78,14 @@
.icon,
.icon-dropdown {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
width: deprecated.$s-16;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
diff --git a/frontend/src/app/main/ui/viewer/login.scss b/frontend/src/app/main/ui/viewer/login.scss
index f107742588..965a7e9ccf 100644
--- a/frontend/src/app/main/ui/viewer/login.scss
+++ b/frontend/src/app/main/ui/viewer/login.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
width: deprecated.$s-368;
}
@@ -20,17 +21,19 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.flexColumn;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-column;
+ @include deprecated.body-small-typography;
+
gap: deprecated.$s-24;
max-height: deprecated.$s-400;
overflow: hidden auto;
@@ -66,7 +69,8 @@
}
a {
- @extend .button-secondary;
+ @extend %button-secondary;
+
height: deprecated.$s-40;
text-transform: uppercase;
font-size: deprecated.$fs-11;
diff --git a/frontend/src/app/main/ui/viewer/share_link.scss b/frontend/src/app/main/ui/viewer/share_link.scss
index 2c8bcc60b1..a0dc26278d 100644
--- a/frontend/src/app/main/ui/viewer/share_link.scss
+++ b/frontend/src/app/main/ui/viewer/share_link.scss
@@ -16,7 +16,8 @@
}
.share-link-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
min-height: unset;
}
@@ -25,27 +26,30 @@
}
.share-link-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-button {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.bodySmallTypography;
- @include deprecated.flexColumn;
+ @include deprecated.body-small-typography;
+ @include deprecated.flex-column;
+
gap: deprecated.$s-24;
}
.share-link-section {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
gap: deprecated.$s-8;
}
.hint-wrapper {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
.hint {
@@ -54,7 +58,8 @@
}
.custon-input-wrapper {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
border-radius: deprecated.$br-8;
height: deprecated.$s-32;
width: 100%;
@@ -62,12 +67,14 @@
}
.input-text {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
color: var(--input-foreground-color-active);
padding-left: deprecated.$s-8;
margin: 0;
flex-grow: 1;
+
&:focus {
outline: none;
border: deprecated.$s-1 solid var(--input-border-color-active);
@@ -75,48 +82,55 @@
}
.copy-button {
- @extend .button-secondary;
- @include deprecated.flexRow;
+ @extend %button-secondary;
+ @include deprecated.flex-row;
+
gap: deprecated.$s-8;
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground-hover);
}
}
.description {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--modal-text-foreground-color);
margin-bottom: deprecated.$s-24;
}
.actions {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
justify-content: flex-end;
}
.button-active {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
}
.button-cancel {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.button-danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
.permissions-section {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
gap: deprecated.$s-8;
}
.manage-permissions {
- @include deprecated.buttonStyle;
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.button-style;
+ @include deprecated.uppercase-title-typography;
+
color: var(--menu-foreground-color-rest);
height: deprecated.$s-32;
display: flex;
@@ -125,12 +139,16 @@
}
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
margin-right: deprecated.$s-6;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
+
&.rotated {
transform: rotate(90deg);
}
@@ -162,39 +180,51 @@
flex-grow: 1;
color: var(--input-foreground-color-active);
}
+
.select-all-row {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
justify-content: space-between;
height: deprecated.$s-32;
border-bottom: deprecated.$s-1 solid var(--input-border-color-disabled);
}
+
.select-all-label {
color: var(--input-foreground-color-active);
}
+
.pages-selection {
margin: 0;
+
li {
border-bottom: deprecated.$s-1 solid var(--input-border-color-disabled);
}
+
li:last-child {
border-bottom: none;
}
}
+
.count-pages,
.current-tag {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--input-foreground-color);
}
.checkbox-wrapper {
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
height: deprecated.$s-32;
padding: 0;
+
span.checked {
background-color: var(--input-checkbox-background-color-active);
border: deprecated.$s-1 solid var(--input-checkbox-background-color-active);
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-checkbox-foreground-color-active);
}
}
diff --git a/frontend/src/app/main/ui/viewer/thumbnails.scss b/frontend/src/app/main/ui/viewer/thumbnails.scss
index c0735ddce1..10bc4e7d31 100644
--- a/frontend/src/app/main/ui/viewer/thumbnails.scss
+++ b/frontend/src/app/main/ui/viewer/thumbnails.scss
@@ -33,22 +33,26 @@
}
.counter {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--viewer-thumbnails-control-foreground-color);
}
.actions {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
width: deprecated.$s-60;
}
.expand-btn,
.close-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
@@ -72,8 +76,9 @@
.right-scroll-handler,
.left-scroll-handler {
- @extend .button-tertiary;
- @include deprecated.flexCenter;
+ @extend %button-tertiary;
+ @include deprecated.flex-center;
+
grid-column: 3 / span 1;
grid-row: 1 / span 1;
width: deprecated.$s-32;
@@ -81,11 +86,14 @@
margin: auto 0;
z-index: deprecated.$z-index-10;
opacity: 0;
+
&:hover {
opacity: 1;
}
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
@@ -93,6 +101,7 @@
.left-scroll-handler {
grid-column: 1 / span 1;
grid-row: 1 / span 1;
+
svg {
transform: rotate(180deg);
}
@@ -112,14 +121,16 @@
}
.thumbnail-item {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
flex-direction: column;
padding: deprecated.$s-16;
}
.thumbnail-preview {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-132;
min-height: deprecated.$s-132;
height: deprecated.$s-132;
@@ -142,8 +153,9 @@
}
.thumbnail-info {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.text-ellipsis;
+
text-align: center;
color: var(--viewer-thumbnails-control-foreground-color);
padding: deprecated.$s-8 0;
diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs
index c0e600b835..0ef0936d22 100644
--- a/frontend/src/app/main/ui/workspace.cljs
+++ b/frontend/src/app/main/ui/workspace.cljs
@@ -36,6 +36,7 @@
[app.main.ui.workspace.tokens.import]
[app.main.ui.workspace.tokens.import.modal]
[app.main.ui.workspace.tokens.management.forms.modals]
+ [app.main.ui.workspace.tokens.management.forms.rename-node-modal]
[app.main.ui.workspace.tokens.remapping-modal]
[app.main.ui.workspace.tokens.settings]
[app.main.ui.workspace.tokens.themes.create-modal]
diff --git a/frontend/src/app/main/ui/workspace.scss b/frontend/src/app/main/ui/workspace.scss
index 5cd617bab4..558c7a4170 100644
--- a/frontend/src/app/main/ui/workspace.scss
+++ b/frontend/src/app/main/ui/workspace.scss
@@ -7,24 +7,20 @@
@use "refactor/common-refactor.scss" as deprecated;
.workspace {
- @extend .new-scrollbar;
+ @extend %new-scrollbar;
+
width: 100vw;
height: 100vh;
max-height: 100vh;
user-select: none;
display: grid;
- grid-template-areas: "left-sidebar viewport right-sidebar";
- grid-template-rows: 1fr;
- grid-template-columns: auto 1fr auto;
+ grid-template: "left-sidebar viewport right-sidebar" 1fr / auto 1fr auto;
overflow: hidden;
}
.workspace-loader {
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ inset: 0;
z-index: var(--z-index-loaders);
background-color: var(--color-background-primary);
}
diff --git a/frontend/src/app/main/ui/workspace/color_palette.cljs b/frontend/src/app/main/ui/workspace/color_palette.cljs
index 7a8dae308c..5e2eb534f1 100644
--- a/frontend/src/app/main/ui/workspace/color_palette.cljs
+++ b/frontend/src/app/main/ui/workspace/color_palette.cljs
@@ -15,7 +15,10 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.color-bullet :as cb]
+ [app.main.ui.components.search-bar :refer [search-bar*]]
[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.utilities.swatch :refer [swatch*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.color :as uc]
@@ -23,6 +26,7 @@
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
+ [app.util.strings :refer [matches-search]]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@@ -59,21 +63,54 @@
{::mf/wrap [mf/memo]}
[{:keys [colors size width selected]}]
(let [state (mf/use-state #(do {:show-menu false}))
+ search-term* (mf/use-state "")
+ search-term (deref search-term*)
+ search-open* (mf/use-state false)
+ search-open? (deref search-open*)
+ has-colors? (seq colors)
+
+ filtered-colors
+ (mf/with-memo [colors search-term]
+ (if (empty? search-term)
+ colors
+ (filterv #(matches-search (or (uc/get-color-name %) "") search-term)
+ colors)))
+
+ on-search-change
+ (mf/use-fn #(reset! search-term* %))
+
+ on-toggle-search
+ (mf/use-fn
+ (fn [_]
+ (when @search-open*
+ (reset! search-term* ""))
+ (swap! search-open* not)))
+
+ on-search-clear
+ (mf/use-fn
+ (fn [_]
+ (reset! search-term* "")
+ (reset! search-open* false)))
+
offset-step (cond
(<= size 64) 40
(<= size 80) 72
:else 72)
+ ;; Reserve room for the search bar, icon button, or nothing
+ search-width (cond (not has-colors?) 0
+ search-open? 192
+ :else 32)
buttons-size (cond
- (<= size 64) 164
- :else 132)
+ (<= size 64) (+ 164 search-width)
+ :else (+ 132 search-width))
width (- width buttons-size)
visible (int (/ width offset-step))
- show-arrows? (> (count colors) visible)
+ show-arrows? (> (count filtered-colors) visible)
visible (if show-arrows?
(int (/ (- width 48) offset-step))
visible)
offset (:offset @state 0)
- max-offset (- (count colors)
+ max-offset (- (count filtered-colors)
visible)
container (mf/use-ref nil)
bullet-size (cond
@@ -121,16 +158,35 @@
width (obj/get dom "clientWidth")]
(swap! state assoc :width width)))
- (mf/with-effect [width colors]
+ (mf/with-effect [width filtered-colors]
(when (not= 0 (:offset @state))
(swap! state assoc :offset 0)))
+ (mf/with-effect [has-colors?]
+ (when-not has-colors?
+ (reset! search-open* false)
+ (reset! search-term* "")))
+
[:div {:class (stl/css-case
:color-palette true
:no-text (< size 64))
:style #js {"--bullet-size" (dm/str bullet-size "px")
"--color-cell-width" (dm/str color-cell-width "px")}}
+ (when has-colors?
+ [:div {:class (stl/css-case :palette-search search-open?
+ :palette-search-collapsed (not search-open?))}
+ (when search-open?
+ [:> search-bar* {:on-change on-search-change
+ :on-clear on-search-clear
+ :value search-term
+ :placeholder (tr "workspace.assets.search")
+ :auto-focus true}])
+ [:> icon-button* {:variant "ghost"
+ :icon i/search
+ :on-click on-toggle-search
+ :aria-label (tr "workspace.assets.search")}]])
+
(when show-arrows?
[:button {:class (stl/css :left-arrow)
:disabled (= offset 0)
@@ -138,18 +194,20 @@
[:div {:class (stl/css :color-palette-content)
:ref container
:on-wheel on-scroll}
- (if (empty? colors)
+ (if (empty? filtered-colors)
[:div {:class (stl/css :color-palette-empty)
:style {:position "absolute"
:left "50%"
:top "50%"
:transform "translate(-50%, -50%)"}}
- (tr "workspace.libraries.colors.empty-palette")]
+ (if (empty? search-term)
+ (tr "workspace.libraries.colors.empty-palette")
+ (tr "workspace.assets.not-found"))]
[:div {:class (stl/css :color-palette-inside)
:style {:position "relative"
:max-width (str width "px")
:right (str (* offset-step offset) "px")}}
- (for [[idx item] (map-indexed vector colors)]
+ (for [[idx item] (map-indexed vector filtered-colors)]
[:> palette-item* {:color item :key idx :size size :selected selected}])])]
(when show-arrows?
diff --git a/frontend/src/app/main/ui/workspace/color_palette.scss b/frontend/src/app/main/ui/workspace/color_palette.scss
index 7a4cb7c09b..2d240b86a8 100644
--- a/frontend/src/app/main/ui/workspace/color_palette.scss
+++ b/frontend/src/app/main/ui/workspace/color_palette.scss
@@ -11,19 +11,41 @@
display: flex;
}
+.palette-search,
+.palette-search-collapsed {
+ display: flex;
+ align-items: center;
+ padding-inline: deprecated.$s-4;
+}
+
+.palette-search {
+ gap: deprecated.$s-4;
+ width: deprecated.$s-192;
+ min-width: deprecated.$s-192;
+}
+
+.palette-search-collapsed {
+ width: deprecated.$s-32;
+ min-width: deprecated.$s-32;
+}
+
.left-arrow,
.right-arrow {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
position: relative;
height: 100%;
width: deprecated.$s-24;
padding: 0;
z-index: deprecated.$z-index-5;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
+
&::after {
content: "";
position: absolute;
@@ -39,20 +61,24 @@
);
pointer-events: none;
}
+
&:hover {
svg {
stroke: var(--button-foreground-hover);
}
}
+
&:disabled {
svg {
stroke: var(--button-foreground-color-disabled);
}
+
&::after {
background-image: none;
}
}
}
+
.left-arrow {
&::after {
left: deprecated.$s-24;
@@ -98,12 +124,14 @@
height: 100%;
&.no-text {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-32;
}
}
.color-palette-empty {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--palette-text-color);
}
diff --git a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss
index a3703f8588..5aa8ee06a7 100644
--- a/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss
+++ b/frontend/src/app/main/ui/workspace/color_palette_ctx_menu.scss
@@ -27,37 +27,50 @@
padding: deprecated.$s-8;
border-radius: deprecated.$br-8;
margin-bottom: deprecated.$s-4;
+
&:last-child {
margin-bottom: 0;
}
+
.option-wrapper {
width: 100%;
+
.library-name {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--context-menu-foreground-color);
display: grid;
grid-template-columns: 1fr deprecated.$s-24;
+
.lib-name-wrapper {
display: flex;
max-width: deprecated.$s-400;
+
.lib-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
max-width: deprecated.$s-380;
}
+
.lib-num {
margin-left: deprecated.$s-4;
}
}
+
.icon-wrapper {
margin-left: deprecated.$s-4;
- @include deprecated.flexCenter;
+
+ @include deprecated.flex-center;
+
svg {
- @extend .button-icon-small;
- @include deprecated.flexCenter;
+ @extend %button-icon-small;
+ @include deprecated.flex-center;
+
stroke: var(--icon-foreground);
}
}
}
+
.color-sample {
display: flex;
flex-direction: row;
@@ -70,11 +83,14 @@
&:hover {
.option-wrapper .library-name {
color: var(--context-menu-foreground-color-selected);
+
.icon-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
svg {
- @include deprecated.flexCenter;
- @extend .button-icon-small;
+ @include deprecated.flex-center;
+ @extend %button-icon-small;
+
stroke: var(--context-menu-foreground-color-selected);
}
}
diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs
index d6d4300848..7b4c5bfa62 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs
@@ -26,7 +26,6 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
- [app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.foundations.assets.icon :as i]
@@ -83,6 +82,15 @@
hsl-from (cc/hsv->hsl [h 0.0 v])
hsl-to (cc/hsv->hsl [h 1.0 v])
+ ;; HSL-mode gradients. For S: fix current lightness, sweep
+ ;; saturation 0 → 1. For L: fix current saturation, sweep
+ ;; lightness 0 → 0.5 (pure hue) → 1. All computed at the
+ ;; current hue.
+ [_ cur-hsl-s cur-hsl-l] (cc/rgb->hsl rgb)
+ hsl-sat-from [h 0.0 cur-hsl-l]
+ hsl-sat-to [h 1.0 cur-hsl-l]
+ lightness-mid [h cur-hsl-s 0.5]
+
format-hsl (fn [[h s l]]
(str/fmt "hsl(%s, %s, %s)"
h
@@ -91,7 +99,10 @@
(dom/set-css-property! node "--color" (str/join ", " rgb))
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
- (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
+ (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))
+ (dom/set-css-property! node "--hsl-saturation-grad-from" (format-hsl hsl-sat-from))
+ (dom/set-css-property! node "--hsl-saturation-grad-to" (format-hsl hsl-sat-to))
+ (dom/set-css-property! node "--lightness-grad-mid" (format-hsl lightness-mid)))))
(mf/defc colorpicker*
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab applied-token]}]
@@ -129,10 +140,15 @@
active-color-tab* (hooks/use-persisted-state ::color-tab "ramp")
active-color-tab (deref active-color-tab*)
+ ;; Inline HSB/HSL toggle inside the HSBA tab — shared between
+ ;; the slider selector (for labels) and the numeric inputs.
+ hsb-mode* (hooks/use-persisted-state ::hsb-mode :hsb)
+ hsb-mode (deref hsb-mode*)
+
drag?* (mf/use-state false)
drag? (deref drag?*)
- type (if (= active-color-tab "hsva") :hsv :rgb)
+ type (if (= active-color-tab "hsva") :hsb :rgb)
fill-image-ref (mf/use-ref nil)
@@ -341,11 +357,6 @@
(mapv #(assoc %2 :offset (:offset %1)) stops new-stops)]
(st/emit! (dc/update-colorpicker-stops stops)))))
- handle-change-gradient-opacity
- (mf/use-fn
- (fn [value]
- (st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100)))))
-
render-wasm?
(features/use-feature "render-wasm/v1")
@@ -357,7 +368,7 @@
{:aria-label "Harmony"
:icon i/rgba-complementary
:id "harmony"}
- {:aria-label "HSVA"
+ {:aria-label "HSBA"
:icon i/hsva
:id "hsva"}])
@@ -394,17 +405,6 @@
[:div {:class (stl/css :top-actions)}
[:div {:class (stl/css :top-actions-right)}
- (when (and (= color-style :direct-color)
- (= :gradient selected-mode))
- [:div {:class (stl/css :opacity-input-wrapper)}
- [:span {:class (stl/css :icon-text)} "%"]
- [:> numeric-input*
- {:value (-> data :opacity opacity->string)
- :on-change handle-change-gradient-opacity
- :default 100
- :data-testid "opacity-global-input"
- :min 0
- :max 100}]])
(when (and (= color-style :direct-color)
(or (not disable-gradient) (not disable-image)))
@@ -522,6 +522,7 @@
[:> hsva-selector*
{:color current-color
:disable-opacity disable-opacity
+ :mode hsb-mode
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]))]]
@@ -529,6 +530,8 @@
[:> color-inputs*
{:type type
:disable-opacity disable-opacity
+ :mode hsb-mode
+ :on-mode-change #(reset! hsb-mode* %)
:color current-color
:on-change handle-change-color}]
diff --git a/frontend/src/app/main/ui/workspace/colorpicker.scss b/frontend/src/app/main/ui/workspace/colorpicker.scss
index 1d7e303d41..5de25d32a1 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker.scss
@@ -5,16 +5,16 @@
// Copyright (c) KALEIDOS INC
@use "ds/typography.scss" as t;
-@use "ds/spacing.scss";
+@use "ds/spacing";
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "refactor/basic-rules.scss" as *;
.colorpicker-tooltip {
- @extend .modal-background;
+ @extend %modal-background;
+
left: calc(10 * px2rem(140));
- width: auto;
padding: var(--sp-m);
width: $sz-284;
overflow: auto;
@@ -41,8 +41,9 @@
}
.opacity-input-wrapper {
- @extend .input-element;
+ @extend %input-element;
@include t.use-typography("body-small");
+
width: px2rem(68);
}
@@ -51,10 +52,8 @@
display: flex;
justify-content: center;
align-items: center;
- border: none;
background: none;
cursor: pointer;
- border-radius: $br-8;
background-color: transparent;
border: $b-1 solid transparent;
height: var(--sp-xl);
@@ -62,29 +61,37 @@
border-radius: $br-4;
padding: 0;
margin-top: var(--sp-xs);
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--button-tertiary-foreground-color-rest);
}
+
&:hover {
svg {
stroke: var(--button-tertiary-foreground-color-focus);
}
}
+
&:focus,
&:focus-visible {
outline: none;
+
svg {
stroke: var(--button-secondary-foreground-color-hover);
}
}
+
&:active {
outline: none;
border: $b-1 solid transparent;
+
svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
+
&.selected {
svg {
stroke: var(--button-tertiary-foreground-color-active);
@@ -99,11 +106,13 @@
}
.gradient-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: var(--sp-xl);
width: var(--sp-xl);
border-radius: $br-4;
border: $b-2 solid transparent;
+
&:hover {
border: $b-2 solid var(--colorpicker-details-color-selected);
}
@@ -111,16 +120,18 @@
.linear-gradient-btn {
background: linear-gradient(180deg, var(--color-foreground-secondary), transparent);
+
&.selected {
- background: linear-gradient(to bottom, rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%);
+ background: linear-gradient(to bottom, rgb(126 255 245 / 1) 0%, rgb(126 255 245 / 0.2) 100%);
border: $b-2 solid var(--colorpicker-details-color-selected);
}
}
.radial-gradient-btn {
background: radial-gradient(transparent, var(--color-foreground-secondary));
+
&.selected {
- background: radial-gradient(rgba(126, 255, 245, 1) 0%, rgba(126, 255, 245, 0.2) 100%);
+ background: radial-gradient(rgb(126 255 245 / 1) 0%, rgb(126 255 245 / 0.2) 100%);
border: $b-2 solid var(--colorpicker-details-color-selected);
}
}
@@ -132,7 +143,8 @@
.accept-color {
@include t.use-typography("headline-small");
- @extend .button-primary;
+ @extend %button-primary;
+
width: 100%;
height: var(--sp-xxxl);
margin-top: var(--sp-s);
@@ -180,6 +192,7 @@
height: px2rem(140);
margin-bottom: $sz-6;
margin-right: $sz-1;
+
img {
height: fit-content;
width: fit-content;
@@ -190,20 +203,23 @@
}
.choose-image {
- @extend .button-secondary;
+ @extend %button-secondary;
@include t.use-typography("headline-small");
+
width: 100%;
margin-top: var(--sp-m);
height: var(--sp-xxxl);
}
.checkbox-option {
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
margin: var(--sp-l) 0 0 0;
}
.token-color-title {
@include t.use-typography("title-small");
+
color: var(--color-foreground-secondary);
display: flex;
align-items: center;
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs
index 09ad2d0e8c..69c466669c 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs
@@ -28,11 +28,23 @@
[val]
(* (/ val 255) 100))
-(mf/defc color-inputs* [{:keys [type color disable-opacity on-change]}]
+(mf/defc color-inputs* [{:keys [type color disable-opacity mode on-mode-change on-change]}]
(let [{red :r green :g blue :b
hue :h saturation :s value :v
hex :hex alpha :alpha} color
+ ;; Sub-model selector for the HSB tab: users can toggle between
+ ;; HSB and HSL input display without leaving the tab. State is
+ ;; lifted to the colorpicker parent so the slider labels stay
+ ;; in sync with the inputs.
+ hsb-mode (or mode :hsb)
+
+ ;; Compute HSL from current RGB (derived; not stored on the color map)
+ [_hsl-h hsl-s hsl-l]
+ (if (and red green blue)
+ (cc/rgb->hsl [red green blue])
+ [0 0 0])
+
refs {:hex (mf/use-ref nil)
:r (mf/use-ref nil)
:g (mf/use-ref nil)
@@ -40,6 +52,8 @@
:h (mf/use-ref nil)
:s (mf/use-ref nil)
:v (mf/use-ref nil)
+ :hsl-s (mf/use-ref nil)
+ :hsl-l (mf/use-ref nil)
:alpha (mf/use-ref nil)}
setup-hex-color
@@ -73,6 +87,7 @@
(let [val (case property
:s (/ val 100)
:v (value->hsv-value val)
+ (:hsl-s :hsl-l) (/ val 100)
:alpha (/ val 100)
val)]
(cond
@@ -87,6 +102,18 @@
:h h :s s :v v
:r r :g g :b b}))
+ ;; HSL changes: recompute RGB/HSV from the new HSL triple,
+ ;; reusing the current hue when only S or L changes.
+ (#{:hsl-s :hsl-l} property)
+ (let [new-s (if (= property :hsl-s) val hsl-s)
+ new-l (if (= property :hsl-l) val hsl-l)
+ [r g b] (cc/hsl->rgb [hue new-s new-l])
+ hex (cc/rgb->hex [r g b])
+ [h s v] (cc/hex->hsv hex)]
+ (on-change {:hex hex
+ :h h :s s :v v
+ :r r :g g :b b}))
+
:else
(let [{:keys [h s v]} (merge color (hash-map property val))
hex (cc/hsv->hex [h s v])
@@ -126,10 +153,13 @@
;; Updates the inputs values when a property is changed in the parent
(mf/use-effect
- (mf/deps color type)
+ (mf/deps color type hsb-mode)
(fn []
(doseq [ref-key (keys refs)]
- (let [property-val (get color ref-key)
+ (let [property-val (case ref-key
+ :hsl-s hsl-s
+ :hsl-l hsl-l
+ (get color ref-key))
property-ref (get refs ref-key)]
(when (and property-val property-ref)
(when-let [node (mf/ref-val property-ref)]
@@ -137,14 +167,32 @@
(case ref-key
(:s :alpha) (mth/precision (* property-val 100) 2)
:v (mth/precision (hsv-value->value property-val) 2)
+ (:hsl-s :hsl-l) (mth/precision (* property-val 100) 2)
property-val)]
(dom/set-value! node new-val))))))))
[:div {:class (stl/css-case :color-values true
:disable-opacity disable-opacity)}
+ ;; Inline HSB/HSL switcher — only shown on the HSB tab so that
+ ;; designers can pick whichever hue-based model matches their
+ ;; workflow (HSB matches Figma/Sketch/XD, HSL matches CSS).
+ (when (and (not= type :rgb) on-mode-change)
+ [:div {:class (stl/css :model-switcher)}
+ [:button {:type "button"
+ :class (stl/css-case :model-pill true
+ :model-pill-active (= hsb-mode :hsb))
+ :on-click #(on-mode-change :hsb)}
+ "HSB"]
+ [:button {:type "button"
+ :class (stl/css-case :model-pill true
+ :model-pill-active (= hsb-mode :hsl))
+ :on-click #(on-mode-change :hsl)}
+ "HSL"]])
+
[:div {:class (stl/css :colors-row)}
- (if (= type :rgb)
+ (cond
+ (= type :rgb)
[:*
[:div {:class (stl/css :input-wrapper)}
[:label {:for "red-value" :class (stl/css :input-label)} "R"]
@@ -177,6 +225,42 @@
:on-change (on-change-property :b 255)
:on-key-down (on-key-down-property :b 255)}]]]
+ (= hsb-mode :hsl)
+ [:*
+ [:div {:class (stl/css :input-wrapper)}
+ [:label {:for "hue-value" :class (stl/css :input-label)} "H"]
+ [:input {:id "hue-value"
+ :ref (:h refs)
+ :type "number"
+ :min 0
+ :max 360
+ :default-value hue
+ :on-change (on-change-property :h 360)
+ :on-key-down (on-key-down-property :h 360)}]]
+ [:div {:class (stl/css :input-wrapper)}
+ [:label {:for "hsl-saturation-value" :class (stl/css :input-label)} "S"]
+ [:input {:id "hsl-saturation-value"
+ :ref (:hsl-s refs)
+ :type "number"
+ :min 0
+ :max 100
+ :step 1
+ :default-value (mth/precision (* hsl-s 100) 2)
+ :on-change (on-change-property :hsl-s 100)
+ :on-key-down (on-key-down-property :hsl-s 100)}]]
+ [:div {:class (stl/css :input-wrapper)}
+ [:label {:for "lightness-value" :class (stl/css :input-label)} "L"]
+ [:input {:id "lightness-value"
+ :ref (:hsl-l refs)
+ :type "number"
+ :min 0
+ :max 100
+ :step 1
+ :default-value (mth/precision (* hsl-l 100) 2)
+ :on-change (on-change-property :hsl-l 100)
+ :on-key-down (on-key-down-property :hsl-l 100)}]]]
+
+ :else
[:*
[:div {:class (stl/css :input-wrapper)}
[:label {:for "hue-value" :class (stl/css :input-label)} "H"]
@@ -200,8 +284,8 @@
:on-change (on-change-property :s 100)
:on-key-down (on-key-down-property :s 100)}]]
[:div {:class (stl/css :input-wrapper)}
- [:label {:for "value-value" :class (stl/css :input-label)} "V"]
- [:input {:id "value-value"
+ [:label {:for "brightness-value" :class (stl/css :input-label)} "B(V)"]
+ [:input {:id "brightness-value"
:ref (:v refs)
:type "number"
:min 0
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss
index 6d653f34e3..3a3034bc95 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.scss
@@ -7,28 +7,66 @@
@use "refactor/common-refactor.scss" as deprecated;
.color-values {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
margin-top: deprecated.$s-8;
+ .model-switcher {
+ display: flex;
+ gap: deprecated.$s-4;
+ margin-bottom: deprecated.$s-8;
+ padding: deprecated.$s-2;
+ background-color: var(--color-background-tertiary);
+ border-radius: deprecated.$s-6;
+ align-self: flex-start;
+
+ .model-pill {
+ @include deprecated.body-small-typography;
+
+ padding: deprecated.$s-2 deprecated.$s-8;
+ border: none;
+ border-radius: deprecated.$s-4;
+ background: transparent;
+ color: var(--color-foreground-secondary);
+ cursor: pointer;
+
+ &:hover {
+ color: var(--color-foreground-primary);
+ }
+
+ &.model-pill-active {
+ background-color: var(--color-background-primary);
+ color: var(--color-accent-primary);
+ }
+ }
+ }
+
&.disable-opacity {
grid-template-columns: 3.5rem repeat(3, 1fr);
}
+
.colors-row {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
.input-wrapper {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
width: deprecated.$s-84;
display: flex;
align-items: baseline;
}
}
+
.hex-alpha-wrapper {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
.input-wrapper {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
width: deprecated.$s-84;
+
&.hex {
width: deprecated.$s-172;
display: flex;
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss
index 37ab3f3e40..ba963307bd 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/color_tokens.scss
@@ -16,6 +16,7 @@
.color-token-item {
--color-token-background: var(--color-background-primary);
+
background-color: var(--color-token-background);
color: var(--color-foreground-primary);
text-align: left;
@@ -29,6 +30,7 @@
block-size: $sz-28;
border: none;
cursor: pointer;
+
&:hover {
--color-token-background: var(--color-background-tertiary);
}
@@ -36,6 +38,7 @@
.color-token-empty-state {
@include t.use-typography("body-small");
+
padding: var(--sp-s) var(--sp-xxl);
text-align: center;
color: var(--color-foreground-secondary);
@@ -57,6 +60,7 @@
.token-name {
@include t.use-typography("body-small");
+
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -97,7 +101,9 @@
.set-title-bar {
--title-color: var(--color-foreground-secondary);
--arrow-color: var(--color-foreground-secondary);
+
@include t.use-typography("title-small");
+
text-transform: none;
display: flex;
overflow: hidden;
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss
index d9e5b75633..af6a439328 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss
@@ -46,7 +46,7 @@
background-size: deprecated.$s-8;
border-radius: deprecated.$br-6;
border: deprecated.$s-2 solid var(--color-foreground-primary);
- box-shadow: 0px 0px deprecated.$s-4 0px var(--menu-shadow-color);
+ box-shadow: 0 0 deprecated.$s-4 0 var(--menu-shadow-color);
height: calc(deprecated.$s-24 - deprecated.$s-2);
left: var(--position);
overflow: hidden;
@@ -59,11 +59,12 @@
outline: deprecated.$s-2 solid var(--color-accent-primary);
}
}
+
.gradient-preview-stop-decoration {
background: var(--color-foreground-primary);
border-radius: 100%;
bottom: deprecated.$s-32;
- box-shadow: 0px 0px deprecated.$s-4 0px var(--menu-shadow-color);
+ box-shadow: 0 0 deprecated.$s-4 0 var(--menu-shadow-color);
height: deprecated.$s-4;
left: calc(var(--position) + deprecated.$s-8);
position: absolute;
@@ -109,8 +110,7 @@
flex-direction: column;
gap: deprecated.$s-4;
max-height: deprecated.$s-180;
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
padding: 0 0 var(--sp-s) var(--sp-m);
}
@@ -120,7 +120,6 @@
padding: deprecated.$s-2;
border-radius: deprecated.$br-12;
border: deprecated.$s-1 solid transparent;
-
position: relative;
&.is-selected {
@@ -141,8 +140,9 @@
}
.offset-input-wrapper {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
width: deprecated.$s-92;
}
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss
index e2438dc416..74b34eab5d 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/harmony.scss
@@ -15,7 +15,8 @@
}
.hue-wheel-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
position: relative;
}
@@ -25,7 +26,8 @@
}
.handler {
- @extend .colorpicker-handler;
+ @extend %colorpicker-handler;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
border: deprecated.$s-2 solid var(--colorpicker-handlers-color);
@@ -37,7 +39,8 @@
}
.handlers-wrapper {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
height: deprecated.$s-200;
width: deprecated.$s-52;
flex-grow: 1;
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs
index 807d976314..802f59c8c2 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.cljs
@@ -11,17 +11,45 @@
[app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector*]]
[rumext.v2 :as mf]))
-(mf/defc hsva-selector* [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}]
- (let [{hue :h saturation :s value :v alpha :alpha} color
- handle-change-slider (fn [key]
- (fn [new-value]
- (let [change (hash-map key new-value)
- {:keys [h s v]} (merge color change)
- hex (cc/hsv->hex [h s v])
- [r g b] (cc/hex->rgb hex)]
- (on-change (merge change
- {:hex hex
- :r r :g g :b b})))))
+(mf/defc hsva-selector* [{:keys [color disable-opacity mode on-change on-start-drag on-finish-drag]}]
+ (let [{hue :h saturation :s value :v alpha :alpha
+ r-val :r g-val :g b-val :b} color
+ hsl-mode? (= mode :hsl)
+
+ ;; Current HSL derived from RGB — used as the starting point
+ ;; for HSL saturation/lightness slider values and for
+ ;; recomputing the color when either is dragged.
+ [_ hsl-s hsl-l] (if (and r-val g-val b-val)
+ (cc/rgb->hsl [r-val g-val b-val])
+ [0 0 0])
+
+ ;; HSB math — current default behavior.
+ handle-change-hsv
+ (fn [key]
+ (fn [new-value]
+ (let [change (hash-map key new-value)
+ {:keys [h s v]} (merge color change)
+ hex (cc/hsv->hex [h s v])
+ [r g b] (cc/hex->rgb hex)]
+ (on-change (merge change
+ {:hex hex
+ :r r :g g :b b})))))
+
+ ;; HSL math — when the user drags the S or L slider in HSL mode,
+ ;; we recompute RGB from the updated HSL triple and derive HSV
+ ;; for the canonical color representation.
+ handle-change-hsl
+ (fn [key]
+ (fn [new-value]
+ (let [new-s (if (= key :hsl-s) new-value hsl-s)
+ new-l (if (= key :hsl-l) new-value hsl-l)
+ [r g b] (cc/hsl->rgb [hue new-s new-l])
+ hex (cc/rgb->hex [r g b])
+ [h s v] (cc/hex->hsv hex)]
+ (on-change {:hex hex
+ :h h :s s :v v
+ :r r :g g :b b}))))
+
on-change-opacity (fn [new-alpha] (on-change {:alpha new-alpha}))]
[:div {:class (stl/css :hsva-selector)}
[:div {:class (stl/css :hsva-row)}
@@ -31,29 +59,47 @@
:type :hue
:max-value 360
:value hue
- :on-change (handle-change-slider :h)
+ :on-change (handle-change-hsv :h)
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]]
[:div {:class (stl/css :hsva-row)}
[:span {:class (stl/css :hsva-selector-label)} "S"]
- [:> slider-selector*
- {:class (stl/css :hsva-bar)
- :type :saturation
- :max-value 1
- :value saturation
- :on-change (handle-change-slider :s)
- :on-start-drag on-start-drag
- :on-finish-drag on-finish-drag}]]
+ (if hsl-mode?
+ [:> slider-selector*
+ {:class (stl/css :hsva-bar)
+ :type :hsl-saturation
+ :max-value 1
+ :value hsl-s
+ :on-change (handle-change-hsl :hsl-s)
+ :on-start-drag on-start-drag
+ :on-finish-drag on-finish-drag}]
+ [:> slider-selector*
+ {:class (stl/css :hsva-bar)
+ :type :saturation
+ :max-value 1
+ :value saturation
+ :on-change (handle-change-hsv :s)
+ :on-start-drag on-start-drag
+ :on-finish-drag on-finish-drag}])]
[:div {:class (stl/css :hsva-row)}
- [:span {:class (stl/css :hsva-selector-label)} "V"]
- [:> slider-selector*
- {:class (stl/css :hsva-bar)
- :type :value
- :max-value 255
- :value value
- :on-change (handle-change-slider :v)
- :on-start-drag on-start-drag
- :on-finish-drag on-finish-drag}]]
+ [:span {:class (stl/css :hsva-selector-label)} (if hsl-mode? "L" "B(V)")]
+ (if hsl-mode?
+ [:> slider-selector*
+ {:class (stl/css :hsva-bar)
+ :type :lightness
+ :max-value 1
+ :value hsl-l
+ :on-change (handle-change-hsl :hsl-l)
+ :on-start-drag on-start-drag
+ :on-finish-drag on-finish-drag}]
+ [:> slider-selector*
+ {:class (stl/css :hsva-bar)
+ :type :value
+ :max-value 255
+ :value value
+ :on-change (handle-change-hsv :v)
+ :on-start-drag on-start-drag
+ :on-finish-drag on-finish-drag}])]
(when (not disable-opacity)
[:div {:class (stl/css :hsva-row)}
[:span {:class (stl/css :hsva-selector-label)} "A"]
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss
index 08def7607f..17e0b52fc6 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/hsva.scss
@@ -7,9 +7,10 @@
@use "refactor/common-refactor.scss" as deprecated;
.hsva-selector {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
padding: deprecated.$s-4;
- grid-row-gap: deprecated.$s-8;
+ row-gap: deprecated.$s-8;
margin-bottom: deprecated.$s-8;
}
@@ -19,7 +20,8 @@
}
.hsva-selector-label {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
display: flex;
align-items: center;
justify-content: flex-start;
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss
index 0fc9028c86..1489da0fd0 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.scss
@@ -23,13 +23,15 @@
.add-color-btn,
.palette-btn {
- @extend .button-secondary;
+ @extend %button-secondary;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
border-radius: deprecated.$br-circle;
padding: 0;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss
index 00e5825af6..952f6f344b 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.scss
@@ -7,7 +7,7 @@
@use "refactor/common-refactor.scss" as deprecated;
.value-saturation-selector {
- background-color: rgba(var(--hue-rgb));
+ background-color: rgb(var(--hue-rgb));
position: relative;
height: deprecated.$s-140;
width: 100%;
@@ -20,7 +20,7 @@
position: absolute;
width: 100%;
height: 100%;
- background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
+ background: linear-gradient(to right, #fff, rgb(255 255 255 / 0));
}
&::after {
@@ -28,12 +28,13 @@
position: absolute;
width: 100%;
height: 100%;
- background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
+ background: linear-gradient(to top, #000, rgb(0 0 0 / 0));
}
}
.handler {
- @extend .colorpicker-handler;
+ @extend %colorpicker-handler;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
border: deprecated.$s-2 solid var(--colorpicker-handlers-color);
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs
index f125b6368b..7021db6767 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs
+++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.cljs
@@ -53,7 +53,9 @@
:slider-selector true
:hue (= type :hue)
:opacity (= type :opacity)
- :value (= type :value)))
+ :value (= type :value)
+ :hsl-saturation (= type :hsl-saturation)
+ :lightness (= type :lightness)))
:data-testid (when (= type :opacity) "slider-opacity")
:on-pointer-down handle-start-drag
:on-pointer-up handle-stop-drag
diff --git a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss
index 460939c0a8..09b9942e22 100644
--- a/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss
+++ b/frontend/src/app/main/ui/workspace/colorpicker/slider_selector.scss
@@ -14,17 +14,14 @@
--gradient-direction: 0deg;
--background-repeat: top;
}
+
position: relative;
align-self: center;
height: deprecated.$s-24;
inline-size: 100%;
border: deprecated.$s-2 solid var(--colorpicker-details-color);
border-radius: deprecated.$br-6;
- background: linear-gradient(
- var(--gradient-direction),
- rgba(var(--color), 0) 0%,
- rgba(var(--color), 1) 100%
- );
+ background: linear-gradient(var(--gradient-direction), rgb(var(--color), 0) 0%, rgb(var(--color), 1) 100%);
cursor: pointer;
&.vertical {
@@ -65,8 +62,8 @@
height: 100%;
background: linear-gradient(
var(--gradient-direction),
- rgba(var(--color), 0) 0%,
- rgba(var(--color), 1) 100%
+ rgb(var(--color), 0) 0%,
+ rgb(var(--color), 1) 100%
);
}
}
@@ -75,6 +72,18 @@
background: linear-gradient(var(--gradient-direction), #000 0%, #fff 100%);
}
+ &.hsl-saturation {
+ background: linear-gradient(
+ var(--gradient-direction),
+ var(--hsl-saturation-grad-from) 0%,
+ var(--hsl-saturation-grad-to) 100%
+ );
+ }
+
+ &.lightness {
+ background: linear-gradient(var(--gradient-direction), #000 0%, var(--lightness-grad-mid) 50%, #fff 100%);
+ }
+
.handler {
position: absolute;
left: 50%;
@@ -109,6 +118,7 @@
.slider-selector.value {
background: linear-gradient(var(--gradient-direction), var(--hue-from, #000) 0%, var(--hue-to, #fff) 100%);
}
+
.slider-selector.saturation {
background: linear-gradient(
var(--gradient-direction),
diff --git a/frontend/src/app/main/ui/workspace/comments.scss b/frontend/src/app/main/ui/workspace/comments.scss
index 1cd9c04b64..9b956523ca 100644
--- a/frontend/src/app/main/ui/workspace/comments.scss
+++ b/frontend/src/app/main/ui/workspace/comments.scss
@@ -23,8 +23,9 @@
}
.mode-dropdown-wrapper {
- @include deprecated.buttonStyle;
- @extend .asset-element;
+ @include deprecated.button-style;
+ @extend %asset-element;
+
background-color: var(--color-background-tertiary);
display: flex;
width: 100%;
@@ -45,18 +46,22 @@
}
.arrow-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
}
.comment-mode-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
top: deprecated.$s-92;
left: deprecated.$s-12;
max-width: deprecated.$s-256;
@@ -68,29 +73,38 @@
}
.dropdown-item {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
justify-content: space-between;
+
.icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: transparent;
}
}
+
.label {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
}
+
&:hover {
.icon svg {
stroke: transparent;
}
}
+
&.selected {
.label {
color: var(--menu-foreground-color);
}
+
.icon svg {
stroke: var(--icon-foreground-hover);
}
diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs
index c310b073be..c70764ffd3 100644
--- a/frontend/src/app/main/ui/workspace/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/context_menu.cljs
@@ -21,6 +21,7 @@
[app.main.data.modal :as modal]
[app.main.data.shortcuts :as scd]
[app.main.data.workspace :as dw]
+ [app.main.data.workspace.guides :as dwg]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.selection :as dws]
@@ -633,6 +634,25 @@
[:> menu-entry* {:title (tr "workspace.shape.menu.combine-as-variants")
:on-click do-combine-as-variants}]])]))
+(mf/defc context-menu-guides*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [shapes]}]
+ (let [frame-ids (into #{} (comp (filter cfh/frame-shape?) d/xf:map-id) shapes)
+ guides (mf/deref refs/workspace-page-guides)
+ has-guides? (some #(contains? frame-ids (:frame-id %)) (vals guides))
+
+ do-remove-guides
+ (mf/use-fn
+ (mf/deps frame-ids)
+ #(st/emit! (dwg/remove-frame-guides frame-ids)))]
+
+ (when (and (seq frame-ids) has-guides?)
+ [:*
+ [:> menu-separator* {}]
+ [:> menu-entry* {:title (tr "workspace.shape.menu.clear-guides")
+ :on-click do-remove-guides}]])))
+
(mf/defc context-menu-delete*
{::mf/private true}
[]
@@ -673,6 +693,7 @@
(when is-not-variant-container?
[:> context-menu-layout* props])
[:> context-menu-component* props]
+ [:> context-menu-guides* props]
[:> context-menu-delete* props]])))
(mf/defc page-item-context-menu*
@@ -778,6 +799,7 @@
[{:keys [mdata]}]
(let [{:keys [grid cells]} mdata
+ grid-id (:id grid)
single? (= (count cells) 1)
can-merge?
@@ -785,17 +807,53 @@
(mf/deps cells)
#(ctl/valid-area-cells? cells))
+ can-copy-rows?
+ (mf/use-memo
+ (mf/deps grid cells)
+ #(dwsl/complete-rows? grid cells))
+
+ can-copy-columns?
+ (mf/use-memo
+ (mf/deps grid cells)
+ #(dwsl/complete-columns? grid cells))
+
+ grid-edition-ref
+ (mf/use-memo
+ (mf/deps grid-id)
+ #(refs/workspace-grid-edition-id grid-id))
+
+ grid-edition (mf/deref grid-edition-ref)
+ has-copied-tracks? (some? (:copied-tracks grid-edition))
+
do-merge-cells
(mf/use-fn
- (mf/deps grid cells)
+ (mf/deps grid-id cells)
(fn []
- (st/emit! (dwsl/merge-cells (:id grid) (map :id cells)))))
+ (st/emit! (dwsl/merge-cells grid-id (map :id cells)))))
do-create-board
(mf/use-fn
- (mf/deps grid cells)
+ (mf/deps grid-id cells)
(fn []
- (st/emit! (dwsl/create-cell-board (:id grid) (map :id cells)))))]
+ (st/emit! (dwsl/create-cell-board grid-id (map :id cells)))))
+
+ do-copy-rows
+ (mf/use-fn
+ (mf/deps grid-id)
+ (fn []
+ (st/emit! (dwsl/copy-grid-tracks grid-id :row))))
+
+ do-copy-columns
+ (mf/use-fn
+ (mf/deps grid-id)
+ (fn []
+ (st/emit! (dwsl/copy-grid-tracks grid-id :column))))
+
+ do-paste-tracks
+ (mf/use-fn
+ (mf/deps grid-id)
+ (fn []
+ (st/emit! (dwsl/paste-grid-tracks grid-id))))]
[:*
(when (not single?)
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.merge")
@@ -808,8 +866,64 @@
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.create-board")
:on-click do-create-board
- :disabled (and (not single?) (not can-merge?))}]]))
+ :disabled (and (not single?) (not can-merge?))}]
+ [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-rows")
+ :on-click do-copy-rows
+ :disabled (not can-copy-rows?)}]
+
+ [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-columns")
+ :on-click do-copy-columns
+ :disabled (not can-copy-columns?)}]
+
+ [:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.paste-tracks")
+ :on-click do-paste-tracks
+ :disabled (not has-copied-tracks?)}]]))
+
+
+(def guide-color-presets
+ ["#ff3277" "#4dabf7" "#51cf66" "#fcc419" "#ff922b" "#cc5de8" "#ffffff" "#868e96"])
+
+(mf/defc guide-color-context-menu*
+ {::mf/props :obj
+ ::mf/private true}
+ [{:keys [mdata]}]
+ (let [{:keys [guide]} mdata
+ guide-id (:id guide)
+ current-color (or (:color guide) (first guide-color-presets))
+
+ do-set-color
+ (mf/use-fn
+ (mf/deps guide-id)
+ (fn [event]
+ (let [color (dom/get-data (dom/get-current-target event) "color")]
+ (st/emit! dw/hide-context-menu
+ (dwg/update-guide-color guide-id color)))))
+
+ do-remove-guide
+ (mf/use-fn
+ (mf/deps guide)
+ (fn []
+ (st/emit! dw/hide-context-menu
+ (dwg/remove-guide guide))))]
+
+ [:*
+ [:li {:class (stl/css :context-menu-item :guide-color-label)}
+ [:span {:class (stl/css :title)}
+ (tr "workspace.context-menu.guides.change-color")]]
+ [:li {:class (stl/css :guide-color-swatches)}
+ (for [color guide-color-presets]
+ [:span {:key color
+ :class (stl/css-case
+ :guide-color-swatch true
+ :selected (= color current-color))
+ :data-color color
+ :on-click do-set-color
+ :title color
+ :style {:background-color color}}])]
+ [:> menu-separator* {}]
+ [:> menu-entry* {:title (tr "workspace.context-menu.guides.remove")
+ :on-click do-remove-guide}]]))
;; FIXME: optimize because it is rendered always
@@ -848,4 +962,5 @@
:page [:> page-item-context-menu* {:mdata mdata}]
:grid-track [:> grid-track-context-menu* {:mdata mdata}]
:grid-cells [:> grid-cells-context-menu* {:mdata mdata}]
+ :guide [:> guide-color-context-menu* {:mdata mdata}]
[:> viewport-context-menu* {:mdata mdata}]))]]]))
diff --git a/frontend/src/app/main/ui/workspace/context_menu.scss b/frontend/src/app/main/ui/workspace/context_menu.scss
index 8d347c2190..a6d1c799a3 100644
--- a/frontend/src/app/main/ui/workspace/context_menu.scss
+++ b/frontend/src/app/main/ui/workspace/context_menu.scss
@@ -15,7 +15,8 @@
.context-list,
.workspace-context-submenu {
- @include deprecated.menuShadow;
+ @include deprecated.menu-shadow;
+
display: grid;
width: deprecated.$s-240;
padding: deprecated.$s-4;
@@ -45,16 +46,21 @@
cursor: pointer;
.title {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--menu-foreground-color);
}
+
.shortcut {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
gap: deprecated.$s-2;
color: var(--menu-shortcut-foreground-color);
+
.shortcut-key {
- @include deprecated.bodySmallTypography;
- @include deprecated.flexCenter;
+ @include deprecated.body-small-typography;
+ @include deprecated.flex-center;
+
height: deprecated.$s-20;
padding: deprecated.$s-2 deprecated.$s-6;
border-radius: deprecated.$br-6;
@@ -63,19 +69,23 @@
}
.submenu-icon svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--menu-foreground-color);
}
&:hover {
background-color: var(--menu-background-color-hover);
+
.title {
color: var(--menu-foreground-color-hover);
}
+
.shortcut {
color: var(--menu-shortcut-foreground-color-hover);
}
}
+
&:focus {
border: 1px solid var(--menu-border-color-focus);
background-color: var(--menu-background-color-focus);
@@ -89,6 +99,7 @@
height: deprecated.$s-28;
padding: deprecated.$s-6;
border-radius: deprecated.$br-8;
+
&:hover {
background-color: var(--menu-background-color-hover);
}
@@ -99,15 +110,18 @@
.selected-icon {
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--menu-foreground-color);
}
}
.shape-icon {
margin-left: deprecated.$s-2;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--menu-foreground-color);
}
}
@@ -124,3 +138,30 @@
pointer-events: none;
opacity: 0.6;
}
+
+.guide-color-label {
+ cursor: default;
+ pointer-events: none;
+}
+
+.guide-color-swatches {
+ display: flex;
+ flex-wrap: wrap;
+ gap: deprecated.$s-6;
+ padding: deprecated.$s-4 deprecated.$s-6 deprecated.$s-8;
+ list-style: none;
+}
+
+.guide-color-swatch {
+ width: deprecated.$s-20;
+ height: deprecated.$s-20;
+ border-radius: 50%;
+ cursor: pointer;
+ flex-shrink: 0;
+ box-sizing: border-box;
+ border: deprecated.$s-2 solid var(--panel-border-color);
+
+ &.selected {
+ border: deprecated.$s-2 solid var(--menu-foreground-color);
+ }
+}
diff --git a/frontend/src/app/main/ui/workspace/coordinates.scss b/frontend/src/app/main/ui/workspace/coordinates.scss
index b338144d06..44c76da73e 100644
--- a/frontend/src/app/main/ui/workspace/coordinates.scss
+++ b/frontend/src/app/main/ui/workspace/coordinates.scss
@@ -11,7 +11,7 @@ $width-settings-bar: 256px;
.container {
background-color: var(--color-background-primary);
border-radius: deprecated.$br-4;
- bottom: 0px;
+ bottom: 0;
padding: deprecated.$s-2 deprecated.$s-8;
position: fixed;
right: calc(#{$width-settings-bar} + #{deprecated.$s-24});
diff --git a/frontend/src/app/main/ui/workspace/left_header.scss b/frontend/src/app/main/ui/workspace/left_header.scss
index 5ecac0793c..a0cd2153e5 100644
--- a/frontend/src/app/main/ui/workspace/left_header.scss
+++ b/frontend/src/app/main/ui/workspace/left_header.scss
@@ -14,11 +14,13 @@
}
.main-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-32;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin-right: deprecated.$s-4;
+
svg {
min-height: deprecated.$s-32;
width: deprecated.$s-32;
@@ -37,8 +39,9 @@
.project-name,
.file-name {
- @include deprecated.uppercaseTitleTipography;
- @include deprecated.textEllipsis;
+ @include deprecated.uppercase-title-typography;
+ @include deprecated.text-ellipsis;
+
height: deprecated.$s-16;
width: 100%;
padding-bottom: deprecated.$s-2;
@@ -47,7 +50,8 @@
}
.file-name {
- @include deprecated.smallTitleTipography;
+ @include deprecated.small-title-typography;
+
text-transform: none;
color: var(--title-foreground-color-hover);
align-items: center;
@@ -56,11 +60,12 @@
}
.file-name-label {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
}
.file-name-input {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: 100%;
margin: 0;
border: 0;
@@ -71,16 +76,19 @@
color: var(--input-foreground-color);
z-index: deprecated.$z-index-20;
white-space: break-spaces;
+
&:focus {
outline: none;
}
}
.shared-badge {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-16;
height: deprecated.$s-32;
margin-right: deprecated.$s-4;
+
svg {
stroke: var(--button-secondary-foreground-color-rest);
fill: none;
@@ -119,9 +127,11 @@
0% {
transform: translateY(0);
}
+
50% {
transform: translateY(-4px);
}
+
100% {
transform: translateY(0);
}
diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs
index 08dad36701..00834db2ca 100644
--- a/frontend/src/app/main/ui/workspace/libraries.cljs
+++ b/frontend/src/app/main/ui/workspace/libraries.cljs
@@ -32,6 +32,7 @@
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.context :as ctx]
+ [app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
@@ -47,12 +48,6 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
-(def ^:private close-icon
- (deprecated-icon/icon-xref :close (stl/css :close-icon)))
-
-(def ^:private add-icon
- (deprecated-icon/icon-xref :add (stl/css :add-icon)))
-
(defn- get-library-summary
"Given a library data return a summary representation of this library"
[data]
@@ -169,12 +164,12 @@
[:div {:class (stl/css :sample-library-item)
:key (dm/str id)}
[:div {:class (stl/css :sample-library-item-name)} (:name library)]
- [:input {:class (stl/css-case :sample-library-button true
- :sample-library-add (nil? importing?)
- :sample-library-adding (some? importing?))
- :type "button"
- :value (if (= importing? id) (tr "labels.adding") (tr "labels.add"))
- :on-click import-library}]]))
+
+ [:> button* {:variant "secondary"
+ :disabled (some? importing?)
+ :on-click import-library
+ :class (stl/css :sample-library-button)}
+ (if (= importing? id) (tr "labels.adding") (tr "labels.add"))]]))
(defn- empty-library?
"Check if currentt library summary has elements or not"
@@ -338,31 +333,31 @@
[:div {:class (stl/css :section-list-item)}
[:div {:class (stl/css :item-content)}
- [:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")]
+ [:div {:class (stl/css :item-title)} (tr "workspace.libraries.file-library")]
[:ul {:class (stl/css :item-contents)}
[:> library-description* {:summary summary}]]]
(if ^boolean is-shared
- [:input {:class (stl/css :item-unpublish)
- :type "button"
- :value (tr "common.unpublish")
- :on-click unpublish}]
- [:input {:class (stl/css :item-publish)
- :type "button"
- :value (tr "common.publish")
- :on-click publish}])]
+ [:> button* {:variant "secondary"
+ :type "button"
+ :on-click unpublish}
+ (tr "common.unpublish")]
+
+ [:> button* {:variant "primary"
+ :type "button"
+ :on-click publish}
+ (tr "common.publish")])]
(for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries]
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
has-tokens? (and (has-tokens? library)
(contains? cf/flags :token-import-from-library))]
- [:div {:class (if has-tokens?
- (stl/css :section-list-item-double-icon)
- (stl/css :section-list-item))
+ [:div {:class (stl/css :section-list-item)
:key (dm/str id)
:data-testid "library-item"}
[:div {:class (stl/css :item-content)}
- [:div {:class (stl/css :item-name)} name]
+ [:div {:class (stl/css-case :item-name true
+ :item-name-short has-tokens?)} name]
[:ul {:class (stl/css :item-contents)}
(let [summary (get-library-summary data)]
[:*
@@ -372,23 +367,23 @@
[:span "(" (tr "workspace.libraries.connected-to") " "]
[:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)]
[:span ")"]])])]]
+ [:div {:class (stl/css :library-actions)}
+ (when ^boolean has-tokens?
+ [:> icon-button*
+ {:type "button"
+ :aria-label (tr "workspace.tokens.import-tokens")
+ :icon i/import-export
+ :data-library-id (dm/str id)
+ :variant "secondary"
+ :on-click import-tokens}])
- (when ^boolean has-tokens?
- [:> icon-button*
- {:type "button"
- :aria-label (tr "workspace.tokens.import-tokens")
- :icon i/import-export
- :data-library-id (dm/str id)
- :variant "secondary"
- :on-click import-tokens}])
-
- [:> icon-button* {:type "button"
- :aria-label (tr "workspace.libraries.unlink-library-btn")
- :icon i/detach
- :data-library-id (dm/str id)
- :variant "secondary"
- :disabled disabled?
- :on-click unlink-library}]]))]]
+ [:> icon-button* {:type "button"
+ :aria-label (tr "workspace.libraries.unlink-library-btn")
+ :icon i/detach
+ :data-library-id (dm/str id)
+ :variant "secondary"
+ :disabled disabled?
+ :on-click unlink-library}]]]))]]
[:div {:class (stl/css :shared-section)}
[:> title-bar* {:collapsable false
@@ -412,11 +407,12 @@
(adapt-backend-summary))]
[:> library-description* {:summary summary}])]]
- [:button {:class (stl/css :item-button-shared)
- :data-library-id (dm/str id)
- :title (tr "workspace.libraries.shared-library-btn")
- :on-click link-library}
- add-icon]])]
+ [:> icon-button* {:class (stl/css :item-button-shared)
+ :variant "secondary"
+ :data-library-id (dm/str id)
+ :icon "add"
+ :aria-label (tr "workspace.libraries.shared-library-btn")
+ :on-click link-library}]])]
(when (empty? shared-libraries)
[:div {:class (stl/css :section-list-empty)}
@@ -437,6 +433,7 @@
(for [library sample-libraries]
[:> sample-library-entry*
{:library library
+ :key (dm/str (:id library))
:importing importing*}])]]
:else
@@ -536,17 +533,17 @@
[:div {:class (stl/css :section-list-item)
:key (dm/str id)}
[:div {:class (stl/css :item-content)}
- [:div {:class (stl/css :item-name)} name]
+ [:div {:class (stl/css :item-name-long)} name]
[:ul {:class (stl/css :item-contents)} (describe-library
(count components)
0
(count colors)
(count typographies))]]
- [:button {:type "button"
- :class (stl/css :item-update)
- :disabled updating?
- :data-library-id (dm/str id)
- :on-click update}
+ [:> button* {:class (stl/css :item-update)
+ :disabled updating?
+ :variant "primary"
+ :data-library-id (dm/str id)
+ :on-click update}
(tr "workspace.libraries.update")]
[:div {:class (stl/css :libraries-updates)}
@@ -680,11 +677,11 @@
:on-click close-dialog-outside
:data-testid "libraries-modal"}
[:div {:class (stl/css :modal-dialog)}
- [:button {:class (stl/css :close-btn)
- :on-click close-dialog
- :aria-label (tr "labels.close")
- :data-testid "close-libraries"}
- close-icon]
+ [:> icon-button* {:class (stl/css :close-btn)
+ :on-click close-dialog
+ :aria-label (tr "labels.close")
+ :variant "ghost"
+ :icon i/close}]
[:div {:class (stl/css :modal-title)}
(tr "workspace.libraries.libraries")]
@@ -756,5 +753,6 @@
"created in your files previously to this new version."]]]
[:div {:class (stl/css :info-bottom)}
- [:button {:class (stl/css :primary-button)
- :on-click handle-gotit-click} "I GOT IT"]]]]]))
+ [:> button* {:class (stl/css :primary-button)
+ :variant "primary"
+ :on-click handle-gotit-click} "I GOT IT"]]]]]))
diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss
index 978c2cbcba..e8a5d9a309 100644
--- a/frontend/src/app/main/ui/workspace/libraries.scss
+++ b/frontend/src/app/main/ui/workspace/libraries.scss
@@ -4,7 +4,6 @@
//
// Copyright (c) KALEIDOS INC
-@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/_utils.scss" as *;
@@ -33,7 +32,7 @@
background-color: var(--modal-background-color);
border: $b-2 solid var(--modal-border-color);
display: grid;
- grid-template-rows: auto 1fr;
+ grid-template-rows: 0 auto 1fr;
min-width: $sz-364;
min-height: $sz-192;
height: $sz-520;
@@ -42,25 +41,9 @@
max-width: $sz-712;
}
-// TODO: Remove this extended creating modal component
-.close-btn {
- @extend .modal-close-btn-base;
-}
-
-.close-icon {
- display: flex;
- justify-content: center;
- align-items: center;
- height: $sz-16;
- width: $sz-16;
- color: transparent;
- fill: none;
- stroke-width: $b-1;
- stroke: var(--icon-foreground);
-}
-
.modal-title {
@include t.use-typography("headline-medium");
+
margin-block-end: var(--sp-l);
color: var(--color-foreground-primary);
}
@@ -81,12 +64,6 @@
display: grid;
grid-template-rows: auto 1fr;
gap: var(--sp-s);
-
- .section-list {
- .section-list-item:first-child {
- border: none;
- }
- }
}
.shared-section {
@@ -107,7 +84,7 @@
overflow-y: auto;
}
-.section-list-item {
+%section-list-item-placeholder {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--sp-s);
@@ -116,8 +93,17 @@
border-radius: $br-8;
}
+.section-list-item {
+ @extend %section-list-item-placeholder;
+
+ &:first-child {
+ border: none;
+ }
+}
+
.section-list-item-double-icon {
- @extend .section-list-item;
+ @extend %section-list-item-placeholder;
+
grid-template-columns: 1fr auto auto;
}
@@ -125,44 +111,10 @@
height: fit-content;
}
-.item-publish,
-.item-unpublish {
- // TODO: remove this extended by using DS button component
- @extend .button-primary;
- @include t.use-typography("headline-small");
- height: $sz-32;
- min-width: px2rem(92);
- padding: var(--sp-s) var(--sp-xxl);
- margin: 0;
- border-radius: $br-8;
-}
-
-.item-unpublish {
- // TODO: remove this extended by using DS button component
- @extend .button-secondary;
-}
-
-.item-button,
-.item-button-shared {
- // TODO: remove this extended by using DS button component
- @extend .button-secondary;
- height: $sz-32;
- width: $sz-32;
- margin-inline-start: var(--sp-xxs);
- padding: var(--sp-s);
-}
-
-.detach-icon,
-.add-icon {
- display: flex;
- justify-content: center;
- align-items: center;
- height: $sz-16;
- width: $sz-16;
- color: transparent;
- fill: none;
- stroke-width: $b-1;
- stroke: var(--icon-foreground);
+.close-btn {
+ position: absolute;
+ inset-block-start: var(--sp-s);
+ inset-inline-end: var(--sp-s);
}
.section-list-shared {
@@ -171,30 +123,11 @@
.section-title {
@include t.use-typography("headline-small");
+
margin-block-end: var(--sp-m);
color: var(--title-foreground-color);
}
-.search-icon {
- display: flex;
- justify-content: center;
- align-items: center;
- width: px2rem(20);
- padding: 0 0 0 var(--sp-s);
-
- svg {
- display: flex;
- justify-content: center;
- align-items: center;
- color: transparent;
- fill: none;
- height: px2rem(12);
- width: px2rem(12);
- stroke-width: 1.33px;
- stroke: var(--icon-foreground);
- }
-}
-
// empty state
.section-list-empty {
display: grid;
@@ -206,21 +139,13 @@
margin-block: var(--sp-l);
}
-.library-icon {
- display: flex;
- justify-content: center;
- align-items: center;
- color: transparent;
- fill: none;
- stroke-width: $b-1;
- stroke: var(--icon-foreground);
- height: $sz-32;
- width: $sz-32;
-}
-
// Update library tab
.libraries-updates-see-all {
- @extend .link;
+ background: unset;
+ border: none;
+ color: var(--link-foreground-color);
+ cursor: pointer;
+ text-decoration: none;
direction: rtl;
grid-column: span 3;
margin-block-start: var(--sp-s);
@@ -236,7 +161,7 @@
display: grid;
grid-column: span 3;
grid-template-columns: repeat(auto-fill, minmax(px2rem(160), 1fr));
- gap: deprecated.$s-24;
+ gap: var(--sp-xxl);
margin-block-start: var(--sp-l);
}
@@ -246,7 +171,8 @@
}
.libraries-updates-item {
- @include deprecated.bodyLargeTypography;
+ @include t.use-typography("body-large");
+
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
@@ -273,36 +199,60 @@
padding-inline-start: calc(var(--sp-xxl) + var(--sp-s));
}
-.item-name {
+%item-name {
@include t.use-typography("body-large");
- @include textEllipsis;
+ @include text-ellipsis;
+
+ margin: 0;
+ max-width: px2rem(236);
+ color: var(--library-name-foreground-color);
+}
+
+.item-name {
+ @extend %item-name;
+}
+
+.item-name-short {
+ max-width: px2rem(206);
+}
+
+.item-name-long {
+ @extend %item-name;
+
+ max-width: px2rem(450);
+}
+
+.item-title {
+ @include t.use-typography("body-large");
+
margin: 0;
- max-width: px2rem(216);
color: var(--library-name-foreground-color);
}
.item-update {
- @extend .button-primary;
@include t.use-typography("headline-small");
+
height: $sz-32;
min-width: px2rem(92);
padding: var(--sp-s) var(--sp-xxl);
margin-inline-end: var(--sp-xxs);
border-radius: $br-8;
-
- &:disabled {
- @extend .button-disabled;
- }
}
.item-contents {
@include t.use-typography("body-small");
+
color: var(--library-content-foreground-color);
display: flex;
flex-wrap: wrap;
margin: 0;
}
+.library-actions {
+ display: flex;
+ gap: var(--sp-xs);
+}
+
.element-count {
white-space: nowrap;
@@ -329,6 +279,7 @@
.modal-v2-title {
@include t.use-typography("headline-medium");
+
color: var(--modal-title-foreground-color);
}
@@ -341,11 +292,10 @@
.info-block {
display: grid;
- grid-template-columns: auto 1fr;
column-gap: var(--sp-xl);
grid-template:
- "icon title"
- "icon content";
+ "icon title" auto
+ "icon content" auto / auto 1fr;
}
.info-icon {
@@ -368,12 +318,14 @@
.info-block-title {
@include t.use-typography("body-large");
+
grid-area: title;
color: var(--modal-title-foreground-color);
}
.info-block-content {
@include t.use-typography("body-medium");
+
grid-area: content;
color: var(--library-content-foreground-color);
}
@@ -386,13 +338,14 @@
}
.primary-button {
- @extend .button-primary;
@include t.use-typography("headline-small");
+
padding: 0 var(--sp-l);
}
.sample-libraries-info {
@include t.use-typography("body-small");
+
display: flex;
flex-direction: column;
margin: var(--sp-xxxl);
@@ -401,6 +354,7 @@
.sample-libraries-link {
@include t.use-typography("body-small");
+
color: var(--color-accent-primary);
&:hover {
@@ -410,6 +364,7 @@
.sample-libraries-container {
@include t.use-typography("body-small");
+
display: flex;
flex-direction: column;
width: 100%;
@@ -427,6 +382,7 @@
.sample-library-item-name {
@include t.use-typography("body-medium");
+
color: var(--color-foreground-primary);
white-space: nowrap;
overflow: hidden;
@@ -434,18 +390,9 @@
max-width: px2rem(232);
}
-// TODO: Remove this extended using a DS component
-.sample-library-add {
- @extend .button-secondary;
-}
-
-// TODO: Remove this extended using a DS component
-.sample-library-adding {
- @extend .button-disabled;
-}
-
.sample-library-button {
@include t.use-typography("headline-small");
+
height: $sz-32;
width: px2rem(80);
margin: 0;
diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs
index 367976be10..ac3a44b57b 100644
--- a/frontend/src/app/main/ui/workspace/main_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/main_menu.cljs
@@ -389,6 +389,17 @@
(tr "workspace.header.menu.show-guides"))]
[:> shortcuts* {:id :toggle-guides}]]
+ [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
+ :on-click toggle-flag
+ :on-key-down (fn [event]
+ (when (kbd/enter? event)
+ (toggle-flag event)))
+ :data-testid "lock-guides"
+ :id "file-menu-lock-guides"}
+ [:span {:class (stl/css :item-name)}
+ (if (contains? layout :lock-guides)
+ (tr "workspace.header.menu.unlock-guides")
+ (tr "workspace.header.menu.lock-guides"))]]
(when-not ^boolean read-only?
[:*
@@ -463,6 +474,12 @@
(mf/use-fn
#(st/emit! (dw/select-all)))
+ find
+ (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find))))
+
+ find-and-replace
+ (mf/use-fn (fn [] (on-close) (st/emit! (dw/open-layers-search :find-and-replace))))
+
undo
(mf/use-fn
#(st/emit! dwu/undo))
@@ -485,6 +502,20 @@
(tr "workspace.header.menu.select-all")]
[:> shortcuts* {:id :select-all}]]
+ [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
+ :on-click find
+ :on-key-down (fn [event] (when (kbd/enter? event) (find event)))
+ :id "file-menu-find"}
+ [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.find")]
+ [:> shortcuts* {:id :find}]]
+
+ [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
+ :on-click find-and-replace
+ :on-key-down (fn [event] (when (kbd/enter? event) (find-and-replace event)))
+ :id "file-menu-find-and-replace"}
+ [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.find-and-replace")]
+ [:> shortcuts* {:id :find-and-replace}]]
+
(when can-edit
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
:on-click undo
diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss
index 1b12e2cdbf..6f615a7263 100644
--- a/frontend/src/app/main/ui/workspace/main_menu.scss
+++ b/frontend/src/app/main/ui/workspace/main_menu.scss
@@ -72,13 +72,13 @@
&.plugins {
max-height: calc(100vh - $sz-200);
- overflow-x: hidden;
- overflow-y: auto;
+ overflow: hidden auto;
}
}
.base-menu-item {
@include t.use-typography("body-small");
+
display: grid;
align-items: center;
grid-template-columns: auto $sz-16 $sz-16;
@@ -99,6 +99,7 @@
&.disabled {
--menu-foreground-color: var(--color-foreground-secondary);
+
pointer-events: none;
}
}
@@ -122,6 +123,7 @@
.item-indicator {
--menu-indicator-color: var(--color-foreground-secondary);
+
grid-area: indicator;
display: flex;
align-items: center;
@@ -171,6 +173,7 @@
.shortcut-key {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
justify-content: center;
diff --git a/frontend/src/app/main/ui/workspace/nudge.scss b/frontend/src/app/main/ui/workspace/nudge.scss
index 9ed244bedd..8b62ca7f10 100644
--- a/frontend/src/app/main/ui/workspace/nudge.scss
+++ b/frontend/src/app/main/ui/workspace/nudge.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
min-width: deprecated.$s-408;
}
@@ -20,30 +21,37 @@
}
.modal-title {
- @include deprecated.headlineMediumTypography;
+ @include deprecated.headline-medium-typography;
+
color: var(--modal-title-foreground-color);
}
+
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
gap: deprecated.$s-24;
- @include deprecated.bodyLargeTypography;
+
+ @include deprecated.body-large-typography;
+
margin-bottom: deprecated.$s-24;
}
.input-wrapper {
- @extend .input-with-label;
- @include deprecated.bodySmallTypography;
+ @extend %input-with-label;
+ @include deprecated.body-small-typography;
+
label {
text-transform: none;
}
}
.modal-msg {
- @include deprecated.bodyLargeTypography;
+ @include deprecated.body-large-typography;
+
color: var(--modal-text-foreground-color);
line-height: 1.5;
}
diff --git a/frontend/src/app/main/ui/workspace/palette.scss b/frontend/src/app/main/ui/workspace/palette.scss
index 7dc42ffc37..53201a64bd 100644
--- a/frontend/src/app/main/ui/workspace/palette.scss
+++ b/frontend/src/app/main/ui/workspace/palette.scss
@@ -7,7 +7,6 @@
@use "ds/spacing.scss" as *;
@use "ds/z-index.scss" as *;
@use "ds/_sizes.scss" as *;
-
@use "refactor/common-refactor.scss" as deprecated;
.palette-wrapper {
@@ -30,11 +29,7 @@
right: 0;
grid-area: color-palette;
display: grid;
- grid-template-areas:
- "resize resize resize"
- "buttons actions palette";
- grid-template-rows: deprecated.$s-8 1fr;
- grid-template-columns: deprecated.$s-32 auto 1fr;
+ grid-template: "resize resize resize" deprecated.$s-8 "buttons actions palette" 1fr / deprecated.$s-32 auto 1fr;
max-height: deprecated.$s-80;
height: var(--height);
width: fit-content;
@@ -46,6 +41,7 @@
right 0.3s,
opacity 0.2s,
width 0.3s;
+
&.wide {
width: 100%;
}
@@ -59,6 +55,7 @@
cursor: ns-resize;
background-color: var(--palette-background-color);
}
+
.palette-btn-list {
grid-area: buttons;
background-color: var(--palette-background-color);
@@ -68,35 +65,44 @@
list-style: none;
z-index: deprecated.$z-index-2;
gap: deprecated.$s-2;
+
&.mid-palette,
&.small-palette {
display: flex;
}
+
.palette-item {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
border-radius: deprecated.$br-8;
opacity: deprecated.$op-10;
transition: opacity 1s ease;
+
.palette-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
border-radius: deprecated.$br-8;
background-clip: padding-box;
padding: 0;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
}
}
.palette-actions {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
grid-area: actions;
height: calc(var(--height) - deprecated.$s-16);
width: deprecated.$s-32;
@@ -105,11 +111,14 @@
border-radius: deprecated.$br-8;
background-color: var(--palette-background-color);
z-index: deprecated.$z-index-2;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
+
.palette {
grid-area: palette;
width: 100%;
@@ -118,10 +127,12 @@
}
.handler {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
width: deprecated.$s-12;
height: 100%;
+
.handler-btn {
width: deprecated.$s-4;
height: 100%;
@@ -147,29 +158,35 @@
border-inline-start: 0;
border-start-start-radius: 0;
border-end-start-radius: 0;
+
.palette-btn-list {
opacity: deprecated.$op-0;
visibility: hidden;
width: 0;
+
.palette-item {
opacity: deprecated.$op-0;
visibility: hidden;
z-index: 0;
}
}
+
.resize-area {
visibility: hidden;
z-index: 0;
width: 0;
}
+
.palette-actions {
visibility: hidden;
z-index: 0;
}
+
.palette {
visibility: hidden;
z-index: 0;
}
+
.handler {
padding-bottom: deprecated.$s-8;
}
@@ -179,21 +196,26 @@
.help-btn {
z-index: var(--z-index-panels);
flex-shrink: 0;
- @extend .button-secondary;
+
+ @extend %button-secondary;
+
inline-size: $sz-40;
block-size: $sz-40;
border-radius: deprecated.$br-circle;
border: none;
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
+
&:hover {
border: none;
}
}
.icon-help {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
inline-size: var(--sp-xxl);
block-size: var(--sp-xxl);
diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs
index fc319bf925..f4068c585a 100644
--- a/frontend/src/app/main/ui/workspace/plugins.cljs
+++ b/frontend/src/app/main/ui/workspace/plugins.cljs
@@ -302,7 +302,20 @@
[:div {:class (stl/css :permissions-list-entry)}
deprecated-icon/oauth-1
[:p {:class (stl/css :permissions-list-text)}
- (tr "workspace.plugins.permissions.allow-localstorage")]])])
+ (tr "workspace.plugins.permissions.allow-localstorage")]])
+
+ (cond
+ (contains? permissions "clipboard:write")
+ [:div {:class (stl/css :permissions-list-entry)}
+ deprecated-icon/oauth-1
+ [:p {:class (stl/css :permissions-list-text)}
+ (tr "workspace.plugins.permissions.clipboard-write")]]
+
+ (contains? permissions "clipboard:read")
+ [:div {:class (stl/css :permissions-list-entry)}
+ deprecated-icon/oauth-1
+ [:p {:class (stl/css :permissions-list-text)}
+ (tr "workspace.plugins.permissions.clipboard-read")]])])
(mf/defc plugins-permissions-dialog
{::mf/register modal/components
diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss
index 87af034d70..06d774ded8 100644
--- a/frontend/src/app/main/ui/workspace/plugins.scss
+++ b/frontend/src/app/main/ui/workspace/plugins.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
display: grid;
grid-template-rows: auto 1fr auto;
max-height: initial;
@@ -41,16 +42,18 @@
}
.close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.close-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
.modal-title {
- @include deprecated.headlineMediumTypography;
+ @include deprecated.headline-medium-typography;
+
margin-block-end: deprecated.$s-32;
color: var(--modal-title-foreground-color);
display: flex;
@@ -82,8 +85,9 @@
}
.primary-button {
- @extend .button-primary;
- @include deprecated.headlineSmallTypography;
+ @extend %button-primary;
+ @include deprecated.headline-small-typography;
+
padding: deprecated.$s-0 deprecated.$s-16;
}
@@ -93,18 +97,21 @@
}
.cancel-button {
- @extend .button-secondary;
- @include deprecated.headlineSmallTypography;
+ @extend %button-secondary;
+ @include deprecated.headline-small-typography;
+
padding: deprecated.$s-0 deprecated.$s-16;
}
.search-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-20;
padding: 0 0 0 deprecated.$s-8;
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
@@ -126,8 +133,7 @@
.plugins-list {
padding-top: deprecated.$s-20;
- overflow-x: hidden;
- overflow-y: auto;
+ overflow: hidden auto;
flex: 1;
display: flex;
flex-direction: column;
@@ -157,12 +163,14 @@
}
.plugin-title {
- @include deprecated.bodyMediumTypography;
+ @include deprecated.body-medium-typography;
+
color: var(--color-foreground-primary);
}
.plugin-summary {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--color-foreground-secondary);
}
@@ -194,7 +202,8 @@
}
.plugins-empty-text {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--color-foreground-primary);
}
@@ -203,7 +212,8 @@ div.input-error {
}
.info {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-top: deprecated.$s-4;
&.error {
@@ -231,9 +241,6 @@ div.input-error {
}
}
-.plugin-permissions {
-}
-
.permissions-list {
display: flex;
flex-direction: column;
@@ -255,13 +262,15 @@ div.input-error {
}
.permissions-list-text {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin: 0;
color: var(--color-foreground-secondary);
}
.permissions-disclaimer {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
padding: deprecated.$s-16;
background: var(--color-background-quaternary);
color: var(--color-foreground-primary);
@@ -274,7 +283,8 @@ div.input-error {
}
.discover {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--color-foreground-secondary);
margin-top: deprecated.$s-24;
diff --git a/frontend/src/app/main/ui/workspace/presence.scss b/frontend/src/app/main/ui/workspace/presence.scss
index 03f6c8134e..76f488e1f2 100644
--- a/frontend/src/app/main/ui/workspace/presence.scss
+++ b/frontend/src/app/main/ui/workspace/presence.scss
@@ -12,7 +12,6 @@
.active-users-opened {
background: none;
cursor: pointer;
-
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
@@ -33,6 +32,7 @@
%user-icon {
@include t.use-typography("body-small");
+
display: grid;
place-content: center;
height: $sz-24;
@@ -48,6 +48,7 @@
.users-num {
@extend %user-icon;
+
background-color: var(--user-count-background-color);
color: var(--user-count-foreground-color);
z-index: 3; // FIXME: this is hardcoded because of the way its component uses z-index from cljs
@@ -57,6 +58,7 @@
.session-icon {
@extend %user-icon;
+
margin-inline-start: var(--user-list-inline-margin, calc(-1 * var(--sp-xs)));
}
diff --git a/frontend/src/app/main/ui/workspace/right_header.cljs b/frontend/src/app/main/ui/workspace/right_header.cljs
index c73b13e4eb..c719aae349 100644
--- a/frontend/src/app/main/ui/workspace/right_header.cljs
+++ b/frontend/src/app/main/ui/workspace/right_header.cljs
@@ -111,10 +111,8 @@
;; --- Header Component
(mf/defc right-header*
- [{:keys [file layout page-id]}]
- (let [file-id (:id file)
-
- threads-map (mf/deref refs/comment-threads)
+ [{:keys [file-id layout page-id]}]
+ (let [threads-map (mf/deref refs/comment-threads)
zoom (mf/deref refs/selected-zoom)
read-only? (mf/use-ctx ctx/workspace-read-only?)
diff --git a/frontend/src/app/main/ui/workspace/right_header.scss b/frontend/src/app/main/ui/workspace/right_header.scss
index e6d7ea2092..50cee33d7a 100644
--- a/frontend/src/app/main/ui/workspace/right_header.scss
+++ b/frontend/src/app/main/ui/workspace/right_header.scss
@@ -28,7 +28,8 @@
}
.zoom-widget {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
align-items: center;
justify-content: center;
@@ -38,7 +39,8 @@
border-radius: deprecated.$br-8;
.label {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
height: 100%;
padding: deprecated.$s-8 0;
color: var(--button-tertiary-foreground-color-rest);
@@ -58,7 +60,8 @@
}
.dropdown {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
right: deprecated.$s-2;
top: calc(deprecated.$s-2 + deprecated.$s-48);
width: deprecated.$s-272;
@@ -76,7 +79,8 @@
}
.zoom-text {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
min-width: deprecated.$s-48;
padding: 0;
@@ -85,20 +89,21 @@
}
.reset-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
color: var(--button-tertiary-foreground-color-hover);
height: deprecated.$s-28;
border-radius: deprecated.$br-8;
}
.zoom-option {
- @extend .menu-item-base;
+ @extend %menu-item-base;
.shortcuts {
- @extend .shortcut-base;
+ @extend %shortcut-base;
.shortcut-key {
- @extend .shortcut-key-base;
+ @extend %shortcut-key-base;
}
}
@@ -114,7 +119,8 @@
}
.comments-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
border-radius: deprecated.$br-8;
margin: 0;
height: deprecated.$s-28;
@@ -122,7 +128,8 @@
border: none;
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
height: deprecated.$s-16;
width: deprecated.$s-16;
@@ -143,7 +150,8 @@
}
.history-button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
border-radius: deprecated.$br-8;
margin: 0;
height: deprecated.$s-28;
@@ -151,7 +159,8 @@
border: none;
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
height: deprecated.$s-16;
width: deprecated.$s-16;
@@ -172,20 +181,23 @@
}
.persistence-status-widget {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-28;
height: deprecated.$s-28;
}
.status-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-24;
height: deprecated.$s-24;
margin: 0;
border-radius: deprecated.$br-circle;
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--status-widget-icon-foreground-color);
}
}
@@ -213,7 +225,8 @@
.share-btn,
.viewer-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
border-radius: deprecated.$br-8;
margin: 0;
width: deprecated.$s-28;
@@ -221,7 +234,8 @@
border: none;
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
stroke: var(--icon-foreground);
@@ -239,7 +253,7 @@
height: 8px;
border: 2px solid var(--color-background-tertiary);
border-radius: 50%;
- background: red;
+ background: var(--color-foreground-error);
top: 6px;
right: 6px;
}
diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss
index d31fa44ef0..21a3db0497 100644
--- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss
+++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss
@@ -8,7 +8,6 @@
.text-editor-container {
height: 100%;
position: relative;
-
cursor: text;
}
@@ -18,21 +17,17 @@
.text-editor-content {
height: 100%;
- font-family: sourcesanspro;
-
+ font-family: "sourcesanspro", sans-serif;
outline: none;
user-select: text;
white-space: pre-wrap;
overflow-wrap: break-word;
-
caret-color: var(--text-editor-caret-color);
-
color: transparent;
// Match Skia's text layout precision: prevent browser text-size
// adjustments and ensure consistent kerning across browsers.
text-size-adjust: none;
- -webkit-text-size-adjust: none;
font-kerning: normal;
&::selection,
@@ -41,16 +36,16 @@
-webkit-text-fill-color: transparent; // WebKit/Safari
}
- &::-moz-selection,
- *::-moz-selection {
+ &::selection,
+ *::selection {
color: transparent;
}
[data-itype="paragraph"] {
line-height: inherit;
user-select: text;
- margin: 0px;
- font-size: 0px;
+ margin: 0;
+ font-size: 0;
}
// Text spans emitted by @penpot/text-editor use `data-itype="span"`.
@@ -61,11 +56,9 @@
display: inline;
line-height: inherit;
caret-color: var(--text-editor-caret-color);
- white-space-collapse: pre;
word-break: normal;
overflow-wrap: break-word;
tab-size: 2;
- -o-tab-size: 2;
}
[data-itype="root"] {
diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss
index 8539a7ca29..5659b0646e 100644
--- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss
+++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.scss
@@ -6,7 +6,6 @@
width: 100%;
height: 100%;
position: absolute;
-
opacity: 0;
overflow: hidden;
white-space: pre;
diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs
index ac219faa2f..b1bfe74db9 100644
--- a/frontend/src/app/main/ui/workspace/sidebar.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar.cljs
@@ -42,6 +42,7 @@
[app.main.ui.workspace.sidebar.versions :refer [versions-toolbox*]]
[app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab*]]
[app.util.debug :as dbg]
+ [app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
@@ -183,6 +184,7 @@
:data-testid "left-sidebar"
:data-width (str width)
:class aside-class
+ :on-context-menu dom/prevent-default-context-menu
:style {:--left-sidebar-width (dm/str width "px")}}
[:> left-header* {:file file
@@ -280,7 +282,7 @@
[:> history-toolbox*]])]))
(mf/defc right-sidebar*
- [{:keys [layout section file page-id drawing-tool active-tokens] :as props}]
+ [{:keys [layout section file-id page-id drawing-tool active-tokens] :as props}]
(let [is-comments? (= drawing-tool :comments)
is-history? (contains? layout :document-history)
is-inspect? (= section :inspect)
@@ -329,6 +331,7 @@
:id "right-sidebar-aside"
:data-testid "right-sidebar"
:data-size (str width)
+ :on-context-menu dom/prevent-default-context-menu
:style {:--right-sidebar-width (if can-be-expanded?
(dm/str width "px")
(dm/str right-sidebar-default-width "px"))}}
@@ -340,7 +343,7 @@
:on-pointer-move on-pointer-move}])
[:> right-header*
- {:file file
+ {:file-id file-id
:layout layout
:page-id page-id}]
@@ -372,14 +375,26 @@
(ctob/get-tokens-in-active-sets tokens-lib)
{}))
+ selected-token-set-id
+ (mf/deref refs/selected-token-set-id)
+
+ active-tokens-force-set
+ (mf/with-memo [tokens-lib selected-token-set-id]
+ (if (and tokens-lib selected-token-set-id)
+ (ctob/get-tokens-in-active-sets-force tokens-lib selected-token-set-id)
+ {}))
+
tokenscript? (contains? cf/flags :tokenscript)
- tokenscript-resolved-active-tokens
- (mf/with-memo [tokens-lib tokenscript?]
- (when tokenscript? (ts/resolve-tokens active-tokens)))
-
resolved-active-tokens
- (sd/use-resolved-tokens* active-tokens)]
+ (sd/use-resolved-tokens* active-tokens)
+
+ tokenscript-resolved-active-tokens-force-set
+ (mf/with-memo [active-tokens-force-set tokenscript?]
+ (when tokenscript? (ts/resolve-tokens active-tokens-force-set)))
+
+ resolved-active-tokens-force-set
+ (sd/use-resolved-tokens* active-tokens-force-set)]
[:*
(if (:collapse-left-sidebar layout)
@@ -388,10 +403,10 @@
:file file
:page-id page-id
:tokens-lib tokens-lib
- :active-tokens active-tokens
- :resolved-active-tokens (if (contains? cf/flags :tokenscript)
- tokenscript-resolved-active-tokens
- resolved-active-tokens)}])
+ :active-tokens active-tokens-force-set
+ :resolved-active-tokens (if tokenscript?
+ tokenscript-resolved-active-tokens-force-set
+ resolved-active-tokens-force-set)}])
[:> right-sidebar* {:section section
:selected selected
:drawing-tool drawing-tool
diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss
index 3c5360b4f4..5747dbcd19 100644
--- a/frontend/src/app/main/ui/workspace/sidebar.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar.scss
@@ -9,11 +9,7 @@
.left-settings-bar {
display: grid;
- grid-template-areas:
- "header header"
- "content resize";
- grid-template-rows: deprecated.$s-52 1fr;
- grid-template-columns: 1fr 0;
+ grid-template: "header header" deprecated.$s-52 "content resize" 1fr / 1fr 0;
position: relative;
grid-area: left-sidebar;
min-width: var(--left-sidebar-width);
@@ -65,9 +61,11 @@
width: var(--right-sidebar-width);
background-color: var(--panel-background-color);
z-index: deprecated.$z-index-1;
+
&.not-expand {
max-width: var(--right-sidebar-width);
}
+
&.expanded {
width: var(--right-sidebar-width, var(--right-sidebar-width));
}
@@ -76,7 +74,6 @@
display: grid;
grid-template-columns: 100%;
grid-template-rows: 100%;
-
height: calc(100vh - deprecated.$s-52);
overflow: hidden;
}
@@ -104,20 +101,24 @@
.collapse-sidebar-button {
--collapse-icon-color: var(--color-foreground-secondary);
- @include deprecated.flexCenter;
- @include deprecated.buttonStyle;
+
+ @include deprecated.flex-center;
+ @include deprecated.button-style;
+
height: 100%;
width: deprecated.$s-24;
border-radius: deprecated.$br-5;
color: var(--collapse-icon-color);
transform: rotate(180deg);
+
&:hover {
--collapse-icon-color: var(--color-foreground-primary);
}
}
.collapsed-sidebar {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
position: absolute;
top: deprecated.$s-48;
left: 0;
@@ -126,27 +127,34 @@
background: var(--color-background-primary);
margin-inline-start: var(--sp-m);
}
+
.collapsed-title {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-36;
width: deprecated.$s-24;
border-radius: deprecated.$br-8;
background: var(--color-background-secondary);
}
+
.collapsed-button {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
height: deprecated.$s-24;
width: deprecated.$s-16;
padding: 0;
border-radius: deprecated.$br-5;
+
svg {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
+
&:hover {
svg {
stroke: var(--icon-foreground-hover);
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
index f4082ac55b..72a709bfb5 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs
@@ -69,16 +69,24 @@
[v a b]
(if (= v a) b a))
+;; Per-file, session-scoped (in-memory only) so the search term and section
+;; filter survive switching between the Layers and Assets sidebar tabs without
+;; leaking across files or persisting across reloads.
+(defonce ^:private session-filters*
+ (atom {}))
+
(mf/defc assets-toolbox*
{::mf/wrap [mf/memo]}
[{:keys [size file-id]}]
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
filters* (mf/use-state
- {:term ""
- :section "all"
- :ordering (dwa/get-current-assets-ordering)
- :list-style (dwa/get-current-assets-list-style)
- :open-menu false})
+ (fn []
+ (-> (or (get @session-filters* file-id)
+ {:term ""
+ :section "all"})
+ (assoc :ordering (dwa/get-current-assets-ordering)
+ :list-style (dwa/get-current-assets-list-style)
+ :open-menu false))))
filters (deref filters*)
term (:term filters)
list-style (:list-style filters)
@@ -161,6 +169,9 @@
:id "typographies"
:handler on-section-filter-change}])]
+ (mf/with-effect [file-id term section]
+ (swap! session-filters* assoc file-id {:term term :section section}))
+
[:article {:class (stl/css :assets-bar)}
[:div {:class (stl/css :assets-header)}
(when-not ^boolean read-only?
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.scss b/frontend/src/app/main/ui/workspace/sidebar/assets.scss
index f89069cca5..d04fac7aa2 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets.scss
@@ -8,8 +8,8 @@
.assets-bar {
display: grid;
- height: 100%;
grid-auto-rows: max-content;
+
// TODO: ugly hack :( Fix this! we shouldn't be hardcoding this height
height: calc(100vh - deprecated.$s-92);
scrollbar-gutter: stable;
@@ -18,8 +18,9 @@
}
.libraries-button {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
gap: deprecated.$s-2;
height: deprecated.$s-32;
width: 100%;
@@ -40,8 +41,9 @@
}
.add-library-button {
- @extend .button-primary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-primary;
+ @include deprecated.uppercase-title-typography;
+
gap: deprecated.$s-2;
height: deprecated.$s-32;
width: 100%;
@@ -50,8 +52,9 @@
}
.section-button {
- @include deprecated.flexCenter;
- @include deprecated.buttonStyle;
+ @include deprecated.flex-center;
+ @include deprecated.button-style;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
margin: 0;
@@ -98,13 +101,14 @@
}
&.opened {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
.sections-container {
- @include deprecated.menuShadow;
- @include deprecated.flexColumn;
+ @include deprecated.menu-shadow;
+ @include deprecated.flex-column;
+
position: absolute;
top: deprecated.$s-84;
left: deprecated.$s-12;
@@ -116,7 +120,8 @@
}
.section-item {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
justify-content: space-between;
@@ -126,7 +131,7 @@
}
.section-btn {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
}
.assets-header {
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs
index b2dc3e8e5b..b4e94bb92d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs
@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
+ [app.common.math :as mth]
[app.common.path-names :as cpn]
[app.config :as cf]
[app.main.constants :refer [max-input-length]]
@@ -28,7 +29,7 @@
[app.main.ui.workspace.sidebar.assets.groups :as grp]
[app.util.color :as uc]
[app.util.dom :as dom]
- [app.util.i18n :as i18n :refer [tr]]
+ [app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[okulary.core :as l]
@@ -61,10 +62,16 @@
menu-state (mf/use-state cmm/initial-context-menu-state)
read-only? (mf/use-ctx ctx/workspace-read-only?)
+ opacity (:opacity color)
+ alpha-suffix (when (and (number? opacity) (< opacity 1))
+ (dm/str " " (mth/round (* opacity 100)) "%"))
default-name (cond
(:gradient color) (uc/gradient-type->string (dm/get-in color [:gradient :type]))
(:color color) (:color color)
:else (:value color))
+ display-name (if (and alpha-suffix (not (:gradient color)))
+ (dm/str default-name alpha-suffix)
+ default-name)
rename-color
(mf/use-fn
@@ -231,16 +238,16 @@
:default-value (cpn/merge-path-item (:path color) (:name color))}]
[:div {:title (if (= (:name color) default-name)
- default-name
- (dm/str (:name color) " (" default-name ")"))
+ display-name
+ (dm/str (:name color) " (" display-name ")"))
:class (stl/css :name-block)
:on-double-click rename-color-clicked}
(if (= (:name color) default-name)
- [:span {:class (stl/css :default-name)} default-name]
+ [:span {:class (stl/css :default-name)} display-name]
[:*
(:name color)
- [:span {:class (stl/css :default-name :default-name-with-color)} default-name]])])
+ [:span {:class (stl/css :default-name :default-name-with-color)} display-name]])])
(when local?
[:> cmm/assets-context-menu*
@@ -273,7 +280,7 @@
(mf/defc colors-group
[{:keys [file-id prefix groups open-groups force-open? local? selected
multi-colors? multi-assets? on-asset-click on-assets-delete
- on-clear-selection on-group on-rename-group on-ungroup colors
+ on-clear-selection on-group on-rename-group on-ungroup on-delete-group colors
selected-full]}]
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
false
@@ -318,7 +325,8 @@
:path prefix
:is-group-open group-open?
:on-rename on-rename-group
- :on-ungroup on-ungroup}]
+ :on-ungroup on-ungroup
+ :on-delete-group on-delete-group}]
(when group-open?
[:*
(let [colors (get groups "" [])]
@@ -371,6 +379,7 @@
:on-group on-group
:on-rename-group on-rename-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:colors colors
:selected-full selected-full}]))])]))
@@ -492,6 +501,13 @@
file-id))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
+ on-delete-group
+ (mf/with-memo [colors on-clear-selection]
+ (cmm/make-delete-asset-group-fn
+ {:assets colors
+ :on-clear-selection on-clear-selection
+ :delete-events #(map (fn [c] (dwl/delete-color {:id (:id c)})) %)}))
+
on-asset-click
(mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))]
@@ -526,5 +542,6 @@
:on-group on-group
:on-rename-group on-rename-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:colors colors
:selected-full selected-full}]]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss
index e2c86936cb..aaa1b09f37 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.scss
@@ -38,30 +38,35 @@ $assets-button-width: deprecated.$s-28;
&.editing {
border: deprecated.$s-1 solid var(--input-border-color-focus);
+
input.element-name {
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
- @include deprecated.removeInputStyle;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.remove-input-style;
+
flex-grow: 1;
margin: 0;
color: var(--layer-row-foreground-color);
}
}
+
&:hover {
background-color: var(--assets-item-background-color-hover);
}
}
.bullet-block {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
justify-content: flex-start;
margin-inline-end: deprecated.$s-4;
}
.name-block {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.text-ellipsis;
+
margin: 0;
color: var(--assets-item-name-foreground-color);
}
@@ -76,7 +81,8 @@ $assets-button-width: deprecated.$s-28;
}
.element-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
color: var(--color-foreground-primary);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
index 4386069eb2..28eb7e4699 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
@@ -174,7 +174,6 @@
[:> title-bar*
{:collapsable (< 0 assets-count)
:collapsed (not is-open)
- :all-clickable true
:on-collapsed on-collapsed
:add-icon-gap (= 0 assets-count)
:title title}
@@ -199,6 +198,39 @@
(run! st/emit!))
(st/emit! (dwu/commit-undo-transaction undo-id))))
+(defn make-delete-asset-group-fn
+ "Build an `:on-delete-group` handler that filters `assets` by group
+ path, asks the user to confirm, and on accept emits every event
+ produced by `delete-events` inside one undo transaction.
+
+ Options:
+ - `:assets` collection to filter.
+ - `:on-clear-selection` invoked before the deletes.
+ - `:delete-events` `(fn [matching-assets] => seq-of-events)`.
+ - `:path-filter` `(fn [asset-path group-path] => bool)` deciding
+ which assets fall under the group. Defaults to
+ `str/starts-with?`."
+ [{:keys [assets on-clear-selection delete-events path-filter]
+ :or {path-filter str/starts-with?}}]
+ (fn [path]
+ (let [matching (filter #(path-filter (:path %) path) assets)
+ undo-id (js/Symbol)
+ do-delete
+ (fn []
+ (on-clear-selection)
+ (st/emit! (dwu/start-undo-transaction undo-id))
+ (run! st/emit! (delete-events matching))
+ (st/emit! (dwu/commit-undo-transaction undo-id)))]
+ (when (seq matching)
+ (st/emit!
+ (modal/show
+ {:type :confirm
+ :title (tr "modals.delete-asset-group.title")
+ :message (tr "modals.delete-asset-group.message"
+ (c (count matching)))
+ :accept-label (tr "labels.delete")
+ :on-accept do-delete}))))))
+
(defn on-drop-asset
[event asset dragging* selected selected-full selected-paths rename]
(let [create-typed-assets-group (partial create-assets-group rename)]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss
index 257bd24b3c..2188db46a8 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss
@@ -7,7 +7,8 @@
@use "refactor/common-refactor.scss" as deprecated;
.title-name {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
display: flex;
align-items: center;
flex-grow: 1;
@@ -15,7 +16,8 @@
}
.title-tokens {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
text-transform: capitalize;
}
@@ -24,14 +26,17 @@
}
.section-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
padding-right: deprecated.$s-2;
+
svg {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-16;
width: deprecated.$s-16;
fill: none;
- stroke: currentColor;
+ stroke: currentcolor;
}
}
@@ -42,7 +47,8 @@
}
.num-assets {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
padding-left: deprecated.$s-8;
}
@@ -56,8 +62,9 @@
}
.drag-counter {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.text-ellipsis;
+
position: absolute;
bottom: 0;
left: 0;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs
index 077742d71d..e2f0637c02 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs
@@ -17,6 +17,7 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
+ [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.variants :as dwv]
[app.main.refs :as refs]
@@ -32,7 +33,7 @@
[app.main.ui.workspace.sidebar.assets.groups :as grp]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
- [app.util.i18n :as i18n :refer [tr]]
+ [app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.v2.core :as ptk]
@@ -191,7 +192,7 @@
(mf/defc components-group*
[{:keys [file-id prefix groups open-groups is-force-open renaming is-listing-thumbs selected on-asset-click
- on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-context-menu
+ on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-delete-group on-context-menu
selected-full is-local count-variants on-group-combine-variants]}]
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
@@ -246,6 +247,7 @@
:is-can-combine can-combine?
:on-rename on-rename-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:on-group-combine-variants on-group-combine-variants}]
(when group-open?
@@ -303,6 +305,7 @@
:cancel-rename cancel-rename
:on-rename-group on-rename-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:on-context-menu on-context-menu
:on-group-combine-variants on-group-combine-variants
:selected-full selected-full
@@ -493,6 +496,33 @@
(map #(dwv/rename-comp-or-variant-and-main (:id %) (cmm/ungroup % path)))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
+ on-delete-group
+ (mf/with-memo [components on-clear-selection]
+ (cmm/make-delete-asset-group-fn
+ {:assets components
+ :on-clear-selection on-clear-selection
+ :path-filter cpn/inside-path?
+ ;; Variants are handled via their variant container
+ ;; (matching the per-item delete dispatch in
+ ;; file_library.cljs); sibling variants sharing a
+ ;; container are deduplicated so we delete each container
+ ;; only once.
+ :delete-events
+ (fn [matching]
+ (let [{variants true non-variants false}
+ (group-by (comp boolean ctc/is-variant?) matching)
+
+ variant-containers
+ (->> variants
+ (group-by :variant-id)
+ (map (fn [[_ comps]] (first comps))))]
+ (concat
+ (map #(dwsh/delete-shapes (:main-instance-page %)
+ #{(:variant-id %)})
+ variant-containers)
+ (map #(dwl/delete-component {:id (:id %)})
+ non-variants))))}))
+
on-group-combine-variants
(mf/use-fn
(mf/deps components on-clear-selection)
@@ -602,6 +632,7 @@
:on-rename-group on-rename-group
:on-group on-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:on-group-combine-variants on-group-combine-variants
:on-context-menu on-context-menu
:selected-full selected-full
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss
index ec23a9d1f4..fa7242d60a 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss
@@ -20,6 +20,7 @@
border-radius: $br-8;
background-color: var(--color-canvas);
overflow: hidden;
+
&:hover {
.component-item-grid-name {
display: flex;
@@ -32,10 +33,7 @@
&::before {
content: " ";
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ inset: 0;
border: calc($b-2 * 2) solid var(--color-background-primary);
border-radius: $br-8;
}
@@ -50,7 +48,6 @@
left: var(--sp-xs);
right: var(--sp-xs);
bottom: var(--sp-xs);
- padding: var(--sp-xxs) var(--sp-s);
border-radius: $br-4;
background-color: var(--color-background-primary);
color: var(--color-foreground-primary);
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs
index 2fc62e001d..0cb88cd0df 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs
@@ -101,7 +101,6 @@
:open is-open)}
[:> title-bar* {:collapsable true
:collapsed (not is-open)
- :all-clickable true
:on-collapsed toggle-open
:title (if is-local
(mf/html [:div {:class (stl/css :special-title)}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss
index 18de784336..e8c63bb18f 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss
@@ -6,6 +6,7 @@
@use "ds/typography.scss" as t;
@use "refactor/common-refactor.scss" as deprecated;
+
.tool-window {
padding: 0 0 deprecated.$s-24 deprecated.$s-12;
display: grid;
@@ -16,6 +17,7 @@
.file-name {
@include t.use-typography("body-small");
+
display: flex;
justify-content: flex-start;
align-items: center;
@@ -25,6 +27,7 @@
.loading {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
justify-content: flex-start;
@@ -34,19 +37,23 @@
}
.special-title {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
color: var(--title-foreground-color-hover);
margin-left: deprecated.$s-2;
text-align: left;
}
.file-link {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
border-radius: deprecated.$br-8;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
fill: var(--title-foreground-color-hover);
}
@@ -68,13 +75,16 @@
}
.no-found-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
background-color: var(--not-found-background-color);
border-radius: deprecated.$br-circle;
height: deprecated.$s-48;
width: deprecated.$s-48;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
height: deprecated.$s-24;
width: deprecated.$s-24;
stroke: var(--not-found-foreground-color);
@@ -82,6 +92,7 @@
}
.no-found-text {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--not-found-foreground-color);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs
index 72a1be6996..c7e72c871b 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs
@@ -23,7 +23,7 @@
[rumext.v2 :as mf]))
(mf/defc asset-group-title*
- [{:keys [file-id section path is-group-open on-rename on-ungroup on-group-combine-variants is-can-combine]}]
+ [{:keys [file-id section path is-group-open on-rename on-ungroup on-delete-group on-group-combine-variants is-can-combine on-add]}]
(when-not (empty? path)
(let [[other-path last-path truncated] (cpn/compact-path path 35 true)
menu-state (mf/use-state cmm/initial-context-menu-state)
@@ -51,7 +51,6 @@
:on-context-menu on-context-menu}
[:> title-bar* {:collapsable true
:collapsed (not is-group-open)
- :all-clickable true
:on-collapsed on-fold-group
:title (mf/html [:* (when-not (empty? other-path)
[:span {:class (stl/css :pre-path)
@@ -70,6 +69,12 @@
{:name (tr "workspace.assets.ungroup")
:id "assets-ungroup-group"
:handler #(on-ungroup path)}]
+ on-delete-group
+ (conj
+ {:name (tr "workspace.assets.delete-group")
+ :id "assets-delete-group"
+ :handler #(on-delete-group path)})
+
is-can-combine
(conj
{:name (tr "workspace.shape.menu.combine-as-variants")
@@ -77,11 +82,27 @@
:handler #(on-group-combine-variants path)}))}]]
[:div {:class (stl/css :title-menu)}
+ (when on-add
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.assets.typography.add-typography")
+ :on-click on-add
+ :icon i/add}])
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.assets.component-group-options")
:on-click on-context-menu
:icon i/menu}]]])))
+(defn- sort-groups
+ "Recursively sort subgroup keys alphabetically at every nesting level."
+ [groups reverse-sort?]
+ (let [cmp (if reverse-sort? #(compare %2 %1) compare)
+ sort-tree (fn sort-tree [m]
+ (into (sorted-map-by cmp)
+ (map (fn [[k v]]
+ [k (if (map? v) (sort-tree v) v)]))
+ m))]
+ (sort-tree groups)))
+
(defn group-assets
"Convert a list of assets in a nested structure like this:
@@ -93,19 +114,17 @@
"
[assets reverse-sort?]
(when-not (empty? assets)
- (reduce (fn [groups {:keys [path] :as asset}]
- (let [path (cpn/split-path (or path ""))]
- (update-in groups
- (conj path "")
- (fn [group]
- (if group
- (conj group asset)
- [asset])))))
- (sorted-map-by (fn [key1 key2]
- (if reverse-sort?
- (compare key2 key1)
- (compare key1 key2))))
- assets)))
+ (-> (reduce (fn [groups {:keys [path] :as asset}]
+ (let [path (cpn/split-path (or path ""))]
+ (update-in groups
+ (conj path "")
+ (fn [group]
+ (if group
+ (conj group asset)
+ [asset])))))
+ {}
+ assets)
+ (sort-groups reverse-sort?))))
(def ^:private schema:group-form
[:map {:title "GroupForm"}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss
index 1237c53323..0152e5e52c 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.scss
@@ -25,6 +25,7 @@
}
.title-menu {
+ display: flex;
visibility: hidden;
}
@@ -39,18 +40,19 @@
}
.path {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
margin-left: deprecated.$s-2;
text-transform: initial;
color: var(--title-foreground-color-hover);
}
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-container {
- @extend .modal-container-base;
+ @extend %modal-container-base;
}
.modal-header {
@@ -58,37 +60,40 @@
}
.modal-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.modal-content {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-bottom: deprecated.$s-24;
}
.input-wrapper {
- @extend .input-with-label;
- @include deprecated.bodySmallTypography;
+ @extend %input-with-label;
+ @include deprecated.body-small-typography;
+
margin-bottom: deprecated.$s-8;
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
}
.cancel-button {
- @extend .modal-cancel-btn;
+ @extend %modal-cancel-btn;
}
.accept-btn {
- @extend .modal-accept-btn;
+ @extend %modal-accept-btn;
&.danger {
- @extend .modal-danger-btn;
+ @extend %modal-danger-btn;
}
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs
index d222ffafd0..174648e8ca 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs
@@ -26,7 +26,7 @@
[app.main.ui.workspace.sidebar.assets.groups :as grp]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]]
[app.util.dom :as dom]
- [app.util.i18n :as i18n :refer [tr]]
+ [app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.v2.core :as ptk]
@@ -125,7 +125,8 @@
:editing? editing?
:renaming? renaming?
:focus-name? rename?
- :external-open* open*}]
+ :external-open* open*
+ :is-asset? true}]
(when ^boolean dragging?
[:div {:class (stl/css :dragging)}])]))
@@ -133,7 +134,7 @@
{::mf/wrap-props false}
[{:keys [file-id prefix groups open-groups force-open? file local? selected local-data
editing-id renaming-id on-asset-click handle-change on-rename-group
- on-ungroup on-context-menu selected-full]}]
+ on-ungroup on-delete-group on-context-menu selected-full is-read-only]}]
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
false
(get open-groups prefix true))
@@ -164,7 +165,14 @@
(mf/use-fn
(mf/deps dragging* prefix selected-paths selected-full move-typography)
(fn [event]
- (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography)))]
+ (cmm/on-drop-asset-group event dragging* prefix selected-paths selected-full move-typography)))
+
+ add-typography-to-group
+ (mf/use-fn
+ (mf/deps file-id prefix)
+ (fn [_]
+ (st/emit! (dw/set-assets-section-open file-id :typographies true)
+ (dwt/add-typography file-id prefix))))]
[:div {:class (stl/css :typographies-group)
:on-drag-enter on-drag-enter
@@ -176,7 +184,10 @@
:path prefix
:is-group-open group-open?
:on-rename on-rename-group
- :on-ungroup on-ungroup}]
+ :on-ungroup on-ungroup
+ :on-delete-group on-delete-group
+ :on-add (when (and local? (not is-read-only))
+ add-typography-to-group)}]
(when group-open?
[:*
@@ -228,8 +239,10 @@
:handle-change handle-change
:on-rename-group on-rename-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:on-context-menu on-context-menu
- :selected-full selected-full}]))])]))
+ :selected-full selected-full
+ :is-read-only is-read-only}]))])]))
(mf/defc typographies-section*
[{:keys [file file-id typographies open-status-ref selected
@@ -341,6 +354,13 @@
(cmm/ungroup % path)))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
+ on-delete-group
+ (mf/with-memo [typographies on-clear-selection]
+ (cmm/make-delete-asset-group-fn
+ {:assets typographies
+ :on-clear-selection on-clear-selection
+ :delete-events #(map (fn [t] (dwl/delete-typography (:id t))) %)}))
+
on-context-menu
(mf/use-fn
(mf/deps selected on-clear-selection read-only?)
@@ -430,8 +450,10 @@
:handle-change handle-change
:on-rename-group on-rename-group
:on-ungroup on-ungroup
+ :on-delete-group on-delete-group
:on-context-menu on-context-menu
- :selected-full selected-full}]
+ :selected-full selected-full
+ :is-read-only read-only?}]
(if is-local
[:> cmm/assets-context-menu*
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss
index d00eeb20f9..d44f702823 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.scss
@@ -27,6 +27,7 @@
margin-bottom: deprecated.$s-4;
border-radius: deprecated.$br-8;
background-color: var(--assets-item-background-color);
+ max-inline-size: var(--options-width);
}
.dragging {
diff --git a/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss
index b5a372a431..5627355c0c 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/common/sidebar.scss
@@ -43,10 +43,15 @@ $column-number: 8; // total number of columns
// -> 8 columns (32px each) + 7 gaps (4px each) = 284px
// Derived widths
-$options-width: calc(#{$column-width} * #{$column-number} + #{$column-gap} * calc(#{$column-number} - 1));
-$seven-column-width: calc(
- #{$column-width} * calc(#{$column-number} - 1) + #{$column-gap} * calc(#{$column-number} - 2)
-);
+@function grid-width($cols) {
+ @return calc(#{$column-width} * #{$cols} + #{$column-gap} * #{$cols - 1});
+}
+
+$options-width: grid-width($column-number);
+$two-column-width: grid-width(2);
+$three-column-width: grid-width(3);
+$four-column-width: grid-width(4);
+$seven-column-width: grid-width(7);
// ------------------------------------------------------------
// Grid mixin — applies the standard structure to any container
@@ -73,17 +78,30 @@ $seven-column-width: calc(
// |___|-|___|-|___|-|___|-|___|-|___|-|___|-|___|
// -> 8 columns (32px each) + 7 gaps (4px each) = 284px
//
-// But one block (grid-exception-input) doesn’t fit perfectly:
+// But two blocks don’t fit perfectly:
+// First (grid-exception-input-width)
// |__________________|-|__________________|-|___|
-//
-// We calculate the width of that grid-exception-input as:
+// We calculate the width of that grid-exception-input-width as:
//
// - 3.5 columns of base grid width
// - + 3 inter-column gaps
// - − half a gap (because it’s visually shared with the next block)
-
$grid-exception-input-width: calc(#{$sz-32} * 3.5 + 3 * var(--sp-xs) - (var(--sp-xs) / 2));
+//
+// |___|-|___|-|___|-|___|-|___|-|___|-|___|-|___|
+//
+// Second (grid-exception-input-width-small)
+// |__________________|-|____________|-|___|-|___|
+//
+// We calculate the width of that grid-exception-input-width-small as:
+//
+// - 2.5 columns of base grid width
+// - + 2 inter-column gaps
+// - − half a gap (because it’s visually shared with the next block)
+
+$grid-exception-input-width-small: calc(#{$sz-32} * 2.5 + 2 * var(--sp-xs) - (var(--sp-xs) / 2));
+
// ============================================================
// CSS VARIABLES (exposed for runtime use)
// ============================================================
@@ -95,7 +113,11 @@ $grid-exception-input-width: calc(#{$sz-32} * 3.5 + 3 * var(--sp-xs) - (var(--sp
--left-sidebar-width-max: #{$left-sidebar-width-max};
--right-sidebar-width: #{$right-sidebar-width};
--right-sidebar-width-max: #{$right-sidebar-width-max};
- --7-columns-dropdown-width: #{$seven-column-width};
+ --two-columns-width: #{$two-column-width};
+ --three-columns-width: #{$three-column-width};
+ --four-columns-width: #{$four-column-width};
+ --seven-columns-width: #{$seven-column-width};
--options-width: #{$options-width};
--grid-exception-input-width: #{$grid-exception-input-width};
+ --grid-exception-input-width-small: #{$grid-exception-input-width-small};
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug.scss b/frontend/src/app/main/ui/workspace/sidebar/debug.scss
index 47f119009c..81a7921fe5 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/debug.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/debug.scss
@@ -21,12 +21,14 @@
}
.checkbox-wrapper {
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
height: deprecated.$s-32;
padding: 0;
}
.checkbox-icon {
- @extend .checkbox-icon;
+ @extend %checkbox-icon;
+
cursor: pointer;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss
index a72bf3a833..1f67e505bc 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.scss
@@ -26,7 +26,6 @@
.shape-title {
font-size: deprecated.$fs-14;
- padding-bottom: deprecated.$s-4;
background: var(--color-background-quaternary);
color: var(--color-foreground-primary);
padding: deprecated.$s-8;
@@ -34,6 +33,7 @@
display: flex;
gap: deprecated.$s-4;
}
+
.shape-name {
flex: 1;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.scss b/frontend/src/app/main/ui/workspace/sidebar/history.scss
index 907e6732bf..069d1d5d73 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/history.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/history.scss
@@ -24,8 +24,7 @@
.history-entries {
height: calc(100vh - deprecated.$s-100);
padding: deprecated.$s-12;
- overflow-x: hidden;
- overflow-y: auto;
+ overflow: hidden auto;
font-size: deprecated.$fs-12;
}
@@ -45,26 +44,33 @@
.history-entry-summary {
display: flex;
align-items: center;
+
.history-entry-summary-icon {
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--entry-foreground-color);
}
}
+
.history-entry-summary-text {
margin: 0 deprecated.$s-8;
color: var(--color-foreground-primary);
}
+
.history-entry-summary-button {
opacity: deprecated.$op-0;
margin-left: auto;
+
&.button-opened {
svg {
transform: rotate(90deg);
}
}
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--entry-foreground-color);
}
}
@@ -74,6 +80,7 @@
display: block;
padding-top: deprecated.$s-16;
color: var(--modal-text-foreground-color);
+
.history-entry-details-list {
margin: 0;
}
@@ -88,14 +95,17 @@
&:hover {
background-color: var(--entry-background-color-hover);
color: var(--entry-foreground-color-hover);
+
.history-entry-summary {
.history-entry-summary-icon {
svg {
stroke: var(--entry-foreground-color-hover);
}
}
+
.history-entry-summary-button {
opacity: deprecated.$op-10;
+
&.button-opened {
svg {
transform: rotate(90deg);
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
index b17698d659..d31351adc8 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs
@@ -275,11 +275,19 @@
toggle-collapse
(mf/use-fn
- (mf/deps is-expanded)
+ (mf/deps is-expanded id objects)
(fn [event]
(dom/stop-propagation event)
- (if (and is-expanded (kbd/shift? event))
+ (cond
+ ;; Shift+click while expanded collapses every layer in the sidebar
+ (and is-expanded (kbd/shift? event))
(st/emit! (dwc/collapse-all))
+
+ ;; Alt+click while collapsed expands the entire subtree rooted at this id
+ (and (not is-expanded) (kbd/alt? event))
+ (st/emit! (dwc/expand-subtree id objects))
+
+ :else
(st/emit! (dwc/toggle-collapse id)))))
toggle-blocking
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss
index 0fc369362d..f66d7b62fe 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss
@@ -13,8 +13,8 @@
--layer-background-color: var(--color-background-primary);
--layer-foreground-color: inherit;
--shadow-color: transparent;
- box-shadow: px2rem(16) px2rem(0) px2rem(0) px2rem(0) var(--shadow-color);
+ box-shadow: px2rem(16) px2rem(0) px2rem(0) px2rem(0) var(--shadow-color);
display: flex;
flex-direction: row;
align-items: center;
@@ -30,6 +30,7 @@
--context-hover-color: var(--layer-row-foreground-color-hover);
--context-hover-opacity: 1;
--layer-foreground-color: var(--layer-row-foreground-color-hover);
+
&.hidden {
opacity: 1;
}
@@ -59,9 +60,11 @@
&.dnd-over-bot {
border-block-end: $b-2 solid var(--color-accent-primary);
}
+
&.dnd-over-top {
border-block-start: $b-2 solid var(--color-accent-primary);
}
+
&.dnd-over {
border: $b-2 solid var(--color-accent-primary);
}
@@ -73,9 +76,11 @@
--layer-background-color: var(--color-background-quaternary);
--shadow-color: var(--color-background-quaternary);
}
+
.layer-row.type-comp & {
--layer-foreground-color: var(--color-accent-secondary);
}
+
.layer-row.selected & {
--layer-background-color: transparent;
--layer-foreground-color: var(--color-accent-primary);
@@ -91,6 +96,7 @@
inline-size: calc(100% - (var(--depth) * var(--layer-indentation-size)));
cursor: pointer;
min-inline-size: px2rem(140);
+
&.filtered {
inline-size: calc(100% - $sz-12);
}
@@ -106,6 +112,7 @@
&.selected {
display: flex;
}
+
.layer-row.highlight &,
.layer-row:hover & {
display: flex;
@@ -130,21 +137,27 @@
inline-size: $sz-24;
padding-inline-start: var(--sp-xs);
color: var(--color-foreground-secondary);
+
.layer-row.selected & {
color: var(--color-accent-primary);
}
+
.layer-row.type-comp & {
color: var(--color-accent-secondary);
}
+
.inverse & {
transform: rotate(-90deg);
}
+
.layer-row.hidden & {
opacity: 0.7;
}
+
.layer-row.highlight &,
.layer-row:hover & {
opacity: 1;
+
svg {
stroke: var(--color-accent-primary);
}
@@ -162,14 +175,17 @@
.layer-row.hidden & {
opacity: 0.1;
}
+
.layer-row.type-comp & {
background-color: var(--color-accent-secondary);
}
+
.layer-row.highlight &,
.layer-row:hover & {
opacity: 0.4;
background-color: var(--color-accent-primary);
}
+
.layer-row.selected & {
background-color: var(--color-accent-primary);
}
@@ -200,12 +216,15 @@
.layer-row.hidden & {
opacity: 0.7;
}
+
.layer-row.selected & {
stroke: var(--color-accent-primary);
}
+
.layer-row.type-comp & {
stroke: var(--color-accent-secondary);
}
+
.layer-row.highlight &,
.layer-row:hover & {
opacity: 1;
@@ -216,6 +235,7 @@
.layer-row.selected & {
background-color: var(--color-background-quaternary);
}
+
&.inverse svg {
transform: rotate(90deg);
}
@@ -224,9 +244,9 @@
.toggle-element,
.block-element {
--layer-row-action-btn-background: none;
+
border: none;
cursor: pointer;
- display: flex;
justify-content: center;
align-items: center;
block-size: 100%;
@@ -235,6 +255,7 @@
display: none;
background: var(--layer-row-action-btn-background);
padding-inline-end: px2rem(6);
+
svg {
display: flex;
justify-content: center;
@@ -249,6 +270,7 @@
.layer-row.hidden & {
opacity: 0.7;
}
+
.type-comp & {
stroke: var(--color-accent-secondary);
}
@@ -257,6 +279,7 @@
.element-actions.selected & {
display: flex;
opacity: 0;
+
&.selected {
opacity: 1;
}
@@ -269,15 +292,20 @@
.layer-row.highlight &,
.layer-row:hover & {
display: flex;
+
--layer-row-action-btn-background: var(--color-background-secondary);
+
svg {
opacity: 1;
stroke: var(--color-accent-primary);
}
}
+
.layer-row.selected & {
display: flex;
+
--layer-row-action-btn-background: var(--color-background-quaternary);
+
svg {
stroke: var(--color-accent-primary);
}
@@ -295,13 +323,16 @@
block-size: $sz-16;
min-inline-size: calc(var(--depth) * var(--layer-indentation-size));
}
+
.filtered {
min-inline-size: $sz-12;
}
+
.lazy-load-sentinel {
min-height: 1px;
pointer-events: none;
}
+
.lazy-load-sentinel {
min-height: 1px;
pointer-events: none;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs
index 5c0f181c1d..181ea70d47 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs
@@ -38,7 +38,7 @@
shape-name)
default-value
- (mf/with-memo [variant-id variant-error variant-properties]
+ (mf/with-memo [variant-id variant-error variant-properties shape-name]
(if variant-id
(or variant-error (ctv/properties-map->formula variant-properties))
shape-name))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss
index e2e4b1a723..659444daaf 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss
@@ -11,11 +11,10 @@
--element-name-comp-color: var(--context-hover-color, var(--layer-row-component-foreground-color));
--element-name-opacity: var(--context-hover-opacity, deprecated.$op-7);
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
color: var(--element-name-color);
-
flex-grow: 1;
block-size: 100%;
align-content: center;
@@ -42,9 +41,9 @@
--element-name-input-border-color: var(--input-border-color-focus);
--element-name-input-color: var(--layer-row-foreground-color);
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
- @include deprecated.removeInputStyle;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.remove-input-style;
flex-grow: 1;
height: deprecated.$s-28;
@@ -62,5 +61,6 @@
.element-name-touched {
--element-name-touched-color: var(--layer-row-component-foreground-color);
+
color: var(--element-name-touched-color);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
index 3440a4e43f..df02e1d0d9 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs
@@ -11,8 +11,10 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.types.shape :as cts]
+ [app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
+ [app.main.data.workspace.texts :as dwt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.search-bar :refer [search-bar*]]
@@ -206,61 +208,70 @@
;; --- Layers Toolbox
+(def ^:private ref:layers-panel-search
+ (l/derived (l/key :layers-panel-search) refs/workspace-local))
+
;; FIXME: optimize
(defn- match-filters?
[state [id shape]]
(let [search (:search-text state)
+ scope (:search-scope state)
filters (:filters state)
filters (cond-> filters
(contains? filters :shape)
- (conj :rect :circle :path :bool))]
+ (conj :rect :circle :path :bool))
+ text-match? (case scope
+ :canvas (and (= :text (:type shape))
+ (some? (:content shape))
+ (txt/content-has-text? (:content shape) search))
+ (or (str/includes? (str/lower (:name shape)) (str/lower search))
+ (str/includes? (str/lower (:variant-name shape)) (str/lower search))
+ ;; Dev-only: allow search by id
+ (and *assert* (str/includes? (dm/str (:id shape)) (str/lower search)))))]
(or (= uuid/zero id)
- (and (or (str/includes? (str/lower (:name shape)) (str/lower search))
- (str/includes? (str/lower (:variant-name shape)) (str/lower search))
- ;; Only for local development we allow search for ids. Otherwise will be hard
- ;; search for numbers or single letter shape names (ie: "A")
- (and *assert*
- (str/includes? (dm/str (:id shape)) (str/lower search))))
+ (and text-match?
(or (empty? filters)
- (and (contains? filters :component)
- (contains? shape :component-id))
- (and (contains? filters :image)
- (some? (cts/has-images? shape)))
-
+ (and (contains? filters :component) (contains? shape :component-id))
+ (and (contains? filters :image) (some? (cts/has-images? shape)))
(let [direct-filters (into #{} (filter #{:frame :rect :circle :path :bool :text}) filters)]
(contains? direct-filters (:type shape)))
(and (contains? filters :group)
- (and (cfh/group-shape? shape)
- (not (contains? shape :component-id))
- (or (not (contains? shape :masked-group))
- (false? (:masked-group shape)))))
- (and (contains? filters :mask)
- (true? (:masked-group shape))))))))
+ (cfh/group-shape? shape)
+ (not (contains? shape :component-id))
+ (or (not (contains? shape :masked-group))
+ (false? (:masked-group shape))))
+ (and (contains? filters :mask) (true? (:masked-group shape))))))))
(defn use-search
[page objects]
- (let [state* (mf/use-state
- #(do {:show-search false
- :show-menu false
- :search-text ""
- :filters #{}
- :num-items 100}))
-
- state (deref state*)
- current-filters (:filters state)
- current-items (:num-items state)
- current-search (:search-text state)
- show-menu? (:show-menu state)
- show-search? (:show-search state)
+ (let [state* (mf/use-state
+ #(do {:show-search false
+ :find-replace-mode? false
+ :search-scope :layers
+ :show-menu false
+ :search-text ""
+ :replace-text ""
+ :filters #{}
+ :num-items 100
+ :current-match-idx 0}))
+ layers-search-request (mf/deref ref:layers-panel-search)
+ state (deref state*)
+ current-filters (:filters state)
+ current-items (:num-items state)
+ current-search (:search-text state)
+ replace-text (:replace-text state)
+ show-menu? (:show-menu state)
+ show-search? (:show-search state)
+ find-replace-mode? (:find-replace-mode? state)
+ search-scope (:search-scope state)
+ current-match-idx (:current-match-idx state)
clear-search-text
(mf/use-fn
- #(swap! state* assoc :search-text "" :num-items 100))
-
+ #(swap! state* assoc :search-text "" :num-items 100 :current-match-idx 0))
toggle-filters
- (mf/use-fn
- #(swap! state* update :show-menu not))
+ (mf/use-fn #(swap! state* update :show-menu not))
on-toggle-filters-click
(mf/use-fn
@@ -269,18 +280,26 @@
(toggle-filters)))
hide-menu
- (mf/use-fn
- #(swap! state* assoc :show-menu false))
+ (mf/use-fn #(swap! state* assoc :show-menu false))
on-key-down
- (mf/use-fn
- (fn [event]
- (when (kbd/esc? event) (hide-menu))))
+ (mf/use-fn (fn [event] (when (kbd/esc? event) (hide-menu))))
update-search-text
(mf/use-fn
(fn [value _event]
- (swap! state* assoc :search-text value :num-items 100)))
+ (swap! state* assoc :search-text value :num-items 100 :current-match-idx 0)))
+
+ update-replace-text
+ (mf/use-fn (fn [value _event] (swap! state* assoc :replace-text value)))
+
+ clear-replace-text
+ (mf/use-fn #(swap! state* assoc :replace-text ""))
+
+ set-search-scope
+ (mf/use-fn
+ (fn [scope]
+ (swap! state* assoc :search-scope scope :num-items 100 :current-match-idx 0)))
toggle-search
(mf/use-fn
@@ -289,30 +308,23 @@
(dom/blur! node)
(swap! state* (fn [state]
(-> state
- (assoc :search-text "")
- (assoc :filters #{})
- (assoc :show-menu false)
- (assoc :num-items 100)
+ (assoc :search-text "" :replace-text "" :filters #{})
+ (assoc :show-menu false :find-replace-mode? false)
+ (assoc :search-scope :layers :num-items 100 :current-match-idx 0)
(update :show-search not)))))))
remove-filter
(mf/use-fn
(fn [event]
- (let [fkey (-> (dom/get-current-target event)
- (dom/get-data "filter")
- (keyword))]
+ (let [fkey (-> (dom/get-current-target event) (dom/get-data "filter") (keyword))]
(swap! state* (fn [state]
- (-> state
- (update :filters disj fkey)
- (assoc :num-items 100)))))))
+ (-> state (update :filters disj fkey) (assoc :num-items 100)))))))
add-filter
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
- (let [key (-> (dom/get-current-target event)
- (dom/get-data "filter")
- (keyword))]
+ (let [key (-> (dom/get-current-target event) (dom/get-data "filter") (keyword))]
(swap! state* (fn [state]
(-> state
(update :filters conj key)
@@ -332,6 +344,65 @@
filtered-objects-total
(count filtered-objects-all)
+ canvas-match-ids
+ (mf/with-memo [objects current-search search-scope]
+ (when (and (= :canvas search-scope) (d/not-empty? current-search))
+ (reduce-kv (fn [acc id shape]
+ (cond-> acc
+ (and (= :text (:type shape))
+ (some? (:content shape))
+ (txt/content-has-text? (:content shape) current-search))
+ (conj id)))
+ [] objects)))
+
+ layer-match-ids
+ (mf/with-memo [objects current-search search-scope]
+ (when (and (= :layers search-scope) (d/not-empty? current-search))
+ (reduce-kv (fn [acc id shape]
+ (cond-> acc
+ (str/includes? (str/lower (:name shape)) (str/lower current-search))
+ (conj id)))
+ [] objects)))
+
+ text-match-ids (if (= :canvas search-scope) canvas-match-ids layer-match-ids)
+ text-match-count (count text-match-ids)
+ safe-match-idx (if (pos? text-match-count) (mod current-match-idx text-match-count) 0)
+
+ navigate-next
+ (mf/use-fn
+ (mf/deps text-match-count)
+ (fn [_]
+ (when (pos? text-match-count)
+ (swap! state* update :current-match-idx
+ (fn [idx] (mod (inc idx) text-match-count))))))
+
+ navigate-prev
+ (mf/use-fn
+ (mf/deps text-match-count)
+ (fn [_]
+ (when (pos? text-match-count)
+ (swap! state* update :current-match-idx
+ (fn [idx] (mod (+ (dec idx) text-match-count) text-match-count))))))
+
+ handle-replace
+ (mf/use-fn
+ (mf/deps text-match-ids safe-match-idx replace-text current-search search-scope)
+ (fn [_]
+ (when (and (pos? text-match-count) (d/not-empty? current-search))
+ (let [id (nth text-match-ids safe-match-idx)]
+ (if (= :canvas search-scope)
+ (st/emit! (dwt/replace-text-in-shapes [id] current-search replace-text))
+ (st/emit! (dwt/replace-layer-names-in-shapes [id] current-search replace-text)))))))
+
+ handle-replace-all
+ (mf/use-fn
+ (mf/deps text-match-ids replace-text current-search search-scope)
+ (fn [_]
+ (when (and (pos? text-match-count) (d/not-empty? current-search))
+ (if (= :canvas search-scope)
+ (st/emit! (dwt/replace-text-in-shapes text-match-ids current-search replace-text))
+ (st/emit! (dwt/replace-layer-names-in-shapes text-match-ids current-search replace-text))))))
+
filtered-objects
(mf/with-memo [active? filtered-objects-all current-items]
(when active?
@@ -353,6 +424,16 @@
(events/unlistenByKey key1)
(events/unlistenByKey key2))))
+ (mf/with-effect [layers-search-request]
+ (when (some? layers-search-request)
+ (let [replace-mode? (= layers-search-request :find-and-replace)]
+ (swap! state* (fn [s]
+ (-> s
+ (assoc :show-search true :find-replace-mode? replace-mode?)
+ (assoc :search-scope (if replace-mode? :canvas :layers))
+ (assoc :search-text "" :replace-text "" :current-match-idx 0)))))
+ (st/emit! dw/clear-layers-search)))
+
[filtered-objects
handle-show-more
#(mf/html
@@ -364,17 +445,62 @@
:on-clear clear-search-text
:placeholder (tr "workspace.sidebar.layers.search")}
[:button {:on-click on-toggle-filters-click
- :class (stl/css-case
- :filter-button true
- :opened show-menu?
- :active active?)}
+ :class (stl/css-case :filter-button true :opened show-menu? :active active?)}
[:> icon* {:icon-id i/filter}]]]
-
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click toggle-search
:icon i/close}]]
+ [:div {:class (stl/css :search-scope-row)}
+ [:label {:class (stl/css-case :scope-option true :scope-selected (= :canvas search-scope))}
+ [:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :canvas search-scope))}]
+ [:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input)
+ :checked (= :canvas search-scope)
+ :on-change (fn [_] (set-search-scope :canvas))}]
+ [:span {:class (stl/css :scope-label)}
+ (tr "workspace.sidebar.layers.search-scope-canvas")]]
+ [:label {:class (stl/css-case :scope-option true :scope-selected (= :layers search-scope))}
+ [:span {:class (stl/css-case :scope-radio true :scope-radio-checked (= :layers search-scope))}]
+ [:input {:type "radio" :name "search-scope" :class (stl/css :scope-radio-input)
+ :checked (= :layers search-scope)
+ :on-change (fn [_] (set-search-scope :layers))}]
+ [:span {:class (stl/css :scope-label)}
+ (tr "workspace.sidebar.layers.search-scope-layers")]]]
+
+ (when ^boolean find-replace-mode?
+ [:*
+ [:div {:class (stl/css :tool-window-bar :replace-row)}
+ [:div {:class (stl/css :replace-input-wrapper)}
+ [:input {:class (stl/css :replace-input)
+ :value replace-text
+ :placeholder (tr "workspace.sidebar.layers.replace-placeholder")
+ :on-change (fn [event]
+ (update-replace-text (dom/get-target-val event) event))}]
+ (when (not= "" replace-text)
+ [:button {:class (stl/css :clear-icon) :on-click clear-replace-text}
+ [:> icon* {:icon-id i/delete-text :size "s"}]])]
+ (when (d/not-empty? current-search)
+ (if (pos? text-match-count)
+ [:div {:class (stl/css :match-navigation)}
+ [:span {:class (stl/css :match-count)}
+ (dm/str (inc safe-match-idx) " / " text-match-count)]
+ [:> icon-button* {:variant "ghost" :aria-label (tr "labels.previous")
+ :on-click navigate-prev :icon i/arrow-up}]
+ [:> icon-button* {:variant "ghost" :aria-label (tr "labels.next")
+ :on-click navigate-next :icon i/arrow-down}]]
+ [:span {:class (stl/css :no-matches)}
+ (tr "workspace.sidebar.layers.no-matches")]))]
+ [:div {:class (stl/css :replace-actions-row)}
+ [:button {:class (stl/css :replace-button)
+ :on-click handle-replace
+ :disabled (or (zero? text-match-count) (str/empty? current-search))}
+ (tr "workspace.sidebar.layers.replace")]
+ [:button {:class (stl/css :replace-button)
+ :on-click handle-replace-all
+ :disabled (or (zero? text-match-count) (str/empty? current-search))}
+ (tr "workspace.sidebar.layers.replace-all")]]])
+
[:div {:class (stl/css :active-filters)}
(for [fkey current-filters]
(let [fname (d/name fkey)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.scss b/frontend/src/app/main/ui/workspace/sidebar/layers.scss
index e89f730323..048aa08549 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/layers.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/layers.scss
@@ -18,65 +18,78 @@
&.search {
padding: 0 deprecated.$s-12 0 deprecated.$s-8;
gap: deprecated.$s-4;
+
.filter-button {
- @include deprecated.flexCenter;
- @include deprecated.buttonStyle;
+ @include deprecated.flex-center;
+ @include deprecated.button-style;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
margin: 0;
border: deprecated.$s-1 solid var(--color-background-tertiary);
border-radius: deprecated.$br-8 deprecated.$br-2 deprecated.$br-2 deprecated.$br-8;
background-color: var(--color-background-tertiary);
+
svg {
height: deprecated.$s-16;
width: deprecated.$s-16;
stroke: var(--icon-foreground);
}
+
&:focus {
border: deprecated.$s-1 solid var(--input-border-color-focus);
outline: 0;
background-color: var(--input-background-color-active);
color: var(--input-foreground-color-active);
+
svg {
background-color: var(--input-background-color-active);
}
}
+
&:hover {
border: deprecated.$s-1 solid var(--input-border-color-hover);
background-color: var(--input-background-color-hover);
+
svg {
background-color: var(--input-background-color-hover);
stroke: var(--button-foreground-hover);
}
}
+
&.opened {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
}
}
.page-name {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
padding: 0 deprecated.$s-12;
color: var(--title-foreground-color);
}
.icon-search {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
border-radius: deprecated.$br-8;
margin-right: deprecated.$s-8;
padding: 0;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
}
.focus-title {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
@@ -85,38 +98,45 @@
}
.back-button {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-24;
padding: 0 deprecated.$s-4 0 deprecated.$s-8;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
transform: rotate(180deg);
}
}
.focus-name {
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
+
padding-left: deprecated.$s-4;
color: var(--title-foreground-color);
}
.focus-mode-tag-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: 100%;
margin-right: deprecated.$s-12;
}
.active-filters {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
flex-wrap: wrap;
margin: 0 deprecated.$s-12;
}
.layer-filter {
- @extend .button-tag;
+ @extend %button-tag;
+
gap: deprecated.$s-6;
height: deprecated.$s-24;
margin: deprecated.$s-2 0;
@@ -131,8 +151,9 @@
}
.layer-filter-name {
- @include deprecated.flexCenter;
- @include deprecated.bodySmallTypography;
+ @include deprecated.flex-center;
+ @include deprecated.body-small-typography;
+
color: var(--pill-foreground-color);
}
@@ -141,12 +162,15 @@
}
.filters-container {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
position: absolute;
left: deprecated.$s-20;
width: deprecated.$s-192;
+
.filter-menu-item {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
justify-content: space-between;
@@ -158,28 +182,34 @@
display: flex;
align-items: center;
gap: deprecated.$s-8;
+
.filter-menu-item-icon {
color: var(--menu-foreground-color);
}
+
.filter-menu-item-name {
padding-top: deprecated.$s-2;
color: var(--menu-foreground-color);
}
}
+
.filter-menu-item-tick {
color: var(--menu-foreground-color);
}
&.selected {
background-color: var(--menu-background-color-selected);
+
.filter-menu-item-name-wrapper {
.filter-menu-item-icon {
color: var(--menu-foreground-color);
}
+
.filter-menu-item-name {
color: var(--menu-foreground-color);
}
}
+
.filter-menu-item-tick {
color: var(--menu-foreground-color);
}
@@ -187,14 +217,17 @@
&:hover {
background-color: var(--menu-background-color-hover);
+
.filter-menu-item-name-wrapper {
.filter-menu-item-icon {
color: var(--menu-foreground-color-hover);
}
+
.filter-menu-item-name {
color: var(--menu-foreground-color-hover);
}
}
+
.filter-menu-item-tick {
color: var(--menu-foreground-color-hover);
}
@@ -204,15 +237,169 @@
.tool-window-content {
--calculated-height: calc(#{deprecated.$s-136} + var(--height, #{deprecated.$s-200}));
+
display: flex;
flex-direction: column;
height: calc(100vh - var(--calculated-height));
width: calc(var(--left-sidebar-width) + var(--depth) * var(--layer-indentation-size));
- overflow-x: auto;
- overflow-y: overlay;
+ overflow: auto;
scrollbar-gutter: stable;
}
+.replace-row {
+ padding: 0 deprecated.$s-12;
+ gap: deprecated.$s-4;
+}
+
+.search-scope-row {
+ display: flex;
+ gap: deprecated.$s-16;
+ padding: deprecated.$s-4 deprecated.$s-12 deprecated.$s-8;
+ align-items: center;
+}
+
+.scope-option {
+ display: flex;
+ align-items: center;
+ gap: deprecated.$s-6;
+ cursor: pointer;
+}
+
+.scope-radio {
+ width: deprecated.$s-12;
+ height: deprecated.$s-12;
+ border: deprecated.$s-1 solid var(--color-foreground-secondary);
+ border-radius: 50%;
+ background-color: transparent;
+ flex-shrink: 0;
+}
+
+.scope-radio-checked {
+ border-color: var(--color-accent-primary);
+ background-color: var(--color-accent-primary);
+ box-shadow: inset 0 0 0 deprecated.$s-2 var(--color-background-primary);
+}
+
+.scope-radio-input {
+ display: none;
+}
+
+.scope-label {
+ @include deprecated.body-small-typography;
+
+ color: var(--color-foreground-secondary);
+ cursor: pointer;
+}
+
+.scope-selected .scope-label {
+ color: var(--color-foreground-primary);
+}
+
+.replace-actions-row {
+ display: flex;
+ gap: deprecated.$s-4;
+ padding: 0 deprecated.$s-12 deprecated.$s-8;
+}
+
+.replace-input-wrapper {
+ @include deprecated.flex-center;
+
+ flex: 1;
+ height: deprecated.$s-32;
+ border: deprecated.$s-1 solid var(--search-bar-input-border-color);
+ border-radius: deprecated.$br-8;
+ background-color: var(--search-bar-input-background-color);
+
+ &:hover {
+ border: deprecated.$s-1 solid var(--input-border-color-hover);
+ background-color: var(--input-background-color-hover);
+
+ .replace-input {
+ background-color: var(--input-background-color-hover);
+ }
+ }
+
+ &:focus-within {
+ background-color: var(--input-background-color-active);
+ color: var(--input-foreground-color-active);
+ border: deprecated.$s-1 solid var(--input-border-color-focus);
+
+ .replace-input {
+ background-color: var(--input-background-color-active);
+ }
+ }
+}
+
+.replace-input {
+ width: 100%;
+ height: 100%;
+ margin: 0 deprecated.$s-8;
+ border: 0;
+ background-color: var(--input-background-color);
+ font-size: deprecated.$fs-12;
+ color: var(--input-foreground-color);
+ border-radius: deprecated.$br-8;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.replace-button {
+ @include deprecated.body-small-typography;
+ @include deprecated.button-style;
+
+ flex: 1;
+ height: deprecated.$s-28;
+ padding: 0 deprecated.$s-8;
+ border: deprecated.$s-1 solid var(--color-background-tertiary);
+ border-radius: deprecated.$br-8;
+ background-color: var(--color-background-tertiary);
+ color: var(--color-foreground-primary);
+ white-space: nowrap;
+ text-transform: uppercase;
+
+ &:hover:not(:disabled) {
+ border: deprecated.$s-1 solid var(--input-border-color-hover);
+ background-color: var(--input-background-color-hover);
+ }
+
+ &:disabled {
+ opacity: 0.4;
+ cursor: default;
+ }
+}
+
+.match-navigation {
+ display: flex;
+ align-items: center;
+ gap: deprecated.$s-2;
+ flex-shrink: 0;
+}
+
+.match-count {
+ @include deprecated.body-small-typography;
+
+ color: var(--color-foreground-secondary);
+ white-space: nowrap;
+}
+
+.no-matches {
+ @include deprecated.body-small-typography;
+
+ color: var(--color-foreground-secondary);
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.clear-icon {
+ @extend %button-tag;
+
+ flex: 0 0 deprecated.$s-32;
+ height: 100%;
+ color: var(--color-icon-default);
+}
+
.element-list {
display: grid;
position: relative;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.scss b/frontend/src/app/main/ui/workspace/sidebar/options.scss
index 8a819471e8..b7428196ab 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options.scss
@@ -18,8 +18,7 @@
}
.content-class {
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
height: calc(100vh - #{$sz-96});
scrollbar-gutter: stable;
}
@@ -29,9 +28,11 @@
flex-direction: column;
gap: var(--sp-s);
width: 100%;
+
/* FIXME: This is hacky and prone to break, we should tackle the whole layout
of the sidebar differently */
--sidebar-element-options-height: calc(100vh - #{$sz-88});
+
height: var(--sidebar-element-options-height);
padding-block-start: var(--sp-s);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
index 7ea8e42132..2f6d8a586c 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs
@@ -7,6 +7,8 @@
(ns app.main.ui.workspace.sidebar.options.common
(:require-macros [app.main.style :as stl])
(:require
+ [app.main.data.workspace.tokens.application :as dwta]
+ [app.main.store :as st]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@@ -24,3 +26,13 @@
:ref ref}
children])))
+(defn emit-value-or-token [value emit-value-fn ids attrs]
+ (if (or (string? value)
+ (number? value)
+ (nil? value))
+ (emit-value-fn value)
+ (st/emit!
+ (dwta/toggle-token {:token (first value)
+ :attrs attrs
+ :shape-ids ids}))))
+
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs
index 275ad11e8d..fa40189182 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs
@@ -13,8 +13,10 @@
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
+ [app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
+ [app.main.ui.workspace.sidebar.options.menus.measures :as measures]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -31,11 +33,35 @@
selected-preset-name
(deref selected-preset-name*)
- on-open
- (mf/use-fn (fn [] (reset! show* true)))
+ search-term*
+ (mf/use-state "")
+
+ search-term
+ (deref search-term*)
+
+ container-ref
+ (mf/use-ref nil)
+
+ on-toggle
+ (mf/use-fn
+ (fn []
+ (swap! show* not)
+ (reset! search-term* "")))
on-close
- (mf/use-fn (fn [] (reset! show* false)))
+ (mf/use-fn
+ (fn []
+ (reset! show* false)
+ (reset! search-term* "")))
+
+ on-search-change
+ (mf/use-fn
+ (fn [value _event]
+ (reset! search-term* value)))
+
+ filtered-presets
+ (mf/with-memo [search-term]
+ (measures/filter-size-presets search-term size-presets))
on-preset-selected
(mf/use-fn
@@ -48,7 +74,9 @@
(d/read-string))]
(reset! selected-preset-name* name)
- (st/emit! (dwd/set-default-size width height)))))
+ (st/emit! (dwd/set-default-size width height))
+ (reset! show* false)
+ (reset! search-term* ""))))
orientation
(when (:width drawing-state)
@@ -65,35 +93,49 @@
[:div {:class (stl/css :presets)}
[:div {:class (stl/css-case :presets-wrapper true
:opened show?)
- :on-click on-open}
+ :ref container-ref
+ :on-click on-toggle}
[:span {:class (stl/css :select-name)}
(or selected-preset-name
(tr "workspace.options.size-presets"))]
[:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow]
[:& dropdown {:show show?
- :on-close on-close}
- [:ul {:class (stl/css :custom-select-dropdown)}
- (for [preset size-presets]
- (if-not (:width preset)
- [:li {:key (:name preset)
- :class (stl/css-case :dropdown-element true
- :disabled true)}
- [:span {:class (stl/css :preset-name)} (:name preset)]]
+ :on-close on-close
+ :container container-ref}
+ [:div {:class (stl/css :custom-select-dropdown)
+ :on-click dom/stop-propagation}
+ [:div {:class (stl/css :preset-search)}
+ [:> search-bar* {:on-change on-search-change
+ :value search-term
+ :auto-focus true
+ :placeholder (tr "workspace.options.search-size-preset")}]]
+ [:ul {:class (stl/css :preset-list)}
+ (if (empty? filtered-presets)
+ [:li {:class (stl/css-case :dropdown-element true
+ :disabled true)}
+ [:span {:class (stl/css :preset-name)}
+ (tr "workspace.options.no-size-preset-results")]]
+ (for [preset filtered-presets]
+ (if-not (:width preset)
+ [:li {:key (:name preset)
+ :class (stl/css-case :dropdown-element true
+ :disabled true)}
+ [:span {:class (stl/css :preset-name)} (:name preset)]]
- (let [preset-match (and (= (:width preset) (:width drawing-state))
- (= (:height preset) (:height drawing-state)))]
- [:li {:key (:name preset)
- :class (stl/css-case :dropdown-element true
- :match preset-match)
- :data-width (str (:width preset))
- :data-height (str (:height preset))
- :data-name (:name preset)
- :on-click on-preset-selected}
- [:div {:class (stl/css :name-wrapper)}
- [:span {:class (stl/css :preset-name)} (:name preset)]
- [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]]
- (when preset-match
- [:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]]
+ (let [preset-match (and (= (:width preset) (:width drawing-state))
+ (= (:height preset) (:height drawing-state)))]
+ [:li {:key (:name preset)
+ :class (stl/css-case :dropdown-element true
+ :match preset-match)
+ :data-width (str (:width preset))
+ :data-height (str (:height preset))
+ :data-name (:name preset)
+ :on-click on-preset-selected}
+ [:div {:class (stl/css :name-wrapper)}
+ [:span {:class (stl/css :preset-name)} (:name preset)]
+ [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]]
+ (when preset-match
+ [:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]]
[:& radio-buttons {:selected (or (d/name orientation) "")
:on-change on-orientation-change
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss
index d66e5de852..53221cd2a5 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss
@@ -10,11 +10,13 @@
.presets {
@include sidebar.option-grid-structure;
+
grid-column: 1 / -1;
}
.presets-wrapper {
- @extend .asset-element;
+ @extend %asset-element;
+
position: relative;
grid-column: span 6;
display: flex;
@@ -23,10 +25,13 @@
border-radius: deprecated.$br-8;
.collapsed-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
cursor: pointer;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
transform: rotate(90deg);
}
@@ -44,7 +49,8 @@
}
.select-name {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
justify-content: flex-start;
align-items: center;
@@ -53,28 +59,52 @@
}
.custom-select-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
margin-top: deprecated.$s-2;
max-height: 70vh;
width: deprecated.$s-252;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.preset-search {
+ padding: deprecated.$s-4;
+ border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent);
+}
+
+.preset-list {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+
.dropdown-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
.name-wrapper {
display: flex;
gap: deprecated.$s-8;
flex-grow: 1;
+
.preset-name {
color: var(--menu-foreground-color-rest);
}
+
.preset-size {
color: var(--menu-foreground-color-rest);
}
}
.check-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
@@ -82,6 +112,7 @@
&.disabled {
pointer-events: none;
cursor: default;
+
.preset-name {
color: var(--menu-foreground-color);
}
@@ -91,6 +122,7 @@
.name-wrapper .preset-name {
color: var(--menu-foreground-color-hover);
}
+
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
@@ -98,9 +130,11 @@
&:hover {
background-color: var(--menu-background-color-hover);
+
.name-wrapper .preset-name {
color: var(--menu-foreground-color-hover);
}
+
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss
index 6535f728b4..698698ec9d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/align.scss
@@ -9,14 +9,15 @@
.align-options {
@include sidebar.option-grid-structure;
+
height: deprecated.$s-32;
}
+
.align-group-horizontal,
.align-group-vertical {
display: grid;
grid-template-columns: subgrid;
- align-items: center;
- justify-items: center;
+ place-items: center center;
}
.align-group-horizontal {
@@ -28,22 +29,29 @@
}
.align-button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
padding: 0;
border-radius: deprecated.$br-8;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
+
&.disabled {
cursor: default;
+
svg {
stroke: var(--button-foreground-color-disabled);
}
+
&:hover {
background-color: var(--panel-background-color);
+
svg {
stroke: var(--button-foreground-color-disabled);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss
index c80e57e1ec..2d1c6263b7 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss
@@ -21,7 +21,8 @@
}
.element-set-content {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
margin-bottom: deprecated.$s-8;
}
@@ -36,25 +37,32 @@
flex-grow: 1;
border-radius: deprecated.$br-8;
background-color: var(--input-details-color);
+
.show-more {
- @extend .button-secondary;
+ @extend %button-secondary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
box-sizing: border-box;
border: deprecated.$s-1 solid var(--button-secondary-background-color-rest);
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
+
&.selected {
background-color: var(--button-radio-background-color-active);
+
svg {
stroke: var(--button-radio-foreground-color-active);
}
}
}
+
.label {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
flex-grow: 1;
display: flex;
align-items: center;
@@ -66,24 +74,30 @@
box-sizing: border-box;
border: deprecated.$s-1 solid var(--input-border-color);
}
+
.blur-type-select {
flex-grow: 1;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
}
}
+
.actions {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
&.hidden {
.blur-info {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
.show-more {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
border: deprecated.$s-1 solid var(--input-border-color-disabled);
}
+
.label {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
border: deprecated.$s-1 solid var(--input-border-color-disabled);
}
}
@@ -91,9 +105,11 @@
}
.second-row {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
width: deprecated.$s-92;
+
.label {
padding-left: deprecated.$s-8;
width: deprecated.$s-60;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss
index ef69c2dd4e..270b922093 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/bool.scss
@@ -9,6 +9,7 @@
.boolean-options {
@include sidebar.option-grid-structure;
+
height: var(--sp-xxxl);
}
@@ -19,26 +20,31 @@
}
.flatten-button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
border-radius: deprecated.$br-8;
grid-column: 5 / span 1;
+
--flatten-icon-foreground-color: var(--icon-foreground);
&.disabled {
cursor: default;
+
--flatten-icon-foreground-color: var(--button-foreground-color-disabled);
&:hover {
background-color: var(--panel-background-color);
+
--flatten-icon-foreground-color: var(--button-foreground-color-disabled);
}
}
}
.flatten-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--flatten-icon-foreground-color);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs
index d1d7a55fc5..16feaa68d0 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs
@@ -11,6 +11,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
+ [app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
@@ -130,26 +131,21 @@
(mf/use-fn
(mf/deps change-radius ids)
(fn [value]
- (if (or (string? value) (number? value))
- (st/emit!
- (change-radius (fn [shape]
- (ctsr/set-radius-to-all-corners shape value))))
- (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs #{:r1 :r2 :r3 :r4}
- :shape-ids ids})))))
-
+ (soc/emit-value-or-token
+ value
+ #(st/emit! (change-radius (fn [shape] (ctsr/set-radius-to-all-corners shape %))))
+ ids
+ #{:r1 :r2 :r3 :r4})))
on-single-radius-change
(mf/use-fn
(mf/deps change-one-radius ids)
(fn [value attr]
- (if (or (string? value) (number? value))
- (st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr))
- (st/emit! (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs #{attr}
- :shape-ids ids}))))))
+ (soc/emit-value-or-token
+ value
+ #(st/emit! (change-one-radius (fn [shape] (ctsr/set-radius-to-single-corner shape attr %)) attr))
+ ids
+ #{attr})))
on-radius-r1-change #(on-single-radius-change % :r1)
on-radius-r2-change #(on-single-radius-change % :r2)
@@ -168,64 +164,50 @@
(mf/with-effect [ids]
(reset! radius-expanded* false))
- [:section {:class (dm/str class " " (stl/css :radius))
- :aria-label "border-radius-section"}
- (if (not radius-expanded)
- (if token-numeric-inputs
- [:> numeric-input-wrapper*
- {:on-change on-all-radius-change
- :on-detach on-detach-all
- :icon i/corner-radius
- :min 0
- :attr :border-radius
- :nillable true
- :property (tr "workspace.options.radius")
- :applied-token (cond
- (not (seq applied-tokens))
- nil
+ (if token-numeric-inputs
+ [:section {:class (dm/str class " " (stl/css :radius-token))
+ :aria-label (tr "workspace.options.radius.radius-section")}
+ [:div {:class (stl/css :radius-first-row)}
+ [:> numeric-input-wrapper*
+ {:on-change on-all-radius-change
+ :on-detach on-detach-all
+ :icon i/corner-radius
+ :min 0
+ :attr :border-radius
+ :nillable true
+ :property (tr "workspace.options.radius")
+ :applied-token (cond
+ (not (seq applied-tokens))
+ nil
- (or (not all-values-equal?) (not all-token-equal?))
- :multiple
+ (or (not all-values-equal?) (not all-token-equal?))
+ :multiple
- :else
- (get applied-tokens :r1))
- :align :right
- :placeholder (cond
- (or (not all-values-equal?)
- (not all-token-equal?))
- (tr "settings.multiple")
- :else
- "--")
- :value (if all-values-equal?
- (if (nil? (:r1 values))
- 0
- (:r1 values))
- nil)}]
-
- [:div {:class (stl/css :radius-1)
- :title (tr "workspace.options.radius")}
- [:> icon* {:icon-id i/corner-radius
- :size "s"
- :class (stl/css :icon)}]
- [:> deprecated-input/numeric-input*
- {:placeholder (cond
- (not all-values-equal?)
- (tr "settings.multiple")
- (= :multiple (:r1 values))
- (tr "settings.multiple")
:else
- "--")
- :min 0
- :nillable true
- :on-change on-all-radius-change
- :value (if all-values-equal?
- (if (nil? (:r1 values))
- 0
- (:r1 values))
- nil)}]])
+ (get applied-tokens :r1))
+ :align :right
+ :placeholder (cond
+ (or (not all-values-equal?)
+ (not all-token-equal?))
+ (tr "settings.multiple")
+ :else
+ "--")
+ :value (if all-values-equal?
+ (if (nil? (:r1 values))
+ 0
+ (:r1 values))
+ nil)}]
+ [:> icon-button* {:class (stl/css-case :selected radius-expanded)
+ :variant "ghost"
+ :tooltip-placement "top-left"
+ :on-click toggle-radius-mode
+ :aria-label (if radius-expanded
+ (tr "workspace.options.radius.hide-all-corners")
+ (tr "workspace.options.radius.show-single-corners"))
+ :icon i/corner-radius}]]
- (if token-numeric-inputs
- [:div {:class (stl/css :radius-4)}
+ (when radius-expanded
+ [:div {:class (stl/css :radius-4-token)}
[:> numeric-input-wrapper*
{:on-change on-radius-r1-change
:on-detach on-detach-r1
@@ -253,6 +235,7 @@
:property (tr "workspace.options.radius-top-right")
:applied-token (get applied-tokens :r2)
:align :right
+ :tooltip-placement "top-left"
:inner-class (stl/css :no-icon-input)
:placeholder (cond
(or (= :multiple (get applied-tokens :r2))
@@ -297,9 +280,33 @@
"--")
:align :right
:class (stl/css :radius-wrapper)
+ :tooltip-placement "top-left"
:inner-class (stl/css :no-icon-input)
- :value (:r3 values)}]]
-
+ :value (:r3 values)}]])]
+ [:section {:class (dm/str class " " (stl/css :radius))
+ :aria-label (tr "workspace.options.radius.radius-section")}
+ (if (not radius-expanded)
+ [:div {:class (stl/css :radius-1)
+ :title (tr "workspace.options.radius")}
+ [:> icon* {:icon-id i/corner-radius
+ :size "s"
+ :class (stl/css :icon)}]
+ [:> deprecated-input/numeric-input*
+ {:placeholder (cond
+ (not all-values-equal?)
+ (tr "settings.multiple")
+ (= :multiple (:r1 values))
+ (tr "settings.multiple")
+ :else
+ "--")
+ :min 0
+ :nillable true
+ :on-change on-all-radius-change
+ :value (if all-values-equal?
+ (if (nil? (:r1 values))
+ 0
+ (:r1 values))
+ nil)}]]
[:div {:class (stl/css :radius-4)}
[:div {:class (stl/css :small-input)}
[:> deprecated-input/numeric-input*
@@ -331,12 +338,11 @@
:title (tr "workspace.options.radius-bottom-right")
:min 0
:on-change on-radius-r3-change
- :value (:r3 values)}]]]))
-
- [:> icon-button* {:class (stl/css-case :selected radius-expanded)
- :variant "ghost"
- :on-click toggle-radius-mode
- :aria-label (if radius-expanded
- (tr "workspace.options.radius.hide-all-corners")
- (tr "workspace.options.radius.show-single-corners"))
- :icon i/corner-radius}]]))
+ :value (:r3 values)}]]])
+ [:> icon-button* {:class (stl/css-case :selected radius-expanded)
+ :variant "ghost"
+ :on-click toggle-radius-mode
+ :aria-label (if radius-expanded
+ (tr "workspace.options.radius.hide-all-corners")
+ (tr "workspace.options.radius.show-single-corners"))
+ :icon i/corner-radius}]])))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss
index 5473314c67..e98b076c02 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.scss
@@ -14,8 +14,9 @@
gap: var(--sp-xs);
}
-.radius-1 {
- @extend .input-element;
+.radius-1,
+.small-input {
+ @extend %input-element;
@include t.use-typography("body-small");
}
@@ -25,23 +26,12 @@
gap: var(--sp-xs);
}
-.small-input {
- @extend .input-element;
- @include t.use-typography("body-small");
-}
-
.selected {
border-color: var(--button-icon-border-color-selected);
background-color: var(--button-icon-background-color-selected);
color: var(--color-accent-primary);
}
-.selected {
- border-color: var(--button-icon-border-color-selected);
- background-color: var(--button-icon-background-color-selected);
- color: var(--button-icon-foreground-color-selected);
-}
-
.icon {
margin-inline: var(--sp-xs);
}
@@ -53,3 +43,20 @@
.dropdown-offset {
--dropdown-offset: #{px2rem(-65)};
}
+
+.radius-token {
+ display: grid;
+ gap: var(--sp-xs);
+}
+
+.radius-first-row {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: var(--sp-xs);
+}
+
+.radius-4-token {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sp-xs);
+}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs
index 0c60429d98..06b3fac9d2 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs
@@ -188,9 +188,10 @@
[color-operations _] (retrieve-color-operations groups old-color prev-colors)]
(mf/set-ref-val! prev-colors-ref
(conj prev-colors color))
- (st/emit! (dwta/apply-token-on-selected color-operations token)))))]
+ (st/emit! (dwta/apply-token-on-color-selected color-operations token)))))]
- [:div {:class (stl/css :element-set)}
+ [:section {:class (stl/css :element-set)
+ :aria-label (tr "workspace.options.selection-color.section")}
[:div {:class (stl/css :element-title)}
[:> title-bar* {:collapsable has-colors?
:collapsed (not open?)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss
index 7519bcb568..d138c82ac2 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.scss
@@ -21,26 +21,31 @@
}
.add-fill {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
.element-content {
grid-column: span 8;
- @include deprecated.flexColumn;
+
+ @include deprecated.flex-column;
+
margin-bottom: deprecated.$s-8;
}
.selected-color-group {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
}
.more-colors-btn {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
height: deprecated.$s-32;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss
index 88ca5dd5b6..521c409223 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss
@@ -14,6 +14,7 @@
.annotation {
@include t.use-typography("body-small");
+
grid-column: span 8;
color: var(--color-foreground-secondary);
border-radius: $br-8;
@@ -123,7 +124,7 @@
// easy way to plop the elements on top of each other and have them both sized based on the tallest one's height
display: grid;
- &:after {
+ &::after {
// The space is needed to preventy jumpy behavior
content: attr(data-replicated-value) " ";
white-space: pre-wrap;
@@ -132,7 +133,6 @@
/* Identical styling required!! */
font: inherit;
overflow-wrap: anywhere;
-
padding: var(--sp-m);
/* Place on top of each other */
@@ -143,20 +143,15 @@
.annotation-textarea {
background-color: var(--color-background-primary);
color: var(--color-foreground-primary);
- padding: var(--sp-m);
-
border: none;
overflow: hidden;
outline: none;
-
box-shadow: none;
-
resize: none;
/* Identical styling required!! */
font: inherit;
overflow-wrap: anywhere;
-
padding: var(--sp-m);
/* Place on top of each other */
@@ -165,6 +160,7 @@
.annotation-counter {
@include t.use-typography("body-small");
+
text-align: right;
color: var(--color-foreground-secondary);
margin: 0 var(--sp-s) var(--sp-s) 0;
@@ -181,6 +177,7 @@
--swap-item-thumbnail-background-color: var(--color-canvas);
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
padding: px2rem(1) var(--sp-m) px2rem(1) px2rem(1);
@@ -237,8 +234,6 @@
--swap-item-thumbnail-background-color-disabled: var(--color-foreground-secondary);
display: flex;
- justify-content: center;
- align-items: center;
place-items: center;
aspect-ratio: 1 / 1;
flex-wrap: wrap;
@@ -257,6 +252,7 @@
.swap-item-name {
@include t.use-typography("body-small");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -295,10 +291,8 @@
&::before {
content: " ";
position: absolute;
- inset-inline-start: 0;
- inset-inline-end: 0;
- inset-block-start: 0;
- inset-block-end: 0;
+ inset-inline: 0;
+ inset-block: 0;
border: calc($b-2 * 2) solid var(--swap-item-border-inner-color-selected);
border-radius: $br-8;
}
@@ -334,6 +328,7 @@
.swap-group {
@include t.use-typography("body-small");
+
cursor: pointer;
display: grid;
grid-template-columns: 1fr var(--sp-m);
@@ -365,6 +360,7 @@
.swap-title {
@include t.use-typography("headline-small");
+
display: flex;
align-items: center;
block-size: $sz-32;
@@ -397,6 +393,7 @@
.swap-library-name {
@include t.use-typography("body-small");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -419,6 +416,7 @@
.swap-library-back-name {
@include t.use-typography("body-small");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -429,6 +427,7 @@
.swap-library-empty {
@include t.use-typography("body-small");
+
margin: 0 var(--sp-xs) 0 var(--sp-s);
color: var(--color-foreground-secondary);
}
@@ -457,6 +456,7 @@
.component-title-swap {
@include t.use-typography("headline-small");
+
cursor: pointer;
display: flex;
align-items: center;
@@ -482,6 +482,7 @@
.component-title-bar-type {
@include t.use-typography("body-small");
+
block-size: 100%;
display: flex;
align-items: center;
@@ -498,8 +499,7 @@
display: flex;
flex-direction: column;
row-gap: var(--sp-m);
- padding-block-start: var(--sp-xs);
- padding-block-end: var(--sp-s);
+ padding-block: var(--sp-xs) var(--sp-s);
}
.component-pill {
@@ -562,6 +562,7 @@
.pill-btn-text {
@include t.use-typography("body-small");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -572,6 +573,7 @@
.pill-btn-subtext {
@include t.use-typography("body-small");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -587,19 +589,21 @@
}
.pill-actions-btn {
- @extend .button-secondary;
+ @extend %button-secondary;
+
cursor: unset;
block-size: 100%;
inline-size: 100%;
border-radius: 0 $br-8 $br-8 0;
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
.pill-actions-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
inline-size: $sz-252;
inset-inline-end: 0;
inset-inline-start: unset;
@@ -610,12 +614,11 @@
}
.pill-actions-dropdown-item {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
}
.variant-property-list {
grid-column: span 8;
-
display: grid;
flex-direction: column;
gap: var(--sp-xs);
@@ -672,6 +675,7 @@
.variant-property-name {
@include t.use-typography("body-small");
+
margin-inline-start: var(--sp-s);
color: var(--color-foreground-secondary);
display: block;
@@ -682,8 +686,8 @@
.variant-warning {
@include t.use-typography("body-small");
- grid-column: span 8;
+ grid-column: span 8;
border: $b-1 solid var(--color-background-quaternary);
border-radius: $br-8;
padding: var(--sp-m);
@@ -702,6 +706,7 @@
.variant-warning-button {
@include t.use-typography("body-small");
+
cursor: pointer;
background-color: transparent;
border: none;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss
index 5f7578afe1..e0fbb84843 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.scss
@@ -18,12 +18,7 @@
.constraints-widget {
background-color: var(--constraint-widget-background-color);
display: grid;
- grid-template-columns: deprecated.$s-24 deprecated.$s-60 deprecated.$s-24;
- grid-template-rows: deprecated.$s-24 deprecated.$s-60 deprecated.$s-24;
- grid-template-areas:
- "top top top"
- "left center right"
- "bottom bottom bottom";
+ grid-template: "top top top" deprecated.$s-24 "left center right" deprecated.$s-60 "bottom bottom bottom" deprecated.$s-24 / deprecated.$s-24 deprecated.$s-60 deprecated.$s-24;
height: deprecated.$s-108;
width: deprecated.$s-108;
border-radius: deprecated.$br-8;
@@ -34,22 +29,28 @@
.constraints-center,
.constraints-right,
.constraints-bottom {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
grid-area: top;
}
+
.constraint-btn,
.constraint-btn-special,
.constraint-btn-rotated {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
width: 100%;
height: 100%;
+
--resalted-area-background-color: var(--button-constraint-background-color-rest);
--resalted-area-border-color: none;
+
&.active {
--resalted-area-border-color: var(--button-constraint-border-color-hover);
--resalted-area-background-color: var(--button-constraint-background-color-hover);
}
+
&:hover,
&:focus-visible {
--resalted-area-border-color: var(--button-constraint-border-color-hover);
@@ -69,9 +70,11 @@
.constraints-left {
grid-area: left;
+
.constraint-btn-rotated {
height: deprecated.$s-60;
width: deprecated.$s-24;
+
.resalted-area {
height: deprecated.$s-32;
width: deprecated.$s-3;
@@ -84,18 +87,22 @@
position: relative;
background-color: var(--constraint-center-area-background-color);
border-radius: deprecated.$br-8;
+
.constraint-btn {
width: deprecated.$s-60;
height: deprecated.$s-24;
+
.resalted-area {
width: deprecated.$s-32;
height: deprecated.$s-3;
}
}
+
.constraint-btn-special {
position: absolute;
height: deprecated.$s-60;
width: deprecated.$s-24;
+
.resalted-area {
height: deprecated.$s-32;
width: deprecated.$s-3;
@@ -105,9 +112,11 @@
.constraints-right {
grid-area: right;
+
.constraint-btn-rotated {
height: deprecated.$s-72;
width: deprecated.$s-24;
+
.resalted-area {
height: deprecated.$s-32;
width: deprecated.$s-3;
@@ -137,33 +146,42 @@
margin-bottom: deprecated.$s-8;
margin-top: deprecated.$s-8;
padding-left: 0;
+
input {
margin: 0;
}
label {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
gap: deprecated.$s-2;
cursor: pointer;
color: var(--input-checkbox-text-foreground-color);
+
.check-mark {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-16;
height: deprecated.$s-16;
border-radius: deprecated.$br-6;
background-color: var(--input-checkbox-inactive-background-color);
+
&.checked {
background-color: var(--input-checkbox-background-color-active);
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--input-details-color);
}
}
+
&:hover {
border-color: var(--input-checkbox-border-color-hover);
}
+
&:focus {
border-color: var(--input-checkbox-border-color-focus);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss
index 487e4805bc..f224403269 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/exports.scss
@@ -22,17 +22,21 @@
.element-set-content {
@include sidebar.option-grid-structure;
+
gap: var(--sp-xs);
}
.multiple-exports {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
grid-column: 1 / span 9;
+
.label {
- @extend .mixed-bar;
+ @extend %mixed-bar;
}
+
.actions {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
}
@@ -62,6 +66,7 @@
.size-select {
grid-column: span 2;
padding: 0;
+
.dropdown-upwards {
bottom: deprecated.$s-36;
top: unset;
@@ -71,13 +76,15 @@
.suffix-input {
grid-column: span 3;
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+
+ @extend %input-element;
+ @include deprecated.body-small-typography;
}
.export-btn {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
grid-column: 1 / span 9;
height: deprecated.$s-32;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
index 67d6d1370d..c420053c43 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs
@@ -52,16 +52,15 @@
[n-props o-props]
(and (identical? (unchecked-get n-props "ids")
(unchecked-get o-props "ids"))
+ (identical? (unchecked-get n-props "appliedTokens")
+ (unchecked-get o-props "appliedTokens"))
(let [o-vals (unchecked-get o-props "values")
n-vals (unchecked-get n-props "values")
o-fills (get o-vals :fills)
n-fills (get n-vals :fills)
- o-applied-tokens (get o-vals :applied-tokens)
- n-applied-tokens (get n-vals :applied-tokens)
o-hide (get o-vals :hide-fill-on-export)
n-hide (get n-vals :hide-fill-on-export)]
(and (identical? o-hide n-hide)
- (identical? o-applied-tokens n-applied-tokens)
(identical? o-fills n-fills)))))
(mf/defc fill-menu*
@@ -174,10 +173,10 @@
(mf/deps ids)
(fn [_ token]
(st/emit!
- (dwta/toggle-token {:token token
- :attrs #{:fill}
- :shape-ids ids
- :expand-with-children true}))))
+ (dwta/apply-token-from-input {:token token
+ :attrs #{:fill}
+ :shape-ids ids
+ :expand-with-children true}))))
on-detach-token
(mf/use-fn
@@ -196,7 +195,7 @@
(dom/remove-attribute! checkbox "indeterminate"))))
[:section {:class (stl/css :fill-section)
- :aria-label "Fill section"}
+ :aria-label (tr "workspace.options.fill.section")}
[:div {:class (stl/css :fill-title)}
[:> title-bar* {:collapsable has-fills?
:collapsed (not open?)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss
index 28a159b4de..a9d920386e 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.scss
@@ -24,7 +24,6 @@
.fill-content {
grid-column: span 8;
-
display: flex;
flex-direction: column;
gap: var(--sp-m);
@@ -39,6 +38,7 @@
.fill-multiple-label {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
flex-grow: 1;
@@ -51,12 +51,16 @@
.fill-checkbox {
// TODO create a checkbox component in the DS
- @extend .input-checkbox;
+ @extend %input-checkbox;
+
padding-inline-start: var(--sp-s);
+
span.checked {
background-color: var(--color-accent-primary);
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--color-background-primary);
}
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss
index 51b82c5434..75b108f768 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/frame_grid.scss
@@ -21,7 +21,8 @@
}
.element-set-content {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
grid-column: span 8;
margin: deprecated.$s-4 0 deprecated.$s-8 0;
}
@@ -37,70 +38,87 @@
gap: deprecated.$s-1;
border-radius: deprecated.$br-8;
background-color: var(--input-details-color);
+
.show-options {
- @extend .button-secondary;
+ @extend %button-secondary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
box-sizing: border-box;
border: deprecated.$s-1 solid var(--input-border-color);
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
+
.type-select-wrapper {
flex-grow: 1;
width: deprecated.$s-96;
padding: 0;
border-radius: 0;
height: deprecated.$s-32;
+
.grid-type-select {
border-radius: 0;
height: 100%;
box-sizing: border-box;
border: deprecated.$s-1 solid var(--input-border-color);
+
&:hover {
border: deprecated.$s-1 solid var(--input-border-color-hover);
}
}
}
+
.grid-size {
- @extend .asset-element;
+ @extend %asset-element;
+
width: deprecated.$s-60;
margin: 0;
padding: 0;
padding-left: deprecated.$s-8;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
+
.numeric-input {
- @extend .input-base;
- @include deprecated.bodySmallTypography;
+ @extend %input-base;
+ @include deprecated.body-small-typography;
}
}
+
.editable-select-wrapper {
- @extend .asset-element;
+ @extend %asset-element;
+
width: deprecated.$s-60;
margin: 0;
padding: 0;
position: relative;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
+
.column-select {
height: deprecated.$s-32;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
box-sizing: border-box;
border: deprecated.$s-1 solid var(--input-border-color);
+
.numeric-input {
- @extend .input-base;
- @include deprecated.bodySmallTypography;
+ @extend %input-base;
+ @include deprecated.body-small-typography;
+
margin: 0;
padding: 0;
}
+
span {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
}
@@ -108,39 +126,52 @@
&.hidden {
.show-options {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
border: deprecated.$s-1 solid var(--input-border-color-disabled);
}
+
.type-select-wrapper,
.editable-select-wrapper {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
.column-select,
.grid-type-select {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
border: deprecated.$s-1 solid var(--input-border-color-disabled);
}
+
.column-select {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
+
.numeric-input {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
}
}
}
+
.grid-size {
- @include deprecated.hiddenElement;
+ @include deprecated.hidden-element;
+
border: deprecated.$s-1 solid var(--input-border-color-disabled);
+
.icon {
stroke: var(--input-foreground-color-disabled);
}
+
.numeric-input {
color: var(--input-foreground-color-disabled);
}
}
+
.actions {
.hidden-btn,
.lock-btn {
background-color: transparent;
+
svg {
stroke: var(--input-foreground-color-disabled);
}
@@ -150,18 +181,21 @@
}
.actions {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
grid-column: span 2;
}
.grid-advanced-options {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
margin-top: deprecated.$s-4;
}
.column-row,
.square-row {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
position: relative;
}
@@ -169,35 +203,45 @@
position: relative;
display: flex;
gap: deprecated.$s-4;
+
.orientation-select-wrapper {
width: deprecated.$s-92;
padding: 0;
}
+
.color-wrapper {
width: deprecated.$s-156;
}
+
.show-more-options {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-32;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
+
.height {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
.icon-text {
padding-top: deprecated.$s-1;
}
}
+
.gutter,
.margin {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
.icon {
&.rotated svg {
transform: rotate(90deg);
@@ -206,8 +250,9 @@
}
.more-options {
- @include deprecated.menuShadow;
- @include deprecated.flexColumn;
+ @include deprecated.menu-shadow;
+ @include deprecated.flex-column;
+
position: absolute;
top: calc(deprecated.$s-2 + deprecated.$s-28);
right: 0;
@@ -220,8 +265,10 @@
z-index: deprecated.$z-index-4;
overflow-y: auto;
background-color: var(--menu-background-color);
+
.option-btn {
- @include deprecated.buttonStyle;
+ @include deprecated.button-style;
+
display: flex;
align-items: center;
height: deprecated.$s-32;
@@ -238,13 +285,16 @@
}
.second-row {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
left: unset;
right: 0;
width: deprecated.$s-108;
+
.btn-options {
- @include deprecated.buttonStyle;
- @extend .dropdown-element-base;
+ @include deprecated.button-style;
+ @extend %dropdown-element-base;
+
width: 100%;
}
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss
index 5b61b4dabf..9e4aa4ca3b 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.scss
@@ -8,7 +8,8 @@
@use "../../../sidebar/common/sidebar.scss" as sidebar;
.grid-cell-menu-container {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
margin-top: deprecated.$s-8;
gap: deprecated.$s-16;
}
@@ -27,7 +28,7 @@
}
.row {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
}
.cell-mode :global(label) {
@@ -35,34 +36,39 @@
}
.edit-grid-btn {
- @extend .button-secondary;
- @include deprecated.uppercaseTitleTipography;
+ @extend %button-secondary;
+ @include deprecated.uppercase-title-typography;
+
width: 100%;
padding: deprecated.$s-8;
}
.area-input {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
width: 100%;
padding: deprecated.$s-8;
}
.grid-coord-group {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
border-radius: deprecated.$br-8;
padding-left: deprecated.$s-4;
background-color: var(--input-background-color);
}
.icon svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
.coord-input {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
border-left: deprecated.$s-1 solid var(--panel-background-color);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs
index be7c58ebb2..5ca03e3d29 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.cljs
@@ -1,9 +1,9 @@
(ns app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens
(:require-macros [app.main.style :as stl])
(:require
- [app.common.types.token :as tk]
[app.main.ui.context :as muc]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
+ [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -11,11 +11,8 @@
[{:keys [value attr applied-token align on-detach placeholder input-type class] :rest props}]
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
- tokens (mf/with-memo [tokens input-type]
- (delay
- (-> (deref tokens)
- (select-keys (get tk/tokens-by-input (or input-type attr)))
- (not-empty))))
+ tokens (mf/with-memo [tokens input-type attr]
+ (csu/filter-tokens-for-input tokens (or input-type attr)))
on-detach-attr
(mf/use-fn
@@ -28,7 +25,7 @@
(tr "settings.multiple")
"--"))
:class [class (stl/css :numeric-input-wrapper)]
- :applied-token applied-token
+ :applied-token-name applied-token
:tokens (if (delay? tokens) @tokens tokens)
:align align
:on-detach on-detach-attr
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss
index b5d6de75d1..3097f16e76 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/input_wrapper_tokens.scss
@@ -5,5 +5,5 @@
// Copyright (c) KALEIDOS INC
.numeric-input-wrapper {
- --dropdown-width: var(--7-columns-dropdown-width);
+ --dropdown-width: var(--seven-columns-width);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
index ac8fdc23cf..c1a54d9764 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
@@ -475,16 +475,6 @@
(when (ctsi/has-overlay-opts interaction)
[:*
- ;; Overlay position relative-to (select)
- [:div {:class (stl/css :interaction-row)}
- [:div {:class (stl/css :interaction-row-label)}
- [:div {:class (stl/css :interaction-row-name)}
- (tr "workspace.options.interaction-relative-to")]]
- [:div {:class (stl/css :interaction-row-select)}
- [:& select {:default-value (str (:position-relative-to interaction))
- :options relative-to-opts
- :on-change change-position-relative-to}]]]
-
;; Overlay position (select)
[:div {:class (stl/css :interaction-row)}
[:div {:class (stl/css :interaction-row-label)}
@@ -495,6 +485,16 @@
:options overlay-position-opts
:on-change change-overlay-pos-type}]]]
+ ;; Overlay position relative-to (select)
+ [:div {:class (stl/css :interaction-row)}
+ [:div {:class (stl/css :interaction-row-label)}
+ [:div {:class (stl/css :interaction-row-name)}
+ (tr "workspace.options.interaction-relative-to")]]
+ [:div {:class (stl/css :interaction-row-select)}
+ [:& select {:default-value (str (:position-relative-to interaction))
+ :options relative-to-opts
+ :on-change change-position-relative-to}]]]
+
;; Overlay position (buttons)
[:div {:class (stl/css :interaction-row)}
[:div {:class (stl/css :interaction-row-position)}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss
index 066145fa14..c4457a8aed 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.scss
@@ -32,7 +32,6 @@
.content {
grid-column: span 8;
-
display: flex;
flex-direction: column;
gap: var(--sp-xs);
@@ -62,12 +61,12 @@
align-items: center;
gap: px2rem(1);
border-radius: $br-8;
- padding: var(--sp-s) var(--sp-m);
block-size: $sz-32;
padding: 0;
&.double {
block-size: $sz-48;
+
.prototype-pill-button {
block-size: $sz-48;
}
@@ -114,13 +113,12 @@
.prototype-pill-input {
@include t.use-typography("body-small");
+
border: none;
background: none;
outline: none;
block-size: 100%;
- inline-size: 100%;
flex-grow: 1;
- margin: var(--sp-xxs) 0;
padding: 0 0 0 var(--sp-s);
margin: 0;
background-color: var(--color-background-tertiary);
@@ -130,6 +128,7 @@
&:hover {
background-color: var(--color-background-quaternary);
+
&:active {
background-color: var(--color-background-quaternary);
}
@@ -142,13 +141,15 @@
.prototype-pill-name {
@include t.use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
color: var(--color-foreground-primary);
}
.prototype-pill-description {
@include t.use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
color: var(--color-foreground-secondary);
}
@@ -164,8 +165,9 @@
}
.interaction-row-name {
- @include twoLineTextEllipsis;
+ @include two-line-text-ellipsis;
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
}
@@ -191,12 +193,10 @@
.interaction-row-position {
grid-column: 4 / span 5;
display: grid;
- grid-template-areas:
- "topleft top topright"
- "left center right"
- "bottomleft bottom bottomright";
- grid-template-columns: repeat(3, 1fr);
- grid-template-rows: repeat(3, 1fr);
+ grid-template:
+ "topleft top topright" 1fr
+ "left center right" 1fr
+ "bottomleft bottom bottomright" 1fr / repeat(3, 1fr);
inline-size: calc($sz-32 * 3);
block-size: calc($sz-32 * 3);
border-radius: $br-8;
@@ -205,21 +205,27 @@
.center {
grid-area: center;
}
+
.top-left {
grid-area: topleft;
}
+
.top-center {
grid-area: top;
}
+
.top-right {
grid-area: topright;
}
+
.bottom-left {
grid-area: bottomleft;
}
+
.bottom-center {
grid-area: bottom;
}
+
.bottom-right {
grid-area: bottomright;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
index 3e5cfe9921..41ec031902 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs
@@ -69,6 +69,8 @@
hidden? (get values :hidden)
blocked? (get values :blocked)
+ opacity (get values :opacity)
+
on-detach-token
(mf/use-fn
(mf/deps ids)
@@ -138,14 +140,13 @@
on-opacity-change
(mf/use-fn
- (mf/deps on-change handle-opacity-change)
+ (mf/deps handle-opacity-change)
(fn [value]
(if (or (string? value) (number? value))
(handle-opacity-change value)
- (do
- (st/emit! (dwta/toggle-token {:token (first value)
- :attrs #{:opacity}
- :shape-ids ids}))))))
+ (st/emit! (dwta/apply-token-from-input {:token (first value)
+ :attrs #{:opacity}
+ :shape-ids ids})))))
handle-set-hidden
(mf/use-fn
@@ -205,20 +206,25 @@
preview-complete?))
(swap! state* assoc :selected-blend-mode current-blend-mode)))
- [:section {:class (stl/css-case :element-set-content true
- :hidden hidden?)
- :aria-label "layer-menu-section"}
- [:div {:class (stl/css :select)}
- [:& select
- {:default-value selected-blend-mode
- :options options
- :on-change handle-change-blend-mode
- :is-open? option-highlighted?
- :class (stl/css-case :hidden-select hidden?)
- :on-pointer-enter-option handle-blend-mode-enter
- :on-pointer-leave-option handle-blend-mode-leave}]]
+ ;; NOTE:
+ ;; This code is temporarily duplicated because the UI is changing with a new feature.
+ ;; The new implementation is currently behind a feature/config flag and not yet released.
+ ;; Once the feature is released, the duplicated ClojureScript and SCSS code should be removed.
+ ;; https://tree.taiga.io/project/penpot/task/13704
+
+ (if token-numeric-inputs
+ ;; TODO: When duplicated code is remove rename this class removing the "token" reference from it
+ [:section {:class (stl/css :element-set-content-token)
+ :aria-label (tr "workspace.options.layer-options.layer-section")}
+ [:& select
+ {:default-value selected-blend-mode
+ :options options
+ :on-change handle-change-blend-mode
+ :is-open? option-highlighted?
+ :class (stl/css-case :hidden-select hidden?)
+ :on-pointer-enter-option handle-blend-mode-enter
+ :on-pointer-leave-option handle-blend-mode-leave}]
- (if token-numeric-inputs
[:> numeric-input-wrapper*
{:on-change on-opacity-change
:on-detach on-detach-token
@@ -233,9 +239,54 @@
(tr "settings.multiple")
"--")
:align :right
+ :disabled (if (or (= :multiple hidden?) hidden?) true false)
:class (stl/css :numeric-input-wrapper)
- :value (* 100
- (or (get values :opacity) 1))}]
+ :value (if (= :multiple opacity)
+ opacity
+ (* 100 (d/nilv opacity 1)))}]
+
+ (cond
+ (or (= :multiple hidden?) (not hidden?))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.layer-options.toggle-layer")
+ :on-click handle-set-hidden
+ :tooltip-placement "top-left"
+ :icon i/shown}]
+
+ :else
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.layer-options.toggle-layer")
+ :on-click handle-set-visible
+ :tooltip-placement "top-left"
+ :icon i/hide}])
+
+ (cond
+ (or (= :multiple blocked?) (not blocked?))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.shape.menu.lock")
+ :on-click handle-set-blocked
+ :tooltip-placement "top-left"
+ :icon i/unlock}]
+
+ :else
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.shape.menu.unlock")
+ :on-click handle-set-unblocked
+ :tooltip-placement "top-left"
+ :icon i/lock}])]
+
+ [:section {:class (stl/css-case :element-set-content true
+ :hidden hidden?)
+ :aria-label (tr "workspace.options.layer-options.layer-section")}
+ [:div {:class (stl/css :select)}
+ [:& select
+ {:default-value selected-blend-mode
+ :options options
+ :on-change handle-change-blend-mode
+ :is-open? option-highlighted?
+ :class (stl/css-case :hidden-select hidden?)
+ :on-pointer-enter-option handle-blend-mode-enter
+ :on-pointer-leave-option handle-blend-mode-leave}]]
[:div {:class (stl/css :input)
:title (tr "workspace.options.opacity")}
@@ -246,31 +297,31 @@
:on-change handle-opacity-change
:min 0
:max 100
- :className (stl/css :numeric-input)}]])
+ :className (stl/css :numeric-input)}]]
- [:div {:class (stl/css :actions)}
- (cond
- (or (= :multiple hidden?) (not hidden?))
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "workspace.options.layer-options.toggle-layer")
- :on-click handle-set-hidden
- :icon i/shown}]
+ [:div {:class (stl/css :actions)}
+ (cond
+ (or (= :multiple hidden?) (not hidden?))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.layer-options.toggle-layer")
+ :on-click handle-set-hidden
+ :icon i/shown}]
- :else
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "workspace.options.layer-options.toggle-layer")
- :on-click handle-set-visible
- :icon i/hide}])
+ :else
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.layer-options.toggle-layer")
+ :on-click handle-set-visible
+ :icon i/hide}])
- (cond
- (or (= :multiple blocked?) (not blocked?))
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "workspace.shape.menu.lock")
- :on-click handle-set-blocked
- :icon i/unlock}]
+ (cond
+ (or (= :multiple blocked?) (not blocked?))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.shape.menu.lock")
+ :on-click handle-set-blocked
+ :icon i/unlock}]
- :else
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "workspace.shape.menu.unlock")
- :on-click handle-set-unblocked
- :icon i/lock}])]]))
+ :else
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.shape.menu.unlock")
+ :on-click handle-set-unblocked
+ :icon i/lock}])]])))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss
index 637cf9a090..333cb66bbf 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.scss
@@ -7,20 +7,30 @@
@use "refactor/common-refactor.scss" as deprecated;
@use "../../../sidebar/common/sidebar.scss" as sidebar;
@use "ds/_utils.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/_borders.scss" as *;
+@use "ds/typography.scss" as t;
+// This code should be remove when numeric-input-tokens are activated
+// https://tree.taiga.io/project/penpot/task/13704
.element-set-content {
@include sidebar.option-grid-structure;
- height: deprecated.$s-32;
- margin-bottom: deprecated.$s-8;
+
+ block-size: $sz-32;
+ margin-block-end: var(--sp-s);
+
.select {
grid-column: span 4;
padding: 0;
}
+
.input {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include t.use-typography("body-small");
+
grid-column: span 2;
}
+
.actions {
grid-column: span 2;
display: grid;
@@ -29,15 +39,28 @@
&.hidden {
.hidden-select {
- @include deprecated.hiddenElement;
- border: deprecated.$s-1 solid var(--input-border-color-disabled);
+ cursor: default;
+ pointer-events: none;
+ box-sizing: border-box;
+ color: var(--input-foreground-color-disabled);
+ stroke: var(--input-foreground-color-disabled);
+ background-color: transparent;
+ border: $b-1 solid var(--input-border-color-disabled);
}
+
.input {
- @include deprecated.hiddenElement;
- border: deprecated.$s-1 solid var(--input-border-color-disabled);
+ cursor: default;
+ pointer-events: none;
+ box-sizing: border-box;
+ color: var(--input-foreground-color-disabled);
+ stroke: var(--input-foreground-color-disabled);
+ background-color: transparent;
+ border: $b-1 solid var(--input-border-color-disabled);
+
.icon {
stroke: var(--input-foreground-color-disabled);
}
+
.numeric-input {
color: var(--input-foreground-color-disabled);
}
@@ -45,7 +68,29 @@
}
}
+// This code should remain when numeric-input-tokens are activated
+// https://tree.taiga.io/project/penpot/task/13704
+
+// This rule should be rename when numeric-input-tokens are
+// activated removing the token reference on the class
+.element-set-content-token {
+ @include sidebar.option-grid-structure;
+
+ block-size: $sz-32;
+ margin-block-end: var(--sp-s);
+ grid-template-columns: var(--grid-exception-input-width) var(--grid-exception-input-width-small) auto auto;
+}
+
+.hidden-select {
+ cursor: default;
+ pointer-events: none;
+ box-sizing: border-box;
+ color: var(--input-foreground-color-disabled);
+ stroke: var(--input-foreground-color-disabled);
+ background-color: transparent;
+ border: $b-1 solid var(--input-border-color-disabled);
+}
+
.numeric-input-wrapper {
- grid-column: span 2;
--dropdown-offset: #{px2rem(-35)};
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs
index e07c3cd958..dd992ce08b 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.cljs
@@ -30,6 +30,7 @@
[app.main.ui.formats :as fmt]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
+ [app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -335,15 +336,8 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr event]
- (if (or (string? value) (number? value))
- (on-change :simple attr value event)
- (do
- (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs (if (= :p1 attr)
- #{:p1 :p3}
- #{:p2 :p4})
- :shape-ids ids}))))))
+ (let [on-change-fn #(on-change :simple attr % event)]
+ (soc/emit-value-or-token value on-change-fn ids attr))))
on-detach-token
(mf/use-fn
@@ -370,10 +364,10 @@
(mf/use-fn (mf/deps on-focus) #(on-focus :p2))
on-p1-change
- (mf/use-fn (mf/deps on-change') #(on-change' % :p1))
+ (mf/use-fn (mf/deps on-change') #(on-change' % #{:p1 :p3}))
on-p2-change
- (mf/use-fn (mf/deps on-change') #(on-change' % :p2))]
+ (mf/use-fn (mf/deps on-change') #(on-change' % #{:p2 :p4}))]
[:div {:class (stl/css :paddings-simple)}
(if token-numeric-inputs
@@ -466,12 +460,8 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr event]
- (if (or (string? value) (number? value))
- (on-change :multiple attr value event)
- (do
- (st/emit! (dwta/toggle-token {:token (first value)
- :attrs #{attr}
- :shape-ids ids}))))))
+ (let [on-change-fn #(on-change :multiple attr % event)]
+ (soc/emit-value-or-token value on-change-fn ids #{attr}))))
on-focus
(mf/use-fn
@@ -648,7 +638,7 @@
:value p4}]])]))
(mf/defc padding-section*
- [{:keys [type on-type-change on-change] :as props}]
+ [{:keys [type on-type-change] :as props}]
(let [on-type-change'
(mf/use-fn
(mf/deps on-type-change)
@@ -656,9 +646,7 @@
(let [type (-> (dom/get-current-target event)
(dom/get-data "type"))
type (if (= type "multiple") :simple :multiple)]
- (on-type-change type))))
-
- props (mf/spread-object props {:on-change on-change})]
+ (on-type-change type))))]
(mf/with-effect []
;; on destroy component
@@ -719,15 +707,8 @@
(mf/use-fn
(mf/deps on-change wrap-type ids)
(fn [value event attr]
- (if (or (string? value) (number? value))
- (on-change (= "nowrap" wrap-type) attr value event)
- (do
- (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs (if (= "nowrap" wrap-type)
- #{:row-gap :colum-gap}
- #{attr})
- :shape-ids ids}))))))
+ (let [on-change-fn #(on-change (= "nowrap" wrap-type) attr % event)]
+ (soc/emit-value-or-token value on-change-fn ids #{attr}))))
on-detach-token
(mf/use-fn
@@ -1195,10 +1176,10 @@
(fn [type prop val]
(let [val (mth/finite val 0)]
(cond
- (and (= type :simple) (= prop :p1))
+ (and (= type :simple) (or (= prop :p1) (= prop #{:p1 :p3})))
(st/emit! (dwsl/update-layout ids {:layout-padding {:p1 val :p3 val}}))
- (and (= type :simple) (= prop :p2))
+ (and (= type :simple) (or (= prop :p2) (= prop #{:p2 :p4})))
(st/emit! (dwsl/update-layout ids {:layout-padding {:p2 val :p4 val}}))
(some? prop)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss
index 10247a86b1..c4ec2b3f77 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_container.scss
@@ -42,6 +42,7 @@
.flex-layout-menu {
@include sidebar.option-grid-structure;
+
margin-block-end: var(--sp-s);
}
@@ -49,8 +50,7 @@
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
- margin-block-end: var(--sp-m);
- margin-block-start: var(--sp-xs);
+ margin-block: var(--sp-xs) var(--sp-m);
}
.align-row {
@@ -63,16 +63,20 @@
// TODO: Replace this buttons with DS buttons
.wrap-button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
border-radius: $br-8;
block-size: $sz-32;
inline-size: $sz-32;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-foreground-secondary);
}
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
@@ -95,6 +99,7 @@
var(--grid-exception-input-width) /* first input block */
var(--grid-exception-input-width) /* second input block */
var(--sp-xxxl); /* action button */
+
gap: var(--sp-xs);
grid-column: 1 / -1;
}
@@ -115,10 +120,11 @@
// TODO: Remove when activating token numeric inputs
.column-gap,
.row-gap {
- @extend .input-element;
+ @extend %input-element;
@include t.use-typography("body-small");
+
&.disabled {
- @extend .disabled-input;
+ @extend %disabled-input;
}
}
@@ -126,7 +132,7 @@
.padding-simple,
.padding-multiple {
@include t.use-typography("body-small");
- @extend .input-element;
+ @extend %input-element;
}
.padding-group {
@@ -151,16 +157,20 @@
// TODO: Replace this buttons with DS buttons
.padding-toggle {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
block-size: $sz-32;
inline-size: $sz-32;
border-radius: $br-8;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-foreground-secondary);
}
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
@@ -188,6 +198,7 @@
align-items: flex-start;
position: relative;
gap: var(--sp-xs);
+ margin-block-end: var(--sp-s);
}
.locate-button {
@@ -197,6 +208,7 @@
.grid-layout-menu-title {
@include t.use-typography("headline-small");
+
flex: 1;
color: var(--color-foreground-primary);
grid-column: span 5;
@@ -204,8 +216,9 @@
// TODO: Replace this buttons with DS buttons
.edit-mode-btn {
- @extend .button-secondary;
+ @extend %button-secondary;
@include t.use-typography("headline-small");
+
inline-size: 100%;
padding: var(--sp-s);
grid-column: span 7;
@@ -213,8 +226,9 @@
// TODO: Replace this buttons with DS buttons
.exit-btn {
- @extend .button-secondary;
+ @extend %button-secondary;
@include t.use-typography("headline-small");
+
padding: var(--sp-s) var(--sp-xl);
grid-column: span 2;
}
@@ -267,19 +281,23 @@
border-radius: $br-8 0 0 $br-8;
background-color: var(--color-background-tertiary);
padding: 0 var(--sp-s);
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-foreground-secondary);
block-size: 100%;
}
+
&:hover svg {
stroke: var(--color-foreground-primary);
}
}
.track-info-value {
- @extend .input-element;
+ @extend %input-element;
@include t.use-typography("body-small");
+
border-radius: 0;
border-inline-end: $b-1 solid var(--color-background-primary);
}
@@ -298,6 +316,7 @@
.grid-track-header {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
gap: var(--sp-xs);
@@ -330,16 +349,19 @@
// TODO: Replace this buttons with DS buttons
.expand-icon {
- @extend .button-secondary;
- block-size: px2rem(52);
+ @extend %button-secondary;
+ block-size: px2rem(52);
border-radius: $br-8 0 0 $br-8;
border-inline-end: $b-1 solid var(--color-background-primary);
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-foreground-secondary);
fill: var(--color-foreground-secondary);
}
+
&:hover,
&:active {
svg {
@@ -351,7 +373,8 @@
// TODO: Replace this buttons with DS buttons
.add-column {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
block-size: px2rem(52);
svg {
@@ -359,7 +382,6 @@
justify-content: center;
align-items: center;
color: transparent;
- fill: none;
stroke-width: px2rem(1);
block-size: $sz-12;
inline-size: $sz-12;
@@ -369,7 +391,7 @@
}
.layout-options {
- box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark);
+ box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark);
position: absolute;
display: flex;
flex-direction: column;
@@ -382,8 +404,7 @@
margin-block-start: px2rem(1);
border-radius: $br-8;
z-index: var(--z-index-dropdown);
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
border: $b-2 solid var(--color-background-quaternary);
@@ -423,7 +444,9 @@
var(--grid-exception-input-width) /* first input block */
var(--grid-exception-input-width) /* second input block */
var(--sp-xxxl); /* action button */
+
gap: var(--sp-xs);
+ margin-block-end: var(--sp-xs);
}
.grid-first-row {
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
index d239dbd1e3..c8e43e7f1c 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
@@ -21,6 +21,7 @@
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
+ [app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]]
[app.util.dom :as dom]
@@ -117,15 +118,10 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr]
- (if (or (string? value) (number? value) (nil? value))
- (on-change :simple attr value)
- (do
- (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs (if (= :m1 attr)
- #{:m1 :m3}
- #{:m2 :m4})
- :shape-ids ids}))))))
+ (soc/emit-value-or-token value
+ #(on-change :simple attr %)
+ ids
+ (if (= :m1 attr) #{:m1 :m3} #{:m2 :m4}))))
on-focus-m1
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
@@ -247,14 +243,10 @@
(mf/use-fn
(mf/deps on-change ids)
(fn [value attr]
- (if (or (string? value) (number? value) (nil? value))
- (on-change :multiple attr value)
- (do
- (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs #{attr}
- :shape-ids ids}))))))
-
+ (soc/emit-value-or-token value
+ #(on-change :multiple attr %)
+ ids
+ #{attr})))
on-m1-change
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
@@ -579,13 +571,10 @@
(mf/use-fn
(mf/deps ids)
(fn [value attr]
- (if (or (string? value) (number? value) (nil? value))
- (st/emit! (dwsl/update-layout-child ids {attr value}))
- (do
- (st/emit!
- (dwta/toggle-token {:token (first value)
- :attrs #{attr}
- :shape-ids ids}))))))
+ (soc/emit-value-or-token value
+ #(st/emit! (dwsl/update-layout-child ids {attr %}))
+ ids
+ #{attr})))
on-layout-item-min-w-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-w))
@@ -599,7 +588,8 @@
on-layout-item-max-h-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-h))]
- [:div {:class (stl/css :advanced-options)}
+ [:section {:class (stl/css :advanced-options)
+ :aria-label "Layout item size constraints"}
(when (= (:layout-item-h-sizing values) :fill)
[:div {:class (stl/css :horizontal-fill)}
(if token-numeric-inputs
@@ -847,7 +837,7 @@
(st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))))]
[:section {:class (stl/css :element-set)
- :aria-label "layout item menu"}
+ :aria-label "Layout item section"}
[:div {:class (stl/css :element-title)}
[:> title-bar* {:collapsable has-content?
:collapsed (not open?)
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss
index d54b68e918..90a7f772f4 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.scss
@@ -25,6 +25,7 @@
.flex-element-menu {
@include sidebar.option-grid-structure;
+
gap: var(--sp-xs);
}
@@ -42,7 +43,8 @@
.z-index-wrapper {
@include use-typography("body-small");
- @extend .input-element;
+ @extend %input-element;
+
grid-column: 6 / span 3;
}
@@ -71,18 +73,22 @@
var(--grid-exception-input-width) /* first input block */
var(--grid-exception-input-width) /* second input block */
var(--sp-xxxl); /* action button */
+
gap: var(--sp-xs);
}
.margin-mode {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
grid-column: 3;
height: deprecated.$s-32;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
@@ -90,14 +96,17 @@
display: grid;
gap: var(--sp-xs);
grid-template-columns: subgrid;
+
.vertical-margin,
.horizontal-margin {
- @extend .input-element;
+ @extend %input-element;
@include use-typography("body-small");
}
+
.vertical-margin {
grid-column: 1;
}
+
.horizontal-margin {
grid-column: 2;
}
@@ -121,7 +130,7 @@
.bottom-margin,
.left-margin,
.right-margin {
- @extend .input-element;
+ @extend %input-element;
@include use-typography("body-small");
}
@@ -155,6 +164,7 @@
var(--grid-exception-input-width) /* first input block */
var(--grid-exception-input-width) /* second input block */
var(--sp-xxxl); /* action button */
+
gap: var(--sp-xs);
}
@@ -169,8 +179,9 @@
.layout-item-min-h,
.layout-item-max-w,
.layout-item-max-h {
- @extend .input-element;
+ @extend %input-element;
@include use-typography("body-small");
+
.icon-text {
justify-content: flex-start;
inline-size: px2rem(80);
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
index 9c5030e99c..b29d3fd47b 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
@@ -26,6 +26,7 @@
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :as deprecated-input]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
+ [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 :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon]
@@ -34,6 +35,7 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[clojure.set :as set]
+ [cuerdas.core :as str]
[rumext.v2 :as mf]))
(def measure-attrs
@@ -105,6 +107,29 @@
(number? value)
(parse-double (.toFixed value decimals)))))
+(defn filter-size-presets
+ "Filter the `size-presets` list by `term`, preserving category headers only
+ when at least one of their following presets matches."
+ [term presets]
+ (if (str/blank? term)
+ presets
+ (let [lterm (str/lower term)
+ matches? (fn [p] (and (:width p)
+ (str/includes? (str/lower (:name p)) lterm)))]
+ (loop [remaining presets
+ acc []]
+ (if-let [head (first remaining)]
+ (if (:width head)
+ (recur (rest remaining)
+ (cond-> acc (matches? head) (conj head)))
+ (let [[items tail] (split-with :width (rest remaining))
+ matching-items (filter matches? items)]
+ (recur tail
+ (if (seq matching-items)
+ (into (conj acc head) matching-items)
+ acc))))
+ acc)))))
+
(mf/defc measures-menu*
[{:keys [ids values applied-tokens type shapes]}]
(let [token-numeric-inputs
@@ -235,17 +260,36 @@
show-presets-dropdown?
(deref preset-state*)
- open-presets
+ preset-search-term*
+ (mf/use-state "")
+
+ preset-search-term
+ (deref preset-search-term*)
+
+ preset-container-ref
+ (mf/use-ref nil)
+
+ toggle-presets
(mf/use-fn
- (mf/deps show-presets-dropdown?)
(fn []
- (reset! preset-state* true)))
+ (swap! preset-state* not)
+ (reset! preset-search-term* "")))
close-presets
(mf/use-fn
(mf/deps show-presets-dropdown?)
(fn []
- (reset! preset-state* false)))
+ (reset! preset-state* false)
+ (reset! preset-search-term* "")))
+
+ on-preset-search-change
+ (mf/use-fn
+ (fn [value _event]
+ (reset! preset-search-term* value)))
+
+ filtered-size-presets
+ (mf/with-memo [preset-search-term]
+ (filter-size-presets preset-search-term size-presets))
on-preset-selected
(mf/use-fn
@@ -258,7 +302,9 @@
(dom/get-data "height")
(d/read-string))]
(st/emit! (udw/update-dimensions ids :width width)
- (udw/update-dimensions ids :height height)))))
+ (udw/update-dimensions ids :height height))
+ (reset! preset-state* false)
+ (reset! preset-search-term* ""))))
;; ORIENTATION
@@ -283,9 +329,9 @@
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/update-dimensions ids attr value))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
- (dwta/toggle-token {:token (first value)
- :attrs #{attr}
- :shape-ids ids})))))
+ (dwta/apply-token-from-input {:token (first value)
+ :attrs #{attr}
+ :shape-ids ids})))))
on-proportion-lock-change
(mf/use-fn
@@ -304,9 +350,9 @@
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/update-positions ids {attr value})))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
- (dwta/toggle-token {:token (first value)
- :attrs #{attr}
- :shape-ids ids})))))
+ (dwta/apply-token-from-input {:token (first value)
+ :attrs #{attr}
+ :shape-ids ids})))))
on-rotation-change
(mf/use-fn
@@ -317,9 +363,9 @@
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/increase-rotation ids value)))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
- (dwta/toggle-token {:token (first value)
- :attrs #{:rotation}
- :shape-ids ids})))))
+ (dwta/apply-token-from-input {:token (first value)
+ :attrs #{:rotation}
+ :shape-ids ids})))))
on-width-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :width))
@@ -379,33 +425,47 @@
[:div {:class (stl/css :presets)}
[:div {:class (stl/css-case :presets-wrapper true
:opened show-presets-dropdown?)
- :on-click open-presets}
+ :ref preset-container-ref
+ :on-click toggle-presets}
[:span {:class (stl/css :select-name)} (tr "workspace.options.size-presets")]
[:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow]
[:& dropdown {:show show-presets-dropdown?
- :on-close close-presets}
- [:ul {:class (stl/css :custom-select-dropdown)}
- (for [size-preset size-presets]
- (if-not (:width size-preset)
- [:li {:key (:name size-preset)
- :class (stl/css-case :dropdown-element true
- :disabled true)}
- [:span {:class (stl/css :preset-name)} (:name size-preset)]]
+ :on-close close-presets
+ :container preset-container-ref}
+ [:div {:class (stl/css :custom-select-dropdown)
+ :on-click dom/stop-propagation}
+ [:div {:class (stl/css :preset-search)}
+ [:> search-bar* {:on-change on-preset-search-change
+ :value preset-search-term
+ :auto-focus true
+ :placeholder (tr "workspace.options.search-size-preset")}]]
+ [:ul {:class (stl/css :preset-list)}
+ (if (empty? filtered-size-presets)
+ [:li {:class (stl/css-case :dropdown-element true
+ :disabled true)}
+ [:span {:class (stl/css :preset-name)}
+ (tr "workspace.options.no-size-preset-results")]]
+ (for [size-preset filtered-size-presets]
+ (if-not (:width size-preset)
+ [:li {:key (:name size-preset)
+ :class (stl/css-case :dropdown-element true
+ :disabled true)}
+ [:span {:class (stl/css :preset-name)} (:name size-preset)]]
- (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0))
- (= (:height size-preset) (d/parse-integer (:height values) 0)))]
- [:li {:key (:name size-preset)
- :class (stl/css-case :dropdown-element true
- :match preset-match)
- :data-width (str (:width size-preset))
- :data-height (str (:height size-preset))
- :on-click on-preset-selected}
- [:div {:class (stl/css :name-wrapper)}
- [:span {:class (stl/css :preset-name)} (:name size-preset)]
- [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]]
- (when preset-match
- [:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]]
+ (let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0))
+ (= (:height size-preset) (d/parse-integer (:height values) 0)))]
+ [:li {:key (:name size-preset)
+ :class (stl/css-case :dropdown-element true
+ :match preset-match)
+ :data-width (str (:width size-preset))
+ :data-height (str (:height size-preset))
+ :on-click on-preset-selected}
+ [:div {:class (stl/css :name-wrapper)}
+ [:span {:class (stl/css :preset-name)} (:name size-preset)]
+ [:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]]
+ (when preset-match
+ [:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]]
[:& radio-buttons {:selected (or (d/name orientation) "")
:on-change on-orientation-change
@@ -559,8 +619,7 @@
:value (get values :rotation)}]
[:div {:class (stl/css :rotation)
- :title (tr "workspace.options.rotation")
- :data-testid "rotation"}
+ :title (tr "workspace.options.rotation")}
[:span {:class (stl/css :icon)} deprecated-icon/rotation]
[:> deprecated-input/numeric-input*
{:no-validate true
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss
index e214e0f636..357df42145 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.scss
@@ -14,17 +14,20 @@
var(--grid-exception-input-width) /* first input block */
var(--grid-exception-input-width) /* second input block */
var(--sp-xxxl); /* action button */
+
gap: var(--sp-xs);
margin-bottom: var(--sp-s);
}
.presets {
@include sidebar.option-grid-structure;
+
grid-column: 1 / -1;
}
.presets-wrapper {
- @extend .asset-element;
+ @extend %asset-element;
+
position: relative;
grid-column: span 5;
display: flex;
@@ -33,10 +36,13 @@
border-radius: deprecated.$br-8;
.collapsed-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
cursor: pointer;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
transform: rotate(90deg);
}
@@ -54,7 +60,8 @@
}
.select-name {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
justify-content: flex-start;
align-items: center;
@@ -63,28 +70,52 @@
}
.custom-select-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
margin-top: deprecated.$s-2;
max-height: 70vh;
width: deprecated.$s-252;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.preset-search {
+ padding: deprecated.$s-4;
+ border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent);
+}
+
+.preset-list {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+
.dropdown-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
.name-wrapper {
display: flex;
gap: deprecated.$s-8;
flex-grow: 1;
+
.preset-name {
color: var(--menu-foreground-color-rest);
}
+
.preset-size {
color: var(--menu-foreground-color-rest);
}
}
.check-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
svg {
- @extend .button-icon-small;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
@@ -92,6 +123,7 @@
&.disabled {
pointer-events: none;
cursor: default;
+
.preset-name {
color: var(--menu-foreground-color);
}
@@ -101,6 +133,7 @@
.name-wrapper .preset-name {
color: var(--menu-foreground-color-hover);
}
+
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
@@ -108,9 +141,11 @@
&:hover {
background-color: var(--menu-background-color-hover);
+
.name-wrapper .preset-name {
color: var(--menu-foreground-color-hover);
}
+
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
@@ -131,13 +166,15 @@
.x-position,
.y-position,
.rotation {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
.icon-text {
padding-top: deprecated.$s-1;
}
+
&.disabled {
- @extend .disabled-input;
+ @extend %disabled-input;
}
}
@@ -146,17 +183,20 @@
}
.lock-size-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
border-radius: deprecated.$br-8;
height: deprecated.$s-32;
width: deprecated.$s-28;
+
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
.lock-ratio-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--icon-foreground);
}
@@ -175,21 +215,22 @@
}
.clip-content-label {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: var(--sp-xxxl);
width: var(--sp-xxxl);
border-radius: deprecated.$br-8;
}
.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
.checkbox-button {
- @extend .button-icon;
+ @extend %button-icon;
}
// TODO: Add a proper variable to this sizing
.numeric-input-measures {
- --dropdown-width: var(--7-columns-dropdown-width);
+ --dropdown-width: var(--seven-columns-width);
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss
index 4e2453ff6d..310ac1eb8c 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.scss
@@ -23,7 +23,6 @@
.shadow-content {
grid-column: span 8;
-
display: flex;
flex-direction: column;
gap: var(--sp-xs);
@@ -38,6 +37,7 @@
.shadow-multiple-label {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
flex-grow: 1;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
index f1a708d8c0..306963bcd4 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs
@@ -12,6 +12,7 @@
[app.common.types.stroke :as cts]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
+ [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]]
@@ -155,6 +156,13 @@
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-cap-start stroke-cap-end
:stroke-cap-end stroke-cap-start} index)))))
+ on-toggle-visibility
+ (mf/use-fn
+ (mf/deps ids)
+ (fn [index]
+ (st/emit! (udw/trigger-bounding-box-cloaking ids)
+ (dwsh/update-shapes ids #(update-in % [:strokes index :hidden] not)))))
+
on-add-stroke
(fn [_]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
@@ -168,6 +176,7 @@
on-blur (fn [_]
(reset! disable-drag false))
+
on-detach-token
(mf/use-fn
(mf/deps ids)
@@ -207,7 +216,7 @@
(seq strokes)
[:> h/sortable-container* {}
(for [[index value] (d/enumerate (:strokes values []))]
- [:> stroke-row* {:key (dm/str "stroke-" index)
+ [:> stroke-row* {:key (dm/str "stroke-" index "-" (hash applied-tokens))
:stroke value
:title (tr "workspace.options.stroke-color")
:index index
@@ -224,9 +233,10 @@
:on-stroke-cap-start-change on-stroke-cap-start-change
:on-stroke-cap-end-change on-stroke-cap-end-change
:on-stroke-cap-switch on-stroke-cap-switch
- :applied-tokens applied-tokens
+ :applied-tokens (when (= 0 index) applied-tokens)
:on-detach-token on-detach-token
:on-remove on-remove
+ :on-toggle-visibility on-toggle-visibility
:on-reorder handle-reorder
:disable-drag disable-drag
:on-focus on-focus
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss
index 874beac840..ab0f622225 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.scss
@@ -23,7 +23,6 @@
.stroke-content {
grid-column: span 8;
-
display: flex;
flex-direction: column;
gap: var(--sp-m);
@@ -42,6 +41,7 @@
.stroke-multiple-label {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
flex-grow: 1;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss
index 50ba70a209..d8fabe2295 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.scss
@@ -16,7 +16,8 @@
}
.element-set-content {
- @include deprecated.flexColumn;
+ @include deprecated.flex-column;
+
margin: deprecated.$s-4 0 0 0;
}
@@ -26,8 +27,9 @@
}
.attr-name {
- @include deprecated.bodySmallTypography;
- @include deprecated.twoLineTextEllipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.two-line-text-ellipsis;
+
width: deprecated.$s-88;
margin: auto deprecated.$s-4;
margin-right: 0;
@@ -36,8 +38,9 @@
}
.attr-input {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
+ @extend %input-element;
+ @include deprecated.body-small-typography;
+
width: deprecated.$s-124;
}
@@ -47,11 +50,13 @@
}
.attr-action-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
width: deprecated.$s-28;
height: deprecated.$s-32;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
@@ -61,7 +66,8 @@
}
.attr-title {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
font-size: deprecated.$fs-10;
text-transform: uppercase;
margin-inline-start: deprecated.$s-4;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
index 38b624b2cc..504856d22e 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs
@@ -10,27 +10,31 @@
[app.common.data :as d]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
+ [app.config :as cf]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.shortcuts :as sc]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.texts-v3 :as dwt-v3]
+ [app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
- [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
+ [app.main.ui.ds.controls.shared.searchable-options-dropdown :refer [searchable-options-dropdown*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.hooks :as hooks]
- [app.main.ui.icons :as deprecated-icon]
- [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options*
- typography-entry]]
+ [app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]]
+ [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry]]
+ [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
+ [app.util.object :as obj]
[app.util.text.content :as content]
[app.util.text.ui :as txu]
[app.util.timers :as ts]
@@ -38,9 +42,40 @@
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def ^:private token-typography-row-enabled?
+ "True when the token-typography-row feature flag is enabled.
+ Evaluated once at module load time; cf/flags is immutable after startup."
+ (contains? cf/flags :token-typography-row))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Sub-components
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
(mf/defc text-align-options*
- [{:keys [values on-change on-blur] :as props}]
- (let [{:keys [text-align]} values
+ [{:keys [values on-change on-blur]}]
+ (let [options
+ (mf/with-memo []
+ [{:value "left"
+ :id "text-align-left"
+ :label (tr "workspace.options.text-options.text-align-left")
+ :icon i/text-align-left}
+ {:value "center"
+ :id "text-align-center"
+ :label (tr "workspace.options.text-options.text-align-center")
+ :icon i/text-align-center}
+ {:value "right"
+ :id "text-align-right"
+ :label (tr "workspace.options.text-options.text-align-right")
+ :icon i/text-align-right}
+ {:value "justify"
+ :id "text-align-justify"
+ :label (tr "workspace.options.text-options.text-align-justify")
+ :icon i/text-justify}])
+
handle-change
(mf/use-fn
(mf/deps on-change on-blur)
@@ -48,60 +83,57 @@
(on-change {:text-align value})
(when (some? on-blur) (on-blur))))]
- ;; --- Align
[:div {:class (stl/css :align-options)}
- [:& radio-buttons {:selected text-align
- :on-change handle-change
- :name "align-text-options"}
- [:& radio-button {:value "left"
- :id "text-align-left"
- :title (tr "workspace.options.text-options.text-align-left")
- :icon i/text-align-left}]
- [:& radio-button {:value "center"
- :id "text-align-center"
- :title (tr "workspace.options.text-options.text-align-center")
- :icon i/text-align-center}]
- [:& radio-button {:value "right"
- :id "text-align-right"
- :title (tr "workspace.options.text-options.text-align-right")
- :icon i/text-align-right}]
- [:& radio-button {:value "justify"
- :id "text-align-justify"
- :title (tr "workspace.options.text-options.text-align-justify")
- :icon i/text-justify}]]]))
+ [:> radio-buttons* {:selected (:text-align values)
+ :on-change handle-change
+ :name "align-text-options"
+ :options options}]]))
(mf/defc text-direction-options*
- [{:keys [values on-change on-blur] :as props}]
- (let [direction (:text-direction values)
+ [{:keys [values on-change on-blur]}]
+ (let [direction (:text-direction values)
+ options
+ (mf/with-memo []
+ [{:value "ltr"
+ :id "ltr-text-direction"
+ :label (tr "workspace.options.text-options.direction-ltr")
+ :icon i/text-ltr}
+ {:value "rtl"
+ :id "rtl-text-direction"
+ :label (tr "workspace.options.text-options.direction-rtl")
+ :icon i/text-rtl}])
+
handle-change
(mf/use-fn
(mf/deps on-change on-blur direction)
(fn [value]
- (let [dir (if (= value direction)
- "none"
- value)]
- (on-change {:text-direction dir})
- (when (some? on-blur) (on-blur)))))]
+ (on-change {:text-direction (if (= value direction) "none" value)})
+ (when (some? on-blur) (on-blur))))]
[:div {:class (stl/css :text-direction-options)}
- [:& radio-buttons {:selected direction
- :on-change handle-change
- :name "text-direction-options"}
- [:& radio-button {:value "ltr"
- :type "checkbox"
- :id "ltr-text-direction"
- :title (tr "workspace.options.text-options.direction-ltr")
- :icon i/text-ltr}]
- [:& radio-button {:value "rtl"
- :type "checkbox"
- :id "rtl-text-direction"
- :title (tr "workspace.options.text-options.direction-rtl")
- :icon i/text-rtl}]]]))
+ [:> radio-buttons* {:selected direction
+ :on-change handle-change
+ :name "text-direction-options"
+ :options options}]]))
(mf/defc vertical-align*
- [{:keys [values on-change on-blur] :as props}]
- (let [{:keys [vertical-align]} values
- vertical-align (or vertical-align "top")
+ [{:keys [values on-change on-blur]}]
+ (let [vertical-align (or (:vertical-align values) "top")
+ options
+ (mf/with-memo []
+ [{:value "top"
+ :id "vertical-text-align-top"
+ :label (tr "workspace.options.text-options.align-top")
+ :icon i/text-top}
+ {:value "center"
+ :id "vertical-text-align-center"
+ :label (tr "workspace.options.text-options.align-middle")
+ :icon i/text-middle}
+ {:value "bottom"
+ :id "vertical-text-align-bottom"
+ :label (tr "workspace.options.text-options.align-bottom")
+ :icon i/text-bottom}])
+
handle-change
(mf/use-fn
(mf/deps on-change on-blur)
@@ -110,29 +142,31 @@
(when (some? on-blur) (on-blur))))]
[:div {:class (stl/css :vertical-align-options)}
- [:& radio-buttons {:selected vertical-align
- :on-change handle-change
- :name "vertical-align-text-options"}
- [:& radio-button {:value "top"
- :id "vertical-text-align-top"
- :title (tr "workspace.options.text-options.align-top")
- :icon i/text-top}]
- [:& radio-button {:value "center"
- :id "vertical-text-align-center"
- :title (tr "workspace.options.text-options.align-middle")
- :icon i/text-middle}]
- [:& radio-button {:value "bottom"
- :id "vertical-text-align-bottom"
- :title (tr "workspace.options.text-options.align-bottom")
- :icon i/text-bottom}]]]))
+ [:> radio-buttons* {:selected vertical-align
+ :on-change handle-change
+ :name "vertical-align-text-options"
+ :options options}]]))
(mf/defc grow-options*
- [{:keys [ids values on-blur] :as props}]
- (let [grow-type (:grow-type values)
-
+ [{:keys [ids values on-blur]}]
+ (let [grow-type (:grow-type values)
editor-instance (mf/deref refs/workspace-editor)
+ options
+ (mf/with-memo []
+ [{:value "fixed"
+ :id "text-fixed-grow"
+ :label (tr "workspace.options.text-options.grow-fixed")
+ :icon i/text-fixed}
+ {:value "auto-width"
+ :id "text-auto-width-grow"
+ :label (tr "workspace.options.text-options.grow-auto-width")
+ :icon i/text-auto-width}
+ {:value "auto-height"
+ :id "text-auto-height-grow"
+ :label (tr "workspace.options.text-options.grow-auto-height")
+ :icon i/text-auto-height}])
- handle-change-grow
+ handle-change
(mf/use-fn
(mf/deps ids on-blur editor-instance)
(fn [value]
@@ -156,79 +190,182 @@
(when (some? on-blur) (on-blur))))]
[:div {:class (stl/css :grow-options)}
- [:& radio-buttons {:selected (d/name grow-type)
- :on-change handle-change-grow
- :name "grow-text-options"}
- [:& radio-button {:value "fixed"
- :id "text-fixed-grow"
- :title (tr "workspace.options.text-options.grow-fixed")
- :icon i/text-fixed}]
- [:& radio-button {:value "auto-width"
- :id "text-auto-width-grow"
- :title (tr "workspace.options.text-options.grow-auto-width")
- :icon i/text-auto-width}]
- [:& radio-button {:value "auto-height"
- :id "text-auto-height-grow"
- :title (tr "workspace.options.text-options.grow-auto-height")
- :icon i/text-auto-height}]]]))
+ [:> radio-buttons* {:selected (d/name grow-type)
+ :on-change handle-change
+ :name "grow-text-options"
+ :options options}]]))
(mf/defc text-decoration-options*
- [{:keys [values on-change on-blur] :as props}]
- (let [text-decoration (or (:text-decoration values) "none")
+ [{:keys [values on-change on-blur token-applied]}]
+ (let [text-decoration (some-> (:text-decoration values) d/name)
+ options
+ (mf/with-memo [token-applied]
+ [{:value "underline"
+ :id "underline-text-decoration"
+ :disabled (and token-typography-row-enabled? (some? token-applied))
+ :label (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline))
+ :icon i/text-underlined}
+ {:value "line-through"
+ :id "line-through-text-decoration"
+ :disabled (and token-typography-row-enabled? (some? token-applied))
+ :label (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through))
+ :icon i/text-stroked}])
+
handle-change
(mf/use-fn
- (mf/deps on-change on-blur text-decoration)
+ (mf/deps on-change on-blur)
(fn [value]
- (let [decoration (if (= value text-decoration)
- "none"
- value)]
- (on-change {:text-decoration decoration})
- (when (some? on-blur) (on-blur)))))]
+ (on-change {:text-decoration value})
+ (when (some? on-blur)
+ (on-blur))))]
+
[:div {:class (stl/css :text-decoration-options)}
- [:& radio-buttons {:selected text-decoration
- :on-change handle-change
- :name "text-decoration-options"}
- [:& radio-button {:value "underline"
- :type "checkbox"
- :id "underline-text-decoration"
- :title (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline))
- :icon i/text-underlined}]
- [:& radio-button {:value "line-through"
- :type "checkbox"
- :id "line-through-text-decoration"
- :title (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through))
- :icon i/text-stroked}]]]))
+ [:> radio-buttons* {:selected (if (= text-decoration "none")
+ nil
+ text-decoration)
+ :on-change handle-change
+ :name "text-decoration-options"
+ :disabled (and token-typography-row-enabled? (some? token-applied))
+ :allow-empty true
+ :options options}]]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helpers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- get-option-by-name [options name]
+ (let [options (if (delay? options) (deref options) options)]
+ (d/seek #(= name (get % :name)) options)))
+
+(defn- resolve-delay [tokens]
+ (if (delay? tokens) @tokens tokens))
+
+(defn- find-token-by-id [tokens id]
+ (->> (:typography tokens)
+ (d/seek #(= (:id %) (uuid/uuid id)))))
+
+(defn- check-props [n-props o-props]
+ (and (identical? (unchecked-get n-props "ids")
+ (unchecked-get o-props "ids"))
+ (identical? (unchecked-get n-props "appliedTokens")
+ (unchecked-get o-props "appliedTokens"))
+ (identical? (unchecked-get n-props "values")
+ (unchecked-get o-props "values"))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Main component
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc text-menu*
- {::mf/wrap [mf/memo]}
- [{:keys [ids type values] :as props}]
+ {::mf/wrap [#(mf/memo' % check-props)]}
+ [{:keys [ids type values applied-tokens]}]
- (let [file-id (mf/use-ctx ctx/current-file-id)
- typographies (mf/deref refs/workspace-file-typography)
- libraries (mf/deref refs/files)
- label (case type
- :multiple (tr "workspace.options.text-options.title-selection")
- :group (tr "workspace.options.text-options.title-group")
- (tr "workspace.options.text-options.title"))
+ (let [file-id (mf/use-ctx ctx/current-file-id)
+ typographies (mf/deref refs/workspace-file-typography)
+ libraries (mf/deref refs/files)
+ ;; --- UI state
+ menu-state* (mf/use-state {:main-menu true
+ :more-options false})
+ menu-state (deref menu-state*)
+ main-menu-open? (:main-menu menu-state)
+ more-options-open? (:more-options menu-state)
- state* (mf/use-state {:main-menu true
- :more-options false})
- state (deref state*)
- main-menu-open? (:main-menu state)
- more-options-open? (:more-options state)
+ token-dropdown-open* (mf/use-state false)
+ token-dropdown-open? (deref token-dropdown-open*)
+ ;; --- Applied token
+ applied-token-name (:typography applied-tokens)
+ current-token-name* (mf/use-state applied-token-name)
+ current-token-name (deref current-token-name*)
+
+ ;; --- Available tokens
+ active-tokens (mf/use-ctx ctx/active-tokens-by-type)
+ typography-tokens (mf/with-memo [active-tokens] (csu/filter-tokens-for-input active-tokens :typography))
+
+ ;; --- Dropdown
+ listbox-id (mf/use-id)
+ nodes-ref (mf/use-ref nil)
+ dropdown-ref (mf/use-ref nil)
+
+ dropdown-options
+ (mf/with-memo [typography-tokens]
+ (csu/get-token-dropdown-options typography-tokens nil))
+
+ selected-token-id*
+ (mf/use-state #(when current-token-name
+ (:id (get-option-by-name dropdown-options current-token-name))))
+ selected-token-id (deref selected-token-id*)
+
+ ;; --- Typography
+ typography-id (:typography-ref-id values)
+ typography-file-id (:typography-ref-file values)
+
+ typography
+ (mf/with-memo [typography-id typography-file-id file-id libraries]
+ (cond
+ (and typography-id
+ (not= typography-id :multiple)
+ (not= typography-file-id file-id))
+ (-> (get-in libraries [typography-file-id :data :typographies typography-id])
+ (assoc :file-id typography-file-id))
+
+ (and typography-id
+ (not= typography-id :multiple)
+ (= typography-file-id file-id))
+ (get typographies typography-id)))
+
+ ;; --- Helpers
+ multiple? (->> values vals (d/seek #(= % :multiple)))
+
+ apply-token!
+ (mf/use-fn
+ (mf/deps ids typography-tokens)
+ (fn [id]
+ (let [token (find-token-by-id (resolve-delay typography-tokens) id)]
+ (reset! selected-token-id* id)
+ (reset! token-dropdown-open* false)
+ (st/emit!
+ (dwta/apply-token {:shape-ids ids
+ :attributes #{:typography}
+ :token token
+ :on-update-shape dwta/update-typography})))))
+ label
+ (mf/with-memo [type]
+ (case type
+ :multiple (tr "workspace.options.text-options.title-selection")
+ :group (tr "workspace.options.text-options.title-group")
+ (tr "workspace.options.text-options.title")))
+ set-option-ref
+ (mf/use-fn
+ (fn [node]
+ (let [state (d/nilv (mf/ref-val nodes-ref) #js {})
+ id (dom/get-data node "id")]
+ (mf/set-ref-val! nodes-ref (obj/set! state id node))
+ (fn []
+ (let [state (d/nilv (mf/ref-val nodes-ref) #js {})]
+ (mf/set-ref-val! nodes-ref (obj/unset! state id)))))))
+
+ ;; --- Toggles
toggle-main-menu
(mf/use-fn
- (mf/deps main-menu-open?)
- #(swap! state* assoc-in [:main-menu] (not main-menu-open?)))
+ #(swap! menu-state* update :main-menu not))
toggle-more-options
(mf/use-fn
- (mf/deps more-options-open?)
- #(swap! state* assoc-in [:more-options] (not more-options-open?)))
+ #(swap! menu-state* update :more-options not))
- typography-id (:typography-ref-id values)
- typography-file-id (:typography-ref-file values)
+ toggle-token-dropdown
+ (mf/use-fn
+ #(swap! token-dropdown-open* not))
+
+ ;; --- Event handlers
+ on-option-click
+ (mf/use-fn
+ (mf/deps apply-token!)
+ (fn [event]
+ (dom/stop-propagation event)
+ (let [id (dom/get-data (dom/get-current-target event) "id")]
+ (apply-token! id))))
emit-update!
(mf/use-fn
@@ -247,42 +384,24 @@
(fn [attrs]
(emit-update! ids attrs)))
- typography
- (mf/with-memo [values file-id libraries]
- (cond
- (and typography-id
- (not= typography-id :multiple)
- (not= typography-file-id file-id))
- (-> libraries
- (get-in [typography-file-id :data :typographies typography-id])
- (assoc :file-id typography-file-id))
-
- (and typography-id
- (not= typography-id :multiple)
- (= typography-file-id file-id))
- (get typographies typography-id)))
-
on-convert-to-typography
- (fn [_]
- (let [set-values (-> (d/without-nils values)
- (select-keys
- (d/concat-vec txt/text-font-attrs
- txt/text-spacing-attrs
- txt/text-transform-attrs)))
- typography (merge txt/default-typography set-values)
- typography (dwt/generate-typography-name typography)
- id (uuid/next)]
- (st/emit! (dwl/add-typography (assoc typography :id id) false))
- (emit-update! ids
- {:typography-ref-id id
- :typography-ref-file file-id})))
+ (mf/use-fn
+ (mf/deps values ids file-id emit-update!)
+ (fn [_]
+ (let [set-values (-> (d/without-nils values)
+ (select-keys (d/concat-vec txt/text-font-attrs
+ txt/text-spacing-attrs
+ txt/text-transform-attrs)))
+ typography (-> (merge txt/default-typography set-values)
+ (dwt/generate-typography-name))
+ id (uuid/next)]
+ (st/emit! (dwl/add-typography (assoc typography :id id) false))
+ (emit-update! ids {:typography-ref-id id :typography-ref-file file-id}))))
handle-detach-typography
(mf/use-fn
(mf/deps on-change)
- (fn []
- (on-change {:typography-ref-file nil
- :typography-ref-id nil})))
+ #(on-change {:typography-ref-file nil :typography-ref-id nil}))
handle-change-typography
(mf/use-fn
@@ -290,77 +409,116 @@
(fn [changes]
(st/emit! (dwl/update-typography (merge typography changes) file-id))))
+ detach-token
+ (mf/use-fn
+ (fn [token-name]
+ (st/emit! (dwta/unapply-token {:token-name token-name
+ :attributes #{:typography}
+ :shape-ids ids}))))
+
expand-stream
(mf/with-memo []
- (->> st/stream
- (rx/filter (ptk/type? :expand-text-more-options))))
+ (->> st/stream (rx/filter (ptk/type? :expand-text-more-options))))
- multiple? (->> values vals (d/seek #(= % :multiple)))
+ on-text-blur
+ (mf/use-fn
+ (fn []
+ (ts/schedule
+ 100
+ (fn []
+ (when (not= "INPUT" (-> (dom/get-active) dom/get-tag-name))
+ (dom/focus! (txu/get-text-editor-content)))))))
- opts (mf/props
- {:ids ids
- :values values
- :on-change on-change
- :show-recent true
- :on-blur
- (fn []
- (ts/schedule
- 100
- (fn []
- (when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name)))
- (let [node (txu/get-text-editor-content)]
- (dom/focus! node))))))})]
+ common-props (mf/props
+ {:ids ids
+ :values values
+ :on-change on-change
+ :show-recent true
+ :on-blur on-text-blur})]
(hooks/use-stream
expand-stream
- #(swap! state* assoc-in [:more-options] true))
+ #(swap! menu-state* assoc :more-options true))
- [:section {:class (stl/css :element-set)
- :aria-label "Text section"}
+ (mf/with-effect [applied-token-name]
+ (reset! current-token-name* applied-token-name))
+
+ (mf/with-effect [applied-token-name dropdown-options]
+ (reset! selected-token-id*
+ (when applied-token-name
+ (:id (get-option-by-name dropdown-options applied-token-name)))))
+
+ (mf/with-effect [token-dropdown-open?]
+ (when token-dropdown-open?
+ (ts/schedule 0 #(some-> (mf/ref-val dropdown-ref) dom/focus!))))
+
+ [:section {:class (stl/css :element-set)
+ :aria-label (tr "workspace.options.text-options.text-section")}
[:div {:class (stl/css :element-title)}
[:> title-bar* {:collapsable true
:collapsed (not main-menu-open?)
:on-collapsed toggle-main-menu
:title label
:class (stl/css :title-spacing-text)}
- (when (and (not typography) (not multiple?))
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "labels.options")
- :on-click on-convert-to-typography
- :icon i/add}])]]
+ [:*
+ (when (and token-typography-row-enabled? (some? (resolve-delay typography-tokens)) (not typography))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
+ :on-click toggle-token-dropdown
+ :tooltip-placement "top-left"
+ :icon i/tokens}])
+ (when (and (not typography) (not multiple?) (not applied-token-name))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.convert-to-typography")
+ :on-click on-convert-to-typography
+ :tooltip-placement "top-left"
+ :icon i/add}])]]]
(when main-menu-open?
[:div {:class (stl/css :element-content)}
(cond
+ (and token-typography-row-enabled? current-token-name)
+ [:> token-typography-row* {:token-name current-token-name
+ :detach-token detach-token
+ :active-tokens (resolve-delay typography-tokens)}]
+
typography
- [:& typography-entry {:file-id typography-file-id
+ [:& typography-entry {:file-id typography-file-id
:typography typography
- :local? (= typography-file-id file-id)
- :on-detach handle-detach-typography
- :on-change handle-change-typography}]
+ :local? (= typography-file-id file-id)
+ :on-detach handle-detach-typography
+ :on-change handle-change-typography}]
(= typography-id :multiple)
[:div {:class (stl/css :multiple-typography)}
[:span {:class (stl/css :multiple-text)} (tr "workspace.libraries.text.multiple-typography")]
- [:div {:class (stl/css :multiple-typography-button)
- :on-click handle-detach-typography
- :title (tr "workspace.libraries.text.multiple-typography-tooltip")}
- deprecated-icon/detach]]
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.libraries.text.multiple-typography-tooltip")
+ :on-click handle-detach-typography
+ :icon i/detach}]]
:else
- [:> text-options* opts])
+ [:> text-options* common-props])
[:div {:class (stl/css :text-align-options)}
- [:> text-align-options* opts]
- [:> grow-options* opts]
- [:> icon-button* {:variant "ghost"
- :aria-label (tr "labels.options")
+ [:> text-align-options* common-props]
+ [:> grow-options* common-props]
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "labels.options")
:data-testid "text-align-options-button"
- :on-click toggle-more-options
- :icon i/menu}]]
+ :on-click toggle-more-options
+ :icon i/menu}]]
(when more-options-open?
- [:div {:class (stl/css :text-decoration-options)}
- [:> vertical-align* opts]
- [:> text-decoration-options* opts]
- [:> text-direction-options* opts]])])]))
+ [:div {:class (stl/css :text-decoration-options)}
+ [:> vertical-align* common-props]
+ [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})]
+ [:> text-direction-options* common-props]])])
+
+ (when (and token-typography-row-enabled? token-dropdown-open?)
+ [:> searchable-options-dropdown* {:on-click on-option-click
+ :id listbox-id
+ :options (resolve-delay dropdown-options)
+ :selected selected-token-id
+ :align "right"
+ :ref set-option-ref}])]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss
index aa97d0ee32..e589c4dd79 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.scss
@@ -9,6 +9,8 @@
.element-set {
@include sidebar.option-grid-structure;
+
+ position: relative;
}
.element-title {
@@ -16,27 +18,31 @@
}
.element-content {
+ @include deprecated.flex-column;
+
grid-column: span 8;
- @include deprecated.flexColumn;
margin-top: deprecated.$s-4;
}
.multiple-typography {
- @extend .mixed-bar;
+ @extend %mixed-bar;
}
.multiple-text {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
flex-grow: 1;
color: var(--input-foreground-color-active);
}
.multiple-typography-button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-32;
width: deprecated.$s-28;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs
new file mode 100644
index 0000000000..9d9944a1d2
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.cljs
@@ -0,0 +1,87 @@
+;; 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.workspace.sidebar.options.menus.token-typography-row
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.data :as d]
+ [app.common.data.macros :as dm]
+ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
+ [app.main.ui.ds.tooltip :refer [tooltip*]]
+ [app.util.i18n :as i18n :refer [tr]]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(mf/defc resolved-value-tooltip*
+ {::mf/private true}
+ [{:keys [token-name resolved-value]}]
+ [:*
+ [:span (dm/str (tr "workspace.tokens.token-name") ": ")]
+ [:span {:class (stl/css :token-name-tooltip)} token-name]
+ [:div
+ [:span (tr "inspect.tabs.styles.token-resolved-value")]
+ [:ul
+ (for [[k v] resolved-value]
+ [:li {:key (d/name k)}
+ [:span {:class (stl/css :resolved-key)} (str "- " (d/name k) ": ")]
+ [:span {:class (stl/css :resolved-value)}
+ (if (sequential? v)
+ (str/join ", " (map #(dm/str "\"" % "\"") v))
+ (dm/str v))]])]]])
+
+(mf/defc token-typography-row*
+ [{:keys [token-name active-tokens detach-token] :rest props}]
+ (let [element-ref (mf/use-ref nil)
+ id (mf/use-id)
+
+ token (->> (:typography active-tokens)
+ (d/seek #(= (:name %) token-name)))
+
+ has-errors (some? (:errors token))
+ display-name (or (:name token) token-name)
+
+ resolved-value (:resolved-value token)
+ not-active (or (nil? token)
+ (empty? (:typography active-tokens)))
+ on-detach
+ (mf/use-fn
+ (mf/deps display-name)
+ (fn []
+ (detach-token display-name)))
+
+ tooltip-content (cond
+ not-active
+ (tr "not-active-token.no-name")
+ has-errors
+ (tr "options.deleted-token")
+ :else
+ (mf/html [:> resolved-value-tooltip* {:token-name token-name
+ :resolved-value resolved-value}]))]
+
+ [:div {:class (stl/css-case :token-typography-row true
+ :token-typography-row-with-errors has-errors
+ :token-typography-row-not-active not-active)}
+ (when (or has-errors not-active)
+ [:div {:class (stl/css :error-dot)}])
+ [:> icon* {:icon-id i/text-typography
+ :class (stl/css :icon)}]
+ [:> tooltip* {:content tooltip-content
+ :trigger-ref element-ref
+ :class (stl/css :token-tooltip)
+ :id id}
+
+ [:span {:aria-labelledby (dm/str id)
+ :class (stl/css :token-name)
+ :ref element-ref}
+ display-name]]
+
+ [:> icon-button* {:variant "action"
+ :aria-label (tr "token-actions.detach-token")
+ :tooltip-class (stl/css :detach-button)
+ :tooltip-placement "top-left"
+ :on-click on-detach
+ :icon i/detach}]]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss
new file mode 100644
index 0000000000..ac89984a55
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/token_typography_row.scss
@@ -0,0 +1,101 @@
+// 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/typography.scss" as t;
+@use "ds/_sizes.scss" as *;
+@use "ds/_borders.scss" as *;
+@use "ds/mixins.scss" as *;
+@use "ds/_utils.scss" as *;
+
+.token-typography-row {
+ --token-typography-row-background-color: var(--color-background-tertiary);
+ --token-typography-row-foreground-color: var(--color-token-foreground);
+ --token-typography-row-border-color: var(--color-token-border);
+
+ display: flex;
+ align-items: center;
+ position: relative;
+ gap: var(--sp-xs);
+ block-size: $sz-32;
+ min-inline-size: 0;
+ inline-size: 100%;
+ padding: var(--sp-s);
+ margin-inline-end: 0;
+ background: var(--token-typography-row-background-color);
+ border: $b-1 solid var(--token-typography-row-border-color);
+ border-radius: $br-8;
+
+ &:hover {
+ --token-typography-row-background-color: var(--color-token-background);
+ --token-typography-row-foreground-color: var(--color-foreground-primary);
+ --token-typography-row-border-color: var(--color-token-accent);
+ }
+}
+
+.token-typography-row-with-errors,
+.token-typography-row-not-active {
+ --token-typography-row-background-color: var(--color-background-primary);
+ --token-typography-row-foreground-color: var(--color-foreground-secondary);
+ --token-typography-row-border-color: var(--color-token-border);
+
+ &:hover {
+ --token-typography-row-background-color: var(--color-background-primary);
+ --token-typography-row-foreground-color: var(--color-foreground-secondary);
+ --token-typography-row-border-color: var(--color-token-background);
+ }
+}
+
+.icon {
+ display: block;
+ min-inline-size: $sz-16;
+ color: var(--token-typography-row-foreground-color);
+}
+
+.token-name {
+ @include t.use-typography("body-small");
+ @include text-ellipsis;
+
+ color: var(--token-typography-row-foreground-color);
+ block-size: $sz-32;
+ flex: 1;
+ line-height: $sz-32;
+}
+
+.token-tooltip {
+ min-inline-size: 0;
+ inline-size: inherit;
+}
+
+.token-name-tooltip {
+ color: var(--color-foreground-primary);
+}
+
+.detach-button {
+ flex-shrink: 0;
+ inline-size: 0;
+ max-inline-size: 0;
+ overflow: hidden;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.token-typography-row:hover .detach-button {
+ inline-size: auto;
+ opacity: 1;
+ pointer-events: auto;
+ max-inline-size: $sz-32;
+}
+
+.error-dot {
+ inline-size: px2rem(4);
+ block-size: px2rem(4);
+ border-radius: 50%;
+ background-color: var(--color-foreground-error);
+ margin-inline-start: var(--sp-xs);
+ position: absolute;
+ inset-inline-end: px2rem(1);
+ inset-block-start: px2rem(5);
+}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
index 3a27f8aefa..4266716f7b 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
@@ -16,6 +16,8 @@
[app.main.data.common :as dcm]
[app.main.data.fonts :as fts]
[app.main.data.shortcuts :as dsc]
+ [app.main.data.workspace.libraries :as dwl]
+ [app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
@@ -26,7 +28,9 @@
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.context :as ctx]
- [app.main.ui.ds.foundations.assets.icon :as i]
+ [app.main.ui.ds.buttons.button :refer [button*]]
+ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -79,8 +83,10 @@
:on-click on-click}
[:div {:class (stl/css-case :font-item true
:selected is-current)}
- [:span {:class (stl/css :label)} (:name font)]
- [:span {:class (stl/css :icon)} (when is-current deprecated-icon/tick)]]]))
+ [:span {:class (stl/css :font-item-label)} (:name font)]
+ (when is-current
+ [:> icon* {:icon-id i/tick
+ :size "s"}])]]))
(declare row-renderer)
@@ -188,7 +194,7 @@
:placeholder (tr "workspace.options.search-font")}]
(when (and recent-fonts show-recent)
[:section {:class (stl/css :show-recent)}
- [:p {:class (stl/css :title)} (tr "workspace.options.recent-fonts")]
+ [:p {:class (stl/css :header-title)} (tr "workspace.options.recent-fonts")]
(for [[idx font] (d/enumerate recent-fonts)]
[:> font-item* {:key (dm/str "font-" idx)
:font font
@@ -313,10 +319,11 @@
(some? font)
[:*
- [:span {:class (stl/css :name)}
+ [:span {:class (stl/css :font-option-name)}
(:name font)]
- [:span {:class (stl/css :icon)}
- deprecated-icon/arrow]]
+ [:> icon* {:icon-id i/arrow-down
+ :class (stl/css :dropdown-icon)
+ :size "s"}]]
:else
(tr "dashboard.fonts.deleted-placeholder"))]
@@ -463,9 +470,29 @@
(mf/defc typography-advanced-options
{::mf/wrap [mf/memo]}
- [{:keys [visible? typography editable? name-input-ref on-close on-change on-name-blur local? navigate-to-library on-key-down]}]
- (let [ref (mf/use-ref nil)
- font-data (fonts/get-font-data (:font-id typography))]
+ [{:keys [visible? typography editable? name-input-ref on-close on-change on-name-blur
+ local? navigate-to-library on-key-down file-id is-asset?]}]
+ (let [ref (mf/use-ref nil)
+ font-data (fonts/get-font-data (:font-id typography))
+ typography-id (:id typography)
+ show-actions? (and is-asset? editable?)
+
+ on-delete
+ (mf/use-fn
+ (mf/deps typography-id file-id on-close)
+ (fn []
+ (on-close)
+ (let [undo-id (js/Symbol)]
+ (st/emit! (dwu/start-undo-transaction undo-id)
+ (dwl/delete-typography typography-id)
+ (dwl/sync-file file-id file-id :typographies typography-id)
+ (dwu/commit-undo-transaction undo-id)))))
+
+ on-duplicate
+ (mf/use-fn
+ (mf/deps file-id typography-id)
+ (fn []
+ (st/emit! (dwl/duplicate-typography file-id typography-id))))]
(fonts/ensure-loaded! (:font-id typography))
(mf/use-effect
@@ -497,9 +524,21 @@
:on-key-down on-key-down
:on-blur on-name-blur}]
- [:div {:class (stl/css :action-btn)
- :on-click on-close}
- deprecated-icon/tick]]
+ [:div {:class (stl/css :action-btns)}
+ (when show-actions?
+ [:*
+ [:> icon-button* {:variant "action"
+ :aria-label (tr "workspace.assets.duplicate")
+ :on-click on-duplicate
+ :icon i/clipboard}]
+ [:> icon-button* {:variant "action"
+ :aria-label (tr "workspace.assets.delete")
+ :on-click on-delete
+ :icon i/delete}]])
+ [:> icon-button* {:variant "action"
+ :aria-label (tr "labels.close")
+ :on-click on-close
+ :icon i/tick}]]]
[:> text-options* {:values typography
:on-change on-change
@@ -519,9 +558,10 @@
(:name typography)]
[:span {:class (stl/css :typography-font)}
(:name font-data)]
- [:div {:class (stl/css :action-btn)
- :on-click on-close}
- deprecated-icon/menu]]
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "labels.close")
+ :on-click on-close
+ :icon i/menu}]]
[:div {:class (stl/css :info-row)}
[:span {:class (stl/css :info-label)} (tr "workspace.assets.typography.font-style")]
@@ -544,13 +584,13 @@
[:span {:class (stl/css :info-content)} (:text-transform typography)]]
(when-not local?
- [:a {:class (stl/css :link-btn)
- :on-click navigate-to-library}
+ [:> button* {:variant "secondary"
+ :on-click navigate-to-library}
(tr "workspace.assets.typography.go-to-edit")])])])))
(mf/defc typography-entry
{::mf/wrap-props false}
- [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? renaming? focus-name? external-open*]}]
+ [{:keys [file-id typography local? selected? on-click on-change on-detach on-context-menu editing? renaming? focus-name? external-open* is-asset?]}]
(let [name-input-ref (mf/use-ref)
read-only? (mf/use-ctx ctx/workspace-read-only?)
editable? (and local? (not read-only?))
@@ -650,12 +690,14 @@
(:name font-data)])])
[:div {:class (stl/css :element-set-actions)}
(when ^boolean on-detach
- [:button {:class (stl/css :element-set-actions-button)
- :on-click on-detach}
- deprecated-icon/detach])
- [:button {:class (stl/css :menu-btn)
- :on-click on-open}
- deprecated-icon/menu]]]
+ [:> icon-button* {:variant "action"
+ :aria-label (tr "settings.detach")
+ :on-click on-detach
+ :icon i/detach}])
+ [:> icon-button* {:variant "action"
+ :aria-label (tr "labels.open")
+ :on-click on-open
+ :icon i/menu}]]]
[:& typography-advanced-options
{:visible? open?
@@ -666,5 +708,7 @@
:on-change on-change
:on-name-blur on-name-blur
:on-key-down on-key-down
+ :file-id file-id
+ :is-asset? is-asset?
:local? local?
:navigate-to-library navigate-to-library}]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss
index 506fb58b34..d01a04d186 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.scss
@@ -5,58 +5,59 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
+@use "ds/typography.scss" as t;
+@use "ds/_sizes.scss" as *;
+@use "ds/_borders.scss" as *;
+@use "ds/mixins.scss" as *;
+@use "ds/_utils.scss" as *;
.typography-entry {
+ --actions-visibility: hidden;
+ --typography-entry-background-color: var(--color-background-tertiary);
+ --typography-entry-foreground-color: var(--color-foreground-primary);
+ --typography-entry-border-color: transparent;
+
display: flex;
flex-direction: row;
align-items: center;
- height: deprecated.$s-32;
- width: 100%;
- border-radius: deprecated.$br-8;
- background-color: var(--assets-item-background-color);
- color: var(--assets-item-name-foreground-color-hover);
+ block-size: $sz-32;
+ inline-size: 100%;
+ border-radius: $br-8;
+ background-color: var(--typography-entry-background-color);
+ color: var(--typography-entry-foreground-color);
+ border: $b-1 solid var(--typography-entry-border-color);
+
&:hover,
&:focus-within {
- background-color: var(--assets-item-background-color-hover);
- color: var(--assets-item-name-foreground-color-hover);
+ --typography-entry-background-color: var(--color-background-quaternary);
+ --typography-entry-foreground-color: var(--color-foreground-primary);
}
&.selected {
- border: deprecated.$s-1 solid var(--assets-item-border-color);
- }
-
- .element-set-actions {
- display: flex;
- visibility: hidden;
- .element-set-actions-button,
- .menu-btn {
- @extend .button-tertiary;
- height: deprecated.$s-32;
- width: deprecated.$s-28;
- svg {
- @extend .button-icon;
- }
- &:active {
- background-color: transparent;
- }
- }
+ --typography-entry-border-color: var(--color-accent-primary);
}
&:hover {
- background-color: var(--assets-item-background-color-hover);
+ --typography-entry-background-color: var(--color-background-quaternary);
+
.element-set-actions {
- visibility: visible;
+ --actions-visibility: visible;
}
}
}
+.element-set-actions {
+ display: flex;
+ visibility: var(--actions-visibility);
+}
+
.typography-selection-wrapper {
display: grid;
- grid-template-columns: deprecated.$s-24 auto 1fr;
+ grid-template-columns: $sz-24 auto 1fr;
flex: 1;
- height: 100%;
- width: 100%;
- padding: 0 deprecated.$s-12;
+ block-size: 100%;
+ inline-size: 100%;
+ padding: 0 var(--sp-m);
&.is-selectable {
cursor: pointer;
@@ -64,372 +65,333 @@
}
.typography-sample {
- @include deprecated.flexCenter;
- min-width: deprecated.$s-24;
- height: deprecated.$s-32;
- color: var(--assets-item-name-foreground-color);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-inline-size: $sz-24;
+ block-size: $sz-32;
+ color: var(--color-foreground-secondary);
}
.typography-name,
.typography-font {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
+ @include t.use-typography("body-small");
+ @include text-ellipsis;
+
display: block;
align-self: center;
- margin-left: deprecated.$s-6;
+ margin-inline-start: px2rem(6);
}
.typography-name {
- color: var(--assets-item-name-foreground-color);
+ color: var(--color-foreground-primary);
}
.typography-font {
- color: var(--assets-item-name-foreground-color-rest);
+ color: var(--color-foreground-secondary);
}
.font-name-wrapper {
- @include deprecated.bodySmallTypography;
+ --font-name-wrapper-foreground-color: var(--color-foreground-primary);
+ --font-name-wrapper-background-color: var(--color-background-tertiary);
+ --font-name-wrapper-border-color: transparent;
+
+ @include t.use-typography("body-small");
+
display: flex;
align-items: center;
- height: deprecated.$s-32;
- width: 100%;
- border-radius: deprecated.$br-8;
- border: deprecated.$s-1 solid transparent;
+ block-size: $sz-32;
+ inline-size: 100%;
+ border-radius: $br-8;
+ border: $b-1 solid var(--font-name-wrapper-border-color);
box-sizing: border-box;
- background-color: var(--assets-item-background-color);
- margin-bottom: deprecated.$s-4;
- padding: deprecated.$s-8 deprecated.$s-0 deprecated.$s-8 deprecated.$s-12;
+ background-color: var(--font-name-wrapper-background-color);
+ margin-block-end: var(--sp-s);
+ padding: var(--sp-s) 0 var(--sp-s) var(--sp-m);
- .typography-sample-input {
- @include deprecated.flexCenter;
- width: deprecated.$s-24;
- height: 100%;
- font-size: deprecated.$fs-16;
- color: var(--assets-item-name-foreground-color-hover);
- }
- .adv-typography-name {
- @include deprecated.removeInputStyle;
- font-size: deprecated.$fs-12;
- color: var(--input-foreground-color-active);
- flex-grow: 1;
- padding-left: deprecated.$s-6;
- margin: 0;
- }
- .action-btn {
- @extend .button-tertiary;
- @include deprecated.flexCenter;
- width: deprecated.$s-28;
- height: deprecated.$s-28;
- svg {
- @extend .button-icon-small;
- stroke: var(--icon-foreground);
- }
- &:active {
- background-color: transparent;
- }
- }
&:focus-within {
- border: deprecated.$s-1 solid var(--input-border-color-active);
- .adv-typography-name {
- color: var(--input-foreground-color-active);
- }
+ --font-name-wrapper-border-color: var(--color-accent-primary);
}
+
&:hover {
- background-color: var(--assets-item-background-color-hover);
+ --font-name-wrapper-background-color: var(--color-background-quaternary);
}
}
+.typography-sample-input {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ inline-size: $sz-24;
+ block-size: 100%;
+ font-size: px2rem(16);
+ color: var(--color-foreground-primary);
+}
+
+.adv-typography-name {
+ font-size: px2rem(12);
+ color: var(--font-name-wrapper-foreground-color);
+ flex-grow: 1;
+ padding-inline-start: px2rem(6);
+ margin: 0;
+ border: none;
+ background: none;
+ outline: none;
+}
+
+.action-btns {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-xxs);
+}
+
.advanced-options-wrapper {
- height: 100%;
- width: 100%;
- background-color: var(--assets-title-background-color);
+ block-size: 100%;
+ inline-size: 100%;
+ background-color: var(--color-background-primary);
}
.typography-info-wrapper {
- @include deprecated.flexColumn;
- margin-bottom: deprecated.$s-12;
- .typography-name-wrapper {
- @extend .asset-element;
- display: grid;
- grid-template-columns: deprecated.$s-24 auto 1fr deprecated.$s-28;
- flex: 1;
- height: deprecated.$s-32;
- width: 100%;
- padding: 0 0 0 deprecated.$s-12;
- background-color: var(--assets-item-background-color-hover);
- margin-bottom: deprecated.$s-4;
- .typography-sample {
- @include deprecated.flexCenter;
- min-width: deprecated.$s-24;
- font-size: deprecated.$fs-16;
- height: deprecated.$s-32;
- padding: 0;
- color: var(--assets-item-name-foreground-color-hover);
- }
- .typography-name {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
- display: flex;
- align-items: center;
- justify-content: flex-start;
- margin-left: deprecated.$s-6;
- color: var(--assets-item-name-foreground-color-hover);
- }
- .typography-font {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
- margin-left: deprecated.$s-6;
- display: flex;
- align-items: center;
- justify-content: flex-start;
- min-width: 0;
- color: var(--assets-item-name-foreground-color);
- }
- .action-btn {
- @extend .button-tertiary;
- width: deprecated.$s-28;
- height: deprecated.$s-32;
- svg {
- @extend .button-icon;
- }
- &:active {
- background-color: transparent;
- }
- }
- }
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xxs);
+ margin-block-end: var(--sp-m);
+}
- .info-row {
- display: grid;
- grid-template-columns: 50% 50%;
- height: deprecated.$s-32;
- --calculated-width: calc(var(--right-sidebar-width) - deprecated.$s-48);
- padding-left: deprecated.$s-2;
- .info-label {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
- width: calc(var(--calculated-width) / 2);
- padding-top: deprecated.$s-8;
- color: var(--assets-item-name-foreground-color);
- }
- .info-content {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
- padding-top: deprecated.$s-8;
- width: calc(var(--calculated-width) / 2);
- color: var(--assets-item-name-foreground-color-hover);
- }
- }
+.typography-name-wrapper {
+ @extend %asset-element;
- .link-btn {
- @include deprecated.uppercaseTitleTipography;
- @extend .button-secondary;
- width: 100%;
- height: deprecated.$s-32;
- border-radius: deprecated.$br-8;
- &:hover {
- background-color: var(--button-secondary-background-color-hover);
- color: var(--button-secondary-foreground-color-hover);
- border: deprecated.$s-1 solid var(--button-secondary-border-color-hover);
- text-decoration: none;
- svg {
- stroke: var(--button-secondary-foreground-color-hover);
- }
- }
- &:focus {
- background-color: var(--button-secondary-background-color-focus);
- color: var(--button-secondary-foreground-color-focus);
- border: deprecated.$s-1 solid var(--button-secondary-border-color-focus);
- svg {
- stroke: var(--button-secondary-foreground-color-focus);
- }
- }
- }
+ display: grid;
+ grid-template-columns: $sz-24 auto 1fr $sz-28;
+ flex: 1;
+ block-size: $sz-32;
+ inline-size: 100%;
+ padding: 0 0 0 var(--sp-m);
+ background-color: var(--color-background-quaternary);
+ margin-block-end: var(--sp-xs);
+}
+
+.typography-sample {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-inline-size: $sz-24;
+ font-size: px2rem(16);
+ block-size: $sz-32;
+ padding: 0;
+ color: var(--color-foreground-primary);
+}
+
+.typography-name,
+.typography-font {
+ @include t.use-typography("body-small");
+ @include text-ellipsis;
+
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin-inline-start: px2rem(6);
+ min-inline-size: 0;
+ color: var(--color-foreground-primary);
+}
+
+.info-row {
+ --calculated-width: calc(var(--right-sidebar-width) - $sz-48);
+
+ display: grid;
+ grid-template-columns: 50% 50%;
+ block-size: $sz-32;
+ padding-left: var(--sp-xs);
+}
+
+.info-label,
+.info-content {
+ @include t.use-typography("body-small");
+ @include text-ellipsis;
+
+ inline-size: calc(var(--calculated-width) / 2);
+ padding-block-start: var(--sp-s);
+ color: var(--color-foreground-primary);
}
.text-options {
- @include deprecated.flexColumn;
- max-width: var(--options-width);
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
+ max-inline-size: var(--options-width);
&:not(.text-options-full-size) {
position: relative;
}
- .font-option {
- @include deprecated.bodySmallTypography;
- @extend .asset-element;
- padding: deprecated.$s-8 0 deprecated.$s-8 deprecated.$s-8;
- cursor: pointer;
+}
- .name {
- flex-grow: 1;
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
- .icon {
- @include deprecated.flexCenter;
- height: deprecated.$s-28;
- width: deprecated.$s-28;
- svg {
- @extend .button-icon-small;
- stroke: var(--icon-foreground);
- transform: rotate(90deg);
- }
- }
- }
- .font-modifiers {
+.font-option {
+ @include t.use-typography("body-small");
+ @extend %asset-element;
+
+ padding: var(--sp-s);
+ cursor: pointer;
+}
+
+.font-option-name {
+ @include text-ellipsis;
+
+ flex-grow: 1;
+ inline-size: 100%;
+}
+
+.font-modifiers {
+ display: flex;
+ gap: var(--sp-xs);
+}
+
+.font-size-options {
+ @extend %asset-element;
+ @include t.use-typography("body-small");
+
+ flex-grow: 1;
+ inline-size: px2rem(60);
+ margin: 0;
+ padding: 0;
+ border: $b-1 solid var(--color-background-tertiary);
+ position: relative;
+}
+
+.font-variant-options {
+ padding: 0;
+ flex-grow: 2;
+}
+
+.typography-variations {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-xs);
+}
+
+.spacing-options {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-xs);
+}
+
+.line-height,
+.letter-spacing {
+ @extend %input-element;
+ @include t.use-typography("body-small");
+
+ .icon {
display: flex;
- gap: deprecated.$s-4;
- .font-size-options {
- @extend .asset-element;
- @include deprecated.bodySmallTypography;
- flex-grow: 1;
- width: deprecated.$s-60;
- margin: 0;
- padding: 0;
- border: deprecated.$s-1 solid var(--input-border-color);
- position: relative;
+ justify-content: center;
+ align-items: center;
+ inline-size: $sz-28;
- .icon {
- @include deprecated.flexCenter;
- height: deprecated.$s-28;
- min-width: deprecated.$s-28;
- svg {
- @extend .button-icon-small;
- stroke: var(--icon-foreground);
- transform: rotate(90deg);
- }
- }
- }
- .font-variant-options {
- padding: 0;
- flex-grow: 2;
- }
- }
- .typography-variations {
- @include deprecated.flexRow;
- .spacing-options {
- @include deprecated.flexRow;
- .line-height,
- .letter-spacing {
- @extend .input-element;
- @include deprecated.bodySmallTypography;
- .icon {
- @include deprecated.flexCenter;
- width: deprecated.$s-28;
- svg {
- @extend .button-icon-small;
- }
- }
- }
- }
- .text-transform {
- @extend .asset-element;
- width: fit-content;
- padding: 0;
- background-color: var(--radio-btns-background-color);
- &:hover {
- background-color: var(--radio-btns-background-color);
- }
+ svg {
+ @extend %button-icon-small;
}
}
}
+.text-transform {
+ @extend %asset-element;
+
+ inline-size: fit-content;
+ padding: 0;
+ background-color: var(--color-background-tertiary);
+}
+
.font-size-select {
- @include deprecated.removeInputStyle;
- @include deprecated.bodySmallTypography;
- height: deprecated.$s-32;
- height: 100%;
- width: 100%;
+ @include t.use-typography("body-small");
+
+ block-size: 100%;
+ inline-size: 100%;
margin: 0;
- padding: deprecated.$s-8;
+ padding: var(--sp-s);
+ border: none;
+ background: none;
+ outline: none;
+
.numeric-input {
- @extend .input-base;
- @include deprecated.bodySmallTypography;
+ @extend %input-base;
+ @include t.use-typography("body-small");
+
padding: 0;
}
}
.font-selector {
- @include deprecated.flexCenter;
+ display: flex;
+ justify-content: center;
+ align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
- height: 100%;
- width: 100%;
- z-index: deprecated.$z-index-4;
+ block-size: 100%;
+ inline-size: 100%;
+ z-index: var(--z-index-dropdown);
}
.show-recent {
- border-radius: deprecated.$br-8 deprecated.$br-8 0 0;
- background: var(--dropdown-background-color);
- border: deprecated.$s-1 solid var(--color-background-quaternary);
+ border-radius: $br-8 $br-8 0 0;
+ background: var(--color-background-tertiary);
+ border: $b-1 solid var(--color-background-quaternary);
border-block-end: none;
}
.font-selector-dropdown {
- width: 100%;
+ inline-size: 100%;
+
&:not(.font-selector-dropdown-full-size) {
display: flex;
flex-direction: column;
flex-grow: 1;
- height: 100%;
- }
- .header {
- display: grid;
- row-gap: deprecated.$s-2;
- .title {
- @include deprecated.uppercaseTitleTipography;
- color: var(--title-foreground-color);
- margin: 0;
- padding: deprecated.$s-12;
- }
+ block-size: 100%;
}
}
+.header {
+ display: grid;
+ row-gap: var(--sp-xxs);
+}
+
+.header-title {
+ @include t.use-typography("headline-small");
+
+ color: var(--color-foreground-secondary);
+ margin: 0;
+ padding: var(--sp-m);
+}
+
.font-wrapper {
- padding-bottom: deprecated.$s-4;
+ padding-bottom: var(--sp-xs);
cursor: pointer;
}
.font-item {
- @extend .asset-element;
- margin-bottom: deprecated.$s-4;
- border-radius: deprecated.$br-8;
- display: flex;
- .icon {
- @include deprecated.flexCenter;
- height: deprecated.$s-28;
- width: deprecated.$s-28;
- svg {
- @extend .button-icon-small;
- stroke: var(--icon-foreground);
- }
- }
- &.selected {
- color: var(--assets-item-name-foreground-color-hover);
- .icon {
- svg {
- stroke: var(--assets-item-name-foreground-color-hover);
- }
- }
- }
+ @extend %asset-element;
- .label {
- @include deprecated.bodySmallTypography;
- @include deprecated.textEllipsis;
- flex-grow: 1;
- min-width: 0;
+ margin-bottom: var(--sp-xs);
+ border-radius: $br-8;
+ display: flex;
+
+ &.selected {
+ color: var(--color-foreground-primary);
}
}
+.font-item-label {
+ @include t.use-typography("body-small");
+ @include text-ellipsis;
+
+ flex-grow: 1;
+ min-inline-size: 0;
+}
+
.font-selector-dropdown-full-size {
- height: calc(100vh - 48px); // TODO: ugly hack :( Find a workaround for this.
+ block-size: calc(100vh - 48px); // TODO: ugly hack :( Find a workaround for this.
display: grid;
grid-template-rows: auto 1fr;
- padding: deprecated.$s-2 deprecated.$s-12 deprecated.$s-12 deprecated.$s-12;
+ padding: var(--sp-xxs) var(--sp-m) var(--sp-m) var(--sp-m);
}
.fonts-list {
@@ -437,22 +399,23 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
- min-height: 100%;
- width: 100%;
- height: 100%;
- padding: deprecated.$s-2;
- border-radius: deprecated.$br-8;
- background-color: var(--dropdown-background-color);
+ min-block-size: 100%;
+ inline-size: 100%;
+ block-size: 100%;
+ padding: var(--sp-xxs);
+ border-radius: $br-8;
+ background-color: var(--color-background-tertiary);
overflow: hidden;
+
&:not(.fonts-list-full-size) {
- margin-block-start: deprecated.$s-2;
+ margin-block-start: var(--sp-xxs);
}
}
.fonts-list-full-size {
border-start-start-radius: 0;
border-start-end-radius: 0;
- border: deprecated.$s-1 solid var(--color-background-quaternary);
+ border: $b-1 solid var(--color-background-quaternary);
// TODO: this should belong to typography-entry , but atm we don't have a clear
// way of accessing whether we are in fullsize mode or not
@@ -460,3 +423,7 @@
padding-inline-end: 0;
}
}
+
+.dropdown-icon {
+ color: var(--color-foreground-secondary);
+}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss
index 5d77b269d2..98b71bd436 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.scss
@@ -7,11 +7,11 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
max-width: deprecated.$s-888;
width: 100%;
@@ -31,7 +31,7 @@
}
.modal-close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.rule-list {
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs
index 740750b0cf..c5876940b8 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs
@@ -24,17 +24,39 @@
(-> (l/key :background)
(l/derived refs/workspace-page)))
+(def ^:private ref:pixel-grid-color
+ (-> (l/key :pixel-grid-color)
+ (l/derived refs/workspace-page)))
+
+(def ^:private ref:pixel-grid-opacity
+ (-> (l/key :pixel-grid-opacity)
+ (l/derived refs/workspace-page)))
+
+;; Default pixel grid color shown in the picker when the user hasn't
+;; set a custom one. Matches the legacy hardcoded CSS variable.
+(def ^:private default-pixel-grid-color "#0070E4")
+
(mf/defc options*
{::mf/wrap [mf/memo]}
[]
(let [background (mf/deref ref:background-color)
+ grid-color (mf/deref ref:pixel-grid-color)
+ grid-alpha (mf/deref ref:pixel-grid-opacity)
+
on-change (mf/use-fn #(st/emit! (dw/change-canvas-color %)))
on-open (mf/use-fn #(st/emit! (dwu/start-undo-transaction :options)))
on-close (mf/use-fn #(st/emit! (dwu/commit-undo-transaction :options)))
+ on-grid-change
+ (mf/use-fn #(st/emit! (dw/change-pixel-grid-color %)))
+
color (mf/with-memo [background]
{:color (d/nilv background clr/canvas)
- :opacity 1})]
+ :opacity 1})
+
+ grid (mf/with-memo [grid-color grid-alpha]
+ {:color (d/nilv grid-color default-pixel-grid-color)
+ :opacity (d/nilv grid-alpha 0.2)})]
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
@@ -52,5 +74,15 @@
:on-change on-change
:origin :canvas
:on-open on-open
+ :on-close on-close}]
+
+ [:> color-row*
+ {:disable-gradient true
+ :disable-image true
+ :title "Pixel grid color"
+ :color grid
+ :on-change on-grid-change
+ :origin :pixel-grid
+ :on-open on-open
:on-close on-close}]]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
index 1e0e3bb67b..229deb9460 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
@@ -11,7 +11,6 @@
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[app.common.types.shape.attrs :refer [default-color]]
- [app.common.types.token :as tk]
[app.config :as cfg]
[app.main.data.modal :as modal]
[app.main.data.workspace.colors :as dwc]
@@ -27,6 +26,7 @@
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.main.ui.formats :as fmt]
[app.main.ui.hooks :as h]
+ [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -98,16 +98,16 @@
token-name-ref (mf/use-ref nil)
swatch-tooltip-content (cond
not-active
- (tr "ds.inputs.token-field.no-active-color.token-option")
+ (tr "not-active-token.no-name")
has-errors
- (tr "color-row.token-color-row.deleted-token")
+ (tr "options.deleted-token")
:else
(tr "workspace.tokens.resolved-value" resolved))
name-tooltip-content (cond
not-active
- (tr "ds.inputs.token-field.no-active-color.token-option")
+ (tr "not-active-token.no-name")
has-errors
- (tr "color-row.token-color-row.deleted-token")
+ (tr "options.deleted-token")
:else
#(mf/html
[:div
@@ -138,7 +138,7 @@
[:div {:class (stl/css :token-actions)}
[:> icon-button*
{:variant "action"
- :aria-label (tr "ds.inputs.token-field.detach-token")
+ :aria-label (tr "token-actions.detach-token")
:on-click on-detach-token
:icon i/detach}]
[:> icon-button*
@@ -177,12 +177,9 @@
active-tokens* (mf/use-ctx ctx/active-tokens-by-type)
- tokens (mf/with-memo [active-tokens* origin]
- (let [origin (if (= :color-selection origin) :fill origin)]
- (delay
- (-> (deref active-tokens*)
- (select-keys (get tk/tokens-by-input origin))
- (not-empty)))))
+ tokens (mf/with-memo [active-tokens* origin]
+ (csu/filter-tokens-for-input active-tokens* origin))
+
on-focus'
(mf/use-fn
(mf/deps on-focus)
@@ -355,10 +352,6 @@
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))]
- (when (= applied-token :multiple)
- ;; (js/console.trace "color-row*")
- (prn "color-row*" index color applied-token))
-
(mf/with-effect [color prev-color disable-picker]
(when (and (not disable-picker) (not= prev-color color))
(modal/update-props! :colorpicker {:data (parse-color color)})))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss
index 8e88e54ed6..39f56950d8 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.scss
@@ -54,8 +54,10 @@
--color-name-wrapper-background-color: var(--color-background-tertiary);
--color-name-wrapper-foreground-color: var(--color-foreground-primary);
--color-name-wrapper-boder-color: var(--color-background-tertiary);
+
@include t.use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
display: flex;
align-items: center;
flex-grow: 1;
@@ -66,13 +68,13 @@
padding: 0;
margin-inline-end: 0;
border: $b-1 solid var(--color-name-wrapper-boder-color);
- border-radius: $br-8;
background-color: var(--color-name-wrapper-background-color);
color: var(--color-name-wrapper-foreground-color);
border-radius: $br-8 0 0 $br-8;
&.no-opacity {
border-radius: $br-8;
+
.color-input-wrapper {
border-radius: $br-8;
}
@@ -86,6 +88,7 @@
.detach-btn {
display: grid;
}
+
&.editing {
--color-name-wrapper-background-color: var(--color-background-primary);
}
@@ -101,6 +104,7 @@
&:focus-within {
--color-name-wrapper-background-color: var(--color-background-tertiary);
--color-name-wrapper-boder-color: var(--color-accent-primary);
+
&:hover {
--color-name-wrapper-background-color: var(--color-background-quaternary);
}
@@ -108,6 +112,7 @@
&.editing {
--color-name-wrapper-background-color: var(--color-background-primary);
+
&:hover {
--color-name-wrapper-boder-color: var(--color-accent-primary);
}
@@ -121,6 +126,7 @@
.color-input-wrapper {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
flex-grow: 1;
@@ -135,7 +141,8 @@
.color-name {
@include t.use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
flex-grow: 1;
padding-inline: px2rem(6);
border-radius: $br-8;
@@ -150,13 +157,15 @@
padding: 0 var(--sp-xxs) 0 var(--sp-s);
border-radius: $br-8 0 0 $br-8;
background-color: transparent;
+
&:hover {
background-color: transparent;
}
}
.color-input {
- @include textEllipsis;
+ @include text-ellipsis;
+
border: none;
background: none;
outline: none;
@@ -167,6 +176,7 @@
padding: 0 0 0 px2rem(6);
border-radius: $br-8;
color: var(--input-foreground-color-active);
+
&[disabled] {
opacity: 0.5;
pointer-events: none;
@@ -182,8 +192,14 @@
--opacity-input-boder-color: var(--color-background-tertiary);
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
+
+ &:not(:focus-within) {
+ cursor: ew-resize;
+ }
+
block-size: $sz-32;
inline-size: px2rem(60);
padding-inline-start: var(--sp-xs);
@@ -198,6 +214,7 @@
.detach-btn {
display: grid;
}
+
&.editing {
--opacity-input-background-color: var(--color-background-primary);
}
@@ -213,6 +230,7 @@
&:focus-within {
--opacity-input-background-color: var(--color-background-tertiary);
--opacity-input-boder-color: var(--color-accent-primary);
+
&:hover {
--opacity-input-background-color: var(--color-background-quaternary);
}
@@ -220,6 +238,7 @@
&.editing {
--opacity-input-background-color: var(--color-background-primary);
+
&:hover {
--opacity-input-boder-color: var(--color-accent-primary);
}
@@ -227,12 +246,12 @@
}
.opacity-input {
- @include textEllipsis;
+ @include text-ellipsis;
+
block-size: $sz-28;
min-inline-size: $sz-28;
flex-grow: 1;
inline-size: 100%;
- padding: 0;
border-radius: 0 $br-8 $br-8 0;
border: none;
background: none;
@@ -240,6 +259,12 @@
margin: var(--sp-xxs) 0;
padding: 0 0 0 px2rem(6);
color: var(--color-foreground-primary);
+ cursor: ew-resize;
+
+ &:focus {
+ cursor: text;
+ }
+
&[disabled] {
opacity: 0.5;
pointer-events: none;
@@ -263,6 +288,7 @@
--token-color-wrapper-foreground-color: var(--color-token-foreground);
--token-color-wrapper-border-color: var(--color-token-border);
--token-actions-display: none;
+
display: grid;
grid-template-columns: auto 1fr auto;
gap: var(--sp-xs);
@@ -274,6 +300,7 @@
background: var(--token-color-wrapper-background-color);
border: $b-1 solid var(--token-color-wrapper-border-color);
border-radius: $br-8;
+
&:hover {
--token-color-wrapper-background-color: var(--color-token-background);
--token-color-wrapper-foreground-color: var(--color-foreground-primary);
@@ -287,6 +314,7 @@
--token-color-wrapper-background-color: var(--color-background-primary);
--token-color-wrapper-foreground-color: var(--color-foreground-secondary);
--token-color-wrapper-border-color: var(--color-token-border);
+
&:hover {
--token-color-wrapper-background-color: var(--color-background-primary);
--token-color-wrapper-foreground-color: var(--color-foreground-secondary);
@@ -297,7 +325,8 @@
.token-name {
@include t.use-typography("body-small");
- @include textEllipsis;
+ @include text-ellipsis;
+
color: var(--token-color-wrapper-foreground-color);
block-size: $sz-32;
line-height: $sz-32;
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss
index f13404d03e..84109fe162 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/shadow_row.scss
@@ -80,8 +80,9 @@
.shadow-advanced-spread,
.shadow-advanced-offset-y {
// TODO remove this input by changing the input to DS component
- @extend .input-element;
+ @extend %input-element;
@include t.use-typography("body-small");
+
.shadow-advanced-label {
padding-inline-start: var(--sp-s);
inline-size: px2rem(60);
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs
index 64f34b7927..0e6a28612a 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs
@@ -16,8 +16,10 @@
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
+ [app.main.ui.workspace.sidebar.options.common :as soc]
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]]
@@ -38,6 +40,7 @@
on-stroke-cap-start-change
on-stroke-cap-end-change
on-stroke-cap-switch
+ on-toggle-visibility
disable-drag
on-focus
on-blur
@@ -47,7 +50,10 @@
select-on-focus
ids]}]
- (let [token-numeric-inputs
+ (let [hidden? (:hidden stroke)
+ hidden? (if (nil? hidden?) false hidden?)
+
+ token-numeric-inputs
(features/use-feature "tokens/numeric-input")
on-drop
@@ -92,14 +98,13 @@
on-width-change
(mf/use-fn
- (mf/deps index on-stroke-width-change)
+ (mf/deps index on-stroke-width-change ids)
(fn [value]
- (if (or (string? value) (number? value))
- (on-stroke-width-change index value)
-
- (st/emit! (dwta/toggle-token {:token (first value)
- :attrs #{:stroke-width}
- :shape-ids ids})))))
+ (soc/emit-value-or-token
+ value
+ #(on-stroke-width-change index %)
+ ids
+ #{:stroke-width})))
stroke-alignment (or (:stroke-alignment stroke) :center)
@@ -108,9 +113,9 @@
(d/concat-vec
(when (= :multiple stroke-alignment)
[{:value :multiple :label "--"}])
- [{:value :center :label (tr "workspace.options.stroke.center")}
- {:value :inner :label (tr "workspace.options.stroke.inner")}
- {:value :outer :label (tr "workspace.options.stroke.outer")}]))
+ [{:value :center :label (tr "workspace.options.stroke.center") :id "center" :icon "stroke-center"}
+ {:value :inner :label (tr "workspace.options.stroke.inner") :id "inner" :icon "stroke-inside"}
+ {:value :outer :label (tr "workspace.options.stroke.outer") :id "outer" :icon "stroke-outside"}]))
on-alignment-change
(mf/use-fn
@@ -122,10 +127,10 @@
(mf/deps ids)
(fn [_ token]
(st/emit!
- (dwta/toggle-token {:token token
- :attrs #{:stroke-color}
- :shape-ids ids
- :expand-with-children true}))))
+ (dwta/apply-token-from-input {:token token
+ :attrs #{:stroke-color}
+ :shape-ids ids
+ :expand-with-children true}))))
stroke-style (or (:stroke-style stroke) :solid)
@@ -134,10 +139,10 @@
(d/concat-vec
(when (= :multiple stroke-style)
[{:value :multiple :label "--"}])
- [{:value :solid :label (tr "workspace.options.stroke.solid")}
- {:value :dotted :label (tr "workspace.options.stroke.dotted")}
- {:value :dashed :label (tr "workspace.options.stroke.dashed")}
- {:value :mixed :label (tr "workspace.options.stroke.mixed")}]))
+ [{:value :solid :label (tr "workspace.options.stroke.solid") :id "solid" :icon "stroke-solid"}
+ {:value :dotted :label (tr "workspace.options.stroke.dotted") :id "dotted" :icon "stroke-dotted"}
+ {:value :dashed :label (tr "workspace.options.stroke.dashed") :id "dashed" :icon "stroke-dashed"}
+ {:value :mixed :label (tr "workspace.options.stroke.mixed") :id "mixed" :icon "stroke-mixed"}]))
on-style-change
(mf/use-fn
@@ -164,7 +169,7 @@
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
- (on-detach-token (first token) #{:stroke-width})))
+ (on-detach-token token #{:stroke-width})))
stroke-caps-options
[{:value nil :label (tr "workspace.options.stroke-cap.none")}
@@ -181,10 +186,18 @@
on-cap-switch
(mf/use-fn
(mf/deps index on-stroke-cap-switch)
- #(on-stroke-cap-switch index))]
+ #(on-stroke-cap-switch index))
+
+ on-toggle-visibility
+ (mf/use-fn
+ (mf/deps index on-toggle-visibility)
+ (fn []
+ (when on-toggle-visibility
+ (on-toggle-visibility index))))]
[:div {:class (stl/css-case
:stroke-data true
+ :hidden hidden?
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:aria-label (str "stroke-row-" index)}
@@ -196,26 +209,37 @@
;; Stroke Color
;; FIXME: memorize stroke color
- [:> color-row* {:color (ctc/stroke->color stroke)
- :index index
- :title title
- :on-change on-color-change-refactor
- :on-detach on-color-detach
- :on-remove on-remove
- :disable-drag disable-drag
- :applied-token (if (= index 0)
- stroke-color-token
- nil)
- :on-detach-token on-detach-token-color
- :on-token-change on-token-change
- :on-focus on-focus
- :origin :stroke-color
- :select-on-focus select-on-focus
- :on-blur on-blur}]
+ [:div {:class (stl/css :stroke-color-actions)}
+ [:> color-row* {:color (ctc/stroke->color stroke)
+ :index index
+ :title title
+ :on-change on-color-change-refactor
+ :on-detach on-color-detach
+ :disable-drag disable-drag
+ :applied-token (if (= index 0)
+ stroke-color-token
+ nil)
+ :on-detach-token on-detach-token-color
+ :on-token-change on-token-change
+ :on-focus on-focus
+ :origin :stroke-color
+ :select-on-focus select-on-focus
+ :on-blur on-blur}]
+
+ (when (some? on-toggle-visibility)
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.stroke.toggle-stroke")
+ :on-click on-toggle-visibility
+ :icon (if hidden? "hide" "shown")}])
+
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "workspace.options.stroke.remove-stroke")
+ :on-click on-remove
+ :icon i/remove}]]
;; Stroke Width, Alignment & Style
- [:div {:class (stl/css :stroke-options)}
- (if token-numeric-inputs
+ (if token-numeric-inputs
+ [:div {:class (stl/css :stroke-options-tokens)}
[:> numeric-input-wrapper* {:on-change on-width-change
:on-detach on-detach-token-width
:icon i/stroke-size
@@ -227,7 +251,25 @@
:property (tr "workspace.options.stroke-width")
:applied-token (get applied-tokens :stroke-width)
:value stroke-width}]
+ [:> select* {:default-selected (d/name stroke-alignment)
+ :options stroke-alignment-options
+ :variant "icon-only"
+ :data-testid "stroke.alignment"
+ :disabled (if (= :multiple hidden?) true hidden?)
+ :wrapper-class (stl/css :stroke-align-icon-select)
+ :on-change on-alignment-change}]
+ (when-not disable-stroke-style
+ [:> select* {:default-selected (d/name stroke-style)
+ :options stroke-style-options
+ :wrapper-class (stl/css :stroke-style-icon-select)
+ :data-testid "stroke.style"
+ :variant "icon-only"
+ :disabled (if (= :multiple hidden?) true hidden?)
+ :dropdown-alignment :right
+ :on-change on-style-change}])]
+
+ [:div {:class (stl/css :stroke-options)}
[:div {:class (stl/css :stroke-width-input)
:title (tr "workspace.options.stroke-width")}
[:> icon* {:icon-id i/stroke-size
@@ -238,31 +280,35 @@
:on-change on-width-change
:on-focus on-focus
:select-on-focus select-on-focus
- :on-blur on-blur}]])
+ :on-blur on-blur}]]
+ [:div {:class (stl/css :stroke-alignment-select)
+ :data-testid "stroke.alignment"}
+ [:& select {:default-value stroke-alignment
+ :options stroke-alignment-options
+ :disabled hidden?
+ :on-change on-alignment-change}]]
- [:div {:class (stl/css :stroke-alignment-select)
- :data-testid "stroke.alignment"}
- [:& select {:default-value stroke-alignment
- :options stroke-alignment-options
- :on-change on-alignment-change}]]
-
- (when-not disable-stroke-style
- [:div {:class (stl/css :stroke-style-select)
- :data-testid "stroke.style"}
- [:& select {:default-value stroke-style
- :options stroke-style-options
- :on-change on-style-change}]])]
+ (when-not disable-stroke-style
+ [:div {:class (stl/css :stroke-style-select)
+ :data-testid "stroke.style"}
+ [:& select {:default-value stroke-style
+ :options stroke-style-options
+ :disabled hidden?
+ :on-change on-style-change}]])])
;; Stroke Caps
(when show-caps
[:div {:class (stl/css :stroke-caps-options)}
[:& select {:default-value (:stroke-cap-start stroke)
:options stroke-caps-options
+ :disabled hidden?
:on-change on-caps-start-change}]
[:> icon-button* {:variant "secondary"
:aria-label (tr "labels.switch")
+ :disabled hidden?
:on-click on-cap-switch
:icon i/switch}]
[:& select {:default-value (:stroke-cap-end stroke)
:options stroke-caps-options
+ :disabled hidden?
:on-change on-caps-end-change}]])]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss
index 19f81ac9c2..c764e60f3f 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.scss
@@ -12,7 +12,6 @@
display: flex;
flex-direction: column;
gap: var(--sp-xs);
-
position: relative;
--reorder-left-position: calc(-1 * var(--sp-l));
@@ -28,25 +27,40 @@
&.dnd-over-bot {
--reorder-bottom-display: block;
}
+
+ &.hidden {
+ .stroke-options,
+ .stroke-options-tokens,
+ .stroke-caps-options {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ }
+}
+
+.stroke-color-actions {
+ display: flex;
+ align-items: center;
+
+ > :first-child {
+ flex: 1;
+ min-width: 0;
+ }
}
.stroke-options {
@include sidebar.option-grid-structure;
+
align-items: center;
}
.stroke-width-input {
grid-column: span 2;
- // TODO replace with numeric-input* from DS
- @extend .input-element;
-
+ @extend %input-element;
@include t.use-typography("body-small");
- padding-inline-start: var(--sp-xs);
-}
-.numeric-input-wrapper {
- grid-column: span 2;
+ padding-inline-start: var(--sp-xs);
}
.stroke-alignment-select {
@@ -62,3 +76,19 @@
grid-template-columns: 1fr auto 1fr;
column-gap: var(--sp-xs);
}
+
+.stroke-options-tokens {
+ @include sidebar.option-grid-structure;
+
+ grid-template-columns: var(--three-columns-width) var(--grid-exception-input-width-small) var(
+ --grid-exception-input-width-small
+ );
+}
+
+.stroke-align-icon-select {
+ --dropdown-width: var(--four-columns-width);
+}
+
+.stroke-style-icon-select {
+ --dropdown-width: var(--four-columns-width);
+}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs
index 6d6815da66..4da35907b2 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/group.cljs
@@ -102,7 +102,7 @@
[stroke-ids stroke-values stroke-tokens]
(get-attrs shapes objects :stroke)
- [text-ids text-values]
+ [text-ids text-values text-tokens]
(get-attrs shapes objects :text)
[layout-item-ids layout-item-values]
@@ -171,7 +171,10 @@
[:> blur-menu* {:type type :ids blur-ids :values blur-values}])
(when-not (empty? text-ids)
- [:> ot/text-menu* {:type type :ids text-ids :values text-values}])
+ [:> ot/text-menu* {:type type
+ :ids text-ids
+ :values text-values
+ :applied-tokens text-tokens}])
(when-not (empty? svg-values)
[:> svg-attrs-menu* {:ids ids :values svg-values}])
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
index 4a810239f1..70827abf97 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs
@@ -255,6 +255,9 @@
(cond
(= attr-group :measure) (select-measure-keys shape)
:else (select-keys shape editable-attrs)))
+ shape-values (cond-> shape-values
+ (= attr-group :layer)
+ (update :hidden #(if (nil? %) false %)))
new-token-acc (merge-token-values token-acc editable-attrs applied-tokens)]
[(conj ids id)
(merge-attrs values shape-values)
@@ -385,7 +388,7 @@
[layer-ids layer-values layer-tokens]
(get-attrs shapes objects :layer)
- [text-ids text-values]
+ [text-ids text-values text-tokens]
(get-attrs shapes objects :text)
[constraint-ids constraint-values]
@@ -478,7 +481,11 @@
[:> constraints-menu* {:ids constraint-ids :values constraint-values}])
(when-not (empty? text-ids)
- [:> ot/text-menu* {:type type :ids text-ids :values text-values}])
+ [:> ot/text-menu*
+ {:type type
+ :ids text-ids
+ :values text-values
+ :applied-tokens text-tokens}])
(when-not (empty? fill-ids)
[:> fill/fill-menu* {:type type
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs
index 96cc29b6ba..5825af579a 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs
@@ -179,6 +179,7 @@
[:> text-menu*
{:ids ids
:type type
+ :applied-tokens applied-tokens
:values text-values}]
[:> fill/fill-menu*
diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss
index 56376d9f02..8279a4970c 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss
@@ -9,6 +9,7 @@
.shortcuts {
display: grid;
grid-template-rows: auto auto 1fr;
+
// TODO: Fix this once we start implementing the DS.
// We should not be doign these hardcoded calc's.
height: calc(100vh - #{deprecated.$s-60});
@@ -27,7 +28,8 @@
}
.not-found {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--empty-message-foreground-color);
margin: deprecated.$s-12;
}
@@ -43,7 +45,8 @@
.section-title,
.subsection-title {
- @include deprecated.uppercaseTitleTipography;
+ @include deprecated.uppercase-title-typography;
+
display: flex;
align-items: center;
margin: 0;
@@ -64,9 +67,11 @@
text-transform: none;
padding-left: deprecated.$s-12;
}
+
.subsection-menu {
margin-bottom: deprecated.$s-4;
}
+
.sub-menu {
margin-bottom: deprecated.$s-4;
@@ -82,24 +87,29 @@
background-color: var(--pill-background-color);
.command-name {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
margin-left: deprecated.$s-2;
color: var(--pill-foreground-color);
}
+
.keys {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
gap: deprecated.$s-2;
color: var(--pill-foreground-color);
.key {
- @include deprecated.bodySmallTypography;
- @include deprecated.flexCenter;
+ @include deprecated.body-small-typography;
+ @include deprecated.flex-center;
+
text-transform: capitalize;
height: deprecated.$s-20;
padding: deprecated.$s-2 deprecated.$s-6;
border-radius: deprecated.$s-6;
background-color: var(--menu-shortcut-background-color);
}
+
.space {
margin: 0 deprecated.$s-2;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
index 4e06a6aea6..06be0b5d94 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs
@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
+ [app.common.types.page :as ctp]
[app.main.data.common :as dcm]
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
@@ -19,9 +20,8 @@
[app.main.ui.components.title-bar :refer [title-bar*]]
[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 :refer [icon*]]
[app.main.ui.hooks :as hooks]
- [app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.badge :refer [badge-notification]]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom]
@@ -51,8 +51,10 @@
each object change)"
[page-id]
(l/derived (fn [fdata]
- (-> (dsh/get-page fdata page-id)
- (dissoc :objects)))
+ (let [page (dsh/get-page fdata page-id)]
+ (-> page
+ (assoc :empty? (ctp/is-empty? page))
+ (dissoc :objects))))
refs/workspace-data
=))
@@ -63,32 +65,35 @@
(mf/defc page-item
{::mf/wrap-props false}
[{:keys [page index deletable? selected? editing? hovering? current-page-id]}]
- (let [input-ref (mf/use-ref)
- id (:id page)
- delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id)))
- navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id)))
- read-only? (mf/use-ctx ctx/workspace-read-only?)
+ (let [input-ref (mf/use-ref)
+ id (:id page)
+ name (:name page "")
+ is-separator? (and (= "---" (str/trim name)) (:empty? page))
+ delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id)))
+ navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id)))
+ read-only? (mf/use-ctx ctx/workspace-read-only?)
on-click
(mf/use-fn
- (mf/deps id current-page-id)
+ (mf/deps id current-page-id is-separator?)
(fn []
- ;; WASM page transitions:
- ;; - Capture the current page (A) once
- ;; - Show a blurred snapshot while the target page (B/C/...) renders
- ;; - If the user clicks again during the transition, keep showing the original (A) snapshot
- (if (and (features/active-feature? @st/state "render-wasm/v1")
- (not= id current-page-id))
- (do
- (-> (wasm.api/apply-canvas-blur)
- (p/finally
- (fn []
- ;; NOTE: it seems we need two RAF so the blur is actually applied and visible
- ;; in the canvas :(
- (timers/raf
- (fn []
- (timers/raf navigate-fn)))))))
- (navigate-fn))))
+ (when-not is-separator?
+ ;; WASM page transitions:
+ ;; - Capture the current page (A) once
+ ;; - Show a blurred snapshot while the target page (B/C/...) renders
+ ;; - If the user clicks again during the transition, keep showing the original (A) snapshot
+ (if (and (features/active-feature? @st/state "render-wasm/v1")
+ (not= id current-page-id))
+ (do
+ (-> (wasm.api/apply-canvas-blur)
+ (p/finally
+ (fn []
+ ;; NOTE: it seems we need two RAF so the blur is actually applied and visible
+ ;; in the canvas :(
+ (timers/raf
+ (fn []
+ (timers/raf navigate-fn)))))))
+ (navigate-fn)))))
on-delete
(mf/use-fn
@@ -112,11 +117,14 @@
on-blur
(mf/use-fn
+ (mf/deps id is-separator?)
(fn [event]
- (let [name (str/trim (dom/get-target-val event))]
- (when-not (str/empty? name)
- (st/emit! (dw/rename-page id name)))
- (st/emit! (dw/stop-rename-page-item)))))
+ (let [new-name (str/trim (dom/get-target-val event))]
+ (if (str/empty? new-name)
+ (when is-separator?
+ (st/emit! (dw/delete-page id)))
+ (st/emit! (dw/rename-page id new-name))))
+ (st/emit! (dw/stop-rename-page-item))))
on-key-down
(mf/use-fn
@@ -172,40 +180,49 @@
(dom/select-text! edit-input))
nil)))
- [:li {:class (stl/css-case
- :page-element true
- :selected selected?
- :dnd-over-top (= (:over dprops) :top)
- :dnd-over-bot (= (:over dprops) :bot))
- :ref dref}
- [:div {:class (stl/css-case
- :element-list-body true
- :hover hovering?
- :selected selected?)
- :data-testid (dm/str "page-" id)
- :tab-index "0"
- :on-click on-click
- :on-double-click on-double-click
- :on-context-menu on-context-menu}
- [:div {:class (stl/css :page-icon)}
- deprecated-icon/document]
-
- (if editing?
- [:*
- [:input {:class (stl/css :element-name)
- :type "text"
- :ref input-ref
- :on-blur on-blur
- :on-key-down on-key-down
- :auto-focus true
- :default-value (:name page "")}]]
- [:*
- [:span {:class (stl/css :page-name) :title (:name page) :data-testid "page-name"}
- (:name page)]
- [:div {:class (stl/css :page-actions)}
- (when (and deletable? (not read-only?))
- [:button {:on-click on-delete}
- deprecated-icon/delete])]])]]))
+ (let [selected? (and selected? (not is-separator?))]
+ [:li {:class (stl/css-case
+ :page-element true
+ :separator is-separator?
+ :selected selected?
+ :dnd-over-top (= (:over dprops) :top)
+ :dnd-over-bot (= (:over dprops) :bot))
+ :ref dref}
+ [:div {:class (stl/css-case
+ :element-list-body true
+ :separator-body is-separator?
+ :hover (and hovering? (not is-separator?))
+ :selected selected?)
+ :data-testid (dm/str "page-" id)
+ :tab-index "0"
+ :on-click on-click
+ :on-double-click on-double-click
+ :on-context-menu on-context-menu}
+ (if (and is-separator? (not editing?))
+ [:div {:class (stl/css :page-separator)
+ :data-testid "page-separator"}]
+ [:*
+ (when-not is-separator?
+ [:div {:class (stl/css :page-icon)}
+ [:> icon* {:icon-id i/document :size "s"}]])
+ (if editing?
+ [:input {:class (stl/css :element-name)
+ :type "text"
+ :ref input-ref
+ :on-blur on-blur
+ :on-key-down on-key-down
+ :auto-focus true
+ :default-value name}]
+ [:*
+ [:span {:class (stl/css :page-name) :title name :data-testid "page-name"}
+ name]
+ [:div {:class (stl/css :page-actions)}
+ (when (and deletable? (not read-only?))
+ [:> icon-button* {:variant "ghost"
+ :aria-label (tr "modals.delete-page.title")
+ :on-click on-delete
+ :icon-size "s"
+ :icon i/delete}])]])])]])))
;; --- Page Item Wrapper
@@ -266,7 +283,6 @@
[:> title-bar* {:collapsable true
:collapsed collapsed
:on-collapsed on-toggle-collapsed
- :all-clickable true
:title (tr "workspace.sidebar.sitemap")
:class (stl/css :title-spacing-sitemap)}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss
index 086af118e8..dd0370c5ed 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss
@@ -5,6 +5,7 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
+@use "ds/_borders.scss" as *;
.sitemap {
position: relative;
@@ -29,6 +30,7 @@
border-top: deprecated.$s-2 solid var(--resize-area-border-color);
background-color: var(--resize-area-background-color);
cursor: ns-resize;
+
&:hover {
border-color: var(--resize-area-border-color);
}
@@ -39,8 +41,7 @@
flex-direction: column;
height: calc(-38px + var(--height, deprecated.$s-200));
width: var(--left-sidebar-width);
- overflow-x: hidden;
- overflow-y: overlay;
+ overflow: hidden auto;
scrollbar-gutter: stable;
.element-list {
@@ -55,19 +56,24 @@
}
.page-element {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
min-height: deprecated.$s-32;
width: 100%;
cursor: pointer;
+
&.dnd-over-top {
border-top: deprecated.$s-1 solid var(--layer-row-foreground-color-drag);
}
+
&.dnd-over-bot {
border-bottom: deprecated.$s-1 solid var(--layer-row-foreground-color-drag);
}
+
.dnd-over > .element-list-body {
border: deprecated.$s-1 solid var(--layer-row-foreground-color-drag);
}
+
.element-list-body {
display: flex;
align-items: center;
@@ -76,51 +82,64 @@
padding: 0 deprecated.$s-12 0 0;
transition: none;
color: var(--layer-row-foreground-color);
+
.page-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
flex-grow: 1;
padding-left: deprecated.$s-2;
}
+
.page-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
height: deprecated.$s-32;
width: deprecated.$s-24;
padding: 0 deprecated.$s-4 0 deprecated.$s-8;
+
svg {
- @extend .button-icon-small;
- height: deprecated.$s-12;
- width: deprecated.$s-12;
+ @extend %button-icon-small;
+
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
}
+
.page-actions {
height: deprecated.$s-32;
+ display: flex;
+ align-items: center;
+
button {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
width: deprecated.$s-24;
height: 100%;
opacity: deprecated.$op-0;
+
svg {
- @extend .button-icon-small;
- height: deprecated.$s-12;
- width: deprecated.$s-12;
+ @extend %button-icon-small;
+
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
}
}
+
.element-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
color: var(--layer-row-foreground-color-focus);
}
+
input.element-name {
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
- @include deprecated.removeInputStyle;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.remove-input-style;
+
flex-grow: 1;
height: deprecated.$s-28;
max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size)));
@@ -131,21 +150,25 @@
color: var(--layer-row-foreground-color);
}
}
+
&:active,
&.on-drag {
.element-list-body {
color: var(--layer-row-foreground-color-drag);
background-color: var(--layer-row-background-color-drag);
+
.page-actions button {
svg {
stroke: var(--layer-row-foreground-color-drag);
}
}
+
.page-icon svg {
stroke: var(--layer-row-foreground-color-drag);
}
}
}
+
&.selected,
&.selected:hover {
.element-list-body {
@@ -153,16 +176,19 @@
background-color: var(--layer-row-background-color-selected);
box-shadow: deprecated.$s-16 deprecated.$s-0 deprecated.$s-0 deprecated.$s-0
var(--layer-row-background-color-selected);
+
.page-actions button {
svg {
stroke: var(--layer-row-foreground-color-selected);
}
}
+
.page-icon svg {
stroke: var(--layer-row-foreground-color-selected);
}
}
}
+
&:hover,
&.hover {
.element-list-body {
@@ -170,30 +196,37 @@
background-color: var(--layer-row-background-color-hover);
box-shadow: deprecated.$s-16 deprecated.$s-0 deprecated.$s-0 deprecated.$s-0
var(--layer-row-background-color-hover);
+
.page-actions button {
opacity: deprecated.$op-10;
+
svg {
stroke: var(--layer-row-foreground-color-hover);
}
}
+
.page-icon svg {
stroke: var(--layer-row-foreground-color-hover);
}
}
}
+
&:focus {
.element-list-body {
color: var(--layer-row-foreground-color-focus);
border: deprecated.$s-1 solid var(--layer-row-border-color-focus);
outline: none;
+
.page-actions button {
opacity: deprecated.$op-10;
}
}
}
+
&:focus-within {
.element-list-body {
outline: none;
+
.page-actions button {
opacity: deprecated.$op-10;
}
@@ -205,11 +238,13 @@
color: var(--layer-row-foreground-color-hidden);
background-color: var(--layer-row-background-color-hidden);
opacity: deprecated.$op-7;
+
.page-actions button {
svg {
stroke: var(--layer-row-foreground-color-hidden);
}
}
+
.page-icon svg {
stroke: var(--layer-row-foreground-color-hidden);
}
@@ -217,8 +252,27 @@
}
}
+.element-list-body.separator-body {
+ height: auto;
+ min-height: var(--sp-xxxl);
+ padding: 0;
+}
+
+.page-separator {
+ width: 100%;
+ height: $b-1;
+ margin: var(--sp-s);
+ background-color: var(--color-background-quaternary);
+}
+
+.page-element.separator:hover .element-list-body,
+.page-element.separator.hover .element-list-body {
+ color: var(--layer-row-foreground-color);
+ background-color: transparent;
+ box-shadow: none;
+}
+
.title-spacing-sitemap {
padding-inline-start: deprecated.$s-8;
- margin-block-start: deprecated.$s-8;
- margin-block-end: deprecated.$s-4;
+ margin-block: deprecated.$s-8 deprecated.$s-4;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
index 37edf428cd..0289f7c2f6 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
@@ -11,7 +11,7 @@
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cfg]
- [app.main.data.notifications :as ntf]
+ [app.main.data.event :as ev]
[app.main.data.workspace.versions :as dwv]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -77,20 +77,49 @@
(assoc item :index index)))
(reverse)))
-(defn- open-restore-version-dialog
- [origin id]
- (st/emit! (ntf/dialog
- :content (tr "workspace.versions.restore-warning")
- :controls :inline-actions
- :cancel {:label (tr "workspace.updates.dismiss")
- :callback #(st/emit! (ntf/hide))}
- :accept {:label (tr "labels.restore")
- :callback #(st/emit! (dwv/restore-version id origin))}
- :tag :restore-dialog)))
+(defn- on-name-input-focus
+ [event]
+ (dom/select-text! (dom/get-target event)))
+
+(defn- extract-id-from-event
+ [event]
+ (-> event dom/get-current-target (dom/get-data "id") uuid/parse))
+
+(defn- on-create-version
+ []
+ (st/emit! (dwv/create-version)))
+
+(defn- on-edit-version
+ [id _event]
+ (st/emit! (dwv/update-versions-state {:editing id})))
+
+(defn- on-cancel-version-edition
+ [_id _event]
+ (st/emit! (dwv/update-versions-state {:editing nil})))
+
+(defn- on-rename-version
+ [id label]
+ (st/emit! (dwv/rename-version id label)))
+
+(defn- on-delete-version
+ [id]
+ (st/emit! (dwv/delete-version id)))
+
+(defn- on-pin-version
+ [id]
+ (st/emit! (dwv/pin-version id)))
+
+(defn- on-lock-version
+ [id]
+ (st/emit! (dwv/lock-version id)))
+
+(defn- on-unlock-version
+ [id]
+ (st/emit! (dwv/unlock-version id)))
(mf/defc version-entry*
{::mf/private true}
- [{:keys [entry current-profile on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}]
+ [{:keys [entry current-profile on-preview on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}]
(let [show-menu? (mf/use-state false)
profiles (mf/deref refs/profiles)
@@ -108,6 +137,13 @@
(fn [event]
(on-edit (:id entry) event)))
+ on-preview
+ (mf/use-fn
+ (mf/deps entry on-preview)
+ (fn []
+ (when (fn? on-preview)
+ (on-preview (:id entry)))))
+
on-restore
(mf/use-fn
(mf/deps entry on-restore)
@@ -136,11 +172,6 @@
(when on-unlock
(on-unlock (:id entry)))))
- on-name-input-focus
- (mf/use-fn
- (fn [event]
- (dom/select-text! (dom/get-target event))))
-
on-name-input-blur
(mf/use-fn
(mf/deps entry on-rename on-cancel-edit)
@@ -191,6 +222,11 @@
:on-click on-edit}
(tr "labels.rename")])
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :on-click on-preview}
+ (tr "workspace.versions.button.preview")]
+
[:li {:class (stl/css :menu-option)
:role "button"
:on-click on-restore}
@@ -216,7 +252,7 @@
(tr "labels.delete")])])]]))
(mf/defc snapshot-entry*
- [{:keys [entry on-pin-snapshot on-restore-snapshot]}]
+ [{:keys [entry on-pin-snapshot on-restore-snapshot on-preview-snapshot]}]
(let [open-menu* (mf/use-state nil)
entry-ref (mf/use-ref nil)
@@ -225,23 +261,22 @@
(mf/use-fn
(mf/deps on-pin-snapshot)
(fn [event]
- (let [node (dom/get-current-target event)
- id (-> node
- (dom/get-data "id")
- (uuid/parse))]
- (when (fn? on-pin-snapshot)
- (on-pin-snapshot id event)))))
+ (when (fn? on-pin-snapshot)
+ (on-pin-snapshot (extract-id-from-event event) event))))
on-restore-snapshot
(mf/use-fn
(mf/deps on-restore-snapshot)
(fn [event]
- (let [node (dom/get-current-target event)
- id (-> node
- (dom/get-data "id")
- (uuid/parse))]
- (when (fn? on-restore-snapshot)
- (on-restore-snapshot id event)))))
+ (when (fn? on-restore-snapshot)
+ (on-restore-snapshot (extract-id-from-event event) event))))
+
+ on-preview-snapshot
+ (mf/use-fn
+ (mf/deps on-preview-snapshot)
+ (fn [event]
+ (when (fn? on-preview-snapshot)
+ (on-preview-snapshot (extract-id-from-event event) event))))
on-open-snapshot-menu
(mf/use-fn
@@ -266,6 +301,11 @@
:on-close #(reset! open-menu* nil)}
[:ul {:class (stl/css :version-options-dropdown)
:style {"--offset" (dm/str (:offset @open-menu*) "px")}}
+ [:li {:class (stl/css :menu-option)
+ :role "button"
+ :data-id (dm/str (:snapshot @open-menu*))
+ :on-click on-preview-snapshot}
+ (tr "workspace.versions.button.preview")]
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:snapshot @open-menu*))
@@ -302,66 +342,50 @@
(= (:filter state) (:profile-id %)))))
(group-snapshots)))
- on-create-version
+ on-preview-version
(mf/use-fn
- (fn [] (st/emit! (dwv/create-version))))
+ (fn [id]
+ (st/emit! (dwv/enter-preview id)
+ (ev/event {::ev/name "preview-version"
+ ::ev/origin "workspace:sidebar"
+ :type "pinned-version"}))))
- on-edit-version
+ on-preview-snapshot
(mf/use-fn
(fn [id _event]
- (st/emit! (dwv/update-versions-state {:editing id}))))
-
- on-cancel-version-edition
- (mf/use-fn
- (fn [_id _event]
- (st/emit! (dwv/update-versions-state {:editing nil}))))
-
- on-rename-version
- (mf/use-fn
- (fn [id label]
- (st/emit! (dwv/rename-version id label))))
+ (st/emit! (dwv/enter-preview id)
+ (ev/event {::ev/name "preview-version"
+ ::ev/origin "workspace:sidebar"
+ :type "autosaved-version"}))))
on-restore-version
(mf/use-fn
(fn [id _event]
- (open-restore-version-dialog :version id)))
+ (st/emit! (dwv/enter-restore id)
+ (ev/event {::ev/name "restore-version"
+ ::ev/origin "workspace:sidebar"
+ :type "pinned-version"}))))
on-restore-snapshot
(mf/use-fn
(fn [id _event]
- (open-restore-version-dialog :snapshot id)))
-
- on-delete-version
- (mf/use-fn
- (fn [id]
- (st/emit! (dwv/delete-version id))))
-
- on-pin-version
- (mf/use-fn
- (fn [id] (st/emit! (dwv/pin-version id))))
-
- on-lock-version
- (mf/use-fn
- (fn [id]
- (st/emit! (dwv/lock-version id))))
-
- on-unlock-version
- (mf/use-fn
- (fn [id]
- (st/emit! (dwv/unlock-version id))))
+ (st/emit! (dwv/enter-restore id)
+ (ev/event {::ev/name "restore-version"
+ ::ev/origin "workspace:sidebar"
+ :type "autosaved-version"}))))
on-change-filter
(mf/use-fn
- (fn [filter]
+ (fn [filter-value]
(cond
- (= :all filter)
+ (= :all filter-value)
(st/emit! (dwv/update-versions-state {:filter nil}))
- (= :own filter)
+ (= :own filter-value)
(st/emit! (dwv/update-versions-state {:filter (:id profile)}))
:else
- (st/emit! (dwv/update-versions-state {:filter filter})))))
+ (st/emit! (dwv/update-versions-state {:filter filter-value})))))
options
(mf/with-memo [users profile]
@@ -415,6 +439,7 @@
:on-edit on-edit-version
:on-cancel-edit on-cancel-version-edition
:on-rename on-rename-version
+ :on-preview on-preview-version
:on-restore on-restore-version
:on-delete on-delete-version
:on-lock on-lock-version
@@ -423,6 +448,7 @@
:snapshot
[:> snapshot-entry* {:key (:index entry)
:entry entry
+ :on-preview-snapshot on-preview-snapshot
:on-restore-snapshot on-restore-snapshot
:on-pin-snapshot on-pin-version}]
diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.scss b/frontend/src/app/main/ui/workspace/sidebar/versions.scss
index eb4a736edb..41c04c3521 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/versions.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/versions.scss
@@ -132,15 +132,17 @@
}
.version-options-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
+
position: absolute;
width: fit-content;
max-width: deprecated.$s-200;
right: 0;
left: unset;
top: var(--offset);
+
.menu-option {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
}
}
@@ -164,6 +166,7 @@
&:hover {
color: var(--color-accent-primary);
+
.icon-arrow {
stroke: var(--color-accent-primary);
}
@@ -214,6 +217,7 @@
&:active {
color: var(--color-accent-primary);
+
:global(.icon-pin) {
visibility: initial;
fill: var(--color-accent-primary);
@@ -227,6 +231,7 @@
.cta {
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
a {
diff --git a/frontend/src/app/main/ui/workspace/text_palette.scss b/frontend/src/app/main/ui/workspace/text_palette.scss
index b8b438eb89..8090f88e7c 100644
--- a/frontend/src/app/main/ui/workspace/text_palette.scss
+++ b/frontend/src/app/main/ui/workspace/text_palette.scss
@@ -10,18 +10,22 @@
height: 100%;
display: flex;
}
+
.left-arrow,
.right-arrow {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
position: relative;
height: 100%;
width: deprecated.$s-24;
padding: 0;
z-index: deprecated.$z-index-2;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
+
&::after {
content: "";
position: absolute;
@@ -37,20 +41,24 @@
);
pointer-events: none;
}
+
&:hover {
svg {
stroke: var(--button-foreground-hover);
}
}
+
&:disabled {
svg {
stroke: var(--button-foreground-color-disabled);
}
+
&::after {
background-image: none;
}
}
}
+
.left-arrow {
&::after {
left: deprecated.$s-24;
@@ -60,6 +68,7 @@
var(--palette-button-shadow-final) 100%
);
}
+
&.disabled ::after {
background-image: none;
}
@@ -80,7 +89,8 @@
}
.typography-item {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
flex-direction: column;
justify-content: center;
@@ -90,26 +100,30 @@
padding: deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--palette-text-background-color);
+
&:first-child {
margin-left: deprecated.$s-8;
}
.typography-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
height: deprecated.$s-16;
width: deprecated.$s-120;
color: var(--palette-text-color-selected);
}
.typography-font {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
height: deprecated.$s-16;
width: deprecated.$s-120;
color: var(--palette-text-color);
}
.typography-data {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
height: deprecated.$s-16;
width: deprecated.$s-120;
color: var(--palette-text-color);
@@ -119,22 +133,26 @@
.typography-name {
height: deprecated.$s-16;
}
+
.typography-data {
display: none;
}
}
+
&.small-item {
.typography-data,
.typography-font {
display: none;
}
}
+
&:hover {
background-color: var(--palette-text-background-color-hover);
}
}
.text-palette-empty {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--palette-text-color);
}
diff --git a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss
index fe450d0b1a..0a8a3985d7 100644
--- a/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss
+++ b/frontend/src/app/main/ui/workspace/text_palette_ctx_menu.scss
@@ -31,39 +31,52 @@
&:last-child {
margin-bottom: 0;
}
+
.library-name {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
color: var(--context-menu-foreground-color);
display: grid;
grid-template-columns: 1fr deprecated.$s-24;
max-width: deprecated.$s-400;
+
.lib-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
max-width: deprecated.$s-380;
}
+
.lib-num {
margin-left: deprecated.$s-4;
}
}
+
.icon-wrapper {
margin-left: deprecated.$s-4;
- @include deprecated.flexCenter;
+
+ @include deprecated.flex-center;
+
svg {
- @include deprecated.flexCenter;
- @extend .button-icon-small;
+ @include deprecated.flex-center;
+ @extend %button-icon-small;
+
stroke: var(--icon-foreground);
}
}
+
&.selected,
&:hover {
.icon-wrapper {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
svg {
- @include deprecated.flexCenter;
- @extend .button-icon-small;
+ @include deprecated.flex-center;
+ @extend %button-icon-small;
+
stroke: var(--context-menu-foreground-color-selected);
}
}
+
.library-name {
color: var(--context-menu-foreground-color-selected);
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/export.scss b/frontend/src/app/main/ui/workspace/tokens/export.scss
index 53f9be2209..d7ecd3cb12 100644
--- a/frontend/src/app/main/ui/workspace/tokens/export.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/export.scss
@@ -10,14 +10,16 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-dialog {
--modal-width: 32rem;
--modal-padding: var(--sp-xxxl);
--container-max-height: 16rem;
- @extend .modal-container-base;
+
+ @extend %modal-container-base;
+
user-select: none;
width: var(--modal-width);
max-width: 100%;
diff --git a/frontend/src/app/main/ui/workspace/tokens/export/modal.scss b/frontend/src/app/main/ui/workspace/tokens/export/modal.scss
index 81d4af36f8..a8d711103f 100644
--- a/frontend/src/app/main/ui/workspace/tokens/export/modal.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/export/modal.scss
@@ -62,13 +62,10 @@
.file-name {
display: block;
max-width: 99%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+
@include t.use-typography("body-medium");
+
flex-grow: 1;
- overflow: hidden;
- text-overflow: ellipsis;
padding: var(--sp-xs);
overflow: hidden;
text-overflow: ellipsis;
@@ -90,14 +87,14 @@
border-radius: $br-8;
margin: 0;
max-height: var(--container-max-height);
- overflow-y: auto;
- overflow-x: auto;
- word-wrap: normal;
+ overflow: auto;
+ overflow-wrap: normal;
white-space: pre;
}
.disabled-message {
@include t.use-typography("body-small");
+
color: var(--color-foreground-secondary);
display: flex;
align-items: center;
diff --git a/frontend/src/app/main/ui/workspace/tokens/import.scss b/frontend/src/app/main/ui/workspace/tokens/import.scss
index 314edf94c8..26b77c7dc5 100644
--- a/frontend/src/app/main/ui/workspace/tokens/import.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/import.scss
@@ -7,11 +7,12 @@
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
user-select: none;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/import/modal.scss b/frontend/src/app/main/ui/workspace/tokens/import/modal.scss
index 9d8671d48d..971c0abc88 100644
--- a/frontend/src/app/main/ui/workspace/tokens/import/modal.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/import/modal.scss
@@ -30,6 +30,7 @@
.import-actions {
@include t.use-typography("body-small");
+
display: flex;
justify-content: flex-end;
gap: var(--sp-s);
@@ -40,8 +41,7 @@
border-end-start-radius: 0;
border-inline-start: $b-1 solid var(--color-accent-tertiary);
width: var(--sp-xxxl);
- padding-inline-start: 0;
- padding-inline-end: 0;
+ padding-inline: 0;
justify-content: center;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss b/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss
index d1394861db..b63eaf6648 100644
--- a/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/import_from_library.scss
@@ -5,7 +5,6 @@
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
-
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@@ -20,7 +19,8 @@
--modal-title-foreground-color: var(--color-foreground-primary);
--modal-text-foreground-color: var(--color-foreground-secondary);
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
display: flex;
justify-content: center;
align-items: center;
@@ -33,7 +33,8 @@
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
inline-size: 100%;
max-inline-size: 32rem;
max-block-size: unset;
@@ -50,12 +51,14 @@
.modal-title {
@include t.use-typography("headline-medium");
+
color: var(--modal-title-foreground-color);
- word-break: break-word;
+ overflow-wrap: break-word;
}
.modal-content {
@include t.use-typography("body-large");
+
color: var(--modal-text-foreground-color);
}
@@ -65,6 +68,7 @@
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
+
gap: var(--sp-s);
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs
index 0ff4deafa4..e3a0dafed1 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs
@@ -2,12 +2,17 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
+ [app.common.path-names :as cpn]
[app.common.types.shape.layout :as ctsl]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
+ [app.main.data.helpers :as dh]
+ [app.main.data.modal :as modal]
[app.main.data.style-dictionary :as sd]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
+ [app.main.data.workspace.tokens.propagation :as dwtp]
+ [app.main.data.workspace.tokens.remapping :as remap]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
@@ -124,6 +129,16 @@
(mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))
+ ;; Filter tokens by their path and return the tokens
+ filter-tokens-by-path
+ (mf/use-fn
+ (fn [tokens-filtered-by-type node]
+ (->> tokens-filtered-by-type
+ (filter (fn [token]
+ (let [token-path (cpn/split-path (:name token) :separator ".")]
+ (and (> (count token-path) 0)
+ (str/starts-with? (:name token) (str (:path node) ".")))))))))
+
;; Filter tokens by their path and return their ids
filter-tokens-by-path-ids
(mf/use-fn
@@ -132,7 +147,7 @@
(->> selected-token-set-tokens
(filter (fn [token]
(let [[_ token-value] token]
- (and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
+ (and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path "."))))))
(mapv (fn [token]
(let [[_ token-value] token]
(:id token-value)))))))
@@ -154,14 +169,11 @@
path (:name token)
tokens-by-type (ctob/group-by-type selected-token-set-tokens)
tokens-filtered-by-type (get tokens-by-type type)
- tokens-in-path-ids (filter-tokens-by-path-ids type path)
- remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)]
- ;; Delete the token
+ remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type [id])]
(st/emit! (dwtl/delete-token selected-token-set-id id))
- ;; Remove from unfolded tree path
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
- (st/emit! (dwtl/toggle-token-path (name type)))))))
+ (st/emit! (dwtl/close-token-type type))))))
delete-node
(mf/with-memo [selected-token-set-tokens selected-token-set-id]
@@ -176,7 +188,107 @@
;; Remove from unfolded tree path
(if remaining-tokens?
(st/emit! (dwtl/toggle-token-path (str (name type) "." path)))
- (st/emit! (dwtl/toggle-token-path (name type)))))))]
+ (st/emit! (dwtl/close-token-type type))))))
+
+
+
+ bulk-rename-tokens-in-path
+ ;; Rename tokens in bulk affected by a node rename.
+ (mf/use-fn
+ (mf/deps filter-tokens-by-path-ids selected-token-set-id)
+ (fn [node type new-node-name]
+ (let [old-path (:path node)
+ new-path (ctob/rename-path node new-node-name)
+ tokens-in-path-ids (filter-tokens-by-path-ids type old-path)]
+ (st/emit!
+ (modal/hide)
+ (dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-path)))))
+
+ bulk-remap-tokens-in-path
+ ;; Remap tokens in bulk affected by a node rename.
+ ;; It will update the token names and propagate the changes to the workspace.
+ (mf/use-fn
+ (mf/deps filter-tokens-by-path filter-tokens-by-path-ids selected-token-set-tokens selected-token-set-id)
+ (fn [node type new-node-name]
+ (let [old-path (:path node)
+ ;; Get tokens in path to remap their names after remapping the node
+ tokens-by-type (ctob/group-by-type selected-token-set-tokens)
+ tokens-filtered-by-type (get tokens-by-type type)
+ tokens-in-path (filter-tokens-by-path tokens-filtered-by-type node)
+ tokens-in-path-ids (filter-tokens-by-path-ids type old-path)
+ new-node-path (ctob/rename-path node new-node-name)
+ new-tokens (map (fn [token]
+ (let [new-token-path (ctob/rename-path node token new-node-name)]
+ (assoc token :name new-token-path)))
+ tokens-in-path)]
+ (st/emit!
+ (dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path)
+ (remap/bulk-remap-tokens tokens-in-path new-tokens)
+ (dwtp/propagate-workspace-tokens)
+ (modal/hide)))))
+
+ on-remap-node-warning
+ ;; If there are tokens that will be affected by the node rename, we show the remap modal
+ (mf/use-fn
+ (mf/deps bulk-remap-tokens-in-path bulk-rename-tokens-in-path)
+ (fn [node type new-node-name]
+ (let [remap-data {:new-name new-node-name
+ :old-name (:name node)
+ :type "node"}
+ remap-handler #(bulk-remap-tokens-in-path node type new-node-name)
+ rename-handler #(bulk-rename-tokens-in-path node type new-node-name)]
+ (st/emit!
+ (modal/hide)
+ (modal/show :tokens/remapping-confirmation {:remap-data remap-data
+ :on-remap remap-handler
+ :on-rename rename-handler})))))
+
+ on-rename-node
+ ;; When user renames a node, we need to check if there are tokens that will be affected by this change.
+ ;; If there are, we display the remap modal, otherwise, we rename the tokens directly.
+ (mf/use-fn
+ (mf/deps selected-token-set-tokens filter-tokens-by-path on-remap-node-warning bulk-rename-tokens-in-path)
+ (fn [node type new-node-name]
+ (let [state @st/state
+ file-data (dh/lookup-file-data state)
+ tokens-by-type (ctob/group-by-type selected-token-set-tokens)
+ tokens-filtered-by-type (get tokens-by-type type)
+ tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node)
+ token-references-count (reduce (fn [count token]
+ (+ count (remap/count-token-references file-data (:name token))))
+ 0
+ tokens-in-current-path)]
+ (if (> token-references-count 0)
+ (on-remap-node-warning node type new-node-name)
+ (bulk-rename-tokens-in-path node type new-node-name)))))
+
+ on-duplicate-node
+ (fn [node type new-node-name]
+ (let [tokens-in-path-ids (filter-tokens-by-path-ids type (:path node))]
+ (st/emit!
+ (modal/hide)
+ (dwtl/bulk-create-tokens selected-token-set-id tokens-in-path-ids type node new-node-name))))
+
+ open-rename-node-modal
+ ;; When user renames a node, we display a form modal
+ (mf/use-fn
+ (mf/deps selected-token-set-tokens on-rename-node)
+ (fn [node type]
+ (let [on-rename-node-handler #(on-rename-node node type %)]
+ (st/emit! (modal/show :tokens/rename-node {:node node
+ :tokens-in-active-set selected-token-set-tokens
+ :on-rename on-rename-node-handler})))))
+
+ open-duplicate-node-modal
+ (mf/use-fn
+ (mf/deps selected-token-set-tokens on-duplicate-node)
+ (fn [node type]
+ (let [on-duplicate-node-handler #(on-duplicate-node node type %)]
+ (st/emit! (modal/show :tokens/rename-node {:new-node-name (str (:name node) "-copy")
+ :node node
+ :variant "duplicate"
+ :tokens-in-active-set selected-token-set-tokens
+ :on-rename on-duplicate-node-handler})))))]
(mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib
@@ -190,7 +302,9 @@
[:*
[:& token-context-menu {:on-delete-token delete-token}]
- [:> token-node-context-menu* {:on-delete-node delete-node}]
+ [:> token-node-context-menu* {:on-rename-node open-rename-node-modal
+ :on-duplicate-node open-duplicate-node-modal
+ :on-delete-node delete-node}]
[:> selected-set-info* {:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]
diff --git a/frontend/src/app/main/ui/workspace/tokens/management.scss b/frontend/src/app/main/ui/workspace/tokens/management.scss
index 135d3bcc76..39ba2ef087 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management.scss
@@ -8,9 +8,10 @@
.sets-header-container {
@include use-typography("headline-small");
+
padding: var(--sp-s);
color: var(--title-foreground-color);
- word-break: break-word;
+ overflow-wrap: break-word;
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -24,6 +25,7 @@
.sets-header-status {
@include use-typography("body-small");
+
text-transform: none;
color: var(--color-foreground-secondary);
display: flex;
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
index 8efa0ba66c..5ce0b1c16c 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs
@@ -21,6 +21,7 @@
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
+ [app.util.clipboard :as clipboard]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.timers :as timers]
@@ -334,6 +335,7 @@
(defn default-actions [{:keys [token selected-token-set-id on-delete-token]}]
(let [{:keys [modal]} (dwta/get-token-properties token)
+ on-copy-name #(clipboard/to-clipboard (:name token))
on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))]
[{:title (tr "workspace.tokens.edit")
:no-selectable true
@@ -351,6 +353,9 @@
{:title (tr "workspace.tokens.duplicate")
:no-selectable true
:action on-duplicate-token}
+ {:title (tr "workspace.tokens.copy-name")
+ :no-selectable true
+ :action on-copy-name}
{:title (tr "workspace.tokens.delete")
:no-selectable true
:action #(on-delete-token token)}]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss
index 207166d747..284040b470 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.scss
@@ -22,7 +22,8 @@
.context-list,
.token-context-submenu {
- @include deprecated.menuShadow;
+ @include deprecated.menu-shadow;
+
display: grid;
width: deprecated.$s-240;
padding: deprecated.$s-4;
@@ -56,7 +57,9 @@
--context-menu-item-bg-color: none;
--context-menu-item-fg-color: var(--color-foreground-primary);
--context-menu-item-border-color: none;
+
@include use-typography("body-small");
+
display: flex;
align-items: center;
height: deprecated.$s-32;
@@ -67,6 +70,7 @@
background-color: var(--context-menu-item-bg-color);
border: deprecated.$s-1 solid var(--context-menu-item-border-color);
cursor: pointer;
+
&:hover {
--context-menu-item-bg-color: var(--color-background-quaternary);
}
@@ -124,6 +128,7 @@
.item-with-icon-space {
padding-left: deprecated.$s-20;
}
+
.icon-wrapper {
margin-right: deprecated.$s-4;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs
index 3e6dec43b1..137551c260 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.cljs
@@ -85,11 +85,15 @@
filter-term* (mf/use-state "")
filter-term (deref filter-term*)
+ selected-id* (mf/use-state nil)
+ selected-id (deref selected-id*)
+
options-ref (mf/use-ref nil)
dropdown-ref (mf/use-ref nil)
internal-ref (mf/use-ref nil)
nodes-ref (mf/use-ref nil)
wrapper-ref (mf/use-ref nil)
+ input-wrapper-ref (mf/use-ref nil)
icon-button-ref (mf/use-ref nil)
ref (or ref internal-ref)
@@ -120,12 +124,28 @@
state (obj/set! state id node)]
(mf/set-ref-val! nodes-ref state))))
+ get-selected-id
+ (mf/use-fn
+ (mf/deps dropdown-options)
+ (fn []
+ (let [input-node (mf/ref-val ref)
+ value (dom/get-input-value input-node)
+ cursor (dom/selection-start input-node)
+ token-name (tp/token-at-cursor value cursor)
+ options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
+ (when token-name
+ (->> options
+ (filter #(= (:name %) token-name))
+ first
+ :id)))))
+
toggle-dropdown
(mf/use-fn
(mf/deps is-open)
(fn [event]
(dom/prevent-default event)
(swap! is-open* not)
+ (reset! selected-id* (get-selected-id))
(let [input-node (mf/ref-val ref)]
(dom/focus! input-node))))
@@ -160,7 +180,8 @@
:options dropdown-options
:toggle-dropdown toggle-dropdown
:is-open* is-open*
- :on-enter on-option-enter})
+ :on-enter on-option-enter
+ :get-selected-id get-selected-id})
on-change
(mf/use-fn
@@ -219,11 +240,13 @@
:hint-message (:message hint)
:on-key-down on-key-down
:hint-type (:type hint)
+ :input-wrapper-ref input-wrapper-ref
:ref ref
:role "combobox"
:aria-activedescendant focused-id
:aria-controls listbox-id
:aria-expanded is-open
+ :data-option-focused (boolean focused-id)
:slot-end
(when (some? @filtered-tokens-by-type)
(mf/html
@@ -244,7 +267,7 @@
props)
- {:keys [style ready?]} (use-floating-dropdown is-open wrapper-ref dropdown-ref)]
+ {:keys [style ready?]} (use-floating-dropdown is-open input-wrapper-ref wrapper-ref dropdown-ref)]
(mf/with-effect [resolve-stream tokens token name token-name]
(let [subs (->> resolve-stream
@@ -303,7 +326,7 @@
:id listbox-id
:options options
:focused focused-id
- :selected nil
+ :selected selected-id
:align :right
:empty-to-end empty-to-end
:wrapper-ref dropdown-ref
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss
index b484cacfce..41cc87fd25 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox.scss
@@ -12,5 +12,6 @@
position: fixed;
max-block-size: $sz-400;
overflow-y: auto;
- @include custom-scrollbar();
+
+ @include custom-scrollbar;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs
index b8be6dad81..3508833797 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/combobox_navigation.cljs
@@ -26,14 +26,13 @@
[focusables focused-id direction]
(let [ids (vec (map :id focusables))
idx (.indexOf (clj->js ids) focused-id)
- idx (if (= idx -1) -1 idx)
- next-idx (case direction
- :down (min (dec (count ids)) (inc idx))
- :up (max 0 (dec (if (= idx -1) 0 idx))))]
- (nth ids next-idx nil)))
+ count (count ids)]
+ (case direction
+ :down (nth ids (mod (inc idx) count) nil)
+ :up (nth ids (mod (if (= idx -1) 0 (dec idx)) count) nil))))
(defn use-navigation
- [{:keys [is-open options nodes-ref is-open* toggle-dropdown on-enter]}]
+ [{:keys [is-open options nodes-ref is-open* toggle-dropdown on-enter get-selected-id]}]
(let [focused-id* (mf/use-state nil)
focused-id (deref focused-id*)
@@ -46,6 +45,7 @@
down? (kbd/down-arrow? event)
enter? (kbd/enter? event)
esc? (kbd/esc? event)
+ tab? (kbd/tab? event)
open-dropdown (kbd/is-key? event "{")
close-dropdown (kbd/is-key? event "}")
options (if (delay? options) @options options)]
@@ -56,18 +56,21 @@
(dom/prevent-default event)
(let [focusables (focusable-options options)]
(cond
+ ;; Dropdown open: move focus to next option
is-open
(when (seq focusables)
(let [next-id (next-focus-id focusables focused-id :down)]
(reset! focused-id* next-id)))
+ ;; Dropdown closed with options: open and focus first
(seq focusables)
(do
(toggle-dropdown event)
+ (when get-selected-id
+ (get-selected-id))
(reset! focused-id* (first-focusable-id focusables)))
- :else
- nil)))
+ :else nil)))
up?
(when is-open
@@ -77,7 +80,9 @@
(reset! focused-id* next-id)))
open-dropdown
- (reset! is-open* true)
+ (do
+ (reset! is-open* true)
+ (reset! focused-id* nil))
close-dropdown
(reset! is-open* false)
@@ -89,21 +94,23 @@
(dom/prevent-default event)
(when (some #(= (:id %) focused-id) focusables)
(on-enter focused-id)))))
+
esc?
- (do
+ (when is-open
(dom/prevent-default event)
+ (dom/stop-propagation event)
(reset! is-open* false))
+
+ tab?
+ (when is-open
+ (reset! is-open* false)
+ (reset! focused-id* nil))
+
:else nil))))]
- ;; Initial focus on first option
- (mf/with-effect [is-open options]
- (when is-open
- (let [opts (if (delay? options) @options options)
- focusables (focusable-options opts)
- ids (set (map :id focusables))]
- (when (and (seq focusables)
- (not (contains? ids focused-id)))
- (reset! focused-id* (:id (first focusables)))))))
+ (mf/with-effect [is-open]
+ (when (not is-open)
+ (reset! focused-id* nil)))
;; auto scroll when key down
(mf/with-effect [focused-id nodes-ref]
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs
index 739ca0628c..d7cf90a3f3 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs
@@ -9,7 +9,7 @@
[app.util.dom :as dom]
[rumext.v2 :as mf]))
-(defn use-floating-dropdown [is-open wrapper-ref dropdown-ref]
+(defn use-floating-dropdown [is-open input-wrapper-ref outer-wrapper-ref dropdown-ref]
(let [position* (mf/use-state nil)
position (deref position*)
ready* (mf/use-state false)
@@ -32,7 +32,7 @@
(> dropdown-height space-below))
position (if open-up?
- {:bottom (str (- windows-height (:top combobox-rect) 12) "px")
+ {:bottom (str (- windows-height (:top combobox-rect) -8) "px")
:left (str (:left combobox-rect) "px")
:width (str (:width combobox-rect) "px")
:placement :top}
@@ -44,27 +44,41 @@
(reset! ready* true)
(reset! position* position)))]
- (mf/with-effect [is-open dropdown-ref wrapper-ref]
+ (mf/with-effect [is-open dropdown-ref input-wrapper-ref outer-wrapper-ref]
(when is-open
- (let [handler (fn [event]
- (let [dropdown-node (mf/ref-val dropdown-ref)
- target (dom/get-target event)]
- (when (or (nil? dropdown-node)
- (not (instance? js/Node target))
- (not (.contains dropdown-node target)))
- (js/requestAnimationFrame
- (fn []
- (let [wrapper-node (mf/ref-val wrapper-ref)]
- (reset! ready* true)
- (calculate-position wrapper-node)))))))]
+ (let [recalculate
+ (fn []
+ (js/requestAnimationFrame
+ (fn []
+ (let [input-node (mf/ref-val input-wrapper-ref)]
+ (calculate-position input-node)))))
+
+ handler
+ (fn [event]
+ (let [dropdown-node (mf/ref-val dropdown-ref)
+ target (dom/get-target event)]
+ (when (or (nil? dropdown-node)
+ (not (instance? js/Node target))
+ (not (.contains dropdown-node target)))
+ (recalculate))))
+
+ resize-observer (js/ResizeObserver. (fn [_] (recalculate)))
+ outer-node (mf/ref-val outer-wrapper-ref)
+ dropdown-node (mf/ref-val dropdown-ref)]
+
(handler nil)
(.addEventListener js/window "resize" handler)
(.addEventListener js/window "scroll" handler true)
+ (when outer-node
+ (.observe resize-observer outer-node))
+ (when dropdown-node
+ (.observe resize-observer dropdown-node))
(fn []
(.removeEventListener js/window "resize" handler)
- (.removeEventListener js/window "scroll" handler true)))))
+ (.removeEventListener js/window "scroll" handler true)
+ (.disconnect resize-observer)))))
{:style position
:ready? ready
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs
index 2308bef9c7..1c317ff3e3 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/token_parsing.cljs
@@ -22,6 +22,18 @@
:end (or (str/index-of value "}" last-open) cursor)
:partial (subs text-before (inc last-open))})))
+(defn token-at-cursor
+ "Returns the full token name at the cursor position if cursor is
+ inside a complete {token-name} reference, nil otherwise."
+ [value cursor]
+ (let [last-open (str/last-index-of (subs value 0 cursor) "{")
+ last-close (str/index-of value "}" (or last-open 0))]
+ (when (and last-open last-close (> last-close last-open))
+ (let [token-name (subs value (inc last-open) last-close)]
+ (when (and (seq token-name)
+ (not (str/includes? token-name " ")))
+ token-name)))))
+
(defn active-token [value input-node]
(let [cursor (dom/selection-start input-node)]
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs
index f29c348e9d..39a4675dc0 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/utils.cljs
@@ -9,7 +9,8 @@
[token]
{:id (str (get token :id))
:type :token
- :resolved-value (get token :value)
+ :value (get token :value)
+ :resolved-value (get token :resolved-value)
:name (get token :name)})
(defn- generate-dropdown-options
@@ -53,7 +54,7 @@
tokens)))
(defn- sort-groups-and-tokens
- "Sorts both the groups and the tokens inside them alphabetically.
+ "Sorts the tokens inside the groups alphabetically.
Input:
A map where:
@@ -65,18 +66,18 @@
:colors [{:name \"azul\"} {:name \"rojo\"}]}
Output:
- A sorted map where:
- - groups are ordered alphabetically by key
+ A map which:
- tokens inside each group are sorted alphabetically by :name
Example output:
- {:colors [{:name \"azul\"} {:name \"rojo\"}]
- :dimensions [{:name \"quini\"} {:name \"tres\"}]}"
+ {:dimensions [{:name \"quini\"} {:name \"tres\"}]
+ :colors [{:name \"azul\"} {:name \"rojo\"}]}"
[groups->tokens]
- (into (sorted-map) ;; ensure groups are ordered alphabetically by their key
- (for [[group tokens] groups->tokens]
- [group (sort-by :name tokens)])))
+ (reduce (fn [acc [group tokens]]
+ (assoc acc group (sort-by :name tokens)))
+ {}
+ groups->tokens))
(defn get-token-dropdown-options
[tokens filter-term]
@@ -94,9 +95,21 @@
(defn filter-tokens-for-input
[raw-tokens input-type]
(delay
- (-> (deref raw-tokens)
- (select-keys (get cto/tokens-by-input input-type))
- (not-empty))))
+ (let [raw-tokens (deref raw-tokens)
+ key-order (case input-type
+ :color-selection
+ (concat
+ (get cto/tokens-by-input :fill)
+ (get cto/tokens-by-input :stroke-color))
+
+ (get cto/tokens-by-input input-type))]
+ (-> (reduce (fn [acc k]
+ (if (contains? raw-tokens k)
+ (assoc acc k (get raw-tokens k))
+ acc))
+ (array-map)
+ key-order)
+ (not-empty)))))
(defn focusable-options [options]
(filter #(= (:type %) :token) options))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs
index 8225a52887..a4cc813bbc 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs
@@ -7,7 +7,6 @@
(ns app.main.ui.workspace.tokens.management.forms.generic-form
(:require-macros [app.main.style :as stl])
(:require
- [app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob]
@@ -160,13 +159,13 @@
on-remap-token
(mf/use-fn
(mf/deps token)
- (fn [valid-token name old-name description]
+ (fn [valid-token new-name old-name description]
(st/emit!
(dwtl/update-token (:id token)
- {:name name
+ {:name new-name
:value (:value valid-token)
:description description})
- (remap/remap-tokens old-name name)
+ (remap/remap-tokens old-name new-name)
(dwtp/propagate-workspace-tokens)
(modal/hide!))))
@@ -186,7 +185,6 @@
(mf/deps validate-token token tokens token-type value-subfield value-type active-tab on-remap-token on-rename-token is-create)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
- path (str (d/name token-type) "." name)
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])
value-for-validation (get-value-for-validator active-tab value value-subfield value-type)]
@@ -203,11 +201,12 @@
is-rename (and (= action "edit") (not= name old-name))
references-count (remap/count-token-references file-data old-name)
on-remap #(on-remap-token valid-token name old-name description)
- on-rename #(on-rename-token valid-token name description)]
+ on-rename #(on-rename-token valid-token name description)
+ remap-data {:new-name name
+ :old-name old-name
+ :type "token"}]
(if (and is-rename (> references-count 0))
- (st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name
- :new-token-name name
- :references-count references-count
+ (st/emit! (modal/show :tokens/remapping-confirmation {:remap-data remap-data
:on-remap on-remap
:on-rename on-rename}))
(st/emit!
@@ -220,7 +219,7 @@
{:name name
:value (:value valid-token)
:description description}))
- (dwtl/toggle-token-path path)
+ (dwtl/open-token-type (:type token))
(dwtp/propagate-workspace-tokens)
(modal/hide!)))))
;; WORKAROUND: display validation errors in the form instead of crashing
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss
index 00eb38a2f2..3ac3d7a753 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss
@@ -33,6 +33,7 @@
.form-modal-title {
@include t.use-typography("headline-medium");
+
color: var(--color-foreground-primary);
display: flex;
align-items: center;
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss
index c9dc66715a..2dd229e37a 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss
@@ -13,18 +13,17 @@
border-radius: $br-4;
background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
- min-width: $sz-364;
min-height: $sz-192;
max-width: $sz-512;
max-height: $sz-512;
- box-shadow: 0px 0px $sz-12 0px var(--color-shadow-dark);
+ box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark);
position: absolute;
width: auto;
min-width: auto;
z-index: var(--z-index-set);
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
padding: var(--sp-xxxl);
+
&.token-modal-large {
max-block-size: 95vh;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
new file mode 100644
index 0000000000..194b731f03
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs
@@ -0,0 +1,123 @@
+(ns app.main.ui.workspace.tokens.management.forms.rename-node-modal
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.data :as d]
+ [app.common.files.tokens :as cfo]
+ [app.common.types.tokens-lib :as ctob]
+ [app.main.data.modal :as modal]
+ [app.main.store :as st]
+ [app.main.ui.ds.buttons.button :refer [button*]]
+ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
+ [app.main.ui.ds.foundations.assets.icon :as i]
+ [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
+ [app.main.ui.forms :as fc]
+ [app.util.forms :as fm]
+ [app.util.i18n :refer [tr]]
+ [app.util.keyboard :as kbd]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(mf/defc rename-node-form*
+ [{:keys [new-node-name node active-tokens tokens-tree variant on-close on-submit]}]
+ (let [make-schema #(cfo/make-node-token-schema active-tokens tokens-tree node)
+
+ schema
+ (mf/with-memo [active-tokens]
+ (make-schema))
+
+ initial (mf/with-memo [node new-node-name]
+ {:name (d/nilv new-node-name (:name node))})
+
+ form (fm/use-form :schema schema
+ :initial initial)
+
+ on-submit (mf/use-fn
+ (mf/deps form on-submit)
+ (fn []
+ (let [name (get-in @form [:clean-data :name])]
+ (when (not= name (:name node))
+ (on-submit name)))))
+
+ is-disabled? (or (not (:valid @form))
+ (= (get-in @form [:clean-data :name]) (:name node)))
+
+ hint-path (mf/with-memo [@form node]
+ (let [new-name (get-in @form [:clean-data :name])
+ path (str (:path node))
+ new-path (str/replace path (:name node) new-name)]
+ (if (get-in @form [:touched :name])
+ new-path
+ path)))]
+
+ [:> fc/form* {:class (stl/css :form-wrapper)
+ :form form
+ :on-submit on-submit}
+ [:> heading* {:level 2
+ :typography "headline-medium"
+ :class (stl/css :form-modal-title)}
+ (if (= variant "rename")
+ (tr "workspace.tokens.rename-group")
+ (tr "workspace.tokens.duplicate-group"))]
+ [:> fc/form-input* {:id "rename-node"
+ :name :name
+ :label (tr "workspace.tokens.token-name")
+ :placeholder (tr "workspace.tokens.token-name")
+ :max-length 255
+ :variant "comfortable"
+ :hint-type "hint"
+ :hint-message (when (= variant "rename") (tr "workspace.tokens.rename-group-name-hint" hint-path))
+ :auto-focus true}]
+ [:div {:class (stl/css :form-actions)}
+ [:> button* {:variant "secondary"
+ :name "cancel"
+ :on-click on-close} (tr "labels.cancel")]
+ [:> fc/form-submit* {:variant "primary"
+ :disabled is-disabled?
+ :name "rename"} (if (= variant "rename") (tr "labels.rename") (tr "labels.duplicate"))]]]))
+
+(mf/defc rename-node-modal
+ {::mf/register modal/components
+ ::mf/register-as :tokens/rename-node}
+ [{:keys [new-node-name node tokens-in-active-set on-rename variant]}]
+
+ (let [variant (d/nilv variant "rename") ;; "rename" or "duplicate"
+
+ tokens-tree-in-selected-set
+ (mf/with-memo [tokens-in-active-set node]
+ (-> (ctob/tokens-tree tokens-in-active-set)
+ (d/dissoc-in (:name node))))
+
+ close-modal
+ (mf/use-fn
+ (fn []
+ (st/emit! (modal/hide))))
+
+ rename
+ (mf/use-fn
+ (mf/deps on-rename)
+ (fn [new-name]
+ (on-rename new-name)))
+
+ on-key-down
+ (mf/use-fn
+ (mf/deps [close-modal])
+ (fn [event]
+ (when (kbd/esc? event)
+ (close-modal))))]
+
+ [:div {:class (stl/css :modal-overlay)
+ :on-key-down on-key-down
+ :data-testid "token-rename-node-modal"}
+ [:div {:class (stl/css :modal-dialog)}
+ [:> icon-button* {:class (stl/css :close-btn)
+ :on-click close-modal
+ :aria-label (tr "labels.close")
+ :variant "ghost"
+ :icon i/close}]
+ [:> rename-node-form* {:new-node-name new-node-name
+ :node node
+ :variant variant
+ :active-tokens tokens-in-active-set
+ :tokens-tree tokens-tree-in-selected-set
+ :on-close close-modal
+ :on-submit rename}]]]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss
new file mode 100644
index 0000000000..16206e3ea2
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.scss
@@ -0,0 +1,60 @@
+// 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/typography.scss" as t;
+@use "refactor/common-refactor.scss" as deprecated;
+
+.modal-overlay {
+ --modal-title-foreground-color: var(--color-foreground-primary);
+ --modal-text-foreground-color: var(--color-foreground-secondary);
+
+ @extend %modal-overlay-base;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: fixed;
+ inset-inline-start: 0;
+ inset-block-start: 0;
+ block-size: 100%;
+ inline-size: 100%;
+ background-color: var(--overlay-color);
+}
+
+.close-btn {
+ position: absolute;
+ inset-block-start: $sz-6;
+ inset-inline-end: $sz-6;
+}
+
+.modal-dialog {
+ @extend %modal-container-base;
+
+ inline-size: 100%;
+ max-inline-size: 32rem;
+ max-block-size: unset;
+ user-select: none;
+ position: relative;
+}
+
+.form-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-l);
+}
+
+.form-modal-title {
+ @include t.use-typography("headline-medium");
+
+ color: var(--color-foreground-primary);
+}
+
+.form-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--sp-m);
+}
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
index 722683d54a..3878305e76 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
@@ -296,7 +296,7 @@
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field [:value :reference]
- :error/fn #(tr "workspace.tokens.self-reference")}
+ :error/fn #(tr "errors.tokens.self-reference")}
(fn [{:keys [name value]}]
(let [reference (get value :reference)]
(if (and reference name)
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss
index f0a973f0b5..fa23fa90a4 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss
@@ -40,6 +40,7 @@
.title {
@include t.use-typography("body-small");
+
color: var(--color-foreground-primary);
display: flex;
align-items: center;
@@ -51,6 +52,7 @@
.visible-label {
@include t.use-typography("headline-small");
+
color: var(--color-foreground-secondary);
line-height: $sz-32;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs
index cb926f72fd..63e62bfbdb 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs
@@ -239,7 +239,7 @@
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field [:value :reference]
- :error/fn #(tr "workspace.tokens.self-reference")}
+ :error/fn #(tr "errors.tokens.self-reference")}
(fn [{:keys [name value]}]
(let [reference (get value :reference)]
(if (and reference name)
@@ -247,7 +247,7 @@
true)))]
[:fn {:error/field [:value :line-height]
- :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")}
+ :error/fn #(tr "errors.tokens.composite-line-height-needs-font-size")}
(fn [{:keys [value]}]
(let [line-heigh (get value :line-height)
font-size (get value :font-size)]
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss
index 5bac11ad27..67b16bef4b 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss
@@ -30,6 +30,7 @@
.title {
@include t.use-typography("body-small");
+
color: var(--color-foreground-primary);
display: flex;
align-items: center;
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs
index 19c3636e28..a9b6914b9f 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs
@@ -27,8 +27,11 @@
[okulary.core :as l]
[rumext.v2 :as mf]))
-(def ref:unfolded-token-paths
- (l/derived (l/key :unfolded-token-paths) refs/workspace-tokens))
+(def ref:folded-token-paths
+ (l/derived (l/key :folded-token-paths) refs/workspace-tokens))
+
+(def ref:unfolded-token-types
+ (l/derived (l/key :unfolded-token-types) refs/workspace-tokens))
(defn token-section-icon
[type]
@@ -72,8 +75,15 @@
(let [{:keys [modal title]}
(get dwta/token-properties type)
- unfolded-token-paths (mf/deref ref:unfolded-token-paths)
- is-type-unfolded (contains? (set unfolded-token-paths) (name type))
+ folded-token-paths (mf/deref ref:folded-token-paths)
+ unfolded-token-types-state (mf/deref ref:unfolded-token-types)
+
+ current-file (mf/deref refs/file)
+
+ is-same-file-set? (and (= (:file-id unfolded-token-types-state) (:id current-file))
+ (= (:set-id unfolded-token-types-state) selected-token-set-id))
+ is-type-unfolded (and is-same-file-set?
+ (contains? (set (:types unfolded-token-types-state)) type))
editing-ref (mf/deref refs/workspace-editor-state)
edition (mf/deref refs/selected-edition)
@@ -117,7 +127,7 @@
(mf/deps type expandable?)
(fn []
(when expandable?
- (st/emit! (dwtl/toggle-token-path (name type))))))
+ (st/emit! (dwtl/toggle-token-type type)))))
on-popover-open-click
(mf/use-fn
@@ -152,6 +162,10 @@
:level :warning
:timeout 3000}))))))))]
+ (mf/use-effect
+ (fn []
+ (st/emit! (dwtl/restore-unfolded-token-types))))
+
[:div {:class (stl/css :token-section-wrapper)
:data-testid (dm/str "section-" (name type))}
[:> layer-button* {:label title
@@ -172,13 +186,12 @@
(when is-type-unfolded
[:> token-tree* {:tokens tokens
:type type
- :id (dm/str "token-tree-" (name type))
- :tokens-lib tokens-lib
- :unfolded-token-paths unfolded-token-paths
+ :folded-token-paths folded-token-paths
:selected-shapes selected-shapes
+ :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:selected-token-set-id selected-token-set-id
- :is-selected-inside-layout is-selected-inside-layout
+ :tokens-lib tokens-lib
:on-token-pill-click on-token-pill-click
:on-pill-context-menu on-pill-context-menu
:on-node-context-menu on-node-context-menu}])]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs
index f150240cf1..6978243ec3 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.cljs
@@ -14,6 +14,7 @@
(def ^:private schema:token-node-context-menu
[:map
+ [:on-rename-node fn?]
[:on-delete-node fn?]])
(def ^:private tokens-node-menu-ref
@@ -26,7 +27,7 @@
(mf/defc token-node-context-menu*
{::mf/schema schema:token-node-context-menu}
- [{:keys [on-delete-node]}]
+ [{:keys [on-rename-node on-duplicate-node on-delete-node]}]
(let [mdata (mf/deref tokens-node-menu-ref)
is-open? (boolean mdata)
dropdown-ref (mf/use-ref)
@@ -38,13 +39,29 @@
left (+ (get-in mdata [:position :x]) 5)
container (hooks/use-portal-container :popup)
- delete-node (mf/use-fn
- (mf/deps mdata)
- (fn []
- (let [node (get mdata :node)
- type (get mdata :type)]
- (when node
- (on-delete-node node type)))))]
+ rename-node (mf/use-fn
+ (mf/deps mdata on-rename-node)
+ (fn []
+ (let [node (get mdata :node)
+ type (get mdata :type)]
+ (when node
+ (on-rename-node node type)))))
+
+ duplicate-node (mf/use-fn
+ (mf/deps mdata on-duplicate-node)
+ (fn []
+ (let [node (get mdata :node)
+ type (get mdata :type)]
+ (when node
+ (on-duplicate-node node type)))))
+
+ delete-node (mf/use-fn
+ (mf/deps mdata)
+ (fn []
+ (let [node (get mdata :node)
+ type (get mdata :type)]
+ (when node
+ (on-delete-node node type)))))]
(mf/with-effect [is-open?]
(when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
@@ -77,6 +94,16 @@
:on-context-menu prevent-default}
(when mdata
[:ul {:class (stl/css :token-node-context-menu-list)}
+ [:li {:class (stl/css :token-node-context-menu-listitem)}
+ [:button {:class (stl/css :token-node-context-menu-action)
+ :type "button"
+ :on-click rename-node}
+ (tr "labels.rename")]]
+ [:li {:class (stl/css :token-node-context-menu-listitem)}
+ [:button {:class (stl/css :token-node-context-menu-action)
+ :type "button"
+ :on-click duplicate-node}
+ (tr "labels.duplicate")]]
[:li {:class (stl/css :token-node-context-menu-listitem)}
[:button {:class (stl/css :token-node-context-menu-action)
:type "button"
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss
index 7e84dfa6d8..362699c88c 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/node_context_menu.scss
@@ -34,7 +34,7 @@
background-color: var(--color-background-tertiary);
max-block-size: 100vh;
overflow-y: auto;
- box-shadow: 0px 0px $sz-12 0px var(--menu-shadow-color);
+ box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color);
}
.token-node-context-menu-action {
@@ -43,6 +43,7 @@
--context-menu-item-border-color: none;
@include t.use-typography("body-small");
+
appearance: none;
background: var(--context-menu-item-bg-color);
border: $b-1 solid var(--context-menu-item-border-color);
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss
index e6df6d482e..ff9b9cc8c3 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/token_pill.scss
@@ -10,8 +10,7 @@
.token-pill {
@include use-typography("code-font");
- border: none;
- background: none;
+
cursor: pointer;
display: grid;
grid-template-columns: auto 1fr;
@@ -33,6 +32,7 @@
.name-wrapper {
@include use-typography("code-font");
+
display: block;
overflow: hidden;
text-overflow: ellipsis;
@@ -49,6 +49,7 @@
.first-name-wrapper {
@include use-typography("code-font");
+
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -57,6 +58,7 @@
.last-name-wrapper {
@include use-typography("code-font");
+
flex-shrink: 0;
}
@@ -70,6 +72,7 @@
--token-pill-border: var(--color-background-tertiary);
--token-pill-outline: none;
--token-pill-accent: var(--color-background-quaternary);
+
&:hover {
--token-pill-background: var(--color-token-background);
--token-pill-foreground: var(--color-foreground-primary);
@@ -81,6 +84,7 @@
&:focus-visible {
--token-pill-outline: var(--color-background-primary);
--token-pill-border: var(--color-accent-primary);
+
outline-offset: -3px;
}
}
@@ -124,11 +128,13 @@
--token-pill-background: var(--color-background-tertiary);
--token-pill-accent: var(--color-foreground-error);
}
+
&:hover {
--token-pill-foreground: var(--color-foreground-primary);
--token-pill-outline: none;
--token-pill-border: var(--color-foreground-error);
}
+
&:focus-visible {
--token-pill-foreground: var(--color-foreground-error);
--token-pill-border: var(--color-accent-primary);
@@ -138,6 +144,7 @@
.token-pill-invalid-applied {
--token-pill-border: var(--color-foreground-error);
+
&:hover,
&:focus-visible {
--token-pill-border: var(--color-foreground-error);
@@ -177,6 +184,7 @@
--token-pill-border: var(--color-accent-error);
--token-pill-foreground: var(--color-foreground-error);
--token-pill-accent: var(--color-foreground-error);
+
&:hover,
&:focus-visible {
--token-pill-border: var(--color-accent-error);
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs
index c08c1cd618..b27db0bc85 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.management.token-tree
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.library-edit :as dwtl]
@@ -20,7 +21,7 @@
[:map
[:node :any]
[:type :keyword]
- [:unfolded-token-paths {:optional true} [:vector :string]]
+ [:folded-token-paths {:optional true} [:maybe [:vector :string]]]
[:selected-shapes :any]
[:is-selected-inside-layout {:optional true} :boolean]
[:active-theme-tokens {:optional true} :any]
@@ -34,7 +35,7 @@
{::mf/schema schema:folder-node}
[{:keys [node
type
- unfolded-token-paths
+ folded-token-paths
selected-shapes
is-selected-inside-layout
active-theme-tokens
@@ -44,12 +45,11 @@
on-pill-context-menu
on-node-context-menu]}]
(let [full-path (str (name type) "." (:path node))
- is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path)
+ is-folder-expanded (not (contains? (set (or folded-token-paths [])) full-path))
swap-folder-expanded (mf/use-fn
- (mf/deps (:path node) type)
+ (mf/deps full-path)
(fn []
- (let [path (str (name type) "." (:path node))]
- (st/emit! (dwtl/toggle-token-path path)))))
+ (st/emit! (dwtl/toggle-token-path full-path))))
node-context-menu-prep (mf/use-fn
(mf/deps on-node-context-menu node)
@@ -65,18 +65,18 @@
:on-toggle-expand swap-folder-expanded
:on-context-menu node-context-menu-prep}]
(when is-folder-expanded
- (let [children-fn (:children-fn node)]
+ (let [children (:children node)]
[:div {:class (stl/css :folder-children-wrapper)
:id (str "folder-children-" (:path node))}
- (when children-fn
- (let [children (children-fn)]
- (for [child children]
+ (when (seq children)
+ (let [sorted-children (d/natural-sort-by :name children)]
+ (for [child sorted-children]
(if (not (:leaf child))
[:ul {:class (stl/css :node-parent)
:key (:path child)}
[:> folder-node* {:type type
:node child
- :unfolded-token-paths unfolded-token-paths
+ :folded-token-paths folded-token-paths
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
@@ -100,12 +100,12 @@
[:map
[:tokens :any]
[:type :keyword]
- [:unfolded-token-paths {:optional true} [:vector :string]]
+ [:folded-token-paths {:optional true} [:maybe [:vector :string]]]
[:selected-shapes :any]
[:is-selected-inside-layout {:optional true} :boolean]
[:active-theme-tokens {:optional true} :any]
- [:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any]
+ [:selected-token-set-id {:optional true} :any]
[:on-token-pill-click {:optional true} fn?]
[:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
@@ -114,7 +114,7 @@
{::mf/schema schema:token-tree}
[{:keys [tokens
type
- unfolded-token-paths
+ folded-token-paths
selected-shapes
is-selected-inside-layout
active-theme-tokens
@@ -127,7 +127,8 @@
tree (mf/use-memo
(mf/deps tokens)
(fn []
- (cpn/build-tree-root tokens separator)))
+ (->> (cpn/build-tree-root tokens separator)
+ (d/natural-sort-by :name))))
can-edit? (:can-edit (deref refs/permissions))
on-node-context-menu (mf/use-fn
(mf/deps can-edit? on-node-context-menu)
@@ -151,7 +152,7 @@
:key (:path node)}
[:> folder-node* {:node node
:type type
- :unfolded-token-paths unfolded-token-paths
+ :folded-token-paths folded-token-paths
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss
index 7b1ea0244e..9edd7caf40 100644
--- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss
@@ -12,7 +12,6 @@
padding-block-end: var(--sp-s);
display: flex;
flex-wrap: wrap;
- gap: var(--sp-s);
padding-inline-start: calc(var(--node-spacing));
& .node-parent {
@@ -50,6 +49,7 @@
margin-block-end: var(--sp-s);
}
}
+
& .token-pill {
flex: 0 0 auto;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs
index 198972a193..1ac692c484 100644
--- a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.cljs
@@ -20,27 +20,41 @@
[app.util.keyboard :as kbd]
[rumext.v2 :as mf]))
-(defn hide-remapping-modal
+(defn- hide-remapping-modal
"Hide the token remapping confirmation modal"
[]
(st/emit! (modal/hide)))
+;; TODO: Uncomment when modal components support schema validation
+
+;; (def ^:private schema:remap-data
+;; [:map
+;; [:old-name :string]
+;; [:new-name :string]
+;; [:type [:enum "token" "node"]]])
+
+;; (def ^:private schema:token-remapping-modal
+;; [:map
+;; [:remap-data [:maybe schema:remap-data]]
+;; [:on-remap {:optional true} [:maybe fn?]]
+;; [:on-rename {:optional true} [:maybe fn?]]])
+
;; Remapping Modal Component
(mf/defc token-remapping-modal
{::mf/register modal/components
- ::mf/register-as :tokens/remapping-confirmation}
- [{:keys [old-token-name new-token-name on-remap on-rename]}]
- (let [remap-modal (get @st/state :remap-modal)
+ ::mf/register-as :tokens/remapping-confirmation
+ ;; TODO: Uncomment when modal components support schema validation
+ ;; ::mf/schema schema:token-remapping-modal
+ }
+ [{:keys [remap-data on-remap on-rename]}]
+ (let [old-name (:old-name remap-data)
+ new-name (:new-name remap-data)
;; Remap logic on confirm
confirm-remap
(mf/use-fn
- (mf/deps on-remap remap-modal)
+ (mf/deps on-remap old-name new-name)
(fn []
- ;; Call shared remapping logic
- (let [old-token-name (:old-token-name remap-modal)
- new-token-name (:new-token-name remap-modal)]
- (st/emit! [:tokens/remap-tokens old-token-name new-token-name]))
(when (fn? on-remap)
(on-remap))))
@@ -83,9 +97,13 @@
:id "modal-title"
:typography "headline-large"
:class (stl/css :modal-title)}
- (tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
+ (if (= (:type remap-data) "token")
+ (tr "workspace.tokens.remap-token-references-title" old-name new-name)
+ (tr "workspace.tokens.remap-node-references-title" old-name new-name))]]
[:div {:class (stl/css :modal-content)}
- [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
+ (if (= (:type remap-data) "token")
+ [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-token-warning-effects")]
+ [:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-node-warning-effects")])
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
diff --git a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss
index 704df4e5d3..3dd5b7db84 100644
--- a/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/remapping_modal.scss
@@ -6,14 +6,14 @@
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t;
-
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
--modal-title-foreground-color: var(--color-foreground-primary);
--modal-text-foreground-color: var(--color-foreground-secondary);
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
+
display: flex;
justify-content: center;
align-items: center;
@@ -32,7 +32,8 @@
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
+
inline-size: 100%;
max-inline-size: 32rem;
max-block-size: unset;
@@ -49,12 +50,14 @@
.modal-title {
@include t.use-typography("headline-medium");
+
color: var(--modal-title-foreground-color);
- word-break: break-word;
+ overflow-wrap: break-word;
}
.modal-content {
@include t.use-typography("body-large");
+
color: var(--modal-text-foreground-color);
}
@@ -64,12 +67,14 @@
}
.action-buttons {
- @extend .modal-action-btns;
+ @extend %modal-action-btns;
+
gap: var(--sp-s);
}
.modal-scd-msg,
.modal-msg {
@include t.use-typography("body-large");
+
color: var(--modal-text-foreground-color);
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs
index fd35710a74..56621eceac 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs
@@ -16,8 +16,8 @@
[rumext.v2 :as mf]))
(defn- on-select-token-set-click [id]
- (st/emit! (dwtl/clear-tokens-paths))
- (st/emit! (dwtl/set-selected-token-set-id id)))
+ (st/emit! (dwtl/clear-tokens-paths)
+ (dwtl/set-selected-token-set-id id)))
(defn- on-toggle-token-set-click [name]
(st/emit! (dwtl/toggle-token-set name)))
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss
index dee8bfe07b..f7141b973b 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss
@@ -21,6 +21,7 @@
.create-set-button {
@include use-typography("body-small");
+
background-color: transparent;
border: none;
appearance: none;
@@ -29,7 +30,8 @@
}
.set-item-container {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
width: 100%;
@@ -43,9 +45,11 @@
&.dnd-over-bot {
border-bottom: deprecated.$s-2 solid var(--layer-row-foreground-color-hover);
}
+
&.dnd-over-top {
border-top: deprecated.$s-2 solid var(--layer-row-foreground-color-hover);
}
+
&.dnd-over {
border: deprecated.$s-2 solid var(--layer-row-foreground-color-hover);
}
@@ -64,7 +68,8 @@
}
.set-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
flex-grow: 1;
padding-left: deprecated.$s-2;
}
@@ -112,7 +117,7 @@
}
.check-icon {
- color: currentColor;
+ color: currentcolor;
}
.set-item-container:hover {
@@ -129,6 +134,7 @@
padding: deprecated.$s-12;
color: var(--color-foreground-secondary);
}
+
.selected-set {
background-color: var(--layer-row-background-color-selected);
color: var(--layer-row-foreground-color-selected);
@@ -136,19 +142,21 @@
}
.collapsabled-icon {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
border-radius: deprecated.$br-8;
+
&:hover {
color: var(--title-foreground-color-hover);
}
}
.editing-node {
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
- @include deprecated.removeInputStyle;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.remove-input-style;
border: deprecated.$s-1 solid var(--input-border-color-focus);
border-radius: deprecated.$br-8;
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss
index 1e36266233..53e6e72901 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/sets/context_menu.scss
@@ -13,7 +13,8 @@
}
.context-list {
- @include deprecated.menuShadow;
+ @include deprecated.menu-shadow;
+
display: grid;
width: deprecated.$s-240;
padding: deprecated.$s-4;
@@ -26,6 +27,7 @@
.context-menu-item {
@include t.use-typography("body-small");
+
color: var(--menu-foreground-color);
display: flex;
align-items: center;
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs
index e7b9bf98c6..825e31571c 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/sets/helpers.cljs
@@ -41,7 +41,9 @@
(dwtl/clear-token-set-creation))
(if (empty? errors)
(let [token-set (ctob/make-token-set :name name)]
- (st/emit! (dwtl/create-token-set token-set)))
+ (st/emit! (dwtl/create-token-set token-set)
+ (dwtl/clear-tokens-paths)
+ (dwtl/clear-tokens-types)))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss
index dee8bfe07b..f7141b973b 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.scss
@@ -21,6 +21,7 @@
.create-set-button {
@include use-typography("body-small");
+
background-color: transparent;
border: none;
appearance: none;
@@ -29,7 +30,8 @@
}
.set-item-container {
- @include deprecated.bodySmallTypography;
+ @include deprecated.body-small-typography;
+
display: flex;
align-items: center;
width: 100%;
@@ -43,9 +45,11 @@
&.dnd-over-bot {
border-bottom: deprecated.$s-2 solid var(--layer-row-foreground-color-hover);
}
+
&.dnd-over-top {
border-top: deprecated.$s-2 solid var(--layer-row-foreground-color-hover);
}
+
&.dnd-over {
border: deprecated.$s-2 solid var(--layer-row-foreground-color-hover);
}
@@ -64,7 +68,8 @@
}
.set-name {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
flex-grow: 1;
padding-left: deprecated.$s-2;
}
@@ -112,7 +117,7 @@
}
.check-icon {
- color: currentColor;
+ color: currentcolor;
}
.set-item-container:hover {
@@ -129,6 +134,7 @@
padding: deprecated.$s-12;
color: var(--color-foreground-secondary);
}
+
.selected-set {
background-color: var(--layer-row-background-color-selected);
color: var(--layer-row-foreground-color-selected);
@@ -136,19 +142,21 @@
}
.collapsabled-icon {
- @include deprecated.buttonStyle;
- @include deprecated.flexCenter;
+ @include deprecated.button-style;
+ @include deprecated.flex-center;
+
height: deprecated.$s-24;
border-radius: deprecated.$br-8;
+
&:hover {
color: var(--title-foreground-color-hover);
}
}
.editing-node {
- @include deprecated.textEllipsis;
- @include deprecated.bodySmallTypography;
- @include deprecated.removeInputStyle;
+ @include deprecated.text-ellipsis;
+ @include deprecated.body-small-typography;
+ @include deprecated.remove-input-style;
border: deprecated.$s-1 solid var(--input-border-color-focus);
border-radius: deprecated.$br-8;
diff --git a/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss b/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss
index ae5339a979..fc4164d92a 100644
--- a/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/settings/menu.scss
@@ -5,19 +5,18 @@
// Copyright (c) KALEIDOS INC
@use "ds/spacing.scss" as *;
-
@use "refactor/common-refactor.scss" as deprecated;
.setting-modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.setting-modal {
- @extend .modal-container-base;
+ @extend %modal-container-base;
}
.close-btn {
- @extend .modal-close-btn-base;
+ @extend %modal-close-btn-base;
}
.settings-modal-layout {
diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss
index a48adee402..3eefaa09b0 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss
@@ -11,6 +11,7 @@
.sidebar-wrapper {
display: grid;
grid-template-rows: auto 1fr auto;
+
// Overflow on the bottom section can't be done without hardcoded values for the height
// This has to be changed from the wrapping sidebar styles
height: calc(100vh - #{deprecated.$s-92});
@@ -18,7 +19,6 @@
}
.token-management-section-wrapper {
- position: relative;
display: flex;
flex: 1;
height: var(--resize-height);
@@ -55,8 +55,9 @@
.section-icon {
margin-right: var(--sp-xs);
+
// Align better with the label
- translate: 0px -1px;
+ translate: 0 -1px;
}
.import-export-button-wrapper {
@@ -72,7 +73,8 @@
}
.import-export-button {
- @extend .button-secondary;
+ @extend %button-secondary;
+
display: flex;
align-items: center;
justify-content: end;
@@ -80,12 +82,12 @@
text-transform: uppercase;
gap: var(--sp-s);
background-color: var(--color-background-primary);
-
box-shadow: var(--el-shadow-dark);
}
.import-export-menu {
- @extend .menu-dropdown;
+ @extend %menu-dropdown;
+
top: -#{deprecated.$s-6};
right: 0;
translate: 0 -100%;
@@ -94,8 +96,10 @@
}
.import-export-menu-item {
- @extend .menu-item-base;
+ @extend %menu-item-base;
+
cursor: pointer;
+
&:hover {
color: var(--menu-foreground-color-hover);
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/themes.scss b/frontend/src/app/main/ui/workspace/tokens/themes.scss
index a96f9f341a..4dc34ad389 100644
--- a/frontend/src/app/main/ui/workspace/tokens/themes.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/themes.scss
@@ -15,7 +15,7 @@
.themes-header {
padding: var(--sp-s);
color: var(--title-foreground-color);
- word-break: break-word;
+ overflow-wrap: break-word;
}
.empty-theme-wrapper {
@@ -29,6 +29,7 @@
.create-theme-button {
@include use-typography("body-small");
+
background-color: transparent;
border: none;
appearance: none;
diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss
index 6be2add41c..4cc6c1ef06 100644
--- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.scss
@@ -76,6 +76,7 @@
gap: var(--sp-xs);
align-items: center;
padding: 0;
+
&:hover {
color: var(--color-accent-primary);
}
@@ -142,7 +143,7 @@
}
.group-title-name {
- @include textEllipsis;
+ @include text-ellipsis;
flex-grow: 1;
}
@@ -171,7 +172,7 @@
}
.theme-name-row {
- @include textEllipsis;
+ @include text-ellipsis;
flex-grow: 1;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss
index 867d65eafe..cf9a2077a4 100644
--- a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.scss
@@ -11,6 +11,7 @@
--custom-select-bg-color: var(--menu-background-color);
--custom-select-icon-color: var(--color-foreground-secondary);
--custom-select-text-color: var(--menu-foreground-color);
+
position: relative;
display: grid;
grid-template-columns: 1fr auto;
@@ -24,6 +25,7 @@
border: deprecated.$s-1 solid var(--custom-select-border-color);
color: var(--custom-select-text-color);
cursor: pointer;
+
&:hover {
--custom-select-bg-color: var(--menu-background-color-hover);
--custom-select-border-color: var(--menu-background-color);
@@ -41,7 +43,8 @@
}
.group {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
+
display: block;
padding: deprecated.$s-8;
color: var(--color-foreground-secondary);
@@ -52,23 +55,26 @@
--custom-select-border-color: var(--menu-border-color-disabled);
--custom-select-icon-color: var(--menu-foreground-color-disabled);
--custom-select-text-color: var(--menu-foreground-color-disabled);
+
pointer-events: none;
cursor: default;
}
.dropdown-button {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
color: var(--color-foreground-secondary);
}
.current-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
width: deprecated.$s-24;
padding-right: deprecated.$s-4;
}
.custom-select-dropdown {
- @extend .dropdown-wrapper;
+ @extend %dropdown-wrapper;
}
.separator {
@@ -88,7 +94,8 @@
}
.checked-element-button {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
position: relative;
display: flex;
justify-content: space-between;
@@ -96,17 +103,20 @@
}
.checked-element {
- @extend .dropdown-element-base;
+ @extend %dropdown-element-base;
+
&.is-selected {
color: var(--menu-foreground-color);
}
+
&.disabled {
display: none;
}
}
.check-icon {
- @include deprecated.flexCenter;
+ @include deprecated.flex-center;
+
color: var(--icon-foreground-primary);
visibility: hidden;
}
@@ -121,12 +131,13 @@
}
.current-label {
- @include deprecated.textEllipsis;
+ @include deprecated.text-ellipsis;
}
.dropdown-portal {
--menu-max-height: #{deprecated.$s-400};
- @extend .new-scrollbar;
+
+ @extend %new-scrollbar;
position: absolute;
}
diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss
index d005682878..f513b7b048 100644
--- a/frontend/src/app/main/ui/workspace/top_toolbar.scss
+++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss
@@ -27,6 +27,7 @@
--toolbar-position-y: #{deprecated.$s-28};
--toolbar-offset-y: 0px;
+
top: calc(var(--toolbar-position-y) + var(--toolbar-offset-y));
}
@@ -37,6 +38,7 @@
.main-toolbar-hidden {
--toolbar-offset-y: -#{deprecated.$s-4};
+
height: deprecated.$s-16;
z-index: deprecated.$z-index-1;
border-radius: 0 0 deprecated.$s-8 deprecated.$s-8;
@@ -62,7 +64,8 @@
}
.main-toolbar-options-button {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
height: deprecated.$s-36;
width: deprecated.$s-36;
flex-shrink: 0;
@@ -70,18 +73,20 @@
margin: 0 deprecated.$s-2;
svg {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--color-foreground-secondary);
}
&.selected {
- @extend .button-icon-selected;
+ @extend %button-icon-selected;
}
}
.toolbar-handler {
- @include deprecated.flexCenter;
- @include deprecated.buttonStyle;
+ @include deprecated.flex-center;
+ @include deprecated.button-style;
+
position: absolute;
left: 0;
bottom: 0;
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index b27b543901..26553b35d8 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -97,6 +97,7 @@
{:keys [options-mode
tooltip
+ preview-id
show-distances?
picking-color?]}
wglobal
@@ -260,7 +261,8 @@
show-rulers? (and (contains? layout :rulers) (not hide-ui?))
- disabled-guides? (or drawing-tool transform path-drawing? path-editing? @space? @mod?)
+ disabled-guides? (or drawing-tool transform path-drawing? path-editing? @space? @mod?
+ (contains? layout :lock-guides))
single-select? (= (count selected-shapes) 1)
@@ -308,28 +310,33 @@
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?)
(hooks/setup-keyboard alt? mod? space? z? shift?)
(hooks/setup-hover-shapes page-id move-stream base-objects selected mod? hover measure-hover
- hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?)
+ hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only?)
(hooks/setup-viewport-modifiers modifiers base-objects)
(hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
- [:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}
- (when (:can-edit permissions)
- (if read-only?
- [:> view-only-bar* {}]
- [:*
- (when-not hide-ui?
- [:> top-toolbar* {:layout layout}])
+ [:div {:class (stl/css :viewport) :style {"--zoom" zoom} :data-testid "viewport"}
+ (cond
+ (some? preview-id)
+ nil
- (when (and ^boolean path-editing?
- ^boolean single-select?)
- [:> path-edition-bar* {:shape editing-shape
- :edit-path-state edit-path-state
- :layout layout}])
+ (and read-only? (:can-edit permissions))
+ [:> view-only-bar* {}]
- (when (and ^boolean grid-editing?
- ^boolean single-select?)
- [:> grid-edition-bar* {:shape editing-shape}])]))
+ :else
+ [:*
+ (when-not hide-ui?
+ [:> top-toolbar* {:layout layout}])
+
+ (when (and ^boolean path-editing?
+ ^boolean single-select?)
+ [:> path-edition-bar* {:shape editing-shape
+ :edit-path-state edit-path-state
+ :layout layout}])
+
+ (when (and ^boolean grid-editing?
+ ^boolean single-select?)
+ [:> grid-edition-bar* {:shape editing-shape}])])
[:div {:class (stl/css :viewport-overlays)}
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap
diff --git a/frontend/src/app/main/ui/workspace/viewport.scss b/frontend/src/app/main/ui/workspace/viewport.scss
index 89ebd06403..cfd2209f7b 100644
--- a/frontend/src/app/main/ui/workspace/viewport.scss
+++ b/frontend/src/app/main/ui/workspace/viewport.scss
@@ -31,9 +31,6 @@
overflow: hidden;
pointer-events: none;
position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
+ inset: 0;
z-index: deprecated.$z-index-1;
}
diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss
index d0f1549cb1..9009fe8d76 100644
--- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss
+++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss
@@ -10,10 +10,11 @@
.marker-shape {
fill: var(--grid-editor-marker-color);
}
+
.marker-text {
fill: var(--app-white);
font-size: calc(deprecated.$s-12 / var(--zoom));
- font-family: worksans;
+ font-family: "worksans", "vazirmatn", sans-serif;
}
}
@@ -36,7 +37,7 @@
background: none;
border: 0;
color: var(--grid-editor-marker-text);
- font-family: worksans;
+ font-family: "worksans", "vazirmatn", sans-serif;
font-size: calc(deprecated.$fs-12 / var(--zoom));
font-weight: 400;
margin: 0;
@@ -118,10 +119,11 @@
}
.grid-actions-container {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
background: var(--panel-background-color);
border-radius: deprecated.$br-12;
- box-shadow: 0px 0px deprecated.$s-12 0px var(--menu-shadow-color);
+ box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color);
gap: deprecated.$s-8;
height: deprecated.$s-48;
margin-left: -50%;
@@ -139,22 +141,25 @@
}
.locate-btn {
- @extend .button-secondary;
+ @extend %button-secondary;
+
text-transform: uppercase;
padding: deprecated.$s-8 deprecated.$s-20;
font-size: deprecated.$fs-11;
}
.done-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
text-transform: uppercase;
padding: deprecated.$s-8 deprecated.$s-20;
font-size: deprecated.$fs-11;
}
.close-btn {
- @extend .button-tertiary;
+ @extend %button-tertiary;
+
svg {
- @extend .button-icon;
+ @extend %button-icon;
}
}
diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs
index d542d983c9..c79cafe702 100644
--- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs
@@ -6,6 +6,7 @@
(ns app.main.ui.workspace.viewport.guides
(:require
+ [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
@@ -23,12 +24,15 @@
[app.main.ui.formats :as fmt]
[app.main.ui.workspace.viewport.rulers :as rulers]
[app.util.dom :as dom]
+ [app.util.keyboard :as kbd]
+ [cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:const guide-width 1)
(def ^:const guide-opacity 0.7)
(def ^:const guide-opacity-hover 1)
-(def ^:const guide-color colors/new-danger)
+(def ^:const default-guide-color colors/new-danger)
+
(def ^:const guide-pill-width 34)
(def ^:const guide-pill-height 20)
(def ^:const guide-pill-corner-radius 4)
@@ -282,13 +286,29 @@
(mf/defc guide*
{::mf/wrap [mf/memo]}
[{:keys [guide is-hover on-guide-change get-hover-frame vbox zoom
- hover-frame disabled-guides frame-modifier frame-transform]}]
+ hover-frame disabled-guides frame-modifier frame-transform
+ on-guide-context-menu]}]
(let [axis
(get guide :axis)
+ guide-color
+ (or (:color guide) default-guide-color)
+
+ read-only?
+ (mf/use-ctx ctx/workspace-read-only?)
+
+ is-editing*
+ (mf/use-state false)
+
+ is-editing
+ (deref is-editing*)
+
+ input-ref
+ (mf/use-ref nil)
+
handle-change-position
(mf/use-fn
- (mf/deps on-guide-change)
+ (mf/deps on-guide-change guide)
(fn [changes]
(when on-guide-change
(on-guide-change (merge guide changes)))))
@@ -329,14 +349,68 @@
frame-guide-outside?
(and (some? frame)
- (not (is-guide-inside-frame? (assoc guide :position pos) frame)))]
+ (not (is-guide-inside-frame? (assoc guide :position pos) frame)))
+
+ frame-offset
+ (if (some? frame)
+ (if (= axis :x) (:x frame) (:y frame))
+ 0)
+
+ accept-editing
+ (mf/use-fn
+ (mf/deps frame-offset on-guide-change guide)
+ (fn []
+ ;; Enter both fires this and triggers a blur that calls it again;
+ ;; bail out on the second invocation when the input is already gone.
+ (when-let [input (mf/ref-val input-ref)]
+ (let [parsed (-> input dom/get-value str/trim d/parse-double)]
+ (reset! is-editing* false)
+ (when (and (some? parsed) (some? on-guide-change))
+ (on-guide-change (assoc guide :position (+ parsed frame-offset))))))))
+
+ cancel-editing
+ (mf/use-fn
+ #(reset! is-editing* false))
+
+ on-input-key-down
+ (mf/use-fn
+ (mf/deps accept-editing cancel-editing)
+ (fn [event]
+ (cond
+ (kbd/enter? event)
+ (do (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (accept-editing))
+
+ (kbd/esc? event)
+ (do (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (cancel-editing)))))
+
+ on-double-click
+ (mf/use-fn
+ (mf/deps read-only?)
+ (fn [event]
+ (when-not read-only?
+ (dom/stop-propagation event)
+ (reset! is-editing* true))))]
+
+ (mf/with-effect [is-editing]
+ (when is-editing
+ (some-> (mf/ref-val input-ref) dom/select-text!)))
(when (or (nil? frame)
(and (cfh/root-frame? frame)
(not (ctst/rotated-frame? frame))))
[:g.guide-area {:opacity (when frame-guide-outside? 0)}
(when-not disabled-guides
- (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)]
+ (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)
+ on-context-menu
+ (fn [event]
+ (dom/prevent-default event)
+ (dom/stop-propagation event)
+ (when on-guide-context-menu
+ (on-guide-context-menu event guide)))]
[:rect {:x x
:y y
:width width
@@ -349,7 +423,9 @@
:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up
:on-lost-pointer-capture on-lost-pointer-capture
- :on-pointer-move on-pointer-move}]))
+ :on-pointer-move on-pointer-move
+ :on-context-menu on-context-menu
+ :on-double-click on-double-click}]))
(if (some? frame)
(let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2
@@ -398,9 +474,12 @@
guide-opacity-hover
guide-opacity)}}]))
- (when (or is-hover (:hover @state))
+ (when (or is-hover (:hover @state) is-editing)
(let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]}
- (guide-pill-axis pos vbox zoom axis)]
+ (guide-pill-axis pos vbox zoom axis)
+ display-value (fmt/format-number (- pos frame-offset))
+ input-w (/ guide-pill-width zoom)
+ input-h (/ guide-pill-height zoom)]
[:g.guide-pill
[:rect {:x rect-x
:y rect-y
@@ -408,18 +487,46 @@
:height rect-height
:rx guide-pill-corner-radius
:ry guide-pill-corner-radius
- :style {:fill guide-color}}]
+ :style {:fill guide-color}
+ :on-double-click on-double-click}]
- [:text {:x text-x
- :y text-y
- :text-anchor "middle"
- :dominant-baseline "middle"
- :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")"))
- :style {:font-size (/ rulers/font-size zoom)
- :font-family rulers/font-family
- :fill colors/white}}
- ;; If the guide is associated to a frame we show the position relative to the frame
- (fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))])))
+ (if is-editing
+ [:foreignObject {:x (- text-x (/ input-w 2))
+ :y (- text-y (/ input-h 2))
+ :width input-w
+ :height input-h
+ :transform (when (= axis :y)
+ (str "rotate(-90 " text-x "," text-y ")"))}
+ [:input {:ref input-ref
+ :type "number"
+ :step "any"
+ :default-value display-value
+ :auto-focus true
+ :on-key-down on-input-key-down
+ :on-blur accept-editing
+ :on-pointer-down dom/stop-propagation
+ :style {:width "100%"
+ :height "100%"
+ :border "none"
+ :outline "none"
+ :padding 0
+ :margin 0
+ :background "transparent"
+ :color colors/white
+ :font-family rulers/font-family
+ :font-size (str (/ rulers/font-size zoom) "px")
+ :text-align "center"
+ :-moz-appearance "textfield"}}]]
+ [:text {:x text-x
+ :y text-y
+ :text-anchor "middle"
+ :dominant-baseline "middle"
+ :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")"))
+ :style {:font-size (/ rulers/font-size zoom)
+ :font-family rulers/font-family
+ :fill colors/white}}
+ ;; If the guide is associated to a frame we show the position relative to the frame
+ display-value])]))])))
(mf/defc new-guide-area*
[{:keys [vbox zoom axis get-hover-frame disabled-guides]}]
@@ -502,6 +609,13 @@
(st/emit! (dw/update-guides guide))
(st/emit! (dw/remove-guide guide)))))
+ on-guide-context-menu
+ (mf/use-fn
+ (fn [event guide]
+ (let [position (dom/get-client-position event)]
+ (st/emit! (dw/show-guide-context-menu {:position position
+ :guide guide})))))
+
frame-modifiers
(-> (group-by :id modifiers)
(update-vals (comp :transform first)))]
@@ -533,4 +647,5 @@
:frame-transform (get frame-modifiers frame-id)
:get-hover-frame get-hover-frame
:on-guide-change on-guide-change
+ :on-guide-context-menu on-guide-context-menu
:disabled-guides disabled-guides}]))]))
diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
index 86280c6a43..140b5d5dd1 100644
--- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs
@@ -177,7 +177,7 @@
(dw/increase-zoom)))))))
(defn setup-hover-shapes
- [page-id move-stream objects selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures?]
+ [page-id move-stream objects selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures? read-only?]
(let [;; We use ref so we don't recreate the stream on a change
zoom-ref (mf/use-ref zoom)
mod-ref (mf/use-ref @mod?)
@@ -261,7 +261,7 @@
(let [sorted-ids-cache (mf/use-ref {})]
(hooks/use-stream
over-shapes-stream
- (mf/deps page-id objects show-measures?)
+ (mf/deps page-id objects show-measures? read-only?)
(fn [ids]
(let [selected (mf/ref-val selected-ref)
focus (mf/ref-val focus-ref)
@@ -273,7 +273,7 @@
(let [sorted-ids
(into (d/ordered-set)
(comp (remove (partial cfh/hidden-parent? objects))
- (remove #(dm/get-in objects [% :blocked]))
+ (remove #(and (not read-only?) (dm/get-in objects [% :blocked])))
(remove (partial cfh/svg-raw-shape? objects)))
(ctt/sort-z-index objects ids {:bottom-frames? mod?}))]
(mf/set-ref-val! sorted-ids-cache (assoc cached-ids [mod? ids] sorted-ids))
diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs
index 8617405003..8b1ae15552 100644
--- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs
@@ -33,6 +33,10 @@
:move-overlay-index])
refs/workspace-local =))
+(def ^:private outgoing-link-color "var(--color-accent-tertiary)")
+(def ^:private incoming-link-color "var(--color-accent-quaternary)")
+(def ^:private neutral-link-color "var(--df-secondary)")
+
(defn- on-pointer-down
[event index {:keys [id] :as shape}]
(dom/stop-propagation event)
@@ -139,7 +143,7 @@
(mf/defc interaction-path*
- [{:keys [index level orig-shape dest-shape dest-point is-selected action-type zoom]}]
+ [{:keys [index level orig-shape dest-shape dest-point selected is-selected action-type zoom]}]
(let [[orig-pos orig-x orig-y dest-pos dest-x dest-y]
(cond
dest-shape
@@ -160,11 +164,17 @@
path ["M" orig-x orig-y "C" (+ orig-x orig-dx) orig-y (+ dest-x dest-dx) dest-y dest-x dest-y]
pdata (str/join " " path)
- arrow-dir (if (= dest-pos :left) :right :left)]
+ arrow-dir (if (= dest-pos :left) :right :left)
+ incoming? (and (some? dest-shape)
+ (contains? selected (:id dest-shape)))
+ stroke-color (cond
+ is-selected outgoing-link-color
+ incoming? incoming-link-color
+ :else neutral-link-color)]
(if-not is-selected
[:g {:on-pointer-down #(on-pointer-down % index orig-shape)}
- [:path {:stroke "var(--df-secondary)"
+ [:path {:stroke stroke-color
:fill "none"
:pointer-events "visible"
:stroke-width (/ 2 zoom)
@@ -173,13 +183,13 @@
[:> interaction-marker* {:index index
:x dest-x
:y dest-y
- :stroke "var(--df-secondary)"
+ :stroke stroke-color
:action-type action-type
:arrow-dir arrow-dir
:zoom zoom}])]
[:g {:on-pointer-down #(on-pointer-down % index orig-shape)}
- [:path {:stroke "var(--color-accent-tertiary)"
+ [:path {:stroke stroke-color
:fill "none"
:pointer-events "visible"
:stroke-width (/ 2 zoom)
@@ -188,17 +198,17 @@
(when dest-shape
[:& outline {:zoom zoom
:shape dest-shape
- :color "var(--color-accent-tertiary)"}])
+ :color stroke-color}])
[:> interaction-marker* {:index index
:x orig-x
:y orig-y
- :stroke "var(--color-accent-tertiary)"
+ :stroke stroke-color
:zoom zoom}]
[:> interaction-marker* {:index index
:x dest-x
:y dest-y
- :stroke "var(--color-accent-tertiary)"
+ :stroke stroke-color
:action-type action-type
:arrow-dir arrow-dir
:zoom zoom}]])))
diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss
index 94c86e6a8d..e079d01f1a 100644
--- a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss
+++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss
@@ -39,7 +39,9 @@
.topbar-btn {
--pathbar-icon-color: var(--color-foreground-secondary);
- @extend .button-tertiary;
+
+ @extend %button-tertiary;
+
height: deprecated.$s-36;
width: deprecated.$s-36;
flex-shrink: 0;
@@ -50,11 +52,13 @@
&.is-toggled {
--pathbar-icon-color: var(--button-radio-foreground-color-active);
+
background-color: var(--button-radio-background-color-active);
}
.pathbar-icon {
- @extend .button-icon;
+ @extend %button-icon;
+
stroke: var(--pathbar-icon-color);
}
}
diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss
index fbbe82c72e..87d3abe4bc 100644
--- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss
+++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.scss
@@ -5,11 +5,8 @@
// Copyright (c) KALEIDOS INC
.pixel-overlay {
- left: 0;
+ inset: 0;
pointer-events: initial;
position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
z-index: 1;
}
diff --git a/frontend/src/app/main/ui/workspace/viewport/presence.scss b/frontend/src/app/main/ui/workspace/viewport/presence.scss
index d71cd38e21..32486a0244 100644
--- a/frontend/src/app/main/ui/workspace/viewport/presence.scss
+++ b/frontend/src/app/main/ui/workspace/viewport/presence.scss
@@ -8,7 +8,7 @@
.profile-name {
width: fit-content;
- font-family: worksans;
+ font-family: "worksans", "vazirmatn", sans-serif;
padding: 2px 12px;
border-radius: deprecated.$br-4;
display: flex;
diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs
index b9f4f69cb4..2fca82347f 100644
--- a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs
@@ -20,14 +20,12 @@
;; branch.
(mf/defc view-only-bar*
- {::mf/private true}
[]
- (let [handle-close-view-mode
+ (let [on-close
(mf/use-fn
- (fn []
- (st/emit! :interrupt
- (dw/set-options-mode :design)
- (dwc/set-workspace-read-only false))))]
+ #(st/emit! :interrupt
+ (dw/set-options-mode :design)
+ (dwc/set-workspace-read-only false)))]
[:div {:class (stl/css :viewport-actions)}
[:div {:class (stl/css :viewport-actions-container)}
[:div {:class (stl/css :viewport-actions-title)}
@@ -35,7 +33,7 @@
{:tag-name "span"
:content (tr "workspace.top-bar.view-only")}]]
[:button {:class (stl/css :done-btn)
- :on-click handle-close-view-mode}
+ :on-click on-close}
(tr "workspace.top-bar.read-only.done")]]]))
(mf/defc path-edition-bar*
diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss
index 802dc32ab7..5ee297d756 100644
--- a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss
+++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss
@@ -10,6 +10,7 @@
.viewport-actions-path {
pointer-events: none;
position: absolute;
+
--actions-toolbar-position-y: #{deprecated.$s-28};
--actions-toolbar-offset-y: #{deprecated.$s-6};
@@ -23,7 +24,8 @@
}
.viewport-actions-container {
- @include deprecated.flexRow;
+ @include deprecated.flex-row;
+
background: var(--panel-background-color);
border-radius: deprecated.$br-12;
box-shadow: 0 0 deprecated.$s-12 0 var(--menu-shadow-color);
@@ -45,7 +47,8 @@
}
.done-btn {
- @extend .button-primary;
+ @extend %button-primary;
+
text-transform: uppercase;
padding: deprecated.$s-8 deprecated.$s-20;
font-size: deprecated.$fs-11;
diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
index a47897d2d6..dac8657ad2 100644
--- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs
@@ -33,24 +33,36 @@
(mf/defc pixel-grid*
[{:keys [vbox zoom]}]
- [:g.pixel-grid
- [:defs
- [:pattern {:id "pixel-grid"
- :viewBox "0 0 1 1"
- :width 1
- :height 1
- :pattern-units "userSpaceOnUse"}
- [:path {:d "M 1 0 L 0 0 0 1"
- :style {:fill "none"
- :stroke (if (dbg/enabled? :pixel-grid) "red" "var(--status-color-info-500)")
- :stroke-opacity (if (dbg/enabled? :pixel-grid) 1 "0.2")
- :stroke-width (str (/ 1 zoom))}}]]]
- [:rect {:x (:x vbox)
- :y (:y vbox)
- :width (:width vbox)
- :height (:height vbox)
- :fill (str "url(#pixel-grid)")
- :style {:pointer-events "none"}}]])
+ (let [page (mf/deref refs/workspace-page)
+ custom-color (:pixel-grid-color page)
+ custom-alpha (:pixel-grid-opacity page)
+ debug? (dbg/enabled? :pixel-grid)
+ stroke (cond
+ debug? "red"
+ custom-color custom-color
+ :else "var(--status-color-info-500)")
+ opacity (cond
+ debug? 1
+ (some? custom-alpha) custom-alpha
+ :else 0.2)]
+ [:g.pixel-grid
+ [:defs
+ [:pattern {:id "pixel-grid"
+ :viewBox "0 0 1 1"
+ :width 1
+ :height 1
+ :pattern-units "userSpaceOnUse"}
+ [:path {:d "M 1 0 L 0 0 0 1"
+ :style {:fill "none"
+ :stroke stroke
+ :stroke-opacity opacity
+ :stroke-width (str (/ 1 zoom))}}]]]
+ [:rect {:x (:x vbox)
+ :y (:y vbox)
+ :width (:width vbox)
+ :height (:height vbox)
+ :fill (str "url(#pixel-grid)")
+ :style {:pointer-events "none"}}]]))
(mf/defc cursor-tooltip*
[{:keys [zoom tooltip]}]
diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss
index 319888776c..4a1b949374 100644
--- a/frontend/src/app/main/ui/workspace/viewport/widgets.scss
+++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss
@@ -21,13 +21,13 @@
.frame-flow-badge-content {
@include t.use-typography("body-small");
+
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-xs);
border-radius: $br-6;
- padding-inline-start: var(--sp-xs);
- padding-inline-end: var(--sp-s);
+ padding-inline: var(--sp-xs) var(--sp-s);
height: var(--sp-xxl);
background-color: var(--frame-flow-badge-background-color);
color: var(--frame-flow-badge-foreground-color);
@@ -47,6 +47,7 @@
.frame-title-label {
@include t.use-typography("body-small");
+
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -56,6 +57,7 @@
.frame-title-input {
@include t.use-typography("body-small");
+
flex-grow: 1;
width: 100%;
max-width: initial;
@@ -73,6 +75,7 @@
cursor: pointer;
fill: var(--button-add-background-color);
+
&:hover {
--button-add-background-color: var(--button-add-background-color-hover);
}
diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs
index 8a9f8caad4..837c72f598 100644
--- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs
@@ -98,6 +98,7 @@
{:keys [options-mode
tooltip
show-distances?
+ preview-id
picking-color?]}
wglobal
@@ -283,7 +284,9 @@
hide-ui? (contains? layout :hide-ui)
show-rulers? (and (contains? layout :rulers) (not hide-ui?))
- disabled-guides? (or drawing-tool transform path-drawing? path-editing?)
+
+ disabled-guides? (or drawing-tool transform path-drawing? path-editing?
+ (contains? layout :lock-guides))
single-select? (= (count selected-shapes) 1)
@@ -450,27 +453,33 @@
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?)
(hooks/setup-keyboard alt? mod? space? z? shift?)
(hooks/setup-hover-shapes page-id move-stream base-objects selected mod? hover measure-hover
- hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?)
+ hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only?)
(hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
[:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}
- (when (:can-edit permissions)
- (if read-only?
- [:> view-only-bar* {}]
- [:*
- (when-not hide-ui?
- [:> top-toolbar* {:layout layout}])
- (when (and ^boolean path-editing?
- ^boolean single-select?)
- [:> path-edition-bar* {:shape editing-shape
- :edit-path-state edit-path-state
- :layout layout}])
+ (cond
+ (some? preview-id)
+ nil
- (when (and ^boolean grid-editing?
- ^boolean single-select?)
- [:> grid-edition-bar* {:shape editing-shape}])]))
+ (and read-only? (:can-edit permissions))
+ [:> view-only-bar* {}]
+
+ :else
+ [:*
+ (when-not hide-ui?
+ [:> top-toolbar* {:layout layout}])
+
+ (when (and ^boolean path-editing?
+ ^boolean single-select?)
+ [:> path-edition-bar* {:shape editing-shape
+ :edit-path-state edit-path-state
+ :layout layout}])
+
+ (when (and ^boolean grid-editing?
+ ^boolean single-select?)
+ [:> grid-edition-bar* {:shape editing-shape}])])
[:div {:class (stl/css :viewport-overlays)}
(when show-comments?
diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.scss b/frontend/src/app/main/ui/workspace/viewport_wasm.scss
index a83fde4650..99af2bee48 100644
--- a/frontend/src/app/main/ui/workspace/viewport_wasm.scss
+++ b/frontend/src/app/main/ui/workspace/viewport_wasm.scss
@@ -29,10 +29,7 @@
overflow: hidden;
pointer-events: none;
position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
+ inset: 0;
z-index: 10;
}
@@ -40,7 +37,7 @@
position: fixed;
inset: 0;
z-index: 100;
- background-color: rgba(0, 0, 0, 0.5);
+ background-color: rgb(0 0 0 / 0.5);
display: grid;
place-items: center;
cursor: default;
diff --git a/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss
index 0ad86b0c29..d57571ef0c 100644
--- a/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss
+++ b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss
@@ -6,15 +6,14 @@
@use "ds/_utils.scss" as *;
@use "ds/_borders.scss" as *;
-
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
- @extend .modal-overlay-base;
+ @extend %modal-overlay-base;
}
.modal-dialog {
- @extend .modal-container-base;
+ @extend %modal-container-base;
color: var(--color-foreground-secondary);
display: grid;
diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs
index 68526ae8a4..f7c5a324a6 100644
--- a/frontend/src/app/plugins/api.cljs
+++ b/frontend/src/app/plugins/api.cljs
@@ -284,7 +284,23 @@
{:file-id file-id
:local? false
:name name
- :blobs [(js/Blob. #js [data] #js {:type mime-type})]
+ :blobs [(js/Blob.
+ #js [(cond
+ (instance? js/Uint8Array data)
+ data
+
+ (instance? js/ArrayBuffer data)
+ (js/Uint8Array. data)
+
+ (array? data)
+ (js/Uint8Array.from data)
+
+ (and (some? data) (= (type data) js/Object))
+ (js/Uint8Array.from (js/Object.values data))
+
+ :else
+ data)]
+ #js {:type mime-type})]
:on-image identity
:on-svg identity})
(rx/take 1)
diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs
index 1022a1a65c..8a249d32dc 100644
--- a/frontend/src/app/plugins/library.cljs
+++ b/frontend/src/app/plugins/library.cljs
@@ -1108,23 +1108,20 @@
:connectLibrary
(fn [library-id]
- (cond
- (not (r/check-permission plugin-id "library:write"))
- (u/not-valid plugin-id :connectLibrary "Plugin doesn't have 'library:write' permission")
+ (js/Promise.
+ (fn [resolve reject]
+ (cond
+ (not (r/check-permission plugin-id "library:write"))
+ (u/reject-not-valid reject :connectLibrary "Plugin doesn't have 'library:write' permission")
- :else
- (js/Promise.
- (fn [resolve reject]
- (cond
- (not (string? library-id))
- (do (u/not-valid plugin-id :connectLibrary library-id)
- (reject nil))
+ (not (string? library-id))
+ (u/reject-not-valid reject :connectLibrary library-id)
- :else
- (let [file-id (:current-file-id @st/state)
- library-id (uuid/parse library-id)]
- (->> st/stream
- (rx/filter (ptk/type? ::dwl/attach-library-finished))
- (rx/take 1)
- (rx/subs! #(resolve (library-proxy plugin-id library-id)) reject))
- (st/emit! (dwl/link-file-to-library file-id library-id))))))))))
+ :else
+ (let [file-id (:current-file-id @st/state)
+ library-id (uuid/parse library-id)]
+ (->> st/stream
+ (rx/filter (ptk/type? ::dwl/attach-library-finished))
+ (rx/take 1)
+ (rx/subs! #(resolve (library-proxy plugin-id library-id)) reject))
+ (st/emit! (dwl/link-file-to-library file-id library-id)))))))))
diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs
index 5d4148662d..29ec5c2bf7 100644
--- a/frontend/src/app/plugins/parser.cljs
+++ b/frontend/src/app/plugins/parser.cljs
@@ -7,6 +7,7 @@
(ns app.plugins.parser
(:require
[app.common.data :as d]
+ [app.common.geom.point :as gpt]
[app.common.json :as json]
[app.common.types.path :as path]
[app.common.uuid :as uuid]
@@ -26,10 +27,16 @@
(if (string? color) (-> color str/lower) color))
(defn parse-point
+ "Parses a point-like JS object into a `gpt/point` record.
+
+ The schema for shape interactions (`schema:open-overlay-interaction`,
+ `::gpt/point`) requires a Point record — returning a plain map caused
+ plugin `addInteraction` calls with an `open-overlay` action and a
+ `manualPositionLocation` to be silently rejected. See issue #8409."
[^js point]
(when point
- {:x (obj/get point "x")
- :y (obj/get point "y")}))
+ (gpt/point (obj/get point "x")
+ (obj/get point "y"))))
(defn parse-shape-type
[type]
diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs
index e3792f3fc9..df9afb6380 100644
--- a/frontend/src/app/plugins/register.cljs
+++ b/frontend/src/app/plugins/register.cljs
@@ -54,7 +54,10 @@
(conj "library:read")
(contains? permissions "comment:write")
- (conj "comment:read"))
+ (conj "comment:read")
+
+ (contains? permissions "clipboard:write")
+ (conj "clipboard:read"))
plugin-url
(u/uri plugin-url)
diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs
index 696df23001..8c90b74ca9 100644
--- a/frontend/src/app/plugins/ruler_guides.cljs
+++ b/frontend/src/app/plugins/ruler_guides.cljs
@@ -24,7 +24,7 @@
(defn ruler-guide-proxy
[plugin-id file-id page-id id]
- (obj/reify {:name "RuleGuideProxy"}
+ (obj/reify {:name "RulerGuideProxy"}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file {:enumerable false :get (constantly file-id)}
:$page {:enumerable false :get (constantly page-id)}
@@ -94,6 +94,22 @@
value)]
(st/emit! (dwgu/update-guides (assoc guide :position position))))))}
+ :color
+ {:this true
+ :get
+ (fn [self]
+ (-> self u/proxy->ruler-guide :color))
+
+ :set
+ (fn [self value]
+ (cond
+ (not (r/check-permission plugin-id "content:write"))
+ (u/not-valid plugin-id :color "Plugin doesn't have 'content:write' permission")
+
+ :else
+ (let [guide (u/proxy->ruler-guide self)]
+ (st/emit! (dwgu/update-guides (assoc guide :color value))))))}
+
:remove
(fn []
(let [guide (u/locate-ruler-guide file-id page-id id)]
diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs
index 1a1144b086..63037e405c 100644
--- a/frontend/src/app/plugins/shape.cljs
+++ b/frontend/src/app/plugins/shape.cljs
@@ -1186,8 +1186,8 @@
(let [objects (u/locate-objects file-id page-id)
shape (u/locate-shape file-id page-id id)]
(when (ctn/in-any-component? objects shape)
- (let [[root component] (u/locate-component objects shape)]
- (lib-component-proxy plugin-id (:component-file root) (:id component))))))
+ (when-let [[head component] (u/locate-head-component objects shape)]
+ (lib-component-proxy plugin-id (:component-file head) (:id component))))))
:detach
(fn []
@@ -1293,7 +1293,7 @@
(u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")
:else
- (let [id (uuid/next)
+ (let [ruler-id (uuid/next)
axis (parser/orientation->axis orientation)
objects (u/locate-objects file-id page-id)
frame (get objects id)
@@ -1301,11 +1301,11 @@
position (+ board-pos value)]
(st/emit!
(dwgu/update-guides
- {:id id
+ {:id ruler-id
:axis axis
:position position
:frame-id id}))
- (rg/ruler-guide-proxy plugin-id file-id page-id id)))))
+ (rg/ruler-guide-proxy plugin-id file-id page-id ruler-id)))))
:removeRulerGuide
(fn [_ value]
diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs
index a6f1d3a850..776a905308 100644
--- a/frontend/src/app/plugins/tokens.cljs
+++ b/frontend/src/app/plugins/tokens.cljs
@@ -214,127 +214,134 @@
(obj/type-of? p "TokenSetProxy"))
(defn token-set-proxy
- [plugin-id file-id id]
- (obj/reify {:name "TokenSetProxy"
- :on-error (u/handle-error plugin-id)}
- :$plugin {:enumerable false :get (constantly plugin-id)}
- :$file-id {:enumerable false :get (constantly file-id)}
- :$id {:enumerable false :get (constantly id)}
+ ([plugin-id file-id id]
+ (token-set-proxy plugin-id file-id id nil))
+ ([plugin-id file-id id initial-name]
+ (obj/reify {:name "TokenSetProxy"
+ :on-error (u/handle-error plugin-id)}
+ :$plugin {:enumerable false :get (constantly plugin-id)}
+ :$file-id {:enumerable false :get (constantly file-id)}
+ :$id {:enumerable false :get (constantly id)}
- :id
- {:get #(dm/str id)}
+ :id
+ {:get #(dm/str id)}
- :name
- {:this true
- :get
+ :name
+ {:this true
+ :get
+ (fn [_]
+ ;; Prefer the authoritative state lookup; fall back to initial-name
+ ;; when the async state update from `catalog.addSet()` hasn't
+ ;; propagated yet.
+ (let [set (u/locate-token-set file-id id)]
+ (if (some? set)
+ (ctob/get-name set)
+ initial-name)))
+ :schema (cfo/make-token-set-name-schema
+ (u/locate-tokens-lib file-id)
+ id)
+ :set
+ (fn [_ name]
+ (let [set (u/locate-token-set file-id id)]
+ (st/emit! (dwtl/rename-token-set set name))))}
+
+ :active
+ {:this true
+ :enumerable false
+ :get
+ (fn [_]
+ (let [tokens-lib (u/locate-tokens-lib file-id)
+ set (u/locate-token-set file-id id)]
+ (ctob/token-set-active? tokens-lib (ctob/get-name set))))
+ :schema ::sm/boolean
+ :set
+ (fn [_ value]
+ (let [set (u/locate-token-set file-id id)]
+ (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))}
+
+ :toggleActive
(fn [_]
(let [set (u/locate-token-set file-id id)]
- (ctob/get-name set)))
- :schema (cfo/make-token-set-name-schema
- (u/locate-tokens-lib file-id)
- id)
- :set
- (fn [_ name]
- (let [set (u/locate-token-set file-id id)]
- (st/emit! (dwtl/rename-token-set set name))))}
+ (st/emit! (dwtl/toggle-token-set (ctob/get-name set)))))
- :active
- {:this true
- :enumerable false
- :get
- (fn [_]
- (let [tokens-lib (u/locate-tokens-lib file-id)
- set (u/locate-token-set file-id id)]
- (ctob/token-set-active? tokens-lib (ctob/get-name set))))
- :schema ::sm/boolean
- :set
- (fn [_ value]
- (let [set (u/locate-token-set file-id id)]
- (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))}
+ :tokens
+ {:this true
+ :enumerable false
+ :get
+ (fn [_]
+ (let [tokens-lib (u/locate-tokens-lib file-id)]
+ (->> (ctob/get-tokens tokens-lib id)
+ (vals)
+ (map #(token-proxy plugin-id file-id id (:id %)))
+ (apply array))))}
- :toggleActive
- (fn [_]
- (let [set (u/locate-token-set file-id id)]
- (st/emit! (dwtl/toggle-token-set (ctob/get-name set)))))
+ :tokensByType
+ {:this true
+ :enumerable false
+ :get
+ (fn [_]
+ (let [tokens-lib (u/locate-tokens-lib file-id)
+ tokens (ctob/get-tokens tokens-lib id)]
+ (->> tokens
+ (vals)
+ (sort-by :name)
+ (group-by #(cto/token-type->dtcg-token-type (:type %)))
+ (into [])
+ (mapv (fn [[type tokens]]
+ #js [(name type)
+ (->> tokens
+ (map #(token-proxy plugin-id file-id id (:id %)))
+ (apply array))]))
+ (apply array))))}
- :tokens
- {:this true
- :enumerable false
- :get
- (fn [_]
- (let [tokens-lib (u/locate-tokens-lib file-id)]
- (->> (ctob/get-tokens tokens-lib id)
- (vals)
- (map #(token-proxy plugin-id file-id id (:id %)))
- (apply array))))}
+ :getTokenById
+ {:enumerable false
+ :schema [:tuple ::sm/uuid]
+ :fn (fn [token-id]
+ (let [token (u/locate-token file-id id token-id)]
+ (when (some? token)
+ (token-proxy plugin-id file-id id token-id))))}
- :tokensByType
- {:this true
- :enumerable false
- :get
- (fn [_]
- (let [tokens-lib (u/locate-tokens-lib file-id)
- tokens (ctob/get-tokens tokens-lib id)]
- (->> tokens
- (vals)
- (sort-by :name)
- (group-by #(cto/token-type->dtcg-token-type (:type %)))
- (into [])
- (mapv (fn [[type tokens]]
- #js [(name type)
- (->> tokens
- (map #(token-proxy plugin-id file-id id (:id %)))
- (apply array))]))
- (apply array))))}
+ :addToken
+ {:enumerable false
+ :schema (fn [args]
+ (let [tokens-tree (-> (u/locate-tokens-lib file-id)
+ (ctob/get-tokens id)
+ ;; Convert to the adecuate format for schema
+ (ctob/tokens-tree))]
+ [:tuple (-> (cfo/make-token-schema
+ tokens-tree
+ (cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
+ ;; Don't allow plugins to set the id
+ (sm/dissoc-key :id)
+ ;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
+ ;; and set a converter that changes DTCG types to internal types (:decode/json).
+ ;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
+ (sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]))
+ :decode/options {:key-fn identity}
+ :fn (fn [attrs]
+ (let [tokens-lib (u/locate-tokens-lib file-id)
+ token (ctob/make-token attrs)
+ tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib)
+ (assoc (:name token) token))
+ resolved-tokens (ts/resolve-tokens tokens-tree)
- :getTokenById
- {:enumerable false
- :schema [:tuple ::sm/uuid]
- :fn (fn [token-id]
- (let [token (u/locate-token file-id id token-id)]
- (when (some? token)
- (token-proxy plugin-id file-id id token-id))))}
+ {:keys [errors resolved-value] :as resolved-token}
+ (get resolved-tokens (:name token))]
- :addToken
- {:enumerable false
- :schema (fn [args]
- (let [tokens-tree (-> (u/locate-tokens-lib file-id)
- (ctob/get-tokens id)
- ;; Convert to the adecuate format for schema
- (ctob/tokens-tree))]
- [:tuple (-> (cfo/make-token-schema
- tokens-tree
- (cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
- ;; Don't allow plugins to set the id
- (sm/dissoc-key :id)
- ;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
- ;; and set a converter that changes DTCG types to internal types (:decode/json).
- ;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
- (sm/update-properties assoc :decode/json cfo/convert-dtcg-token))]))
- :decode/options {:key-fn identity}
- :fn (fn [attrs]
- (let [tokens-lib (u/locate-tokens-lib file-id)
- token (ctob/make-token attrs)
- tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib)
- (assoc (:name token) token))
- resolved-tokens (ts/resolve-tokens tokens-tree)
+ (if resolved-value
+ (do (st/emit! (dwtl/create-token id token))
+ (token-proxy plugin-id file-id id (:id token)))
+ (do (u/not-valid plugin-id :addToken (str errors))
+ nil))))}
- {:keys [errors resolved-value] :as resolved-token}
- (get resolved-tokens (:name token))]
+ :duplicate
+ (fn []
+ (st/emit! (dwtl/duplicate-token-set id)))
- (if resolved-value
- (do (st/emit! (dwtl/create-token id token))
- (token-proxy plugin-id file-id id (:id token)))
- (do (u/not-valid plugin-id :addToken (str errors))
- nil))))}
-
- :duplicate
- (fn []
- (st/emit! (dwtl/duplicate-token-set id)))
-
- :remove
- (fn []
- (st/emit! (dwtl/delete-token-set id)))))
+ :remove
+ (fn []
+ (st/emit! (dwtl/delete-token-set id))))))
(defn token-theme-proxy? [p]
(obj/type-of? p "TokenThemeProxy"))
@@ -423,15 +430,26 @@
{:enumerable false
:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set]
- (let [theme (u/locate-token-theme file-id id)]
- (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme (obj/get token-set :name))))))}
+ ;; Resolve the set name before the theme lookup. The proxy's :name
+ ;; getter now falls back to `initial-name` when state hasn't
+ ;; propagated, so this is safe even for freshly created sets.
+ ;; Guard against nil to prevent `enable-set` from conj'ing nil
+ ;; into the theme's :sets — which would send `:sets #{nil}` to the
+ ;; backend and crash the workspace.
+ (let [set-name (obj/get token-set :name)
+ theme (u/locate-token-theme file-id id)]
+ (when (and (some? set-name) (some? theme))
+ (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))}
:removeSet
{:enumerable false
:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set]
- (let [theme (u/locate-token-theme file-id id)]
- (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme (obj/get token-set :name))))))}
+ ;; Same nil guard as addSet — see comment above.
+ (let [set-name (obj/get token-set :name)
+ theme (u/locate-token-theme file-id id)]
+ (when (and (some? set-name) (some? theme))
+ (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))}
:duplicate
(fn []
@@ -499,7 +517,10 @@
(let [attrs (update attrs :name ctob/normalize-set-name)
set (ctob/make-token-set attrs)]
(st/emit! (dwtl/create-token-set set))
- (token-set-proxy plugin-id file-id (ctob/get-id set))))}
+ ;; Pass the set name as `initial-name` so the proxy can resolve
+ ;; it immediately, before the async `st/emit!` above propagates
+ ;; the new set into `@st/state`.
+ (token-set-proxy plugin-id file-id (ctob/get-id set) (ctob/get-name set))))}
:getThemeById
{:enumerable false
diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs
index 19de73fcde..81dfb6cb82 100644
--- a/frontend/src/app/plugins/utils.cljs
+++ b/frontend/src/app/plugins/utils.cljs
@@ -100,6 +100,17 @@
root (ctn/get-instance-root objects shape)]
[root (ctf/resolve-component root file libraries {:include-deleted? true})]))
+(defn locate-head-component
+ "Like locate-component but resolves via the nearest component head
+ instead of the outermost instance root."
+ [objects shape]
+ (let [state (deref st/state)
+ file (dsh/lookup-file state)
+ libraries (dsh/lookup-libraries state)
+ head (ctn/get-head-shape objects shape)]
+ (when head
+ [head (ctf/resolve-component head file libraries {:include-deleted? true})])))
+
(defn proxy->file
[proxy]
(let [id (obj/get proxy "$id")]
diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs
index e42afc9e25..737131d243 100644
--- a/frontend/src/app/render_wasm/api.cljs
+++ b/frontend/src/app/render_wasm/api.cljs
@@ -656,47 +656,49 @@
[shape-id strokes thumbnail?]
(h/call wasm/internal-module "_clear_shape_strokes")
(keep (fn [stroke]
- (let [opacity (or (:stroke-opacity stroke) 1.0)
- color (:stroke-color stroke)
- gradient (:stroke-color-gradient stroke)
- image (:stroke-image stroke)
- width (:stroke-width stroke)
- align (:stroke-alignment stroke)
- style (-> stroke :stroke-style sr/translate-stroke-style)
- cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap)
- cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)
- offset (mem/alloc types.fills.impl/FILL-U8-SIZE)
- heap (mem/get-heap-u8)
- dview (js/DataView. (.-buffer heap))]
- (case align
- :inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end)
- :outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end)
- (h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end))
+ (when-not (:hidden stroke)
+ (let [opacity (or (:stroke-opacity stroke) 1.0)
+ color (:stroke-color stroke)
+ gradient (:stroke-color-gradient stroke)
+ image (:stroke-image stroke)
+ width (:stroke-width stroke)
+ align (:stroke-alignment stroke)
+ style (-> stroke :stroke-style sr/translate-stroke-style)
+ cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap)
+ cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)
+ offset (mem/alloc types.fills.impl/FILL-U8-SIZE)
+ heap (mem/get-heap-u8)
+ dview (js/DataView. (.-buffer heap))]
+ (case align
+ :inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end)
+ :outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end)
+ (h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end))
- (cond
- (some? gradient)
- (do
- (types.fills.impl/write-gradient-fill offset dview opacity gradient)
- (h/call wasm/internal-module "_add_shape_stroke_fill")
- nil)
+ (cond
+ (some? gradient)
+ (do
+ (types.fills.impl/write-gradient-fill offset dview opacity gradient)
+ (h/call wasm/internal-module "_add_shape_stroke_fill")
+ nil)
- (some? image)
- (let [image-id (get image :id)
- buffer (uuid/get-u32 image-id)
- cached-image? (h/call wasm/internal-module "_is_image_cached"
- (aget buffer 0) (aget buffer 1)
- (aget buffer 2) (aget buffer 3)
- thumbnail?)]
- (types.fills.impl/write-image-fill offset dview opacity image)
- (h/call wasm/internal-module "_add_shape_stroke_fill")
- (when (== cached-image? 0)
- (fetch-image shape-id image-id thumbnail?)))
+ (some? image)
+ (let [image-id (get image :id)
+ buffer (uuid/get-u32 image-id)
+ cached-image? (h/call wasm/internal-module "_is_image_cached"
+ (aget buffer 0) (aget buffer 1)
+ (aget buffer 2) (aget buffer 3)
+ thumbnail?)]
+ (types.fills.impl/write-image-fill offset dview opacity image)
+ (h/call wasm/internal-module "_add_shape_stroke_fill")
+ (when (== cached-image? 0)
+ (fetch-image shape-id image-id thumbnail?)))
+
+ (some? color)
+ (do
+ (types.fills.impl/write-solid-fill offset dview opacity color)
+ (h/call wasm/internal-module "_add_shape_stroke_fill")
+ nil)))))
- (some? color)
- (do
- (types.fills.impl/write-solid-fill offset dview opacity color)
- (h/call wasm/internal-module "_add_shape_stroke_fill")
- nil))))
strokes))
(defn set-shape-svg-attrs
diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs
index 5f18798847..d06aa5c22e 100644
--- a/frontend/src/app/util/clipboard.cljs
+++ b/frontend/src/app/util/clipboard.cljs
@@ -88,3 +88,22 @@
(let [clipboard (unchecked-get js/navigator "clipboard")
data (create-clipboard-item mimetype promise)]
(.write ^js clipboard #js [data])))
+
+(defn to-clipboard-multi
+ "Write multiple MIME representations as a single ClipboardItem.
+ `items` is a map of mime-type (string) -> string payload.
+ Falls back to `writeText` when the async Clipboard API is unavailable."
+ [items]
+ (let [clipboard (unchecked-get js/navigator "clipboard")]
+ (if (and clipboard (unchecked-get clipboard "write"))
+ (let [obj (reduce-kv
+ (fn [acc mime payload]
+ (let [blob (js/Blob. #js [payload] #js {:type mime})]
+ (unchecked-set acc mime (js/Promise.resolve blob))
+ acc))
+ #js {} items)
+ item (js/ClipboardItem. obj)]
+ (.write ^js clipboard #js [item]))
+ (when-let [text (or (get items "text/plain")
+ (first (vals items)))]
+ (.writeText ^js clipboard text)))))
diff --git a/frontend/src/app/util/clipboard.js b/frontend/src/app/util/clipboard.js
index 294652666a..43bd636b88 100644
--- a/frontend/src/app/util/clipboard.js
+++ b/frontend/src/app/util/clipboard.js
@@ -24,6 +24,13 @@ const exclusiveTypes = [
"text/plain"
];
+const svgTextPattern =
+ /^(\s*<\?xml[^?]*\?>\s*)?(\s*\s*)*