convex-feedback-ui 0.2.0-beta.2 → 0.2.0-beta.3

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 (64) hide show
  1. package/README.md +38 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/RoadmapRoutes.d.ts +3 -0
  4. package/dist/native/expo/RoadmapRoutes.d.ts.map +1 -0
  5. package/dist/native/expo/RoadmapRoutes.js +44 -0
  6. package/dist/native/expo/RoadmapRoutes.js.map +1 -0
  7. package/dist/native/expo/RoadmapScreen.d.ts +24 -0
  8. package/dist/native/expo/RoadmapScreen.d.ts.map +1 -0
  9. package/dist/native/expo/RoadmapScreen.js +36 -0
  10. package/dist/native/expo/RoadmapScreen.js.map +1 -0
  11. package/dist/native/expo/RoadmapStackLayout.d.ts +8 -0
  12. package/dist/native/expo/RoadmapStackLayout.d.ts.map +1 -0
  13. package/dist/native/expo/RoadmapStackLayout.js +56 -0
  14. package/dist/native/expo/RoadmapStackLayout.js.map +1 -0
  15. package/dist/native/expo/RoutedRoadmapContext.d.ts +4 -0
  16. package/dist/native/expo/RoutedRoadmapContext.d.ts.map +1 -0
  17. package/dist/native/expo/RoutedRoadmapContext.js +11 -0
  18. package/dist/native/expo/RoutedRoadmapContext.js.map +1 -0
  19. package/dist/native/expo/index.d.ts +4 -1
  20. package/dist/native/expo/index.d.ts.map +1 -1
  21. package/dist/native/expo/index.js +4 -1
  22. package/dist/native/expo/index.js.map +1 -1
  23. package/dist/native/expo/routes.d.ts +17 -1
  24. package/dist/native/expo/routes.d.ts.map +1 -1
  25. package/dist/native/expo/routes.js +46 -0
  26. package/dist/native/expo/routes.js.map +1 -1
  27. package/dist/native/expo/types.d.ts +35 -1
  28. package/dist/native/expo/types.d.ts.map +1 -1
  29. package/dist/native/shared/ui/RoadmapScreen.d.ts +21 -1
  30. package/dist/native/shared/ui/RoadmapScreen.d.ts.map +1 -1
  31. package/dist/native/shared/ui/RoadmapScreen.js +228 -50
  32. package/dist/native/shared/ui/RoadmapScreen.js.map +1 -1
  33. package/dist/native/shared/ui/primitives.d.ts +1 -1
  34. package/dist/native/shared/ui/primitives.d.ts.map +1 -1
  35. package/dist/native/shared/ui/primitives.js +3 -1
  36. package/dist/native/shared/ui/primitives.js.map +1 -1
  37. package/dist/react/roadmap.d.ts.map +1 -1
  38. package/dist/react/roadmap.js +24 -12
  39. package/dist/react/roadmap.js.map +1 -1
  40. package/dist/react/styles.css +123 -0
  41. package/dist/shared/messages.d.ts.map +1 -1
  42. package/dist/shared/messages.js +23 -0
  43. package/dist/shared/messages.js.map +1 -1
  44. package/dist/shared/types/messages.d.ts +26 -1
  45. package/dist/shared/types/messages.d.ts.map +1 -1
  46. package/dist/shared/types/roadmap.d.ts +10 -1
  47. package/dist/shared/types/roadmap.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/native/expo/README.md +6 -2
  50. package/src/native/expo/RoadmapRoutes.tsx +133 -0
  51. package/src/native/expo/RoadmapScreen.tsx +101 -0
  52. package/src/native/expo/RoadmapStackLayout.tsx +111 -0
  53. package/src/native/expo/RoutedRoadmapContext.tsx +21 -0
  54. package/src/native/expo/index.ts +8 -1
  55. package/src/native/expo/routes.ts +69 -1
  56. package/src/native/expo/types.ts +43 -0
  57. package/src/native/native/README.md +4 -3
  58. package/src/native/shared/ui/RoadmapScreen.tsx +550 -228
  59. package/src/native/shared/ui/primitives.tsx +3 -0
  60. package/src/react/roadmap.tsx +145 -85
  61. package/src/react/styles.css +123 -0
  62. package/src/shared/messages.ts +24 -0
  63. package/src/shared/types/messages.ts +38 -1
  64. package/src/shared/types/roadmap.ts +18 -1
@@ -54,6 +54,7 @@ export type BoardRootProps = PropsWithChildren<FeedbackColorProps>;
54
54
 
55
55
  function BoardRoot({
56
56
  primaryColor,
57
+ primaryForeground,
57
58
  backgroundColor,
58
59
  surfaceColor,
59
60
  textColor,
@@ -65,6 +66,7 @@ function BoardRoot({
65
66
  const colorOverrides = useMemo(
66
67
  () => ({
67
68
  ...(primaryColor === undefined ? {} : { primary: primaryColor }),
69
+ ...(primaryForeground === undefined ? {} : { primaryForeground }),
68
70
  ...(backgroundColor === undefined ? {} : { background: backgroundColor }),
69
71
  ...(surfaceColor === undefined ? {} : { surface: surfaceColor }),
70
72
  ...(textColor === undefined ? {} : { text: textColor }),
@@ -78,6 +80,7 @@ function BoardRoot({
78
80
  dangerColor,
79
81
  mutedColor,
80
82
  primaryColor,
83
+ primaryForeground,
81
84
  surfaceColor,
82
85
  textColor,
83
86
  ],
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import type { RoadmapItem } from "convex-feedback";
3
+ import type { RoadmapItem, RoadmapStatus } from "convex-feedback";
4
4
  import { useState } from "react";
5
5
 
6
6
  import {
@@ -12,6 +12,12 @@ import type { RoadmapScreenProps } from "../shared/types/index.js";
12
12
  import { FeedbackActionError, useFeedbackAction } from "./action.js";
13
13
  import { FeedbackBoard, FeedbackEntry } from "./primitives.js";
14
14
 
15
+ const stages: readonly { value: RoadmapStatus }[] = [
16
+ { value: "planned" },
17
+ { value: "in_progress" },
18
+ { value: "shipped" },
19
+ ];
20
+
15
21
  export function RoadmapScreen({
16
22
  hooks,
17
23
  messages,
@@ -25,6 +31,7 @@ export function RoadmapScreen({
25
31
  }: RoadmapScreenProps) {
26
32
  const resolvedPageSize = pageSize ?? hooks.pageSizes.roadmap;
27
33
  const resolvedEntryPageSize = entryPageSize ?? hooks.pageSizes.entries;
34
+
28
35
  return (
29
36
  <FeedbackProvider messages={messages} theme={theme} unstyled={unstyled}>
30
37
  <RoadmapScreenInner
@@ -50,7 +57,6 @@ function RoadmapScreenInner({
50
57
  pageSize: number;
51
58
  entryPageSize: number;
52
59
  }) {
53
- const { messages } = useFeedbackUi();
54
60
  const [query, setQuery] = useState("");
55
61
  const [selectedId, setSelectedId] = useState<string | null>(null);
56
62
  const roadmap = hooks.useRoadmap();
@@ -59,9 +65,29 @@ function RoadmapScreenInner({
59
65
  const items = searching ? (search ?? []) : roadmap.results;
60
66
  const selected = items.find((item) => item.id === selectedId) ?? null;
61
67
 
62
- if (selected !== null) {
63
- return (
64
- <FeedbackBoard.Root {...colors}>
68
+ return (
69
+ <FeedbackBoard.Root {...colors}>
70
+ {selected === null ? (
71
+ <RoadmapBoard
72
+ items={items}
73
+ loading={
74
+ searching
75
+ ? search === undefined
76
+ : roadmap.status === "LoadingFirstPage"
77
+ }
78
+ searching={searching}
79
+ query={query}
80
+ onQueryChange={setQuery}
81
+ onItemOpen={(item) => setSelectedId(item.id)}
82
+ canLoadMore={
83
+ !searching &&
84
+ (roadmap.status === "CanLoadMore" ||
85
+ roadmap.status === "LoadingMore")
86
+ }
87
+ loadingMore={roadmap.status === "LoadingMore"}
88
+ onLoadMore={() => roadmap.loadMore(pageSize)}
89
+ />
90
+ ) : (
65
91
  <RoadmapDetail
66
92
  item={selected}
67
93
  hooks={hooks}
@@ -70,59 +96,107 @@ function RoadmapScreenInner({
70
96
  onUnauthenticated={onUnauthenticated}
71
97
  onBack={() => setSelectedId(null)}
72
98
  />
73
- </FeedbackBoard.Root>
74
- );
75
- }
99
+ )}
100
+ </FeedbackBoard.Root>
101
+ );
102
+ }
76
103
 
77
- const loading = searching
78
- ? search === undefined
79
- : roadmap.status === "LoadingFirstPage";
104
+ function RoadmapBoard({
105
+ items,
106
+ loading,
107
+ searching,
108
+ query,
109
+ onQueryChange,
110
+ onItemOpen,
111
+ canLoadMore,
112
+ loadingMore,
113
+ onLoadMore,
114
+ }: {
115
+ items: readonly RoadmapItem[];
116
+ loading: boolean;
117
+ searching: boolean;
118
+ query: string;
119
+ onQueryChange: (value: string) => void;
120
+ onItemOpen: (item: RoadmapItem) => void;
121
+ canLoadMore: boolean;
122
+ loadingMore: boolean;
123
+ onLoadMore: () => void;
124
+ }) {
125
+ const { messages } = useFeedbackUi();
80
126
 
81
127
  return (
82
- <FeedbackBoard.Root {...colors}>
128
+ <>
83
129
  <FeedbackBoard.Header>
84
130
  <div>
85
- <FeedbackBoard.Title>Roadmap</FeedbackBoard.Title>
86
- <p className="cf-board__subtitle">
87
- See what is planned, in progress, and shipped.
88
- </p>
131
+ <FeedbackBoard.Title>{messages.roadmap.title}</FeedbackBoard.Title>
132
+ <p className="cf-board__subtitle">{messages.roadmap.subtitle}</p>
89
133
  </div>
90
134
  </FeedbackBoard.Header>
91
135
  <FeedbackBoard.Search
92
136
  value={query}
93
- onValueChange={setQuery}
94
- placeholder="Search roadmap…"
137
+ onValueChange={onQueryChange}
138
+ placeholder={messages.roadmap.searchPlaceholder}
95
139
  />
96
140
  {loading ? (
97
141
  <FeedbackBoard.State>{messages.board.loading}</FeedbackBoard.State>
98
142
  ) : items.length === 0 ? (
99
143
  <FeedbackBoard.State>
100
- {searching ? "No roadmap items found." : "No roadmap items yet."}
144
+ {searching
145
+ ? messages.roadmap.noSearchResults
146
+ : messages.roadmap.noItems}
101
147
  </FeedbackBoard.State>
102
148
  ) : (
103
- <FeedbackBoard.List>
104
- {items.map((item) => (
105
- <RoadmapCard
106
- key={item.id}
107
- item={item}
108
- onOpen={() => setSelectedId(item.id)}
109
- />
110
- ))}
111
- </FeedbackBoard.List>
149
+ <div className="cf-roadmap-board">
150
+ {stages.map((stage) => {
151
+ const stageItems = items
152
+ .filter((item) => item.status === stage.value)
153
+ .sort((a, b) => a.position - b.position);
154
+
155
+ return (
156
+ <section className="cf-roadmap-column" key={stage.value}>
157
+ <header className="cf-roadmap-column__header">
158
+ <div className="cf-roadmap-column__heading">
159
+ <span
160
+ className="cf-roadmap-status__dot"
161
+ aria-hidden="true"
162
+ />
163
+ <h3>{messages.roadmap.statuses[stage.value]}</h3>
164
+ </div>
165
+ <span className="cf-roadmap-column__count">
166
+ {stageItems.length}
167
+ </span>
168
+ </header>
169
+ <div className="cf-roadmap-column__list">
170
+ {stageItems.length === 0 ? (
171
+ <p className="cf-roadmap-column__empty">
172
+ {messages.roadmap.noItems}
173
+ </p>
174
+ ) : (
175
+ stageItems.map((item) => (
176
+ <RoadmapCard
177
+ key={item.id}
178
+ item={item}
179
+ onOpen={() => onItemOpen(item)}
180
+ />
181
+ ))
182
+ )}
183
+ </div>
184
+ </section>
185
+ );
186
+ })}
187
+ </div>
112
188
  )}
113
- {!searching &&
114
- (roadmap.status === "CanLoadMore" ||
115
- roadmap.status === "LoadingMore") && (
116
- <button
117
- type="button"
118
- className="cf-button"
119
- disabled={roadmap.status === "LoadingMore"}
120
- onClick={() => roadmap.loadMore(pageSize)}
121
- >
122
- {messages.board.loadMore}
123
- </button>
124
- )}
125
- </FeedbackBoard.Root>
189
+ {canLoadMore && (
190
+ <button
191
+ type="button"
192
+ className="cf-button"
193
+ disabled={loadingMore}
194
+ onClick={onLoadMore}
195
+ >
196
+ {messages.board.loadMore}
197
+ </button>
198
+ )}
199
+ </>
126
200
  );
127
201
  }
128
202
 
@@ -133,19 +207,24 @@ function RoadmapCard({
133
207
  item: RoadmapItem;
134
208
  onOpen: () => void;
135
209
  }) {
210
+ const { messages } = useFeedbackUi();
211
+
136
212
  return (
137
- <button type="button" className="cf-entry cf-roadmap-card" onClick={onOpen}>
138
- <span className="cf-entry__content">
139
- <span className="cf-entry__meta">
140
- <span className="cf-entry__kind">
141
- {item.status.replaceAll("_", " ")}
142
- </span>
143
- <span className="cf-status">{item.feedbackCount} linked</span>
213
+ <button type="button" className="cf-roadmap-card" onClick={onOpen}>
214
+ <span className="cf-roadmap-card__meta">
215
+ <span className="cf-roadmap-status">
216
+ {messages.roadmap.statuses[item.status]}
144
217
  </span>
145
- <strong className="cf-entry__title">{item.title}</strong>
146
- {item.description && (
147
- <span className="cf-entry__body">{item.description}</span>
148
- )}
218
+ <span className="cf-roadmap-card__count">
219
+ {messages.roadmap.linkedEntries(item.feedbackCount)}
220
+ </span>
221
+ </span>
222
+ <strong className="cf-roadmap-card__title">{item.title}</strong>
223
+ {item.description && (
224
+ <span className="cf-roadmap-card__body">{item.description}</span>
225
+ )}
226
+ <span className="cf-roadmap-card__link">
227
+ {messages.entry.open} <span aria-hidden="true">›</span>
149
228
  </span>
150
229
  </button>
151
230
  );
@@ -168,6 +247,7 @@ function RoadmapDetail({
168
247
  }) {
169
248
  const { messages } = useFeedbackUi();
170
249
  const feedback = hooks.useRoadmapFeedback(item.id);
250
+
171
251
  return (
172
252
  <div className="cf-detail">
173
253
  <button
@@ -177,46 +257,26 @@ function RoadmapDetail({
177
257
  >
178
258
  {messages.entry.back}
179
259
  </button>
180
- <header>
181
- <div className="cf-entry__meta">
182
- <span className="cf-entry__kind">
183
- {item.status.replaceAll("_", " ")}
260
+ <header className="cf-roadmap-detail__header">
261
+ <div className="cf-roadmap-card__meta">
262
+ <span className="cf-roadmap-status">
263
+ {messages.roadmap.statuses[item.status]}
264
+ </span>
265
+ <span className="cf-roadmap-card__count">
266
+ {messages.roadmap.linkedEntries(item.feedbackCount)}
184
267
  </span>
185
- <span className="cf-status">Position {item.position}</span>
186
268
  </div>
187
269
  <h2 className="cf-board__title">{item.title}</h2>
188
- <p className="cf-entry__body">
189
- {item.description ?? "No description."}
190
- </p>
270
+ {item.description && (
271
+ <p className="cf-entry__body">{item.description}</p>
272
+ )}
191
273
  </header>
192
- <dl className="cf-roadmap-meta">
193
- <div>
194
- <dt>ID</dt>
195
- <dd>{item.id}</dd>
196
- </div>
197
- <div>
198
- <dt>Linked feedback</dt>
199
- <dd>{item.feedbackCount}</dd>
200
- </div>
201
- <div>
202
- <dt>Created</dt>
203
- <dd>{new Date(item.createdAt).toLocaleString()}</dd>
204
- </div>
205
- <div>
206
- <dt>Updated</dt>
207
- <dd>{new Date(item.updatedAt).toLocaleString()}</dd>
208
- </div>
209
- <div>
210
- <dt>Document created</dt>
211
- <dd>{new Date(item.creationTime).toLocaleString()}</dd>
212
- </div>
213
- </dl>
214
274
  <section className="cf-discussion">
215
- <h3>Attached feedback</h3>
275
+ <h3>{messages.roadmap.attachedFeedback}</h3>
216
276
  {feedback.status === "LoadingFirstPage" ? (
217
277
  <p className="cf-state">{messages.board.loading}</p>
218
278
  ) : feedback.results.length === 0 ? (
219
- <p className="cf-state">No feedback attached.</p>
279
+ <p className="cf-state">{messages.roadmap.noAttachedFeedback}</p>
220
280
  ) : (
221
281
  <div className="cf-comments">
222
282
  {feedback.results.map((entry) => (
@@ -238,7 +298,7 @@ function RoadmapDetail({
238
298
  disabled={feedback.status === "LoadingMore"}
239
299
  onClick={() => feedback.loadMore(entryPageSize)}
240
300
  >
241
- {messages.comments.loadMore}
301
+ {messages.board.loadMore}
242
302
  </button>
243
303
  )}
244
304
  </section>
@@ -95,11 +95,130 @@
95
95
  background: var(--cf-surface);
96
96
  }
97
97
  :where(.cf-board) :where(.cf-roadmap-card) {
98
+ display: grid;
99
+ gap: 10px;
98
100
  width: 100%;
101
+ border: 1px solid var(--cf-border);
102
+ border-radius: calc(var(--cf-radius) * 0.9);
103
+ padding: 14px;
104
+ background: var(--cf-surface);
99
105
  color: inherit;
100
106
  font: inherit;
101
107
  text-align: left;
102
108
  cursor: pointer;
109
+ transition:
110
+ transform 140ms ease,
111
+ border-color 140ms ease,
112
+ box-shadow 140ms ease;
113
+ }
114
+ :where(.cf-board) :where(.cf-roadmap-card:hover) {
115
+ transform: translateY(-1px);
116
+ border-color: color-mix(in srgb, var(--cf-primary) 35%, var(--cf-border));
117
+ box-shadow: 0 8px 20px rgb(20 20 35 / 0.06);
118
+ }
119
+ :where(.cf-board) :where(.cf-roadmap-card:focus-visible) {
120
+ outline: 3px solid color-mix(in srgb, var(--cf-primary) 25%, transparent);
121
+ outline-offset: 2px;
122
+ }
123
+ :where(.cf-board) :where(.cf-roadmap-board) {
124
+ display: grid;
125
+ grid-template-columns: repeat(3, minmax(0, 1fr));
126
+ gap: 1px;
127
+ margin-bottom: var(--cf-space);
128
+ overflow: hidden;
129
+ border: 1px solid var(--cf-border);
130
+ border-radius: var(--cf-radius);
131
+ background: var(--cf-border);
132
+ }
133
+ :where(.cf-board) :where(.cf-roadmap-column) {
134
+ min-width: 0;
135
+ background: var(--cf-background);
136
+ }
137
+ :where(.cf-board) :where(.cf-roadmap-column__header) {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: space-between;
141
+ gap: 10px;
142
+ min-height: 58px;
143
+ padding: 12px 14px;
144
+ border-bottom: 1px solid var(--cf-border);
145
+ background: color-mix(in srgb, var(--cf-background) 94%, var(--cf-surface));
146
+ }
147
+ :where(.cf-board) :where(.cf-roadmap-column__heading) {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ }
152
+ :where(.cf-board) :where(.cf-roadmap-column__heading h3) {
153
+ margin: 0;
154
+ font-size: 0.9rem;
155
+ }
156
+ :where(.cf-board) :where(.cf-roadmap-column__count),
157
+ :where(.cf-board) :where(.cf-roadmap-card__count) {
158
+ color: var(--cf-muted);
159
+ font-size: 0.75rem;
160
+ font-weight: 600;
161
+ }
162
+ :where(.cf-board) :where(.cf-roadmap-column__list) {
163
+ display: grid;
164
+ align-content: start;
165
+ gap: 10px;
166
+ min-height: 180px;
167
+ padding: 10px;
168
+ }
169
+ :where(.cf-board) :where(.cf-roadmap-column__empty) {
170
+ margin: 0;
171
+ padding: 28px 10px;
172
+ color: var(--cf-muted);
173
+ font-size: 0.85rem;
174
+ line-height: 1.45;
175
+ text-align: center;
176
+ }
177
+ :where(.cf-board) :where(.cf-roadmap-status__dot) {
178
+ width: 7px;
179
+ height: 7px;
180
+ border-radius: 999px;
181
+ background: var(--cf-primary);
182
+ }
183
+ :where(.cf-board) :where(.cf-roadmap-status) {
184
+ display: inline-flex;
185
+ width: fit-content;
186
+ border: 1px solid var(--cf-border);
187
+ border-radius: 999px;
188
+ padding: 4px 8px;
189
+ background: var(--cf-surface-muted);
190
+ color: var(--cf-muted);
191
+ font-size: 0.74rem;
192
+ font-weight: 700;
193
+ }
194
+ :where(.cf-board) :where(.cf-roadmap-card__meta) {
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: space-between;
198
+ gap: 8px;
199
+ }
200
+ :where(.cf-board) :where(.cf-roadmap-card__title) {
201
+ font-size: 1rem;
202
+ line-height: 1.35;
203
+ }
204
+ :where(.cf-board) :where(.cf-roadmap-card__body) {
205
+ overflow: hidden;
206
+ color: var(--cf-muted);
207
+ font-size: 0.88rem;
208
+ line-height: 1.5;
209
+ white-space: pre-wrap;
210
+ display: -webkit-box;
211
+ line-clamp: 3;
212
+ -webkit-box-orient: vertical;
213
+ }
214
+ :where(.cf-board) :where(.cf-roadmap-card__link) {
215
+ color: var(--cf-primary);
216
+ font-size: 0.82rem;
217
+ font-weight: 700;
218
+ }
219
+ :where(.cf-board) :where(.cf-roadmap-detail__header) {
220
+ display: grid;
221
+ gap: 10px;
103
222
  }
104
223
  :where(.cf-board) :where(.cf-entry__content) {
105
224
  min-width: 0;
@@ -348,6 +467,10 @@
348
467
  width: 40px;
349
468
  min-height: 48px;
350
469
  }
470
+ :where(.cf-board) :where(.cf-roadmap-board) {
471
+ grid-template-columns: repeat(3, minmax(17rem, 1fr));
472
+ overflow-x: auto;
473
+ }
351
474
  }
352
475
  :where(.cf-board) :where(.cf-confirm-backdrop) {
353
476
  position: fixed;
@@ -12,6 +12,22 @@ export const englishFeedbackMessages: FeedbackMessages = {
12
12
  loading: "Loading…",
13
13
  loadMore: "Load more",
14
14
  },
15
+ roadmap: {
16
+ title: "Roadmap",
17
+ subtitle: "See what is planned, in progress, and shipped.",
18
+ searchPlaceholder: "Search roadmap…",
19
+ statuses: {
20
+ planned: "Planned",
21
+ in_progress: "In progress",
22
+ shipped: "Shipped",
23
+ },
24
+ linkedEntries: (count) =>
25
+ `${count} linked ${count === 1 ? "entry" : "entries"}`,
26
+ noItems: "No roadmap items yet.",
27
+ noSearchResults: "No matching roadmap items found.",
28
+ attachedFeedback: "Feedback behind this item",
29
+ noAttachedFeedback: "No feedback attached yet.",
30
+ },
15
31
  kinds: {
16
32
  feedback: "Feedback",
17
33
  feature_request: "Feature request",
@@ -86,6 +102,14 @@ export function mergeFeedbackMessages(
86
102
  ): FeedbackMessages {
87
103
  return {
88
104
  board: { ...englishFeedbackMessages.board, ...overrides.board },
105
+ roadmap: {
106
+ ...englishFeedbackMessages.roadmap,
107
+ ...overrides.roadmap,
108
+ statuses: {
109
+ ...englishFeedbackMessages.roadmap.statuses,
110
+ ...overrides.roadmap?.statuses,
111
+ },
112
+ },
89
113
  kinds: {
90
114
  ...englishFeedbackMessages.kinds,
91
115
  ...overrides.kinds,
@@ -1,4 +1,4 @@
1
- import type { EntryKind, EntryStatus } from "convex-feedback";
1
+ import type { EntryKind, EntryStatus, RoadmapStatus } from "convex-feedback";
2
2
 
3
3
  /**
4
4
  * Localizable package-owned UI copy used by both React DOM and React Native.
@@ -40,6 +40,36 @@ export interface FeedbackMessages {
40
40
  loadMore: string;
41
41
  };
42
42
 
43
+ /** Public roadmap copy. */
44
+ roadmap: {
45
+ /** Main roadmap heading. */
46
+ title: string;
47
+
48
+ /** Supporting copy shown near the roadmap heading. */
49
+ subtitle: string;
50
+
51
+ /** Search input placeholder. */
52
+ searchPlaceholder: string;
53
+
54
+ /** Display labels for each roadmap stage. */
55
+ statuses: Record<RoadmapStatus, string>;
56
+
57
+ /** Formats the number of feedback entries attached to an item. */
58
+ linkedEntries: (count: number) => string;
59
+
60
+ /** Empty-state copy when the roadmap has no items. */
61
+ noItems: string;
62
+
63
+ /** Empty-state copy when a search produces no results. */
64
+ noSearchResults: string;
65
+
66
+ /** Heading for feedback attached to a roadmap item. */
67
+ attachedFeedback: string;
68
+
69
+ /** Empty-state copy when an item has no attached feedback. */
70
+ noAttachedFeedback: string;
71
+ };
72
+
43
73
  /** Localized display labels for each fixed entry kind. */
44
74
  kinds: Record<EntryKind, string>;
45
75
 
@@ -176,6 +206,13 @@ export interface FeedbackMessageOverrides {
176
206
  /** Board copy overrides. */
177
207
  board?: Partial<FeedbackMessages["board"]> | undefined;
178
208
 
209
+ /** Roadmap copy overrides. */
210
+ roadmap?:
211
+ | (Omit<Partial<FeedbackMessages["roadmap"]>, "statuses"> & {
212
+ statuses?: Partial<FeedbackMessages["roadmap"]["statuses"]>;
213
+ })
214
+ | undefined;
215
+
179
216
  /** Entry-kind label overrides. */
180
217
  kinds?: Partial<FeedbackMessages["kinds"]> | undefined;
181
218
 
@@ -1,7 +1,12 @@
1
+ import type { CommentSort } from "convex-feedback";
1
2
  import type { FeedbackHooks } from "convex-feedback/react";
2
3
 
3
4
  import type { FeedbackColorProps, FeedbackProviderProps } from "./context.js";
4
- import type { FeedbackUnauthenticatedHandler } from "./screen.js";
5
+ import type {
6
+ FeedbackCommentTransform,
7
+ FeedbackActorRenderer,
8
+ FeedbackUnauthenticatedHandler,
9
+ } from "./screen.js";
5
10
 
6
11
  /** Shared props for the public roadmap screen implementations. */
7
12
  export interface RoadmapScreenProps
@@ -17,6 +22,18 @@ export interface RoadmapScreenProps
17
22
  /** Called when a visitor tries an authenticated-only action anonymously. */
18
23
  onUnauthenticated?: FeedbackUnauthenticatedHandler;
19
24
 
25
+ /** Server-side ordering used when displaying comments on an attached entry. */
26
+ commentSort?: CommentSort;
27
+
28
+ /** Maximum depth exposed when an attached entry's discussion is opened. */
29
+ maxCommentDepth?: number;
30
+
31
+ /** Presentation-only transform applied to loaded comments. */
32
+ transformComments?: FeedbackCommentTransform;
33
+
34
+ /** Optional host renderer for actor information in comments. */
35
+ renderActor?: FeedbackActorRenderer;
36
+
20
37
  /** Number of roadmap items initially requested. */
21
38
  pageSize?: number;
22
39