@sqlrooms/discuss 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,361 @@
1
- A simple discussion system for SQLRooms applications.
1
+ A simple discussion system for SQLRooms applications. Can be used for commenting and annotation with support for threaded conversations, real-time updates, and anchor-based discussions.
2
+
3
+ ## Overview
4
+
5
+ The `@sqlrooms/discuss` module provides a complete discussion system with the following key features:
6
+
7
+ - **Threaded conversations**: Support for replies to discussions and comments
8
+ - **Anchor-based discussions**: Link discussions to specific data points or UI elements
9
+ - **Real-time state management**: Built on Zustand for reactive updates
10
+ - **Customizable rendering**: Flexible component system for custom UI implementations
11
+ - **Delete confirmation**: Built-in confirmation dialogs for safe content removal
12
+ - **Highlighting**: Visual highlighting of specific discussions
13
+
14
+ ## Main Components
15
+
16
+ ### Core Components
17
+
18
+ #### `DiscussionList`
19
+
20
+ The main container component that renders all discussions with built-in forms for adding, editing, and replying to content.
21
+
22
+ ```tsx
23
+ import {DiscussionList} from '@sqlrooms/discuss';
24
+
25
+ <DiscussionList
26
+ className="flex flex-col gap-4"
27
+ renderComment={(props) => <CustomCommentRenderer {...props} />}
28
+ renderDiscussion={(props) => <CustomDiscussionRenderer {...props} />}
29
+ />;
30
+ ```
31
+
32
+ #### `CommentItem`
33
+
34
+ Individual comment renderer with built-in edit/delete actions.
35
+
36
+ ```tsx
37
+ import {CommentItem} from '@sqlrooms/discuss';
38
+
39
+ <CommentItem comment={comment} discussion={discussion}>
40
+ <div className="flex flex-col gap-1">
41
+ <div className="text-muted-foreground text-xs">
42
+ {comment.userId} - {formatTimeRelative(comment.timestamp)}
43
+ </div>
44
+ <div className="whitespace-pre-wrap text-sm">{comment.text}</div>
45
+ </div>
46
+ </CommentItem>;
47
+ ```
48
+
49
+ #### `DiscussionItem`
50
+
51
+ Container for a complete discussion thread including root comment and replies.
52
+
53
+ ```tsx
54
+ import {DiscussionItem} from '@sqlrooms/discuss';
55
+
56
+ <DiscussionItem
57
+ discussion={discussion}
58
+ renderComment={customCommentRenderer}
59
+ className="rounded border p-4"
60
+ />;
61
+ ```
62
+
63
+ ### Store Integration
64
+
65
+ #### Store Setup with `createDiscussSlice`
66
+
67
+ To use the discussion system, you need to integrate it with your project store using `createDiscussSlice`:
68
+
69
+ ```tsx
70
+ import {
71
+ createDefaultDiscussConfig,
72
+ createDiscussSlice,
73
+ DiscussSliceConfig,
74
+ DiscussSliceState,
75
+ } from '@sqlrooms/discuss';
76
+ import {
77
+ BaseProjectConfig,
78
+ createProjectBuilderSlice,
79
+ createProjectBuilderStore,
80
+ ProjectBuilderState,
81
+ } from '@sqlrooms/project-builder';
82
+ import {z} from 'zod';
83
+
84
+ // 1. Extend your app config with DiscussSliceConfig
85
+ export const AppConfig = BaseProjectConfig.merge(DiscussSliceConfig);
86
+ export type AppConfig = z.infer<typeof AppConfig>;
87
+
88
+ // 2. Extend your app state with DiscussSliceState
89
+ export type AppState = ProjectBuilderState<AppConfig> & DiscussSliceState;
90
+
91
+ // 3. Create the store with discuss slice
92
+ export const {projectStore, useProjectStore} = createProjectBuilderStore<
93
+ AppConfig,
94
+ AppState
95
+ >((set, get, store) => ({
96
+ // Add the discuss slice with a user ID
97
+ ...createDiscussSlice({userId: 'current-user-id'})(set, get, store),
98
+
99
+ // Add your project builder slice
100
+ ...createProjectBuilderSlice<AppConfig>({
101
+ connector: yourDatabaseConnector,
102
+ config: {
103
+ // Include default discuss config
104
+ ...createDefaultDiscussConfig(),
105
+ // Your other config...
106
+ layout: {
107
+ /* your layout */
108
+ },
109
+ dataSources: [
110
+ /* your data sources */
111
+ ],
112
+ },
113
+ project: {
114
+ // Your project configuration
115
+ },
116
+ })(set, get, store),
117
+ }));
118
+ ```
119
+
120
+ #### Using the Store Hook
121
+
122
+ Access discussion state and actions using the provided hook:
123
+
124
+ ```tsx
125
+ import {useStoreWithDiscussion} from '@sqlrooms/discuss';
126
+
127
+ function MyComponent() {
128
+ // Get discussions
129
+ const discussions = useStoreWithDiscussion(
130
+ (state) => state.config.discuss.discussions,
131
+ );
132
+
133
+ // Get actions
134
+ const addDiscussion = useStoreWithDiscussion(
135
+ (state) => state.discuss.addDiscussion,
136
+ );
137
+ const setReplyToItem = useStoreWithDiscussion(
138
+ (state) => state.discuss.setReplyToItem,
139
+ );
140
+
141
+ // Add a new discussion
142
+ const handleAddDiscussion = (text: string, anchorId?: string) => {
143
+ addDiscussion(text, anchorId);
144
+ };
145
+
146
+ // Start replying to a discussion
147
+ const handleReply = (discussionId: string) => {
148
+ setReplyToItem({discussionId});
149
+ };
150
+ }
151
+ ```
152
+
153
+ ## Usage Examples
154
+
155
+ ### Basic Discussion Panel
156
+
157
+ ```tsx
158
+ import {DiscussionList, CommentItem} from '@sqlrooms/discuss';
159
+ import {useStoreWithDiscussion} from '@sqlrooms/discuss';
160
+ import {formatTimeRelative} from '@sqlrooms/utils';
161
+
162
+ const DiscussionPanel = () => {
163
+ const discussions = useStoreWithDiscussion(
164
+ (state) => state.config.discuss.discussions,
165
+ );
166
+
167
+ return (
168
+ <div className="h-full">
169
+ {discussions.length === 0 ? (
170
+ <div className="py-10 text-center text-gray-400">
171
+ <p>No comments yet. Start a discussion!</p>
172
+ </div>
173
+ ) : (
174
+ <DiscussionList
175
+ className="flex flex-col gap-4"
176
+ renderComment={(props) => {
177
+ const {comment, discussion} = props;
178
+ const {anchorId} = discussion;
179
+ const isRootComment = comment.id === discussion.rootComment.id;
180
+
181
+ return (
182
+ <CommentItem {...props}>
183
+ <div className="flex flex-col gap-1">
184
+ {anchorId && isRootComment && (
185
+ <div className="text-md flex items-center gap-2 font-medium">
186
+ 📍 Linked to: {anchorId}
187
+ </div>
188
+ )}
189
+ <div className="text-muted-foreground text-xs">
190
+ {comment.userId} - {formatTimeRelative(comment.timestamp)}
191
+ </div>
192
+ <div className="whitespace-pre-wrap text-sm">
193
+ {comment.text}
194
+ </div>
195
+ </div>
196
+ </CommentItem>
197
+ );
198
+ }}
199
+ />
200
+ )}
201
+ </div>
202
+ );
203
+ };
204
+ ```
205
+
206
+ ### Anchor-Based Discussions
207
+
208
+ Link discussions to specific data points or UI elements:
209
+
210
+ ```tsx
211
+ import {useStoreWithDiscussion} from '@sqlrooms/discuss';
212
+
213
+ function DataVisualization() {
214
+ const addDiscussion = useStoreWithDiscussion(
215
+ (state) => state.discuss.addDiscussion,
216
+ );
217
+ const setHighlightedDiscussionId = useStoreWithDiscussion(
218
+ (state) => state.discuss.setHighlightedDiscussionId,
219
+ );
220
+
221
+ // Handle clicking on a data point
222
+ const handleDataPointClick = (dataId: string) => {
223
+ // Add a discussion linked to this data point
224
+ addDiscussion('What do you think about this data point?', dataId);
225
+ };
226
+
227
+ // Highlight related discussion when hovering over data
228
+ const handleDataPointHover = (dataId: string) => {
229
+ const discussions =
230
+ useStoreWithDiscussion.getState().config.discuss.discussions;
231
+ const relatedDiscussion = discussions.find((d) => d.anchorId === dataId);
232
+ if (relatedDiscussion) {
233
+ setHighlightedDiscussionId(relatedDiscussion.id);
234
+ }
235
+ };
236
+
237
+ return (
238
+ <div>
239
+ {/* Your data visualization with clickable elements */}
240
+ <div
241
+ onClick={() => handleDataPointClick('airport-LAX')}
242
+ onMouseEnter={() => handleDataPointHover('airport-LAX')}
243
+ className="cursor-pointer hover:bg-blue-100"
244
+ >
245
+ LAX Airport
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+ ```
251
+
252
+ ### Custom Comment Rendering
253
+
254
+ ```tsx
255
+ import {DiscussionList, CommentItem} from '@sqlrooms/discuss';
256
+ import {Avatar, Badge} from '@sqlrooms/ui';
257
+
258
+ const CustomDiscussionPanel = () => {
259
+ return (
260
+ <DiscussionList
261
+ renderComment={(props) => {
262
+ const {comment, discussion} = props;
263
+
264
+ return (
265
+ <CommentItem {...props}>
266
+ <div className="flex gap-3">
267
+ <Avatar className="h-8 w-8">
268
+ {comment.userId.charAt(0).toUpperCase()}
269
+ </Avatar>
270
+ <div className="flex-1">
271
+ <div className="mb-1 flex items-center gap-2">
272
+ <span className="font-medium">{comment.userId}</span>
273
+ <Badge variant="secondary" className="text-xs">
274
+ {formatTimeRelative(comment.timestamp)}
275
+ </Badge>
276
+ </div>
277
+ <div className="text-sm leading-relaxed">{comment.text}</div>
278
+ </div>
279
+ </div>
280
+ </CommentItem>
281
+ );
282
+ }}
283
+ />
284
+ );
285
+ };
286
+ ```
287
+
288
+ ### Programmatic Discussion Management
289
+
290
+ ```tsx
291
+ import {useStoreWithDiscussion} from '@sqlrooms/discuss';
292
+
293
+ function DiscussionManager() {
294
+ const {
295
+ addDiscussion,
296
+ removeDiscussion,
297
+ addComment,
298
+ setReplyToItem,
299
+ setEditingItem,
300
+ submitEdit,
301
+ } = useStoreWithDiscussion((state) => state.discuss);
302
+
303
+ // Add a new discussion
304
+ const createDiscussion = () => {
305
+ addDiscussion('New discussion topic', 'optional-anchor-id');
306
+ };
307
+
308
+ // Reply to a discussion
309
+ const replyToDiscussion = (discussionId: string) => {
310
+ setReplyToItem({discussionId});
311
+ // User can now type in the form and call submitEdit
312
+ };
313
+
314
+ // Edit a comment
315
+ const editComment = (discussionId: string, commentId: string) => {
316
+ setEditingItem({discussionId, commentId});
317
+ // User can now edit in the form and call submitEdit
318
+ };
319
+
320
+ // Direct comment addition (bypassing UI state)
321
+ const addDirectComment = (discussionId: string, text: string) => {
322
+ addComment(discussionId, text);
323
+ };
324
+
325
+ return (
326
+ <div className="flex gap-2">
327
+ <button onClick={createDiscussion}>New Discussion</button>
328
+ <button onClick={() => replyToDiscussion('discussion-id')}>Reply</button>
329
+ <button onClick={() => editComment('discussion-id', 'comment-id')}>
330
+ Edit
331
+ </button>
332
+ </div>
333
+ );
334
+ }
335
+ ```
336
+
337
+ ## API Reference
338
+
339
+ ### Types
340
+
341
+ - `Comment`: Individual comment with id, userId, text, timestamp, and optional parentId
342
+ - `Discussion`: Container with id, optional anchorId, rootComment, and array of reply comments
343
+ - `DiscussSliceConfig`: Configuration type for the discuss slice
344
+ - `DiscussSliceState`: State type including all discussion actions and UI state
345
+
346
+ ### Key Functions
347
+
348
+ - `createDiscussSlice({ userId })`: Creates the discussion slice for your store
349
+ - `createDefaultDiscussConfig()`: Returns default configuration for discussions
350
+ - `useStoreWithDiscussion(selector)`: Hook to access discussion state and actions
351
+
352
+ ### Main Actions
353
+
354
+ - `submitEdit(text)`: Submit based on current UI state (add/reply/edit)
355
+ - `addDiscussion(text, anchorId?)`: Add new discussion
356
+ - `addComment(discussionId, text, parentId?)`: Add reply to discussion
357
+ - `setReplyToItem(item)`: Set reply context for UI
358
+ - `setEditingItem(item)`: Set editing context for UI
359
+ - `setHighlightedDiscussionId(id)`: Highlight specific discussion
360
+
361
+ This module integrates seamlessly with the SQLRooms ecosystem and provides a complete foundation for building collaborative discussion features in your applications.
@@ -17,14 +17,14 @@ export declare const CommentBase: z.ZodObject<{
17
17
  timestamp: Date;
18
18
  }>;
19
19
  export type CommentBase = z.infer<typeof CommentBase>;
20
- export declare const Comment: z.ZodObject<z.objectUtil.extendShape<{
20
+ export declare const Comment: z.ZodObject<{
21
21
  id: z.ZodString;
22
22
  userId: z.ZodString;
23
23
  text: z.ZodString;
24
24
  timestamp: z.ZodDate;
25
- }, {
25
+ } & {
26
26
  parentId: z.ZodOptional<z.ZodString>;
27
- }>, "strip", z.ZodTypeAny, {
27
+ }, "strip", z.ZodTypeAny, {
28
28
  id: string;
29
29
  userId: string;
30
30
  text: string;
@@ -41,14 +41,14 @@ export type Comment = z.infer<typeof Comment>;
41
41
  export declare const Discussion: z.ZodObject<{
42
42
  id: z.ZodString;
43
43
  anchorId: z.ZodOptional<z.ZodString>;
44
- rootComment: z.ZodObject<z.objectUtil.extendShape<{
44
+ rootComment: z.ZodObject<{
45
45
  id: z.ZodString;
46
46
  userId: z.ZodString;
47
47
  text: z.ZodString;
48
48
  timestamp: z.ZodDate;
49
- }, {
49
+ } & {
50
50
  parentId: z.ZodOptional<z.ZodString>;
51
- }>, "strip", z.ZodTypeAny, {
51
+ }, "strip", z.ZodTypeAny, {
52
52
  id: string;
53
53
  userId: string;
54
54
  text: string;
@@ -61,14 +61,14 @@ export declare const Discussion: z.ZodObject<{
61
61
  timestamp: Date;
62
62
  parentId?: string | undefined;
63
63
  }>;
64
- comments: z.ZodArray<z.ZodObject<z.objectUtil.extendShape<{
64
+ comments: z.ZodArray<z.ZodObject<{
65
65
  id: z.ZodString;
66
66
  userId: z.ZodString;
67
67
  text: z.ZodString;
68
68
  timestamp: z.ZodDate;
69
- }, {
69
+ } & {
70
70
  parentId: z.ZodOptional<z.ZodString>;
71
- }>, "strip", z.ZodTypeAny, {
71
+ }, "strip", z.ZodTypeAny, {
72
72
  id: string;
73
73
  userId: string;
74
74
  text: string;
@@ -122,14 +122,14 @@ export declare const DiscussSliceConfig: z.ZodObject<{
122
122
  discussions: z.ZodArray<z.ZodObject<{
123
123
  id: z.ZodString;
124
124
  anchorId: z.ZodOptional<z.ZodString>;
125
- rootComment: z.ZodObject<z.objectUtil.extendShape<{
125
+ rootComment: z.ZodObject<{
126
126
  id: z.ZodString;
127
127
  userId: z.ZodString;
128
128
  text: z.ZodString;
129
129
  timestamp: z.ZodDate;
130
- }, {
130
+ } & {
131
131
  parentId: z.ZodOptional<z.ZodString>;
132
- }>, "strip", z.ZodTypeAny, {
132
+ }, "strip", z.ZodTypeAny, {
133
133
  id: string;
134
134
  userId: string;
135
135
  text: string;
@@ -142,14 +142,14 @@ export declare const DiscussSliceConfig: z.ZodObject<{
142
142
  timestamp: Date;
143
143
  parentId?: string | undefined;
144
144
  }>;
145
- comments: z.ZodArray<z.ZodObject<z.objectUtil.extendShape<{
145
+ comments: z.ZodArray<z.ZodObject<{
146
146
  id: z.ZodString;
147
147
  userId: z.ZodString;
148
148
  text: z.ZodString;
149
149
  timestamp: z.ZodDate;
150
- }, {
150
+ } & {
151
151
  parentId: z.ZodOptional<z.ZodString>;
152
- }>, "strip", z.ZodTypeAny, {
152
+ }, "strip", z.ZodTypeAny, {
153
153
  id: string;
154
154
  userId: string;
155
155
  text: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlrooms/discuss",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,11 +16,11 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@paralleldrive/cuid2": "^2.2.2",
19
- "@sqlrooms/project-builder": "0.14.0",
20
- "@sqlrooms/ui": "0.14.0",
21
- "@sqlrooms/utils": "0.14.0",
19
+ "@sqlrooms/project-builder": "0.16.0",
20
+ "@sqlrooms/ui": "0.16.0",
21
+ "@sqlrooms/utils": "0.16.0",
22
22
  "immer": "^10.1.1",
23
- "zod": "^3.24.1"
23
+ "zod": "^3.25.57"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": "*",
@@ -29,7 +29,8 @@
29
29
  "scripts": {
30
30
  "dev": "tsc -w",
31
31
  "build": "tsc",
32
- "lint": "eslint ."
32
+ "lint": "eslint .",
33
+ "typedoc": "typedoc"
33
34
  },
34
- "gitHead": "fb82a7e1be842348246bafdfeb93b6c41f5f9eb5"
35
+ "gitHead": "94dfdc97417dd715b9d6886122a3944b789b0b13"
35
36
  }