@mrrocket/sdk 0.0.0-stage → 0.2.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 (52) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +284 -2
  4. package/dist/api.d.ts +113 -0
  5. package/dist/api.js +71 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/browser.d.ts +11 -0
  8. package/dist/browser.js +25 -0
  9. package/dist/browser.js.map +1 -0
  10. package/dist/capture.d.ts +50 -0
  11. package/dist/capture.js +91 -0
  12. package/dist/capture.js.map +1 -0
  13. package/dist/client.d.ts +83 -0
  14. package/dist/client.js +468 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/context.d.ts +33 -0
  17. package/dist/context.js +15 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/core.d.ts +132 -0
  20. package/dist/core.js +104 -0
  21. package/dist/core.js.map +1 -0
  22. package/dist/feedback.d.ts +28 -0
  23. package/dist/feedback.js +459 -0
  24. package/dist/feedback.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +58 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/proxy.d.ts +28 -0
  29. package/dist/proxy.js +12 -0
  30. package/dist/proxy.js.map +1 -0
  31. package/dist/referral.d.ts +86 -0
  32. package/dist/referral.js +128 -0
  33. package/dist/referral.js.map +1 -0
  34. package/dist/replay-core.d.ts +93 -0
  35. package/dist/replay-core.js +122 -0
  36. package/dist/replay-core.js.map +1 -0
  37. package/dist/replay.d.ts +34 -0
  38. package/dist/replay.js +200 -0
  39. package/dist/replay.js.map +1 -0
  40. package/dist/server.d.ts +47 -0
  41. package/dist/server.js +43 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/styles.d.ts +4 -0
  44. package/dist/styles.js +147 -0
  45. package/dist/styles.js.map +1 -0
  46. package/dist/survey.d.ts +4 -0
  47. package/dist/survey.js +201 -0
  48. package/dist/survey.js.map +1 -0
  49. package/dist/ui.d.ts +29 -0
  50. package/dist/ui.js +77 -0
  51. package/dist/ui.js.map +1 -0
  52. package/package.json +96 -5
package/dist/survey.js ADDED
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ // src/survey.tsx
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useApi } from "./api.js";
6
+ import { useCtx } from "./context.js";
7
+ import { Check, Cross, Root, Star } from "./ui.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ var DISMISS_PREFIX = "ck_survey_";
10
+ var dismissed = /* @__PURE__ */ new Set();
11
+ var remember = (id, persist) => {
12
+ dismissed.add(id);
13
+ if (!persist) return;
14
+ try {
15
+ localStorage.setItem(`${DISMISS_PREFIX}${id}`, "1");
16
+ } catch {
17
+ }
18
+ };
19
+ var wasDismissed = (id, persist) => {
20
+ if (dismissed.has(id)) return true;
21
+ if (!persist) return false;
22
+ try {
23
+ return !!localStorage.getItem(`${DISMISS_PREFIX}${id}`);
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+ var forgetStoredDismissals = () => {
29
+ try {
30
+ for (let i = localStorage.length - 1; i >= 0; i--) {
31
+ const key = localStorage.key(i);
32
+ if (!key?.startsWith(DISMISS_PREFIX)) continue;
33
+ dismissed.add(key.slice(DISMISS_PREFIX.length));
34
+ localStorage.removeItem(key);
35
+ }
36
+ } catch {
37
+ }
38
+ };
39
+ var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
40
+ var Question = ({ q, value, onChange, onEnter }) => {
41
+ if (q.kind === "nps" || q.kind === "rating") {
42
+ const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
43
+ const start = q.kind === "nps" ? 0 : 1;
44
+ const items = Array.from({ length: scale }, (_, i) => i + start);
45
+ const stars = q.kind === "rating" && scale <= 5;
46
+ return /* @__PURE__ */ jsxs("div", { children: [
47
+ /* @__PURE__ */ jsx("div", { className: "ckw-scale", role: "group", "aria-label": q.title, children: items.map(
48
+ (n) => stars ? /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-star", "data-on": typeof value === "number" && n <= value, "aria-pressed": value === n, "aria-label": `${n} of ${scale}`, onClick: () => onChange(n), children: /* @__PURE__ */ jsx(Star, {}) }, n) : /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-num", "aria-pressed": value === n, onClick: () => onChange(n), children: n }, n)
49
+ ) }),
50
+ (q.low || q.high || q.kind === "nps") && /* @__PURE__ */ jsxs("div", { className: "ckw-lh", children: [
51
+ /* @__PURE__ */ jsx("span", { children: q.low ?? (q.kind === "nps" ? "Not likely" : "") }),
52
+ /* @__PURE__ */ jsx("span", { children: q.high ?? (q.kind === "nps" ? "Very likely" : "") })
53
+ ] })
54
+ ] });
55
+ }
56
+ if (q.kind === "single" || q.kind === "multi") {
57
+ const multi = q.kind === "multi";
58
+ const picked = multi ? value ?? [] : [];
59
+ return /* @__PURE__ */ jsx("div", { className: "ckw-opts", role: multi ? "group" : "radiogroup", "aria-label": q.title, children: (q.options ?? []).map((o) => /* @__PURE__ */ jsxs("label", { className: "ckw-opt", children: [
60
+ /* @__PURE__ */ jsx(
61
+ "input",
62
+ {
63
+ type: multi ? "checkbox" : "radio",
64
+ name: q.key,
65
+ checked: multi ? picked.includes(o) : value === o,
66
+ onChange: () => onChange(multi ? picked.includes(o) ? picked.filter((x) => x !== o) : [...picked, o] : o)
67
+ }
68
+ ),
69
+ /* @__PURE__ */ jsx("span", { children: o })
70
+ ] }, o)) });
71
+ }
72
+ return /* @__PURE__ */ jsx(
73
+ "textarea",
74
+ {
75
+ className: "ckw-textarea",
76
+ "aria-label": q.title,
77
+ placeholder: q.placeholder ?? "Type your answer",
78
+ value: value ?? "",
79
+ maxLength: 2e3,
80
+ onChange: (e) => onChange(e.target.value),
81
+ onKeyDown: (e) => (e.metaKey || e.ctrlKey) && e.key === "Enter" && onEnter()
82
+ }
83
+ );
84
+ };
85
+ var Card = ({ survey, onClose }) => {
86
+ const api = useApi();
87
+ const persist = useCtx().effectiveConsent === "cookies";
88
+ const [step, setStep] = useState(0);
89
+ const [answers, setAnswers] = useState({});
90
+ const [done, setDone] = useState(false);
91
+ const [error, setError] = useState("");
92
+ const started = useRef(Date.now());
93
+ const finished = useRef(false);
94
+ const live = useRef(answers);
95
+ live.current = answers;
96
+ const heading = useRef(null);
97
+ const q = survey.questions[step];
98
+ const last = step === survey.questions.length - 1;
99
+ const has = isAnswered(answers[q?.key]);
100
+ useEffect(() => {
101
+ api.seen(survey.id).catch(() => void 0);
102
+ }, [api, survey.id]);
103
+ useEffect(() => {
104
+ heading.current?.focus();
105
+ }, [step]);
106
+ const finish = useCallback(
107
+ async (completed) => {
108
+ if (finished.current) return;
109
+ finished.current = true;
110
+ remember(survey.id, persist);
111
+ api.respond(survey.id, { answers: live.current, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
112
+ },
113
+ [api, survey.id, persist]
114
+ );
115
+ useEffect(() => {
116
+ if (!done) return;
117
+ const t = window.setTimeout(onClose, 2500);
118
+ return () => window.clearTimeout(t);
119
+ }, [done, onClose]);
120
+ const dismiss = useCallback(() => {
121
+ if (Object.values(live.current).some(isAnswered)) void finish(false);
122
+ else remember(survey.id, persist);
123
+ onClose();
124
+ }, [finish, onClose, survey.id, persist]);
125
+ useEffect(() => {
126
+ const h = (e) => e.key === "Escape" && !done && dismiss();
127
+ document.addEventListener("keydown", h);
128
+ return () => document.removeEventListener("keydown", h);
129
+ }, [dismiss, done]);
130
+ const next = async () => {
131
+ if (q.required && !has) {
132
+ setError("Please answer this question.");
133
+ return;
134
+ }
135
+ setError("");
136
+ if (!last) return setStep(step + 1);
137
+ await finish(true);
138
+ setDone(true);
139
+ };
140
+ if (done)
141
+ return /* @__PURE__ */ jsxs("div", { className: "ckw-thanks", role: "status", children: [
142
+ /* @__PURE__ */ jsx("span", { className: "ckw-tick", children: /* @__PURE__ */ jsx(Check, {}) }),
143
+ /* @__PURE__ */ jsx("b", { children: survey.thanks || "Thanks for your feedback." })
144
+ ] });
145
+ return /* @__PURE__ */ jsxs(
146
+ "form",
147
+ {
148
+ onSubmit: (e) => {
149
+ e.preventDefault();
150
+ void next();
151
+ },
152
+ children: [
153
+ /* @__PURE__ */ jsxs("div", { className: "ckw-sv-top", children: [
154
+ /* @__PURE__ */ jsx("div", { className: "ckw-dots", role: "img", "aria-label": `Question ${step + 1} of ${survey.questions.length}`, children: survey.questions.map((x, i) => /* @__PURE__ */ jsx("span", { className: "ckw-dot", "data-on": i <= step }, x.key)) }),
155
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: dismiss, "aria-label": "Close survey", style: { height: 24, padding: "0 4px" }, children: /* @__PURE__ */ jsx(Cross, {}) })
156
+ ] }),
157
+ /* @__PURE__ */ jsxs("h3", { className: "ckw-q", tabIndex: -1, ref: heading, children: [
158
+ q.title,
159
+ !q.required && /* @__PURE__ */ jsx("span", { className: "ckw-time", children: " optional" })
160
+ ] }),
161
+ q.description && /* @__PURE__ */ jsx("p", { className: "ckw-qd", children: q.description }),
162
+ /* @__PURE__ */ jsx(Question, { q, value: answers[q.key], onChange: (v) => (setAnswers((a) => ({ ...a, [q.key]: v })), setError("")), onEnter: () => void next() }),
163
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", children: error && /* @__PURE__ */ jsx("p", { className: "ckw-qd", style: { color: "var(--ckw-danger)", margin: "8px 0 0" }, children: error }) }),
164
+ /* @__PURE__ */ jsxs("div", { className: "ckw-sv-foot", children: [
165
+ /* @__PURE__ */ jsx("span", {}),
166
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", children: last ? "Send" : "Next" })
167
+ ] })
168
+ ]
169
+ }
170
+ );
171
+ };
172
+ var Survey = () => {
173
+ const { embedded, distinctId, effectiveConsent } = useCtx();
174
+ const api = useApi();
175
+ const [survey, setSurvey] = useState(null);
176
+ const off = embedded || effectiveConsent === "off" || distinctId === "anonymous";
177
+ const persist = effectiveConsent === "cookies";
178
+ useEffect(() => {
179
+ if (off) return;
180
+ const ctl = new AbortController();
181
+ const timer = window.setTimeout(() => {
182
+ api.survey(window.location.href, ctl.signal).then((r) => {
183
+ const s = r.survey;
184
+ if (!s || s.questions.length === 0 || wasDismissed(s.id, persist)) return;
185
+ setSurvey(s);
186
+ }).catch(() => void 0);
187
+ }, 3e3);
188
+ return () => {
189
+ window.clearTimeout(timer);
190
+ ctl.abort();
191
+ };
192
+ }, [api, off, persist]);
193
+ const close = useCallback(() => setSurvey(null), []);
194
+ if (!survey || off) return null;
195
+ return /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx("div", { className: "ckw-survey", role: "dialog", "aria-label": survey.name, children: /* @__PURE__ */ jsx(Card, { survey, onClose: close }, survey.id) }) });
196
+ };
197
+ export {
198
+ Survey,
199
+ forgetStoredDismissals
200
+ };
201
+ //# sourceMappingURL=survey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/survey.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { type SurveyAnswer, type SurveyDefinition, type SurveyQuestion, useApi } from \"./api\";\nimport { useCtx } from \"./context\";\nimport { Check, Cross, Root, Star } from \"./ui\";\n\nconst DISMISS_PREFIX = \"ck_survey_\";\n/** Surveys dismissed or answered during this page load. Only \"cookies\" mode also keeps them in localStorage. */\nconst dismissed = new Set<string>();\n\nconst remember = (id: string, persist: boolean) => {\n dismissed.add(id);\n if (!persist) return;\n try {\n localStorage.setItem(`${DISMISS_PREFIX}${id}`, \"1\");\n } catch {\n // Private mode: the card just may show again next visit.\n }\n};\n\nconst wasDismissed = (id: string, persist: boolean) => {\n if (dismissed.has(id)) return true;\n if (!persist) return false;\n try {\n return !!localStorage.getItem(`${DISMISS_PREFIX}${id}`);\n } catch {\n return false;\n }\n};\n\n/** Consent for storage was withdrawn: move the stored dismissals into memory for this page load and delete them. */\nexport const forgetStoredDismissals = () => {\n try {\n for (let i = localStorage.length - 1; i >= 0; i--) {\n const key = localStorage.key(i);\n if (!key?.startsWith(DISMISS_PREFIX)) continue;\n dismissed.add(key.slice(DISMISS_PREFIX.length));\n localStorage.removeItem(key);\n }\n } catch {\n // No storage: nothing to delete.\n }\n};\n\nconst isAnswered = (a: SurveyAnswer | undefined) => (Array.isArray(a) ? a.length > 0 : typeof a === \"number\" ? true : !!a?.trim());\n\nconst Question = ({ q, value, onChange, onEnter }: { q: SurveyQuestion; value: SurveyAnswer | undefined; onChange: (v: SurveyAnswer) => void; onEnter: () => void }) => {\n if (q.kind === \"nps\" || q.kind === \"rating\") {\n const scale = q.kind === \"nps\" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);\n const start = q.kind === \"nps\" ? 0 : 1;\n const items = Array.from({ length: scale }, (_, i) => i + start);\n const stars = q.kind === \"rating\" && scale <= 5;\n return (\n <div>\n <div className=\"ckw-scale\" role=\"group\" aria-label={q.title}>\n {items.map((n) =>\n stars ? (\n <button key={n} type=\"button\" className=\"ckw-star\" data-on={typeof value === \"number\" && n <= value} aria-pressed={value === n} aria-label={`${n} of ${scale}`} onClick={() => onChange(n)}>\n <Star />\n </button>\n ) : (\n <button key={n} type=\"button\" className=\"ckw-num\" aria-pressed={value === n} onClick={() => onChange(n)}>\n {n}\n </button>\n ),\n )}\n </div>\n {(q.low || q.high || q.kind === \"nps\") && (\n <div className=\"ckw-lh\">\n <span>{q.low ?? (q.kind === \"nps\" ? \"Not likely\" : \"\")}</span>\n <span>{q.high ?? (q.kind === \"nps\" ? \"Very likely\" : \"\")}</span>\n </div>\n )}\n </div>\n );\n }\n if (q.kind === \"single\" || q.kind === \"multi\") {\n const multi = q.kind === \"multi\";\n const picked = multi ? ((value as string[] | undefined) ?? []) : [];\n return (\n <div className=\"ckw-opts\" role={multi ? \"group\" : \"radiogroup\"} aria-label={q.title}>\n {(q.options ?? []).map((o) => (\n <label className=\"ckw-opt\" key={o}>\n <input\n type={multi ? \"checkbox\" : \"radio\"}\n name={q.key}\n checked={multi ? picked.includes(o) : value === o}\n onChange={() => onChange(multi ? (picked.includes(o) ? picked.filter((x) => x !== o) : [...picked, o]) : o)}\n />\n <span>{o}</span>\n </label>\n ))}\n </div>\n );\n }\n return (\n <textarea\n className=\"ckw-textarea\"\n aria-label={q.title}\n placeholder={q.placeholder ?? \"Type your answer\"}\n value={(value as string | undefined) ?? \"\"}\n maxLength={2000}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => (e.metaKey || e.ctrlKey) && e.key === \"Enter\" && onEnter()}\n />\n );\n};\n\nconst Card = ({ survey, onClose }: { survey: SurveyDefinition; onClose: () => void }) => {\n const api = useApi();\n const persist = useCtx().effectiveConsent === \"cookies\";\n const [step, setStep] = useState(0);\n const [answers, setAnswers] = useState<Record<string, SurveyAnswer>>({});\n const [done, setDone] = useState(false);\n const [error, setError] = useState(\"\");\n const started = useRef(Date.now());\n const finished = useRef(false);\n const live = useRef(answers);\n live.current = answers;\n const heading = useRef<HTMLHeadingElement>(null);\n const q = survey.questions[step];\n const last = step === survey.questions.length - 1;\n const has = isAnswered(answers[q?.key]);\n\n useEffect(() => {\n api.seen(survey.id).catch(() => undefined);\n }, [api, survey.id]);\n useEffect(() => {\n heading.current?.focus();\n }, [step]);\n\n const finish = useCallback(\n async (completed: boolean) => {\n if (finished.current) return;\n finished.current = true;\n remember(survey.id, persist);\n api.respond(survey.id, { answers: live.current, completed, seconds: Math.round((Date.now() - started.current) / 1000) }).catch(() => undefined);\n },\n [api, survey.id, persist],\n );\n\n // After the thank-you screen the card closes by itself.\n useEffect(() => {\n if (!done) return;\n const t = window.setTimeout(onClose, 2500);\n return () => window.clearTimeout(t);\n }, [done, onClose]);\n\n const dismiss = useCallback(() => {\n // Partial answers still count, so nothing the visitor typed is lost.\n if (Object.values(live.current).some(isAnswered)) void finish(false);\n else remember(survey.id, persist);\n onClose();\n }, [finish, onClose, survey.id, persist]);\n\n useEffect(() => {\n const h = (e: KeyboardEvent) => e.key === \"Escape\" && !done && dismiss();\n document.addEventListener(\"keydown\", h);\n return () => document.removeEventListener(\"keydown\", h);\n }, [dismiss, done]);\n\n const next = async () => {\n if (q.required && !has) {\n setError(\"Please answer this question.\");\n return;\n }\n setError(\"\");\n if (!last) return setStep(step + 1);\n await finish(true);\n setDone(true);\n };\n\n if (done)\n return (\n <div className=\"ckw-thanks\" role=\"status\">\n <span className=\"ckw-tick\">\n <Check />\n </span>\n <b>{survey.thanks || \"Thanks for your feedback.\"}</b>\n </div>\n );\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void next();\n }}\n >\n <div className=\"ckw-sv-top\">\n <div className=\"ckw-dots\" role=\"img\" aria-label={`Question ${step + 1} of ${survey.questions.length}`}>\n {survey.questions.map((x, i) => (\n <span className=\"ckw-dot\" key={x.key} data-on={i <= step} />\n ))}\n </div>\n <button type=\"button\" className=\"ckw-btn ckw-btn-ghost\" onClick={dismiss} aria-label=\"Close survey\" style={{ height: 24, padding: \"0 4px\" }}>\n <Cross />\n </button>\n </div>\n <h3 className=\"ckw-q\" tabIndex={-1} ref={heading}>\n {q.title}\n {!q.required && <span className=\"ckw-time\"> optional</span>}\n </h3>\n {q.description && <p className=\"ckw-qd\">{q.description}</p>}\n <Question q={q} value={answers[q.key]} onChange={(v) => (setAnswers((a) => ({ ...a, [q.key]: v })), setError(\"\"))} onEnter={() => void next()} />\n <div aria-live=\"polite\">\n {error && (\n <p className=\"ckw-qd\" style={{ color: \"var(--ckw-danger)\", margin: \"8px 0 0\" }}>\n {error}\n </p>\n )}\n </div>\n <div className=\"ckw-sv-foot\">\n <span />\n <button type=\"submit\" className=\"ckw-btn ckw-btn-primary\">\n {last ? \"Send\" : \"Next\"}\n </button>\n </div>\n </form>\n );\n};\n\n/** Asks the visitor the survey Cockpit picked for this page. Mounted by CockpitProvider unless `surveys={false}`. */\nexport const Survey = () => {\n const { embedded, distinctId, effectiveConsent } = useCtx();\n const api = useApi();\n const [survey, setSurvey] = useState<SurveyDefinition | null>(null);\n // Never inside Cockpit previews, never without consent, and not before the visitor id is known.\n const off = embedded || effectiveConsent === \"off\" || distinctId === \"anonymous\";\n const persist = effectiveConsent === \"cookies\";\n\n useEffect(() => {\n if (off) return;\n const ctl = new AbortController();\n const timer = window.setTimeout(() => {\n api.survey(window.location.href, ctl.signal)\n .then((r) => {\n const s = r.survey;\n if (!s || s.questions.length === 0 || wasDismissed(s.id, persist)) return;\n setSurvey(s);\n })\n .catch(() => undefined);\n }, 3000);\n return () => {\n window.clearTimeout(timer);\n ctl.abort();\n };\n }, [api, off, persist]);\n\n const close = useCallback(() => setSurvey(null), []);\n if (!survey || off) return null;\n return (\n <Root>\n <div className=\"ckw-survey\" role=\"dialog\" aria-label={survey.name}>\n <Card key={survey.id} survey={survey} onClose={close} />\n </div>\n </Root>\n );\n};\n"],"mappings":"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","names":[]}
package/dist/ui.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from "react";
2
+ import { type FeedbackPost, type PostStatus } from "./api.js";
3
+ export type Theme = "light" | "dark" | "auto";
4
+ /** The scoped wrapper every widget renders inside. Carries the accent and the theme. */
5
+ export declare const Root: ({ theme, accent, className, children }: {
6
+ theme?: Theme;
7
+ accent?: string;
8
+ className?: string;
9
+ children: ReactNode;
10
+ }) => import("react").JSX.Element;
11
+ export declare const ChevronUp: () => import("react").JSX.Element;
12
+ export declare const Plus: () => import("react").JSX.Element;
13
+ export declare const Cross: () => import("react").JSX.Element;
14
+ export declare const Check: () => import("react").JSX.Element;
15
+ export declare const Chat: () => import("react").JSX.Element;
16
+ export declare const Star: () => import("react").JSX.Element;
17
+ export declare const StatusPill: ({ status }: {
18
+ status: PostStatus;
19
+ }) => import("react").JSX.Element;
20
+ export declare const VoteButton: ({ post, onVote, small }: {
21
+ post: FeedbackPost;
22
+ onVote: (p: FeedbackPost) => void;
23
+ small?: boolean;
24
+ }) => import("react").JSX.Element;
25
+ export declare const timeAgo: (iso: string) => string;
26
+ /** Calls `onClose` on Esc while mounted. */
27
+ export declare const useEscape: (onClose: () => void) => void;
28
+ /** Up to three existing posts that share words with what the visitor is typing. */
29
+ export declare const similarPosts: (title: string, posts: FeedbackPost[]) => FeedbackPost[];
package/dist/ui.js ADDED
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ // src/ui.tsx
4
+ import { useEffect, useRef } from "react";
5
+ import { STATUS_LABEL } from "./api.js";
6
+ import { useStyles } from "./styles.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var Root = ({ theme = "auto", accent = "#3f76ff", className, children }) => {
9
+ useStyles();
10
+ return /* @__PURE__ */ jsx("div", { className: `ckw-root${className ? ` ${className}` : ""}`, "data-theme": theme, style: { ["--ckw-accent"]: accent }, children });
11
+ };
12
+ var Svg = ({ children, size = 16 }) => /* @__PURE__ */ jsx("svg", { className: "ckw-icon", width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
13
+ var ChevronUp = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M3.5 10 8 5.5 12.5 10" }) });
14
+ var Plus = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M8 3v10M3 8h10" }) });
15
+ var Cross = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" }) });
16
+ var Check = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "m3 8.5 3.2 3.2L13 4.8" }) });
17
+ var Chat = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z" }) });
18
+ var Star = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "m12 2.8 2.8 5.8 6.4.9-4.6 4.5 1.1 6.3L12 17.2l-5.7 3.1 1.1-6.3L2.8 9.5l6.4-.9z" }) });
19
+ var StatusPill = ({ status }) => /* @__PURE__ */ jsx("span", { className: "ckw-pill", "data-s": status, children: STATUS_LABEL[status] ?? status });
20
+ var VoteButton = ({ post, onVote, small }) => /* @__PURE__ */ jsxs(
21
+ "button",
22
+ {
23
+ type: "button",
24
+ className: `ckw-vote${small ? " ckw-vote-sm" : ""}`,
25
+ "aria-pressed": post.voted,
26
+ "aria-label": `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
27
+ onClick: () => onVote(post),
28
+ children: [
29
+ /* @__PURE__ */ jsx(ChevronUp, {}),
30
+ /* @__PURE__ */ jsx("span", { children: post.votes })
31
+ ]
32
+ }
33
+ );
34
+ var timeAgo = (iso) => {
35
+ const t = new Date(iso).getTime();
36
+ if (Number.isNaN(t)) return "";
37
+ const d = Math.max(0, Date.now() - t) / 1e3;
38
+ if (d < 60) return "just now";
39
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
40
+ if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
41
+ if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;
42
+ return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
43
+ };
44
+ var useEscape = (onClose) => {
45
+ const ref = useRef(onClose);
46
+ ref.current = onClose;
47
+ useEffect(() => {
48
+ const h = (e) => e.key === "Escape" && ref.current();
49
+ document.addEventListener("keydown", h);
50
+ return () => document.removeEventListener("keydown", h);
51
+ }, []);
52
+ };
53
+ var words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !["the", "and", "for", "with", "add", "can", "please", "want", "would", "our", "you", "have"].includes(w));
54
+ var similarPosts = (title, posts) => {
55
+ const mine = words(title);
56
+ if (mine.length === 0 || title.trim().length < 4) return [];
57
+ return posts.map((p) => {
58
+ const theirs = new Set(words(`${p.title} ${p.body}`.slice(0, 300)));
59
+ const hit = mine.filter((w) => theirs.has(w) || [...theirs].some((t) => t.startsWith(w) || w.startsWith(t))).length;
60
+ return { p, score: hit / mine.length };
61
+ }).filter((x) => x.score >= 0.5).sort((a, b) => b.score - a.score || b.p.votes - a.p.votes).slice(0, 3).map((x) => x.p);
62
+ };
63
+ export {
64
+ Chat,
65
+ Check,
66
+ ChevronUp,
67
+ Cross,
68
+ Plus,
69
+ Root,
70
+ Star,
71
+ StatusPill,
72
+ VoteButton,
73
+ similarPosts,
74
+ timeAgo,
75
+ useEscape
76
+ };
77
+ //# sourceMappingURL=ui.js.map
package/dist/ui.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useEffect, useRef } from \"react\";\nimport { type FeedbackPost, type PostStatus, STATUS_LABEL } from \"./api\";\nimport { useStyles } from \"./styles\";\n\nexport type Theme = \"light\" | \"dark\" | \"auto\";\n\n/** The scoped wrapper every widget renders inside. Carries the accent and the theme. */\nexport const Root = ({ theme = \"auto\", accent = \"#3f76ff\", className, children }: { theme?: Theme; accent?: string; className?: string; children: ReactNode }) => {\n useStyles();\n return (\n <div className={`ckw-root${className ? ` ${className}` : \"\"}`} data-theme={theme} style={{ [\"--ckw-accent\" as string]: accent }}>\n {children}\n </div>\n );\n};\n\nconst Svg = ({ children, size = 16 }: { children: ReactNode; size?: number }) => (\n <svg className=\"ckw-icon\" width={size} height={size} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n {children}\n </svg>\n);\n\nexport const ChevronUp = () => (\n <Svg>\n <path d=\"M3.5 10 8 5.5 12.5 10\" />\n </Svg>\n);\nexport const Plus = () => (\n <Svg>\n <path d=\"M8 3v10M3 8h10\" />\n </Svg>\n);\nexport const Cross = () => (\n <Svg>\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </Svg>\n);\nexport const Check = () => (\n <Svg>\n <path d=\"m3 8.5 3.2 3.2L13 4.8\" />\n </Svg>\n);\nexport const Chat = () => (\n <Svg>\n <path d=\"M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z\" />\n </Svg>\n);\nexport const Star = () => (\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m12 2.8 2.8 5.8 6.4.9-4.6 4.5 1.1 6.3L12 17.2l-5.7 3.1 1.1-6.3L2.8 9.5l6.4-.9z\" />\n </svg>\n);\n\nexport const StatusPill = ({ status }: { status: PostStatus }) => (\n <span className=\"ckw-pill\" data-s={status}>\n {STATUS_LABEL[status] ?? status}\n </span>\n);\n\nexport const VoteButton = ({ post, onVote, small }: { post: FeedbackPost; onVote: (p: FeedbackPost) => void; small?: boolean }) => (\n <button\n type=\"button\"\n className={`ckw-vote${small ? \" ckw-vote-sm\" : \"\"}`}\n aria-pressed={post.voted}\n aria-label={`${post.voted ? \"Remove your vote from\" : \"Upvote\"} ${post.title}. ${post.votes} votes`}\n onClick={() => onVote(post)}\n >\n <ChevronUp />\n <span>{post.votes}</span>\n </button>\n);\n\nexport const timeAgo = (iso: string) => {\n const t = new Date(iso).getTime();\n if (Number.isNaN(t)) return \"\";\n const d = Math.max(0, Date.now() - t) / 1000;\n if (d < 60) return \"just now\";\n if (d < 3600) return `${Math.floor(d / 60)}m ago`;\n if (d < 86400) return `${Math.floor(d / 3600)}h ago`;\n if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;\n return new Date(iso).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\", year: \"numeric\" });\n};\n\n/** Calls `onClose` on Esc while mounted. */\nexport const useEscape = (onClose: () => void) => {\n const ref = useRef(onClose);\n ref.current = onClose;\n useEffect(() => {\n const h = (e: KeyboardEvent) => e.key === \"Escape\" && ref.current();\n document.addEventListener(\"keydown\", h);\n return () => document.removeEventListener(\"keydown\", h);\n }, []);\n};\n\n/** Words of a title, lowercased, without filler. */\nconst words = (s: string) =>\n s\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((w) => w.length > 2 && ![\"the\", \"and\", \"for\", \"with\", \"add\", \"can\", \"please\", \"want\", \"would\", \"our\", \"you\", \"have\"].includes(w));\n\n/** Up to three existing posts that share words with what the visitor is typing. */\nexport const similarPosts = (title: string, posts: FeedbackPost[]) => {\n const mine = words(title);\n if (mine.length === 0 || title.trim().length < 4) return [];\n return posts\n .map((p) => {\n const theirs = new Set(words(`${p.title} ${p.body}`.slice(0, 300)));\n const hit = mine.filter((w) => theirs.has(w) || [...theirs].some((t) => t.startsWith(w) || w.startsWith(t))).length;\n return { p, score: hit / mine.length };\n })\n .filter((x) => x.score >= 0.5)\n .sort((a, b) => b.score - a.score || b.p.votes - a.p.votes)\n .slice(0, 3)\n .map((x) => x.p);\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,97 @@
1
1
  {
2
- "name": "@mrrocket/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@mrrocket/sdk",
3
+ "version": "0.2.0",
4
+ "description": "MRRocket SDK: page views, events, heatmap clicks, session recordings, A/B tests, feedback board and surveys that your MRRocket agents can run and read. Next.js today (@mrrocket/sdk/next), more frameworks next.",
5
+ "keywords": [
6
+ "nextjs",
7
+ "analytics",
8
+ "ab-testing",
9
+ "heatmaps",
10
+ "session-replay",
11
+ "experiments",
12
+ "feedback",
13
+ "surveys",
14
+ "mrrocket",
15
+ "cloudflare-workers",
16
+ "react",
17
+ "sdk"
18
+ ],
19
+ "license": "MIT",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ "./next": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./next/server": {
29
+ "types": "./dist/server.d.ts",
30
+ "import": "./dist/server.js",
31
+ "default": "./dist/server.js"
32
+ },
33
+ "./next/proxy": {
34
+ "types": "./dist/proxy.d.ts",
35
+ "import": "./dist/proxy.js",
36
+ "default": "./dist/proxy.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "typesVersions": {
41
+ "*": {
42
+ "next": [
43
+ "./dist/index.d.ts"
44
+ ],
45
+ "next/server": [
46
+ "./dist/server.d.ts"
47
+ ],
48
+ "next/proxy": [
49
+ "./dist/proxy.d.ts"
50
+ ]
51
+ }
52
+ },
53
+ "files": [
54
+ "dist",
55
+ "README.md",
56
+ "LICENSE",
57
+ "CHANGELOG.md"
58
+ ],
59
+ "engines": {
60
+ "node": ">=18.18"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup && node scripts/build-types.mjs && node scripts/check-dist.mjs",
67
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "node --import tsx --test test/*.test.ts",
70
+ "check:dist": "node scripts/check-dist.mjs",
71
+ "check:publish": "node scripts/check-publish.mjs",
72
+ "prepublishOnly": "npm run check:publish && npm run typecheck && npm run build"
73
+ },
74
+ "dependencies": {
75
+ "@rrweb/record": "2.1.6"
76
+ },
77
+ "peerDependencies": {
78
+ "next": ">=15",
79
+ "react": ">=19"
80
+ },
81
+ "devDependencies": {
82
+ "@types/node": "^22",
83
+ "@types/react": "^19",
84
+ "next": "^16",
85
+ "react": "^19",
86
+ "react-dom": "^19",
87
+ "tsup": "^8.5.1",
88
+ "typescript": "^5.9"
89
+ },
90
+ "overrides": {
91
+ "rrweb": "2.1.6",
92
+ "rrdom": "2.1.6",
93
+ "rrweb-snapshot": "2.1.6",
94
+ "@rrweb/types": "2.1.6",
95
+ "@rrweb/utils": "2.1.6"
96
+ }
97
+ }