@mrrocket/sdk 0.17.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +24 -1
  3. package/dist/api-client.d.ts +28 -4
  4. package/dist/api-client.js +30 -13
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +15 -3
  7. package/dist/browser/mrrocket-messenger.js +38 -5
  8. package/dist/browser/mrrocket.js +1 -1
  9. package/dist/browser.d.ts +2 -2
  10. package/dist/browser.js +2 -1
  11. package/dist/browser.js.map +1 -1
  12. package/dist/client.d.ts +28 -3
  13. package/dist/client.js +37 -5
  14. package/dist/client.js.map +1 -1
  15. package/dist/context.d.ts +3 -1
  16. package/dist/context.js.map +1 -1
  17. package/dist/core.d.ts +7 -1
  18. package/dist/core.js +1 -1
  19. package/dist/core.js.map +1 -1
  20. package/dist/feedback.js +161 -119
  21. package/dist/feedback.js.map +1 -1
  22. package/dist/finish-web.d.ts +18 -0
  23. package/dist/finish-web.js +139 -0
  24. package/dist/finish-web.js.map +1 -0
  25. package/dist/flows-ui.d.ts +1 -10
  26. package/dist/flows-ui.js +175 -48
  27. package/dist/flows-ui.js.map +1 -1
  28. package/dist/flows-web.d.ts +8 -1
  29. package/dist/flows-web.js +10 -1
  30. package/dist/flows-web.js.map +1 -1
  31. package/dist/flows.d.ts +193 -9
  32. package/dist/flows.js +284 -38
  33. package/dist/flows.js.map +1 -1
  34. package/dist/i18n.d.ts +22 -0
  35. package/dist/i18n.js +74 -0
  36. package/dist/i18n.js.map +1 -0
  37. package/dist/icons.d.ts +24 -0
  38. package/dist/icons.js +61 -0
  39. package/dist/icons.js.map +1 -0
  40. package/dist/index.d.ts +6 -2
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/messenger-ui.d.ts +9 -3
  44. package/dist/messenger-ui.js +388 -191
  45. package/dist/messenger-ui.js.map +1 -1
  46. package/dist/messenger.d.ts +3 -1
  47. package/dist/messenger.js +3 -2
  48. package/dist/messenger.js.map +1 -1
  49. package/dist/onboarding-state.d.ts +23 -0
  50. package/dist/onboarding-state.js +43 -0
  51. package/dist/onboarding-state.js.map +1 -0
  52. package/dist/paywall.js +2 -1
  53. package/dist/paywall.js.map +1 -1
  54. package/dist/react-native/index.d.cts +239 -19
  55. package/dist/react-native/index.d.ts +239 -19
  56. package/dist/react-native/index.js +1305 -518
  57. package/dist/react-native/index.js.map +1 -1
  58. package/dist/server.d.ts +4 -2
  59. package/dist/server.js +2 -2
  60. package/dist/server.js.map +1 -1
  61. package/dist/styles-css.d.ts +1 -1
  62. package/dist/styles-css.js +35 -2
  63. package/dist/styles-css.js.map +1 -1
  64. package/dist/survey.js +18 -14
  65. package/dist/survey.js.map +1 -1
  66. package/dist/ui.d.ts +2 -0
  67. package/dist/ui.js +15 -12
  68. package/dist/ui.js.map +1 -1
  69. package/package.json +6 -3
package/dist/flows-ui.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // src/flows-ui.ts
2
- import { discounted, mediaOf, money, offerButton, videoEmbed } from "./flows.js";
2
+ import { playFinish } from "./finish-web.js";
3
+ import { OTHER_PREFIX, answerOk, forMonths, monthsWord, scaleOf, discounted, mediaOf, money, offerButton, videoEmbed } from "./flows.js";
4
+ import { onWords, t, tf, wordsLang } from "./i18n.js";
5
+ import { FLOW_ICONS, iconOf } from "./icons.js";
3
6
  var STYLE_ID = "ckf-styles";
4
7
  var FLOWS_CSS = `
5
8
  .ckf-root{--ckf-accent:#3f76ff;--ckf-accent-ink:#fff;--ckf-bg:#fff;--ckf-fg:#1c1d20;--ckf-muted:#5d6068;--ckf-soft:#f4f5f7;--ckf-line:#e4e5e8;--ckf-danger:#d92d20;--ckf-radius:14px;--ckf-font:inherit;--ckf-shadow:0 16px 40px -12px rgba(16,18,24,.28),0 0 0 1px rgba(16,18,24,.07)}
@@ -28,6 +31,11 @@ var FLOWS_CSS = `
28
31
  .ckf-radio{width:16px;height:16px;flex:none;border-radius:50%;border:1.5px solid var(--ckf-line);display:inline-flex;align-items:center;justify-content:center}
29
32
  .ckf-opt[aria-checked=true] .ckf-radio{border-color:var(--ckf-accent)}
30
33
  .ckf-opt[aria-checked=true] .ckf-radio::after{content:"";width:8px;height:8px;border-radius:50%;background:var(--ckf-accent)}
34
+ .ckf-scale{display:flex;gap:4px}
35
+ .ckf-num{flex:1;min-width:0;height:38px;padding:0;border:1px solid var(--ckf-line);border-radius:8px;background:var(--ckf-bg);cursor:pointer;font-weight:500}
36
+ .ckf-num:hover{background:var(--ckf-soft)}
37
+ .ckf-num[aria-checked=true]{border-color:var(--ckf-accent);background:var(--ckf-accent);color:var(--ckf-accent-ink)}
38
+ .ckf-ends{display:flex;justify-content:space-between;gap:12px;margin-top:6px}
31
39
  .ckf-box{border-radius:10px;background:var(--ckf-soft);padding:12px;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
32
40
  .ckf-big{font-size:22px;font-weight:650;font-variant-numeric:tabular-nums}
33
41
  .ckf-strike{color:var(--ckf-muted);text-decoration:line-through;font-variant-numeric:tabular-nums}
@@ -53,6 +61,12 @@ var FLOWS_CSS = `
53
61
  [data-side=bottom]>.ckf-arrow{top:0;left:var(--ckf-ap)}[data-side=top]>.ckf-arrow{top:100%;left:var(--ckf-ap)}
54
62
  [data-side=right]>.ckf-arrow{left:0;top:var(--ckf-ap)}[data-side=left]>.ckf-arrow{left:100%;top:var(--ckf-ap)}
55
63
  .ckf-check .ckf-go{margin-left:auto;align-self:center;color:var(--ckf-accent);font-weight:700}
64
+ .ckf-ic{width:20px;height:20px;flex:none;display:inline-flex;align-items:center;justify-content:center;color:var(--ckf-accent);font-size:16px;line-height:1}
65
+ .ckf-ic svg{width:20px;height:20px}.ckf-ic img{width:20px;height:20px;border-radius:5px;object-fit:cover}
66
+ .ckf-hic{width:40px;height:40px;border-radius:12px;background:color-mix(in srgb,var(--ckf-accent) 12%,var(--ckf-bg));display:inline-flex;align-items:center;justify-content:center;color:var(--ckf-accent);font-size:22px;line-height:1}
67
+ .ckf-hic svg{width:22px;height:22px}.ckf-hic img{width:40px;height:40px;border-radius:12px;object-fit:cover}
68
+ .ckf-input{width:100%;height:40px;border:1px solid var(--ckf-line);border-radius:10px;background:var(--ckf-bg);padding:0 12px}
69
+ .ckf-input::placeholder{color:var(--ckf-muted)}
56
70
  .ckf-ring{position:fixed;z-index:2147483000;pointer-events:none;border-radius:10px;box-shadow:0 0 0 2px var(--ckf-accent),0 0 0 6px color-mix(in srgb,var(--ckf-accent) 25%,transparent);transition:all .15s ease-out}
57
71
  .ckf-check{display:flex;align-items:flex-start;gap:10px;width:100%;padding:8px 6px;border:0;border-radius:8px;background:none;text-align:left}
58
72
  .ckf-check:where(button){cursor:pointer}
@@ -67,7 +81,7 @@ var FLOWS_CSS = `
67
81
  .ckf-banner>span{flex:1;min-width:0;display:flex;flex-direction:column}
68
82
  .ckf-banner .ckf-btn{height:32px}
69
83
  .ckf-pw{max-width:420px;gap:14px}
70
- .ckf-root[data-still] :is(.ckf-scrim,.ckf-card){animation:none}
84
+ .ckf-root[data-still] :is(.ckf-scrim,.ckf-card,.ckf-banner){animation:none}
71
85
  .ckf-li{display:flex;gap:10px;align-items:flex-start;margin:0}
72
86
  .ckf-li::before{content:"\u2713";flex:none;width:20px;height:20px;border-radius:50%;background:color-mix(in srgb,var(--ckf-accent) 14%,var(--ckf-bg));color:var(--ckf-accent);font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}
73
87
  .ckf-quote{margin:0;padding:12px 14px;border-radius:12px;background:var(--ckf-soft)}
@@ -106,10 +120,11 @@ var button = (label, kind, onClick, opts = {}) => h(
106
120
  var closeButton = (label, onClick) => h("button", { type: "button", className: "ckf-x", "aria-label": label, "data-ckf-id": "x", onClick: () => onClick() }, "\xD7");
107
121
  var dots = (step, steps) => steps > 1 ? h(
108
122
  "span",
109
- { className: "ckf-dots", role: "img", "aria-label": `Step ${step} of ${steps}` },
123
+ { className: "ckf-dots", role: "img", "aria-label": tf("Step {n} of {total}", { n: step, total: steps }) },
110
124
  ...Array.from({ length: steps }, (_, i) => h("span", { className: "ckf-dot", "data-on": i < step }))
111
125
  ) : h("span");
112
126
  var screenKey = (v) => v.type === "cancel" ? `c:${v.screen}:${v.round ?? 1}` : v.type === "step" ? `s:${v.step.id}` : v.type === "checklist" ? `l:${v.flow.id}` : `m:${v.message.id}`;
127
+ var surfaceKey = (v) => v.type === "cancel" ? "c" : v.type === "step" && (v.step.kind === "question" || v.step.kind === "modal") ? `d:${v.flow.id}` : screenKey(v);
113
128
  var rootFor = (theme) => {
114
129
  if (!document.getElementById(STYLE_ID)) {
115
130
  const style = document.createElement("style");
@@ -122,11 +137,38 @@ var rootFor = (theme) => {
122
137
  document.body.appendChild(root);
123
138
  return root;
124
139
  };
140
+ var setRootLang = (root) => {
141
+ const lang = wordsLang();
142
+ if (lang) root.setAttribute("lang", lang);
143
+ else root.removeAttribute("lang");
144
+ };
145
+ var iconEl = (icon, className) => {
146
+ const i = iconOf(icon);
147
+ if (!i) return false;
148
+ const box = h("span", { className, "aria-hidden": "true" });
149
+ if (i.kind === "emoji") box.textContent = i.value;
150
+ else if (i.kind === "image") box.append(h("img", { src: i.url, alt: "" }));
151
+ else {
152
+ const ns = "http://www.w3.org/2000/svg";
153
+ const svg = document.createElementNS(ns, "svg");
154
+ for (const [k, v] of Object.entries({ viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }))
155
+ svg.setAttribute(k, v);
156
+ for (const d of FLOW_ICONS[i.name].paths) {
157
+ const p = document.createElementNS(ns, "path");
158
+ p.setAttribute("d", d);
159
+ svg.append(p);
160
+ }
161
+ box.append(svg);
162
+ }
163
+ return box;
164
+ };
125
165
  var mountFlows = (ctl, theme = {}) => {
126
166
  if (typeof document === "undefined") return () => {
127
167
  };
128
168
  const root = rootFor(theme);
169
+ let played = "";
129
170
  let lastKey = "";
171
+ let lastSurface = "";
130
172
  let returnFocus = null;
131
173
  let comment = "";
132
174
  let placed = null;
@@ -136,7 +178,7 @@ var mountFlows = (ctl, theme = {}) => {
136
178
  const body = [];
137
179
  if (v.screen === "survey") {
138
180
  body.push(
139
- h("h2", { className: "ckf-h", id: titleId }, v.flow.question || "Why are you leaving?"),
181
+ h("h2", { className: "ckf-h", id: titleId }, v.flow.question || t("Why are you leaving?")),
140
182
  h(
141
183
  "div",
142
184
  { className: "ckf-opts", role: "radiogroup", "aria-labelledby": titleId },
@@ -159,8 +201,8 @@ var mountFlows = (ctl, theme = {}) => {
159
201
  h(
160
202
  "div",
161
203
  { className: "ckf-btns" },
162
- button("Continue", "primary", ctl.cancel.next, { disabled: !v.reason, id: "go" }),
163
- button("Never mind, keep my plan", "quiet", ctl.cancel.close)
204
+ button(t("Continue"), "primary", ctl.cancel.next, { disabled: !v.reason, id: "go" }),
205
+ button(t("Never mind, keep my plan"), "quiet", ctl.cancel.close)
164
206
  )
165
207
  );
166
208
  } else if (v.screen === "followup" && v.followUp) {
@@ -182,14 +224,15 @@ var mountFlows = (ctl, theme = {}) => {
182
224
  onClick: () => ctl.cancel.answer(a)
183
225
  },
184
226
  h("span", { className: "ckf-radio" }),
185
- a
227
+ // The label in the visitor's language; what is recorded stays `a`.
228
+ f.answerLabels?.[i] || a
186
229
  )
187
230
  )
188
231
  );
189
232
  else {
190
233
  const text = h("textarea", {
191
234
  className: "ckf-text",
192
- placeholder: "Your answer (optional)",
235
+ placeholder: t("Your answer (optional)"),
193
236
  "aria-labelledby": titleId,
194
237
  maxlength: "500",
195
238
  "data-ckf-id": "answer",
@@ -200,14 +243,14 @@ var mountFlows = (ctl, theme = {}) => {
200
243
  }
201
244
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
202
245
  body.push(
203
- label ? h("p", { className: "ckf-small" }, `You said: ${label}`) : false,
246
+ label ? h("p", { className: "ckf-small" }, tf("You said: {label}", { label })) : false,
204
247
  h("h2", { className: "ckf-h", id: titleId }, f.question),
205
248
  field,
206
249
  h(
207
250
  "div",
208
251
  { className: "ckf-btns" },
209
- button("Continue", "primary", ctl.cancel.next, { id: "go" }),
210
- button("Never mind, keep my plan", "quiet", ctl.cancel.close)
252
+ button(t("Continue"), "primary", ctl.cancel.next, { id: "go" }),
253
+ button(t("Never mind, keep my plan"), "quiet", ctl.cancel.close)
211
254
  )
212
255
  );
213
256
  } else if (v.screen === "offer" && v.offer) {
@@ -223,48 +266,48 @@ var mountFlows = (ctl, theme = {}) => {
223
266
  h(
224
267
  "span",
225
268
  { className: "ckf-small" },
226
- `/${v.price.interval ?? "month"}${o.months ? ` for ${o.months} ${o.months === 1 ? "month" : "months"}` : ""}`
269
+ `/${v.price.interval === "year" ? t("year") : t("month")}${o.months ? ` ${forMonths(o.months)}` : ""}`
227
270
  )
228
271
  ) : h(
229
272
  "div",
230
273
  { className: "ckf-box" },
231
- h("span", { className: "ckf-big" }, `${o.percent}% off`),
232
- o.months ? h("span", { className: "ckf-small" }, `for ${o.months} ${o.months === 1 ? "month" : "months"}`) : false
274
+ h("span", { className: "ckf-big" }, tf("{percent}% off", { percent: o.percent })),
275
+ o.months ? h("span", { className: "ckf-small" }, forMonths(o.months)) : false
233
276
  );
234
277
  if (o.kind === "pause" && o.months)
235
278
  detail = h(
236
279
  "div",
237
280
  { className: "ckf-box" },
238
- h("span", {}, `Pause for up to ${o.months} ${o.months === 1 ? "month" : "months"}. Your data stays.`)
281
+ h("span", {}, tf("Pause for up to {months}. Your data stays.", { months: monthsWord(o.months) }))
239
282
  );
240
283
  if (o.kind === "downgrade" && o.planName) detail = h("div", { className: "ckf-box" }, h("b", {}, o.planName));
241
284
  const text = o.kind === "contact" ? h("textarea", {
242
285
  className: "ckf-text",
243
- placeholder: "What would make you stay?",
244
- "aria-label": "Your message",
286
+ placeholder: t("What would make you stay?"),
287
+ "aria-label": t("Your message"),
245
288
  maxlength: "500",
246
289
  "data-ckf-id": "comment",
247
290
  onInput: (e) => comment = e.target.value
248
291
  }) : null;
249
292
  if (text) text.value = comment;
250
293
  body.push(
251
- label ? h("p", { className: "ckf-small" }, `You said: ${label}`) : false,
294
+ label ? h("p", { className: "ckf-small" }, tf("You said: {label}", { label })) : false,
252
295
  h("h2", { className: "ckf-h", id: titleId }, o.headline),
253
296
  detail,
254
297
  text ?? false,
255
298
  h(
256
299
  "div",
257
300
  { className: "ckf-btns" },
258
- button(busy ? "One moment\u2026" : offerButton(o), "primary", () => ctl.cancel.accept(comment), { disabled: busy, id: "go" }),
259
- button("No thanks, continue cancelling", "quiet", ctl.cancel.decline, { disabled: busy })
301
+ button(busy ? t("One moment\u2026") : offerButton(o), "primary", () => ctl.cancel.accept(comment), { disabled: busy, id: "go" }),
302
+ button(t("No thanks, continue cancelling"), "quiet", ctl.cancel.decline, { disabled: busy })
260
303
  )
261
304
  );
262
305
  } else {
263
306
  const plain = v.steps === 1;
264
307
  const text = plain ? null : h("textarea", {
265
308
  className: "ckf-text",
266
- placeholder: "Anything else we should know? (optional)",
267
- "aria-label": "Anything else we should know? (optional)",
309
+ placeholder: t("Anything else we should know? (optional)"),
310
+ "aria-label": t("Anything else we should know? (optional)"),
268
311
  maxlength: "500",
269
312
  "data-ckf-id": "comment",
270
313
  onInput: (e) => comment = e.target.value
@@ -277,8 +320,8 @@ var mountFlows = (ctl, theme = {}) => {
277
320
  h(
278
321
  "div",
279
322
  { className: "ckf-btns" },
280
- button(busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => ctl.cancel.confirm(comment), { disabled: busy, id: "go" }),
281
- button("Keep my plan", "secondary", ctl.cancel.close, { disabled: busy })
323
+ button(busy ? t("One moment\u2026") : v.flow.confirm.button, "danger", () => ctl.cancel.confirm(comment), { disabled: busy, id: "go" }),
324
+ button(t("Keep my plan"), "secondary", ctl.cancel.close, { disabled: busy })
282
325
  )
283
326
  );
284
327
  }
@@ -288,7 +331,7 @@ var mountFlows = (ctl, theme = {}) => {
288
331
  h(
289
332
  "div",
290
333
  { className: "ckf-card", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId },
291
- h("div", { className: "ckf-top" }, dots(v.step, v.steps), closeButton("Close", ctl.cancel.close)),
334
+ h("div", { className: "ckf-top" }, dots(v.step, v.steps), closeButton(t("Close"), ctl.cancel.close)),
292
335
  ...body,
293
336
  v.error ? h("p", { className: "ckf-err", role: "alert" }, v.error) : false
294
337
  )
@@ -302,24 +345,91 @@ var mountFlows = (ctl, theme = {}) => {
302
345
  return v.kind === "iframe" ? h(
303
346
  "div",
304
347
  { className: "ckf-video" },
305
- h("iframe", { src: v.src, title: m.alt || "Video", allow: "fullscreen; picture-in-picture", allowfullscreen: true, loading: "lazy", referrerpolicy: "strict-origin-when-cross-origin" })
348
+ h("iframe", { src: v.src, title: m.alt || t("Video"), allow: "fullscreen; picture-in-picture", allowfullscreen: true, loading: "lazy", referrerpolicy: "strict-origin-when-cross-origin" })
306
349
  ) : h("video", { className: "ckf-media", src: v.src, controls: true, playsinline: true, preload: "metadata", "aria-label": m.alt || void 0, onLoadedmetadata: () => follow() });
307
350
  };
351
+ const questionCard = (v) => {
352
+ const s = v.step;
353
+ const q = s.question;
354
+ const titleId = "ckf-title";
355
+ const ok = answerOk(q, v.answer);
356
+ let field = false;
357
+ const textField = (hint, value, write) => {
358
+ const input = h("input", {
359
+ type: "text",
360
+ className: "ckf-input",
361
+ placeholder: hint,
362
+ "aria-labelledby": titleId,
363
+ maxlength: "500",
364
+ "data-ckf-id": "answer",
365
+ onInput: (e) => write(e.target.value),
366
+ onKeydown: (e) => e.key === "Enter" && ctl.onboarding.next()
367
+ });
368
+ input.value = value;
369
+ return input;
370
+ };
371
+ const picked = (on, id, label, press) => h("button", { type: "button", role: "radio", className: "ckf-opt", "aria-checked": String(on), "data-ckf-id": `q:${id}`, onClick: press }, h("span", { className: "ckf-radio" }), label);
372
+ if (q?.type === "choice") {
373
+ const other = !!q.other && !!v.answer?.startsWith(OTHER_PREFIX);
374
+ field = h(
375
+ "div",
376
+ { className: "ckf-opts", role: "radiogroup", "aria-labelledby": titleId },
377
+ ...(q.options ?? []).map((o) => picked(v.answer === o.id, o.id, o.label, () => ctl.onboarding.answer(o.id))),
378
+ q.other ? picked(other, "other", q.otherLabel || t("Other"), () => other || ctl.onboarding.answer(OTHER_PREFIX)) : false,
379
+ other ? textField(t("Your answer"), v.answer.slice(OTHER_PREFIX.length), (t2) => ctl.onboarding.answer(OTHER_PREFIX + t2)) : false
380
+ );
381
+ } else if (q?.type === "scale")
382
+ field = h(
383
+ "div",
384
+ {},
385
+ h(
386
+ "div",
387
+ { className: "ckf-scale", role: "radiogroup", "aria-labelledby": titleId },
388
+ ...scaleOf(q).map(
389
+ (n) => h("button", { type: "button", role: "radio", className: "ckf-num", "aria-checked": String(v.answer === String(n)), "data-ckf-id": `q:${n}`, onClick: () => ctl.onboarding.answer(String(n)) }, String(n))
390
+ )
391
+ ),
392
+ q.lowLabel || q.highLabel ? h("div", { className: "ckf-ends ckf-small" }, h("span", {}, q.lowLabel || ""), h("span", {}, q.highLabel || "")) : false
393
+ );
394
+ else if (q) field = textField(q.placeholder || t("Your answer"), v.answer ?? "", (t2) => ctl.onboarding.answer(t2));
395
+ return h(
396
+ "div",
397
+ { className: "ckf-scrim" },
398
+ h(
399
+ "div",
400
+ { className: "ckf-card", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId },
401
+ h("div", { className: "ckf-top" }, dots(v.index + 1, v.total), closeButton(t("Close"), ctl.onboarding.dismiss)),
402
+ v.index === 0 ? iconEl(v.flow.icon, "ckf-hic") : false,
403
+ h("h2", { className: "ckf-h", id: titleId }, s.title),
404
+ s.body ? h("p", { className: "ckf-p" }, s.body) : false,
405
+ field,
406
+ h(
407
+ "div",
408
+ { className: "ckf-row" },
409
+ v.back ? button(t("Back"), "quiet", ctl.onboarding.back) : false,
410
+ q?.optional ? button(q.skipLabel || t("Skip"), "quiet", ctl.onboarding.skip) : false,
411
+ button(s.button || (v.last ? t("Done") : t("Continue")), "primary", ctl.onboarding.next, { disabled: !ok, id: "go" })
412
+ )
413
+ )
414
+ );
415
+ };
308
416
  const stepCard = (v) => {
309
417
  const s = v.step;
418
+ if (s.kind === "question") return questionCard(v);
310
419
  const card = h(
311
420
  "div",
312
421
  { className: "ckf-card", role: "dialog", "aria-modal": s.kind === "modal" ? "true" : void 0, "aria-labelledby": "ckf-title" },
313
422
  h(
314
423
  "div",
315
424
  { className: "ckf-top" },
316
- v.total > 1 ? h("p", { className: "ckf-small" }, `Step ${v.index + 1} of ${v.total}`) : h("span"),
317
- closeButton("Close the tour", ctl.onboarding.dismiss)
425
+ v.total > 1 ? h("p", { className: "ckf-small" }, tf("Step {n} of {total}", { n: v.index + 1, total: v.total })) : h("span"),
426
+ closeButton(t("Close the tour"), ctl.onboarding.dismiss)
318
427
  ),
428
+ s.kind === "modal" ? iconEl(v.flow.icon, "ckf-hic") : false,
319
429
  media(s),
320
430
  h("h2", { className: "ckf-h", id: "ckf-title" }, s.title),
321
431
  s.body ? h("p", { className: "ckf-p" }, s.body) : false,
322
- h("div", { className: "ckf-row" }, button(s.button || (v.last ? "Done" : "Next"), "primary", ctl.onboarding.next, { id: "go" }))
432
+ h("div", { className: "ckf-row" }, button(s.button || (v.last ? t("Done") : t("Next")), "primary", ctl.onboarding.next, { id: "go" }))
323
433
  );
324
434
  if (s.kind === "modal") return h("div", { className: "ckf-scrim" }, card);
325
435
  const target = s.target ? findTarget(s.target) : null;
@@ -333,10 +443,10 @@ var mountFlows = (ctl, theme = {}) => {
333
443
  }
334
444
  return float;
335
445
  };
336
- const findTarget = (t) => {
337
- const named = () => document.querySelector(`[data-ck-target="${CSS.escape(t)}"]`);
338
- if (!/^[#.[]|[\s>:=]/.test(t)) return named();
339
- return safeQuery(t) ?? named();
446
+ const findTarget = (t2) => {
447
+ const named = () => document.querySelector(`[data-ck-target="${CSS.escape(t2)}"]`);
448
+ if (!/^[#.[]|[\s>:=]/.test(t2)) return named();
449
+ return safeQuery(t2) ?? named();
340
450
  };
341
451
  const safeQuery = (sel) => {
342
452
  try {
@@ -383,9 +493,9 @@ var mountFlows = (ctl, theme = {}) => {
383
493
  "div",
384
494
  { className: "ckf-top" },
385
495
  h("h2", { className: "ckf-h", id: "ckf-title" }, v.flow.name),
386
- closeButton("Hide the checklist", ctl.onboarding.dismiss)
496
+ closeButton(t("Hide the checklist"), ctl.onboarding.dismiss)
387
497
  ),
388
- h("p", { className: "ckf-small" }, `${done} of ${v.items.length} done`),
498
+ h("p", { className: "ckf-small" }, tf("{done} of {total} done", { done, total: v.items.length })),
389
499
  h(
390
500
  "div",
391
501
  { className: "ckf-bar", role: "progressbar", "aria-valuemin": "0", "aria-valuemax": String(v.items.length), "aria-valuenow": String(done) },
@@ -394,20 +504,21 @@ var mountFlows = (ctl, theme = {}) => {
394
504
  h(
395
505
  "div",
396
506
  { className: "ckf-opts" },
397
- ...v.items.map(({ step, done: d, tour }) => {
507
+ ...v.items.map(({ step, done: d, opens }) => {
398
508
  const inner = [
399
509
  h("span", { className: "ckf-tick", "aria-hidden": "true" }, d ? "\u2713" : ""),
510
+ iconEl(step.icon, "ckf-ic"),
400
511
  h("span", {}, h("b", {}, step.title), step.body ? h("span", { className: "ckf-small" }, step.body) : false)
401
512
  ];
402
- if (!d && tour)
513
+ if (!d && opens)
403
514
  return h(
404
515
  "button",
405
516
  {
406
517
  type: "button",
407
518
  className: "ckf-check",
408
- "aria-label": `Start \u201C${step.title}\u201D`,
519
+ "aria-label": tf("Start \u201C{title}\u201D", { title: step.title }),
409
520
  "data-ckf-id": `t:${step.id}`,
410
- onClick: () => ctl.onboarding.startTour(step.id)
521
+ onClick: () => ctl.onboarding.open(step.id)
411
522
  },
412
523
  ...inner,
413
524
  h("span", { className: "ckf-go", "aria-hidden": "true" }, "\u203A")
@@ -417,7 +528,7 @@ var mountFlows = (ctl, theme = {}) => {
417
528
  {
418
529
  type: "button",
419
530
  className: "ckf-check",
420
- "aria-label": `Mark \u201C${step.title}\u201D done`,
531
+ "aria-label": tf("Mark \u201C{title}\u201D done", { title: step.title }),
421
532
  "data-ckf-id": `i:${step.id}`,
422
533
  onClick: () => ctl.onboarding.check(step.id)
423
534
  },
@@ -437,7 +548,7 @@ var mountFlows = (ctl, theme = {}) => {
437
548
  { className: "ckf-banner", role: "status" },
438
549
  h("span", {}, h("b", {}, m.title), m.body ? h("span", { className: "ckf-small" }, m.body) : false),
439
550
  action,
440
- closeButton("Dismiss", ctl.message.dismiss)
551
+ closeButton(t("Dismiss"), ctl.message.dismiss)
441
552
  );
442
553
  return h(
443
554
  "div",
@@ -445,7 +556,7 @@ var mountFlows = (ctl, theme = {}) => {
445
556
  h(
446
557
  "div",
447
558
  { className: "ckf-card", role: "dialog", "aria-modal": "true", "aria-labelledby": "ckf-title" },
448
- h("div", { className: "ckf-top" }, h("span"), closeButton("Dismiss", ctl.message.dismiss)),
559
+ h("div", { className: "ckf-top" }, h("span"), closeButton(t("Dismiss"), ctl.message.dismiss)),
449
560
  h("h2", { className: "ckf-h", id: "ckf-title" }, m.title),
450
561
  m.body ? h("p", { className: "ckf-p" }, m.body) : false,
451
562
  action ? h("div", { className: "ckf-row" }, action) : false
@@ -454,6 +565,14 @@ var mountFlows = (ctl, theme = {}) => {
454
565
  };
455
566
  const draw = () => {
456
567
  const v = ctl.view();
568
+ setRootLang(root);
569
+ const accent = theme.accent ?? ctl.accent?.();
570
+ if (accent) root.style.setProperty("--ckf-accent", accent);
571
+ const fin = ctl.finale?.();
572
+ if (fin && fin.key !== played) {
573
+ played = fin.key;
574
+ playFinish(fin.kind, { accent });
575
+ }
457
576
  const key = v ? screenKey(v) : "";
458
577
  const focused = document.activeElement?.closest?.(".ckf-root") ? document.activeElement?.getAttribute("data-ckf-id") : null;
459
578
  if (key && !lastKey && document.activeElement instanceof HTMLElement && !document.activeElement.closest(".ckf-root"))
@@ -461,13 +580,16 @@ var mountFlows = (ctl, theme = {}) => {
461
580
  if (key !== lastKey && !key.startsWith("c:")) comment = "";
462
581
  if (!key) comment = "";
463
582
  placed = null;
583
+ const surface = v ? surfaceKey(v) : "";
584
+ root.toggleAttribute("data-still", !!surface && surface === lastSurface);
585
+ lastSurface = surface;
464
586
  root.replaceChildren(
465
587
  ...v ? [v.type === "cancel" ? cancelCard(v) : v.type === "step" ? stepCard(v) : v.type === "checklist" ? checklist(v) : message(v)] : []
466
588
  );
467
589
  follow();
468
590
  if (key !== lastKey) {
469
591
  const modal = root.querySelector("[aria-modal=true]");
470
- const first = modal?.querySelector("[role=radio], [data-ckf-id=go], textarea") ?? null;
592
+ const first = modal?.querySelector("[role=radio], input, [data-ckf-id=go], textarea") ?? null;
471
593
  first?.focus();
472
594
  if (!key && returnFocus) {
473
595
  returnFocus.focus();
@@ -481,7 +603,7 @@ var mountFlows = (ctl, theme = {}) => {
481
603
  const v = ctl.view();
482
604
  if (v?.type === "cancel") ctl.cancel.close();
483
605
  else if (v?.type === "message") ctl.message.dismiss();
484
- else if (v?.type === "step" && v.step.kind === "modal") ctl.onboarding.dismiss();
606
+ else if (v?.type === "step" && (v.step.kind === "modal" || v.step.kind === "question")) ctl.onboarding.dismiss();
485
607
  };
486
608
  function follow() {
487
609
  if (placed) place(placed.float, placed.ring, placed.target, placed.side);
@@ -490,9 +612,11 @@ var mountFlows = (ctl, theme = {}) => {
490
612
  window.addEventListener("resize", follow);
491
613
  window.addEventListener("scroll", follow, { passive: true, capture: true });
492
614
  const off = ctl.subscribe(draw);
615
+ const offWords = onWords(draw);
493
616
  draw();
494
617
  return () => {
495
618
  off();
619
+ offWords();
496
620
  document.removeEventListener("keydown", onKey);
497
621
  window.removeEventListener("resize", follow);
498
622
  window.removeEventListener("scroll", follow, { capture: true });
@@ -507,6 +631,7 @@ var mountPaywall = (host, theme = {}) => {
507
631
  let returnFocus = null;
508
632
  const draw = () => {
509
633
  const v = host.view();
634
+ setRootLang(root);
510
635
  const key = v ? `${v.paywall.id}:${v.index}` : "";
511
636
  if (key && !lastKey && document.activeElement instanceof HTMLElement) returnFocus = document.activeElement;
512
637
  if (!v) {
@@ -520,7 +645,7 @@ var mountPaywall = (host, theme = {}) => {
520
645
  const p = v.page;
521
646
  const plans = p.kind === "plans" ? h(
522
647
  "div",
523
- { className: "ckf-opts", role: "radiogroup", "aria-label": "Plans" },
648
+ { className: "ckf-opts", role: "radiogroup", "aria-label": t("Plans") },
524
649
  ...v.plans.map(
525
650
  (plan) => h(
526
651
  "button",
@@ -545,7 +670,7 @@ var mountPaywall = (host, theme = {}) => {
545
670
  h(
546
671
  "div",
547
672
  { className: "ckf-card ckf-pw", role: "dialog", "aria-modal": "true", "aria-labelledby": "ckf-title" },
548
- h("div", { className: "ckf-top" }, dots(v.index + 1, v.total), closeButton("Close", host.close)),
673
+ h("div", { className: "ckf-top" }, dots(v.index + 1, v.total), closeButton(t("Close"), host.close)),
549
674
  h("h2", { className: "ckf-h", id: "ckf-title" }, p.title),
550
675
  p.body ? h("p", { className: "ckf-p" }, p.body) : false,
551
676
  p.kind === "benefits" && p.items?.length ? h("div", { className: "ckf-opts" }, ...p.items.map((i) => h("p", { className: "ckf-li" }, i))) : false,
@@ -555,11 +680,11 @@ var mountPaywall = (host, theme = {}) => {
555
680
  h(
556
681
  "div",
557
682
  { className: "ckf-btns" },
558
- button(v.busy ? "One moment\u2026" : p.button || "Continue", "primary", host.next, {
683
+ button(v.busy ? t("One moment\u2026") : p.button || t("Continue"), "primary", host.next, {
559
684
  disabled: v.busy || p.kind === "plans" && !v.plan,
560
685
  id: "go"
561
686
  }),
562
- v.index ? button("Back", "quiet", host.back, { disabled: v.busy }) : false
687
+ v.index ? button(t("Back"), "quiet", host.back, { disabled: v.busy }) : false
563
688
  )
564
689
  )
565
690
  )
@@ -573,9 +698,11 @@ var mountPaywall = (host, theme = {}) => {
573
698
  };
574
699
  document.addEventListener("keydown", onKey);
575
700
  const off = host.subscribe(draw);
701
+ const offWords = onWords(draw);
576
702
  draw();
577
703
  return () => {
578
704
  off();
705
+ offWords();
579
706
  document.removeEventListener("keydown", onKey);
580
707
  root.remove();
581
708
  };