From b484415a9ff1b107151cdb81246da14f9598609e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 23 Mar 2026 20:03:14 +0100 Subject: [PATCH 1/9] :bug: Fix generic error shown on clipboard permission denial (#8666) When the browser denies clipboard read permission (NotAllowedError), the unhandled exception handler was showing a generic 'Something wrong has happened' toast. This change adds proper error handling for clipboard permission errors in paste operations and shows a user-friendly warning message instead. Changes: - Add error handling in paste-from-clipboard for NotAllowedError - Improve error handling in paste-selected-props to detect permission errors - Mark clipboard NotAllowedError as ignorable in the uncaught error handler to prevent duplicate generic error toasts - Add translation key for clipboard permission denied message Signed-off-by: Andrey Antukh --- .../app/main/data/workspace/clipboard.cljs | 38 ++++++++++++++++--- frontend/translations/en.po | 4 ++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 4bd8190895..e32de485e5 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -294,6 +294,22 @@ (def default-paste-from-blob (create-paste-from-blob false)) +(defn- clipboard-permission-error? + "Check if the given error is a clipboard permission error + (NotAllowedError DOMException)." + [cause] + (and (instance? js/DOMException cause) + (= (.-name cause) "NotAllowedError"))) + +(defn- on-clipboard-permission-error + [cause] + (if (clipboard-permission-error? cause) + (rx/of (ntf/show {:content (tr "errors.clipboard-permission-denied") + :type :toast + :level :warning + :timeout 5000})) + (rx/throw cause))) + (defn paste-from-clipboard "Perform a `paste` operation using the Clipboard API." [] @@ -302,7 +318,8 @@ (watch [_ _ _] (->> (clipboard/from-navigator default-options) (rx/mapcat default-paste-from-blob) - (rx/take 1))))) + (rx/take 1) + (rx/catch on-clipboard-permission-error))))) (defn paste-from-event "Perform a `paste` operation from user emmited event." @@ -482,11 +499,20 @@ (-> entry t/decode-str paste-transit-props)) (on-error [cause] - (let [data (ex-data cause)] - (if (:not-implemented data) - (rx/of (ntf/warn (tr "errors.clipboard-not-implemented"))) - (js/console.error "Clipboard error:" cause)) - (rx/empty)))] + (cond + (clipboard-permission-error? cause) + (rx/of (ntf/show {:content (tr "errors.clipboard-permission-denied") + :type :toast + :level :warning + :timeout 5000})) + + (:not-implemented (ex-data cause)) + (rx/of (ntf/warn (tr "errors.clipboard-not-implemented"))) + + :else + (do + (js/console.error "Clipboard error:" cause) + (rx/empty))))] (->> (clipboard/from-navigator default-options) (rx/mapcat #(.text %)) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 495fd1b9c2..a39cfaf85c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1375,6 +1375,10 @@ msgstr "Character limit exceeded" msgid "errors.clipboard-not-implemented" msgstr "Your browser cannot do this operation" +#: src/app/main/data/workspace/clipboard.cljs +msgid "errors.clipboard-permission-denied" +msgstr "Clipboard access denied. Please allow clipboard permissions in your browser to paste content" + #: src/app/main/errors.cljs:235 msgid "errors.comment-error" msgstr "There was an error with the comment" From 577f00dd24aa762171ab47263930114f1fbdbba9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 23 Mar 2026 19:19:27 +0000 Subject: [PATCH 2/9] :bug: Fix error when get-parent-with-data encounters non-Element nodes The get-parent-with-data function traverses the DOM using parentElement to find an ancestor with a specific data-* attribute. When the current node is a non-Element DOM node (e.g. Document node reached from event handlers on window), accessing .-dataset returns undefined, causing obj/in? to throw "right-hand side of 'in' should be an object". This adds a nodeType check to skip non-Element nodes during traversal and continue up the parent chain. Signed-off-by: Andrey Antukh --- frontend/src/app/util/dom.cljs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index ffa2b8f361..4e068cc5d3 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -170,8 +170,17 @@ [^js node name] (let [name (str/camel name)] (loop [current node] - (if (or (nil? current) (obj/in? (.-dataset current) name)) + (cond + (nil? current) + nil + + (not= (.-nodeType current) js/Node.ELEMENT_NODE) + (recur (.-parentElement current)) + + (obj/in? (.-dataset current) name) current + + :else (recur (.-parentElement current)))))) (defn get-parent-with-selector From d051a3ba4572c9aea3c43b8d2062ce4b4c50d679 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 00:18:44 +0000 Subject: [PATCH 3/9] :bug: Ensure path content is always PathData when saving The save-path-content function only converted content to PathData when there was a trailing :move-to command. When there was no trailing :move-to, the content from get-path was stored as-is, which could be a plain vector if the shape was already a :path type with non-PathData content. This caused segment/get-points to fail with 'can't access property "get", cache is undefined' when the with-cache macro tried to access the cache field on a non-PathData object. The fix ensures content is always converted to PathData via path/content before being stored in the state. Signed-off-by: Andrey Antukh --- frontend/src/app/main/data/workspace/path/changes.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index c2bc293da3..8eb73b3663 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -69,7 +69,7 @@ content (if (and (not preserve-move-to) (= (-> content last :command) :move-to)) (path/content (take (dec (count content)) content)) - content)] + (path/content content))] (st/set-content state content))) ptk/WatchEvent From 13b5c96a429070b959d5cd6e0082112ce6724efc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 09:19:58 +0100 Subject: [PATCH 4/9] :paperclip: Update changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6d52b2b01f..d5f46c6c3b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # CHANGELOG -## 2.14.0 (Unreleased) +## 2.14.0 ### :boom: Breaking changes & Deprecations From 8729fed7240a7a150d49b76fa65c02ed6a0d5f64 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 12:52:18 +0100 Subject: [PATCH 5/9] :paperclip: Add opencode and copilot deps on root package.json --- package.json | 5 +- pnpm-lock.yaml | 176 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 150 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index f38f80617d..b35ad1b50a 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "fmt": "./scripts/fmt" }, "devDependencies": { - "@github/copilot": "^1.0.2", + "@github/copilot": "^1.0.11", "@types/node": "^20.12.7", - "esbuild": "^0.25.9" + "esbuild": "^0.25.9", + "opencode-ai": "^1.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bec7b49e31..4d683348b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,14 +9,17 @@ importers: .: devDependencies: '@github/copilot': - specifier: ^1.0.2 - version: 1.0.2 + specifier: ^1.0.11 + version: 1.0.11 '@types/node': specifier: ^20.12.7 version: 20.19.37 esbuild: specifier: ^0.25.9 version: 0.25.12 + opencode-ai: + specifier: ^1.3.0 + version: 1.3.0 packages: @@ -176,44 +179,44 @@ packages: cpu: [x64] os: [win32] - '@github/copilot-darwin-arm64@1.0.2': - resolution: {integrity: sha512-dYoeaTidsphRXyMjvAgpjEbBV41ipICnXURrLFEiATcjC4IY6x2BqPOocrExBYW/Tz2VZvDw51iIZaf6GXrTmw==} + '@github/copilot-darwin-arm64@1.0.11': + resolution: {integrity: sha512-wdKimjtbsVeXqMqQSnGpGBPFEYHljxXNuWeH8EIJTNRgFpAsimcivsFgql3Twq4YOp0AxfsH36icG4IEen30mA==} cpu: [arm64] os: [darwin] hasBin: true - '@github/copilot-darwin-x64@1.0.2': - resolution: {integrity: sha512-8+Z9dYigEfXf0wHl9c2tgFn8Cr6v4RAY8xTgHMI9mZInjQyxVeBXCxbE2VgzUtDUD3a705Ka2d8ZOz05aYtGsg==} + '@github/copilot-darwin-x64@1.0.11': + resolution: {integrity: sha512-VeuPv8rzBVGBB8uDwMEhcHBpldoKaq26yZ5YQm+G9Ka5QIF+1DMah8ZNRMVsTeNKkb1ji9G8vcuCsaPbnG3fKg==} cpu: [x64] os: [darwin] hasBin: true - '@github/copilot-linux-arm64@1.0.2': - resolution: {integrity: sha512-ik0Y5aTXOFRPLFrNjZJdtfzkozYqYeJjVXGBAH3Pp1nFZRu/pxJnrnQ1HrqO/LEgQVbJzAjQmWEfMbXdQIxE4Q==} + '@github/copilot-linux-arm64@1.0.11': + resolution: {integrity: sha512-/d8p6RlFYKj1Va2hekFIcYNMHWagcEkaxgcllUNXSyQLnmEtXUkaWtz62VKGWE+n/UMkEwCB6vI2xEwPTlUNBQ==} cpu: [arm64] os: [linux] hasBin: true - '@github/copilot-linux-x64@1.0.2': - resolution: {integrity: sha512-mHSPZjH4nU9rwbfwLxYJ7CQ90jK/Qu1v2CmvBCUPfmuGdVwrpGPHB5FrB+f+b0NEXjmemDWstk2zG53F7ppHfw==} + '@github/copilot-linux-x64@1.0.11': + resolution: {integrity: sha512-UujTRO3xkPFC1CybchBbCnaTEAG6JrH0etIst07JvfekMWgvRxbiCHQPpDPSzBCPiBcGu0gba0/IT+vUCORuIw==} cpu: [x64] os: [linux] hasBin: true - '@github/copilot-win32-arm64@1.0.2': - resolution: {integrity: sha512-tLW2CY/vg0fYLp8EuiFhWIHBVzbFCDDpohxT/F/XyMAdTVSZLnopCcxQHv2BOu0CVGrYjlf7YOIwPfAKYml1FA==} + '@github/copilot-win32-arm64@1.0.11': + resolution: {integrity: sha512-EOW8HUM+EmnHEZEa+iUMl4pP1+2eZUk2XCbynYiMehwX9sidc4BxEHp2RuxADSzFPTieQEWzgjQmHWrtet8pQg==} cpu: [arm64] os: [win32] hasBin: true - '@github/copilot-win32-x64@1.0.2': - resolution: {integrity: sha512-cFlc3xMkKKFRIYR00EEJ2XlYAemeh5EZHsGA8Ir2G0AH+DOevJbomdP1yyCC5gaK/7IyPkHX3sGie5sER2yPvQ==} + '@github/copilot-win32-x64@1.0.11': + resolution: {integrity: sha512-fKGkSNamzs3h9AbmswNvPYJBORCb2Y8CbusijU3C7fT3ohvqnHJwKo5iHhJXLOKZNOpFZgq9YKha410u9sIs6Q==} cpu: [x64] os: [win32] hasBin: true - '@github/copilot@1.0.2': - resolution: {integrity: sha512-716SIZMYftldVcJay2uZOzsa9ROGGb2Mh2HnxbDxoisFsWNNgZlQXlV7A+PYoGsnAo2Zk/8e1i5SPTscGf2oww==} + '@github/copilot@1.0.11': + resolution: {integrity: sha512-cptVopko/tNKEXyBP174yBjHQBEwg6CqaKN2S0M3J+5LEB8u31bLL75ioOPd+5vubqBrA0liyTdcHeZ8UTRbmg==} hasBin: true '@types/node@20.19.37': @@ -224,6 +227,70 @@ packages: engines: {node: '>=18'} hasBin: true + opencode-ai@1.3.0: + resolution: {integrity: sha512-il/dC3B55m5mZV2u72emfPqkZBTzrlZwqGI4Ds5Ld6kt2LTUzBZtKB8sOfy7Bmw2qIel0hLZdoKc8wxLjaXQDw==} + hasBin: true + + opencode-darwin-arm64@1.3.0: + resolution: {integrity: sha512-OB+yl/BZkjQhnjjFc+KT57iqhPlXNq3E0oIcHHlGiG63L2LTY3zfi9OhzaoemL+or2CWnpCITUe91yTAddiSEQ==} + cpu: [arm64] + os: [darwin] + + opencode-darwin-x64-baseline@1.3.0: + resolution: {integrity: sha512-Th5yiWOSDeEcjnKWhR8b267Uf8r+jwLFhv30JK4x07Zdmu3Jjjr6TdMvjLgEOv3PWmHf/1yYz22Xachb+QST0A==} + cpu: [x64] + os: [darwin] + + opencode-darwin-x64@1.3.0: + resolution: {integrity: sha512-jivDUpmhzkT7WZp7pXVSb9fdnEVuhKBsnve/9fIkI/UFHxomiZ2NIaNRbHxG26PYT9a1IR4D5QvXBq623g2Mnw==} + cpu: [x64] + os: [darwin] + + opencode-linux-arm64-musl@1.3.0: + resolution: {integrity: sha512-EmXBHyRSzWCnD/KDpaSi8ldgjOa+1t5c5tRASyL/lnbinsrZekxub3lI+oxRvKJXESKdgq9EP4gkp6t2fqGsFw==} + cpu: [arm64] + os: [linux] + + opencode-linux-arm64@1.3.0: + resolution: {integrity: sha512-rWEEKo4oqgJ/zk670ywg6uhEPwbUIQCwYCeh+xJ3IlgPltQNiIjqUbzbRqAmEfI1Uj9DCdbZ2TUtHayRv8umKw==} + cpu: [arm64] + os: [linux] + + opencode-linux-x64-baseline-musl@1.3.0: + resolution: {integrity: sha512-sb7LyPlf+5/t4pQ3whcHPVlb7R7SRY0Bgjgy55amEs3xRuKnC3BfSoj8CAoY50M/yVAbOj0haoxu4LFixljwNw==} + cpu: [x64] + os: [linux] + + opencode-linux-x64-baseline@1.3.0: + resolution: {integrity: sha512-STZtcgGgeRlaFCmkk+mNm+01d02JCzCPvP9kWwNpRF6FBGTcFZ97MxEoGvk+7mEqMueImVQZOR21NiYN6anQhw==} + cpu: [x64] + os: [linux] + + opencode-linux-x64-musl@1.3.0: + resolution: {integrity: sha512-Jc/EbYgqmT2J2WLPm7EQWBYfSqetWTrI4Ipc4KFrSB/LbM/7lfXkjpemjQaYNlDTVkvPXaUPFJUpisH64xZ+4g==} + cpu: [x64] + os: [linux] + + opencode-linux-x64@1.3.0: + resolution: {integrity: sha512-U9aS0wl0uBDxXncqSYhYBDDQP2ZwiTiuJSLM6MgtFJTbUXuTZZCKmQ8p7C5/+Nxpl4sY5xK+ZaCJcS3k3WGN3g==} + cpu: [x64] + os: [linux] + + opencode-windows-arm64@1.3.0: + resolution: {integrity: sha512-3iWo9lOctaWQ+8QHRKszINPTLjLtb0ztzedlvdY5HAiot9MUK/G5MHeskutxQ7sMvTACiAp02ey+Ml/f/jyf7Q==} + cpu: [arm64] + os: [win32] + + opencode-windows-x64-baseline@1.3.0: + resolution: {integrity: sha512-pYuY+9LqPLB/GrlZQr67Cl8RlV6vcay4fW8L3TjabwJOinFMDX9OpNo+DkdKJW7YtPtHD78cXaNDEV8tv9Nx2A==} + cpu: [x64] + os: [win32] + + opencode-windows-x64@1.3.0: + resolution: {integrity: sha512-iFd/6GwfM3jlI2tOb3f12m5ddDY8Ug2HiUU1xmxWJvDnbDBdftlHrzD5twlbIHnKoGvohepX8iWk+A/UN2cXKQ==} + cpu: [x64] + os: [win32] + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -307,32 +374,32 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@github/copilot-darwin-arm64@1.0.2': + '@github/copilot-darwin-arm64@1.0.11': optional: true - '@github/copilot-darwin-x64@1.0.2': + '@github/copilot-darwin-x64@1.0.11': optional: true - '@github/copilot-linux-arm64@1.0.2': + '@github/copilot-linux-arm64@1.0.11': optional: true - '@github/copilot-linux-x64@1.0.2': + '@github/copilot-linux-x64@1.0.11': optional: true - '@github/copilot-win32-arm64@1.0.2': + '@github/copilot-win32-arm64@1.0.11': optional: true - '@github/copilot-win32-x64@1.0.2': + '@github/copilot-win32-x64@1.0.11': optional: true - '@github/copilot@1.0.2': + '@github/copilot@1.0.11': optionalDependencies: - '@github/copilot-darwin-arm64': 1.0.2 - '@github/copilot-darwin-x64': 1.0.2 - '@github/copilot-linux-arm64': 1.0.2 - '@github/copilot-linux-x64': 1.0.2 - '@github/copilot-win32-arm64': 1.0.2 - '@github/copilot-win32-x64': 1.0.2 + '@github/copilot-darwin-arm64': 1.0.11 + '@github/copilot-darwin-x64': 1.0.11 + '@github/copilot-linux-arm64': 1.0.11 + '@github/copilot-linux-x64': 1.0.11 + '@github/copilot-win32-arm64': 1.0.11 + '@github/copilot-win32-x64': 1.0.11 '@types/node@20.19.37': dependencies: @@ -367,4 +434,55 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + opencode-ai@1.3.0: + optionalDependencies: + opencode-darwin-arm64: 1.3.0 + opencode-darwin-x64: 1.3.0 + opencode-darwin-x64-baseline: 1.3.0 + opencode-linux-arm64: 1.3.0 + opencode-linux-arm64-musl: 1.3.0 + opencode-linux-x64: 1.3.0 + opencode-linux-x64-baseline: 1.3.0 + opencode-linux-x64-baseline-musl: 1.3.0 + opencode-linux-x64-musl: 1.3.0 + opencode-windows-arm64: 1.3.0 + opencode-windows-x64: 1.3.0 + opencode-windows-x64-baseline: 1.3.0 + + opencode-darwin-arm64@1.3.0: + optional: true + + opencode-darwin-x64-baseline@1.3.0: + optional: true + + opencode-darwin-x64@1.3.0: + optional: true + + opencode-linux-arm64-musl@1.3.0: + optional: true + + opencode-linux-arm64@1.3.0: + optional: true + + opencode-linux-x64-baseline-musl@1.3.0: + optional: true + + opencode-linux-x64-baseline@1.3.0: + optional: true + + opencode-linux-x64-musl@1.3.0: + optional: true + + opencode-linux-x64@1.3.0: + optional: true + + opencode-windows-arm64@1.3.0: + optional: true + + opencode-windows-x64-baseline@1.3.0: + optional: true + + opencode-windows-x64@1.3.0: + optional: true + undici-types@6.21.0: {} From d863c7065f603048925379b3ef580143911892b7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 23 Mar 2026 12:05:50 +0000 Subject: [PATCH 6/9] :bug: Fix null text crash on paste in text editor The splitTextIntoTextBlocks function in @penpot/draft-js called .split() on the text parameter without a null check. When pasting content without text data (e.g., images only), Draft.js passes null to handlePastedText, causing a TypeError. Signed-off-by: Andrey Antukh --- frontend/packages/draft-js/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/packages/draft-js/index.js b/frontend/packages/draft-js/index.js index 23bd20af01..ed8e93932f 100644 --- a/frontend/packages/draft-js/index.js +++ b/frontend/packages/draft-js/index.js @@ -366,6 +366,9 @@ export function getInlineStyle(state, blockKey, offset) { const NEWLINE_REGEX = /\r\n?|\n/g; function splitTextIntoTextBlocks(text) { + if (text == null) { + return []; + } return text.split(NEWLINE_REGEX); } From 56f1fcdb53be62629331032496f91b8cec375e1c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 13:00:28 +0000 Subject: [PATCH 7/9] :bug: Fix crash when pasting image into text editor When pasting an image (with no text content) into the text editor, Draft.js calls handlePastedText with null/empty text. The previous fix guarded splitTextIntoTextBlocks against null, but insertText still attempted to build a fragment from an empty block array, causing Modifier.replaceWithFragment to crash with 'Cannot read properties of undefined (reading getLength)'. Fix insertText to return the original state unchanged when there are no text blocks to insert. Also guard handle-pasted-text in the ClojureScript editor to skip the insert-text call entirely when text is nil or empty. Signed-off-by: Andrey Antukh --- frontend/packages/draft-js/index.js | 4 ++++ .../app/main/ui/workspace/shapes/text/editor.cljs | 13 +++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/packages/draft-js/index.js b/frontend/packages/draft-js/index.js index ed8e93932f..84b7190537 100644 --- a/frontend/packages/draft-js/index.js +++ b/frontend/packages/draft-js/index.js @@ -375,6 +375,10 @@ function splitTextIntoTextBlocks(text) { export function insertText(state, text, attrs, inlineStyles) { const blocks = splitTextIntoTextBlocks(text); + if (blocks.length === 0) { + return state; + } + const character = CharacterMetadata.create({style: OrderedSet(inlineStyles)}); let blockArray = DraftPasteProcessor.processText( diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 135df9ecb3..616cd17b98 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -221,12 +221,13 @@ handle-pasted-text (fn [text _ _] - (let [current-block-styles (ted/get-editor-current-block-data state) - inline-styles (ted/get-editor-current-inline-styles state) - style (merge current-block-styles inline-styles) - state (-> (ted/insert-text state text style) - (handle-change))] - (st/emit! (dwt/update-editor-state shape state))) + (when (seq text) + (let [current-block-styles (ted/get-editor-current-block-data state) + inline-styles (ted/get-editor-current-inline-styles state) + style (merge current-block-styles inline-styles) + state (-> (ted/insert-text state text style) + (handle-change))] + (st/emit! (dwt/update-editor-state shape state)))) "handled")] (mf/use-layout-effect on-mount) From 750e8a9d513b3a668bad3f1f4c09a13906f40f93 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 15:35:32 +0100 Subject: [PATCH 8/9] :bug: Fix dissoc error when detaching stroke color from library (#8738) * :bug: Fix dissoc error when detaching stroke color from library The detach-value function in color-row was only passing index to on-detach, but the stroke's on-color-detach handler expects both index and color arguments. This caused a protocol error when trying to dissoc from a number instead of a map. Signed-off-by: Andrey Antukh * :bug: Fix crash when detaching color asset from stroke The color_row detach-value callback calls on-detach with (index, color), but stroke_row's local on-color-detach wrapper only took a single argument (fn [color] ...), so it received index as color and passed it to stroke.cljs which then called (dissoc index :ref-id :ref-file), crashing with 'No protocol method IMap.-dissoc defined for type number'. Fix the wrapper to accept (fn [_ color] ...) so it correctly ignores the index passed by color_row (it already has index in the closure) and forwards the actual color map to the parent handler. Signed-off-by: Andrey Antukh --------- Signed-off-by: Andrey Antukh --- .../app/main/ui/workspace/sidebar/options/rows/color_row.cljs | 4 ++-- .../main/ui/workspace/sidebar/options/rows/stroke_row.cljs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 7e6e47241a..71e6ec2eb9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -205,10 +205,10 @@ detach-value (mf/use-fn - (mf/deps on-detach index) + (mf/deps on-detach index color) (fn [_] (when on-detach - (on-detach index)))) + (on-detach index color)))) handle-select (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index ec5770eabb..7cb0956d11 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -80,7 +80,7 @@ on-color-detach (mf/use-fn (mf/deps index on-color-detach) - (fn [color] + (fn [_ color] (on-color-detach index color))) on-remove From cc03f3f884f3ffbc9a71193131d6ac5d5c5c609b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 17:49:24 +0100 Subject: [PATCH 9/9] :books: Add minor improvements to ai agents documentation --- AGENTS.md | 168 ++++++++++++------------------------------ backend/AGENTS.md | 33 +++++---- common/AGENTS.md | 27 ++++--- frontend/AGENTS.md | 73 +++++++++--------- opencode.json | 2 +- render-wasm/AGENTS.md | 2 +- 6 files changed, 114 insertions(+), 191 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 59c4ac0d26..bcb947da47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,139 +1,63 @@ -# IA Agent guide for Penpot monorepo +# AI Agent Guide -This document provides comprehensive context and guidelines for AI -agents working on this repository. +This document provides the core context and operating guidelines for AI agents +working in this repository. -CRITICAL: When you encounter a file reference (e.g., -@rules/general.md), use your Read tool to load it on a need-to-know -basis. They're relevant to the SPECIFIC task at hand. +## Before You Start +Before responding to any user request, you must: -## STOP - DO NOT PROCEED WITHOUT COMPLETING THESE STEPS +1. Read this file completely. +2. Identify which modules are affected by the task. +3. Load the `AGENTS.md` file **only** for each affected module (see the + architecture table below). Not all modules have an `AGENTS.md` — verify the + file exists before attempting to read it. +4. Do **not** load `AGENTS.md` files for unrelated modules. -Before responding to ANY user request, you MUST: +## Role: Senior Software Engineer -1. **READ** the CONTRIBUTING.md file -2. **READ** this file and has special focus on your ROLE. +You are a high-autonomy Senior Full-Stack Software Engineer. You have full +permission to navigate the codebase, modify files, and execute commands to +fulfill your tasks. Your goal is to solve complex technical tasks with high +precision while maintaining a strong focus on maintainability and performance. +### Operational Guidelines -## ROLE: SENIOR SOFTWARE ENGINEER +1. Before writing code, describe your plan. If the task is complex, break it + down into atomic steps. +2. Be concise and autonomous. +3. Do **not** touch unrelated modules unless the task explicitly requires it. +4. Commit only when explicitly asked. Follow the commit format rules in + `CONTRIBUTING.md`. +5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects + `.gitignore` by default. -You are a high-autonomy Senior Software Engineer. You have full -permission to navigate the codebase, modify files, and execute -commands to fulfill your tasks. Your goal is to solve complex -technical tasks with high precision, focusing on maintainability and -performance. +## Architecture Overview +Penpot is an open-source design tool composed of several modules: -### OPERATIONAL GUIDELINES +| Directory | Language | Purpose | Has `AGENTS.md` | +|-----------|----------|---------|:----------------:| +| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | Yes | +| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | Yes | +| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | Yes | +| `render-wasm/` | Rust -> WebAssembly | High-performance canvas renderer (Skia) | Yes | +| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | No | +| `mcp/` | TypeScript | Model Context Protocol integration | No | +| `plugins/` | TypeScript | Plugin runtime and example plugins | No | -1. Always begin by analyzing this document and understand the - architecture and read the additional context from AGENTS.md of the - affected modules. -2. Before writing code, describe your plan. If the task is complex, - break it down into atomic steps. -3. Be concise and autonomous as possible in your task. -4. Commit only if it explicitly asked, and use the CONTRIBUTING.md - document to understand the commit format guidelines. -5. Do not touch unrelated modules if not proceed or not explicitly - asked (per example you probably do not need to touch and read - docker/ directory unless the task explicitly requires it) -6. When searching code, always use `ripgrep` (rg) instead of grep if - available, as it respects `.gitignore` by default. +Some submodules use `pnpm` workspaces. The root `package.json` and +`pnpm-lock.yaml` manage shared dependencies. Helper scripts live in `scripts/`. - -## ARCHITECTURE OVERVIEW - -Penpot is a full-stack design tool composed of several distinct -components separated in modules and subdirectories: - -| Component | Language | Role | IA Agent CONTEXT | -|-----------|----------|------|---------------- -| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | @frontend/AGENTS.md | -| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | @backend/AGENTS.md | -| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | @common/AGENTS.md | -| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | @exporter/AGENTS.md | -| `render-wasm/` | Rust → WebAssembly | High-performance canvas renderer using Skia | @render-wasm/AGENTS.md | -| `mcp/` | TypeScript | Model Context Protocol integration | @mcp/AGENTS.md | -| `plugins/` | TypeScript | Plugin runtime and example plugins | @plugins/AGENTS.md | - -Several of the mentionend submodules are internall managed with `pnpm` workspaces. - - -## COMMIT FORMAT - -We have very precise rules on how our git commit messages must be -formatted. - -The commit message format is: +### Module Dependency Graph ``` - - -[body] - -[footer] +frontend ──> common +backend ──> common +exporter ──> common +frontend ──> render-wasm (loads compiled WASM) ``` -Where type is: - -- :bug: `:bug:` a commit that fixes a bug -- :sparkles: `:sparkles:` a commit that adds an improvement -- :tada: `:tada:` a commit with a new feature -- :recycle: `:recycle:` a commit that introduces a refactor -- :lipstick: `:lipstick:` a commit with cosmetic changes -- :ambulance: `:ambulance:` a commit that fixes a critical bug -- :books: `:books:` a commit that improves or adds documentation -- :construction: `:construction:` a WIP commit -- :boom: `:boom:` a commit with breaking changes -- :wrench: `:wrench:` a commit for config updates -- :zap: `:zap:` a commit with performance improvements -- :whale: `:whale:` a commit for Docker-related stuff -- :paperclip: `:paperclip:` a commit with other non-relevant changes -- :arrow_up: `:arrow_up:` a commit with dependency updates -- :arrow_down: `:arrow_down:` a commit with dependency downgrades -- :fire: `:fire:` a commit that removes files or code -- :globe_with_meridians: `:globe_with_meridians:` a commit that adds or updates - translations - -The commit should contain a sign-off at the end of the patch/commit -description body. It can be automatically added by adding the `-s` -parameter to `git commit`. - -This is an example of what the line should look like: - -``` -Signed-off-by: Andrey Antukh -``` - -Please, use your real name (sorry, no pseudonyms or anonymous -contributions are allowed). - -CRITICAL: The commit Signed-off-by is mandatory and should match the commit author. - -Each commit should have: - -- A concise subject using the imperative mood. -- The subject should capitalize the first letter, omit the period - at the end, and be no longer than 65 characters. -- A blank line between the subject line and the body. -- An entry in the CHANGES.md file if applicable, referencing the - GitHub or Taiga issue/user story using these same rules. - -Examples of good commit messages: - -- `:bug: Fix unexpected error on launching modal` -- `:bug: Set proper error message on generic error` -- `:sparkles: Enable new modal for profile` -- `:zap: Improve performance of dashboard navigation` -- `:wrench: Update default backend configuration` -- `:books: Add more documentation for authentication process` -- `:ambulance: Fix critical bug on user registration process` -- `:tada: Add new approach for user registration` - -More info: - - - https://gist.github.com/parmentf/035de27d6ed1dce0b36a - - https://gist.github.com/rxaviers/7360908 - - +`common` is referenced as a local dependency (`{:local/root "../common"}`) by +both `frontend` and `backend`. Changes to `common` can therefore affect multiple +modules — test across consumers when modifying shared code. diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 278df26e52..b4ac2ac1dd 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -7,8 +7,8 @@ Redis for messaging/caching. ## General Guidelines -This is a golden rule for backend development standards. To ensure consistency -across the Penpot JVM stack, all contributions must adhere to these criteria: +To ensure consistency across the Penpot JVM stack, all contributions must adhere +to these criteria: ### 1. Testing & Validation @@ -16,14 +16,14 @@ across the Penpot JVM stack, all contributions must adhere to these criteria: tests in `test/backend_tests/` must be added or updated. * **Execution:** - * **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific task. - * **Regression:** Run `clojure -M:dev:test` for ensure the suite passes without regressions in related functional areas. + * **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace. + * **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas. ### 2. Code Quality & Formatting * **Linting:** All code must pass `clj-kondo` checks (run `pnpm run lint:clj`) * **Formatting:** All the code must pass the formatting check (run `pnpm run - check-fmt`). Use the `pnpm run fmt` fix the formatting issues. Avoid "dirty" + check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty" diffs caused by unrelated whitespace changes. * **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in performance-critical paths to avoid reflection overhead. @@ -40,18 +40,18 @@ namespaces structure: - `app.db.*` – Database layer - `app.tasks.*` – Background job tasks - `app.main` – Integrant system setup and entrypoint -- `app.loggers` – Internal loggers (auditlog, mattermost, etc) (do not be confused with `app.common.loggin`) +- `app.loggers` – Internal loggers (auditlog, mattermost, etc.) (not to be confused with `app.common.logging`) ### RPC -The PRC methods are implement in a some kind of multimethod structure using -`app.util.serivices` namespace. The main RPC methods are collected under +The RPC methods are implemented using a multimethod-like structure via the +`app.util.services` namespace. The main RPC methods are collected under `app.rpc.commands` namespace and exposed under `/api/rpc/command/`. -The RPC method accepts POST and GET requests indistinctly and uses `Accept` -header for negotiate the response encoding (which can be transit, the defaut or -plain json). It also accepts transit (defaut) or json as input, which should be -indicated using `Content-Type` header. +The RPC method accepts POST and GET requests indistinctly and uses the `Accept` +header to negotiate the response encoding (which can be Transit — the default — +or plain JSON). It also accepts Transit (default) or JSON as input, which should +be indicated using the `Content-Type` header. The main convention is: use `get-` prefix on RPC name when we want READ operation. @@ -107,7 +107,7 @@ are config maps with `::ig/ref` for dependencies. Components implement (db/insert! conn :table row))) ``` -Almost all methods on `app.db` namespace accepts `pool`, `conn` or +Almost all methods in the `app.db` namespace accept `pool`, `conn`, or `cfg` as params. Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup. @@ -116,7 +116,7 @@ Migrations live in `src/app/migrations/` as numbered SQL files. They run automat ### Error Handling The exception helpers are defined on Common module, and are available under -`app.commin.exceptions` namespace. +`app.common.exceptions` namespace. Example of raising an exception: @@ -132,10 +132,11 @@ Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:inte ### Performance Macros (`app.common.data.macros`) -Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript: +Always prefer these macros over their `clojure.core` equivalents — they provide +optimized implementations: ```clojure -(dm/select-keys m [:a :b]) ;; ~6x faster than core/select-keys +(dm/select-keys m [:a :b]) ;; faster than core/select-keys (dm/get-in obj [:a :b :c]) ;; faster than core/get-in (dm/str "a" "b" "c") ;; string concatenation ``` diff --git a/common/AGENTS.md b/common/AGENTS.md index 996a7f4953..2659b83939 100644 --- a/common/AGENTS.md +++ b/common/AGENTS.md @@ -1,14 +1,13 @@ # Penpot Common – Agent Instructions -A shared module with code written in Clojure, ClojureScript and -JavaScript. Contains multplatform code that can be used and executed -from frontend, backend or exporter modules. It uses clojure reader -conditionals for specify platform specific implementation. +A shared module with code written in Clojure, ClojureScript, and +JavaScript. Contains multiplatform code that can be used and executed +from the frontend, backend, or exporter modules. It uses Clojure reader +conditionals to specify platform-specific implementations. ## General Guidelines -This is a golden rule for common module development. To ensure -consistency across the penpot stack, all contributions must adhere to +To ensure consistency across the Penpot stack, all contributions must adhere to these criteria: ### 1. Testing & Validation @@ -16,11 +15,11 @@ these criteria: If code is added or modified in `src/`, corresponding tests in `test/common_tests/` must be added or updated. -* **Environment:** Tests should run in a JS (nodejs) and JVM + * **Environment:** Tests should run in both JS (Node.js) and JVM environments. * **Location:** Place tests in the `test/common_tests/` directory, following the namespace structure of the source code (e.g., `app.common.colors` -> `common-tests.colors-test`). -* **Execution:** The tests should be executed on both: JS (nodejs) and JVM environments +* **Execution:** Tests should be executed on both JS (Node.js) and JVM environments: * **Isolated:** * JS: To run a focused ClojureScript unit test: edit the `test/common_tests/runner.cljs` to narrow the test suite, then @@ -37,8 +36,8 @@ If code is added or modified in `src/`, corresponding tests in * **Formatting:** All code changes must pass the formatting check * Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC * Run `pnpm run check-fmt:js` for JS - * Use the `pnpm run fmt` fix all the formatting issues (`pnpm run - fmt:clj` or `pnpm run fmt:js` for isolated formatting fix) + * Use `pnpm run fmt` to fix all formatting issues (`pnpm run + fmt:clj` or `pnpm run fmt:js` for isolated formatting fix). ## Code Conventions @@ -50,16 +49,16 @@ namespaces structure: - `app.common.types.*` – Shared data types for shapes, files, pages using Malli schemas - `app.common.schema` – Malli abstraction layer, exposes the most used functions from malli - `app.common.geom.*` – Geometry and shape transformation helpers -- `app.common.data` – Generic helpers used around all application -- `app.common.math` – Generic math helpers used around all aplication +- `app.common.data` – Generic helpers used across the entire application +- `app.common.math` – Generic math helpers used across the entire application - `app.common.json` – Generic JSON encoding/decoding helpers - `app.common.data.macros` – Performance macros used everywhere ### Reader Conditionals -We use reader conditionals to target for differentiate an -implementation depending on the target platform where code should run: +We use reader conditionals to differentiate implementations depending on the +target platform where the code runs: ```clojure #?(:clj (import java.util.UUID) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index b6f63794cc..b4ad811522 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1,13 +1,12 @@ # Penpot Frontend – Agent Instructions -ClojureScript based frontend application that uses React, RxJS as main +ClojureScript-based frontend application that uses React and RxJS as its main architectural pieces. - ## General Guidelines -This is a golden rule for frontend development standards. To ensure consistency -across the penpot stack, all contributions must adhere to these criteria: +To ensure consistency across the Penpot stack, all contributions must adhere to +these criteria: ### 1. Testing & Validation @@ -22,7 +21,7 @@ If code is added or modified in `src/`, corresponding tests in running backend. Test are developed using cljs.test. * **Mocks & Stubs:** * Use proper mocks for any side-effecting functions (e.g., API calls, storage access). - * Avoid testing through the UI (DOM), we have e2e tests for that/ + * Avoid testing through the UI (DOM); we have e2e tests for that. * Use `with-redefs` or similar ClojureScript mocking utilities to isolate the logic under test. * **No Flakiness:** Tests must be deterministic. Do not use `setTimeout` or real network calls. Use synchronous mocks for asynchronous workflows where @@ -34,15 +33,15 @@ If code is added or modified in `src/`, corresponding tests in * **Isolated:** To run a focused ClojureScript unit test: edit the `test/frontend_tests/runner.cljs` to narrow the test suite, then `pnpm run test`. - * **Regression:** Run `pnpm run test` without modifications on the runner (preferred) + * **Regression:** To run `pnpm run test` without modifications on the runner (preferred) #### Integration Tests (Playwright) Integration tests are developed under `frontend/playwright` directory, we use -mocks for remove communication with backend. +mocks for remote communication with the backend. -You should not add, modify or run the integration tests unless it exlicitly asked for. +You should not add, modify or run the integration tests unless explicitly asked. ``` @@ -50,7 +49,7 @@ pnpm run test:e2e # Playwright e2e tests pnpm run test:e2e --grep "pattern" # Single e2e test by pattern ``` -Ensure everything installed before executing tests with `./scripts/setup` script. +Ensure everything is installed before executing tests with the `./scripts/setup` script. ### 2. Code Quality & Formatting @@ -68,8 +67,8 @@ Ensure everything installed before executing tests with `./scripts/setup` script ### 3. Implementation Rules -* **Logic vs. View:** If logic is embedded in an UI component, extract it into a - function in the same namespace if is only used locally or look for a helper +* **Logic vs. View:** If logic is embedded in a UI component, extract it into a + function in the same namespace if it is only used locally, or look for a helper namespace to make it unit-testable. @@ -113,7 +112,7 @@ State is a single atom managed by a Potok store. Events implement protocols ``` The state is located under `app.main.store` namespace where we have -the `emit!` function responsible of emiting events. +the `emit!` function responsible for emitting events. Example: @@ -128,15 +127,14 @@ Example: (st/emit! (my-event))) ``` -On `app.main.refs` we have reactive references which lookup into the main state -for just inner data or precalculated data. That references are very usefull but -should be used with care because, per example if we have complex operation, this -operation will be executed on each state change, and sometimes is better to have -simple references and use react `use-memo` for more granular memoization. +On `app.main.refs` we have reactive references which look up the main state +for inner data or precalculated data. These references are very useful but +should be used with care because, for example, if we have a complex operation, +this operation will be executed on each state change. Sometimes it is better to +have simple references and use React `use-memo` for more granular memoization. -Prefer helpers from `app.util.dom` instead of using direct dom calls, if no helper is -available, prefer adding a new helper for handling it and the use the -new helper. +Prefer helpers from `app.util.dom` instead of using direct DOM calls. If no +helper is available, prefer adding a new helper and then using it. ### UI Components (React & Rumext: mf/defc) @@ -175,19 +173,20 @@ lifecycle management. These are analogous to standard React hooks: ``` The `mf/use-state` in difference with React.useState, returns an atom-like -object, where you can use `swap!` or `reset!` for to perform an update and -`deref` for get the current value. +object, where you can use `swap!` or `reset!` to perform an update and +`deref` to get the current value. -You also has `mf/deref` hook (which does not follow the `use-` naming pattern) -and it's purpose is watch (subscribe to changes) on atom or derived atom (from -okulary) and get the current value. Is mainly used for subscribe to lenses -defined in `app.main.refs` or (private lenses defined in namespaces). +You also have the `mf/deref` hook (which does not follow the `use-` naming +pattern) and its purpose is to watch (subscribe to changes on) an atom or +derived atom (from okulary) and get the current value. It is mainly used to +subscribe to lenses defined in `app.main.refs` or private lenses defined in +namespaces. Rumext also comes with improved syntax macros as alternative to `mf/use-effect` and `mf/use-memo` functions. Examples: -Example for `mf/with-memo` macro: +Example for `mf/with-effect` macro: ```clj ;; Using functions @@ -221,7 +220,7 @@ Example for `mf/with-memo` macro: (filterv #(= team-id (:team-id %))))) ``` -Prefer using the macros for it syntax simplicity. +Prefer using the macros for their syntax simplicity. #### 4. Component Usage (Hiccup Syntax) @@ -282,22 +281,22 @@ CSS modules pattern): - If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss" as *; padding: px2rem(23);`. - Do **not** create new SCSS variables for one-off values. -- Use physical directions with logical ones to support RTL/LTR naturally. - - ❌ `margin-left`, `padding-right`, `left`, `right`. - - ✅ `margin-inline-start`, `padding-inline-end`, `inset-inline-start`. -- Always use the `use-typography` mixin from `ds/typography.scss`. - - ✅ `@include t.use-typography("title-small");` +- Use physical directions with logical ones to support RTL/LTR naturally: + - Avoid: `margin-left`, `padding-right`, `left`, `right`. + - Prefer: `margin-inline-start`, `padding-inline-end`, `inset-inline-start`. +- Always use the `use-typography` mixin from `ds/typography.scss`: + - Example: `@include t.use-typography("title-small");` - Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`. - Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or legacy color variables. -- Use mixins only those defined in`ds/mixins.scss`. Avoid legacy mixins like +- Use mixins only from `ds/mixins.scss`. Avoid legacy mixins like `@include flexCenter;`. Write standard CSS (flex/grid) instead. - Use the `@use` instead of `@import`. If you go to refactor existing SCSS file, try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as *;` (Use `as *` to expose variables directly). - Avoid deep selector nesting or high-specificity (IDs). Flatten selectors: - - ❌ `.card { .title { ... } }` - - ✅ `.card-title { ... }` + - Avoid: `.card { .title { ... } }` + - Prefer: `.card-title { ... }` - Leverage component-level CSS variables for state changes (hover/focus) instead of rewriting properties. @@ -324,5 +323,5 @@ Always prefer these macros over their `clojure.core` equivalents — they compil ### Configuration `src/app/config.clj` reads globally defined variables and exposes precomputed -configuration vars ready to be used from other parts of the application +configuration values ready to be used from other parts of the application. diff --git a/opencode.json b/opencode.json index 0ab56b6192..6376bc70e5 100644 --- a/opencode.json +++ b/opencode.json @@ -1,4 +1,4 @@ { "$schema": "https://opencode.ai/config.json", - "instructions": ["CONTRIBUTING.md", "AGENTS.md"] + "instructions": ["AGENTS.md"] } diff --git a/render-wasm/AGENTS.md b/render-wasm/AGENTS.md index dfe9c3def9..511b7da0c9 100644 --- a/render-wasm/AGENTS.md +++ b/render-wasm/AGENTS.md @@ -59,4 +59,4 @@ parent/child relationships are tracked separately. The WASM module is loaded by `app.render-wasm.*` namespaces in the frontend. ClojureScript calls exported Rust functions to push shape data, then calls `render_frame`. Do not change export function -signatures without updating the ClojureScript bridge. +signatures without updating the corresponding ClojureScript bridge.