Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-10-09 09:01:12 +02:00
commit 473a59b134
125 changed files with 5509 additions and 5350 deletions

View File

@ -99,13 +99,18 @@ query is needed.
**Breaking changes override everything:** an issue with the `breaking change`
label goes under `### :boom: Breaking changes & Deprecations`, no matter its
issue type. This is the only label-based rule — it is an explicit exception
to the "never by labels" principle above. The `:boom:` subsection goes first
in the version, before `:rocket:` (matching the existing precedent).
issue type. This and `release highlight` below are the only label-based
rules — explicit exceptions to the "never by labels" principle above. The
`:boom:` subsection goes first in the version, before `:rocket:` (matching
the existing precedent).
**Preserve highlighted entries:** if an entry already sits under
`### :rocket: Epics and highlights`, keep it there when refreshing. Do not
move it down just because its type would place it under `:sparkles:`.
**Highlights come from the `release highlight` label:** an issue with that
label also gets an entry under `### :rocket: Epics and highlights`, on top of
its regular entry (usually `:sparkles:`). In the section being refreshed,
`:rocket:` lists exactly the labelled issues: never pick highlights yourself
and never keep an unlabelled entry there. Product decides the highlights by
labelling issues on GitHub, so a regeneration cannot undo that choice. Older
version sections are left as they are.
**Community attribution:** if the issue or its fix PR has the
`community contribution` label, add `(by @<github_username>)` on the entry
@ -207,7 +212,10 @@ not trust cached data):
6. **Issue type changed to `Task`** → remove the entry.
7. **Breaking change misplaced** (issue has the `breaking change` label but
sits in another section) → move the entry to `:boom:`.
8. **Missing valid issues** (closed, non-excluded, unreferenced anywhere) →
8. **Highlight out of sync** (`:rocket:` entry without the `release
highlight` label, or a labelled issue missing from `:rocket:`) → remove
the `:rocket:` entry or add it (step 7b). The regular entry stays.
9. **Missing valid issues** (closed, non-excluded, unreferenced anywhere) →
add them to the current section per step 5.
### 7. Build the description text
@ -225,14 +233,22 @@ behavior:
Insert the new version section right after the `# CHANGELOG` header with the
`edit` tool and enough context for a unique match.
### 7b. Propose and populate `:rocket: Epics and highlights`
### 7b. Populate `:rocket: Epics and highlights` from the label
Create the subsection if missing (place it before `### :sparkles:`) and pick
2–5 of the most impactful/user-visible `:sparkles:` entries: new visible
features, big capabilities, items that make self-hosted users want to update.
`frontend/src/app/main/ui/releases/v2_<MINOR>.cljs` slide titles are optional
hints (they may not exist for every version). Every `:rocket:` entry MUST
carry issue AND PR references; never remove entries from a prior run.
List the milestone issues that pass step 2 exclusions and carry the
`release highlight` label:
```bash
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" \
| python3 -c "import sys,json; [print(i['number'], i['title']) for i in json.load(sys.stdin) if 'release highlight' in i['labels']]"
```
Create the subsection if missing (place it before `### :sparkles:`) with one
entry per labelled issue, same text and references as its regular entry.
Every `:rocket:` entry MUST carry issue AND PR references. If no issue is
labelled, leave `:rocket:` out and tell the user: the highlights are
product's call, not the agent's. To change them, add or remove the label on
GitHub and re-run; never edit `:rocket:` by hand.
### 8. Cross-reference milestone PRs against the changelog
@ -264,6 +280,8 @@ pairing is misleading and a human must judge intent):
3. `:boom:` entry whose issue lacks the `breaking change` label. These stay
listed in the report **and** in the changelog: either label the issue or
move the entry to its regular section.
4. `:rocket:` out of sync with the `release highlight` label. Step 6
pre-flight should have fixed it: if it shows up, re-run the workflow.
**Highlight gaps are warnings, not anomalies:** missing `:rocket:` on a
released X.Y.0 (patches never carry highlights); `:rocket:` entry without
@ -277,7 +295,7 @@ pre-flight. If one shows up in the report, re-run the workflow.
- **Issue = changelog unit**, PR = implementation detail inline.
- **Latest version first**, below the `# CHANGELOG` header.
- **Issue Type decides the section — exclusively**, except `breaking change`
label → `:boom:` first.
label → `:boom:` first, and `release highlight` label → also `:rocket:`.
- **User-facing descriptions** in imperative mood.
- **Community attribution** uses the PR author, placed before the issue link.
- **Only closed issues**; **Rejected** project status excludes.

View File

@ -6,7 +6,7 @@ JVM `clojure.test` (kaocha runner) under `backend/test/backend_tests/`.
- All CLI commands must be executed from the `backend/` subdirectory.
- Tests are invoked directly via `clojure -M:dev:test` (kaocha) — there is no pnpm wrapper. Kaocha auto-discovers test namespaces, so no runner registration is needed.
- **Preferred way to run them: `scripts/ci <module> --test`**, which runs lint, tests and format per module and writes a log per task under `.ci-logs/`. It exports `PENPOT_FLAGS` with `enable-backend-asserts` appended, so use it rather than calling `clojure -M:dev:test` by hand.
- **Why the flag matters:** `clojure.core/assert` is elided at compile time when `*assert*` is false, and `app.config` only sets that var from the flags when it loads. The test fixture redefines the flags after that, so they never reach it: without the flag the suite runs with every assertion compiled out, and a test that depends on an assert only means something with it set. GitHub Actions sets it too. When running the suite directly, the command is `PENPOT_FLAGS="enable-backend-asserts" clojure -M:dev:test`.
- **Why the flag matters:** `clojure.core/assert` is elided at compile time when `*assert*` is false, and `app.config` only sets that var from the flags when it loads. The test fixture redefines the flags after that, so they never reach it: without the flag the suite runs with every assertion compiled out, and a test that depends on an assert only means something with it set. `scripts/ci` and `tests-backend.yml` export the flag (GitHub Actions sets it too). When running the suite directly — including a `--focus` run — the command is `PENPOT_FLAGS="enable-backend-asserts" clojure -M:dev:test`.
- Coverage: if code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
- Isolated run: `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace, or `clojure -M:dev:test --focus backend-tests.my-ns-test/my-test-var` for a specific test var.
- Multiple focus symbols are supported by REPEATING the flag (kaocha accumulates them, last-wins does not apply): `clojure -M:dev:test --focus backend-tests.ns-one --focus backend-tests.ns-two` — preferred over a shell loop of isolated runs.

View File

@ -61,6 +61,9 @@ Flags:
- `mcp` has no lint task (shows as skipped). `render-wasm` uses `./lint`,
`./test`, and `cargo fmt`.
- Exports `PENPOT_FLAGS` with `enable-backend-asserts` appended to any
existing value, so backend test runs compile in and exercise
precondition asserts; see `mem:backend/testing`.
- Test tasks are long-running (backend: `clojure -M:dev:test`); use a
generous timeout when calling it from an agent shell.
- Skill entry point: `.agents/skills/local-ci/SKILL.md`.

View File

@ -6,12 +6,12 @@
- Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237))
- Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807))
- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958))
- Add individual stroke widths per side (top, right, bottom, left) [#9854](https://github.com/penpot/penpot/issues/9854) (PR: [#10648](https://github.com/penpot/penpot/pull/10648))
- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334) (PR: [#10293](https://github.com/penpot/penpot/pull/10293))
### :bug: Bugs fixed
- Fix copying text from Penpot to the clipboard not working on MS Windows [#11303](https://github.com/penpot/penpot/issues/11303) (PR: [#11305](https://github.com/penpot/penpot/pull/11305))
- Fix performance issue with WebGL render [#11240](https://github.com/penpot/penpot/issues/11240) (PR: [#11259](https://github.com/penpot/penpot/pull/11259))
- Fix comment bubbles rendering on top of workspace dropdown menus [#10283](https://github.com/penpot/penpot/issues/10283) (PR: [#11201](https://github.com/penpot/penpot/pull/11201))
- Fix inconsistent Mixed label in blur options and numeric inputs across 24 locales (by @filipsajdak) [#11148](https://github.com/penpot/penpot/issues/11148) (PR: [#11151](https://github.com/penpot/penpot/pull/11151))
- Fix overlay shifting left when shown with top-center alignment in viewer prototype (by @filipsajdak) [#9048](https://github.com/penpot/penpot/issues/9048) (PR: [#10454](https://github.com/penpot/penpot/pull/10454))
@ -21,31 +21,105 @@
- Fix plugin postMessage channel broadcasting messages to all plugins without origin validation [#10968](https://github.com/penpot/penpot/issues/10968) (PR: [#10970](https://github.com/penpot/penpot/pull/10970))
- Fix MCP plugin page navigation while connected crashing the workspace (by @makesomethingshit) [#11001](https://github.com/penpot/penpot/issues/11001) (PR: [#11521](https://github.com/penpot/penpot/pull/11521))
- Fix shortcut search never matching on key combination, only on action label [#11003](https://github.com/penpot/penpot/issues/11003) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix Shift + special character key shortcut capturing the shifted character instead of the physical key [#11004](https://github.com/penpot/penpot/issues/11004) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix Shift + special character key shortcut capturing the shifted character instead of the physical key [#11004](https://github.com/penpot/penpot/issues/11004) (PR: [#11081](https://github.com/penpot/penpot/pull/11081), [#11669](https://github.com/penpot/penpot/pull/11669))
- Fix reassigning the "Paste" shortcut not updating the UI or taking effect in the workspace [#11005](https://github.com/penpot/penpot/issues/11005) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix font-size dropdown clipping multi-digit values in Firefox (by @0xTHAC0) [#11008](https://github.com/penpot/penpot/issues/11008) (PR: [#11162](https://github.com/penpot/penpot/pull/11162), [#11500](https://github.com/penpot/penpot/pull/11500))
- Fix exporting shortcuts producing an invalid "toggle-fullscreen" entry that breaks re-import [#11032](https://github.com/penpot/penpot/issues/11032) (PR: [#11081](https://github.com/penpot/penpot/pull/11081))
- Fix plugin API missing permission checks in tokens, shapes, variants, flows, layouts, and user identity [#11137](https://github.com/penpot/penpot/issues/11137) (PR: [#11139](https://github.com/penpot/penpot/pull/11139))
- Fix library summary Redis cache keys omitting the tenant [#11407](https://github.com/penpot/penpot/issues/11407) (PR: [#11408](https://github.com/penpot/penpot/pull/11408))
- Fix active theme name in the inspect tab displaying an id instead of the name [#11437](https://github.com/penpot/penpot/issues/11437) (PR: [#11439](https://github.com/penpot/penpot/pull/11439))
- Fix triple-click not selecting the full line in text editor v3 [#11483](https://github.com/penpot/penpot/issues/11483) (PR: [#11493](https://github.com/penpot/penpot/pull/11493))
- Fix pasted text losing formatting on last lines after resizing and adding new lines from the top [#11501](https://github.com/penpot/penpot/issues/11501) (PR: [#11503](https://github.com/penpot/penpot/pull/11503))
- Fix variant property dropdown appearing empty and throwing an internal error when the component has no sibling variants [#11524](https://github.com/penpot/penpot/issues/11524) (PR: [#11499](https://github.com/penpot/penpot/pull/11499))
- Fix invitation links not allowing registration when `disable-registration` is enabled [#5178](https://github.com/penpot/penpot/issues/5178) (PR: [#11912](https://github.com/penpot/penpot/pull/11912))
- Fix Windows build failures caused by path resolution and start command conflicts (by @0xTHAC0) [#8637](https://github.com/penpot/penpot/issues/8637) (PR: [#11197](https://github.com/penpot/penpot/pull/11197))
- Fix editing a path on an image, rectangle or ellipse corrupting the shape and failing backend validation [#10621](https://github.com/penpot/penpot/issues/10621) (PR: [#10807](https://github.com/penpot/penpot/pull/10807))
- Fix board content disappearing when dragging a board near another board [#10689](https://github.com/penpot/penpot/issues/10689) (PR: [#11620](https://github.com/penpot/penpot/pull/11620))
- Fix dashboard context menu being cut off for long lists, hiding the bottom options [#10724](https://github.com/penpot/penpot/issues/10724) (PR: [#11511](https://github.com/penpot/penpot/pull/11511))
- Fix moving an element changing its size [#10751](https://github.com/penpot/penpot/issues/10751) (PR: [#11560](https://github.com/penpot/penpot/pull/11560))
- Fix remote MCP websocket connecting but never receiving live browser commands [#10753](https://github.com/penpot/penpot/issues/10753)
- Fix stroke to path not working with path ends (by @ShreyashAgare26) [#10825](https://github.com/penpot/penpot/issues/10825) (PR: [#11614](https://github.com/penpot/penpot/pull/11614))
- Fix border style not working with stroke per side [#10841](https://github.com/penpot/penpot/issues/10841) (PR: [#11883](https://github.com/penpot/penpot/pull/11883))
- Fix multiple selection not showing stroke per side when all shapes share the same per side values [#10844](https://github.com/penpot/penpot/issues/10844) (PR: [#10913](https://github.com/penpot/penpot/pull/10913))
- Fix stroke width tokens not overriding stroke per side values [#10850](https://github.com/penpot/penpot/issues/10850) (PR: [#10913](https://github.com/penpot/penpot/pull/10913))
- Fix internal error when applying a dimension token to stroke width [#10918](https://github.com/penpot/penpot/issues/10918) (PR: [#10913](https://github.com/penpot/penpot/pull/10913))
- Fix stroke per side button detaching the applied token [#10920](https://github.com/penpot/penpot/issues/10920) (PR: [#10913](https://github.com/penpot/penpot/pull/10913))
- Fix WebSocket subscription handlers missing authorization checks [#11067](https://github.com/penpot/penpot/issues/11067) (PR: [#11069](https://github.com/penpot/penpot/pull/11069))
- Fix project title being truncated in the dashboard despite available space [#11306](https://github.com/penpot/penpot/issues/11306) (PR: [#11564](https://github.com/penpot/penpot/pull/11564), [#11675](https://github.com/penpot/penpot/pull/11675))
- Fix `rect` attribute NaN length error in the console when panning a file [#11327](https://github.com/penpot/penpot/issues/11327) (PR: [#11856](https://github.com/penpot/penpot/pull/11856))
- Fix unclipped and rotated shapes being cut on render tiles [#11401](https://github.com/penpot/penpot/issues/11401) (PR: [#11423](https://github.com/penpot/penpot/pull/11423))
- Fix Plugin API rejecting the documented `fontFamilies` applyToken property (by @makesomethingshit) [#11405](https://github.com/penpot/penpot/issues/11405) (PR: [#11566](https://github.com/penpot/penpot/pull/11566))
- Fix flattening text ignoring non-zero letter spacing [#11462](https://github.com/penpot/penpot/issues/11462) (PR: [#11555](https://github.com/penpot/penpot/pull/11555))
- Fix boolean results serialized as unordered subpaths, deforming them in the WebGL render and the exporter [#11482](https://github.com/penpot/penpot/issues/11482) (PR: [#11551](https://github.com/penpot/penpot/pull/11551))
- Fix MCP reconnecting endlessly in inactive tabs and eventually freezing the browser (by @breken-ai) [#11510](https://github.com/penpot/penpot/issues/11510) (PR: [#11520](https://github.com/penpot/penpot/pull/11520), [#11906](https://github.com/penpot/penpot/pull/11906))
- Fix dashed strokes losing their properties when changing color (by @Shlok1729) [#11549](https://github.com/penpot/penpot/issues/11549) (PR: [#11557](https://github.com/penpot/penpot/pull/11557))
- Fix text decoration not applying per text span in the WebGL render [#11582](https://github.com/penpot/penpot/issues/11582) (PR: [#11583](https://github.com/penpot/penpot/pull/11583))
- Fix binfile import exhausting backend memory when a ZIP JSON entry decompresses without limit [#11606](https://github.com/penpot/penpot/issues/11606) (PR: [#11607](https://github.com/penpot/penpot/pull/11607))
- Fix modal not closing when going to the plans page via "See my current plan" (by @makesomethingshit) [#11611](https://github.com/penpot/penpot/issues/11611) (PR: [#11615](https://github.com/penpot/penpot/pull/11615))
- Fix view-only share links disclosing unshared library pages when the file links a library [#11617](https://github.com/penpot/penpot/issues/11617) (PR: [#11627](https://github.com/penpot/penpot/pull/11627))
- Fix canvas tiles showing only the background color while dragging the canvas background color picker [#11642](https://github.com/penpot/penpot/issues/11642) (PR: [#11643](https://github.com/penpot/penpot/pull/11643))
- Fix title spacing and "added" pill padding in the libraries modal [#11645](https://github.com/penpot/penpot/issues/11645) (PR: [#11650](https://github.com/penpot/penpot/pull/11650))
- Fix workspace crash when pasting before the page finishes loading [#11666](https://github.com/penpot/penpot/issues/11666) (PR: [#11674](https://github.com/penpot/penpot/pull/11674))
- Fix tap area not matching the visible screen when zoomed out in the WebGL render (by @makesomethingshit) [#11689](https://github.com/penpot/penpot/issues/11689) (PR: [#11793](https://github.com/penpot/penpot/pull/11793))
- Fix app crash when logging receives an empty level, and add the missing fatal level [#11690](https://github.com/penpot/penpot/issues/11690) (PR: [#11693](https://github.com/penpot/penpot/pull/11693))
- Fix WebGL render ignoring drop and inner shadows on masked groups [#11697](https://github.com/penpot/penpot/issues/11697) (PR: [#11754](https://github.com/penpot/penpot/pull/11754))
- Fix curves being created while Ctrl+dragging a path segment [#11702](https://github.com/penpot/penpot/issues/11702) (PR: [#11715](https://github.com/penpot/penpot/pull/11715))
- Fix viewer role showing the search and replace option [#11706](https://github.com/penpot/penpot/issues/11706) (PR: [#11713](https://github.com/penpot/penpot/pull/11713))
- Fix toggling the visibility of a child in a flex layout main component deleting the prototype interactions of its copies (by @ShreyashAgare26) [#11746](https://github.com/penpot/penpot/issues/11746) (PR: [#11788](https://github.com/penpot/penpot/pull/11788))
- Fix cursor not visible on page load in the WebGL render [#11769](https://github.com/penpot/penpot/issues/11769) (PR: [#11770](https://github.com/penpot/penpot/pull/11770))
- Fix library text styles detaching when copying and pasting an element [#11776](https://github.com/penpot/penpot/issues/11776) (PR: [#11785](https://github.com/penpot/penpot/pull/11785))
- Fix Plugin API crashing the workspace when a shapechange listener is registered with an empty shape id [#11786](https://github.com/penpot/penpot/issues/11786) (PR: [#11787](https://github.com/penpot/penpot/pull/11787))
- Fix borders not being fully rendered [#11805](https://github.com/penpot/penpot/issues/11805) (PR: [#11807](https://github.com/penpot/penpot/pull/11807))
- Fix design token controls being enabled for fills and strokes below the first position [#11819](https://github.com/penpot/penpot/issues/11819) (PR: [#10913](https://github.com/penpot/penpot/pull/10913))
- Fix viewer role showing the open libraries modal button [#11824](https://github.com/penpot/penpot/issues/11824) (PR: [#11849](https://github.com/penpot/penpot/pull/11849))
- Fix extra padding on the "Sets" title in the tokens panel [#11826](https://github.com/penpot/penpot/issues/11826) (PR: [#11849](https://github.com/penpot/penpot/pull/11849))
- Fix thumbnails failing on older Chromium browsers and each thumbnail rendering several times [#11878](https://github.com/penpot/penpot/issues/11878) (PR: [#11879](https://github.com/penpot/penpot/pull/11879))
- Fix internal error when opening the libraries modal [#11943](https://github.com/penpot/penpot/issues/11943) (PR: [#11946](https://github.com/penpot/penpot/pull/11946))
- Fix Plugin API `connectLibrary` linking unpublished files as libraries [#12001](https://github.com/penpot/penpot/issues/12001) (PR: [#12002](https://github.com/penpot/penpot/pull/12002))
### :sparkles: New features & Enhancements
- Make backend storage resilient to interrupted writes, missing files and stalled cleanup [#11344](https://github.com/penpot/penpot/issues/11344) (PR: [#11345](https://github.com/penpot/penpot/pull/11345))
- Implement RTL support in the text editor v3 [#11262](https://github.com/penpot/penpot/issues/11262)
- Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807))
- Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237))
- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958))
- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334)
- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334) (PR: [#10293](https://github.com/penpot/penpot/pull/10293))
- Warn self-hosted users when their Penpot version is outdated and surface what they're missing [#10497](https://github.com/penpot/penpot/issues/10497) (PR: [#11411](https://github.com/penpot/penpot/pull/11411))
- Add dedicated RPC methods for plugin registry operations with permission validation [#10952](https://github.com/penpot/penpot/issues/10952) (PR: [#10957](https://github.com/penpot/penpot/pull/10957))
- Document MCP and internal resolver environment variables (by @ShreyashAgare26) [#11318](https://github.com/penpot/penpot/issues/11318) (PR: [#11572](https://github.com/penpot/penpot/pull/11572))
- Add tokens source indicator to assets tab [#11365](https://github.com/penpot/penpot/issues/11365) (PR: [#11439](https://github.com/penpot/penpot/pull/11439))
- Export multiple fills to SVG [#11466](https://github.com/penpot/penpot/issues/11466) (PR: [#11467](https://github.com/penpot/penpot/pull/11467))
- Add Penpot-specific board size presets (file thumbnail, template cover, plugin icon/cover) [#11561](https://github.com/penpot/penpot/issues/11561) (PR: [#11565](https://github.com/penpot/penpot/pull/11565))
- Apply tokens to stroke per side [#9842](https://github.com/penpot/penpot/issues/9842) (PR: [#10913](https://github.com/penpot/penpot/pull/10913))
- Add individual stroke widths per side (top, right, bottom, left) [#9854](https://github.com/penpot/penpot/issues/9854) (PR: [#10648](https://github.com/penpot/penpot/pull/10648))
- Expand the token tree right after importing it (by @AKnassa) [#9819](https://github.com/penpot/penpot/issues/9819) (PR: [#10664](https://github.com/penpot/penpot/pull/10664))
- Update the selection size badge live during resize, replacing the resize tooltip [#10259](https://github.com/penpot/penpot/issues/10259)
- Add color variants, rotation correction, viewport flip and hide-when-too-small to the selection size badge [#10264](https://github.com/penpot/penpot/issues/10264) (PR: [#11884](https://github.com/penpot/penpot/pull/11884))
- Support single-file SVG export with the WebGL render [#10546](https://github.com/penpot/penpot/issues/10546)
- Persist the hide resolved comments preference and separate the filter options in the comments menu [#10686](https://github.com/penpot/penpot/issues/10686) (PR: [#10694](https://github.com/penpot/penpot/pull/10694))
- Add rich link previews (Open Graph) for shared Penpot links [#10699](https://github.com/penpot/penpot/issues/10699) (PR: [#10700](https://github.com/penpot/penpot/pull/10700))
- Add 3x export scale option [#10740](https://github.com/penpot/penpot/issues/10740) (PR: [#11598](https://github.com/penpot/penpot/pull/11598))
- Show a validation message in the input instead of a toast when the email is already in use (by @ShreyashAgare26) [#10890](https://github.com/penpot/penpot/issues/10890) (PR: [#11647](https://github.com/penpot/penpot/pull/11647))
- Take `waitForLayoutUpdate` into account in MCP instructions and implementation [#10901](https://github.com/penpot/penpot/issues/10901) (PR: [#11433](https://github.com/penpot/penpot/pull/11433))
- Add a standard context menu (Cut, Copy, Paste, Select all) on right-click when editing text [#10914](https://github.com/penpot/penpot/issues/10914) (PR: [#11677](https://github.com/penpot/penpot/pull/11677))
- Process export requests as export jobs [#11133](https://github.com/penpot/penpot/issues/11133) (PR: [#11296](https://github.com/penpot/penpot/pull/11296))
- Enable closed schemas for RPC method parameters [#11135](https://github.com/penpot/penpot/issues/11135) (PR: [#11136](https://github.com/penpot/penpot/pull/11136))
- Add Content-Security-Policy and Strict-Transport-Security headers to the frontend image [#11374](https://github.com/penpot/penpot/issues/11374) (PR: [#11375](https://github.com/penpot/penpot/pull/11375))
- Send a notification email when the password is changed or set [#11392](https://github.com/penpot/penpot/issues/11392) (PR: [#11393](https://github.com/penpot/penpot/pull/11393))
- Lock accounts after repeated failed login attempts [#11397](https://github.com/penpot/penpot/issues/11397) (PR: [#11402](https://github.com/penpot/penpot/pull/11402), [#11895](https://github.com/penpot/penpot/pull/11895))
- Enforce an absolute maximum session duration [#11444](https://github.com/penpot/penpot/issues/11444) (PR: [#11447](https://github.com/penpot/penpot/pull/11447))
- Add a loading message to the libraries section [#11452](https://github.com/penpot/penpot/issues/11452) (PR: [#11594](https://github.com/penpot/penpot/pull/11594))
- Resize image bounds independently from the image content on the canvas [#11516](https://github.com/penpot/penpot/issues/11516) (PR: [#11590](https://github.com/penpot/penpot/pull/11590))
- Limit binfile import database connection time and stop rescanning ZIP entries [#11579](https://github.com/penpot/penpot/issues/11579) (PR: [#11584](https://github.com/penpot/penpot/pull/11584))
- Apply the same validation rules in the Plugin API as in the UI [#11586](https://github.com/penpot/penpot/issues/11586) (PR: [#11587](https://github.com/penpot/penpot/pull/11587))
- Fetch only the caller's share link in the view-only bundle (by @makesomethingshit) [#11633](https://github.com/penpot/penpot/issues/11633) (PR: [#11657](https://github.com/penpot/penpot/pull/11657))
- Add Prometheus metrics for backend storage operations [#11676](https://github.com/penpot/penpot/issues/11676) (PR: [#11700](https://github.com/penpot/penpot/pull/11700))
- Make "Fix when scrolling" work in the WebGL render [#11684](https://github.com/penpot/penpot/issues/11684) (PR: [#11752](https://github.com/penpot/penpot/pull/11752))
- Complete Catalan translations [#11739](https://github.com/penpot/penpot/issues/11739) (PR: [#11741](https://github.com/penpot/penpot/pull/11741))
- Remove the optional client-provided id from creation RPC endpoints [#11783](https://github.com/penpot/penpot/issues/11783) (PR: [#11784](https://github.com/penpot/penpot/pull/11784))
- Allow color tokens for the canvas background [#11791](https://github.com/penpot/penpot/issues/11791) (PR: [#11923](https://github.com/penpot/penpot/pull/11923))
- Show a "no assets yet" message for empty files in the libraries modal [#11811](https://github.com/penpot/penpot/issues/11811) (PR: [#11818](https://github.com/penpot/penpot/pull/11818))
- Upgrade the MCP SDK to v2 and adopt the new protocol version [#11827](https://github.com/penpot/penpot/issues/11827) (PR: [#11841](https://github.com/penpot/penpot/pull/11841))
- Remove legacy SSE support from the MCP server [#11846](https://github.com/penpot/penpot/issues/11846) (PR: [#11841](https://github.com/penpot/penpot/pull/11841))
- Delete selected path handles with Backspace or Delete [#11858](https://github.com/penpot/penpot/issues/11858) (PR: [#11896](https://github.com/penpot/penpot/pull/11896))
- Use the tooltip component in the token pill info [#11886](https://github.com/penpot/penpot/issues/11886) (PR: [#11900](https://github.com/penpot/penpot/pull/11900))
- Hide the duplicate cursor when alt-dragging a shape that cannot be duplicated (by @0xTHAC0) [#11905](https://github.com/penpot/penpot/issues/11905) (PR: [#11195](https://github.com/penpot/penpot/pull/11195))
## 2.18.3

View File

@ -6,7 +6,7 @@
org.clojure/clojure {:mvn/version "1.12.6"}
org.clojure/tools.namespace {:mvn/version "1.5.1"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-20"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-21"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@ -37,7 +37,7 @@
{:mvn/version "1.3.1118"}
metosin/reitit-core {:mvn/version "0.11.0"}
nrepl/nrepl {:mvn/version "1.7.0"}
nrepl/nrepl {:mvn/version "1.8.0"}
org.postgresql/postgresql {:mvn/version "42.7.13"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.4.0"}
@ -50,7 +50,7 @@
buddy/buddy-sign {:mvn/version "3.6.1-359"}
org.passay/passay {:mvn/version "2.0.0"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.3.0"}
org.jsoup/jsoup {:mvn/version "1.23.2"}
@ -64,10 +64,10 @@
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.55.6"}
software.amazon.awssdk/sts {:mvn/version "2.55.6"}
software.amazon.awssdk/s3 {:mvn/version "2.55.11"}
software.amazon.awssdk/sts {:mvn/version "2.55.11"}
com.ladybugdb/lbug {:mvn/version "0.20.4"}
com.ladybugdb/lbug {:mvn/version "0.21.2"}
;; Required by Arrow RootAllocator (lbug only pulls arrow-memory-core).
org.apache.arrow/arrow-memory-netty {:mvn/version "19.0.0"}}

View File

@ -257,6 +257,7 @@
(assoc ::bfc/project-id (:default-project-id profile))
(assoc ::bfc/team-id (:default-team-id profile))
(assoc ::bfc/profile-id (:id profile))
(assoc ::bfc/team-id (:default-team-id profile))
(assoc ::bfc/input evil)
(v3/import-files!))]
(t/is (= 1 (count (:file-ids result))))

View File

@ -35,6 +35,7 @@
[app.common.types.shape.shadow :as ctss]
[app.common.types.shape.text :as ctst]
[app.common.types.text :as types.text]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
@ -2151,6 +2152,33 @@
(update :pages-index d/update-vals repair-container)
(d/update-when :components d/update-vals repair-container))))
(defmethod migrate-data "0031-migrate-stroke-width-token-attr"
;; Stroke width tokens used to be applied to the single `:stroke-width` token
;; attribute, replaced by the four per-side attributes. The editor reads only
;; the new keys, so a file written before the change keeps the token on
;; `:stroke-width` and the token looks unapplied even though its value is
;; still set. Copy the legacy reference to every side (matching how applying
;; the token resolves all sides) and drop the old key. A side that already
;; carries a reference wins over the recycled global one.
[data _]
(letfn [(repair-shape [shape]
(let [applied (:applied-tokens shape)
token (:stroke-width applied)]
(if (some? token)
(update shape :applied-tokens
(fn [applied]
(-> (zipmap ctt/per-side-stroke-width-keys (repeat token))
(merge applied)
(dissoc :stroke-width))))
shape)))
(repair-container [container]
(d/update-when container :objects d/update-vals repair-shape))]
(-> data
(update :pages-index d/update-vals repair-container)
(d/update-when :components d/update-vals repair-container))))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@ -2238,4 +2266,5 @@
"0027-separate-tokens-status"
"0028-normalize-constrained-values"
"0029-move-background-blur-out-of-blur"
"0030-remove-stroke-per-side-attr"]))
"0030-remove-stroke-per-side-attr"
"0031-migrate-stroke-width-token-attr"]))

View File

@ -460,10 +460,13 @@
:active-set-ids active-set-ids)))
(defn ensure-tokens-lib
"Ensure file-data has a :tokens-lib or :tokens-source, and also a :tokens-status, creating them if necessary."
"Ensure file-data has a :tokens-lib or an external :tokens-source, and also a :tokens-status,
creating them if necessary."
[file-data]
(cond-> file-data
(nil? (:tokens-source file-data))
(let [tokens-source (:tokens-source file-data)]
(or (nil? tokens-source)
(= tokens-source (:id file-data))))
(update :tokens-lib #(or % (ctob/make-tokens-lib)))
:always
@ -488,9 +491,12 @@
(or (:tokens-source file-data) (:id file-data)))
(defn set-tokens-source
"Set the tokens source of the file. Setting it to nil or to the file itself
removes the attribute, so the file uses its own tokens-lib."
[file-data tokens-source]
(assert (or (nil? tokens-source) (uuid? tokens-source)) "expected nil or valid uuid")
(if (nil? tokens-source)
(if (or (nil? tokens-source)
(= tokens-source (:id file-data)))
(dissoc file-data :tokens-source)
(assoc file-data :tokens-source tokens-source)))

View File

@ -25,11 +25,8 @@
:stroke-width 1})
(defn materialize-stroke-side-widths
"Given a stroke (or nil) and the set of edited side keys, returns the stroke
with the four per-side width keys concretized: edited sides take `value`,
the rest keep their current per-side width, falling back to `:stroke-width`
(0 when `stroke` is nil). `:stroke-width` mirrors the top side so legacy
consumers that only read it see the top value."
"Sets edited widths and fills missing sides from `:stroke-width` (default 0).
Keeps `:stroke-width` equal to the top width."
[stroke edited-keys value]
(let [base-width (or (:stroke-width stroke) 0)
current (or stroke default-stroke)
@ -42,3 +39,29 @@
{}
ctt/per-side-stroke-width-keys)]
(merge current side-attrs {:stroke-width (get side-attrs :stroke-width-top)})))
(defn set-width-to-all-sides
"Sets every side and `:stroke-width` to `value`."
[stroke value]
(materialize-stroke-side-widths stroke ctt/per-side-stroke-width-keys value))
(defn set-width-to-single-side
"Sets one side and preserves the other widths.
Keeps `:stroke-width` equal to the top width."
[stroke attr value]
(materialize-stroke-side-widths (or stroke default-stroke) #{attr} value))
(defn side-width
"Returns the side width, falling back to `:stroke-width` when unset."
[stroke attr]
(d/nilv (get stroke attr) (:stroke-width stroke)))
(defn width-type
"Returns simple when all effective side widths match, multiple otherwise."
[stroke]
(if (= (side-width stroke :stroke-width-top)
(side-width stroke :stroke-width-right)
(side-width stroke :stroke-width-bottom)
(side-width stroke :stroke-width-left))
:simple
:multiple))

View File

@ -254,6 +254,14 @@
(t/is (not (contains? file-data' :tokens-lib)))
(t/is (= source-id (:tokens-source file-data')))
(t/is (contains? file-data' :tokens-status))
(t/is (ctos/tokens-status? (:tokens-status file-data')))))
(t/testing "ensure-tokens-lib should add a tokens-lib if the tokens-source is the file itself"
(let [file (thf/sample-file :file1)
file-data (-> (ctf/file-data file)
(assoc :tokens-source (:id file)))
file-data' (cfo/ensure-tokens-lib file-data)]
(t/is (ctob/tokens-lib? (:tokens-lib file-data')))
(t/is (ctos/tokens-status? (:tokens-status file-data'))))))
(t/deftest test-get-tokens-lib
@ -293,7 +301,16 @@
file (thf/sample-file :file1)
file-data (:data file)
file-data' (cfo/set-tokens-source file-data source-id)]
(t/is (= source-id (:tokens-source file-data'))))))
(t/is (= source-id (:tokens-source file-data')))))
(t/testing "removes tokens-source when set to the file itself"
(let [source-id (thi/new-id! :tokens-source)
file (thf/sample-file :file1)
file-data (-> (ctf/file-data file)
(assoc :tokens-source source-id))
file-data' (cfo/set-tokens-source file-data (:id file))]
(t/is (not (contains? file-data' :tokens-source)))
(t/is (= (:id file) (cfo/get-effective-tokens-source file-data'))))))
(t/deftest test-effective-tokens-source?
(t/testing "no tokens, no tokens-source, ask for file id -> true"

View File

@ -8,8 +8,10 @@
(:require
[app.common.data :as d]
[app.common.files.migrations :as cfm]
[app.common.files.tokens :as cfo]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.types.token :as ctt]
[app.common.uuid :as uuid]
[clojure.test :as t]))
@ -459,3 +461,96 @@
(t/is (nil? (:stroke-per-side stroke'))
"obsolete attr removed before schema validation")
(t/is (= 5 (:stroke-width stroke')) "stroke width preserved")))
(t/deftest migration-0031-legacy-applied-token-is-not-recognized-before-migration
;; Reproduces #12016: the editor reads only the per-side token attributes, so
;; a shape carrying the old global `:stroke-width` reference looks unapplied.
(let [shape (-> (cts/setup-shape {:id (uuid/next) :type :rect})
(assoc :applied-tokens {:stroke-width "30"}))]
(t/is (not (cfo/shapes-token-applied? {:name "30"} [shape]
ctt/stroke-width-keys))
"a legacy applied token is invisible to the token lookup")))
(t/deftest migration-0031-recycles-global-stroke-width-token
(let [migration-id "0031-migrate-stroke-width-token-attr"
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"
:fill "red"}))
data {:pages-index {page-id {:objects {shape-id shape}}}}
shape' (get-in (cfm/migrate-data data migration-id)
[:pages-index page-id :objects shape-id])
applied' (:applied-tokens shape')]
(t/is (nil? (:stroke-width applied')) "legacy key removed")
(t/is (= "30" (:stroke-width-top applied')))
(t/is (= "30" (:stroke-width-right applied')))
(t/is (= "30" (:stroke-width-bottom applied')))
(t/is (= "30" (:stroke-width-left applied')))
(t/is (= "red" (:fill applied')) "unrelated tokens preserved")
(t/is (cfo/shapes-token-applied? {:name "30"} [shape']
ctt/stroke-width-keys)
"the token is recognized after migration")))
(t/deftest migration-0031-keeps-side-token-that-was-already-set
(let [migration-id "0031-migrate-stroke-width-token-attr"
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"
:stroke-width-top "10"}))
data {:pages-index {page-id {:objects {shape-id shape}}}}
applied' (get-in (cfm/migrate-data data migration-id)
[:pages-index page-id :objects shape-id :applied-tokens])]
(t/is (= "10" (:stroke-width-top applied'))
"an explicit side reference is not overwritten")
(t/is (= "30" (:stroke-width-right applied')))))
(t/deftest migration-0031-repairs-component-shapes
(let [migration-id "0031-migrate-stroke-width-token-attr"
component-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"}))
data {:components {component-id {:objects {shape-id shape}}}}
applied' (get-in (cfm/migrate-data data migration-id)
[:components component-id :objects shape-id :applied-tokens])]
(t/is (nil? (:stroke-width applied')) "legacy key removed")
(t/is (= "30" (:stroke-width-left applied'))
"component side reference created")))
(t/deftest migration-0031-leaves-shapes-without-the-legacy-token
(let [migration-id "0031-migrate-stroke-width-token-attr"
page-id (uuid/next)
shape-id (uuid/next)
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width-top "30"
:stroke-width-right "30"
:stroke-width-bottom "30"
:stroke-width-left "30"}))
data {:pages-index {page-id {:objects {shape-id shape}}}}]
(t/is (= data (cfm/migrate-data data migration-id))
"a shape without the legacy token is left untouched")))
(t/deftest migration-0031-runs-through-file-migration
(let [migration-id "0031-migrate-stroke-width-token-attr"
shape-id (uuid/next)
file (ctf/make-file {:name "Legacy stroke width token"})
page-id (first (get-in file [:data :pages]))
shape (-> (cts/setup-shape {:id shape-id :type :rect})
(assoc :applied-tokens {:stroke-width "30"}))
file (-> file
(assoc :migrations (disj cfm/available-migrations migration-id))
(assoc-in [:data :pages-index page-id :objects shape-id] shape))
file' (cfm/migrate-file file {})
applied' (get-in file' [:data :pages-index page-id :objects shape-id :applied-tokens])]
(t/is (cfm/need-migration? file) "new migration detected")
(t/is (not (cfm/need-migration? file')) "new migration recorded")
(t/is (contains? (:migrations file') migration-id) "migration id persisted")
(t/is (= "30" (:stroke-width-top applied'))
"legacy token applied to the sides")))

View File

@ -146,7 +146,7 @@
(t/is (= (ctos/get-active-set-ids file-tokens-status)
(ctos/get-active-set-ids undo-tokens-status)))))
(t/testing "if the library is the same file, the source is set to self and the status is the same one"
(t/testing "if the library is the same file, the source is removed and the status is the same one"
(let [set-id (uuid/next)
file (tht/sample-file-with-tokens
:file-id :file1
@ -166,7 +166,7 @@
undo-tokens-status (tht/get-tokens-status undo)]
;; Redo: source and status copied from library, replacing the previous status
(t/is (= (:id file) redo-tokens-source))
(t/is (nil? redo-tokens-source))
(t/is (= (ctos/get-active-theme-ids file-tokens-status)
(ctos/get-active-theme-ids redo-tokens-status)))
(t/is (= (ctos/get-active-set-ids file-tokens-status)

View File

@ -72,3 +72,101 @@
(t/is (= (:stroke-width-bottom stroke) 5))
(t/is (= (:stroke-width-left stroke) 5))
(t/is (= (:stroke-width stroke) 10)))))
;; --- set-width-to-all-sides
(t/deftest set-width-to-all-sides-uniform
(t/testing "all four sides take the value and the alias mirrors the top"
(let [stroke (cts/set-width-to-all-sides {:stroke-width 5 :stroke-color "#000000"} 8)]
(t/is (= 8 (:stroke-width-top stroke)))
(t/is (= 8 (:stroke-width-right stroke)))
(t/is (= 8 (:stroke-width-bottom stroke)))
(t/is (= 8 (:stroke-width-left stroke)))
(t/is (= 8 (:stroke-width stroke)))
(t/is (= "#000000" (:stroke-color stroke)))))
(t/testing "a nil stroke is materialized from the default stroke"
(let [stroke (cts/set-width-to-all-sides nil 3)]
(t/is (= 3 (:stroke-width stroke)))
(t/is (= 3 (:stroke-width-top stroke)))
(t/is (= 3 (:stroke-width-right stroke)))
(t/is (= :solid (:stroke-style stroke))))))
;; --- set-width-to-single-side
(t/deftest set-width-to-single-side-non-top
(t/testing "a non-top side only changes that side, keeping the alias on top"
(let [stroke (cts/set-width-to-single-side
{:stroke-width 5
:stroke-width-top 5
:stroke-width-right 5
:stroke-width-bottom 6
:stroke-width-left 7}
:stroke-width-right
9)]
(t/is (= 5 (:stroke-width-top stroke)))
(t/is (= 9 (:stroke-width-right stroke)))
(t/is (= 6 (:stroke-width-bottom stroke)))
(t/is (= 7 (:stroke-width-left stroke)))
(t/is (= 5 (:stroke-width stroke))))))
(t/deftest set-width-to-single-side-top
(t/testing "the top side also updates the :stroke-width alias"
(let [stroke (cts/set-width-to-single-side {:stroke-width 5} :stroke-width-top 9)]
(t/is (= 9 (:stroke-width-top stroke)))
(t/is (= 9 (:stroke-width stroke)))))
(t/testing "a nil stroke is materialized from the default stroke"
(let [stroke (cts/set-width-to-single-side nil :stroke-width-bottom 4)]
(t/is (= 4 (:stroke-width-bottom stroke)))
(t/is (= 1 (:stroke-width stroke)))
(t/is (= :solid (:stroke-style stroke))))))
;; --- side-width
(t/deftest side-width-falls-back-to-global
(t/testing "a side without its own value reads the uniform width"
(t/is (= 5 (cts/side-width {:stroke-width 5} :stroke-width-right)))
(t/is (= 5 (cts/side-width {:stroke-width 5 :stroke-width-left 4} :stroke-width-right))))
(t/testing "a side with its own value wins over the global width"
(t/is (= 9 (cts/side-width {:stroke-width 5 :stroke-width-right 9} :stroke-width-right))))
(t/testing "a nil stroke reads nil"
(t/is (nil? (cts/side-width nil :stroke-width-top)))))
(t/deftest width-setters-select-mode
(let [uniform (cts/set-width-to-all-sides {:stroke-color "#000000"} 1)
top (cts/set-width-to-single-side uniform :stroke-width-top 8)
equal (-> top
(cts/set-width-to-single-side :stroke-width-right 8)
(cts/set-width-to-single-side :stroke-width-bottom 8)
(cts/set-width-to-single-side :stroke-width-left 8))]
(t/is (= :simple (cts/width-type uniform)))
(t/is (= :multiple (cts/width-type top)))
(t/is (= [8 1 1 1]
(mapv #(cts/side-width top %)
[:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))
(t/is (= :simple (cts/width-type equal)))
(t/is (= 8 (:stroke-width equal)))
(t/is (not (contains? equal :stroke-width-type)))
(t/is (= :simple (cts/width-type (cts/set-width-to-all-sides equal 3))))))
(t/deftest top-width-preserves-unset-sides
(let [stroke (cts/set-width-to-single-side {:stroke-width 5} :stroke-width-top 9)]
(t/is (= [9 5 5 5]
(mapv #(cts/side-width stroke %)
[:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))))
(t/deftest width-type-follows-effective-side-widths
(let [stroke {:stroke-width 2 :stroke-width-top 2 :stroke-width-right 4
:stroke-width-bottom 6 :stroke-width-left 8}
simple (cts/set-width-to-all-sides stroke 2)]
(t/is (= :multiple (cts/width-type stroke)))
(t/is (= :simple (cts/width-type {:stroke-width 2})))
(t/is (= :simple (cts/width-type {:stroke-width 2 :stroke-width-top 2})))
(t/is (= :multiple (cts/width-type {:stroke-width 2 :stroke-width-right 4})))
(t/is (= [2 2 2 2]
(mapv simple [:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))
(t/is (= :simple (cts/width-type simple)))
(t/is (not (contains? simple :stroke-width-type)))))

View File

@ -112,18 +112,18 @@ RUN set -eux; \
FROM base AS setup-opencode
ENV OPENCODE_VERSION=2.0.24
ENV OPENCODE_VERSION=2.0.25
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://registry.npmjs.org/@opencode/cli-linux-arm64/-/cli-linux-arm64-${OPENCODE_VERSION}.tgz"; \
ESUM='9d0cd2bfc060fd6c2afdf6db69837f8690d46a8d8327bb864421851ce004f453'; \
ESUM='92ce31022cd32f57ac3ccc775ba3a96f6aa36128a176aea0c7ef266986047537'; \
;; \
amd64|x86_64) \
BINARY_URL="https://registry.npmjs.org/@opencode/cli-linux-x64/-/cli-linux-x64-${OPENCODE_VERSION}.tgz"; \
ESUM='21b1ee0683841405d69541fc44481f8e5752e95bea72e8b8ec31dbcdb102e7f8'; \
ESUM='5b3e50ad1c06214aaf1c26eb30f90a15f66975a6e3e48fef5a31c34466e37b5a'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; exit 1; \

View File

@ -26,7 +26,7 @@
"dependencies": {
"@11ty/eleventy": "^3.1.6",
"@11ty/eleventy-navigation": "^1.0.5",
"@11ty/eleventy-plugin-rss": "^3.0.0",
"@11ty/eleventy-plugin-rss": "^3.1.0",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
"@tigersway/eleventy-plugin-ancestry": "^1.0.3",
"@types/markdown-it": "14.2.0",

98
docs/pnpm-lock.yaml generated
View File

@ -173,8 +173,8 @@ importers:
specifier: ^1.0.5
version: 1.0.5
'@11ty/eleventy-plugin-rss':
specifier: ^3.0.0
version: 3.0.0
specifier: ^3.1.0
version: 3.1.0
'@11ty/eleventy-plugin-syntaxhighlight':
specifier: ^5.0.2
version: 5.0.2
@ -233,8 +233,8 @@ packages:
resolution: {integrity: sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==}
engines: {node: '>=18'}
'@11ty/eleventy-plugin-rss@3.0.0':
resolution: {integrity: sha512-kKW4DcR57xAyRx0e8gNhKh56ahHVEaAj8/TuXQDnw+B46ig2bWADJAlyj/GdV37IG5ja9dZ4SgKZrs/CHz6YWQ==}
'@11ty/eleventy-plugin-rss@3.1.0':
resolution: {integrity: sha512-eby7wIYU3XBTAel8qPzbZSEhzNg/WXcAqNULYRqzMBU+gVlx4SL2mUvWDYxkp72KJqefnMMIo2Zz6w09N6S5XQ==}
'@11ty/eleventy-plugin-syntaxhighlight@5.0.2':
resolution: {integrity: sha512-T6xVVRDJuHlrFMHbUiZkHjj5o1IlLzZW+1IL9eUsyXFU7rY2ztcYhZew/64vmceFFpQwzuSfxQOXxTJYmKkQ+A==}
@ -293,8 +293,8 @@ packages:
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
engines: {node: '>=0.4.0'}
acorn@8.18.0:
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
acorn@8.19.0:
resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==}
engines: {node: '>=0.4.0'}
hasBin: true
@ -308,8 +308,8 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
argparse@3.0.1:
resolution: {integrity: sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==}
argparse@3.0.2:
resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==}
asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
@ -333,8 +333,8 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
brace-expansion@1.1.18:
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
brace-expansion@1.1.21:
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@ -462,8 +462,8 @@ packages:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
entities@8.0.0:
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
entities@8.1.0:
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
engines: {node: '>=20.19.0'}
errno@1.0.0:
@ -601,12 +601,12 @@ packages:
resolution: {integrity: sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==}
engines: {node: '>=6.0'}
js-yaml@3.15.1:
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
js-yaml@3.15.2:
resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==}
hasBin: true
js-yaml@4.3.1:
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
js-yaml@4.3.2:
resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
hasBin: true
junk@3.1.0:
@ -627,8 +627,8 @@ packages:
linkify-it@6.1.0:
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
liquidjs@10.29.0:
resolution: {integrity: sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==}
liquidjs@10.30.0:
resolution: {integrity: sha512-Fw4wA+8CZsJkaeLEIlPE3AoponfjEZhj7FQvYNcAEcg7jNRik7mEMoaco382FBdrYlykVf6w4vClPNctz2OYjg==}
engines: {node: '>=16'}
hasBin: true
@ -720,6 +720,10 @@ packages:
chokidar:
optional: true
obug@3.0.0:
resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
engines: {node: '>=12.20.0'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@ -867,8 +871,8 @@ packages:
uc.micro@3.0.0:
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
undici@7.29.0:
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
undici@7.30.0:
resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==}
engines: {node: '>=20.18.1'}
unpipe@1.0.0:
@ -887,8 +891,8 @@ packages:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
ws@8.21.3:
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
ws@8.22.0:
resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@ -904,7 +908,7 @@ snapshots:
'@11ty/dependency-tree-esm@2.0.4':
dependencies:
'@11ty/eleventy-utils': 2.0.7
acorn: 8.18.0
acorn: 8.19.0
dependency-graph: 1.0.0
normalize-path: 3.0.0
@ -925,7 +929,7 @@ snapshots:
send: 1.2.1
ssri: 11.0.0
urlpattern-polyfill: 10.1.0
ws: 8.21.3
ws: 8.22.0
transitivePeerDependencies:
- bufferutil
- supports-color
@ -954,14 +958,12 @@ snapshots:
- posthtml
- supports-color
'@11ty/eleventy-plugin-rss@3.0.0':
'@11ty/eleventy-plugin-rss@3.1.0':
dependencies:
'@11ty/eleventy-utils': 2.0.7
'@11ty/posthtml-urls': 1.0.3
debug: 4.4.3
obug: 3.0.0
posthtml: 0.16.7
transitivePeerDependencies:
- supports-color
'@11ty/eleventy-plugin-syntaxhighlight@5.0.2':
dependencies:
@ -988,9 +990,9 @@ snapshots:
filesize: 10.1.6
gray-matter: 4.0.3
iso-639-1: 3.1.6
js-yaml: 4.3.1
js-yaml: 4.3.2
kleur: 4.1.5
liquidjs: 10.29.0
liquidjs: 10.30.0
luxon: 3.7.2
markdown-it: 14.3.2
minimist: 1.2.8
@ -1053,9 +1055,9 @@ snapshots:
acorn-walk@8.3.5:
dependencies:
acorn: 8.18.0
acorn: 8.19.0
acorn@8.18.0: {}
acorn@8.19.0: {}
anymatch@3.1.3:
dependencies:
@ -1068,7 +1070,7 @@ snapshots:
argparse@2.0.1: {}
argparse@3.0.1: {}
argparse@3.0.2: {}
asap@2.0.6: {}
@ -1091,7 +1093,7 @@ snapshots:
boolbase@1.0.0: {}
brace-expansion@1.1.18:
brace-expansion@1.1.21:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
@ -1120,7 +1122,7 @@ snapshots:
parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2
undici: 7.29.0
undici: 7.30.0
whatwg-mimetype: 4.0.0
chokidar@3.6.0:
@ -1242,7 +1244,7 @@ snapshots:
entities@7.0.1: {}
entities@8.0.0: {}
entities@8.1.0: {}
errno@1.0.0:
dependencies:
@ -1254,7 +1256,7 @@ snapshots:
esm-import-transformer@3.0.5:
dependencies:
acorn: 8.18.0
acorn: 8.19.0
esprima@4.0.1: {}
@ -1305,7 +1307,7 @@ snapshots:
gray-matter@4.0.3:
dependencies:
js-yaml: 3.15.1
js-yaml: 3.15.2
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
@ -1367,12 +1369,12 @@ snapshots:
iso-639-1@3.1.6: {}
js-yaml@3.15.1:
js-yaml@3.15.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.3.1:
js-yaml@4.3.2:
dependencies:
argparse: 2.0.1
@ -1390,7 +1392,7 @@ snapshots:
dependencies:
uc.micro: 3.0.0
liquidjs@10.29.0:
liquidjs@10.30.0:
dependencies:
commander: 10.0.1
@ -1418,8 +1420,8 @@ snapshots:
markdown-it@15.0.2:
dependencies:
argparse: 3.0.1
entities: 8.0.0
argparse: 3.0.2
entities: 8.1.0
linkify-it: 6.1.0
mdurl: 2.1.0
punycode.js: 2.3.1
@ -1441,7 +1443,7 @@ snapshots:
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.18
brace-expansion: 1.1.21
minimist@1.2.8: {}
@ -1457,7 +1459,7 @@ snapshots:
node-retrieve-globals@6.0.1:
dependencies:
acorn: 8.18.0
acorn: 8.19.0
acorn-walk: 8.3.5
esm-import-transformer: 3.0.5
@ -1475,6 +1477,8 @@ snapshots:
optionalDependencies:
chokidar: 3.6.0
obug@3.0.0: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
@ -1604,7 +1608,7 @@ snapshots:
uc.micro@3.0.0: {}
undici@7.29.0: {}
undici@7.30.0: {}
unpipe@1.0.0: {}
@ -1616,4 +1620,4 @@ snapshots:
whatwg-mimetype@4.0.0: {}
ws@8.21.3: {}
ws@8.22.0: {}

View File

@ -386,9 +386,13 @@ By default, <code class="language-bash">smtp</code> flag is disabled, the email
printed to the console, which means that the emails will be shown in the stdout.
Note that if you plan to invite members to a team, it is recommended that you enable SMTP
as they will need to login to their account after receiving the invite link sent an in email.
It is currently not possible to just add someone to a team without them accepting an
invitation email.
as they will need to login to their account after receiving the invite link sent in an email.
There is one exception: with the `disable-email-verification` flag set, inviting an email
that already has a profile adds that user to the team directly, without sending any
invitation email and without requiring acceptance. Emails without an existing profile
still follow the normal invitation flow. See the
[`disable-email-verification` flag][11] description for more detail.
If you have an SMTP service, uncomment the appropriate settings section in
<code class="language-bash">docker-compose.yml</code> and configure those
@ -926,6 +930,13 @@ for the user:
<code class="language-bash">penpot-mcp</code> service. Check the [MCP section][8] to get more detail.
- <code class="language-bash">enable-prepl-server</code>: enables PREPL server, used by manage.py and other additional
tools to communicate internally with Penpot backend. Check the [CLI section][5] to get more detail.
- `disable-email-verification`: skips the email verification step on registration, making
newly registered profiles active immediately without any verification email. It also
changes the team invitation flow: inviting an email that already has a profile adds that
user to the team directly, without sending an invitation email (check the
[email configuration section][10] for more detail). Note that registering with an email
that already has an inactive profile is rejected as "email already exists" while this
flag is set. Not recommended for production environments.
__Since version 1.13.0__
@ -952,3 +963,5 @@ __Since version 2.0.0__
[7]: /technical-guide/integration/#access-tokens
[8]: /mcp/
[9]: /technical-guide/developer/subsystems/link-preview/
[10]: /technical-guide/configuration/#email-configuration
[11]: /technical-guide/configuration/#other-flags

View File

@ -144,6 +144,13 @@ is routed, depending if the email corresponds to an existing account or not. The
<code class="language-clojure">:register-profile</code> or <code class="language-clojure">:login</code> services are used, and the invitation token is
attached so that the profile is linked to the team at the end.
There is a shortcut when the `:email-verification` flag is disabled: if the invited email
already corresponds to an existing profile, the `create-invitation` function (in
`app.rpc.commands.teams-invitations`) adds the profile to the team directly, without
creating an invitation token or sending an email, and marks the profile as active if it
was inactive. Emails without an existing profile follow the normal invitation flow
described above.
## Handling unfinished registrations and bouncing users
All tokens have an expiration date, and when they are put in a permanent

View File

@ -98,6 +98,10 @@ or
docker exec -ti penpot-penpot-backend-1 python3 manage.py create-profile --skip-tutorial --skip-walkthrough
```
**NOTE:** profiles created with this command have no verified email. If the
`disable-email-verification` flag is set, inviting these users to a team adds them to it
directly, without sending any invitation email, so SMTP is not required for this flow.
Check the [Configuration][1] section for more detail.
**NOTE:** the exact container name depends on your docker version and platform.
For example it could be <code class="language-bash">penpot-penpot-backend-1</code> or <code class="language-bash">penpot_penpot-backend-1</code>.

139
exporter/pnpm-lock.yaml generated
View File

@ -240,8 +240,8 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
b4a@1.8.1:
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
b4a@1.9.0:
resolution: {integrity: sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==}
peerDependencies:
react-native-b4a: '*'
peerDependenciesMeta:
@ -252,32 +252,28 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
bare-events@2.9.1:
resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}
bare-events@2.9.2:
resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
bare-fs@4.7.2:
resolution: {integrity: sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==}
engines: {bare: '>=1.16.0'}
bare-fs@4.8.2:
resolution: {integrity: sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==}
engines: {bare: '>=1.28.0'}
peerDependencies:
bare-buffer: '*'
peerDependenciesMeta:
bare-buffer:
optional: true
bare-os@3.9.1:
resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==}
engines: {bare: '>=1.14.0'}
bare-path@3.1.2:
resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==}
bare-path@3.0.1:
resolution: {integrity: sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==}
bare-stream@2.13.1:
resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==}
bare-stream@2.13.4:
resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==}
peerDependencies:
bare-abort-controller: '*'
bare-buffer: '*'
@ -290,8 +286,8 @@ packages:
bare-events:
optional: true
bare-url@2.4.5:
resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==}
bare-url@2.5.4:
resolution: {integrity: sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@ -300,8 +296,8 @@ packages:
resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==}
engines: {node: '>=20.19.0'}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
brace-expansion@5.0.12:
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
engines: {node: 20 || >=22}
buffer-crc32@1.0.0:
@ -330,8 +326,8 @@ packages:
resolution: {integrity: sha512-8BIbcC6tPZMv2/PD4PrB3CtKks7LmAWhA7FRZK/6/b/WJ2RUz00KCga7gq50S4/RhZ22kxefRFGgsuts0v3eoA==}
engines: {node: '>= 0.8'}
core-js-pure@3.49.0:
resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==}
core-js-pure@3.50.0:
resolution: {integrity: sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -401,8 +397,8 @@ packages:
resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==}
engines: {node: '>=20.19.0'}
entities@8.0.0:
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
entities@8.1.0:
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
engines: {node: '>=20.19.0'}
event-target-shim@5.0.1:
@ -470,8 +466,8 @@ packages:
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
minimatch@10.2.6:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
ms@2.1.3:
@ -527,15 +523,15 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
sax@1.6.1:
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
engines: {node: '>=11.0.0'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
@ -552,8 +548,8 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
streamx@2.27.0:
resolution: {integrity: sha512-WZ189TKnHoAokYHvwzaAQMpd55cgUmFIcJFzBSgGcb886jau5DL+XdDhTWV4ps3FLvk+OORp0dLRTPsLZ21CSA==}
streamx@2.28.1:
resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@ -561,8 +557,8 @@ packages:
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
tar-stream@3.2.0:
resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==}
tar-stream@3.2.2:
resolution: {integrity: sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==}
teex@1.0.1:
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
@ -612,7 +608,7 @@ snapshots:
'@babel/runtime-corejs3@7.29.7':
dependencies:
core-js-pure: 3.49.0
core-js-pure: 3.50.0
'@ioredis/commands@2.0.0': {}
@ -622,7 +618,7 @@ snapshots:
css-tree: 3.2.1
csso: 5.0.5
lodash: 4.18.1
sax: 1.6.0
sax: 1.6.1
abort-controller@3.0.0:
dependencies:
@ -637,7 +633,7 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 4.7.0
readdir-glob: 3.0.0
tar-stream: 3.2.0
tar-stream: 3.2.2
zip-stream: 7.0.5
transitivePeerDependencies:
- bare-abort-controller
@ -646,47 +642,44 @@ snapshots:
async@3.2.6: {}
b4a@1.8.1: {}
b4a@1.9.0: {}
balanced-match@4.0.4: {}
bare-events@2.9.1: {}
bare-events@2.9.2: {}
bare-fs@4.7.2:
bare-fs@4.8.2:
dependencies:
bare-events: 2.9.1
bare-path: 3.0.1
bare-stream: 2.13.1(bare-events@2.9.1)
bare-url: 2.4.5
bare-events: 2.9.2
bare-path: 3.1.2
bare-stream: 2.13.4(bare-events@2.9.2)
bare-url: 2.5.4
fast-fifo: 1.3.2
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
bare-os@3.9.1: {}
bare-path@3.1.2: {}
bare-path@3.0.1:
bare-stream@2.13.4(bare-events@2.9.2):
dependencies:
bare-os: 3.9.1
bare-stream@2.13.1(bare-events@2.9.1):
dependencies:
streamx: 2.27.0
b4a: 1.9.0
streamx: 2.28.1
teex: 1.0.1
optionalDependencies:
bare-events: 2.9.1
bare-events: 2.9.2
transitivePeerDependencies:
- react-native-b4a
bare-url@2.4.5:
bare-url@2.5.4:
dependencies:
bare-path: 3.0.1
bare-path: 3.1.2
base64-js@1.5.1: {}
boolbase@2.0.0: {}
brace-expansion@5.0.9:
brace-expansion@5.0.12:
dependencies:
balanced-match: 4.0.4
@ -716,7 +709,7 @@ snapshots:
depd: 2.0.0
keygrip: 1.1.0
core-js-pure@3.49.0: {}
core-js-pure@3.50.0: {}
core-util-is@1.0.3: {}
@ -738,12 +731,12 @@ snapshots:
css-tree@2.2.1:
dependencies:
mdn-data: 2.0.28
source-map-js: 1.2.1
source-map-js: 1.2.2
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
source-map-js: 1.2.1
source-map-js: 1.2.2
css-what@8.0.0: {}
@ -765,7 +758,7 @@ snapshots:
dependencies:
domelementtype: 3.0.0
domhandler: 6.0.1
entities: 8.0.0
entities: 8.1.0
domelementtype@3.0.0: {}
@ -779,13 +772,13 @@ snapshots:
domelementtype: 3.0.0
domhandler: 6.0.1
entities@8.0.0: {}
entities@8.1.0: {}
event-target-shim@5.0.1: {}
events-universal@1.0.1:
dependencies:
bare-events: 2.9.1
bare-events: 2.9.2
transitivePeerDependencies:
- bare-abort-controller
@ -841,9 +834,9 @@ snapshots:
mdn-data@2.27.1: {}
minimatch@10.2.5:
minimatch@10.2.6:
dependencies:
brace-expansion: 5.0.9
brace-expansion: 5.0.12
ms@2.1.3: {}
@ -890,7 +883,7 @@ snapshots:
readdir-glob@3.0.0:
dependencies:
minimatch: 10.2.5
minimatch: 10.2.6
redis-errors@1.2.0: {}
@ -898,11 +891,11 @@ snapshots:
safe-buffer@5.2.1: {}
sax@1.6.0: {}
sax@1.6.1: {}
setprototypeof@1.2.0: {}
source-map-js@1.2.1: {}
source-map-js@1.2.2: {}
source-map-support@0.5.21:
dependencies:
@ -915,7 +908,7 @@ snapshots:
statuses@2.0.2: {}
streamx@2.27.0:
streamx@2.28.1:
dependencies:
events-universal: 1.0.1
fast-fifo: 1.3.2
@ -932,12 +925,12 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
tar-stream@3.2.0:
tar-stream@3.2.2:
dependencies:
b4a: 1.8.1
bare-fs: 4.7.2
b4a: 1.9.0
bare-fs: 4.8.2
fast-fifo: 1.3.2
streamx: 2.27.0
streamx: 2.28.1
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
@ -945,14 +938,14 @@ snapshots:
teex@1.0.1:
dependencies:
streamx: 2.27.0
streamx: 2.28.1
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
text-decoder@1.2.7:
dependencies:
b4a: 1.8.1
b4a: 1.9.0
transitivePeerDependencies:
- react-native-b4a
@ -968,7 +961,7 @@ snapshots:
xml-js@1.6.11:
dependencies:
sax: 1.6.0
sax: 1.6.1
xregexp@5.1.3:
dependencies:

View File

@ -59,15 +59,15 @@
"@penpot/tokenscript": "link:packages/tokenscript",
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
"@playwright/test": "1.63.0",
"@storybook/addon-docs": "10.6.0",
"@storybook/addon-themes": "10.6.0",
"@storybook/addon-vitest": "10.6.0",
"@storybook/react-vite": "10.6.0",
"@storybook/addon-docs": "10.6.1",
"@storybook/addon-themes": "10.6.1",
"@storybook/addon-vitest": "10.6.1",
"@storybook/react-vite": "10.6.1",
"@tokens-studio/sd-transforms": "2.0.3",
"@types/node": "^26.6.3",
"@vitest/browser": "5.0.2",
"@vitest/browser-playwright": "5.0.2",
"@vitest/coverage-v8": "5.0.2",
"@types/node": "^26.6.4",
"@vitest/browser": "5.0.3",
"@vitest/browser-playwright": "5.0.3",
"@vitest/coverage-v8": "5.0.3",
"@zip.js/zip.js": "2.8.34",
"autoprefixer": "^10.6.1",
"compression": "^1.8.2",
@ -75,17 +75,17 @@
"date-fns": "^4.4.0",
"esbuild": "^0.28.2",
"eventsource-parser": "^4.1.1",
"express": "^5.1.0",
"express": "^5.2.1",
"fancy-log": "^2.0.0",
"getopts": "^2.3.0",
"gettext-parser": "^9.1.1",
"highlight.js": "^11.12.0",
"js-beautify": "^2.0.3",
"jsdom": "^30.1.1",
"jsdom": "^30.1.2",
"lodash": "^4.18.1",
"lodash.debounce": "^4.0.8",
"map-stream": "0.0.7",
"marked": "^18.0.14",
"marked": "^18.1.0",
"mkdirp": "^3.0.1",
"mustache": "^4.2.0",
"nodemon": "^3.1.14",
@ -93,7 +93,7 @@
"opentype.js": "^2.0.0",
"p-limit": "^7.3.3",
"playwright": "1.63.0",
"postcss": "^8.5.28",
"postcss": "^8.5.29",
"postcss-clean": "^1.2.2",
"postcss-modules": "^9.0.1",
"postcss-scss": "^4.0.9",
@ -107,24 +107,24 @@
"react-virtualized": "^9.22.6",
"rimraf": "^6.1.3",
"rxjs": "8.0.0-alpha.14",
"sass": "^1.105.0",
"sass-embedded": "^1.105.0",
"sass": "^1.105.1",
"sass-embedded": "^1.105.1",
"sax": "^1.6.1",
"scheduler": "^0.28.0",
"source-map-support": "^0.5.21",
"storybook": "10.6.0",
"storybook": "10.6.1",
"style-dictionary": "5.5.5",
"stylelint": "^17.15.0",
"stylelint": "^17.16.0",
"stylelint-config-standard-scss": "^17.0.0",
"stylelint-plugin-logical-css": "^2.1.0",
"stylelint-scss": "^7.3.0",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.3",
"tinycolor2": "^1.6.0",
"typescript": "^6.0.2",
"vite": "^8.3.1",
"vitest": "^5.0.2",
"wait-on": "^9.1.0",
"typescript": "^6.0.3",
"vite": "^8.3.3",
"vitest": "^5.0.3",
"wait-on": "^9.5.1",
"watcher": "^2.3.1",
"workerpool": "^10.0.3",
"xregexp": "^5.1.3"

View File

@ -25,21 +25,17 @@
"devDependencies": {
"@babel/core": "^8.0.6",
"@babel/preset-react": "^8.0.1",
"@storybook/react": "10.6.0",
"@storybook/react-vite": "10.6.0",
"@storybook/react": "10.6.1",
"@storybook/react-vite": "10.6.1",
"@testing-library/dom": "10.4.2",
"@testing-library/react": "16.3.3",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"@vitejs/plugin-react": "^6.1.2",
"babel-plugin-react-compiler": "^1.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"react-compiler-runtime": "^1.0.0",
"storybook": "10.6.0",
"vite-plugin-dts": "^5.1.1"
"storybook": "10.6.1",
"vite-plugin-dts": "^5.1.2"
},
"dependencies": {
"react-aria-components": "^1.20.0"

View File

@ -143,11 +143,11 @@ export class ShortcutsPage extends BaseWebSocketPage {
this.searchInput = page.getByRole("textbox", { name: /shortcuts/i });
this.allTab = page.getByRole("tab", { name: "All" });
this.personalizedTab = page.getByRole("tab", { name: "Personalized" });
this.personalizedTab = page.getByRole("tab", { name: "Custom" });
this.disabledTab = page.getByRole("tab", { name: "Disabled" });
this.restoreAllButton = page.getByRole("button", {
name: /restore all/i,
name: /reset all/i,
});
this.importExportButton = page.getByRole("button", {
@ -194,7 +194,7 @@ export class ShortcutsPage extends BaseWebSocketPage {
name: new RegExp(`Edit ${shortcutName}`, "i"),
});
await button.click();
const recordingArea = this.page.getByText("Press the key combination");
const recordingArea = this.page.getByText("Press the new keys");
await expect(recordingArea).toBeVisible();
await recordingArea.focus();
}
@ -265,7 +265,7 @@ export class ShortcutsPage extends BaseWebSocketPage {
async restoreAllShortcuts() {
await this.restoreAllButton.click();
const confirmButton = this.page.getByRole("button", {
name: "Restore",
name: "Reset",
exact: true,
});
await confirmButton.click();
@ -307,7 +307,8 @@ export class ShortcutsPage extends BaseWebSocketPage {
async confirmImportApply() {
const applyButton = this.page.getByRole("button", {
name: /apply/i,
name: "Import",
exact: true,
});
await applyButton.click();
}

View File

@ -25,12 +25,12 @@ test.describe("Shortcuts Settings Page", () => {
await expect(shortcutsPage.personalizedTab).toBeVisible();
await expect(shortcutsPage.disabledTab).toBeVisible();
await shortcutsPage.clickTab("Personalized");
await shortcutsPage.clickTab("Custom");
await expect(shortcutsPage.personalizedTab).toHaveAttribute(
"aria-selected",
"true",
);
const personalizedPlacehonder = page.getByText(/Head to All to start/i);
const personalizedPlacehonder = page.getByText(/No custom shortcuts yet/i);
await expect(personalizedPlacehonder).toBeVisible();
await shortcutsPage.clickTab("Disabled");
@ -38,7 +38,7 @@ test.describe("Shortcuts Settings Page", () => {
"aria-selected",
"true",
);
const disabledPlaceholder = page.getByText(/There are not disabled/i);
const disabledPlaceholder = page.getByText(/No disabled shortcuts/i);
await expect(disabledPlaceholder).toBeVisible();
await shortcutsPage.clickTab("All");
@ -81,7 +81,7 @@ test.describe("Shortcut Import", () => {
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.clickTab("Personalized");
await shortcutsPage.clickTab("Custom");
await shortcutsPage.searchForShortcut("Align bottom");
await shortcutsPage.expectShortcutVisible("Align bottom");
});
@ -93,7 +93,9 @@ test.describe("Shortcut Import", () => {
await shortcutsPage.importShortcutsRaw("{invalid");
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
page
.getByRole("alert")
.filter({ hasText: /not a valid shortcuts export/i }),
).toBeVisible();
});
@ -106,7 +108,9 @@ test.describe("Shortcut Import", () => {
});
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
page
.getByRole("alert")
.filter({ hasText: /not a valid shortcuts export/i }),
).toBeVisible();
});
@ -121,7 +125,9 @@ test.describe("Shortcut Import", () => {
await shortcutsPage.confirmImportApply();
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
page
.getByRole("alert")
.filter({ hasText: /not a valid shortcuts export/i }),
).not.toBeVisible();
});
@ -134,7 +140,9 @@ test.describe("Shortcut Import", () => {
});
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
page
.getByRole("alert")
.filter({ hasText: /not a valid shortcuts export/i }),
).toBeVisible();
});
@ -213,7 +221,7 @@ test.describe("Shortcut Conflict Detection", () => {
await shortcutsPage.pressKey("Alt+a");
await expect(
page.getByRole("alert").filter({ hasText: /Combination assigned to/i }),
page.getByRole("alert").filter({ hasText: /Already assigned to/i }),
).toBeVisible();
await shortcutsPage.saveShortcut();
@ -259,7 +267,7 @@ test.describe("Shortcut Reset", () => {
await shortcutsPage.resetShortcut("Align bottom");
await expect(
page.getByText(/If you save, this shortcut will return/i),
page.getByText(/Saving will reset this shortcut/i),
).toBeVisible();
await shortcutsPage.saveShortcut();
@ -317,7 +325,7 @@ test.describe("Duplicate Shortcut Prevention", () => {
await shortcutsPage.pressKey("Control+y");
await expect(
page.getByRole("alert").filter({ hasText: /Combination assigned to/i }),
page.getByRole("alert").filter({ hasText: /Already assigned to/i }),
).toBeVisible();
await shortcutsPage.saveShortcut();

View File

@ -298,6 +298,83 @@ test.describe("BUG 10910 - Text is not replaced when there is a selection", () =
});
});
test.describe("BUG 12187 - Typing after a triple-click selection", () => {
// Creates a text and enters the editor again, so the input surface is fresh.
async function editNewText(page, text) {
const workspace = new WasmWorkspacePage(page, { textEditor: true });
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await workspace.waitForFirstRender();
await workspace.createAutoWidthTextShape(200, 150, text);
await workspace.textEditor.stopEditing();
await workspace.textEditor.startEditing();
return workspace;
}
async function tripleClickText(page) {
await page
.getByTestId("text-editor-container")
.click({ position: { x: 10, y: 8 }, clickCount: 3 });
}
// Some browsers (Chrome on Linux) select the whole input surface on a
// triple-click. That selection is not editable, so typing fires no input.
async function selectWholeInputSurface(page) {
await page.evaluate(() => {
const range = document.createRange();
range.selectNode(document.getElementById("text-editor-wasm-input"));
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
}
test("Typing over a triple-click selection replaces it", async ({ page }) => {
const workspace = await editNewText(page, "Hello brave new world");
await tripleClickText(page);
await page.keyboard.type("ok");
await workspace.textEditor.stopEditing();
await workspace.layers.getByTestId("layer-row").first().click();
await workspace.waitForSelectedShapeName("ok");
});
test("Typing works when the browser selects the whole input surface", async ({
page,
}) => {
const workspace = await editNewText(page, "Hello brave new world");
await tripleClickText(page);
await selectWholeInputSurface(page);
await page.keyboard.type("ok");
await workspace.textEditor.stopEditing();
await workspace.layers.getByTestId("layer-row").first().click();
await workspace.waitForSelectedShapeName("ok");
});
test("Typing works after deleting a whole input surface selection", async ({
page,
}) => {
const workspace = await editNewText(page, "Hello brave new world");
await tripleClickText(page);
await selectWholeInputSurface(page);
await page.keyboard.press("Backspace");
await page.keyboard.type("ok");
await workspace.textEditor.stopEditing();
await workspace.layers.getByTestId("layer-row").first().click();
await workspace.waitForSelectedShapeName("ok");
});
});
test("BUG 10531 - Entering the editor auto-selects the whole text", async ({
page,
}) => {

4131
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -34,3 +34,7 @@ overrides:
postcss@<8.5.10: ^8.5.10
yaml@>=2.0.0 <2.8.3: ^2.8.3
playwright@>=1.61.1 <2.0.0-0: "1.62.1"
postcss@<=8.5.11: ^8.5.12
postcss@<=8.5.17: ^8.5.18
postcss@<=8.5.22: ^8.5.23
shell-quote@>=1.8.4 <1.11.0: ^1.11.0

View File

@ -467,13 +467,10 @@
(assoc-in [:strokes index] stroke))))
options))))))))
;; --- Per-side controls expansion (Design tab UI state)
;; --- Per-side controls expansion
(defn toggle-stroke-per-side
"Toggle the per-side width controls of the stroke at `index` for the shapes
in `ids`. Purely ephemeral UI state: it lives in `:workspace-local` keyed by
`[ids index]`, so it survives selecting another shape and coming back, but
resets on reload."
"Toggles per-side controls for stroke `index` on shapes `ids` in UI state."
[ids index]
(ptk/reify ::toggle-stroke-per-side
ptk/UpdateEvent

View File

@ -65,15 +65,18 @@
[{:keys [objects id]}]
(reduce-kv (fn [changes shape-id shape]
(if (shape-has-invalid-font-family?? shape)
(conj changes {:type :mod-obj
:id shape-id
:page-id id
:operations [{:type :set
:attr :content
:val (fix-shape-content shape)}
{:type :set
:attr :position-data
:val nil}]})
(let [fixed-content (fix-shape-content shape)]
(if (not= fixed-content (:content shape))
(conj changes {:type :mod-obj
:id shape-id
:page-id id
:operations [{:type :set
:attr :content
:val fixed-content}
{:type :set
:attr :position-data
:val nil}]})
changes))
changes))
[]
objects))
@ -82,8 +85,11 @@
[fdata]
(reduce-kv (fn [changes _ typography]
(if (has-invalid-font-family? typography)
(conj changes {:type :mod-typography
:typography (fix-typography typography)})
(let [fixed-typography (fix-typography typography)]
(if (not= fixed-typography typography)
(conj changes {:type :mod-typography
:typography fixed-typography})
changes))
changes))
[]
(:typographies fdata)))

View File

@ -37,13 +37,16 @@
(declare change-edit-mode)
(defn start-created-path-edition
[id]
(ptk/reify ::start-created-path-edition
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwe/start-edition-mode id)
(edition/start-path-edit id)
(change-edit-mode :draw)))))
"Edits a just created path in `edit-mode` (`:draw` by default)."
([id]
(start-created-path-edition id :draw))
([id edit-mode]
(ptk/reify ::start-created-path-edition
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwe/start-edition-mode id)
(edition/start-path-edit id)
(change-edit-mode edit-mode))))))
;; Draw-loop stop signals either restart the same path or exit drawing.
@ -434,11 +437,12 @@
(when-let [content (dm/get-in state [:workspace-drawing :object :content])]
(cond
(and (> (count content) 1) restart?)
(rx/of (common/finish-path)
(clean-drawn-content)
(setup-frame)
(dwdc/handle-finish-drawing)
(start-created-path-edition shape-id))
(let [edit-mode (dm/get-in state [:workspace-local :edit-path shape-id :edit-mode])]
(rx/of (common/finish-path)
(clean-drawn-content)
(setup-frame)
(dwdc/handle-finish-drawing)
(start-created-path-edition shape-id (d/nilv edit-mode :draw))))
(> (count content) 1)
(rx/of (clean-drawn-content)
@ -545,6 +549,10 @@
state)))
(defn change-edit-mode
"Switches the active path between `:draw` and `:move`.
A new path being drawn has no edition yet; switching it to `:move`
finishes the drawing, which then reenters edition in `:move`."
[mode]
(ptk/reify ::change-edit-mode
ptk/UpdateEvent
@ -552,16 +560,20 @@
(if-let [id (dm/get-in state [:workspace-local :edition])]
(cond-> (d/update-in-when state [:workspace-local :edit-path id] assoc :edit-mode mode)
(= mode :draw) (enter-draw-from-selected-node id))
state))
(cond-> state
(and (= mode :move) (st/drawing? state))
(assoc-in [:workspace-local :edit-path (st/get-path-id state) :edit-mode] :move))))
ptk/WatchEvent
(watch [_ state _]
(when-let [id (dm/get-in state [:workspace-local :edition])]
(if-let [id (dm/get-in state [:workspace-local :edition])]
(let [mode (dm/get-in state [:workspace-local :edit-path id :edit-mode])]
(case mode
:move (rx/of (common/finish-path))
:draw (rx/of (start-draw-mode))
(rx/empty)))))))
(rx/empty)))
(when (and (= mode :move) (st/drawing? state))
(rx/of (common/finish-path)))))))
(defn reset-last-handler
[]

View File

@ -157,6 +157,7 @@
:label (fn [] (tr "shortcuts.deselect-all"))
:command (ds/c-mod "shift+a")
:subsections [:path-editor]
:overwrite true
:fn #(st/emit! (drp/deselect-all))}
:flip-horizontal {:tooltip (ds/shift "H")

View File

@ -245,7 +245,7 @@
:section [:workspace]
:fn #(update-attrs-when-no-readonly {:text-decoration "toggle-underline"})}
:line-through {:tooltip (ds/alt (ds/meta-shift "5"))
:line-through {:tooltip (ds/alt (ds/shift "5"))
:label (fn [] (tr "shortcuts.line-through"))
:command "alt+shift+5"
:subsections [:text-editor]

View File

@ -140,6 +140,13 @@
(u/query-string->map (:query uri)))})))
(defn- legacy-query-token
"Legacy root links (e.g. `#?template=<url>`) keep their params in a
bare `#?…` fragment. Return them as a query token, or nil."
[hash]
(when (str/starts-with? hash "#?")
(subs hash 1)))
(defn- store-session-params
[{:keys [template plugin]}]
(binding [storage/*sync* true]
@ -226,7 +233,8 @@
(let [location (.-location js/document)
location-path (dm/str (.-origin location) (.-pathname location))
valid-location? (= location-path (dm/str cf/public-uri))
legacy-hash (.-hash location)]
legacy-hash (.-hash location)
legacy-token (legacy-query-token legacy-hash)]
(cond
(not valid-location?)
@ -242,6 +250,10 @@
(st/emit! (rt/nav name params {::rt/replace true}))
(on-query-navigate router token send-event-info?))
;; TODO(next-version): delete with `legacy-routes`.
(some? legacy-token)
(on-query-navigate router legacy-token send-event-info?)
:else
(on-query-navigate router token send-event-info?))))
@ -251,7 +263,10 @@
(subs token 1)
(or token ""))
query-params (u/query-string->map token-query)
empty-token? (str/blank? token-query)
;; Without a `screen` the token targets the app root; its params
;; (e.g. `template`) are forwarded to the dashboard or kept for
;; after login.
root? (nil? (rt/get-query-param query-params :screen))
match (rt/match router token)]
(if (some? match)
(handle-sso-error-and-navigate match send-event-info? (rt/get-current-href))
@ -261,9 +276,13 @@
;; on invitations workflows (and probably other cases).
(->> (rp/cmd! :get-profile)
(rx/mapcat (fn [profile]
(->> (rp/cmd! :get-teams {})
(rx/map (fn [teams]
(assoc profile ::teams (into #{} (map :id) teams)))))))
;; get-teams requires a session; anonymous users
;; only need their params kept for after login.
(if (= (:id profile) uuid/zero)
(rx/of profile)
(->> (rp/cmd! :get-teams {})
(rx/map (fn [teams]
(assoc profile ::teams (into #{} (map :id) teams))))))))
(rx/subs! (fn [{:keys [id ::teams] :as profile}]
(cond
(= id uuid/zero)
@ -271,7 +290,7 @@
(store-session-params query-params)
(st/emit! (rt/nav :auth-login)))
empty-token?
root?
(let [default-team-id (:default-team-id profile)
last-team-id (dtm/get-last-team-id)
team-id (if (contains? teams last-team-id)

View File

@ -106,11 +106,11 @@
[:thead
[:tr {:class (stl/css :shortcuts-list-header)}
[:th {:class (stl/css :shortcut-header-name)}
(tr "restore-shortcuts.acction")]
(tr "restore-shortcuts.action")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.current")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.import")]]]
(tr "import-shortcuts.imported")]]]
[:tbody {:class (stl/css :shortcuts-list-body)}
(let [last-ctx* (volatile! nil)]
(for [entry diff-entries]

View File

@ -97,7 +97,7 @@
[:thead
[:tr {:class (stl/css :shortcuts-list-header)}
[:th {:class (stl/css :shortcut-header-name)}
(tr "restore-shortcuts.acction")]
(tr "restore-shortcuts.action")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.current")]
[:th {:class (stl/css :shortcut-header-command)}

View File

@ -41,7 +41,7 @@
[:> search-bar* {:on-change on-search-term-change
:on-clear on-search-clear-click
:value filter-term
:placeholder (tr "shortcuts.title")
:placeholder (tr "shortcuts.search")
:icon-id i/search
:auto-focus true}]
(when (and show-restore-all has-custom-shortcuts)
@ -50,7 +50,7 @@
:on-click on-restore-all
:class (stl/css :restore-all-button)
:icon i/reload}
(tr "dashboard.restore-all-deleted-button")])])
(tr "shortcuts.reset-all")])])
(defn- filter-shortcuts-tree
[tree shortcut-filter search-term]
@ -457,9 +457,9 @@
:imported-shortcuts shortcuts
:custom-shortcuts custom-shortcuts
:all-shortcuts-raw all-shortcuts-raw}))
(st/emit! (ntf/error (tr "errors.invalid-data")))))
(st/emit! (ntf/error (tr "import-shortcuts.invalid-file")))))
(catch :default _
(st/emit! (ntf/error (tr "errors.invalid-data"))))))))
(st/emit! (ntf/error (tr "import-shortcuts.invalid-file"))))))))
(-> (mf/ref-val input-ref)
(dom/set-value! "")))))]

View File

@ -586,7 +586,7 @@
:key command-translate}
[:button {:on-click start-editing
:disabled is-editing
:aria-label (dm/str "Edit " command-translate)
:aria-label (tr "shortcuts.edit" command-translate)
:class (stl/css-case :shortcut-button-editable true
:shortcut-button-editing is-editing)}
[:span {:class (stl/css :command-name)

View File

@ -107,6 +107,58 @@
:token-sets token-sets
:token-themes token-themes}))
(defn sort-linked-libraries
"Given the linked libraries of a file, each with a `:connected-to`
collection of the ids of the files that use it, return the libraries
ordered as a tree: each library is followed by the libraries nested
under it, marked with `:nested?` and `:parent-name`.
Direct libraries (`:is-indirect` false) and libraries not used by
any other linked library are roots. Every library is returned
exactly once, at any depth and even when the dependencies form a
cycle."
[libraries]
(let [by-id (d/index-by :id libraries)
sort-by-name (partial sort-by (comp str/lower :name))
parents
(d/update-vals by-id
(fn [{:keys [id connected-to]}]
(into #{}
(filter #(and (not= % id)
(contains? by-id %)))
connected-to)))
root?
(fn [{:keys [id is-indirect]}]
(or (false? is-indirect)
(empty? (get parents id))))
children
(fn [parent-id]
(->> libraries
(remove root?)
(filter #(contains? (get parents (:id %)) parent-id))
(sort-by-name)))
visit
(fn visit [[result visited :as acc] library parent]
(if (contains? visited (:id library))
acc
(reduce #(visit %1 %2 library)
[(conj result (assoc library
:nested? (some? parent)
:parent-name (:name parent)))
(conj visited (:id library))]
(children (:id library)))))]
;; The second pass picks up libraries only reachable through a
;; cycle, which have no root to hang from.
(as-> [[] #{}] $
(reduce #(visit %1 %2 nil) $ (sort-by-name (filter root? libraries)))
(reduce #(visit %1 %2 nil) $ (sort-by-name libraries))
(first $))))
(defn- describe-library
[components-count graphics-count colors-count typography-count]
(let [all-zero? (and (zero? components-count)
@ -666,11 +718,6 @@
dependencies (mf/with-memo [shared-libraries]
(into {} (map (juxt :id :library-file-ids) (vals shared-libraries))))
library-names (mf/with-memo [shared-libraries]
(into {} (map (fn [{:keys [id name]}]
[id name])
(vals shared-libraries))))
find-connected-to
(mf/use-fn
(mf/deps dependencies)
@ -679,36 +726,10 @@
(keep (fn [[k v]] (when (contains? v library-id) k))))))
linked-libraries
(mf/with-memo [linked-libraries find-connected-to library-names]
(let [libs
(->> (vals linked-libraries)
(map #(assoc % :connected-to (find-connected-to (:id %)))))
linked-ids
(into #{} (map :id) libs)
sort-by-name
(partial sort-by (comp str/lower :name))
parent-id
(fn [{:keys [connected-to]}]
(first (filter linked-ids connected-to)))
nested-by-parent
(->> libs
(filter parent-id)
(map #(assoc %
:nested? true
:parent-name (library-names (parent-id %))))
(group-by parent-id)
(d/mapm (fn [_ v] (sort-by-name v))))]
(->> libs
(remove parent-id)
(map #(assoc % :nested? false))
(sort-by-name)
(mapcat (fn [{:keys [id] :as library}]
(cons library (get nested-by-parent id [])))))))
(mf/with-memo [linked-libraries find-connected-to]
(->> (vals linked-libraries)
(map #(assoc % :connected-to (find-connected-to (:id %))))
(sort-linked-libraries)))
linked-libraries-ids
(mf/with-memo [linked-libraries]

View File

@ -36,6 +36,9 @@
(def path-preview-dasharray 4)
(def path-snap-stroke-width 1)
;; Stand-in for shapes that reach the editor without content.
(def ^:private empty-content (path/content nil))
(def accent-color "var(--color-accent-tertiary)")
(def secondary-color "var(--color-accent-quaternary)")
(def black-color "var(--app-black)")
@ -502,7 +505,7 @@
(some? drag-handler))
base-content
(get shape :content)
(or (get shape :content) empty-content)
;; Cache segment midpoints used by insertion previews.
insertion-mid-points

View File

@ -72,6 +72,27 @@
(doseq [[mime value] items]
(.setData data mime value)))
(defn- collapse-input-caret
"Replaces the browser selection with a collapsed caret at the end of `node`."
[^js node]
(when-let [sel (.getSelection js/window)]
(let [range (.createRange js/document)]
(.selectNodeContents range node)
(.collapse range false)
(.removeAllRanges sel)
(.addRange sel range))))
(defn- ensure-input-caret
"Collapses the browser selection into the capture surface when it leaves it:
such a selection is not editable, so typed keys would fire no `input`."
[^js node]
(when (some? node)
(let [sel (.getSelection js/window)]
(when-not (and (some? sel)
(.contains node (.-anchorNode sel))
(.contains node (.-focusNode sel)))
(collapse-input-caret node)))))
(defn- reset-input-node
"Empties the contenteditable capture surface and restores a collapsed caret
inside it.
@ -87,12 +108,7 @@
(set! (.-textContent node) "")
(when (not= (.-activeElement js/document) node)
(.focus node))
(when-let [sel (.getSelection js/window)]
(let [range (.createRange js/document)]
(.selectNodeContents range node)
(.collapse range true)
(.removeAllRanges sel)
(.addRange sel range)))))
(collapse-input-caret node)))
(defn- keep-input-alive
"Keeps the capture surface able to receive further input WITHOUT clearing it.
@ -372,8 +388,10 @@
(text-editor/text-editor-move-cursor 5 ctrl? shift?)
(wasm.api/render-text-editor-overlay!))
;; Let contenteditable handle text input via on-input
:else nil)))))
;; Let contenteditable handle text input via on-input, which
;; needs an editable browser selection.
:else
(ensure-input-caret (mf/ref-val contenteditable-ref)))))))
;; Native `beforeinput` listener (see the use-effect that registers it).
;; We use the native event, not React's synthetic `onBeforeInput`, because
@ -490,6 +508,7 @@
(triple-click? native-event)
(do
(wasm.api/text-editor-select-paragraph off-pt)
(ensure-input-caret (mf/ref-val contenteditable-ref))
(wasm.api/render-text-editor-overlay!))
;; `dblclick` selects the word right after. Shift+click still goes
@ -509,6 +528,7 @@
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-select-word-boundary off-pt)
(ensure-input-caret (mf/ref-val contenteditable-ref))
(wasm.api/render-text-editor-overlay!))))
on-context-menu

View File

@ -19,6 +19,7 @@
[app.main.store :as st]
[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.controls.input :refer [input*]]
@ -260,18 +261,30 @@
[:span {:class (stl/css :radio-text)}
text]])
(def ^:private default-search-state
{: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})
;; Per-file, session-scoped (in-memory only) so an open search and its
;; filters survive switching between the sidebar tabs, which unmounts the
;; layers panel, without leaking across files or persisting across reloads.
(defonce ^:private session-search*
(atom {}))
(defn use-search
[page objects]
(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}))
(let [file-id (mf/use-ctx ctx/current-file-id)
state* (mf/use-state
#(-> default-search-state
(merge (get @session-search* file-id))
(assoc :show-menu false)))
layers-search (mf/deref ref:layers-search)
state (deref state*)
current-filters (:filters state)
@ -522,6 +535,9 @@
(events/unlistenByKey key1)
(events/unlistenByKey key2))))
(mf/with-effect [file-id state]
(swap! session-search* assoc file-id state))
(mf/with-effect [layers-search]
(if-let [{:keys [open? find-replace-mode? scope]} layers-search]
(when open?
@ -821,6 +837,8 @@
[filtered-objects show-more filter-component]
(use-search page objects)
searching? (some? filtered-objects)
intersection-callback
(fn [entries]
(when (and (.-isIntersecting (first entries)) (some? show-more))
@ -864,8 +882,12 @@
(mf/use-fn
#(st/emit! (dw/toggle-focus-mode)))]
(sc/use-restore-scroll scroll-store :layers page-id tree-ref)
(sc/use-restore-scroll scroll-store :layers-search page-id search-ref)
(sc/use-restore-scroll scroll-store :layers page-id tree-ref (not searching?))
(sc/use-restore-scroll scroll-store :layers-search page-id search-ref searching?)
(mf/with-effect [page-id searching?]
(when-not ^boolean searching?
(sc/forget-scroll! scroll-store [:layers-search page-id])))
[:div {:id "layers"
:class (stl/css :layers)

View File

@ -50,10 +50,11 @@
:show-content
:hide-in-viewer
;; Needed to disable/enable width/height
;; Needed to disable/enable width/height and x/y
;; otherwise the memo will not detect changes
:layout-item-h-sizing
:layout-item-v-sizing])
:layout-item-v-sizing
:layout-item-absolute])
(def ^:private generic-options
#{:size :position :rotation})
@ -161,6 +162,8 @@
(get n-values :layout-item-h-sizing))
(identical? (get o-values :layout-item-v-sizing)
(get n-values :layout-item-v-sizing))
(identical? (get o-values :layout-item-absolute)
(get n-values :layout-item-absolute))
(identical? (get o-values :points)
(get n-values :points))
(identical? (get o-values :selrect)
@ -396,10 +399,8 @@
(mf/deps ids shapes)
(fn [value attr]
(if (or (string? value) (number? value))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/update-dimensions-coalesced ids attr value))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwta/apply-token-from-input {:token (first value)
(st/emit! (udw/update-dimensions-coalesced ids attr value))
(st/emit! (dwta/apply-token-from-input {:token (first value)
:attrs #{attr}
:shape-ids ids})))))
@ -416,11 +417,8 @@
(mf/deps ids)
(fn [value attr]
(if (or (string? value) (number? value))
(do
(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/apply-token-from-input {:token (first value)
(st/emit! (udw/update-positions ids {attr value}))
(st/emit! (dwta/apply-token-from-input {:token (first value)
:attrs #{attr}
:shape-ids ids})))))
@ -430,10 +428,8 @@
(fn [value]
(if (or (string? value) (number? value))
(let [value (fixed-decimal-value value)]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/increase-rotation-coalesced ids value)))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwta/apply-token-from-input {:token (first value)
(st/emit! (dwta/apply-token-from-input {:token (first value)
:attrs #{:rotation}
:shape-ids ids})))))

View File

@ -27,6 +27,12 @@
(when-let [target (dom/get-target event)]
(swap! store* assoc k (.-scrollTop target))))
(defn forget-scroll!
"Drops the position saved under k, so the next restore starts at the
top. Plain swap!, never triggers renders."
[store* k]
(swap! store* dissoc k))
(defn needs-restore-retry?
"Pure decision for the restore loop: keep retrying while the content
height is still changing (lazily rendered lists) and attempts remain."
@ -59,12 +65,19 @@
panel or id change. Retries while the content height keeps changing so
deep positions in lazily rendered lists are not clamped to the first
chunk. With no saved position the node goes to the top. Missing node is
a silent no-op."
[store* panel id node-ref]
(mf/with-effect [panel id]
(let [raf* (volatile! nil)]
(when-let [node (mf/ref-val node-ref)]
(restore-position! node (get @store* [panel id]) raf*))
(fn []
(when-some [raf @raf*]
(tm/cancel-af! raf))))))
a silent no-op.
`active?` is for panels that swap lists in place: pass whether the list
behind node-ref is shown, so its position is restored again each time
it comes back. Nothing is restored while it is false."
([store* panel id node-ref]
(use-restore-scroll store* panel id node-ref true))
([store* panel id node-ref active?]
(mf/with-effect [panel id active?]
(let [raf* (volatile! nil)]
(when ^boolean active?
(when-let [node (mf/ref-val node-ref)]
(restore-position! node (get @store* [panel id]) raf*)))
(fn []
(when-some [raf @raf*]
(tm/cancel-af! raf)))))))

View File

@ -136,7 +136,7 @@
[:> search-bar* {:on-change on-search-term-change-2
:on-clear on-search-clear-click
:value filter-term
:placeholder (tr "shortcuts.title")
:placeholder (tr "shortcuts.search")
:icon-id i/search
:auto-focus true}]]

View File

@ -35,6 +35,17 @@
(update fill :fill-opacity #(* (or % 1) edit-fill-opacity)))
fills))
(defn draw-area-kind
"Returns the overlay to render for the drawing object `shape`:
`:path`, `:curve`, `:generic` or nil. Non-path objects always get the
generic overlay, because the active tool can change while a box
drawing is still in progress."
[tool shape]
(cond
(not= :path (dm/get-prop shape :type)) :generic
(= tool :path) :path
(= tool :curve) :curve))
(defn path-edit-shape
"Builds the path shape rendered during editing."
[drawing-obj stored]
@ -98,15 +109,8 @@
[:g {:style {:pointer-events "none"}}
[:& shapes/shape-wrapper {:shape render-shape}]]
(cond
(= tool :path)
[:> path-draw-area* props]
(= tool :curve)
[:& path-shape {:shape shape :zoom zoom}]
(= (:type shape) :path)
nil
:else
[:> generic-draw-area* props])]))
(case (draw-area-kind tool shape)
:path [:> path-draw-area* props]
:curve [:& path-shape {:shape shape :zoom zoom}]
:generic [:> generic-draw-area* props]
nil)]))

View File

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.stroke :as cts]
[app.plugins.image-data :refer [create-image-data]]
[app.util.object :as obj]))
@ -211,8 +212,9 @@
(defn format-stroke
[{:keys [stroke-color stroke-color-ref-file stroke-color-ref-id
stroke-opacity stroke-style stroke-width stroke-alignment
stroke-cap-start stroke-cap-end stroke-color-gradient
stroke-image] :as stroke}]
stroke-width-top stroke-width-right stroke-width-bottom
stroke-width-left stroke-cap-start stroke-cap-end
stroke-color-gradient stroke-image] :as stroke}]
(when (some? stroke)
(obj/without-empty
@ -222,6 +224,11 @@
:strokeOpacity stroke-opacity
:strokeStyle (format-key stroke-style)
:strokeWidth stroke-width
:strokeWidthType (format-key (cts/width-type stroke))
:strokeWidthTop stroke-width-top
:strokeWidthRight stroke-width-right
:strokeWidthBottom stroke-width-bottom
:strokeWidthLeft stroke-width-left
:strokeAlignment (format-key stroke-alignment)
:strokeCapStart (format-key stroke-cap-start)
:strokeCapEnd (format-key stroke-cap-end)

View File

@ -11,6 +11,7 @@
[app.common.json :as json]
[app.common.types.path :as path]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.stroke :as cts]
[app.common.uuid :as uuid]
[app.util.object :as obj]
[cuerdas.core :as str]))
@ -223,18 +224,34 @@
(defn parse-stroke
[^js stroke]
(when (some? stroke)
(d/without-nils
{:stroke-color (-> (obj/get stroke "strokeColor") parse-hex)
:stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id)
:stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id)
:stroke-opacity (obj/get stroke "strokeOpacity")
:stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword)
:stroke-width (obj/get stroke "strokeWidth")
:stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword)
:stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword)
:stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword)
:stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)
:stroke-image (-> (obj/get stroke "strokeImage") parse-image-data)})))
(let [raw (obj/get stroke "$state")
parsed (d/without-nils
{:stroke-color (-> (obj/get stroke "strokeColor") parse-hex)
:stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id)
:stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id)
:stroke-opacity (obj/get stroke "strokeOpacity")
:stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword)
:stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword)
:stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword)
:stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword)
:stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)
:stroke-image (-> (obj/get stroke "strokeImage") parse-image-data)})]
(if (map? raw)
;; Reads the proxy's stored widths without replaying its public getters.
(merge parsed (select-keys raw [:stroke-width
:stroke-width-top :stroke-width-right
:stroke-width-bottom :stroke-width-left]))
(let [width (obj/get stroke "strokeWidth")
top (obj/get stroke "strokeWidthTop")
right (obj/get stroke "strokeWidthRight")
bottom (obj/get stroke "strokeWidthBottom")
left (obj/get stroke "strokeWidthLeft")]
(cond-> parsed
(some? width) (cts/set-width-to-all-sides width)
(some? top) (cts/set-width-to-single-side :stroke-width-top top)
(some? right) (cts/set-width-to-single-side :stroke-width-right right)
(some? bottom) (cts/set-width-to-single-side :stroke-width-bottom bottom)
(some? left) (cts/set-width-to-single-side :stroke-width-left left)))))))
(defn parse-strokes
[^js strokes]

View File

@ -31,7 +31,9 @@
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.radius :as ctsr]
[app.common.types.shape.shadow :as ctss]
[app.common.types.stroke :as types.stroke]
[app.common.types.text :as txt]
[app.common.types.token :as ctt]
[app.common.uuid :as uuid]
[app.main.data.exports.assets :as de]
[app.main.data.exports.wasm :as wasm.exports]
@ -64,7 +66,7 @@
[app.plugins.strokes :as strokes]
[app.plugins.system-events :as se]
[app.plugins.text :as text]
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr? valid-token-resolution?]]
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens expand-token-attrs token-attr-plugin->token-attr token-attr? valid-token-resolution?]]
[app.plugins.utils :as u]
[app.util.http :as http]
[app.util.object :as obj]
@ -273,21 +275,27 @@
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))
(defn commit-strokes!
[plugin-id ^js self value]
(let [id (obj/get self "$id")
value (parser/parse-strokes value)]
(cond
(not (sm/validate [:vector cts/schema:stroke] value))
(u/not-valid plugin-id :strokes value)
([plugin-id self value]
(commit-strokes! plugin-id self value nil))
([plugin-id ^js self value options]
(let [id (obj/get self "$id")
value (parser/parse-strokes value)]
(cond
(not (sm/validate [:vector cts/schema:stroke] value))
(u/not-valid plugin-id :strokes value)
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
(not (u/page-active? (obj/get self "$page")))
(u/not-valid plugin-id :strokes "Cannot modify a page that is not currently active")
(not (u/page-active? (obj/get self "$page")))
(u/not-valid plugin-id :strokes "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))
(and (not (ctt/per-side-stroke-shape? (:type (u/proxy->shape self))))
(some #(= :multiple (types.stroke/width-type %)) value))
(u/not-valid plugin-id :strokes "Per-side stroke widths require a board or rectangle")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value) options))))))
(defn commit-shadows!
[plugin-id ^js self value]
@ -992,8 +1000,12 @@
:strokes
{:this true
:get (fn [^js self]
(strokes/format-strokes (-> self u/proxy->shape :strokes)
#(commit-strokes! plugin-id self %)))
(let [shape (u/proxy->shape self)]
(strokes/format-strokes
(:strokes shape)
#(commit-strokes! plugin-id self %1 %2)
{:per-side-allowed? (ctt/per-side-stroke-shape? (:type shape))
:not-valid (fn [code value] (u/not-valid plugin-id code value))})))
:set (fn [self value] (commit-strokes! plugin-id self value))}
:layoutChild
@ -1782,7 +1794,7 @@
(let [set-id (obj/get token "$set-id")
token-id (obj/get token "$id")
token (u/locate-token file-id set-id token-id)
kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))]
kw-attrs (expand-token-attrs attrs)]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :applyToken "Plugin doesn't have 'content:write' permission")

View File

@ -6,78 +6,121 @@
(ns app.plugins.strokes
(:require
[app.common.types.stroke :as cts]
[app.plugins.format :as format]
[app.plugins.gradients :as gradients]
[app.plugins.parser :as parser]
[app.util.object :as obj]))
(defn- set-side-width!
"Sets one side width and reports the edited attribute to the commit callback.
Rejects shape types that do not support per-side widths."
[state attr value on-change! per-side-allowed? not-valid]
(if (and (false? per-side-allowed?) (fn? not-valid))
(not-valid attr value)
(do
(swap! state #(cts/set-width-to-single-side % attr value))
(on-change! [attr]))))
(defn stroke-proxy
[stroke-data on-change!]
(let [state (atom stroke-data)]
(obj/reify {:name "StrokeProxy"}
:strokeColor
{:get (fn [] (:stroke-color @state))
:set (fn [v]
(swap! state #(-> % (assoc :stroke-color v) (dissoc :stroke-color-gradient :stroke-image)))
(on-change!))}
([stroke-data on-change!]
(stroke-proxy stroke-data on-change! nil))
([stroke-data on-change! {:keys [per-side-allowed? not-valid]}]
(let [state (atom stroke-data)]
(obj/reify {:name "StrokeProxy"}
;; The parser reads stored widths without invoking fallback getters.
:$state {:enumerable false :get (fn [] @state)}
:strokeColorRefFile
{:get (fn [] (format/format-id (:stroke-color-ref-file @state)))
:set (fn [v] (swap! state assoc :stroke-color-ref-file (parser/parse-id v)) (on-change!))}
:strokeColor
{:get (fn [] (:stroke-color @state))
:set (fn [v]
(swap! state #(-> % (assoc :stroke-color v) (dissoc :stroke-color-gradient :stroke-image)))
(on-change! [:stroke-color]))}
:strokeColorRefId
{:get (fn [] (format/format-id (:stroke-color-ref-id @state)))
:set (fn [v] (swap! state assoc :stroke-color-ref-id (parser/parse-id v)) (on-change!))}
:strokeColorRefFile
{:get (fn [] (format/format-id (:stroke-color-ref-file @state)))
:set (fn [v] (swap! state assoc :stroke-color-ref-file (parser/parse-id v)) (on-change! [:stroke-color]))}
:strokeOpacity
{:get (fn [] (:stroke-opacity @state))
:set (fn [v] (swap! state assoc :stroke-opacity v) (on-change!))}
:strokeColorRefId
{:get (fn [] (format/format-id (:stroke-color-ref-id @state)))
:set (fn [v] (swap! state assoc :stroke-color-ref-id (parser/parse-id v)) (on-change! [:stroke-color]))}
:strokeStyle
{:get (fn [] (format/format-key (:stroke-style @state)))
:set (fn [v] (swap! state assoc :stroke-style (parser/parse-keyword v)) (on-change!))}
:strokeOpacity
{:get (fn [] (:stroke-opacity @state))
:set (fn [v] (swap! state assoc :stroke-opacity v) (on-change! [:stroke-color]))}
:strokeWidth
{:get (fn [] (:stroke-width @state))
:set (fn [v] (swap! state assoc :stroke-width v) (on-change!))}
:strokeStyle
{:get (fn [] (format/format-key (:stroke-style @state)))
:set (fn [v] (swap! state assoc :stroke-style (parser/parse-keyword v)) (on-change! [:stroke-style]))}
:strokeAlignment
{:get (fn [] (format/format-key (:stroke-alignment @state)))
:set (fn [v] (swap! state assoc :stroke-alignment (parser/parse-keyword v)) (on-change!))}
:strokeWidth
{:get (fn [] (:stroke-width @state))
:set (fn [v]
(swap! state #(cts/set-width-to-all-sides % v))
(on-change! [:stroke-width]))}
:strokeCapStart
{:get (fn [] (format/format-key (:stroke-cap-start @state)))
:set (fn [v] (swap! state assoc :stroke-cap-start (parser/parse-keyword v)) (on-change!))}
:strokeWidthType
{:get (fn [] (format/format-key (cts/width-type @state)))}
:strokeCapEnd
{:get (fn [] (format/format-key (:stroke-cap-end @state)))
:set (fn [v] (swap! state assoc :stroke-cap-end (parser/parse-keyword v)) (on-change!))}
:strokeWidthTop
{:get (fn [] (cts/side-width @state :stroke-width-top))
:set (fn [v] (set-side-width! state :stroke-width-top v on-change! per-side-allowed? not-valid))}
:strokeColorGradient
{:get (fn []
(when-let [gradient (:stroke-color-gradient @state)]
(let [gradient-state (atom gradient)
gradient-change! (fn []
(swap! state assoc :stroke-color-gradient @gradient-state)
(on-change!))]
(gradients/gradient-proxy gradient-state gradient-change!))))
:set (fn [v]
(swap! state #(-> % (assoc :stroke-color-gradient (parser/parse-gradient v)) (dissoc :stroke-color :stroke-image)))
(on-change!))}
:strokeWidthRight
{:get (fn [] (cts/side-width @state :stroke-width-right))
:set (fn [v] (set-side-width! state :stroke-width-right v on-change! per-side-allowed? not-valid))}
:strokeImage
{:get (fn [] (format/format-image (:stroke-image @state)))
:set (fn [v]
(swap! state #(-> % (assoc :stroke-image (parser/parse-image-data v)) (dissoc :stroke-color :stroke-color-gradient)))
(on-change!))})))
:strokeWidthBottom
{:get (fn [] (cts/side-width @state :stroke-width-bottom))
:set (fn [v] (set-side-width! state :stroke-width-bottom v on-change! per-side-allowed? not-valid))}
:strokeWidthLeft
{:get (fn [] (cts/side-width @state :stroke-width-left))
:set (fn [v] (set-side-width! state :stroke-width-left v on-change! per-side-allowed? not-valid))}
:strokeAlignment
{:get (fn [] (format/format-key (:stroke-alignment @state)))
:set (fn [v] (swap! state assoc :stroke-alignment (parser/parse-keyword v)) (on-change! [:stroke-alignment]))}
:strokeCapStart
{:get (fn [] (format/format-key (:stroke-cap-start @state)))
:set (fn [v] (swap! state assoc :stroke-cap-start (parser/parse-keyword v)) (on-change! [:stroke-cap-start]))}
:strokeCapEnd
{:get (fn [] (format/format-key (:stroke-cap-end @state)))
:set (fn [v] (swap! state assoc :stroke-cap-end (parser/parse-keyword v)) (on-change! [:stroke-cap-end]))}
:strokeColorGradient
{:get (fn []
(when-let [gradient (:stroke-color-gradient @state)]
(let [gradient-state (atom gradient)
gradient-change! (fn []
(swap! state assoc :stroke-color-gradient @gradient-state)
(on-change! [:stroke-color]))]
(gradients/gradient-proxy gradient-state gradient-change!))))
:set (fn [v]
(swap! state #(-> % (assoc :stroke-color-gradient (parser/parse-gradient v)) (dissoc :stroke-color :stroke-image)))
(on-change! [:stroke-color]))}
:strokeImage
{:get (fn [] (format/format-image (:stroke-image @state)))
:set (fn [v]
(swap! state #(-> % (assoc :stroke-image (parser/parse-image-data v)) (dissoc :stroke-color :stroke-color-gradient)))
(on-change! [:stroke-color]))}))))
(defn format-strokes
([strokes] (format-strokes strokes nil))
([strokes commit-fn]
([strokes] (format-strokes strokes nil nil))
([strokes commit-fn] (format-strokes strokes commit-fn nil))
([strokes commit-fn options]
(if (and (some? strokes) (fn? commit-fn))
(let [arr-ref (atom nil)
on-change! (fn [] (commit-fn @arr-ref))
arr (apply array (mapv #(stroke-proxy % on-change!) strokes))]
on-change! (fn [index attrs]
(commit-fn @arr-ref {:changed-sub-attr attrs :changed-item-index index}))
arr (apply array
(map-indexed
(fn [index stroke]
(stroke-proxy stroke (partial on-change! index) options))
strokes))]
(reset! arr-ref arr)
arr)
(format/format-array format/format-stroke strokes))))

View File

@ -79,15 +79,38 @@
(let [k (cond-> k (string? k) keyword)]
(get map:token-attr-plugin->token-attr k k)))
(def ^:private stroke-width-alias
"Uniform stroke width alias that expands to all four side token attributes."
:stroke-width)
(defn expand-token-attrs
"Resolves token attributes to internal keywords and expands `:stroke-width`
to all four side attributes."
[attrs]
(let [attrs (into #{} (map token-attr-plugin->token-attr) attrs)]
(if (contains? attrs stroke-width-alias)
(into (disj attrs stroke-width-alias) cto/per-side-stroke-width-keys)
attrs)))
(defn applied-tokens-plugin->applied-tokens
[value]
(into {}
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
value))
(let [value (into {}
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
value)
per-side (map #(get value %)
[:stroke-width-top :stroke-width-right
:stroke-width-bottom :stroke-width-left])]
(cond-> value
;; Exposes `strokeWidth` when all four sides share the same token.
(and (every? some? per-side)
(apply = per-side))
(assoc :stroke-width (first per-side)))))
(defn token-attr?
[attr]
(cto/token-attr? (token-attr-plugin->token-attr attr)))
(let [attr (token-attr-plugin->token-attr attr)]
(or (= attr stroke-width-alias)
(cto/token-attr? attr))))
(defn- token-name-schema
[file-id set-id token]
@ -180,7 +203,7 @@
:else
(st/emit!
(-> (dwta/toggle-token {:token token
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
:attrs (expand-token-attrs attrs)
:shape-ids shape-ids
:expand-with-children false})
(se/add-event plugin-id)))))))

View File

@ -0,0 +1,75 @@
;; 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 SUBSIDIARY SL
(ns frontend-tests.data.fix-deleted-fonts-test
(:require
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths]
[app.common.types.shape :as cts]
[app.common.types.text :as txt]
[app.main.data.changes :as dwc]
[app.main.data.workspace.fix-deleted-fonts :as fdf]
[app.main.fonts :as fonts]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
[potok.v2.core :as ptk]))
(t/use-fixtures :each
{:before cthi/reset-idmap!})
(def ^:private missing-font-id
"custom-missing-font-id")
(defn- text-content-with-font
[text font-id font-family]
(-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
:content
(txt/change-text text :font-id font-id :font-family font-family)))
(defn- file-with-text-shape
[font-id font-family]
(-> (cthf/sample-file :file1 :page-label :page1)
(cths/add-sample-shape :text
{:type :text
:content (text-content-with-font "TEST"
font-id
font-family)})))
(defn- watch-fix-deleted-fonts-events
[file]
(let [events (atom [])
store (ths/setup-store file)
state @store]
(when-let [result (ptk/watch (fdf/fix-deleted-fonts-for-page (:id file)
(cthf/current-page-id file))
state
(rx/empty))]
(->> result (rx/subs! #(swap! events conj %))))
@events))
(t/deftest fix-deleted-fonts-skips-unfixable-missing-font
(let [file (file-with-text-shape missing-font-id "Gibson")
events (watch-fix-deleted-fonts-events file)]
(t/is (empty? events)
"missing custom fonts with no replacement must not commit changes")))
(t/deftest fix-deleted-fonts-remaps-when-replacement-exists
(let [replacement-id "custom-gibson-replacement"
cleanup #(swap! fonts/fontsdb dissoc replacement-id)]
(try
(swap! fonts/fontsdb assoc replacement-id
{:id replacement-id
:family "Gibson"
:name "Gibson"
:variants []})
(let [file (file-with-text-shape missing-font-id "Gibson")
events (watch-fix-deleted-fonts-events file)]
(t/is (= 1 (count events)))
(t/is (ptk/type? ::dwc/commit-changes (first events))))
(finally
(cleanup)))))

View File

@ -25,6 +25,7 @@
[app.main.data.workspace.path.streams :as path.streams]
[app.main.data.workspace.path.tools :as path.tools]
[app.main.data.workspace.path.undo :as path.undo]
[app.main.data.workspace.shortcuts :as wsc]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
@ -138,22 +139,23 @@
(defn- run-handle-drawing-end
"Runs the draw-ending flow and passes its events to `callback`."
[restart? callback]
(let [state (pth/drawing-path-state)
stream (rx/subject)
emissions (atom [])]
(->> (ptk/watch (path.drawing/handle-drawing) state stream)
(rx/subs! #(swap! emissions conj %)))
(rx/push! stream (ptk/data-event ::path.drawing/end-edition
{:restart? restart?}))
;; Wait for the asynchronous drawing-end event.
(js/setTimeout
(fn []
(let [end-event (last @emissions)
end-emissions (atom [])]
(->> (ptk/watch end-event state stream)
(rx/subs! #(swap! end-emissions conj %)))
(callback @end-emissions))))))
([restart? callback]
(run-handle-drawing-end (pth/drawing-path-state) restart? callback))
([state restart? callback]
(let [stream (rx/subject)
emissions (atom [])]
(->> (ptk/watch (path.drawing/handle-drawing) state stream)
(rx/subs! #(swap! emissions conj %)))
(rx/push! stream (ptk/data-event ::path.drawing/end-edition
{:restart? restart?}))
;; Wait for the asynchronous drawing-end event.
(js/setTimeout
(fn []
(let [end-event (last @emissions)
end-emissions (atom [])]
(->> (ptk/watch end-event state stream)
(rx/subs! #(swap! end-emissions conj %)))
(callback @end-emissions)))))))
(t/deftest escape-ending-new-path-draw-does-not-reenter-edition
(t/async
@ -182,6 +184,43 @@
(mapv ptk/type emissions)))
(done)))))
(t/deftest finishing-new-path-draw-in-move-mode-reenters-edition-in-move-mode
(t/async
done
(let [state (pth/drawing-path-state)
id (get-in state [:workspace-drawing :object :id])
state (assoc-in state [:workspace-local :edit-path id :edit-mode] :move)]
(run-handle-drawing-end
state
true
(fn [emissions]
(let [edition-emissions (atom [])]
(->> (ptk/watch (last emissions) state nil)
(rx/subs! #(swap! edition-emissions conj %)))
(let [change-mode (last @edition-emissions)
edited {:workspace-local {:edition id
:edit-path {id {:edit-mode :draw}}}}]
(t/is (= ::path.drawing/change-edit-mode (ptk/type change-mode)))
(t/is (= :move (get-in (ptk/update change-mode edited)
[:workspace-local :edit-path id :edit-mode])))))
(done))))))
(t/deftest move-mode-while-creating-path-finishes-it
(let [id (random-uuid)
state {:workspace-local
{:edit-path {id {:edit-mode :draw
:last-point (gpt/point 10 10)}}}
:workspace-drawing {:tool :path
:object {:id id :type :path}}}
event (path.drawing/change-edit-mode :move)
state' (ptk/update event state)
emissions (atom [])]
(t/is (= :move (get-in state' [:workspace-local :edit-path id :edit-mode])))
(->> (ptk/watch event state' nil)
(rx/subs! #(swap! emissions conj %)))
(t/is (= [::path.common/finish-path]
(mapv ptk/type @emissions)))))
(t/deftest ending-a-draw-collapses-the-nodes-drawn-on-top-of-each-other
(t/async
done
@ -237,6 +276,12 @@
(t/is (= [::path.common/finish-path]
(mapv ptk/type @emissions)))))
(t/deftest deselect-all-shortcut-replaces-the-grid-layout-toggle
(let [deselect-all (:deselect-all path.shortcuts/shortcuts)
grid-toggle (:toggle-layout-grid wsc/shortcuts)]
(t/is (= (:command grid-toggle) (:command deselect-all)))
(t/is (true? (:overwrite deselect-all)))))
(t/deftest escape-without-pending-segment-interrupts-edition
(let [id (random-uuid)
state {:workspace-local

View File

@ -9,6 +9,7 @@
[app.common.math :as m]
[app.common.test-helpers.files :as cthf]
[app.common.uuid :as uuid]
[app.main.data.workspace.colors :as dwc]
[app.main.data.workspace.reflow :as wrf]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwtxt]
@ -254,11 +255,49 @@
(t/testing " - strokes"
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5}]))
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5}]))
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeColor")) "#fabada"))
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeOpacity")) 1))
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidth")) 5)))
(t/testing " - strokes per-side widths"
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeWidth 5}])
(t/is (= 5 (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidthTop")))
"a uniform stroke reads the global width on every side")
(obj/set! (aget (.-strokes shape) 0) "strokeWidthRight" 9)
(t/is (= 9 (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidthRight"))))
(t/is (= 5 (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidthTop"))))
(t/is (= 9 (get-in @store (conj (get-shape-path :strokes) 0 :stroke-width-right)))
"the edited side is stored")
(t/is (= 5 (get-in @store (conj (get-shape-path :strokes) 0 :stroke-width-top)))
"the untouched sides keep their uniform width"))
(t/testing " - UI toggles preserve equal side widths and their computed mode"
(set! (.-strokes shape)
#js [#js {:strokeColor "#fabada" :strokeWidth 1
:strokeWidthTop 8 :strokeWidthRight 8
:strokeWidthBottom 8 :strokeWidthLeft 8}])
(t/is (not (contains? (get-in @store (conj (get-shape-path :strokes) 0)) :stroke-width-type)))
(t/is (= 8 (get-in @store (conj (get-shape-path :strokes) 0 :stroke-width))))
(ptk/emit! store (dwc/toggle-stroke-per-side [(aget shape "$id")] 0))
(t/is (true? (get-in @store [:workspace-local :stroke-per-side [[(aget shape "$id")] 0]])))
(t/is (= "simple" (aget (aget (.-strokes shape) 0) "strokeWidthType")))
(ptk/emit! store (dwc/toggle-stroke-per-side [(aget shape "$id")] 0))
(t/is (= "simple" (aget (aget (.-strokes shape) 0) "strokeWidthType")))
(t/is (false? (get-in @store [:workspace-local :stroke-per-side [[(aget shape "$id")] 0]])))
(t/is (= [8 8 8 8]
(mapv (get-in @store (conj (get-shape-path :strokes) 0))
[:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left]))))
(t/testing " - per-side widths rejected on unsupported shapes"
(let [^js ellipse (.createEllipse context)]
(set! (.-strokes ellipse) #js [#js {:strokeColor "#fabada" :strokeWidth 1}])
(t/is (thrown? js/Error
(obj/set! (aget (.-strokes ellipse) 0) "strokeWidthTop" 4)))
(t/is (thrown? js/Error
(set! (.-strokes ellipse) #js [#js {:strokeColor "#fabada" :strokeWidthTop 4}])))))
(t/testing " - fills per-element property mutation (bug #8357)"
(set! (.-fills shape) #js [#js {:fillColor "#fabada" :fillOpacity 1}])
(obj/set! (aget (.-fills shape) 0) "fillColor" "#ff0000")
@ -287,13 +326,15 @@
(t/testing " - strokes per-element property mutation (bug #8357)"
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
(obj/set! (aget (.-strokes shape) 0) "strokeColor" "#0000ff")
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#0000ff" :stroke-opacity 1 :stroke-width 5}])))
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#0000ff" :stroke-opacity 1 :stroke-width 5
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5}])))
(t/testing " - strokes gradient assignment replaces solid color (bug #8357)"
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
(obj/set! (aget (.-strokes shape) 0) "strokeColorGradient" (gradient))
(t/is (= (get-in @store (get-shape-path :strokes))
[{:stroke-opacity 1 :stroke-width 5 :stroke-color-gradient parsed-gradient}])))
[{:stroke-opacity 1 :stroke-width 5
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5 :stroke-color-gradient parsed-gradient}])))
(t/testing " - strokes nested gradient mutation (bug #8357)"
(set! (.-strokes shape) #js [#js {:strokeColorGradient (gradient) :strokeOpacity 1 :strokeWidth 5}])
@ -304,6 +345,7 @@
(t/is (= (get-in @store (get-shape-path :strokes))
[{:stroke-opacity 1
:stroke-width 5
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5
:stroke-color-gradient (-> parsed-gradient
(assoc :end-y 0.75)
(assoc-in [:stops 1 :opacity] 0.25))}])))))

View File

@ -55,3 +55,16 @@
(t/deftest test-shape-type-reports-boolean
(t/is (= "boolean" (format/shape-type :bool))))
(t/deftest test-format-stroke-exposes-per-side-widths
(let [stroke (format/format-stroke {:stroke-color "#000000"
:stroke-width 1
:stroke-width-top 2
:stroke-width-right 3
:stroke-width-bottom 4
:stroke-width-left 5})]
(t/is (= 1 (aget stroke "strokeWidth")))
(t/is (= 2 (aget stroke "strokeWidthTop")))
(t/is (= 3 (aget stroke "strokeWidthRight")))
(t/is (= 4 (aget stroke "strokeWidthBottom")))
(t/is (= 5 (aget stroke "strokeWidthLeft")))))

View File

@ -116,6 +116,10 @@
["ShapeProxy.rotation" #(set! (.-rotation rect) 45)]
["ShapeProxy.fills" #(set! (.-fills rect) #js [#js {:fillColor "#fabada" :fillOpacity 1}])]
["ShapeProxy.strokes" #(set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 2}])]
["ShapeProxy.strokeWidthTop" #(do (set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeWidth 2}])
(set! (.-strokeWidthTop (aget (.-strokes rect) 0)) 5))]
["ShapeProxy.strokeWidthRight" #(do (set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeWidth 2}])
(set! (.-strokeWidthRight (aget (.-strokes rect) 0)) 5))]
;; relative geometry (shape inside a board)
["ShapeProxy.boardX" #(set! (.-boardX child) 10)]
["ShapeProxy.boardY" #(set! (.-boardY child) 10)]

View File

@ -11,7 +11,9 @@
[app.common.types.grid :as ctg]
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.plugins.format :as format]
[app.plugins.parser :as parser]
[app.plugins.strokes :as strokes]
[cljs.test :as t :include-macros true]))
(defn- overlay-action
@ -193,3 +195,70 @@
(t/testing "a malformed id raises, so plugin bugs stay visible"
(t/is (thrown? js/Error (parser/parse-id "not-a-uuid")))))
(t/deftest test-parse-stroke-maps-per-side-widths
(let [stroke (parser/parse-stroke
#js {:strokeColor "#000000"
:strokeWidth 1
:strokeWidthTop 2
:strokeWidthRight 3
:strokeWidthBottom 4
:strokeWidthLeft 5})]
(t/is (= 2 (:stroke-width-top stroke)))
(t/is (= 3 (:stroke-width-right stroke)))
(t/is (= 4 (:stroke-width-bottom stroke)))
(t/is (= 5 (:stroke-width-left stroke))))
(t/testing "a uniform literal initializes all sides in simple mode"
(let [stroke (parser/parse-stroke #js {:strokeColor "#000000" :strokeWidth 1})]
(t/is (= "simple" (aget (format/format-stroke stroke) "strokeWidthType")))
(t/is (= 1 (:stroke-width-top stroke)))
(t/is (= 1 (:stroke-width-left stroke))))))
(t/deftest test-parse-stroke-applies-uniform-width-before-sides
(let [stroke (parser/parse-stroke
#js {:strokeColor "#000000"
:strokeWidthLeft 8
:strokeWidthBottom 8
:strokeWidthRight 8
:strokeWidthTop 8
:strokeWidth 1})]
(t/is (= "simple" (aget (format/format-stroke stroke) "strokeWidthType")))
(t/is (= 8 (:stroke-width stroke)))
(t/is (= [8 8 8 8]
(mapv stroke [:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))))
(t/deftest test-parse-stroke-top-preserves-other-widths
(let [stroke (parser/parse-stroke #js {:strokeColor "#000000" :strokeWidth 5 :strokeWidthTop 9})]
(t/is (= [9 5 5 5]
(mapv stroke [:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))))
(t/deftest test-stroke-proxy-mode-transitions
(let [^js stroke (strokes/stroke-proxy {:stroke-color "#000000" :stroke-width 1} (fn [_] nil))]
(t/is (= "simple" (.-strokeWidthType stroke)))
(set! (.-strokeWidthTop stroke) 8)
(t/is (= "multiple" (.-strokeWidthType stroke)))
(t/is (= 1 (.-strokeWidthRight stroke)))
(set! (.-strokeWidthRight stroke) 8)
(set! (.-strokeWidthBottom stroke) 8)
(set! (.-strokeWidthLeft stroke) 8)
(t/is (= "simple" (.-strokeWidthType stroke)))
(t/is (= 8 (.-strokeWidth stroke)))
(set! (.-strokeColor stroke) "#ff0000")
(t/is (not (contains? (parser/parse-stroke stroke) :stroke-width-type)))
(set! (.-strokeWidth stroke) 3)
(t/is (= "simple" (.-strokeWidthType stroke)))
(t/is (= [3 3 3 3]
(mapv #(aget stroke %) ["strokeWidthTop" "strokeWidthRight" "strokeWidthBottom" "strokeWidthLeft"])))
(set! (.-strokeWidthRight stroke) 9)
(t/is (= "multiple" (.-strokeWidthType stroke)))
(set! (.-strokeWidth stroke) 3)
(t/is (= [3 3 3 3]
(mapv #(aget stroke %) ["strokeWidthTop" "strokeWidthRight" "strokeWidthBottom" "strokeWidthLeft"])))
(t/is (= "simple" (.-strokeWidthType stroke)))))
(t/deftest test-stroke-mode-round-trips-through-format
(doseq [right [8 12]]
(let [stroke (parser/parse-stroke
#js {:strokeColor "#000000" :strokeWidth 8 :strokeWidthRight right})]
(t/is (= stroke (parser/parse-stroke (format/format-stroke stroke)))))))

View File

@ -11,17 +11,21 @@
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.tokens :as ctht]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
[app.common.uuid :as uuid]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.register :as r]
[app.plugins.tokens :as ptok]
[app.plugins.utils :as u]
[app.util.object :as obj]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.async :as tha]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw]
@ -35,6 +39,73 @@
(def ^:private get-resolved-value @#'ptok/get-resolved-value)
(t/deftest ^:async stroke-property-edits-only-remove-related-token-bindings
(let [tokens {:stroke-color "border.color"
:stroke-width-top "border.top"
:stroke-width-right "border.right"
:stroke-width-bottom "border.bottom"
:stroke-width-left "border.left"}
cases [[0 "strokeWidthTop" 8 #{:stroke-width-top}]
[0 "strokeWidthRight" 8 #{:stroke-width-right}]
[0 "strokeWidthBottom" 8 #{:stroke-width-bottom}]
[0 "strokeWidthLeft" 8 #{:stroke-width-left}]
[0 "strokeWidth" 8 ctt/per-side-stroke-width-keys]
[0 "strokeColor" "#ff0000" #{:stroke-color}]
[0 "strokeOpacity" 0.5 #{:stroke-color}]
[0 "strokeStyle" "dashed" #{}]
[0 "strokeAlignment" "center" #{}]
[1 "strokeWidthTop" 8 #{}]
[1 "strokeWidth" 8 #{}]
[1 "strokeColor" "#ff0000" #{}]]]
(doseq [[index property value removed] cases]
(t/testing (str "stroke " index " property " property)
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
_ (set! st/state store)
^js ctx (api/create-context "00000000-0000-0000-0000-000000000000")
^js rect (.createRectangle ctx)
id (obj/get rect "$id")
path [:files (:current-file-id @store) :data :pages-index
(:current-page-id @store) :objects id :applied-tokens]]
(set! (.-strokes rect)
#js [#js {:strokeColor "#000000" :strokeWidth 1 :strokeWidthRight 2}
#js {:strokeColor "#000000" :strokeWidth 1}])
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
(obj/set! (aget (.-strokes rect) index) property value)
(await (tha/settle))
(t/is (= (apply dissoc tokens removed) (get-in @store path)))
(t/is (= value (obj/get (aget (.-strokes rect) index) property))))))))
(t/deftest ^:async stroke-gradient-edits-preserve-width-token-bindings
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
_ (set! st/state store)
^js ctx (api/create-context "00000000-0000-0000-0000-000000000000")
^js rect (.createRectangle ctx)
id (obj/get rect "$id")
path [:files (:current-file-id @store) :data :pages-index
(:current-page-id @store) :objects id :applied-tokens]
tokens {:stroke-color "border.color"
:stroke-width-top "border.width"
:stroke-width-right "border.width"
:stroke-width-bottom "border.width"
:stroke-width-left "border.width"}]
(set! (.-strokes rect)
#js [#js {:strokeWidth 1
:strokeColorGradient
#js {:type "linear" :startX 0 :startY 0 :endX 1 :endY 1 :width 1
:stops #js [#js {:color "#000000" :opacity 1 :offset 0}
#js {:color "#ffffff" :opacity 1 :offset 1}]}}])
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
(let [^js gradient (obj/get (aget (.-strokes rect) 0) "strokeColorGradient")]
(set! (.-startX gradient) 0.5)
(await (tha/settle))
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
(t/is (= 0.5 (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "startX")))
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
(obj/set! (aget (.-stops gradient) 0) "color" "#ff0000")
(await (tha/settle))
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
(t/is (= "#ff0000" (obj/get (aget (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "stops") 0) "color"))))))
;; Regression coverage for issue #9162.
;;
;; Plugin code calling `shape.applyToken(token, ["fill"])` or
@ -122,6 +193,25 @@
(t/is (true? (boolean (ptok/token-attr? "r1"))))
(t/is (true? (boolean (ptok/token-attr? "m3")))))
(t/deftest token-attr?-accepts-per-side-stroke-width
;; Accepts the uniform width alias and side attributes as keywords or strings.
(t/is (true? (boolean (ptok/token-attr? :stroke-width))))
(t/is (true? (boolean (ptok/token-attr? "stroke-width"))))
(t/is (true? (boolean (ptok/token-attr? :stroke-width-top))))
(t/is (true? (boolean (ptok/token-attr? "stroke-width-left")))))
(t/deftest expand-token-attrs-expands-uniform-stroke-width
(t/testing "the uniform alias becomes the four per-side attributes"
(t/is (= ctt/per-side-stroke-width-keys
(ptok/expand-token-attrs #{:stroke-width})))
(t/is (= ctt/per-side-stroke-width-keys
(ptok/expand-token-attrs #{"stroke-width"}))))
(t/testing "per-side and unrelated attributes pass through unchanged"
(t/is (= #{:stroke-width-top} (ptok/expand-token-attrs #{:stroke-width-top})))
(t/is (= #{:fill} (ptok/expand-token-attrs #{:fill})))
(t/is (= #{} (ptok/expand-token-attrs nil)))))
(t/deftest shape-apply-token-accepts-padding-top
(t/async
done
@ -807,4 +897,3 @@
(.addSet catalog #js {"name" "NewSet"})
(t/is (= 1 (count @errors)))
(t/is (= [plugin-id :addSet "Plugin doesn't have 'content:write' permission"] (first @errors)))))))

View File

@ -13,6 +13,7 @@
[frontend-tests.data.event-test]
[frontend-tests.data.exports-assets-test]
[frontend-tests.data.exports-files-test]
[frontend-tests.data.fix-deleted-fonts-test]
[frontend-tests.data.imports-test]
[frontend-tests.data.jobs-test]
[frontend-tests.data.nitrate-test]
@ -113,6 +114,7 @@
[frontend-tests.ui.comment-input-ime-test]
[frontend-tests.ui.comments-clustering-test]
[frontend-tests.ui.comments-position-modifier-test]
[frontend-tests.ui.drawarea-test]
[frontend-tests.ui.ds-controls-numeric-input-test]
[frontend-tests.ui.gradient-handlers-test]
[frontend-tests.ui.history-test]
@ -131,6 +133,7 @@
[frontend-tests.ui.sidebar-scroll-test]
[frontend-tests.ui.stroke-menu-test]
[frontend-tests.ui.text-attrs-multiple-test]
[frontend-tests.ui.workspace-libraries-test]
[frontend-tests.util-clipboard-test]
[frontend-tests.util-object-test]
[frontend-tests.util-queue-test]
@ -163,6 +166,7 @@
'frontend-tests.data.comments-filters-test
'frontend-tests.data.dashboard-test
'frontend-tests.data.event-test
'frontend-tests.data.fix-deleted-fonts-test
'frontend-tests.data.nitrate-test
'frontend-tests.data.persistence-retry-test
'frontend-tests.data.persistence-test
@ -263,6 +267,7 @@
'frontend-tests.ui.comment-input-ime-test
'frontend-tests.ui.comments-clustering-test
'frontend-tests.ui.comments-position-modifier-test
'frontend-tests.ui.drawarea-test
'frontend-tests.ui.ds-controls-numeric-input-test
'frontend-tests.ui.gradient-handlers-test
'frontend-tests.ui.history-test
@ -283,6 +288,7 @@
'frontend-tests.ui.sidebar-scroll-test
'frontend-tests.ui.stroke-menu-test
'frontend-tests.ui.text-attrs-multiple-test
'frontend-tests.ui.workspace-libraries-test
'frontend-tests.util-clipboard-test
'frontend-tests.util-object-test
'frontend-tests.util-queue-test

View File

@ -0,0 +1,28 @@
;; 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 SUBSIDIARY SL
(ns frontend-tests.ui.drawarea-test
(:require
[app.common.types.shape :as cts]
[app.main.ui.workspace.viewport.drawarea :as drawarea]
[cljs.test :as t :include-macros true]))
(def ^:private box-types
[:rect :frame :circle :text])
(t/deftest box-drawings-get-the-generic-overlay-for-any-tool
(doseq [type box-types
tool [:path :curve type nil]]
(let [shape (cts/setup-shape {:type type :x 10 :y 10 :width 0.01 :height 0.01})]
(t/is (= :generic (drawarea/draw-area-kind tool shape))
(str "type " type " with tool " tool)))))
(t/deftest path-drawings-follow-the-active-tool
(let [shape (cts/setup-shape {:type :path})]
(t/is (= :path (drawarea/draw-area-kind :path shape)))
(t/is (= :curve (drawarea/draw-area-kind :curve shape)))
(t/is (nil? (drawarea/draw-area-kind :line shape)))
(t/is (nil? (drawarea/draw-area-kind nil shape)))))

View File

@ -48,4 +48,11 @@
(t/testing "re-renders when width changes"
(t/is (false? (check-measures-menu-props
(props base-values)
(props (assoc base-values :width 150)))))))
(props (assoc base-values :width 150))))))
(t/testing "re-renders when absolute positioning changes but nothing else does"
;; Regression test: toggling static <-> absolute on a flex child must
;; force a re-render so the X/Y inputs enabled/disabled state updates.
(t/is (false? (check-measures-menu-props
(props (assoc base-values :layout-item-absolute false))
(props (assoc base-values :layout-item-absolute true)))))))

View File

@ -9,9 +9,11 @@
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.team :as dtm]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.routes :as routes]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]))
@ -95,3 +97,61 @@
(t/is (= 2 (count @events)))
(done'))
done))))
;; Template links (`#?template=<url>`) predate query-string routing: the
;; params live in a bare `#?…` fragment and there is no `screen` (#12127).
(def ^:private template-url
"https://penpot.github.io/penpot-files/tutorial-for-beginners%20v.2.0.penpot")
(def ^:private template-param
"The query param as the router decodes it."
"https://penpot.github.io/penpot-files/tutorial-for-beginners v.2.0.penpot")
(t/deftest legacy-query-hash-becomes-a-query-token
(t/is (= (str "?template=" template-url)
(#'routes/legacy-query-token (str "#?template=" template-url))))
(t/is (nil? (#'routes/legacy-query-token "#/workspace?team-id=1")))
(t/is (nil? (#'routes/legacy-query-token ""))))
(t/deftest query-without-screen-goes-to-dashboard-with-its-params
(t/async done
(let [team-id (uuid/next)
events (atom [])]
(mock/with-mocks
{rp/cmd! (mock/stub
(fn [command _]
(case command
:get-profile (rx/of {:id (uuid/next) :default-team-id team-id})
:get-teams (rx/of [{:id team-id}]))))
dtm/get-last-team-id (mock/stub (fn [] nil))
st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))}
(fn [done']
(#'routes/on-query-navigate #{:dashboard-recent} (str "?template=" template-url) false)
(let [{:keys [id params]} (some-> (first @events) deref)]
(t/is (= :dashboard-recent id))
(t/is (= template-param (:template params)))
(t/is (= team-id (:team-id params))))
(done'))
done))))
(t/deftest query-without-screen-keeps-template-for-after-login
(t/async done
(let [events (atom [])]
(mock/with-mocks
{rp/cmd! (mock/stub
(fn [command _]
(case command
:get-profile (rx/of {:id uuid/zero})
;; The backend requires a session for get-teams.
:get-teams (rx/throw (ex-info "authentication required"
{:type :authentication})))))
st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))}
(fn [done']
(#'routes/on-query-navigate #{:auth-login} (str "?template=" template-url) false)
(t/is (= template-param (:template @storage/session)))
(t/is (= :auth-login (:id (some-> (first @events) deref))))
(binding [storage/*sync* true]
(swap! storage/session dissoc :template))
(done'))
done))))

View File

@ -31,6 +31,14 @@
(sc/save-scroll! store [:layers :page-1] #js {})
(t/is (= {} @store))))
(t/deftest forget-scroll-drops-only-that-key
(let [store (atom {[:layers :page-1] 250
[:layers-search :page-1] 90})]
(sc/forget-scroll! store [:layers-search :page-1])
(t/is (= {[:layers :page-1] 250} @store))
(sc/forget-scroll! store [:layers-search :page-1])
(t/is (= {[:layers :page-1] 250} @store))))
(t/deftest restore-scroll-writes-saved-position
(let [node #js {:scrollTop 0}]
(sc/restore-scroll! node 320)

View File

@ -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 SUBSIDIARY SL
(ns frontend-tests.ui.workspace-libraries-test
(:require
[app.main.ui.workspace.libraries :as wl]
[cljs.test :as t :include-macros true]))
(defn- lib
"A linked library; `connected-to` lists the ids of the files using it."
[id indirect? connected-to]
{:id id :name (name id) :is-indirect indirect? :connected-to connected-to})
(defn- summarize
[libraries]
(mapv (juxt :id :nested? :parent-name) libraries))
(t/deftest sort-linked-libraries-cycle-with-direct-root
;; file -> App, Foundations; App -> Foundations;
;; Foundations <-> Comments (cycle, as left by an import)
(let [libraries [(lib :foundations false [:file :app :comments])
(lib :app false [:file])
(lib :comments true [:foundations])]
result (wl/sort-linked-libraries libraries)]
(t/is (= [[:app false nil]
[:foundations false nil]
[:comments true "foundations"]]
(summarize result)))
(t/is (= (count libraries) (count result)))))
(t/deftest sort-linked-libraries-cycle-without-root
;; The direct flag is still unknown (nil) while the shared files load,
;; so both libraries are only reachable through the cycle. `:app` is
;; not a linked library, so it is ignored as a parent.
(let [libraries [(lib :foundations nil [:app :comments])
(lib :comments nil [:foundations])]
result (wl/sort-linked-libraries libraries)]
(t/is (= [[:comments false nil]
[:foundations true "comments"]]
(summarize result)))
(t/is (= (count libraries) (count result)))))
(t/deftest sort-linked-libraries-cycle-of-three-without-root
;; a -> b -> c -> a, all indirect: no root, nothing is lost
(let [libraries [(lib :c true [:b])
(lib :b true [:a])
(lib :a true [:c])]
result (wl/sort-linked-libraries libraries)]
(t/is (= [[:a false nil]
[:b true "a"]
[:c true "b"]]
(summarize result)))
(t/is (= (count libraries) (count result)))))
(t/deftest sort-linked-libraries-deep-chain
;; file -> a -> b -> c
(let [libraries [(lib :c true [:b])
(lib :b true [:a])
(lib :a false [:file])]]
(t/is (= [[:a false nil]
[:b true "a"]
[:c true "b"]]
(summarize (wl/sort-linked-libraries libraries))))))
(t/deftest sort-linked-libraries-diamond
;; a -> b, c; b -> d; c -> d: d is listed once, under its first parent
(let [libraries [(lib :d true [:b :c])
(lib :c true [:a])
(lib :b true [:a])
(lib :a false [:file])]
result (wl/sort-linked-libraries libraries)]
(t/is (= [[:a false nil]
[:b true "a"]
[:d true "b"]
[:c true "a"]]
(summarize result)))
(t/is (= (count libraries) (count result)))))
(t/deftest sort-linked-libraries-self-dependency
(let [libraries [(lib :a true [:a])]]
(t/is (= [[:a false nil]]
(summarize (wl/sort-linked-libraries libraries))))))
(t/deftest sort-linked-libraries-parent-not-linked
;; The only user of each library is not a linked library, so both are roots
(let [libraries [(lib :b true [:other])
(lib :a true [:file])]]
(t/is (= [[:a false nil]
[:b false nil]]
(summarize (wl/sort-linked-libraries libraries))))))
(t/deftest sort-linked-libraries-without-dependencies
;; Before get-team-shared-files answers, all libraries are flat
(let [libraries [(lib :b true nil)
(lib :a false nil)]]
(t/is (= [[:a false nil]
[:b false nil]]
(summarize (wl/sort-linked-libraries libraries))))))

View File

@ -1032,7 +1032,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restaura tots els projectes i fitxers"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Restaura-ho tot"
@ -1611,7 +1611,7 @@ msgstr "S'ha produït un problema amb el web worker."
msgid "errors.invalid-color"
msgstr "El color no és vàlid"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Dades no vàlides"
@ -2946,7 +2946,7 @@ msgstr "Amaga"
msgid "labels.hide-resolved-comments"
msgstr "Amaga els comentaris resolts"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importa"
@ -4710,7 +4710,7 @@ msgid "profile.recovery.go-to-login"
msgstr "Vés a l'inici de sessió"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Acció"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5355,7 +5355,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Estableix miniatures"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Dreceres de teclat"

View File

@ -1498,7 +1498,7 @@ msgstr "Centrum nápovědy"
msgid "labels.hide-resolved-comments"
msgstr "Skrýt vyřešené komentáře"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Import"
@ -3083,7 +3083,7 @@ msgstr "Přiblížení"
msgid "shortcuts.thumbnail-set"
msgstr "Nastavit náhledy"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Klávesové zkratky"

View File

@ -904,7 +904,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "Alle Projekte und Dateien wiederherstellen"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Alles wiederherstellen"
@ -1341,7 +1341,7 @@ msgstr "Mit dem Web Worker ist ein Fehler aufgetreten."
msgid "errors.invalid-color"
msgstr "Ungültige Farbe"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Ungültige Daten"
@ -2238,7 +2238,7 @@ msgstr "Ausblenden"
msgid "labels.hide-resolved-comments"
msgstr "Erledigte Kommentare ausblenden"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importieren"
@ -4009,7 +4009,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Miniaturansichten festlegen"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Tastaturkürzel"

View File

@ -1017,7 +1017,7 @@ msgstr "You're going to restore all your projects and files. This may take a whi
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restore all projects and files"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Restore All"
@ -1605,7 +1605,7 @@ msgstr "Something wrong has happened with the web worker."
msgid "errors.invalid-color"
msgstr "Invalid color"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Invalid data"
@ -2111,16 +2111,24 @@ msgstr "An error has occurred"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:161
msgid "import-shortcuts.apply"
msgstr "Apply"
msgstr "Import"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:102
msgid "import-shortcuts.diff-modal-text"
msgstr "The following shortcuts will be changed by the imported configuration."
msgstr "These shortcuts will change:"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:98
msgid "import-shortcuts.diff-modal-title"
msgstr "Import shortcuts"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113
msgid "import-shortcuts.imported"
msgstr "Imported"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462
msgid "import-shortcuts.invalid-file"
msgstr "This file is not a valid shortcuts export."
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:148
msgid "import-shortcuts.no-changes"
msgstr "No differences found between the current and imported shortcuts."
@ -2976,7 +2984,7 @@ msgstr "Hide"
msgid "labels.hide-resolved-comments"
msgstr "Hide resolved comments"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Import"
@ -4714,20 +4722,20 @@ msgid "profile.recovery.go-to-login"
msgstr "Go to login"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgstr "Acction"
msgid "restore-shortcuts.action"
msgstr "Action"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
msgid "restore-shortcuts.modal-text"
msgstr "The following shortcuts will be restored to their default values."
msgstr "These shortcuts will be reset to default:"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:90
msgid "restore-shortcuts.modal-title"
msgstr "Restore to default configuration"
msgstr "Reset all shortcuts to default"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:170
msgid "restore-shortcuts.restore"
msgstr "Restore"
msgstr "Reset"
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:990, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:425
msgid "settings.detach"
@ -4921,21 +4929,25 @@ msgstr "Text"
msgid "shortcuts.duplicate"
msgstr "Duplicate"
#: src/app/main/ui/shortcuts.cljs:589
msgid "shortcuts.edit"
msgstr "Edit %s"
#: src/app/main/ui/shortcuts.cljs:645
msgid "shortcuts.edit-modal.conflict"
msgstr "Combination assigned to \"%s\". Saving will remove it there."
msgstr "Already assigned to \"%s\". Saving will move it to this action."
#: src/app/main/ui/shortcuts.cljs:636
msgid "shortcuts.edit-modal.reset-pending"
msgstr "If you save, this shortcut will return to the original value:"
msgstr "Saving will reset this shortcut to default:"
#: src/app/main/ui/shortcuts.cljs:647
msgid "shortcuts.edit-modal.success"
msgstr "This key combination is available and can be assigned without conflicts."
msgstr "Shortcut available."
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:157
msgid "shortcuts.edit-on-settings"
msgstr "Customize on settings"
msgstr "Customize shortcuts"
#: src/app/main/data/workspace/grid_layout/shortcuts.cljs:37, src/app/main/data/workspace/path/shortcuts.cljs:175, src/app/main/data/workspace/shortcuts.cljs:177
msgid "shortcuts.escape"
@ -4967,11 +4979,11 @@ msgstr "Flip vertically"
#: src/app/main/data/workspace/text/shortcuts.cljs:263
msgid "shortcuts.font-size-dec"
msgstr "Decrement font size"
msgstr "Decrease font size"
#: src/app/main/data/workspace/text/shortcuts.cljs:256
msgid "shortcuts.font-size-inc"
msgstr "Increment font size"
msgstr "Increase font size"
#: src/app/main/data/dashboard/shortcuts.cljs:29
msgid "shortcuts.go-to-drafts"
@ -5019,7 +5031,7 @@ msgstr "Join nodes"
#: src/app/main/ui/shortcuts.cljs:624
msgid "shortcuts.key-combo"
msgstr "Press the key combination"
msgstr "Press the new keys"
#: src/app/main/data/workspace/text/shortcuts.cljs:249
msgid "shortcuts.line-through"
@ -5087,19 +5099,19 @@ msgstr "Next board"
#: src/app/main/ui/settings/shortcuts.cljs:510
msgid "shortcuts.no-disabled"
msgstr "There are not disabled shortcuts"
msgstr "No disabled shortcuts."
#: src/app/main/ui/settings/shortcuts.cljs:500
msgid "shortcuts.no-personalized"
msgstr "Head to All to start customizing your shortcuts."
msgstr "No custom shortcuts yet. Customize one from the All tab."
#: src/app/main/ui/settings/shortcuts.cljs:242, src/app/main/ui/settings/shortcuts.cljs:491
msgid "shortcuts.no-shortcuts"
msgstr "There are no shortcuts that match your search"
msgstr "No shortcuts match your search."
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:154
msgid "shortcuts.not-found"
msgstr "No shortcuts found"
msgstr "No shortcuts match your search."
#: src/app/main/data/workspace/shortcuts.cljs:848
msgid "shortcuts.opacity-0"
@ -5175,7 +5187,7 @@ msgstr " or "
#: src/app/main/ui/shortcuts.cljs:605, src/app/main/ui/shortcuts.cljs:747
msgid "shortcuts.overwritten-by"
msgstr "This shortcut has been reassigned to %s"
msgstr "Reassigned to \"%s\""
#: src/app/main/ui/settings/shortcuts.cljs:468
msgid "shortcuts.page"
@ -5195,7 +5207,7 @@ msgstr "Paste and replace"
#: src/app/main/ui/settings/shortcuts.cljs:296, src/app/main/ui/settings/shortcuts.cljs:523
msgid "shortcuts.personalized"
msgstr "Personalized"
msgstr "Custom"
#: src/app/main/data/workspace/shortcuts.cljs:832
msgid "shortcuts.plugins"
@ -5223,6 +5235,10 @@ msgstr "Rename"
msgid "shortcuts.reset"
msgstr "Reset to default"
#: src/app/main/ui/settings/shortcuts.cljs:53
msgid "shortcuts.reset-all"
msgstr "Reset all"
#: src/app/main/data/viewer/shortcuts.cljs:48, src/app/main/data/workspace/grid_layout/shortcuts.cljs:69, src/app/main/data/workspace/path/shortcuts.cljs:220, src/app/main/data/workspace/shortcuts.cljs:704
msgid "shortcuts.reset-zoom"
msgstr "Reset zoom"
@ -5231,6 +5247,10 @@ msgstr "Reset zoom"
msgid "shortcuts.scale"
msgstr "Scale"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
msgid "shortcuts.search"
msgstr "Search shortcuts"
# SECTIONS
#: src/app/main/ui/shortcuts.cljs:126, src/app/main/ui/shortcuts.cljs:298
msgid "shortcuts.section.basics"
@ -5339,7 +5359,7 @@ msgstr "Shapes"
#: src/app/main/ui/shortcuts.cljs:143
msgid "shortcuts.subsection.text-editor"
msgstr "Texts"
msgstr "Text"
#: src/app/main/ui/shortcuts.cljs:144
msgid "shortcuts.subsection.tools"
@ -5357,7 +5377,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Set thumbnails"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Keyboard shortcuts"

View File

@ -1030,7 +1030,7 @@ msgstr "Vas a restaurar todos tus proyectos y archivos. Esto puede tardar un poc
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restaurar todos los proyectos y archivos"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Restaurar todo"
@ -1621,7 +1621,7 @@ msgstr "Ha ocurrido un problema con el web worker."
msgid "errors.invalid-color"
msgstr "Color no válido"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Datos no válidos"
@ -2152,16 +2152,24 @@ msgstr "Ha ocurrido un error"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:161
msgid "import-shortcuts.apply"
msgstr "Aplicar"
msgstr "Importar"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:102
msgid "import-shortcuts.diff-modal-text"
msgstr "Los siguientes atajos se modificarán con la configuración importada."
msgstr "Estos atajos cambiarán:"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:98
msgid "import-shortcuts.diff-modal-title"
msgstr "Importar atajos"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113
msgid "import-shortcuts.imported"
msgstr "Importado"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462
msgid "import-shortcuts.invalid-file"
msgstr "Este archivo no es una exportación de atajos válida."
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:148
msgid "import-shortcuts.no-changes"
msgstr "No se encontraron diferencias entre los atajos actuales y los importados."
@ -3019,7 +3027,7 @@ msgstr "Ocultar"
msgid "labels.hide-resolved-comments"
msgstr "Ocultar comentarios resueltos"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importar"
@ -4783,22 +4791,20 @@ msgid "profile.recovery.go-to-login"
msgstr "Ir al login"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Acción"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
msgid "restore-shortcuts.modal-text"
msgstr ""
"Los siguientes atajos de teclado se restaurarán a sus valores "
"predeterminados."
msgstr "Estos atajos volverán a su valor por defecto:"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:90
msgid "restore-shortcuts.modal-title"
msgstr "Restaurar la configuración predeterminada"
msgstr "Restablecer todos los atajos por defecto"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:170
msgid "restore-shortcuts.restore"
msgstr "Restaurar"
msgstr "Restablecer"
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:990, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:425
msgid "settings.detach"
@ -4992,21 +4998,25 @@ msgstr "Texto"
msgid "shortcuts.duplicate"
msgstr "Duplicar"
#: src/app/main/ui/shortcuts.cljs:589
msgid "shortcuts.edit"
msgstr "Editar %s"
#: src/app/main/ui/shortcuts.cljs:645
msgid "shortcuts.edit-modal.conflict"
msgstr "Combinación usada en \"%s\". Guardar este atajo hará que se elimine allí."
msgstr "Ya asignado a \"%s\". Al guardar, pasará a esta acción."
#: src/app/main/ui/shortcuts.cljs:636
msgid "shortcuts.edit-modal.reset-pending"
msgstr "Si guardas, este atajo volvera a su valor original:"
msgstr "Al guardar, este atajo volverá a su valor por defecto:"
#: src/app/main/ui/shortcuts.cljs:647
msgid "shortcuts.edit-modal.success"
msgstr "Esta combinación de teclas está disponible y puede asignarse sin conflictos."
msgstr "Atajo disponible."
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:157
msgid "shortcuts.edit-on-settings"
msgstr "Personalízalos en ajustes"
msgstr "Personalízalos"
#: src/app/main/data/workspace/grid_layout/shortcuts.cljs:37, src/app/main/data/workspace/path/shortcuts.cljs:175, src/app/main/data/workspace/shortcuts.cljs:177
msgid "shortcuts.escape"
@ -5038,11 +5048,11 @@ msgstr "Voltear verticalmente"
#: src/app/main/data/workspace/text/shortcuts.cljs:263
msgid "shortcuts.font-size-dec"
msgstr "Decrementar el tamaño de fuente"
msgstr "Reducir el tamaño de fuente"
#: src/app/main/data/workspace/text/shortcuts.cljs:256
msgid "shortcuts.font-size-inc"
msgstr "Incrementar el tamaño de fuente"
msgstr "Aumentar el tamaño de fuente"
#: src/app/main/data/dashboard/shortcuts.cljs:29
msgid "shortcuts.go-to-drafts"
@ -5090,7 +5100,7 @@ msgstr "Unir nodos"
#: src/app/main/ui/shortcuts.cljs:624
msgid "shortcuts.key-combo"
msgstr "Pulsa la combinación de teclas deseada"
msgstr "Pulsa las nuevas teclas"
#: src/app/main/data/workspace/text/shortcuts.cljs:249
msgid "shortcuts.line-through"
@ -5162,7 +5172,7 @@ msgstr "No hay atajos deshabilitados."
#: src/app/main/ui/settings/shortcuts.cljs:500
msgid "shortcuts.no-personalized"
msgstr "Dirígete a TODO para personalizar tus atajos."
msgstr "Aún no hay atajos personalizados. Personaliza uno desde la pestaña Todo."
#: src/app/main/ui/settings/shortcuts.cljs:242, src/app/main/ui/settings/shortcuts.cljs:491
msgid "shortcuts.no-shortcuts"
@ -5170,7 +5180,7 @@ msgstr "No hay atajos que coincidan con tu búsqueda."
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:154
msgid "shortcuts.not-found"
msgstr "No hay resultados"
msgstr "No hay atajos que coincidan con tu búsqueda."
#: src/app/main/data/workspace/shortcuts.cljs:848
msgid "shortcuts.opacity-0"
@ -5246,7 +5256,7 @@ msgstr " o "
#: src/app/main/ui/shortcuts.cljs:605, src/app/main/ui/shortcuts.cljs:747
msgid "shortcuts.overwritten-by"
msgstr "Este atajo ha sido reasignado a %s"
msgstr "Reasignado a \"%s\""
#: src/app/main/ui/settings/shortcuts.cljs:468
msgid "shortcuts.page"
@ -5294,6 +5304,10 @@ msgstr "Renombrar"
msgid "shortcuts.reset"
msgstr "Restablecer valor por defecto"
#: src/app/main/ui/settings/shortcuts.cljs:53
msgid "shortcuts.reset-all"
msgstr "Restablecer todo"
#: src/app/main/data/viewer/shortcuts.cljs:48, src/app/main/data/workspace/grid_layout/shortcuts.cljs:69, src/app/main/data/workspace/path/shortcuts.cljs:220, src/app/main/data/workspace/shortcuts.cljs:704
msgid "shortcuts.reset-zoom"
msgstr "Reiniciar zoom"
@ -5302,6 +5316,10 @@ msgstr "Reiniciar zoom"
msgid "shortcuts.scale"
msgstr "Escalado"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
msgid "shortcuts.search"
msgstr "Buscar atajos"
# SECTIONS
#: src/app/main/ui/shortcuts.cljs:126, src/app/main/ui/shortcuts.cljs:298
msgid "shortcuts.section.basics"
@ -5410,7 +5428,7 @@ msgstr "Formas"
#: src/app/main/ui/shortcuts.cljs:143
msgid "shortcuts.subsection.text-editor"
msgstr "Textos"
msgstr "Texto"
#: src/app/main/ui/shortcuts.cljs:144
msgid "shortcuts.subsection.tools"
@ -5428,7 +5446,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Activar miniaturas"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Atajos de teclado"

View File

@ -2336,7 +2336,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Aktibatu iruditxoak"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Lasterteklak"

View File

@ -1015,7 +1015,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "بازیابی همهٔ پروژه‌ها و فایل‌ها"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "بازیابی همه"
@ -1593,7 +1593,7 @@ msgstr "مشکلی در وب‌ورکر رخ داده است."
msgid "errors.invalid-color"
msgstr "رنگ نامعتبر"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "دادهٔ نامعتبر"
@ -2901,7 +2901,7 @@ msgstr "پنهان کردن"
msgid "labels.hide-resolved-comments"
msgstr "پنهان کردن نظرات حل شده"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "درون‌ریزی"
@ -4643,7 +4643,7 @@ msgid "profile.recovery.go-to-login"
msgstr "رفتن به ورود"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "اقدام"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5286,7 +5286,7 @@ msgstr "بزرگنمایی"
msgid "shortcuts.thumbnail-set"
msgstr "تنظیم تصاویر بندانگشتی"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "میانبرهای صفحه‌کلید"

View File

@ -1040,7 +1040,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restaurer tous les projets et fichiers"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Tout restaurer"
@ -1634,7 +1634,7 @@ msgstr "Un problème est survenu avec le web worker."
msgid "errors.invalid-color"
msgstr "Couleur non valide"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Données non valides"
@ -2977,7 +2977,7 @@ msgstr "Cacher"
msgid "labels.hide-resolved-comments"
msgstr "Masquer les commentaires résolus"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importer"
@ -4750,7 +4750,7 @@ msgid "profile.recovery.go-to-login"
msgstr "Aller à la page de connexion"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Action"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5397,7 +5397,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Définir les vignettes"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Raccourcis clavier"

View File

@ -922,7 +922,7 @@ msgstr "Tu vas restaurer tous tes projets et fichiers. Ça pourrait être long."
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restaurer tous les projets et fichiers"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Tout restaurer"
@ -1502,7 +1502,7 @@ msgstr "Il y a eu une erreur avec le travailleur web en arrière-plan (web worke
msgid "errors.invalid-color"
msgstr "Couleur invalide"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Données invalides"
@ -2758,7 +2758,7 @@ msgstr "Cacher"
msgid "labels.hide-resolved-comments"
msgstr "Cacher les commentaires résolus"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importer"
@ -4474,7 +4474,7 @@ msgid "profile.recovery.go-to-login"
msgstr "Aller à la connexion"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Action"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5113,7 +5113,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Définir l'imagette"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Raccourcis clavier"

View File

@ -2435,7 +2435,7 @@ msgstr "Zukowa"
msgid "shortcuts.thumbnail-set"
msgstr "saita babban yatsa"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "allon harufa yanke"

View File

@ -898,7 +898,7 @@ msgstr "הפעולה הזאת תשחזר את כל המיזמים והקבצים
msgid "dashboard.restore-all-confirmation.title"
msgstr "שחזור כל המיזמים והקבצים"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "לשחזר הכול"
@ -1462,7 +1462,7 @@ msgstr "משהו השתבש עם מנגנון הדפדפן."
msgid "errors.invalid-color"
msgstr "צבע שגוי"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "נתונים שגויים"
@ -2534,7 +2534,7 @@ msgstr "הסתרה"
msgid "labels.hide-resolved-comments"
msgstr "הסתרת הערות שנפתרו"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "ייבוא"
@ -4473,7 +4473,7 @@ msgstr "תקריב"
msgid "shortcuts.thumbnail-set"
msgstr "הגדרת תמונות ממוזערות"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "קיצורי מקלדת"

View File

@ -1187,7 +1187,7 @@ msgstr "वेब वर्कर के साथ कुछ गलत हो
msgid "errors.invalid-color"
msgstr "अमान्य रंग"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "अमान्य डेटा"
@ -2076,7 +2076,7 @@ msgstr "छुपाएँ"
msgid "labels.hide-resolved-comments"
msgstr "सुलझाए गए टिप्पणियाँ छुपाएँ"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "आयात"
@ -3868,7 +3868,7 @@ msgstr "ज़ूम"
msgid "shortcuts.thumbnail-set"
msgstr "थंबनेल सेट करें"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "कीबोर्ड शॉर्टकट्स"

View File

@ -1491,7 +1491,7 @@ msgstr "Centar za pomoć"
msgid "labels.hide-resolved-comments"
msgstr "Sakrij riješene komentare"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Uvoz"
@ -3086,7 +3086,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Postavi sličice"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Prečaci na tipkovnici"

View File

@ -993,7 +993,7 @@ msgstr "Sesuatu yang salah terjadi."
msgid "errors.invalid-color"
msgstr "Warna tidak valid"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Data tidak valid"
@ -1632,7 +1632,7 @@ msgstr "Sembunyikan"
msgid "labels.hide-resolved-comments"
msgstr "Sembunyikan komentar yang telah diselesaikan"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Impor"
@ -3307,7 +3307,7 @@ msgstr "Zum"
msgid "shortcuts.thumbnail-set"
msgstr "Tetapkan gambar kecil"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Pintasan papan ketik"

View File

@ -907,7 +907,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "Ripristina tutti i progetti e file"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Ripristina tutto"
@ -1475,7 +1475,7 @@ msgstr "Qualcosa è andato storto con il web worker."
msgid "errors.invalid-color"
msgstr "Colore non valido"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Dati non validi"
@ -2569,7 +2569,7 @@ msgstr "Nascondi"
msgid "labels.hide-resolved-comments"
msgstr "Non mostrare i commenti risolti"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importa"
@ -4381,7 +4381,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Imposta miniature"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Scorciatoie da tastiera"

View File

@ -1005,7 +1005,7 @@ msgstr "すべてのプロジェクトとファイルを復元します。しば
msgid "dashboard.restore-all-confirmation.title"
msgstr "すべてのプロジェクトとファイルを復元"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "すべて復元"
@ -1567,7 +1567,7 @@ msgstr "Webワーカーで問題が発生しました。"
msgid "errors.invalid-color"
msgstr "無効な色"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "無効なデータ"
@ -2842,7 +2842,7 @@ msgstr "非表示"
msgid "labels.hide-resolved-comments"
msgstr "解決済みのコメントを非表示"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "インポート"
@ -4486,7 +4486,7 @@ msgid "profile.recovery.go-to-login"
msgstr "ログインへ移動"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "アクション"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5127,7 +5127,7 @@ msgstr "ズーム"
msgid "shortcuts.thumbnail-set"
msgstr "サムネイルに設定"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "キーボードショートカット"

View File

@ -789,7 +789,7 @@ msgstr "모든 프로젝트와 파일을 복원하려 합니다. 시간이 다
msgid "dashboard.restore-all-confirmation.title"
msgstr "모든 프로젝트 및 파일 복원"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "모두 복원"
@ -1209,7 +1209,7 @@ msgstr "웹 워커에서 문제가 발생했습니다."
msgid "errors.invalid-color"
msgstr "유효하지 않은 컬러"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "유효하지 않은 데이터"
@ -1973,7 +1973,7 @@ msgstr "숨기기"
msgid "labels.hide-resolved-comments"
msgstr "해결된 댓글 숨기기"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "가져오기"
@ -3706,7 +3706,7 @@ msgstr "확대/축소"
msgid "shortcuts.thumbnail-set"
msgstr "썸네일 설정"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "키보드 단축키"

View File

@ -1123,7 +1123,7 @@ msgstr "Kaut kas nogāja greizi ar tīmekļa strādni."
msgid "errors.invalid-color"
msgstr "Nederīga krāsa"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Nederīgi dati"
@ -1877,7 +1877,7 @@ msgstr "Paslēpt"
msgid "labels.hide-resolved-comments"
msgstr "Paslēpt atrisinātos komentārus"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Ievietot"
@ -3608,7 +3608,7 @@ msgstr "Tālummaiņa"
msgid "shortcuts.thumbnail-set"
msgstr "Iestatīt sīktēlus"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Īsinājumtaustiņi"

View File

@ -1024,7 +1024,7 @@ msgstr "Je gaat al je projecten en bestanden herstellen. Dit kan even duren."
msgid "dashboard.restore-all-confirmation.title"
msgstr "Alle projecten en bestanden herstellen"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Alles herstellen"
@ -1613,7 +1613,7 @@ msgstr "Er is iets mis gegaan met de webwerker."
msgid "errors.invalid-color"
msgstr "Ongeldige kleur"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Ongeldige gegevens"
@ -2948,7 +2948,7 @@ msgstr "Verbergen"
msgid "labels.hide-resolved-comments"
msgstr "Verwerkt commentaar verbergen"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importeren"
@ -4720,7 +4720,7 @@ msgid "profile.recovery.go-to-login"
msgstr "Ga naar inlogscherm"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Actie"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5365,7 +5365,7 @@ msgstr "Zoomen"
msgid "shortcuts.thumbnail-set"
msgstr "Miniaturen instellen"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Sneltoetsen"

View File

@ -2298,7 +2298,7 @@ msgstr "Przybliżenie"
msgid "shortcuts.thumbnail-set"
msgstr "Ustaw miniaturki"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Skróty klawiaturowe"

View File

@ -1095,7 +1095,7 @@ msgstr "Algo de errado aconteceu com o web worker."
msgid "errors.invalid-color"
msgstr "Cor inválida"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Data inválida"
@ -2953,7 +2953,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Definir como miniatura"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Atalhos do teclado"

View File

@ -949,7 +949,7 @@ msgstr "Ocorreu algo inesperado."
msgid "errors.invalid-color"
msgstr "Cor inválida"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Dados inválidos"
@ -1520,7 +1520,7 @@ msgstr "Ocultar"
msgid "labels.hide-resolved-comments"
msgstr "Ocultar comentários resolvidos"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importar"
@ -3147,7 +3147,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Definir imagem destaque"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Atalhos do teclado"

View File

@ -1110,7 +1110,7 @@ msgstr "A apărut o problemă cu web worker-ul."
msgid "errors.invalid-color"
msgstr "Culoare invalidă"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Date invalide"
@ -1906,7 +1906,7 @@ msgstr "Ascunde"
msgid "labels.hide-resolved-comments"
msgstr "Ascunde comentariile rezolvate"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importă"
@ -3695,7 +3695,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Setează miniaturile"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Comenzi rapide de la tastatură"

View File

@ -827,7 +827,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "Восстановить все проекты и файлы"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Восстановить все"
@ -1259,7 +1259,7 @@ msgstr "Произошла ошибка в работе веб-воркера."
msgid "errors.invalid-color"
msgstr "Неверный цвет"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Неверные данные"
@ -2039,7 +2039,7 @@ msgstr "Скрыть"
msgid "labels.hide-resolved-comments"
msgstr "Скрыть решённые комментарии"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Импорт"
@ -3717,7 +3717,7 @@ msgstr "Увеличение"
msgid "shortcuts.thumbnail-set"
msgstr "Установить миниатюры"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Горячие клавиши"

View File

@ -2682,7 +2682,7 @@ msgstr "Увећање"
msgid "shortcuts.thumbnail-set"
msgstr "Подеси сличице"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Пречице на тастатури"

View File

@ -1015,7 +1015,7 @@ msgstr "Du kommer att återställa alla dina projekt och filer. Det kan ta ett t
msgid "dashboard.restore-all-confirmation.title"
msgstr "Återställ alla projekt och filer"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Återställ allt"
@ -1600,7 +1600,7 @@ msgstr "Något fel har hänt med webbarbetaren."
msgid "errors.invalid-color"
msgstr "Ogiltig färg"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Ogiltig data"
@ -2921,7 +2921,7 @@ msgstr "Dölj"
msgid "labels.hide-resolved-comments"
msgstr "Dölj åtgärdade kommentarer"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Importera"
@ -4671,7 +4671,7 @@ msgid "profile.recovery.go-to-login"
msgstr "Gå till Logga in"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Åtgärd"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5316,7 +5316,7 @@ msgstr "Zoom"
msgid "shortcuts.thumbnail-set"
msgstr "Ställ in miniatyrbilder"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Snabbkommandon"

View File

@ -1020,7 +1020,7 @@ msgstr ""
msgid "dashboard.restore-all-confirmation.title"
msgstr "Tüm projeleri ve dosyaları geri yükle"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Tümünü Geri Yükle"
@ -1602,7 +1602,7 @@ msgstr "Web çalıştırıcısında bir sorun oluştu."
msgid "errors.invalid-color"
msgstr "Geçersiz renk"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Geçersiz veri"
@ -2918,7 +2918,7 @@ msgstr "Gizle"
msgid "labels.hide-resolved-comments"
msgstr "Çözülen yorumları gizle"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "İçe aktar"
@ -4667,7 +4667,7 @@ msgid "profile.recovery.go-to-login"
msgstr "Oturum açmaya git"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "Eylem"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5312,7 +5312,7 @@ msgstr "Yakınlaştır"
msgid "shortcuts.thumbnail-set"
msgstr "Küçük resimleri ayarla"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Klavye kısayolları"

View File

@ -876,7 +876,7 @@ msgstr "Ви збираєтесь відновити усі проєкти та
msgid "dashboard.restore-all-confirmation.title"
msgstr "Відновити усі проєкти й файли"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "Відновити Все"
@ -1387,7 +1387,7 @@ msgstr "Під час роботи веб-виконавця сталась по
msgid "errors.invalid-color"
msgstr "Недійсний колір"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "Помилкові дані"
@ -2272,7 +2272,7 @@ msgstr "Приховати"
msgid "labels.hide-resolved-comments"
msgstr "Приховати вирішені коментарі"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "Імпортувати"
@ -4087,7 +4087,7 @@ msgstr "Масштабування"
msgid "shortcuts.thumbnail-set"
msgstr "Вставити мініатюри"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "Клавіатурні скорочення"

View File

@ -828,7 +828,7 @@ msgstr "你将恢复所有项目和文件。这可能需要一些时间。"
msgid "dashboard.restore-all-confirmation.title"
msgstr "恢复所有的项目及其文件"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "恢复所有"
@ -1316,7 +1316,7 @@ msgstr "Web Worker 出错。"
msgid "errors.invalid-color"
msgstr "无效的颜色"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "无效数据"
@ -2474,7 +2474,7 @@ msgstr "隐藏"
msgid "labels.hide-resolved-comments"
msgstr "隐藏已解决的评论"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "导入"
@ -4466,7 +4466,7 @@ msgstr "缩放"
msgid "shortcuts.thumbnail-set"
msgstr "设置缩略图"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "快捷键"

View File

@ -966,7 +966,7 @@ msgstr "即將還原您所有的專案與檔案,這可能需要一些時間。
msgid "dashboard.restore-all-confirmation.title"
msgstr "還原所有專案與檔案"
#: src/app/main/ui/dashboard/deleted.cljs:317, src/app/main/ui/settings/shortcuts.cljs:53
#: src/app/main/ui/dashboard/deleted.cljs:317
msgid "dashboard.restore-all-deleted-button"
msgstr "全部還原"
@ -1515,7 +1515,7 @@ msgstr "Web Worker 發生錯誤。"
msgid "errors.invalid-color"
msgstr "色彩無效"
#: src/app/main/ui/settings/shortcuts.cljs:460, src/app/main/ui/settings/shortcuts.cljs:462, src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
#: src/app/plugins/utils.cljs:517, ../common/src/app/common/schema/messages.cljc:33, ../common/src/app/common/schema/messages.cljc:87
msgid "errors.invalid-data"
msgstr "資料無效"
@ -2774,7 +2774,7 @@ msgstr "隱藏"
msgid "labels.hide-resolved-comments"
msgstr "隱藏已解決的留言"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:113, src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
#: src/app/main/ui/settings/shortcuts.cljs:544, src/app/main/ui/workspace/tokens/sidebar.cljs:142
msgid "labels.import"
msgstr "匯入"
@ -4408,7 +4408,7 @@ msgid "profile.recovery.go-to-login"
msgstr "前往登入"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs:109, src/app/main/ui/settings/restore_shortcuts_modal.cljs:100
msgid "restore-shortcuts.acction"
msgid "restore-shortcuts.action"
msgstr "操作"
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs:94
@ -5049,7 +5049,7 @@ msgstr "縮放"
msgid "shortcuts.thumbnail-set"
msgstr "設定縮圖"
#: src/app/main/ui/settings/shortcuts.cljs:44, src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132, src/app/main/ui/workspace/sidebar/shortcuts.cljs:139
#: src/app/main/ui/settings/shortcuts.cljs:170, src/app/main/ui/workspace/sidebar/shortcuts.cljs:132
msgid "shortcuts.title"
msgstr "鍵盤快速鍵"

View File

@ -34,7 +34,7 @@
"watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"
},
"devDependencies": {
"@types/node": "^26.1.2",
"@types/node": "^26.6.4",
"@zip.js/zip.js": "2.8.34",
"concurrently": "^10.0.5",
"date-fns": "^4.4.0",

61
library/pnpm-lock.yaml generated
View File

@ -162,6 +162,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
shell-quote@>=1.8.4 <1.11.0: ^1.11.0
patchedDependencies:
'@zip.js/zip.js@2.8.34': 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95
@ -170,8 +173,8 @@ importers:
.:
devDependencies:
'@types/node':
specifier: ^26.1.2
version: 26.1.2
specifier: ^26.6.4
version: 26.6.4
'@zip.js/zip.js':
specifier: 2.8.34
version: 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)
@ -190,15 +193,15 @@ importers:
packages:
'@types/node@26.1.2':
resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==}
'@types/node@26.6.4':
resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==}
'@zip.js/zip.js@2.8.34':
resolution: {integrity: sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==}
engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
ansi-regex@6.4.0:
resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==}
engines: {node: '>=12'}
ansi-styles@6.2.3:
@ -217,8 +220,8 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
brace-expansion@5.0.12:
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
engines: {node: 20 || >=22}
braces@3.0.3:
@ -277,8 +280,8 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
get-east-asian-width@1.7.0:
resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
engines: {node: '>=18'}
glob-parent@5.1.2:
@ -308,8 +311,8 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
minimatch@10.2.6:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
ms@2.1.3:
@ -343,8 +346,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
shell-quote@1.9.0:
resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==}
shell-quote@1.12.0:
resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==}
engines: {node: '>= 0.4'}
simple-update-notifier@2.0.0:
@ -392,8 +395,8 @@ packages:
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
undici-types@8.9.0:
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
@ -413,13 +416,13 @@ packages:
snapshots:
'@types/node@26.1.2':
'@types/node@26.6.4':
dependencies:
undici-types: 8.3.0
undici-types: 8.9.0
'@zip.js/zip.js@2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {}
ansi-regex@6.2.2: {}
ansi-regex@6.4.0: {}
ansi-styles@6.2.3: {}
@ -432,7 +435,7 @@ snapshots:
binary-extensions@2.3.0: {}
brace-expansion@5.0.9:
brace-expansion@5.0.12:
dependencies:
balanced-match: 4.0.4
@ -466,7 +469,7 @@ snapshots:
dependencies:
chalk: 5.6.2
rxjs: 7.8.2
shell-quote: 1.9.0
shell-quote: 1.12.0
supports-color: 10.2.2
tree-kill: 1.2.2
yargs: 18.0.0
@ -492,7 +495,7 @@ snapshots:
get-caller-file@2.0.5: {}
get-east-asian-width@1.6.0: {}
get-east-asian-width@1.7.0: {}
glob-parent@5.1.2:
dependencies:
@ -514,9 +517,9 @@ snapshots:
is-number@7.0.0: {}
minimatch@10.2.5:
minimatch@10.2.6:
dependencies:
brace-expansion: 5.0.9
brace-expansion: 5.0.12
ms@2.1.3: {}
@ -525,7 +528,7 @@ snapshots:
chokidar: 3.6.0
debug: 4.4.3(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 10.2.5
minimatch: 10.2.6
pstree.remy: 1.1.8
semver: 7.8.5
simple-update-notifier: 2.0.0
@ -549,7 +552,7 @@ snapshots:
semver@7.8.5: {}
shell-quote@1.9.0: {}
shell-quote@1.12.0: {}
simple-update-notifier@2.0.0:
dependencies:
@ -565,12 +568,12 @@ snapshots:
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
get-east-asian-width: 1.6.0
get-east-asian-width: 1.7.0
strip-ansi: 7.2.0
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
ansi-regex: 6.4.0
supports-color@10.2.2: {}
@ -590,7 +593,7 @@ snapshots:
undefsafe@2.0.5: {}
undici-types@8.3.0: {}
undici-types@8.9.0: {}
wrap-ansi@9.0.2:
dependencies:

View File

@ -4,3 +4,5 @@ minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9
patchedDependencies:
'@zip.js/zip.js@2.8.34': patches/@zip.js__zip.js@2.8.11.patch
overrides:
shell-quote@>=1.8.4 <1.11.0: ^1.11.0

View File

@ -12,7 +12,7 @@
"clean": "rm -rf dist/"
},
"devDependencies": {
"typescript": "^6.0.3"
"typescript": "^7.0.2"
},
"files": [
"dist/**/*"

View File

@ -18,8 +18,8 @@
},
"devDependencies": {
"cross-env": "^10.1.0",
"typescript": "^6.0.3",
"vite": "^8.3.1",
"typescript": "^7.0.2",
"vite": "^8.3.2",
"vite-live-preview": "^0.4.0"
}
}

View File

@ -29,34 +29,34 @@
},
"packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45",
"dependencies": {
"@modelcontextprotocol/node": "^2.1.0",
"@modelcontextprotocol/server": "^2.1.0",
"@modelcontextprotocol/node": "^2.1.1",
"@modelcontextprotocol/server": "^2.3.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"express": "^5.2.1",
"ioredis": "^6.0.0",
"js-yaml": "^5.4.2",
"nrepl-client": "^0.3.0",
"pino": "^10.3.1",
"pino": "^10.4.0",
"pino-loki": "^3.0.0",
"pino-pretty": "^13.1.3",
"pino-pretty": "^13.2.0",
"reflect-metadata": "^0.2.2",
"sharp": "^0.35.5",
"ws": "^8.22.0",
"zod": "^4.6.5"
},
"devDependencies": {
"@modelcontextprotocol/client": "^2.1.0",
"@modelcontextprotocol/client": "^2.3.1",
"@penpot/mcp-common": "workspace:../common",
"@types/express": "^5.0.6",
"@types/js-yaml": "^4.0.9",
"@types/node": "^26.6.3",
"@types/ws": "^8.18.1",
"@types/node": "^26.6.4",
"@types/ws": "^8.18.2",
"cross-env": "^10.1.0",
"esbuild": "^0.28.1",
"ts-node": "^10.9.2",
"tsx": "^4.23.15",
"typescript": "^6.0.3"
"typescript": "^7.0.2"
},
"ts-node": {
"esm": true

678
mcp/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More