Show and manage comments while designing in the workspace (#10275)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
andrés gonzález 2026-06-19 09:30:56 +02:00 committed by GitHub
parent 1c8d26faaf
commit 564cd1b528
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 156 additions and 18 deletions

View File

@ -8,7 +8,7 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi
<p class="main-paragraph">Comments allow the team to have one priceless conversation getting and providing feedback right over the designs and prototypes.<p>
<h2 id="comment-workspace">At the workspace</h2>
<p>At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the <kbd>C</kbd> key. <a href="/user-guide/designing/workspace-basics/#comments">More about comments at the Workspace</a></p>
<p>At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the <kbd>C</kbd> key. Comment bubbles remain visible while you design, so you can read and manage feedback without leaving your design tools. Use <strong>View → Hide comments</strong> / <strong>Show comments</strong> or <kbd>Ctrl+Shift+C</kbd> (<kbd>Cmd+Shift+C</kbd> on macOS) to toggle bubbles on the canvas. <a href="/user-guide/designing/workspace-basics/#comments">More about comments at the Workspace</a></p>
<p>URLs in comments are detected automatically and open in a new browser tab when clicked.</p>
<h2 id="comment-viewmode">At the View mode</h2>

View File

@ -160,6 +160,9 @@ press <kbd>Shift/⇧</kbd> + left click over the right arrow of a group or a boa
<li>Write your comment in the text box.</li>
<li>Press Post to leave the comment or Cancel to not do it.</li>
</ol>
<p>Comment bubbles stay visible on the canvas while you design. You can reply to, edit, and manage threads without staying in Comments mode.</p>
<p>Use <strong>View → Hide comments</strong> or <strong>View → Show comments</strong> (after Lock guides) to toggle comment bubbles on the canvas. You can also press <kbd>Ctrl+Shift+C</kbd> (<kbd>Cmd+Shift+C</kbd> on macOS). This preference is saved in your user settings.</p>
<p>Press <kbd>C</kbd> 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.</p>
<p>URLs in comments are detected automatically and open in a new browser tab when clicked.</p>
<h3>How to reply a comment</h3>
<ol>

View File

@ -211,6 +211,11 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
<td style="text-align: center;"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>R</kbd></td>
<td style="text-align: center;"><kbd>⌘</kbd><kbd>⇧</kbd><kbd>R</kbd></td>
</tr>
<tr>
<td>Show/hide comments</td>
<td style="text-align: center;"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>C</kbd></td>
<td style="text-align: center;"><kbd>⌘</kbd><kbd>⇧</kbd><kbd>C</kbd></td>
</tr>
<tr>
<td>Show/hide shortcuts</td>
<td style="text-align: center;"><kbd>?</kbd></td>

View File

@ -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))))))))

View File

@ -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)]

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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)]

View File

@ -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)

View File

@ -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"

View File

@ -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"