@mrrocket/sdk 0.9.0 → 0.11.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.
@@ -49,7 +49,7 @@ module.exports = __toCommonJS(native_exports);
49
49
  var import_react3 = require("react");
50
50
 
51
51
  // src/core.ts
52
- var SDK_VERSION = "0.9.0";
52
+ var SDK_VERSION = "0.11.0";
53
53
  var propOf = (person, name) => {
54
54
  const want = name.trim().toLowerCase();
55
55
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -172,6 +172,16 @@ var textChanges = (config, distinctId, preview) => {
172
172
  var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
173
173
 
174
174
  // src/api-client.ts
175
+ var replyLine = (t) => t === "minutes" ? "We usually reply in a few minutes" : t === "day" ? "We usually reply within a day" : "We usually reply in a few hours";
176
+ var inkOn = (hex) => {
177
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
178
+ if (!m) return "#ffffff";
179
+ const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
180
+ const c = parseInt(x, 16) / 255;
181
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
182
+ });
183
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
184
+ };
175
185
  var ApiError = class extends Error {
176
186
  constructor(message, status) {
177
187
  super(message);
@@ -319,6 +329,19 @@ var safeUrl = (url) => {
319
329
  const u = (url ?? "").trim();
320
330
  return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
321
331
  };
332
+ var nodesOf = (f) => f.nodes ?? f.steps;
333
+ var waitMs = (w) => Math.max(0, w.amount) * (w.unit === "hours" ? 36e5 : 864e5);
334
+ var pageMatches = (pattern, path) => {
335
+ const p = pattern.trim();
336
+ const at = (path.split(/[?#]/)[0] || "/").replace(/\/+$/, "") || "/";
337
+ if (!p) return false;
338
+ if (p.endsWith("*")) {
339
+ const base = p.slice(0, -1).replace(/\/+$/, "");
340
+ return !base || at === base || at.startsWith(`${base}/`);
341
+ }
342
+ return at === (p.replace(/\/+$/, "") || "/");
343
+ };
344
+ var stepsIn = (nodes) => nodes.flatMap((n) => n.kind === "condition" ? [...stepsIn(n.yes), ...stepsIn(n.no)] : n.kind === "wait" ? [] : [n]);
322
345
  var createFlows = (env) => {
323
346
  let config = null;
324
347
  let cancel = null;
@@ -333,25 +356,99 @@ var createFlows = (env) => {
333
356
  const shown = /* @__PURE__ */ new Set();
334
357
  const listeners = /* @__PURE__ */ new Set();
335
358
  const auto = env.auto !== false;
359
+ const seenEvents = /* @__PURE__ */ new Set();
360
+ const now = () => env.now?.() ?? Date.now();
361
+ let timer = null;
336
362
  const flows = () => config?.onboarding ?? [];
337
363
  const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
338
364
  const prog = (id) => {
339
365
  let p = progress.get(id);
340
- if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false });
366
+ if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false, answers: /* @__PURE__ */ new Map(), at: /* @__PURE__ */ new Map(), reached: /* @__PURE__ */ new Map() });
341
367
  return p;
342
368
  };
369
+ let dirty = false;
370
+ const record = (props) => {
371
+ env.track(ONBOARDING_EVENT, props);
372
+ dirty = true;
373
+ };
374
+ const markDone = (p, id) => {
375
+ p.done.add(id);
376
+ p.at.set(id, now());
377
+ };
378
+ const decide = (c) => {
379
+ if (c.page?.trim()) {
380
+ const path = env.path?.();
381
+ return path === void 0 ? false : pageMatches(c.page, path);
382
+ }
383
+ if (c.event?.trim()) {
384
+ if (!known) return void 0;
385
+ for (const e of seenEvents) if (matchesTrigger(c.event, e)) return true;
386
+ return false;
387
+ }
388
+ return false;
389
+ };
390
+ const walkLine = (flow, line, p, live, before = 0, last) => {
391
+ for (let i = 0; i < line.length; i++) {
392
+ const n = line[i];
393
+ if (n.kind === "wait") {
394
+ if (p.done.has(n.id)) {
395
+ last = p.at.get(n.id) ?? last;
396
+ continue;
397
+ }
398
+ let since = p.reached.get(n.id);
399
+ if (since === void 0) {
400
+ if (!live) return { at: "pending" };
401
+ since = last ?? now();
402
+ p.reached.set(n.id, since);
403
+ record({ flow: flow.id, step: n.id, kind: "wait", action: "shown" });
404
+ }
405
+ const until = since + waitMs(n);
406
+ if (now() < until) return { at: "wait", until };
407
+ if (!live) return { at: "pending" };
408
+ markDone(p, n.id);
409
+ record({ flow: flow.id, step: n.id, kind: "wait", action: "completed" });
410
+ last = until;
411
+ continue;
412
+ }
413
+ if (n.kind === "condition") {
414
+ let answer = p.answers.get(n.id);
415
+ if (answer === void 0) {
416
+ if (!live) return { at: "pending" };
417
+ answer = decide(n);
418
+ if (answer === void 0) return { at: "pending" };
419
+ p.answers.set(n.id, answer);
420
+ record({ flow: flow.id, step: n.id, kind: "condition", action: "answered", answer: answer ? "yes" : "no" });
421
+ }
422
+ return walkLine(flow, answer ? n.yes : n.no, p, live, before, last);
423
+ }
424
+ if (!p.done.has(n.id)) return { at: "step", step: n, line, index: i, before };
425
+ last = p.at.get(n.id) ?? last;
426
+ before++;
427
+ }
428
+ return { at: "end" };
429
+ };
343
430
  const finished = (f) => {
344
431
  const p = progress.get(f.id);
345
- return !!p && (p.dismissed || f.steps.every((s) => p.done.has(s.id)));
432
+ return !!p && (p.dismissed || walkLine(f, nodesOf(f), p, false).at === "end");
346
433
  };
347
434
  const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
435
+ const eventsKeyOf = (pid) => `${PROGRESS_KEY}:events:${pid ?? "visitor"}`;
348
436
  const save = () => {
437
+ dirty = false;
349
438
  const store = env.storage?.();
350
439
  if (!store) return;
351
440
  const out = {};
352
- for (const [id, p] of progress) out[id] = { done: [...p.done], ...p.dismissed ? { dismissed: true } : {} };
441
+ for (const [id, p] of progress)
442
+ out[id] = {
443
+ done: [...p.done],
444
+ ...p.dismissed ? { dismissed: true } : {},
445
+ ...p.answers.size ? { answers: Object.fromEntries([...p.answers].map(([k, v]) => [k, v ? "yes" : "no"])) } : {},
446
+ ...p.at.size ? { at: Object.fromEntries(p.at) } : {},
447
+ ...p.reached.size ? { reached: Object.fromEntries(p.reached) } : {}
448
+ };
353
449
  try {
354
450
  store.set(keyOf(env.personId()), JSON.stringify(out));
451
+ if (seenEvents.size) store.set(eventsKeyOf(env.personId()), JSON.stringify([...seenEvents].slice(-200)));
355
452
  } catch {
356
453
  }
357
454
  };
@@ -360,23 +457,55 @@ var createFlows = (env) => {
360
457
  const p = prog(id);
361
458
  for (const d of s.done ?? []) p.done.add(d);
362
459
  if (s.dismissed) p.dismissed = true;
460
+ for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
461
+ for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
462
+ for (const [k, v] of Object.entries(s.reached ?? {})) if (typeof v === "number" && (!p.reached.has(k) || v < p.reached.get(k))) p.reached.set(k, v);
463
+ }
464
+ };
465
+ const wakeAt = (until) => {
466
+ if (timer) clearTimeout(timer);
467
+ timer = null;
468
+ const ms = until - now();
469
+ if (ms > 0 && ms <= 864e5) {
470
+ timer = setTimeout(() => (timer = null, emit()), ms + 50);
471
+ timer.unref?.();
363
472
  }
364
473
  };
474
+ let holding = false;
365
475
  const onboardingView = () => {
476
+ holding = false;
366
477
  const flow = flowById(active);
367
478
  if (!flow) return null;
368
479
  const p = prog(flow.id);
369
- const at = flow.steps.findIndex((s) => !p.done.has(s.id));
370
- if (at < 0 || p.dismissed) return null;
371
- const step = flow.steps[at];
480
+ if (p.dismissed) return null;
481
+ const w = walkLine(flow, nodesOf(flow), p, true);
482
+ if (dirty) save();
483
+ if (w.at === "wait") wakeAt(w.until);
484
+ holding = w.at === "wait" || w.at === "pending";
485
+ if (w.at !== "step") return null;
486
+ const { step, line, index } = w;
487
+ const isStep = (n) => !!n && n.kind !== "wait" && n.kind !== "condition";
372
488
  if (step.kind === "checklist") {
373
- let from = at;
374
- while (from > 0 && flow.steps[from - 1].kind === "checklist") from--;
375
- let to = at;
376
- while (to + 1 < flow.steps.length && flow.steps[to + 1].kind === "checklist") to++;
377
- return { type: "checklist", flow, items: flow.steps.slice(from, to + 1).map((s) => ({ step: s, done: p.done.has(s.id) })) };
489
+ let from = index;
490
+ while (from > 0 && isStep(line[from - 1]) && line[from - 1].kind === "checklist") from--;
491
+ let to = index;
492
+ while (to + 1 < line.length && isStep(line[to + 1]) && line[to + 1].kind === "checklist") to++;
493
+ const items = line.slice(from, to + 1).filter(isStep);
494
+ for (const i of items)
495
+ if (!p.done.has(i.id) && !shown.has(`s:${i.id}`)) {
496
+ shown.add(`s:${i.id}`);
497
+ env.track(ONBOARDING_EVENT, { flow: flow.id, step: i.id, kind: "checklist", action: "shown" });
498
+ }
499
+ return { type: "checklist", flow, items: items.map((s) => ({ step: s, done: p.done.has(s.id) })) };
500
+ }
501
+ if (!shown.has(`s:${step.id}`)) {
502
+ shown.add(`s:${step.id}`);
503
+ env.track(ONBOARDING_EVENT, { flow: flow.id, step: step.id, kind: step.kind, action: "shown" });
378
504
  }
379
- return { type: "step", flow, step, index: at, total: flow.steps.length, last: at === flow.steps.length - 1 };
505
+ const rest = line.slice(index + 1);
506
+ const fork = rest.findIndex((n) => n.kind === "condition");
507
+ const ahead = (fork < 0 ? rest : rest.slice(0, fork)).filter(isStep).length;
508
+ return { type: "step", flow, step, index: w.before, total: w.before + 1 + ahead, last: !ahead && fork < 0 };
380
509
  };
381
510
  const view = () => {
382
511
  if (cancel) {
@@ -396,22 +525,13 @@ var createFlows = (env) => {
396
525
  }
397
526
  const ob = onboardingView();
398
527
  if (ob) return ob;
399
- if (active) active = null;
528
+ if (active && !holding) active = null;
400
529
  const m = messages.find((x) => !handled.has(x.id));
401
530
  return m ? { type: "message", message: m } : null;
402
531
  };
403
532
  const emit = () => {
404
533
  const v = view();
405
- if (v?.type === "step" && !shown.has(`s:${v.step.id}`)) {
406
- shown.add(`s:${v.step.id}`);
407
- env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "shown" });
408
- } else if (v?.type === "checklist") {
409
- for (const i of v.items)
410
- if (!i.done && !shown.has(`s:${i.step.id}`)) {
411
- shown.add(`s:${i.step.id}`);
412
- env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: i.step.id, kind: "checklist", action: "shown" });
413
- }
414
- } else if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
534
+ if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
415
535
  shown.add(`m:${v.message.id}`);
416
536
  env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "shown" });
417
537
  }
@@ -433,13 +553,14 @@ var createFlows = (env) => {
433
553
  const autoStart = () => {
434
554
  if (!auto || active || !known) return;
435
555
  if (!env.personId() && !env.storage?.()) return;
436
- const next = flows().find((f) => startsOnLoad(f.trigger) && f.steps.length && !finished(f));
556
+ const next = flows().find((f) => startsOnLoad(f.trigger) && nodesOf(f).length && !finished(f));
437
557
  if (next) start(next);
438
558
  };
439
559
  const refresh = () => {
440
560
  const pid = env.personId();
441
561
  if (progressFor !== null && progressFor !== pid) {
442
562
  progress.clear();
563
+ seenEvents.clear();
443
564
  handled.clear();
444
565
  active = null;
445
566
  loadedFor = void 0;
@@ -451,6 +572,10 @@ var createFlows = (env) => {
451
572
  try {
452
573
  const raw = await store.get(keyOf(pid));
453
574
  if (raw) merge(JSON.parse(raw));
575
+ const seen = await store.get(eventsKeyOf(pid));
576
+ if (seen) {
577
+ for (const e of JSON.parse(seen)) if (typeof e === "string") seenEvents.add(e);
578
+ }
454
579
  } catch {
455
580
  }
456
581
  }
@@ -460,6 +585,7 @@ var createFlows = (env) => {
460
585
  const state = await env.fetchInApp(pid).catch(() => null);
461
586
  if (state && env.personId() === pid) {
462
587
  merge(state.onboarding);
588
+ for (const e of state.events ?? []) if (typeof e === "string") seenEvents.add(eventSlug(e));
463
589
  messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
464
590
  }
465
591
  }
@@ -536,29 +662,32 @@ var createFlows = (env) => {
536
662
  refresh,
537
663
  onEvent: (name) => {
538
664
  if (!name || name.startsWith("$")) return;
665
+ const slug = eventSlug(name);
666
+ const fresh = !!slug && !seenEvents.has(slug);
667
+ if (fresh) seenEvents.add(slug);
539
668
  const flow = flowById(active);
540
669
  let ticked = false;
541
670
  if (flow) {
542
671
  const p = prog(flow.id);
543
- for (const s of flow.steps)
672
+ for (const s of stepsIn(nodesOf(flow)))
544
673
  if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
545
- p.done.add(s.id);
674
+ markDone(p, s.id);
546
675
  ticked = true;
547
676
  env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
548
677
  }
549
678
  }
550
- if (ticked) {
679
+ if (ticked || fresh && flow) {
551
680
  save();
552
681
  emit();
553
- }
682
+ } else if (fresh) save();
554
683
  if (!auto || !env.personId() && !env.storage?.()) return;
555
- const next = flows().find((f) => f.steps.length && matchesTrigger(f.trigger, name) && !finished(f));
684
+ const next = flows().find((f) => nodesOf(f).length && matchesTrigger(f.trigger, name) && !finished(f));
556
685
  if (next) start(next);
557
686
  },
558
687
  openCancelFlow,
559
688
  startOnboarding: (id) => {
560
- const f = id ? flowById(id) : flows().find((x) => x.steps.length && !finished(x));
561
- if (!f || !f.steps.length) return false;
689
+ const f = id ? flowById(id) : flows().find((x) => nodesOf(x).length && !finished(x));
690
+ if (!f || !nodesOf(f).length) return false;
562
691
  const p = prog(f.id);
563
692
  p.dismissed = false;
564
693
  return start(f);
@@ -629,7 +758,7 @@ var createFlows = (env) => {
629
758
  next: () => {
630
759
  const v = onboardingView();
631
760
  if (v?.type !== "step") return;
632
- prog(v.flow.id).done.add(v.step.id);
761
+ markDone(prog(v.flow.id), v.step.id);
633
762
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
634
763
  save();
635
764
  emit();
@@ -639,7 +768,7 @@ var createFlows = (env) => {
639
768
  if (v?.type !== "checklist") return;
640
769
  const item = v.items.find((i) => i.step.id === stepId);
641
770
  if (!item || item.done) return;
642
- prog(v.flow.id).done.add(stepId);
771
+ markDone(prog(v.flow.id), stepId);
643
772
  env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
644
773
  save();
645
774
  emit();
@@ -2369,6 +2498,35 @@ var buzz = () => {
2369
2498
  }
2370
2499
  };
2371
2500
  var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
2501
+ var svg = null;
2502
+ try {
2503
+ svg = require("react-native-svg");
2504
+ } catch {
2505
+ svg = null;
2506
+ }
2507
+ var GLYPH = {
2508
+ chat: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z",
2509
+ home: "M2.5 7 8 2.5 13.5 7v6a1 1 0 0 1-1 1h-3v-4h-3v4h-3a1 1 0 0 1-1-1z",
2510
+ help: "M8 14.5A6.5 6.5 0 1 0 8 1.5a6.5 6.5 0 0 0 0 13zM6.2 6.2a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M8 11.6v.1",
2511
+ send: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5",
2512
+ up: "M8 13V3M3.5 7.5 8 3l4.5 4.5",
2513
+ next: "M6 3.5 10.5 8 6 12.5",
2514
+ search: "M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3",
2515
+ cross: "m4 4 8 8M12 4l-8 8"
2516
+ };
2517
+ var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7" };
2518
+ var glyph = (name, color, size = 20) => {
2519
+ const Svg = svg?.Svg ?? svg?.default;
2520
+ if (!svg || !Svg) return (0, import_react2.createElement)(RN3.Text, { style: { color, fontSize: Math.round(size * 0.85), lineHeight: size + 2, textAlign: "center", minWidth: size } }, FALLBACK[name]);
2521
+ return (0, import_react2.createElement)(Svg, { width: size, height: size, viewBox: "0 0 16 16", fill: "none" }, (0, import_react2.createElement)(svg.Path, { d: GLYPH[name], stroke: color, strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }));
2522
+ };
2523
+ var alpha = (hex, a) => /^#[0-9a-f]{6}$/i.test(hex) ? `${hex}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex;
2524
+ var teamMark = (title, bg, fg, size = 36) => (0, import_react2.createElement)(
2525
+ RN3.View,
2526
+ { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: bg, alignItems: "center", justifyContent: "center" } },
2527
+ (0, import_react2.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
2528
+ );
2529
+ var lift = { shadowColor: "#101218", shadowOpacity: 0.07, shadowRadius: 14, shadowOffset: { width: 0, height: 4 }, elevation: 2 };
2372
2530
  var useChat = (client, o, open, view) => {
2373
2531
  const [thread, setThread] = (0, import_react2.useState)(null);
2374
2532
  const [loaded, setLoaded] = (0, import_react2.useState)(false);
@@ -2570,55 +2728,102 @@ var MessengerSheet = ({
2570
2728
  setPending(null);
2571
2729
  }
2572
2730
  };
2731
+ const ink = inkOn(p.accent);
2732
+ const dark = p.bg === DARK.bg;
2733
+ const canvas = dark ? p.bg : p.soft;
2734
+ const raised = dark ? p.soft : p.bg;
2735
+ const title = o.title ?? "Chat with us";
2736
+ const reply = replyLine(o.replyTime);
2573
2737
  const bubble = (m, isPending = false) => {
2574
2738
  const mine = m.from === "visitor";
2575
2739
  return (0, import_react2.createElement)(
2576
2740
  RN3.View,
2577
2741
  { key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
2578
- (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: mine ? p.accent : p.soft, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, opacity: isPending ? 0.7 : 1 } }, k.text(m.body, { color: mine ? "#ffffff" : p.fg })),
2742
+ (0, import_react2.createElement)(
2743
+ RN3.View,
2744
+ {
2745
+ style: {
2746
+ backgroundColor: mine ? p.accent : dark ? p.soft : "#eeeff1",
2747
+ borderRadius: 18,
2748
+ borderBottomRightRadius: mine ? 6 : 18,
2749
+ borderBottomLeftRadius: mine ? 18 : 6,
2750
+ paddingHorizontal: 14,
2751
+ paddingVertical: 9,
2752
+ opacity: isPending ? 0.7 : 1
2753
+ }
2754
+ },
2755
+ k.text(m.body, { color: mine ? ink : p.fg })
2756
+ ),
2579
2757
  ...(m.articles ?? []).map(
2580
2758
  (a) => (0, import_react2.createElement)(
2581
2759
  RN3.Pressable,
2582
- { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6 } },
2760
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 7 } },
2583
2761
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
2584
2762
  )
2585
2763
  ),
2586
- m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left" }) : null
2764
+ m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
2587
2765
  );
2588
2766
  };
2589
2767
  const row = (a) => (0, import_react2.createElement)(
2590
2768
  RN3.Pressable,
2591
- { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", paddingVertical: 11, paddingHorizontal: 4 } },
2769
+ { key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 11, paddingHorizontal: 8 } },
2592
2770
  k.text(a.title, { flex: 1 }),
2593
- k.muted("\u203A", { fontSize: 18 })
2771
+ glyph("next", p.accent, 14)
2772
+ );
2773
+ const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: 16, padding: 16, ...dark ? { borderWidth: 1, borderColor: p.line } : lift, ...style } }, ...children);
2774
+ const searchRow = (onPress) => (0, import_react2.createElement)(
2775
+ RN3.Pressable,
2776
+ { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: 12, paddingHorizontal: 14, backgroundColor: canvas } },
2777
+ k.muted("Search for help", { flex: 1, fontSize: 15 }),
2778
+ glyph("search", p.accent, 16)
2594
2779
  );
2595
- const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.bg, borderRadius: 12, padding: 12, borderWidth: 1, borderColor: p.line, ...style } }, ...children);
2596
- const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200 });
2780
+ const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: 12, backgroundColor: raised, ...dark ? {} : lift } });
2597
2781
  const last = thread?.messages.at(-1);
2598
2782
  const showHelp = helpOn && arts.any !== false;
2599
2783
  let content;
2600
2784
  if (view === "home")
2601
2785
  content = (0, import_react2.createElement)(
2602
2786
  RN3.ScrollView,
2603
- { key: "home", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2604
- (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 22, fontWeight: "700", paddingVertical: 6 } }, greeting),
2787
+ { key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
2788
+ (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, (0, import_react2.createElement)(RN3.Text, { style: { color: ink, fontSize: 28, fontWeight: "700", letterSpacing: -0.4, lineHeight: 34 } }, greeting)),
2605
2789
  (0, import_react2.createElement)(
2606
- RN3.Pressable,
2607
- { onPress: () => setView("messages"), accessibilityRole: "button" },
2608
- card([
2609
- k.text(last ? "Recent message" : "Send us a message", { fontWeight: "700" }),
2610
- k.muted(last ? `${last.from === "visitor" ? "You" : whoLabel[last.from]}: ${last.body}` : "We usually answer within a few hours", { marginTop: 2 })
2611
- ])
2612
- ),
2613
- showHelp ? card([(0, import_react2.createElement)(RN3.Pressable, { key: "s", onPress: () => setView("help") }, (0, import_react2.createElement)(RN3.View, { pointerEvents: "none" }, k.muted("\u{1F50D} Search for help", { paddingVertical: 8 }))), ...(arts.list ?? []).slice(0, 4).map(row)]) : null
2790
+ RN3.View,
2791
+ { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } },
2792
+ last ? (0, import_react2.createElement)(
2793
+ RN3.Pressable,
2794
+ { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
2795
+ card([
2796
+ k.muted("Recent message", { fontSize: 12, fontWeight: "600", marginBottom: 10 }),
2797
+ (0, import_react2.createElement)(
2798
+ RN3.View,
2799
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 12 } },
2800
+ teamMark(title, p.accent, ink),
2801
+ (0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: p.fg, fontSize: 15 } }, last.body), k.muted(`${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`, { fontSize: 12 })),
2802
+ chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { minWidth: 20, height: 20, borderRadius: 10, paddingHorizontal: 6, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(chat.unseen.length), { color: "#fff", fontSize: 12, fontWeight: "700" })) : glyph("next", p.muted, 14)
2803
+ )
2804
+ ])
2805
+ ) : null,
2806
+ (0, import_react2.createElement)(
2807
+ RN3.Pressable,
2808
+ { key: "send", onPress: () => setView("messages"), accessibilityRole: "button" },
2809
+ card(
2810
+ [
2811
+ (0, import_react2.createElement)(RN3.View, { key: "t", style: { flex: 1, gap: 2 } }, k.text("Send us a message", { fontWeight: "700", fontSize: 16 }), k.muted(reply)),
2812
+ (0, import_react2.createElement)(RN3.View, { key: "g", style: { width: 36, height: 36, borderRadius: 18, backgroundColor: alpha(p.accent, 0.12), alignItems: "center", justifyContent: "center" } }, glyph("send", p.accent, 18))
2813
+ ],
2814
+ { flexDirection: "row", alignItems: "center", gap: 12 }
2815
+ )
2816
+ ),
2817
+ showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
2818
+ )
2614
2819
  );
2615
2820
  else if (view === "messages")
2616
2821
  content = (0, import_react2.createElement)(
2617
2822
  RN3.View,
2618
- { key: "messages", style: { flex: 1 } },
2823
+ { key: "messages", style: { flex: 1, backgroundColor: p.bg } },
2619
2824
  (0, import_react2.createElement)(
2620
2825
  RN3.ScrollView,
2621
- { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
2826
+ { ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 12 } },
2622
2827
  bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
2623
2828
  !chat.loaded ? k.muted("Loading\u2026") : null,
2624
2829
  ...(thread?.messages ?? []).map((m) => bubble(m)),
@@ -2627,14 +2832,24 @@ var MessengerSheet = ({
2627
2832
  ),
2628
2833
  (0, import_react2.createElement)(
2629
2834
  RN3.View,
2630
- { style: { padding: 12, gap: 8, borderTopWidth: 1, borderTopColor: p.line } },
2835
+ { style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 10, gap: 8 } },
2631
2836
  chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
2632
- needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: emailOk ? {} : { borderColor: p.danger } }) : null,
2837
+ needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: { borderWidth: emailOk ? 0 : 1, borderColor: p.danger, borderRadius: 12, backgroundColor: canvas } }) : null,
2633
2838
  (0, import_react2.createElement)(
2634
2839
  RN3.View,
2635
- { style: { flexDirection: "row", gap: 8, alignItems: "flex-end" } },
2636
- k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 44, maxHeight: 120 } }),
2637
- k.btn("Send", "primary", () => void send(), !body.trim() || !!pending || !emailOk)
2840
+ { style: { flexDirection: "row", alignItems: "flex-end", gap: 6, borderWidth: 1, borderColor: p.line, borderRadius: 22, paddingVertical: 5, paddingRight: 5, paddingLeft: 16, backgroundColor: p.bg } },
2841
+ k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 34, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 7, backgroundColor: "transparent" } }),
2842
+ (0, import_react2.createElement)(
2843
+ RN3.Pressable,
2844
+ {
2845
+ onPress: () => void send(),
2846
+ disabled: !body.trim() || !!pending || !emailOk,
2847
+ accessibilityRole: "button",
2848
+ accessibilityLabel: "Send",
2849
+ style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: !body.trim() || !!pending || !emailOk ? p.line : p.accent }
2850
+ },
2851
+ glyph("up", !body.trim() || !!pending || !emailOk ? p.muted : ink, 18)
2852
+ )
2638
2853
  )
2639
2854
  )
2640
2855
  );
@@ -2643,11 +2858,11 @@ var MessengerSheet = ({
2643
2858
  for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
2644
2859
  content = (0, import_react2.createElement)(
2645
2860
  RN3.ScrollView,
2646
- { key: "help", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2861
+ { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
2647
2862
  search,
2648
2863
  arts.list === null ? k.muted("Loading\u2026") : null,
2649
2864
  arts.list?.length === 0 ? (0, import_react2.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), k.btn("Send us a message", "primary", () => setView("messages"))) : null,
2650
- ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row))] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`), card(items.map(row))))
2865
+ ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row), { padding: 8 })] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`, { fontSize: 12, fontWeight: "600", paddingHorizontal: 2 }), card(items.map(row), { padding: 8 })))
2651
2866
  );
2652
2867
  } else
2653
2868
  content = (0, import_react2.createElement)(
@@ -2656,25 +2871,44 @@ var MessengerSheet = ({
2656
2871
  (0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
2657
2872
  );
2658
2873
  const current = typeof view === "string" ? view : "help";
2659
- const tab = (v, name, glyph, badge = 0) => (0, import_react2.createElement)(
2874
+ const tab = (v, name, icon, badge = 0) => (0, import_react2.createElement)(
2660
2875
  RN3.Pressable,
2661
- { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 2 } },
2662
- k.text(glyph, { fontSize: 18, color: current === v ? p.fg : p.muted }),
2663
- k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.fg : p.muted }),
2664
- badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "30%", minWidth: 18, height: 18, borderRadius: 9, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(badge, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
2876
+ { key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 3 } },
2877
+ glyph(icon, current === v ? p.accent : p.muted, 22),
2878
+ k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.accent : p.muted }),
2879
+ badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "28%", minWidth: 18, height: 18, borderRadius: 9, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(badge, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
2665
2880
  );
2666
- const titleOf = typeof view !== "string" || view === "help" ? "Help" : view === "messages" ? o.title ?? "Chat with us" : o.title ?? "Chat with us";
2667
- return k.page(titleOf, close, [
2668
- typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", style: { paddingHorizontal: 16, paddingVertical: 8 } }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : null,
2669
- content,
2881
+ const onHome = view === "home";
2882
+ const headFg = onHome ? ink : p.fg;
2883
+ const head = (0, import_react2.createElement)(
2884
+ RN3.View,
2885
+ { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 10, paddingLeft: 16, paddingRight: 12, paddingTop: 16, paddingBottom: 12, backgroundColor: onHome ? p.accent : p.bg, borderBottomWidth: onHome ? 0 : 1, borderBottomColor: p.line } },
2886
+ typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", hitSlop: 10 }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : view === "help" ? null : teamMark(title, onHome ? ink : p.accent, onHome ? p.accent : ink, onHome ? 28 : 36),
2670
2887
  (0, import_react2.createElement)(
2671
2888
  RN3.View,
2672
- { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingBottom: 20, backgroundColor: p.bg } },
2673
- tab("home", "Home", "\u2302"),
2674
- tab("messages", "Messages", "\u2709\uFE0E", view === "messages" ? 0 : chat.unseen.length),
2675
- showHelp ? tab("help", "Help", "?") : null
2889
+ { style: { flex: 1, minWidth: 0 } },
2890
+ typeof view !== "string" ? null : (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", numberOfLines: 1, style: { color: headFg, fontSize: 17, fontWeight: "700" } }, view === "help" ? "Help" : title),
2891
+ view === "messages" ? k.muted(reply, { fontSize: 12 }) : null
2892
+ ),
2893
+ (0, import_react2.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onHome ? ink : p.muted, 18))
2894
+ );
2895
+ return (0, import_react2.createElement)(
2896
+ RN3.Modal,
2897
+ { visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: close },
2898
+ (0, import_react2.createElement)(
2899
+ RN3.KeyboardAvoidingView,
2900
+ { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
2901
+ head,
2902
+ content,
2903
+ (0, import_react2.createElement)(
2904
+ RN3.View,
2905
+ { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingHorizontal: 8, paddingBottom: 22, backgroundColor: p.bg } },
2906
+ tab("home", "Home", "home"),
2907
+ tab("messages", "Messages", "chat", view === "messages" ? 0 : chat.unseen.length),
2908
+ showHelp ? tab("help", "Help", "help") : null
2909
+ )
2676
2910
  )
2677
- ]);
2911
+ );
2678
2912
  };
2679
2913
  var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2680
2914
  const [look, setLook] = (0, import_react2.useState)(null);
@@ -2686,7 +2920,19 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2686
2920
  return () => ctrl.abort();
2687
2921
  }, [client]);
2688
2922
  const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
2689
- const options = look ? { title: look.title, greeting: look.greeting, help: look.help, preview: look.preview, askEmail: look.askEmail, ...defined } : given;
2923
+ const options = look ? {
2924
+ title: look.title,
2925
+ greeting: look.greeting,
2926
+ replyTime: look.replyTime,
2927
+ launcher: look.appLauncher ?? false,
2928
+ position: look.position,
2929
+ label: look.label,
2930
+ help: look.help,
2931
+ preview: look.preview,
2932
+ askEmail: look.askEmail,
2933
+ haptics: look.sound,
2934
+ ...defined
2935
+ } : given;
2690
2936
  const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
2691
2937
  const k = kit(p);
2692
2938
  const open = messengerOpen.useOpen();
@@ -2711,6 +2957,9 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2711
2957
  if (open) setBanner(null);
2712
2958
  }, [open]);
2713
2959
  const unseen = chat.unseen.length;
2960
+ const ink = inkOn(p.accent);
2961
+ const label = options.label?.trim() ?? "";
2962
+ const side = options.position === "bottom-left" ? { left: 20 } : { right: 20 };
2714
2963
  const launcher = options.launcher ? (0, import_react2.createElement)(
2715
2964
  RN3.Pressable,
2716
2965
  {
@@ -2718,9 +2967,28 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2718
2967
  onPress: () => openAt(unseen ? "messages" : "home"),
2719
2968
  accessibilityRole: "button",
2720
2969
  accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
2721
- style: { position: "absolute", right: 20, bottom: 34, width: 54, height: 54, borderRadius: 27, backgroundColor: p.accent, alignItems: "center", justifyContent: "center", shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, elevation: 6 }
2970
+ style: {
2971
+ position: "absolute",
2972
+ ...side,
2973
+ bottom: 34,
2974
+ height: label ? 50 : 56,
2975
+ minWidth: label ? 0 : 56,
2976
+ paddingHorizontal: label ? 18 : 0,
2977
+ borderRadius: 28,
2978
+ flexDirection: "row",
2979
+ gap: 8,
2980
+ backgroundColor: p.accent,
2981
+ alignItems: "center",
2982
+ justifyContent: "center",
2983
+ shadowColor: p.accent,
2984
+ shadowOpacity: 0.4,
2985
+ shadowRadius: 12,
2986
+ shadowOffset: { width: 0, height: 6 },
2987
+ elevation: 6
2988
+ }
2722
2989
  },
2723
- k.text("\u{1F4AC}", { fontSize: 22 }),
2990
+ glyph("chat", ink, label ? 20 : 26),
2991
+ label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
2724
2992
  unseen ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
2725
2993
  ) : null;
2726
2994
  const toast = banner ? (0, import_react2.createElement)(
@@ -2730,10 +2998,15 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
2730
2998
  onPress: () => openAt("messages"),
2731
2999
  accessibilityRole: "button",
2732
3000
  accessibilityLiveRegion: "polite",
2733
- style: { position: "absolute", top: 54, left: 16, right: 16, backgroundColor: p.bg, borderRadius: 14, padding: 14, gap: 2, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 16, elevation: 8 }
3001
+ style: { position: "absolute", top: 54, left: 16, right: 16, flexDirection: "row", alignItems: "flex-start", gap: 12, backgroundColor: p.bg, borderRadius: 18, padding: 14, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.16, shadowRadius: 18, shadowOffset: { width: 0, height: 8 }, elevation: 8 }
2734
3002
  },
2735
- k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
2736
- (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
3003
+ teamMark(options.title ?? "Chat with us", p.accent, ink),
3004
+ (0, import_react2.createElement)(
3005
+ RN3.View,
3006
+ { style: { flex: 1, gap: 2 } },
3007
+ k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
3008
+ (0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
3009
+ )
2737
3010
  ) : null;
2738
3011
  if (!launcher && !toast && !open) return null;
2739
3012
  return (0, import_react2.createElement)(