@mrrocket/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js ADDED
@@ -0,0 +1,104 @@
1
+ // src/core.ts
2
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
3
+ var PREVIEW_PARAM = "ck_preview";
4
+ var EMBED_PARAM = "ck_embed";
5
+ var ID_COOKIE = "ck_id";
6
+ var SESSION_COOKIE = "ck_sid";
7
+ var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
8
+ var readUtm = (search) => {
9
+ const params = new URLSearchParams(search);
10
+ const out = {};
11
+ for (const key of UTM_KEYS) {
12
+ const value = params.get(key)?.trim();
13
+ if (value) out[key] = value.slice(0, 150);
14
+ }
15
+ return out;
16
+ };
17
+ var hashUnit = (input) => {
18
+ let h = 2166136261;
19
+ for (let i = 0; i < input.length; i++) {
20
+ h ^= input.charCodeAt(i);
21
+ h = Math.imul(h, 16777619);
22
+ }
23
+ return (h >>> 0) / 4294967296;
24
+ };
25
+ var assign = (experiment, distinctId) => {
26
+ const control = experiment.variants[0]?.id ?? "control";
27
+ if (experiment.winner) return experiment.winner;
28
+ if (experiment.status !== "running") return control;
29
+ const u = hashUnit(`${experiment.key}:${distinctId}`);
30
+ let acc = 0;
31
+ for (const v of experiment.variants) {
32
+ acc += v.weight;
33
+ if (u < acc) return v.id;
34
+ }
35
+ return experiment.variants[experiment.variants.length - 1]?.id ?? control;
36
+ };
37
+ var parsePreview = (value) => {
38
+ const raw = Array.isArray(value) ? value[0] : value;
39
+ if (!raw) return {};
40
+ const out = {};
41
+ for (const pair of raw.split(",")) {
42
+ const [key, variant] = pair.split(":");
43
+ if (key && variant) out[key.trim()] = variant.trim();
44
+ }
45
+ return out;
46
+ };
47
+ var previewHref = (url, forced, embed = true) => {
48
+ const [path, query = ""] = url.split("?");
49
+ const params = new URLSearchParams(query);
50
+ const value = Object.entries(forced).map(([k, v]) => `${k}:${v}`).join(",");
51
+ if (value) params.set(PREVIEW_PARAM, value);
52
+ if (embed) params.set(EMBED_PARAM, "1");
53
+ return `${path}?${params.toString()}`;
54
+ };
55
+ var resolveVariant = (key, config, distinctId, preview) => {
56
+ if (preview[key]) return preview[key];
57
+ const experiment = config?.experiments.find((e) => e.key === key);
58
+ return experiment ? assign(experiment, distinctId) : "control";
59
+ };
60
+ var textChanges = (config, distinctId, preview) => {
61
+ const out = {};
62
+ for (const experiment of config?.experiments ?? []) {
63
+ const id = resolveVariant(experiment.key, config, distinctId, preview);
64
+ if (id === (experiment.variants[0]?.id ?? "control")) continue;
65
+ Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});
66
+ }
67
+ return out;
68
+ };
69
+ var envHost = () => {
70
+ try {
71
+ return process.env.NEXT_PUBLIC_MRROCKET_HOST || void 0;
72
+ } catch {
73
+ return void 0;
74
+ }
75
+ };
76
+ var envKey = () => {
77
+ try {
78
+ return process.env.NEXT_PUBLIC_MRROCKET_KEY || void 0;
79
+ } catch {
80
+ return void 0;
81
+ }
82
+ };
83
+ var resolveHost = (host) => (host ?? envHost() ?? "").trim().replace(/\/+$/, "");
84
+ var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
85
+ export {
86
+ EMBED_PARAM,
87
+ ID_COOKIE,
88
+ PREVIEW_PARAM,
89
+ SESSION_COOKIE,
90
+ SESSION_TIMEOUT_MS,
91
+ UTM_KEYS,
92
+ assign,
93
+ envHost,
94
+ envKey,
95
+ hashUnit,
96
+ newDistinctId,
97
+ parsePreview,
98
+ previewHref,
99
+ readUtm,
100
+ resolveHost,
101
+ resolveVariant,
102
+ textChanges
103
+ };
104
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.\n * No React, no browser APIs, so the server helper, the proxy helper and the client share it.\n */\n\nexport type VariantId = string;\n\n/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */\nexport interface VariantConfig {\n id: VariantId;\n /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */\n weight: number;\n changes?: Record<string, string>;\n}\n\nexport interface ExperimentConfig {\n /** The experiment's key, the same as in Cockpit. */\n key: string;\n status: \"running\" | \"draft\" | \"stopped\";\n /** Control first. */\n variants: VariantConfig[];\n /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */\n winner?: VariantId;\n}\n\nexport interface CockpitConfig {\n /** The product the public key belongs to. */\n project: string;\n experiments: ExperimentConfig[];\n /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */\n referral?: { cookieDays: number };\n}\n\n/**\n * What the SDK may send and store.\n * - \"cookieless\" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors\n * with a hash of key, date, IP and user agent that changes every day.\n * - \"cookies\": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.\n * - \"off\": nothing is sent and nothing is stored.\n */\nexport type ConsentMode = \"cookieless\" | \"cookies\" | \"off\";\n\n/** The campaign parameters the SDK reads from the landing URL. */\nexport const UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\nexport type UtmKey = (typeof UTM_KEYS)[number];\n\n/** One event as it goes over the wire to `POST /api/sdk/events`. */\nexport interface CockpitEvent {\n event: string;\n /** The visitor. Missing in \"cookieless\" mode until identify(): the server then uses its daily visitor hash. */\n distinctId?: string;\n /** Ends after 30 minutes without an event. */\n sessionId?: string;\n props?: Record<string, unknown>;\n /** Milliseconds since the epoch. */\n ts: number;\n url?: string;\n /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */\n referrer?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n /** The screen size in CSS pixels. */\n screen?: { w: number; h: number };\n}\n\n/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */\nexport const PREVIEW_PARAM = \"ck_preview\";\n/** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */\nexport const EMBED_PARAM = \"ck_embed\";\n/** The cookie that keeps a visitor in the same variant across visits. Only written in \"cookies\" mode. */\nexport const ID_COOKIE = \"ck_id\";\n/** The cookie that holds the session id in \"cookies\" mode. */\nexport const SESSION_COOKIE = \"ck_sid\";\n/** A session ends after this long without an event. */\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** The UTM parameters of a query string, like `\"?utm_source=x\"`. Empty values are left out. */\nexport const readUtm = (search: string): Partial<Record<UtmKey, string>> => {\n const params = new URLSearchParams(search);\n const out: Partial<Record<UtmKey, string>> = {};\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim();\n if (value) out[key] = value.slice(0, 150);\n }\n return out;\n};\n\n/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */\nexport const hashUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0) / 4294967296;\n};\n\n/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */\nexport const assign = (experiment: ExperimentConfig, distinctId: string): VariantId => {\n const control = experiment.variants[0]?.id ?? \"control\";\n if (experiment.winner) return experiment.winner;\n if (experiment.status !== \"running\") return control;\n const u = hashUnit(`${experiment.key}:${distinctId}`);\n let acc = 0;\n for (const v of experiment.variants) {\n acc += v.weight;\n if (u < acc) return v.id;\n }\n return experiment.variants[experiment.variants.length - 1]?.id ?? control;\n};\n\n/** `\"a:b,c:d\"` → `{ a: \"b\", c: \"d\" }`. Ignores anything malformed. */\nexport const parsePreview = (value: string | string[] | null | undefined): Record<string, VariantId> => {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) return {};\n const out: Record<string, VariantId> = {};\n for (const pair of raw.split(\",\")) {\n const [key, variant] = pair.split(\":\");\n if (key && variant) out[key.trim()] = variant.trim();\n }\n return out;\n};\n\n/** A link that shows one variant of a page, for example in Cockpit's preview frames. */\nexport const previewHref = (url: string, forced: Record<string, VariantId>, embed = true) => {\n const [path, query = \"\"] = url.split(\"?\");\n const params = new URLSearchParams(query);\n const value = Object.entries(forced)\n .map(([k, v]) => `${k}:${v}`)\n .join(\",\");\n if (value) params.set(PREVIEW_PARAM, value);\n if (embed) params.set(EMBED_PARAM, \"1\");\n return `${path}?${params.toString()}`;\n};\n\n/** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */\nexport const resolveVariant = (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>): VariantId => {\n if (preview[key]) return preview[key];\n const experiment = config?.experiments.find((e) => e.key === key);\n return experiment ? assign(experiment, distinctId) : \"control\";\n};\n\n/**\n * Every text change that applies to this visitor, merged in config order. Control keeps the page's own words,\n * so only a non-control variant (assigned, forced in a preview, or a shipped winner) changes text.\n */\nexport const textChanges = (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => {\n const out: Record<string, string> = {};\n for (const experiment of config?.experiments ?? []) {\n const id = resolveVariant(experiment.key, config, distinctId, preview);\n if (id === (experiment.variants[0]?.id ?? \"control\")) continue;\n Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});\n }\n return out;\n};\n\n/**\n * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js\n * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on\n * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.\n */\nexport const envHost = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_HOST || undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */\nexport const envKey = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_KEY || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else \"\" (the page's own origin, which is how\n * MRRocket's own demo pages call it). Without a trailing slash.\n */\nexport const resolveHost = (host?: string) => (host ?? envHost() ?? \"\").trim().replace(/\\/+$/, \"\");\n\n/** A random visitor id. */\nexport const newDistinctId = () =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,28 @@
1
+ import { type Theme } from "./ui.js";
2
+ export interface FeedbackBoardProps {
3
+ /** Heading above the board. */
4
+ title?: string;
5
+ /** Line under the heading. */
6
+ subtitle?: string;
7
+ theme?: Theme;
8
+ /** Accent color for buttons and votes. Default #3f76ff. */
9
+ accent?: string;
10
+ }
11
+ /** The full board for a /feedback page: requests, roadmap and changelog, with voting, comments and a submit form. */
12
+ export declare const FeedbackBoard: ({ title, subtitle, theme, accent }: FeedbackBoardProps) => import("react").JSX.Element;
13
+ /** Open or close the feedback widget from your own button. */
14
+ export declare const useFeedback: () => {
15
+ open: () => void;
16
+ close: () => void;
17
+ };
18
+ export interface FeedbackWidgetProps {
19
+ position?: "bottom-right" | "bottom-left";
20
+ /** Text on the floating button. */
21
+ label?: string;
22
+ /** Where "See all" goes, for example your /feedback page. */
23
+ boardUrl?: string;
24
+ theme?: Theme;
25
+ accent?: string;
26
+ }
27
+ /** A floating button that opens a small popover with the top requests and a quick suggest form. */
28
+ export declare const FeedbackWidget: ({ position, label, boardUrl, theme, accent }: FeedbackWidgetProps) => import("react").JSX.Element;
@@ -0,0 +1,459 @@
1
+ "use client";
2
+
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";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ var useFeedbackList = (opts) => {
9
+ const api = useApi();
10
+ const [posts, setPosts] = useState([]);
11
+ const [boards, setBoards] = useState([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [error, setError] = useState("");
14
+ const [tick, setTick] = useState(0);
15
+ const { board, sort, q } = opts;
16
+ useEffect(() => {
17
+ const ctl = new AbortController();
18
+ setLoading(true);
19
+ const wait = window.setTimeout(
20
+ () => {
21
+ api.list({ board, sort, q: q?.trim() || void 0 }, ctl.signal).then((r) => {
22
+ setPosts(r.posts);
23
+ setBoards(r.boards);
24
+ setError("");
25
+ setLoading(false);
26
+ }).catch((e) => {
27
+ if (e.name === "AbortError") return;
28
+ setError(e.message);
29
+ setLoading(false);
30
+ });
31
+ },
32
+ q ? 250 : 0
33
+ );
34
+ return () => {
35
+ window.clearTimeout(wait);
36
+ ctl.abort();
37
+ };
38
+ }, [api, board, sort, q, tick]);
39
+ const patch = useCallback((id, fn) => setPosts((all) => all.map((p) => p.id === id ? fn(p) : p)), []);
40
+ const vote = useCallback(
41
+ (post) => {
42
+ const flip = (p) => ({ ...p, voted: !p.voted, votes: p.votes + (p.voted ? -1 : 1) });
43
+ patch(post.id, flip);
44
+ api.vote(post.id).then((r) => patch(post.id, (p) => ({ ...p, voted: r.voted, votes: r.votes }))).catch((e) => {
45
+ patch(post.id, flip);
46
+ setError(`Your vote was not saved. ${e.message}`);
47
+ });
48
+ },
49
+ [api, patch]
50
+ );
51
+ return { api, posts, setPosts, patch, boards, loading, error, setError, vote, reload: () => setTick((t) => t + 1) };
52
+ };
53
+ var SubmitForm = ({
54
+ api,
55
+ boards,
56
+ posts,
57
+ onCreated,
58
+ onVote,
59
+ onCancel,
60
+ compact
61
+ }) => {
62
+ const id = useId();
63
+ const [title, setTitle] = useState("");
64
+ const [body, setBody] = useState("");
65
+ const [board, setBoard] = useState("");
66
+ const [busy, setBusy] = useState(false);
67
+ const [error, setError] = useState("");
68
+ const chosen = board || boards[0]?.id || "";
69
+ const dupes = similarPosts(title, posts);
70
+ const submit = async (e) => {
71
+ e.preventDefault();
72
+ if (!title.trim() || busy) return;
73
+ setBusy(true);
74
+ setError("");
75
+ try {
76
+ const created = await api.submit({ title: title.trim(), body: body.trim() || void 0, board: chosen });
77
+ setTitle("");
78
+ setBody("");
79
+ onCreated(created);
80
+ } catch (err) {
81
+ setError(err.message);
82
+ } finally {
83
+ setBusy(false);
84
+ }
85
+ };
86
+ return /* @__PURE__ */ jsxs("form", { className: "ckw-form", onSubmit: submit, children: [
87
+ /* @__PURE__ */ jsxs("div", { className: "ckw-field", children: [
88
+ /* @__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" })
90
+ ] }),
91
+ dupes.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ckw-dupes", role: "status", children: [
92
+ /* @__PURE__ */ jsx("p", { children: "Similar requests. Vote instead of adding a duplicate:" }),
93
+ dupes.map((d) => /* @__PURE__ */ jsxs("div", { className: "ckw-dupe", children: [
94
+ /* @__PURE__ */ jsx("span", { title: d.title, children: d.title }),
95
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn", disabled: d.voted, onClick: () => onVote(d), children: d.voted ? "Voted" : "Vote instead" })
96
+ ] }, d.id))
97
+ ] }),
98
+ !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?" })
101
+ ] }),
102
+ 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)) })
105
+ ] }),
106
+ error && /* @__PURE__ */ jsx("div", { className: "ckw-error", role: "alert", children: error }),
107
+ /* @__PURE__ */ jsxs("div", { className: "ckw-actions", children: [
108
+ 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" })
110
+ ] })
111
+ ] });
112
+ };
113
+ var PostRow = ({ post, onOpen, onVote }) => /* @__PURE__ */ jsxs("li", { className: "ckw-row", children: [
114
+ /* @__PURE__ */ jsx(VoteButton, { post, onVote }),
115
+ /* @__PURE__ */ jsxs("div", { className: "ckw-main", children: [
116
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-link", onClick: () => onOpen(post), children: post.title }),
117
+ post.body && /* @__PURE__ */ jsx("p", { className: "ckw-excerpt", children: post.body }),
118
+ /* @__PURE__ */ jsxs("div", { className: "ckw-meta", children: [
119
+ /* @__PURE__ */ jsx(StatusPill, { status: post.status }),
120
+ /* @__PURE__ */ jsxs("span", { className: "ckw-stat", children: [
121
+ /* @__PURE__ */ jsx(Chat, {}),
122
+ " ",
123
+ post.comments
124
+ ] }),
125
+ /* @__PURE__ */ jsx("span", { children: timeAgo(post.createdAt) })
126
+ ] })
127
+ ] })
128
+ ] });
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();
136
+ const id = useId();
137
+ const [comments, setComments] = useState(null);
138
+ const [error, setError] = useState("");
139
+ const [text, setText] = useState("");
140
+ const [busy, setBusy] = useState(false);
141
+ const [added, setAdded] = useState(0);
142
+ useEffect(() => {
143
+ const ctl = new AbortController();
144
+ api.comments(post.id, ctl.signal).then((r) => setComments(r.comments)).catch((e) => e.name !== "AbortError" && setError(e.message));
145
+ return () => ctl.abort();
146
+ }, [api, post.id]);
147
+ const send = async (e) => {
148
+ e.preventDefault();
149
+ const body = text.trim();
150
+ 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
+ }
167
+ };
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"
172
+ ] }),
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) })
180
+ ] })
181
+ ] })
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
+ ] })
205
+ ] });
206
+ };
207
+ var Roadmap = ({ onOpen }) => {
208
+ const api = useApi();
209
+ const [data, setData] = useState(null);
210
+ const [error, setError] = useState("");
211
+ const [tick, setTick] = useState(0);
212
+ useEffect(() => {
213
+ const ctl = new AbortController();
214
+ api.roadmap(ctl.signal).then((r) => (setData(r), setError(""))).catch((e) => e.name !== "AbortError" && setError(e.message));
215
+ return () => ctl.abort();
216
+ }, [api, tick]);
217
+ 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
238
+ ] }),
239
+ /* @__PURE__ */ jsxs("span", { className: "ckw-stat", children: [
240
+ /* @__PURE__ */ jsx(Chat, {}),
241
+ " ",
242
+ p.comments
243
+ ] })
244
+ ] })
245
+ ] }, p.id))
246
+ ] }, k)) });
247
+ };
248
+ var Changelog = () => {
249
+ const api = useApi();
250
+ const [entries, setEntries] = useState(null);
251
+ const [error, setError] = useState("");
252
+ const [tick, setTick] = useState(0);
253
+ useEffect(() => {
254
+ const ctl = new AbortController();
255
+ api.changelog(ctl.signal).then((r) => (setEntries(r.entries), setError(""))).catch((e) => e.name !== "AbortError" && setError(e.message));
256
+ return () => ctl.abort();
257
+ }, [api, tick]);
258
+ if (error) return /* @__PURE__ */ jsx(ErrorBar, { message: error, onRetry: () => setTick((t) => t + 1) });
259
+ 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: [
262
+ /* @__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 })
265
+ ] }, e.id)) });
266
+ };
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");
269
+ const [sort, setSort] = useState("top");
270
+ const [board, setBoard] = useState();
271
+ const [q, setQ] = useState("");
272
+ const [creating, setCreating] = useState(false);
273
+ const [opened, setOpened] = useState(null);
274
+ const list = useFeedbackList({ board, sort, q });
275
+ const { posts, setPosts, vote } = list;
276
+ 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
+ });
284
+ return;
285
+ }
286
+ vote(p);
287
+ };
288
+ const tabs = [
289
+ ["requests", "Requests"],
290
+ ["roadmap", "Roadmap"],
291
+ ["changelog", "Changelog"]
292
+ ];
293
+ const onTabKey = (e) => {
294
+ const i = tabs.findIndex(([k]) => k === tab);
295
+ const n = e.key === "ArrowRight" ? i + 1 : e.key === "ArrowLeft" ? i - 1 : null;
296
+ if (n === null) return;
297
+ e.preventDefault();
298
+ setTab(tabs[(n + tabs.length) % tabs.length][0]);
299
+ setOpened(null);
300
+ };
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 })
306
+ ] }),
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);
340
+ }
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)) })
346
+ ] }),
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))
350
+ ] }),
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, {})
353
+ ] })
354
+ ] });
355
+ };
356
+ var listeners = /* @__PURE__ */ new Set();
357
+ var widgetOpen = false;
358
+ var setWidgetOpen = (v) => {
359
+ widgetOpen = v;
360
+ listeners.forEach((l) => l(v));
361
+ };
362
+ var useFeedback = () => ({ open: () => setWidgetOpen(true), close: () => setWidgetOpen(false) });
363
+ var WidgetPanel = ({ boardUrl, onClose }) => {
364
+ const [suggesting, setSuggesting] = useState(false);
365
+ const [thanks, setThanks] = useState(false);
366
+ const list = useFeedbackList({ sort: "top" });
367
+ const { posts, setPosts } = list;
368
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
369
+ /* @__PURE__ */ jsxs("div", { className: "ckw-pop-head", children: [
370
+ /* @__PURE__ */ jsx("span", { children: suggesting ? "Suggest something" : "Top requests" }),
371
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: onClose, "aria-label": "Close feedback", children: /* @__PURE__ */ jsx(Cross, {}) })
372
+ ] }),
373
+ /* @__PURE__ */ jsxs("div", { className: "ckw-pop-body", children: [
374
+ 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(
376
+ SubmitForm,
377
+ {
378
+ compact: true,
379
+ api: list.api,
380
+ boards: list.boards,
381
+ posts,
382
+ onVote: list.vote,
383
+ onCancel: () => setSuggesting(false),
384
+ onCreated: (p) => {
385
+ setPosts((all) => [p, ...all]);
386
+ setSuggesting(false);
387
+ setThanks(true);
388
+ }
389
+ }
390
+ ) : 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 }),
392
+ /* @__PURE__ */ jsxs("div", { className: "ckw-main", children: [
393
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, overflowWrap: "anywhere" }, children: p.title }),
394
+ /* @__PURE__ */ jsx("div", { className: "ckw-meta", children: /* @__PURE__ */ jsx(StatusPill, { status: p.status }) })
395
+ ] })
396
+ ] }, p.id)) })
397
+ ] }),
398
+ /* @__PURE__ */ jsxs("div", { className: "ckw-pop-foot", children: [
399
+ 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: [
401
+ /* @__PURE__ */ jsx(Plus, {}),
402
+ " Suggest something"
403
+ ] }),
404
+ thanks && /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn", onClick: () => setThanks(false), children: "Back" })
405
+ ] })
406
+ ] });
407
+ };
408
+ var FeedbackWidget = ({ position = "bottom-right", label = "Feedback", boardUrl, theme, accent }) => {
409
+ const [open, setOpen] = useState(false);
410
+ const fab = useRef(null);
411
+ const pop = useRef(null);
412
+ useEffect(() => {
413
+ const l = (v) => setOpen(v);
414
+ listeners.add(l);
415
+ setOpen(widgetOpen);
416
+ const click = (e) => {
417
+ if (e.target?.closest("[data-ck-feedback]")) {
418
+ e.preventDefault();
419
+ setWidgetOpen(true);
420
+ }
421
+ };
422
+ document.addEventListener("click", click);
423
+ return () => {
424
+ listeners.delete(l);
425
+ document.removeEventListener("click", click);
426
+ };
427
+ }, []);
428
+ const close = useCallback(() => {
429
+ setWidgetOpen(false);
430
+ fab.current?.focus();
431
+ }, []);
432
+ useEscape(() => open && close());
433
+ useEffect(() => {
434
+ if (open) pop.current?.querySelector("button, input")?.focus();
435
+ }, [open]);
436
+ const trap = (e) => {
437
+ if (e.key !== "Tab" || !pop.current) return;
438
+ const f = [...pop.current.querySelectorAll("a[href], button:not([disabled]), input, select, textarea")];
439
+ if (!f.length) return;
440
+ const first = f[0];
441
+ const last = f[f.length - 1];
442
+ if (e.shiftKey && document.activeElement === first) e.preventDefault(), last.focus();
443
+ else if (!e.shiftKey && document.activeElement === last) e.preventDefault(), first.focus();
444
+ };
445
+ return /* @__PURE__ */ jsxs(Root, { theme, accent, children: [
446
+ /* @__PURE__ */ jsxs("button", { ref: fab, type: "button", className: "ckw-fab", "data-pos": position, "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => open ? close() : setWidgetOpen(true), children: [
447
+ /* @__PURE__ */ jsx(Chat, {}),
448
+ " ",
449
+ label
450
+ ] }),
451
+ open && /* @__PURE__ */ jsx("div", { ref: pop, className: "ckw-pop", "data-pos": position, role: "dialog", "aria-label": "Feedback", onKeyDown: trap, children: /* @__PURE__ */ jsx(WidgetPanel, { boardUrl, onClose: close }) })
452
+ ] });
453
+ };
454
+ export {
455
+ FeedbackBoard,
456
+ FeedbackWidget,
457
+ useFeedback
458
+ };
459
+ //# sourceMappingURL=feedback.js.map