Give the editor a place to read and write comments from and it grows a review panel: threads with replies, per-block anchors, resolve and reopen, and a count badge on every commented block in the canvas.
user is a top-level key, and comments require it. Without an author the feature reports itself unavailable rather than writing an anonymous comment: no trigger, no panel, no indicators.
user.id is compared against each comment's author.id to decide what may be edited or deleted. user.name is shown on comments this session writes.
Why it sits outside the provider
Collaboration presence needs the same value. A copy inside the comments provider would be the first thing to drift once a second feature wants it.
Not a security boundary
user identifies the person to the editor's UI, in the user's own browser. Attribute writes server-side, from the session your backend already trusts. Nothing here stops a browser claiming a different name.
list cannot be turned off — without it the panel has nothing to show. Each of the four mutations takes false in place of a function, and is required rather than optional: false states that the action is unavailable, and the editor hides it rather than disabling it. See Read-only review.
The editor renders list()'s order verbatim and never re-sorts. Ordering is your store's call.
Present means edited — the panel shows an (edited) marker. Set it on edit, not on creation: a store that stamps it alongside createdAt marks every comment edited.
Takes the target state, not a toggle. The call is idempotent, so two clicks in flight cannot leave a thread inverted and your endpoint never has to read current state before writing.
The editor reports the result your store returned, not the state it asked for. A store that refuses to reopen a thread answers "still resolved", and that is what the UI and onComment report.
Withhold all four mutations and the panel stays readable and navigable. Threads and replies render, jump-to-block works, and the composer, resolve, edit and delete affordances are absent rather than disabled:
Each is independent: setResolved: false alone leaves commenting and editing intact with nothing to resolve; update: false alone removes only the pencil.
A programmatic call to a withheld mutation rejects rather than resolving — a resolved promise reads as "saved" to whoever awaited it.
Per-comment ownership composes on top: edit and delete are offered only on the current user's own comments, and only where the store supplied those mutations.
subscribe pushes remote changes into the open panel so a colleague's comment appears without a reload. Optional — without it, comments work the same and someone else's appears on the next read.
A delete carries only the id and its parent — there is no comment left to send, and the parent saves the editor a lookup.
Your own writes may echo back through here, and need no de-duplication on your side: a created for a comment already in the list is ignored, and an updated replaces it in place.
The panel filters in memory over whatever list() returned — unresolved (the default), all, or this block. Your provider decides what is visible; the editor decides how it is narrowed within that.
CommentsListParams is reserved for future filters and is empty today; the editor always calls list bare.
Comments are deliberately not paginated
list returns every thread at once. The unresolved badge and the per-block canvas counts are derived over the whole list, so a partial page would make both under-report silently — wrong rather than slow. A long-lived template caps its own growth by having list() stop returning resolved threads past some age; the panel hides those by default anyway. Version history does page, because its list is a flat menu with nothing aggregating over it.
A Comments button in the header, badged with the unresolved thread count. It appears once a template is loaded and the feature is available.
A comment indicator on each commented block, showing that block's count. Clicking it opens the panel filtered to that block.
The panel on the right: thread cards with author, relative time, an (edited) marker, the resolve toggle, and reply / edit / delete where the store allows them.
A "missing block" badge on a comment whose anchor block no longer exists, so an orphaned thread reads as orphaned rather than as a mystery.
Fires for every change the editor applied — local writes and anything subscribe pushed in. This is the hook for a "3 new comments" badge outside the editor:
Comments
Give the editor a place to read and write comments from and it grows a review panel: threads with replies, per-block anchors, resolve and reopen, and a count badge on every commented block in the canvas.
Omitted by default. With no provider the panel, its trigger and the per-block indicators do not render, and none of that UI is downloaded.
Comments are scoped to a template id, so the panel appears only once
create()orload()has attached one. See Saving & Loading.Author identity
useris a top-level key, and comments require it. Without an author the feature reports itself unavailable rather than writing an anonymous comment: no trigger, no panel, no indicators.user.idis compared against each comment'sauthor.idto decide what may be edited or deleted.user.nameis shown on comments this session writes.Why it sits outside the provider
Collaboration presence needs the same value. A copy inside the comments provider would be the first thing to drift once a second feature wants it.
Not a security boundary
useridentifies the person to the editor's UI, in the user's own browser. Attribute writes server-side, from the session your backend already trusts. Nothing here stops a browser claiming a different name.The contract
listcannot be turned off — without it the panel has nothing to show. Each of the four mutations takesfalsein place of a function, and is required rather than optional:falsestates that the action is unavailable, and the editor hides it rather than disabling it. See Read-only review.The editor renders
list()'s order verbatim and never re-sorts. Ordering is your store's call.Thread depth
One level. A thread root carries
replies; a reply never does. Flatten deeper trees in your store.updatedAtPresent means edited — the panel shows an (edited) marker. Set it on edit, not on creation: a store that stamps it alongside
createdAtmarks every comment edited.setResolvedTakes the target state, not a toggle. The call is idempotent, so two clicks in flight cannot leave a thread inverted and your endpoint never has to read current state before writing.
The editor reports the result your store returned, not the state it asked for. A store that refuses to reopen a thread answers "still resolved", and that is what the UI and
onCommentreport.Read-only review
Withhold all four mutations and the panel stays readable and navigable. Threads and replies render, jump-to-block works, and the composer, resolve, edit and delete affordances are absent rather than disabled:
Each is independent:
setResolved: falsealone leaves commenting and editing intact with nothing to resolve;update: falsealone removes only the pencil.A programmatic call to a withheld mutation rejects rather than resolving — a resolved promise reads as "saved" to whoever awaited it.
Per-comment ownership composes on top: edit and delete are offered only on the current user's own comments, and only where the store supplied those mutations.
Realtime updates
subscribepushes remote changes into the open panel so a colleague's comment appears without a reload. Optional — without it, comments work the same and someone else's appears on the next read.Return an unsubscribe function; the editor calls it when the template changes and on teardown.
A delete carries only the id and its parent — there is no comment left to send, and the parent saves the editor a lookup.
Your own writes may echo back through here, and need no de-duplication on your side: a
createdfor a comment already in the list is ignored, and anupdatedreplaces it in place.Filtering
The panel filters in memory over whatever
list()returned — unresolved (the default), all, or this block. Your provider decides what is visible; the editor decides how it is narrowed within that.CommentsListParamsis reserved for future filters and is empty today; the editor always callslistbare.Comments are deliberately not paginated
listreturns every thread at once. The unresolved badge and the per-block canvas counts are derived over the whole list, so a partial page would make both under-report silently — wrong rather than slow. A long-lived template caps its own growth by havinglist()stop returning resolved threads past some age; the panel hides those by default anyway. Version history does page, because its list is a flat menu with nothing aggregating over it.In the editor
onCommentFires for every change the editor applied — local writes and anything
subscribepushed in. This is the hook for a "3 new comments" badge outside the editor:resolvedandunresolvedare reported separately from a plainupdated, because a consumer notifying a team cares about the difference.Headless use
useCommentsfrom@templatical/coreis the reactive state on its own, without the editor:useCommentListenerwires a provider'ssubscribeinto the same state, and is a no-op for a provider without one:Using Templatical Cloud? It implements this contract with nothing to configure — see Comments on Cloud.