@btst/stack 2.11.8 → 2.12.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.
Files changed (35) hide show
  1. package/dist/packages/stack/src/plugins/comments/api/query-key-defs.cjs +2 -1
  2. package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +2 -1
  3. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +6 -2
  4. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +6 -2
  5. package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +5 -3
  6. package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +5 -3
  7. package/dist/plugins/comments/api/index.d.cts +4 -4
  8. package/dist/plugins/comments/api/index.d.mts +4 -4
  9. package/dist/plugins/comments/api/index.d.ts +4 -4
  10. package/dist/plugins/comments/client/components/index.d.cts +11 -0
  11. package/dist/plugins/comments/client/components/index.d.mts +11 -0
  12. package/dist/plugins/comments/client/components/index.d.ts +11 -0
  13. package/dist/plugins/comments/client/hooks/index.d.cts +17 -1
  14. package/dist/plugins/comments/client/hooks/index.d.mts +17 -1
  15. package/dist/plugins/comments/client/hooks/index.d.ts +17 -1
  16. package/dist/plugins/comments/client/index.d.cts +9 -0
  17. package/dist/plugins/comments/client/index.d.mts +9 -0
  18. package/dist/plugins/comments/client/index.d.ts +9 -0
  19. package/dist/plugins/comments/query-keys.cjs +1 -0
  20. package/dist/plugins/comments/query-keys.d.cts +3 -2
  21. package/dist/plugins/comments/query-keys.d.mts +3 -2
  22. package/dist/plugins/comments/query-keys.d.ts +3 -2
  23. package/dist/plugins/comments/query-keys.mjs +1 -0
  24. package/dist/shared/{stack.CJE9sAjV.d.ts → stack.Bq3kiUaM.d.ts} +1 -1
  25. package/dist/shared/{stack.C-b3Sn8j.d.cts → stack.C7KNNiJm.d.cts} +3 -1
  26. package/dist/shared/{stack.C-b3Sn8j.d.mts → stack.C7KNNiJm.d.mts} +3 -1
  27. package/dist/shared/{stack.C-b3Sn8j.d.ts → stack.C7KNNiJm.d.ts} +3 -1
  28. package/dist/shared/{stack.CmHRdhl8.d.cts → stack.CCncwK6g.d.cts} +1 -1
  29. package/dist/shared/{stack.Dcz6636A.d.mts → stack.CO1g47IS.d.mts} +1 -1
  30. package/package.json +1 -1
  31. package/src/plugins/comments/api/query-key-defs.ts +5 -1
  32. package/src/plugins/comments/client/components/comment-thread.tsx +15 -0
  33. package/src/plugins/comments/client/hooks/use-comments.tsx +26 -3
  34. package/src/plugins/comments/client/overrides.ts +10 -0
  35. package/src/plugins/comments/query-keys.ts +2 -0
@@ -27,6 +27,7 @@ function commentsThreadDiscriminator(params) {
27
27
  parentId: params?.parentId,
28
28
  status: params?.status,
29
29
  currentUserId: params?.currentUserId,
30
+ sort: params?.sort,
30
31
  limit: params?.limit ?? 20
31
32
  };
32
33
  }
@@ -48,7 +49,7 @@ const COMMENTS_QUERY_KEYS = {
48
49
  commentCount: (params) => ["comments", "count", commentCountDiscriminator(params)],
49
50
  /**
50
51
  * Key for the infinite thread query (top-level comments, load-more).
51
- * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
52
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
52
53
  * Offset is excluded — it is driven by `pageParam`, not baked into the key.
53
54
  */
54
55
  commentsThread: (params) => ["commentsThread", "list", commentsThreadDiscriminator(params)]
@@ -25,6 +25,7 @@ function commentsThreadDiscriminator(params) {
25
25
  parentId: params?.parentId,
26
26
  status: params?.status,
27
27
  currentUserId: params?.currentUserId,
28
+ sort: params?.sort,
28
29
  limit: params?.limit ?? 20
29
30
  };
30
31
  }
@@ -46,7 +47,7 @@ const COMMENTS_QUERY_KEYS = {
46
47
  commentCount: (params) => ["comments", "count", commentCountDiscriminator(params)],
47
48
  /**
48
49
  * Key for the infinite thread query (top-level comments, load-more).
49
- * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
50
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
50
51
  * Offset is excluded — it is driven by `pageParam`, not baked into the key.
51
52
  */
52
53
  commentsThread: (params) => ["commentsThread", "list", commentsThreadDiscriminator(params)]
@@ -195,12 +195,14 @@ function CommentThreadInner({
195
195
  localization: localizationProp,
196
196
  pageSize: pageSizeProp,
197
197
  allowPosting: allowPostingProp,
198
- allowEditing: allowEditingProp
198
+ allowEditing: allowEditingProp,
199
+ sort: sortProp
199
200
  }) {
200
201
  const overrides = context.usePluginOverrides("comments", {});
201
202
  const pageSize = pageSizeProp ?? overrides.defaultCommentPageSize ?? DEFAULT_PAGE_SIZE;
202
203
  const allowPosting = allowPostingProp ?? overrides.allowPosting ?? true;
203
204
  const allowEditing = allowEditingProp ?? overrides.allowEditing ?? true;
205
+ const sort = sortProp ?? overrides.defaultCommentSort ?? "desc";
204
206
  const loc = { ...index.COMMENTS_LOCALIZATION, ...localizationProp };
205
207
  const [replyingTo, setReplyingTo] = React.useState(null);
206
208
  const [expandedReplies, setExpandedReplies] = React.useState(
@@ -222,6 +224,7 @@ function CommentThreadInner({
222
224
  status: "approved",
223
225
  parentId: null,
224
226
  currentUserId,
227
+ sort,
225
228
  pageSize
226
229
  });
227
230
  const postMutation = useComments.usePostComment(config, {
@@ -229,7 +232,8 @@ function CommentThreadInner({
229
232
  resourceType,
230
233
  currentUserId,
231
234
  infiniteKey: threadQueryKey,
232
- pageSize
235
+ pageSize,
236
+ sort
233
237
  });
234
238
  const handlePost = async (body) => {
235
239
  if (!currentUserId) return;
@@ -193,12 +193,14 @@ function CommentThreadInner({
193
193
  localization: localizationProp,
194
194
  pageSize: pageSizeProp,
195
195
  allowPosting: allowPostingProp,
196
- allowEditing: allowEditingProp
196
+ allowEditing: allowEditingProp,
197
+ sort: sortProp
197
198
  }) {
198
199
  const overrides = usePluginOverrides("comments", {});
199
200
  const pageSize = pageSizeProp ?? overrides.defaultCommentPageSize ?? DEFAULT_PAGE_SIZE;
200
201
  const allowPosting = allowPostingProp ?? overrides.allowPosting ?? true;
201
202
  const allowEditing = allowEditingProp ?? overrides.allowEditing ?? true;
203
+ const sort = sortProp ?? overrides.defaultCommentSort ?? "desc";
202
204
  const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
203
205
  const [replyingTo, setReplyingTo] = useState(null);
204
206
  const [expandedReplies, setExpandedReplies] = useState(
@@ -220,6 +222,7 @@ function CommentThreadInner({
220
222
  status: "approved",
221
223
  parentId: null,
222
224
  currentUserId,
225
+ sort,
223
226
  pageSize
224
227
  });
225
228
  const postMutation = usePostComment(config, {
@@ -227,7 +230,8 @@ function CommentThreadInner({
227
230
  resourceType,
228
231
  currentUserId,
229
232
  infiniteKey: threadQueryKey,
230
- pageSize
233
+ pageSize,
234
+ sort
231
235
  });
232
236
  const handlePost = async (body) => {
233
237
  if (!currentUserId) return;
@@ -84,6 +84,7 @@ function useInfiniteComments(config, params, options) {
84
84
  parentId: params.parentId ?? null,
85
85
  status: params.status,
86
86
  currentUserId: params.currentUserId,
87
+ sort: params.sort,
87
88
  limit: pageSize
88
89
  });
89
90
  const query = reactQuery.useInfiniteQuery({
@@ -183,6 +184,7 @@ function usePostComment(config, params) {
183
184
  };
184
185
  if (isInfinitePost(input.parentId)) {
185
186
  const previous2 = queryClient.getQueryData(listKey);
187
+ const isDesc = params.sort === "desc";
186
188
  queryClient.setQueryData(
187
189
  listKey,
188
190
  (old) => {
@@ -199,15 +201,15 @@ function usePostComment(config, params) {
199
201
  pageParams: [0]
200
202
  };
201
203
  }
202
- const lastIdx = old.pages.length - 1;
204
+ const targetIdx = isDesc ? 0 : old.pages.length - 1;
203
205
  return {
204
206
  ...old,
205
207
  // Increment `total` on every page so the header count (which reads
206
208
  // pages[0].total) stays in sync even after multiple pages are loaded.
207
209
  pages: old.pages.map(
208
- (page, idx) => idx === lastIdx ? {
210
+ (page, idx) => idx === targetIdx ? {
209
211
  ...page,
210
- items: [...page.items, optimistic],
212
+ items: isDesc ? [optimistic, ...page.items] : [...page.items, optimistic],
211
213
  total: page.total + 1
212
214
  } : { ...page, total: page.total + 1 }
213
215
  )
@@ -82,6 +82,7 @@ function useInfiniteComments(config, params, options) {
82
82
  parentId: params.parentId ?? null,
83
83
  status: params.status,
84
84
  currentUserId: params.currentUserId,
85
+ sort: params.sort,
85
86
  limit: pageSize
86
87
  });
87
88
  const query = useInfiniteQuery({
@@ -181,6 +182,7 @@ function usePostComment(config, params) {
181
182
  };
182
183
  if (isInfinitePost(input.parentId)) {
183
184
  const previous2 = queryClient.getQueryData(listKey);
185
+ const isDesc = params.sort === "desc";
184
186
  queryClient.setQueryData(
185
187
  listKey,
186
188
  (old) => {
@@ -197,15 +199,15 @@ function usePostComment(config, params) {
197
199
  pageParams: [0]
198
200
  };
199
201
  }
200
- const lastIdx = old.pages.length - 1;
202
+ const targetIdx = isDesc ? 0 : old.pages.length - 1;
201
203
  return {
202
204
  ...old,
203
205
  // Increment `total` on every page so the header count (which reads
204
206
  // pages[0].total) stays in sync even after multiple pages are loaded.
205
207
  pages: old.pages.map(
206
- (page, idx) => idx === lastIdx ? {
208
+ (page, idx) => idx === targetIdx ? {
207
209
  ...page,
208
- items: [...page.items, optimistic],
210
+ items: isDesc ? [optimistic, ...page.items] : [...page.items, optimistic],
209
211
  total: page.total + 1
210
212
  } : { ...page, total: page.total + 1 }
211
213
  )
@@ -1,8 +1,8 @@
1
- import { a as CommentListParamsSchema, b as CommentCountQuerySchema } from '../../../shared/stack.CmHRdhl8.cjs';
2
- export { d as CommentsApiContext, C as CommentsApiRouter, e as CommentsBackendOptions, c as commentsBackendPlugin } from '../../../shared/stack.CmHRdhl8.cjs';
1
+ import { a as CommentListParamsSchema, b as CommentCountQuerySchema } from '../../../shared/stack.CCncwK6g.cjs';
2
+ export { d as CommentsApiContext, C as CommentsApiRouter, e as CommentsBackendOptions, c as commentsBackendPlugin } from '../../../shared/stack.CCncwK6g.cjs';
3
3
  import { DBAdapter } from '@btst/db';
4
- import { C as CommentListResult, S as SerializedComment, d as Comment } from '../../../shared/stack.C-b3Sn8j.cjs';
5
- export { e as COMMENTS_QUERY_KEYS } from '../../../shared/stack.C-b3Sn8j.cjs';
4
+ import { C as CommentListResult, S as SerializedComment, d as Comment } from '../../../shared/stack.C7KNNiJm.cjs';
5
+ export { e as COMMENTS_QUERY_KEYS } from '../../../shared/stack.C7KNNiJm.cjs';
6
6
  import { z } from 'zod';
7
7
  import '@btst/stack/plugins/api';
8
8
  import 'better-call';
@@ -1,8 +1,8 @@
1
- import { a as CommentListParamsSchema, b as CommentCountQuerySchema } from '../../../shared/stack.Dcz6636A.mjs';
2
- export { d as CommentsApiContext, C as CommentsApiRouter, e as CommentsBackendOptions, c as commentsBackendPlugin } from '../../../shared/stack.Dcz6636A.mjs';
1
+ import { a as CommentListParamsSchema, b as CommentCountQuerySchema } from '../../../shared/stack.CO1g47IS.mjs';
2
+ export { d as CommentsApiContext, C as CommentsApiRouter, e as CommentsBackendOptions, c as commentsBackendPlugin } from '../../../shared/stack.CO1g47IS.mjs';
3
3
  import { DBAdapter } from '@btst/db';
4
- import { C as CommentListResult, S as SerializedComment, d as Comment } from '../../../shared/stack.C-b3Sn8j.mjs';
5
- export { e as COMMENTS_QUERY_KEYS } from '../../../shared/stack.C-b3Sn8j.mjs';
4
+ import { C as CommentListResult, S as SerializedComment, d as Comment } from '../../../shared/stack.C7KNNiJm.mjs';
5
+ export { e as COMMENTS_QUERY_KEYS } from '../../../shared/stack.C7KNNiJm.mjs';
6
6
  import { z } from 'zod';
7
7
  import '@btst/stack/plugins/api';
8
8
  import 'better-call';
@@ -1,8 +1,8 @@
1
- import { a as CommentListParamsSchema, b as CommentCountQuerySchema } from '../../../shared/stack.CJE9sAjV.js';
2
- export { d as CommentsApiContext, C as CommentsApiRouter, e as CommentsBackendOptions, c as commentsBackendPlugin } from '../../../shared/stack.CJE9sAjV.js';
1
+ import { a as CommentListParamsSchema, b as CommentCountQuerySchema } from '../../../shared/stack.Bq3kiUaM.js';
2
+ export { d as CommentsApiContext, C as CommentsApiRouter, e as CommentsBackendOptions, c as commentsBackendPlugin } from '../../../shared/stack.Bq3kiUaM.js';
3
3
  import { DBAdapter } from '@btst/db';
4
- import { C as CommentListResult, S as SerializedComment, d as Comment } from '../../../shared/stack.C-b3Sn8j.js';
5
- export { e as COMMENTS_QUERY_KEYS } from '../../../shared/stack.C-b3Sn8j.js';
4
+ import { C as CommentListResult, S as SerializedComment, d as Comment } from '../../../shared/stack.C7KNNiJm.js';
5
+ export { e as COMMENTS_QUERY_KEYS } from '../../../shared/stack.C7KNNiJm.js';
6
6
  import { z } from 'zod';
7
7
  import '@btst/stack/plugins/api';
8
8
  import 'better-call';
@@ -59,6 +59,17 @@ interface CommentThreadProps {
59
59
  * Defaults to true.
60
60
  */
61
61
  allowEditing?: boolean;
62
+ /**
63
+ * Sort direction for top-level comments by `createdAt`.
64
+ * - `"desc"` (default): newest first.
65
+ * - `"asc"`: oldest first.
66
+ *
67
+ * Replies inside each thread always render chronologically (oldest → newest)
68
+ * and are unaffected by this prop.
69
+ *
70
+ * Overrides the global `defaultCommentSort` from `CommentsPluginOverrides`.
71
+ */
72
+ sort?: "asc" | "desc";
62
73
  }
63
74
  declare function CommentThread(props: CommentThreadProps): react_jsx_runtime.JSX.Element;
64
75
 
@@ -59,6 +59,17 @@ interface CommentThreadProps {
59
59
  * Defaults to true.
60
60
  */
61
61
  allowEditing?: boolean;
62
+ /**
63
+ * Sort direction for top-level comments by `createdAt`.
64
+ * - `"desc"` (default): newest first.
65
+ * - `"asc"`: oldest first.
66
+ *
67
+ * Replies inside each thread always render chronologically (oldest → newest)
68
+ * and are unaffected by this prop.
69
+ *
70
+ * Overrides the global `defaultCommentSort` from `CommentsPluginOverrides`.
71
+ */
72
+ sort?: "asc" | "desc";
62
73
  }
63
74
  declare function CommentThread(props: CommentThreadProps): react_jsx_runtime.JSX.Element;
64
75
 
@@ -59,6 +59,17 @@ interface CommentThreadProps {
59
59
  * Defaults to true.
60
60
  */
61
61
  allowEditing?: boolean;
62
+ /**
63
+ * Sort direction for top-level comments by `createdAt`.
64
+ * - `"desc"` (default): newest first.
65
+ * - `"asc"`: oldest first.
66
+ *
67
+ * Replies inside each thread always render chronologically (oldest → newest)
68
+ * and are unaffected by this prop.
69
+ *
70
+ * Overrides the global `defaultCommentSort` from `CommentsPluginOverrides`.
71
+ */
72
+ sort?: "asc" | "desc";
62
73
  }
63
74
  declare function CommentThread(props: CommentThreadProps): react_jsx_runtime.JSX.Element;
64
75
 
@@ -1,4 +1,4 @@
1
- import { C as CommentListResult, S as SerializedComment, a as CommentsThreadDiscriminator } from '../../../../shared/stack.C-b3Sn8j.cjs';
1
+ import { C as CommentListResult, S as SerializedComment, a as CommentsThreadDiscriminator } from '../../../../shared/stack.C7KNNiJm.cjs';
2
2
  import * as _tanstack_react_query from '@tanstack/react-query';
3
3
  import { InfiniteData } from '@tanstack/react-query';
4
4
 
@@ -81,6 +81,11 @@ declare function useInfiniteComments(config: CommentsClientConfig, params: {
81
81
  parentId?: string | null;
82
82
  status?: "pending" | "approved" | "spam";
83
83
  currentUserId?: string;
84
+ /**
85
+ * Sort direction by `createdAt`. Default: `"asc"` (oldest first) — matches
86
+ * the server-side default. Pass `"desc"` for newest-first threads.
87
+ */
88
+ sort?: "asc" | "desc";
84
89
  pageSize?: number;
85
90
  }, options?: {
86
91
  enabled?: boolean;
@@ -129,6 +134,17 @@ declare function usePostComment(config: CommentsClientConfig, params: {
129
134
  * `nextOffset` from `lastPage.limit` instead of a hardcoded fallback.
130
135
  */
131
136
  pageSize?: number;
137
+ /**
138
+ * Sort direction of the surrounding infinite thread.
139
+ * - `"asc"` (default): newest comments belong on the LAST page → optimistic
140
+ * item is appended to `pages[last].items`.
141
+ * - `"desc"`: newest comments belong on the FIRST page → optimistic item
142
+ * is prepended to `pages[0].items`.
143
+ *
144
+ * Must match the `sort` passed to `useInfiniteComments` so the optimistic
145
+ * item appears in the same position the server will place it.
146
+ */
147
+ sort?: "asc" | "desc";
132
148
  }): _tanstack_react_query.UseMutationResult<SerializedComment, Error, {
133
149
  body: string;
134
150
  parentId?: string | null;
@@ -1,4 +1,4 @@
1
- import { C as CommentListResult, S as SerializedComment, a as CommentsThreadDiscriminator } from '../../../../shared/stack.C-b3Sn8j.mjs';
1
+ import { C as CommentListResult, S as SerializedComment, a as CommentsThreadDiscriminator } from '../../../../shared/stack.C7KNNiJm.mjs';
2
2
  import * as _tanstack_react_query from '@tanstack/react-query';
3
3
  import { InfiniteData } from '@tanstack/react-query';
4
4
 
@@ -81,6 +81,11 @@ declare function useInfiniteComments(config: CommentsClientConfig, params: {
81
81
  parentId?: string | null;
82
82
  status?: "pending" | "approved" | "spam";
83
83
  currentUserId?: string;
84
+ /**
85
+ * Sort direction by `createdAt`. Default: `"asc"` (oldest first) — matches
86
+ * the server-side default. Pass `"desc"` for newest-first threads.
87
+ */
88
+ sort?: "asc" | "desc";
84
89
  pageSize?: number;
85
90
  }, options?: {
86
91
  enabled?: boolean;
@@ -129,6 +134,17 @@ declare function usePostComment(config: CommentsClientConfig, params: {
129
134
  * `nextOffset` from `lastPage.limit` instead of a hardcoded fallback.
130
135
  */
131
136
  pageSize?: number;
137
+ /**
138
+ * Sort direction of the surrounding infinite thread.
139
+ * - `"asc"` (default): newest comments belong on the LAST page → optimistic
140
+ * item is appended to `pages[last].items`.
141
+ * - `"desc"`: newest comments belong on the FIRST page → optimistic item
142
+ * is prepended to `pages[0].items`.
143
+ *
144
+ * Must match the `sort` passed to `useInfiniteComments` so the optimistic
145
+ * item appears in the same position the server will place it.
146
+ */
147
+ sort?: "asc" | "desc";
132
148
  }): _tanstack_react_query.UseMutationResult<SerializedComment, Error, {
133
149
  body: string;
134
150
  parentId?: string | null;
@@ -1,4 +1,4 @@
1
- import { C as CommentListResult, S as SerializedComment, a as CommentsThreadDiscriminator } from '../../../../shared/stack.C-b3Sn8j.js';
1
+ import { C as CommentListResult, S as SerializedComment, a as CommentsThreadDiscriminator } from '../../../../shared/stack.C7KNNiJm.js';
2
2
  import * as _tanstack_react_query from '@tanstack/react-query';
3
3
  import { InfiniteData } from '@tanstack/react-query';
4
4
 
@@ -81,6 +81,11 @@ declare function useInfiniteComments(config: CommentsClientConfig, params: {
81
81
  parentId?: string | null;
82
82
  status?: "pending" | "approved" | "spam";
83
83
  currentUserId?: string;
84
+ /**
85
+ * Sort direction by `createdAt`. Default: `"asc"` (oldest first) — matches
86
+ * the server-side default. Pass `"desc"` for newest-first threads.
87
+ */
88
+ sort?: "asc" | "desc";
84
89
  pageSize?: number;
85
90
  }, options?: {
86
91
  enabled?: boolean;
@@ -129,6 +134,17 @@ declare function usePostComment(config: CommentsClientConfig, params: {
129
134
  * `nextOffset` from `lastPage.limit` instead of a hardcoded fallback.
130
135
  */
131
136
  pageSize?: number;
137
+ /**
138
+ * Sort direction of the surrounding infinite thread.
139
+ * - `"asc"` (default): newest comments belong on the LAST page → optimistic
140
+ * item is appended to `pages[last].items`.
141
+ * - `"desc"`: newest comments belong on the FIRST page → optimistic item
142
+ * is prepended to `pages[0].items`.
143
+ *
144
+ * Must match the `sort` passed to `useInfiniteComments` so the optimistic
145
+ * item appears in the same position the server will place it.
146
+ */
147
+ sort?: "asc" | "desc";
132
148
  }): _tanstack_react_query.UseMutationResult<SerializedComment, Error, {
133
149
  body: string;
134
150
  parentId?: string | null;
@@ -197,6 +197,15 @@ interface CommentsPluginOverrides {
197
197
  * Defaults to 100 when not set.
198
198
  */
199
199
  defaultCommentPageSize?: number;
200
+ /**
201
+ * Default sort direction (by `createdAt`) for top-level comments in
202
+ * `CommentThread`.
203
+ * - `"desc"` (default): newest comments first.
204
+ * - `"asc"`: oldest comments first.
205
+ *
206
+ * Can be overridden per-instance via the `sort` prop on `CommentThread`.
207
+ */
208
+ defaultCommentSort?: "asc" | "desc";
200
209
  /**
201
210
  * When false, the comment form and reply buttons are hidden in all
202
211
  * `CommentThread` instances. Users can still read existing comments.
@@ -197,6 +197,15 @@ interface CommentsPluginOverrides {
197
197
  * Defaults to 100 when not set.
198
198
  */
199
199
  defaultCommentPageSize?: number;
200
+ /**
201
+ * Default sort direction (by `createdAt`) for top-level comments in
202
+ * `CommentThread`.
203
+ * - `"desc"` (default): newest comments first.
204
+ * - `"asc"`: oldest comments first.
205
+ *
206
+ * Can be overridden per-instance via the `sort` prop on `CommentThread`.
207
+ */
208
+ defaultCommentSort?: "asc" | "desc";
200
209
  /**
201
210
  * When false, the comment form and reply buttons are hidden in all
202
211
  * `CommentThread` instances. Users can still read existing comments.
@@ -197,6 +197,15 @@ interface CommentsPluginOverrides {
197
197
  * Defaults to 100 when not set.
198
198
  */
199
199
  defaultCommentPageSize?: number;
200
+ /**
201
+ * Default sort direction (by `createdAt`) for top-level comments in
202
+ * `CommentThread`.
203
+ * - `"desc"` (default): newest comments first.
204
+ * - `"asc"`: oldest comments first.
205
+ *
206
+ * Can be overridden per-instance via the `sort` prop on `CommentThread`.
207
+ */
208
+ defaultCommentSort?: "asc" | "desc";
200
209
  /**
201
210
  * When false, the comment form and reply buttons are hidden in all
202
211
  * `CommentThread` instances. Users can still read existing comments.
@@ -90,6 +90,7 @@ function createCommentsThreadQueries(client, headers) {
90
90
  // The server resolves the caller's identity server-side via the
91
91
  // resolveCurrentUserId hook. It is still included in the queryKey
92
92
  // above for client-side cache segregation.
93
+ sort: params?.sort,
93
94
  limit: params?.limit ?? 20,
94
95
  offset: pageParam ?? 0
95
96
  },
@@ -1,7 +1,7 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { b as CommentsListDiscriminator, C as CommentListResult, c as CommentCountDiscriminator, a as CommentsThreadDiscriminator } from '../../shared/stack.C-b3Sn8j.cjs';
2
+ import { b as CommentsListDiscriminator, C as CommentListResult, c as CommentCountDiscriminator, a as CommentsThreadDiscriminator } from '../../shared/stack.C7KNNiJm.cjs';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
- import { C as CommentsApiRouter } from '../../shared/stack.CmHRdhl8.cjs';
4
+ import { C as CommentsApiRouter } from '../../shared/stack.CCncwK6g.cjs';
5
5
  import '@btst/stack/plugins/api';
6
6
  import 'better-call';
7
7
  import 'zod';
@@ -56,6 +56,7 @@ declare function createCommentsQueryKeys(client: ReturnType<typeof createApiClie
56
56
  parentId?: string | null;
57
57
  status?: "pending" | "approved" | "spam";
58
58
  currentUserId?: string;
59
+ sort?: "asc" | "desc";
59
60
  limit?: number;
60
61
  } | undefined) => Omit<Omit<{
61
62
  queryKey: readonly ["commentsThread", "list", CommentsThreadDiscriminator];
@@ -1,7 +1,7 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { b as CommentsListDiscriminator, C as CommentListResult, c as CommentCountDiscriminator, a as CommentsThreadDiscriminator } from '../../shared/stack.C-b3Sn8j.mjs';
2
+ import { b as CommentsListDiscriminator, C as CommentListResult, c as CommentCountDiscriminator, a as CommentsThreadDiscriminator } from '../../shared/stack.C7KNNiJm.mjs';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
- import { C as CommentsApiRouter } from '../../shared/stack.Dcz6636A.mjs';
4
+ import { C as CommentsApiRouter } from '../../shared/stack.CO1g47IS.mjs';
5
5
  import '@btst/stack/plugins/api';
6
6
  import 'better-call';
7
7
  import 'zod';
@@ -56,6 +56,7 @@ declare function createCommentsQueryKeys(client: ReturnType<typeof createApiClie
56
56
  parentId?: string | null;
57
57
  status?: "pending" | "approved" | "spam";
58
58
  currentUserId?: string;
59
+ sort?: "asc" | "desc";
59
60
  limit?: number;
60
61
  } | undefined) => Omit<Omit<{
61
62
  queryKey: readonly ["commentsThread", "list", CommentsThreadDiscriminator];
@@ -1,7 +1,7 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { b as CommentsListDiscriminator, C as CommentListResult, c as CommentCountDiscriminator, a as CommentsThreadDiscriminator } from '../../shared/stack.C-b3Sn8j.js';
2
+ import { b as CommentsListDiscriminator, C as CommentListResult, c as CommentCountDiscriminator, a as CommentsThreadDiscriminator } from '../../shared/stack.C7KNNiJm.js';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
- import { C as CommentsApiRouter } from '../../shared/stack.CJE9sAjV.js';
4
+ import { C as CommentsApiRouter } from '../../shared/stack.Bq3kiUaM.js';
5
5
  import '@btst/stack/plugins/api';
6
6
  import 'better-call';
7
7
  import 'zod';
@@ -56,6 +56,7 @@ declare function createCommentsQueryKeys(client: ReturnType<typeof createApiClie
56
56
  parentId?: string | null;
57
57
  status?: "pending" | "approved" | "spam";
58
58
  currentUserId?: string;
59
+ sort?: "asc" | "desc";
59
60
  limit?: number;
60
61
  } | undefined) => Omit<Omit<{
61
62
  queryKey: readonly ["commentsThread", "list", CommentsThreadDiscriminator];
@@ -88,6 +88,7 @@ function createCommentsThreadQueries(client, headers) {
88
88
  // The server resolves the caller's identity server-side via the
89
89
  // resolveCurrentUserId hook. It is still included in the queryKey
90
90
  // above for client-side cache segregation.
91
+ sort: params?.sort,
91
92
  limit: params?.limit ?? 20,
92
93
  offset: pageParam ?? 0
93
94
  },
@@ -1,5 +1,5 @@
1
1
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
- import { d as Comment, S as SerializedComment, C as CommentListResult } from './stack.C-b3Sn8j.js';
2
+ import { d as Comment, S as SerializedComment, C as CommentListResult } from './stack.C7KNNiJm.js';
3
3
  import * as better_call from 'better-call';
4
4
  import { z } from 'zod';
5
5
 
@@ -90,6 +90,7 @@ interface CommentsThreadDiscriminator {
90
90
  parentId: string | null | undefined;
91
91
  status: string | undefined;
92
92
  currentUserId: string | undefined;
93
+ sort: string | undefined;
93
94
  limit: number;
94
95
  }
95
96
  /** Full query key builders — use with queryClient.setQueryData() */
@@ -125,7 +126,7 @@ declare const COMMENTS_QUERY_KEYS: {
125
126
  }) => readonly ["comments", "count", CommentCountDiscriminator];
126
127
  /**
127
128
  * Key for the infinite thread query (top-level comments, load-more).
128
- * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
129
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
129
130
  * Offset is excluded — it is driven by `pageParam`, not baked into the key.
130
131
  */
131
132
  commentsThread: (params?: {
@@ -134,6 +135,7 @@ declare const COMMENTS_QUERY_KEYS: {
134
135
  parentId?: string | null;
135
136
  status?: string;
136
137
  currentUserId?: string;
138
+ sort?: string;
137
139
  limit?: number;
138
140
  }) => readonly ["commentsThread", "list", CommentsThreadDiscriminator];
139
141
  };
@@ -90,6 +90,7 @@ interface CommentsThreadDiscriminator {
90
90
  parentId: string | null | undefined;
91
91
  status: string | undefined;
92
92
  currentUserId: string | undefined;
93
+ sort: string | undefined;
93
94
  limit: number;
94
95
  }
95
96
  /** Full query key builders — use with queryClient.setQueryData() */
@@ -125,7 +126,7 @@ declare const COMMENTS_QUERY_KEYS: {
125
126
  }) => readonly ["comments", "count", CommentCountDiscriminator];
126
127
  /**
127
128
  * Key for the infinite thread query (top-level comments, load-more).
128
- * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
129
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
129
130
  * Offset is excluded — it is driven by `pageParam`, not baked into the key.
130
131
  */
131
132
  commentsThread: (params?: {
@@ -134,6 +135,7 @@ declare const COMMENTS_QUERY_KEYS: {
134
135
  parentId?: string | null;
135
136
  status?: string;
136
137
  currentUserId?: string;
138
+ sort?: string;
137
139
  limit?: number;
138
140
  }) => readonly ["commentsThread", "list", CommentsThreadDiscriminator];
139
141
  };
@@ -90,6 +90,7 @@ interface CommentsThreadDiscriminator {
90
90
  parentId: string | null | undefined;
91
91
  status: string | undefined;
92
92
  currentUserId: string | undefined;
93
+ sort: string | undefined;
93
94
  limit: number;
94
95
  }
95
96
  /** Full query key builders — use with queryClient.setQueryData() */
@@ -125,7 +126,7 @@ declare const COMMENTS_QUERY_KEYS: {
125
126
  }) => readonly ["comments", "count", CommentCountDiscriminator];
126
127
  /**
127
128
  * Key for the infinite thread query (top-level comments, load-more).
128
- * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
129
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
129
130
  * Offset is excluded — it is driven by `pageParam`, not baked into the key.
130
131
  */
131
132
  commentsThread: (params?: {
@@ -134,6 +135,7 @@ declare const COMMENTS_QUERY_KEYS: {
134
135
  parentId?: string | null;
135
136
  status?: string;
136
137
  currentUserId?: string;
138
+ sort?: string;
137
139
  limit?: number;
138
140
  }) => readonly ["commentsThread", "list", CommentsThreadDiscriminator];
139
141
  };
@@ -1,5 +1,5 @@
1
1
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
- import { d as Comment, S as SerializedComment, C as CommentListResult } from './stack.C-b3Sn8j.cjs';
2
+ import { d as Comment, S as SerializedComment, C as CommentListResult } from './stack.C7KNNiJm.cjs';
3
3
  import * as better_call from 'better-call';
4
4
  import { z } from 'zod';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
2
- import { d as Comment, S as SerializedComment, C as CommentListResult } from './stack.C-b3Sn8j.mjs';
2
+ import { d as Comment, S as SerializedComment, C as CommentListResult } from './stack.C7KNNiJm.mjs';
3
3
  import * as better_call from 'better-call';
4
4
  import { z } from 'zod';
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.11.8",
3
+ "version": "2.12.0",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -71,6 +71,7 @@ export interface CommentsThreadDiscriminator {
71
71
  parentId: string | null | undefined;
72
72
  status: string | undefined;
73
73
  currentUserId: string | undefined;
74
+ sort: string | undefined;
74
75
  limit: number;
75
76
  }
76
77
 
@@ -80,6 +81,7 @@ export function commentsThreadDiscriminator(params?: {
80
81
  parentId?: string | null;
81
82
  status?: string;
82
83
  currentUserId?: string;
84
+ sort?: string;
83
85
  limit?: number;
84
86
  }): CommentsThreadDiscriminator {
85
87
  return {
@@ -88,6 +90,7 @@ export function commentsThreadDiscriminator(params?: {
88
90
  parentId: params?.parentId,
89
91
  status: params?.status,
90
92
  currentUserId: params?.currentUserId,
93
+ sort: params?.sort,
91
94
  limit: params?.limit ?? 20,
92
95
  };
93
96
  }
@@ -128,7 +131,7 @@ export const COMMENTS_QUERY_KEYS = {
128
131
 
129
132
  /**
130
133
  * Key for the infinite thread query (top-level comments, load-more).
131
- * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
134
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
132
135
  * Offset is excluded — it is driven by `pageParam`, not baked into the key.
133
136
  */
134
137
  commentsThread: (params?: {
@@ -137,6 +140,7 @@ export const COMMENTS_QUERY_KEYS = {
137
140
  parentId?: string | null;
138
141
  status?: string;
139
142
  currentUserId?: string;
143
+ sort?: string;
140
144
  limit?: number;
141
145
  }) =>
142
146
  ["commentsThread", "list", commentsThreadDiscriminator(params)] as const,
@@ -98,6 +98,17 @@ export interface CommentThreadProps {
98
98
  * Defaults to true.
99
99
  */
100
100
  allowEditing?: boolean;
101
+ /**
102
+ * Sort direction for top-level comments by `createdAt`.
103
+ * - `"desc"` (default): newest first.
104
+ * - `"asc"`: oldest first.
105
+ *
106
+ * Replies inside each thread always render chronologically (oldest → newest)
107
+ * and are unaffected by this prop.
108
+ *
109
+ * Overrides the global `defaultCommentSort` from `CommentsPluginOverrides`.
110
+ */
111
+ sort?: "asc" | "desc";
101
112
  }
102
113
 
103
114
  const DEFAULT_RENDERER: ComponentType<CommentRendererProps> = ({ body }) => (
@@ -325,6 +336,7 @@ function CommentThreadInner({
325
336
  pageSize: pageSizeProp,
326
337
  allowPosting: allowPostingProp,
327
338
  allowEditing: allowEditingProp,
339
+ sort: sortProp,
328
340
  }: CommentThreadProps) {
329
341
  const overrides = usePluginOverrides<
330
342
  CommentsPluginOverrides,
@@ -334,6 +346,7 @@ function CommentThreadInner({
334
346
  pageSizeProp ?? overrides.defaultCommentPageSize ?? DEFAULT_PAGE_SIZE;
335
347
  const allowPosting = allowPostingProp ?? overrides.allowPosting ?? true;
336
348
  const allowEditing = allowEditingProp ?? overrides.allowEditing ?? true;
349
+ const sort = sortProp ?? overrides.defaultCommentSort ?? "desc";
337
350
  const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
338
351
  const [replyingTo, setReplyingTo] = useState<string | null>(null);
339
352
  const [expandedReplies, setExpandedReplies] = useState<Set<string>>(
@@ -357,6 +370,7 @@ function CommentThreadInner({
357
370
  status: "approved",
358
371
  parentId: null,
359
372
  currentUserId,
373
+ sort,
360
374
  pageSize,
361
375
  });
362
376
 
@@ -366,6 +380,7 @@ function CommentThreadInner({
366
380
  currentUserId,
367
381
  infiniteKey: threadQueryKey,
368
382
  pageSize,
383
+ sort,
369
384
  });
370
385
 
371
386
  const handlePost = async (body: string) => {
@@ -164,6 +164,11 @@ export function useInfiniteComments(
164
164
  parentId?: string | null;
165
165
  status?: "pending" | "approved" | "spam";
166
166
  currentUserId?: string;
167
+ /**
168
+ * Sort direction by `createdAt`. Default: `"asc"` (oldest first) — matches
169
+ * the server-side default. Pass `"desc"` for newest-first threads.
170
+ */
171
+ sort?: "asc" | "desc";
167
172
  pageSize?: number;
168
173
  },
169
174
  options?: { enabled?: boolean },
@@ -178,6 +183,7 @@ export function useInfiniteComments(
178
183
  parentId: params.parentId ?? null,
179
184
  status: params.status,
180
185
  currentUserId: params.currentUserId,
186
+ sort: params.sort,
181
187
  limit: pageSize,
182
188
  });
183
189
 
@@ -266,6 +272,17 @@ export function usePostComment(
266
272
  * `nextOffset` from `lastPage.limit` instead of a hardcoded fallback.
267
273
  */
268
274
  pageSize?: number;
275
+ /**
276
+ * Sort direction of the surrounding infinite thread.
277
+ * - `"asc"` (default): newest comments belong on the LAST page → optimistic
278
+ * item is appended to `pages[last].items`.
279
+ * - `"desc"`: newest comments belong on the FIRST page → optimistic item
280
+ * is prepended to `pages[0].items`.
281
+ *
282
+ * Must match the `sort` passed to `useInfiniteComments` so the optimistic
283
+ * item appears in the same position the server will place it.
284
+ */
285
+ sort?: "asc" | "desc";
269
286
  },
270
287
  ) {
271
288
  const queryClient = useQueryClient();
@@ -350,6 +367,7 @@ export function usePostComment(
350
367
  const previous =
351
368
  queryClient.getQueryData<InfiniteData<CommentListResult>>(listKey);
352
369
 
370
+ const isDesc = params.sort === "desc";
353
371
  queryClient.setQueryData<InfiniteData<CommentListResult>>(
354
372
  listKey,
355
373
  (old) => {
@@ -366,16 +384,21 @@ export function usePostComment(
366
384
  pageParams: [0],
367
385
  };
368
386
  }
369
- const lastIdx = old.pages.length - 1;
387
+ // For asc (oldest-first) threads the new comment lives at the end →
388
+ // append to the last page. For desc (newest-first) threads it lives
389
+ // at the top → prepend to the first page.
390
+ const targetIdx = isDesc ? 0 : old.pages.length - 1;
370
391
  return {
371
392
  ...old,
372
393
  // Increment `total` on every page so the header count (which reads
373
394
  // pages[0].total) stays in sync even after multiple pages are loaded.
374
395
  pages: old.pages.map((page, idx) =>
375
- idx === lastIdx
396
+ idx === targetIdx
376
397
  ? {
377
398
  ...page,
378
- items: [...page.items, optimistic],
399
+ items: isDesc
400
+ ? [optimistic, ...page.items]
401
+ : [...page.items, optimistic],
379
402
  total: page.total + 1,
380
403
  }
381
404
  : { ...page, total: page.total + 1 },
@@ -75,6 +75,16 @@ export interface CommentsPluginOverrides {
75
75
  */
76
76
  defaultCommentPageSize?: number;
77
77
 
78
+ /**
79
+ * Default sort direction (by `createdAt`) for top-level comments in
80
+ * `CommentThread`.
81
+ * - `"desc"` (default): newest comments first.
82
+ * - `"asc"`: oldest comments first.
83
+ *
84
+ * Can be overridden per-instance via the `sort` prop on `CommentThread`.
85
+ */
86
+ defaultCommentSort?: "asc" | "desc";
87
+
78
88
  /**
79
89
  * When false, the comment form and reply buttons are hidden in all
80
90
  * `CommentThread` instances. Users can still read existing comments.
@@ -142,6 +142,7 @@ function createCommentsThreadQueries(
142
142
  parentId?: string | null;
143
143
  status?: "pending" | "approved" | "spam";
144
144
  currentUserId?: string;
145
+ sort?: "asc" | "desc";
145
146
  limit?: number;
146
147
  }) => ({
147
148
  // Offset is excluded from the key — it is driven by pageParam.
@@ -162,6 +163,7 @@ function createCommentsThreadQueries(
162
163
  // The server resolves the caller's identity server-side via the
163
164
  // resolveCurrentUserId hook. It is still included in the queryKey
164
165
  // above for client-side cache segregation.
166
+ sort: params?.sort,
165
167
  limit: params?.limit ?? 20,
166
168
  offset: pageParam ?? 0,
167
169
  },