@samva/markup 0.14.0 → 0.15.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.
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@ Authoring entries are what a template file imports:
23
23
  functions a body may call.
24
24
  - `@samva/markup/sms` and `@samva/markup/whatsapp` — the channel components (`Sms`; `WhatsApp` with
25
25
  `WhatsApp.Header`, `Body`, `Footer`, `Buttons`, `UrlButton`, `QuickReplyButton`, `PhoneButton`,
26
- `CopyCodeButton`). `whatsappTemplate` derives Meta's positional `{{n}}` text.
26
+ `CopyCodeButton`, `FlowButton`). `whatsappTemplate` derives Meta's positional `{{n}}` text.
27
27
  - `@samva/markup/input-schema` — `inputSchema`, which turns any Standard JSON Schema into the
28
28
  portable JSON Schema and validator a template's input is checked against.
29
29
  - `samva:brand` — `BrandLogo` (light logo, swapped for the dark one where the client honors dark
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## @samva/markup@0.15.0
2
+
3
+ ### WhatsApp template FLOW buttons
4
+
5
+ `<WhatsApp.FlowButton flow="order-survey">Take survey</WhatsApp.FlowButton>` adds a Meta FLOW
6
+ button to a WhatsApp template. `flow` is the Samva flow handle (lowercase kebab-case, the file
7
+ `flows/<handle>.json` in the template project); Samva maps it to Meta's flow id per business
8
+ account when it registers the template. `action` is `navigate` by default or `data_exchange`, and
9
+ `screen` names the screen a `navigate` flow opens on (omitted, the flow's first entry screen);
10
+ `screen` is refused with `data_exchange`. `flow`, `action` and `screen` are string literals, and a
11
+ template holds at most one flow button.
12
+
13
+ The IR button is `{ type: "flow", text, flow, action, screen? }`, and the rendered WhatsApp
14
+ channel and the editor preview carry it. The editor shows a flow button as a locked row; it is
15
+ authored in source.
16
+
1
17
  ## @samva/markup@0.14.0
2
18
 
3
19
  ### A semantic theme vocabulary, one dark-mode mechanism and a contrast check
package/README.md CHANGED
@@ -194,7 +194,7 @@ explicit argument overrides it.
194
194
  One template carries one schema and a body for each channel it supports. `@samva/markup/sms` and
195
195
  `@samva/markup/whatsapp` export the channel components (`Sms`; `WhatsApp` with `WhatsApp.Header`,
196
196
  `.Body`, `.Footer`, `.Buttons`, `.UrlButton`, `.QuickReplyButton`, `.PhoneButton` and
197
- `.CopyCodeButton`). Their names are distinct per channel, so a wrong prop fails in the type checker.
197
+ `.CopyCodeButton` and `.FlowButton`). Their names are distinct per channel, so a wrong prop fails in the type checker.
198
198
  WhatsApp bindings compile to Meta's positional `{{1}}` placeholders (`whatsappTemplate`), and a
199
199
  conditional in a WhatsApp text part is refused because Meta templates cannot branch. The platform
200
200
  gates these channels.
@@ -1,4 +1,4 @@
1
- import { D as samvaBrandPlugin, O as BRAND_FOOTER_MARKER, k as UNSUBSCRIBE_LINK, r as escapeAttribute } from "./html-eOZfDwVf.js";
1
+ import { D as samvaBrandPlugin, O as BRAND_FOOTER_MARKER, k as UNSUBSCRIBE_LINK, r as escapeAttribute } from "./html-BvRFepeO.js";
2
2
  import { a as jsx, n as Fragment } from "./jsx-runtime-CBlexoeR.js";
3
3
  //#region src/email/assets.ts
4
4
  /** Files a template may import, and what they are served as. */
package/dist/brand.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as samvaBrandPlugin } from "./html-eOZfDwVf.js";
1
+ import { D as samvaBrandPlugin } from "./html-BvRFepeO.js";
2
2
  import { a as THEME_COLOR_ROLES, c as foregroundDefaults, i as THEME_COLORS, l as parseBrandCss, n as DEFAULT_THEME_CSS, o as THEME_TOKENS, r as FILL_COLORS, s as contrastForeground, t as DEFAULT_THEME, u as printBrandCss } from "./theme-css-s9QwD5Ko.js";
3
- import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-B-t62NJB.js";
3
+ import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-nreeKWxF.js";
4
4
  export { ASSET_CONTENT_TYPES, BRAND_COMPONENT_CLASSES, DEFAULT_THEME, DEFAULT_THEME_CSS, FILL_COLORS, THEME_COLORS, THEME_COLOR_ROLES, THEME_TOKENS, assetEntry, assetUrl, brandComponents, checkAssetBase, contrastForeground, foregroundDefaults, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
@@ -2,7 +2,7 @@ import { n as parseBrandSpecifier, t as BrandPlugin } from "./brand-plugin-CHhce
2
2
  import { a as TemplateDiagnostic, r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
3
3
  import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
4
4
  import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-tISHaQNF.js";
5
- import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-C1X3S5or.js";
5
+ import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-BdvewoQt.js";
6
6
  import { a as TEMPLATE_NAME_PATTERN, n as EmailContent } from "./template-B-UDXK8a.js";
7
7
  import { caniemail } from "caniemail";
8
8
  //#region src/email/compatibility.d.ts
package/dist/compiler.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { n as TEMPLATE_NAME_PATTERN } from "./template-zLUjcwfF.js";
3
- import { D as samvaBrandPlugin, T as parseBrandSpecifier, _ as renderIr, w as brandSpecifierPattern } from "./html-eOZfDwVf.js";
4
- import { m as PRIMITIVE_CSS } from "./components-CwWSoG5N.js";
3
+ import { D as samvaBrandPlugin, T as parseBrandSpecifier, _ as renderIr, w as brandSpecifierPattern } from "./html-BvRFepeO.js";
4
+ import { m as PRIMITIVE_CSS } from "./components-3XHjYD3i.js";
5
5
  import { c as foregroundDefaults, d as downlevelColors, f as formatColor, l as parseBrandCss, n as DEFAULT_THEME_CSS, p as parseColor, u as printBrandCss } from "./theme-css-s9QwD5Ko.js";
6
6
  import { n as isDarkSchemeCondition, r as isDarkSchemeQuery, t as DARK_SCHEME_MEDIA } from "./dark-scheme-CbCwGDWH.js";
7
- import { a as downlevelMediaQuery, c as renderHeadCss, d as matchesSelector, f as unquote, i as renderDarkRules, l as resolveVariables, o as finalizeValue, r as applyStylesheet, s as foldCalc, u as classListOf } from "./ir-D-gTGquU.js";
7
+ import { a as downlevelMediaQuery, c as renderHeadCss, d as matchesSelector, f as unquote, i as renderDarkRules, l as resolveVariables, o as finalizeValue, r as applyStylesheet, s as foldCalc, u as classListOf } from "./ir-DNL6H7jA.js";
8
8
  import { i as buildLineMap, s as lineColumn } from "./diagnostic-codes-UXwOO3A-.js";
9
- import { a as lowerProject, d as resolveFromDirectory, f as resolveThemeImports, i as completed, l as Findings, n as introducedProfileErrors, r as loadProject, t as checkStaticProfile, u as projectDirectory } from "./profile-Cc8VxOKA.js";
10
- import { c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, s as assetUrl, t as BRAND_COMPONENT_CLASSES } from "./brand-B-t62NJB.js";
9
+ import { a as lowerProject, d as resolveFromDirectory, f as resolveThemeImports, i as completed, l as Findings, n as introducedProfileErrors, r as loadProject, t as checkStaticProfile, u as projectDirectory } from "./profile-AatO_SvP.js";
10
+ import { c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, s as assetUrl, t as BRAND_COMPONENT_CLASSES } from "./brand-nreeKWxF.js";
11
11
  import { t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
12
- import { n as compileEmail, r as renderEmailContent } from "./render-6gLei61p.js";
12
+ import { n as compileEmail, r as renderEmailContent } from "./render-DpYouYYG.js";
13
13
  import { caniemail, parseHtml, rawData } from "caniemail";
14
14
  import { generate, ident, parse, walk } from "css-tree";
15
15
  import { parse as parse$1 } from "@babel/parser";
@@ -1,4 +1,4 @@
1
- import { i as escapeText, r as escapeAttribute } from "./html-eOZfDwVf.js";
1
+ import { i as escapeText, r as escapeAttribute } from "./html-BvRFepeO.js";
2
2
  import { a as jsx, c as rawMarkup, l as reportEmailDiagnostic, n as Fragment, s as normalizeChildren, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
3
3
  //#region src/email/primitive-css.ts
4
4
  /** The classes the primitives put on the elements they generate. */
package/dist/edit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-CBlexoeR.js";
3
3
  import { i as buildLineMap } from "./diagnostic-codes-UXwOO3A-.js";
4
- import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-Cc8VxOKA.js";
4
+ import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-AatO_SvP.js";
5
5
  import { parse } from "@babel/parser";
6
6
  //#region src/source-edit.ts
7
7
  const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
package/dist/email.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-CwWSoG5N.js";
1
+ import { a as Email, c as PREHEADER_ATTRIBUTE, d as equalColumnWidth, f as preheaderNode, i as Divider, l as Section, n as Column, o as Image, r as Columns, s as Link, t as Button, u as Spacer } from "./components-3XHjYD3i.js";
2
2
  export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };
@@ -1137,6 +1137,15 @@ const resolveButtons = (ctx, nodes, out, depth) => {
1137
1137
  code: value(node.code)
1138
1138
  });
1139
1139
  break;
1140
+ case "flow":
1141
+ out.push({
1142
+ type: "flow",
1143
+ text: value(node.text),
1144
+ flow: node.flow,
1145
+ action: node.action,
1146
+ ...node.screen === void 0 ? {} : { screen: node.screen }
1147
+ });
1148
+ break;
1140
1149
  default: invalid("Unknown WhatsApp button.");
1141
1150
  }
1142
1151
  }
@@ -1,5 +1,5 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
- import { a as kebab, n as declarationValue, r as escapeAttribute } from "./html-eOZfDwVf.js";
2
+ import { a as kebab, n as declarationValue, r as escapeAttribute } from "./html-BvRFepeO.js";
3
3
  import { t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
4
4
  import { i as withinDarkScheme, t as DARK_SCHEME_MEDIA } from "./dark-scheme-CbCwGDWH.js";
5
5
  //#region src/email/font-stacks.ts
@@ -1,13 +1,13 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { n as inputSchema, r as jsonSchema } from "./input-schema-zjkgrXn2.js";
3
3
  import { n as TEMPLATE_NAME_PATTERN, t as TEMPLATE_ID_PATTERN } from "./template-zLUjcwfF.js";
4
- import { D as samvaBrandPlugin, E as reservedBrandAttributes, O as BRAND_FOOTER_MARKER, S as emailElement, T as parseBrandSpecifier, b as EMAIL_ELEMENTS, s as serializeStyle, t as checkAttribute } from "./html-eOZfDwVf.js";
4
+ import { D as samvaBrandPlugin, E as reservedBrandAttributes, O as BRAND_FOOTER_MARKER, S as emailElement, T as parseBrandSpecifier, b as EMAIL_ELEMENTS, s as serializeStyle, t as checkAttribute } from "./html-BvRFepeO.js";
5
5
  import { FORMATTER_NAMES } from "./fmt.js";
6
6
  import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
7
- import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-CwWSoG5N.js";
8
- import { i as renderDarkRules, n as DARK_CLASS_PREFIX, r as applyStylesheet } from "./ir-D-gTGquU.js";
7
+ import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-3XHjYD3i.js";
8
+ import { i as renderDarkRules, n as DARK_CLASS_PREFIX, r as applyStylesheet } from "./ir-DNL6H7jA.js";
9
9
  import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-UXwOO3A-.js";
10
- import { l as isAssetPath, n as brandComponents } from "./brand-B-t62NJB.js";
10
+ import { l as isAssetPath, n as brandComponents } from "./brand-nreeKWxF.js";
11
11
  import { parse } from "css-tree";
12
12
  import { parse as parse$1 } from "@babel/parser";
13
13
  //#region src/email/theme-imports.ts
@@ -2530,7 +2530,12 @@ const BUTTON_TEXT_LIMIT = 25;
2530
2530
  const URL_BUTTON_LIMIT = 2;
2531
2531
  const PHONE_BUTTON_LIMIT = 1;
2532
2532
  const QUICK_REPLY_LIMIT = 10;
2533
+ const FLOW_BUTTON_LIMIT = 1;
2533
2534
  const BUTTON_LIMIT = 10;
2535
+ const FLOW_HANDLE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2536
+ const FLOW_HANDLE_LIMIT = 64;
2537
+ const FLOW_SCREEN_PATTERN = /^[A-Za-z_]{1,100}$/;
2538
+ const FLOW_ACTIONS = ["navigate", "data_exchange"];
2534
2539
  const jsxName = (name) => {
2535
2540
  if (name.type === "JSXIdentifier") return text(name, "name") ?? "";
2536
2541
  if (name.type === "JSXMemberExpression") return `${jsxName(child(name, "object") ?? name)}.${text(child(name, "property") ?? name, "name") ?? ""}`;
@@ -2636,6 +2641,7 @@ const ZERO = {
2636
2641
  url: 0,
2637
2642
  phone: 0,
2638
2643
  quickReply: 0,
2644
+ flow: 0,
2639
2645
  total: 0
2640
2646
  };
2641
2647
  /** The most buttons of each kind any combination of the conditions can produce. */
@@ -2650,18 +2656,21 @@ const maximalCounts = (nodes) => {
2650
2656
  url: Math.max(then.url, otherwise.url),
2651
2657
  phone: Math.max(then.phone, otherwise.phone),
2652
2658
  quickReply: Math.max(then.quickReply, otherwise.quickReply),
2659
+ flow: Math.max(then.flow, otherwise.flow),
2653
2660
  total: Math.max(then.total, otherwise.total)
2654
2661
  };
2655
2662
  } else add = {
2656
2663
  url: node.type === "url" ? 1 : 0,
2657
2664
  phone: node.type === "phone" ? 1 : 0,
2658
2665
  quickReply: node.type === "quick-reply" ? 1 : 0,
2666
+ flow: node.type === "flow" ? 1 : 0,
2659
2667
  total: 1
2660
2668
  };
2661
2669
  counts = {
2662
2670
  url: counts.url + add.url,
2663
2671
  phone: counts.phone + add.phone,
2664
2672
  quickReply: counts.quickReply + add.quickReply,
2673
+ flow: counts.flow + add.flow,
2665
2674
  total: counts.total + add.total
2666
2675
  };
2667
2676
  }
@@ -2679,6 +2688,7 @@ const buttonNodes = (buttons) => buttons.flatMap((button) => {
2679
2688
  case "url": return [button.text, button.url];
2680
2689
  case "phone": return [button.text, button.phone];
2681
2690
  case "copy-code": return [button.code];
2691
+ case "flow": return [button.text];
2682
2692
  }
2683
2693
  });
2684
2694
  const whatsappChannel = (input, field) => {
@@ -2772,6 +2782,7 @@ const whatsappChannel = (input, field) => {
2772
2782
  over("URL buttons", counts.url, URL_BUTTON_LIMIT);
2773
2783
  over("phone buttons", counts.phone, PHONE_BUTTON_LIMIT);
2774
2784
  over("quick-reply buttons", counts.quickReply, QUICK_REPLY_LIMIT);
2785
+ over("flow buttons", counts.flow, FLOW_BUTTON_LIMIT);
2775
2786
  over("buttons in all", counts.total, BUTTON_LIMIT);
2776
2787
  roots.push(...buttonNodes(buttons));
2777
2788
  }
@@ -2790,7 +2801,8 @@ const BUTTON_TAGS = [
2790
2801
  "UrlButton",
2791
2802
  "QuickReplyButton",
2792
2803
  "PhoneButton",
2793
- "CopyCodeButton"
2804
+ "CopyCodeButton",
2805
+ "FlowButton"
2794
2806
  ];
2795
2807
  const buttonList = (input, ctx, local, parent) => {
2796
2808
  const { lowerer } = input;
@@ -2815,6 +2827,54 @@ const buttonList = (input, ctx, local, parent) => {
2815
2827
  if (value === void 0) lowerer.error(ctx, element, "missing-attribute", `This button needs \`${name}\`.`, `Write ${name}="…" or ${name}={input.field}.`);
2816
2828
  return value;
2817
2829
  };
2830
+ const flowButton = (element) => {
2831
+ const label_ = label(element, "label");
2832
+ const values = /* @__PURE__ */ new Map();
2833
+ let valid = true;
2834
+ const refuse = (node, code, message, fix) => {
2835
+ valid = false;
2836
+ lowerer.error(ctx, node, code, message, fix);
2837
+ };
2838
+ for (const item of attributesOf(element)) {
2839
+ if (item.type === "JSXSpreadAttribute") {
2840
+ refuse(item, "invalid-attribute", "A spread attribute runs code, and templates render without running code.", "Write flow, action and screen as string literals.");
2841
+ continue;
2842
+ }
2843
+ const nameNode = child(item, "name");
2844
+ const name = nameNode === void 0 ? "" : jsxName(nameNode);
2845
+ if (name === "key") continue;
2846
+ if (name !== "flow" && name !== "action" && name !== "screen") {
2847
+ refuse(item, "invalid-attribute", `\`${name}\` is not an attribute of this button.`, "The attributes are flow, action and screen.");
2848
+ continue;
2849
+ }
2850
+ const raw = child(item, "value");
2851
+ const literal = raw?.type === "JSXExpressionContainer" ? child(raw, "expression") : raw;
2852
+ if (literal?.type !== "StringLiteral") {
2853
+ refuse(item, "invalid-attribute", `\`${name}\` is fixed when the template is registered, so it is a string literal, not a binding.`, `Write ${name}="…".`);
2854
+ continue;
2855
+ }
2856
+ values.set(name, text(literal, "value") ?? "");
2857
+ }
2858
+ const flow = values.get("flow");
2859
+ if (flow === void 0) {
2860
+ if (valid) refuse(element, "missing-attribute", "This button needs `flow`.", "Write flow=\"handle\".");
2861
+ } else if (flow.length > FLOW_HANDLE_LIMIT || !FLOW_HANDLE_PATTERN.test(flow)) refuse(element, "invalid-attribute", `\`${flow}\` is not a flow handle.`, `Use the lowercase kebab-case handle of flows/<handle>.json, at most ${FLOW_HANDLE_LIMIT} characters.`);
2862
+ const action = values.get("action") ?? "navigate";
2863
+ if (!FLOW_ACTIONS.includes(action)) refuse(element, "invalid-attribute", `\`${action}\` is not a flow action.`, `Use one of ${FLOW_ACTIONS.join(", ")}.`);
2864
+ const screen = values.get("screen");
2865
+ if (screen !== void 0) {
2866
+ if (action === "data_exchange") refuse(element, "invalid-attribute", "A data_exchange flow has its endpoint choose the first screen, so it takes no `screen`.", "Remove screen, or use action=\"navigate\".");
2867
+ else if (!FLOW_SCREEN_PATTERN.test(screen) || screen === "SUCCESS") refuse(element, "invalid-attribute", `\`${screen}\` is not a flow screen id.`, "Use the screen's id: letters and underscores, at most 100 characters, and not SUCCESS.");
2868
+ }
2869
+ if (!valid || flow === void 0 || label_ === void 0) return void 0;
2870
+ return {
2871
+ type: "flow",
2872
+ text: label_,
2873
+ flow,
2874
+ action,
2875
+ ...screen === void 0 ? {} : { screen }
2876
+ };
2877
+ };
2818
2878
  const single = (element) => {
2819
2879
  const tag = tagOf(element);
2820
2880
  switch (tag.startsWith(`${local}.`) ? tag.slice(local.length + 1) : "") {
@@ -2851,6 +2911,7 @@ const buttonList = (input, ctx, local, parent) => {
2851
2911
  code
2852
2912
  };
2853
2913
  }
2914
+ case "FlowButton": return flowButton(element);
2854
2915
  default:
2855
2916
  lowerer.error(ctx, element, "whatsapp-structure", `<WhatsApp.Buttons> holds ${BUTTON_TAGS.map((name) => `<WhatsApp.${name}>`).join(", ")}.`, "Use one of the button components.");
2856
2917
  return;
@@ -167,6 +167,14 @@ type IrWhatsAppButton = {
167
167
  } | {
168
168
  readonly type: "copy-code";
169
169
  readonly code: IrValue;
170
+ } | {
171
+ readonly type: "flow";
172
+ readonly text: IrValue;
173
+ /** The Samva flow handle, resolved to Meta's flow id when the template is registered. */
174
+ readonly flow: string;
175
+ readonly action: "navigate" | "data_exchange";
176
+ /** Meta's `navigate_screen`; absent opens the flow's first entry screen. */
177
+ readonly screen?: string;
170
178
  };
171
179
  /** A button, or buttons that appear when a condition over the input holds. */
172
180
  type IrWhatsAppButtonNode = IrWhatsAppButton | {
@@ -243,6 +251,12 @@ type RenderedWhatsAppButton = {
243
251
  } | {
244
252
  readonly type: "copy-code";
245
253
  readonly code: string;
254
+ } | {
255
+ readonly type: "flow";
256
+ readonly text: string;
257
+ readonly flow: string;
258
+ readonly action: "navigate" | "data_exchange";
259
+ readonly screen?: string;
246
260
  };
247
261
  /** A WhatsApp message with every text rendered and every conditional button resolved. */
248
262
  interface RenderedIrWhatsApp {
@@ -1,7 +1,7 @@
1
- import { S as emailElement, o as serializeEmailHtml } from "./html-eOZfDwVf.js";
1
+ import { S as emailElement, o as serializeEmailHtml } from "./html-BvRFepeO.js";
2
2
  import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
3
- import { f as preheaderNode } from "./components-CwWSoG5N.js";
4
- import { i as renderDarkRules, r as applyStylesheet } from "./ir-D-gTGquU.js";
3
+ import { f as preheaderNode } from "./components-3XHjYD3i.js";
4
+ import { i as renderDarkRules, r as applyStylesheet } from "./ir-DNL6H7jA.js";
5
5
  import { r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
6
6
  //#region src/email/text.ts
7
7
  const PARAGRAPH = "\n\n";
package/dist/render.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as isUrlAttribute, S as emailElement, _ as EmailGlobalProps, b as EmailStyle, g as EmailElementSpec, h as EmailAttributeSpec, m as EMAIL_GLOBAL_ATTRIBUTES, p as EMAIL_ELEMENTS, r as EmailNode, v as EmailIntrinsicElements, x as EmailTag, y as EmailLayout } from "./jsx-runtime-DC7xG0aA.js";
2
- import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-C1X3S5or.js";
2
+ import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-BdvewoQt.js";
3
3
  //#region src/render-ir.d.ts
4
4
  interface IrRenderDiagnostic {
5
5
  readonly code: string;
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
- import { C as isUrlAttribute, S as emailElement, _ as renderIr, a as kebab, b as EMAIL_ELEMENTS, c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, g as IrRenderError, h as renderIrPreview, i as escapeText, l as emailAuthoringTarget, m as emailSelections, n as declarationValue, o as serializeEmailHtml, p as emailSelectionSiblings, r as escapeAttribute, s as serializeStyle, t as checkAttribute, u as emailDefinitionInstances, v as renderIrSms, x as EMAIL_GLOBAL_ATTRIBUTES, y as renderIrWhatsApp } from "./html-eOZfDwVf.js";
2
- import { t as SML_IR_VERSION } from "./ir-D-gTGquU.js";
3
- import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-6gLei61p.js";
1
+ import { C as isUrlAttribute, S as emailElement, _ as renderIr, a as kebab, b as EMAIL_ELEMENTS, c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, g as IrRenderError, h as renderIrPreview, i as escapeText, l as emailAuthoringTarget, m as emailSelections, n as declarationValue, o as serializeEmailHtml, p as emailSelectionSiblings, r as escapeAttribute, s as serializeStyle, t as checkAttribute, u as emailDefinitionInstances, v as renderIrSms, x as EMAIL_GLOBAL_ATTRIBUTES, y as renderIrWhatsApp } from "./html-BvRFepeO.js";
2
+ import { t as SML_IR_VERSION } from "./ir-DNL6H7jA.js";
3
+ import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-DpYouYYG.js";
4
4
  //#region src/sms-segments.ts
5
5
  const GSM7_EXTENDED = /* @__PURE__ */ new Set([
6
6
  "[",
@@ -36,9 +36,26 @@ interface WhatsAppCopyCodeButtonProps {
36
36
  /** The code the button copies. */
37
37
  readonly children?: EmailChild;
38
38
  }
39
+ type WhatsAppFlowAction = "navigate" | "data_exchange";
40
+ interface WhatsAppFlowButtonProps {
41
+ /**
42
+ * The Samva flow handle: lowercase kebab-case, the file `flows/<handle>.json` in the template
43
+ * project. Samva maps it to Meta's flow id per business account when it registers the template.
44
+ */
45
+ readonly flow: string;
46
+ /** `navigate` by default; `data_exchange` has the flow's endpoint choose the first screen. */
47
+ readonly action?: WhatsAppFlowAction | undefined;
48
+ /**
49
+ * The screen a `navigate` flow opens on (Meta's `navigate_screen`); omitted, the flow's first
50
+ * entry screen. Refused with `data_exchange`.
51
+ */
52
+ readonly screen?: string | undefined;
53
+ /** The button label. */
54
+ readonly children?: EmailChild;
55
+ }
39
56
  /**
40
57
  * The root of a WhatsApp body. Meta's template anatomy is the vocabulary: `WhatsApp.Header`,
41
- * `WhatsApp.Body`, `WhatsApp.Footer` and `WhatsApp.Buttons`, whose children are the four button
58
+ * `WhatsApp.Body`, `WhatsApp.Footer` and `WhatsApp.Buttons`, whose children are the five button
42
59
  * kinds.
43
60
  */
44
61
  declare const WhatsApp: ((props: WhatsAppProps) => EmailNode) & {
@@ -50,6 +67,7 @@ declare const WhatsApp: ((props: WhatsAppProps) => EmailNode) & {
50
67
  QuickReplyButton: (props: WhatsAppQuickReplyButtonProps) => EmailNode;
51
68
  PhoneButton: (props: WhatsAppPhoneButtonProps) => EmailNode;
52
69
  CopyCodeButton: (props: WhatsAppCopyCodeButtonProps) => EmailNode;
70
+ FlowButton: (props: WhatsAppFlowButtonProps) => EmailNode;
53
71
  };
54
72
  //#endregion
55
- export { WhatsApp, WhatsAppButtonsProps, type WhatsAppCategory, WhatsAppCopyCodeButtonProps, WhatsAppHeaderProps, type WhatsAppHeaderType, WhatsAppPhoneButtonProps, WhatsAppProps, WhatsAppQuickReplyButtonProps, WhatsAppTextProps, WhatsAppUrlButtonProps };
73
+ export { WhatsApp, WhatsAppButtonsProps, type WhatsAppCategory, WhatsAppCopyCodeButtonProps, WhatsAppFlowAction, WhatsAppFlowButtonProps, WhatsAppHeaderProps, type WhatsAppHeaderType, WhatsAppPhoneButtonProps, WhatsAppProps, WhatsAppQuickReplyButtonProps, WhatsAppTextProps, WhatsAppUrlButtonProps };
package/dist/whatsapp.js CHANGED
@@ -3,7 +3,7 @@ import { a as jsx, n as Fragment } from "./jsx-runtime-CBlexoeR.js";
3
3
  const fragment = (children) => jsx(Fragment, { children });
4
4
  /**
5
5
  * The root of a WhatsApp body. Meta's template anatomy is the vocabulary: `WhatsApp.Header`,
6
- * `WhatsApp.Body`, `WhatsApp.Footer` and `WhatsApp.Buttons`, whose children are the four button
6
+ * `WhatsApp.Body`, `WhatsApp.Footer` and `WhatsApp.Buttons`, whose children are the five button
7
7
  * kinds.
8
8
  */
9
9
  const WhatsApp = Object.assign((props) => fragment(props.children), {
@@ -14,7 +14,8 @@ const WhatsApp = Object.assign((props) => fragment(props.children), {
14
14
  UrlButton: (props) => fragment(props.children),
15
15
  QuickReplyButton: (props) => fragment(props.children),
16
16
  PhoneButton: (props) => fragment(props.children),
17
- CopyCodeButton: (props) => fragment(props.children)
17
+ CopyCodeButton: (props) => fragment(props.children),
18
+ FlowButton: (props) => fragment(props.children)
18
19
  });
19
20
  //#endregion
20
21
  export { WhatsApp };
@@ -720,6 +720,36 @@
720
720
  "description": "The code the button copies."
721
721
  }
722
722
  ]
723
+ },
724
+ {
725
+ "name": "WhatsApp.FlowButton",
726
+ "import": "@samva/markup/whatsapp",
727
+ "props": [
728
+ {
729
+ "name": "flow",
730
+ "type": "string",
731
+ "optional": false,
732
+ "description": "The Samva flow handle: lowercase kebab-case, the file `flows/<handle>.json` in the template project. Samva maps it to Meta's flow id per business account when it registers the template."
733
+ },
734
+ {
735
+ "name": "action",
736
+ "type": "WhatsAppFlowAction",
737
+ "optional": true,
738
+ "description": "`navigate` by default; `data_exchange` has the flow's endpoint choose the first screen."
739
+ },
740
+ {
741
+ "name": "screen",
742
+ "type": "string",
743
+ "optional": true,
744
+ "description": "The screen a `navigate` flow opens on (Meta's `navigate_screen`); omitted, the flow's first entry screen. Refused with `data_exchange`."
745
+ },
746
+ {
747
+ "name": "children",
748
+ "type": "EmailChild",
749
+ "optional": true,
750
+ "description": "The button label."
751
+ }
752
+ ]
723
753
  }
724
754
  ],
725
755
  "elements": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/markup",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Type-safe authoring and compilation primitives for Samva templates",
5
5
  "keywords": [
6
6
  "email",