@evinvest/kitstart 0.16.0 → 0.18.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 (97) hide show
  1. package/README.md +179 -19
  2. package/dist/core/ab-gate.js +3 -1
  3. package/dist/core/ab-switcher.d.ts +8 -0
  4. package/dist/core/ab-switcher.js +11 -0
  5. package/dist/core/accept.d.ts +14 -0
  6. package/dist/core/accept.js +21 -3
  7. package/dist/core/analytics.d.ts +28 -3
  8. package/dist/core/analytics.js +46 -2
  9. package/dist/core/channels.d.ts +12 -4
  10. package/dist/core/channels.js +11 -1
  11. package/dist/core/lead-capture-text.d.ts +6 -2
  12. package/dist/core/lead.d.ts +33 -10
  13. package/dist/core/lead.js +21 -8
  14. package/dist/core/messenger-message.d.ts +31 -0
  15. package/dist/core/messenger-message.js +33 -0
  16. package/dist/core/messenger-text.d.ts +104 -0
  17. package/dist/core/messenger-text.js +150 -0
  18. package/dist/core/messenger.d.ts +92 -0
  19. package/dist/core/messenger.js +55 -0
  20. package/dist/core/place/index.d.ts +1 -1
  21. package/dist/core/place/live.d.ts +5 -1
  22. package/dist/core/place/live.js +22 -1
  23. package/dist/core/place/types.d.ts +17 -1
  24. package/dist/core/site.d.ts +8 -1
  25. package/dist/core/site.js +22 -1
  26. package/dist/index.d.ts +9 -6
  27. package/dist/index.js +8 -5
  28. package/dist/next/quote-route.d.ts +7 -0
  29. package/dist/next/quote-route.js +9 -9
  30. package/dist/react/AbSwitcherPanel.d.ts +3 -1
  31. package/dist/react/AbSwitcherPanel.js +10 -6
  32. package/dist/react/AnalyticsBoundary.d.ts +3 -2
  33. package/dist/react/AnalyticsBoundary.js +7 -20
  34. package/dist/react/LeadCapture.d.ts +24 -1
  35. package/dist/react/LeadCapture.js +96 -11
  36. package/dist/react/LeadCaptureChannels.d.ts +7 -2
  37. package/dist/react/LeadCaptureChannels.js +2 -2
  38. package/dist/react/LeadCaptureEstimate.d.ts +6 -0
  39. package/dist/react/LeadCaptureFields.d.ts +17 -0
  40. package/dist/react/LeadCaptureFields.js +42 -33
  41. package/dist/react/LeadCaptureMessenger.d.ts +41 -0
  42. package/dist/react/LeadCaptureMessenger.js +133 -0
  43. package/dist/react/LeadCapturePrice.d.ts +9 -3
  44. package/dist/react/LeadCapturePrice.js +24 -4
  45. package/dist/react/LeadCaptureSubmit.d.ts +3 -0
  46. package/dist/react/LeadCaptureSubmit.js +2 -2
  47. package/dist/react/LeadCaptureTiles.js +9 -4
  48. package/dist/react/ab-switcher-types.d.ts +1 -1
  49. package/dist/react/analytics-context.d.ts +9 -3
  50. package/dist/react/analytics-context.js +7 -3
  51. package/dist/react/index.d.ts +1 -0
  52. package/dist/react/messenger/action.d.ts +144 -0
  53. package/dist/react/messenger/action.js +301 -0
  54. package/dist/react/messenger/chip.d.ts +8 -0
  55. package/dist/react/messenger/chip.js +22 -0
  56. package/dist/react/messenger/fallback.d.ts +8 -0
  57. package/dist/react/messenger/fallback.js +11 -0
  58. package/dist/react/messenger/saga.d.ts +10 -0
  59. package/dist/react/messenger/saga.js +33 -0
  60. package/dist/react/messenger/segment.d.ts +8 -0
  61. package/dist/react/messenger/segment.js +15 -0
  62. package/dist/react/messenger/select.d.ts +10 -0
  63. package/dist/react/messenger/select.js +26 -0
  64. package/dist/react/messenger/sheet.d.ts +10 -0
  65. package/dist/react/messenger/sheet.js +57 -0
  66. package/dist/react/messenger/split.d.ts +9 -0
  67. package/dist/react/messenger/split.js +17 -0
  68. package/dist/react/messenger/swap.d.ts +10 -0
  69. package/dist/react/messenger/swap.js +18 -0
  70. package/dist/react/messenger/thanks.d.ts +10 -0
  71. package/dist/react/messenger/thanks.js +18 -0
  72. package/dist/react/messenger/tiles.d.ts +8 -0
  73. package/dist/react/messenger/tiles.js +20 -0
  74. package/dist/react/messenger/types.d.ts +98 -0
  75. package/dist/react/messenger/types.js +1 -0
  76. package/dist/react/messenger/urgency.d.ts +10 -0
  77. package/dist/react/messenger/urgency.js +29 -0
  78. package/dist/react/use-lead-error.d.ts +3 -3
  79. package/dist/react/use-lead-events.d.ts +21 -1
  80. package/dist/react/use-lead-events.js +9 -3
  81. package/dist/react/use-lead-submit.d.ts +20 -6
  82. package/dist/react/use-lead-submit.js +23 -1
  83. package/dist/server/index.d.ts +1 -1
  84. package/dist/server/lead-store-sqlite.js +11 -5
  85. package/dist/server/lead-webhook.d.ts +30 -5
  86. package/dist/server/lead-webhook.js +13 -7
  87. package/dist/server/notify.js +11 -5
  88. package/dist/testing/index.d.ts +1 -1
  89. package/dist/testing/index.js +1 -1
  90. package/dist/testing/landing-contract.js +6 -1
  91. package/dist/testing/lead-rules.d.ts +8 -0
  92. package/dist/testing/lead-rules.js +30 -1
  93. package/package.json +5 -2
  94. package/template/app/quote/route.ts +3 -0
  95. package/template/flake.nix +1 -1
  96. package/template/package.json +2 -2
  97. package/template/tests/e2e/ab-switcher.spec.ts +2 -0
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@evinvest/uikit";
3
+ import { ChannelPicker, Glyph, ModeBody, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
4
+ const chipOf = (words, mode) => mode === "whatsapp" ? words.messengerChipWhatsapp : mode === "telegram" ? words.messengerChipTelegram : words.messengerChipCall;
5
+ /**
6
+ * VF-5: the lede becomes a chip naming where the answer comes — its menu an
7
+ * overlay over the form — and the slot follows it: the message, the bot, or the phone.
8
+ */
9
+ export default function Chip({ kit }) {
10
+ return kit.at === "afterHead" ? _jsx(ChipMenu, { kit: kit }) : _jsx(ChipBody, { kit: kit });
11
+ }
12
+ function ChipMenu({ kit }) {
13
+ const mode = kit.mode ?? "whatsapp";
14
+ return (_jsx(ChannelPicker, { kit: kit,
15
+ // The chip is the picker itself: the picker's part dresses it too.
16
+ triggerClassName: cn("h-8 w-fit rounded-full bg-muted px-3 text-sm font-medium text-ink", kit.classNames?.messengerPicker), trigger: _jsxs("span", { className: "flex items-center gap-2", children: [_jsx(Glyph, { kit: kit, channel: mode }), chipOf(wordsOf(kit), mode)] }) }));
17
+ }
18
+ function ChipBody({ kit }) {
19
+ const message = useMessage(kit);
20
+ const action = useMessengerAction(kit, message, () => toCall(kit));
21
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsx(ModeBody, { kit: kit, message: message, action: action }), action.overlay] }));
22
+ }
@@ -0,0 +1,8 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * Any variant on a place without WhatsApp but with a bot: the control, and
4
+ * «ou via Telegram» under its submit.
5
+ */
6
+ export default function Fallback({ kit }: {
7
+ kit: MessengerKit;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TelegramLink, toCall, useMessage, useMessengerAction } from "./action.js";
3
+ /**
4
+ * Any variant on a place without WhatsApp but with a bot: the control, and
5
+ * «ou via Telegram» under its submit.
6
+ */
7
+ export default function Fallback({ kit }) {
8
+ const message = useMessage(kit);
9
+ const action = useMessengerAction(kit, message, () => toCall(kit));
10
+ return (_jsxs(_Fragment, { children: [_jsx(TelegramLink, { kit: kit, action: action }), action.overlay] }));
11
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * AQ-5: the channel first, on a screen of its own — WhatsApp (recommended),
4
+ * a call, the bot — then the job and the channel's slot, «Changer de canal»
5
+ * in the same place on every screen. The card's height is the brand's
6
+ * (`root`): the screens change what is in it, the button stays at the bottom.
7
+ */
8
+ export default function Saga({ kit }: {
9
+ kit: MessengerKit;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, cn } from "@evinvest/uikit";
4
+ import { ModeBody, modesOf, OptionCard, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
5
+ const headOf = (words, mode) => mode === "whatsapp" ? [words.messengerWhatsappTitle, words.messengerWhatsappLede] : mode === "telegram" ? [words.messengerTelegramTitle, words.messengerTelegramLede] : [words.messengerCallTitle, words.messengerCallLede];
6
+ /**
7
+ * AQ-5: the channel first, on a screen of its own — WhatsApp (recommended),
8
+ * a call, the bot — then the job and the channel's slot, «Changer de canal»
9
+ * in the same place on every screen. The card's height is the brand's
10
+ * (`root`): the screens change what is in it, the button stays at the bottom.
11
+ */
12
+ export default function Saga({ kit }) {
13
+ if (kit.at === "head")
14
+ return _jsx(SagaHead, { kit: kit });
15
+ if (kit.at === "body")
16
+ return _jsx(SagaPick, { kit: kit });
17
+ return _jsx(SagaContact, { kit: kit });
18
+ }
19
+ function SagaHead({ kit }) {
20
+ const words = wordsOf(kit);
21
+ const [title, lede] = kit.mode === null ? [words.messengerSagaTitle, words.messengerSagaLede] : headOf(words, kit.mode);
22
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [kit.mode !== null && (_jsxs(Button, { type: "button", variant: "link", size: "sm", className: "self-start px-0", onClick: () => kit.setMode(null), children: ["\u2039 ", words.messengerChange] })), _jsx("p", { className: "font-display text-2xl font-bold text-ink", children: title }), _jsx("p", { className: "text-ink-soft", children: lede })] }));
23
+ }
24
+ function SagaPick({ kit }) {
25
+ const words = wordsOf(kit);
26
+ return (_jsxs("div", { className: cn("flex flex-1 flex-col gap-3", kit.classNames?.messenger), children: [modesOf(kit).map(mode => (_jsx(OptionCard, { kit: kit, mode: mode, recommended: mode === "whatsapp", onPick: () => (mode === "call" ? toCall(kit) : kit.setMode(mode)) }, mode))), _jsx("p", { className: "mt-auto text-center text-sm text-ink-soft", children: words.messengerSagaFoot })] }));
27
+ }
28
+ function SagaContact({ kit }) {
29
+ const words = wordsOf(kit);
30
+ const message = useMessage(kit);
31
+ const action = useMessengerAction(kit, message, () => toCall(kit));
32
+ return (_jsxs("div", { className: cn("flex flex-1 flex-col gap-3", kit.classNames?.messenger), children: [_jsx(ModeBody, { kit: kit, message: message, action: action, labels: { whatsapp: words.messengerOpenWhatsapp } }), action.overlay] }));
33
+ }
@@ -0,0 +1,8 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * AQ-2: «WhatsApp | Appel» over a slot of one height — the message ready to
4
+ * send, or the phone — and the bot as a link under the button.
5
+ */
6
+ export default function Segment({ kit }: {
7
+ kit: MessengerKit;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn, ToggleGroup, ToggleGroupItem } from "@evinvest/uikit";
4
+ import { Glyph, labelOf, ModeBody, TelegramLink, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
5
+ /**
6
+ * AQ-2: «WhatsApp | Appel» over a slot of one height — the message ready to
7
+ * send, or the phone — and the bot as a link under the button.
8
+ */
9
+ export default function Segment({ kit }) {
10
+ const words = wordsOf(kit);
11
+ const message = useMessage(kit);
12
+ const action = useMessengerAction(kit, message, () => toCall(kit));
13
+ const mode = kit.mode === "call" ? "call" : "whatsapp";
14
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsx(ToggleGroup, { role: "group", type: "single", value: mode, onValueChange: v => (v === "call" || v === "whatsapp" ? kit.setMode(v) : undefined), "aria-label": words.messengerChannelLabel, className: cn("w-full gap-1 rounded-[var(--control-radius)] bg-muted p-1", kit.classNames?.messengerPicker), children: ["whatsapp", "call"].map(m => (_jsxs(ToggleGroupItem, { value: m, className: cn("h-10 flex-1 gap-2 rounded-[var(--control-radius)] data-[state=on]:bg-card data-[state=on]:text-ink", kit.classNames?.messengerOption, kit.classNames?.messengerSegment), children: [_jsx(Glyph, { kit: kit, channel: m }), labelOf(words, m)] }, m))) }), _jsx(ModeBody, { kit: { ...kit, mode }, message: message, action: action }), _jsx(TelegramLink, { kit: kit, action: action }), action.overlay] }));
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * AQ-1 / VF-1: the channel picked in the phone field itself — before the
4
+ * number (`prefix`) or after it (`suffix`). WhatsApp: the number optional,
5
+ * the button opens the chat. Appel: the number required, the control's
6
+ * submit. Telegram: no number, the button opens the bot.
7
+ */
8
+ export default function SelectVariant({ kit }: {
9
+ kit: MessengerKit;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn, InputGroup, InputGroupAddon } from "@evinvest/uikit";
3
+ import { ChannelPicker, Glyph, InappHint, labelOf, MessengerCta, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
4
+ /**
5
+ * AQ-1 / VF-1: the channel picked in the phone field itself — before the
6
+ * number (`prefix`) or after it (`suffix`). WhatsApp: the number optional,
7
+ * the button opens the chat. Appel: the number required, the control's
8
+ * submit. Telegram: no number, the button opens the bot.
9
+ */
10
+ export default function SelectVariant({ kit }) {
11
+ const words = wordsOf(kit);
12
+ const message = useMessage(kit);
13
+ const action = useMessengerAction(kit, message, () => toCall(kit));
14
+ const mode = kit.mode ?? "whatsapp";
15
+ const side = kit.variant.kind === "select" && kit.variant.side === "suffix" ? "end" : "start";
16
+ const picker = (_jsx(ChannelPicker, { kit: kit, triggerClassName: cn("h-full rounded-none bg-transparent px-3", side === "start" ? "border-r border-input" : "border-l border-input"), trigger: _jsxs("span", { className: "flex items-center gap-2 font-medium text-ink", children: [_jsx(Glyph, { kit: kit, channel: mode }), labelOf(words, mode)] }) }));
17
+ const hint = mode === "call" ? undefined : _jsx("p", { className: cn("text-sm text-ink-soft", kit.classNames?.messengerHint), children: mode === "whatsapp" ? words.messengerWhatsappHint : words.messengerTelegramHint });
18
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [kit.phone({
19
+ optional: mode !== "call",
20
+ disabled: mode === "telegram",
21
+ ...(mode !== "call" ? { placeholder: words.messengerPhoneOptional } : {}),
22
+ frame: (input) => (_jsxs(InputGroup, { className: cn("h-12 rounded-[var(--control-radius)]", kit.classNames?.messengerPicker), children: [side === "start" && _jsx(InputGroupAddon, { align: "inline-start", children: picker }), input, side === "end" && _jsx(InputGroupAddon, { align: "inline-end", children: picker })] })),
23
+ ...(hint ? { after: hint } : {}),
24
+ }), action.panel ??
25
+ (mode === "call" ? (kit.submit()) : (_jsx(MessengerCta, { kit: kit, action: action, channel: mode, label: mode === "whatsapp" ? words.messengerWhatsappCta : words.messengerTelegramCta }))), mode === "whatsapp" && _jsx(InappHint, { kit: kit, message: message.message }), action.overlay] }));
26
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * VF-4: the card is the estimate, the postcode and one button; the button
4
+ * opens the kit's drawer — «Où recevoir votre devis ?»: WhatsApp
5
+ * (recommended), the bot, a call — and a call asks the phone in the drawer,
6
+ * a field of the card's form wherever the drawer is drawn (`form`).
7
+ */
8
+ export default function Sheet({ kit }: {
9
+ kit: MessengerKit;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, cn, Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@evinvest/uikit";
4
+ import { useEffect, useId, useRef, useState } from "react";
5
+ import { InappHint, modesOf, OptionCard, useMessage, useMessengerAction, wordsOf } from "./action.js";
6
+ /**
7
+ * VF-4: the card is the estimate, the postcode and one button; the button
8
+ * opens the kit's drawer — «Où recevoir votre devis ?»: WhatsApp
9
+ * (recommended), the bot, a call — and a call asks the phone in the drawer,
10
+ * a field of the card's form wherever the drawer is drawn (`form`).
11
+ */
12
+ export default function Sheet({ kit }) {
13
+ const words = wordsOf(kit);
14
+ const message = useMessage(kit);
15
+ const [open, setOpen] = useState(false);
16
+ const [step, setStep] = useState("pick");
17
+ const action = useMessengerAction(kit, message, () => {
18
+ setStep("call");
19
+ setOpen(true);
20
+ });
21
+ const formId = `${kit.id}-form`;
22
+ // The dialog is named by its heading, on either step: uikit's drawer does not wire it.
23
+ const titleId = useId();
24
+ // A step swaps the drawer's content, the pressed button with it: the focus
25
+ // would fall to the page, and Escape with it. The call's step gives it to
26
+ // the phone, the way back to the first channel — on whatever path led there.
27
+ const shown = useRef(step);
28
+ useEffect(() => {
29
+ const was = shown.current;
30
+ shown.current = step;
31
+ if (!open || (step === "pick" && was === "pick"))
32
+ return;
33
+ // After the drawer's own focus scope, which takes the focus as it opens.
34
+ const frame = requestAnimationFrame(() => {
35
+ const dialog = document.getElementById(titleId)?.closest("[role=dialog]");
36
+ const target = step === "call"
37
+ ? document.querySelector(`[data-lead-field="phone"][form="${CSS.escape(formId)}"]`)
38
+ : dialog?.querySelector("[data-messenger-option]");
39
+ target?.focus();
40
+ });
41
+ return () => cancelAnimationFrame(frame);
42
+ }, [open, step, formId, titleId]);
43
+ // The app takes over: the drawer goes with it — unless the tap drew the QR code in it.
44
+ const closing = (link) => link && {
45
+ ...link,
46
+ onClick: (event) => {
47
+ link.onClick(event);
48
+ if (!event.defaultPrevented)
49
+ setOpen(false);
50
+ },
51
+ };
52
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsxs(Drawer, { open: open, onOpenChange: next => {
53
+ setOpen(next);
54
+ if (!next)
55
+ setStep("pick");
56
+ }, children: [_jsx(DrawerTrigger, { asChild: true, children: _jsx(Button, { type: "button", size: "touch", className: cn("w-full", kit.classNames?.messengerCta), children: words.messengerSheetCta }) }), _jsx(DrawerContent, { "aria-labelledby": titleId, children: _jsx("div", { className: cn("flex flex-col gap-4 p-4", kit.classNames?.messengerPicker), children: step === "pick" ? (_jsxs(_Fragment, { children: [_jsx(DrawerTitle, { id: titleId, className: "font-display text-xl font-bold text-ink", children: words.messengerSheetTitle }), action.panel, modesOf(kit, ["whatsapp", "telegram", "call"]).map(mode => mode === "call" ? (_jsx(OptionCard, { kit: kit, mode: mode, onPick: () => setStep("call") }, mode)) : (_jsx(OptionCard, { kit: kit, mode: mode, link: closing(action.link(mode)), recommended: mode === "whatsapp" }, mode))), _jsx(InappHint, { kit: kit, message: message.message })] })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "link", size: "sm", className: "self-start px-0", onClick: () => setStep("pick"), children: ["\u2039 ", words.messengerChange] }), _jsx(DrawerTitle, { id: titleId, className: "font-display text-xl font-bold text-ink", children: words.messengerCallTitle }), kit.phone({ form: formId }), kit.submit(words.messengerCallback, { form: formId })] })) }) })] }), action.overlay] }));
57
+ }
@@ -0,0 +1,9 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * VF-6: the slot — the message, or the phone — over one row: «Devis sur
4
+ * WhatsApp» with the bot and a call as squares beside it; the call's square
5
+ * turns the row into «Être rappelé» with WhatsApp and the bot as squares.
6
+ */
7
+ export default function Split({ kit }: {
8
+ kit: MessengerKit;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "@evinvest/uikit";
4
+ import { InappHint, MessengerCta, Preview, Slot, SquareButton, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
5
+ /**
6
+ * VF-6: the slot — the message, or the phone — over one row: «Devis sur
7
+ * WhatsApp» with the bot and a call as squares beside it; the call's square
8
+ * turns the row into «Être rappelé» with WhatsApp and the bot as squares.
9
+ */
10
+ export default function Split({ kit }) {
11
+ const words = wordsOf(kit);
12
+ const message = useMessage(kit);
13
+ const action = useMessengerAction(kit, message, () => toCall(kit));
14
+ const call = kit.mode === "call";
15
+ const telegram = kit.facts.telegram && _jsx(MessengerCta, { kit: kit, action: action, channel: "telegram", label: words.messengerOptionTelegram, variant: "outline", square: true });
16
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsx(Slot, { kit: kit, children: call ? kit.phone() : (action.panel ?? _jsx(Preview, { kit: kit, preview: message.preview })) }), _jsx("div", { className: "flex gap-2", children: call ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-0 flex-1", children: kit.submit(words.messengerCallback) }), _jsx(SquareButton, { kit: kit, channel: "whatsapp", label: words.messengerBackToWhatsapp, onClick: () => kit.setMode("whatsapp") }), telegram] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(MessengerCta, { kit: kit, action: action, channel: "whatsapp", label: words.messengerSplitCta }) }), telegram, _jsx(SquareButton, { kit: kit, channel: "call", label: words.messengerCallback, onClick: () => toCall(kit) })] })) }), !call && _jsx(InappHint, { kit: kit, message: message.message }), action.overlay] }));
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * AQ-4 / VF-3: no phone until it is asked for. Slot A is the WhatsApp button,
4
+ * row B the bot and «Être rappelé»; «Être rappelé» swaps them — A becomes the
5
+ * phone, B a square back to WhatsApp beside the call's submit — so the card
6
+ * keeps its height. On a computer, the QR code takes slot A.
7
+ */
8
+ export default function Swap({ kit }: {
9
+ kit: MessengerKit;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, cn } from "@evinvest/uikit";
4
+ import { Glyph, InappHint, MessengerCta, Slot, SquareButton, toCall, useDesktop, useMessage, useMessengerAction, wordsOf } from "./action.js";
5
+ /**
6
+ * AQ-4 / VF-3: no phone until it is asked for. Slot A is the WhatsApp button,
7
+ * row B the bot and «Être rappelé»; «Être rappelé» swaps them — A becomes the
8
+ * phone, B a square back to WhatsApp beside the call's submit — so the card
9
+ * keeps its height. On a computer, the QR code takes slot A.
10
+ */
11
+ export default function Swap({ kit }) {
12
+ const words = wordsOf(kit);
13
+ const message = useMessage(kit);
14
+ const action = useMessengerAction(kit, message, () => toCall(kit));
15
+ const call = kit.mode === "call";
16
+ const desktop = useDesktop();
17
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsx(Slot, { kit: kit, children: call ? kit.phone({ after: null }) : (action.panel ?? _jsx(MessengerCta, { kit: kit, action: action, channel: "whatsapp", label: words.messengerWhatsappCta })) }), _jsx("div", { className: "flex gap-3", children: call ? (_jsxs(_Fragment, { children: [_jsx(SquareButton, { kit: kit, channel: "whatsapp", label: words.messengerBackToWhatsapp, onClick: () => kit.setMode("whatsapp") }), _jsx("div", { className: "min-w-0 flex-1", children: kit.submit() })] })) : (_jsxs(_Fragment, { children: [kit.facts.telegram && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(MessengerCta, { kit: kit, action: action, channel: "telegram", label: desktop ? words.messengerTelegramCta : words.messengerOptionTelegram, variant: "outline" }) })), _jsxs(Button, { type: "button", variant: "outline", size: "touch", className: cn("min-w-0 flex-1", kit.classNames?.messengerSecondary), onClick: () => toCall(kit), children: [_jsx(Glyph, { kit: kit, channel: "call" }), words.messengerCallback] })] })) }), !call && _jsx(InappHint, { kit: kit, message: message.message }), action.overlay] }));
18
+ }
@@ -0,0 +1,10 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * AQ-3: the form is the control's; its success adds the faster way — a photo
4
+ * on WhatsApp, the message carrying the lead's own reference — and the bot.
5
+ * On a computer, WhatsApp is a QR code to scan with the phone that takes the
6
+ * photo. The lead is sent already: a tap here posts nothing.
7
+ */
8
+ export default function Thanks({ kit }: {
9
+ kit: MessengerKit;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "@evinvest/uikit";
4
+ import { hrefOf, InappHint, MessengerCta, QrPanel, useDesktop, useMessage, useMessengerAction, wordsOf } from "./action.js";
5
+ /**
6
+ * AQ-3: the form is the control's; its success adds the faster way — a photo
7
+ * on WhatsApp, the message carrying the lead's own reference — and the bot.
8
+ * On a computer, WhatsApp is a QR code to scan with the phone that takes the
9
+ * photo. The lead is sent already: a tap here posts nothing.
10
+ */
11
+ export default function Thanks({ kit }) {
12
+ const words = wordsOf(kit);
13
+ const message = useMessage(kit);
14
+ const action = useMessengerAction(kit, message, () => undefined, { followUp: true });
15
+ const desktop = useDesktop();
16
+ const whatsapp = hrefOf(kit, "whatsapp", message.message);
17
+ return (_jsxs("div", { className: cn("flex flex-col gap-3 border-t border-border pt-4", kit.classNames?.messenger), children: [whatsapp && !desktop && _jsx("p", { className: "font-medium text-ink", children: words.messengerThanksTitle }), whatsapp && (desktop ? _jsx(QrPanel, { href: whatsapp, words: words, onCallback: null, kit: kit }) : _jsx(MessengerCta, { kit: kit, action: action, channel: "whatsapp", label: words.messengerThanksCta })), whatsapp && !desktop && _jsx(InappHint, { kit: kit, message: message.message }), kit.facts.telegram && _jsx(MessengerCta, { kit: kit, action: action, channel: "telegram", label: desktop ? words.messengerTelegramCta : words.messengerOptionTelegram, variant: "outline" })] }));
18
+ }
@@ -0,0 +1,8 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * VF-2: «Recevoir mon devis par» and a tile per channel — WhatsApp, Telegram,
4
+ * Appel — over a slot of one height: the message, what the bot does, or the phone.
5
+ */
6
+ export default function Tiles({ kit }: {
7
+ kit: MessengerKit;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn, ToggleGroup, ToggleGroupItem } from "@evinvest/uikit";
4
+ import { Glyph, labelOf, ModeBody, modesOf, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
5
+ /**
6
+ * VF-2: «Recevoir mon devis par» and a tile per channel — WhatsApp, Telegram,
7
+ * Appel — over a slot of one height: the message, what the bot does, or the phone.
8
+ */
9
+ export default function Tiles({ kit }) {
10
+ const words = wordsOf(kit);
11
+ const message = useMessage(kit);
12
+ const action = useMessengerAction(kit, message, () => toCall(kit));
13
+ const modes = modesOf(kit, ["whatsapp", "telegram", "call"]);
14
+ const mode = kit.mode ?? "whatsapp";
15
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsx("p", { className: "text-sm font-medium text-ink", children: words.messengerTilesLabel }), _jsx(ToggleGroup, { role: "group", type: "single", value: mode, onValueChange: v => {
16
+ const picked = modes.find((m) => m === v);
17
+ if (picked)
18
+ kit.setMode(picked);
19
+ }, "aria-label": words.messengerTilesLabel, className: cn("grid w-full gap-2", modes.length === 3 ? "grid-cols-3" : "grid-cols-2", kit.classNames?.messengerPicker), children: modes.map(m => (_jsxs(ToggleGroupItem, { value: m, className: cn("h-11 gap-2 rounded-[var(--control-radius)] border border-border first:rounded-[var(--control-radius)] last:rounded-[var(--control-radius)] data-[state=on]:border-primary data-[state=on]:text-ink", kit.classNames?.messengerOption, kit.classNames?.messengerTile), children: [_jsx(Glyph, { kit: kit, channel: m }), labelOf(words, m)] }, m))) }), _jsx(ModeBody, { kit: kit, message: message, action: action }), action.overlay] }));
20
+ }
@@ -0,0 +1,98 @@
1
+ import type { ReactNode } from "react";
2
+ import type { LeadWire } from "../../core/lead.js";
3
+ import type { LeadCaptureText } from "../../core/lead-capture-text.js";
4
+ import type { MessengerFacts, MessengerMode, MessengerVariant } from "../../core/messenger.js";
5
+ import type { ChannelIconKey, Experiment } from "../LeadCaptureChannels.js";
6
+ import type { LeadNeedOption } from "../LeadCaptureNeed.js";
7
+ import type { PartClassNames } from "../parts.js";
8
+ import type { LeadEvents } from "../use-lead-events.js";
9
+ /**
10
+ * Where in the card a variant draws: in place of the heading (`head`), under
11
+ * it (`afterHead`), before the need (`beforeNeed`), in place of the form's
12
+ * fields (`body`), in place of the phone and the submit (`contact`), under
13
+ * the submit (`afterSubmit`), or in the success (`done`).
14
+ */
15
+ export type MessengerAt = "head" | "afterHead" | "beforeNeed" | "body" | "contact" | "afterSubmit" | "done";
16
+ /** A variant as drawn: its own kind, or the control with a way to the bot (`fallback`). */
17
+ export type MessengerShown = MessengerVariant["kind"] | "fallback";
18
+ /**
19
+ * The parts a variant dresses; a brand sizes `messengerSlot` (92 px, 72 px)
20
+ * so no state of a variant moves the card. `messengerCta`: a variant's main
21
+ * button — a messenger's, or the call's submit; `messengerSecondary`: the
22
+ * others (Telegram and «Être rappelé» beside it, «ou via Telegram», the way
23
+ * back to the chat); `messengerSquare`: the icon squares; `messengerSegment`,
24
+ * `messengerTile`: a segment's and a tile's item, after `messengerOption`;
25
+ * `messengerTrigger`: the channel picker's button (`select`, `chip`).
26
+ */
27
+ export type MessengerPart = "messenger" | "messengerSlot" | "messengerCta" | "messengerSecondary" | "messengerSegment" | "messengerTile" | "messengerTrigger" | "messengerPreview" | "messengerPicker" | "messengerOption" | "messengerSquare" | "messengerQr" | "messengerReturn" | "messengerHint";
28
+ /** How a variant asks for the card's phone field. */
29
+ export interface PhoneOptions {
30
+ optional?: boolean;
31
+ disabled?: boolean;
32
+ placeholder?: string;
33
+ /** The variant's box around the input (`PhoneField`'s `frame`). */
34
+ frame?: (input: ReactNode) => ReactNode;
35
+ /** The form's id, for a field drawn outside the form's element (a drawer). */
36
+ form?: string;
37
+ /** Under the field instead of the brand's `afterPhone`. */
38
+ after?: ReactNode;
39
+ }
40
+ /**
41
+ * What `LeadCapture` hands a variant: the card's facts and state, and its own
42
+ * pieces — the phone field and the submit — so a variant arranges them and
43
+ * never re-implements them.
44
+ */
45
+ export interface MessengerKit {
46
+ at: MessengerAt;
47
+ variant: MessengerVariant;
48
+ shown: MessengerShown;
49
+ facts: MessengerFacts;
50
+ /** The card's id; its form is `<id>-form`. */
51
+ id: string;
52
+ locale: string;
53
+ /** The brand's name, the message's greeting. */
54
+ brand: string;
55
+ text: LeadCaptureText;
56
+ wire: LeadWire;
57
+ needs: readonly LeadNeedOption[];
58
+ /** The need on screen, by its label — for the message. */
59
+ needLabel: string | null;
60
+ /** The estimate the card shows, formatted — for the message. */
61
+ priceText: string | null;
62
+ /** The estimate's answers, by their short labels, in order — the timing question's left out. */
63
+ answers: readonly string[];
64
+ /**
65
+ * The answer to the estimate's question the brand names (`messengerTiming`):
66
+ * its label for the message's timing line, its short one for the preview.
67
+ */
68
+ timing: {
69
+ label: string;
70
+ short: string;
71
+ } | null;
72
+ /** The channel the visitor is on; `null` before a variant asks (`saga`, `urgency`). */
73
+ mode: MessengerMode | null;
74
+ setMode: (mode: MessengerMode | null) => void;
75
+ /** The card's reference (`message_ref`), minted once the script runs; `null` before. */
76
+ messageRef: string | null;
77
+ /**
78
+ * Whether a messenger link may lead anywhere: the script runs and the
79
+ * reference is minted. Until then a tap would open the chat with no lead
80
+ * posted and no `Réf.` to find it by — the links are inert.
81
+ */
82
+ ready: boolean;
83
+ /** A new reference, once a messenger lead is posted. */
84
+ renewRef: () => void;
85
+ phone: (options?: PhoneOptions) => ReactNode;
86
+ /** The card's submit — a call lead, posted as the control posts it. */
87
+ submit: (label?: string, options?: {
88
+ form?: string;
89
+ className?: string;
90
+ }) => ReactNode;
91
+ /** The default heading's words, for a variant that redraws the heading. */
92
+ title: string;
93
+ lede: string;
94
+ events: LeadEvents;
95
+ icons: Partial<Record<ChannelIconKey, ReactNode>> | undefined;
96
+ experiment: Experiment | undefined;
97
+ classNames: PartClassNames<MessengerPart> | undefined;
98
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { MessengerKit } from "./types.js";
2
+ /**
3
+ * AQ-6: «C’est urgent ?» picks the channel — today is a call, «je compare»
4
+ * the message ready in the slot — and the answer is the lead's, posted as
5
+ * the brand's extra (`field`, which `site.lead.extras` must list). Without
6
+ * WhatsApp the question stays, and both answers ask the phone.
7
+ */
8
+ export default function Urgency({ kit }: {
9
+ kit: MessengerKit;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn, ToggleGroup, ToggleGroupItem } from "@evinvest/uikit";
4
+ import { fillText } from "../../core/lead-capture-format.js";
5
+ import { MessengerCta, InappHint, Preview, Slot, TelegramLink, toCall, useMessage, useMessengerAction, wordsOf } from "./action.js";
6
+ /** What the answer is posted as, in the brand's extra: a slug, never the label. */
7
+ const ANSWER = { call: "today", whatsapp: "later" };
8
+ /**
9
+ * AQ-6: «C’est urgent ?» picks the channel — today is a call, «je compare»
10
+ * the message ready in the slot — and the answer is the lead's, posted as
11
+ * the brand's extra (`field`, which `site.lead.extras` must list). Without
12
+ * WhatsApp the question stays, and both answers ask the phone.
13
+ */
14
+ export default function Urgency({ kit }) {
15
+ return kit.at === "beforeNeed" ? _jsx(Question, { kit: kit }) : _jsx(UrgencyContact, { kit: kit });
16
+ }
17
+ function Question({ kit }) {
18
+ const words = wordsOf(kit);
19
+ const field = kit.variant.kind === "urgency" ? kit.variant.field : "urgency";
20
+ const answer = kit.mode === "call" || kit.mode === "whatsapp" ? kit.mode : null;
21
+ return (_jsxs("div", { className: cn("flex flex-col gap-2", kit.classNames?.messenger), children: [_jsx("p", { className: "text-sm font-medium text-ink", children: words.messengerUrgencyLabel }), _jsx(ToggleGroup, { role: "group", type: "single", value: answer ?? "", onValueChange: v => (v === "call" || v === "whatsapp" ? kit.setMode(v) : undefined), "aria-label": words.messengerUrgencyLabel, className: cn("grid w-full grid-cols-2 gap-2", kit.classNames?.messengerPicker), children: ["call", "whatsapp"].map(m => (_jsx(ToggleGroupItem, { value: m, className: cn("h-11 rounded-[var(--control-radius)] border border-border first:rounded-[var(--control-radius)] last:rounded-[var(--control-radius)] data-[state=on]:border-primary data-[state=on]:text-ink", kit.classNames?.messengerOption, kit.classNames?.messengerTile), children: m === "call" ? words.messengerUrgencyYes : words.messengerUrgencyNo }, m))) }), answer && _jsx("input", { type: "hidden", name: field, value: ANSWER[answer] })] }));
22
+ }
23
+ function UrgencyContact({ kit }) {
24
+ const words = wordsOf(kit);
25
+ const message = useMessage(kit, kit.mode === "call" ? words.messengerUrgencyToday : kit.mode === "whatsapp" ? words.messengerUrgencyLater : null);
26
+ const action = useMessengerAction(kit, message, () => toCall(kit));
27
+ const chat = kit.mode === "whatsapp" && kit.facts.whatsapp !== null;
28
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", kit.classNames?.messenger), children: [_jsx(Slot, { kit: kit, children: chat ? (action.panel ?? _jsx(Preview, { kit: kit, preview: message.preview, title: words.messengerNotUrgentTitle, lede: fillText(words.messengerNotUrgentLede, { ref: kit.messageRef ?? "" }) })) : kit.phone() }), chat ? _jsx(MessengerCta, { kit: kit, action: action, channel: "whatsapp", label: words.messengerWhatsappCta }) : kit.submit(kit.mode === "call" ? words.messengerUrgentSubmit : undefined), chat && _jsx(InappHint, { kit: kit, message: message.message }), _jsx(TelegramLink, { kit: kit, action: action }), action.overlay] }));
29
+ }
@@ -1,11 +1,11 @@
1
1
  import { type RefObject } from "react";
2
- import { type LeadChannel, type LeadError } from "../core/lead.js";
2
+ import { type LeadError } from "../core/lead.js";
3
3
  import type { LeadCaptureText } from "../core/lead-capture-text.js";
4
4
  export type { LeadError };
5
5
  /** The fields `LeadCapture` draws itself, which show a refusal under themselves. */
6
- export declare const OWN_FIELDS: Readonly<Record<LeadChannel, readonly string[]>>;
6
+ export declare const OWN_FIELDS: Readonly<Record<LeadError["channel"], readonly string[]>>;
7
7
  /** The id of the message a refusal of a field the card does not draw is shown in, above the submit. */
8
- export declare const formMessageId: (id: string, channel: LeadChannel) => string;
8
+ export declare const formMessageId: (id: string, channel: LeadError["channel"]) => string;
9
9
  /** The words for a refused field: the field's own rule where the form has one. */
10
10
  export declare function errorText(field: string, text: LeadCaptureText): string;
11
11
  /**
@@ -1,5 +1,6 @@
1
1
  import { type RefObject } from "react";
2
2
  import { type LeadField, type LeadStep } from "../core/analytics.js";
3
+ import type { ChannelsAvailable } from "../core/messenger.js";
3
4
  /** The attribute a lead form's control names its role in, for `lead_form_field_error`. */
4
5
  export declare const FIELD_ATTR = "data-lead-field";
5
6
  export interface LeadEvents {
@@ -10,13 +11,30 @@ export interface LeadEvents {
10
11
  submitError(reason: "network" | "timeout", channel: "form" | "callback"): void;
11
12
  /** An estimate's price for a full set of answers: its band only, once per need and band. */
12
13
  estimateShown(need: string, cents: number): void;
14
+ /** A messenger CTA: the lead posted, the visitor sent on — never what was typed. */
15
+ messengerOpen(open: {
16
+ channel: "whatsapp" | "telegram";
17
+ device: "mobile" | "desktop";
18
+ inapp: boolean;
19
+ ref: string | null;
20
+ }): void;
21
+ /** On a computer: the QR code that opens WhatsApp on the phone was shown (`contact_intent_click`, `whatsapp_qr`). */
22
+ qrShown(): void;
23
+ /** Back on the page after a messenger: what the visitor said of it. */
24
+ messengerReturn(back: {
25
+ channel: "whatsapp" | "telegram";
26
+ answer: "sent" | "failed";
27
+ ref: string | null;
28
+ }): void;
13
29
  }
14
30
  /**
15
31
  * The lead form's own funnel, reported through the boundary's sink: seen
16
32
  * (half of it in view, once), started (the first focus inside, once), a
17
33
  * field the browser refused (`invalid`, by role — never the value), and the
18
34
  * step `qualify-first` moved to. Every event carries the form, the layout and
19
- * the site's experiment, so the arms of a test compare on one schema.
35
+ * the site's experiment, so the arms of a test compare on one schema — and
36
+ * the messengers the card offered (`channels_available`) and its `messenger`
37
+ * variant, so an arm that fell back to the control is told apart.
20
38
  */
21
39
  export declare function useLeadEvents(root: RefObject<HTMLElement | null>, tags: {
22
40
  formId: string;
@@ -25,4 +43,6 @@ export declare function useLeadEvents(root: RefObject<HTMLElement | null>, tags:
25
43
  name: string;
26
44
  variant: string;
27
45
  } | undefined;
46
+ channels?: ChannelsAvailable | undefined;
47
+ messenger?: string | undefined;
28
48
  }): LeadEvents;
@@ -10,14 +10,16 @@ export const FIELD_ATTR = "data-lead-field";
10
10
  * (half of it in view, once), started (the first focus inside, once), a
11
11
  * field the browser refused (`invalid`, by role — never the value), and the
12
12
  * step `qualify-first` moved to. Every event carries the form, the layout and
13
- * the site's experiment, so the arms of a test compare on one schema.
13
+ * the site's experiment, so the arms of a test compare on one schema — and
14
+ * the messengers the card offered (`channels_available`) and its `messenger`
15
+ * variant, so an arm that fell back to the control is told apart.
14
16
  */
15
17
  export function useLeadEvents(root, tags) {
16
18
  const sink = useAnalyticsSink();
17
- const { formId, layout } = tags;
19
+ const { formId, layout, channels, messenger } = tags;
18
20
  const name = tags.experiment?.name;
19
21
  const variant = tags.experiment?.variant;
20
- const props = useMemo(() => ({ form_id: formId, layout, ...experimentProps(name, variant) }), [formId, layout, name, variant]);
22
+ const props = useMemo(() => ({ form_id: formId, layout, ...experimentProps(name, variant), ...(channels ? { channels_available: channels } : {}), ...(messenger ? { messenger_variant: messenger } : {}) }), [formId, layout, name, variant, channels, messenger]);
21
23
  const started = useRef(false);
22
24
  const shown = useRef(new Set());
23
25
  useEffect(() => {
@@ -69,5 +71,9 @@ export function useLeadEvents(root, tags) {
69
71
  shown.current.add(key);
70
72
  sink?.capture(EVENTS.estimateShown, { ...props, need, cents_bucket });
71
73
  },
74
+ // Beacons: the page is being left for the app.
75
+ messengerOpen: ({ channel, device, inapp, ref }) => sink?.capture(EVENTS.messengerOpen, { ...props, channel, device, inapp, ...(ref ? { message_ref: ref } : {}) }, { transport: "beacon" }),
76
+ qrShown: () => sink?.capture(EVENTS.intent, { ...props, channel: "whatsapp_qr" }),
77
+ messengerReturn: ({ channel, answer, ref }) => sink?.capture(EVENTS.messengerReturn, { ...props, channel, answer, ...(ref ? { message_ref: ref } : {}) }),
72
78
  }), [sink, props]);
73
79
  }