@ringg/react-native 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,20 +17,47 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var src_exports = {};
22
32
  __export(src_exports, {
33
+ GradientFill: () => GradientFill,
34
+ Markdown: () => Markdown,
35
+ RinggWidget: () => RinggWidget,
36
+ WidgetThemeProvider: () => WidgetThemeProvider,
37
+ appOrigin: () => appOrigin,
38
+ createAudioSession: () => createAudioSession,
23
39
  createCallbackEventBus: () => createCallbackEventBus,
40
+ createHostActionDispatcher: () => createHostActionDispatcher,
41
+ createLiveKitTransport: () => createLiveKitTransport,
42
+ createNativeMicPermission: () => createNativeMicPermission,
43
+ createNotificationPlayer: () => createNotificationPlayer,
24
44
  createRinggWidgetController: () => createRinggWidgetController,
45
+ createSilentNotificationPlayer: () => createSilentNotificationPlayer,
46
+ resolveButtonRadius: () => resolveButtonRadius,
47
+ resolveFontFamily: () => resolveFontFamily,
48
+ resolveRadius: () => resolveRadius,
49
+ ringgId: () => ringgId,
50
+ solidColor: () => solidColor,
51
+ toSize: () => toSize,
52
+ toViewStyle: () => toViewStyle,
25
53
  useRinggComponents: () => useRinggComponents,
26
54
  useRinggMessages: () => useRinggMessages,
27
55
  useRinggSession: () => useRinggSession,
28
56
  useRinggShell: () => useRinggShell,
29
57
  useRinggSlashCommands: () => useRinggSlashCommands,
30
58
  useRinggTyping: () => useRinggTyping,
31
- useStoreSnapshot: () => useStoreSnapshot
59
+ useStoreSnapshot: () => useStoreSnapshot,
60
+ useWidgetTheme: () => useWidgetTheme
32
61
  });
33
62
  module.exports = __toCommonJS(src_exports);
34
63
 
@@ -125,7 +154,8 @@ var silentNotificationPlayer = {
125
154
  function createRinggApiClient(config) {
126
155
  const { backendUrl, livekitUrl } = config.urlResolver.resolve(config.mode);
127
156
  const authHeaders = config.authorization ? { Authorization: config.authorization } : config.xApiKey ? { "X-API-KEY": config.xApiKey } : {};
128
- const headers = () => ({ "Content-Type": "application/json", ...authHeaders });
157
+ const originHeader = config.clientOrigin ? { Origin: config.clientOrigin } : {};
158
+ const headers = () => ({ "Content-Type": "application/json", ...originHeader, ...authHeaders });
129
159
  return {
130
160
  backendUrl,
131
161
  livekitUrl,
@@ -177,10 +207,90 @@ function createRinggApiClient(config) {
177
207
  };
178
208
  }
179
209
 
210
+ // ../core/dist/theme/gradient-utils.js
211
+ var GRADIENT_FUNCTIONS = ["linear-gradient", "radial-gradient", "conic-gradient", "repeating-linear-gradient", "repeating-radial-gradient", "repeating-conic-gradient"];
212
+ var COLOR_FUNCTIONS = ["rgba", "rgb", "hsla", "hsl"];
213
+ function isGradient(color) {
214
+ const normalized = color.trim().toLowerCase();
215
+ return GRADIENT_FUNCTIONS.some((fn) => normalized.startsWith(`${fn}(`));
216
+ }
217
+ function splitGradientArguments(gradient) {
218
+ const open = gradient.indexOf("(");
219
+ const close = gradient.lastIndexOf(")");
220
+ if (open === -1 || close <= open)
221
+ return [];
222
+ const body = gradient.slice(open + 1, close);
223
+ const args = [];
224
+ let depth = 0;
225
+ let current = "";
226
+ for (const char of body) {
227
+ if (char === "(")
228
+ depth++;
229
+ else if (char === ")")
230
+ depth--;
231
+ if (char === "," && depth === 0) {
232
+ args.push(current.trim());
233
+ current = "";
234
+ } else {
235
+ current += char;
236
+ }
237
+ }
238
+ if (current.trim())
239
+ args.push(current.trim());
240
+ return args;
241
+ }
242
+ function isHexDigit(char) {
243
+ return char >= "0" && char <= "9" || char >= "a" && char <= "f" || char >= "A" && char <= "F";
244
+ }
245
+ function takeHexColor(value) {
246
+ let digits = "";
247
+ for (const char of value.slice(1)) {
248
+ if (!isHexDigit(char))
249
+ break;
250
+ digits += char;
251
+ }
252
+ const validLengths = [3, 4, 6, 8];
253
+ return validLengths.includes(digits.length) ? `#${digits}` : null;
254
+ }
255
+ function takeColorFunction(value) {
256
+ const normalized = value.toLowerCase();
257
+ if (!COLOR_FUNCTIONS.some((fn) => normalized.startsWith(`${fn}(`)))
258
+ return null;
259
+ const close = value.indexOf(")");
260
+ return close === -1 ? null : value.slice(0, close + 1);
261
+ }
262
+ function leadingColor(argument) {
263
+ const value = argument.trim();
264
+ if (value.startsWith("#"))
265
+ return takeHexColor(value);
266
+ return takeColorFunction(value);
267
+ }
268
+ function extractColorsFromGradient(gradient) {
269
+ if (!isGradient(gradient))
270
+ return [];
271
+ return splitGradientArguments(gradient).map(leadingColor).filter((color) => color !== null);
272
+ }
273
+ function getDominantColor(color) {
274
+ if (!isGradient(color))
275
+ return color;
276
+ return extractColorsFromGradient(color)[0] ?? color;
277
+ }
278
+
180
279
  // ../core/dist/theme/theme-engine.js
181
280
  function mergeWidgetTheme(theme) {
182
281
  return { ...DEFAULT_WIDGET_THEME, ...theme };
183
282
  }
283
+ function getButtonRadius(style) {
284
+ switch (style) {
285
+ case "pill":
286
+ return "9999px";
287
+ case "square":
288
+ return "6px";
289
+ case "rounded":
290
+ default:
291
+ return "12px";
292
+ }
293
+ }
184
294
 
185
295
  // ../core/dist/rpc/formatter.js
186
296
  function transformRpcToComponent(componentName, rpcPayload) {
@@ -819,7 +929,8 @@ function createRinggWidgetController(config, ports) {
819
929
  urlResolver: ports.urlResolver,
820
930
  mode: config.mode ?? WIDGET_DEFAULTS.mode,
821
931
  xApiKey: config.xApiKey,
822
- authorization: config.authorization
932
+ authorization: config.authorization,
933
+ clientOrigin: config.clientOrigin
823
934
  });
824
935
  const shell = createShellStore(eventBus, config.defaultTab ?? WIDGET_DEFAULTS.defaultTab, config.defaultExpanded ?? WIDGET_DEFAULTS.defaultExpanded);
825
936
  const session = createSessionStore(api, transport, ports.micPermission ?? grantedMicPermission);
@@ -1149,16 +1260,4506 @@ var useRinggSession = (controller) => useStoreSnapshot(controller.session);
1149
1260
  var useRinggShell = (controller) => useStoreSnapshot(controller.shell);
1150
1261
  var useRinggComponents = (controller) => useStoreSnapshot(controller.components);
1151
1262
  var useRinggSlashCommands = (controller) => useStoreSnapshot(controller.slashCommands);
1263
+
1264
+ // src/RinggWidget.tsx
1265
+ var import_react24 = require("react");
1266
+ var import_react_native28 = require("react-native");
1267
+ var import_lucide_react_native10 = require("lucide-react-native");
1268
+
1269
+ // src/components/action-button.tsx
1270
+ var import_react_native3 = require("react-native");
1271
+
1272
+ // src/lib/gradient.tsx
1273
+ var import_react2 = require("react");
1274
+ var import_react_native = require("react-native");
1275
+ var import_react_native_svg = __toESM(require("react-native-svg"));
1276
+ var import_jsx_runtime = require("react/jsx-runtime");
1277
+ var DEFAULT_ANGLE_DEG = 180;
1278
+ var SIDE_ANGLES = {
1279
+ "to top": 0,
1280
+ "to right": 90,
1281
+ "to bottom": 180,
1282
+ "to left": 270,
1283
+ "to top right": 45,
1284
+ "to right top": 45,
1285
+ "to bottom right": 135,
1286
+ "to right bottom": 135,
1287
+ "to bottom left": 225,
1288
+ "to left bottom": 225,
1289
+ "to top left": 315,
1290
+ "to left top": 315
1291
+ };
1292
+ var angleToDegrees = (value) => {
1293
+ const match = /^(-?\d+(?:\.\d+)?)(deg|rad|grad|turn)$/.exec(value.trim().toLowerCase());
1294
+ if (!match) return void 0;
1295
+ const amount = Number(match[1]);
1296
+ switch (match[2]) {
1297
+ case "deg":
1298
+ return amount;
1299
+ case "rad":
1300
+ return amount * 180 / Math.PI;
1301
+ case "grad":
1302
+ return amount * 0.9;
1303
+ case "turn":
1304
+ return amount * 360;
1305
+ default:
1306
+ return void 0;
1307
+ }
1308
+ };
1309
+ var readAngle = (gradient) => {
1310
+ const open = gradient.indexOf("(");
1311
+ const close = gradient.lastIndexOf(")");
1312
+ if (open === -1 || close <= open) return DEFAULT_ANGLE_DEG;
1313
+ const first = gradient.slice(open + 1, close).split(",")[0]?.trim().toLowerCase() ?? "";
1314
+ if (first.startsWith("to ")) return SIDE_ANGLES[first.replace(/\s+/g, " ")] ?? DEFAULT_ANGLE_DEG;
1315
+ return angleToDegrees(first) ?? DEFAULT_ANGLE_DEG;
1316
+ };
1317
+ var angleToLine = (degrees) => {
1318
+ const radians = degrees * Math.PI / 180;
1319
+ const dx = Math.sin(radians);
1320
+ const dy = -Math.cos(radians);
1321
+ return {
1322
+ x1: `${0.5 - dx / 2}`,
1323
+ y1: `${0.5 - dy / 2}`,
1324
+ x2: `${0.5 + dx / 2}`,
1325
+ y2: `${0.5 + dy / 2}`
1326
+ };
1327
+ };
1328
+ var stopOffset = (index, total) => total <= 1 ? "0" : `${index / (total - 1)}`;
1329
+ var gradientId = 0;
1330
+ var nextGradientId = () => `ringg-gradient-${++gradientId}`;
1331
+ var GradientFill = ({ color, style, children }) => {
1332
+ if (!isGradient(color)) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [{ backgroundColor: color }, style], children });
1333
+ const colors = extractColorsFromGradient(color);
1334
+ if (colors.length < 2) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [{ backgroundColor: getDominantColor(color) }, style], children });
1335
+ const id = nextGradientId();
1336
+ const line = angleToLine(readAngle(color));
1337
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: [styles.clipped, style], children: [
1338
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native_svg.default, { style: import_react_native.StyleSheet.absoluteFill, pointerEvents: "none", children: [
1339
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_svg.Defs, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_svg.LinearGradient, { id, x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, children: colors.map((stop, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_svg.Stop, { offset: stopOffset(index, colors.length), stopColor: stop }, `${stop}-${index}`)) }) }),
1340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native_svg.Rect, { x: "0", y: "0", width: "100%", height: "100%", fill: `url(#${id})` })
1341
+ ] }),
1342
+ import_react2.Children.count(children) > 0 ? children : null
1343
+ ] });
1344
+ };
1345
+ var styles = import_react_native.StyleSheet.create({
1346
+ clipped: { overflow: "hidden" }
1347
+ });
1348
+ var solidColor = (color) => getDominantColor(color);
1349
+
1350
+ // src/lib/icons.tsx
1351
+ var import_react_native_svg2 = __toESM(require("react-native-svg"));
1352
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1353
+ var DEFAULT_SIZE = 18;
1354
+ var DEFAULT_FILL = "#000000";
1355
+ var PhoneCallRingIcon = ({ size = DEFAULT_SIZE, width, height, fill = DEFAULT_FILL, style, testID }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.default, { testID, viewBox: "0 0 19 19", width: width ?? size, height: height ?? size, style, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M11.5253 1.30593C11.5508 1.21075 11.5947 1.12151 11.6547 1.04333C11.7147 0.965143 11.7894 0.899537 11.8748 0.85026C11.9601 0.800983 12.0543 0.769 12.152 0.756139C12.2496 0.743278 12.3489 0.749791 12.4441 0.775306C13.8344 1.13807 15.103 1.86491 16.119 2.88096C17.1351 3.89702 17.8619 5.16556 18.2247 6.55593C18.2502 6.6511 18.2567 6.75036 18.2439 6.84805C18.231 6.94573 18.199 7.03993 18.1497 7.12525C18.1005 7.21057 18.0349 7.28534 17.9567 7.3453C17.8785 7.40526 17.7893 7.44922 17.6941 7.47468C17.6307 7.49132 17.5655 7.49983 17.5 7.49999C17.3347 7.49999 17.1741 7.44539 17.043 7.34469C16.912 7.24399 16.8179 7.10282 16.7753 6.94312C16.4795 5.80811 15.8863 4.77253 15.0569 3.94314C14.2275 3.11375 13.1919 2.52054 12.0569 2.22468C11.9616 2.19932 11.8723 2.15543 11.794 2.09552C11.7157 2.0356 11.65 1.96084 11.6006 1.87551C11.5513 1.79018 11.5192 1.69595 11.5063 1.59822C11.4933 1.50048 11.4998 1.40116 11.5253 1.30593ZM11.3069 5.22468C12.5997 5.56968 13.4303 6.40031 13.7753 7.69312C13.8179 7.85282 13.912 7.99399 14.043 8.09469C14.1741 8.19539 14.3347 8.24999 14.5 8.24999C14.5655 8.24983 14.6307 8.24132 14.6941 8.22468C14.7893 8.19922 14.8785 8.15526 14.9567 8.0953C15.0349 8.03534 15.1005 7.96057 15.1497 7.87525C15.199 7.78993 15.231 7.69573 15.2439 7.59805C15.2567 7.50036 15.2502 7.4011 15.2247 7.30593C14.7447 5.50968 13.4903 4.25531 11.6941 3.77531C11.5019 3.72396 11.2971 3.75107 11.1249 3.85067C10.9527 3.95027 10.8271 4.1142 10.7758 4.3064C10.7244 4.4986 10.7516 4.70332 10.8512 4.87553C10.9508 5.04774 11.1147 5.17334 11.3069 5.22468ZM18.9888 14.1637C18.8216 15.4341 18.1977 16.6001 17.2337 17.4441C16.2696 18.2881 15.0313 18.7523 13.75 18.75C6.30626 18.75 0.250008 12.6937 0.250008 5.24999C0.247712 3.9687 0.711903 2.73039 1.55588 1.76633C2.39986 0.802276 3.56592 0.178406 4.83626 0.0112434C5.1575 -0.0279808 5.4828 0.0377391 5.76362 0.198592C6.04444 0.359445 6.2657 0.606804 6.39438 0.903743L8.37438 5.32406V5.33531C8.4729 5.56261 8.51359 5.81077 8.49282 6.05763C8.47204 6.30449 8.39044 6.54236 8.25532 6.74999C8.23845 6.77531 8.22063 6.79874 8.20188 6.82218L6.25001 9.13593C6.9522 10.5628 8.4447 12.0422 9.89032 12.7462L12.1722 10.8047C12.1946 10.7858 12.2181 10.7683 12.2425 10.7522C12.45 10.6138 12.6887 10.5293 12.937 10.5064C13.1853 10.4835 13.4354 10.5229 13.6647 10.6209L13.6769 10.6266L18.0934 12.6056C18.3909 12.7338 18.6389 12.9549 18.8003 13.2358C18.9616 13.5166 19.0278 13.8422 18.9888 14.1637ZM17.5 13.9762C17.5 13.9762 17.4934 13.9762 17.4897 13.9762L13.0834 12.0028L10.8006 13.9444C10.7785 13.9631 10.7553 13.9807 10.7313 13.9969C10.5154 14.1409 10.2659 14.2264 10.0071 14.2452C9.74828 14.2639 9.48904 14.2152 9.2547 14.1037C7.49876 13.2553 5.74845 11.5181 4.89907 9.78093C4.7866 9.5483 4.73613 9.29055 4.75255 9.03267C4.76898 8.7748 4.85174 8.52554 4.99282 8.30906C5.00872 8.28364 5.02659 8.2595 5.04626 8.23687L7.00001 5.92031L5.03126 1.51406C5.03089 1.51032 5.03089 1.50655 5.03126 1.50281C4.12212 1.6214 3.28739 2.06733 2.68339 2.7571C2.0794 3.44687 1.74755 4.33316 1.75001 5.24999C1.75348 8.43153 3.01888 11.4818 5.26856 13.7314C7.51825 15.9811 10.5685 17.2465 13.75 17.25C14.6663 17.2531 15.5523 16.9225 16.2425 16.3198C16.9327 15.7171 17.3797 14.8837 17.5 13.9753V13.9762Z", fill }) });
1356
+ var PhoneEndCallIcon = ({ size = DEFAULT_SIZE, width, height, fill = DEFAULT_FILL, style, testID }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.default, { testID, viewBox: "0 0 20 16", width: width ?? size, height: height ?? size, style, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M4.21328 0.704679C4.15839 0.642809 4.09177 0.592448 4.01728 0.556515C3.94278 0.520582 3.8619 0.499791 3.77931 0.495348C3.69672 0.490905 3.61408 0.502898 3.53616 0.530632C3.45824 0.558365 3.3866 0.601288 3.32539 0.656913C3.26419 0.712537 3.21463 0.779756 3.17959 0.854675C3.14455 0.929593 3.12473 1.01072 3.12128 1.09336C3.11783 1.17599 3.13081 1.25849 3.15948 1.33607C3.18814 1.41365 3.23192 1.48477 3.28828 1.5453L5.44375 3.91718C4.12152 4.47256 2.92021 5.28033 1.90703 6.2953C0.344526 7.8578 0.194526 10.1594 1.54062 11.8945C1.70579 12.1059 1.93457 12.2585 2.19315 12.3298C2.45172 12.4011 2.72639 12.3874 2.97656 12.2906L6.80468 10.9336L6.82734 10.925C7.02045 10.8478 7.19125 10.7236 7.32426 10.5637C7.45727 10.4039 7.54829 10.2133 7.58906 10.0094L8.05 7.7039C8.26906 7.62838 8.49236 7.56578 8.71875 7.5164L15.7898 15.2953C15.8447 15.3572 15.9113 15.4075 15.9858 15.4435C16.0603 15.4794 16.1412 15.5002 16.2238 15.5046C16.3064 15.5091 16.389 15.4971 16.467 15.4694C16.5449 15.4416 16.6165 15.3987 16.6777 15.3431C16.7389 15.2874 16.7885 15.2202 16.8235 15.1453C16.8586 15.0704 16.8784 14.9893 16.8818 14.9066C16.8853 14.824 16.8723 14.7415 16.8436 14.6639C16.815 14.5863 16.7712 14.5152 16.7148 14.4547L4.21328 0.704679ZM7.62734 6.52655C7.42378 6.59909 7.24251 6.72315 7.1012 6.88664C6.9599 7.05013 6.86339 7.24745 6.82109 7.45937L6.36015 9.76405L2.54921 11.1156C2.5414 11.1156 2.53593 11.1258 2.52812 11.1289C1.56484 9.88671 1.66874 8.29921 2.79062 7.1789C3.79625 6.17293 5.00827 5.39726 6.34296 4.90546L7.77265 6.4789C7.72421 6.49452 7.67578 6.50937 7.62734 6.52655ZM18.4594 11.8945C18.2942 12.1059 18.0654 12.2585 17.8068 12.3298C17.5483 12.4011 17.2736 12.3874 17.0234 12.2906L16.3008 12.0344C16.2234 12.0069 16.1522 11.9645 16.0913 11.9096C16.0303 11.8546 15.9808 11.7882 15.9455 11.7141C15.8742 11.5644 15.8653 11.3925 15.9207 11.2363C15.9481 11.159 15.9906 11.0878 16.0455 11.0268C16.1005 10.9658 16.1669 10.9163 16.241 10.881C16.3907 10.8097 16.5625 10.8008 16.7187 10.8562L17.4508 11.1156L17.4742 11.125C18.4352 9.88671 18.3336 8.29921 17.2094 7.1789C15.2047 5.17421 12.375 4.11093 9.44765 4.26405C9.36558 4.26831 9.28346 4.25636 9.20601 4.22889C9.12855 4.20141 9.05726 4.15895 8.99621 4.10392C8.93517 4.0489 8.88556 3.98238 8.85021 3.90819C8.81487 3.83399 8.79449 3.75355 8.79023 3.67148C8.78597 3.5894 8.79792 3.50729 8.8254 3.42983C8.85287 3.35237 8.89534 3.28109 8.95036 3.22004C9.00539 3.15899 9.0719 3.10938 9.1461 3.07404C9.2203 3.03869 9.30073 3.01831 9.38281 3.01405C12.6641 2.84296 15.8383 4.03749 18.093 6.2953C19.6547 7.85702 19.8055 10.1594 18.4594 11.8945Z", fill }) });
1357
+ var MicrophoneIcon = ({ size = DEFAULT_SIZE, width, height, fill = DEFAULT_FILL, style, testID }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.default, { testID, viewBox: "0 0 14 19", width: width ?? size, height: height ?? size, style, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M7 12.75C7.99424 12.749 8.94747 12.3535 9.65051 11.6505C10.3535 10.9475 10.749 9.99424 10.75 9V4C10.75 3.00544 10.3549 2.05161 9.65165 1.34835C8.94839 0.645088 7.99456 0.25 7 0.25C6.00544 0.25 5.05161 0.645088 4.34835 1.34835C3.64509 2.05161 3.25 3.00544 3.25 4V9C3.25103 9.99424 3.64645 10.9475 4.34949 11.6505C5.05253 12.3535 6.00576 12.749 7 12.75ZM4.5 4C4.5 3.33696 4.76339 2.70107 5.23223 2.23223C5.70107 1.76339 6.33696 1.5 7 1.5C7.66304 1.5 8.29893 1.76339 8.76777 2.23223C9.23661 2.70107 9.5 3.33696 9.5 4V9C9.5 9.66304 9.23661 10.2989 8.76777 10.7678C8.29893 11.2366 7.66304 11.5 7 11.5C6.33696 11.5 5.70107 11.2366 5.23223 10.7678C4.76339 10.2989 4.5 9.66304 4.5 9V4ZM7.625 15.2188V17.75C7.625 17.9158 7.55915 18.0747 7.44194 18.1919C7.32473 18.3092 7.16576 18.375 7 18.375C6.83424 18.375 6.67527 18.3092 6.55806 18.1919C6.44085 18.0747 6.375 17.9158 6.375 17.75V15.2188C4.8341 15.062 3.40607 14.3393 2.36707 13.1907C1.32806 12.042 0.751903 10.5489 0.75 9C0.75 8.83424 0.815848 8.67527 0.933058 8.55806C1.05027 8.44085 1.20924 8.375 1.375 8.375C1.54076 8.375 1.69973 8.44085 1.81694 8.55806C1.93415 8.67527 2 8.83424 2 9C2 10.3261 2.52678 11.5979 3.46447 12.5355C4.40215 13.4732 5.67392 14 7 14C8.32608 14 9.59785 13.4732 10.5355 12.5355C11.4732 11.5979 12 10.3261 12 9C12 8.83424 12.0658 8.67527 12.1831 8.55806C12.3003 8.44085 12.4592 8.375 12.625 8.375C12.7908 8.375 12.9497 8.44085 13.0669 8.55806C13.1842 8.67527 13.25 8.83424 13.25 9C13.2481 10.5489 12.6719 12.042 11.6329 13.1907C10.5939 14.3393 9.1659 15.062 7.625 15.2188Z", fill }) });
1358
+ var ChatIcon = ({ size = DEFAULT_SIZE, width, height, fill = DEFAULT_FILL, style, testID }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.default, { testID, viewBox: "0 0 18 18", width: width ?? size, height: height ?? size, style, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M17.1305 13.5905C17.5623 12.6948 17.7742 11.709 17.7486 10.7149C17.7229 9.72084 17.4604 8.74725 16.983 7.87501C16.5055 7.00276 15.8267 6.25704 15.0032 5.69978C14.1796 5.14253 13.2349 4.78982 12.2477 4.67097C11.9192 3.9072 11.4416 3.21664 10.8429 2.63974C10.2442 2.06284 9.53637 1.6112 8.76093 1.31126C7.98549 1.01131 7.158 0.869103 6.32692 0.892953C5.49584 0.916802 4.67787 1.10623 3.9209 1.45015C3.16394 1.79407 2.48321 2.28556 1.91857 2.89584C1.35393 3.50612 0.916724 4.22293 0.632564 5.00429C0.348404 5.78565 0.222999 6.61585 0.263692 7.44628C0.304386 8.27671 0.510359 9.09067 0.869551 9.8405L0.301582 11.771C0.238079 11.9865 0.233833 12.2151 0.289291 12.4328C0.344749 12.6506 0.457861 12.8493 0.616727 13.0082C0.775593 13.167 0.97434 13.2801 1.19206 13.3356C1.40978 13.3911 1.63842 13.3868 1.85393 13.3233L3.7844 12.7553C4.40426 13.0532 5.06884 13.2472 5.75158 13.3296C6.08328 14.1068 6.56914 14.8086 7.17979 15.3927C7.79043 15.9768 8.51318 16.431 9.30434 16.7279C10.0955 17.0247 10.9386 17.158 11.7828 17.1198C12.6269 17.0815 13.4546 16.8725 14.2156 16.5053L16.1461 17.0733C16.3615 17.1367 16.5901 17.141 16.8077 17.0855C17.0253 17.0301 17.224 16.9171 17.3828 16.7583C17.5417 16.5996 17.6548 16.4009 17.7103 16.1834C17.7659 15.9658 17.7618 15.7372 17.6985 15.5218L17.1305 13.5905ZM3.84377 11.4608C3.78404 11.461 3.72463 11.4694 3.66721 11.4858L1.50002 12.1249L2.1383 9.95613C2.1837 9.79944 2.16603 9.63122 2.08908 9.48738C1.5237 8.43002 1.35711 7.2047 1.61968 6.03477C1.88225 4.86485 2.5565 3.82824 3.51951 3.1139C4.48252 2.39957 5.67017 2.05508 6.86595 2.14324C8.06173 2.2314 9.18601 2.74633 10.0339 3.59417C10.8817 4.44201 11.3966 5.5663 11.4848 6.76208C11.5729 7.95786 11.2285 9.1455 10.5141 10.1085C9.79979 11.0715 8.76318 11.7458 7.59325 12.0083C6.42333 12.2709 5.198 12.1043 4.14065 11.5389C4.04972 11.4886 3.94768 11.4618 3.84377 11.4608ZM15.8586 13.7053L16.5 15.8749L14.3313 15.2366C14.1746 15.1912 14.0064 15.2089 13.8625 15.2858C12.7116 15.9004 11.3662 16.0411 10.113 15.678C8.85983 15.3149 7.79803 14.4767 7.15393 13.3421C8.00969 13.2527 8.83776 12.9875 9.58619 12.5631C10.3346 12.1386 10.9873 11.5641 11.5032 10.8755C12.0191 10.1869 12.3871 9.39916 12.5842 8.56163C12.7813 7.72409 12.8032 6.85486 12.6485 6.00847C13.3941 6.18423 14.0893 6.52896 14.6806 7.01605C15.2718 7.50315 15.7432 8.11959 16.0584 8.81779C16.3736 9.51599 16.5242 10.2773 16.4985 11.0429C16.4728 11.8085 16.2715 12.558 15.9102 13.2335C15.8324 13.3782 15.8147 13.5477 15.861 13.7053H15.8586Z", fill }) });
1359
+ var RinggAiIcon = ({ size = DEFAULT_SIZE, width, height, style, testID }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native_svg2.default, { testID, viewBox: "0 0 30 31", width: width ?? size, height: height ?? size, style, children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M2.33836 25.8591C2.32963 26.2022 2.58051 26.4188 2.90888 26.3661C3.15878 26.3258 3.34088 26.1572 3.53558 26.0172C5.34692 24.712 7.32487 23.7335 9.43263 22.9812C11.49 22.2462 13.6568 22.1312 15.7791 21.7671C18.5242 21.2975 21.2954 20.9563 23.9408 20.0257C25.5875 19.4508 27.2002 18.8135 28.687 17.9059C29.0154 17.7056 29.3263 17.444 29.0861 17.0147C28.8459 16.5854 28.5001 16.8498 28.2096 16.9878C26.5697 17.7734 24.87 18.4303 23.1261 18.9525C19.8987 19.9041 16.5637 20.2941 13.2461 20.7685C9.35416 21.3243 5.88451 22.8135 2.77812 25.1768C2.54758 25.355 2.36644 25.5735 2.33836 25.8591Z", fill: "white", stroke: "white", strokeWidth: "0.789474" }),
1361
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M29.3604 13.1643C29.3779 12.74 29.0807 12.5106 28.7124 12.6469C28.4429 12.7602 28.1795 12.8874 27.9231 13.028C25.8484 14.0541 23.7055 14.89 21.4164 15.2869C18.4297 15.8082 15.44 16.3139 12.4513 16.8342C8.42668 17.534 4.81134 19.1862 1.53711 21.6097C0.945592 22.0467 0.797471 22.4387 1.04987 22.769C1.30225 23.0992 1.54295 23.0434 2.1861 22.5897C4.52484 20.9364 7.03997 19.6302 9.79484 18.8383C12.5146 18.0544 15.326 17.7437 18.1022 17.2537C20.3523 16.8558 22.6277 16.5903 24.7813 15.7573C26.1914 15.2115 27.5742 14.6117 28.8468 13.7836C29.0914 13.6239 29.337 13.4544 29.3604 13.1643Z", fill: "white", stroke: "white", strokeWidth: "0.789474" }),
1362
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M0.837274 18.3214C0.845042 18.7597 1.21893 18.9989 1.61418 18.7901C1.91329 18.6309 2.18423 18.42 2.46975 18.2365C5.45114 16.312 8.6355 14.8964 12.1423 14.2549C15.4966 13.6417 18.8848 13.2171 22.2071 12.435C24.5505 11.8834 26.7773 11.0173 28.8273 9.73247C29.1643 9.5206 29.5401 9.27944 29.2663 8.80787C28.9817 8.31969 28.6089 8.64384 28.2951 8.80299C26.0956 9.93946 23.7939 10.7898 21.3651 11.2565C19.1908 11.6744 17.0115 12.0767 14.8217 12.4057C11.6557 12.8822 8.56555 13.5871 5.67061 15.0145C4.12845 15.7751 2.6397 16.6216 1.26166 17.6565C1.04218 17.8234 0.823671 17.9992 0.837274 18.3214Z", fill: "white", stroke: "white", strokeWidth: "0.789474" }),
1363
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_svg2.Path, { d: "M29.1706 5.18743C29.1706 4.74357 28.8611 4.59203 28.3805 4.83058C25.4943 6.26384 22.4515 7.16329 19.2705 7.65507C16.3902 8.0999 13.4972 8.50466 10.6577 9.17338C7.26364 9.97213 4.12157 11.3702 1.29671 13.4487C1.11021 13.5771 0.941662 13.7299 0.795578 13.9033C0.625291 14.1214 0.573716 14.3834 0.784874 14.618C0.996033 14.8527 1.2393 14.8536 1.49133 14.7089C1.68595 14.5955 1.87473 14.4655 2.06155 14.3355C4.21952 12.8555 6.58876 11.7136 9.08819 10.9488C11.6542 10.1452 14.3253 9.90663 16.9487 9.41389C19.3347 8.96611 21.7557 8.70214 24.0717 7.91023C25.6851 7.35785 27.2751 6.7605 28.7181 5.82878C28.9555 5.67627 29.1803 5.51495 29.1706 5.18743Z", fill: "white", stroke: "white", strokeWidth: "0.789474" })
1364
+ ] });
1365
+
1366
+ // src/lib/portable-styles.ts
1367
+ var PERCENT = /^-?\d+(\.\d+)?%$/;
1368
+ var isPercent = (value) => PERCENT.test(value.trim());
1369
+ var toNumber = (value) => {
1370
+ if (value === void 0) return void 0;
1371
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1372
+ const trimmed = value.trim();
1373
+ const numeric = trimmed.endsWith("px") ? trimmed.slice(0, -2) : trimmed;
1374
+ const parsed = Number(numeric);
1375
+ return Number.isFinite(parsed) && numeric.trim() !== "" ? parsed : void 0;
1376
+ };
1377
+ var toDimension = (value) => {
1378
+ if (typeof value === "string" && isPercent(value)) return value.trim();
1379
+ return toNumber(value);
1380
+ };
1381
+ var compact = (style) => {
1382
+ const result = {};
1383
+ for (const [key, value] of Object.entries(style)) {
1384
+ if (value !== void 0) result[key] = value;
1385
+ }
1386
+ return result;
1387
+ };
1388
+ var toViewStyle = (styles25) => {
1389
+ if (!styles25) return {};
1390
+ return compact({
1391
+ width: toDimension(styles25.width),
1392
+ height: toDimension(styles25.height),
1393
+ // Gradient strings are not valid RN colors — the gradient-aware surfaces
1394
+ // read `theme.primaryColor` through <GradientFill> instead.
1395
+ backgroundColor: typeof styles25.backgroundColor === "string" && !styles25.backgroundColor.includes("(") ? styles25.backgroundColor : void 0,
1396
+ padding: toNumber(styles25.padding),
1397
+ borderRadius: toNumber(styles25.borderRadius)
1398
+ });
1399
+ };
1400
+ var toSize = (value, fallback) => toNumber(value) ?? fallback;
1401
+
1402
+ // src/lib/testing.ts
1403
+ var ringgId = (name) => `ringg-${name}`;
1404
+
1405
+ // src/lib/widget-theme.tsx
1406
+ var import_react3 = require("react");
1407
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1408
+ var INHERIT = "inherit";
1409
+ var PILL_RADIUS = 9999;
1410
+ var WidgetThemeContext = (0, import_react3.createContext)(DEFAULT_WIDGET_THEME);
1411
+ var resolveRadius = (value, fallback = 0) => {
1412
+ if (value === void 0) return fallback;
1413
+ const numeric = Number(value.trim().endsWith("px") ? value.trim().slice(0, -2) : value.trim());
1414
+ if (!Number.isFinite(numeric)) return fallback;
1415
+ return Math.min(numeric, PILL_RADIUS);
1416
+ };
1417
+ var resolveButtonRadius = (style) => resolveRadius(getButtonRadius(style));
1418
+ var resolveFontFamily = (fontFamily) => !fontFamily || fontFamily === INHERIT ? void 0 : fontFamily;
1419
+ var WidgetThemeProvider = ({ theme, children }) => {
1420
+ const merged = (0, import_react3.useMemo)(() => mergeWidgetTheme(theme), [theme]);
1421
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(WidgetThemeContext.Provider, { value: merged, children });
1422
+ };
1423
+ var useWidgetTheme = () => (0, import_react3.useContext)(WidgetThemeContext);
1424
+
1425
+ // src/components/ui/pressable.tsx
1426
+ var import_react_native2 = require("react-native");
1427
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1428
+ var PRESSED_OPACITY = 0.9;
1429
+ var DISABLED_OPACITY = 0.5;
1430
+ var DEFAULT_HIT_SLOP = 8;
1431
+ var Touchable = ({ onPress, onPressIn, onPressOut, disabled = false, style, testID, accessibilityLabel, hitSlop = DEFAULT_HIT_SLOP, children }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1432
+ import_react_native2.Pressable,
1433
+ {
1434
+ testID,
1435
+ accessibilityRole: "button",
1436
+ accessibilityLabel,
1437
+ accessibilityState: { disabled },
1438
+ disabled,
1439
+ onPress,
1440
+ onPressIn,
1441
+ onPressOut,
1442
+ hitSlop,
1443
+ style: ({ pressed }) => [style, disabled ? styles2.disabled : pressed ? styles2.pressed : null],
1444
+ children
1445
+ }
1446
+ );
1447
+ var styles2 = import_react_native2.StyleSheet.create({
1448
+ pressed: { opacity: PRESSED_OPACITY },
1449
+ disabled: { opacity: DISABLED_OPACITY }
1450
+ });
1451
+
1452
+ // src/components/action-button.tsx
1453
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1454
+ var ICON_SIZE = 16;
1455
+ var CONNECTING_LABEL = "Connecting...";
1456
+ var ActionButton = ({ callMode, isLoading, buttonConfig, isCalling, dual = false, onCallTrigger }) => {
1457
+ const theme = useWidgetTheme();
1458
+ const buttonRadius = resolveButtonRadius(theme.buttonStyle);
1459
+ const defaultLabel = callMode === "audio" ? dual ? "Call" : "Start Call" : dual ? "Chat" : "Start Chat";
1460
+ const label = (callMode === "audio" ? buttonConfig?.call?.textBeforeCall : buttonConfig?.text?.textBeforeCall) || defaultLabel;
1461
+ const isConnecting = isLoading === true && callMode === "audio";
1462
+ const widthStyle = dual ? styles3.dual : isCalling ? styles3.compact : styles3.full;
1463
+ const surfaceStyle = dual ? styles3.dualSurface : isCalling ? styles3.compactSurface : styles3.singleSurface;
1464
+ const labelStyle = [styles3.label, dual ? styles3.dualLabel : styles3.singleLabel, { color: theme.primaryTextColor, fontFamily: resolveFontFamily(theme.fontFamily) }];
1465
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Touchable, { testID: ringgId(callMode === "audio" ? "start-call-button" : "start-chat-button"), disabled: isLoading, onPress: () => onCallTrigger(callMode), style: widthStyle, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GradientFill, { color: theme.primaryColor, style: [styles3.surface, surfaceStyle, { borderRadius: buttonRadius }, toViewStyle(buttonConfig?.call?.styles)], children: isConnecting ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1466
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native3.ActivityIndicator, { size: "small", color: theme.primaryTextColor }),
1467
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native3.Text, { style: labelStyle, children: CONNECTING_LABEL })
1468
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1469
+ callMode === "audio" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneCallRingIcon, { size: ICON_SIZE, fill: theme.primaryTextColor }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ChatIcon, { size: ICON_SIZE, fill: theme.primaryTextColor }),
1470
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native3.Text, { style: labelStyle, children: label })
1471
+ ] }) }) });
1472
+ };
1473
+ var action_button_default = ActionButton;
1474
+ var styles3 = import_react_native3.StyleSheet.create({
1475
+ dual: { flex: 1 },
1476
+ full: { alignSelf: "stretch" },
1477
+ compact: { alignSelf: "flex-start" },
1478
+ // The layer <GradientFill> paints is absolutely positioned, so it sits out of
1479
+ // this row and the gap only ever spaces the icon from the label.
1480
+ surface: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 },
1481
+ dualSurface: { height: 40 },
1482
+ singleSurface: { height: 44 },
1483
+ compactSurface: { height: 44, paddingHorizontal: 24 },
1484
+ label: { fontWeight: "500" },
1485
+ dualLabel: { fontSize: 15 },
1486
+ // No text-size utility on the web button — it inherits the widget container's
1487
+ // 16px.
1488
+ singleLabel: { fontSize: 16 }
1489
+ });
1490
+
1491
+ // src/components/call-controls.tsx
1492
+ var import_react5 = require("react");
1493
+ var import_react_native6 = require("react-native");
1494
+ var import_lucide_react_native3 = require("lucide-react-native");
1495
+
1496
+ // src/components/message-input.tsx
1497
+ var import_react4 = require("react");
1498
+ var import_react_native5 = require("react-native");
1499
+ var import_lucide_react_native2 = require("lucide-react-native");
1500
+
1501
+ // src/lib/slash-command-types.ts
1502
+ function filterSlashCommands(commands, query) {
1503
+ if (!query.startsWith("/")) return [];
1504
+ const search = query.slice(1).toLowerCase();
1505
+ if (!search) return commands;
1506
+ return commands.filter(
1507
+ (cmd) => cmd.command.slice(1).toLowerCase().includes(search) || cmd.display_name.toLowerCase().includes(search)
1508
+ );
1509
+ }
1510
+
1511
+ // src/components/slash-command-menu.tsx
1512
+ var import_react_native4 = require("react-native");
1513
+ var import_lucide_react_native = require("lucide-react-native");
1514
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1515
+ var LIST_MAX_HEIGHT = 180;
1516
+ var ACTIVE_ICON_ALPHA = "18";
1517
+ var SIX_DIGIT_HEX = /^#[0-9a-f]{6}$/i;
1518
+ var iconBackground = (theme, isActive) => {
1519
+ if (!isActive) return theme.surfaceColor;
1520
+ const primary = solidColor(theme.primaryColor);
1521
+ return SIX_DIGIT_HEX.test(primary) ? `${primary}${ACTIVE_ICON_ALPHA}` : theme.surfaceColor;
1522
+ };
1523
+ var SlashCommandMenu = ({ commands, activeIndex, theme, onSelect, onActiveIndexChange }) => {
1524
+ if (commands.length === 0) return null;
1525
+ const fontFamily = resolveFontFamily(theme.fontFamily);
1526
+ const primaryColor = solidColor(theme.primaryColor);
1527
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native4.View, { testID: ringgId("slash-command-menu"), children: [
1528
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.View, { testID: ringgId("slash-command-header"), style: [styles4.header, { borderBottomColor: solidColor(theme.borderColor) }], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.Text, { testID: ringgId("slash-command-heading"), style: [styles4.heading, { color: theme.mutedTextColor, fontFamily }], children: "Commands" }) }),
1529
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.ScrollView, { testID: ringgId("slash-command-list"), style: styles4.list, contentContainerStyle: styles4.listContent, keyboardShouldPersistTaps: "handled", children: commands.map((cmd, index) => {
1530
+ const isActive = index === activeIndex;
1531
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1532
+ Touchable,
1533
+ {
1534
+ testID: ringgId("slash-command-item"),
1535
+ onPress: () => {
1536
+ onActiveIndexChange(index);
1537
+ onSelect(cmd);
1538
+ },
1539
+ hitSlop: 0,
1540
+ style: [styles4.item, { backgroundColor: isActive ? theme.surfaceColor : "transparent" }],
1541
+ children: [
1542
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.View, { testID: ringgId("slash-command-icon"), style: [styles4.icon, { backgroundColor: iconBackground(theme, isActive) }], children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react_native.Command, { size: 12, color: primaryColor }) }),
1543
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native4.View, { testID: ringgId("slash-command-text"), style: styles4.text, children: [
1544
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.Text, { testID: ringgId("slash-command-name"), style: [styles4.name, { color: theme.textColor, fontFamily }], children: cmd.display_name }),
1545
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native4.Text, { testID: ringgId("slash-command-description"), style: [styles4.description, { color: theme.mutedTextColor, fontFamily }], numberOfLines: 1, accessibilityLabel: cmd.description, children: cmd.description })
1546
+ ] })
1547
+ ]
1548
+ },
1549
+ cmd.command
1550
+ );
1551
+ }) })
1552
+ ] });
1553
+ };
1554
+ var slash_command_menu_default = SlashCommandMenu;
1555
+ var styles4 = import_react_native4.StyleSheet.create({
1556
+ header: { paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: import_react_native4.StyleSheet.hairlineWidth },
1557
+ heading: { fontSize: 10, fontWeight: "500", textTransform: "uppercase", letterSpacing: 0.5 },
1558
+ list: { maxHeight: LIST_MAX_HEIGHT },
1559
+ listContent: { paddingVertical: 4 },
1560
+ item: { flexDirection: "row", alignItems: "flex-start", gap: 10, paddingHorizontal: 12, paddingVertical: 8 },
1561
+ icon: { marginTop: 2, flexShrink: 0, width: 24, height: 24, borderRadius: 6, alignItems: "center", justifyContent: "center" },
1562
+ text: { flex: 1 },
1563
+ name: { fontSize: 12, lineHeight: 16, fontWeight: "500" },
1564
+ description: { fontSize: 10 }
1565
+ });
1566
+
1567
+ // src/components/message-input.tsx
1568
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1569
+ var INPUT_FONT_SIZE = 15;
1570
+ var INPUT_LINE_HEIGHT = 20;
1571
+ var MIN_INPUT_HEIGHT = 48;
1572
+ var MAX_INPUT_LINES = 4;
1573
+ var MAX_INPUT_HEIGHT = MIN_INPUT_HEIGHT + (MAX_INPUT_LINES - 1) * INPUT_LINE_HEIGHT;
1574
+ var INPUT_VERTICAL_PADDING = (MIN_INPUT_HEIGHT - INPUT_LINE_HEIGHT) / 2;
1575
+ var PILL_RADIUS2 = 9999;
1576
+ var MENU_OPEN_RADIUS = 12;
1577
+ var SEND_BUTTON_SIZE = 32;
1578
+ var clampInputHeight = (height) => Math.min(Math.max(height, MIN_INPUT_HEIGHT), MAX_INPUT_HEIGHT);
1579
+ var MessageInput = ({ isSending, enabledSlashCommands, onMessageSubmit, onSlashCommand }) => {
1580
+ const theme = useWidgetTheme();
1581
+ const [message, setMessage] = (0, import_react4.useState)("");
1582
+ const [activeCommandIndex, setActiveCommandIndex] = (0, import_react4.useState)(0);
1583
+ const [inputHeight, setInputHeight] = (0, import_react4.useState)(MIN_INPUT_HEIGHT);
1584
+ const isSlashQuery = message.startsWith("/");
1585
+ const availableCommands = enabledSlashCommands ?? [];
1586
+ const filteredCommands = (0, import_react4.useMemo)(() => {
1587
+ if (!isSlashQuery || availableCommands.length === 0) return [];
1588
+ return filterSlashCommands(availableCommands, message);
1589
+ }, [isSlashQuery, availableCommands, message]);
1590
+ const showMenu = isSlashQuery && filteredCommands.length > 0;
1591
+ const clearDraft = (0, import_react4.useCallback)(() => {
1592
+ setMessage("");
1593
+ setActiveCommandIndex(0);
1594
+ setInputHeight(MIN_INPUT_HEIGHT);
1595
+ }, []);
1596
+ const handleSelectCommand = (0, import_react4.useCallback)(
1597
+ (command) => {
1598
+ if (onSlashCommand) {
1599
+ onSlashCommand(command);
1600
+ } else {
1601
+ onMessageSubmit?.(command.command);
1602
+ }
1603
+ clearDraft();
1604
+ },
1605
+ [clearDraft, onMessageSubmit, onSlashCommand]
1606
+ );
1607
+ const submitDraft = (0, import_react4.useCallback)(() => {
1608
+ if (message.length === 0) return;
1609
+ onMessageSubmit?.(message);
1610
+ clearDraft();
1611
+ }, [clearDraft, message, onMessageSubmit]);
1612
+ const handleSubmitEditing = (0, import_react4.useCallback)(() => {
1613
+ if (!showMenu) {
1614
+ submitDraft();
1615
+ return;
1616
+ }
1617
+ const command = filteredCommands[activeCommandIndex];
1618
+ if (command) handleSelectCommand(command);
1619
+ }, [activeCommandIndex, filteredCommands, handleSelectCommand, showMenu, submitDraft]);
1620
+ const handleChangeText = (0, import_react4.useCallback)((value) => {
1621
+ setMessage(value);
1622
+ setActiveCommandIndex(0);
1623
+ }, []);
1624
+ const handleContentSizeChange = (0, import_react4.useCallback)((event) => {
1625
+ setInputHeight(clampInputHeight(event.nativeEvent.contentSize.height));
1626
+ }, []);
1627
+ const borderColor = solidColor(theme.borderColor);
1628
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native5.View, { testID: ringgId("message-input-form"), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1629
+ import_react_native5.View,
1630
+ {
1631
+ testID: ringgId("message-input"),
1632
+ style: [styles5.container, { backgroundColor: theme.backgroundColor, borderColor, borderRadius: showMenu ? MENU_OPEN_RADIUS : PILL_RADIUS2 }],
1633
+ children: [
1634
+ showMenu ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(slash_command_menu_default, { commands: filteredCommands, activeIndex: activeCommandIndex, theme, onSelect: handleSelectCommand, onActiveIndexChange: setActiveCommandIndex }) : null,
1635
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native5.View, { testID: ringgId("message-input-row"), style: [styles5.row, showMenu ? { borderTopWidth: import_react_native5.StyleSheet.hairlineWidth, borderTopColor: borderColor } : null], children: [
1636
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1637
+ import_react_native5.TextInput,
1638
+ {
1639
+ testID: ringgId("message-input-field"),
1640
+ value: message,
1641
+ onChangeText: handleChangeText,
1642
+ onContentSizeChange: handleContentSizeChange,
1643
+ onSubmitEditing: handleSubmitEditing,
1644
+ placeholder: availableCommands.length > 0 ? "Type /command..." : "Type here...",
1645
+ placeholderTextColor: theme.mutedTextColor,
1646
+ autoComplete: "off",
1647
+ multiline: true,
1648
+ returnKeyType: "send",
1649
+ submitBehavior: "submit",
1650
+ underlineColorAndroid: "transparent",
1651
+ style: [styles5.input, { height: inputHeight, color: theme.textColor, fontFamily: resolveFontFamily(theme.fontFamily) }]
1652
+ }
1653
+ ),
1654
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Touchable, { onPress: submitDraft, disabled: isSending, testID: ringgId("message-send-button"), accessibilityLabel: "Send message", style: styles5.sendButton, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GradientFill, { color: theme.primaryColor, style: styles5.sendButtonSurface, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react_native2.ArrowUp, { size: 16, color: theme.primaryTextColor }) }) })
1655
+ ] })
1656
+ ]
1657
+ }
1658
+ ) });
1659
+ };
1660
+ var message_input_default = MessageInput;
1661
+ var styles5 = import_react_native5.StyleSheet.create({
1662
+ container: { borderWidth: import_react_native5.StyleSheet.hairlineWidth, overflow: "hidden" },
1663
+ row: { flexDirection: "row", alignItems: "flex-end" },
1664
+ input: {
1665
+ flex: 1,
1666
+ fontSize: INPUT_FONT_SIZE,
1667
+ paddingLeft: 20,
1668
+ // 12 here plus the button's 32 and its 4 of margin reproduce web's `pr-12`.
1669
+ paddingRight: 12,
1670
+ paddingVertical: INPUT_VERTICAL_PADDING,
1671
+ // Text grows downward from the top edge instead of re-centring on Android.
1672
+ textAlignVertical: "top"
1673
+ },
1674
+ sendButton: { marginRight: 4, marginBottom: (MIN_INPUT_HEIGHT - SEND_BUTTON_SIZE) / 2 },
1675
+ sendButtonSurface: { width: SEND_BUTTON_SIZE, height: SEND_BUTTON_SIZE, borderRadius: PILL_RADIUS2, alignItems: "center", justifyContent: "center" }
1676
+ });
1677
+
1678
+ // src/components/call-controls.tsx
1679
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1680
+ var ICON_SIZE2 = 20;
1681
+ var END_CALL_ICON_COLOR = "#ffffff";
1682
+ var AudioControlsView = ({ micEnabled, onToggleMic, buttonConfig, onCallEnd }) => {
1683
+ const theme = useWidgetTheme();
1684
+ const handleCallEndTrigger = () => onCallEnd?.();
1685
+ const micStyles = toViewStyle(buttonConfig?.mic?.styles);
1686
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native6.View, { testID: ringgId("call-controls"), style: styles6.row, children: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1688
+ Touchable,
1689
+ {
1690
+ testID: ringgId("mute-button"),
1691
+ accessibilityLabel: micEnabled ? "Mute" : "Unmute",
1692
+ onPress: onToggleMic ?? (() => {
1693
+ }),
1694
+ style: [styles6.circle, styles6.muteButton, { backgroundColor: solidColor(theme.backgroundColor), borderColor: solidColor(theme.borderColor) }, micStyles],
1695
+ children: micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MicrophoneIcon, { size: ICON_SIZE2, fill: solidColor(theme.textColor) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react_native3.MicOff, { size: ICON_SIZE2, color: solidColor(theme.textColor) })
1696
+ }
1697
+ ),
1698
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1699
+ Touchable,
1700
+ {
1701
+ testID: ringgId("end-call-button"),
1702
+ accessibilityLabel: "End call",
1703
+ onPress: handleCallEndTrigger,
1704
+ style: [styles6.circle, { backgroundColor: solidColor(theme.errorColor) }, micStyles],
1705
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PhoneEndCallIcon, { size: ICON_SIZE2, fill: END_CALL_ICON_COLOR })
1706
+ }
1707
+ )
1708
+ ] });
1709
+ };
1710
+ var CallControls = ({ isCalling, callMode, buttonConfig, enabledSlashCommands, onSlashCommand, onUserMessage, onCallEnd, onSendMessage, micEnabled = true, onToggleMic }) => {
1711
+ const [isSending, setIsSending] = (0, import_react5.useState)(false);
1712
+ const handleSendMessage = async (message) => {
1713
+ onUserMessage?.();
1714
+ setIsSending(true);
1715
+ try {
1716
+ await onSendMessage?.(message);
1717
+ } finally {
1718
+ setIsSending(false);
1719
+ }
1720
+ };
1721
+ if (!isCalling) return null;
1722
+ switch (callMode) {
1723
+ case "text":
1724
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(message_input_default, { isSending, enabledSlashCommands, onMessageSubmit: handleSendMessage, onSlashCommand });
1725
+ case "audio":
1726
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AudioControlsView, { micEnabled, onToggleMic, buttonConfig, onCallEnd });
1727
+ default:
1728
+ return null;
1729
+ }
1730
+ };
1731
+ var call_controls_default = CallControls;
1732
+ var styles6 = import_react_native6.StyleSheet.create({
1733
+ row: { flexDirection: "row", width: "100%", justifyContent: "center", alignItems: "center", gap: 24 },
1734
+ circle: { height: 48, width: 48, borderRadius: 9999, alignItems: "center", justifyContent: "center" },
1735
+ muteButton: { borderWidth: 1 }
1736
+ });
1737
+
1738
+ // src/components/feedback-screen.tsx
1739
+ var import_react15 = require("react");
1740
+ var import_react_native17 = require("react-native");
1741
+ var import_react_native_svg4 = __toESM(require("react-native-svg"));
1742
+
1743
+ // src/components/header.tsx
1744
+ var import_react6 = require("react");
1745
+ var import_react_native7 = require("react-native");
1746
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1747
+ var INTRO_MARK_SIZE = 32;
1748
+ var CALLING_MARK_SIZE = 18;
1749
+ var PULSE_MIN_OPACITY = 0.5;
1750
+ var PULSE_HALF_CYCLE_MS = 1e3;
1751
+ var PULSE_EASING = import_react_native7.Easing.bezier(0.4, 0, 0.6, 1);
1752
+ var SCRIM_OPACITY = 0.96;
1753
+ var HeaderLogo = ({ logoUrl, logoStyles, style, markSize }) => {
1754
+ const theme = useWidgetTheme();
1755
+ return (
1756
+ // <GradientFill> owns the painted node, so the contract testID rides on a
1757
+ // wrapper that shrink-wraps the tile and centres it on the cross axis (the
1758
+ // intro's `mx-auto`; a no-op inside the already-centred calling row).
1759
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.View, { testID: ringgId("header-logo"), style: styles7.logoSlot, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GradientFill, { color: theme.primaryColor, style: [styles7.logo, style, toViewStyle(logoStyles)], children: logoUrl ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.Image, { testID: ringgId("header-logo-image"), accessibilityLabel: "Logo", source: { uri: logoUrl }, resizeMode: "contain", style: styles7.logoImage }) : (
1760
+ // Nothing is bundled here (web inlines its default logo as a data URI
1761
+ // at build time), so the brand mark stands in until an integrator
1762
+ // supplies logoUrl.
1763
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.View, { testID: ringgId("header-logo-image"), style: styles7.logoMark, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RinggAiIcon, { size: markSize }) })
1764
+ ) }) })
1765
+ );
1766
+ };
1767
+ var StatusDot = ({ isLoading, color }) => {
1768
+ const pulse = (0, import_react6.useRef)(new import_react_native7.Animated.Value(1)).current;
1769
+ (0, import_react6.useEffect)(() => {
1770
+ if (!isLoading) {
1771
+ pulse.setValue(1);
1772
+ return;
1773
+ }
1774
+ const loop = import_react_native7.Animated.loop(
1775
+ import_react_native7.Animated.sequence([
1776
+ import_react_native7.Animated.timing(pulse, { toValue: PULSE_MIN_OPACITY, duration: PULSE_HALF_CYCLE_MS, easing: PULSE_EASING, useNativeDriver: true }),
1777
+ import_react_native7.Animated.timing(pulse, { toValue: 1, duration: PULSE_HALF_CYCLE_MS, easing: PULSE_EASING, useNativeDriver: true })
1778
+ ])
1779
+ );
1780
+ loop.start();
1781
+ return () => loop.stop();
1782
+ }, [isLoading, pulse]);
1783
+ return (
1784
+ // Web carries the connecting flag as a second `data-ringg-connecting`
1785
+ // attribute; RN has no attribute namespace, so the machine-readable half
1786
+ // becomes `busy` and the colour carries the visual half.
1787
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.Animated.View, { testID: ringgId("header-status-dot"), accessibilityState: { busy: isLoading }, style: [styles7.statusDot, { backgroundColor: color, opacity: pulse }] })
1788
+ );
1789
+ };
1790
+ var Header = ({ isCalling, isLoading, title, description, logoUrl, logoStyles }) => {
1791
+ const theme = useWidgetTheme();
1792
+ const fontFamily = resolveFontFamily(theme.fontFamily);
1793
+ if (!isCalling)
1794
+ return (
1795
+ // flex-1 + centring: the logo/title/description block floats in the
1796
+ // panel's free space so the start buttons and legal text sit low.
1797
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native7.View, { testID: ringgId("intro"), style: styles7.intro, children: [
1798
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(HeaderLogo, { logoUrl, logoStyles, style: styles7.introLogo, markSize: INTRO_MARK_SIZE }),
1799
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native7.View, { testID: ringgId("header-text"), style: styles7.introText, children: [
1800
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.Text, { testID: ringgId("header-title"), accessibilityRole: "header", style: [styles7.introTitle, { color: theme.textColor, fontFamily }], children: title }),
1801
+ description ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.Text, { testID: ringgId("header-description"), style: [styles7.introDescription, { color: theme.mutedTextColor, fontFamily }], children: description }) : null
1802
+ ] })
1803
+ ] })
1804
+ );
1805
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1806
+ import_react_native7.View,
1807
+ {
1808
+ testID: ringgId("header"),
1809
+ accessibilityLabel: isLoading ? "Calling..." : "Call in Progress",
1810
+ accessibilityLiveRegion: "polite",
1811
+ style: styles7.header,
1812
+ children: [
1813
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.View, { pointerEvents: "none", style: [import_react_native7.StyleSheet.absoluteFill, styles7.scrim, { backgroundColor: solidColor(theme.backgroundColor) }] }),
1814
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(HeaderLogo, { logoUrl, logoStyles, style: styles7.callingLogo, markSize: CALLING_MARK_SIZE }),
1815
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native7.View, { testID: ringgId("header-text"), style: styles7.headerText, children: [
1816
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_react_native7.View, { style: styles7.headerTitleRow, children: [
1817
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.Text, { testID: ringgId("header-title"), accessibilityRole: "header", numberOfLines: 1, style: [styles7.callingTitle, { color: theme.textColor, fontFamily }], children: title }),
1818
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StatusDot, { isLoading, color: solidColor(isLoading ? theme.mutedTextColor : theme.successColor) })
1819
+ ] }),
1820
+ description ? (
1821
+ // Truncated on screen; web exposes the full string through a `title`
1822
+ // tooltip, which here is the accessibility label.
1823
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native7.Text, { testID: ringgId("header-description"), accessibilityLabel: description, numberOfLines: 1, style: [styles7.callingDescription, { color: theme.mutedTextColor, fontFamily }], children: description })
1824
+ ) : null
1825
+ ] })
1826
+ ]
1827
+ }
1828
+ );
1829
+ };
1830
+ var header_default = Header;
1831
+ var styles7 = import_react_native7.StyleSheet.create({
1832
+ intro: { flex: 1, justifyContent: "center", gap: 16 },
1833
+ introLogo: { width: 64, height: 64, borderRadius: 16 },
1834
+ introText: { gap: 4 },
1835
+ introTitle: { fontSize: 17, fontWeight: "500", textAlign: "center" },
1836
+ // `leading-snug` (1.375) at 14px.
1837
+ introDescription: { fontSize: 14, lineHeight: 19, fontWeight: "400", textAlign: "center", maxWidth: 210, alignSelf: "center" },
1838
+ header: {
1839
+ position: "absolute",
1840
+ top: 0,
1841
+ left: 0,
1842
+ right: 0,
1843
+ zIndex: 10,
1844
+ flexDirection: "row",
1845
+ alignItems: "center",
1846
+ justifyContent: "flex-start",
1847
+ gap: 10,
1848
+ paddingVertical: 12,
1849
+ paddingLeft: 20,
1850
+ // Clears the absolute minimize/close chrome so header content — including
1851
+ // integrator-injected brand text — never slides under it.
1852
+ paddingRight: 76
1853
+ },
1854
+ scrim: { opacity: SCRIM_OPACITY },
1855
+ callingLogo: { width: 36, height: 36, borderRadius: 12 },
1856
+ headerText: { flexShrink: 1 },
1857
+ headerTitleRow: { flexDirection: "row", alignItems: "center", gap: 8 },
1858
+ // flexShrink lets the title truncate instead of pushing the dot out of view.
1859
+ callingTitle: { fontSize: 15, fontWeight: "600", flexShrink: 1 },
1860
+ callingDescription: { fontSize: 13, fontWeight: "400" },
1861
+ statusDot: { width: 6, height: 6, borderRadius: 3, flexShrink: 0 },
1862
+ logoSlot: { alignSelf: "center", flexShrink: 0 },
1863
+ logo: { alignItems: "center", justifyContent: "center", overflow: "hidden" },
1864
+ logoImage: { width: "100%", height: "100%" },
1865
+ logoMark: { alignItems: "center", justifyContent: "center" }
1866
+ });
1867
+
1868
+ // src/components/interactive-flow.tsx
1869
+ var import_react12 = require("react");
1870
+ var import_react_native13 = require("react-native");
1871
+ var import_lucide_react_native8 = require("lucide-react-native");
1872
+
1873
+ // src/lib/component-types.ts
1874
+ function groupSlotsByDate(slots) {
1875
+ const grouped = /* @__PURE__ */ new Map();
1876
+ slots.forEach((slot) => {
1877
+ const date = new Date(slot.datetime);
1878
+ const dateKey = date.toISOString().split("T")[0] ?? "";
1879
+ if (!grouped.has(dateKey)) {
1880
+ grouped.set(dateKey, []);
1881
+ }
1882
+ grouped.get(dateKey).push(slot);
1883
+ });
1884
+ return Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([dateKey, dateSlots]) => {
1885
+ const date = new Date(dateKey);
1886
+ return {
1887
+ date: dateKey,
1888
+ dateLabel: date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }),
1889
+ slots: dateSlots.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
1890
+ };
1891
+ });
1892
+ }
1893
+ function formatSlotTime(datetime, timezone) {
1894
+ const date = new Date(datetime);
1895
+ return date.toLocaleTimeString("en-US", {
1896
+ hour: "numeric",
1897
+ minute: "2-digit",
1898
+ hour12: true,
1899
+ timeZone: timezone
1900
+ });
1901
+ }
1902
+ function formatSlotDate(datetime, timezone) {
1903
+ const date = new Date(datetime);
1904
+ return date.toLocaleDateString("en-US", {
1905
+ weekday: "short",
1906
+ month: "short",
1907
+ day: "numeric",
1908
+ timeZone: timezone
1909
+ });
1910
+ }
1911
+ function buildPayload(template, values) {
1912
+ const result = {};
1913
+ for (const [key, value] of Object.entries(template)) {
1914
+ if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
1915
+ const placeholder = value.slice(1, -1);
1916
+ result[key] = values[placeholder] ?? value;
1917
+ } else {
1918
+ result[key] = value;
1919
+ }
1920
+ }
1921
+ return result;
1922
+ }
1923
+ function isButtons(payload) {
1924
+ return payload.component_type === "buttons";
1925
+ }
1926
+ function isInteractiveFlow(payload) {
1927
+ return payload.component_type === "interactive_flow";
1928
+ }
1929
+ function isBlocks(payload) {
1930
+ return payload.component_type === "blocks";
1931
+ }
1932
+
1933
+ // src/lib/theme-utils.ts
1934
+ var DEFAULT_THEME = {
1935
+ primaryColor: "#0a0a0b",
1936
+ primaryTextColor: "#ffffff",
1937
+ backgroundColor: "#ffffff",
1938
+ surfaceColor: "#f3f4f6",
1939
+ textColor: "#0a0a0b",
1940
+ mutedTextColor: "#6b7280",
1941
+ borderColor: "#e5e7eb",
1942
+ errorColor: "#ef4444",
1943
+ successColor: "#16a34a",
1944
+ fontFamily: "inherit",
1945
+ fontSize: "14px",
1946
+ borderRadius: "8px",
1947
+ padding: "12px",
1948
+ buttonStyle: "pill",
1949
+ buttonSize: "md"
1950
+ };
1951
+ var PILL_RADIUS3 = 9999;
1952
+ var mergeTheme = (theme) => ({ ...DEFAULT_THEME, ...theme });
1953
+ var toNumber2 = (value, fallback) => {
1954
+ if (value === void 0) return fallback;
1955
+ const trimmed = value.trim();
1956
+ const parsed = Number(trimmed.endsWith("px") ? trimmed.slice(0, -2) : trimmed);
1957
+ return Number.isFinite(parsed) ? parsed : fallback;
1958
+ };
1959
+ var getButtonRadius2 = (style) => {
1960
+ switch (style) {
1961
+ case "pill":
1962
+ return PILL_RADIUS3;
1963
+ case "square":
1964
+ return 4;
1965
+ case "rounded":
1966
+ default:
1967
+ return 8;
1968
+ }
1969
+ };
1970
+
1971
+ // src/components/steps/buttons-step.tsx
1972
+ var import_react7 = require("react");
1973
+ var import_react_native8 = require("react-native");
1974
+ var import_lucide_react_native4 = require("lucide-react-native");
1975
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1976
+ var SPIN_DURATION_MS = 1e3;
1977
+ var Spinner = ({ size, color }) => {
1978
+ const rotation = (0, import_react7.useRef)(new import_react_native8.Animated.Value(0)).current;
1979
+ (0, import_react7.useEffect)(() => {
1980
+ const animation = import_react_native8.Animated.loop(import_react_native8.Animated.timing(rotation, { toValue: 1, duration: SPIN_DURATION_MS, easing: import_react_native8.Easing.linear, useNativeDriver: true }));
1981
+ animation.start();
1982
+ return () => animation.stop();
1983
+ }, [rotation]);
1984
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.Animated.View, { style: { transform: [{ rotate: rotation.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] }) }] }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react_native4.LoaderCircle, { size, color }) });
1985
+ };
1986
+ var buttonSurface = (theme, style) => {
1987
+ switch (style) {
1988
+ case "secondary":
1989
+ case "outline":
1990
+ return { background: theme.surfaceColor, foreground: theme.textColor };
1991
+ case "destructive":
1992
+ return { background: theme.errorColor, foreground: theme.primaryTextColor };
1993
+ case "primary":
1994
+ default:
1995
+ return { background: theme.primaryColor, foreground: theme.primaryTextColor };
1996
+ }
1997
+ };
1998
+ var ButtonsStep = ({ payload, isProcessing, align = "end", onButtonClick }) => {
1999
+ const widgetTheme = useWidgetTheme();
2000
+ const theme = (0, import_react7.useMemo)(
2001
+ () => mergeTheme({
2002
+ primaryColor: widgetTheme.primaryColor,
2003
+ primaryTextColor: widgetTheme.primaryTextColor,
2004
+ backgroundColor: widgetTheme.backgroundColor,
2005
+ surfaceColor: widgetTheme.surfaceColor,
2006
+ textColor: widgetTheme.textColor,
2007
+ mutedTextColor: widgetTheme.mutedTextColor,
2008
+ borderColor: widgetTheme.borderColor,
2009
+ ...payload.theme
2010
+ }),
2011
+ [payload.theme, widgetTheme]
2012
+ );
2013
+ const fontFamily = resolveFontFamily(theme.fontFamily);
2014
+ const { data } = payload;
2015
+ const isFreePresentation = data.presentation === "free";
2016
+ const [loadingButtonId, setLoadingButtonId] = (0, import_react7.useState)(null);
2017
+ const handleClick = async (button) => {
2018
+ if (button.action.type === "navigate") {
2019
+ onButtonClick(button.id, button.action);
2020
+ return;
2021
+ }
2022
+ setLoadingButtonId(button.id);
2023
+ try {
2024
+ await onButtonClick(button.id, button.action);
2025
+ } finally {
2026
+ setLoadingButtonId(null);
2027
+ }
2028
+ };
2029
+ const title = data.title ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.Text, { testID: ringgId("buttons-step-title"), style: [styles8.title, { color: theme.textColor, fontFamily }], children: data.title }) : null;
2030
+ if (isFreePresentation) {
2031
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native8.View, { testID: ringgId("quick-replies"), style: styles8.root, children: [
2032
+ title,
2033
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.View, { style: [styles8.chipRow, align === "center" ? styles8.alignCenter : styles8.alignEnd], children: data.buttons.map((button) => {
2034
+ const isLoading = loadingButtonId === button.id;
2035
+ const isNavigate = button.action.type === "navigate";
2036
+ const isDisabled = isLoading || isProcessing;
2037
+ const surface = buttonSurface(theme, button.style);
2038
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Touchable, { testID: ringgId("quick-reply-button"), accessibilityLabel: button.label, disabled: isDisabled, onPress: () => void handleClick(button), style: styles8.chip, children: [
2039
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(GradientFill, { color: surface.background, style: import_react_native8.StyleSheet.absoluteFill }),
2040
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, { size: 12, color: solidColor(surface.foreground) }) : null,
2041
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.Text, { style: [styles8.chipLabel, { color: surface.foreground, fontFamily }], children: button.label }),
2042
+ isNavigate ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react_native4.ExternalLink, { size: 12, color: solidColor(surface.foreground) }) : null
2043
+ ] }, button.id);
2044
+ }) })
2045
+ ] });
2046
+ }
2047
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_react_native8.View, { children: [
2048
+ title,
2049
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.View, { testID: ringgId("options-list"), style: styles8.optionsList, children: data.buttons.map((button, index) => {
2050
+ const isLoading = loadingButtonId === button.id;
2051
+ const isNavigate = button.action.type === "navigate";
2052
+ const isDisabled = isLoading || isProcessing;
2053
+ const isDestructive = button.style === "destructive";
2054
+ const isLastRow = index === data.buttons.length - 1;
2055
+ const labelColor = isDestructive ? theme.errorColor : theme.textColor;
2056
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2057
+ Touchable,
2058
+ {
2059
+ testID: ringgId("option-row"),
2060
+ accessibilityLabel: button.label,
2061
+ disabled: isDisabled,
2062
+ onPress: () => void handleClick(button),
2063
+ hitSlop: 0,
2064
+ style: [styles8.optionRow, isLastRow ? null : { borderBottomWidth: import_react_native8.StyleSheet.hairlineWidth, borderBottomColor: solidColor(theme.borderColor) }],
2065
+ children: [
2066
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Spinner, { size: 16, color: solidColor(labelColor) }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.View, { testID: ringgId("option-radio"), accessibilityElementsHidden: true, importantForAccessibility: "no", style: [styles8.optionRadio, { borderColor: solidColor(theme.mutedTextColor) }] }),
2067
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react_native8.Text, { style: [styles8.optionLabel, { color: labelColor, fontFamily }], children: button.label }),
2068
+ isNavigate ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react_native4.ExternalLink, { size: 14, color: solidColor(labelColor) }) : null
2069
+ ]
2070
+ },
2071
+ button.id
2072
+ );
2073
+ }) })
2074
+ ] });
2075
+ };
2076
+ var buttons_step_default = ButtonsStep;
2077
+ var styles8 = import_react_native8.StyleSheet.create({
2078
+ root: { width: "100%" },
2079
+ title: { fontSize: 14, lineHeight: 20, fontWeight: "500", marginBottom: 8 },
2080
+ chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
2081
+ alignCenter: { justifyContent: "center" },
2082
+ alignEnd: { justifyContent: "flex-end" },
2083
+ // `flexShrink` is 1 by default on web and 0 in RN, so a chip wide enough to
2084
+ // fill the row has to be told it may shrink or it overflows the panel.
2085
+ chip: { flexDirection: "row", alignItems: "center", gap: 6, height: 36, paddingHorizontal: 16, borderRadius: 9999, flexShrink: 1, overflow: "hidden" },
2086
+ chipLabel: { flexShrink: 1, fontSize: 15, fontWeight: "500" },
2087
+ optionsList: { flexDirection: "column" },
2088
+ optionRow: { flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 8, paddingVertical: 12 },
2089
+ optionRadio: { width: 16, height: 16, borderRadius: 9999, borderWidth: 1, flexShrink: 0 },
2090
+ optionLabel: { flex: 1, fontSize: 15 }
2091
+ });
2092
+
2093
+ // src/components/steps/calendar-step.tsx
2094
+ var import_react8 = require("react");
2095
+ var import_react_native9 = require("react-native");
2096
+ var import_lucide_react_native5 = require("lucide-react-native");
2097
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2098
+ var SPIN_DURATION_MS2 = 1e3;
2099
+ var SLOT_FADE_MS = 200;
2100
+ var SLOT_GUTTER = 6;
2101
+ var DATE_HIT_SLOP = 4;
2102
+ var SLOT_HIT_SLOP = SLOT_GUTTER / 2;
2103
+ var useStepTheme = (componentTheme) => {
2104
+ const widgetTheme = useWidgetTheme();
2105
+ return (0, import_react8.useMemo)(
2106
+ () => mergeTheme({
2107
+ primaryColor: widgetTheme.primaryColor,
2108
+ primaryTextColor: widgetTheme.primaryTextColor,
2109
+ // Cards read as bordered surfaces sitting ON the panel, so they take
2110
+ // the widget background rather than the (greyer) surface colour.
2111
+ backgroundColor: widgetTheme.backgroundColor,
2112
+ surfaceColor: widgetTheme.surfaceColor,
2113
+ textColor: widgetTheme.textColor,
2114
+ mutedTextColor: widgetTheme.mutedTextColor,
2115
+ borderColor: widgetTheme.borderColor,
2116
+ errorColor: widgetTheme.errorColor,
2117
+ successColor: widgetTheme.successColor,
2118
+ buttonStyle: widgetTheme.buttonStyle,
2119
+ // Web inherits the family from the flow container; RN has no cascade,
2120
+ // so it travels through the theme and is applied per <Text>.
2121
+ fontFamily: widgetTheme.fontFamily,
2122
+ ...componentTheme
2123
+ }),
2124
+ [componentTheme, widgetTheme]
2125
+ );
2126
+ };
2127
+ var Spinner2 = ({ color }) => {
2128
+ const turn = (0, import_react8.useRef)(new import_react_native9.Animated.Value(0)).current;
2129
+ (0, import_react8.useEffect)(() => {
2130
+ const loop = import_react_native9.Animated.loop(import_react_native9.Animated.timing(turn, { toValue: 1, duration: SPIN_DURATION_MS2, easing: import_react_native9.Easing.linear, useNativeDriver: true }));
2131
+ loop.start();
2132
+ return () => loop.stop();
2133
+ }, [turn]);
2134
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Animated.View, { style: { transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] }) }] }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react_native5.LoaderCircle, { size: 16, color }) });
2135
+ };
2136
+ var CalendarStep = ({ payload, isProcessing, onComplete }) => {
2137
+ const theme = useStepTheme(payload.theme);
2138
+ const buttonRadius = getButtonRadius2(theme.buttonStyle);
2139
+ const fontFamily = resolveFontFamily(theme.fontFamily);
2140
+ const { data } = payload;
2141
+ const groupedSlots = (0, import_react8.useMemo)(() => groupSlotsByDate(data.available_slots), [data.available_slots]);
2142
+ const [selectedDate, setSelectedDate] = (0, import_react8.useState)(groupedSlots[0]?.date || "");
2143
+ const [selectedSlot, setSelectedSlot] = (0, import_react8.useState)(null);
2144
+ const selectedDateGroup = groupedSlots.find((g) => g.date === selectedDate);
2145
+ const slotFade = (0, import_react8.useRef)(new import_react_native9.Animated.Value(0)).current;
2146
+ (0, import_react8.useLayoutEffect)(() => {
2147
+ slotFade.setValue(0);
2148
+ const fade = import_react_native9.Animated.timing(slotFade, { toValue: 1, duration: SLOT_FADE_MS, useNativeDriver: true });
2149
+ fade.start();
2150
+ return () => fade.stop();
2151
+ }, [selectedDate, slotFade]);
2152
+ const handleConfirm = () => {
2153
+ if (selectedSlot) {
2154
+ onComplete(selectedSlot.id, selectedSlot.datetime);
2155
+ }
2156
+ };
2157
+ const isConfirmDisabled = !selectedSlot || isProcessing;
2158
+ const confirmTextColor = isConfirmDisabled ? theme.mutedTextColor : theme.primaryTextColor;
2159
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native9.View, { children: [
2160
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native9.View, { style: styles9.header, children: [
2161
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react_native5.Calendar, { size: 16, color: solidColor(theme.primaryColor) }),
2162
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.title, { color: theme.textColor, fontFamily }], children: data.title })
2163
+ ] }),
2164
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native9.Text, { style: [styles9.timezone, { color: theme.mutedTextColor, fontFamily }], children: [
2165
+ "Timezone: ",
2166
+ data.timezone
2167
+ ] }),
2168
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native9.View, { style: styles9.section, children: [
2169
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.sectionLabel, styles9.sectionLabelSpacing, { color: theme.mutedTextColor, fontFamily }], children: "Select Date" }),
2170
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.View, { style: styles9.dateRow, children: groupedSlots.map((group) => {
2171
+ const isSelected = selectedDate === group.date;
2172
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2173
+ Touchable,
2174
+ {
2175
+ hitSlop: DATE_HIT_SLOP,
2176
+ onPress: () => {
2177
+ setSelectedDate(group.date);
2178
+ setSelectedSlot(null);
2179
+ },
2180
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2181
+ GradientFill,
2182
+ {
2183
+ color: isSelected ? theme.primaryColor : theme.backgroundColor,
2184
+ style: [styles9.dateChip, { borderRadius: buttonRadius }, isSelected ? styles9.selectedShadow : { borderWidth: 1, borderColor: theme.borderColor }],
2185
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.dateChipLabel, { color: isSelected ? theme.primaryTextColor : theme.textColor, fontFamily }], children: group.dateLabel })
2186
+ }
2187
+ )
2188
+ },
2189
+ group.date
2190
+ );
2191
+ }) })
2192
+ ] }),
2193
+ selectedDateGroup ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native9.Animated.View, { style: [styles9.section, { opacity: slotFade }], children: [
2194
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_react_native9.View, { style: styles9.sectionLabelRow, children: [
2195
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react_native5.Clock, { size: 12, color: theme.mutedTextColor }),
2196
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.sectionLabel, { color: theme.mutedTextColor, fontFamily }], children: "Select Time" })
2197
+ ] }),
2198
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.View, { style: styles9.slotGrid, children: selectedDateGroup.slots.map((slot) => {
2199
+ const isSelected = selectedSlot?.id === slot.id;
2200
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.View, { style: styles9.slotCell, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Touchable, { hitSlop: SLOT_HIT_SLOP, onPress: () => setSelectedSlot(slot), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2201
+ GradientFill,
2202
+ {
2203
+ color: isSelected ? theme.primaryColor : theme.backgroundColor,
2204
+ style: [styles9.slot, { borderRadius: buttonRadius, borderColor: isSelected ? solidColor(theme.primaryColor) : theme.borderColor }, isSelected ? styles9.selectedShadow : null],
2205
+ children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.slotLabel, { color: isSelected ? theme.primaryTextColor : theme.textColor, fontFamily }], numberOfLines: 1, children: formatSlotTime(slot.datetime, data.timezone) })
2206
+ }
2207
+ ) }) }, slot.id);
2208
+ }) })
2209
+ ] }) : null,
2210
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Touchable, { onPress: handleConfirm, disabled: isConfirmDisabled, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(GradientFill, { color: isConfirmDisabled ? theme.borderColor : theme.primaryColor, style: [styles9.confirmButton, { borderRadius: buttonRadius }], children: isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
2211
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner2, { color: confirmTextColor }),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.confirmLabel, { color: confirmTextColor, fontFamily }], children: "Processing..." })
2213
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native9.Text, { style: [styles9.confirmLabel, { color: confirmTextColor, fontFamily }], children: "Next" }) }) })
2214
+ ] });
2215
+ };
2216
+ var styles9 = import_react_native9.StyleSheet.create({
2217
+ header: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 12 },
2218
+ title: { fontSize: 14, lineHeight: 20, fontWeight: "500" },
2219
+ timezone: { fontSize: 12, lineHeight: 16, marginBottom: 12 },
2220
+ section: { marginBottom: 12 },
2221
+ sectionLabel: { fontSize: 12, lineHeight: 16, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.3 },
2222
+ sectionLabelSpacing: { marginBottom: 8 },
2223
+ sectionLabelRow: { flexDirection: "row", alignItems: "center", gap: 4, marginBottom: 8 },
2224
+ dateRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
2225
+ dateChip: { paddingHorizontal: 12, paddingVertical: 8, alignItems: "center", justifyContent: "center" },
2226
+ dateChipLabel: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
2227
+ // Web's `0 2px 4px rgba(0,0,0,0.1)` on the chosen chip, in both platforms' terms.
2228
+ selectedShadow: { shadowColor: "#000000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 },
2229
+ // Three equal columns with `SLOT_GUTTER` between them: each cell pads half a
2230
+ // gutter per side and the row pulls that padding back off its outer edges.
2231
+ slotGrid: { flexDirection: "row", flexWrap: "wrap", rowGap: SLOT_GUTTER, marginHorizontal: -SLOT_GUTTER / 2 },
2232
+ slotCell: { width: "33.3333%", paddingHorizontal: SLOT_GUTTER / 2 },
2233
+ slot: { paddingHorizontal: 8, paddingVertical: 8, alignItems: "center", justifyContent: "center", borderWidth: 1 },
2234
+ slotLabel: { fontSize: 12, lineHeight: 16, fontWeight: "500" },
2235
+ confirmButton: { height: 36, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 },
2236
+ confirmLabel: { fontSize: 14, lineHeight: 20, fontWeight: "600" }
2237
+ });
2238
+ var calendar_step_default = CalendarStep;
2239
+
2240
+ // src/components/steps/confirmation-step.tsx
2241
+ var import_react9 = require("react");
2242
+ var import_react_native10 = require("react-native");
2243
+ var import_lucide_react_native6 = require("lucide-react-native");
2244
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2245
+ var ENTER_DURATION_MS = 300;
2246
+ var TITLE_DELAY_MS = 100;
2247
+ var DETAILS_DELAY_MS = 150;
2248
+ var WARNING_COLOR = "#f59e0b";
2249
+ var GLYPH_COLOR = "#ffffff";
2250
+ var PRIORITY_KEYS = ["slot_date", "slot_time", "name", "email", "phone"];
2251
+ var MAX_DETAILS = 4;
2252
+ var useEnter = (delayMs = 0) => {
2253
+ const progress = (0, import_react9.useRef)(new import_react_native10.Animated.Value(0)).current;
2254
+ (0, import_react9.useEffect)(() => {
2255
+ import_react_native10.Animated.timing(progress, { toValue: 1, duration: ENTER_DURATION_MS, delay: delayMs, useNativeDriver: true }).start();
2256
+ }, [delayMs, progress]);
2257
+ return progress;
2258
+ };
2259
+ var usePop = () => {
2260
+ const scale = (0, import_react9.useRef)(new import_react_native10.Animated.Value(0)).current;
2261
+ (0, import_react9.useEffect)(() => {
2262
+ import_react_native10.Animated.spring(scale, { toValue: 1, stiffness: 400, damping: 15, mass: 1, useNativeDriver: true }).start();
2263
+ }, [scale]);
2264
+ return scale;
2265
+ };
2266
+ var formatValue = (value) => {
2267
+ if (Array.isArray(value)) return value.join(", ");
2268
+ if (typeof value === "boolean") return value ? "Yes" : "No";
2269
+ return String(value);
2270
+ };
2271
+ var getDetailIcon = (key) => {
2272
+ if (!key) return null;
2273
+ if (key.includes("date")) return import_lucide_react_native6.Calendar;
2274
+ if (key.includes("time")) return import_lucide_react_native6.Clock;
2275
+ if (key.includes("email")) return import_lucide_react_native6.Mail;
2276
+ if (key.includes("name")) return import_lucide_react_native6.User;
2277
+ return null;
2278
+ };
2279
+ var ConfirmationStep = ({ data, collectedData }) => {
2280
+ const widgetTheme = useWidgetTheme();
2281
+ const theme = mergeTheme({
2282
+ primaryColor: widgetTheme.primaryColor,
2283
+ primaryTextColor: widgetTheme.primaryTextColor,
2284
+ backgroundColor: widgetTheme.backgroundColor,
2285
+ surfaceColor: widgetTheme.surfaceColor,
2286
+ textColor: widgetTheme.textColor,
2287
+ mutedTextColor: widgetTheme.mutedTextColor,
2288
+ borderColor: widgetTheme.borderColor
2289
+ });
2290
+ const fontFamily = resolveFontFamily(theme.fontFamily);
2291
+ const cardProgress = useEnter();
2292
+ const glyphScale = usePop();
2293
+ const titleProgress = useEnter(TITLE_DELAY_MS);
2294
+ const detailsProgress = useEnter(DETAILS_DELAY_MS);
2295
+ const iconType = data.icon || "success";
2296
+ const getIconConfig = () => {
2297
+ switch (iconType) {
2298
+ case "success":
2299
+ return { icon: import_lucide_react_native6.Check, bgColor: theme.successColor };
2300
+ case "info":
2301
+ return { icon: import_lucide_react_native6.Info, bgColor: theme.primaryColor };
2302
+ case "warning":
2303
+ return { icon: import_lucide_react_native6.TriangleAlert, bgColor: WARNING_COLOR };
2304
+ case "error":
2305
+ return { icon: import_lucide_react_native6.CircleX, bgColor: theme.errorColor };
2306
+ default:
2307
+ return { icon: import_lucide_react_native6.Check, bgColor: theme.successColor };
2308
+ }
2309
+ };
2310
+ const { icon: Icon, bgColor } = getIconConfig();
2311
+ const displayDetails = data.details || [];
2312
+ const formattedCollectedData = collectedData ? Object.entries(collectedData).filter(([key]) => PRIORITY_KEYS.includes(key)).sort((a, b) => PRIORITY_KEYS.indexOf(a[0]) - PRIORITY_KEYS.indexOf(b[0])).map(([key, value]) => ({
2313
+ key,
2314
+ label: key.replace(/slot_/g, "").replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
2315
+ value: formatValue(value)
2316
+ })) : [];
2317
+ const detailsToShow = displayDetails.length > 0 ? displayDetails : formattedCollectedData;
2318
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native10.Animated.View, { style: [styles10.root, { opacity: cardProgress }], children: [
2319
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native10.View, { style: styles10.header, children: [
2320
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native10.Animated.View, { style: [styles10.glyph, { transform: [{ scale: glyphScale }] }], children: [
2321
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(GradientFill, { color: bgColor, style: import_react_native10.StyleSheet.absoluteFill }),
2322
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Icon, { size: 18, color: GLYPH_COLOR })
2323
+ ] }),
2324
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native10.Animated.View, { style: [styles10.headerText, { opacity: titleProgress, transform: [{ translateX: titleProgress.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) }] }], children: [
2325
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native10.Text, { style: [styles10.title, { color: theme.textColor, fontFamily }], children: data.title }),
2326
+ data.message ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native10.Text, { style: [styles10.message, { color: theme.mutedTextColor, fontFamily }], children: data.message }) : null
2327
+ ] })
2328
+ ] }),
2329
+ detailsToShow.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native10.Animated.View, { style: [styles10.details, { backgroundColor: theme.surfaceColor, opacity: detailsProgress, transform: [{ translateY: detailsProgress.interpolate({ inputRange: [0, 1], outputRange: [8, 0] }) }] }], children: detailsToShow.slice(0, MAX_DETAILS).map((detail, index) => {
2330
+ const DetailIcon = detail.key ? getDetailIcon(detail.key) : null;
2331
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react_native10.View, { style: [styles10.detailRow, index === 0 ? null : { borderTopWidth: import_react_native10.StyleSheet.hairlineWidth, borderTopColor: solidColor(theme.borderColor) }], children: [
2332
+ DetailIcon ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DetailIcon, { size: 14, color: solidColor(theme.mutedTextColor) }) : null,
2333
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native10.Text, { style: [styles10.detailLabel, { color: theme.mutedTextColor, fontFamily }], children: detail.label }),
2334
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react_native10.Text, { numberOfLines: 1, accessibilityLabel: detail.value, style: [styles10.detailValue, { color: theme.textColor, fontFamily }], children: detail.value })
2335
+ ] }, index);
2336
+ }) })
2337
+ ] });
2338
+ };
2339
+ var confirmation_step_default = ConfirmationStep;
2340
+ var styles10 = import_react_native10.StyleSheet.create({
2341
+ root: { paddingVertical: 8 },
2342
+ header: { flexDirection: "row", alignItems: "flex-start", gap: 12, marginBottom: 16 },
2343
+ glyph: { width: 36, height: 36, borderRadius: 9999, alignItems: "center", justifyContent: "center", flexShrink: 0, overflow: "hidden" },
2344
+ headerText: { flex: 1, paddingTop: 2 },
2345
+ title: { fontSize: 14, lineHeight: 17.5, fontWeight: "600" },
2346
+ message: { fontSize: 12, lineHeight: 19.5, marginTop: 4 },
2347
+ details: { borderRadius: 8, overflow: "hidden" },
2348
+ detailRow: { flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 12, paddingVertical: 10 },
2349
+ detailLabel: { fontSize: 12, lineHeight: 16, flexShrink: 0 },
2350
+ // `ml-auto` — pushes the value to the trailing edge, and lets it truncate
2351
+ // rather than shove the label out of the row.
2352
+ detailValue: { fontSize: 12, lineHeight: 16, fontWeight: "500", marginLeft: "auto", flexShrink: 1 }
2353
+ });
2354
+
2355
+ // src/components/steps/form-step.tsx
2356
+ var import_react10 = require("react");
2357
+ var import_react_native11 = require("react-native");
2358
+ var import_lucide_react_native7 = require("lucide-react-native");
2359
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2360
+ var SPIN_DURATION_MS3 = 1e3;
2361
+ var FIELD_RADIUS_FALLBACK = 8;
2362
+ var OPTION_HIT_SLOP = 3;
2363
+ var fieldIcons = {
2364
+ text: import_lucide_react_native7.Type,
2365
+ email: import_lucide_react_native7.Mail,
2366
+ tel: import_lucide_react_native7.Phone,
2367
+ number: import_lucide_react_native7.Type,
2368
+ select: import_lucide_react_native7.ChevronDown,
2369
+ multiselect: import_lucide_react_native7.List,
2370
+ textarea: import_lucide_react_native7.FileText,
2371
+ boolean: import_lucide_react_native7.SquareCheckBig,
2372
+ date: import_lucide_react_native7.Calendar
2373
+ };
2374
+ var useStepTheme2 = (componentTheme) => {
2375
+ const widgetTheme = useWidgetTheme();
2376
+ return (0, import_react10.useMemo)(
2377
+ () => mergeTheme({
2378
+ primaryColor: widgetTheme.primaryColor,
2379
+ primaryTextColor: widgetTheme.primaryTextColor,
2380
+ // Cards read as bordered surfaces sitting ON the panel, so they take
2381
+ // the widget background rather than the (greyer) surface colour.
2382
+ backgroundColor: widgetTheme.backgroundColor,
2383
+ surfaceColor: widgetTheme.surfaceColor,
2384
+ textColor: widgetTheme.textColor,
2385
+ mutedTextColor: widgetTheme.mutedTextColor,
2386
+ borderColor: widgetTheme.borderColor,
2387
+ errorColor: widgetTheme.errorColor,
2388
+ successColor: widgetTheme.successColor,
2389
+ buttonStyle: widgetTheme.buttonStyle,
2390
+ // Web inherits the family from the flow container; RN has no cascade,
2391
+ // so it travels through the theme and is applied per <Text>.
2392
+ fontFamily: widgetTheme.fontFamily,
2393
+ ...componentTheme
2394
+ }),
2395
+ [componentTheme, widgetTheme]
2396
+ );
2397
+ };
2398
+ var getOptionLabel = (option) => typeof option === "string" ? option : option.label;
2399
+ var getOptionValue = (option) => typeof option === "string" ? option : option.value;
2400
+ var placeholderFor = (field) => field.placeholder || `Enter ${field.label.toLowerCase()}`;
2401
+ var textInputTraits = (type) => {
2402
+ switch (type) {
2403
+ case "email":
2404
+ return { keyboardType: "email-address", autoCapitalize: "none", autoCorrect: false };
2405
+ case "tel":
2406
+ return { keyboardType: "phone-pad", autoCapitalize: "none", autoCorrect: false };
2407
+ // No picker without a dependency, so a date is typed. The punctuation
2408
+ // keyboard at least puts `-` and `/` on the first layer.
2409
+ case "date":
2410
+ return { keyboardType: "numbers-and-punctuation", autoCapitalize: "none", autoCorrect: false };
2411
+ default:
2412
+ return { keyboardType: "default", autoCapitalize: "sentences", autoCorrect: true };
2413
+ }
2414
+ };
2415
+ var inputSurface = (theme, hasError, isFocused) => ({
2416
+ backgroundColor: theme.backgroundColor,
2417
+ borderWidth: 1,
2418
+ // Web draws a 2px focus ring, which RN has no equivalent for, so the border
2419
+ // itself carries focus. An error outranks it — it is the more urgent signal.
2420
+ borderColor: hasError ? theme.errorColor : isFocused ? solidColor(theme.primaryColor) : theme.borderColor,
2421
+ // Fields follow the card radius, not the button radius — pill-shaped
2422
+ // textareas read badly.
2423
+ borderRadius: toNumber2(theme.borderRadius, FIELD_RADIUS_FALLBACK)
2424
+ });
2425
+ var validateField = (field, value) => {
2426
+ if (field.required) {
2427
+ if (value === void 0 || value === null || value === "") {
2428
+ return `${field.label} is required`;
2429
+ }
2430
+ if (Array.isArray(value) && value.length === 0) {
2431
+ return `${field.label} is required`;
2432
+ }
2433
+ if (typeof value === "string" && !value.trim()) {
2434
+ return `${field.label} is required`;
2435
+ }
2436
+ }
2437
+ if (typeof value === "string" && value) {
2438
+ if (field.validation) {
2439
+ if (field.validation.minLength && value.length < field.validation.minLength) {
2440
+ return field.validation.message || `${field.label} must be at least ${field.validation.minLength} characters`;
2441
+ }
2442
+ if (field.validation.maxLength && value.length > field.validation.maxLength) {
2443
+ return field.validation.message || `${field.label} must be at most ${field.validation.maxLength} characters`;
2444
+ }
2445
+ if (field.validation.pattern) {
2446
+ const regex = new RegExp(field.validation.pattern);
2447
+ if (!regex.test(value)) {
2448
+ return field.validation.message || `${field.label} is invalid`;
2449
+ }
2450
+ }
2451
+ }
2452
+ if (field.type === "email") {
2453
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2454
+ if (!emailRegex.test(value)) {
2455
+ return "Please enter a valid email address";
2456
+ }
2457
+ }
2458
+ if (field.type === "tel") {
2459
+ const phoneRegex = /^[\d\s\-+()]{7,}$/;
2460
+ if (!phoneRegex.test(value)) {
2461
+ return "Please enter a valid phone number";
2462
+ }
2463
+ }
2464
+ }
2465
+ return null;
2466
+ };
2467
+ var Spinner3 = ({ color }) => {
2468
+ const turn = (0, import_react10.useRef)(new import_react_native11.Animated.Value(0)).current;
2469
+ (0, import_react10.useEffect)(() => {
2470
+ const loop = import_react_native11.Animated.loop(import_react_native11.Animated.timing(turn, { toValue: 1, duration: SPIN_DURATION_MS3, easing: import_react_native11.Easing.linear, useNativeDriver: true }));
2471
+ loop.start();
2472
+ return () => loop.stop();
2473
+ }, [turn]);
2474
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Animated.View, { style: { transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] }) }] }, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react_native7.LoaderCircle, { size: 16, color }) });
2475
+ };
2476
+ var Checkbox = ({ checked, size, theme }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GradientFill, { color: checked ? theme.primaryColor : theme.backgroundColor, style: [styles11.checkbox, { width: size, height: size, borderColor: checked ? solidColor(theme.primaryColor) : theme.borderColor }], children: checked ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react_native7.Check, { size: size - 4, color: theme.primaryTextColor, strokeWidth: 3 }) : null });
2477
+ var OptionRow = ({ label, selected, muted = false, theme, fontFamily, onPress }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(Touchable, { style: styles11.optionRow, hitSlop: OPTION_HIT_SLOP, onPress, children: [
2478
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.optionLabel, { color: muted ? theme.mutedTextColor : theme.textColor, fontFamily }, selected ? styles11.optionLabelSelected : null], numberOfLines: 1, children: label }),
2479
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react_native7.Check, { size: 14, color: solidColor(theme.primaryColor) }) : null
2480
+ ] });
2481
+ var FieldControl = ({ field, value, theme, fontFamily, hasError, onChange, onToggleOption }) => {
2482
+ const [isFocused, setIsFocused] = (0, import_react10.useState)(false);
2483
+ const [numberText, setNumberText] = (0, import_react10.useState)(typeof value === "number" ? String(value) : "");
2484
+ const surface = inputSurface(theme, hasError, isFocused);
2485
+ const textStyle = [styles11.input, surface, { color: theme.textColor, fontFamily }];
2486
+ const focusHandlers = { onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false) };
2487
+ switch (field.type) {
2488
+ case "select": {
2489
+ if (!field.options) return null;
2490
+ const selected = typeof value === "string" ? value : "";
2491
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react_native11.View, { style: [styles11.optionList, surface], children: [
2492
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(OptionRow, { label: field.placeholder || `Select ${field.label.toLowerCase()}`, muted: true, selected: selected === "", theme, fontFamily, onPress: () => onChange("") }),
2493
+ field.options.map((option, idx) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(OptionRow, { label: getOptionLabel(option), selected: selected === getOptionValue(option), theme, fontFamily, onPress: () => onChange(getOptionValue(option)) }, idx))
2494
+ ] });
2495
+ }
2496
+ case "multiselect": {
2497
+ if (!field.options) return null;
2498
+ const chosen = Array.isArray(value) ? value : [];
2499
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.View, { style: [styles11.optionList, surface], children: field.options.map((option, idx) => {
2500
+ const optionValue = getOptionValue(option);
2501
+ const isChecked = chosen.includes(optionValue);
2502
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(Touchable, { style: styles11.checkRow, hitSlop: OPTION_HIT_SLOP, onPress: () => onToggleOption(optionValue, !isChecked), children: [
2503
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Checkbox, { checked: isChecked, size: 14, theme }),
2504
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.optionLabel, { color: theme.textColor, fontFamily }], children: getOptionLabel(option) })
2505
+ ] }, idx);
2506
+ }) });
2507
+ }
2508
+ case "boolean":
2509
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(Touchable, { style: styles11.booleanRow, onPress: () => onChange(value !== true), children: [
2510
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Checkbox, { checked: value === true, size: 16, theme }),
2511
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.optionLabel, { color: theme.mutedTextColor, fontFamily }], children: field.placeholder || "Yes" })
2512
+ ] });
2513
+ case "textarea":
2514
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2515
+ import_react_native11.TextInput,
2516
+ {
2517
+ ...focusHandlers,
2518
+ style: [textStyle, styles11.textarea],
2519
+ placeholder: placeholderFor(field),
2520
+ placeholderTextColor: theme.mutedTextColor,
2521
+ value: typeof value === "string" ? value : "",
2522
+ onChangeText: onChange,
2523
+ multiline: true,
2524
+ numberOfLines: 2
2525
+ }
2526
+ );
2527
+ case "number":
2528
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2529
+ import_react_native11.TextInput,
2530
+ {
2531
+ ...focusHandlers,
2532
+ style: textStyle,
2533
+ placeholder: placeholderFor(field),
2534
+ placeholderTextColor: theme.mutedTextColor,
2535
+ value: numberText,
2536
+ onChangeText: (text) => {
2537
+ setNumberText(text);
2538
+ const parsed = Number(text);
2539
+ onChange(text.trim() === "" || Number.isNaN(parsed) ? "" : parsed);
2540
+ },
2541
+ keyboardType: "numeric"
2542
+ }
2543
+ );
2544
+ default: {
2545
+ const traits = textInputTraits(field.type);
2546
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2547
+ import_react_native11.TextInput,
2548
+ {
2549
+ ...focusHandlers,
2550
+ style: textStyle,
2551
+ placeholder: placeholderFor(field),
2552
+ placeholderTextColor: theme.mutedTextColor,
2553
+ value: typeof value === "string" ? value : "",
2554
+ onChangeText: onChange,
2555
+ keyboardType: traits.keyboardType,
2556
+ autoCapitalize: traits.autoCapitalize,
2557
+ autoCorrect: traits.autoCorrect
2558
+ }
2559
+ );
2560
+ }
2561
+ }
2562
+ };
2563
+ var FormStep = ({ payload, isProcessing, onComplete }) => {
2564
+ const theme = useStepTheme2(payload.theme);
2565
+ const buttonRadius = getButtonRadius2(theme.buttonStyle);
2566
+ const fontFamily = resolveFontFamily(theme.fontFamily);
2567
+ const { data } = payload;
2568
+ const [formData, setFormData] = (0, import_react10.useState)({});
2569
+ const [fieldErrors, setFieldErrors] = (0, import_react10.useState)({});
2570
+ const clearFieldError = (fieldName) => {
2571
+ setFieldErrors((prev) => {
2572
+ if (!prev[fieldName]) return prev;
2573
+ const next = { ...prev };
2574
+ delete next[fieldName];
2575
+ return next;
2576
+ });
2577
+ };
2578
+ const validateAllFields = () => {
2579
+ const errors = {};
2580
+ let isValid = true;
2581
+ data.fields.forEach((field) => {
2582
+ const error = validateField(field, formData[field.name]);
2583
+ if (error) {
2584
+ errors[field.name] = error;
2585
+ isValid = false;
2586
+ }
2587
+ });
2588
+ setFieldErrors(errors);
2589
+ return isValid;
2590
+ };
2591
+ const handleFieldChange = (fieldName, value) => {
2592
+ setFormData((prev) => ({ ...prev, [fieldName]: value }));
2593
+ clearFieldError(fieldName);
2594
+ };
2595
+ const handleMultiselectChange = (fieldName, optionValue, checked) => {
2596
+ setFormData((prev) => {
2597
+ const currentValues = prev[fieldName] || [];
2598
+ if (checked) {
2599
+ return { ...prev, [fieldName]: [...currentValues, optionValue] };
2600
+ } else {
2601
+ return { ...prev, [fieldName]: currentValues.filter((v) => v !== optionValue) };
2602
+ }
2603
+ });
2604
+ clearFieldError(fieldName);
2605
+ };
2606
+ const handleSubmit = () => {
2607
+ if (validateAllFields()) {
2608
+ onComplete(formData);
2609
+ }
2610
+ };
2611
+ const submitTextColor = isProcessing ? theme.mutedTextColor : theme.primaryTextColor;
2612
+ return (
2613
+ // The form may be the tallest thing on screen in a host app, so it owns its
2614
+ // keyboard handling. With an unbounded parent the ScrollView simply lays
2615
+ // out at content height and never steals a scroll gesture from the chat.
2616
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.KeyboardAvoidingView, { behavior: import_react_native11.Platform.OS === "ios" ? "padding" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react_native11.ScrollView, { keyboardShouldPersistTaps: "handled", nestedScrollEnabled: true, bounces: false, showsVerticalScrollIndicator: false, children: [
2617
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react_native11.View, { style: styles11.header, children: [
2618
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react_native7.FileText, { size: 16, color: solidColor(theme.primaryColor) }),
2619
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.title, { color: theme.textColor, fontFamily }], children: data.title })
2620
+ ] }),
2621
+ data.description ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.description, { color: theme.mutedTextColor, fontFamily }], children: data.description }) : null,
2622
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.View, { style: styles11.fields, children: data.fields.map((field) => {
2623
+ const FieldIcon = fieldIcons[field.type] ?? import_lucide_react_native7.Type;
2624
+ const error = fieldErrors[field.name];
2625
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react_native11.View, { children: [
2626
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react_native11.View, { style: styles11.labelRow, children: [
2627
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(FieldIcon, { size: 12, color: theme.mutedTextColor }),
2628
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.label, { color: theme.mutedTextColor, fontFamily }], children: field.label }),
2629
+ field.required ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.label, { color: theme.errorColor, fontFamily }], children: "*" }) : null
2630
+ ] }),
2631
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2632
+ FieldControl,
2633
+ {
2634
+ field,
2635
+ value: formData[field.name],
2636
+ theme,
2637
+ fontFamily,
2638
+ hasError: !!error,
2639
+ onChange: (value) => handleFieldChange(field.name, value),
2640
+ onToggleOption: (optionValue, checked) => handleMultiselectChange(field.name, optionValue, checked)
2641
+ }
2642
+ ),
2643
+ error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.error, { color: theme.errorColor, fontFamily }], children: error }) : null
2644
+ ] }, field.name);
2645
+ }) }),
2646
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Touchable, { onPress: handleSubmit, disabled: isProcessing, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GradientFill, { color: isProcessing ? theme.borderColor : theme.primaryColor, style: [styles11.submitButton, { borderRadius: buttonRadius }], children: isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
2647
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Spinner3, { color: submitTextColor }),
2648
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.submitLabel, { color: submitTextColor, fontFamily }], children: "Submitting..." })
2649
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native11.Text, { style: [styles11.submitLabel, { color: submitTextColor, fontFamily }], children: data.submit_label || "Next" }) }) })
2650
+ ] }) })
2651
+ );
2652
+ };
2653
+ var styles11 = import_react_native11.StyleSheet.create({
2654
+ header: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
2655
+ title: { fontSize: 14, lineHeight: 20, fontWeight: "500" },
2656
+ description: { fontSize: 12, lineHeight: 16, marginBottom: 12 },
2657
+ fields: { gap: 10, marginBottom: 12 },
2658
+ labelRow: { flexDirection: "row", alignItems: "center", gap: 4, marginBottom: 4 },
2659
+ label: { fontSize: 12, lineHeight: 16, fontWeight: "500" },
2660
+ // No `lineHeight`: Android centres single-line input text off it.
2661
+ input: { fontSize: 12, paddingHorizontal: 12, paddingVertical: 8, minHeight: 34 },
2662
+ // Web's `rows={2}` at this type size: two 16px lines plus the padding.
2663
+ textarea: { minHeight: 50, textAlignVertical: "top" },
2664
+ optionList: { padding: 8, gap: 6 },
2665
+ optionRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 },
2666
+ optionLabel: { flexShrink: 1, fontSize: 12, lineHeight: 16 },
2667
+ optionLabelSelected: { fontWeight: "600" },
2668
+ checkRow: { flexDirection: "row", alignItems: "center", gap: 8 },
2669
+ checkbox: { borderRadius: 4, borderWidth: 1, alignItems: "center", justifyContent: "center" },
2670
+ booleanRow: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 4 },
2671
+ error: { fontSize: 12, lineHeight: 16, marginTop: 2 },
2672
+ submitButton: { height: 36, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 },
2673
+ submitLabel: { fontSize: 14, lineHeight: 20, fontWeight: "600" }
2674
+ });
2675
+ var form_step_default = FormStep;
2676
+
2677
+ // src/components/ui/swap.tsx
2678
+ var import_react11 = require("react");
2679
+ var import_react_native12 = require("react-native");
2680
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2681
+ var DEFAULT_DURATION_MS = 200;
2682
+ var SWAP_EASING = import_react_native12.Easing.inOut(import_react_native12.Easing.ease);
2683
+ var offsetsOf = (props) => ({ enterX: props.enterX ?? 0, enterY: props.enterY ?? 0, exitX: props.exitX ?? 0, exitY: props.exitY ?? 0 });
2684
+ var AnimatedSwap = ({ swapKey, durationMs = DEFAULT_DURATION_MS, children, ...offsets }) => {
2685
+ const progress = (0, import_react11.useRef)(new import_react_native12.Animated.Value(1)).current;
2686
+ const [held, setHeld] = (0, import_react11.useState)({ key: swapKey, node: children, ...offsetsOf(offsets) });
2687
+ const incoming = (0, import_react11.useRef)(held);
2688
+ incoming.current = { key: swapKey, node: children, ...offsetsOf(offsets) };
2689
+ const heldKey = (0, import_react11.useRef)(held.key);
2690
+ heldKey.current = held.key;
2691
+ (0, import_react11.useEffect)(() => {
2692
+ if (heldKey.current === swapKey) {
2693
+ setHeld(incoming.current);
2694
+ return;
2695
+ }
2696
+ const exit = import_react_native12.Animated.timing(progress, { toValue: 0, duration: durationMs, easing: SWAP_EASING, useNativeDriver: true });
2697
+ exit.start(({ finished }) => {
2698
+ if (!finished) return;
2699
+ setHeld(incoming.current);
2700
+ progress.setValue(0);
2701
+ import_react_native12.Animated.timing(progress, { toValue: 1, duration: durationMs, easing: SWAP_EASING, useNativeDriver: true }).start();
2702
+ });
2703
+ return () => exit.stop();
2704
+ }, [swapKey, durationMs, progress]);
2705
+ const translateX = progress.interpolate({ inputRange: [0, 1], outputRange: [held.key === swapKey ? held.enterX ?? 0 : held.exitX ?? 0, 0] });
2706
+ const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [held.key === swapKey ? held.enterY ?? 0 : held.exitY ?? 0, 0] });
2707
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_react_native12.Animated.View, { style: { opacity: progress, transform: [{ translateX }, { translateY }] }, children: held.node });
2708
+ };
2709
+
2710
+ // src/components/interactive-flow.tsx
2711
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2712
+ var ENTER_DURATION_MS2 = 300;
2713
+ var STEP_ENTER_DURATION_MS = 200;
2714
+ var CARD_ENTER_OFFSET = 10;
2715
+ var STEP_ENTER_OFFSET = 20;
2716
+ var PILL_ENTER_OFFSET = 8;
2717
+ var SURFACE_TINT_OPACITY = 0.5;
2718
+ var DEFAULT_CARD_RADIUS = 8;
2719
+ var openExternalUrl = (url) => {
2720
+ void import_react_native13.Linking.openURL(url).catch(() => {
2721
+ });
2722
+ };
2723
+ var EnterFade = ({ translateX = 0, translateY = 0, durationMs = ENTER_DURATION_MS2, style, testID, children }) => {
2724
+ const progress = (0, import_react12.useRef)(new import_react_native13.Animated.Value(0)).current;
2725
+ (0, import_react12.useEffect)(() => {
2726
+ import_react_native13.Animated.timing(progress, { toValue: 1, duration: durationMs, useNativeDriver: true }).start();
2727
+ }, [durationMs, progress]);
2728
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2729
+ import_react_native13.Animated.View,
2730
+ {
2731
+ testID,
2732
+ style: [
2733
+ style,
2734
+ {
2735
+ opacity: progress,
2736
+ transform: [
2737
+ { translateX: progress.interpolate({ inputRange: [0, 1], outputRange: [translateX, 0] }) },
2738
+ { translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }) }
2739
+ ]
2740
+ }
2741
+ ],
2742
+ children
2743
+ }
2744
+ );
2745
+ };
2746
+ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse, onSelectionDisplayed, quickReplyAlign = "end" }) => {
2747
+ const isMultiStepFlow = isInteractiveFlow(initialComponent);
2748
+ const isFreeButtonGroup = !isMultiStepFlow && isButtons(initialComponent) && initialComponent.data.presentation === "free";
2749
+ const widgetTheme = useWidgetTheme();
2750
+ const theme = (0, import_react12.useMemo)(
2751
+ () => mergeTheme({
2752
+ primaryColor: widgetTheme.primaryColor,
2753
+ primaryTextColor: widgetTheme.primaryTextColor,
2754
+ backgroundColor: widgetTheme.backgroundColor,
2755
+ // Cards sit on the panel as bordered white surfaces
2756
+ surfaceColor: widgetTheme.surfaceColor,
2757
+ textColor: widgetTheme.textColor,
2758
+ mutedTextColor: widgetTheme.mutedTextColor,
2759
+ borderColor: widgetTheme.borderColor,
2760
+ ...initialComponent.theme
2761
+ }),
2762
+ [initialComponent.theme, widgetTheme]
2763
+ );
2764
+ const fontFamily = resolveFontFamily(theme.fontFamily);
2765
+ const flowSteps = isMultiStepFlow ? initialComponent.data.steps : [{ id: "single", title: "", component: initialComponent }];
2766
+ const totalSteps = flowSteps.length;
2767
+ const [currentStepIndex, setCurrentStepIndex] = (0, import_react12.useState)(0);
2768
+ const [isProcessing, setIsProcessing] = (0, import_react12.useState)(false);
2769
+ const [allCollectedData, setAllCollectedData] = (0, import_react12.useState)({});
2770
+ const [confirmation, setConfirmation] = (0, import_react12.useState)(null);
2771
+ const [submittedSelection, setSubmittedSelection] = (0, import_react12.useState)(null);
2772
+ const [stepCompleted, setStepCompleted] = (0, import_react12.useState)(new Array(totalSteps).fill(false));
2773
+ const currentStep = flowSteps[currentStepIndex];
2774
+ const currentComponent = currentStep?.component;
2775
+ const showStepper = totalSteps > 1;
2776
+ const handleBack = (0, import_react12.useCallback)(() => {
2777
+ if (currentStepIndex > 0) {
2778
+ setCurrentStepIndex((prev) => prev - 1);
2779
+ }
2780
+ }, [currentStepIndex]);
2781
+ const handleStepComplete = (0, import_react12.useCallback)(
2782
+ async (stepData) => {
2783
+ setIsProcessing(true);
2784
+ const newAllData = { ...allCollectedData, ...stepData };
2785
+ setAllCollectedData(newAllData);
2786
+ setStepCompleted((prev) => {
2787
+ const updated = [...prev];
2788
+ updated[currentStepIndex] = true;
2789
+ return updated;
2790
+ });
2791
+ try {
2792
+ const isLastStep = currentStepIndex === totalSteps - 1;
2793
+ const selectedButtonId = typeof stepData.selected_button === "string" ? stepData.selected_button : null;
2794
+ const buttonsComponent = currentComponent !== void 0 && isButtons(currentComponent) ? currentComponent : null;
2795
+ const submittedButton = buttonsComponent && selectedButtonId ? buttonsComponent.data.buttons.find((button) => button.id === selectedButtonId) ?? { id: selectedButtonId, label: selectedButtonId, action: { type: "trigger_component" } } : null;
2796
+ const shouldShowSubmittedSelection = buttonsComponent?.data.completionDisplay === "selected_item" && !!submittedButton;
2797
+ if (onSendResponse) {
2798
+ const isInstantSelectedItem = isLastStep && shouldShowSubmittedSelection && !!submittedButton && !!onSelectionDisplayed;
2799
+ if (isInstantSelectedItem) {
2800
+ onSelectionDisplayed(initialComponent.component_id, submittedButton.label);
2801
+ onComplete?.(newAllData);
2802
+ }
2803
+ await onSendResponse(initialComponent.component_id, {
2804
+ step_index: currentStepIndex,
2805
+ step_data: stepData,
2806
+ all_data: newAllData,
2807
+ is_final_step: isLastStep
2808
+ });
2809
+ if (isLastStep) {
2810
+ if (isInstantSelectedItem) {
2811
+ } else if (shouldShowSubmittedSelection && submittedButton) {
2812
+ setSubmittedSelection(submittedButton.label);
2813
+ onComplete?.(newAllData);
2814
+ } else {
2815
+ setConfirmation({
2816
+ title: "Success!",
2817
+ message: "Your selection has been submitted.",
2818
+ icon: "success"
2819
+ });
2820
+ onComplete?.(newAllData);
2821
+ }
2822
+ } else {
2823
+ setCurrentStepIndex((prev) => prev + 1);
2824
+ }
2825
+ } else {
2826
+ if (isLastStep) {
2827
+ if (isMultiStepFlow) {
2828
+ const { on_complete } = initialComponent.data;
2829
+ const stringifiedData = {};
2830
+ for (const [key, value] of Object.entries(newAllData)) {
2831
+ stringifiedData[key] = Array.isArray(value) ? JSON.stringify(value) : String(value);
2832
+ }
2833
+ const apiPayload = buildPayload(on_complete.payload, {
2834
+ ...stringifiedData,
2835
+ all_data: JSON.stringify(newAllData)
2836
+ });
2837
+ const result = await onApiCall(on_complete.method, on_complete.endpoint, apiPayload);
2838
+ if (result.success) {
2839
+ setConfirmation(
2840
+ result.confirmation || {
2841
+ title: "Success!",
2842
+ message: result.message,
2843
+ icon: "success"
2844
+ }
2845
+ );
2846
+ onComplete?.(newAllData);
2847
+ } else {
2848
+ throw new Error(result.message || "Failed to complete");
2849
+ }
2850
+ } else {
2851
+ onComplete?.(newAllData);
2852
+ }
2853
+ } else {
2854
+ setCurrentStepIndex((prev) => prev + 1);
2855
+ }
2856
+ }
2857
+ } catch (error) {
2858
+ console.error("Error processing step:", error);
2859
+ setConfirmation({
2860
+ title: "Something went wrong",
2861
+ message: error.message || "Please try again.",
2862
+ icon: "error"
2863
+ });
2864
+ } finally {
2865
+ setIsProcessing(false);
2866
+ }
2867
+ },
2868
+ [allCollectedData, currentStepIndex, totalSteps, isMultiStepFlow, initialComponent, currentComponent, onApiCall, onComplete, onSendResponse, onSelectionDisplayed]
2869
+ );
2870
+ const handleButtonClick = (0, import_react12.useCallback)(
2871
+ async (buttonId, action) => {
2872
+ if (action.type === "navigate" && action.url) {
2873
+ openExternalUrl(action.url);
2874
+ return;
2875
+ }
2876
+ if (onSendResponse) {
2877
+ handleStepComplete({ selected_button: buttonId });
2878
+ return;
2879
+ }
2880
+ if (action.type === "api_call" && action.endpoint) {
2881
+ setIsProcessing(true);
2882
+ try {
2883
+ const result = await onApiCall(action.method || "POST", action.endpoint, action.payload || {});
2884
+ if (result.success) {
2885
+ handleStepComplete({ selected_button: buttonId });
2886
+ }
2887
+ } catch (error) {
2888
+ console.error("Error in button action:", error);
2889
+ } finally {
2890
+ setIsProcessing(false);
2891
+ }
2892
+ } else {
2893
+ handleStepComplete({ selected_button: buttonId });
2894
+ }
2895
+ },
2896
+ [onApiCall, handleStepComplete, onSendResponse]
2897
+ );
2898
+ if (!currentStep) return null;
2899
+ const renderStepComponent = (component) => {
2900
+ switch (component.component_type) {
2901
+ case "calendar_booking":
2902
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2903
+ calendar_step_default,
2904
+ {
2905
+ payload: component,
2906
+ isProcessing,
2907
+ onComplete: (slotId, slotDatetime) => {
2908
+ const timezone = component.data.timezone;
2909
+ handleStepComplete({
2910
+ slot_id: slotId,
2911
+ slot_datetime: slotDatetime,
2912
+ slot_time: formatSlotTime(slotDatetime, timezone),
2913
+ slot_date: formatSlotDate(slotDatetime, timezone)
2914
+ });
2915
+ }
2916
+ }
2917
+ );
2918
+ case "form":
2919
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(form_step_default, { payload: component, isProcessing, onComplete: (formData) => handleStepComplete(formData) });
2920
+ case "buttons":
2921
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(buttons_step_default, { payload: component, isProcessing, align: quickReplyAlign, onButtonClick: handleButtonClick });
2922
+ case "confirmation":
2923
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(confirmation_step_default, { data: component.data });
2924
+ // Block Kit payloads reach the blocks renderer, never this router — web
2925
+ // has no branch for them either.
2926
+ case "blocks":
2927
+ default:
2928
+ return null;
2929
+ }
2930
+ };
2931
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2932
+ EnterFade,
2933
+ {
2934
+ testID: ringgId("interactive-flow"),
2935
+ translateY: CARD_ENTER_OFFSET,
2936
+ style: [
2937
+ styles12.root,
2938
+ isFreeButtonGroup ? null : [
2939
+ styles12.card,
2940
+ {
2941
+ backgroundColor: solidColor(theme.backgroundColor),
2942
+ borderColor: solidColor(theme.borderColor),
2943
+ borderRadius: toNumber2(theme.borderRadius, DEFAULT_CARD_RADIUS)
2944
+ }
2945
+ ]
2946
+ ],
2947
+ children: [
2948
+ isFreeButtonGroup ? null : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.View, { pointerEvents: "none", style: [import_react_native13.StyleSheet.absoluteFill, styles12.surfaceTint, { backgroundColor: solidColor(theme.surfaceColor) }] }),
2949
+ showStepper && !confirmation && !submittedSelection && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native13.View, { testID: ringgId("flow-stepper"), style: styles12.stepper, children: [
2950
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.View, { testID: ringgId("flow-stepper-progress"), style: styles12.stepperProgress, children: flowSteps.map((step, index) => {
2951
+ const isCompleted = stepCompleted[index] ?? false;
2952
+ const isActive = index === currentStepIndex;
2953
+ const indicatorColor = isCompleted ? theme.successColor : isActive ? theme.primaryColor : theme.borderColor;
2954
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native13.View, { testID: ringgId("flow-step"), style: styles12.step, children: [
2955
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native13.View, { testID: ringgId("flow-step-indicator"), accessibilityState: { selected: isActive }, style: styles12.stepIndicator, children: [
2956
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(GradientFill, { color: indicatorColor, style: import_react_native13.StyleSheet.absoluteFill }),
2957
+ isCompleted ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react_native8.Check, { size: 14, color: solidColor(theme.primaryTextColor) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.Text, { style: [styles12.stepIndicatorLabel, { color: isActive ? theme.primaryTextColor : theme.mutedTextColor, fontFamily }], children: index + 1 })
2958
+ ] }),
2959
+ index < flowSteps.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.View, { testID: ringgId("flow-step-connector"), style: [styles12.stepConnector, { backgroundColor: solidColor(isCompleted ? theme.successColor : theme.borderColor) }] })
2960
+ ] }, step.id);
2961
+ }) }),
2962
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.Text, { testID: ringgId("flow-step-title"), numberOfLines: 1, style: [styles12.stepTitle, { color: theme.textColor, fontFamily }], children: currentStep.title || `Step ${currentStepIndex + 1} of ${totalSteps}` })
2963
+ ] }),
2964
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native13.View, { testID: ringgId("flow-content"), style: [isFreeButtonGroup ? null : styles12.content, !showStepper && !isFreeButtonGroup ? styles12.contentTopPadding : null], children: [
2965
+ currentStepIndex > 0 && !confirmation && !submittedSelection && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(Touchable, { onPress: handleBack, testID: ringgId("flow-back-button"), accessibilityLabel: "Back", style: styles12.backButton, children: [
2966
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react_native8.ChevronLeft, { size: 14, color: solidColor(theme.mutedTextColor) }),
2967
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.Text, { style: [styles12.backLabel, { color: theme.mutedTextColor, fontFamily }], children: "Back" })
2968
+ ] }),
2969
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2970
+ AnimatedSwap,
2971
+ {
2972
+ swapKey: submittedSelection ? "submitted" : confirmation ? "confirmation" : `step-${currentStepIndex}`,
2973
+ durationMs: STEP_ENTER_DURATION_MS,
2974
+ enterX: submittedSelection || confirmation ? 0 : STEP_ENTER_OFFSET,
2975
+ exitX: submittedSelection || confirmation ? 0 : -STEP_ENTER_OFFSET,
2976
+ enterY: submittedSelection ? PILL_ENTER_OFFSET : 0,
2977
+ exitY: submittedSelection ? -PILL_ENTER_OFFSET : 0,
2978
+ children: submittedSelection ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.View, { testID: ringgId("flow-submitted-selection"), style: styles12.submittedSelection, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react_native13.View, { testID: ringgId("flow-submitted-pill"), style: [styles12.submittedPill, { borderColor: solidColor(theme.primaryColor) }], children: [
2979
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(GradientFill, { color: theme.primaryColor, style: import_react_native13.StyleSheet.absoluteFill }),
2980
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react_native8.Check, { size: 16, color: solidColor(theme.primaryTextColor) }),
2981
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native13.Text, { numberOfLines: 1, accessibilityLabel: submittedSelection, style: [styles12.submittedLabel, { color: theme.primaryTextColor, fontFamily }], children: submittedSelection })
2982
+ ] }) }) : confirmation ? (
2983
+ /* Show Confirmation */
2984
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(confirmation_step_default, { data: confirmation, collectedData: allCollectedData })
2985
+ ) : renderStepComponent(currentStep.component)
2986
+ }
2987
+ )
2988
+ ] })
2989
+ ]
2990
+ }
2991
+ );
2992
+ };
2993
+ var interactive_flow_default = InteractiveFlow;
2994
+ var styles12 = import_react_native13.StyleSheet.create({
2995
+ root: { width: "100%" },
2996
+ card: { borderWidth: 1, overflow: "hidden" },
2997
+ surfaceTint: { opacity: SURFACE_TINT_OPACITY },
2998
+ stepper: { paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 },
2999
+ stepperProgress: { flexDirection: "row", alignItems: "center", gap: 8 },
3000
+ step: { flexDirection: "row", alignItems: "center", gap: 8, flex: 1 },
3001
+ stepIndicator: { width: 28, height: 28, borderRadius: 9999, alignItems: "center", justifyContent: "center", flexShrink: 0, overflow: "hidden" },
3002
+ stepIndicatorLabel: { fontSize: 12, lineHeight: 16, fontWeight: "600" },
3003
+ stepConnector: { flex: 1, height: 2, borderRadius: 9999 },
3004
+ stepTitle: { fontSize: 12, lineHeight: 16, fontWeight: "500", marginTop: 12 },
3005
+ content: { paddingHorizontal: 16, paddingBottom: 16 },
3006
+ contentTopPadding: { paddingTop: 16 },
3007
+ // `alignSelf` keeps the touch target on the label: a stretched Pressable
3008
+ // would make the whole width of the card tappable, which the <button> is not.
3009
+ backButton: { alignSelf: "flex-start", flexDirection: "row", alignItems: "center", gap: 4, marginBottom: 12 },
3010
+ backLabel: { fontSize: 12, lineHeight: 16 },
3011
+ submittedSelection: { paddingVertical: 8 },
3012
+ submittedPill: { alignSelf: "flex-start", maxWidth: "100%", flexDirection: "row", alignItems: "center", gap: 8, borderRadius: 10, borderWidth: 1, paddingHorizontal: 12, paddingVertical: 8, overflow: "hidden" },
3013
+ submittedLabel: { flexShrink: 1, fontSize: 14, lineHeight: 20, fontWeight: "600" }
3014
+ });
3015
+
3016
+ // src/components/message-item.tsx
3017
+ var import_react_native15 = require("react-native");
3018
+
3019
+ // src/lib/markdown.tsx
3020
+ var import_react13 = require("react");
3021
+ var import_react_native14 = require("react-native");
3022
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3023
+ var DEFAULT_FONT_SIZE = 15;
3024
+ var LINE_HEIGHT_RATIO = 1.3;
3025
+ var CODE_FONT_RATIO = 0.8;
3026
+ var BLOCK_GAP = 8;
3027
+ var LIST_ITEM_GAP = 4;
3028
+ var BULLET = "\u2022";
3029
+ var MONOSPACE_FONT = import_react_native14.Platform.select({ ios: "Menlo", default: "monospace" });
3030
+ var FENCE_PATTERN = /^\s{0,3}(`{3,}|~{3,})/;
3031
+ var HEADING_PATTERN = /^\s{0,3}(#{1,6})\s+(.*)$/;
3032
+ var RULE_PATTERN = /^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/;
3033
+ var QUOTE_PATTERN = /^\s{0,3}>\s?(.*)$/;
3034
+ var BULLET_PATTERN = /^(\s*)[-*+]\s+(.*)$/;
3035
+ var ORDERED_PATTERN = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
3036
+ var INLINE_PATTERN = /\\([\\`*_[\]()#+\-.!>])|`([^`]+)`|(\*{1,3}|_{1,3})(\S[\s\S]*?\S|\S)\3|(!?)\[([^\]]*)\]\(\s*<?([^)>\s]*)>?(?:\s+"[^"]*")?\s*\)/;
3037
+ var HEADING_SCALE = {
3038
+ // 20 / 18 / 16 / 15 against the 15px body — prose-sm's scale, and the same
3039
+ // steps the Flutter widget uses. h4 and below share the body size.
3040
+ 1: { sizeDelta: 5, fontWeight: "700" },
3041
+ 2: { sizeDelta: 3, fontWeight: "700" },
3042
+ 3: { sizeDelta: 1, fontWeight: "600" },
3043
+ 4: { sizeDelta: 0, fontWeight: "600" }
3044
+ };
3045
+ var isWordChar = (char) => char !== void 0 && /[A-Za-z0-9]/.test(char);
3046
+ var emphasis = (delimiters, children) => {
3047
+ switch (delimiters) {
3048
+ case 1:
3049
+ return { kind: "em", children };
3050
+ case 2:
3051
+ return { kind: "strong", children };
3052
+ default:
3053
+ return { kind: "strong", children: [{ kind: "em", children }] };
3054
+ }
3055
+ };
3056
+ var headingLevel = (hashes) => {
3057
+ switch (hashes.length) {
3058
+ case 1:
3059
+ return 1;
3060
+ case 2:
3061
+ return 2;
3062
+ case 3:
3063
+ return 3;
3064
+ default:
3065
+ return 4;
3066
+ }
3067
+ };
3068
+ var openUrl = (url) => {
3069
+ void import_react_native14.Linking.openURL(url).catch(() => {
3070
+ });
3071
+ };
3072
+ var tokenizeInline = (source) => {
3073
+ const tokens = [];
3074
+ const pattern = new RegExp(INLINE_PATTERN.source, "g");
3075
+ let cursor = 0;
3076
+ const pushText = (value) => {
3077
+ if (value) tokens.push({ kind: "text", value });
3078
+ };
3079
+ let match = pattern.exec(source);
3080
+ while (match !== null) {
3081
+ pushText(source.slice(cursor, match.index));
3082
+ cursor = match.index + match[0].length;
3083
+ const escaped = match[1];
3084
+ const code = match[2];
3085
+ const delimiter = match[3];
3086
+ const label = match[6];
3087
+ if (escaped !== void 0) {
3088
+ pushText(escaped);
3089
+ } else if (code !== void 0) {
3090
+ tokens.push({ kind: "code", value: code });
3091
+ } else if (delimiter !== void 0) {
3092
+ if (delimiter.startsWith("_") && isWordChar(source[match.index - 1])) pushText(match[0]);
3093
+ else tokens.push(emphasis(delimiter.length, tokenizeInline(match[4] ?? "")));
3094
+ } else if (label !== void 0) {
3095
+ const href = match[7] ?? "";
3096
+ if (match[5] === "!" || !href || !label) pushText(label);
3097
+ else tokens.push({ kind: "link", href, children: tokenizeInline(label) });
3098
+ }
3099
+ match = pattern.exec(source);
3100
+ }
3101
+ pushText(source.slice(cursor));
3102
+ return tokens;
3103
+ };
3104
+ var startsBlock = (line) => FENCE_PATTERN.test(line) || HEADING_PATTERN.test(line) || RULE_PATTERN.test(line) || QUOTE_PATTERN.test(line) || BULLET_PATTERN.test(line) || ORDERED_PATTERN.test(line);
3105
+ var splitBlocks = (source) => {
3106
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
3107
+ const blocks = [];
3108
+ let index = 0;
3109
+ while (index < lines.length) {
3110
+ const line = lines[index] ?? "";
3111
+ if (!line.trim()) {
3112
+ index += 1;
3113
+ continue;
3114
+ }
3115
+ const fence = FENCE_PATTERN.exec(line);
3116
+ if (fence !== null) {
3117
+ const marker = fence[1] ?? "```";
3118
+ const body = [];
3119
+ index += 1;
3120
+ while (index < lines.length) {
3121
+ const next = lines[index] ?? "";
3122
+ index += 1;
3123
+ if (next.trimStart().startsWith(marker)) break;
3124
+ body.push(next);
3125
+ }
3126
+ blocks.push({ kind: "code", text: body.join("\n") });
3127
+ continue;
3128
+ }
3129
+ const heading = HEADING_PATTERN.exec(line);
3130
+ if (heading !== null) {
3131
+ blocks.push({ kind: "heading", level: headingLevel(heading[1] ?? ""), content: tokenizeInline((heading[2] ?? "").trim()) });
3132
+ index += 1;
3133
+ continue;
3134
+ }
3135
+ if (RULE_PATTERN.test(line)) {
3136
+ blocks.push({ kind: "rule" });
3137
+ index += 1;
3138
+ continue;
3139
+ }
3140
+ if (QUOTE_PATTERN.test(line)) {
3141
+ const quoted = [];
3142
+ while (index < lines.length) {
3143
+ const marker = QUOTE_PATTERN.exec(lines[index] ?? "");
3144
+ if (marker === null) break;
3145
+ quoted.push(marker[1] ?? "");
3146
+ index += 1;
3147
+ }
3148
+ blocks.push({ kind: "quote", children: splitBlocks(quoted.join("\n")) });
3149
+ continue;
3150
+ }
3151
+ const bullet = BULLET_PATTERN.exec(line);
3152
+ const numbered = bullet === null ? ORDERED_PATTERN.exec(line) : null;
3153
+ if (bullet !== null || numbered !== null) {
3154
+ const ordered = numbered !== null;
3155
+ const start = numbered === null ? 1 : Number(numbered[2] ?? "1");
3156
+ const items = [];
3157
+ while (index < lines.length) {
3158
+ const current = lines[index] ?? "";
3159
+ const item = ordered ? ORDERED_PATTERN.exec(current) : BULLET_PATTERN.exec(current);
3160
+ if (item !== null) {
3161
+ items.push((ordered ? item[3] : item[2]) ?? "");
3162
+ index += 1;
3163
+ continue;
3164
+ }
3165
+ const lastIndex = items.length - 1;
3166
+ const last = items[lastIndex];
3167
+ if (last !== void 0 && current.trim() && !startsBlock(current)) {
3168
+ items[lastIndex] = `${last} ${current.trim()}`;
3169
+ index += 1;
3170
+ continue;
3171
+ }
3172
+ break;
3173
+ }
3174
+ blocks.push({ kind: "list", ordered, start, items: items.map((item) => tokenizeInline(item)) });
3175
+ continue;
3176
+ }
3177
+ const paragraph = [];
3178
+ while (index < lines.length) {
3179
+ const current = lines[index] ?? "";
3180
+ if (!current.trim()) break;
3181
+ if (paragraph.length > 0 && startsBlock(current)) break;
3182
+ paragraph.push(current.trim());
3183
+ index += 1;
3184
+ }
3185
+ blocks.push({ kind: "paragraph", content: tokenizeInline(paragraph.join(" ")) });
3186
+ }
3187
+ return blocks;
3188
+ };
3189
+ var renderInline = (tokens, theme) => tokens.map((token, index) => {
3190
+ const key = `${token.kind}-${index}`;
3191
+ switch (token.kind) {
3192
+ case "text":
3193
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react13.Fragment, { children: token.value }, key);
3194
+ case "code":
3195
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: [styles13.code, { fontFamily: MONOSPACE_FONT, fontSize: theme.codeFontSize }], children: token.value }, key);
3196
+ case "strong":
3197
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: styles13.strong, children: renderInline(token.children, theme) }, key);
3198
+ case "em":
3199
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: styles13.em, children: renderInline(token.children, theme) }, key);
3200
+ case "link":
3201
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { accessibilityRole: "link", style: [styles13.link, { color: theme.linkColor }], onPress: () => openUrl(token.href), children: renderInline(token.children, theme) }, key);
3202
+ default:
3203
+ return null;
3204
+ }
3205
+ });
3206
+ var Blocks = ({ blocks, theme }) => {
3207
+ const body = { color: theme.color, fontSize: theme.fontSize, lineHeight: theme.lineHeight, fontFamily: theme.fontFamily };
3208
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.View, { style: styles13.stack, children: blocks.map((block, index) => {
3209
+ const key = `${block.kind}-${index}`;
3210
+ switch (block.kind) {
3211
+ case "paragraph":
3212
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: body, children: renderInline(block.content, theme) }, key);
3213
+ case "heading": {
3214
+ const scale = HEADING_SCALE[block.level];
3215
+ const fontSize = theme.fontSize + scale.sizeDelta;
3216
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: [body, { fontSize, lineHeight: fontSize * LINE_HEIGHT_RATIO, fontWeight: scale.fontWeight }], children: renderInline(block.content, theme) }, key);
3217
+ }
3218
+ case "code":
3219
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.View, { style: [styles13.codeBlock, { borderColor: theme.mutedColor }], children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: { color: theme.color, fontFamily: MONOSPACE_FONT, fontSize: theme.codeFontSize, lineHeight: theme.codeFontSize * LINE_HEIGHT_RATIO }, children: block.text }) }, key);
3220
+ case "quote":
3221
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.View, { style: [styles13.quote, { borderLeftColor: theme.mutedColor }], children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Blocks, { blocks: block.children, theme }) }, key);
3222
+ case "list":
3223
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.View, { style: styles13.list, children: block.items.map((item, itemIndex) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native14.View, { style: styles13.listItem, children: [
3224
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: body, children: block.ordered ? `${block.start + itemIndex}.` : BULLET }),
3225
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.Text, { style: [body, styles13.listItemText], children: renderInline(item, theme) })
3226
+ ] }, `item-${itemIndex}`)) }, key);
3227
+ case "rule":
3228
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native14.View, { style: [styles13.rule, { backgroundColor: theme.mutedColor }] }, key);
3229
+ default:
3230
+ return null;
3231
+ }
3232
+ }) });
3233
+ };
3234
+ var Markdown = ({ content, color, mutedColor, linkColor, fontSize = DEFAULT_FONT_SIZE, fontFamily }) => {
3235
+ const blocks = (0, import_react13.useMemo)(() => splitBlocks(content), [content]);
3236
+ const theme = (0, import_react13.useMemo)(
3237
+ () => ({ color, mutedColor, linkColor, fontSize, lineHeight: fontSize * LINE_HEIGHT_RATIO, codeFontSize: Math.round(fontSize * CODE_FONT_RATIO), fontFamily }),
3238
+ [color, mutedColor, linkColor, fontSize, fontFamily]
3239
+ );
3240
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Blocks, { blocks, theme });
3241
+ };
3242
+ var styles13 = import_react_native14.StyleSheet.create({
3243
+ stack: { gap: BLOCK_GAP },
3244
+ strong: { fontWeight: "600" },
3245
+ em: { fontStyle: "italic" },
3246
+ /** prose renders inline code semibold, which is what separates it from prose text. */
3247
+ code: { fontWeight: "600" },
3248
+ link: { textDecorationLine: "underline" },
3249
+ codeBlock: { borderWidth: import_react_native14.StyleSheet.hairlineWidth, borderRadius: 4, paddingHorizontal: 8, paddingVertical: 6 },
3250
+ quote: { borderLeftWidth: 4, paddingLeft: 12, paddingVertical: 4 },
3251
+ list: { gap: LIST_ITEM_GAP },
3252
+ listItem: { flexDirection: "row", gap: 6 },
3253
+ // Shrink rather than grow: the bubble hugs its content, and `flex: 1` would
3254
+ // collapse that intrinsic width to zero.
3255
+ listItemText: { flexShrink: 1 },
3256
+ rule: { height: import_react_native14.StyleSheet.hairlineWidth, alignSelf: "stretch" }
3257
+ });
3258
+
3259
+ // src/components/message-item.tsx
3260
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3261
+ var BODY_FONT_SIZE = 15;
3262
+ var LINE_HEIGHT_RATIO2 = 1.3;
3263
+ var SOURCE_FONT_SIZE = 13;
3264
+ var BUBBLE_RADIUS = 12;
3265
+ var ANCHORED_RADIUS = 2;
3266
+ var BUBBLE_PADDING_LEFT = 12;
3267
+ var BUBBLE_PADDING_RIGHT = 8;
3268
+ var BUBBLE_PADDING_VERTICAL = 8;
3269
+ var CONTENT_GAP = 10;
3270
+ var MessageItem = ({ message, children, hideMessageContent = false }) => {
3271
+ const theme = useWidgetTheme();
3272
+ const isAgentMessage = !message.isSelf;
3273
+ const agentBubble = theme.agentBubbleColor.trim();
3274
+ const bubbled = !isAgentMessage || Boolean(agentBubble);
3275
+ const shouldRenderMessageBody = !hideMessageContent && Boolean(message.message.trim());
3276
+ const sourceUrl = message.sourceUrl;
3277
+ const fontFamily = resolveFontFamily(theme.fontFamily);
3278
+ const body = isAgentMessage ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native15.View, { testID: ringgId("message-body"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3279
+ Markdown,
3280
+ {
3281
+ content: message.message,
3282
+ color: theme.textColor,
3283
+ mutedColor: theme.mutedTextColor,
3284
+ linkColor: solidColor(theme.primaryColor),
3285
+ fontSize: BODY_FONT_SIZE,
3286
+ fontFamily
3287
+ }
3288
+ ) }) : (
3289
+ // Self messages are an echo of what the user typed; running them through
3290
+ // markdown would reformat their own text back at them.
3291
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native15.Text, { testID: ringgId("message-body"), style: [styles14.body, { color: theme.textColor, fontFamily }], children: message.message })
3292
+ );
3293
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native15.View, { style: isAgentMessage ? styles14.rowStart : styles14.rowEnd, testID: ringgId("message"), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native15.View, { testID: ringgId("message-bubble"), style: [styles14.bubble, bubbled ? [styles14.bubbleBox, isAgentMessage ? styles14.agentCorners : styles14.userCorners] : null], children: [
3294
+ bubbled ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(GradientFill, { color: isAgentMessage ? agentBubble : theme.surfaceColor, style: import_react_native15.StyleSheet.absoluteFill }) : null,
3295
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native15.View, { testID: ringgId("message-content"), style: [styles14.content, bubbled ? styles14.contentPadding : null], children: [
3296
+ shouldRenderMessageBody ? body : null,
3297
+ sourceUrl ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3298
+ Touchable,
3299
+ {
3300
+ testID: ringgId("message-source"),
3301
+ style: styles14.source,
3302
+ accessibilityLabel: `Source: ${sourceUrl}`,
3303
+ onPress: () => {
3304
+ void import_react_native15.Linking.openURL(sourceUrl).catch(() => {
3305
+ });
3306
+ },
3307
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native15.Text, { style: [styles14.sourceText, { color: solidColor(theme.primaryColor), fontFamily }], children: [
3308
+ "Source: ",
3309
+ sourceUrl
3310
+ ] })
3311
+ }
3312
+ ) : null,
3313
+ children ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native15.View, { testID: ringgId("message-attachment"), style: styles14.attachment, children }) : null
3314
+ ] })
3315
+ ] }) });
3316
+ };
3317
+ var message_item_default = MessageItem;
3318
+ var styles14 = import_react_native15.StyleSheet.create({
3319
+ // Web anchors the bubble with `mr-auto` / `ml-auto` inside a `flex-col
3320
+ // items-start` column; alignItems says the same thing natively.
3321
+ rowStart: { alignItems: "flex-start" },
3322
+ rowEnd: { alignItems: "flex-end" },
3323
+ // `max-w-[80%]` — the bubble hugs its content up to 80% of the transcript.
3324
+ bubble: { maxWidth: "80%" },
3325
+ // The gradient layer fills the bubble, so the padding lives one level in on
3326
+ // the content; `overflow` is what clips that layer to the rounded corners.
3327
+ bubbleBox: { overflow: "hidden" },
3328
+ agentCorners: { borderTopLeftRadius: BUBBLE_RADIUS, borderTopRightRadius: BUBBLE_RADIUS, borderBottomLeftRadius: ANCHORED_RADIUS, borderBottomRightRadius: BUBBLE_RADIUS },
3329
+ userCorners: { borderTopLeftRadius: BUBBLE_RADIUS, borderTopRightRadius: BUBBLE_RADIUS, borderBottomLeftRadius: BUBBLE_RADIUS, borderBottomRightRadius: ANCHORED_RADIUS },
3330
+ content: { gap: CONTENT_GAP },
3331
+ contentPadding: { paddingLeft: BUBBLE_PADDING_LEFT, paddingRight: BUBBLE_PADDING_RIGHT, paddingVertical: BUBBLE_PADDING_VERTICAL },
3332
+ body: { fontSize: BODY_FONT_SIZE, lineHeight: BODY_FONT_SIZE * LINE_HEIGHT_RATIO2 },
3333
+ // `w-fit`: the citation's touch area stops at the end of the URL rather than
3334
+ // spanning the bubble. Press feedback replaces the web's hover underline.
3335
+ source: { alignSelf: "flex-start" },
3336
+ sourceText: { fontSize: SOURCE_FONT_SIZE, lineHeight: SOURCE_FONT_SIZE * LINE_HEIGHT_RATIO2 },
3337
+ attachment: { alignSelf: "stretch" }
3338
+ });
3339
+
3340
+ // src/components/steps/blocks-step.tsx
3341
+ var import_lucide_react_native9 = require("lucide-react-native");
3342
+ var import_react14 = require("react");
3343
+ var import_react_native16 = require("react-native");
3344
+ var import_react_native_svg3 = __toESM(require("react-native-svg"));
3345
+
3346
+ // src/lib/block-types.ts
3347
+ var SUPPORTED_BLOCKS_SCHEMA_VERSION = 1;
3348
+
3349
+ // src/components/steps/blocks-step.tsx
3350
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3351
+ var ALLOWED_URL_SCHEMES = ["https:", "mailto:", "tel:"];
3352
+ var ALIGN = { start: "flex-start", center: "center", end: "flex-end" };
3353
+ var JUSTIFY = { start: "flex-start", center: "center", end: "flex-end", between: "space-between" };
3354
+ var TEXT_ALIGN = { start: "left", center: "center", end: "right" };
3355
+ var INPUT_KEYBOARD_TYPE = {
3356
+ input_email: "email-address",
3357
+ input_phone_number: "phone-pad",
3358
+ input_number: "numeric"
3359
+ };
3360
+ var RATING_COLOR = "#F5A623";
3361
+ var CAROUSEL_CARD_WIDTH = 256;
3362
+ var CAROUSEL_GAP = 12;
3363
+ var IMAGE_DEFAULT_HEIGHT = 112;
3364
+ var PULSE_HALF_CYCLE_MS2 = 1e3;
3365
+ var PULSE_MIN_OPACITY2 = 0.5;
3366
+ var TEXTAREA_MIN_HEIGHT = 60;
3367
+ var DIVIDER_THICKNESS = 1.5;
3368
+ var NO_HIT_SLOP = 0;
3369
+ var NO_BLOCKS_STATE = {
3370
+ getValue: () => void 0,
3371
+ setValue: () => {
3372
+ },
3373
+ errors: {},
3374
+ disabled: true,
3375
+ submitRespond: () => {
3376
+ }
3377
+ };
3378
+ var BlocksStateContext = (0, import_react14.createContext)(NO_BLOCKS_STATE);
3379
+ var useBlocksState = () => (0, import_react14.useContext)(BlocksStateContext);
3380
+ var useBlocksTheme = () => {
3381
+ const widget = useWidgetTheme();
3382
+ return (0, import_react14.useMemo)(
3383
+ () => mergeTheme({
3384
+ primaryColor: widget.primaryColor,
3385
+ primaryTextColor: widget.primaryTextColor,
3386
+ backgroundColor: widget.backgroundColor,
3387
+ surfaceColor: widget.surfaceColor,
3388
+ textColor: widget.textColor,
3389
+ mutedTextColor: widget.mutedTextColor,
3390
+ borderColor: widget.borderColor,
3391
+ errorColor: widget.errorColor,
3392
+ successColor: widget.successColor,
3393
+ buttonStyle: widget.buttonStyle,
3394
+ fontFamily: widget.fontFamily
3395
+ }),
3396
+ [widget]
3397
+ );
3398
+ };
3399
+ var collectInputNames = (nodes) => {
3400
+ const names = [];
3401
+ for (const node of nodes || []) {
3402
+ if (node.type.startsWith("input_") && node.name) names.push(node.name);
3403
+ names.push(...collectInputNames(node.children));
3404
+ }
3405
+ return names;
3406
+ };
3407
+ var findInputs = (nodes, scope) => {
3408
+ const inputs = [];
3409
+ for (const node of nodes || []) {
3410
+ if (node.type.startsWith("input_") && node.name && (scope === null || scope.includes(node.name))) inputs.push(node);
3411
+ inputs.push(...findInputs(node.children, scope));
3412
+ }
3413
+ return inputs;
3414
+ };
3415
+ var evaluateVisibleIf = (condition, getValue) => {
3416
+ if (!condition) return true;
3417
+ if (condition.all) return condition.all.every((leaf) => evaluateVisibleIf(leaf, getValue));
3418
+ if (condition.any) return condition.any.some((leaf) => evaluateVisibleIf(leaf, getValue));
3419
+ if (!condition.field) return true;
3420
+ const actual = getValue(condition.field);
3421
+ switch (condition.op || "truthy") {
3422
+ case "eq":
3423
+ return actual === condition.value;
3424
+ case "ne":
3425
+ return actual !== condition.value;
3426
+ case "in":
3427
+ return Array.isArray(condition.value) && condition.value.includes(actual);
3428
+ case "gt":
3429
+ return typeof actual === "number" && typeof condition.value === "number" && actual > condition.value;
3430
+ case "lt":
3431
+ return typeof actual === "number" && typeof condition.value === "number" && actual < condition.value;
3432
+ case "truthy":
3433
+ default:
3434
+ return Array.isArray(actual) ? actual.length > 0 : !!actual;
3435
+ }
3436
+ };
3437
+ var percentWidth = (value) => `${value}%`;
3438
+ var fontSizeOf = (props, fallback) => typeof props.font_size === "number" ? props.font_size : fallback;
3439
+ var sizeStyle = (props, defaultFontPx) => {
3440
+ const fontSize = typeof props.font_size === "number" ? props.font_size : defaultFontPx;
3441
+ return {
3442
+ ...typeof props.width === "number" ? { width: percentWidth(props.width) } : null,
3443
+ ...typeof props.height === "number" ? { height: props.height } : null,
3444
+ ...fontSize !== void 0 ? { fontSize } : null
3445
+ };
3446
+ };
3447
+ var spacing = (value, fallback) => typeof value === "number" ? value : fallback;
3448
+ var alignOf = (value) => typeof value === "string" ? ALIGN[value] : void 0;
3449
+ var justifyOf = (value) => typeof value === "string" ? JUSTIFY[value] : void 0;
3450
+ var textAlignOf = (value) => typeof value === "string" ? TEXT_ALIGN[value] : void 0;
3451
+ var parseColor = (color) => {
3452
+ const value = color.trim();
3453
+ const hex = /^#([0-9a-f]+)$/i.exec(value)?.[1];
3454
+ if (hex) {
3455
+ const six = hex.length === 3 || hex.length === 4 ? hex.slice(0, 3).replace(/./g, (digit) => digit + digit) : hex.length === 6 || hex.length === 8 ? hex.slice(0, 6) : "";
3456
+ if (!six) return void 0;
3457
+ const packed = Number.parseInt(six, 16);
3458
+ return Number.isFinite(packed) ? [packed >> 16 & 255, packed >> 8 & 255, packed & 255] : void 0;
3459
+ }
3460
+ const channels = /^rgba?\(([^)]+)\)$/i.exec(value)?.[1];
3461
+ if (!channels) return void 0;
3462
+ const parts = channels.split(/[\s,/]+/).filter(Boolean).slice(0, 3).map(Number);
3463
+ if (parts.length < 3 || parts.some((part) => !Number.isFinite(part))) return void 0;
3464
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
3465
+ };
3466
+ var tint = (base, overlay, amount) => {
3467
+ const from = parseColor(base);
3468
+ const to = parseColor(overlay);
3469
+ if (!from || !to) return base;
3470
+ const blend = (channel) => Math.round(from[channel] * (1 - amount) + to[channel] * amount);
3471
+ return `rgb(${blend(0)}, ${blend(1)}, ${blend(2)})`;
3472
+ };
3473
+ var headerFontSize = (level) => {
3474
+ switch (level) {
3475
+ case 1:
3476
+ return 16;
3477
+ case 2:
3478
+ return 14;
3479
+ default:
3480
+ return 12;
3481
+ }
3482
+ };
3483
+ var badgeToneColor = (tone, theme) => {
3484
+ switch (tone) {
3485
+ case "success":
3486
+ return theme.successColor;
3487
+ case "danger":
3488
+ case "warning":
3489
+ return theme.errorColor;
3490
+ case "info":
3491
+ return solidColor(theme.primaryColor);
3492
+ default:
3493
+ return theme.mutedTextColor;
3494
+ }
3495
+ };
3496
+ var calloutAccent = (tone, theme) => {
3497
+ switch (tone) {
3498
+ case "success":
3499
+ return solidColor(theme.successColor);
3500
+ case "warning":
3501
+ case "error":
3502
+ return solidColor(theme.errorColor);
3503
+ default:
3504
+ return solidColor(theme.primaryColor);
3505
+ }
3506
+ };
3507
+ var buttonVisual = (style, theme) => {
3508
+ switch (style) {
3509
+ case "destructive":
3510
+ return { background: theme.errorColor, color: theme.primaryTextColor, borderColor: "transparent" };
3511
+ case "secondary":
3512
+ return { background: theme.surfaceColor, color: theme.textColor, borderColor: theme.borderColor };
3513
+ case "primary":
3514
+ return { background: theme.primaryColor, color: theme.primaryTextColor, borderColor: "transparent" };
3515
+ default:
3516
+ return { background: "transparent", color: theme.textColor, borderColor: theme.borderColor };
3517
+ }
3518
+ };
3519
+ var openExternalUrl2 = (url) => {
3520
+ void import_react_native16.Linking.openURL(url).catch(() => {
3521
+ });
3522
+ };
3523
+ var CheckSquare = ({ checked, theme }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3524
+ import_react_native16.View,
3525
+ {
3526
+ style: [
3527
+ styles15.checkSquare,
3528
+ checked ? { backgroundColor: solidColor(theme.primaryColor), borderColor: solidColor(theme.primaryColor) } : { backgroundColor: theme.backgroundColor, borderColor: solidColor(theme.borderColor) }
3529
+ ],
3530
+ children: checked && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_svg3.default, { width: 11, height: 11, viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_svg3.Path, { d: "M2.5 6.2 5 8.7l4.5-5.4", stroke: theme.primaryTextColor, strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round" }) })
3531
+ }
3532
+ );
3533
+ var PillToggle = ({ label, selected, disabled, fontSize, theme, onPress }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3534
+ Touchable,
3535
+ {
3536
+ disabled,
3537
+ onPress,
3538
+ accessibilityLabel: label,
3539
+ hitSlop: NO_HIT_SLOP,
3540
+ style: [
3541
+ styles15.pill,
3542
+ selected ? { backgroundColor: solidColor(theme.primaryColor), borderColor: solidColor(theme.primaryColor) } : { backgroundColor: "transparent", borderColor: solidColor(theme.borderColor) }
3543
+ ],
3544
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.pillLabel, { color: selected ? theme.primaryTextColor : theme.textColor, fontSize, fontFamily: resolveFontFamily(theme.fontFamily) }], children: label })
3545
+ }
3546
+ );
3547
+ var SelectDropdown = ({ options, selected, placeholder, disabled, multiple, fontSize, inputStyle, theme, onSelect }) => {
3548
+ const [open, setOpen] = (0, import_react14.useState)(false);
3549
+ const font = resolveFontFamily(theme.fontFamily);
3550
+ const border = solidColor(theme.borderColor);
3551
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { children: [
3552
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Touchable, { disabled, onPress: () => setOpen((prev) => !prev), accessibilityLabel: selected.length ? selected.join(", ") : placeholder, hitSlop: NO_HIT_SLOP, style: [styles15.selectTrigger, inputStyle], children: [
3553
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { numberOfLines: 1, style: [styles15.selectValue, { color: selected.length ? theme.textColor : theme.mutedTextColor, fontSize, fontFamily: font }], children: selected.length ? selected.join(", ") : placeholder }),
3554
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react_native9.ChevronDown, { size: 14, color: theme.mutedTextColor, style: { transform: [{ rotate: open ? "180deg" : "0deg" }] } })
3555
+ ] }),
3556
+ open && // Web floats this menu over the page. RN has no overlay layer inside the
3557
+ // transcript and a nested vertical scroller would fight the transcript's
3558
+ // own, so the list expands in flow and pushes the tree below it down.
3559
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.selectMenu, { backgroundColor: theme.backgroundColor, borderColor: border, borderRadius: toNumber2(theme.borderRadius, 8) }], children: options.map((option, index) => {
3560
+ const isSelected = selected.includes(option);
3561
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3562
+ Touchable,
3563
+ {
3564
+ disabled,
3565
+ onPress: () => {
3566
+ onSelect(option);
3567
+ if (!multiple) setOpen(false);
3568
+ },
3569
+ accessibilityLabel: option,
3570
+ hitSlop: NO_HIT_SLOP,
3571
+ style: styles15.selectOption,
3572
+ children: [
3573
+ multiple && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CheckSquare, { checked: isSelected, theme }),
3574
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { numberOfLines: 1, style: [styles15.selectOptionLabel, { color: theme.textColor, fontSize: 14, fontFamily: font }], children: option }),
3575
+ !multiple && isSelected && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react_native9.Check, { size: 14, color: solidColor(theme.primaryColor) })
3576
+ ]
3577
+ },
3578
+ `${option}-${index}`
3579
+ );
3580
+ }) })
3581
+ ] });
3582
+ };
3583
+ var ImageBlock = ({ node }) => {
3584
+ const theme = useBlocksTheme();
3585
+ const props = node.props || {};
3586
+ const url = typeof props.url === "string" ? props.url : "";
3587
+ const isBinding = url.includes("${{");
3588
+ const isBlockedScheme = !!url && !isBinding && !url.trim().toLowerCase().startsWith("https:");
3589
+ const [status, setStatus] = (0, import_react14.useState)("loading");
3590
+ const pulse = (0, import_react14.useRef)(new import_react_native16.Animated.Value(1)).current;
3591
+ (0, import_react14.useEffect)(() => {
3592
+ setStatus("loading");
3593
+ }, [url]);
3594
+ (0, import_react14.useEffect)(() => {
3595
+ if (status !== "loading") return;
3596
+ const loop = import_react_native16.Animated.loop(
3597
+ import_react_native16.Animated.sequence([
3598
+ import_react_native16.Animated.timing(pulse, { toValue: PULSE_MIN_OPACITY2, duration: PULSE_HALF_CYCLE_MS2, useNativeDriver: true }),
3599
+ import_react_native16.Animated.timing(pulse, { toValue: 1, duration: PULSE_HALF_CYCLE_MS2, useNativeDriver: true })
3600
+ ])
3601
+ );
3602
+ loop.start();
3603
+ return () => loop.stop();
3604
+ }, [pulse, status]);
3605
+ const radius = toNumber2(theme.borderRadius, 8);
3606
+ const alignSelf = alignOf(props.align) ?? "flex-start";
3607
+ if (!url || isBinding || isBlockedScheme || status === "error") {
3608
+ const label = !url ? "Add an image URL" : isBinding ? url : "Couldn't load image";
3609
+ const broken = isBlockedScheme || status === "error" && !isBinding;
3610
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3611
+ import_react_native16.View,
3612
+ {
3613
+ testID: ringgId("block-image"),
3614
+ style: [
3615
+ styles15.imagePlaceholder,
3616
+ { alignSelf, borderColor: solidColor(theme.borderColor), borderRadius: radius, backgroundColor: tint(theme.backgroundColor, theme.surfaceColor, 0.5) },
3617
+ sizeStyle(props)
3618
+ ],
3619
+ children: [
3620
+ broken ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react_native9.ImageOff, { size: 20, color: theme.mutedTextColor }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react_native9.Image, { size: 20, color: theme.mutedTextColor }),
3621
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { numberOfLines: 1, style: [styles15.placeholderText, { color: theme.mutedTextColor, fontFamily: resolveFontFamily(theme.fontFamily) }], children: label })
3622
+ ]
3623
+ }
3624
+ );
3625
+ }
3626
+ const box = {
3627
+ width: typeof props.width === "number" ? percentWidth(props.width) : "100%",
3628
+ height: typeof props.height === "number" ? props.height : IMAGE_DEFAULT_HEIGHT
3629
+ };
3630
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { style: [styles15.imageBox, box, { alignSelf, borderRadius: radius }], children: [
3631
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3632
+ import_react_native16.Image,
3633
+ {
3634
+ testID: ringgId("block-image"),
3635
+ source: { uri: url },
3636
+ accessibilityLabel: String(props.alt ?? ""),
3637
+ resizeMode: "contain",
3638
+ onLoad: () => setStatus("loaded"),
3639
+ onError: () => setStatus("error"),
3640
+ style: styles15.imageFill
3641
+ },
3642
+ url
3643
+ ),
3644
+ status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Animated.View, { testID: ringgId("block-image-loading"), style: [import_react_native16.StyleSheet.absoluteFill, { backgroundColor: theme.surfaceColor, opacity: pulse }] })
3645
+ ] });
3646
+ };
3647
+ var ButtonBlock = ({ node, formNames }) => {
3648
+ const theme = useBlocksTheme();
3649
+ const state = useBlocksState();
3650
+ const props = node.props || {};
3651
+ const label = String(props.label ?? "");
3652
+ const press = node.on?.press;
3653
+ const handlePress = () => {
3654
+ if (!press || state.disabled) return;
3655
+ switch (press.action) {
3656
+ case "respond":
3657
+ state.submitRespond(press, formNames, label);
3658
+ return;
3659
+ case "open_url": {
3660
+ if (state.onSandboxAction) {
3661
+ state.onSandboxAction(press);
3662
+ return;
3663
+ }
3664
+ if (!press.url) return;
3665
+ const url = String(press.url);
3666
+ if (ALLOWED_URL_SCHEMES.some((scheme) => url.trim().toLowerCase().startsWith(scheme))) openExternalUrl2(url);
3667
+ return;
3668
+ }
3669
+ case "dom_event":
3670
+ state.onSandboxAction?.(press);
3671
+ return;
3672
+ default:
3673
+ return;
3674
+ }
3675
+ };
3676
+ const visual = buttonVisual(typeof props.style === "string" && props.style ? props.style : "primary", theme);
3677
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3678
+ Touchable,
3679
+ {
3680
+ testID: ringgId("block-button"),
3681
+ accessibilityLabel: label,
3682
+ onPress: handlePress,
3683
+ disabled: state.disabled,
3684
+ hitSlop: NO_HIT_SLOP,
3685
+ style: [{ alignSelf: alignOf(props.align) }, sizeStyle(props)],
3686
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(GradientFill, { color: visual.background, style: [styles15.button, { borderColor: visual.borderColor, borderRadius: resolveButtonRadius(theme.buttonStyle) }], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.buttonLabel, { color: visual.color, fontSize: fontSizeOf(props, 14), fontFamily: resolveFontFamily(theme.fontFamily) }], children: label }) })
3687
+ }
3688
+ );
3689
+ };
3690
+ var InputBlock = ({ node }) => {
3691
+ const theme = useBlocksTheme();
3692
+ const state = useBlocksState();
3693
+ const props = node.props || {};
3694
+ const name = node.name ?? "";
3695
+ const error = state.errors[name];
3696
+ const font = resolveFontFamily(theme.fontFamily);
3697
+ if (!name) {
3698
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.nameless, { borderColor: solidColor(theme.borderColor) }], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.namelessText, { color: theme.mutedTextColor, fontFamily: font }], children: "Input needs a name" }) });
3699
+ }
3700
+ const inputStyle = {
3701
+ backgroundColor: theme.backgroundColor,
3702
+ borderColor: error ? solidColor(theme.errorColor) : solidColor(theme.borderColor),
3703
+ borderRadius: toNumber2(theme.borderRadius, 8)
3704
+ };
3705
+ const label = String(props.label ?? (node.type === "input_rating" ? "" : name));
3706
+ const bodyFontSize = fontSizeOf(props, 14);
3707
+ const placeholder = String(props.placeholder ?? `Enter ${label.toLowerCase()}`);
3708
+ const selectPlaceholder = String(props.placeholder ?? `Select ${label.toLowerCase()}`);
3709
+ const options = props.options || [];
3710
+ const control = () => {
3711
+ switch (node.type) {
3712
+ case "input_checkbox": {
3713
+ const checked = state.getValue(name) || false;
3714
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Touchable, { disabled: state.disabled, onPress: () => state.setValue(name, !checked), accessibilityLabel: label, hitSlop: NO_HIT_SLOP, style: styles15.checkboxRow, children: [
3715
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(CheckSquare, { checked, theme }),
3716
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: { color: theme.textColor, fontSize: 14, fontFamily: font }, children: label })
3717
+ ] });
3718
+ }
3719
+ case "input_rating": {
3720
+ const max = Math.min(Math.max(Math.round(Number(props.max) || 5), 3), 10);
3721
+ const current = Math.round(Number(state.getValue(name)) || 0);
3722
+ const starPx = fontSizeOf(props, 24);
3723
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.ratingRow, { justifyContent: justifyOf(props.align) }], accessibilityRole: "radiogroup", accessibilityLabel: label, children: Array.from({ length: max }, (_unused, index) => {
3724
+ const filled = index < current;
3725
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3726
+ Touchable,
3727
+ {
3728
+ disabled: state.disabled,
3729
+ onPress: () => state.setValue(name, index + 1),
3730
+ accessibilityLabel: `${index + 1} star${index ? "s" : ""}`,
3731
+ hitSlop: NO_HIT_SLOP,
3732
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: { color: filled ? RATING_COLOR : solidColor(theme.borderColor), fontSize: starPx, fontFamily: font }, children: filled ? "\u2605" : "\u2606" })
3733
+ },
3734
+ index
3735
+ );
3736
+ }) });
3737
+ }
3738
+ case "input_single_select": {
3739
+ const value = state.getValue(name) || "";
3740
+ if (props.variant === "pills") {
3741
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.pillRow, { justifyContent: justifyOf(props.align) }], children: options.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PillToggle, { label: option, selected: value === option, disabled: state.disabled, fontSize: bodyFontSize, theme, onPress: () => state.setValue(name, option) }, `${option}-${index}`)) });
3742
+ }
3743
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3744
+ SelectDropdown,
3745
+ {
3746
+ options,
3747
+ selected: value ? [value] : [],
3748
+ placeholder: selectPlaceholder,
3749
+ disabled: state.disabled,
3750
+ multiple: false,
3751
+ fontSize: bodyFontSize,
3752
+ inputStyle,
3753
+ theme,
3754
+ onSelect: (option) => state.setValue(name, option)
3755
+ }
3756
+ );
3757
+ }
3758
+ case "input_multi_select": {
3759
+ const current = state.getValue(name) || [];
3760
+ const toggle = (option) => state.setValue(name, current.includes(option) ? current.filter((entry) => entry !== option) : [...current, option]);
3761
+ if (props.variant === "pills") {
3762
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.pillRow, { justifyContent: justifyOf(props.align) }], children: options.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PillToggle, { label: option, selected: current.includes(option), disabled: state.disabled, fontSize: bodyFontSize, theme, onPress: () => toggle(option) }, `${option}-${index}`)) });
3763
+ }
3764
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SelectDropdown, { options, selected: current, placeholder: selectPlaceholder, disabled: state.disabled, multiple: true, fontSize: 14, inputStyle, theme, onSelect: toggle });
3765
+ }
3766
+ case "input_textarea":
3767
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3768
+ import_react_native16.TextInput,
3769
+ {
3770
+ multiline: true,
3771
+ placeholder,
3772
+ placeholderTextColor: theme.mutedTextColor,
3773
+ accessibilityLabel: label,
3774
+ value: String(state.getValue(name) ?? ""),
3775
+ onChangeText: (text) => state.setValue(name, text),
3776
+ editable: !state.disabled,
3777
+ style: [
3778
+ styles15.field,
3779
+ styles15.textarea,
3780
+ inputStyle,
3781
+ { color: theme.textColor, fontSize: bodyFontSize, fontFamily: font },
3782
+ // height sizes the control itself, not the label+error wrapper.
3783
+ typeof props.height === "number" ? { height: props.height } : null
3784
+ ]
3785
+ }
3786
+ );
3787
+ default: {
3788
+ const isDateOrTime = node.type === "input_date" || node.type === "input_time";
3789
+ const field = /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3790
+ import_react_native16.TextInput,
3791
+ {
3792
+ placeholder,
3793
+ placeholderTextColor: theme.mutedTextColor,
3794
+ accessibilityLabel: label,
3795
+ value: String(state.getValue(name) ?? ""),
3796
+ keyboardType: INPUT_KEYBOARD_TYPE[node.type] ?? "default",
3797
+ autoCapitalize: node.type === "input_email" ? "none" : "sentences",
3798
+ autoCorrect: node.type !== "input_email",
3799
+ onChangeText: (text) => {
3800
+ if (node.type !== "input_number") {
3801
+ state.setValue(name, text);
3802
+ return;
3803
+ }
3804
+ const parsed = Number(text);
3805
+ state.setValue(name, text === "" || !Number.isFinite(parsed) ? "" : parsed);
3806
+ },
3807
+ editable: !state.disabled,
3808
+ style: [styles15.field, isDateOrTime && styles15.fieldWithIcon, inputStyle, { color: theme.textColor, fontSize: 14, fontFamily: font }]
3809
+ }
3810
+ );
3811
+ if (!isDateOrTime) return field;
3812
+ const PickerIcon = node.type === "input_date" ? import_lucide_react_native9.Calendar : import_lucide_react_native9.Clock;
3813
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { children: [
3814
+ field,
3815
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { pointerEvents: "none", style: styles15.fieldIcon, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PickerIcon, { size: 16, color: theme.mutedTextColor }) })
3816
+ ] });
3817
+ }
3818
+ }
3819
+ };
3820
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { testID: ringgId(`block-${node.type}`), style: sizeStyle({ ...props, height: void 0 }), children: [
3821
+ node.type !== "input_checkbox" && label.trim() !== "" && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { style: styles15.labelRow, children: [
3822
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.label, { color: theme.mutedTextColor, fontFamily: font }], children: label }),
3823
+ !!props.required && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.label, { color: theme.errorColor, fontFamily: font }], children: "*" })
3824
+ ] }),
3825
+ control(),
3826
+ !!error && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.error, { color: theme.errorColor, fontFamily: font }], children: error })
3827
+ ] });
3828
+ };
3829
+ var ExpanderBlock = ({ node, children }) => {
3830
+ const theme = useBlocksTheme();
3831
+ const props = node.props || {};
3832
+ const [open, setOpen] = (0, import_react14.useState)(!!props.open);
3833
+ const font = resolveFontFamily(theme.fontFamily);
3834
+ const title = String(props.title ?? "");
3835
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { testID: ringgId(`block-${node.type}`), style: [styles15.expander, { borderColor: solidColor(theme.borderColor), borderRadius: toNumber2(theme.borderRadius, 8) }, sizeStyle(props)], children: [
3836
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Touchable, { onPress: () => setOpen((prev) => !prev), accessibilityLabel: title, hitSlop: NO_HIT_SLOP, style: styles15.expanderSummary, children: [
3837
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.expanderTitle, { color: theme.textColor, fontSize: fontSizeOf(props, 14), fontFamily: font }], children: title }),
3838
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react_native9.ChevronDown, { size: 14, color: theme.mutedTextColor, style: { transform: [{ rotate: open ? "180deg" : "0deg" }] } })
3839
+ ] }),
3840
+ open && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: styles15.expanderBody, children })
3841
+ ] });
3842
+ };
3843
+ var BlockRenderer = ({ node, formNames, inCarousel }) => {
3844
+ const theme = useBlocksTheme();
3845
+ const state = useBlocksState();
3846
+ const props = node.props || {};
3847
+ if (!evaluateVisibleIf(node.visible_if, state.getValue)) return null;
3848
+ const children = (scope, carousel = false) => (node.children || []).map((child, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(BlockRenderer, { node: child, formNames: scope, inCarousel: carousel }, `${child.id ?? "b"}-${index}`));
3849
+ const testID = ringgId(`block-${node.type}`);
3850
+ const font = resolveFontFamily(theme.fontFamily);
3851
+ const border = solidColor(theme.borderColor);
3852
+ const radius = toNumber2(theme.borderRadius, 8);
3853
+ switch (node.type) {
3854
+ case "box":
3855
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID, style: [styles15.column, { alignItems: alignOf(props.align), gap: spacing(props.gap, 8), padding: spacing(props.padding, 0) }, sizeStyle(props)], children: children(formNames) });
3856
+ case "column":
3857
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID, style: [styles15.column, { alignItems: alignOf(props.align), gap: spacing(props.gap, 12) }, sizeStyle(props)], children: children(formNames) });
3858
+ case "row":
3859
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3860
+ import_react_native16.View,
3861
+ {
3862
+ testID,
3863
+ style: [
3864
+ styles15.row,
3865
+ { alignItems: alignOf(props.align) ?? "center", justifyContent: justifyOf(props.justify), flexWrap: props.wrap === false ? "nowrap" : "wrap", gap: spacing(props.gap, 8) },
3866
+ sizeStyle(props)
3867
+ ],
3868
+ children: children(formNames)
3869
+ }
3870
+ );
3871
+ case "form":
3872
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID, style: [styles15.form, sizeStyle(props)], children: children(collectInputNames(node.children)) });
3873
+ case "card": {
3874
+ const kids = node.children || [];
3875
+ let bodyEnd = kids.length;
3876
+ while (bodyEnd > 0 && kids[bodyEnd - 1]?.type === "button") bodyEnd -= 1;
3877
+ const renderKids = (list, offset) => list.map((child, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(BlockRenderer, { node: child, formNames, inCarousel: false }, `${child.id ?? "b"}-${offset + index}`));
3878
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3879
+ import_react_native16.View,
3880
+ {
3881
+ testID,
3882
+ style: [
3883
+ styles15.card,
3884
+ { alignItems: alignOf(props.align), backgroundColor: theme.backgroundColor, borderColor: border, borderRadius: radius, padding: spacing(props.padding, 12) },
3885
+ inCarousel && styles15.carouselCard,
3886
+ sizeStyle(props)
3887
+ ],
3888
+ children: [
3889
+ renderKids(kids.slice(0, bodyEnd), 0),
3890
+ bodyEnd < kids.length && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: styles15.cardFooter, children: renderKids(kids.slice(bodyEnd), bodyEnd) })
3891
+ ]
3892
+ }
3893
+ );
3894
+ }
3895
+ case "carousel":
3896
+ return (
3897
+ // alignItems stretch (explicit): every card matches the tallest one.
3898
+ // Snapping runs on the fixed card pitch — the only interval RN can snap
3899
+ // to, and the only child width the catalog pins down.
3900
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3901
+ import_react_native16.ScrollView,
3902
+ {
3903
+ testID,
3904
+ horizontal: true,
3905
+ showsHorizontalScrollIndicator: false,
3906
+ snapToInterval: CAROUSEL_CARD_WIDTH + CAROUSEL_GAP,
3907
+ snapToAlignment: "start",
3908
+ decelerationRate: "fast",
3909
+ style: [styles15.carousel, sizeStyle(props)],
3910
+ contentContainerStyle: styles15.carouselContent,
3911
+ children: children(formNames, true)
3912
+ }
3913
+ )
3914
+ );
3915
+ case "header":
3916
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { testID, style: [styles15.header, { color: theme.textColor, fontFamily: font, textAlign: textAlignOf(props.align) }, sizeStyle(props, headerFontSize(Number(props.level) || 1))], children: String(props.text ?? "") });
3917
+ case "text": {
3918
+ if (props.markdown === true) {
3919
+ return (
3920
+ // Markdown renders a stack of blocks, so `align` places those blocks
3921
+ // in the column; it cannot re-align the wrapped lines inside them.
3922
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID, style: [{ alignItems: alignOf(props.align) }, sizeStyle(props)], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3923
+ Markdown,
3924
+ {
3925
+ content: String(props.text ?? ""),
3926
+ color: theme.textColor,
3927
+ mutedColor: theme.mutedTextColor,
3928
+ linkColor: solidColor(theme.primaryColor),
3929
+ fontSize: fontSizeOf(props, 14),
3930
+ fontFamily: font
3931
+ }
3932
+ ) })
3933
+ );
3934
+ }
3935
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { testID, style: [{ color: theme.textColor, fontFamily: font, textAlign: textAlignOf(props.align) }, sizeStyle(props, 14)], children: String(props.text ?? "") });
3936
+ }
3937
+ case "image":
3938
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ImageBlock, { node });
3939
+ case "divider":
3940
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3941
+ import_react_native16.View,
3942
+ {
3943
+ testID,
3944
+ style: [
3945
+ styles15.divider,
3946
+ {
3947
+ alignSelf: alignOf(props.align),
3948
+ backgroundColor: tint(border, "#000000", 0.08),
3949
+ height: typeof props.height === "number" ? props.height : DIVIDER_THICKNESS,
3950
+ width: typeof props.width === "number" ? percentWidth(props.width) : "100%"
3951
+ }
3952
+ ]
3953
+ }
3954
+ );
3955
+ case "facts": {
3956
+ const items = props.items || [];
3957
+ const fontSize = fontSizeOf(props, 14);
3958
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID, style: [styles15.facts, sizeStyle(props)], children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { style: styles15.factsRow, children: [
3959
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: { color: theme.mutedTextColor, fontSize, fontFamily: font }, children: item.label }),
3960
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.factsValue, { color: theme.textColor, fontSize, fontFamily: font }], children: item.value })
3961
+ ] }, index)) });
3962
+ }
3963
+ case "badge": {
3964
+ const toneColor = badgeToneColor(String(props.tone ?? "neutral"), theme);
3965
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID, style: [styles15.badge, { alignSelf: alignOf(props.align) ?? "flex-start", borderColor: toneColor }, sizeStyle(props)], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { numberOfLines: 1, style: [styles15.badgeText, { color: toneColor, fontSize: fontSizeOf(props, 12), fontFamily: font }], children: String(props.text ?? "") }) });
3966
+ }
3967
+ case "table": {
3968
+ const columns = props.columns || [];
3969
+ const rows = (Array.isArray(props.rows) ? props.rows : []).filter(Array.isArray);
3970
+ const cellFontSize = fontSizeOf(props, 14);
3971
+ const cellBorder = { borderBottomWidth: 1, borderColor: border };
3972
+ return (
3973
+ // Card chrome (surface, border, theme radius) so the table reads like
3974
+ // the other block widgets instead of bare rows floating in the chat.
3975
+ // Columns share the width evenly and cells wrap: RN has no auto table
3976
+ // layout, and equal shares are the only split that keeps every row's
3977
+ // cells aligned without measuring text.
3978
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { testID, style: [styles15.table, { backgroundColor: tint(theme.backgroundColor, theme.surfaceColor, 0.5), borderColor: border, borderRadius: radius }, sizeStyle(props)], children: [
3979
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: styles15.row, children: columns.map((column, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.tableCell, cellBorder], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { numberOfLines: 1, style: [styles15.tableHeaderText, { color: theme.mutedTextColor, fontFamily: font }], children: column }) }, index)) }),
3980
+ rows.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: styles15.row, children: row.map((cell, cellIndex) => (
3981
+ // Last row keeps the card's own edge — no trailing border line.
3982
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.tableCell, rowIndex < rows.length - 1 && cellBorder], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: { color: theme.textColor, fontSize: cellFontSize, fontFamily: font }, children: String(cell) }) }, cellIndex)
3983
+ )) }, rowIndex))
3984
+ ] })
3985
+ );
3986
+ }
3987
+ case "steps": {
3988
+ const items = props.items || [];
3989
+ const current = Math.max(1, Math.min(Number(props.current) || 1, items.length));
3990
+ const fontSize = fontSizeOf(props, 15);
3991
+ return (
3992
+ // align moves the whole tracker as a unit so rows stay internally
3993
+ // left-aligned and the dots keep a straight vertical line. compact
3994
+ // (default): natural gap. stretch: spread steps to fill the container
3995
+ // height (pairs with the height prop).
3996
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3997
+ import_react_native16.View,
3998
+ {
3999
+ testID,
4000
+ style: [styles15.column, { alignSelf: alignOf(props.align) }, props.layout === "stretch" ? styles15.stepsStretch : styles15.stepsCompact, sizeStyle(props)],
4001
+ children: items.map((label, index) => {
4002
+ const stepNo = index + 1;
4003
+ const done = stepNo < current;
4004
+ const active = stepNo === current;
4005
+ const dotColor = done || active ? solidColor(theme.primaryColor) : border;
4006
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { style: styles15.stepRow, children: [
4007
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { style: [styles15.stepDot, { backgroundColor: done || active ? dotColor : "transparent", borderColor: dotColor }], children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.stepIndex, { color: done || active ? theme.primaryTextColor : theme.mutedTextColor, fontFamily: font }], children: done ? "\u2713" : String(stepNo) }) }),
4008
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.stepLabel, { color: active ? theme.textColor : theme.mutedTextColor, fontWeight: active ? "600" : "400", fontSize, fontFamily: font }], children: label })
4009
+ ] }, index);
4010
+ })
4011
+ }
4012
+ )
4013
+ );
4014
+ }
4015
+ case "callout": {
4016
+ const accent = calloutAccent(String(props.tone ?? "info"), theme);
4017
+ const fontSize = fontSizeOf(props, 14);
4018
+ const textAlign = textAlignOf(props.align);
4019
+ return (
4020
+ // Soft brand-tinted card; the tint + accent title carry the tone.
4021
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native16.View, { testID, style: [styles15.callout, { backgroundColor: tint(theme.backgroundColor, accent, 0.07), borderRadius: radius }, sizeStyle(props)], children: [
4022
+ !!props.title && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: [styles15.calloutTitle, { color: accent, fontFamily: font, textAlign }], children: String(props.title) }),
4023
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { style: { color: theme.textColor, fontSize, lineHeight: fontSize * 1.625, fontFamily: font, textAlign }, children: String(props.text ?? "") })
4024
+ ] })
4025
+ );
4026
+ }
4027
+ case "expander":
4028
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ExpanderBlock, { node, children: children(formNames) });
4029
+ case "button":
4030
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ButtonBlock, { node, formNames });
4031
+ default:
4032
+ if (node.type.startsWith("input_")) return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(InputBlock, { node });
4033
+ return node.children?.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_jsx_runtime18.Fragment, { children: children(formNames) }) : null;
4034
+ }
4035
+ };
4036
+ var BlocksStep = ({ config, disabled, onAction, onSandboxAction }) => {
4037
+ const theme = useBlocksTheme();
4038
+ const [values, setValues] = (0, import_react14.useState)({});
4039
+ const [errors, setErrors] = (0, import_react14.useState)({});
4040
+ const [submitting, setSubmitting] = (0, import_react14.useState)(false);
4041
+ if ((config.schema_version ?? 1) > SUPPORTED_BLOCKS_SCHEMA_VERSION) {
4042
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.Text, { testID: ringgId("blocks-unsupported"), style: [styles15.unsupported, { color: theme.mutedTextColor, fontFamily: resolveFontFamily(theme.fontFamily) }], children: "This content needs a newer version of the widget." });
4043
+ }
4044
+ const defaultsOf = (name) => findInputs(config.blocks, [name])[0]?.props?.default;
4045
+ const getValue = (name, fallback) => {
4046
+ if (name in values) return values[name];
4047
+ const fromProps = defaultsOf(name);
4048
+ return fromProps ?? fallback;
4049
+ };
4050
+ const setValue = (name, value) => {
4051
+ setValues((prev) => ({ ...prev, [name]: value }));
4052
+ setErrors((prev) => {
4053
+ if (!prev[name]) return prev;
4054
+ const next = { ...prev };
4055
+ delete next[name];
4056
+ return next;
4057
+ });
4058
+ };
4059
+ const submitRespond = (action, formNames, label) => {
4060
+ if (!action.action_id) return;
4061
+ const scopeInputs = findInputs(config.blocks, formNames).filter((input) => evaluateVisibleIf(input.visible_if, getValue));
4062
+ const nextErrors = {};
4063
+ const collected = {};
4064
+ for (const input of scopeInputs) {
4065
+ const name = input.name;
4066
+ if (!name) continue;
4067
+ const value = getValue(name);
4068
+ const isEmpty = value === void 0 || value === "" || Array.isArray(value) && value.length === 0;
4069
+ if (input.props?.required && isEmpty) {
4070
+ nextErrors[name] = `${input.props?.label || name} is required`;
4071
+ continue;
4072
+ }
4073
+ if (input.type === "input_email" && typeof value === "string" && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
4074
+ nextErrors[name] = "Please enter a valid email address";
4075
+ continue;
4076
+ }
4077
+ if (!isEmpty && value !== void 0) collected[name] = value;
4078
+ }
4079
+ if (Object.keys(nextErrors).length) {
4080
+ setErrors(nextErrors);
4081
+ return;
4082
+ }
4083
+ const result = onAction({ action_id: action.action_id, value: action.value, values: collected, label });
4084
+ if (result instanceof Promise) {
4085
+ setSubmitting(true);
4086
+ void result.finally(() => setSubmitting(false));
4087
+ }
4088
+ };
4089
+ const state = { getValue, setValue, errors, disabled: !!disabled || submitting, submitRespond, onSandboxAction };
4090
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(BlocksStateContext.Provider, { value: state, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native16.View, { testID: ringgId("blocks"), style: styles15.blocks, children: config.blocks.map((node, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(BlockRenderer, { node, formNames: null, inCarousel: false }, `${node.id ?? "b"}-${index}`)) }) });
4091
+ };
4092
+ var styles15 = import_react_native16.StyleSheet.create({
4093
+ badge: { flexDirection: "row", alignItems: "center", borderWidth: 1, borderRadius: 9999, paddingHorizontal: 8, paddingVertical: 2 },
4094
+ badgeText: { fontWeight: "600" },
4095
+ blocks: { width: "100%", flexDirection: "column", gap: 12 },
4096
+ button: { flexDirection: "row", alignItems: "center", justifyContent: "center", borderWidth: 1, paddingHorizontal: 16, paddingVertical: 8 },
4097
+ buttonLabel: { fontWeight: "600", textAlign: "center" },
4098
+ callout: { flexDirection: "column", gap: 6, paddingHorizontal: 16, paddingVertical: 12 },
4099
+ // tracking-wider (0.05em) at the title's 11px.
4100
+ calloutTitle: { fontSize: 11, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.55 },
4101
+ card: { flexDirection: "column", gap: 8, borderWidth: 1 },
4102
+ cardFooter: { marginTop: "auto", flexDirection: "column", gap: 8 },
4103
+ carousel: { marginHorizontal: -4 },
4104
+ carouselCard: { width: CAROUSEL_CARD_WIDTH, flexShrink: 0 },
4105
+ carouselContent: { flexDirection: "row", alignItems: "stretch", gap: CAROUSEL_GAP, paddingHorizontal: 4, paddingBottom: 8 },
4106
+ checkSquare: { width: 18, height: 18, flexShrink: 0, alignItems: "center", justifyContent: "center", borderWidth: 1, borderRadius: 5 },
4107
+ checkboxRow: { flexDirection: "row", alignItems: "center", gap: 10, paddingVertical: 4 },
4108
+ column: { flexDirection: "column" },
4109
+ divider: { flexShrink: 0 },
4110
+ error: { fontSize: 12, marginTop: 2 },
4111
+ expander: { borderWidth: 1, overflow: "hidden" },
4112
+ expanderBody: { flexDirection: "column", gap: 8, paddingHorizontal: 12, paddingBottom: 12 },
4113
+ expanderSummary: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, paddingHorizontal: 12, paddingVertical: 8 },
4114
+ expanderTitle: { flexShrink: 1, fontWeight: "500" },
4115
+ facts: { flexDirection: "column", gap: 4 },
4116
+ factsRow: { flexDirection: "row", alignItems: "baseline", justifyContent: "space-between", gap: 12 },
4117
+ factsValue: { flexShrink: 1, textAlign: "right", fontWeight: "500" },
4118
+ field: { borderWidth: 1, paddingHorizontal: 12, paddingVertical: 10 },
4119
+ fieldIcon: { position: "absolute", right: 12, top: 0, bottom: 0, justifyContent: "center" },
4120
+ fieldWithIcon: { minHeight: 42, paddingRight: 40 },
4121
+ form: { flexDirection: "column", gap: 12 },
4122
+ header: { fontWeight: "600" },
4123
+ imageBox: { overflow: "hidden" },
4124
+ imageFill: { width: "100%", height: "100%" },
4125
+ imagePlaceholder: {
4126
+ minHeight: 80,
4127
+ width: "100%",
4128
+ flexDirection: "column",
4129
+ alignItems: "center",
4130
+ justifyContent: "center",
4131
+ gap: 6,
4132
+ borderWidth: 1,
4133
+ borderStyle: "dashed",
4134
+ paddingHorizontal: 12,
4135
+ paddingVertical: 16
4136
+ },
4137
+ label: { fontSize: 12, fontWeight: "500" },
4138
+ labelRow: { flexDirection: "row", alignItems: "center", gap: 4, marginBottom: 4 },
4139
+ nameless: { borderWidth: 1, borderStyle: "dashed", borderRadius: 4, paddingHorizontal: 12, paddingVertical: 8 },
4140
+ namelessText: { fontSize: 12 },
4141
+ pill: { borderWidth: 1, borderRadius: 9999, paddingHorizontal: 14, paddingVertical: 6 },
4142
+ pillLabel: { fontWeight: "500" },
4143
+ pillRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
4144
+ placeholderText: { maxWidth: "100%", fontSize: 12 },
4145
+ ratingRow: { flexDirection: "row", alignItems: "center", gap: 6 },
4146
+ row: { flexDirection: "row" },
4147
+ selectMenu: { borderWidth: 1, marginTop: 4, paddingVertical: 4 },
4148
+ selectOption: { flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 12, paddingVertical: 8 },
4149
+ selectOptionLabel: { flex: 1 },
4150
+ selectTrigger: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, borderWidth: 1, paddingHorizontal: 12, paddingVertical: 10 },
4151
+ selectValue: { flex: 1 },
4152
+ stepDot: { width: 20, height: 20, flexShrink: 0, alignItems: "center", justifyContent: "center", borderWidth: 1.5, borderRadius: 9999 },
4153
+ stepIndex: { fontSize: 10, fontWeight: "600" },
4154
+ stepLabel: { flexShrink: 1 },
4155
+ stepRow: { flexDirection: "row", alignItems: "center", gap: 10 },
4156
+ stepsCompact: { gap: 8 },
4157
+ stepsStretch: { justifyContent: "space-between" },
4158
+ table: { overflow: "hidden", borderWidth: 1 },
4159
+ tableCell: { flex: 1, paddingHorizontal: 12, paddingVertical: 8 },
4160
+ tableHeaderText: { fontSize: 12, fontWeight: "600" },
4161
+ textarea: { minHeight: TEXTAREA_MIN_HEIGHT, textAlignVertical: "top" },
4162
+ unsupported: { fontSize: 15 }
4163
+ });
4164
+ var blocks_step_default = BlocksStep;
4165
+
4166
+ // src/components/feedback-screen.tsx
4167
+ var import_jsx_runtime19 = require("react/jsx-runtime");
4168
+ var DEFAULT_STAR_FILLED_COLOR = "#F5A623";
4169
+ var DEFAULT_STAR_EMPTY_COLOR = "#D1D5DB";
4170
+ var PLACEHOLDER_COLOR = "#9CA3AF";
4171
+ var DEFAULT_STARS_COUNT = 5;
4172
+ var STAR_SIZE = 32;
4173
+ var COMMENT_MAX_LENGTH = 500;
4174
+ var COMMENT_RADIUS_FALLBACK = 16;
4175
+ var SUBMIT_HEIGHT = 44;
4176
+ var STAR_PATH = "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z";
4177
+ var STAR_VIEW_BOX = 24;
4178
+ var STAR_STROKE_WIDTH = 1.5;
4179
+ var STAR_PRESS_SCALE = 1.1;
4180
+ var STAR_PRESS_SPRING = { stiffness: 400, damping: 22, mass: 1 };
4181
+ var StarIcon = ({ filled, filledColor, emptyColor, size = STAR_SIZE, accessibilityLabel, onPress }) => {
4182
+ const scale = (0, import_react15.useRef)(new import_react_native17.Animated.Value(1)).current;
4183
+ const springTo = (toValue) => {
4184
+ import_react_native17.Animated.spring(scale, { toValue, ...STAR_PRESS_SPRING, useNativeDriver: true }).start();
4185
+ };
4186
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4187
+ Touchable,
4188
+ {
4189
+ onPress,
4190
+ onPressIn: () => springTo(STAR_PRESS_SCALE),
4191
+ onPressOut: () => springTo(1),
4192
+ testID: ringgId("feedback-star"),
4193
+ accessibilityLabel,
4194
+ style: styles16.star,
4195
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Animated.View, { style: { transform: [{ scale }] }, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native_svg4.default, { width: size, height: size, viewBox: `0 0 ${STAR_VIEW_BOX} ${STAR_VIEW_BOX}`, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4196
+ import_react_native_svg4.Path,
4197
+ {
4198
+ d: STAR_PATH,
4199
+ fill: filled ? filledColor : "none",
4200
+ stroke: filled ? filledColor : emptyColor,
4201
+ strokeWidth: STAR_STROKE_WIDTH,
4202
+ strokeLinecap: "round",
4203
+ strokeLinejoin: "round"
4204
+ }
4205
+ ) }) })
4206
+ }
4207
+ );
4208
+ };
4209
+ var noopApi = async () => ({ success: true, message: "" });
4210
+ var TRANSCRIPT_STAR_SIZE = 28;
4211
+ var FeedbackScreen = ({ callMode, isSubmitting, feedbackScreenConfig, messages, title, logoUrl, logoStyles, onSubmit, onSkip }) => {
4212
+ const theme = useWidgetTheme();
4213
+ const fontFamily = resolveFontFamily(theme.fontFamily);
4214
+ const [rating, setRating] = (0, import_react15.useState)(0);
4215
+ const [comment, setComment] = (0, import_react15.useState)("");
4216
+ const [isCommentFocused, setIsCommentFocused] = (0, import_react15.useState)(false);
4217
+ const starsCount = feedbackScreenConfig?.starsCount ?? DEFAULT_STARS_COUNT;
4218
+ const stars = Array.from({ length: starsCount }, (_, index) => index + 1);
4219
+ const starFilledColor = feedbackScreenConfig?.starsStyles?.filledColor ?? DEFAULT_STAR_FILLED_COLOR;
4220
+ const starEmptyColor = feedbackScreenConfig?.starsStyles?.emptyColor ?? DEFAULT_STAR_EMPTY_COLOR;
4221
+ const isSubmitDisabled = rating === 0 || isSubmitting;
4222
+ const submitOverrides = toViewStyle(feedbackScreenConfig?.submitBtnStyles);
4223
+ const submitFill = typeof submitOverrides.backgroundColor === "string" ? submitOverrides.backgroundColor : isSubmitDisabled ? theme.surfaceColor : theme.primaryColor;
4224
+ const handleSubmit = () => {
4225
+ if (rating > 0) onSubmit(rating, comment);
4226
+ };
4227
+ const transcript = (0, import_react15.useMemo)(
4228
+ () => (messages ?? []).filter((message) => message.kind !== "system" && (message.componentType ? Boolean(message.componentData) : Boolean(message.message.trim()))),
4229
+ [messages]
4230
+ );
4231
+ const scroller = (0, import_react15.useRef)(null);
4232
+ if (transcript.length > 0) {
4233
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native17.View, { testID: ringgId("feedback-screen"), style: styles16.transcriptScreen, children: [
4234
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(header_default, { isCalling: true, isLoading: false, title: title ?? "", description: "", logoUrl, logoStyles }),
4235
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4236
+ import_react_native17.ScrollView,
4237
+ {
4238
+ testID: ringgId("transcript"),
4239
+ ref: scroller,
4240
+ style: styles16.transcript,
4241
+ contentContainerStyle: styles16.transcriptList,
4242
+ onContentSizeChange: () => scroller.current?.scrollToEnd({ animated: false }),
4243
+ showsVerticalScrollIndicator: false,
4244
+ children: [
4245
+ transcript.map((message, index) => {
4246
+ if (message.componentType && message.componentData) {
4247
+ const componentData = message.componentData;
4248
+ if (isBlocks(componentData)) {
4249
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { testID: ringgId("blocks-widget"), style: styles16.replayed, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(blocks_step_default, { config: componentData.data, disabled: true, onAction: async () => {
4250
+ } }) }, `c_${message.timestamp}__${index}`);
4251
+ }
4252
+ return (
4253
+ // pointerEvents none is what makes the replay read-only — the
4254
+ // flow renders exactly as it did, and answers nothing.
4255
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { pointerEvents: "none", style: styles16.replayed, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(interactive_flow_default, { initialComponent: componentData, onApiCall: noopApi }) }, `c_${message.timestamp}__${index}`)
4256
+ );
4257
+ }
4258
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(message_item_default, { itemIndex: index, message }, `${message.timestamp}__${index}`);
4259
+ }),
4260
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native17.View, { testID: ringgId("feedback-footer"), style: styles16.feedbackFooter, children: [
4261
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native17.View, { testID: ringgId("conversation-ended"), style: styles16.endedRow, children: [
4262
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { style: [styles16.endedRule, { backgroundColor: solidColor(theme.borderColor) }] }),
4263
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Text, { style: [styles16.endedLabel, { color: theme.mutedTextColor, fontFamily }], children: "Conversation ended" }),
4264
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { style: [styles16.endedRule, { backgroundColor: solidColor(theme.borderColor) }] })
4265
+ ] }),
4266
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Text, { testID: ringgId("feedback-prompt"), style: [styles16.description, { color: theme.mutedTextColor, fontFamily }], children: feedbackScreenConfig?.title ?? "Please rate your experience" }),
4267
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { testID: ringgId("feedback-stars"), style: styles16.stars, children: stars.map((star) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4268
+ StarIcon,
4269
+ {
4270
+ filled: star <= rating,
4271
+ filledColor: starFilledColor,
4272
+ emptyColor: starEmptyColor,
4273
+ size: TRANSCRIPT_STAR_SIZE,
4274
+ accessibilityLabel: `Rate ${star} of ${starsCount}`,
4275
+ onPress: () => {
4276
+ if (isSubmitting) return;
4277
+ setRating(star);
4278
+ onSubmit(star, "");
4279
+ }
4280
+ },
4281
+ star
4282
+ )) })
4283
+ ] })
4284
+ ]
4285
+ }
4286
+ )
4287
+ ] });
4288
+ }
4289
+ return (
4290
+ // iOS keeps the keyboard over the content; Android resizes the window
4291
+ // itself, and padding on top of that double-counts the inset.
4292
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native17.KeyboardAvoidingView, { testID: ringgId("feedback-screen"), behavior: import_react_native17.Platform.OS === "ios" ? "padding" : void 0, style: styles16.screen, children: [
4293
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native17.View, { testID: ringgId("feedback-header"), style: styles16.header, children: [
4294
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Text, { testID: ringgId("feedback-title"), style: [styles16.title, { color: theme.textColor, fontFamily }], children: feedbackScreenConfig?.title ?? `How was your ${callMode === "text" ? "chat" : "call"}?` }),
4295
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Text, { testID: ringgId("feedback-description"), style: [styles16.description, { color: theme.mutedTextColor, fontFamily }], children: feedbackScreenConfig?.description ?? "Your feedback helps us improve!" })
4296
+ ] }),
4297
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.View, { testID: ringgId("feedback-stars"), style: styles16.stars, children: stars.map((star) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4298
+ StarIcon,
4299
+ {
4300
+ filled: star <= rating,
4301
+ filledColor: starFilledColor,
4302
+ emptyColor: starEmptyColor,
4303
+ accessibilityLabel: `Rate ${star} of ${starsCount}`,
4304
+ onPress: () => setRating(star)
4305
+ },
4306
+ star
4307
+ )) }),
4308
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4309
+ import_react_native17.TextInput,
4310
+ {
4311
+ testID: ringgId("feedback-comment"),
4312
+ value: comment,
4313
+ onChangeText: setComment,
4314
+ placeholder: feedbackScreenConfig?.placeholder ?? "We'd love to hear more...",
4315
+ placeholderTextColor: PLACEHOLDER_COLOR,
4316
+ multiline: true,
4317
+ textAlignVertical: "top",
4318
+ maxLength: COMMENT_MAX_LENGTH,
4319
+ onFocus: () => setIsCommentFocused(true),
4320
+ onBlur: () => setIsCommentFocused(false),
4321
+ style: [
4322
+ styles16.comment,
4323
+ {
4324
+ backgroundColor: solidColor(theme.backgroundColor),
4325
+ color: theme.textColor,
4326
+ // Web's focus ring sits OUTSIDE the border; growing a border here
4327
+ // would reflow the box, so the border itself takes the accent.
4328
+ borderColor: isCommentFocused ? solidColor(theme.primaryColor) : solidColor(theme.borderColor),
4329
+ borderRadius: resolveRadius(theme.borderRadius, COMMENT_RADIUS_FALLBACK),
4330
+ fontFamily
4331
+ }
4332
+ ]
4333
+ }
4334
+ ),
4335
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native17.View, { testID: ringgId("feedback-actions"), style: styles16.actions, children: [
4336
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Touchable, { onPress: handleSubmit, disabled: isSubmitDisabled, testID: ringgId("feedback-submit"), children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(GradientFill, { color: submitFill, style: [styles16.submit, { borderRadius: resolveButtonRadius(theme.buttonStyle) }, submitOverrides], children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Text, { style: [styles16.submitLabel, { color: isSubmitDisabled ? theme.mutedTextColor : theme.primaryTextColor, fontFamily }], numberOfLines: 1, children: feedbackScreenConfig?.submitBtnCTA ?? "Submit Feedback" }) }) }),
4337
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Touchable, { onPress: onSkip, disabled: isSubmitting, testID: ringgId("feedback-skip"), style: styles16.skip, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native17.Text, { style: [styles16.skipLabel, { color: theme.mutedTextColor, fontFamily }], children: "Skip" }) })
4338
+ ] })
4339
+ ] })
4340
+ );
4341
+ };
4342
+ var feedback_screen_default = FeedbackScreen;
4343
+ var styles16 = import_react_native17.StyleSheet.create({
4344
+ screen: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, gap: 16 },
4345
+ // Transcript variant. The header floats over the list the same way it does
4346
+ // during a live call, so the list clears it with padding rather than layout.
4347
+ transcriptScreen: { flex: 1, padding: 8, gap: 8 },
4348
+ transcript: { flex: 1 },
4349
+ transcriptList: { flexGrow: 1, justifyContent: "flex-end", gap: 12, paddingTop: 48 },
4350
+ replayed: { width: "100%" },
4351
+ feedbackFooter: { gap: 10, paddingHorizontal: 8, paddingVertical: 8 },
4352
+ endedRow: { flexDirection: "row", alignItems: "center", gap: 12 },
4353
+ endedRule: { flex: 1, height: import_react_native17.StyleSheet.hairlineWidth },
4354
+ endedLabel: { fontSize: 13 },
4355
+ header: { alignItems: "center" },
4356
+ title: { fontSize: 18, lineHeight: 28, fontWeight: "500", textAlign: "center", marginBottom: 4 },
4357
+ description: { fontSize: 15, lineHeight: 22, textAlign: "center" },
4358
+ stars: { flexDirection: "row", gap: 4, marginVertical: 8 },
4359
+ star: { padding: 4 },
4360
+ comment: { alignSelf: "stretch", minHeight: 80, padding: 12, fontSize: 15, borderWidth: 1 },
4361
+ actions: { alignSelf: "stretch", gap: 4, marginTop: 8 },
4362
+ submit: { height: SUBMIT_HEIGHT, alignItems: "center", justifyContent: "center" },
4363
+ submitLabel: { fontSize: 15, fontWeight: "500" },
4364
+ skip: { paddingVertical: 8, alignItems: "center" },
4365
+ skipLabel: { fontSize: 15 }
4366
+ });
4367
+
4368
+ // src/components/loading-bar.tsx
4369
+ var import_react16 = require("react");
4370
+ var import_react_native18 = require("react-native");
4371
+ var import_jsx_runtime20 = require("react/jsx-runtime");
4372
+ var SWEEP_DURATION_MS = 1400;
4373
+ var MIDPOINT = 0.4;
4374
+ var KEYFRAMES = [0, MIDPOINT, 1];
4375
+ var TRAVEL_RATIOS = [-1, 0, 2.5];
4376
+ var STRETCH_SCALES = [0.4, 0.6, 0.4];
4377
+ var SEGMENT_EASING = import_react_native18.Easing.bezier(0.65, 0, 0.35, 1);
4378
+ var SWEEP_EASING = (t) => t < MIDPOINT ? MIDPOINT * SEGMENT_EASING(t / MIDPOINT) : MIDPOINT + (1 - MIDPOINT) * SEGMENT_EASING((t - MIDPOINT) / (1 - MIDPOINT));
4379
+ var FILL_WIDTH_RATIO = 1 / 3;
4380
+ var ROW_HEIGHT = 48;
4381
+ var TRACK_HEIGHT = 6;
4382
+ var PILL_RADIUS4 = 9999;
4383
+ var LoadingBar = ({ ariaLabel = "Connecting", style }) => {
4384
+ const theme = useWidgetTheme();
4385
+ const [trackWidth, setTrackWidth] = (0, import_react16.useState)(0);
4386
+ const progress = (0, import_react16.useRef)(new import_react_native18.Animated.Value(0)).current;
4387
+ (0, import_react16.useEffect)(() => {
4388
+ const sweep = import_react_native18.Animated.loop(import_react_native18.Animated.timing(progress, { toValue: 1, duration: SWEEP_DURATION_MS, easing: SWEEP_EASING, useNativeDriver: true }));
4389
+ sweep.start();
4390
+ return () => sweep.stop();
4391
+ }, [progress]);
4392
+ const onTrackLayout = (event) => setTrackWidth(event.nativeEvent.layout.width);
4393
+ const fillWidth = trackWidth * FILL_WIDTH_RATIO;
4394
+ const translateX = progress.interpolate({ inputRange: KEYFRAMES, outputRange: TRAVEL_RATIOS.map((ratio) => ratio * fillWidth) });
4395
+ const scaleX = progress.interpolate({ inputRange: KEYFRAMES, outputRange: STRETCH_SCALES });
4396
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native18.View, { testID: ringgId("loading-bar"), style: [styles17.container, style], children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4397
+ import_react_native18.View,
4398
+ {
4399
+ testID: ringgId("loading-bar-track"),
4400
+ accessibilityRole: "progressbar",
4401
+ accessibilityLabel: ariaLabel,
4402
+ accessibilityState: { busy: true },
4403
+ onLayout: onTrackLayout,
4404
+ style: [styles17.track, { backgroundColor: solidColor(theme.borderColor) }],
4405
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native18.Animated.View, { testID: ringgId("loading-bar-fill"), style: [styles17.fill, { width: fillWidth, transform: [{ translateX }, { scaleX }] }], children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GradientFill, { color: theme.primaryColor, style: styles17.fillSurface }) })
4406
+ }
4407
+ ) });
4408
+ };
4409
+ var loading_bar_default = LoadingBar;
4410
+ var styles17 = import_react_native18.StyleSheet.create({
4411
+ container: { height: ROW_HEIGHT, flexDirection: "row", alignItems: "center", justifyContent: "center" },
4412
+ track: { height: TRACK_HEIGHT, width: "90%", borderRadius: PILL_RADIUS4, overflow: "hidden" },
4413
+ fill: { position: "absolute", top: 0, bottom: 0, left: 0, borderRadius: PILL_RADIUS4 },
4414
+ fillSurface: { ...import_react_native18.StyleSheet.absoluteFill, borderRadius: PILL_RADIUS4 }
4415
+ });
4416
+
4417
+ // src/components/system-log.tsx
4418
+ var import_react_native19 = require("react-native");
4419
+ var import_jsx_runtime21 = require("react/jsx-runtime");
4420
+ var INFO_DOT_COLOR = "#22c55e";
4421
+ var ERROR_DOT_COLOR = "#ef4444";
4422
+ var MONOSPACE_FAMILY = import_react_native19.Platform.select({ ios: "Menlo", android: "monospace" });
4423
+ var dotColorFor = (level) => {
4424
+ switch (level) {
4425
+ case "error":
4426
+ return ERROR_DOT_COLOR;
4427
+ case "info":
4428
+ default:
4429
+ return INFO_DOT_COLOR;
4430
+ }
4431
+ };
4432
+ var SystemLog = ({ label, meta, level = "info" }) => {
4433
+ const theme = useWidgetTheme();
4434
+ const fontFamily = resolveFontFamily(theme.fontFamily);
4435
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native19.View, { testID: ringgId("system-log"), style: styles18.row, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4436
+ import_react_native19.View,
4437
+ {
4438
+ testID: ringgId("system-log-pill"),
4439
+ accessibilityRole: "text",
4440
+ accessibilityLiveRegion: "polite",
4441
+ style: [styles18.pill, { borderColor: solidColor(theme.borderColor), backgroundColor: theme.backgroundColor }],
4442
+ children: [
4443
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native19.View, { testID: ringgId("system-log-dot"), style: [styles18.dot, { backgroundColor: dotColorFor(level) }] }),
4444
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native19.Text, { testID: ringgId("system-log-label"), numberOfLines: 1, style: [styles18.label, { color: theme.textColor, fontFamily }], children: label }),
4445
+ meta ? (
4446
+ // Web puts the full string in a `title` tooltip for the truncated case.
4447
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native19.Text, { testID: ringgId("system-log-meta"), numberOfLines: 1, accessibilityLabel: meta, style: [styles18.meta, { color: theme.mutedTextColor }], children: meta })
4448
+ ) : null
4449
+ ]
4450
+ }
4451
+ ) });
4452
+ };
4453
+ var system_log_default = SystemLog;
4454
+ var styles18 = import_react_native19.StyleSheet.create({
4455
+ row: { flexDirection: "row", justifyContent: "center", marginVertical: 4 },
4456
+ pill: {
4457
+ flexDirection: "row",
4458
+ alignItems: "center",
4459
+ gap: 8,
4460
+ paddingHorizontal: 12,
4461
+ paddingVertical: 4,
4462
+ borderRadius: 9999,
4463
+ borderWidth: 1,
4464
+ // `max-w-full`: the pill may shrink, but never past the transcript width.
4465
+ flexShrink: 1
4466
+ },
4467
+ dot: { width: 6, height: 6, borderRadius: 3, flexShrink: 0 },
4468
+ label: { fontSize: 11, fontWeight: "500", flexShrink: 1 },
4469
+ meta: { fontSize: 11, fontFamily: MONOSPACE_FAMILY, flexShrink: 1 }
4470
+ });
4471
+
4472
+ // src/components/typing-indicator.tsx
4473
+ var import_react17 = require("react");
4474
+ var import_react_native20 = require("react-native");
4475
+ var import_jsx_runtime22 = require("react/jsx-runtime");
4476
+ var DEFAULT_TYPING_WORDS = ["Thinking", "Reasoning", "Working on it", "Almost there"];
4477
+ var WORD_INTERVAL_MS = 2200;
4478
+ var SWAP_MS = 250;
4479
+ var SWAP_OFFSET = 2;
4480
+ var WORD_TINT = 0.45;
4481
+ var DOT_COUNT = 3;
4482
+ var DOT_CYCLE_MS = 1200;
4483
+ var DOT_STAGGER_MS = 160;
4484
+ var DOT_RISE_MS = 300;
4485
+ var DOT_LIFT = 2;
4486
+ var DOT_DIM_OPACITY = 0.4;
4487
+ var DOT_SLOTS = Array.from({ length: DOT_COUNT }, (_, index) => index);
4488
+ var mixColors = (color, base, amount) => {
4489
+ const from = (0, import_react_native20.processColor)(color);
4490
+ const to = (0, import_react_native20.processColor)(base);
4491
+ if (typeof from !== "number" || typeof to !== "number") return base;
4492
+ const channel = (shift) => Math.round((from >>> shift & 255) * amount + (to >>> shift & 255) * (1 - amount));
4493
+ return `rgba(${channel(16)}, ${channel(8)}, ${channel(0)}, ${(channel(24) / 255).toFixed(3)})`;
4494
+ };
4495
+ var swapTo = (fade, lift, opacity, offset) => import_react_native20.Animated.parallel([
4496
+ import_react_native20.Animated.timing(fade, { toValue: opacity, duration: SWAP_MS, useNativeDriver: true }),
4497
+ import_react_native20.Animated.timing(lift, { toValue: offset, duration: SWAP_MS, useNativeDriver: true })
4498
+ ]);
4499
+ var TypingDot = ({ index, color, fontFamily }) => {
4500
+ const bounce = (0, import_react17.useRef)(new import_react_native20.Animated.Value(0)).current;
4501
+ (0, import_react17.useEffect)(() => {
4502
+ const loop = import_react_native20.Animated.loop(
4503
+ import_react_native20.Animated.sequence([
4504
+ import_react_native20.Animated.delay(index * DOT_STAGGER_MS),
4505
+ import_react_native20.Animated.timing(bounce, { toValue: 1, duration: DOT_RISE_MS, useNativeDriver: true }),
4506
+ import_react_native20.Animated.timing(bounce, { toValue: 0, duration: DOT_RISE_MS, useNativeDriver: true }),
4507
+ import_react_native20.Animated.delay(DOT_CYCLE_MS - index * DOT_STAGGER_MS - 2 * DOT_RISE_MS)
4508
+ ])
4509
+ );
4510
+ loop.start();
4511
+ return () => loop.stop();
4512
+ }, [bounce, index]);
4513
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4514
+ import_react_native20.Animated.Text,
4515
+ {
4516
+ style: [
4517
+ styles19.text,
4518
+ {
4519
+ color,
4520
+ fontFamily,
4521
+ opacity: bounce.interpolate({ inputRange: [0, 1], outputRange: [DOT_DIM_OPACITY, 1] }),
4522
+ transform: [{ translateY: bounce.interpolate({ inputRange: [0, 1], outputRange: [0, -DOT_LIFT] }) }]
4523
+ }
4524
+ ],
4525
+ children: "."
4526
+ }
4527
+ );
4528
+ };
4529
+ var TypingIndicator = ({ words }) => {
4530
+ const theme = useWidgetTheme();
4531
+ const list = words && words.length > 0 ? words : DEFAULT_TYPING_WORDS;
4532
+ const [index, setIndex] = (0, import_react17.useState)(0);
4533
+ const fade = (0, import_react17.useRef)(new import_react_native20.Animated.Value(0)).current;
4534
+ const lift = (0, import_react17.useRef)(new import_react_native20.Animated.Value(SWAP_OFFSET)).current;
4535
+ (0, import_react17.useEffect)(() => {
4536
+ const enter = swapTo(fade, lift, 1, 0);
4537
+ enter.start();
4538
+ return () => enter.stop();
4539
+ }, [fade, lift]);
4540
+ (0, import_react17.useEffect)(() => {
4541
+ const count = list.length;
4542
+ if (count <= 1) return;
4543
+ let swap;
4544
+ const id = setInterval(() => {
4545
+ swap = swapTo(fade, lift, 0, -SWAP_OFFSET);
4546
+ swap.start(({ finished }) => {
4547
+ if (!finished) return;
4548
+ setIndex((current) => (current + 1) % count);
4549
+ lift.setValue(SWAP_OFFSET);
4550
+ swap = swapTo(fade, lift, 1, 0);
4551
+ swap.start();
4552
+ });
4553
+ }, WORD_INTERVAL_MS);
4554
+ return () => {
4555
+ clearInterval(id);
4556
+ swap?.stop();
4557
+ fade.setValue(1);
4558
+ lift.setValue(0);
4559
+ };
4560
+ }, [fade, lift, list.length]);
4561
+ const word = list[index % list.length] ?? "";
4562
+ const color = mixColors(solidColor(theme.primaryColor), solidColor(theme.mutedTextColor), WORD_TINT);
4563
+ const fontFamily = resolveFontFamily(theme.fontFamily);
4564
+ return (
4565
+ // RN has no `role="status"`; an accessible container with a live region is
4566
+ // how both platforms announce a changing status without reading each word.
4567
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native20.View, { testID: ringgId("typing-indicator"), accessible: true, accessibilityLiveRegion: "polite", accessibilityLabel: "Agent is thinking", style: styles19.row, children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_react_native20.Animated.View, { testID: ringgId("typing-indicator-word"), style: [styles19.word, { opacity: fade, transform: [{ translateY: lift }] }], children: [
4568
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native20.Text, { style: [styles19.text, { color, fontFamily }], children: word }),
4569
+ DOT_SLOTS.map((slot) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TypingDot, { index: slot, color, fontFamily }, slot))
4570
+ ] }) })
4571
+ );
4572
+ };
4573
+ var typing_indicator_default = TypingIndicator;
4574
+ var styles19 = import_react_native20.StyleSheet.create({
4575
+ /** `mr-auto` keeps the indicator hugging the left edge of the transcript. */
4576
+ row: { flexDirection: "row", alignItems: "center", alignSelf: "flex-start", paddingVertical: 8 },
4577
+ word: { flexDirection: "row", alignItems: "center" },
4578
+ text: { fontSize: 14, lineHeight: 20, fontWeight: "500" }
4579
+ });
4580
+
4581
+ // src/components/voice-leaves.tsx
4582
+ var import_react19 = require("react");
4583
+ var import_react_native23 = require("react-native");
4584
+ var import_livekit_client2 = require("livekit-client");
4585
+
4586
+ // src/transport/agent.ts
4587
+ var import_livekit_client = require("livekit-client");
4588
+ function isAgentIdentity(identity) {
4589
+ return identity?.includes("agent") ?? false;
4590
+ }
4591
+ function isAgentParticipant(participant) {
4592
+ return participant.kind === import_livekit_client.ParticipantKind.AGENT || isAgentIdentity(participant.identity);
4593
+ }
4594
+ function findAgentParticipant(room) {
4595
+ const remotes = Array.from(room.remoteParticipants.values());
4596
+ return remotes.find((p) => p.kind === import_livekit_client.ParticipantKind.AGENT) ?? remotes.find((p) => isAgentIdentity(p.identity));
4597
+ }
4598
+
4599
+ // src/components/dot-audio-visualizer.tsx
4600
+ var import_react18 = require("react");
4601
+ var import_react_native21 = require("react-native");
4602
+ var import_react_native22 = require("@livekit/react-native");
4603
+ var import_jsx_runtime23 = require("react/jsx-runtime");
4604
+ var BANDS = 22;
4605
+ var MIN_ROWS = 5;
4606
+ var MAX_ROWS = 21;
4607
+ var PX_PER_ROW = 22;
4608
+ var DOT_SIZE = 8;
4609
+ var UNLIT_OPACITY = 0.15;
4610
+ var UNLIT_SCALE = 0.6;
4611
+ var DOT_SPRING = { stiffness: 220, damping: 22, mass: 1 };
4612
+ var AMBIENT_TICK_MS = 40;
4613
+ var WEB_STEP_PER_FRAME = 0.04;
4614
+ var WEB_FRAMES_PER_SECOND = 60;
4615
+ var AMBIENT_STEP = WEB_STEP_PER_FRAME * WEB_FRAMES_PER_SECOND / (1e3 / AMBIENT_TICK_MS);
4616
+ var AMBIENT_FLOOR = 0.15;
4617
+ var AMBIENT_SWING = 0.25;
4618
+ var AMBIENT_PHASE = 0.5;
4619
+ var rowsForHeight = (height) => {
4620
+ const raw = Math.max(MIN_ROWS, Math.min(MAX_ROWS, Math.floor(height / PX_PER_ROW)));
4621
+ return raw % 2 === 0 ? raw - 1 : raw;
4622
+ };
4623
+ var useResponsiveRows = () => {
4624
+ const [rows, setRows] = (0, import_react18.useState)(MIN_ROWS);
4625
+ const onLayout = (0, import_react18.useCallback)((event) => {
4626
+ setRows(rowsForHeight(event.nativeEvent.layout.height));
4627
+ }, []);
4628
+ return { rows, onLayout };
4629
+ };
4630
+ var useAmbientWave = (bands, enabled) => {
4631
+ const [vals, setVals] = (0, import_react18.useState)(() => Array.from({ length: bands }, () => AMBIENT_FLOOR));
4632
+ (0, import_react18.useEffect)(() => {
4633
+ if (!enabled) return;
4634
+ let t = 0;
4635
+ const id = setInterval(() => {
4636
+ t += AMBIENT_STEP;
4637
+ setVals(Array.from({ length: bands }, (_, i) => AMBIENT_FLOOR + AMBIENT_SWING * Math.abs(Math.sin(t + i * AMBIENT_PHASE))));
4638
+ }, AMBIENT_TICK_MS);
4639
+ return () => clearInterval(id);
4640
+ }, [bands, enabled]);
4641
+ return vals;
4642
+ };
4643
+ var Dot = (0, import_react18.memo)(({ lit, color }) => {
4644
+ const progress = (0, import_react18.useRef)(new import_react_native21.Animated.Value(lit ? 1 : 0)).current;
4645
+ (0, import_react18.useEffect)(() => {
4646
+ const spring = import_react_native21.Animated.spring(progress, { toValue: lit ? 1 : 0, ...DOT_SPRING, useNativeDriver: true });
4647
+ spring.start();
4648
+ return () => spring.stop();
4649
+ }, [lit, progress]);
4650
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4651
+ import_react_native21.Animated.View,
4652
+ {
4653
+ testID: ringgId("voice-animation-dot"),
4654
+ style: [
4655
+ styles20.dot,
4656
+ {
4657
+ backgroundColor: color,
4658
+ // Spring overshoot is welcome on scale (it gives the pop) but must
4659
+ // not push opacity past opaque.
4660
+ opacity: progress.interpolate({ inputRange: [0, 1], outputRange: [UNLIT_OPACITY, 1], extrapolate: "clamp" }),
4661
+ transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [UNLIT_SCALE, 1] }) }]
4662
+ }
4663
+ ]
4664
+ }
4665
+ );
4666
+ });
4667
+ var DotColumn = ({ volume, color, rows }) => {
4668
+ const centerIdx = Math.floor(rows / 2);
4669
+ const perceived = Math.pow(Math.min(1, volume), 0.4);
4670
+ const litRadius = perceived * (centerIdx + 1);
4671
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native21.View, { testID: ringgId("voice-animation-column"), style: styles20.column, children: Array.from({ length: rows }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(Dot, { lit: Math.abs(i - centerIdx) <= litRadius, color }, i)) });
4672
+ };
4673
+ var DotAudioVisualizer = ({ localTrack, remoteTrack, style }) => {
4674
+ const theme = useWidgetTheme();
4675
+ const dotColor = solidColor(theme.primaryColor);
4676
+ const localVols = (0, import_react_native22.useMultibandTrackVolume)(localTrack, { bands: BANDS });
4677
+ const remoteVols = (0, import_react_native22.useMultibandTrackVolume)(remoteTrack, { bands: BANDS });
4678
+ const hasLiveTrack = Boolean(localTrack || remoteTrack);
4679
+ const ambient = useAmbientWave(BANDS, !hasLiveTrack);
4680
+ const liveVolumes = (0, import_react18.useMemo)(() => Array.from({ length: BANDS }, (_, i) => Math.max(localVols[i] ?? 0, remoteVols[i] ?? 0)), [localVols, remoteVols]);
4681
+ const volumes = hasLiveTrack ? liveVolumes : ambient;
4682
+ const { rows, onLayout } = useResponsiveRows();
4683
+ return (
4684
+ // Decorative, so hidden from both screen readers — the web leaf is
4685
+ // `aria-hidden`, which takes one prop per platform here.
4686
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
4687
+ import_react_native21.View,
4688
+ {
4689
+ testID: ringgId("voice-animation"),
4690
+ accessibilityElementsHidden: true,
4691
+ importantForAccessibility: "no-hide-descendants",
4692
+ onLayout,
4693
+ style: [styles20.container, style],
4694
+ children: volumes.map((vol, i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DotColumn, { volume: vol, color: dotColor, rows }, i))
4695
+ }
4696
+ )
4697
+ );
4698
+ };
4699
+ var dot_audio_visualizer_default = DotAudioVisualizer;
4700
+ var styles20 = import_react_native21.StyleSheet.create({
4701
+ container: { flexDirection: "row", width: "100%", height: "100%", alignItems: "stretch", justifyContent: "space-around" },
4702
+ column: { flexDirection: "column", alignItems: "center", justifyContent: "space-around" },
4703
+ dot: { width: DOT_SIZE, height: DOT_SIZE, borderRadius: DOT_SIZE / 2 }
4704
+ });
4705
+
4706
+ // src/components/voice-leaves.tsx
4707
+ var import_jsx_runtime24 = require("react/jsx-runtime");
4708
+ var TRACK_EVENTS = [import_livekit_client2.RoomEvent.TrackSubscribed, import_livekit_client2.RoomEvent.TrackUnsubscribed, import_livekit_client2.RoomEvent.LocalTrackPublished, import_livekit_client2.RoomEvent.LocalTrackUnpublished, import_livekit_client2.RoomEvent.ParticipantConnected];
4709
+ var resolveTracks = (room) => {
4710
+ if (!room) return {};
4711
+ const agent = Array.from(room.remoteParticipants.values()).find(isAgentParticipant);
4712
+ return {
4713
+ localTrack: room.localParticipant.getTrackPublication(import_livekit_client2.Track.Source.Microphone)?.track,
4714
+ remoteTrack: agent?.getTrackPublication(import_livekit_client2.Track.Source.Microphone)?.track
4715
+ };
4716
+ };
4717
+ var useMicrophoneTracks = (room) => {
4718
+ const [tracks, setTracks] = (0, import_react19.useState)(() => resolveTracks(room));
4719
+ (0, import_react19.useEffect)(() => {
4720
+ setTracks(resolveTracks(room));
4721
+ if (!room) return;
4722
+ const update = () => setTracks(resolveTracks(room));
4723
+ for (const event of TRACK_EVENTS) room.on(event, update);
4724
+ return () => {
4725
+ for (const event of TRACK_EVENTS) room.off(event, update);
4726
+ };
4727
+ }, [room]);
4728
+ return tracks;
4729
+ };
4730
+ var VoiceAnimationLeaf = ({ room, style }) => {
4731
+ const { localTrack, remoteTrack } = useMicrophoneTracks(room);
4732
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(dot_audio_visualizer_default, { localTrack, remoteTrack, style: [styles21.fill, style] });
4733
+ };
4734
+ var styles21 = import_react_native23.StyleSheet.create({
4735
+ fill: { flex: 1 }
4736
+ });
4737
+
4738
+ // src/components/ui/dialog.tsx
4739
+ var import_react20 = require("react");
4740
+ var import_react_native25 = require("react-native");
4741
+
4742
+ // src/components/ui/button.tsx
4743
+ var import_react_native24 = require("react-native");
4744
+ var import_jsx_runtime25 = require("react/jsx-runtime");
4745
+ var SIZES = {
4746
+ default: { height: 36, paddingHorizontal: 16 },
4747
+ sm: { height: 32, paddingHorizontal: 12 },
4748
+ lg: { height: 40, paddingHorizontal: 32 },
4749
+ icon: { height: 36, width: 36 }
4750
+ };
4751
+ var TEXT_SIZES = { default: 14, sm: 12, lg: 14, icon: 14 };
4752
+ var surfaceFor = (variant, theme) => {
4753
+ switch (variant) {
4754
+ case "outline":
4755
+ return { background: theme.surfaceColor, foreground: theme.textColor, borderColor: solidColor(theme.borderColor) };
4756
+ case "ghost":
4757
+ return { background: "transparent", foreground: theme.mutedTextColor };
4758
+ case "destructive":
4759
+ return { background: theme.errorColor, foreground: "#ffffff" };
4760
+ case "default":
4761
+ default:
4762
+ return { background: theme.primaryColor, foreground: theme.primaryTextColor };
4763
+ }
4764
+ };
4765
+ var Button = ({ variant = "default", size = "default", onPress, disabled = false, style, textStyle, testID, accessibilityLabel, children }) => {
4766
+ const theme = useWidgetTheme();
4767
+ const surface = surfaceFor(variant, theme);
4768
+ const content = typeof children === "string" ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react_native24.Text, { style: [styles22.label, { color: surface.foreground, fontSize: TEXT_SIZES[size], fontFamily: resolveFontFamily(theme.fontFamily) }, textStyle], numberOfLines: 1, children }) : children;
4769
+ const body = variant === "default" ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(GradientFill, { color: surface.background, style: [styles22.base, SIZES[size], style], children: content }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_react_native24.View, { style: [styles22.base, SIZES[size], { backgroundColor: surface.background }, surface.borderColor ? { borderWidth: import_react_native24.StyleSheet.hairlineWidth, borderColor: surface.borderColor } : null, style], children: content });
4770
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Touchable, { onPress, disabled, testID, accessibilityLabel, children: body });
4771
+ };
4772
+ var styles22 = import_react_native24.StyleSheet.create({
4773
+ base: {
4774
+ flexDirection: "row",
4775
+ alignItems: "center",
4776
+ justifyContent: "center",
4777
+ gap: 8,
4778
+ borderRadius: 6
4779
+ },
4780
+ label: { fontWeight: "500" }
4781
+ });
4782
+
4783
+ // src/components/ui/dialog.tsx
4784
+ var import_jsx_runtime26 = require("react/jsx-runtime");
4785
+ var SCRIM_OPACITY2 = 0.4;
4786
+ var CARD_MAX_WIDTH = 448;
4787
+ var CARD_RADIUS_FALLBACK = 16;
4788
+ var CARD_SPRING = { stiffness: 420, damping: 30, mass: 1 };
4789
+ var CARD_ENTER_SCALE = 0.94;
4790
+ var CARD_ENTER_OFFSET2 = 8;
4791
+ var ConfirmDialog = ({ open, title, description, confirmLabel, cancelLabel, onConfirm, onCancel }) => {
4792
+ const theme = useWidgetTheme();
4793
+ const fontFamily = resolveFontFamily(theme.fontFamily);
4794
+ const buttonRadius = resolveButtonRadius(theme.buttonStyle);
4795
+ const enter = (0, import_react20.useRef)(new import_react_native25.Animated.Value(0)).current;
4796
+ (0, import_react20.useEffect)(() => {
4797
+ if (!open) {
4798
+ enter.setValue(0);
4799
+ return;
4800
+ }
4801
+ const pop = import_react_native25.Animated.spring(enter, { toValue: 1, ...CARD_SPRING, useNativeDriver: true });
4802
+ pop.start();
4803
+ return () => pop.stop();
4804
+ }, [open, enter]);
4805
+ (0, import_react20.useEffect)(() => {
4806
+ if (!open) return;
4807
+ const subscription = import_react_native25.BackHandler.addEventListener("hardwareBackPress", () => {
4808
+ onCancel();
4809
+ return true;
4810
+ });
4811
+ return () => subscription.remove();
4812
+ }, [open, onCancel]);
4813
+ if (!open) return null;
4814
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native25.View, { style: styles23.root, children: [
4815
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native25.View, { pointerEvents: "none", style: [import_react_native25.StyleSheet.absoluteFill, styles23.scrim, { backgroundColor: solidColor(theme.backgroundColor) }] }),
4816
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
4817
+ import_react_native25.Animated.View,
4818
+ {
4819
+ testID: ringgId("end-call-dialog"),
4820
+ accessibilityViewIsModal: true,
4821
+ style: [
4822
+ styles23.card,
4823
+ {
4824
+ opacity: enter,
4825
+ transform: [{ scale: enter.interpolate({ inputRange: [0, 1], outputRange: [CARD_ENTER_SCALE, 1] }) }, { translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [CARD_ENTER_OFFSET2, 0] }) }],
4826
+ backgroundColor: solidColor(theme.backgroundColor),
4827
+ borderColor: solidColor(theme.borderColor),
4828
+ borderRadius: resolveRadius(theme.borderRadius, CARD_RADIUS_FALLBACK)
4829
+ }
4830
+ ],
4831
+ children: [
4832
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native25.View, { testID: ringgId("end-call-dialog-header"), style: styles23.header, children: [
4833
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native25.Text, { testID: ringgId("end-call-dialog-title"), style: [styles23.title, { color: theme.textColor, fontFamily }], children: title }),
4834
+ description ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react_native25.Text, { testID: ringgId("end-call-dialog-description"), style: [styles23.description, { color: theme.mutedTextColor, fontFamily }], children: description }) : null
4835
+ ] }),
4836
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_react_native25.View, { testID: ringgId("end-call-dialog-footer"), style: styles23.footer, children: [
4837
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Button, { variant: "outline", onPress: onCancel, testID: ringgId("end-call-cancel"), style: { borderRadius: buttonRadius }, children: cancelLabel }),
4838
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Button, { onPress: onConfirm, testID: ringgId("end-call-confirm"), style: { borderRadius: buttonRadius }, children: confirmLabel })
4839
+ ] })
4840
+ ]
4841
+ }
4842
+ )
4843
+ ] });
4844
+ };
4845
+ var styles23 = import_react_native25.StyleSheet.create({
4846
+ // Fills the widget panel rather than the window, and sits above the panel's
4847
+ // own chrome (header 10, minimize/close 20) so nothing pokes through it.
4848
+ root: { ...import_react_native25.StyleSheet.absoluteFill, zIndex: 30, alignItems: "center", justifyContent: "center", padding: 16 },
4849
+ scrim: { opacity: SCRIM_OPACITY2 },
4850
+ card: {
4851
+ width: "100%",
4852
+ maxWidth: CARD_MAX_WIDTH,
4853
+ marginHorizontal: 16,
4854
+ padding: 16,
4855
+ borderWidth: 1,
4856
+ gap: 12,
4857
+ // The web card's two stacked shadows collapse into the one elevation each
4858
+ // platform can express.
4859
+ shadowColor: "#000000",
4860
+ shadowOpacity: 0.08,
4861
+ shadowRadius: 13,
4862
+ shadowOffset: { width: 0, height: 6 },
4863
+ elevation: 8
4864
+ },
4865
+ header: { gap: 6 },
4866
+ title: { fontSize: 17, lineHeight: 24, fontWeight: "500" },
4867
+ description: { fontSize: 15, lineHeight: 21 },
4868
+ footer: { flexDirection: "row", justifyContent: "flex-end", gap: 8, paddingTop: 8 }
4869
+ });
4870
+
4871
+ // src/hooks/use-microphone.ts
4872
+ var import_react21 = require("react");
4873
+ var import_livekit_client3 = require("livekit-client");
4874
+ var MIC_EVENTS = [import_livekit_client3.RoomEvent.TrackMuted, import_livekit_client3.RoomEvent.TrackUnmuted, import_livekit_client3.RoomEvent.LocalTrackPublished, import_livekit_client3.RoomEvent.LocalTrackUnpublished, import_livekit_client3.RoomEvent.ConnectionStateChanged];
4875
+ var useMicrophone = (room) => {
4876
+ const [enabled, setEnabled] = (0, import_react21.useState)(() => room?.localParticipant.isMicrophoneEnabled ?? false);
4877
+ (0, import_react21.useEffect)(() => {
4878
+ if (!room) return;
4879
+ const sync = () => setEnabled(room.localParticipant.isMicrophoneEnabled);
4880
+ sync();
4881
+ MIC_EVENTS.forEach((event) => room.on(event, sync));
4882
+ return () => {
4883
+ MIC_EVENTS.forEach((event) => room.off(event, sync));
4884
+ };
4885
+ }, [room]);
4886
+ const toggle = (0, import_react21.useCallback)(() => {
4887
+ if (!room) return;
4888
+ const next = !room.localParticipant.isMicrophoneEnabled;
4889
+ setEnabled(next);
4890
+ void room.localParticipant.setMicrophoneEnabled(next).catch(() => setEnabled(!next));
4891
+ }, [room]);
4892
+ return room ? { enabled, toggle } : void 0;
4893
+ };
4894
+
4895
+ // src/hooks/use-presence.ts
4896
+ var import_react22 = require("react");
4897
+ var import_react_native26 = require("react-native");
4898
+ var DEFAULT_MASS = 1;
4899
+ var usePresence = (visible, spring) => {
4900
+ const progress = (0, import_react22.useRef)(new import_react_native26.Animated.Value(0)).current;
4901
+ const [mounted, setMounted] = (0, import_react22.useState)(false);
4902
+ (0, import_react22.useEffect)(() => {
4903
+ if (visible) setMounted(true);
4904
+ const animation = import_react_native26.Animated.spring(progress, {
4905
+ toValue: visible ? 1 : 0,
4906
+ stiffness: spring.stiffness,
4907
+ damping: spring.damping,
4908
+ mass: spring.mass ?? DEFAULT_MASS,
4909
+ useNativeDriver: true
4910
+ });
4911
+ animation.start(({ finished }) => {
4912
+ if (finished && !visible) setMounted(false);
4913
+ });
4914
+ return () => animation.stop();
4915
+ }, [visible, progress, spring.stiffness, spring.damping, spring.mass]);
4916
+ return { mounted, progress };
4917
+ };
4918
+
4919
+ // src/lib/text-helpers.tsx
4920
+ var import_react23 = require("react");
4921
+ var import_react_native27 = require("react-native");
4922
+ var import_jsx_runtime27 = require("react/jsx-runtime");
4923
+ var LINK_COLOR = "#3b82f6";
4924
+ var escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4925
+ var openUrl2 = (url) => {
4926
+ void import_react_native27.Linking.openURL(url).catch(() => {
4927
+ });
4928
+ };
4929
+ var renderTextWithLinks = (text, links, linkStyle) => {
4930
+ if (!links) return text;
4931
+ let parts = [text];
4932
+ Object.entries(links).forEach(([word, url]) => {
4933
+ parts = parts.flatMap((part, partIndex) => {
4934
+ if (typeof part !== "string") return part;
4935
+ const pattern = new RegExp(`\\b(${escapeForRegExp(word)})\\b`, "g");
4936
+ return part.split(pattern).map(
4937
+ (segment, segmentIndex) => segment === word ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_native27.Text, { style: [{ color: LINK_COLOR }, linkStyle], onPress: () => openUrl2(url), children: word }, `${partIndex}-${segmentIndex}`) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react23.Fragment, { children: segment }, `${partIndex}-${segmentIndex}`)
4938
+ );
4939
+ });
4940
+ });
4941
+ return parts;
4942
+ };
4943
+
4944
+ // src/RinggWidget.tsx
4945
+ var import_jsx_runtime28 = require("react/jsx-runtime");
4946
+ var PANEL_MAX_WIDTH = 400;
4947
+ var PANEL_HEIGHT = 456;
4948
+ var PANEL_HEIGHT_RATIO = 0.9;
4949
+ var EDGE_INSET = 16;
4950
+ var BOTTOM_SAFE_INSET = 24;
4951
+ var TRIGGER_SIZE = 52;
4952
+ var TRIGGER_RADIUS = 20;
4953
+ var DEFAULT_TRIGGER_ICON_SIZE = 24;
4954
+ var PANEL_SPRING = { stiffness: 380, damping: 30 };
4955
+ var TRIGGER_SPRING = { stiffness: 400, damping: 25 };
4956
+ var LEGAL_LINKS = { "Privacy Policy": "https://www.ringg.ai/privacy", "T&C": "https://www.ringg.ai/terms" };
4957
+ var LEGAL_TEXT = "By continuing, you agree to our\nPrivacy Policy and T&C.";
4958
+ var isFreeButtonsMessage = (message) => {
4959
+ const componentData = message?.componentData;
4960
+ return !!componentData && isButtons(componentData) && componentData.data.presentation === "free";
4961
+ };
4962
+ var isAttachableAgentMessage = (message) => !!message && !message.componentType && !message.isSelf && Boolean(message.message.trim());
4963
+ var WidgetPanel = ({ controller, room, width, height }) => {
4964
+ const theme = useWidgetTheme();
4965
+ const { config } = controller;
4966
+ const {
4967
+ title = WIDGET_DEFAULTS.title,
4968
+ description = WIDGET_DEFAULTS.description,
4969
+ buttons,
4970
+ defaultExpanded = WIDGET_DEFAULTS.defaultExpanded,
4971
+ hideTabSelector = WIDGET_DEFAULTS.hideTabSelector,
4972
+ bypassStartScreen = WIDGET_DEFAULTS.bypassStartScreen,
4973
+ logoUrl,
4974
+ logoStyles,
4975
+ typingWords,
4976
+ legalDisclaimer,
4977
+ feedbackScreen,
4978
+ voiceCall
4979
+ } = config;
4980
+ const shell = useRinggShell(controller);
4981
+ const session = useRinggSession(controller);
4982
+ const { messages } = useRinggMessages(controller);
4983
+ const { isTyping: isAgentTyping } = useRinggTyping(controller);
4984
+ const { completedFlowIds } = useRinggComponents(controller);
4985
+ const { commands } = useRinggSlashCommands(controller);
4986
+ const [alertEndCall, setAlertEndCall] = (0, import_react24.useState)(false);
4987
+ const microphone = useMicrophone(room);
4988
+ const callMode = shell.callMode;
4989
+ const showFeedback = shell.viewState === "feedback";
4990
+ const isLoading = session.isLoading;
4991
+ const error = session.error;
4992
+ const connectionState = session.connectionState;
4993
+ const isCalling = connectionState === "connected";
4994
+ const isSessionLive = session.isSessionLive;
4995
+ const isAudioMode = callMode === "audio";
4996
+ const showVoiceAnimation = isAudioMode && isCalling && (voiceCall?.showAnimation ?? WIDGET_DEFAULTS.voiceCallShowAnimation);
4997
+ const showVoiceTranscript = !isAudioMode || (voiceCall?.showTranscript ?? WIDGET_DEFAULTS.voiceCallShowTranscript);
4998
+ const mergedSlashCommands = (0, import_react24.useMemo)(() => [...commands], [commands]);
4999
+ const sortedMessages = (0, import_react24.useMemo)(() => [...messages].sort((a, b) => a.timestamp - b.timestamp), [messages]);
5000
+ const transcriptRef = (0, import_react24.useRef)(null);
5001
+ const previousMessagesCountRef = (0, import_react24.useRef)(0);
5002
+ (0, import_react24.useEffect)(() => {
5003
+ const newMessageAdded = messages.length > previousMessagesCountRef.current;
5004
+ previousMessagesCountRef.current = messages.length;
5005
+ transcriptRef.current?.scrollToEnd({ animated: newMessageAdded });
5006
+ }, [messages, isAgentTyping]);
5007
+ const handleCallStart = (mediaType) => void controller.startCall(mediaType);
5008
+ const handleCallEnd = () => {
5009
+ setAlertEndCall(false);
5010
+ void controller.endCall();
5011
+ };
5012
+ const handleFeedbackSubmit = (rating, comment) => void controller.submitFeedback(rating, comment);
5013
+ const handleFeedbackSkip = () => controller.skipFeedback();
5014
+ const handleUserMessageSent = () => controller.typing.showOnUserSend();
5015
+ const handleFlowComplete = (componentId) => controller.markFlowComplete(componentId);
5016
+ const handleSelectionDisplayed = (componentId, label) => controller.displaySelection(componentId, label);
5017
+ const sendComponentResponse = (componentId, responseData) => controller.sendComponentResponse(componentId, responseData);
5018
+ const sendSlashCommand = (command) => void controller.sendSlashCommand(command);
5019
+ const callComponentApi = controller.callComponentApi;
5020
+ const panelRadius = resolveRadius(theme.borderRadius, 16);
5021
+ const fontFamily = resolveFontFamily(theme.fontFamily);
5022
+ const chrome = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
5023
+ !defaultExpanded && (isSessionLive || showFeedback) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Touchable, { testID: ringgId("minimize-button"), accessibilityLabel: "Minimize widget", onPress: () => controller.minimizeWidget(), style: [styles24.chromeButton, styles24.minimizeButton], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react_native10.Minus, { size: 16, color: solidColor(theme.mutedTextColor) }) }) : null,
5024
+ !defaultExpanded ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5025
+ Touchable,
5026
+ {
5027
+ testID: ringgId("close-button"),
5028
+ accessibilityLabel: isSessionLive ? "End call" : "Close widget",
5029
+ onPress: () => isSessionLive ? setAlertEndCall(true) : showFeedback ? handleFeedbackSkip() : controller.minimizeWidget(),
5030
+ style: [styles24.chromeButton, styles24.closeButton],
5031
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_lucide_react_native10.X, { size: 16, color: solidColor(theme.mutedTextColor) })
5032
+ }
5033
+ ) : null
5034
+ ] });
5035
+ const transcript = /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
5036
+ import_react_native28.ScrollView,
5037
+ {
5038
+ testID: ringgId("transcript"),
5039
+ ref: transcriptRef,
5040
+ style: styles24.transcript,
5041
+ contentContainerStyle: [styles24.transcriptList, !showVoiceAnimation && styles24.transcriptTopClearance, !isAudioMode && styles24.transcriptBottomClearance],
5042
+ keyboardShouldPersistTaps: "handled",
5043
+ showsVerticalScrollIndicator: false,
5044
+ children: [
5045
+ sortedMessages.map((segment, index) => {
5046
+ if (segment.kind === "system") return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(system_log_default, { label: segment.message, meta: segment.systemMeta, level: segment.systemLevel }, `sys_${segment.timestamp}_${index}`);
5047
+ const previousSegment = sortedMessages[index - 1];
5048
+ const nextSegment = sortedMessages[index + 1];
5049
+ const attachFreeButtonsToPrevious = isFreeButtonsMessage(segment) && isAttachableAgentMessage(previousSegment);
5050
+ const skipStandaloneRender = isAttachableAgentMessage(segment) && isFreeButtonsMessage(nextSegment);
5051
+ if (skipStandaloneRender) return null;
5052
+ if (attachFreeButtonsToPrevious && previousSegment && segment.componentData) {
5053
+ const componentData = segment.componentData;
5054
+ const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5055
+ if (isCompletedSelectedItem) return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: previousSegment }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5056
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react24.Fragment, { children: [
5057
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: previousSegment }),
5058
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("attached-buttons"), style: styles24.fullWidth, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5059
+ interactive_flow_default,
5060
+ {
5061
+ initialComponent: segment.componentData,
5062
+ onApiCall: callComponentApi,
5063
+ onComplete: () => handleFlowComplete(componentData.component_id),
5064
+ onSendResponse: sendComponentResponse,
5065
+ onSelectionDisplayed: handleSelectionDisplayed,
5066
+ quickReplyAlign: isAudioMode ? "center" : "end"
5067
+ }
5068
+ ) })
5069
+ ] }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5070
+ }
5071
+ if (segment.componentType === "blocks" && segment.componentData) {
5072
+ const blocksPayload = segment.componentData;
5073
+ if (isBlocks(blocksPayload)) {
5074
+ const blocksConfig = blocksPayload.data;
5075
+ const blockId = blocksPayload.component_id;
5076
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("blocks-widget"), style: styles24.fullWidth, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5077
+ blocks_step_default,
5078
+ {
5079
+ config: blocksConfig,
5080
+ disabled: completedFlowIds.has(blockId),
5081
+ onAction: (action) => {
5082
+ handleUserMessageSent();
5083
+ handleFlowComplete(blockId);
5084
+ const selection = typeof action.value === "string" || typeof action.value === "number" ? String(action.value) : action.label;
5085
+ if (selection) handleSelectionDisplayed(blockId, selection);
5086
+ return controller.sendBlocksAction(blocksConfig.tool_id, blockId, { action_id: action.action_id, value: action.value, values: action.values });
5087
+ }
5088
+ }
5089
+ ) }, `blocks_${blockId}`);
5090
+ }
5091
+ }
5092
+ if (segment.componentType && segment.componentData) {
5093
+ const componentData = segment.componentData;
5094
+ const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5095
+ if (isCompletedSelectedItem) return null;
5096
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5097
+ interactive_flow_default,
5098
+ {
5099
+ initialComponent: segment.componentData,
5100
+ onApiCall: callComponentApi,
5101
+ onComplete: () => handleFlowComplete(componentData.component_id),
5102
+ onSendResponse: sendComponentResponse,
5103
+ onSelectionDisplayed: handleSelectionDisplayed,
5104
+ quickReplyAlign: isAudioMode ? "center" : "end"
5105
+ },
5106
+ `flow_${segment.componentData.component_id}_${index}`
5107
+ );
5108
+ }
5109
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(message_item_default, { itemIndex: index, message: segment }, `${segment.timestamp}__${index}`);
5110
+ }),
5111
+ isAgentTyping ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(typing_indicator_default, { words: typingWords }) : null
5112
+ ]
5113
+ }
5114
+ );
5115
+ const controls = /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5116
+ call_controls_default,
5117
+ {
5118
+ buttonConfig: buttons,
5119
+ callMode,
5120
+ isCalling: isSessionLive,
5121
+ enabledSlashCommands: mergedSlashCommands,
5122
+ micEnabled: microphone?.enabled ?? true,
5123
+ onToggleMic: microphone?.toggle,
5124
+ onSlashCommand: sendSlashCommand,
5125
+ onUserMessage: handleUserMessageSent,
5126
+ onCallEnd: handleCallEnd,
5127
+ onSendMessage: (message) => controller.sendMessage(message)
5128
+ }
5129
+ );
5130
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
5131
+ import_react_native28.View,
5132
+ {
5133
+ testID: ringgId("widget-root"),
5134
+ style: [styles24.panel, { width, height, borderRadius: panelRadius, backgroundColor: solidColor(theme.backgroundColor), borderColor: solidColor(theme.borderColor) }],
5135
+ children: [
5136
+ chrome,
5137
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5138
+ ConfirmDialog,
5139
+ {
5140
+ open: alertEndCall,
5141
+ title: `Are you sure you want to end ${callMode === "text" ? "chat" : "call"}?`,
5142
+ description: "Your current conversation will be closed and cannot be accessed later.",
5143
+ confirmLabel: `End ${callMode === "text" ? "Chat" : "Call"}`,
5144
+ cancelLabel: "Cancel",
5145
+ onConfirm: handleCallEnd,
5146
+ onCancel: () => setAlertEndCall(false)
5147
+ }
5148
+ ),
5149
+ showFeedback ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5150
+ feedback_screen_default,
5151
+ {
5152
+ callMode,
5153
+ isSubmitting: isLoading,
5154
+ feedbackScreenConfig: feedbackScreen,
5155
+ messages,
5156
+ title,
5157
+ logoUrl,
5158
+ logoStyles,
5159
+ onSubmit: handleFeedbackSubmit,
5160
+ onSkip: handleFeedbackSkip
5161
+ }
5162
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("widget-body"), style: [styles24.body, isSessionLive ? styles24.bodyLive : styles24.bodyIdle], children: [
5163
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(header_default, { isCalling: isSessionLive, isLoading, title, description, logoUrl, logoStyles }),
5164
+ showVoiceAnimation ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("voice-animation-wrapper"), style: [styles24.voiceWrapper, showVoiceTranscript && messages.length > 0 ? styles24.voiceWrapperCompact : styles24.voiceWrapperFull], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(VoiceAnimationLeaf, { room }) }) : null,
5165
+ isSessionLive && !showVoiceAnimation && (messages.length === 0 && !isAgentTyping || !showVoiceTranscript) ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.View, { testID: ringgId("body-spacer"), style: styles24.bodySpacer }) : null,
5166
+ (messages.length > 0 || isAgentTyping) && isSessionLive && showVoiceTranscript ? transcript : null,
5167
+ isSessionLive && callMode === "text" ? (
5168
+ // Transparent bottom bar: it only positions the composer over the
5169
+ // transcript, so the gutters stay clear while messages scroll under.
5170
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("controls-bar"), style: styles24.controlsBar, children: [
5171
+ error.hasError ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null,
5172
+ controls
5173
+ ] })
5174
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_jsx_runtime28.Fragment, { children: [
5175
+ controls,
5176
+ error.hasError ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null
5177
+ ] }),
5178
+ !isSessionLive && isLoading ? /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("connecting-status"), children: [
5179
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("connecting-label"), style: [styles24.connecting, { color: theme.mutedTextColor, fontFamily }], children: "Connecting..." }),
5180
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(loading_bar_default, { ariaLabel: callMode === "text" ? "Starting chat" : "Connecting" })
5181
+ ] }) : null,
5182
+ !isLoading && connectionState === "disconnected" && (!bypassStartScreen || error.hasError) ? hideTabSelector ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { callMode, buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }) : /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { testID: ringgId("start-actions"), style: styles24.startActions, children: [
5183
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { dual: true, callMode: "text", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }),
5184
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(action_button_default, { dual: true, callMode: "audio", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart })
5185
+ ] }) : null,
5186
+ !isSessionLive ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Text, { testID: ringgId("legal-disclaimer"), style: [styles24.legal, { color: theme.mutedTextColor, fontFamily }], children: legalDisclaimer ? renderTextWithLinks(legalDisclaimer.text, legalDisclaimer.links) : renderTextWithLinks(LEGAL_TEXT, LEGAL_LINKS, { color: solidColor(theme.primaryColor), textDecorationLine: "underline" }) }) : null
5187
+ ] })
5188
+ ]
5189
+ }
5190
+ );
5191
+ };
5192
+ var RinggWidget = ({ controller, room }) => {
5193
+ const shell = useRinggShell(controller);
5194
+ const { width: screenWidth, height: screenHeight } = (0, import_react_native28.useWindowDimensions)();
5195
+ const { buttons, defaultExpanded = WIDGET_DEFAULTS.defaultExpanded, widgetPosition } = controller.config;
5196
+ const isOpen = shell.viewState === "open" || shell.viewState === "feedback";
5197
+ const hideTriggerOnExpand = widgetPosition?.hideTriggerOnExpand ?? true;
5198
+ const showTrigger = !defaultExpanded && (!hideTriggerOnExpand || !isOpen);
5199
+ const panel = usePresence(isOpen, PANEL_SPRING);
5200
+ const trigger = usePresence(showTrigger, TRIGGER_SPRING);
5201
+ const panelWidth = Math.min(screenWidth - EDGE_INSET * 2, PANEL_MAX_WIDTH);
5202
+ const panelHeight = Math.min(screenHeight * PANEL_HEIGHT_RATIO, PANEL_HEIGHT);
5203
+ const triggerIconSize = toSize(buttons?.modalTrigger?.icon?.size, DEFAULT_TRIGGER_ICON_SIZE);
5204
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetThemeProvider, { theme: controller.theme, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(import_react_native28.View, { pointerEvents: "box-none", style: import_react_native28.StyleSheet.absoluteFill, children: [
5205
+ panel.mounted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.KeyboardAvoidingView, { pointerEvents: "box-none", behavior: import_react_native28.Platform.OS === "ios" ? "padding" : void 0, style: styles24.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5206
+ import_react_native28.Animated.View,
5207
+ {
5208
+ renderToHardwareTextureAndroid: true,
5209
+ shouldRasterizeIOS: true,
5210
+ style: [
5211
+ styles24.panelAnchor,
5212
+ {
5213
+ opacity: panel.progress,
5214
+ transform: [{ translateY: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }, { scale: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }]
5215
+ }
5216
+ ],
5217
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetPanel, { controller, room, width: panelWidth, height: panelHeight })
5218
+ }
5219
+ ) }) : null,
5220
+ trigger.mounted ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(WidgetTrigger, { controller, iconSize: triggerIconSize, progress: trigger.progress }) : null
5221
+ ] }) });
5222
+ };
5223
+ var WidgetTrigger = ({ controller, iconSize, progress }) => {
5224
+ const theme = useWidgetTheme();
5225
+ const { buttons } = controller.config;
5226
+ return (
5227
+ // Web pops the trigger with a spring and fades it out again when the panel
5228
+ // takes over; both ends of that are visible on every open/close cycle.
5229
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_native28.Animated.View, { style: [styles24.trigger, { opacity: progress, transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] }) }] }], children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(Touchable, { testID: ringgId("trigger-button"), accessibilityLabel: "Open call widget", onPress: () => controller.handleTriggerClick(), children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(GradientFill, { color: theme.primaryColor, style: [styles24.triggerSurface, toViewStyle(buttons?.modalTrigger?.styles)], children: buttons?.modalTrigger?.icon?.url ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
5230
+ import_react_native28.Image,
5231
+ {
5232
+ testID: ringgId("trigger-icon"),
5233
+ accessibilityLabel: "Widget trigger",
5234
+ source: { uri: buttons.modalTrigger.icon.url },
5235
+ resizeMode: "contain",
5236
+ style: { width: iconSize, height: iconSize }
5237
+ }
5238
+ ) : /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(PhoneCallRingIcon, { testID: ringgId("trigger-icon"), size: iconSize, fill: solidColor(theme.primaryTextColor) }) }) }) })
5239
+ );
5240
+ };
5241
+ var styles24 = import_react_native28.StyleSheet.create({
5242
+ overlay: { ...import_react_native28.StyleSheet.absoluteFill, justifyContent: "flex-end", alignItems: "flex-end" },
5243
+ panelAnchor: { marginHorizontal: EDGE_INSET, marginTop: EDGE_INSET, marginBottom: BOTTOM_SAFE_INSET },
5244
+ panel: {
5245
+ borderWidth: import_react_native28.StyleSheet.hairlineWidth,
5246
+ // `overflow: hidden` is what keeps the transcript, the header bar and the
5247
+ // rounded corners agreeing with each other.
5248
+ overflow: "hidden",
5249
+ // Web stacks three shadows at 1-2% alpha — the panel sits just off the page
5250
+ // rather than floating above it. 12% read as a hard slab, and on Android
5251
+ // `elevation` is the only one of these that applies, so it was the entire
5252
+ // effect instead of a supporting layer.
5253
+ shadowColor: "#000000",
5254
+ shadowOpacity: 0.05,
5255
+ shadowRadius: 20,
5256
+ shadowOffset: { width: 0, height: 8 },
5257
+ elevation: 4
5258
+ },
5259
+ trigger: { position: "absolute", right: EDGE_INSET, bottom: BOTTOM_SAFE_INSET },
5260
+ triggerSurface: { width: TRIGGER_SIZE, height: TRIGGER_SIZE, borderRadius: TRIGGER_RADIUS, alignItems: "center", justifyContent: "center" },
5261
+ chromeButton: { position: "absolute", top: 10, zIndex: 20, width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" },
5262
+ minimizeButton: { right: 42 },
5263
+ closeButton: { right: 10 },
5264
+ body: { flex: 1 },
5265
+ bodyLive: { paddingHorizontal: 20, paddingVertical: 8, gap: 10 },
5266
+ bodyIdle: { padding: 24, gap: 12 },
5267
+ bodySpacer: { flex: 1, marginTop: 56 },
5268
+ voiceWrapper: { width: "100%", alignItems: "stretch", justifyContent: "center", marginTop: 56 },
5269
+ voiceWrapperCompact: { height: 56, flexShrink: 0 },
5270
+ voiceWrapperFull: { flex: 1, minHeight: 0 },
5271
+ transcript: { flex: 1 },
5272
+ // `justifyContent: flex-end` is web's `mt-auto`: a short conversation sits at
5273
+ // the bottom of the panel rather than floating at the top.
5274
+ transcriptList: { flexGrow: 1, justifyContent: "flex-end", gap: 12 },
5275
+ transcriptTopClearance: { paddingTop: 64 },
5276
+ transcriptBottomClearance: { paddingBottom: 80 },
5277
+ fullWidth: { width: "100%" },
5278
+ controlsBar: { position: "absolute", zIndex: 10, bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingVertical: 12 },
5279
+ error: { textAlign: "center", fontSize: 14, marginBottom: 8 },
5280
+ connecting: { fontSize: 14, textAlign: "center" },
5281
+ startActions: { flexDirection: "row", alignItems: "center", gap: 8, width: "100%" },
5282
+ legal: { fontSize: 12, lineHeight: 20, textAlign: "center" }
5283
+ });
5284
+
5285
+ // src/transport/livekit-transport.ts
5286
+ var import_react_native30 = require("@livekit/react-native");
5287
+ var import_livekit_client5 = require("livekit-client");
5288
+
5289
+ // src/platform/audio-session.ts
5290
+ var import_react_native29 = require("@livekit/react-native");
5291
+ var PREFERRED_OUTPUTS = ["bluetooth", "headset", "speaker", "earpiece"];
5292
+ var createAudioSession = () => {
5293
+ let active = false;
5294
+ const start = async () => {
5295
+ if (active) return;
5296
+ active = true;
5297
+ try {
5298
+ await import_react_native29.AudioSession.configureAudio({
5299
+ android: {
5300
+ audioTypeOptions: import_react_native29.AndroidAudioTypePresets.communication,
5301
+ preferredOutputList: [...PREFERRED_OUTPUTS]
5302
+ },
5303
+ ios: { defaultOutput: "speaker" }
5304
+ });
5305
+ await import_react_native29.AudioSession.startAudioSession();
5306
+ } catch {
5307
+ }
5308
+ };
5309
+ const stop = async () => {
5310
+ if (!active) return;
5311
+ active = false;
5312
+ try {
5313
+ await import_react_native29.AudioSession.stopAudioSession();
5314
+ } catch {
5315
+ }
5316
+ };
5317
+ return { start, stop };
5318
+ };
5319
+
5320
+ // src/transport/legacy-chat.ts
5321
+ var import_livekit_client4 = require("livekit-client");
5322
+
5323
+ // src/lib/utf8.ts
5324
+ var globalEncoder = globalThis.TextEncoder;
5325
+ var globalDecoder = globalThis.TextDecoder;
5326
+ var CONTINUATION = 128;
5327
+ var encodeCodePoint = (code, out) => {
5328
+ if (code < 128) {
5329
+ out.push(code);
5330
+ return;
5331
+ }
5332
+ if (code < 2048) {
5333
+ out.push(192 | code >> 6, CONTINUATION | code & 63);
5334
+ return;
5335
+ }
5336
+ if (code < 65536) {
5337
+ out.push(224 | code >> 12, CONTINUATION | code >> 6 & 63, CONTINUATION | code & 63);
5338
+ return;
5339
+ }
5340
+ out.push(240 | code >> 18, CONTINUATION | code >> 12 & 63, CONTINUATION | code >> 6 & 63, CONTINUATION | code & 63);
5341
+ };
5342
+ var encodeFallback = (value) => {
5343
+ const bytes = [];
5344
+ for (const character of value) {
5345
+ encodeCodePoint(character.codePointAt(0) ?? 0, bytes);
5346
+ }
5347
+ return Uint8Array.from(bytes);
5348
+ };
5349
+ var sequenceLength = (lead) => {
5350
+ if (lead < 128) return 1;
5351
+ if (lead >= 240) return 4;
5352
+ if (lead >= 224) return 3;
5353
+ if (lead >= 192) return 2;
5354
+ return 1;
5355
+ };
5356
+ var LEAD_MASKS = [0, 127, 31, 15, 7];
5357
+ var decodeFallback = (bytes) => {
5358
+ let result = "";
5359
+ let index = 0;
5360
+ while (index < bytes.length) {
5361
+ const lead = bytes[index] ?? 0;
5362
+ const length = Math.min(sequenceLength(lead), bytes.length - index);
5363
+ let code = lead & (LEAD_MASKS[length] ?? 127);
5364
+ for (let offset = 1; offset < length; offset++) {
5365
+ code = code << 6 | (bytes[index + offset] ?? 0) & 63;
5366
+ }
5367
+ result += String.fromCodePoint(code);
5368
+ index += length;
5369
+ }
5370
+ return result;
5371
+ };
5372
+ var encodeUtf8 = (value) => globalEncoder ? new globalEncoder().encode(value) : encodeFallback(value);
5373
+ var decodeUtf8 = (bytes) => globalDecoder ? new globalDecoder().decode(bytes) : decodeFallback(bytes);
5374
+
5375
+ // src/transport/legacy-chat.ts
5376
+ var CHAT_TOPIC = "lk.chat";
5377
+ var LEGACY_CHAT_TOPIC = "lk-chat-topic";
5378
+ var serverSupportsDataStreams = (room) => room.serverInfo?.edition === 1 || !!room.serverInfo?.version && (0, import_livekit_client4.compareVersions)(room.serverInfo.version, "1.8.2") > 0;
5379
+ var encodeLegacyChatMessage = (message) => encodeUtf8(JSON.stringify(message));
5380
+ var decodeLegacyChatMessage = (payload) => {
5381
+ try {
5382
+ const parsed = JSON.parse(decodeUtf8(payload));
5383
+ if (typeof parsed !== "object" || parsed === null) return void 0;
5384
+ const candidate = parsed;
5385
+ if (typeof candidate.message !== "string") return void 0;
5386
+ return {
5387
+ id: typeof candidate.id === "string" ? candidate.id : "",
5388
+ timestamp: typeof candidate.timestamp === "number" ? candidate.timestamp : Date.now(),
5389
+ message: candidate.message,
5390
+ ignoreLegacy: candidate.ignoreLegacy === true
5391
+ };
5392
+ } catch {
5393
+ return void 0;
5394
+ }
5395
+ };
5396
+
5397
+ // src/transport/livekit-transport.ts
5398
+ var FIRST_SEEN_LIMIT = 200;
5399
+ var createLiveKitTransport = (options = {}) => {
5400
+ if (options.registerGlobals !== false) (0, import_react_native30.registerGlobals)();
5401
+ const room = new import_livekit_client5.Room({ dynacast: true, adaptiveStream: true, ...options.roomOptions });
5402
+ const audioSession = options.manageAudioSession === false ? void 0 : createAudioSession();
5403
+ const chatHandlers = /* @__PURE__ */ new Set();
5404
+ const registeredRpcMethods = /* @__PURE__ */ new Set();
5405
+ const registeredTextStreamTopics = /* @__PURE__ */ new Set();
5406
+ const firstSeenAt = /* @__PURE__ */ new Map();
5407
+ const firstSeen = (id) => {
5408
+ const existing = firstSeenAt.get(id);
5409
+ if (existing !== void 0) return existing;
5410
+ const now = Date.now();
5411
+ firstSeenAt.set(id, now);
5412
+ if (firstSeenAt.size > FIRST_SEEN_LIMIT) {
5413
+ const oldest = firstSeenAt.keys().next();
5414
+ if (!oldest.done) firstSeenAt.delete(oldest.value);
5415
+ }
5416
+ return now;
5417
+ };
5418
+ const setTextStreamHandler = (topic, handler) => {
5419
+ if (registeredTextStreamTopics.has(topic)) room.unregisterTextStreamHandler(topic);
5420
+ room.registerTextStreamHandler(topic, handler);
5421
+ registeredTextStreamTopics.add(topic);
5422
+ };
5423
+ const emitChat = (message) => {
5424
+ chatHandlers.forEach((handler) => handler(message));
5425
+ };
5426
+ const classifyRemoteAsAgent = (identity) => {
5427
+ const participant = room.getParticipantByIdentity(identity);
5428
+ return participant ? isAgentParticipant(participant) : true;
5429
+ };
5430
+ const mapConnectionState = (state) => {
5431
+ switch (state) {
5432
+ case import_livekit_client5.ConnectionState.Connecting:
5433
+ return "connecting";
5434
+ case import_livekit_client5.ConnectionState.Connected:
5435
+ return "connected";
5436
+ case import_livekit_client5.ConnectionState.Reconnecting:
5437
+ case import_livekit_client5.ConnectionState.SignalReconnecting:
5438
+ return "reconnecting";
5439
+ case import_livekit_client5.ConnectionState.Disconnected:
5440
+ return "disconnected";
5441
+ }
5442
+ };
5443
+ setTextStreamHandler(CHAT_TOPIC, (reader, participantInfo) => {
5444
+ void (async () => {
5445
+ let message;
5446
+ try {
5447
+ message = await reader.readAll();
5448
+ } catch {
5449
+ return;
5450
+ }
5451
+ const identity = participantInfo.identity;
5452
+ if (identity === room.localParticipant.identity) return;
5453
+ emitChat({
5454
+ message,
5455
+ isSelf: false,
5456
+ isAgent: classifyRemoteAsAgent(identity),
5457
+ senderName: room.getParticipantByIdentity(identity)?.name,
5458
+ timestamp: reader.info.timestamp
5459
+ });
5460
+ })();
5461
+ });
5462
+ const handleDataReceived = (payload, participant, _kind, topic) => {
5463
+ if (topic !== LEGACY_CHAT_TOPIC) return;
5464
+ const legacy = decodeLegacyChatMessage(payload);
5465
+ if (!legacy || legacy.ignoreLegacy) return;
5466
+ if (participant && participant.identity === room.localParticipant.identity) return;
5467
+ emitChat({
5468
+ message: legacy.message,
5469
+ isSelf: false,
5470
+ isAgent: participant ? isAgentParticipant(participant) : true,
5471
+ senderName: participant?.name,
5472
+ timestamp: legacy.timestamp
5473
+ });
5474
+ };
5475
+ room.on(import_livekit_client5.RoomEvent.DataReceived, handleDataReceived);
5476
+ const handleRoomDisconnected = () => {
5477
+ firstSeenAt.clear();
5478
+ void audioSession?.stop();
5479
+ };
5480
+ room.on(import_livekit_client5.RoomEvent.Disconnected, handleRoomDisconnected);
5481
+ const transport = {
5482
+ async connect(url, token) {
5483
+ await audioSession?.start();
5484
+ try {
5485
+ await room.connect(url, token);
5486
+ } catch (error) {
5487
+ await audioSession?.stop();
5488
+ throw error;
5489
+ }
5490
+ },
5491
+ async disconnect() {
5492
+ await room.disconnect();
5493
+ },
5494
+ async prepareConnection(url) {
5495
+ try {
5496
+ await room.prepareConnection(url);
5497
+ } catch {
5498
+ }
5499
+ },
5500
+ async setMicrophoneEnabled(enabled) {
5501
+ await room.localParticipant.setMicrophoneEnabled(enabled);
5502
+ },
5503
+ async sendChatMessage(text) {
5504
+ const info = await room.localParticipant.sendText(text, { topic: CHAT_TOPIC });
5505
+ await room.localParticipant.publishData(
5506
+ encodeLegacyChatMessage({
5507
+ id: info.id,
5508
+ timestamp: Date.now(),
5509
+ message: text,
5510
+ ignoreLegacy: serverSupportsDataStreams(room)
5511
+ }),
5512
+ { reliable: true, topic: LEGACY_CHAT_TOPIC }
5513
+ );
5514
+ },
5515
+ async performRpcToAgent(method, payload) {
5516
+ const agent = findAgentParticipant(room);
5517
+ if (!agent) {
5518
+ throw new Error(`Cannot perform RPC "${method}": no agent participant in the room`);
5519
+ }
5520
+ return room.localParticipant.performRpc({
5521
+ destinationIdentity: agent.identity,
5522
+ method,
5523
+ payload
5524
+ });
5525
+ },
5526
+ registerRpcMethod(method, handler) {
5527
+ if (registeredRpcMethods.has(method)) room.unregisterRpcMethod(method);
5528
+ room.registerRpcMethod(
5529
+ method,
5530
+ (data) => (
5531
+ // Handler rejections must propagate: livekit surfaces non-RpcError
5532
+ // throws to the caller as RPC application error 1500 (spec §4).
5533
+ handler({ method, payload: data.payload, callerIdentity: data.callerIdentity })
5534
+ )
5535
+ );
5536
+ registeredRpcMethods.add(method);
5537
+ },
5538
+ unregisterRpcMethod(method) {
5539
+ room.unregisterRpcMethod(method);
5540
+ registeredRpcMethods.delete(method);
5541
+ },
5542
+ registerTextStreamHandler(topic, handler) {
5543
+ setTextStreamHandler(topic, (reader, participantInfo) => {
5544
+ handler({
5545
+ readAll: () => reader.readAll(),
5546
+ participantIdentity: participantInfo.identity,
5547
+ isAgent: classifyRemoteAsAgent(participantInfo.identity)
5548
+ });
5549
+ });
5550
+ },
5551
+ unregisterTextStreamHandler(topic) {
5552
+ room.unregisterTextStreamHandler(topic);
5553
+ registeredTextStreamTopics.delete(topic);
5554
+ },
5555
+ onConnectionStateChange(handler) {
5556
+ const listener = (state) => {
5557
+ handler(mapConnectionState(state));
5558
+ };
5559
+ room.on(import_livekit_client5.RoomEvent.ConnectionStateChanged, listener);
5560
+ return () => {
5561
+ room.off(import_livekit_client5.RoomEvent.ConnectionStateChanged, listener);
5562
+ };
5563
+ },
5564
+ onSessionEnded(handler) {
5565
+ const handleParticipantDisconnected = (_participant) => {
5566
+ handler();
5567
+ };
5568
+ const handleDisconnected = () => {
5569
+ handler();
5570
+ };
5571
+ room.on(import_livekit_client5.RoomEvent.ParticipantDisconnected, handleParticipantDisconnected);
5572
+ room.on(import_livekit_client5.RoomEvent.Disconnected, handleDisconnected);
5573
+ return () => {
5574
+ room.off(import_livekit_client5.RoomEvent.ParticipantDisconnected, handleParticipantDisconnected);
5575
+ room.off(import_livekit_client5.RoomEvent.Disconnected, handleDisconnected);
5576
+ };
5577
+ },
5578
+ onTranscription(handler) {
5579
+ const listener = (segments, participant) => {
5580
+ if (!participant) return;
5581
+ const isLocal = participant.isLocal;
5582
+ if (!isLocal && !isAgentParticipant(participant)) return;
5583
+ for (const segment of segments) {
5584
+ handler({
5585
+ id: segment.id,
5586
+ text: segment.text,
5587
+ final: segment.final,
5588
+ isLocal,
5589
+ participantName: participant.name,
5590
+ receivedAt: firstSeen(segment.id)
5591
+ });
5592
+ }
5593
+ };
5594
+ room.on(import_livekit_client5.RoomEvent.TranscriptionReceived, listener);
5595
+ return () => {
5596
+ room.off(import_livekit_client5.RoomEvent.TranscriptionReceived, listener);
5597
+ };
5598
+ },
5599
+ onChatMessage(handler) {
5600
+ chatHandlers.add(handler);
5601
+ return () => {
5602
+ chatHandlers.delete(handler);
5603
+ };
5604
+ },
5605
+ onMediaDevicesError(handler) {
5606
+ const listener = () => {
5607
+ handler();
5608
+ };
5609
+ room.on(import_livekit_client5.RoomEvent.MediaDevicesError, listener);
5610
+ return () => {
5611
+ room.off(import_livekit_client5.RoomEvent.MediaDevicesError, listener);
5612
+ };
5613
+ }
5614
+ };
5615
+ const dispose = () => {
5616
+ room.off(import_livekit_client5.RoomEvent.DataReceived, handleDataReceived);
5617
+ room.off(import_livekit_client5.RoomEvent.Disconnected, handleRoomDisconnected);
5618
+ for (const topic of Array.from(registeredTextStreamTopics)) {
5619
+ room.unregisterTextStreamHandler(topic);
5620
+ }
5621
+ registeredTextStreamTopics.clear();
5622
+ for (const method of Array.from(registeredRpcMethods)) {
5623
+ room.unregisterRpcMethod(method);
5624
+ }
5625
+ registeredRpcMethods.clear();
5626
+ chatHandlers.clear();
5627
+ firstSeenAt.clear();
5628
+ void room.disconnect().finally(() => audioSession?.stop());
5629
+ };
5630
+ return { transport, room, dispose };
5631
+ };
5632
+
5633
+ // src/platform/mic-permission.ts
5634
+ var import_react_native31 = require("react-native");
5635
+ var import_react_native_webrtc = require("@livekit/react-native-webrtc");
5636
+ var stopTracks = (stream) => {
5637
+ stream.getTracks().forEach((track) => track.stop());
5638
+ };
5639
+ var probeMicrophone = async () => {
5640
+ try {
5641
+ const stream = await import_react_native_webrtc.mediaDevices.getUserMedia({ audio: true });
5642
+ stopTracks(stream);
5643
+ return true;
5644
+ } catch {
5645
+ return false;
5646
+ }
5647
+ };
5648
+ var ANDROID_RATIONALE = {
5649
+ title: "Microphone access",
5650
+ message: "Voice calls need the microphone to hear you.",
5651
+ buttonPositive: "Allow",
5652
+ buttonNegative: "Not now"
5653
+ };
5654
+ var createNativeMicPermission = () => ({
5655
+ async isGranted() {
5656
+ if (import_react_native31.Platform.OS === "android") {
5657
+ return import_react_native31.PermissionsAndroid.check(import_react_native31.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
5658
+ }
5659
+ return probeMicrophone();
5660
+ },
5661
+ async request() {
5662
+ if (import_react_native31.Platform.OS === "android") {
5663
+ const result = await import_react_native31.PermissionsAndroid.request(import_react_native31.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, ANDROID_RATIONALE);
5664
+ return result === import_react_native31.PermissionsAndroid.RESULTS.GRANTED;
5665
+ }
5666
+ return probeMicrophone();
5667
+ }
5668
+ });
5669
+
5670
+ // src/platform/app-origin.ts
5671
+ var import_react_native32 = require("react-native");
5672
+ var SCHEMES = {
5673
+ android: "android",
5674
+ ios: "ios",
5675
+ macos: "macos",
5676
+ windows: "windows"
5677
+ };
5678
+ var appOrigin = (bundleId) => {
5679
+ const scheme = SCHEMES[import_react_native32.Platform.OS];
5680
+ if (!scheme || !bundleId) return void 0;
5681
+ return `${scheme}://${bundleId}`;
5682
+ };
5683
+
5684
+ // src/platform/notification.ts
5685
+ var createSilentNotificationPlayer = () => ({
5686
+ play() {
5687
+ }
5688
+ });
5689
+ var createNotificationPlayer = (tuneUrl, playTune) => ({
5690
+ play() {
5691
+ try {
5692
+ playTune(tuneUrl);
5693
+ } catch {
5694
+ }
5695
+ }
5696
+ });
5697
+
5698
+ // src/lib/host-actions.ts
5699
+ var warn = (...args) => {
5700
+ console.warn("[HOST_ACTION]", ...args);
5701
+ };
5702
+ var createHostActionDispatcher = (onAction) => (action, onLog) => {
5703
+ const emit = (entry) => {
5704
+ try {
5705
+ onLog?.(entry);
5706
+ } catch (error) {
5707
+ warn("logger threw", error);
5708
+ }
5709
+ };
5710
+ if (!action || typeof action !== "object") {
5711
+ warn("Missing or invalid action config");
5712
+ emit({ label: "Invalid action", level: "error" });
5713
+ return;
5714
+ }
5715
+ const { id, kind } = action;
5716
+ if (kind !== "trigger_event") {
5717
+ warn(`[${id}] unknown kind`, kind);
5718
+ emit({ id, label: "Unknown kind", meta: String(kind), level: "error" });
5719
+ return;
5720
+ }
5721
+ if (!action.event_name) {
5722
+ warn(`[${id}] trigger_event missing event_name`);
5723
+ emit({ id, label: "Event missing name", level: "error" });
5724
+ return;
5725
+ }
5726
+ try {
5727
+ onAction({ name: action.event_name, payload: { ...action.default_payload ?? {}, action_id: id } });
5728
+ emit({ id, label: "Triggered", meta: action.event_name, level: "info" });
5729
+ } catch (error) {
5730
+ warn(`[${id}] execution failed`, error);
5731
+ emit({ id, label: "Action failed", level: "error" });
5732
+ }
5733
+ };
1152
5734
  // Annotate the CommonJS export names for ESM import in node:
1153
5735
  0 && (module.exports = {
5736
+ GradientFill,
5737
+ Markdown,
5738
+ RinggWidget,
5739
+ WidgetThemeProvider,
5740
+ appOrigin,
5741
+ createAudioSession,
1154
5742
  createCallbackEventBus,
5743
+ createHostActionDispatcher,
5744
+ createLiveKitTransport,
5745
+ createNativeMicPermission,
5746
+ createNotificationPlayer,
1155
5747
  createRinggWidgetController,
5748
+ createSilentNotificationPlayer,
5749
+ resolveButtonRadius,
5750
+ resolveFontFamily,
5751
+ resolveRadius,
5752
+ ringgId,
5753
+ solidColor,
5754
+ toSize,
5755
+ toViewStyle,
1156
5756
  useRinggComponents,
1157
5757
  useRinggMessages,
1158
5758
  useRinggSession,
1159
5759
  useRinggShell,
1160
5760
  useRinggSlashCommands,
1161
5761
  useRinggTyping,
1162
- useStoreSnapshot
5762
+ useStoreSnapshot,
5763
+ useWidgetTheme
1163
5764
  });
1164
5765
  //# sourceMappingURL=index.js.map