@mrrocket/sdk 0.2.0 → 0.4.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/native/index.tsx","../../src/core.ts","../../src/batch.ts","../../src/errors.ts","../../src/referral.ts","../../src/native/client.ts","../../src/native/platform.ts"],"sourcesContent":["/**\n * `@mrrocket/sdk/react-native`: the MRRocket SDK for React Native and Expo apps.\n *\n * // app/_layout.tsx (Expo Router)\n * import { MRRocketProvider, useExpoRouterTracking } from \"@mrrocket/sdk/react-native\";\n *\n * function Screens() {\n * useExpoRouterTracking();\n * return <Stack />;\n * }\n *\n * export default function RootLayout() {\n * return (\n * <MRRocketProvider apiKey=\"mrr_pub_…\">\n * <Screens />\n * </MRRocketProvider>\n * );\n * }\n *\n * No DOM, no \"use client\", no session recorder: Metro bundles it as is. See the README for what it sends.\n */\nimport { type ReactNode, createContext, createElement, useCallback, useContext, useEffect, useState, useSyncExternalStore } from \"react\";\nimport { type CockpitConfig, SDK_VERSION, type VariantId } from \"../core\";\nimport { type KeyValueStorage, type MRRocketClient, type MRRocketOptions, activeRouteName, createClient } from \"./client\";\nimport { nativeDeps, optionalModules } from \"./platform\";\n\nexport type { AppContext, CockpitConfig, FlagConfig, VariantId } from \"../core\";\nexport type { KeyValueStorage, MRRocketClient, MRRocketOptions, NativeConsent } from \"./client\";\nexport { SDK_VERSION, activeRouteName };\n\n/* eslint-disable @typescript-eslint/no-require-imports */\ndeclare const require: (id: string) => unknown;\n\nlet shared: MRRocketClient | null = null;\nlet sharedKey = \"\";\n\n/**\n * The SDK's client outside React (a background task, a store, an API layer): the one the provider made, or a new one.\n * One client per app; a second call returns the first.\n */\nexport const createMRRocket = (options: MRRocketOptions & { storage?: KeyValueStorage | null }): MRRocketClient => {\n if (shared) {\n if (sharedKey !== options.apiKey) console.warn(\"[MRRocket] The SDK is already running with another key; keeping the first one.\");\n return shared;\n }\n const { storage, ...rest } = options;\n const deps = nativeDeps({ storage });\n shared = createClient(rest, deps);\n sharedKey = options.apiKey;\n if (options.debug) console.log(\"[MRRocket] optional modules\", JSON.stringify(optionalModules()));\n return shared;\n};\n\n/** The running client, or null before the provider (or createMRRocket) started it. */\nexport const getMRRocket = () => shared;\n\nconst Ctx = createContext<MRRocketClient | null>(null);\n\nexport interface MRRocketProviderProps extends Partial<MRRocketOptions> {\n /** The product's public key (MRRocket › Settings › Products), the same as on the website. */\n apiKey: string;\n /** A storage with AsyncStorage's shape instead of AsyncStorage (MMKV, SecureStore); null keeps everything in memory. */\n storage?: KeyValueStorage | null;\n /** A client you made with createMRRocket; the other props are then ignored. */\n client?: MRRocketClient;\n children?: ReactNode;\n}\n\n/**\n * Put it once around your app (Expo Router: app/_layout.tsx; React Navigation: around the NavigationContainer). It\n * starts the SDK: lifecycle events, the event queue, flags and experiments, error capture. Changing `consent` later\n * calls setConsent.\n */\nexport const MRRocketProvider = ({ client: given, children, consent, ...options }: MRRocketProviderProps) => {\n const [client] = useState(() => given ?? createMRRocket({ ...options, consent }));\n useEffect(() => {\n if (consent) client.setConsent(consent);\n }, [client, consent]);\n return createElement(Ctx.Provider, { value: client }, children);\n};\n\nconst useClient = () => {\n const c = useContext(Ctx);\n if (!c) throw new Error(\"MRRocket: wrap your app in <MRRocketProvider apiKey=…>.\");\n return c;\n};\n\n/** Re-renders when flags, the person or the consent change. */\nconst useVersion = (client: MRRocketClient) => useSyncExternalStore(client.subscribe, client.getVersion, client.getVersion);\n\n/**\n * `track`, `screen`, `identify`, `reset`, `setConsent`, `captureError`, `flush`, the visitor id, the consent and whether\n * flags and experiments have loaded.\n */\nexport const useMRRocket = () => {\n const client = useClient();\n useVersion(client);\n return {\n track: client.track,\n screen: client.screen,\n identify: client.identify,\n reset: client.reset,\n setConsent: client.setConsent,\n captureError: client.captureError,\n flush: client.flush,\n distinctId: client.getDistinctId(),\n consent: client.getConsent(),\n ready: client.isReady(),\n };\n};\n\n/** A feature flag from MRRocket › Build › Feature flags: true while it is on for this person; false while off, unknown or loading. */\nexport const useFlag = (key: string): boolean => {\n const client = useClient();\n useVersion(client);\n return client.getFlag(key);\n};\n\n/** The variant of a multivariate flag (\"control\", \"test\" …), or null while it is off. */\nexport const useFlagVariant = (key: string): string | null => {\n const client = useClient();\n useVersion(client);\n return client.getFlagVariant(key);\n};\n\n/**\n * The variant of an experiment this person sees (\"control\" until the config is known; the cached config is used on a\n * cold start, so the first screen is already stable). Records the exposure once per screen.\n */\nexport const useVariant = (key: string): VariantId => {\n const client = useClient();\n useVersion(client);\n const config: CockpitConfig | null = client.getConfig();\n const variant = client.peekVariant(key);\n useEffect(() => {\n if (config) client.expose(key, variant);\n }, [client, config, key, variant]);\n return variant;\n};\n\ntype ExpoRouterModule = { usePathname: () => string };\nlet expoRouter: ExpoRouterModule | null | undefined;\nconst loadExpoRouter = (): ExpoRouterModule | null => {\n if (expoRouter !== undefined) return expoRouter;\n try {\n expoRouter = require(\"expo-router\") as ExpoRouterModule;\n } catch {\n expoRouter = null;\n }\n return expoRouter;\n};\nconst noPathname = () => undefined;\nlet routerWarned = false;\n\n/**\n * Screen views for Expo Router: one `$screen` per path (\"/settings\", \"/product/[id]\" as its real path). Call it in a\n * component inside both the router and MRRocketProvider, for example the root layout's child.\n */\nexport const useExpoRouterTracking = () => {\n const client = useClient();\n const router = loadExpoRouter();\n if (router === null && !routerWarned) {\n routerWarned = true;\n console.warn(\"[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.\");\n }\n const pathname = (router?.usePathname ?? noPathname)();\n useEffect(() => {\n if (pathname) client.screen(pathname);\n }, [client, pathname]);\n};\n\n/**\n * Screen views for React Navigation: pass the result to the container's `onStateChange`, and call it once in `onReady`\n * with the root state, so the first screen counts too.\n *\n * const ref = useNavigationContainerRef();\n * const onStateChange = useNavigationTracking();\n * <NavigationContainer ref={ref} onReady={() => onStateChange(ref.getRootState())} onStateChange={onStateChange}>\n */\nexport const useNavigationTracking = () => {\n const client = useClient();\n return useCallback(\n (state: unknown) => {\n const name = activeRouteName(state);\n if (name) client.screen(name);\n },\n [client],\n );\n};\n","/**\n * The part of the SDK that runs anywhere: experiment config, variant assignment, preview links and event shapes.\n * No React, no browser APIs, so the server helper, the proxy helper and the client share it.\n */\n\nexport type VariantId = string;\n\n/** This build of the SDK. Sent with the config request so MRRocket knows what the site runs (error tracking, flags). */\nexport const SDK_VERSION = \"0.4.0\";\n\n/** One variant of an experiment. `changes` replaces texts that the page marks with `<Text id>`. */\nexport interface VariantConfig {\n id: VariantId;\n /** Share of traffic, 0 to 1. The weights of an experiment add up to 1. */\n weight: number;\n changes?: Record<string, string>;\n}\n\nexport interface ExperimentConfig {\n /** The experiment's key, the same as in Cockpit. */\n key: string;\n status: \"running\" | \"draft\" | \"stopped\";\n /** Control first. */\n variants: VariantConfig[];\n /** A winner that shipped: everyone gets it, and nobody is assigned anymore. */\n winner?: VariantId;\n}\n\nexport interface CockpitConfig {\n /** The product the public key belongs to. */\n project: string;\n experiments: ExperimentConfig[];\n /** The product's affiliate program: how many days a partner's referral is kept. Missing without a program. */\n referral?: { cookieDays: number };\n /** Session recordings: present only while the product records (Cockpit › Sessions). See `ReplayConfig`. */\n replay?: ReplayConfig;\n /** Feature flags of the product in Production (MRRocket › Build › Feature flags), evaluated by `useFlag` and `getFlag`. */\n flags?: FlagConfig[];\n}\n\n/** One condition of a flag's audience, on a property of the person (`identify(id, props)`), the id or the page. */\nexport interface FlagCondition {\n /** \"Email\", \"Plan\", \"Country\", \"id\", \"path\" … Matched against the person's properties without regard to case. */\n property: string;\n op: \"is\" | \"is not\" | \"in\" | \"not in\" | \"≥\" | \"in cohort\";\n values: string[];\n}\n\n/** Users who match every condition, of whom `rollout` percent get the flag. No conditions: everyone. */\nexport interface FlagConditionSet {\n conditions: FlagCondition[];\n /** 0 to 100. */\n rollout: number;\n}\n\nexport interface FlagConfig {\n key: string;\n /** Off: everyone gets the off side. */\n on: boolean;\n /** The first set a person matches decides; none matched: off. */\n sets: FlagConditionSet[];\n /** Multivariate flags: the variants and their split in percent (control included). Missing for an on/off flag. */\n variants?: { key: string; split: number; payload?: string }[];\n}\n\n/** What a person is for flag conditions: their properties from identify(), their id and the page they are on. */\nexport type FlagPerson = Record<string, unknown>;\n\nconst propOf = (person: FlagPerson, name: string) => {\n const want = name.trim().toLowerCase();\n for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;\n return undefined;\n};\n\n/** `\"@acme.com\"` matches every address at that domain; anything else matches the whole value, without regard to case. */\nconst same = (actual: string, value: string) => {\n const a = actual.trim().toLowerCase();\n const v = value.trim().toLowerCase();\n return v.startsWith(\"@\") ? a.endsWith(v) : a === v;\n};\n\nconst matchesCondition = (c: FlagCondition, person: FlagPerson) => {\n // Cohorts live in MRRocket; a browser cannot know them, so a cohort condition never matches here.\n if (c.op === \"in cohort\") return false;\n const raw = propOf(person, c.property);\n const actual = raw === undefined || raw === null ? \"\" : String(raw);\n if (c.op === \"≥\") {\n const a = Number(actual);\n const b = Number(c.values[0]);\n return actual !== \"\" && Number.isFinite(a) && Number.isFinite(b) && a >= b;\n }\n const hit = actual !== \"\" && c.values.some((v) => same(actual, v));\n return c.op === \"is\" || c.op === \"in\" ? hit : !hit;\n};\n\n/**\n * The flag for one person: `false` (off), `true` (on, an on/off flag) or the variant's key (a multivariate flag). The\n * bucket is a stable hash of flag and person, so a person keeps their side as the rollout grows.\n */\nexport const evaluateFlag = (flag: FlagConfig, distinctId: string, person: FlagPerson = {}): boolean | string => {\n if (!flag.on) return false;\n const subject = { id: distinctId, ...person };\n const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };\n if (!set) return false;\n if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;\n if (!flag.variants?.length) return true;\n const u = flagUnit(`${flag.key}:variant:${distinctId}`) * 100;\n let acc = 0;\n for (const v of flag.variants) {\n acc += v.split;\n if (u < acc) return v.key;\n }\n return flag.variants[flag.variants.length - 1].key;\n};\n\n/** A flag of the config for this person; an unknown flag (or no config) is off. */\nexport const flagFor = (key: string, config: CockpitConfig | null, distinctId: string, person: FlagPerson = {}) => {\n const flag = config?.flags?.find((f) => f.key === key);\n return flag ? evaluateFlag(flag, distinctId, person) : false;\n};\n\n/** One JavaScript error as it goes over the wire to `POST /api/sdk/errors`. */\nexport interface ErrorReport {\n /** \"TypeError\", \"Error\" … */\n type: string;\n message: string;\n /** The raw stack, at most 8 KB. MRRocket parses it into frames. */\n stack?: string;\n /** \"onerror\" (uncaught), \"onunhandledrejection\" (a rejected promise nobody caught) or \"manual\" (captureError). */\n mechanism: \"onerror\" | \"onunhandledrejection\" | \"manual\";\n handled: boolean;\n url: string;\n ts: number;\n /** What happened just before: pages and clicks, newest last, at most 20. */\n crumbs?: { ts: number; type: \"navigation\" | \"click\" | \"lifecycle\"; message: string }[];\n /** Your release (a version or commit), from the provider's `release` prop. */\n release?: string;\n distinctId?: string;\n sessionId?: string;\n}\n\n/**\n * Session recordings of a product, from the config. Recording also needs the visitor's own replay consent\n * (`<CockpitProvider replay>` or `setConsent({ replay: true })`); without it nothing is recorded or buffered.\n */\nexport interface ReplayConfig {\n /**\n * Share of sessions uploaded from their start, 0 to 1 (default 0.1). The others keep only the last minute in\n * memory and upload it when something goes wrong: a JS error, a rage click, a `purchase…` or `checkout…` event.\n */\n sampleRate: number;\n}\n\n/**\n * What the SDK may send and store.\n * - \"cookieless\" (default): no cookie, no localStorage. Events go without a visitor id; the server counts visitors\n * with a hash of key, date, IP and user agent that changes every day.\n * - \"cookies\": a persistent `ck_id` cookie identifies the visitor across visits, `ck_sid` holds the session.\n * - \"off\": nothing is sent and nothing is stored.\n */\nexport type ConsentMode = \"cookieless\" | \"cookies\" | \"off\";\n\n/**\n * What `setConsent` takes: a mode as before, or the parts to change. `replay` is the separate consent for session\n * recordings (default false); it applies only while analytics is not \"off\".\n *\n * setConsent(\"cookies\");\n * setConsent({ replay: true });\n * setConsent({ analytics: \"cookieless\", replay: false });\n */\nexport type ConsentChoice = ConsentMode | { analytics?: ConsentMode; replay?: boolean };\n\n/** The campaign parameters the SDK reads from the landing URL. */\nexport const UTM_KEYS = [\"utm_source\", \"utm_medium\", \"utm_campaign\", \"utm_term\", \"utm_content\"] as const;\nexport type UtmKey = (typeof UTM_KEYS)[number];\n\n/** One event as it goes over the wire to `POST /api/sdk/events`. */\nexport interface CockpitEvent {\n event: string;\n /** The visitor. Missing in \"cookieless\" mode until identify(): the server then uses its daily visitor hash. */\n distinctId?: string;\n /** Ends after 30 minutes without an event. */\n sessionId?: string;\n props?: Record<string, unknown>;\n /** Milliseconds since the epoch. */\n ts: number;\n url?: string;\n /** `document.referrer` on the first page of a page load, the previous in-app URL after client-side navigation. */\n referrer?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n utm_term?: string;\n utm_content?: string;\n /** The screen size in CSS pixels (points on a phone). */\n screen?: { w: number; h: number };\n /** The app that sent it (React Native only): MRRocket stores it with the event and fills OS and device from it. */\n app?: AppContext;\n}\n\n/** Where a React Native event comes from. Every field is optional except the platform; nothing identifies the person. */\nexport interface AppContext {\n /** \"ios\" or \"android\" (React Native's `Platform.OS`). */\n platform: string;\n /** The app's version, like \"1.4.0\" (`expo-application`'s `nativeApplicationVersion`). */\n appVersion?: string;\n /** The build number, like \"42\". */\n build?: string;\n /** The OS version, like \"17.4\". */\n osVersion?: string;\n /** The device model, like \"iPhone 15 Pro\" (`expo-device`). */\n deviceModel?: string;\n /** \"phone\", \"tablet\", \"desktop\" or \"tv\" when `expo-device` knows it. */\n deviceType?: string;\n /** The first preferred locale, like \"de-DE\". */\n locale?: string;\n}\n\n/** The query parameter that forces variants, for previews in Cockpit: `?ck_preview=pricing-annual:a,home-hero:control`. */\nexport const PREVIEW_PARAM = \"ck_preview\";\n/** Set when Cockpit embeds the page: nothing is tracked, so previews never count as visits. */\nexport const EMBED_PARAM = \"ck_embed\";\n/** The cookie that keeps a visitor in the same variant across visits. Only written in \"cookies\" mode. */\nexport const ID_COOKIE = \"ck_id\";\n/** The cookie that holds the session id in \"cookies\" mode. */\nexport const SESSION_COOKIE = \"ck_sid\";\n/** A session ends after this long without an event. */\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\n/** The UTM parameters from a parameter lookup (`URLSearchParams.get` in a browser). Empty values are left out. */\nexport const utmFrom = (get: (name: string) => string | null | undefined): Partial<Record<UtmKey, string>> => {\n const out: Partial<Record<UtmKey, string>> = {};\n for (const key of UTM_KEYS) {\n const value = get(key)?.trim();\n if (value) out[key] = value.slice(0, 150);\n }\n return out;\n};\n\n/** The UTM parameters of a query string, like `\"?utm_source=x\"`. Empty values are left out. */\nexport const readUtm = (search: string): Partial<Record<UtmKey, string>> => {\n const params = new URLSearchParams(search);\n return utmFrom((k) => params.get(k));\n};\n\n/**\n * The query parameters of a URL or query string, without `URLSearchParams` (React Native's is incomplete): `+` is a\n * space, values are percent-decoded, the first of a repeated name wins.\n */\nexport const parseQuery = (url: string): Record<string, string> => {\n const out: Record<string, string> = {};\n const q = url.split(\"#\")[0];\n const at = q.indexOf(\"?\");\n if (at < 0) return out;\n const dec = (s: string) => {\n try {\n return decodeURIComponent(s.replace(/\\+/g, \" \"));\n } catch {\n return s;\n }\n };\n for (const pair of q.slice(at + 1).split(\"&\")) {\n if (!pair) continue;\n const eq = pair.indexOf(\"=\");\n const k = dec(eq < 0 ? pair : pair.slice(0, eq));\n if (k && !(k in out)) out[k] = eq < 0 ? \"\" : dec(pair.slice(eq + 1));\n }\n return out;\n};\n\n/** FNV-1a, 32 bit, mapped to [0, 1). Stable across server and browser. */\nexport const hashUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0) / 4294967296;\n};\n\n/**\n * FNV-1a with murmur3's finalizer, mapped to [0, 1): ids that differ only at the end (\"user-1\", \"user-2\") still spread\n * evenly, so a 30 % rollout reaches about 30 % of people. Flags only; experiments keep `hashUnit` so nobody switches.\n */\nexport const flagUnit = (input: string) => {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return (h >>> 0) / 4294967296;\n};\n\n/** The variant a visitor sees: a shipped winner, control while the experiment does not run, else a stable bucket by visitor. */\nexport const assign = (experiment: ExperimentConfig, distinctId: string): VariantId => {\n const control = experiment.variants[0]?.id ?? \"control\";\n if (experiment.winner) return experiment.winner;\n if (experiment.status !== \"running\") return control;\n const u = hashUnit(`${experiment.key}:${distinctId}`);\n let acc = 0;\n for (const v of experiment.variants) {\n acc += v.weight;\n if (u < acc) return v.id;\n }\n return experiment.variants[experiment.variants.length - 1]?.id ?? control;\n};\n\n/** `\"a:b,c:d\"` → `{ a: \"b\", c: \"d\" }`. Ignores anything malformed. */\nexport const parsePreview = (value: string | string[] | null | undefined): Record<string, VariantId> => {\n const raw = Array.isArray(value) ? value[0] : value;\n if (!raw) return {};\n const out: Record<string, VariantId> = {};\n for (const pair of raw.split(\",\")) {\n const [key, variant] = pair.split(\":\");\n if (key && variant) out[key.trim()] = variant.trim();\n }\n return out;\n};\n\n/** A link that shows one variant of a page, for example in Cockpit's preview frames. */\nexport const previewHref = (url: string, forced: Record<string, VariantId>, embed = true) => {\n const [path, query = \"\"] = url.split(\"?\");\n const params = new URLSearchParams(query);\n const value = Object.entries(forced)\n .map(([k, v]) => `${k}:${v}`)\n .join(\",\");\n if (value) params.set(PREVIEW_PARAM, value);\n if (embed) params.set(EMBED_PARAM, \"1\");\n return `${path}?${params.toString()}`;\n};\n\n/** The variant for one experiment, from a forced preview first, then the config. Unknown experiments get control. */\nexport const resolveVariant = (key: string, config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>): VariantId => {\n if (preview[key]) return preview[key];\n const experiment = config?.experiments.find((e) => e.key === key);\n return experiment ? assign(experiment, distinctId) : \"control\";\n};\n\n/**\n * Every text change that applies to this visitor, merged in config order. Control keeps the page's own words,\n * so only a non-control variant (assigned, forced in a preview, or a shipped winner) changes text.\n */\nexport const textChanges = (config: CockpitConfig | null, distinctId: string, preview: Record<string, VariantId>) => {\n const out: Record<string, string> = {};\n for (const experiment of config?.experiments ?? []) {\n const id = resolveVariant(experiment.key, config, distinctId, preview);\n if (id === (experiment.variants[0]?.id ?? \"control\")) continue;\n Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});\n }\n return out;\n};\n\n/**\n * The MRRocket API host from `NEXT_PUBLIC_MRROCKET_HOST`. Written as a literal property access on purpose: Next.js\n * inlines `process.env.NEXT_PUBLIC_*` at build time, in the browser bundle and in the server bundle (also on\n * Cloudflare Workers), and only replaces the literal form. Without a bundler (or a `process`) it is undefined.\n */\nexport const envHost = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_HOST || undefined;\n } catch {\n return undefined;\n }\n};\n\n/** The public key from `NEXT_PUBLIC_MRROCKET_KEY`, the default for the `apiKey` prop. Same literal-access rule as `envHost`. */\nexport const envKey = (): string | undefined => {\n try {\n return process.env.NEXT_PUBLIC_MRROCKET_KEY || undefined;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Where the API lives: the `host` prop, else `NEXT_PUBLIC_MRROCKET_HOST`, else \"\" (the page's own origin, which is how\n * MRRocket's own demo pages call it). Without a trailing slash.\n */\nexport const resolveHost = (host?: string) => (host ?? envHost() ?? \"\").trim().replace(/\\/+$/, \"\");\n\n/** A random visitor id. */\nexport const newDistinctId = () =>\n typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n","/**\n * How events go over the wire to `POST /api/sdk/events`, shared by every runtime (Next.js, the script tag, React\n * Native): request bodies of at most 100 events each, under the server's 64 KB. No React, no browser APIs, no\n * TextEncoder (Hermes on older React Native lacks it), so it runs anywhere.\n */\nimport type { CockpitEvent } from \"./core\";\n\n/** The server takes at most 100 events and 64 KB per request; stay a little under the size. */\nexport const MAX_BATCH = 100;\nexport const MAX_BYTES = 60_000;\n\n/** What a batch says about the SDK that sent it (Build › Errors and Feature flags show it). */\nexport interface SdkInfo {\n v: string;\n errors: boolean;\n /** \"web\" (script tag), \"ios\" or \"android\" (React Native). Missing from the Next.js provider. */\n platform?: string;\n}\n\n/** UTF-8 length of a string, the way TextEncoder counts it (a lone surrogate becomes U+FFFD, three bytes). */\nexport const byteLength = (s: string) => {\n let n = 0;\n for (let i = 0; i < s.length; i++) {\n const c = s.charCodeAt(i);\n if (c < 0x80) n += 1;\n else if (c < 0x800) n += 2;\n else if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length && (s.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {\n n += 4;\n i++;\n } else n += 3;\n }\n return n;\n};\n\n/** One request: its body and the events in it (so a client can keep the ones that did not arrive). */\nexport interface BatchRequest<E> {\n body: string;\n events: E[];\n}\n\n/**\n * Splits events into request bodies of at most 100 events each, under the size limit. An event too large on its own is\n * dropped (it is in no request).\n */\nexport const batchRequests = <E extends CockpitEvent>(key: string, events: E[], sdk: SdkInfo): BatchRequest<E>[] => {\n const head = `{\"key\":${JSON.stringify(key)},\"sdk\":${JSON.stringify(sdk)},\"events\":[`;\n const empty = byteLength(head) + 2;\n const out: BatchRequest<E>[] = [];\n let chunk: string[] = [];\n let items: E[] = [];\n let size = empty;\n const close = () => {\n if (chunk.length) out.push({ body: `${head}${chunk.join(\",\")}]}`, events: items });\n chunk = [];\n items = [];\n size = empty;\n };\n for (const event of events) {\n const json = JSON.stringify(event);\n const n = byteLength(json) + 1;\n if (empty + n > MAX_BYTES) continue;\n if (chunk.length === MAX_BATCH || size + n > MAX_BYTES) close();\n chunk.push(json);\n items.push(event);\n size += n;\n }\n close();\n return out;\n};\n\n/**\n * Request bodies of at most 100 events each, under the size limit. An event too large on its own is dropped. `sdk` tells\n * MRRocket which SDK the site runs and whether it reports errors (Build › Errors and Feature flags show it).\n */\nexport const bodies = (key: string, events: CockpitEvent[], sdk: SdkInfo) => batchRequests(key, events, sdk).map((r) => r.body);\n","/**\n * Error tracking: uncaught errors and unhandled promise rejections of the page (and `captureError` for ones you\n * catch), sent to `POST /api/sdk/errors`, where MRRocket groups them into issues for Build › Errors. Independent of\n * session recordings. Only the error itself, the page URL and the last pages and clicks go along (no typed text, no\n * network bodies), under the same consent as analytics.\n *\n * Limits on the page: the same error is sent once per page load, at most 20 errors per page load, in batches every\n * 2 seconds and with `sendBeacon` when the page hides.\n */\nimport type { ErrorReport } from \"./core\";\n\nconst MAX_STACK = 8 * 1024;\nconst MAX_PER_PAGE = 20;\nconst MAX_CRUMBS = 20;\nconst FLUSH_MS = 2000;\n\nexport interface ErrorCapture {\n /** An error you caught yourself; `handled` is true. */\n capture: (error: unknown, mechanism?: ErrorReport[\"mechanism\"]) => void;\n /** A page view or a click for the breadcrumbs. */\n crumb: (type: \"navigation\" | \"click\" | \"lifecycle\", message: string) => void;\n stop: () => void;\n}\n\nconst clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);\n\n/** Type, message and stack of anything thrown. */\nexport const describeError = (error: unknown): { type: string; message: string; stack?: string } => {\n if (error instanceof Error) return { type: error.name || \"Error\", message: clip(error.message || String(error), 1000), stack: error.stack ? clip(error.stack, MAX_STACK) : undefined };\n if (typeof error === \"string\") return { type: \"Error\", message: clip(error, 1000) };\n try {\n return { type: \"Error\", message: clip(JSON.stringify(error) ?? String(error), 1000) };\n } catch {\n return { type: \"Error\", message: String(error) };\n }\n};\n\nexport const startErrorCapture = (opts: {\n host: string;\n apiKey: string;\n release?: string;\n /** Read when an error happens: whether to send at all (consent), and who and which session. */\n context: () => { send: boolean; distinctId?: string; sessionId?: string };\n warn: (message: string) => void;\n}): ErrorCapture => {\n const queue: ErrorReport[] = [];\n const crumbs: NonNullable<ErrorReport[\"crumbs\"]> = [];\n const seen = new Set<string>();\n let sent = 0;\n const url = `${opts.host}/api/sdk/errors`;\n\n const flush = (beacon = false) => {\n // At most four reports per request: four full stacks stay under the server's 64 KB.\n while (queue.length) {\n const body = JSON.stringify({ key: opts.apiKey, errors: queue.splice(0, 4) });\n const ok = beacon && typeof navigator !== \"undefined\" && \"sendBeacon\" in navigator && navigator.sendBeacon(url, body);\n if (!ok)\n fetch(url, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, keepalive: true })\n .then((r) => {\n if (!r.ok && r.status !== 429) opts.warn(`Error reports were rejected with ${r.status}. Check the public key and NEXT_PUBLIC_MRROCKET_HOST.`);\n })\n .catch(() => {});\n }\n };\n\n const capture = (error: unknown, mechanism: ErrorReport[\"mechanism\"] = \"manual\") => {\n const ctx = opts.context();\n if (!ctx.send || sent >= MAX_PER_PAGE) return;\n const d = describeError(error);\n // Our own reports failing must never report themselves.\n if (/\\/api\\/sdk\\/errors/.test(d.message)) return;\n const mark = `${d.type}:${d.message}`;\n if (seen.has(mark)) return;\n seen.add(mark);\n sent++;\n queue.push({\n ...d,\n mechanism,\n handled: mechanism === \"manual\",\n url: window.location.href,\n ts: Date.now(),\n crumbs: crumbs.slice(),\n ...(opts.release ? { release: opts.release } : {}),\n ...(ctx.distinctId ? { distinctId: ctx.distinctId } : {}),\n ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),\n });\n };\n\n const onError = (e: ErrorEvent) => {\n // \"Script error.\" from a cross-origin script carries nothing useful.\n if (!e.error && (!e.message || e.message === \"Script error.\")) return;\n capture(e.error ?? new Error(e.message), \"onerror\");\n };\n const onRejection = (e: PromiseRejectionEvent) => capture(e.reason ?? new Error(\"Unhandled promise rejection\"), \"onunhandledrejection\");\n const onHide = () => flush(true);\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n window.addEventListener(\"pagehide\", onHide);\n const timer = window.setInterval(() => flush(), FLUSH_MS);\n\n return {\n capture,\n crumb: (type, message) => {\n crumbs.push({ ts: Date.now(), type, message: clip(message, 120) });\n if (crumbs.length > MAX_CRUMBS) crumbs.shift();\n },\n stop: () => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n window.removeEventListener(\"pagehide\", onHide);\n window.clearInterval(timer);\n flush(true);\n },\n };\n};\n","/**\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 return referralFrom((k) => q.get(k), param);\n};\n\n/** The same from a parameter lookup, for runtimes without `URLSearchParams` (React Native deep links). */\nexport const referralFrom = (get: (name: string) => string | null | undefined, param = \"ref\"): { code: string; clickId: string | null; direct: boolean } | null => {\n const marked = get(REF_PARAM);\n if (marked && CODE.test(marked)) {\n const click = get(CLICK_PARAM);\n return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };\n }\n const direct = 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","/**\n * The React Native client without React: the event queue (batched, persisted for offline use, retried with backoff),\n * the app lifecycle (`$app_open`, `$app_background`, a new session after 30 minutes away), screens, identity and\n * consent, the cached config for flags and experiments, install attribution from the first open's deep link, and error\n * capture. Everything the platform gives (storage, AppState, ErrorUtils, device info, fetch) comes in as `deps`, so\n * it runs in tests without React Native; `platform.ts` builds the real ones.\n */\nimport { type SdkInfo, batchRequests } from \"../batch\";\nimport {\n type AppContext,\n type CockpitConfig,\n type CockpitEvent,\n type ErrorReport,\n SDK_VERSION,\n SESSION_TIMEOUT_MS,\n type UtmKey,\n type VariantId,\n flagFor,\n newDistinctId,\n parseQuery,\n resolveVariant,\n utmFrom,\n} from \"../core\";\nimport { describeError } from \"../errors\";\nimport { carriesReferral, referralFrom } from \"../referral\";\n\n/**\n * What the app may send and store.\n * - \"anonymous\" (default): no id is stored on the device. Events go without a person id until `identify()` (MRRocket\n * counts visitors by its daily hash); flags and experiments use a random id that lives until the app is closed.\n * - \"persistent\": a random install id is stored on the device (removed with the app) and sent with every event.\n * - \"off\": nothing is sent, and what the SDK stored is deleted.\n */\nexport type NativeConsent = \"anonymous\" | \"persistent\" | \"off\";\n\nexport interface MRRocketOptions {\n /** The product's public key (MRRocket › Settings › Products), the same as on the website. */\n apiKey: string;\n /** Where MRRocket runs. Default `https://app.mrrocket.dev`. */\n host?: string;\n /** Default \"anonymous\". The SDK does not remember it: pass it on every start (from your consent screen's stored answer). */\n consent?: NativeConsent;\n /** Uncaught JavaScript errors and `captureError` go to Build › Errors (default true). */\n errors?: boolean;\n /** Your release, for Build › Errors. Default: the app version and build, like \"1.4.0 (42)\". */\n release?: string;\n /** `$app_open` and `$app_background` events (default true). */\n lifecycle?: boolean;\n /** Read UTM and referral parameters of the deep link that opened the app the first time, sent with `$install` (default true). */\n attribution?: boolean;\n /** How often the queue is sent while the app is open, in ms (default 10 000). It is also sent when the app goes to the background. */\n flushIntervalMs?: number;\n /** At most this many events wait on the device; the oldest go first (default 1,000). */\n maxQueue?: number;\n /** Log what the SDK does to the console. */\n debug?: boolean;\n}\n\n/** AsyncStorage's shape. */\nexport interface KeyValueStorage {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\n/** React Native's AppState. */\nexport interface AppStateLike {\n currentState?: string | null;\n addEventListener(type: \"change\", listener: (state: string) => void): { remove(): void } | undefined;\n}\n\n/** React Native's global ErrorUtils. */\nexport interface ErrorUtilsLike {\n getGlobalHandler(): ((error: unknown, isFatal?: boolean) => void) | undefined;\n setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;\n}\n\nexport type FetchLike = (\n url: string,\n init: { method?: string; headers?: Record<string, string>; body?: string; signal?: AbortSignal },\n) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;\n\nexport interface NativeDeps {\n /** Null: everything stays in memory (no offline queue, no install attribution). */\n storage: KeyValueStorage | null;\n fetch: FetchLike;\n appState: AppStateLike | null;\n errorUtils: ErrorUtilsLike | null;\n /** The app and device, read once at start. */\n context: () => AppContext;\n /** The app's id (bundle id), for the `app://<id>/<screen>` address of events. */\n appId?: string;\n /** The screen size in points. */\n screen?: () => { w: number; h: number } | undefined;\n /** The deep link that opened the app (`Linking.getInitialURL`). */\n initialUrl?: () => Promise<string | null>;\n now?: () => number;\n setInterval?: (fn: () => void, ms: number) => unknown;\n clearInterval?: (id: unknown) => void;\n setTimeout?: (fn: () => void, ms: number) => unknown;\n clearTimeout?: (id: unknown) => void;\n log?: (message: string) => void;\n}\n\n/** What the app (and the React hooks) use. */\nexport interface MRRocketClient {\n track(event: string, props?: Record<string, unknown>): void;\n /** A screen view: a route name (\"Settings\") or an Expo Router path (\"/settings\"). */\n screen(name: string, props?: Record<string, unknown>): void;\n identify(id: string, props?: Record<string, unknown>): void;\n /** Forgets the signed-in person (sign-out): a new anonymous id and a new session. */\n reset(): void;\n setConsent(consent: NativeConsent): void;\n captureError(error: unknown): void;\n getFlag(key: string): boolean;\n getFlagVariant(key: string): string | null;\n /** The variant of an experiment (\"control\" until the config is known); records the exposure once per screen. */\n getVariant(key: string): VariantId;\n /** The variant without recording anything (render code); pair it with `expose`. */\n peekVariant(key: string): VariantId;\n /** Records that the person saw `variant` of `key`, once per screen. */\n expose(key: string, variant: VariantId): void;\n /** Sends what is queued now. */\n flush(): Promise<void>;\n /** Resolves once stored state and the config (cached or fetched) are known. */\n ready: Promise<void>;\n isReady(): boolean;\n getDistinctId(): string;\n getConsent(): NativeConsent;\n getConfig(): CockpitConfig | null;\n /** For React: called after every change flags, variants or the consent depend on. Returns the unsubscribe. */\n subscribe(listener: () => void): () => void;\n /** Grows with every such change (useSyncExternalStore's snapshot). */\n getVersion(): number;\n /** Stops timers and listeners and sends what is queued (tests, or an app that tears the SDK down). */\n close(): Promise<void>;\n /** For tests: what waits to be sent. */\n queued(): readonly CockpitEvent[];\n}\n\nexport const DEFAULT_HOST = \"https://app.mrrocket.dev\";\n/** Storage keys. */\nexport const KEYS = {\n id: \"mrrocket:id\",\n queue: \"mrrocket:queue\",\n config: \"mrrocket:config\",\n installed: \"mrrocket:installed\",\n referral: \"mrrocket:referral\",\n} as const;\n\nconst MAX_ERRORS = 20;\nconst MAX_CRUMBS = 20;\nconst MAX_BACKOFF_MS = 5 * 60 * 1000;\nconst REQUEST_TIMEOUT_MS = 15_000;\n/** A batch is sent early once this many events wait. */\nconst EAGER_COUNT = 20;\n\nconst clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);\n\nexport const createClient = (options: MRRocketOptions, deps: NativeDeps): MRRocketClient => {\n const apiKey = (options.apiKey ?? \"\").trim();\n const host = (options.host ?? DEFAULT_HOST).trim().replace(/\\/+$/, \"\");\n const now = deps.now ?? (() => Date.now());\n const setIntervalFn = deps.setInterval ?? ((fn, ms) => setInterval(fn, ms));\n const clearIntervalFn = deps.clearInterval ?? ((id) => clearInterval(id as ReturnType<typeof setInterval>));\n const setTimeoutFn = deps.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));\n const clearTimeoutFn = deps.clearTimeout ?? ((id) => clearTimeout(id as ReturnType<typeof setTimeout>));\n const flushEvery = Math.max(1000, options.flushIntervalMs ?? 10_000);\n const maxQueue = Math.max(10, options.maxQueue ?? 1000);\n const errorsOn = options.errors !== false;\n const storage = deps.storage;\n\n const warned = new Set<string>();\n const warn = (message: string) => {\n if (warned.has(message)) return;\n warned.add(message);\n (deps.log ?? ((m: string) => console.warn(m)))(`[MRRocket] ${message}`);\n };\n const debug = (message: string) => {\n if (options.debug) (deps.log ?? ((m: string) => console.log(m)))(`[MRRocket] ${message}`);\n };\n if (!apiKey) warn(\"No public key. Pass apiKey to MRRocketProvider; nothing is sent until then.\");\n\n let context: AppContext = { platform: \"unknown\" };\n try {\n context = deps.context();\n } catch {\n // Device details are optional.\n }\n const appId = (deps.appId || \"app\").replace(/[^A-Za-z0-9._-]/g, \"\") || \"app\";\n const release = options.release ?? (context.appVersion ? `${context.appVersion}${context.build ? ` (${context.build})` : \"\"}` : undefined);\n const sdk: SdkInfo = { v: SDK_VERSION, errors: errorsOn, platform: context.platform };\n\n let consent: NativeConsent = options.consent ?? \"anonymous\";\n let installId: string | null = null;\n let memoryId = newDistinctId();\n let identified: string | undefined;\n let person: Record<string, unknown> | undefined;\n let config: CockpitConfig | null = null;\n let queue: CockpitEvent[] = [];\n let session: { id: string; last: number } | null = null;\n // The first open's campaign rides on the events of its session only.\n let utm: { session: string; values: Partial<Record<UtmKey, string>> } | null = null;\n let referral: { code: string; clickId: string | null } | null = null;\n let currentScreen: string | undefined;\n let loaded = false;\n let closed = false;\n let isReady = false;\n let version = 0;\n let failures = 0;\n let backoffUntil = 0;\n let inFlight: Promise<void> | null = null;\n let persistTimer: unknown = null;\n let flushTimer: unknown = null;\n let errorTimer: unknown = null;\n let appState = deps.appState?.currentState ?? \"active\";\n let backgroundSince: number | null = null;\n const early: (() => void)[] = [];\n const exposed = new Set<string>();\n const listeners = new Set<() => void>();\n const crumbs: NonNullable<ErrorReport[\"crumbs\"]> = [];\n const errorQueue: ErrorReport[] = [];\n const errorsSeen = new Set<string>();\n let errorsSent = 0;\n\n const changed = () => {\n version++;\n for (const l of [...listeners]) {\n try {\n l();\n } catch {\n // A listener must not stop the others.\n }\n }\n };\n\n const crumb = (type: \"navigation\" | \"click\" | \"lifecycle\", message: string) => {\n crumbs.push({ ts: now(), type, message: clip(message, 120) });\n if (crumbs.length > MAX_CRUMBS) crumbs.shift();\n };\n\n // ---------- Storage (every call may fail: storage full, native module missing) ----------\n\n const get = async (key: string) => {\n if (!storage) return null;\n try {\n return await storage.getItem(key);\n } catch {\n return null;\n }\n };\n const set = async (key: string, value: string) => {\n if (!storage) return;\n try {\n await storage.setItem(key, value);\n } catch {\n warn(\"Could not write to the device storage; events wait in memory only.\");\n }\n };\n const remove = async (key: string) => {\n if (!storage) return;\n try {\n await storage.removeItem(key);\n } catch {\n // Nothing stored.\n }\n };\n\n const persistQueue = async () => {\n if (persistTimer !== null) {\n clearTimeoutFn(persistTimer);\n persistTimer = null;\n }\n if (!storage || consent === \"off\") return;\n if (queue.length) await set(KEYS.queue, JSON.stringify(queue));\n else await remove(KEYS.queue);\n };\n const schedulePersist = () => {\n if (!storage || persistTimer !== null) return;\n persistTimer = setTimeoutFn(() => {\n persistTimer = null;\n void persistQueue();\n }, 1000);\n };\n\n // ---------- Identity and session ----------\n\n const distinctId = () => identified ?? (consent === \"persistent\" && installId ? installId : memoryId);\n // Without the persistent id, events carry an id only after identify(); the server counts everyone else by its daily hash.\n const sendId = () => (consent === \"persistent\" ? distinctId() : identified);\n\n const touchSession = () => {\n const t = now();\n if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };\n else session.last = t;\n return session.id;\n };\n\n const urlOf = (path = currentScreen) => `app://${appId}${path ?? \"/\"}`;\n\n // ---------- Recording ----------\n\n const record = (event: string, props?: Record<string, unknown>, ts = now()) => {\n if (closed) return;\n if (!loaded) {\n early.push(() => record(event, props, ts));\n return;\n }\n if (consent === \"off\" || !apiKey) return;\n const sessionId = touchSession();\n const ref = referral && carriesReferral(event) ? referral : null;\n const size = deps.screen?.();\n queue.push({\n event,\n props: ref ? { ...props, $referral: ref } : props,\n distinctId: sendId(),\n sessionId,\n ts,\n url: urlOf(),\n ...(utm && utm.session === sessionId ? utm.values : {}),\n ...(size ? { screen: { w: Math.round(size.w), h: Math.round(size.h) } } : {}),\n app: context,\n });\n if (queue.length > maxQueue) queue.splice(0, queue.length - maxQueue);\n schedulePersist();\n if (queue.length >= EAGER_COUNT) void flush();\n };\n\n // ---------- Sending ----------\n\n const post = async (path: string, body: string): Promise<number> => {\n const ctl = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ctl ? setTimeoutFn(() => ctl.abort(), REQUEST_TIMEOUT_MS) : null;\n try {\n const r = await deps.fetch(`${host}${path}`, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body, signal: ctl?.signal });\n return r.status;\n } catch {\n return 0;\n } finally {\n if (timer !== null) clearTimeoutFn(timer);\n }\n };\n\n const sendQueue = async () => {\n if (!queue.length || consent === \"off\" || !apiKey) return;\n if (now() < backoffUntil) return;\n const snapshot = queue.slice();\n const requests = batchRequests(apiKey, snapshot, sdk);\n const included = new Set(requests.flatMap((r) => r.events));\n const done = new Set<CockpitEvent>();\n for (const r of requests) {\n const status = await post(\"/api/sdk/events\", r.body);\n if (status >= 200 && status < 300) {\n r.events.forEach((e) => done.add(e));\n failures = 0;\n backoffUntil = 0;\n } else if (status >= 400 && status < 500 && status !== 408 && status !== 429) {\n // Will never be accepted (a wrong key, a malformed event): drop instead of retrying forever.\n r.events.forEach((e) => done.add(e));\n warn(`Events were rejected with ${status}. Check the public key (MRRocket › Settings › Products) and the host.`);\n } else {\n failures++;\n backoffUntil = now() + Math.min(MAX_BACKOFF_MS, 1000 * 2 ** failures);\n debug(`Sending failed (${status || \"offline\"}); next try in ${Math.round((backoffUntil - now()) / 1000)} s.`);\n break;\n }\n }\n // Sent, rejected, or too large to ever fit in a request: out of the queue. Newer events stay.\n queue = queue.filter((e) => !done.has(e) && (included.has(e) || !snapshot.includes(e)));\n await persistQueue();\n };\n\n // A flush asked for while one runs follows it, so events queued meanwhile go too.\n let nextFlush: Promise<void> | null = null;\n const flush = (): Promise<void> => {\n if (inFlight)\n return (nextFlush ??= inFlight.then(() => {\n nextFlush = null;\n return flush();\n }));\n inFlight = sendQueue().finally(() => {\n inFlight = null;\n });\n return inFlight;\n };\n\n const startTimer = () => {\n if (flushTimer !== null || closed) return;\n flushTimer = setIntervalFn(() => void flush(), flushEvery);\n };\n const stopTimer = () => {\n if (flushTimer === null) return;\n clearIntervalFn(flushTimer);\n flushTimer = null;\n };\n\n // ---------- Errors ----------\n\n const sendErrors = async () => {\n if (errorTimer !== null) {\n clearTimeoutFn(errorTimer);\n errorTimer = null;\n }\n while (errorQueue.length) {\n // Four full stacks stay under the server's 64 KB.\n const batch = errorQueue.splice(0, 4);\n const status = await post(\"/api/sdk/errors\", JSON.stringify({ key: apiKey, sdk, errors: batch }));\n if (status >= 400 && status < 500 && status !== 429) warn(`Error reports were rejected with ${status}. Check the public key and the host.`);\n }\n };\n\n const capture = (error: unknown, mechanism: ErrorReport[\"mechanism\"], fatal = false) => {\n if (!errorsOn || closed || consent === \"off\" || !apiKey || errorsSent >= MAX_ERRORS) return;\n const d = describeError(error);\n if (/\\/api\\/sdk\\/errors/.test(d.message)) return;\n const mark = `${d.type}:${d.message}`;\n if (errorsSeen.has(mark)) return;\n errorsSeen.add(mark);\n errorsSent++;\n const who = sendId();\n errorQueue.push({\n ...d,\n mechanism,\n handled: mechanism === \"manual\",\n url: urlOf(),\n ts: now(),\n crumbs: crumbs.slice(),\n ...(release ? { release } : {}),\n ...(who ? { distinctId: who } : {}),\n ...(session ? { sessionId: session.id } : {}),\n });\n // A fatal error ends the app: send at once (it may still not make it); others wait for company.\n if (fatal) void sendErrors();\n else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2000);\n };\n\n let previousHandler: ((error: unknown, isFatal?: boolean) => void) | undefined;\n let handlerInstalled = false;\n const installErrorHandler = () => {\n if (!errorsOn || !deps.errorUtils || handlerInstalled) return;\n handlerInstalled = true;\n previousHandler = deps.errorUtils.getGlobalHandler();\n deps.errorUtils.setGlobalHandler((error, isFatal) => {\n try {\n capture(error, \"onerror\", !!isFatal);\n } catch {\n // Reporting must never change how the app handles its error.\n }\n previousHandler?.(error, isFatal);\n });\n };\n\n // ---------- Lifecycle ----------\n\n let appStateSub: { remove(): void } | undefined;\n const onAppState = (next: string) => {\n if (closed) return;\n const prev = appState;\n appState = next;\n if (next === \"active\" && prev !== \"active\") {\n startTimer();\n if (backgroundSince !== null) {\n // Back from the background: a new session after 30 minutes away (the session rule does it by itself).\n backgroundSince = null;\n crumb(\"lifecycle\", \"app_open\");\n if (options.lifecycle !== false) record(\"$app_open\", { cold: false });\n }\n } else if (next === \"background\" && prev !== \"background\") {\n backgroundSince = now();\n crumb(\"lifecycle\", \"app_background\");\n if (options.lifecycle !== false) record(\"$app_background\");\n stopTimer();\n // The system gives a few seconds in the background: send now, and keep the rest on the device.\n void persistQueue();\n void flush();\n if (errorQueue.length) void sendErrors();\n }\n };\n\n // ---------- Config ----------\n\n const fetchConfig = async () => {\n if (!apiKey) return;\n try {\n const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`, { method: \"GET\" });\n if (!r.ok) {\n warn(`Config request answered ${r.status}. Check the public key and the host.`);\n return;\n }\n const c = (await r.json()) as CockpitConfig | null;\n if (c && Array.isArray(c.experiments)) {\n config = c;\n if (consent !== \"off\") await set(KEYS.config, JSON.stringify(c));\n changed();\n }\n } catch {\n debug(\"Config not reachable; using the cached one.\");\n }\n };\n\n // ---------- Start ----------\n\n let resolveReady: () => void = () => {};\n const ready = new Promise<void>((r) => {\n resolveReady = r;\n });\n const markReady = () => {\n if (isReady) return;\n isReady = true;\n resolveReady();\n changed();\n };\n\n const start = async () => {\n let firstOpen = false;\n if (consent !== \"off\") {\n const [id, stored, cached, installed, ref] = await Promise.all([\n get(KEYS.id),\n get(KEYS.queue),\n get(KEYS.config),\n get(KEYS.installed),\n get(KEYS.referral),\n ]);\n if (consent === \"persistent\") {\n installId = id || newDistinctId();\n if (!id) await set(KEYS.id, installId);\n } else if (id) await remove(KEYS.id);\n try {\n const old = stored ? (JSON.parse(stored) as CockpitEvent[]) : [];\n if (Array.isArray(old)) queue = [...old.filter((e) => e && typeof e.event === \"string\"), ...queue].slice(-maxQueue);\n } catch {\n await remove(KEYS.queue);\n }\n try {\n const c = cached ? (JSON.parse(cached) as CockpitConfig) : null;\n if (c && Array.isArray(c.experiments) && !config) config = c;\n } catch {\n await remove(KEYS.config);\n }\n try {\n const r = ref && consent === \"persistent\" ? (JSON.parse(ref) as { code: string; clickId: string | null }) : null;\n if (r && typeof r.code === \"string\") referral = r;\n } catch {\n // Ignore a damaged referral.\n }\n // Without storage there is no telling a first open from any other: no $install then.\n firstOpen = !!storage && !installed;\n if (firstOpen) await set(KEYS.installed, String(now()));\n } else {\n await Promise.all(Object.values(KEYS).map(remove));\n }\n if (closed) return;\n loaded = true;\n if (config) markReady();\n\n // The first open: the deep link's campaign and referral (no store data, no advertising id).\n let install: Record<string, unknown> | null = null;\n if (firstOpen) {\n install = {};\n if (options.attribution !== false && deps.initialUrl) {\n const url = await deps.initialUrl().catch(() => null);\n if (url) {\n const q = parseQuery(url);\n const values = utmFrom((k) => q[k]);\n if (Object.keys(values).length) utm = { session: touchSession(), values };\n const found = referralFrom((k) => q[k]);\n if (found) {\n referral = { code: found.code, clickId: found.clickId };\n install.$referral = referral;\n if (consent === \"persistent\") await set(KEYS.referral, JSON.stringify(referral));\n }\n install.link = clip(url.split(\"?\")[0], 300);\n }\n }\n }\n\n crumb(\"lifecycle\", \"app_open\");\n if (options.lifecycle !== false) record(\"$app_open\", { cold: true });\n if (install) record(\"$install\", install);\n for (const run of early.splice(0)) run();\n\n if (deps.appState) appStateSub = deps.appState.addEventListener(\"change\", onAppState);\n installErrorHandler();\n if (appState === \"active\" || !deps.appState) startTimer();\n if (queue.length) void flush();\n\n await fetchConfig();\n markReady();\n };\n void start().catch(() => {\n loaded = true;\n markReady();\n });\n\n // ---------- The API ----------\n\n const client: MRRocketClient = {\n track: (event, props) => {\n if (typeof event !== \"string\" || !event) return;\n record(event, props);\n },\n screen: (name, props) => {\n if (typeof name !== \"string\" || !name.trim()) return;\n const path = (name.startsWith(\"/\") ? name : `/${name}`).slice(0, 300);\n if (path === currentScreen && !props) return;\n const previous = currentScreen;\n currentScreen = path;\n crumb(\"navigation\", path);\n record(\"$screen\", { ...props, path, screen: name, ...(previous ? { previous } : {}) });\n },\n identify: (id, props) => {\n if (typeof id !== \"string\" || !id) return;\n if (!loaded) {\n early.push(() => client.identify(id, props));\n return;\n }\n const previous = sendId();\n identified = id;\n person = { ...props, id };\n record(\"$identify\", { ...props, id, ...(previous && previous !== id ? { previousId: previous } : {}) });\n if (consent === \"persistent\") void set(KEYS.id, id);\n changed();\n },\n reset: () => {\n if (!loaded) {\n early.push(() => client.reset());\n return;\n }\n identified = undefined;\n person = undefined;\n session = null;\n utm = null;\n exposed.clear();\n memoryId = newDistinctId();\n if (consent === \"persistent\") {\n installId = newDistinctId();\n void set(KEYS.id, installId);\n }\n changed();\n },\n setConsent: (next) => {\n if (next !== \"anonymous\" && next !== \"persistent\" && next !== \"off\") {\n warn(`setConsent: \"${String(next)}\" is not a consent mode. Use \"anonymous\", \"persistent\" or \"off\".`);\n return;\n }\n if (!loaded) {\n early.push(() => client.setConsent(next));\n return;\n }\n if (next === consent) return;\n const before = consent;\n consent = next;\n if (next === \"off\") {\n queue = [];\n session = null;\n errorQueue.length = 0;\n void Promise.all(Object.values(KEYS).map(remove));\n } else if (next === \"anonymous\") {\n installId = null;\n void remove(KEYS.id);\n void remove(KEYS.referral);\n } else {\n installId = identified ?? newDistinctId();\n void set(KEYS.id, installId);\n if (referral) void set(KEYS.referral, JSON.stringify(referral));\n }\n // Switching tracking on records the screen the person is on.\n if (before === \"off\" && currentScreen && loaded) record(\"$screen\", { path: currentScreen, screen: currentScreen });\n changed();\n },\n captureError: (error) => capture(error, \"manual\"),\n getFlag: (key) => flagFor(key, config, distinctId(), { ...person }) !== false,\n getFlagVariant: (key) => {\n const v = flagFor(key, config, distinctId(), { ...person });\n return typeof v === \"string\" ? v : null;\n },\n getVariant: (key) => {\n const variant = resolveVariant(key, config, distinctId(), {});\n client.expose(key, variant);\n return variant;\n },\n peekVariant: (key) => resolveVariant(key, config, distinctId(), {}),\n expose: (key, variant) => {\n if (!config || consent === \"off\") return;\n const mark = `${currentScreen ?? \"/\"}|${key}`;\n if (exposed.has(mark)) return;\n exposed.add(mark);\n record(\"$exposure\", { experiment: key, variant });\n },\n flush,\n ready,\n isReady: () => isReady,\n getDistinctId: distinctId,\n getConsent: () => consent,\n getConfig: () => config,\n subscribe: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n getVersion: () => version,\n close: async () => {\n if (closed) return;\n stopTimer();\n appStateSub?.remove();\n if (handlerInstalled && deps.errorUtils && previousHandler) deps.errorUtils.setGlobalHandler(previousHandler);\n await flush();\n await sendErrors();\n await persistQueue();\n closed = true;\n },\n queued: () => queue,\n };\n return client;\n};\n\n/** The name of the screen a React Navigation state shows: the focused route, followed into nested navigators. */\nexport const activeRouteName = (state: unknown): string | undefined => {\n let s = state as { index?: number; routes?: { name?: string; state?: unknown }[] } | undefined;\n let name: string | undefined;\n for (let depth = 0; s && Array.isArray(s.routes) && s.routes.length && depth < 20; depth++) {\n const route = s.routes[typeof s.index === \"number\" ? s.index : s.routes.length - 1];\n if (!route) break;\n name = route.name ?? name;\n s = route.state as typeof s;\n }\n return name;\n};\n","/**\n * The real platform for the React Native client: React Native itself, plus optional modules the app may have installed.\n * Each optional `require` sits directly in its own `try` block: that is how Metro knows the module is optional and\n * bundles the app without it when it is not installed (and how the CommonJS build keeps the call literal).\n *\n * - `@react-native-async-storage/async-storage`: the offline queue, the install id (\"persistent\" consent), the cached\n * config and the first-open marker. Without it everything stays in memory for the app's run.\n * - `expo-device`, `expo-application`, `expo-localization`: model, OS version, app version and build, locale.\n * Without them the SDK falls back to what React Native knows (platform, OS version, the JS locale).\n */\nimport type { AppContext } from \"../core\";\nimport type { AppStateLike, ErrorUtilsLike, FetchLike, KeyValueStorage, NativeDeps } from \"./client\";\n\n/* eslint-disable @typescript-eslint/no-require-imports */\ndeclare const require: (id: string) => unknown;\n\ninterface ReactNativeModule {\n Platform: { OS: string; Version?: string | number; isPad?: boolean; constants?: { Model?: string; Manufacturer?: string } };\n AppState: AppStateLike;\n Linking: { getInitialURL(): Promise<string | null> };\n Dimensions: { get(dim: \"screen\" | \"window\"): { width: number; height: number } };\n}\n\ninterface ExpoDevice {\n modelName?: string | null;\n osVersion?: string | null;\n deviceType?: number | null;\n}\ninterface ExpoApplication {\n nativeApplicationVersion?: string | null;\n nativeBuildVersion?: string | null;\n applicationId?: string | null;\n}\ninterface ExpoLocalization {\n getLocales?: () => { languageTag?: string }[];\n}\n\nconst RN = require(\"react-native\") as ReactNativeModule;\n\nlet asyncStorageModule: unknown = null;\ntry {\n asyncStorageModule = require(\"@react-native-async-storage/async-storage\");\n} catch {\n asyncStorageModule = null;\n}\nlet expoDevice: ExpoDevice | null = null;\ntry {\n expoDevice = require(\"expo-device\") as ExpoDevice;\n} catch {\n expoDevice = null;\n}\nlet expoApplication: ExpoApplication | null = null;\ntry {\n expoApplication = require(\"expo-application\") as ExpoApplication;\n} catch {\n expoApplication = null;\n}\nlet expoLocalization: ExpoLocalization | null = null;\ntry {\n expoLocalization = require(\"expo-localization\") as ExpoLocalization;\n} catch {\n expoLocalization = null;\n}\n\n/** AsyncStorage's default export, or null when it is not installed. */\nconst asyncStorage = (): KeyValueStorage | null => {\n const m = asyncStorageModule as { default?: KeyValueStorage } & Partial<KeyValueStorage>;\n const s = (m?.default ?? m) as Partial<KeyValueStorage> | null;\n return s && typeof s.getItem === \"function\" && typeof s.setItem === \"function\" && typeof s.removeItem === \"function\" ? (s as KeyValueStorage) : null;\n};\n\nconst DEVICE_TYPES: Record<number, string> = { 1: \"phone\", 2: \"tablet\", 3: \"desktop\", 4: \"tv\" };\n\nconst text = (v: unknown, max = 80) => (typeof v === \"string\" && v.trim() ? v.trim().slice(0, max) : typeof v === \"number\" ? String(v) : undefined);\n\n/** The app and device, from the Expo modules when installed, else from React Native. Nothing that identifies a person. */\nexport const readContext = (): AppContext => {\n const p = RN.Platform;\n let locale: string | undefined;\n try {\n locale = text(expoLocalization?.getLocales?.()[0]?.languageTag, 35);\n } catch {\n locale = undefined;\n }\n if (!locale) {\n try {\n locale = text(Intl.DateTimeFormat().resolvedOptions().locale, 35);\n } catch {\n locale = undefined;\n }\n }\n const ctx: AppContext = {\n platform: p.OS,\n appVersion: text(expoApplication?.nativeApplicationVersion),\n build: text(expoApplication?.nativeBuildVersion),\n osVersion: text(expoDevice?.osVersion) ?? text(p.Version),\n deviceModel: text(expoDevice?.modelName) ?? text(p.constants?.Model),\n deviceType: (expoDevice?.deviceType ? DEVICE_TYPES[expoDevice.deviceType] : undefined) ?? (p.isPad ? \"tablet\" : undefined),\n locale,\n };\n for (const k of Object.keys(ctx) as (keyof AppContext)[]) if (ctx[k] === undefined) delete ctx[k];\n return ctx;\n};\n\n/** The dependencies of the client on a real device. `storage` overrides AsyncStorage (pass null for memory only). */\nexport const nativeDeps = (overrides: { storage?: KeyValueStorage | null } = {}): NativeDeps => ({\n storage: overrides.storage !== undefined ? overrides.storage : asyncStorage(),\n fetch: ((url, init) => fetch(url, init)) as FetchLike,\n appState: RN.AppState,\n errorUtils: ((globalThis as { ErrorUtils?: ErrorUtilsLike }).ErrorUtils ?? null) as ErrorUtilsLike | null,\n context: readContext,\n appId: text(expoApplication?.applicationId, 150),\n screen: () => {\n const s = RN.Dimensions.get(\"screen\");\n return s ? { w: s.width, h: s.height } : undefined;\n },\n initialUrl: () => RN.Linking.getInitialURL(),\n});\n\n/** Whether the optional modules were found (for the debug log and the docs' checklist). */\nexport const optionalModules = () => ({\n asyncStorage: !!asyncStorage(),\n expoDevice: !!expoDevice,\n expoApplication: !!expoApplication,\n expoLocalization: !!expoLocalization,\n});\n"],"mappings":"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","names":["clip"]}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -40,6 +40,12 @@ export declare const readReferralParams: (search: string, param?: string) => {
40
40
  clickId: string | null;
41
41
  direct: boolean;
42
42
  } | null;
43
+ /** The same from a parameter lookup, for runtimes without `URLSearchParams` (React Native deep links). */
44
+ export declare const referralFrom: (get: (name: string) => string | null | undefined, param?: string) => {
45
+ code: string;
46
+ clickId: string | null;
47
+ direct: boolean;
48
+ } | null;
43
49
  /** `code.clickId.at`: dots never occur in a code or a click id. */
44
50
  export declare const encodeReferral: (r: Referral) => string;
45
51
  /** A stored referral, or null when it is malformed or older than `maxDays`. */
package/dist/referral.js CHANGED
@@ -8,12 +8,15 @@ var CODE = /^[A-Za-z0-9_-]{1,64}$/;
8
8
  var DAY_MS = 864e5;
9
9
  var readReferralParams = (search, param = "ref") => {
10
10
  const q = new URLSearchParams(search);
11
- const marked = q.get(REF_PARAM);
11
+ return referralFrom((k) => q.get(k), param);
12
+ };
13
+ var referralFrom = (get, param = "ref") => {
14
+ const marked = get(REF_PARAM);
12
15
  if (marked && CODE.test(marked)) {
13
- const click = q.get(CLICK_PARAM);
16
+ const click = get(CLICK_PARAM);
14
17
  return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };
15
18
  }
16
- const direct = q.get(param);
19
+ const direct = get(param);
17
20
  return direct && CODE.test(direct) ? { code: direct, clickId: null, direct: true } : null;
18
21
  };
19
22
  var encodeReferral = (r) => `${r.code}.${r.clickId ?? ""}.${r.at}`;
@@ -121,6 +124,7 @@ export {
121
124
  readReferralCookie,
122
125
  readReferralParams,
123
126
  referralForCheckout,
127
+ referralFrom,
124
128
  referralLink,
125
129
  resetReferral,
126
130
  storeReferral
@@ -1 +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":"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","names":[]}
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 return referralFrom((k) => q.get(k), param);\n};\n\n/** The same from a parameter lookup, for runtimes without `URLSearchParams` (React Native deep links). */\nexport const referralFrom = (get: (name: string) => string | null | undefined, param = \"ref\"): { code: string; clickId: string | null; direct: boolean } | null => {\n const marked = get(REF_PARAM);\n if (marked && CODE.test(marked)) {\n const click = get(CLICK_PARAM);\n return { code: marked, clickId: click && CODE.test(click) ? click : null, direct: false };\n }\n const direct = 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":"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","names":[]}
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CockpitConfig, type VariantId } from "./core.js";
1
+ import { type CockpitConfig, type FlagPerson, type VariantId } from "./core.js";
2
2
  /**
3
3
  * Server helpers for the App Router: load the experiments once per request, then render the right variant on the
4
4
  * first paint (no flicker) and hand the same config to `<CockpitProvider config>` so the browser agrees.
@@ -40,6 +40,18 @@ interface RequestInfo {
40
40
  }
41
41
  /** The variant of one experiment for this request. */
42
42
  export declare const getVariant: (key: string, { config, searchParams, cookies, distinctId }: RequestInfo) => VariantId;
43
+ /**
44
+ * A feature flag for this request: true while it is on for the visitor, false while it is off or unknown (or MRRocket
45
+ * could not be reached). `person` takes the properties the flag's audience reads ("Email", "Plan" …), as you pass them
46
+ * to identify(). For a multivariate flag use `getFlagVariant`.
47
+ */
48
+ export declare const getFlag: (key: string, { config, cookies, distinctId, person }: RequestInfo & {
49
+ person?: FlagPerson;
50
+ }) => boolean;
51
+ /** The variant of a multivariate flag for this request, or null while it is off. */
52
+ export declare const getFlagVariant: (key: string, { config, cookies, distinctId, person }: RequestInfo & {
53
+ person?: FlagPerson;
54
+ }) => string | null;
43
55
  /** Every text change for this request, to render `<Text>` defaults server-side. */
44
56
  export declare const getTexts: ({ config, searchParams, cookies, distinctId }: RequestInfo) => Record<string, string>;
45
57
  export * from "./core.js";
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/server.ts
2
- import { EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, envKey, parsePreview, resolveHost, resolveVariant, textChanges } from "./core.js";
2
+ import { EMBED_PARAM, ID_COOKIE, PREVIEW_PARAM, envKey, flagFor, parsePreview, resolveHost, resolveVariant, textChanges } from "./core.js";
3
3
  export * from "./core.js";
4
4
  import { REFERRAL_COOKIE, readReferralCookie, referralForCheckout, referralLink } from "./referral.js";
5
5
  var CONFIG_TIMEOUT_MS = 2500;
@@ -28,11 +28,18 @@ var previewFrom = (searchParams) => {
28
28
  };
29
29
  var distinctIdFrom = (cookies) => cookies?.get(ID_COOKIE)?.value ?? "anonymous";
30
30
  var getVariant = (key, { config, searchParams, cookies, distinctId }) => resolveVariant(key, config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);
31
+ var getFlag = (key, { config, cookies, distinctId, person }) => flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person) !== false;
32
+ var getFlagVariant = (key, { config, cookies, distinctId, person }) => {
33
+ const v = flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person);
34
+ return typeof v === "string" ? v : null;
35
+ };
31
36
  var getTexts = ({ config, searchParams, cookies, distinctId }) => textChanges(config, distinctId ?? distinctIdFrom(cookies), previewFrom(searchParams).preview);
32
37
  export {
33
38
  REFERRAL_COOKIE,
34
39
  distinctIdFrom,
35
40
  getCockpitConfig,
41
+ getFlag,
42
+ getFlagVariant,
36
43
  getTexts,
37
44
  getVariant,
38
45
  previewFrom,
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { type CockpitConfig, EMBED_PARAM, type FlagPerson, ID_COOKIE, PREVIEW_PARAM, type VariantId, envKey, flagFor, 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/**\n * A feature flag for this request: true while it is on for the visitor, false while it is off or unknown (or MRRocket\n * could not be reached). `person` takes the properties the flag's audience reads (\"Email\", \"Plan\" …), as you pass them\n * to identify(). For a multivariate flag use `getFlagVariant`.\n */\nexport const getFlag = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): boolean =>\n flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person) !== false;\n\n/** The variant of a multivariate flag for this request, or null while it is off. */\nexport const getFlagVariant = (key: string, { config, cookies, distinctId, person }: RequestInfo & { person?: FlagPerson }): string | null => {\n const v = flagFor(key, config, distinctId ?? distinctIdFrom(cookies), person);\n return typeof v === \"string\" ? v : null;\n};\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,aAA8B,WAAW,eAA+B,QAAQ,SAAS,cAAc,aAAa,gBAAgB,mBAAmB;AAsFpL,cAAc;AAEd,SAA+C,iBAAiB,oBAAoB,qBAAqB,oBAAoB;AA3E7H,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;AAOjG,IAAM,UAAU,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MACvE,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM,MAAM;AAGrE,IAAM,iBAAiB,CAAC,KAAa,EAAE,QAAQ,SAAS,YAAY,OAAO,MAA4D;AAC1I,QAAM,IAAI,QAAQ,KAAK,QAAQ,cAAc,eAAe,OAAO,GAAG,MAAM;AAC5E,SAAO,OAAO,MAAM,WAAW,IAAI;AACvC;AAGO,IAAM,WAAW,CAAC,EAAE,QAAQ,cAAc,SAAS,WAAW,MACjE,YAAY,QAAQ,cAAc,eAAe,OAAO,GAAG,YAAY,YAAY,EAAE,OAAO;","names":[]}
package/dist/styles.d.ts CHANGED
@@ -1,4 +1,4 @@
1
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";
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-root .ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}\n.ckw-root .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
3
  /** Puts the stylesheet into the page once, however many widgets mount. */
4
4
  export declare const useStyles: () => void;
package/dist/styles.js CHANGED
@@ -119,8 +119,8 @@ font-size:14px;line-height:1.45;font-family:inherit;color:var(--ckw-fg);box-sizi
119
119
  .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}
120
120
  .ckw-num:hover{border-color:var(--ckw-accent)}
121
121
  .ckw-num[aria-pressed=true]{background:var(--ckw-accent);color:var(--ckw-accent-ink);border-color:var(--ckw-accent)}
122
- .ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}
123
- .ckw-star[data-on=true]{color:#f5a524}
122
+ .ckw-root .ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}
123
+ .ckw-root .ckw-star[data-on=true]{color:#f5a524}
124
124
  .ckw-star svg{width:28px;height:28px}
125
125
  .ckw-lh{display:flex;justify-content:space-between;color:var(--ckw-fg3);font-size:11px;margin-top:4px}
126
126
  .ckw-opts{display:flex;flex-direction:column;gap:6px}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/styles.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\nconst STYLE_ID = \"ckw-styles\";\n\n/** The one stylesheet for every Cockpit widget. Everything is scoped under `.ckw-root` and prefixed `ckw-`. */\nexport 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`;\n\n/** Puts the stylesheet into the page once, however many widgets mount. */\nexport const useStyles = () => {\n useEffect(() => {\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = CSS;\n document.head.appendChild(el);\n }, []);\n};\n"],"mappings":";;;AAEA,SAAS,iBAAiB;AAE1B,IAAM,WAAW;AAGV,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkIZ,IAAM,YAAY,MAAM;AAC3B,YAAU,MAAM;AACZ,QAAI,SAAS,eAAe,QAAQ,EAAG;AACvC,UAAM,KAAK,SAAS,cAAc,OAAO;AACzC,OAAG,KAAK;AACR,OAAG,cAAc;AACjB,aAAS,KAAK,YAAY,EAAE;AAAA,EAChC,GAAG,CAAC,CAAC;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/styles.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\nconst STYLE_ID = \"ckw-styles\";\n\n/** The one stylesheet for every Cockpit widget. Everything is scoped under `.ckw-root` and prefixed `ckw-`. */\nexport 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-root .ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}\n.ckw-root .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`;\n\n/** Puts the stylesheet into the page once, however many widgets mount. */\nexport const useStyles = () => {\n useEffect(() => {\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = CSS;\n document.head.appendChild(el);\n }, []);\n};\n"],"mappings":";;;AAEA,SAAS,iBAAiB;AAE1B,IAAM,WAAW;AAGV,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkIZ,IAAM,YAAY,MAAM;AAC3B,YAAU,MAAM;AACZ,QAAI,SAAS,eAAe,QAAQ,EAAG;AACvC,UAAM,KAAK,SAAS,cAAc,OAAO;AACzC,OAAG,KAAK;AACR,OAAG,cAAc;AACjB,aAAS,KAAK,YAAY,EAAE;AAAA,EAChC,GAAG,CAAC,CAAC;AACT;","names":[]}