@mrrocket/sdk 0.4.0 → 0.8.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 (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +102 -7
  3. package/dist/api.d.ts +23 -3
  4. package/dist/api.js +7 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +64 -0
  7. package/dist/browser/mrrocket-push.js +1 -0
  8. package/dist/browser/mrrocket-replay.js +3 -3
  9. package/dist/browser/mrrocket-sw.js +1 -0
  10. package/dist/browser/mrrocket.js +2 -1
  11. package/dist/browser.d.ts +1 -1
  12. package/dist/browser.js +3 -1
  13. package/dist/browser.js.map +1 -1
  14. package/dist/client.d.ts +36 -1
  15. package/dist/client.js +161 -6
  16. package/dist/client.js.map +1 -1
  17. package/dist/context.d.ts +10 -0
  18. package/dist/context.js.map +1 -1
  19. package/dist/core.d.ts +64 -3
  20. package/dist/core.js +54 -2
  21. package/dist/core.js.map +1 -1
  22. package/dist/feedback.d.ts +19 -4
  23. package/dist/feedback.js +652 -190
  24. package/dist/feedback.js.map +1 -1
  25. package/dist/flows-ui.d.ts +23 -0
  26. package/dist/flows-ui.js +384 -0
  27. package/dist/flows-ui.js.map +1 -0
  28. package/dist/flows-web.d.ts +19 -0
  29. package/dist/flows-web.js +42 -0
  30. package/dist/flows-web.js.map +1 -0
  31. package/dist/flows.d.ts +222 -0
  32. package/dist/flows.js +409 -0
  33. package/dist/flows.js.map +1 -0
  34. package/dist/index.d.ts +5 -2
  35. package/dist/index.js +9 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/proxy.d.ts +34 -1
  38. package/dist/proxy.js +14 -1
  39. package/dist/proxy.js.map +1 -1
  40. package/dist/push-web.d.ts +43 -0
  41. package/dist/push-web.js +81 -0
  42. package/dist/push-web.js.map +1 -0
  43. package/dist/react-native/index.d.cts +305 -3
  44. package/dist/react-native/index.d.ts +305 -3
  45. package/dist/react-native/index.js +852 -31
  46. package/dist/react-native/index.js.map +1 -1
  47. package/dist/styles.d.ts +1 -1
  48. package/dist/styles.js +127 -24
  49. package/dist/styles.js.map +1 -1
  50. package/dist/ui.d.ts +3 -0
  51. package/dist/ui.js +12 -0
  52. package/dist/ui.js.map +1 -1
  53. package/package.json +10 -2
package/dist/feedback.js CHANGED
@@ -1,14 +1,71 @@
1
1
  "use client";
2
2
 
3
3
  // src/feedback.tsx
4
- import { useCallback, useEffect, useId, useRef, useState } from "react";
5
- import { useApi } from "./api.js";
6
- import { Chat, ChevronUp, Cross, Plus, Root, StatusPill, VoteButton, similarPosts, timeAgo, useEscape } from "./ui.js";
4
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
5
+ import {
6
+ createApi,
7
+ useApi
8
+ } from "./api.js";
9
+ import { useCtx } from "./context.js";
10
+ import { Chat, ChevronUp, Cross, Moon, Plus, Root, Search, StatusPill, Sun, VoteButton, similarPosts, timeAgo, useEscape } from "./ui.js";
7
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
- var useFeedbackList = (opts) => {
9
- const api = useApi();
12
+ var whoKey = (apiKey) => `ckw-board-who:${apiKey}`;
13
+ var readWho = (apiKey) => {
14
+ try {
15
+ const raw = window.localStorage.getItem(whoKey(apiKey));
16
+ const w = raw ? JSON.parse(raw) : null;
17
+ return w && typeof w.id === "string" && typeof w.name === "string" && typeof w.email === "string" ? w : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ };
22
+ var newWhoId = () => {
23
+ try {
24
+ return `fb_${crypto.randomUUID()}`;
25
+ } catch {
26
+ return `fb_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
27
+ }
28
+ };
29
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
30
+ var useIdentity = () => {
31
+ const ctx = useCtx();
32
+ const [saved, setSaved] = useState(null);
33
+ const [asking, setAsking] = useState(null);
34
+ useEffect(() => setSaved(readWho(ctx.apiKey)), [ctx.apiKey]);
35
+ const fromApp = !!ctx.user && (!!ctx.user.email || !!ctx.user.name);
36
+ const writer = useMemo(
37
+ () => fromApp || !saved ? null : { distinctId: saved.id, user: { name: saved.name, email: saved.email } },
38
+ [fromApp, saved]
39
+ );
40
+ const api = useApi(writer);
41
+ const known = fromApp || !!saved;
42
+ const save = useCallback(
43
+ (name2, email) => {
44
+ const who = { id: saved?.id ?? newWhoId(), name: name2.trim(), email: email.trim().toLowerCase() };
45
+ try {
46
+ window.localStorage.setItem(whoKey(ctx.apiKey), JSON.stringify(who));
47
+ } catch {
48
+ }
49
+ setSaved(who);
50
+ return createApi({ apiKey: ctx.apiKey, host: ctx.host, distinctId: who.id, user: { name: who.name, email: who.email } });
51
+ },
52
+ [ctx.apiKey, ctx.host, saved]
53
+ );
54
+ const forget = useCallback(() => {
55
+ try {
56
+ window.localStorage.removeItem(whoKey(ctx.apiKey));
57
+ } catch {
58
+ }
59
+ setSaved(null);
60
+ }, [ctx.apiKey]);
61
+ const need = useCallback((fn) => known ? fn(api) : setAsking(() => fn), [known, api]);
62
+ const name = fromApp ? String(ctx.user?.name ?? ctx.user?.email ?? "") : saved?.name ?? "";
63
+ return { api, known, fromApp, name, save, forget, need, asking, cancel: () => setAsking(null) };
64
+ };
65
+ var useFeedbackList = (api, opts) => {
10
66
  const [posts, setPosts] = useState([]);
11
67
  const [boards, setBoards] = useState([]);
68
+ const [product, setProduct] = useState(null);
12
69
  const [loading, setLoading] = useState(true);
13
70
  const [error, setError] = useState("");
14
71
  const [tick, setTick] = useState(0);
@@ -21,6 +78,7 @@ var useFeedbackList = (opts) => {
21
78
  api.list({ board, sort, q: q?.trim() || void 0 }, ctl.signal).then((r) => {
22
79
  setPosts(r.posts);
23
80
  setBoards(r.boards);
81
+ if (r.product) setProduct(r.product);
24
82
  setError("");
25
83
  setLoading(false);
26
84
  }).catch((e) => {
@@ -38,41 +96,124 @@ var useFeedbackList = (opts) => {
38
96
  }, [api, board, sort, q, tick]);
39
97
  const patch = useCallback((id, fn) => setPosts((all) => all.map((p) => p.id === id ? fn(p) : p)), []);
40
98
  const vote = useCallback(
41
- (post) => {
99
+ (post, as = api) => {
42
100
  const flip = (p) => ({ ...p, voted: !p.voted, votes: p.votes + (p.voted ? -1 : 1) });
43
101
  patch(post.id, flip);
44
- api.vote(post.id).then((r) => patch(post.id, (p) => ({ ...p, voted: r.voted, votes: r.votes }))).catch((e) => {
102
+ as.vote(post.id).then((r) => patch(post.id, (p) => ({ ...p, voted: r.voted, votes: r.votes }))).catch((e) => {
45
103
  patch(post.id, flip);
46
104
  setError(`Your vote was not saved. ${e.message}`);
47
105
  });
48
106
  },
49
107
  [api, patch]
50
108
  );
51
- return { api, posts, setPosts, patch, boards, loading, error, setError, vote, reload: () => setTick((t) => t + 1) };
109
+ return { posts, setPosts, patch, boards, product, loading, error, setError, vote, reload: () => setTick((t) => t + 1) };
110
+ };
111
+ var BOARD_TONE = { feature: "blue", bug: "red", integration: "violet", improvement: "green" };
112
+ var TypeChip = ({ board, boards }) => /* @__PURE__ */ jsx("span", { className: "ckw-type", "data-tone": BOARD_TONE[board] ?? "gray", children: boards.find((b) => b.id === board)?.label ?? board });
113
+ var Avatar = ({ name, team }) => /* @__PURE__ */ jsx("span", { className: "ckw-av", "data-team": team || void 0, "aria-hidden": "true", children: (name.trim()[0] ?? "?").toUpperCase() });
114
+ var Skeleton = ({ rows = 3 }) => /* @__PURE__ */ jsx("div", { "aria-hidden": "true", children: Array.from({ length: rows }, (_, i) => /* @__PURE__ */ jsx("div", { className: "ckw-skel" }, i)) });
115
+ var ErrorBar = ({ message, onRetry, onClose }) => /* @__PURE__ */ jsxs("div", { className: "ckw-error", role: "alert", children: [
116
+ /* @__PURE__ */ jsx("span", { children: message }),
117
+ onRetry ? /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn", onClick: onRetry, children: "Retry" }) : onClose && /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: onClose, "aria-label": "Dismiss", children: /* @__PURE__ */ jsx(Cross, {}) })
118
+ ] });
119
+ var Modal = ({ title, onClose, children }) => {
120
+ const box = useRef(null);
121
+ const id = useId();
122
+ useEscape(onClose);
123
+ useEffect(() => {
124
+ const before = document.activeElement;
125
+ box.current?.querySelector("input, textarea, button")?.focus();
126
+ return () => before?.focus?.();
127
+ }, []);
128
+ const trap = (e) => {
129
+ if (e.key !== "Tab" || !box.current) return;
130
+ const f = [...box.current.querySelectorAll("a[href], button:not([disabled]), input, select, textarea")];
131
+ if (!f.length) return;
132
+ const first = f[0];
133
+ const last = f[f.length - 1];
134
+ if (e.shiftKey && document.activeElement === first) e.preventDefault(), last.focus();
135
+ else if (!e.shiftKey && document.activeElement === last) e.preventDefault(), first.focus();
136
+ };
137
+ return /* @__PURE__ */ jsx("div", { className: "ckw-scrim", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: /* @__PURE__ */ jsxs("div", { ref: box, className: "ckw-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": id, onKeyDown: trap, children: [
138
+ /* @__PURE__ */ jsxs("div", { className: "ckw-modal-head", children: [
139
+ /* @__PURE__ */ jsx("h2", { id, children: title }),
140
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-icon-btn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(Cross, {}) })
141
+ ] }),
142
+ children
143
+ ] }) });
144
+ };
145
+ var IdentityFields = ({ name, email, setName, setEmail }) => {
146
+ const id = useId();
147
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
148
+ /* @__PURE__ */ jsxs("div", { className: "ckw-pair", children: [
149
+ /* @__PURE__ */ jsxs("div", { className: "ckw-field", children: [
150
+ /* @__PURE__ */ jsx("label", { className: "ckw-label", htmlFor: `${id}-n`, children: "Your name" }),
151
+ /* @__PURE__ */ jsx("input", { id: `${id}-n`, className: "ckw-input", value: name, maxLength: 80, required: true, autoComplete: "name", onChange: (e) => setName(e.target.value) })
152
+ ] }),
153
+ /* @__PURE__ */ jsxs("div", { className: "ckw-field", children: [
154
+ /* @__PURE__ */ jsx("label", { className: "ckw-label", htmlFor: `${id}-e`, children: "Email" }),
155
+ /* @__PURE__ */ jsx("input", { id: `${id}-e`, className: "ckw-input", type: "email", value: email, maxLength: 200, required: true, autoComplete: "email", onChange: (e) => setEmail(e.target.value) })
156
+ ] })
157
+ ] }),
158
+ /* @__PURE__ */ jsx("p", { className: "ckw-fine", children: "Others see your first name and last initial. Your email is only for updates on what you posted or voted for." })
159
+ ] });
160
+ };
161
+ var IdentityDialog = ({ identity }) => {
162
+ const [name, setName] = useState("");
163
+ const [email, setEmail] = useState("");
164
+ const ok = name.trim().length > 0 && EMAIL.test(email.trim());
165
+ const run = identity.asking;
166
+ if (!run) return null;
167
+ return /* @__PURE__ */ jsx(Modal, { title: "Before you vote or comment", onClose: identity.cancel, children: /* @__PURE__ */ jsxs(
168
+ "form",
169
+ {
170
+ className: "ckw-modal-body",
171
+ onSubmit: (e) => {
172
+ e.preventDefault();
173
+ if (!ok) return;
174
+ const api = identity.save(name, email);
175
+ identity.cancel();
176
+ run(api);
177
+ },
178
+ children: [
179
+ /* @__PURE__ */ jsx(IdentityFields, { name, email, setName, setEmail }),
180
+ /* @__PURE__ */ jsxs("div", { className: "ckw-actions", children: [
181
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: identity.cancel, children: "Cancel" }),
182
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", disabled: !ok, children: "Continue" })
183
+ ] })
184
+ ]
185
+ }
186
+ ) });
52
187
  };
53
188
  var SubmitForm = ({
54
- api,
189
+ identity,
55
190
  boards,
56
191
  posts,
57
192
  onCreated,
58
193
  onVote,
59
194
  onCancel,
60
- compact
195
+ compact,
196
+ bare
61
197
  }) => {
62
198
  const id = useId();
63
199
  const [title, setTitle] = useState("");
64
200
  const [body, setBody] = useState("");
65
201
  const [board, setBoard] = useState("");
202
+ const [name, setName] = useState("");
203
+ const [email, setEmail] = useState("");
66
204
  const [busy, setBusy] = useState(false);
67
205
  const [error, setError] = useState("");
68
206
  const chosen = board || boards[0]?.id || "";
69
207
  const dupes = similarPosts(title, posts);
208
+ const ask = !identity.known;
209
+ const ready = title.trim().length >= 3 && (!ask || name.trim().length > 0 && EMAIL.test(email.trim()));
70
210
  const submit = async (e) => {
71
211
  e.preventDefault();
72
- if (!title.trim() || busy) return;
212
+ if (!ready || busy) return;
73
213
  setBusy(true);
74
214
  setError("");
75
215
  try {
216
+ const api = ask ? identity.save(name, email) : identity.api;
76
217
  const created = await api.submit({ title: title.trim(), body: body.trim() || void 0, board: chosen });
77
218
  setTitle("");
78
219
  setBody("");
@@ -83,10 +224,10 @@ var SubmitForm = ({
83
224
  setBusy(false);
84
225
  }
85
226
  };
86
- return /* @__PURE__ */ jsxs("form", { className: "ckw-form", onSubmit: submit, children: [
227
+ return /* @__PURE__ */ jsxs("form", { className: bare ? "ckw-modal-body" : "ckw-form", onSubmit: submit, children: [
87
228
  /* @__PURE__ */ jsxs("div", { className: "ckw-field", children: [
88
229
  /* @__PURE__ */ jsx("label", { className: "ckw-label", htmlFor: `${id}-t`, children: "Title" }),
89
- /* @__PURE__ */ jsx("input", { id: `${id}-t`, className: "ckw-input", value: title, maxLength: 140, required: true, autoFocus: true, onChange: (e) => setTitle(e.target.value), placeholder: "Short, specific summary" })
230
+ /* @__PURE__ */ jsx("input", { id: `${id}-t`, className: "ckw-input", value: title, maxLength: 120, required: true, autoFocus: true, onChange: (e) => setTitle(e.target.value), placeholder: "Short, specific summary" })
90
231
  ] }),
91
232
  dupes.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ckw-dupes", role: "status", children: [
92
233
  /* @__PURE__ */ jsx("p", { children: "Similar requests. Vote instead of adding a duplicate:" }),
@@ -96,116 +237,223 @@ var SubmitForm = ({
96
237
  ] }, d.id))
97
238
  ] }),
98
239
  !compact && /* @__PURE__ */ jsxs("div", { className: "ckw-field", children: [
99
- /* @__PURE__ */ jsx("label", { className: "ckw-label", htmlFor: `${id}-b`, children: "Details (optional)" }),
100
- /* @__PURE__ */ jsx("textarea", { id: `${id}-b`, className: "ckw-textarea", value: body, maxLength: 2e3, onChange: (e) => setBody(e.target.value), placeholder: "What are you trying to do?" })
240
+ /* @__PURE__ */ jsxs("label", { className: "ckw-label", htmlFor: `${id}-b`, children: [
241
+ "Details ",
242
+ /* @__PURE__ */ jsx("span", { className: "ckw-opt-l", children: "optional" })
243
+ ] }),
244
+ /* @__PURE__ */ jsx("textarea", { id: `${id}-b`, className: "ckw-textarea", value: body, maxLength: 2e3, onChange: (e) => setBody(e.target.value), placeholder: "What are you trying to do? What gets in the way?" })
101
245
  ] }),
102
246
  boards.length > 1 && /* @__PURE__ */ jsxs("div", { className: "ckw-field", children: [
103
- /* @__PURE__ */ jsx("label", { className: "ckw-label", htmlFor: `${id}-c`, children: "Board" }),
104
- /* @__PURE__ */ jsx("select", { id: `${id}-c`, className: "ckw-select", value: chosen, onChange: (e) => setBoard(e.target.value), children: boards.map((b) => /* @__PURE__ */ jsx("option", { value: b.id, children: b.label }, b.id)) })
247
+ /* @__PURE__ */ jsx("span", { className: "ckw-label", id: `${id}-c`, children: "Type" }),
248
+ /* @__PURE__ */ jsx("div", { className: "ckw-chips", role: "radiogroup", "aria-labelledby": `${id}-c`, style: { marginBottom: 0 }, children: boards.map((b) => /* @__PURE__ */ jsx("button", { type: "button", role: "radio", "aria-checked": chosen === b.id, className: "ckw-chip", onClick: () => setBoard(b.id), children: b.label }, b.id)) })
105
249
  ] }),
250
+ ask && /* @__PURE__ */ jsx(IdentityFields, { name, email, setName, setEmail }),
106
251
  error && /* @__PURE__ */ jsx("div", { className: "ckw-error", role: "alert", children: error }),
107
252
  /* @__PURE__ */ jsxs("div", { className: "ckw-actions", children: [
108
253
  onCancel && /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: onCancel, children: "Cancel" }),
109
- /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", disabled: busy || !title.trim(), children: busy ? "Sending\u2026" : "Submit request" })
254
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", disabled: busy || !ready, children: busy ? "Sending\u2026" : "Submit" })
110
255
  ] })
111
256
  ] });
112
257
  };
113
- var PostRow = ({ post, onOpen, onVote }) => /* @__PURE__ */ jsxs("li", { className: "ckw-row", children: [
258
+ var PostCard = ({ post, boards, onOpen, onVote }) => /* @__PURE__ */ jsxs("li", { className: "ckw-post", children: [
114
259
  /* @__PURE__ */ jsx(VoteButton, { post, onVote }),
115
260
  /* @__PURE__ */ jsxs("div", { className: "ckw-main", children: [
116
- /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-link", onClick: () => onOpen(post), children: post.title }),
261
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-link ckw-stretch", onClick: () => onOpen(post), children: post.title }),
117
262
  post.body && /* @__PURE__ */ jsx("p", { className: "ckw-excerpt", children: post.body }),
118
263
  /* @__PURE__ */ jsxs("div", { className: "ckw-meta", children: [
119
- /* @__PURE__ */ jsx(StatusPill, { status: post.status }),
120
- /* @__PURE__ */ jsxs("span", { className: "ckw-stat", children: [
264
+ /* @__PURE__ */ jsx(TypeChip, { board: post.board, boards }),
265
+ post.status !== "review" && /* @__PURE__ */ jsx(StatusPill, { status: post.status }),
266
+ /* @__PURE__ */ jsxs("span", { className: "ckw-stat", "aria-label": `${post.comments} comments`, children: [
121
267
  /* @__PURE__ */ jsx(Chat, {}),
122
268
  " ",
123
269
  post.comments
124
270
  ] }),
125
- /* @__PURE__ */ jsx("span", { children: timeAgo(post.createdAt) })
271
+ /* @__PURE__ */ jsx("span", { className: "ckw-dot-sep", children: [post.author, timeAgo(post.createdAt)].filter(Boolean).join(" \xB7 ") })
126
272
  ] })
127
273
  ] })
128
274
  ] });
129
- var Skeleton = () => /* @__PURE__ */ jsx("div", { "aria-hidden": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("div", { className: "ckw-skel" }, i)) });
130
- var ErrorBar = ({ message, onRetry, onClose }) => /* @__PURE__ */ jsxs("div", { className: "ckw-error", role: "alert", children: [
131
- /* @__PURE__ */ jsx("span", { children: message }),
132
- onRetry ? /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn", onClick: onRetry, children: "Retry" }) : onClose && /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: onClose, "aria-label": "Dismiss", children: /* @__PURE__ */ jsx(Cross, {}) })
133
- ] });
134
- var PostDetail = ({ post, onBack, onVote }) => {
135
- const api = useApi();
275
+ var Requests = ({
276
+ list,
277
+ sort,
278
+ setSort,
279
+ board,
280
+ setBoard,
281
+ q,
282
+ onOpen,
283
+ onVote,
284
+ onNew
285
+ }) => {
286
+ const { posts, boards } = list;
287
+ return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs("div", { children: [
288
+ /* @__PURE__ */ jsxs("div", { className: "ckw-tools", children: [
289
+ /* @__PURE__ */ jsx("div", { className: "ckw-seg", role: "group", "aria-label": "Sort", children: ["top", "new"].map((s) => /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": sort === s, onClick: () => setSort(s), children: s === "top" ? "Top" : "New" }, s)) }),
290
+ boards.length > 1 && /* @__PURE__ */ jsxs("div", { className: "ckw-chips ckw-chips-inline", role: "group", "aria-label": "Type", children: [
291
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-chip", "aria-pressed": !board, onClick: () => setBoard(void 0), children: "All" }),
292
+ boards.map((b) => /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-chip", "aria-pressed": board === b.id, onClick: () => setBoard(b.id), children: b.label }, b.id))
293
+ ] })
294
+ ] }),
295
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-busy": list.loading, children: list.loading && posts.length === 0 ? /* @__PURE__ */ jsx(Skeleton, {}) : posts.length === 0 && !list.error ? /* @__PURE__ */ jsxs("div", { className: "ckw-empty", children: [
296
+ /* @__PURE__ */ jsx("p", { children: q ? "No request matches your search." : board ? "Nothing of this type yet." : "No requests yet." }),
297
+ !q && /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-primary", onClick: onNew, children: [
298
+ /* @__PURE__ */ jsx(Plus, {}),
299
+ " Suggest the first one"
300
+ ] })
301
+ ] }) : /* @__PURE__ */ jsx("ul", { className: "ckw-posts", children: posts.map((p) => /* @__PURE__ */ jsx(PostCard, { post: p, boards, onOpen, onVote }, p.id)) }) })
302
+ ] }) });
303
+ };
304
+ var PostDetail = ({
305
+ post,
306
+ boards,
307
+ identity,
308
+ onBack,
309
+ onVote,
310
+ backLabel
311
+ }) => {
136
312
  const id = useId();
137
313
  const [comments, setComments] = useState(null);
138
314
  const [error, setError] = useState("");
139
315
  const [text, setText] = useState("");
140
316
  const [busy, setBusy] = useState(false);
141
317
  const [added, setAdded] = useState(0);
318
+ const { api } = identity;
142
319
  useEffect(() => {
143
320
  const ctl = new AbortController();
144
321
  api.comments(post.id, ctl.signal).then((r) => setComments(r.comments)).catch((e) => e.name !== "AbortError" && setError(e.message));
145
322
  return () => ctl.abort();
146
323
  }, [api, post.id]);
147
- const send = async (e) => {
324
+ const send = (e) => {
148
325
  e.preventDefault();
149
326
  const body = text.trim();
150
327
  if (!body || busy) return;
151
- setBusy(true);
152
- setError("");
153
- const temp = { id: `tmp-${Date.now()}`, body, author: "You", isTeam: false, postedAt: (/* @__PURE__ */ new Date()).toISOString() };
154
- setComments((c) => [...c ?? [], temp]);
155
- setText("");
156
- try {
157
- const real = await api.comment(post.id, body);
158
- setComments((c) => (c ?? []).map((x) => x.id === temp.id ? real : x));
159
- setAdded((n) => n + 1);
160
- } catch (err) {
161
- setComments((c) => (c ?? []).filter((x) => x.id !== temp.id));
162
- setText(body);
163
- setError(err.message);
164
- } finally {
165
- setBusy(false);
166
- }
328
+ identity.need(async (as) => {
329
+ setBusy(true);
330
+ setError("");
331
+ const temp = { id: `tmp-${Date.now()}`, body, author: "You", isTeam: false, postedAt: (/* @__PURE__ */ new Date()).toISOString() };
332
+ setComments((c) => [...c ?? [], temp]);
333
+ setText("");
334
+ try {
335
+ const real = await as.comment(post.id, body);
336
+ setComments((c) => (c ?? []).map((x) => x.id === temp.id ? real : x));
337
+ setAdded((n) => n + 1);
338
+ } catch (err) {
339
+ setComments((c) => (c ?? []).filter((x) => x.id !== temp.id));
340
+ setText(body);
341
+ setError(err.message);
342
+ } finally {
343
+ setBusy(false);
344
+ }
345
+ });
167
346
  };
168
- return /* @__PURE__ */ jsxs("div", { children: [
169
- /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-ghost ckw-back", onClick: onBack, children: [
170
- /* @__PURE__ */ jsx("span", { style: { display: "inline-block", transform: "rotate(-90deg)", lineHeight: 0 }, children: /* @__PURE__ */ jsx(ChevronUp, {}) }),
171
- "All requests"
347
+ const count = Math.max(post.comments + added, comments?.length ?? 0);
348
+ return /* @__PURE__ */ jsxs("article", { className: "ckw-detail", children: [
349
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-back", onClick: onBack, children: [
350
+ /* @__PURE__ */ jsx("span", { className: "ckw-back-icon", children: /* @__PURE__ */ jsx(ChevronUp, {}) }),
351
+ backLabel
172
352
  ] }),
173
- /* @__PURE__ */ jsxs("div", { className: "ckw-detail-head", children: [
174
- /* @__PURE__ */ jsx(VoteButton, { post, onVote }),
175
- /* @__PURE__ */ jsxs("div", { className: "ckw-main", children: [
176
- /* @__PURE__ */ jsx("h2", { children: post.title }),
177
- /* @__PURE__ */ jsxs("div", { className: "ckw-meta", style: { marginTop: 0 }, children: [
178
- /* @__PURE__ */ jsx(StatusPill, { status: post.status }),
179
- /* @__PURE__ */ jsx("span", { children: timeAgo(post.createdAt) })
353
+ /* @__PURE__ */ jsxs("div", { className: "ckw-detail-grid", children: [
354
+ /* @__PURE__ */ jsxs("div", { className: "ckw-detail-main", children: [
355
+ /* @__PURE__ */ jsx("h2", { className: "ckw-detail-title", children: post.title }),
356
+ /* @__PURE__ */ jsxs("div", { className: "ckw-byline", children: [
357
+ /* @__PURE__ */ jsx(Avatar, { name: post.author ?? "?", team: post.author === "Team" }),
358
+ /* @__PURE__ */ jsx("span", { children: post.author ?? "Someone" }),
359
+ /* @__PURE__ */ jsx("span", { className: "ckw-dot-sep", children: timeAgo(post.createdAt) })
360
+ ] }),
361
+ post.body && /* @__PURE__ */ jsx("p", { className: "ckw-body", children: post.body }),
362
+ /* @__PURE__ */ jsxs("h3", { className: "ckw-h", children: [
363
+ "Comments ",
364
+ /* @__PURE__ */ jsx("span", { className: "ckw-count", children: count })
365
+ ] }),
366
+ /* @__PURE__ */ jsxs("form", { className: "ckw-composer", onSubmit: send, children: [
367
+ /* @__PURE__ */ jsx("label", { className: "ckw-sr", htmlFor: id, children: "Leave a comment" }),
368
+ /* @__PURE__ */ jsx("textarea", { id, className: "ckw-textarea", value: text, maxLength: 1e3, placeholder: "Leave a comment", onChange: (e) => setText(e.target.value) }),
369
+ /* @__PURE__ */ jsxs("div", { className: "ckw-actions", children: [
370
+ identity.known && identity.name && /* @__PURE__ */ jsxs("span", { className: "ckw-fine ckw-as", children: [
371
+ "Commenting as ",
372
+ identity.name
373
+ ] }),
374
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", disabled: busy || !text.trim(), children: "Comment" })
375
+ ] })
376
+ ] }),
377
+ error && /* @__PURE__ */ jsx(ErrorBar, { message: error, onClose: () => setError("") }),
378
+ /* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
379
+ comments === null && !error && /* @__PURE__ */ jsx(Skeleton, { rows: 2 }),
380
+ comments?.length === 0 && /* @__PURE__ */ jsx("p", { className: "ckw-sub", children: "No comments yet." }),
381
+ comments?.map((c) => /* @__PURE__ */ jsxs("div", { className: "ckw-comment", children: [
382
+ /* @__PURE__ */ jsx(Avatar, { name: c.author, team: c.isTeam }),
383
+ /* @__PURE__ */ jsxs("div", { className: "ckw-main", children: [
384
+ /* @__PURE__ */ jsxs("div", { children: [
385
+ /* @__PURE__ */ jsx("b", { children: c.author }),
386
+ c.isTeam && /* @__PURE__ */ jsx("span", { className: "ckw-team", children: "Team" }),
387
+ /* @__PURE__ */ jsx("span", { className: "ckw-time", children: timeAgo(c.postedAt) })
388
+ ] }),
389
+ /* @__PURE__ */ jsx("p", { children: c.body })
390
+ ] })
391
+ ] }, c.id))
392
+ ] })
393
+ ] }),
394
+ /* @__PURE__ */ jsxs("aside", { className: "ckw-side", "aria-label": "About this request", children: [
395
+ /* @__PURE__ */ jsxs(
396
+ "button",
397
+ {
398
+ type: "button",
399
+ className: "ckw-upvote",
400
+ "aria-pressed": post.voted,
401
+ "aria-label": `${post.voted ? "Remove your vote" : "Upvote"}. ${post.votes} votes`,
402
+ onClick: () => onVote(post),
403
+ children: [
404
+ /* @__PURE__ */ jsx(ChevronUp, {}),
405
+ /* @__PURE__ */ jsx("span", { children: post.voted ? "Upvoted" : "Upvote" }),
406
+ /* @__PURE__ */ jsx("b", { children: post.votes })
407
+ ]
408
+ }
409
+ ),
410
+ /* @__PURE__ */ jsxs("dl", { className: "ckw-facts", children: [
411
+ /* @__PURE__ */ jsxs("div", { children: [
412
+ /* @__PURE__ */ jsx("dt", { children: "Status" }),
413
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(StatusPill, { status: post.status }) })
414
+ ] }),
415
+ /* @__PURE__ */ jsxs("div", { children: [
416
+ /* @__PURE__ */ jsx("dt", { children: "Type" }),
417
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(TypeChip, { board: post.board, boards }) })
418
+ ] }),
419
+ /* @__PURE__ */ jsxs("div", { children: [
420
+ /* @__PURE__ */ jsx("dt", { children: "Posted" }),
421
+ /* @__PURE__ */ jsx("dd", { children: new Date(post.createdAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" }) })
422
+ ] })
180
423
  ] })
181
424
  ] })
182
- ] }),
183
- post.body && /* @__PURE__ */ jsx("p", { className: "ckw-body", children: post.body }),
184
- /* @__PURE__ */ jsxs("h3", { className: "ckw-h", children: [
185
- "Comments (",
186
- Math.max(post.comments + added, comments?.length ?? 0),
187
- ")"
188
- ] }),
189
- error && /* @__PURE__ */ jsx(ErrorBar, { message: error, onClose: () => setError("") }),
190
- /* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
191
- comments === null && !error && /* @__PURE__ */ jsx(Skeleton, {}),
192
- comments?.length === 0 && /* @__PURE__ */ jsx("p", { className: "ckw-sub", children: "No comments yet. Be the first." }),
193
- comments?.map((c) => /* @__PURE__ */ jsxs("div", { className: "ckw-comment", children: [
194
- /* @__PURE__ */ jsx("b", { children: c.author }),
195
- c.isTeam && /* @__PURE__ */ jsx("span", { className: "ckw-team", children: "Team" }),
196
- /* @__PURE__ */ jsx("span", { className: "ckw-time", children: timeAgo(c.postedAt) }),
197
- /* @__PURE__ */ jsx("p", { children: c.body })
198
- ] }, c.id))
199
- ] }),
200
- /* @__PURE__ */ jsxs("form", { className: "ckw-form", onSubmit: send, style: { marginTop: 12 }, children: [
201
- /* @__PURE__ */ jsx("label", { className: "ckw-label", htmlFor: id, children: "Add a comment" }),
202
- /* @__PURE__ */ jsx("textarea", { id, className: "ckw-textarea", value: text, maxLength: 2e3, onChange: (e) => setText(e.target.value) }),
203
- /* @__PURE__ */ jsx("div", { className: "ckw-actions", children: /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", disabled: busy || !text.trim(), children: "Comment" }) })
204
425
  ] })
205
426
  ] });
206
427
  };
207
- var Roadmap = ({ onOpen }) => {
208
- const api = useApi();
428
+ var COLUMNS = [
429
+ { status: "review", label: "Backlog", note: "Under consideration" },
430
+ { status: "planned", label: "Next up", note: "Committed and queued" },
431
+ { status: "progress", label: "In progress", note: "Actively being built" },
432
+ { status: "shipped", label: "Done", note: "Recently shipped" }
433
+ ];
434
+ var matchesQ = (q, ...texts) => {
435
+ const t = q.trim().toLowerCase();
436
+ return !t || texts.some((x) => x.toLowerCase().includes(t));
437
+ };
438
+ var RoadmapCard = ({ post, boards, onOpen, onVote }) => /* @__PURE__ */ jsxs("li", { className: "ckw-card", children: [
439
+ /* @__PURE__ */ jsx(TypeChip, { board: post.board, boards }),
440
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-link ckw-stretch", onClick: () => onOpen(post), children: post.title }),
441
+ post.body && /* @__PURE__ */ jsx("p", { className: "ckw-excerpt", children: post.body }),
442
+ /* @__PURE__ */ jsxs("div", { className: "ckw-card-foot", children: [
443
+ /* @__PURE__ */ jsxs("span", { className: "ckw-who", children: [
444
+ /* @__PURE__ */ jsx(Avatar, { name: post.author ?? "?", team: post.author === "Team" }),
445
+ /* @__PURE__ */ jsx("span", { children: post.author ?? "" })
446
+ ] }),
447
+ /* @__PURE__ */ jsx(VoteButton, { post, onVote, small: true })
448
+ ] })
449
+ ] });
450
+ var Roadmap = ({
451
+ api,
452
+ boards,
453
+ q,
454
+ onOpen,
455
+ voteRef
456
+ }) => {
209
457
  const [data, setData] = useState(null);
210
458
  const [error, setError] = useState("");
211
459
  const [tick, setTick] = useState(0);
@@ -214,39 +462,34 @@ var Roadmap = ({ onOpen }) => {
214
462
  api.roadmap(ctl.signal).then((r) => (setData(r), setError(""))).catch((e) => e.name !== "AbortError" && setError(e.message));
215
463
  return () => ctl.abort();
216
464
  }, [api, tick]);
465
+ const patch = (id, fn) => setData((d) => {
466
+ if (!d) return d;
467
+ const columns = { ...d.columns };
468
+ for (const k of Object.keys(columns)) columns[k] = columns[k]?.map((p) => p.id === id ? fn(p) : p);
469
+ return { columns };
470
+ });
471
+ const vote = (post) => voteRef(post, (r) => {
472
+ if (r) patch(post.id, (p) => ({ ...p, ...r }));
473
+ });
217
474
  if (error) return /* @__PURE__ */ jsx(ErrorBar, { message: error, onRetry: () => setTick((t) => t + 1) });
218
- if (!data) return /* @__PURE__ */ jsx(Skeleton, {});
219
- const cols = [
220
- ["planned", "Planned"],
221
- ["progress", "In progress"],
222
- ["shipped", "Shipped"]
223
- ];
224
- return /* @__PURE__ */ jsx("div", { className: "ckw-cols", children: cols.map(([k, label]) => /* @__PURE__ */ jsxs("section", { className: "ckw-col", "aria-label": label, children: [
225
- /* @__PURE__ */ jsxs("h3", { children: [
226
- label,
227
- " ",
228
- /* @__PURE__ */ jsx("span", { children: data.columns[k].length })
229
- ] }),
230
- data.columns[k].length === 0 && /* @__PURE__ */ jsx("p", { className: "ckw-sub", children: "Nothing here yet." }),
231
- data.columns[k].map((p) => /* @__PURE__ */ jsxs("div", { className: "ckw-card", children: [
232
- /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-link", onClick: () => onOpen(p), children: p.title }),
233
- /* @__PURE__ */ jsxs("div", { className: "ckw-meta", style: { marginTop: 0 }, children: [
234
- /* @__PURE__ */ jsxs("span", { className: "ckw-stat", children: [
235
- /* @__PURE__ */ jsx(ChevronUp, {}),
236
- " ",
237
- p.votes
475
+ if (!data)
476
+ return /* @__PURE__ */ jsx("div", { className: "ckw-cols", "aria-hidden": "true", children: COLUMNS.map((c) => /* @__PURE__ */ jsx("div", { className: "ckw-col", children: /* @__PURE__ */ jsx(Skeleton, { rows: 2 }) }, c.status)) });
477
+ return /* @__PURE__ */ jsx("div", { className: "ckw-cols", children: COLUMNS.map((c) => {
478
+ const cards = (data.columns[c.status] ?? []).filter((p) => matchesQ(q, p.title, p.body));
479
+ return /* @__PURE__ */ jsxs("section", { className: "ckw-col", "aria-label": c.label, children: [
480
+ /* @__PURE__ */ jsxs("header", { className: "ckw-col-head", children: [
481
+ /* @__PURE__ */ jsxs("h3", { children: [
482
+ /* @__PURE__ */ jsx("span", { className: "ckw-sdot", "data-s": c.status }),
483
+ c.label,
484
+ /* @__PURE__ */ jsx("span", { className: "ckw-count", children: cards.length })
238
485
  ] }),
239
- /* @__PURE__ */ jsxs("span", { className: "ckw-stat", children: [
240
- /* @__PURE__ */ jsx(Chat, {}),
241
- " ",
242
- p.comments
243
- ] })
244
- ] })
245
- ] }, p.id))
246
- ] }, k)) });
486
+ /* @__PURE__ */ jsx("p", { children: c.note })
487
+ ] }),
488
+ cards.length === 0 ? /* @__PURE__ */ jsx("p", { className: "ckw-col-empty", children: q ? "No match" : "Nothing here yet" }) : /* @__PURE__ */ jsx("ul", { className: "ckw-col-list", children: cards.map((p) => /* @__PURE__ */ jsx(RoadmapCard, { post: p, boards, onOpen, onVote: vote }, p.id)) })
489
+ ] }, c.status);
490
+ }) });
247
491
  };
248
- var Changelog = () => {
249
- const api = useApi();
492
+ var Changelog = ({ api, q }) => {
250
493
  const [entries, setEntries] = useState(null);
251
494
  const [error, setError] = useState("");
252
495
  const [tick, setTick] = useState(0);
@@ -257,99 +500,290 @@ var Changelog = () => {
257
500
  }, [api, tick]);
258
501
  if (error) return /* @__PURE__ */ jsx(ErrorBar, { message: error, onRetry: () => setTick((t) => t + 1) });
259
502
  if (!entries) return /* @__PURE__ */ jsx(Skeleton, {});
260
- if (entries.length === 0) return /* @__PURE__ */ jsx("div", { className: "ckw-empty", children: "No updates published yet." });
261
- return /* @__PURE__ */ jsx("div", { children: entries.map((e) => /* @__PURE__ */ jsxs("article", { className: "ckw-entry", children: [
503
+ const shown = entries.filter((e) => matchesQ(q, e.title, e.body));
504
+ if (shown.length === 0) return /* @__PURE__ */ jsx("div", { className: "ckw-empty", children: q ? "No update matches your search." : "No updates published yet." });
505
+ return /* @__PURE__ */ jsx("div", { className: "ckw-log", children: shown.map((e) => /* @__PURE__ */ jsxs("article", { className: "ckw-entry", children: [
262
506
  /* @__PURE__ */ jsx("time", { dateTime: e.publishedAt, children: new Date(e.publishedAt).toLocaleDateString(void 0, { month: "long", day: "numeric", year: "numeric" }) }),
263
- /* @__PURE__ */ jsx("h3", { children: e.title }),
264
- /* @__PURE__ */ jsx("p", { children: e.body })
507
+ /* @__PURE__ */ jsxs("div", { children: [
508
+ /* @__PURE__ */ jsx("h3", { children: e.title }),
509
+ /* @__PURE__ */ jsx("p", { children: e.body })
510
+ ] })
265
511
  ] }, e.id)) });
266
512
  };
267
- var FeedbackBoard = ({ title = "Feedback", subtitle = "Tell us what to build next and vote on what matters to you.", theme, accent }) => {
268
- const [tab, setTab] = useState("requests");
513
+ var TABS = [
514
+ ["requests", "Feedback"],
515
+ ["roadmap", "Roadmap"],
516
+ ["changelog", "Changelog"]
517
+ ];
518
+ var readUrl = () => {
519
+ const p = new URLSearchParams(window.location.search);
520
+ const v = p.get("view");
521
+ return { tab: v === "roadmap" || v === "changelog" ? v : "requests", post: p.get("post") };
522
+ };
523
+ var writeUrl = (tab, post, push) => {
524
+ const url = new URL(window.location.href);
525
+ if (tab === "requests") url.searchParams.delete("view");
526
+ else url.searchParams.set("view", tab);
527
+ if (post) url.searchParams.set("post", post);
528
+ else url.searchParams.delete("post");
529
+ if (url.href === window.location.href) return;
530
+ if (push) window.history.pushState(null, "", url);
531
+ else window.history.replaceState(null, "", url);
532
+ };
533
+ var THEME_KEY = "ckw-theme";
534
+ var systemDark = () => typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
535
+ var FeedbackBoard = ({
536
+ title = "Feedback",
537
+ subtitle = "Tell us what to build next and vote on what matters to you.",
538
+ theme,
539
+ accent,
540
+ header = true,
541
+ syncUrl = false,
542
+ fullPage = false,
543
+ initialTab = "requests"
544
+ }) => {
545
+ const { host } = useCtx();
546
+ const identity = useIdentity();
547
+ const [tab, setTab] = useState(initialTab);
269
548
  const [sort, setSort] = useState("top");
270
549
  const [board, setBoard] = useState();
271
550
  const [q, setQ] = useState("");
551
+ const [searching, setSearching] = useState(false);
272
552
  const [creating, setCreating] = useState(false);
273
553
  const [opened, setOpened] = useState(null);
274
- const list = useFeedbackList({ board, sort, q });
554
+ const [openId, setOpenId] = useState(null);
555
+ const [mode, setMode] = useState(theme ?? "auto");
556
+ const [thanks, setThanks] = useState(false);
557
+ const list = useFeedbackList(identity.api, { board, sort, q: tab === "requests" ? q : "" });
275
558
  const { posts, setPosts, vote } = list;
559
+ const searchRef = useRef(null);
560
+ useEffect(() => {
561
+ if (!header) return;
562
+ try {
563
+ const t = window.localStorage.getItem(THEME_KEY);
564
+ if (t === "light" || t === "dark") setMode(t);
565
+ } catch {
566
+ }
567
+ }, [header]);
568
+ const dark = mode === "dark" || mode === "auto" && systemDark();
569
+ const flipTheme = () => {
570
+ const next = dark ? "light" : "dark";
571
+ setMode(next);
572
+ try {
573
+ window.localStorage.setItem(THEME_KEY, next);
574
+ } catch {
575
+ }
576
+ };
577
+ useEffect(() => {
578
+ if (!syncUrl) return;
579
+ const sync = () => {
580
+ const u = readUrl();
581
+ setTab(u.tab);
582
+ setOpenId(u.post);
583
+ if (!u.post) setOpened(null);
584
+ };
585
+ sync();
586
+ window.addEventListener("popstate", sync);
587
+ return () => window.removeEventListener("popstate", sync);
588
+ }, [syncUrl]);
589
+ const go = (next, post = null) => {
590
+ setTab(next);
591
+ setOpened(post);
592
+ setOpenId(post?.id ?? null);
593
+ if (syncUrl) writeUrl(next, post?.id ?? null, true);
594
+ };
595
+ useEffect(() => {
596
+ if (!openId || opened?.id === openId) return;
597
+ const inList = posts.find((p) => p.id === openId);
598
+ if (inList) return setOpened(inList);
599
+ if (list.loading) return;
600
+ const ctl = new AbortController();
601
+ identity.api.post(openId, ctl.signal).then(setOpened).catch((e) => {
602
+ if (e.name === "AbortError") return;
603
+ setOpenId(null);
604
+ if (syncUrl) writeUrl(tab, null, false);
605
+ });
606
+ return () => ctl.abort();
607
+ }, [openId, posts, list.loading]);
276
608
  const current = opened && (posts.find((p) => p.id === opened.id) ?? opened);
277
- const voteOpened = (p) => {
278
- if (!posts.some((x) => x.id === p.id)) {
279
- setOpened((o) => o ? { ...o, voted: !o.voted, votes: o.votes + (o.voted ? -1 : 1) } : o);
280
- list.api.vote(p.id).then((r) => setOpened((o) => o ? { ...o, voted: r.voted, votes: r.votes } : o)).catch((e) => {
281
- setOpened((o) => o ? { ...o, voted: p.voted, votes: p.votes } : o);
282
- list.setError(`Your vote was not saved. ${e.message}`);
283
- });
609
+ const voteAny = (p, done) => identity.need((as) => {
610
+ const flip = (o) => o && o.id === p.id ? { ...o, voted: !p.voted, votes: p.votes + (p.voted ? -1 : 1) } : o;
611
+ const inList = posts.some((x) => x.id === p.id);
612
+ if (inList && !done) {
613
+ vote(p, as);
284
614
  return;
285
615
  }
286
- vote(p);
287
- };
288
- const tabs = [
289
- ["requests", "Requests"],
290
- ["roadmap", "Roadmap"],
291
- ["changelog", "Changelog"]
292
- ];
616
+ setOpened(flip);
617
+ done?.({ voted: !p.voted, votes: p.votes + (p.voted ? -1 : 1) });
618
+ as.vote(p.id).then((r) => {
619
+ setOpened((o) => o && o.id === p.id ? { ...o, ...r } : o);
620
+ if (inList) list.patch(p.id, (x) => ({ ...x, ...r }));
621
+ done?.(r);
622
+ }).catch((e) => {
623
+ setOpened((o) => o && o.id === p.id ? { ...o, voted: p.voted, votes: p.votes } : o);
624
+ done?.({ voted: p.voted, votes: p.votes });
625
+ list.setError(`Your vote was not saved. ${e.message}`);
626
+ });
627
+ });
293
628
  const onTabKey = (e) => {
294
- const i = tabs.findIndex(([k]) => k === tab);
629
+ const i = TABS.findIndex(([k]) => k === tab);
295
630
  const n = e.key === "ArrowRight" ? i + 1 : e.key === "ArrowLeft" ? i - 1 : null;
296
631
  if (n === null) return;
297
632
  e.preventDefault();
298
- setTab(tabs[(n + tabs.length) % tabs.length][0]);
299
- setOpened(null);
633
+ go(TABS[(n + TABS.length) % TABS.length][0]);
300
634
  };
301
- return /* @__PURE__ */ jsxs(Root, { theme, accent, className: "ckw-board", children: [
302
- /* @__PURE__ */ jsxs("div", { className: "ckw-head", children: [
303
- /* @__PURE__ */ jsxs("div", { children: [
304
- /* @__PURE__ */ jsx("h2", { className: "ckw-title", children: title }),
305
- subtitle && /* @__PURE__ */ jsx("p", { className: "ckw-sub", children: subtitle })
635
+ const product = list.product;
636
+ const logo = product?.logo ? product.logo.startsWith("/") ? `${host}${product.logo}` : product.logo : null;
637
+ const name = product?.name ?? title;
638
+ const newPost = () => (setThanks(false), setCreating(true));
639
+ const searchLabel = tab === "roadmap" ? "Search roadmap" : tab === "changelog" ? "Search updates" : "Search requests";
640
+ return /* @__PURE__ */ jsxs(Root, { theme: header ? dark ? "dark" : "light" : theme, accent, className: `ckw-board${fullPage ? " ckw-full" : ""}`, children: [
641
+ header ? /* @__PURE__ */ jsx("header", { className: "ckw-top", children: /* @__PURE__ */ jsxs("div", { className: "ckw-top-row", children: [
642
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-brand", onClick: () => go("requests"), children: [
643
+ logo ? (
644
+ // eslint-disable-next-line @next/next/no-img-element
645
+ /* @__PURE__ */ jsx("img", { src: logo, alt: "", width: 28, height: 28 })
646
+ ) : /* @__PURE__ */ jsx("span", { className: "ckw-brand-mark", "aria-hidden": "true", children: name.slice(0, 1).toUpperCase() }),
647
+ /* @__PURE__ */ jsx("span", { children: name })
306
648
  ] }),
307
- /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-primary", onClick: () => (setCreating((c) => !c), setOpened(null), setTab("requests")), "aria-expanded": creating, children: [
308
- /* @__PURE__ */ jsx(Plus, {}),
309
- " Request a feature"
310
- ] })
311
- ] }),
312
- /* @__PURE__ */ jsx("div", { className: "ckw-tabs", role: "tablist", "aria-label": "Feedback sections", onKeyDown: onTabKey, children: tabs.map(([k, label]) => /* @__PURE__ */ jsx(
313
- "button",
314
- {
315
- type: "button",
316
- role: "tab",
317
- id: `ckw-tab-${k}`,
318
- "aria-selected": tab === k,
319
- tabIndex: tab === k ? 0 : -1,
320
- className: "ckw-tab",
321
- onClick: () => (setTab(k), setOpened(null)),
322
- children: label
323
- },
324
- k
325
- )) }),
326
- /* @__PURE__ */ jsxs("div", { role: "tabpanel", "aria-labelledby": `ckw-tab-${tab}`, children: [
327
- list.error && /* @__PURE__ */ jsx(ErrorBar, { message: list.error, onRetry: tab === "requests" && !current ? list.reload : void 0, onClose: () => list.setError("") }),
328
- current ? /* @__PURE__ */ jsx(PostDetail, { post: current, onBack: () => setOpened(null), onVote: voteOpened }) : tab === "requests" ? /* @__PURE__ */ jsxs(Fragment, { children: [
329
- creating && /* @__PURE__ */ jsx(
330
- SubmitForm,
331
- {
332
- api: list.api,
333
- boards: list.boards,
334
- posts,
335
- onVote: vote,
336
- onCancel: () => setCreating(false),
337
- onCreated: (p) => {
338
- setPosts((all) => [p, ...all]);
339
- setCreating(false);
649
+ /* @__PURE__ */ jsx("nav", { className: "ckw-nav", role: "tablist", "aria-label": "Board sections", onKeyDown: onTabKey, children: TABS.map(([k, label]) => /* @__PURE__ */ jsx(
650
+ "button",
651
+ {
652
+ type: "button",
653
+ role: "tab",
654
+ id: `ckw-tab-${k}`,
655
+ "aria-selected": tab === k && !current,
656
+ tabIndex: tab === k ? 0 : -1,
657
+ className: "ckw-nav-tab",
658
+ onClick: () => go(k),
659
+ children: label
660
+ },
661
+ k
662
+ )) }),
663
+ /* @__PURE__ */ jsxs("div", { className: "ckw-top-tools", children: [
664
+ /* @__PURE__ */ jsxs("div", { className: "ckw-searchbox", "data-open": searching || !!q || void 0, children: [
665
+ /* @__PURE__ */ jsx(
666
+ "button",
667
+ {
668
+ type: "button",
669
+ className: "ckw-icon-btn",
670
+ "aria-label": searchLabel,
671
+ onClick: () => {
672
+ setSearching(true);
673
+ window.setTimeout(() => searchRef.current?.focus(), 0);
674
+ },
675
+ children: /* @__PURE__ */ jsx(Search, {})
340
676
  }
341
- }
342
- ),
343
- /* @__PURE__ */ jsxs("div", { className: "ckw-tools", children: [
344
- /* @__PURE__ */ jsx("input", { className: "ckw-input ckw-search", type: "search", "aria-label": "Search requests", placeholder: "Search requests", value: q, onChange: (e) => setQ(e.target.value) }),
345
- /* @__PURE__ */ jsx("div", { className: "ckw-seg", role: "group", "aria-label": "Sort", children: ["top", "new"].map((s) => /* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": sort === s, onClick: () => setSort(s), children: s === "top" ? "Top" : "New" }, s)) })
677
+ ),
678
+ /* @__PURE__ */ jsx(
679
+ "input",
680
+ {
681
+ ref: searchRef,
682
+ type: "search",
683
+ className: "ckw-search-input",
684
+ "aria-label": searchLabel,
685
+ placeholder: "Search\u2026",
686
+ value: q,
687
+ onChange: (e) => {
688
+ setQ(e.target.value);
689
+ if (current) go(tab);
690
+ },
691
+ onBlur: () => setSearching(false),
692
+ onKeyDown: (e) => e.key === "Escape" && (setQ(""), e.target.blur())
693
+ }
694
+ )
346
695
  ] }),
347
- list.boards.length > 1 && /* @__PURE__ */ jsxs("div", { className: "ckw-chips", role: "group", "aria-label": "Boards", children: [
348
- /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-chip", "aria-pressed": !board, onClick: () => setBoard(void 0), children: "All" }),
349
- list.boards.map((b) => /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-chip", "aria-pressed": board === b.id, onClick: () => setBoard(b.id), children: b.label }, b.id))
696
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-icon-btn", onClick: flipTheme, "aria-label": dark ? "Switch to light mode" : "Switch to dark mode", children: dark ? /* @__PURE__ */ jsx(Sun, {}) : /* @__PURE__ */ jsx(Moon, {}) }),
697
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-primary ckw-submit", "aria-label": "Submit feedback", onClick: newPost, children: [
698
+ /* @__PURE__ */ jsx(Plus, {}),
699
+ " ",
700
+ /* @__PURE__ */ jsx("span", { children: "Submit feedback" })
701
+ ] })
702
+ ] })
703
+ ] }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
704
+ /* @__PURE__ */ jsxs("div", { className: "ckw-head", children: [
705
+ /* @__PURE__ */ jsxs("div", { children: [
706
+ /* @__PURE__ */ jsx("h2", { className: "ckw-title", children: title }),
707
+ subtitle && /* @__PURE__ */ jsx("p", { className: "ckw-sub", children: subtitle })
350
708
  ] }),
351
- /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-busy": list.loading, children: list.loading && posts.length === 0 ? /* @__PURE__ */ jsx(Skeleton, {}) : posts.length === 0 && !list.error ? /* @__PURE__ */ jsx("div", { className: "ckw-empty", children: q ? "No requests match your search." : "No requests yet. Be the first to suggest something." }) : /* @__PURE__ */ jsx("ul", { className: "ckw-list", children: posts.map((p) => /* @__PURE__ */ jsx(PostRow, { post: p, onOpen: setOpened, onVote: vote }, p.id)) }) })
352
- ] }) : tab === "roadmap" ? /* @__PURE__ */ jsx(Roadmap, { onOpen: setOpened }) : /* @__PURE__ */ jsx(Changelog, {})
709
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-primary", onClick: newPost, children: [
710
+ /* @__PURE__ */ jsx(Plus, {}),
711
+ " Request a feature"
712
+ ] })
713
+ ] }),
714
+ /* @__PURE__ */ jsx("div", { className: "ckw-tabs", role: "tablist", "aria-label": "Feedback sections", onKeyDown: onTabKey, children: TABS.map(([k, label]) => /* @__PURE__ */ jsx("button", { type: "button", role: "tab", id: `ckw-tab-${k}`, "aria-selected": tab === k, tabIndex: tab === k ? 0 : -1, className: "ckw-tab", onClick: () => go(k), children: k === "requests" ? "Requests" : label }, k)) }),
715
+ /* @__PURE__ */ jsx(
716
+ "input",
717
+ {
718
+ className: "ckw-input ckw-search",
719
+ type: "search",
720
+ "aria-label": searchLabel,
721
+ placeholder: searchLabel,
722
+ value: q,
723
+ onChange: (e) => setQ(e.target.value),
724
+ style: { marginBottom: 12 }
725
+ }
726
+ )
727
+ ] }),
728
+ /* @__PURE__ */ jsxs("div", { className: "ckw-body-wrap", role: "tabpanel", "aria-labelledby": `ckw-tab-${tab}`, children: [
729
+ header && !current && /* @__PURE__ */ jsxs("div", { className: "ckw-intro", children: [
730
+ /* @__PURE__ */ jsx("h1", { children: tab === "roadmap" ? "Roadmap" : tab === "changelog" ? "Changelog" : "Feedback" }),
731
+ /* @__PURE__ */ jsx("p", { children: tab === "roadmap" ? "What we are considering, building next and shipped." : tab === "changelog" ? "New and improved, newest first." : subtitle })
732
+ ] }),
733
+ thanks && !current && /* @__PURE__ */ jsxs("div", { className: "ckw-note", role: "status", children: [
734
+ "Thanks, your request is in. You can follow it here.",
735
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-icon-btn", "aria-label": "Dismiss", onClick: () => setThanks(false), children: /* @__PURE__ */ jsx(Cross, {}) })
736
+ ] }),
737
+ list.error && /* @__PURE__ */ jsx(ErrorBar, { message: list.error, onRetry: tab === "requests" && !current ? list.reload : void 0, onClose: () => list.setError("") }),
738
+ current ? /* @__PURE__ */ jsx(
739
+ PostDetail,
740
+ {
741
+ post: current,
742
+ boards: list.boards,
743
+ identity,
744
+ backLabel: tab === "roadmap" ? "Roadmap" : "All requests",
745
+ onBack: () => go(tab),
746
+ onVote: (p) => voteAny(p)
747
+ }
748
+ ) : tab === "requests" ? /* @__PURE__ */ jsx(
749
+ Requests,
750
+ {
751
+ list,
752
+ sort,
753
+ setSort,
754
+ board,
755
+ setBoard,
756
+ q,
757
+ onOpen: (p) => go("requests", p),
758
+ onVote: (p) => voteAny(p),
759
+ onNew: newPost
760
+ }
761
+ ) : tab === "roadmap" ? /* @__PURE__ */ jsx(Roadmap, { api: identity.api, boards: list.boards, q, onOpen: (p) => go("roadmap", p), voteRef: (p, done) => voteAny(p, done) }) : /* @__PURE__ */ jsx(Changelog, { api: identity.api, q })
762
+ ] }),
763
+ creating && /* @__PURE__ */ jsx(Modal, { title: "Submit feedback", onClose: () => setCreating(false), children: /* @__PURE__ */ jsx(
764
+ SubmitForm,
765
+ {
766
+ bare: true,
767
+ identity,
768
+ boards: list.boards,
769
+ posts,
770
+ onVote: (p) => voteAny(p),
771
+ onCancel: () => setCreating(false),
772
+ onCreated: (p) => {
773
+ setPosts((all) => [p, ...all.filter((x) => x.id !== p.id)]);
774
+ setCreating(false);
775
+ setThanks(true);
776
+ go("requests");
777
+ }
778
+ }
779
+ ) }),
780
+ /* @__PURE__ */ jsx(IdentityDialog, { identity }),
781
+ identity.known && !identity.fromApp && header && /* @__PURE__ */ jsxs("p", { className: "ckw-foot", children: [
782
+ "Posting as ",
783
+ identity.name,
784
+ ".",
785
+ " ",
786
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-linkish", onClick: identity.forget, children: "Not you?" })
353
787
  ] })
354
788
  ] });
355
789
  };
@@ -361,10 +795,12 @@ var setWidgetOpen = (v) => {
361
795
  };
362
796
  var useFeedback = () => ({ open: () => setWidgetOpen(true), close: () => setWidgetOpen(false) });
363
797
  var WidgetPanel = ({ boardUrl, onClose }) => {
798
+ const identity = useIdentity();
364
799
  const [suggesting, setSuggesting] = useState(false);
365
800
  const [thanks, setThanks] = useState(false);
366
- const list = useFeedbackList({ sort: "top" });
801
+ const list = useFeedbackList(identity.api, { sort: "top" });
367
802
  const { posts, setPosts } = list;
803
+ const vote = (p) => identity.need((as) => list.vote(p, as));
368
804
  return /* @__PURE__ */ jsxs(Fragment, { children: [
369
805
  /* @__PURE__ */ jsxs("div", { className: "ckw-pop-head", children: [
370
806
  /* @__PURE__ */ jsx("span", { children: suggesting ? "Suggest something" : "Top requests" }),
@@ -372,14 +808,14 @@ var WidgetPanel = ({ boardUrl, onClose }) => {
372
808
  ] }),
373
809
  /* @__PURE__ */ jsxs("div", { className: "ckw-pop-body", children: [
374
810
  list.error && /* @__PURE__ */ jsx(ErrorBar, { message: list.error, onClose: () => list.setError("") }),
375
- thanks ? /* @__PURE__ */ jsx("div", { className: "ckw-empty", role: "status", children: "Thanks, your request is in." }) : suggesting ? /* @__PURE__ */ jsx(
811
+ identity.asking ? /* @__PURE__ */ jsx(WidgetIdentity, { identity }) : thanks ? /* @__PURE__ */ jsx("div", { className: "ckw-empty", role: "status", children: "Thanks, your request is in." }) : suggesting ? /* @__PURE__ */ jsx(
376
812
  SubmitForm,
377
813
  {
378
814
  compact: true,
379
- api: list.api,
815
+ identity,
380
816
  boards: list.boards,
381
817
  posts,
382
- onVote: list.vote,
818
+ onVote: vote,
383
819
  onCancel: () => setSuggesting(false),
384
820
  onCreated: (p) => {
385
821
  setPosts((all) => [p, ...all]);
@@ -388,7 +824,7 @@ var WidgetPanel = ({ boardUrl, onClose }) => {
388
824
  }
389
825
  }
390
826
  ) : list.loading && posts.length === 0 ? /* @__PURE__ */ jsx(Skeleton, {}) : posts.length === 0 && !list.error ? /* @__PURE__ */ jsx("div", { className: "ckw-empty", children: "No requests yet. Suggest the first one." }) : /* @__PURE__ */ jsx("ul", { className: "ckw-list", "aria-live": "polite", children: posts.slice(0, 5).map((p) => /* @__PURE__ */ jsxs("li", { className: "ckw-row", children: [
391
- /* @__PURE__ */ jsx(VoteButton, { post: p, onVote: list.vote }),
827
+ /* @__PURE__ */ jsx(VoteButton, { post: p, onVote: vote }),
392
828
  /* @__PURE__ */ jsxs("div", { className: "ckw-main", children: [
393
829
  /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, overflowWrap: "anywhere" }, children: p.title }),
394
830
  /* @__PURE__ */ jsx("div", { className: "ckw-meta", children: /* @__PURE__ */ jsx(StatusPill, { status: p.status }) })
@@ -397,7 +833,7 @@ var WidgetPanel = ({ boardUrl, onClose }) => {
397
833
  ] }),
398
834
  /* @__PURE__ */ jsxs("div", { className: "ckw-pop-foot", children: [
399
835
  boardUrl ? /* @__PURE__ */ jsx("a", { href: boardUrl, children: "See all" }) : /* @__PURE__ */ jsx("span", {}),
400
- !suggesting && !thanks && /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-primary", onClick: () => setSuggesting(true), children: [
836
+ !suggesting && !thanks && !identity.asking && /* @__PURE__ */ jsxs("button", { type: "button", className: "ckw-btn ckw-btn-primary", onClick: () => setSuggesting(true), children: [
401
837
  /* @__PURE__ */ jsx(Plus, {}),
402
838
  " Suggest something"
403
839
  ] }),
@@ -405,6 +841,32 @@ var WidgetPanel = ({ boardUrl, onClose }) => {
405
841
  ] })
406
842
  ] });
407
843
  };
844
+ var WidgetIdentity = ({ identity }) => {
845
+ const [name, setName] = useState("");
846
+ const [email, setEmail] = useState("");
847
+ const ok = name.trim().length > 0 && EMAIL.test(email.trim());
848
+ return /* @__PURE__ */ jsxs(
849
+ "form",
850
+ {
851
+ className: "ckw-form",
852
+ onSubmit: (e) => {
853
+ e.preventDefault();
854
+ const run = identity.asking;
855
+ if (!ok || !run) return;
856
+ const api = identity.save(name, email);
857
+ identity.cancel();
858
+ run(api);
859
+ },
860
+ children: [
861
+ /* @__PURE__ */ jsx(IdentityFields, { name, email, setName, setEmail }),
862
+ /* @__PURE__ */ jsxs("div", { className: "ckw-actions", children: [
863
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: identity.cancel, children: "Cancel" }),
864
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", disabled: !ok, children: "Continue" })
865
+ ] })
866
+ ]
867
+ }
868
+ );
869
+ };
408
870
  var FeedbackWidget = ({ position = "bottom-right", label = "Feedback", boardUrl, theme, accent }) => {
409
871
  const [open, setOpen] = useState(false);
410
872
  const fab = useRef(null);