@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/feedback.tsx"],"sourcesContent":["\"use client\";\n\nimport { type FormEvent, useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport { type ChangelogEntry, type FeedbackBoardInfo, type FeedbackComment, type FeedbackPost, type FeedbackRoadmap, useApi } from \"./api\";\nimport { Chat, ChevronUp, Cross, Plus, Root, StatusPill, type Theme, VoteButton, similarPosts, timeAgo, useEscape } from \"./ui\";\n\n/** Loads the post list and owns optimistic voting, so the board and the widget behave the same. */\nconst useFeedbackList = (opts: { board?: string; sort?: \"top\" | \"new\"; q?: string; limit?: number }) => {\n const api = useApi();\n const [posts, setPosts] = useState<FeedbackPost[]>([]);\n const [boards, setBoards] = useState<FeedbackBoardInfo[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(\"\");\n const [tick, setTick] = useState(0);\n const { board, sort, q } = opts;\n\n useEffect(() => {\n const ctl = new AbortController();\n setLoading(true);\n const wait = window.setTimeout(\n () => {\n api.list({ board, sort, q: q?.trim() || undefined }, ctl.signal)\n .then((r) => {\n setPosts(r.posts);\n setBoards(r.boards);\n setError(\"\");\n setLoading(false);\n })\n .catch((e: Error) => {\n if (e.name === \"AbortError\") return;\n setError(e.message);\n setLoading(false);\n });\n },\n q ? 250 : 0,\n );\n return () => {\n window.clearTimeout(wait);\n ctl.abort();\n };\n }, [api, board, sort, q, tick]);\n\n const patch = useCallback((id: string, fn: (p: FeedbackPost) => FeedbackPost) => setPosts((all) => all.map((p) => (p.id === id ? fn(p) : p))), []);\n\n /** Flips the vote at once, then confirms with the server; on failure it flips back. */\n const vote = useCallback(\n (post: FeedbackPost) => {\n const flip = (p: FeedbackPost) => ({ ...p, voted: !p.voted, votes: p.votes + (p.voted ? -1 : 1) });\n patch(post.id, flip);\n api.vote(post.id)\n .then((r) => patch(post.id, (p) => ({ ...p, voted: r.voted, votes: r.votes })))\n .catch((e: Error) => {\n patch(post.id, flip);\n setError(`Your vote was not saved. ${e.message}`);\n });\n },\n [api, patch],\n );\n\n return { api, posts, setPosts, patch, boards, loading, error, setError, vote, reload: () => setTick((t) => t + 1) };\n};\n\n/** Title, details and board, with up to three similar requests to vote on instead. */\nconst SubmitForm = ({\n api,\n boards,\n posts,\n onCreated,\n onVote,\n onCancel,\n compact,\n}: {\n api: ReturnType<typeof useApi>;\n boards: FeedbackBoardInfo[];\n posts: FeedbackPost[];\n onCreated: (p: FeedbackPost) => void;\n onVote: (p: FeedbackPost) => void;\n onCancel?: () => void;\n compact?: boolean;\n}) => {\n const id = useId();\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [board, setBoard] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState(\"\");\n const chosen = board || boards[0]?.id || \"\";\n const dupes = similarPosts(title, posts);\n\n const submit = async (e: FormEvent) => {\n e.preventDefault();\n if (!title.trim() || busy) return;\n setBusy(true);\n setError(\"\");\n try {\n const created = await api.submit({ title: title.trim(), body: body.trim() || undefined, board: chosen });\n setTitle(\"\");\n setBody(\"\");\n onCreated(created);\n } catch (err) {\n setError((err as Error).message);\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <form className=\"ckw-form\" onSubmit={submit}>\n <div className=\"ckw-field\">\n <label className=\"ckw-label\" htmlFor={`${id}-t`}>\n Title\n </label>\n <input id={`${id}-t`} className=\"ckw-input\" value={title} maxLength={140} required autoFocus onChange={(e) => setTitle(e.target.value)} placeholder=\"Short, specific summary\" />\n </div>\n {dupes.length > 0 && (\n <div className=\"ckw-dupes\" role=\"status\">\n <p>Similar requests. Vote instead of adding a duplicate:</p>\n {dupes.map((d) => (\n <div className=\"ckw-dupe\" key={d.id}>\n <span title={d.title}>{d.title}</span>\n <button type=\"button\" className=\"ckw-btn\" disabled={d.voted} onClick={() => onVote(d)}>\n {d.voted ? \"Voted\" : \"Vote instead\"}\n </button>\n </div>\n ))}\n </div>\n )}\n {!compact && (\n <div className=\"ckw-field\">\n <label className=\"ckw-label\" htmlFor={`${id}-b`}>\n Details (optional)\n </label>\n <textarea id={`${id}-b`} className=\"ckw-textarea\" value={body} maxLength={2000} onChange={(e) => setBody(e.target.value)} placeholder=\"What are you trying to do?\" />\n </div>\n )}\n {boards.length > 1 && (\n <div className=\"ckw-field\">\n <label className=\"ckw-label\" htmlFor={`${id}-c`}>\n Board\n </label>\n <select id={`${id}-c`} className=\"ckw-select\" value={chosen} onChange={(e) => setBoard(e.target.value)}>\n {boards.map((b) => (\n <option key={b.id} value={b.id}>\n {b.label}\n </option>\n ))}\n </select>\n </div>\n )}\n {error && (\n <div className=\"ckw-error\" role=\"alert\">\n {error}\n </div>\n )}\n <div className=\"ckw-actions\">\n {onCancel && (\n <button type=\"button\" className=\"ckw-btn ckw-btn-ghost\" onClick={onCancel}>\n Cancel\n </button>\n )}\n <button type=\"submit\" className=\"ckw-btn ckw-btn-primary\" disabled={busy || !title.trim()}>\n {busy ? \"Sending…\" : \"Submit request\"}\n </button>\n </div>\n </form>\n );\n};\n\nconst PostRow = ({ post, onOpen, onVote }: { post: FeedbackPost; onOpen: (p: FeedbackPost) => void; onVote: (p: FeedbackPost) => void }) => (\n <li className=\"ckw-row\">\n <VoteButton post={post} onVote={onVote} />\n <div className=\"ckw-main\">\n <button type=\"button\" className=\"ckw-link\" onClick={() => onOpen(post)}>\n {post.title}\n </button>\n {post.body && <p className=\"ckw-excerpt\">{post.body}</p>}\n <div className=\"ckw-meta\">\n <StatusPill status={post.status} />\n <span className=\"ckw-stat\">\n <Chat /> {post.comments}\n </span>\n <span>{timeAgo(post.createdAt)}</span>\n </div>\n </div>\n </li>\n);\n\nconst Skeleton = () => (\n <div aria-hidden=\"true\">\n {[0, 1, 2].map((i) => (\n <div className=\"ckw-skel\" key={i} />\n ))}\n </div>\n);\n\nconst ErrorBar = ({ message, onRetry, onClose }: { message: string; onRetry?: () => void; onClose?: () => void }) => (\n <div className=\"ckw-error\" role=\"alert\">\n <span>{message}</span>\n {onRetry ? (\n <button type=\"button\" className=\"ckw-btn\" onClick={onRetry}>\n Retry\n </button>\n ) : (\n onClose && (\n <button type=\"button\" className=\"ckw-btn ckw-btn-ghost\" onClick={onClose} aria-label=\"Dismiss\">\n <Cross />\n </button>\n )\n )}\n </div>\n);\n\nconst PostDetail = ({ post, onBack, onVote }: { post: FeedbackPost; onBack: () => void; onVote: (p: FeedbackPost) => void }) => {\n const api = useApi();\n const id = useId();\n const [comments, setComments] = useState<FeedbackComment[] | null>(null);\n const [error, setError] = useState(\"\");\n const [text, setText] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [added, setAdded] = useState(0);\n\n useEffect(() => {\n const ctl = new AbortController();\n api.comments(post.id, ctl.signal)\n .then((r) => setComments(r.comments))\n .catch((e: Error) => e.name !== \"AbortError\" && setError(e.message));\n return () => ctl.abort();\n }, [api, post.id]);\n\n const send = async (e: FormEvent) => {\n e.preventDefault();\n const body = text.trim();\n if (!body || busy) return;\n setBusy(true);\n setError(\"\");\n // Show it at once; take it back if the server says no.\n const temp: FeedbackComment = { id: `tmp-${Date.now()}`, body, author: \"You\", isTeam: false, postedAt: new Date().toISOString() };\n setComments((c) => [...(c ?? []), temp]);\n setText(\"\");\n try {\n const real = await api.comment(post.id, body);\n setComments((c) => (c ?? []).map((x) => (x.id === temp.id ? real : x)));\n setAdded((n) => n + 1);\n } catch (err) {\n setComments((c) => (c ?? []).filter((x) => x.id !== temp.id));\n setText(body);\n setError((err as Error).message);\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <div>\n <button type=\"button\" className=\"ckw-btn ckw-btn-ghost ckw-back\" onClick={onBack}>\n <span style={{ display: \"inline-block\", transform: \"rotate(-90deg)\", lineHeight: 0 }}>\n <ChevronUp />\n </span>\n All requests\n </button>\n <div className=\"ckw-detail-head\">\n <VoteButton post={post} onVote={onVote} />\n <div className=\"ckw-main\">\n <h2>{post.title}</h2>\n <div className=\"ckw-meta\" style={{ marginTop: 0 }}>\n <StatusPill status={post.status} />\n <span>{timeAgo(post.createdAt)}</span>\n </div>\n </div>\n </div>\n {post.body && <p className=\"ckw-body\">{post.body}</p>}\n <h3 className=\"ckw-h\">Comments ({Math.max(post.comments + added, comments?.length ?? 0)})</h3>\n {error && <ErrorBar message={error} onClose={() => setError(\"\")} />}\n <div aria-live=\"polite\">\n {comments === null && !error && <Skeleton />}\n {comments?.length === 0 && <p className=\"ckw-sub\">No comments yet. Be the first.</p>}\n {comments?.map((c) => (\n <div className=\"ckw-comment\" key={c.id}>\n <b>{c.author}</b>\n {c.isTeam && <span className=\"ckw-team\">Team</span>}\n <span className=\"ckw-time\">{timeAgo(c.postedAt)}</span>\n <p>{c.body}</p>\n </div>\n ))}\n </div>\n <form className=\"ckw-form\" onSubmit={send} style={{ marginTop: 12 }}>\n <label className=\"ckw-label\" htmlFor={id}>\n Add a comment\n </label>\n <textarea id={id} className=\"ckw-textarea\" value={text} maxLength={2000} onChange={(e) => setText(e.target.value)} />\n <div className=\"ckw-actions\">\n <button type=\"submit\" className=\"ckw-btn ckw-btn-primary\" disabled={busy || !text.trim()}>\n Comment\n </button>\n </div>\n </form>\n </div>\n );\n};\n\ntype Tab = \"requests\" | \"roadmap\" | \"changelog\";\n\nconst Roadmap = ({ onOpen }: { onOpen: (p: FeedbackPost) => void }) => {\n const api = useApi();\n const [data, setData] = useState<FeedbackRoadmap | null>(null);\n const [error, setError] = useState(\"\");\n const [tick, setTick] = useState(0);\n useEffect(() => {\n const ctl = new AbortController();\n api.roadmap(ctl.signal)\n .then((r) => (setData(r), setError(\"\")))\n .catch((e: Error) => e.name !== \"AbortError\" && setError(e.message));\n return () => ctl.abort();\n }, [api, tick]);\n if (error) return <ErrorBar message={error} onRetry={() => setTick((t) => t + 1)} />;\n if (!data) return <Skeleton />;\n const cols: [keyof FeedbackRoadmap[\"columns\"], string][] = [\n [\"planned\", \"Planned\"],\n [\"progress\", \"In progress\"],\n [\"shipped\", \"Shipped\"],\n ];\n return (\n <div className=\"ckw-cols\">\n {cols.map(([k, label]) => (\n <section className=\"ckw-col\" key={k} aria-label={label}>\n <h3>\n {label} <span>{data.columns[k].length}</span>\n </h3>\n {data.columns[k].length === 0 && <p className=\"ckw-sub\">Nothing here yet.</p>}\n {data.columns[k].map((p) => (\n <div className=\"ckw-card\" key={p.id}>\n <button type=\"button\" className=\"ckw-link\" onClick={() => onOpen(p)}>\n {p.title}\n </button>\n <div className=\"ckw-meta\" style={{ marginTop: 0 }}>\n <span className=\"ckw-stat\">\n <ChevronUp /> {p.votes}\n </span>\n <span className=\"ckw-stat\">\n <Chat /> {p.comments}\n </span>\n </div>\n </div>\n ))}\n </section>\n ))}\n </div>\n );\n};\n\nconst Changelog = () => {\n const api = useApi();\n const [entries, setEntries] = useState<ChangelogEntry[] | null>(null);\n const [error, setError] = useState(\"\");\n const [tick, setTick] = useState(0);\n useEffect(() => {\n const ctl = new AbortController();\n api.changelog(ctl.signal)\n .then((r) => (setEntries(r.entries), setError(\"\")))\n .catch((e: Error) => e.name !== \"AbortError\" && setError(e.message));\n return () => ctl.abort();\n }, [api, tick]);\n if (error) return <ErrorBar message={error} onRetry={() => setTick((t) => t + 1)} />;\n if (!entries) return <Skeleton />;\n if (entries.length === 0) return <div className=\"ckw-empty\">No updates published yet.</div>;\n return (\n <div>\n {entries.map((e) => (\n <article className=\"ckw-entry\" key={e.id}>\n <time dateTime={e.publishedAt}>{new Date(e.publishedAt).toLocaleDateString(undefined, { month: \"long\", day: \"numeric\", year: \"numeric\" })}</time>\n <h3>{e.title}</h3>\n <p>{e.body}</p>\n </article>\n ))}\n </div>\n );\n};\n\nexport interface FeedbackBoardProps {\n /** Heading above the board. */\n title?: string;\n /** Line under the heading. */\n subtitle?: string;\n theme?: Theme;\n /** Accent color for buttons and votes. Default #3f76ff. */\n accent?: string;\n}\n\n/** The full board for a /feedback page: requests, roadmap and changelog, with voting, comments and a submit form. */\nexport const FeedbackBoard = ({ title = \"Feedback\", subtitle = \"Tell us what to build next and vote on what matters to you.\", theme, accent }: FeedbackBoardProps) => {\n const [tab, setTab] = useState<Tab>(\"requests\");\n const [sort, setSort] = useState<\"top\" | \"new\">(\"top\");\n const [board, setBoard] = useState<string | undefined>();\n const [q, setQ] = useState(\"\");\n const [creating, setCreating] = useState(false);\n const [opened, setOpened] = useState<FeedbackPost | null>(null);\n const list = useFeedbackList({ board, sort, q });\n const { posts, setPosts, vote } = list;\n\n // The open post follows the list's optimistic votes when it is in the list.\n const current = opened && (posts.find((p) => p.id === opened.id) ?? opened);\n const voteOpened = (p: FeedbackPost) => {\n if (!posts.some((x) => x.id === p.id)) {\n // Opened from the roadmap: it is not in this list, so vote through the list's server call and mirror it locally.\n setOpened((o) => (o ? { ...o, voted: !o.voted, votes: o.votes + (o.voted ? -1 : 1) } : o));\n list.api\n .vote(p.id)\n .then((r) => setOpened((o) => (o ? { ...o, voted: r.voted, votes: r.votes } : o)))\n .catch((e: Error) => {\n setOpened((o) => (o ? { ...o, voted: p.voted, votes: p.votes } : o));\n list.setError(`Your vote was not saved. ${e.message}`);\n });\n return;\n }\n vote(p);\n };\n\n const tabs: [Tab, string][] = [\n [\"requests\", \"Requests\"],\n [\"roadmap\", \"Roadmap\"],\n [\"changelog\", \"Changelog\"],\n ];\n const onTabKey = (e: React.KeyboardEvent) => {\n const i = tabs.findIndex(([k]) => k === tab);\n const n = e.key === \"ArrowRight\" ? i + 1 : e.key === \"ArrowLeft\" ? i - 1 : null;\n if (n === null) return;\n e.preventDefault();\n setTab(tabs[(n + tabs.length) % tabs.length][0]);\n setOpened(null);\n };\n\n return (\n <Root theme={theme} accent={accent} className=\"ckw-board\">\n <div className=\"ckw-head\">\n <div>\n <h2 className=\"ckw-title\">{title}</h2>\n {subtitle && <p className=\"ckw-sub\">{subtitle}</p>}\n </div>\n <button type=\"button\" className=\"ckw-btn ckw-btn-primary\" onClick={() => (setCreating((c) => !c), setOpened(null), setTab(\"requests\"))} aria-expanded={creating}>\n <Plus /> Request a feature\n </button>\n </div>\n\n <div className=\"ckw-tabs\" role=\"tablist\" aria-label=\"Feedback sections\" onKeyDown={onTabKey}>\n {tabs.map(([k, label]) => (\n <button\n key={k}\n type=\"button\"\n role=\"tab\"\n id={`ckw-tab-${k}`}\n aria-selected={tab === k}\n tabIndex={tab === k ? 0 : -1}\n className=\"ckw-tab\"\n onClick={() => (setTab(k), setOpened(null))}\n >\n {label}\n </button>\n ))}\n </div>\n\n <div role=\"tabpanel\" aria-labelledby={`ckw-tab-${tab}`}>\n {list.error && <ErrorBar message={list.error} onRetry={tab === \"requests\" && !current ? list.reload : undefined} onClose={() => list.setError(\"\")} />}\n\n {current ? (\n <PostDetail post={current} onBack={() => setOpened(null)} onVote={voteOpened} />\n ) : tab === \"requests\" ? (\n <>\n {creating && (\n <SubmitForm\n api={list.api}\n boards={list.boards}\n posts={posts}\n onVote={vote}\n onCancel={() => setCreating(false)}\n onCreated={(p) => {\n setPosts((all) => [p, ...all]);\n setCreating(false);\n }}\n />\n )}\n <div className=\"ckw-tools\">\n <input className=\"ckw-input ckw-search\" type=\"search\" aria-label=\"Search requests\" placeholder=\"Search requests\" value={q} onChange={(e) => setQ(e.target.value)} />\n <div className=\"ckw-seg\" role=\"group\" aria-label=\"Sort\">\n {([\"top\", \"new\"] as const).map((s) => (\n <button key={s} type=\"button\" aria-pressed={sort === s} onClick={() => setSort(s)}>\n {s === \"top\" ? \"Top\" : \"New\"}\n </button>\n ))}\n </div>\n </div>\n {list.boards.length > 1 && (\n <div className=\"ckw-chips\" role=\"group\" aria-label=\"Boards\">\n <button type=\"button\" className=\"ckw-chip\" aria-pressed={!board} onClick={() => setBoard(undefined)}>\n All\n </button>\n {list.boards.map((b) => (\n <button key={b.id} type=\"button\" className=\"ckw-chip\" aria-pressed={board === b.id} onClick={() => setBoard(b.id)}>\n {b.label}\n </button>\n ))}\n </div>\n )}\n <div aria-live=\"polite\" aria-busy={list.loading}>\n {list.loading && posts.length === 0 ? (\n <Skeleton />\n ) : posts.length === 0 && !list.error ? (\n <div className=\"ckw-empty\">{q ? \"No requests match your search.\" : \"No requests yet. Be the first to suggest something.\"}</div>\n ) : (\n <ul className=\"ckw-list\">\n {posts.map((p) => (\n <PostRow key={p.id} post={p} onOpen={setOpened} onVote={vote} />\n ))}\n </ul>\n )}\n </div>\n </>\n ) : tab === \"roadmap\" ? (\n <Roadmap onOpen={setOpened} />\n ) : (\n <Changelog />\n )}\n </div>\n </Root>\n );\n};\n\n// ---------- Floating widget ----------\n\nconst listeners = new Set<(open: boolean) => void>();\nlet widgetOpen = false;\nconst setWidgetOpen = (v: boolean) => {\n widgetOpen = v;\n listeners.forEach((l) => l(v));\n};\n\n/** Open or close the feedback widget from your own button. */\nexport const useFeedback = () => ({ open: () => setWidgetOpen(true), close: () => setWidgetOpen(false) });\n\nexport interface FeedbackWidgetProps {\n position?: \"bottom-right\" | \"bottom-left\";\n /** Text on the floating button. */\n label?: string;\n /** Where \"See all\" goes, for example your /feedback page. */\n boardUrl?: string;\n theme?: Theme;\n accent?: string;\n}\n\n/** The popover content. Mounted only while open, so a closed widget costs no request. */\nconst WidgetPanel = ({ boardUrl, onClose }: { boardUrl?: string; onClose: () => void }) => {\n const [suggesting, setSuggesting] = useState(false);\n const [thanks, setThanks] = useState(false);\n const list = useFeedbackList({ sort: \"top\" });\n const { posts, setPosts } = list;\n return (\n <>\n <div className=\"ckw-pop-head\">\n <span>{suggesting ? \"Suggest something\" : \"Top requests\"}</span>\n <button type=\"button\" className=\"ckw-btn ckw-btn-ghost\" onClick={onClose} aria-label=\"Close feedback\">\n <Cross />\n </button>\n </div>\n <div className=\"ckw-pop-body\">\n {list.error && <ErrorBar message={list.error} onClose={() => list.setError(\"\")} />}\n {thanks ? (\n <div className=\"ckw-empty\" role=\"status\">\n Thanks, your request is in.\n </div>\n ) : suggesting ? (\n <SubmitForm\n compact\n api={list.api}\n boards={list.boards}\n posts={posts}\n onVote={list.vote}\n onCancel={() => setSuggesting(false)}\n onCreated={(p) => {\n setPosts((all) => [p, ...all]);\n setSuggesting(false);\n setThanks(true);\n }}\n />\n ) : list.loading && posts.length === 0 ? (\n <Skeleton />\n ) : posts.length === 0 && !list.error ? (\n <div className=\"ckw-empty\">No requests yet. Suggest the first one.</div>\n ) : (\n <ul className=\"ckw-list\" aria-live=\"polite\">\n {posts.slice(0, 5).map((p) => (\n <li className=\"ckw-row\" key={p.id}>\n <VoteButton post={p} onVote={list.vote} />\n <div className=\"ckw-main\">\n <span style={{ fontWeight: 600, overflowWrap: \"anywhere\" }}>{p.title}</span>\n <div className=\"ckw-meta\">\n <StatusPill status={p.status} />\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n <div className=\"ckw-pop-foot\">\n {boardUrl ? <a href={boardUrl}>See all</a> : <span />}\n {!suggesting && !thanks && (\n <button type=\"button\" className=\"ckw-btn ckw-btn-primary\" onClick={() => setSuggesting(true)}>\n <Plus /> Suggest something\n </button>\n )}\n {thanks && (\n <button type=\"button\" className=\"ckw-btn\" onClick={() => setThanks(false)}>\n Back\n </button>\n )}\n </div>\n </>\n );\n};\n\n/** A floating button that opens a small popover with the top requests and a quick suggest form. */\nexport const FeedbackWidget = ({ position = \"bottom-right\", label = \"Feedback\", boardUrl, theme, accent }: FeedbackWidgetProps) => {\n const [open, setOpen] = useState(false);\n const fab = useRef<HTMLButtonElement>(null);\n const pop = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const l = (v: boolean) => setOpen(v);\n listeners.add(l);\n setOpen(widgetOpen);\n const click = (e: MouseEvent) => {\n if ((e.target as HTMLElement | null)?.closest(\"[data-ck-feedback]\")) {\n e.preventDefault();\n setWidgetOpen(true);\n }\n };\n document.addEventListener(\"click\", click);\n return () => {\n listeners.delete(l);\n document.removeEventListener(\"click\", click);\n };\n }, []);\n\n const close = useCallback(() => {\n setWidgetOpen(false);\n fab.current?.focus();\n }, []);\n useEscape(() => open && close());\n\n // Move focus into the popover when it opens.\n useEffect(() => {\n if (open) pop.current?.querySelector<HTMLElement>(\"button, input\")?.focus();\n }, [open]);\n\n // Keep Tab inside the popover while it is open.\n const trap = (e: React.KeyboardEvent) => {\n if (e.key !== \"Tab\" || !pop.current) return;\n const f = [...pop.current.querySelectorAll<HTMLElement>(\"a[href], button:not([disabled]), input, select, textarea\")];\n if (!f.length) return;\n const first = f[0];\n const last = f[f.length - 1];\n if (e.shiftKey && document.activeElement === first) (e.preventDefault(), last.focus());\n else if (!e.shiftKey && document.activeElement === last) (e.preventDefault(), first.focus());\n };\n\n return (\n <Root theme={theme} accent={accent}>\n <button ref={fab} type=\"button\" className=\"ckw-fab\" data-pos={position} aria-haspopup=\"dialog\" aria-expanded={open} onClick={() => (open ? close() : setWidgetOpen(true))}>\n <Chat /> {label}\n </button>\n {open && (\n <div ref={pop} className=\"ckw-pop\" data-pos={position} role=\"dialog\" aria-label=\"Feedback\" onKeyDown={trap}>\n <WidgetPanel boardUrl={boardUrl} onClose={close} />\n </div>\n )}\n </Root>\n );\n};\n"],"mappings":";;;AAEA,SAAyB,aAAa,WAAW,OAAO,QAAQ,gBAAgB;AAChF,SAAqH,cAAc;AACnI,SAAS,MAAM,WAAW,OAAO,MAAM,MAAM,YAAwB,YAAY,cAAc,SAAS,iBAAiB;AAwG7G,SAsWQ,UArWJ,KADJ;AArGZ,IAAM,kBAAkB,CAAC,SAA+E;AACpG,QAAM,MAAM,OAAO;AACnB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAyB,CAAC,CAAC;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8B,CAAC,CAAC;AAC5D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,EAAE,OAAO,MAAM,EAAE,IAAI;AAE3B,YAAU,MAAM;AACZ,UAAM,MAAM,IAAI,gBAAgB;AAChC,eAAW,IAAI;AACf,UAAM,OAAO,OAAO;AAAA,MAChB,MAAM;AACF,YAAI,KAAK,EAAE,OAAO,MAAM,GAAG,GAAG,KAAK,KAAK,OAAU,GAAG,IAAI,MAAM,EAC1D,KAAK,CAAC,MAAM;AACT,mBAAS,EAAE,KAAK;AAChB,oBAAU,EAAE,MAAM;AAClB,mBAAS,EAAE;AACX,qBAAW,KAAK;AAAA,QACpB,CAAC,EACA,MAAM,CAAC,MAAa;AACjB,cAAI,EAAE,SAAS,aAAc;AAC7B,mBAAS,EAAE,OAAO;AAClB,qBAAW,KAAK;AAAA,QACpB,CAAC;AAAA,MACT;AAAA,MACA,IAAI,MAAM;AAAA,IACd;AACA,WAAO,MAAM;AACT,aAAO,aAAa,IAAI;AACxB,UAAI,MAAM;AAAA,IACd;AAAA,EACJ,GAAG,CAAC,KAAK,OAAO,MAAM,GAAG,IAAI,CAAC;AAE9B,QAAM,QAAQ,YAAY,CAAC,IAAY,OAA0C,SAAS,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAO,EAAE,OAAO,KAAK,GAAG,CAAC,IAAI,CAAE,CAAC,GAAG,CAAC,CAAC;AAGjJ,QAAM,OAAO;AAAA,IACT,CAAC,SAAuB;AACpB,YAAM,OAAO,CAAC,OAAqB,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,OAAO,OAAO,EAAE,SAAS,EAAE,QAAQ,KAAK,GAAG;AAChG,YAAM,KAAK,IAAI,IAAI;AACnB,UAAI,KAAK,KAAK,EAAE,EACX,KAAK,CAAC,MAAM,MAAM,KAAK,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,EAAE,CAAC,EAC7E,MAAM,CAAC,MAAa;AACjB,cAAM,KAAK,IAAI,IAAI;AACnB,iBAAS,4BAA4B,EAAE,OAAO,EAAE;AAAA,MACpD,CAAC;AAAA,IACT;AAAA,IACA,CAAC,KAAK,KAAK;AAAA,EACf;AAEA,SAAO,EAAE,KAAK,OAAO,UAAU,OAAO,QAAQ,SAAS,OAAO,UAAU,MAAM,QAAQ,MAAM,QAAQ,CAAC,MAAM,IAAI,CAAC,EAAE;AACtH;AAGA,IAAM,aAAa,CAAC;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,MAQM;AACF,QAAM,KAAK,MAAM;AACjB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AACnC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,SAAS,SAAS,OAAO,CAAC,GAAG,MAAM;AACzC,QAAM,QAAQ,aAAa,OAAO,KAAK;AAEvC,QAAM,SAAS,OAAO,MAAiB;AACnC,MAAE,eAAe;AACjB,QAAI,CAAC,MAAM,KAAK,KAAK,KAAM;AAC3B,YAAQ,IAAI;AACZ,aAAS,EAAE;AACX,QAAI;AACA,YAAM,UAAU,MAAM,IAAI,OAAO,EAAE,OAAO,MAAM,KAAK,GAAG,MAAM,KAAK,KAAK,KAAK,QAAW,OAAO,OAAO,CAAC;AACvG,eAAS,EAAE;AACX,cAAQ,EAAE;AACV,gBAAU,OAAO;AAAA,IACrB,SAAS,KAAK;AACV,eAAU,IAAc,OAAO;AAAA,IACnC,UAAE;AACE,cAAQ,KAAK;AAAA,IACjB;AAAA,EACJ;AAEA,SACI,qBAAC,UAAK,WAAU,YAAW,UAAU,QACjC;AAAA,yBAAC,SAAI,WAAU,aACX;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,GAAG,EAAE,MAAM,mBAEjD;AAAA,MACA,oBAAC,WAAM,IAAI,GAAG,EAAE,MAAM,WAAU,aAAY,OAAO,OAAO,WAAW,KAAK,UAAQ,MAAC,WAAS,MAAC,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAAG,aAAY,2BAA0B;AAAA,OAClL;AAAA,IACC,MAAM,SAAS,KACZ,qBAAC,SAAI,WAAU,aAAY,MAAK,UAC5B;AAAA,0BAAC,OAAE,mEAAqD;AAAA,MACvD,MAAM,IAAI,CAAC,MACR,qBAAC,SAAI,WAAU,YACX;AAAA,4BAAC,UAAK,OAAO,EAAE,OAAQ,YAAE,OAAM;AAAA,QAC/B,oBAAC,YAAO,MAAK,UAAS,WAAU,WAAU,UAAU,EAAE,OAAO,SAAS,MAAM,OAAO,CAAC,GAC/E,YAAE,QAAQ,UAAU,gBACzB;AAAA,WAJ2B,EAAE,EAKjC,CACH;AAAA,OACL;AAAA,IAEH,CAAC,WACE,qBAAC,SAAI,WAAU,aACX;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,GAAG,EAAE,MAAM,gCAEjD;AAAA,MACA,oBAAC,cAAS,IAAI,GAAG,EAAE,MAAM,WAAU,gBAAe,OAAO,MAAM,WAAW,KAAM,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK,GAAG,aAAY,8BAA6B;AAAA,OACvK;AAAA,IAEH,OAAO,SAAS,KACb,qBAAC,SAAI,WAAU,aACX;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,GAAG,EAAE,MAAM,mBAEjD;AAAA,MACA,oBAAC,YAAO,IAAI,GAAG,EAAE,MAAM,WAAU,cAAa,OAAO,QAAQ,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAChG,iBAAO,IAAI,CAAC,MACT,oBAAC,YAAkB,OAAO,EAAE,IACvB,YAAE,SADM,EAAE,EAEf,CACH,GACL;AAAA,OACJ;AAAA,IAEH,SACG,oBAAC,SAAI,WAAU,aAAY,MAAK,SAC3B,iBACL;AAAA,IAEJ,qBAAC,SAAI,WAAU,eACV;AAAA,kBACG,oBAAC,YAAO,MAAK,UAAS,WAAU,yBAAwB,SAAS,UAAU,oBAE3E;AAAA,MAEJ,oBAAC,YAAO,MAAK,UAAS,WAAU,2BAA0B,UAAU,QAAQ,CAAC,MAAM,KAAK,GACnF,iBAAO,kBAAa,kBACzB;AAAA,OACJ;AAAA,KACJ;AAER;AAEA,IAAM,UAAU,CAAC,EAAE,MAAM,QAAQ,OAAO,MACpC,qBAAC,QAAG,WAAU,WACV;AAAA,sBAAC,cAAW,MAAY,QAAgB;AAAA,EACxC,qBAAC,SAAI,WAAU,YACX;AAAA,wBAAC,YAAO,MAAK,UAAS,WAAU,YAAW,SAAS,MAAM,OAAO,IAAI,GAChE,eAAK,OACV;AAAA,IACC,KAAK,QAAQ,oBAAC,OAAE,WAAU,eAAe,eAAK,MAAK;AAAA,IACpD,qBAAC,SAAI,WAAU,YACX;AAAA,0BAAC,cAAW,QAAQ,KAAK,QAAQ;AAAA,MACjC,qBAAC,UAAK,WAAU,YACZ;AAAA,4BAAC,QAAK;AAAA,QAAE;AAAA,QAAE,KAAK;AAAA,SACnB;AAAA,MACA,oBAAC,UAAM,kBAAQ,KAAK,SAAS,GAAE;AAAA,OACnC;AAAA,KACJ;AAAA,GACJ;AAGJ,IAAM,WAAW,MACb,oBAAC,SAAI,eAAY,QACZ,WAAC,GAAG,GAAG,CAAC,EAAE,IAAI,CAAC,MACZ,oBAAC,SAAI,WAAU,cAAgB,CAAG,CACrC,GACL;AAGJ,IAAM,WAAW,CAAC,EAAE,SAAS,SAAS,QAAQ,MAC1C,qBAAC,SAAI,WAAU,aAAY,MAAK,SAC5B;AAAA,sBAAC,UAAM,mBAAQ;AAAA,EACd,UACG,oBAAC,YAAO,MAAK,UAAS,WAAU,WAAU,SAAS,SAAS,mBAE5D,IAEA,WACI,oBAAC,YAAO,MAAK,UAAS,WAAU,yBAAwB,SAAS,SAAS,cAAW,WACjF,8BAAC,SAAM,GACX;AAAA,GAGZ;AAGJ,IAAM,aAAa,CAAC,EAAE,MAAM,QAAQ,OAAO,MAAqF;AAC5H,QAAM,MAAM,OAAO;AACnB,QAAM,KAAK,MAAM;AACjB,QAAM,CAAC,UAAU,WAAW,IAAI,SAAmC,IAAI;AACvE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AACnC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,CAAC;AAEpC,YAAU,MAAM;AACZ,UAAM,MAAM,IAAI,gBAAgB;AAChC,QAAI,SAAS,KAAK,IAAI,IAAI,MAAM,EAC3B,KAAK,CAAC,MAAM,YAAY,EAAE,QAAQ,CAAC,EACnC,MAAM,CAAC,MAAa,EAAE,SAAS,gBAAgB,SAAS,EAAE,OAAO,CAAC;AACvE,WAAO,MAAM,IAAI,MAAM;AAAA,EAC3B,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC;AAEjB,QAAM,OAAO,OAAO,MAAiB;AACjC,MAAE,eAAe;AACjB,UAAM,OAAO,KAAK,KAAK;AACvB,QAAI,CAAC,QAAQ,KAAM;AACnB,YAAQ,IAAI;AACZ,aAAS,EAAE;AAEX,UAAM,OAAwB,EAAE,IAAI,OAAO,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,OAAO,QAAQ,OAAO,WAAU,oBAAI,KAAK,GAAE,YAAY,EAAE;AAChI,gBAAY,CAAC,MAAM,CAAC,GAAI,KAAK,CAAC,GAAI,IAAI,CAAC;AACvC,YAAQ,EAAE;AACV,QAAI;AACA,YAAM,OAAO,MAAM,IAAI,QAAQ,KAAK,IAAI,IAAI;AAC5C,kBAAY,CAAC,OAAO,KAAK,CAAC,GAAG,IAAI,CAAC,MAAO,EAAE,OAAO,KAAK,KAAK,OAAO,CAAE,CAAC;AACtE,eAAS,CAAC,MAAM,IAAI,CAAC;AAAA,IACzB,SAAS,KAAK;AACV,kBAAY,CAAC,OAAO,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,OAAO,KAAK,EAAE,CAAC;AAC5D,cAAQ,IAAI;AACZ,eAAU,IAAc,OAAO;AAAA,IACnC,UAAE;AACE,cAAQ,KAAK;AAAA,IACjB;AAAA,EACJ;AAEA,SACI,qBAAC,SACG;AAAA,yBAAC,YAAO,MAAK,UAAS,WAAU,kCAAiC,SAAS,QACtE;AAAA,0BAAC,UAAK,OAAO,EAAE,SAAS,gBAAgB,WAAW,kBAAkB,YAAY,EAAE,GAC/E,8BAAC,aAAU,GACf;AAAA,MAAO;AAAA,OAEX;AAAA,IACA,qBAAC,SAAI,WAAU,mBACX;AAAA,0BAAC,cAAW,MAAY,QAAgB;AAAA,MACxC,qBAAC,SAAI,WAAU,YACX;AAAA,4BAAC,QAAI,eAAK,OAAM;AAAA,QAChB,qBAAC,SAAI,WAAU,YAAW,OAAO,EAAE,WAAW,EAAE,GAC5C;AAAA,8BAAC,cAAW,QAAQ,KAAK,QAAQ;AAAA,UACjC,oBAAC,UAAM,kBAAQ,KAAK,SAAS,GAAE;AAAA,WACnC;AAAA,SACJ;AAAA,OACJ;AAAA,IACC,KAAK,QAAQ,oBAAC,OAAE,WAAU,YAAY,eAAK,MAAK;AAAA,IACjD,qBAAC,QAAG,WAAU,SAAQ;AAAA;AAAA,MAAW,KAAK,IAAI,KAAK,WAAW,OAAO,UAAU,UAAU,CAAC;AAAA,MAAE;AAAA,OAAC;AAAA,IACxF,SAAS,oBAAC,YAAS,SAAS,OAAO,SAAS,MAAM,SAAS,EAAE,GAAG;AAAA,IACjE,qBAAC,SAAI,aAAU,UACV;AAAA,mBAAa,QAAQ,CAAC,SAAS,oBAAC,YAAS;AAAA,MACzC,UAAU,WAAW,KAAK,oBAAC,OAAE,WAAU,WAAU,4CAA8B;AAAA,MAC/E,UAAU,IAAI,CAAC,MACZ,qBAAC,SAAI,WAAU,eACX;AAAA,4BAAC,OAAG,YAAE,QAAO;AAAA,QACZ,EAAE,UAAU,oBAAC,UAAK,WAAU,YAAW,kBAAI;AAAA,QAC5C,oBAAC,UAAK,WAAU,YAAY,kBAAQ,EAAE,QAAQ,GAAE;AAAA,QAChD,oBAAC,OAAG,YAAE,MAAK;AAAA,WAJmB,EAAE,EAKpC,CACH;AAAA,OACL;AAAA,IACA,qBAAC,UAAK,WAAU,YAAW,UAAU,MAAM,OAAO,EAAE,WAAW,GAAG,GAC9D;AAAA,0BAAC,WAAM,WAAU,aAAY,SAAS,IAAI,2BAE1C;AAAA,MACA,oBAAC,cAAS,IAAQ,WAAU,gBAAe,OAAO,MAAM,WAAW,KAAM,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK,GAAG;AAAA,MACnH,oBAAC,SAAI,WAAU,eACX,8BAAC,YAAO,MAAK,UAAS,WAAU,2BAA0B,UAAU,QAAQ,CAAC,KAAK,KAAK,GAAG,qBAE1F,GACJ;AAAA,OACJ;AAAA,KACJ;AAER;AAIA,IAAM,UAAU,CAAC,EAAE,OAAO,MAA6C;AACnE,QAAM,MAAM,OAAO;AACnB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAiC,IAAI;AAC7D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,YAAU,MAAM;AACZ,UAAM,MAAM,IAAI,gBAAgB;AAChC,QAAI,QAAQ,IAAI,MAAM,EACjB,KAAK,CAAC,OAAO,QAAQ,CAAC,GAAG,SAAS,EAAE,EAAE,EACtC,MAAM,CAAC,MAAa,EAAE,SAAS,gBAAgB,SAAS,EAAE,OAAO,CAAC;AACvE,WAAO,MAAM,IAAI,MAAM;AAAA,EAC3B,GAAG,CAAC,KAAK,IAAI,CAAC;AACd,MAAI,MAAO,QAAO,oBAAC,YAAS,SAAS,OAAO,SAAS,MAAM,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG;AAClF,MAAI,CAAC,KAAM,QAAO,oBAAC,YAAS;AAC5B,QAAM,OAAqD;AAAA,IACvD,CAAC,WAAW,SAAS;AAAA,IACrB,CAAC,YAAY,aAAa;AAAA,IAC1B,CAAC,WAAW,SAAS;AAAA,EACzB;AACA,SACI,oBAAC,SAAI,WAAU,YACV,eAAK,IAAI,CAAC,CAAC,GAAG,KAAK,MAChB,qBAAC,aAAQ,WAAU,WAAkB,cAAY,OAC7C;AAAA,yBAAC,QACI;AAAA;AAAA,MAAM;AAAA,MAAC,oBAAC,UAAM,eAAK,QAAQ,CAAC,EAAE,QAAO;AAAA,OAC1C;AAAA,IACC,KAAK,QAAQ,CAAC,EAAE,WAAW,KAAK,oBAAC,OAAE,WAAU,WAAU,+BAAiB;AAAA,IACxE,KAAK,QAAQ,CAAC,EAAE,IAAI,CAAC,MAClB,qBAAC,SAAI,WAAU,YACX;AAAA,0BAAC,YAAO,MAAK,UAAS,WAAU,YAAW,SAAS,MAAM,OAAO,CAAC,GAC7D,YAAE,OACP;AAAA,MACA,qBAAC,SAAI,WAAU,YAAW,OAAO,EAAE,WAAW,EAAE,GAC5C;AAAA,6BAAC,UAAK,WAAU,YACZ;AAAA,8BAAC,aAAU;AAAA,UAAE;AAAA,UAAE,EAAE;AAAA,WACrB;AAAA,QACA,qBAAC,UAAK,WAAU,YACZ;AAAA,8BAAC,QAAK;AAAA,UAAE;AAAA,UAAE,EAAE;AAAA,WAChB;AAAA,SACJ;AAAA,SAX2B,EAAE,EAYjC,CACH;AAAA,OAnB6B,CAoBlC,CACH,GACL;AAER;AAEA,IAAM,YAAY,MAAM;AACpB,QAAM,MAAM,OAAO;AACnB,QAAM,CAAC,SAAS,UAAU,IAAI,SAAkC,IAAI;AACpE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,YAAU,MAAM;AACZ,UAAM,MAAM,IAAI,gBAAgB;AAChC,QAAI,UAAU,IAAI,MAAM,EACnB,KAAK,CAAC,OAAO,WAAW,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EACjD,MAAM,CAAC,MAAa,EAAE,SAAS,gBAAgB,SAAS,EAAE,OAAO,CAAC;AACvE,WAAO,MAAM,IAAI,MAAM;AAAA,EAC3B,GAAG,CAAC,KAAK,IAAI,CAAC;AACd,MAAI,MAAO,QAAO,oBAAC,YAAS,SAAS,OAAO,SAAS,MAAM,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG;AAClF,MAAI,CAAC,QAAS,QAAO,oBAAC,YAAS;AAC/B,MAAI,QAAQ,WAAW,EAAG,QAAO,oBAAC,SAAI,WAAU,aAAY,uCAAyB;AACrF,SACI,oBAAC,SACI,kBAAQ,IAAI,CAAC,MACV,qBAAC,aAAQ,WAAU,aACf;AAAA,wBAAC,UAAK,UAAU,EAAE,aAAc,cAAI,KAAK,EAAE,WAAW,EAAE,mBAAmB,QAAW,EAAE,OAAO,QAAQ,KAAK,WAAW,MAAM,UAAU,CAAC,GAAE;AAAA,IAC1I,oBAAC,QAAI,YAAE,OAAM;AAAA,IACb,oBAAC,OAAG,YAAE,MAAK;AAAA,OAHqB,EAAE,EAItC,CACH,GACL;AAER;AAaO,IAAM,gBAAgB,CAAC,EAAE,QAAQ,YAAY,WAAW,+DAA+D,OAAO,OAAO,MAA0B;AAClK,QAAM,CAAC,KAAK,MAAM,IAAI,SAAc,UAAU;AAC9C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwB,KAAK;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA6B;AACvD,QAAM,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE;AAC7B,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8B,IAAI;AAC9D,QAAM,OAAO,gBAAgB,EAAE,OAAO,MAAM,EAAE,CAAC;AAC/C,QAAM,EAAE,OAAO,UAAU,KAAK,IAAI;AAGlC,QAAM,UAAU,WAAW,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,OAAO,EAAE,KAAK;AACpE,QAAM,aAAa,CAAC,MAAoB;AACpC,QAAI,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,GAAG;AAEnC,gBAAU,CAAC,MAAO,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,OAAO,OAAO,EAAE,SAAS,EAAE,QAAQ,KAAK,GAAG,IAAI,CAAE;AACzF,WAAK,IACA,KAAK,EAAE,EAAE,EACT,KAAK,CAAC,MAAM,UAAU,CAAC,MAAO,IAAI,EAAE,GAAG,GAAG,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI,CAAE,CAAC,EAChF,MAAM,CAAC,MAAa;AACjB,kBAAU,CAAC,MAAO,IAAI,EAAE,GAAG,GAAG,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI,CAAE;AACnE,aAAK,SAAS,4BAA4B,EAAE,OAAO,EAAE;AAAA,MACzD,CAAC;AACL;AAAA,IACJ;AACA,SAAK,CAAC;AAAA,EACV;AAEA,QAAM,OAAwB;AAAA,IAC1B,CAAC,YAAY,UAAU;AAAA,IACvB,CAAC,WAAW,SAAS;AAAA,IACrB,CAAC,aAAa,WAAW;AAAA,EAC7B;AACA,QAAM,WAAW,CAAC,MAA2B;AACzC,UAAM,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,MAAM,GAAG;AAC3C,UAAM,IAAI,EAAE,QAAQ,eAAe,IAAI,IAAI,EAAE,QAAQ,cAAc,IAAI,IAAI;AAC3E,QAAI,MAAM,KAAM;AAChB,MAAE,eAAe;AACjB,WAAO,MAAM,IAAI,KAAK,UAAU,KAAK,MAAM,EAAE,CAAC,CAAC;AAC/C,cAAU,IAAI;AAAA,EAClB;AAEA,SACI,qBAAC,QAAK,OAAc,QAAgB,WAAU,aAC1C;AAAA,yBAAC,SAAI,WAAU,YACX;AAAA,2BAAC,SACG;AAAA,4BAAC,QAAG,WAAU,aAAa,iBAAM;AAAA,QAChC,YAAY,oBAAC,OAAE,WAAU,WAAW,oBAAS;AAAA,SAClD;AAAA,MACA,qBAAC,YAAO,MAAK,UAAS,WAAU,2BAA0B,SAAS,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,IAAI,GAAG,OAAO,UAAU,IAAI,iBAAe,UACnJ;AAAA,4BAAC,QAAK;AAAA,QAAE;AAAA,SACZ;AAAA,OACJ;AAAA,IAEA,oBAAC,SAAI,WAAU,YAAW,MAAK,WAAU,cAAW,qBAAoB,WAAW,UAC9E,eAAK,IAAI,CAAC,CAAC,GAAG,KAAK,MAChB;AAAA,MAAC;AAAA;AAAA,QAEG,MAAK;AAAA,QACL,MAAK;AAAA,QACL,IAAI,WAAW,CAAC;AAAA,QAChB,iBAAe,QAAQ;AAAA,QACvB,UAAU,QAAQ,IAAI,IAAI;AAAA,QAC1B,WAAU;AAAA,QACV,SAAS,OAAO,OAAO,CAAC,GAAG,UAAU,IAAI;AAAA,QAExC;AAAA;AAAA,MATI;AAAA,IAUT,CACH,GACL;AAAA,IAEA,qBAAC,SAAI,MAAK,YAAW,mBAAiB,WAAW,GAAG,IAC/C;AAAA,WAAK,SAAS,oBAAC,YAAS,SAAS,KAAK,OAAO,SAAS,QAAQ,cAAc,CAAC,UAAU,KAAK,SAAS,QAAW,SAAS,MAAM,KAAK,SAAS,EAAE,GAAG;AAAA,MAElJ,UACG,oBAAC,cAAW,MAAM,SAAS,QAAQ,MAAM,UAAU,IAAI,GAAG,QAAQ,YAAY,IAC9E,QAAQ,aACR,iCACK;AAAA,oBACG;AAAA,UAAC;AAAA;AAAA,YACG,KAAK,KAAK;AAAA,YACV,QAAQ,KAAK;AAAA,YACb;AAAA,YACA,QAAQ;AAAA,YACR,UAAU,MAAM,YAAY,KAAK;AAAA,YACjC,WAAW,CAAC,MAAM;AACd,uBAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,GAAG,CAAC;AAC7B,0BAAY,KAAK;AAAA,YACrB;AAAA;AAAA,QACJ;AAAA,QAEJ,qBAAC,SAAI,WAAU,aACX;AAAA,8BAAC,WAAM,WAAU,wBAAuB,MAAK,UAAS,cAAW,mBAAkB,aAAY,mBAAkB,OAAO,GAAG,UAAU,CAAC,MAAM,KAAK,EAAE,OAAO,KAAK,GAAG;AAAA,UAClK,oBAAC,SAAI,WAAU,WAAU,MAAK,SAAQ,cAAW,QAC3C,WAAC,OAAO,KAAK,EAAY,IAAI,CAAC,MAC5B,oBAAC,YAAe,MAAK,UAAS,gBAAc,SAAS,GAAG,SAAS,MAAM,QAAQ,CAAC,GAC3E,gBAAM,QAAQ,QAAQ,SADd,CAEb,CACH,GACL;AAAA,WACJ;AAAA,QACC,KAAK,OAAO,SAAS,KAClB,qBAAC,SAAI,WAAU,aAAY,MAAK,SAAQ,cAAW,UAC/C;AAAA,8BAAC,YAAO,MAAK,UAAS,WAAU,YAAW,gBAAc,CAAC,OAAO,SAAS,MAAM,SAAS,MAAS,GAAG,iBAErG;AAAA,UACC,KAAK,OAAO,IAAI,CAAC,MACd,oBAAC,YAAkB,MAAK,UAAS,WAAU,YAAW,gBAAc,UAAU,EAAE,IAAI,SAAS,MAAM,SAAS,EAAE,EAAE,GAC3G,YAAE,SADM,EAAE,EAEf,CACH;AAAA,WACL;AAAA,QAEJ,oBAAC,SAAI,aAAU,UAAS,aAAW,KAAK,SACnC,eAAK,WAAW,MAAM,WAAW,IAC9B,oBAAC,YAAS,IACV,MAAM,WAAW,KAAK,CAAC,KAAK,QAC5B,oBAAC,SAAI,WAAU,aAAa,cAAI,mCAAmC,uDAAsD,IAEzH,oBAAC,QAAG,WAAU,YACT,gBAAM,IAAI,CAAC,MACR,oBAAC,WAAmB,MAAM,GAAG,QAAQ,WAAW,QAAQ,QAA1C,EAAE,EAA8C,CACjE,GACL,GAER;AAAA,SACJ,IACA,QAAQ,YACR,oBAAC,WAAQ,QAAQ,WAAW,IAE5B,oBAAC,aAAU;AAAA,OAEnB;AAAA,KACJ;AAER;AAIA,IAAM,YAAY,oBAAI,IAA6B;AACnD,IAAI,aAAa;AACjB,IAAM,gBAAgB,CAAC,MAAe;AAClC,eAAa;AACb,YAAU,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AACjC;AAGO,IAAM,cAAc,OAAO,EAAE,MAAM,MAAM,cAAc,IAAI,GAAG,OAAO,MAAM,cAAc,KAAK,EAAE;AAavG,IAAM,cAAc,CAAC,EAAE,UAAU,QAAQ,MAAkD;AACvF,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,OAAO,gBAAgB,EAAE,MAAM,MAAM,CAAC;AAC5C,QAAM,EAAE,OAAO,SAAS,IAAI;AAC5B,SACI,iCACI;AAAA,yBAAC,SAAI,WAAU,gBACX;AAAA,0BAAC,UAAM,uBAAa,sBAAsB,gBAAe;AAAA,MACzD,oBAAC,YAAO,MAAK,UAAS,WAAU,yBAAwB,SAAS,SAAS,cAAW,kBACjF,8BAAC,SAAM,GACX;AAAA,OACJ;AAAA,IACA,qBAAC,SAAI,WAAU,gBACV;AAAA,WAAK,SAAS,oBAAC,YAAS,SAAS,KAAK,OAAO,SAAS,MAAM,KAAK,SAAS,EAAE,GAAG;AAAA,MAC/E,SACG,oBAAC,SAAI,WAAU,aAAY,MAAK,UAAS,yCAEzC,IACA,aACA;AAAA,QAAC;AAAA;AAAA,UACG,SAAO;AAAA,UACP,KAAK,KAAK;AAAA,UACV,QAAQ,KAAK;AAAA,UACb;AAAA,UACA,QAAQ,KAAK;AAAA,UACb,UAAU,MAAM,cAAc,KAAK;AAAA,UACnC,WAAW,CAAC,MAAM;AACd,qBAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,GAAG,CAAC;AAC7B,0BAAc,KAAK;AACnB,sBAAU,IAAI;AAAA,UAClB;AAAA;AAAA,MACJ,IACA,KAAK,WAAW,MAAM,WAAW,IACjC,oBAAC,YAAS,IACV,MAAM,WAAW,KAAK,CAAC,KAAK,QAC5B,oBAAC,SAAI,WAAU,aAAY,qDAAuC,IAElE,oBAAC,QAAG,WAAU,YAAW,aAAU,UAC9B,gBAAM,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,MACpB,qBAAC,QAAG,WAAU,WACV;AAAA,4BAAC,cAAW,MAAM,GAAG,QAAQ,KAAK,MAAM;AAAA,QACxC,qBAAC,SAAI,WAAU,YACX;AAAA,8BAAC,UAAK,OAAO,EAAE,YAAY,KAAK,cAAc,WAAW,GAAI,YAAE,OAAM;AAAA,UACrE,oBAAC,SAAI,WAAU,YACX,8BAAC,cAAW,QAAQ,EAAE,QAAQ,GAClC;AAAA,WACJ;AAAA,WAPyB,EAAE,EAQ/B,CACH,GACL;AAAA,OAER;AAAA,IACA,qBAAC,SAAI,WAAU,gBACV;AAAA,iBAAW,oBAAC,OAAE,MAAM,UAAU,qBAAO,IAAO,oBAAC,UAAK;AAAA,MAClD,CAAC,cAAc,CAAC,UACb,qBAAC,YAAO,MAAK,UAAS,WAAU,2BAA0B,SAAS,MAAM,cAAc,IAAI,GACvF;AAAA,4BAAC,QAAK;AAAA,QAAE;AAAA,SACZ;AAAA,MAEH,UACG,oBAAC,YAAO,MAAK,UAAS,WAAU,WAAU,SAAS,MAAM,UAAU,KAAK,GAAG,kBAE3E;AAAA,OAER;AAAA,KACJ;AAER;AAGO,IAAM,iBAAiB,CAAC,EAAE,WAAW,gBAAgB,QAAQ,YAAY,UAAU,OAAO,OAAO,MAA2B;AAC/H,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,MAAM,OAA0B,IAAI;AAC1C,QAAM,MAAM,OAAuB,IAAI;AAEvC,YAAU,MAAM;AACZ,UAAM,IAAI,CAAC,MAAe,QAAQ,CAAC;AACnC,cAAU,IAAI,CAAC;AACf,YAAQ,UAAU;AAClB,UAAM,QAAQ,CAAC,MAAkB;AAC7B,UAAK,EAAE,QAA+B,QAAQ,oBAAoB,GAAG;AACjE,UAAE,eAAe;AACjB,sBAAc,IAAI;AAAA,MACtB;AAAA,IACJ;AACA,aAAS,iBAAiB,SAAS,KAAK;AACxC,WAAO,MAAM;AACT,gBAAU,OAAO,CAAC;AAClB,eAAS,oBAAoB,SAAS,KAAK;AAAA,IAC/C;AAAA,EACJ,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,YAAY,MAAM;AAC5B,kBAAc,KAAK;AACnB,QAAI,SAAS,MAAM;AAAA,EACvB,GAAG,CAAC,CAAC;AACL,YAAU,MAAM,QAAQ,MAAM,CAAC;AAG/B,YAAU,MAAM;AACZ,QAAI,KAAM,KAAI,SAAS,cAA2B,eAAe,GAAG,MAAM;AAAA,EAC9E,GAAG,CAAC,IAAI,CAAC;AAGT,QAAM,OAAO,CAAC,MAA2B;AACrC,QAAI,EAAE,QAAQ,SAAS,CAAC,IAAI,QAAS;AACrC,UAAM,IAAI,CAAC,GAAG,IAAI,QAAQ,iBAA8B,0DAA0D,CAAC;AACnH,QAAI,CAAC,EAAE,OAAQ;AACf,UAAM,QAAQ,EAAE,CAAC;AACjB,UAAM,OAAO,EAAE,EAAE,SAAS,CAAC;AAC3B,QAAI,EAAE,YAAY,SAAS,kBAAkB,MAAO,CAAC,EAAE,eAAe,GAAG,KAAK,MAAM;AAAA,aAC3E,CAAC,EAAE,YAAY,SAAS,kBAAkB,KAAM,CAAC,EAAE,eAAe,GAAG,MAAM,MAAM;AAAA,EAC9F;AAEA,SACI,qBAAC,QAAK,OAAc,QAChB;AAAA,yBAAC,YAAO,KAAK,KAAK,MAAK,UAAS,WAAU,WAAU,YAAU,UAAU,iBAAc,UAAS,iBAAe,MAAM,SAAS,MAAO,OAAO,MAAM,IAAI,cAAc,IAAI,GACnK;AAAA,0BAAC,QAAK;AAAA,MAAE;AAAA,MAAE;AAAA,OACd;AAAA,IACC,QACG,oBAAC,SAAI,KAAK,KAAK,WAAU,WAAU,YAAU,UAAU,MAAK,UAAS,cAAW,YAAW,WAAW,MAClG,8BAAC,eAAY,UAAoB,SAAS,OAAO,GACrD;AAAA,KAER;AAER;","names":[]}
@@ -0,0 +1,3 @@
1
+ export { type ChangelogEntry, type CockpitProviderProps, type CockpitUser, type FeedbackBoardInfo, type FeedbackBoardProps, type FeedbackComment, type FeedbackList, type FeedbackPost, type FeedbackRoadmap, type FeedbackWidgetProps, type PostStatus, type SurveyAnswer, type SurveyDefinition, type SurveyQuestion, type SurveyQuestionKind, CockpitProvider, Experiment, FeedbackBoard, FeedbackWidget, Survey, Text, WIDGET_CSS, createApi, useCockpit, useFeedback, useStyles, useVariant, } from "./browser.js";
2
+ export { type CockpitConfig, type CockpitEvent, type ConsentMode, type ExperimentConfig, type VariantConfig, type VariantId, EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, SESSION_COOKIE, assign, parsePreview, previewHref, } from "./core.js";
3
+ export { type AttributionMode, type CheckoutReferral, type Referral, REFERRAL_COOKIE, getReferral, readReferralCookie, referralForCheckout, referralLink, } from "./referral.js";
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ // src/index.ts
2
+ import {
3
+ CockpitProvider,
4
+ Experiment,
5
+ FeedbackBoard,
6
+ FeedbackWidget,
7
+ Survey,
8
+ Text,
9
+ WIDGET_CSS,
10
+ createApi,
11
+ useCockpit,
12
+ useFeedback,
13
+ useStyles,
14
+ useVariant
15
+ } from "./browser.js";
16
+ import {
17
+ EMBED_PARAM,
18
+ ID_COOKIE,
19
+ PREVIEW_PARAM,
20
+ SESSION_COOKIE,
21
+ assign,
22
+ parsePreview,
23
+ previewHref
24
+ } from "./core.js";
25
+ import {
26
+ REFERRAL_COOKIE,
27
+ getReferral,
28
+ readReferralCookie,
29
+ referralForCheckout,
30
+ referralLink
31
+ } from "./referral.js";
32
+ export {
33
+ CockpitProvider,
34
+ EMBED_PARAM,
35
+ Experiment,
36
+ FeedbackBoard,
37
+ FeedbackWidget,
38
+ ID_COOKIE,
39
+ PREVIEW_PARAM,
40
+ REFERRAL_COOKIE,
41
+ SESSION_COOKIE,
42
+ Survey,
43
+ Text,
44
+ WIDGET_CSS,
45
+ assign,
46
+ createApi,
47
+ getReferral,
48
+ parsePreview,
49
+ previewHref,
50
+ readReferralCookie,
51
+ referralForCheckout,
52
+ referralLink,
53
+ useCockpit,
54
+ useFeedback,
55
+ useStyles,
56
+ useVariant
57
+ };
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n type ChangelogEntry,\n type CockpitProviderProps,\n type CockpitUser,\n type FeedbackBoardInfo,\n type FeedbackBoardProps,\n type FeedbackComment,\n type FeedbackList,\n type FeedbackPost,\n type FeedbackRoadmap,\n type FeedbackWidgetProps,\n type PostStatus,\n type SurveyAnswer,\n type SurveyDefinition,\n type SurveyQuestion,\n type SurveyQuestionKind,\n CockpitProvider,\n Experiment,\n FeedbackBoard,\n FeedbackWidget,\n Survey,\n Text,\n WIDGET_CSS,\n createApi,\n useCockpit,\n useFeedback,\n useStyles,\n useVariant,\n} from \"./browser\";\nexport {\n type CockpitConfig,\n type CockpitEvent,\n type ConsentMode,\n type ExperimentConfig,\n type VariantConfig,\n type VariantId,\n EMBED_PARAM,\n ID_COOKIE,\n PREVIEW_PARAM,\n SESSION_COOKIE,\n assign,\n parsePreview,\n previewHref,\n} from \"./core\";\nexport {\n type AttributionMode,\n type CheckoutReferral,\n type Referral,\n REFERRAL_COOKIE,\n getReferral,\n readReferralCookie,\n referralForCheckout,\n referralLink,\n} from \"./referral\";\n"],"mappings":";AAAA;AAAA,EAgBI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EAOI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EAII;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;","names":[]}
@@ -0,0 +1,28 @@
1
+ /** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */
2
+ interface IncomingCookies {
3
+ get(name: string): {
4
+ value: string;
5
+ } | undefined;
6
+ set(name: string, value: string): unknown;
7
+ }
8
+ /** What `response.cookies` (Next's `ResponseCookies`) offers: set with options. */
9
+ interface OutgoingCookies {
10
+ set(name: string, value: string, options?: {
11
+ path?: string;
12
+ maxAge?: number;
13
+ sameSite?: "lax" | "strict" | "none";
14
+ }): unknown;
15
+ }
16
+ /**
17
+ * For `proxy.ts`: gives a first-time visitor their `ck_id` before the page renders, so the server already assigns
18
+ * the variant they will keep. It writes a cookie, so only use it with `consent="cookies"` and only once the visitor
19
+ * agreed; in the default cookieless mode leave it out.
20
+ *
21
+ * export function proxy(request: NextRequest) {
22
+ * const response = NextResponse.next();
23
+ * withCockpitId(request.cookies, response.cookies);
24
+ * return response;
25
+ * }
26
+ */
27
+ export declare const withCockpitId: (incoming: IncomingCookies, outgoing: OutgoingCookies) => void;
28
+ export {};
package/dist/proxy.js ADDED
@@ -0,0 +1,12 @@
1
+ // src/proxy.ts
2
+ import { ID_COOKIE, newDistinctId } from "./core.js";
3
+ var withCockpitId = (incoming, outgoing) => {
4
+ if (incoming.get(ID_COOKIE)) return;
5
+ const id = newDistinctId();
6
+ incoming.set(ID_COOKIE, id);
7
+ outgoing.set(ID_COOKIE, id, { path: "/", maxAge: 60 * 60 * 24 * 365, sameSite: "lax" });
8
+ };
9
+ export {
10
+ withCockpitId
11
+ };
12
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/proxy.ts"],"sourcesContent":["import { ID_COOKIE, newDistinctId } from \"./core\";\n\n/** What `request.cookies` (Next's `RequestCookies`) offers: read, and set so that the page of this request sees the id. */\ninterface IncomingCookies {\n get(name: string): { value: string } | undefined;\n set(name: string, value: string): unknown;\n}\n\n/** What `response.cookies` (Next's `ResponseCookies`) offers: set with options. */\ninterface OutgoingCookies {\n set(name: string, value: string, options?: { path?: string; maxAge?: number; sameSite?: \"lax\" | \"strict\" | \"none\" }): unknown;\n}\n\n/**\n * For `proxy.ts`: gives a first-time visitor their `ck_id` before the page renders, so the server already assigns\n * the variant they will keep. It writes a cookie, so only use it with `consent=\"cookies\"` and only once the visitor\n * agreed; in the default cookieless mode leave it out.\n *\n * export function proxy(request: NextRequest) {\n * const response = NextResponse.next();\n * withCockpitId(request.cookies, response.cookies);\n * return response;\n * }\n */\nexport const withCockpitId = (incoming: IncomingCookies, outgoing: OutgoingCookies) => {\n if (incoming.get(ID_COOKIE)) return;\n const id = newDistinctId();\n incoming.set(ID_COOKIE, id);\n outgoing.set(ID_COOKIE, id, { path: \"/\", maxAge: 60 * 60 * 24 * 365, sameSite: \"lax\" });\n};\n"],"mappings":";AAAA,SAAS,WAAW,qBAAqB;AAwBlC,IAAM,gBAAgB,CAAC,UAA2B,aAA8B;AACnF,MAAI,SAAS,IAAI,SAAS,EAAG;AAC7B,QAAM,KAAK,cAAc;AACzB,WAAS,IAAI,WAAW,EAAE;AAC1B,WAAS,IAAI,WAAW,IAAI,EAAE,MAAM,KAAK,QAAQ,KAAK,KAAK,KAAK,KAAK,UAAU,MAAM,CAAC;AAC1F;","names":[]}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Affiliate referrals, after Rewardful's and Dub's client scripts. A partner's tracked link (`<mrrocket>/r/<code>`)
3
+ * counts the click on MRRocket and sends the visitor to your site with `?mrr_ref=<code>&mrr_click=<clickId>`; a direct
4
+ * link is `https://your.site/?ref=<code>`. The provider reads either on page load, keeps the referral for the program's
5
+ * cookie days in a FIRST-PARTY cookie on your domain (`mrr_ref`) plus localStorage, and sends a click beacon for direct
6
+ * links. MRRocket never sets a cookie for you: its domain is not yours.
7
+ *
8
+ * Storage follows consent (see the README): with `attribution="consent"` (default) the referral is only stored when
9
+ * the provider's consent is "cookies"; otherwise it lives in memory for the page load. `attribution="always"` stores
10
+ * it whatever the consent (your legal call), `"off"` ignores referrals.
11
+ *
12
+ * Plain functions, no React: `getReferral()` and `referralForCheckout()` work in any client code, and on the server
13
+ * with the cookie's value (`readReferralCookie`).
14
+ */
15
+ export interface Referral {
16
+ /** The partner's code. */
17
+ code: string;
18
+ /** The click MRRocket counted (tracked link or beacon); null when it was not counted. */
19
+ clickId: string | null;
20
+ /** When the visitor arrived through the partner, ms. */
21
+ at: number;
22
+ }
23
+ /** What to pass to Stripe Checkout (`stripe.checkout.sessions.create({ ..., ...referralForCheckout() })`). */
24
+ export interface CheckoutReferral {
25
+ client_reference_id: string;
26
+ metadata: {
27
+ mrr_ref: string;
28
+ mrr_click: string;
29
+ };
30
+ }
31
+ export type AttributionMode = "consent" | "always" | "off";
32
+ export declare const REFERRAL_COOKIE = "mrr_ref";
33
+ export declare const REFERRAL_STORAGE_KEY = "mrr_ref";
34
+ export declare const REF_PARAM = "mrr_ref";
35
+ export declare const CLICK_PARAM = "mrr_click";
36
+ export declare const DEFAULT_REFERRAL_DAYS = 60;
37
+ /** The referral in the landing URL: the tracked link's markers first, else the direct-link parameter (`ref`). */
38
+ export declare const readReferralParams: (search: string, param?: string) => {
39
+ code: string;
40
+ clickId: string | null;
41
+ direct: boolean;
42
+ } | null;
43
+ /** `code.clickId.at`: dots never occur in a code or a click id. */
44
+ export declare const encodeReferral: (r: Referral) => string;
45
+ /** A stored referral, or null when it is malformed or older than `maxDays`. */
46
+ export declare const decodeReferral: (value: string | null | undefined, maxDays?: number, now?: number) => Referral | null;
47
+ /** The referral for a Stripe Checkout Session: `client_reference_id` and `metadata`. Null without a referral. */
48
+ export declare const referralForCheckout: (r?: Referral | null) => CheckoutReferral | null;
49
+ /**
50
+ * A Stripe Payment Link (or Pricing Table URL) with the referral as `client_reference_id`. Payment Links only accept
51
+ * letters, digits, `-` and `_` there, so the form is `mrr_ref__<code>__<clickId>`. Without a referral the URL is unchanged.
52
+ */
53
+ export declare const referralLink: (url: string, r?: Referral | null) => string;
54
+ /** The visitor's referral: this page load's, else the stored one. Null on the server (use `readReferralCookie` there). */
55
+ export declare const getReferral: () => Referral | null;
56
+ /** The referral from the `mrr_ref` cookie's value, for server code (a route that creates the Checkout Session). */
57
+ export declare const readReferralCookie: (value: string | null | undefined) => Referral | null;
58
+ /** Writes the referral to the first-party cookie and localStorage for `days`. */
59
+ export declare const storeReferral: (r: Referral, days: number) => void;
60
+ /** Deletes what was stored (consent withdrawn); the referral of this page load stays in memory. */
61
+ export declare const forgetStoredReferral: () => void;
62
+ /** For tests and `consent="off"`: forget the page load's referral too. */
63
+ export declare const resetReferral: () => void;
64
+ export interface CaptureOptions {
65
+ /** `location.search` of the landing page. */
66
+ search: string;
67
+ /** The direct-link parameter, default `ref`. */
68
+ param?: string;
69
+ /** May the referral be stored (consent)? */
70
+ store: boolean;
71
+ /** Days to keep it. */
72
+ days: number;
73
+ /** Sends the click beacon; answers the click id MRRocket counted (or confirmed). */
74
+ beacon?: (body: {
75
+ code: string;
76
+ clickId: string | null;
77
+ }) => Promise<string | null>;
78
+ now?: number;
79
+ }
80
+ /**
81
+ * Reads the referral of a page load: a new one from the URL (sent to the beacon and stored when allowed) or the stored
82
+ * one. A new link replaces an older referral (last click within the window wins). Returns the referral in effect.
83
+ */
84
+ export declare const captureReferral: (o: CaptureOptions) => Promise<Referral | null>;
85
+ /** Events that carry the referral: identify, signup and your own events (not page views or exposures). */
86
+ export declare const carriesReferral: (event: string) => boolean;
@@ -0,0 +1,128 @@
1
+ // src/referral.ts
2
+ var REFERRAL_COOKIE = "mrr_ref";
3
+ var REFERRAL_STORAGE_KEY = "mrr_ref";
4
+ var REF_PARAM = "mrr_ref";
5
+ var CLICK_PARAM = "mrr_click";
6
+ var DEFAULT_REFERRAL_DAYS = 60;
7
+ var CODE = /^[A-Za-z0-9_-]{1,64}$/;
8
+ var DAY_MS = 864e5;
9
+ var readReferralParams = (search, param = "ref") => {
10
+ const q = new URLSearchParams(search);
11
+ const marked = q.get(REF_PARAM);
12
+ if (marked && CODE.test(marked)) {
13
+ const click = q.get(CLICK_PARAM);
14
+ return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };
15
+ }
16
+ const direct = q.get(param);
17
+ return direct && CODE.test(direct) ? { code: direct, clickId: null, direct: true } : null;
18
+ };
19
+ var encodeReferral = (r) => `${r.code}.${r.clickId ?? ""}.${r.at}`;
20
+ var decodeReferral = (value, maxDays = DEFAULT_REFERRAL_DAYS, now = Date.now()) => {
21
+ if (!value) return null;
22
+ const [code, click, at] = value.split(".");
23
+ const t = Number(at);
24
+ if (!code || !CODE.test(code) || !Number.isFinite(t) || click && !CODE.test(click)) return null;
25
+ if (now - t > maxDays * DAY_MS || t > now + DAY_MS) return null;
26
+ return { code, clickId: click || null, at: t };
27
+ };
28
+ var referralForCheckout = (r = getReferral()) => r ? { client_reference_id: `mrr_ref:${r.code}:${r.clickId ?? ""}`, metadata: { mrr_ref: r.code, mrr_click: r.clickId ?? "" } } : null;
29
+ var referralLink = (url, r = getReferral()) => {
30
+ if (!r) return url;
31
+ try {
32
+ const u = new URL(url);
33
+ u.searchParams.set("client_reference_id", `mrr_ref__${r.code}__${r.clickId ?? ""}`);
34
+ return u.toString();
35
+ } catch {
36
+ return url;
37
+ }
38
+ };
39
+ var current = null;
40
+ var hasDocument = () => typeof document !== "undefined";
41
+ var readCookie = (name) => {
42
+ if (!hasDocument()) return void 0;
43
+ const raw = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`))?.slice(name.length + 1);
44
+ try {
45
+ return raw ? decodeURIComponent(raw) : void 0;
46
+ } catch {
47
+ return raw;
48
+ }
49
+ };
50
+ var storage = () => {
51
+ try {
52
+ return typeof localStorage === "undefined" ? null : localStorage;
53
+ } catch {
54
+ return null;
55
+ }
56
+ };
57
+ var getReferral = () => {
58
+ if (current) return current;
59
+ if (!hasDocument()) return null;
60
+ return decodeReferral(readCookie(REFERRAL_COOKIE), 3650) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), 3650);
61
+ };
62
+ var readReferralCookie = (value) => decodeReferral(value, 3650);
63
+ var storeReferral = (r, days) => {
64
+ if (!hasDocument()) return;
65
+ const maxAge = Math.max(1, Math.round(days)) * 86400;
66
+ const secure = typeof location !== "undefined" && location.protocol === "https:" ? "; secure" : "";
67
+ document.cookie = `${REFERRAL_COOKIE}=${encodeURIComponent(encodeReferral(r))}; path=/; max-age=${maxAge}; samesite=lax${secure}`;
68
+ try {
69
+ storage()?.setItem(REFERRAL_STORAGE_KEY, encodeReferral(r));
70
+ } catch {
71
+ }
72
+ };
73
+ var forgetStoredReferral = () => {
74
+ if (!hasDocument()) return;
75
+ document.cookie = `${REFERRAL_COOKIE}=; path=/; max-age=0; samesite=lax`;
76
+ try {
77
+ storage()?.removeItem(REFERRAL_STORAGE_KEY);
78
+ } catch {
79
+ }
80
+ };
81
+ var resetReferral = () => {
82
+ current = null;
83
+ };
84
+ var captureReferral = async (o) => {
85
+ const found = readReferralParams(o.search, o.param);
86
+ const now = o.now ?? Date.now();
87
+ if (!found) {
88
+ if (!current && o.store) {
89
+ current = decodeReferral(readCookie(REFERRAL_COOKIE), o.days, now) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), o.days, now);
90
+ if (!current) forgetStoredReferral();
91
+ }
92
+ return current;
93
+ }
94
+ current = { code: found.code, clickId: found.clickId, at: now };
95
+ if (o.store) storeReferral(current, o.days);
96
+ if (o.beacon) {
97
+ try {
98
+ const id = await o.beacon({ code: found.code, clickId: found.clickId });
99
+ if (id && current && current.code === found.code && !current.clickId) {
100
+ current = { ...current, clickId: id };
101
+ if (o.store) storeReferral(current, o.days);
102
+ }
103
+ } catch {
104
+ }
105
+ }
106
+ return current;
107
+ };
108
+ var carriesReferral = (event) => event === "$identify" || !event.startsWith("$");
109
+ export {
110
+ CLICK_PARAM,
111
+ DEFAULT_REFERRAL_DAYS,
112
+ REFERRAL_COOKIE,
113
+ REFERRAL_STORAGE_KEY,
114
+ REF_PARAM,
115
+ captureReferral,
116
+ carriesReferral,
117
+ decodeReferral,
118
+ encodeReferral,
119
+ forgetStoredReferral,
120
+ getReferral,
121
+ readReferralCookie,
122
+ readReferralParams,
123
+ referralForCheckout,
124
+ referralLink,
125
+ resetReferral,
126
+ storeReferral
127
+ };
128
+ //# sourceMappingURL=referral.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/referral.ts"],"sourcesContent":["/**\n * Affiliate referrals, after Rewardful's and Dub's client scripts. A partner's tracked link (`<mrrocket>/r/<code>`)\n * counts the click on MRRocket and sends the visitor to your site with `?mrr_ref=<code>&mrr_click=<clickId>`; a direct\n * link is `https://your.site/?ref=<code>`. The provider reads either on page load, keeps the referral for the program's\n * cookie days in a FIRST-PARTY cookie on your domain (`mrr_ref`) plus localStorage, and sends a click beacon for direct\n * links. MRRocket never sets a cookie for you: its domain is not yours.\n *\n * Storage follows consent (see the README): with `attribution=\"consent\"` (default) the referral is only stored when\n * the provider's consent is \"cookies\"; otherwise it lives in memory for the page load. `attribution=\"always\"` stores\n * it whatever the consent (your legal call), `\"off\"` ignores referrals.\n *\n * Plain functions, no React: `getReferral()` and `referralForCheckout()` work in any client code, and on the server\n * with the cookie's value (`readReferralCookie`).\n */\n\nexport interface Referral {\n /** The partner's code. */\n code: string;\n /** The click MRRocket counted (tracked link or beacon); null when it was not counted. */\n clickId: string | null;\n /** When the visitor arrived through the partner, ms. */\n at: number;\n}\n\n/** What to pass to Stripe Checkout (`stripe.checkout.sessions.create({ ..., ...referralForCheckout() })`). */\nexport interface CheckoutReferral {\n client_reference_id: string;\n metadata: { mrr_ref: string; mrr_click: string };\n}\n\nexport type AttributionMode = \"consent\" | \"always\" | \"off\";\n\nexport const REFERRAL_COOKIE = \"mrr_ref\";\nexport const REFERRAL_STORAGE_KEY = \"mrr_ref\";\nexport const REF_PARAM = \"mrr_ref\";\nexport const CLICK_PARAM = \"mrr_click\";\nexport const DEFAULT_REFERRAL_DAYS = 60;\n\nconst CODE = /^[A-Za-z0-9_-]{1,64}$/;\nconst DAY_MS = 86_400_000;\n\n/** The referral in the landing URL: the tracked link's markers first, else the direct-link parameter (`ref`). */\nexport const readReferralParams = (search: string, param = \"ref\"): { code: string; clickId: string | null; direct: boolean } | null => {\n const q = new URLSearchParams(search);\n const marked = q.get(REF_PARAM);\n if (marked && CODE.test(marked)) {\n const click = q.get(CLICK_PARAM);\n return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };\n }\n const direct = q.get(param);\n return direct && CODE.test(direct) ? { code: direct, clickId: null, direct: true } : null;\n};\n\n/** `code.clickId.at`: dots never occur in a code or a click id. */\nexport const encodeReferral = (r: Referral) => `${r.code}.${r.clickId ?? \"\"}.${r.at}`;\n\n/** A stored referral, or null when it is malformed or older than `maxDays`. */\nexport const decodeReferral = (value: string | null | undefined, maxDays = DEFAULT_REFERRAL_DAYS, now = Date.now()): Referral | null => {\n if (!value) return null;\n const [code, click, at] = value.split(\".\");\n const t = Number(at);\n if (!code || !CODE.test(code) || !Number.isFinite(t) || (click && !CODE.test(click))) return null;\n if (now - t > maxDays * DAY_MS || t > now + DAY_MS) return null;\n return { code, clickId: click || null, at: t };\n};\n\n/** The referral for a Stripe Checkout Session: `client_reference_id` and `metadata`. Null without a referral. */\nexport const referralForCheckout = (r: Referral | null = getReferral()): CheckoutReferral | null =>\n r ? { client_reference_id: `mrr_ref:${r.code}:${r.clickId ?? \"\"}`, metadata: { mrr_ref: r.code, mrr_click: r.clickId ?? \"\" } } : null;\n\n/**\n * A Stripe Payment Link (or Pricing Table URL) with the referral as `client_reference_id`. Payment Links only accept\n * letters, digits, `-` and `_` there, so the form is `mrr_ref__<code>__<clickId>`. Without a referral the URL is unchanged.\n */\nexport const referralLink = (url: string, r: Referral | null = getReferral()) => {\n if (!r) return url;\n try {\n const u = new URL(url);\n u.searchParams.set(\"client_reference_id\", `mrr_ref__${r.code}__${r.clickId ?? \"\"}`);\n return u.toString();\n } catch {\n return url;\n }\n};\n\n// ---------- The browser side ----------\n\nlet current: Referral | null = null;\n\nconst hasDocument = () => typeof document !== \"undefined\";\n\nconst readCookie = (name: string) => {\n if (!hasDocument()) return undefined;\n const raw = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`))\n ?.slice(name.length + 1);\n try {\n return raw ? decodeURIComponent(raw) : undefined;\n } catch {\n return raw;\n }\n};\n\nconst storage = () => {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage;\n } catch {\n return null;\n }\n};\n\n/** The visitor's referral: this page load's, else the stored one. Null on the server (use `readReferralCookie` there). */\nexport const getReferral = (): Referral | null => {\n if (current) return current;\n if (!hasDocument()) return null;\n return decodeReferral(readCookie(REFERRAL_COOKIE), 3650) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), 3650);\n};\n\n/** The referral from the `mrr_ref` cookie's value, for server code (a route that creates the Checkout Session). */\nexport const readReferralCookie = (value: string | null | undefined) => decodeReferral(value, 3650);\n\n/** Writes the referral to the first-party cookie and localStorage for `days`. */\nexport const storeReferral = (r: Referral, days: number) => {\n if (!hasDocument()) return;\n const maxAge = Math.max(1, Math.round(days)) * 86_400;\n const secure = typeof location !== \"undefined\" && location.protocol === \"https:\" ? \"; secure\" : \"\";\n document.cookie = `${REFERRAL_COOKIE}=${encodeURIComponent(encodeReferral(r))}; path=/; max-age=${maxAge}; samesite=lax${secure}`;\n try {\n storage()?.setItem(REFERRAL_STORAGE_KEY, encodeReferral(r));\n } catch {\n // Storage full or blocked: the cookie is enough.\n }\n};\n\n/** Deletes what was stored (consent withdrawn); the referral of this page load stays in memory. */\nexport const forgetStoredReferral = () => {\n if (!hasDocument()) return;\n document.cookie = `${REFERRAL_COOKIE}=; path=/; max-age=0; samesite=lax`;\n try {\n storage()?.removeItem(REFERRAL_STORAGE_KEY);\n } catch {\n // Nothing stored.\n }\n};\n\n/** For tests and `consent=\"off\"`: forget the page load's referral too. */\nexport const resetReferral = () => {\n current = null;\n};\n\nexport interface CaptureOptions {\n /** `location.search` of the landing page. */\n search: string;\n /** The direct-link parameter, default `ref`. */\n param?: string;\n /** May the referral be stored (consent)? */\n store: boolean;\n /** Days to keep it. */\n days: number;\n /** Sends the click beacon; answers the click id MRRocket counted (or confirmed). */\n beacon?: (body: { code: string; clickId: string | null }) => Promise<string | null>;\n now?: number;\n}\n\n/**\n * Reads the referral of a page load: a new one from the URL (sent to the beacon and stored when allowed) or the stored\n * one. A new link replaces an older referral (last click within the window wins). Returns the referral in effect.\n */\nexport const captureReferral = async (o: CaptureOptions): Promise<Referral | null> => {\n const found = readReferralParams(o.search, o.param);\n const now = o.now ?? Date.now();\n if (!found) {\n if (!current && o.store) {\n current = decodeReferral(readCookie(REFERRAL_COOKIE), o.days, now) ?? decodeReferral(storage()?.getItem(REFERRAL_STORAGE_KEY), o.days, now);\n if (!current) forgetStoredReferral();\n }\n return current;\n }\n current = { code: found.code, clickId: found.clickId, at: now };\n if (o.store) storeReferral(current, o.days);\n if (o.beacon) {\n try {\n const id = await o.beacon({ code: found.code, clickId: found.clickId });\n if (id && current && current.code === found.code && !current.clickId) {\n current = { ...current, clickId: id };\n if (o.store) storeReferral(current, o.days);\n }\n } catch {\n // The referral still counts for checkout without a click id.\n }\n }\n return current;\n};\n\n/** Events that carry the referral: identify, signup and your own events (not page views or exposures). */\nexport const carriesReferral = (event: string) => event === \"$identify\" || !event.startsWith(\"$\");\n"],"mappings":";AAgCO,IAAM,kBAAkB;AACxB,IAAM,uBAAuB;AAC7B,IAAM,YAAY;AAClB,IAAM,cAAc;AACpB,IAAM,wBAAwB;AAErC,IAAM,OAAO;AACb,IAAM,SAAS;AAGR,IAAM,qBAAqB,CAAC,QAAgB,QAAQ,UAA4E;AACnI,QAAM,IAAI,IAAI,gBAAgB,MAAM;AACpC,QAAM,SAAS,EAAE,IAAI,SAAS;AAC9B,MAAI,UAAU,KAAK,KAAK,MAAM,GAAG;AAC7B,UAAM,QAAQ,EAAE,IAAI,WAAW;AAC/B,WAAO,EAAE,MAAM,QAAQ,SAAS,SAAS,KAAK,KAAK,KAAK,IAAI,QAAQ,MAAM,QAAQ,MAAM;AAAA,EAC5F;AACA,QAAM,SAAS,EAAE,IAAI,KAAK;AAC1B,SAAO,UAAU,KAAK,KAAK,MAAM,IAAI,EAAE,MAAM,QAAQ,SAAS,MAAM,QAAQ,KAAK,IAAI;AACzF;AAGO,IAAM,iBAAiB,CAAC,MAAgB,GAAG,EAAE,IAAI,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE;AAG5E,IAAM,iBAAiB,CAAC,OAAkC,UAAU,uBAAuB,MAAM,KAAK,IAAI,MAAuB;AACpI,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,CAAC,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM,GAAG;AACzC,QAAM,IAAI,OAAO,EAAE;AACnB,MAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,OAAO,SAAS,CAAC,KAAM,SAAS,CAAC,KAAK,KAAK,KAAK,EAAI,QAAO;AAC7F,MAAI,MAAM,IAAI,UAAU,UAAU,IAAI,MAAM,OAAQ,QAAO;AAC3D,SAAO,EAAE,MAAM,SAAS,SAAS,MAAM,IAAI,EAAE;AACjD;AAGO,IAAM,sBAAsB,CAAC,IAAqB,YAAY,MACjE,IAAI,EAAE,qBAAqB,WAAW,EAAE,IAAI,IAAI,EAAE,WAAW,EAAE,IAAI,UAAU,EAAE,SAAS,EAAE,MAAM,WAAW,EAAE,WAAW,GAAG,EAAE,IAAI;AAM9H,IAAM,eAAe,CAAC,KAAa,IAAqB,YAAY,MAAM;AAC7E,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACA,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,MAAE,aAAa,IAAI,uBAAuB,YAAY,EAAE,IAAI,KAAK,EAAE,WAAW,EAAE,EAAE;AAClF,WAAO,EAAE,SAAS;AAAA,EACtB,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAIA,IAAI,UAA2B;AAE/B,IAAM,cAAc,MAAM,OAAO,aAAa;AAE9C,IAAM,aAAa,CAAC,SAAiB;AACjC,MAAI,CAAC,YAAY,EAAG,QAAO;AAC3B,QAAM,MAAM,SAAS,OAChB,MAAM,IAAI,EACV,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,CAAC,GACnC,MAAM,KAAK,SAAS,CAAC;AAC3B,MAAI;AACA,WAAO,MAAM,mBAAmB,GAAG,IAAI;AAAA,EAC3C,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAEA,IAAM,UAAU,MAAM;AAClB,MAAI;AACA,WAAO,OAAO,iBAAiB,cAAc,OAAO;AAAA,EACxD,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAGO,IAAM,cAAc,MAAuB;AAC9C,MAAI,QAAS,QAAO;AACpB,MAAI,CAAC,YAAY,EAAG,QAAO;AAC3B,SAAO,eAAe,WAAW,eAAe,GAAG,IAAI,KAAK,eAAe,QAAQ,GAAG,QAAQ,oBAAoB,GAAG,IAAI;AAC7H;AAGO,IAAM,qBAAqB,CAAC,UAAqC,eAAe,OAAO,IAAI;AAG3F,IAAM,gBAAgB,CAAC,GAAa,SAAiB;AACxD,MAAI,CAAC,YAAY,EAAG;AACpB,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,IAAI;AAC/C,QAAM,SAAS,OAAO,aAAa,eAAe,SAAS,aAAa,WAAW,aAAa;AAChG,WAAS,SAAS,GAAG,eAAe,IAAI,mBAAmB,eAAe,CAAC,CAAC,CAAC,qBAAqB,MAAM,iBAAiB,MAAM;AAC/H,MAAI;AACA,YAAQ,GAAG,QAAQ,sBAAsB,eAAe,CAAC,CAAC;AAAA,EAC9D,QAAQ;AAAA,EAER;AACJ;AAGO,IAAM,uBAAuB,MAAM;AACtC,MAAI,CAAC,YAAY,EAAG;AACpB,WAAS,SAAS,GAAG,eAAe;AACpC,MAAI;AACA,YAAQ,GAAG,WAAW,oBAAoB;AAAA,EAC9C,QAAQ;AAAA,EAER;AACJ;AAGO,IAAM,gBAAgB,MAAM;AAC/B,YAAU;AACd;AAoBO,IAAM,kBAAkB,OAAO,MAAgD;AAClF,QAAM,QAAQ,mBAAmB,EAAE,QAAQ,EAAE,KAAK;AAClD,QAAM,MAAM,EAAE,OAAO,KAAK,IAAI;AAC9B,MAAI,CAAC,OAAO;AACR,QAAI,CAAC,WAAW,EAAE,OAAO;AACrB,gBAAU,eAAe,WAAW,eAAe,GAAG,EAAE,MAAM,GAAG,KAAK,eAAe,QAAQ,GAAG,QAAQ,oBAAoB,GAAG,EAAE,MAAM,GAAG;AAC1I,UAAI,CAAC,QAAS,sBAAqB;AAAA,IACvC;AACA,WAAO;AAAA,EACX;AACA,YAAU,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,SAAS,IAAI,IAAI;AAC9D,MAAI,EAAE,MAAO,eAAc,SAAS,EAAE,IAAI;AAC1C,MAAI,EAAE,QAAQ;AACV,QAAI;AACA,YAAM,KAAK,MAAM,EAAE,OAAO,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,QAAQ,CAAC;AACtE,UAAI,MAAM,WAAW,QAAQ,SAAS,MAAM,QAAQ,CAAC,QAAQ,SAAS;AAClE,kBAAU,EAAE,GAAG,SAAS,SAAS,GAAG;AACpC,YAAI,EAAE,MAAO,eAAc,SAAS,EAAE,IAAI;AAAA,MAC9C;AAAA,IACJ,QAAQ;AAAA,IAER;AAAA,EACJ;AACA,SAAO;AACX;AAGO,IAAM,kBAAkB,CAAC,UAAkB,UAAU,eAAe,CAAC,MAAM,WAAW,GAAG;","names":[]}
@@ -0,0 +1,47 @@
1
+ import { type CockpitConfig, type VariantId } from "./core.js";
2
+ /**
3
+ * Server helpers for the App Router: load the experiments once per request, then render the right variant on the
4
+ * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.
5
+ */
6
+ type SearchParams = Record<string, string | string[] | undefined>;
7
+ interface CookieReader {
8
+ get(name: string): {
9
+ value: string;
10
+ } | undefined;
11
+ }
12
+ /**
13
+ * The experiments for a public key. `null` when MRRocket cannot be reached, answers with an error or is slower than
14
+ * `timeoutMs`: everyone then sees control and the page still renders. Runs on Node, the Edge runtime and Cloudflare
15
+ * Workers (only `fetch` and `AbortController`).
16
+ *
17
+ * `apiKey` and `host` default to `NEXT_PUBLIC_MRROCKET_KEY` and `NEXT_PUBLIC_MRROCKET_HOST`.
18
+ */
19
+ export declare const getCockpitConfig: ({ apiKey, host, timeoutMs }?: {
20
+ apiKey?: string;
21
+ host?: string;
22
+ timeoutMs?: number;
23
+ }) => Promise<CockpitConfig | null>;
24
+ /** Forced variants and the embed flag from a page's search params, for previews in Cockpit. */
25
+ export declare const previewFrom: (searchParams: SearchParams | undefined) => {
26
+ preview: Record<string, string>;
27
+ embedded: boolean;
28
+ };
29
+ /** The visitor's id from the `ck_id` cookie, if the browser has one yet (only in consent "cookies" mode). */
30
+ export declare const distinctIdFrom: (cookies: CookieReader | undefined) => string;
31
+ /**
32
+ * Who the visitor is for this request. Pass `distinctId` (your signed-in user id, the same you give
33
+ * `<CockpitProvider distinctId>`) when there is no `ck_id` cookie, as in the default cookieless mode.
34
+ */
35
+ interface RequestInfo {
36
+ config: CockpitConfig | null;
37
+ searchParams?: SearchParams;
38
+ cookies?: CookieReader;
39
+ distinctId?: string;
40
+ }
41
+ /** The variant of one experiment for this request. */
42
+ export declare const getVariant: (key: string, { config, searchParams, cookies, distinctId }: RequestInfo) => VariantId;
43
+ /** Every text change for this request, to render `<Text>` defaults server-side. */
44
+ export declare const getTexts: ({ config, searchParams, cookies, distinctId }: RequestInfo) => Record<string, string>;
45
+ export * from "./core.js";
46
+ /** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */
47
+ export { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
package/dist/server.js ADDED
@@ -0,0 +1,43 @@
1
+ // src/server.ts
2
+ import { EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, envKey, parsePreview, resolveHost, resolveVariant, textChanges } from "./core.js";
3
+ export * from "./core.js";
4
+ import { REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
5
+ var CONFIG_TIMEOUT_MS = 2500;
6
+ var getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS } = {}) => {
7
+ const key = apiKey ?? envKey();
8
+ if (!key) return null;
9
+ const ctl = new AbortController();
10
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
11
+ try {
12
+ const res = await fetch(`${resolveHost(host)}/api/sdk/config?key=${encodeURIComponent(key)}`, {
13
+ signal: ctl.signal,
14
+ next: { revalidate: 0 }
15
+ });
16
+ if (!res.ok) return null;
17
+ const config = await res.json();
18
+ return config && Array.isArray(config.experiments) ? config : null;
19
+ } catch {
20
+ return null;
21
+ } finally {
22
+ clearTimeout(timer);
23
+ }
24
+ };
25
+ var previewFrom = (searchParams) => {
26
+ const embed = searchParams?.[EMBED_PARAM];
27
+ return { preview: parsePreview(searchParams?.[PREVIEW_PARAM]), embedded: (Array.isArray(embed) ? embed[0] : embed) === "1" };
28
+ };
29
+ var distinctIdFrom = (cookies) => cookies?.get(ID_COOKIE)?.value ?? "anonymous";
30
+ var getVariant = (key, { config, searchParams, cookies, distinctId }) => resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);
31
+ var getTexts = ({ config, searchParams, cookies, distinctId }) => textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);
32
+ export {
33
+ REFERRAL_COOKIE,
34
+ distinctIdFrom,
35
+ getCockpitConfig,
36
+ getTexts,
37
+ getVariant,
38
+ previewFrom,
39
+ readReferralCookie,
40
+ referralForCheckout,
41
+ referralLink
42
+ };
43
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { type CockpitConfig, EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, type VariantId, envKey, parsePreview, resolveHost, resolveVariant, textChanges } from \"./core\";\n\n/**\n * Server helpers for the App Router: load the experiments once per request, then render the right variant on the\n * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.\n */\n\ntype SearchParams = Record<string, string | string[] | undefined>;\ninterface CookieReader {\n get(name: string): { value: string } | undefined;\n}\n\n/** How long the server waits for MRRocket before it renders with control. */\nconst CONFIG_TIMEOUT_MS = 2500;\n\n/**\n * The experiments for a public key. `null` when MRRocket cannot be reached, answers with an error or is slower than\n * `timeoutMs`: everyone then sees control and the page still renders. Runs on Node, the Edge runtime and Cloudflare\n * Workers (only `fetch` and `AbortController`).\n *\n * `apiKey` and `host` default to `NEXT_PUBLIC_MRROCKET_KEY` and `NEXT_PUBLIC_MRROCKET_HOST`.\n */\nexport const getCockpitConfig = async ({ apiKey, host, timeoutMs = CONFIG_TIMEOUT_MS }: { apiKey?: string; host?: string; timeoutMs?: number } = {}): Promise<CockpitConfig | null> => {\n const key = apiKey ?? envKey();\n if (!key) return null;\n const ctl = new AbortController();\n const timer = setTimeout(() => ctl.abort(), timeoutMs);\n try {\n // `next.revalidate: 0` keeps this request uncached in Next.js. Not `cache: \"no-store\"`: Cloudflare Workers\n // throws on the `cache` field of a fetch unless a compatibility flag is on, and every config would be null.\n const res = await fetch(`${resolveHost(host)}/api/sdk/config?key=${encodeURIComponent(key)}`, {\n signal: ctl.signal,\n next: { revalidate: 0 },\n } as RequestInit);\n if (!res.ok) return null;\n const config = (await res.json()) as CockpitConfig | null;\n return config && Array.isArray(config.experiments) ? config : null;\n } catch {\n return null;\n } finally {\n clearTimeout(timer);\n }\n};\n\n/** Forced variants and the embed flag from a page's search params, for previews in Cockpit. */\nexport const previewFrom = (searchParams: SearchParams | undefined) => {\n const embed = searchParams?.[EMBED_PARAM];\n return { preview: parsePreview(searchParams?.[PREVIEW_PARAM]), embedded: (Array.isArray(embed) ? embed[0] : embed) === \"1\" };\n};\n\n/** The visitor's id from the `ck_id` cookie, if the browser has one yet (only in consent \"cookies\" mode). */\nexport const distinctIdFrom = (cookies: CookieReader | undefined) => cookies?.get(ID_COOKIE)?.value ?? \"anonymous\";\n\n/**\n * Who the visitor is for this request. Pass `distinctId` (your signed-in user id, the same you give\n * `<CockpitProvider distinctId>`) when there is no `ck_id` cookie, as in the default cookieless mode.\n */\ninterface RequestInfo {\n config: CockpitConfig | null;\n searchParams?: SearchParams;\n cookies?: CookieReader;\n distinctId?: string;\n}\n\n/** The variant of one experiment for this request. */\nexport const getVariant = (key: string, { config, searchParams, cookies, distinctId }: RequestInfo): VariantId =>\n resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\n/** Every text change for this request, to render `<Text>` defaults server-side. */\nexport const getTexts = ({ config, searchParams, cookies, distinctId }: RequestInfo) =>\n textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);\n\nexport * from \"./core\";\n/** Affiliate referrals on the server: read the `mrr_ref` cookie in the route that creates a Stripe Checkout Session. */\nexport { type CheckoutReferral, type Referral, REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from \"./referral\";\n"],"mappings":";AAAA,SAA6B,aAAa,WAAW,eAA+B,QAAQ,cAAc,aAAa,gBAAgB,mBAAmB;AAwE1J,cAAc;AAEd,SAA+C,iBAAiB,oBAAoB,qBAAqB,oBAAoB;AA7D7H,IAAM,oBAAoB;AASnB,IAAM,mBAAmB,OAAO,EAAE,QAAQ,MAAM,YAAY,kBAAkB,IAA4D,CAAC,MAAqC;AACnL,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,IAAI,gBAAgB;AAChC,QAAM,QAAQ,WAAW,MAAM,IAAI,MAAM,GAAG,SAAS;AACrD,MAAI;AAGA,UAAM,MAAM,MAAM,MAAM,GAAG,YAAY,IAAI,CAAC,uBAAuB,mBAAmB,GAAG,CAAC,IAAI;AAAA,MAC1F,QAAQ,IAAI;AAAA,MACZ,MAAM,EAAE,YAAY,EAAE;AAAA,IAC1B,CAAgB;AAChB,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,WAAO,UAAU,MAAM,QAAQ,OAAO,WAAW,IAAI,SAAS;AAAA,EAClE,QAAQ;AACJ,WAAO;AAAA,EACX,UAAE;AACE,iBAAa,KAAK;AAAA,EACtB;AACJ;AAGO,IAAM,cAAc,CAAC,iBAA2C;AACnE,QAAM,QAAQ,eAAe,WAAW;AACxC,SAAO,EAAE,SAAS,aAAa,eAAe,aAAa,CAAC,GAAG,WAAW,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI,WAAW,IAAI;AAC/H;AAGO,IAAM,iBAAiB,CAAC,YAAsC,SAAS,IAAI,SAAS,GAAG,SAAS;AAchG,IAAM,aAAa,CAAC,KAAa,EAAE,QAAQ,cAAc,SAAS,WAAW,MAChF,eAAe,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;AAGjG,IAAM,WAAW,CAAC,EAAE,QAAQ,cAAc,SAAS,WAAW,MACjE,YAAY,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;","names":[]}
@@ -0,0 +1,4 @@
1
+ /** The one stylesheet for every Cockpit widget. Everything is scoped under `.ckw-root` and prefixed `ckw-`. */
2
+ export declare const CSS = "\n.ckw-root{--ckw-accent:#3f76ff;--ckw-accent-ink:#fff;--ckw-bg:#fff;--ckw-bg2:#f7f7f8;--ckw-bg3:#eeeff1;--ckw-fg:#1c1d20;--ckw-fg2:#5d6068;--ckw-fg3:#8b8e96;--ckw-line:#e4e5e8;--ckw-danger:#d92d20;--ckw-shadow:0 8px 30px rgba(16,18,24,.16);\nfont-size:14px;line-height:1.45;font-family:inherit;color:var(--ckw-fg);box-sizing:border-box;-webkit-font-smoothing:antialiased}\n.ckw-root[data-theme=dark]{--ckw-bg:#17181b;--ckw-bg2:#1f2024;--ckw-bg3:#292a30;--ckw-fg:#f1f2f4;--ckw-fg2:#b0b3ba;--ckw-fg3:#7e818a;--ckw-line:#2e2f35;--ckw-shadow:0 8px 30px rgba(0,0,0,.5)}\n@media (prefers-color-scheme:dark){.ckw-root[data-theme=auto]{--ckw-bg:#17181b;--ckw-bg2:#1f2024;--ckw-bg3:#292a30;--ckw-fg:#f1f2f4;--ckw-fg2:#b0b3ba;--ckw-fg3:#7e818a;--ckw-line:#2e2f35;--ckw-shadow:0 8px 30px rgba(0,0,0,.5)}}\n.ckw-root *,.ckw-root *::before,.ckw-root *::after{box-sizing:border-box}\n.ckw-root button,.ckw-root input,.ckw-root select,.ckw-root textarea{font:inherit;color:inherit;margin:0}\n.ckw-root button{cursor:pointer}\n.ckw-root :focus-visible{outline:2px solid var(--ckw-accent);outline-offset:2px}\n.ckw-board{width:100%;max-width:760px;margin:0 auto;background:var(--ckw-bg);min-width:0}\n.ckw-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}\n.ckw-title{font-size:20px;font-weight:600;margin:0;letter-spacing:-.01em}\n.ckw-sub{color:var(--ckw-fg2);margin:2px 0 0;font-size:13px}\n.ckw-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;padding:0 12px;border-radius:8px;border:1px solid var(--ckw-line);background:var(--ckw-bg);font-weight:500;font-size:13px;white-space:nowrap}\n.ckw-btn:hover{background:var(--ckw-bg2)}\n.ckw-btn[disabled]{opacity:.55;cursor:not-allowed}\n.ckw-btn-primary{background:var(--ckw-accent);border-color:var(--ckw-accent);color:var(--ckw-accent-ink)}\n.ckw-btn-primary:hover{background:var(--ckw-accent);filter:brightness(1.08)}\n.ckw-btn-ghost{border-color:transparent;background:transparent;color:var(--ckw-fg2)}\n.ckw-icon{width:16px;height:16px;flex:none;display:block}\n.ckw-tabs{display:flex;gap:4px;border-bottom:1px solid var(--ckw-line);margin-bottom:12px;overflow-x:auto}\n.ckw-tab{background:none;border:0;border-bottom:2px solid transparent;padding:8px 10px;margin-bottom:-1px;color:var(--ckw-fg2);font-weight:500;font-size:13px;white-space:nowrap}\n.ckw-tab[aria-selected=true]{color:var(--ckw-fg);border-bottom-color:var(--ckw-accent)}\n.ckw-tools{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}\n.ckw-input,.ckw-select,.ckw-textarea{width:100%;border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg);padding:7px 10px;font-size:14px;min-height:34px}\n.ckw-input::placeholder,.ckw-textarea::placeholder{color:var(--ckw-fg3)}\n.ckw-textarea{resize:vertical;min-height:72px}\n.ckw-search{flex:1 1 160px;min-width:0}\n.ckw-seg{display:inline-flex;border:1px solid var(--ckw-line);border-radius:8px;overflow:hidden}\n.ckw-seg button{border:0;background:var(--ckw-bg);padding:0 10px;height:32px;font-size:13px;color:var(--ckw-fg2)}\n.ckw-seg button[aria-pressed=true]{background:var(--ckw-bg3);color:var(--ckw-fg);font-weight:500}\n.ckw-chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}\n.ckw-chip{border:1px solid var(--ckw-line);background:var(--ckw-bg);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--ckw-fg2)}\n.ckw-chip[aria-pressed=true]{background:var(--ckw-fg);color:var(--ckw-bg);border-color:var(--ckw-fg)}\n.ckw-list{list-style:none;margin:0;padding:0;border:1px solid var(--ckw-line);border-radius:8px;overflow:hidden}\n.ckw-list>li+li{border-top:1px solid var(--ckw-line)}\n.ckw-row{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;background:var(--ckw-bg)}\n.ckw-row:hover{background:var(--ckw-bg2)}\n.ckw-vote{flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0;width:44px;min-height:48px;border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg);font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;padding:4px 0;color:var(--ckw-fg2)}\n.ckw-vote:hover{border-color:var(--ckw-accent);color:var(--ckw-accent)}\n.ckw-vote[aria-pressed=true]{background:var(--ckw-accent);border-color:var(--ckw-accent);color:var(--ckw-accent-ink)}\n.ckw-vote-sm{width:auto;min-height:0;flex-direction:row;gap:4px;padding:2px 8px}\n.ckw-main{flex:1;min-width:0;text-align:left}\n.ckw-link{display:block;width:100%;text-align:left;background:none;border:0;padding:0;font-weight:600;font-size:14px;color:var(--ckw-fg);overflow-wrap:anywhere}\n.ckw-link:hover{color:var(--ckw-accent)}\n.ckw-excerpt{margin:2px 0 0;color:var(--ckw-fg2);font-size:13px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}\n.ckw-meta{display:flex;gap:8px;align-items:center;margin-top:6px;color:var(--ckw-fg3);font-size:12px;flex-wrap:wrap}\n.ckw-pill{display:inline-flex;align-items:center;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;background:var(--ckw-bg3);color:var(--ckw-fg2)}\n.ckw-pill[data-s=planned]{background:rgba(63,118,255,.14);color:#3f76ff}\n.ckw-pill[data-s=progress]{background:rgba(245,158,11,.16);color:#b45309}\n.ckw-pill[data-s=shipped]{background:rgba(22,163,74,.15);color:#15803d}\n.ckw-root[data-theme=dark] .ckw-pill[data-s=progress]{color:#fbbf24}\n.ckw-root[data-theme=dark] .ckw-pill[data-s=shipped]{color:#4ade80}\n@media (prefers-color-scheme:dark){.ckw-root[data-theme=auto] .ckw-pill[data-s=progress]{color:#fbbf24}.ckw-root[data-theme=auto] .ckw-pill[data-s=shipped]{color:#4ade80}}\n.ckw-stat{display:inline-flex;align-items:center;gap:4px}\n.ckw-empty{border:1px dashed var(--ckw-line);border-radius:8px;padding:28px 16px;text-align:center;color:var(--ckw-fg2)}\n.ckw-error{border:1px solid color-mix(in srgb,var(--ckw-danger) 40%,transparent);background:color-mix(in srgb,var(--ckw-danger) 8%,transparent);color:var(--ckw-danger);border-radius:8px;padding:8px 10px;font-size:13px;display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:10px}\n.ckw-skel{height:68px;border-radius:8px;background:var(--ckw-bg2);animation:ckw-sh 1.2s ease-in-out infinite;margin-bottom:6px}\n@keyframes ckw-sh{50%{opacity:.5}}\n@media (prefers-reduced-motion:reduce){.ckw-skel{animation:none}.ckw-root *{transition:none!important}}\n.ckw-form{display:flex;flex-direction:column;gap:10px;border:1px solid var(--ckw-line);border-radius:8px;padding:12px;margin-bottom:12px;background:var(--ckw-bg)}\n.ckw-field{display:flex;flex-direction:column;gap:4px}\n.ckw-label{font-size:12px;font-weight:600;color:var(--ckw-fg2)}\n.ckw-actions{display:flex;gap:8px;justify-content:flex-end}\n.ckw-dupes{border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg2);padding:8px 10px;font-size:13px}\n.ckw-dupes p{margin:0 0 4px;color:var(--ckw-fg2);font-size:12px}\n.ckw-dupe{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:3px 0}\n.ckw-dupe span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ckw-cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}\n@media (max-width:640px){.ckw-cols{grid-template-columns:1fr}}\n.ckw-col h3{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--ckw-fg2);margin:0 0 8px;display:flex;gap:6px;align-items:center}\n.ckw-card{border:1px solid var(--ckw-line);border-radius:8px;padding:10px;margin-bottom:8px;background:var(--ckw-bg)}\n.ckw-card .ckw-link{margin-bottom:6px}\n.ckw-entry{padding:12px 0;border-bottom:1px solid var(--ckw-line)}\n.ckw-entry time{font-size:12px;color:var(--ckw-fg3)}\n.ckw-entry h3{font-size:15px;margin:2px 0 4px}\n.ckw-entry p{margin:0;color:var(--ckw-fg2);white-space:pre-wrap;overflow-wrap:anywhere}\n.ckw-back{margin-bottom:8px;padding-left:4px}\n.ckw-detail-head{display:flex;gap:12px;align-items:flex-start}\n.ckw-detail-head h2{margin:0 0 4px;font-size:18px;overflow-wrap:anywhere}\n.ckw-body{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--ckw-fg2);margin:12px 0}\n.ckw-h{font-size:13px;font-weight:600;margin:16px 0 8px}\n.ckw-comment{padding:8px 0;border-top:1px solid var(--ckw-line)}\n.ckw-comment b{font-size:13px}\n.ckw-comment p{margin:2px 0 0;white-space:pre-wrap;overflow-wrap:anywhere}\n.ckw-team{margin-left:6px;background:var(--ckw-accent);color:var(--ckw-accent-ink);border-radius:4px;padding:0 5px;font-size:10px;font-weight:700;text-transform:uppercase;vertical-align:1px}\n.ckw-time{color:var(--ckw-fg3);font-size:12px;margin-left:6px}\n.ckw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n/* Floating widget */\n.ckw-fab{position:fixed;z-index:2147483000;bottom:20px;display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--ckw-accent);color:var(--ckw-accent-ink);font-weight:600;font-size:14px;box-shadow:var(--ckw-shadow)}\n.ckw-fab[data-pos=bottom-right],.ckw-pop[data-pos=bottom-right]{right:20px}\n.ckw-fab[data-pos=bottom-left],.ckw-pop[data-pos=bottom-left]{left:20px}\n.ckw-pop{position:fixed;z-index:2147483001;bottom:70px;width:380px;max-width:calc(100vw - 24px);max-height:min(560px,calc(100vh - 90px));display:flex;flex-direction:column;background:var(--ckw-bg);border:1px solid var(--ckw-line);border-radius:12px;box-shadow:var(--ckw-shadow);overflow:hidden}\n.ckw-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--ckw-line);font-weight:600}\n.ckw-pop-body{padding:12px;overflow-y:auto;flex:1}\n.ckw-pop-foot{padding:8px 12px;border-top:1px solid var(--ckw-line);display:flex;justify-content:space-between;align-items:center;font-size:13px}\n.ckw-pop-foot a{color:var(--ckw-accent);text-decoration:none;font-weight:500}\n.ckw-pop .ckw-list{border:0;border-radius:0}\n.ckw-pop .ckw-row{padding:8px 0}\n.ckw-pop .ckw-form{border:0;padding:0;margin:12px 0 0}\n@media (max-width:520px){.ckw-pop{left:0!important;right:0!important;bottom:0;width:100%;max-width:100%;border-radius:12px 12px 0 0;max-height:85vh}.ckw-fab{bottom:14px}.ckw-fab[data-pos=bottom-right]{right:14px}.ckw-fab[data-pos=bottom-left]{left:14px}}\n/* Survey */\n.ckw-survey{position:fixed;z-index:2147483000;left:20px;bottom:20px;width:360px;max-width:calc(100vw - 24px);background:var(--ckw-bg);border:1px solid var(--ckw-line);border-radius:12px;box-shadow:var(--ckw-shadow);padding:14px;animation:ckw-in .2s ease-out}\n@keyframes ckw-in{from{opacity:0;transform:translateY(8px)}}\n@media (max-width:520px){.ckw-survey{left:0;right:0;bottom:0;width:100%;max-width:100%;border-radius:12px 12px 0 0}}\n.ckw-sv-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}\n.ckw-dots{display:flex;gap:4px}\n.ckw-dot{width:6px;height:6px;border-radius:50%;background:var(--ckw-bg3)}\n.ckw-dot[data-on=true]{background:var(--ckw-accent)}\n.ckw-q{font-weight:600;font-size:15px;margin:0 0 2px;border:0;padding:0}\n.ckw-root .ckw-q:focus-visible{outline:none}\n.ckw-qd{color:var(--ckw-fg2);font-size:13px;margin:0 0 10px}\n.ckw-scale{display:flex;gap:4px;flex-wrap:wrap}\n.ckw-num{flex:1 1 0;min-width:26px;height:34px;border:1px solid var(--ckw-line);background:var(--ckw-bg);border-radius:8px;font-size:13px;font-weight:600;padding:0}\n.ckw-num:hover{border-color:var(--ckw-accent)}\n.ckw-num[aria-pressed=true]{background:var(--ckw-accent);color:var(--ckw-accent-ink);border-color:var(--ckw-accent)}\n.ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}\n.ckw-star[data-on=true]{color:#f5a524}\n.ckw-star svg{width:28px;height:28px}\n.ckw-lh{display:flex;justify-content:space-between;color:var(--ckw-fg3);font-size:11px;margin-top:4px}\n.ckw-opts{display:flex;flex-direction:column;gap:6px}\n.ckw-opt{display:flex;gap:8px;align-items:center;border:1px solid var(--ckw-line);border-radius:8px;padding:7px 10px;background:var(--ckw-bg);cursor:pointer}\n.ckw-opt:has(input:checked){border-color:var(--ckw-accent);background:color-mix(in srgb,var(--ckw-accent) 8%,transparent)}\n.ckw-opt input{accent-color:var(--ckw-accent)}\n.ckw-sv-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:12px}\n.ckw-thanks{text-align:center;padding:14px 0;display:flex;flex-direction:column;align-items:center;gap:8px}\n.ckw-tick{width:32px;height:32px;border-radius:50%;background:var(--ckw-accent);color:var(--ckw-accent-ink);display:flex;align-items:center;justify-content:center}\n";
3
+ /** Puts the stylesheet into the page once, however many widgets mount. */
4
+ export declare const useStyles: () => void;