@btst/stack 2.7.0 → 2.8.1

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.
Files changed (181) hide show
  1. package/README.md +1 -0
  2. package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.cjs +13 -0
  3. package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.mjs +11 -0
  4. package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.cjs +17 -0
  5. package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.mjs +15 -0
  6. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -7
  7. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -7
  8. package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.cjs +48 -52
  9. package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.mjs +49 -53
  10. package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.cjs +34 -37
  11. package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.mjs +35 -38
  12. package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.cjs +4 -21
  13. package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.mjs +4 -21
  14. package/dist/packages/stack/src/plugins/comments/api/getters.cjs +284 -0
  15. package/dist/packages/stack/src/plugins/comments/api/getters.mjs +280 -0
  16. package/dist/packages/stack/src/plugins/comments/api/mutations.cjs +118 -0
  17. package/dist/packages/stack/src/plugins/comments/api/mutations.mjs +112 -0
  18. package/dist/packages/stack/src/plugins/comments/api/plugin.cjs +335 -0
  19. package/dist/packages/stack/src/plugins/comments/api/plugin.mjs +333 -0
  20. package/dist/packages/stack/src/plugins/comments/api/query-key-defs.cjs +60 -0
  21. package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +55 -0
  22. package/dist/packages/stack/src/plugins/comments/api/serializers.cjs +23 -0
  23. package/dist/packages/stack/src/plugins/comments/api/serializers.mjs +21 -0
  24. package/dist/packages/stack/src/plugins/comments/client/components/comment-count.cjs +46 -0
  25. package/dist/packages/stack/src/plugins/comments/client/components/comment-count.mjs +44 -0
  26. package/dist/packages/stack/src/plugins/comments/client/components/comment-form.cjs +86 -0
  27. package/dist/packages/stack/src/plugins/comments/client/components/comment-form.mjs +84 -0
  28. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +540 -0
  29. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +538 -0
  30. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.cjs +64 -0
  31. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.cjs +426 -0
  32. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.mjs +424 -0
  33. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.mjs +62 -0
  34. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.cjs +66 -0
  35. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.cjs +256 -0
  36. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.mjs +254 -0
  37. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.mjs +64 -0
  38. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.cjs +86 -0
  39. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.cjs +191 -0
  40. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.mjs +189 -0
  41. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.mjs +84 -0
  42. package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.cjs +27 -0
  43. package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.mjs +25 -0
  44. package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.cjs +37 -0
  45. package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.mjs +35 -0
  46. package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +476 -0
  47. package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +464 -0
  48. package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.cjs +67 -0
  49. package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.mjs +65 -0
  50. package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.cjs +27 -0
  51. package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.mjs +25 -0
  52. package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.cjs +30 -0
  53. package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.mjs +28 -0
  54. package/dist/packages/stack/src/plugins/comments/client/localization/index.cjs +13 -0
  55. package/dist/packages/stack/src/plugins/comments/client/localization/index.mjs +11 -0
  56. package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +116 -0
  57. package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +114 -0
  58. package/dist/packages/stack/src/plugins/comments/client/utils.cjs +41 -0
  59. package/dist/packages/stack/src/plugins/comments/client/utils.mjs +37 -0
  60. package/dist/packages/stack/src/plugins/comments/db.cjs +75 -0
  61. package/dist/packages/stack/src/plugins/comments/db.mjs +73 -0
  62. package/dist/packages/stack/src/plugins/comments/schemas.cjs +45 -0
  63. package/dist/packages/stack/src/plugins/comments/schemas.mjs +38 -0
  64. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +0 -1
  65. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +0 -1
  66. package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +39 -22
  67. package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +40 -23
  68. package/dist/packages/ui/src/components/avatar.mjs +1 -1
  69. package/dist/packages/ui/src/components/pagination-controls.cjs +64 -0
  70. package/dist/packages/ui/src/components/pagination-controls.mjs +62 -0
  71. package/dist/packages/ui/src/components/when-visible.cjs +39 -0
  72. package/dist/packages/ui/src/components/when-visible.mjs +37 -0
  73. package/dist/plugins/blog/client/hooks/index.d.cts +1 -1
  74. package/dist/plugins/blog/client/hooks/index.d.mts +1 -1
  75. package/dist/plugins/blog/client/hooks/index.d.ts +1 -1
  76. package/dist/plugins/blog/client/index.d.cts +24 -2
  77. package/dist/plugins/blog/client/index.d.mts +24 -2
  78. package/dist/plugins/blog/client/index.d.ts +24 -2
  79. package/dist/plugins/comments/api/index.cjs +21 -0
  80. package/dist/plugins/comments/api/index.d.cts +126 -0
  81. package/dist/plugins/comments/api/index.d.mts +126 -0
  82. package/dist/plugins/comments/api/index.d.ts +126 -0
  83. package/dist/plugins/comments/api/index.mjs +5 -0
  84. package/dist/plugins/comments/client/components/index.cjs +15 -0
  85. package/dist/plugins/comments/client/components/index.d.cts +125 -0
  86. package/dist/plugins/comments/client/components/index.d.mts +125 -0
  87. package/dist/plugins/comments/client/components/index.d.ts +125 -0
  88. package/dist/plugins/comments/client/components/index.mjs +5 -0
  89. package/dist/plugins/comments/client/hooks/index.cjs +17 -0
  90. package/dist/plugins/comments/client/hooks/index.d.cts +200 -0
  91. package/dist/plugins/comments/client/hooks/index.d.mts +200 -0
  92. package/dist/plugins/comments/client/hooks/index.d.ts +200 -0
  93. package/dist/plugins/comments/client/hooks/index.mjs +1 -0
  94. package/dist/plugins/comments/client/index.cjs +9 -0
  95. package/dist/plugins/comments/client/index.d.cts +262 -0
  96. package/dist/plugins/comments/client/index.d.mts +262 -0
  97. package/dist/plugins/comments/client/index.d.ts +262 -0
  98. package/dist/plugins/comments/client/index.mjs +2 -0
  99. package/dist/plugins/comments/client.css +2 -0
  100. package/dist/plugins/comments/query-keys.cjs +113 -0
  101. package/dist/plugins/comments/query-keys.d.cts +71 -0
  102. package/dist/plugins/comments/query-keys.d.mts +71 -0
  103. package/dist/plugins/comments/query-keys.d.ts +71 -0
  104. package/dist/plugins/comments/query-keys.mjs +111 -0
  105. package/dist/plugins/comments/style.css +15 -0
  106. package/dist/plugins/kanban/api/index.d.cts +1 -1
  107. package/dist/plugins/kanban/api/index.d.mts +1 -1
  108. package/dist/plugins/kanban/api/index.d.ts +1 -1
  109. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  110. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  111. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  112. package/dist/plugins/kanban/client/index.d.cts +1 -1
  113. package/dist/plugins/kanban/client/index.d.mts +1 -1
  114. package/dist/plugins/kanban/client/index.d.ts +1 -1
  115. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  116. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  117. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  118. package/dist/shared/{stack.FeaWkglm.d.ts → stack.BxFl46lB.d.cts} +24 -1
  119. package/dist/shared/stack.C-b3Sn8j.d.cts +142 -0
  120. package/dist/shared/stack.C-b3Sn8j.d.mts +142 -0
  121. package/dist/shared/stack.C-b3Sn8j.d.ts +142 -0
  122. package/dist/shared/stack.CJE9sAjV.d.ts +335 -0
  123. package/dist/shared/stack.CmHRdhl8.d.cts +335 -0
  124. package/dist/shared/{stack.CNLHlv7r.d.mts → stack.DOZ1EXjM.d.mts} +6 -12
  125. package/dist/shared/{stack.FeaWkglm.d.mts → stack.DRpeDS6X.d.ts} +24 -1
  126. package/dist/shared/{stack.CQAZwXhV.d.cts → stack.DX-tQ93o.d.cts} +6 -12
  127. package/dist/shared/stack.Dcz6636A.d.mts +335 -0
  128. package/dist/shared/{stack.FeaWkglm.d.cts → stack.Jb0kQDJC.d.mts} +24 -1
  129. package/dist/shared/stack.Ldfkr5b2.d.cts +112 -0
  130. package/dist/shared/stack.Ldfkr5b2.d.mts +112 -0
  131. package/dist/shared/stack.Ldfkr5b2.d.ts +112 -0
  132. package/dist/shared/{stack.D3BsrpAz.d.ts → stack.VF6FhyZw.d.ts} +6 -12
  133. package/package.json +69 -4
  134. package/src/plugins/blog/client/components/loading/post-navigation-skeleton.tsx +10 -0
  135. package/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.tsx +18 -0
  136. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +23 -8
  137. package/src/plugins/blog/client/components/shared/post-navigation.tsx +0 -5
  138. package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +1 -5
  139. package/src/plugins/blog/client/hooks/blog-hooks.tsx +8 -33
  140. package/src/plugins/blog/client/overrides.ts +26 -1
  141. package/src/plugins/cms/client/components/shared/pagination.tsx +14 -42
  142. package/src/plugins/comments/api/getters.ts +444 -0
  143. package/src/plugins/comments/api/index.ts +21 -0
  144. package/src/plugins/comments/api/mutations.ts +206 -0
  145. package/src/plugins/comments/api/plugin.ts +628 -0
  146. package/src/plugins/comments/api/query-key-defs.ts +143 -0
  147. package/src/plugins/comments/api/serializers.ts +37 -0
  148. package/src/plugins/comments/client/components/comment-count.tsx +66 -0
  149. package/src/plugins/comments/client/components/comment-form.tsx +112 -0
  150. package/src/plugins/comments/client/components/comment-thread.tsx +799 -0
  151. package/src/plugins/comments/client/components/index.tsx +11 -0
  152. package/src/plugins/comments/client/components/pages/moderation-page.internal.tsx +550 -0
  153. package/src/plugins/comments/client/components/pages/moderation-page.tsx +70 -0
  154. package/src/plugins/comments/client/components/pages/my-comments-page.internal.tsx +367 -0
  155. package/src/plugins/comments/client/components/pages/my-comments-page.tsx +72 -0
  156. package/src/plugins/comments/client/components/pages/resource-comments-page.internal.tsx +225 -0
  157. package/src/plugins/comments/client/components/pages/resource-comments-page.tsx +97 -0
  158. package/src/plugins/comments/client/components/shared/page-wrapper.tsx +32 -0
  159. package/src/plugins/comments/client/components/shared/pagination.tsx +44 -0
  160. package/src/plugins/comments/client/hooks/index.tsx +13 -0
  161. package/src/plugins/comments/client/hooks/use-comments.tsx +717 -0
  162. package/src/plugins/comments/client/index.ts +14 -0
  163. package/src/plugins/comments/client/localization/comments-moderation.ts +75 -0
  164. package/src/plugins/comments/client/localization/comments-my.ts +32 -0
  165. package/src/plugins/comments/client/localization/comments-thread.ts +32 -0
  166. package/src/plugins/comments/client/localization/index.ts +11 -0
  167. package/src/plugins/comments/client/overrides.ts +164 -0
  168. package/src/plugins/comments/client/plugin.tsx +195 -0
  169. package/src/plugins/comments/client/utils.ts +67 -0
  170. package/src/plugins/comments/client.css +2 -0
  171. package/src/plugins/comments/db.ts +77 -0
  172. package/src/plugins/comments/query-keys.ts +189 -0
  173. package/src/plugins/comments/schemas.ts +72 -0
  174. package/src/plugins/comments/style.css +15 -0
  175. package/src/plugins/comments/types.ts +73 -0
  176. package/src/plugins/kanban/client/components/forms/task-form.tsx +0 -1
  177. package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +46 -27
  178. package/src/plugins/kanban/client/overrides.ts +27 -1
  179. package/dist/shared/{stack.Rtcvl8sS.d.cts → stack.BOokfhZD.d.cts} +3 -3
  180. package/dist/shared/{stack.D4Cea8II.d.ts → stack.BvCR4-9H.d.ts} +3 -3
  181. package/dist/shared/{stack.HE_IvqV5.d.mts → stack.CWxAl9K3.d.mts} +3 -3
@@ -0,0 +1,189 @@
1
+ import {
2
+ mergeQueryKeys,
3
+ createQueryKeys,
4
+ } from "@lukemorales/query-key-factory";
5
+ import type { CommentsApiRouter } from "./api";
6
+ import { createApiClient } from "@btst/stack/plugins/client";
7
+ import type { CommentListResult } from "./types";
8
+ import {
9
+ commentsListDiscriminator,
10
+ commentCountDiscriminator,
11
+ commentsThreadDiscriminator,
12
+ } from "./api/query-key-defs";
13
+ import { toError } from "./client/utils";
14
+
15
+ interface CommentsListParams {
16
+ resourceId?: string;
17
+ resourceType?: string;
18
+ parentId?: string | null;
19
+ status?: "pending" | "approved" | "spam";
20
+ currentUserId?: string;
21
+ authorId?: string;
22
+ sort?: "asc" | "desc";
23
+ limit?: number;
24
+ offset?: number;
25
+ }
26
+
27
+ interface CommentCountParams {
28
+ resourceId: string;
29
+ resourceType: string;
30
+ status?: "pending" | "approved" | "spam";
31
+ }
32
+
33
+ function isErrorResponse(
34
+ response: unknown,
35
+ ): response is { error: unknown; data?: never } {
36
+ return (
37
+ typeof response === "object" &&
38
+ response !== null &&
39
+ "error" in response &&
40
+ (response as Record<string, unknown>).error !== null &&
41
+ (response as Record<string, unknown>).error !== undefined
42
+ );
43
+ }
44
+
45
+ export function createCommentsQueryKeys(
46
+ client: ReturnType<typeof createApiClient<CommentsApiRouter>>,
47
+ headers?: HeadersInit,
48
+ ) {
49
+ return mergeQueryKeys(
50
+ createCommentsQueries(client, headers),
51
+ createCommentCountQueries(client, headers),
52
+ createCommentsThreadQueries(client, headers),
53
+ );
54
+ }
55
+
56
+ function createCommentsQueries(
57
+ client: ReturnType<typeof createApiClient<CommentsApiRouter>>,
58
+ headers?: HeadersInit,
59
+ ) {
60
+ return createQueryKeys("comments", {
61
+ list: (params?: CommentsListParams) => ({
62
+ queryKey: [commentsListDiscriminator(params)],
63
+ queryFn: async (): Promise<CommentListResult> => {
64
+ const response = await client("/comments", {
65
+ method: "GET",
66
+ query: {
67
+ resourceId: params?.resourceId,
68
+ resourceType: params?.resourceType,
69
+ parentId: params?.parentId === null ? "null" : params?.parentId,
70
+ status: params?.status,
71
+ // currentUserId is intentionally NOT sent to the server.
72
+ // The server resolves the caller's identity server-side via the
73
+ // resolveCurrentUserId hook. Sending it would allow any caller to
74
+ // impersonate another user and read their pending comments.
75
+ // It is still included in the queryKey above for client-side
76
+ // cache segregation (different users get different cache entries).
77
+ authorId: params?.authorId,
78
+ sort: params?.sort,
79
+ limit: params?.limit ?? 20,
80
+ offset: params?.offset ?? 0,
81
+ },
82
+ headers,
83
+ });
84
+
85
+ if (isErrorResponse(response)) {
86
+ throw toError((response as { error: unknown }).error);
87
+ }
88
+
89
+ const data = (response as { data?: unknown }).data as
90
+ | CommentListResult
91
+ | undefined;
92
+ return data ?? { items: [], total: 0, limit: 20, offset: 0 };
93
+ },
94
+ }),
95
+ });
96
+ }
97
+
98
+ function createCommentCountQueries(
99
+ client: ReturnType<typeof createApiClient<CommentsApiRouter>>,
100
+ headers?: HeadersInit,
101
+ ) {
102
+ return createQueryKeys("commentCount", {
103
+ byResource: (params: CommentCountParams) => ({
104
+ queryKey: [commentCountDiscriminator(params)],
105
+ queryFn: async (): Promise<number> => {
106
+ const response = await client("/comments/count", {
107
+ method: "GET",
108
+ query: {
109
+ resourceId: params.resourceId,
110
+ resourceType: params.resourceType,
111
+ status: params.status,
112
+ },
113
+ headers,
114
+ });
115
+
116
+ if (isErrorResponse(response)) {
117
+ throw toError((response as { error: unknown }).error);
118
+ }
119
+
120
+ const data = (response as { data?: unknown }).data as
121
+ | { count: number }
122
+ | undefined;
123
+ return data?.count ?? 0;
124
+ },
125
+ }),
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Factory for the infinite thread query key family.
131
+ * Mirrors the blog's `createPostsQueries` pattern: the key is stable (no offset),
132
+ * and pages are fetched via `pageParam` passed to the queryFn.
133
+ */
134
+ function createCommentsThreadQueries(
135
+ client: ReturnType<typeof createApiClient<CommentsApiRouter>>,
136
+ headers?: HeadersInit,
137
+ ) {
138
+ return createQueryKeys("commentsThread", {
139
+ list: (params?: {
140
+ resourceId?: string;
141
+ resourceType?: string;
142
+ parentId?: string | null;
143
+ status?: "pending" | "approved" | "spam";
144
+ currentUserId?: string;
145
+ limit?: number;
146
+ }) => ({
147
+ // Offset is excluded from the key — it is driven by pageParam.
148
+ queryKey: [commentsThreadDiscriminator(params)],
149
+ queryFn: async ({
150
+ pageParam,
151
+ }: {
152
+ pageParam?: number;
153
+ } = {}): Promise<CommentListResult> => {
154
+ const response = await client("/comments", {
155
+ method: "GET",
156
+ query: {
157
+ resourceId: params?.resourceId,
158
+ resourceType: params?.resourceType,
159
+ parentId: params?.parentId === null ? "null" : params?.parentId,
160
+ status: params?.status,
161
+ // currentUserId is intentionally NOT sent to the server.
162
+ // The server resolves the caller's identity server-side via the
163
+ // resolveCurrentUserId hook. It is still included in the queryKey
164
+ // above for client-side cache segregation.
165
+ limit: params?.limit ?? 20,
166
+ offset: pageParam ?? 0,
167
+ },
168
+ headers,
169
+ });
170
+
171
+ if (isErrorResponse(response)) {
172
+ throw toError((response as { error: unknown }).error);
173
+ }
174
+
175
+ const data = (response as { data?: unknown }).data as
176
+ | CommentListResult
177
+ | undefined;
178
+ return (
179
+ data ?? {
180
+ items: [],
181
+ total: 0,
182
+ limit: params?.limit ?? 20,
183
+ offset: pageParam ?? 0,
184
+ }
185
+ );
186
+ },
187
+ }),
188
+ });
189
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+
3
+ export const CommentStatusSchema = z.enum(["pending", "approved", "spam"]);
4
+
5
+ // ============ Comment Schemas ============
6
+
7
+ /**
8
+ * Schema for the POST /comments request body.
9
+ * authorId is intentionally absent — the server resolves identity from the
10
+ * session inside onBeforePost and injects it. Never trust authorId from the
11
+ * client body.
12
+ */
13
+ export const createCommentSchema = z.object({
14
+ resourceId: z.string().min(1, "Resource ID is required"),
15
+ resourceType: z.string().min(1, "Resource type is required"),
16
+ parentId: z.string().optional().nullable(),
17
+ body: z.string().min(1, "Body is required").max(10000, "Comment too long"),
18
+ });
19
+
20
+ /**
21
+ * Internal schema used after the authorId has been resolved server-side.
22
+ * This is what gets passed to createComment() in mutations.ts.
23
+ */
24
+ export const createCommentInternalSchema = createCommentSchema.extend({
25
+ authorId: z.string().min(1, "Author ID is required"),
26
+ });
27
+
28
+ export const updateCommentSchema = z.object({
29
+ body: z.string().min(1, "Body is required").max(10000, "Comment too long"),
30
+ });
31
+
32
+ export const updateCommentStatusSchema = z.object({
33
+ status: CommentStatusSchema,
34
+ });
35
+
36
+ // ============ Query Schemas ============
37
+
38
+ /**
39
+ * Schema for GET /comments query parameters.
40
+ *
41
+ * `currentUserId` is intentionally absent — it is never accepted from the client.
42
+ * The server always resolves the caller's identity via the `resolveCurrentUserId`
43
+ * hook and injects it internally. Accepting it from the client would allow any
44
+ * anonymous caller to supply an arbitrary user ID and read that user's pending
45
+ * (pre-moderation) comments.
46
+ */
47
+ export const CommentListQuerySchema = z.object({
48
+ resourceId: z.string().optional(),
49
+ resourceType: z.string().optional(),
50
+ parentId: z.string().optional().nullable(),
51
+ status: CommentStatusSchema.optional(),
52
+ authorId: z.string().optional(),
53
+ sort: z.enum(["asc", "desc"]).optional(),
54
+ limit: z.coerce.number().int().min(1).max(100).optional(),
55
+ offset: z.coerce.number().int().min(0).optional(),
56
+ });
57
+
58
+ /**
59
+ * Internal params schema used by `listComments()` and the `api` factory.
60
+ * Extends the HTTP query schema with `currentUserId`, which is always injected
61
+ * server-side (either by the HTTP handler via `resolveCurrentUserId`, or by a
62
+ * trusted server-side caller such as a Server Component or cron job).
63
+ */
64
+ export const CommentListParamsSchema = CommentListQuerySchema.extend({
65
+ currentUserId: z.string().optional(),
66
+ });
67
+
68
+ export const CommentCountQuerySchema = z.object({
69
+ resourceId: z.string().min(1),
70
+ resourceType: z.string().min(1),
71
+ status: CommentStatusSchema.optional(),
72
+ });
@@ -0,0 +1,15 @@
1
+ @import "./client.css";
2
+
3
+ /*
4
+ * Comments Plugin CSS - Includes Tailwind class scanning
5
+ *
6
+ * When consumed from npm, Tailwind v4 will automatically scan this package's
7
+ * source files for Tailwind classes. Consumers only need:
8
+ * @import "@btst/stack/plugins/comments/css";
9
+ */
10
+
11
+ /* Scan this package's source files for Tailwind classes */
12
+ @source "../../../src/**/*.{ts,tsx}";
13
+
14
+ /* Scan UI package components (when installed as npm package the UI package will be in this dir) */
15
+ @source "../../packages/ui/src";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Comment status values
3
+ */
4
+ export type CommentStatus = "pending" | "approved" | "spam";
5
+
6
+ /**
7
+ * A comment record as stored in the database
8
+ */
9
+ export type Comment = {
10
+ id: string;
11
+ resourceId: string;
12
+ resourceType: string;
13
+ parentId: string | null;
14
+ authorId: string;
15
+ body: string;
16
+ status: CommentStatus;
17
+ likes: number;
18
+ editedAt?: Date;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ };
22
+
23
+ /**
24
+ * A like record linking an author to a comment
25
+ */
26
+ export type CommentLike = {
27
+ id: string;
28
+ commentId: string;
29
+ authorId: string;
30
+ createdAt: Date;
31
+ };
32
+
33
+ /**
34
+ * A comment enriched with server-resolved author info and like status.
35
+ * All dates are ISO strings (safe for serialisation over HTTP / React Query cache).
36
+ */
37
+ export interface SerializedComment {
38
+ id: string;
39
+ resourceId: string;
40
+ resourceType: string;
41
+ parentId: string | null;
42
+ authorId: string;
43
+ /** Resolved from resolveUser(authorId). Falls back to "[deleted]" when user cannot be found. */
44
+ resolvedAuthorName: string;
45
+ /** Resolved avatar URL or null */
46
+ resolvedAvatarUrl: string | null;
47
+ body: string;
48
+ status: CommentStatus;
49
+ /** Denormalized counter — updated atomically on toggleLike */
50
+ likes: number;
51
+ /** True when the currentUserId query param matches an existing commentLike row */
52
+ isLikedByCurrentUser: boolean;
53
+ /** ISO string set when the comment body was edited; null for unedited comments */
54
+ editedAt: string | null;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ /**
58
+ * Number of direct replies visible to the requesting user.
59
+ * Includes approved replies plus any pending replies authored by `currentUserId`.
60
+ * Always 0 for reply comments (non-null parentId).
61
+ */
62
+ replyCount: number;
63
+ }
64
+
65
+ /**
66
+ * Paginated list result for comments
67
+ */
68
+ export interface CommentListResult {
69
+ items: SerializedComment[];
70
+ total: number;
71
+ limit: number;
72
+ offset: number;
73
+ }
@@ -226,7 +226,6 @@ export function TaskForm({
226
226
  }
227
227
  output="markdown"
228
228
  placeholder="Describe the task..."
229
- editable={!isPending}
230
229
  className="min-h-[150px]"
231
230
  />
232
231
  </div>
@@ -66,8 +66,11 @@ export function BoardPage({ boardId }: BoardPageProps) {
66
66
  throw error;
67
67
  }
68
68
 
69
- const { Link: OverrideLink, navigate: overrideNavigate } =
70
- usePluginOverrides<KanbanPluginOverrides>("kanban");
69
+ const {
70
+ Link: OverrideLink,
71
+ navigate: overrideNavigate,
72
+ taskDetailBottomSlot,
73
+ } = usePluginOverrides<KanbanPluginOverrides>("kanban");
71
74
  const navigate =
72
75
  overrideNavigate ||
73
76
  ((path: string) => {
@@ -540,33 +543,49 @@ export function BoardPage({ boardId }: BoardPageProps) {
540
543
  <DialogDescription>Update task details.</DialogDescription>
541
544
  </DialogHeader>
542
545
  {modalState.type === "editTask" && (
543
- <TaskForm
544
- columnId={modalState.columnId}
545
- boardId={boardId}
546
- taskId={modalState.taskId}
547
- task={board.columns
548
- ?.find((c) => c.id === modalState.columnId)
549
- ?.tasks?.find((t) => t.id === modalState.taskId)}
550
- columns={board.columns || []}
551
- onClose={closeModal}
552
- onSuccess={() => {
553
- closeModal();
554
- refetch();
555
- }}
556
- onDelete={async () => {
557
- try {
558
- await deleteTask(modalState.taskId);
546
+ <>
547
+ <TaskForm
548
+ columnId={modalState.columnId}
549
+ boardId={boardId}
550
+ taskId={modalState.taskId}
551
+ task={board.columns
552
+ ?.find((c) => c.id === modalState.columnId)
553
+ ?.tasks?.find((t) => t.id === modalState.taskId)}
554
+ columns={board.columns || []}
555
+ onClose={closeModal}
556
+ onSuccess={() => {
559
557
  closeModal();
560
558
  refetch();
561
- } catch (error) {
562
- const message =
563
- error instanceof Error
564
- ? error.message
565
- : "Failed to delete task";
566
- toast.error(message);
567
- }
568
- }}
569
- />
559
+ }}
560
+ onDelete={async () => {
561
+ try {
562
+ await deleteTask(modalState.taskId);
563
+ closeModal();
564
+ refetch();
565
+ } catch (error) {
566
+ const message =
567
+ error instanceof Error
568
+ ? error.message
569
+ : "Failed to delete task";
570
+ toast.error(message);
571
+ }
572
+ }}
573
+ />
574
+ {taskDetailBottomSlot &&
575
+ (() => {
576
+ const task = board.columns
577
+ ?.find((c) => c.id === modalState.columnId)
578
+ ?.tasks?.find((t) => t.id === modalState.taskId);
579
+ return task ? (
580
+ <div
581
+ className="mt-4 pt-4 border-t"
582
+ data-testid="task-detail-bottom-slot"
583
+ >
584
+ {taskDetailBottomSlot(task)}
585
+ </div>
586
+ ) : null;
587
+ })()}
588
+ </>
570
589
  )}
571
590
  </DialogContent>
572
591
  </Dialog>
@@ -1,5 +1,6 @@
1
- import type { ComponentType } from "react";
1
+ import type { ComponentType, ReactNode } from "react";
2
2
  import type { KanbanLocalization } from "./localization";
3
+ import type { SerializedTask } from "../types";
3
4
 
4
5
  /**
5
6
  * User information for assignee display/selection
@@ -142,4 +143,29 @@ export interface KanbanPluginOverrides {
142
143
  * @param context - Route context
143
144
  */
144
145
  onBeforeNewBoardPageRendered?: (context: RouteContext) => boolean;
146
+
147
+ // ============ Slot Overrides ============
148
+
149
+ /**
150
+ * Optional slot rendered at the bottom of the task detail dialog.
151
+ * Use this to inject a comment thread or any custom content without
152
+ * coupling the kanban plugin to the comments plugin.
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * kanban: {
157
+ * taskDetailBottomSlot: (task) => (
158
+ * <CommentThread
159
+ * resourceId={task.id}
160
+ * resourceType="kanban-task"
161
+ * apiBaseURL={apiBaseURL}
162
+ * apiBasePath="/api/data"
163
+ * currentUserId={session?.userId}
164
+ * loginHref="/login"
165
+ * />
166
+ * ),
167
+ * }
168
+ * ```
169
+ */
170
+ taskDetailBottomSlot?: (task: SerializedTask) => ReactNode;
145
171
  }
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
30
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
- order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
31
  boardId: z.ZodString;
32
+ order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
35
35
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
404
404
  title?: string | undefined;
405
405
  createdAt?: unknown;
406
406
  updatedAt?: unknown;
407
- order?: number | undefined;
408
407
  boardId?: string | undefined;
408
+ order?: number | undefined;
409
409
  }, {
410
410
  title?: string | undefined;
411
411
  createdAt?: unknown;
412
412
  updatedAt?: unknown;
413
- order?: number | undefined;
414
413
  boardId?: string | undefined;
414
+ order?: number | undefined;
415
415
  }>;
416
416
  }, Column>;
417
417
  readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
30
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
- order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
31
  boardId: z.ZodString;
32
+ order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
35
35
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
404
404
  title?: string | undefined;
405
405
  createdAt?: unknown;
406
406
  updatedAt?: unknown;
407
- order?: number | undefined;
408
407
  boardId?: string | undefined;
408
+ order?: number | undefined;
409
409
  }, {
410
410
  title?: string | undefined;
411
411
  createdAt?: unknown;
412
412
  updatedAt?: unknown;
413
- order?: number | undefined;
414
413
  boardId?: string | undefined;
414
+ order?: number | undefined;
415
415
  }>;
416
416
  }, Column>;
417
417
  readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
30
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
- order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
31
  boardId: z.ZodString;
32
+ order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
35
35
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
404
404
  title?: string | undefined;
405
405
  createdAt?: unknown;
406
406
  updatedAt?: unknown;
407
- order?: number | undefined;
408
407
  boardId?: string | undefined;
408
+ order?: number | undefined;
409
409
  }, {
410
410
  title?: string | undefined;
411
411
  createdAt?: unknown;
412
412
  updatedAt?: unknown;
413
- order?: number | undefined;
414
413
  boardId?: string | undefined;
414
+ order?: number | undefined;
415
415
  }>;
416
416
  }, Column>;
417
417
  readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {