From 564cd1b5286bbbfcd1f5c274896dc27fb8fd3142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Fri, 19 Jun 2026 09:30:56 +0200 Subject: [PATCH] :sparkles: Show and manage comments while designing in the workspace (#10275) Co-authored-by: Cursor --- docs/user-guide/account-teams/comments.njk | 2 +- .../user-guide/designing/workspace-basics.njk | 3 + docs/user-guide/first-steps/shortcuts.njk | 5 ++ .../src/app/main/data/workspace/comments.cljs | 60 +++++++++++++++---- .../src/app/main/data/workspace/drawing.cljs | 4 ++ .../src/app/main/data/workspace/layout.cljs | 5 +- .../app/main/data/workspace/shortcuts.cljs | 7 +++ .../src/app/main/ui/workspace/main_menu.cljs | 24 +++++++- .../src/app/main/ui/workspace/viewport.cljs | 4 +- .../main/ui/workspace/viewport/comments.cljs | 32 ++++++++++ .../app/main/ui/workspace/viewport_wasm.cljs | 4 +- frontend/translations/en.po | 12 ++++ frontend/translations/es.po | 12 ++++ 13 files changed, 156 insertions(+), 18 deletions(-) diff --git a/docs/user-guide/account-teams/comments.njk b/docs/user-guide/account-teams/comments.njk index bec75f4858..4d5a2a769b 100644 --- a/docs/user-guide/account-teams/comments.njk +++ b/docs/user-guide/account-teams/comments.njk @@ -8,7 +8,7 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi

Comments allow the team to have one priceless conversation getting and providing feedback right over the designs and prototypes.

At the workspace

-

At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the C key. More about comments at the Workspace

+

At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the C key. Comment bubbles remain visible while you design, so you can read and manage feedback without leaving your design tools. Use View → Hide comments / Show comments or Ctrl+Shift+C (Cmd+Shift+C on macOS) to toggle bubbles on the canvas. More about comments at the Workspace

URLs in comments are detected automatically and open in a new browser tab when clicked.

At the View mode

diff --git a/docs/user-guide/designing/workspace-basics.njk b/docs/user-guide/designing/workspace-basics.njk index f6cc0f14dd..e87ff0d740 100644 --- a/docs/user-guide/designing/workspace-basics.njk +++ b/docs/user-guide/designing/workspace-basics.njk @@ -160,6 +160,9 @@ press Shift/⇧ + left click over the right arrow of a group or a boa
  • Write your comment in the text box.
  • Press Post to leave the comment or Cancel to not do it.
  • +

    Comment bubbles stay visible on the canvas while you design. You can reply to, edit, and manage threads without staying in Comments mode.

    +

    Use View → Hide comments or View → Show comments (after Lock guides) to toggle comment bubbles on the canvas. You can also press Ctrl+Shift+C (Cmd+Shift+C on macOS). This preference is saved in your user settings.

    +

    Press C or the comment button to enter Comments mode when you want to add a new comment on the canvas. Comments mode still works as before and shows comments if they were hidden.

    URLs in comments are detected automatically and open in a new browser tab when clicked.

    How to reply a comment

      diff --git a/docs/user-guide/first-steps/shortcuts.njk b/docs/user-guide/first-steps/shortcuts.njk index f653090fd6..76b323ec31 100644 --- a/docs/user-guide/first-steps/shortcuts.njk +++ b/docs/user-guide/first-steps/shortcuts.njk @@ -211,6 +211,11 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface CtrlShiftR R + + Show/hide comments + CtrlShiftC + C + Show/hide shortcuts ? diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index a617753fa3..450268cbd1 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -16,14 +16,17 @@ [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] + [app.main.data.notifications :as ntf] [app.main.data.workspace.common :as dwco] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.edition :as dwe] + [app.main.data.workspace.layout :as dwlo] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.zoom :as dwz] [app.main.repo :as rp] [app.main.router :as rt] [app.main.streams :as ms] + [app.util.i18n :refer [tr]] [app.util.mouse :as mse] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -31,6 +34,18 @@ (declare handle-interrupt) (declare handle-comment-layer-click) +(defn toggle-comments-visibility + [& {:keys [origin] :as _opts}] + (ptk/reify ::toggle-comments-visibility + ptk/WatchEvent + (watch [_ state _] + (let [visible? (contains? (:workspace-layout state) :display-comments)] + (rx/of (vary-meta (dwlo/toggle-layout-flag :display-comments) + assoc ::ev/origin (or origin "workspace")) + (ntf/success (tr (if visible? + "workspace.toast.comments-hidden" + "workspace.toast.comments-visible")))))))) + (defn initialize-comments [file-id] (ptk/reify ::initialize-comments @@ -58,12 +73,18 @@ (ptk/reify ::handle-interrupt ptk/WatchEvent (watch [_ state _] - (let [local (:comments-local state)] + (let [local (:comments-local state) + comments-mode? (= :comments (get-in state [:workspace-drawing :tool]))] (cond (:draft local) (rx/of (dcmt/close-thread)) (:open local) (rx/of (dcmt/close-thread)) - :else (rx/of (dwe/clear-edition-mode) - (dws/deselect-all true))))))) + ;; Only clear edition / deselect on interrupt while the comments + ;; tool is active. When comments are merely visible during design, + ;; `select-shape` emits `:interrupt` and this would otherwise wipe + ;; the freshly selected shape, breaking click selection. + comments-mode? (rx/of (dwe/clear-edition-mode) + (dws/deselect-all true)) + :else (rx/empty)))))) ;; Event responsible of the what should be executed when user clicked ;; on the comments layer. An option can be create a new draft thread, @@ -74,15 +95,17 @@ (ptk/reify ::handle-comment-layer-click ptk/WatchEvent (watch [_ state _] - (let [local (:comments-local state)] - (if (some? (:open local)) - (rx/of (dcmt/close-thread)) - (let [page-id (:current-page-id state) - file-id (:current-file-id state) - params {:position position - :page-id page-id - :file-id file-id}] - (rx/of (dcmt/create-draft params)))))))) + (if (not= :comments (get-in state [:workspace-drawing :tool])) + (rx/empty) + (let [local (:comments-local state)] + (if (some? (:open local)) + (rx/of (dcmt/close-thread)) + (let [page-id (:current-page-id state) + file-id (:current-file-id state) + params {:position position + :page-id page-id + :file-id file-id}] + (rx/of (dcmt/create-draft params))))))))) (defn center-to-comment-thread [{:keys [position] :as thread}] @@ -126,7 +149,18 @@ thread-id (:id thread)] (rx/concat - (rx/of #(update % :comment-threads assoc thread-id thread)) + (rx/of (fn [state] + (-> state + (update :comment-threads assoc thread-id thread) + ;; Keep the page positions map in sync so subsequent + ;; frame moves compute the relative offset from the + ;; latest position instead of a stale one. + (dsh/update-page page-id + #(update-in % [:comment-thread-positions thread-id] + (fn [pos] + (-> pos + (assoc :position (:position thread)) + (assoc :frame-id (:frame-id thread))))))))) (->> (rp/cmd! :update-comment-thread-position thread) (rx/catch #(rx/throw {:type :update-comment-thread-position})) (rx/ignore)))))))) diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index 68cd8c6b92..03d7545bb8 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -14,6 +14,7 @@ [app.main.data.workspace.drawing.box :as box] [app.main.data.workspace.drawing.common :as common] [app.main.data.workspace.drawing.curve :as curve] + [app.main.data.workspace.layout :as dwlo] [app.main.data.workspace.path :as path] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -57,6 +58,9 @@ ;; NOTE: comments are a special case and they manage they ;; own interrupt cycle. + (when (= tool :comments) + (rx/of (dwlo/toggle-layout-flag :display-comments :force? true))) + (when (and (not= tool :comments) (not= tool :path)) (let [stopper (rx/filter (ptk/type? ::clear-drawing) stream)] diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs index 2c006b4988..2f04bcfa53 100644 --- a/frontend/src/app/main/data/workspace/layout.cljs +++ b/frontend/src/app/main/data/workspace/layout.cljs @@ -25,6 +25,7 @@ :element-options :rulers :display-guides + :display-comments :lock-guides :snap-guides :scale-text @@ -60,6 +61,7 @@ :element-options :rulers :display-guides + :display-comments :snap-guides :dynamic-alignment :display-artboard-names @@ -143,7 +145,8 @@ {:hide-palettes :app.main.data.workspace/hide-palettes? :colorpalette :app.main.data.workspace/show-colorpalette? :textpalette :app.main.data.workspace/show-textpalette? - :rulers :app.main.data.workspace/show-rulers?}) + :rulers :app.main.data.workspace/show-rulers? + :display-comments :app.main.data.workspace/show-comments?}) (defn load-layout-flags "Given the current layout flags, and updates them with the data diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index a68934efc0..5450a6d7a7 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -16,6 +16,7 @@ [app.main.data.shortcuts :as ds] [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as mdc] + [app.main.data.workspace.comments :as dwcm] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.layers :as dwly] [app.main.data.workspace.libraries :as dwl] @@ -322,6 +323,12 @@ :subsections [:tools] :fn #(st/emit! (dwd/select-for-drawing :comments))} + :toggle-comments-visibility + {:tooltip (ds/meta-shift "C") + :command (ds/c-mod "shift+c") + :subsections [:main-menu] + :fn #(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace-shortcuts"}))} + :insert-image {:tooltip (ds/shift "K") :command "shift+k" :subsections [:tools] diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 8b87b01d5a..84ac8883e1 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -22,6 +22,7 @@ [app.main.data.profile :as du] [app.main.data.shortcuts :as scd] [app.main.data.workspace :as dw] + [app.main.data.workspace.comments :as dwcm] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.mcp :as mcp] [app.main.data.workspace.shortcuts :as sc] @@ -355,7 +356,15 @@ (r/set-resize-type! :bottom) (st/emit! (dw/remove-layout-flag :colorpalette) (-> (dw/toggle-layout-flag :textpalette) - (vary-meta assoc ::ev/origin "workspace:menu")))))] + (vary-meta assoc ::ev/origin "workspace:menu"))))) + + toggle-comments-visibility + (mf/use-fn + (mf/deps on-close) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dwcm/toggle-comments-visibility {:origin "workspace:menu"})) + (on-close)))] [:> dropdown-menu* {:show true :class (stl/css :base-menu :sub-menu :pos-3) @@ -400,6 +409,19 @@ (tr "workspace.header.menu.unlock-guides") (tr "workspace.header.menu.lock-guides"))]] + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) + :on-click toggle-comments-visibility + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-comments-visibility event))) + :data-testid "display-comments" + :id "file-menu-comments"} + [:span {:class (stl/css :item-name)} + (if (contains? layout :display-comments) + (tr "workspace.header.menu.hide-comments") + (tr "workspace.header.menu.show-comments"))] + [:> shortcuts* {:id :toggle-comments-visibility}]] + (when-not ^boolean read-only? [:* [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 3c5fb33114..ce03ca1f3a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -223,7 +223,9 @@ on-frame-select (actions/on-frame-select selected read-only?) disable-events? (contains? layout :comments) - show-comments? (= drawing-tool :comments) + comments-mode? (= drawing-tool :comments) + show-comments? (or comments-mode? + (contains? layout :display-comments)) show-cursor-tooltip? tooltip show-draw-area? drawing-obj show-gradient-handlers? (= (count selected) 1) diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index f4b3e9d422..8468b29b4b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -8,6 +8,9 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] [app.main.data.comments :as dcm] [app.main.data.workspace.comments :as dwcm] [app.main.refs :as refs] @@ -31,12 +34,38 @@ threads-map (mf/deref refs/threads) + ;; Active transform modifiers (e.g. while dragging a board). We use + ;; them to move comment bubbles live alongside their frame, instead of + ;; only repositioning them at drop time. The SVG (legacy) renderer keeps + ;; them in `:workspace-modifiers`, while the WASM renderer pushes them + ;; through the `wasm-modifiers` stream as plain transform matrices. + modifiers (mf/deref refs/workspace-modifiers) + wasm-mods (into {} (mf/deref refs/workspace-wasm-modifiers)) + objects (mf/deref refs/workspace-page-objects) + threads (mf/with-memo [threads-map local profile page-id] (->> (vals threads-map) (filter #(= (:page-id %) page-id)) (dcm/apply-filters local profile))) + ;; Returns the position translation matrix for a frame that is being + ;; transformed, or nil when the frame has no active modifier. The delta + ;; matches `move-frame-comment-threads` (frame top-left displacement) so + ;; the bubble does not jump when the modifier is committed. + frame-position-modifier + (fn [frame-id] + (when-let [frame (get objects frame-id)] + (let [frame' + (if-let [modifier (get-in modifiers [frame-id :modifiers])] + (gsh/transform-shape frame modifier) + (when-let [transform (get wasm-mods frame-id)] + (gsh/apply-transform frame transform)))] + (when (some? frame') + (let [delta (gpt/to-vec (gpt/point (:x frame) (:y frame)) + (gpt/point (:x frame') (:y frame')))] + (gmt/translate-matrix delta)))))) + viewport (assoc vport :offset-x pos-x :offset-y pos-y) @@ -67,9 +96,11 @@ (if group? [:> cmt/comment-floating-group* {:thread-group thread-group :zoom zoom + :position-modifier (frame-position-modifier (:frame-id thread)) :key (:seqn thread)}] [:> cmt/comment-floating-bubble* {:thread thread :zoom zoom + :position-modifier (frame-position-modifier (:frame-id thread)) :is-open (= (:id thread) (:open local)) :key (:seqn thread)}]))) @@ -79,6 +110,7 @@ [:> cmt/comment-floating-thread* {:thread thread :viewport viewport + :position-modifier (frame-position-modifier (:frame-id thread)) :zoom zoom}]))) (when-let [draft (:draft local)] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 45d2b5177e..ed2deb2646 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -333,7 +333,9 @@ on-frame-select (actions/on-frame-select selected read-only?) disable-events? (contains? layout :comments) - show-comments? (= drawing-tool :comments) + comments-mode? (= drawing-tool :comments) + show-comments? (or comments-mode? + (contains? layout :display-comments)) show-cursor-tooltip? tooltip show-draw-area? drawing-obj show-gradient-handlers? (= (count selected) 1) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 1faf2b134d..60e3a8abb4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6767,6 +6767,18 @@ msgstr "Hide fonts palette" msgid "workspace.header.menu.lock-guides" msgstr "Lock guides" +msgid "workspace.header.menu.show-comments" +msgstr "Show comments" + +msgid "workspace.header.menu.hide-comments" +msgstr "Hide comments" + +msgid "workspace.toast.comments-visible" +msgstr "Comments visible" + +msgid "workspace.toast.comments-hidden" +msgstr "Comments hidden" + #: src/app/main/ui/workspace/main_menu.cljs:853 msgid "workspace.header.menu.mcp.plugin.status.connect" msgstr "Connect" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 97774a7169..97a32ab802 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6596,6 +6596,18 @@ msgstr "Ocultar nombres de tableros" msgid "workspace.header.menu.hide-guides" msgstr "Ocultar guías" +msgid "workspace.header.menu.show-comments" +msgstr "Mostrar comentarios" + +msgid "workspace.header.menu.hide-comments" +msgstr "Ocultar comentarios" + +msgid "workspace.toast.comments-visible" +msgstr "Comentarios visibles" + +msgid "workspace.toast.comments-hidden" +msgstr "Comentarios ocultos" + #: src/app/main/ui/workspace/main_menu.cljs:413 msgid "workspace.header.menu.hide-palette" msgstr "Ocultar paleta de colores"