@appilots/sdk 0.13.0 → 0.14.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.mjs CHANGED
@@ -1,11 +1,11 @@
1
- export { AppilotsNavigationContainer } from './chunk-L3SY3Q4K.mjs';
2
- import { subscribeSpotlight, getSpotlightTarget, AppilotsI18nProvider, useAppilotsI18n, useAppilotsChat, useAppilotsActions, setSpotlightMode, shouldShowSuggestedPrompts, useSuggestedPrompts } from './chunk-UYBKXULM.mjs';
3
- export { AppilotsI18nProvider, REVEAL_MARGIN, SPOTLIGHT_TTL_MS, appilotsSelfCheck, captureSnapshot, detectDeviceLocale, executeAction, getSpotlightMode, getSpotlightTarget, offsetToReveal, resolveLocale, revealTarget, setSpotlightMode, setSpotlightTarget, subscribeSpotlight, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsI18n, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts } from './chunk-UYBKXULM.mjs';
4
- import { defaultLightTheme, describeAction, isRectOnScreen, mergeThemeTokens, resolveBaseTheme, useAppilotsContext, AppilotsErrorBoundary, subscribeGeometryHost, geometryAvailability, isGeometryResolved, needsWideChatLayout, componentRegistry, appilotsDebugWarn, enhancePlainAssistantContent, looksLikeMarkdown, parseMarkdownBlocks, parseInlineMarkdown, getFiberRoot, shouldSkipSubtree, isModalFiber } from './chunk-HRWBK35Y.mjs';
5
- export { AppilotsClient, AppilotsProvider, AppilotsRegistryProvider, RateLimitedError, SDK_VERSION, _patchJsxRuntimes, clearAppilotsDebugTraces, componentRegistry, createComponentRegistry, createElementRegistry, createListRegistry, defaultDarkTheme, defaultLightTheme, describeAction, elementRegistry, enableAppilotsAutoTracking, getAppilotsDebugTraces, getAutoTrackingState, getGlobalConfig, getIntrospectionDiagnostics, humanizeError, initAppilots, isAutoTrackingEnabled, listRegistry, mergeThemeTokens, recordAppilotsDebugTrace, registerScreen, setCurrentScreen, subscribeAppilotsDebugTraces, useAppilotsContext, useResolvedRegistry } from './chunk-HRWBK35Y.mjs';
1
+ export { AppilotsNavigationContainer } from './chunk-N66DUP54.mjs';
2
+ import { subscribeSpotlight, getSpotlightTarget, AppilotsI18nProvider, useAppilotsI18n, useAppilotsChat, useAppilotsActions, setSpotlightMode, shouldShowSuggestedPrompts, useSuggestedPrompts, subscribeSpotlightAnnouncement } from './chunk-6JWU3NBT.mjs';
3
+ export { AppilotsI18nProvider, REVEAL_MARGIN, SPOTLIGHT_TTL_MS, appilotsSelfCheck, captureSnapshot, detectDeviceLocale, executeAction, getSpotlightMode, getSpotlightTarget, offsetToReveal, resolveLocale, revealTarget, setSpotlightMode, setSpotlightTarget, subscribeSpotlight, useAppilots, useAppilotsActions, useAppilotsChat, useAppilotsField, useAppilotsI18n, useAppilotsNavigation, useAppilotsSlider, useAppilotsTarget, useAppilotsToggle, useSuggestedPrompts } from './chunk-6JWU3NBT.mjs';
4
+ import { defaultLightTheme, describeAction, confirmExpiresAt, isRectOnScreen, mergeThemeTokens, resolveBaseTheme, useAppilotsContext, AppilotsErrorBoundary, isSpeechHost, subscribeGeometryHost, geometryAvailability, isGeometryResolved, needsWideChatLayout, componentRegistry, startStillWorkingTimer, appilotsDebugWarn, enhancePlainAssistantContent, looksLikeMarkdown, parseMarkdownBlocks, parseInlineMarkdown, getFiberRoot, shouldSkipSubtree, isModalFiber } from './chunk-AKAMWKO6.mjs';
5
+ export { AppilotsClient, AppilotsProvider, AppilotsRegistryProvider, RateLimitedError, SDK_VERSION, _patchJsxRuntimes, clearAppilotsDebugTraces, componentRegistry, createComponentRegistry, createElementRegistry, createListRegistry, defaultDarkTheme, defaultLightTheme, describeAction, elementRegistry, enableAppilotsAutoTracking, getAppilotsDebugTraces, getAutoTrackingState, getGlobalConfig, getIntrospectionDiagnostics, humanizeError, initAppilots, isAutoTrackingEnabled, isSpeechHost, listRegistry, mergeThemeTokens, recordAppilotsDebugTrace, registerScreen, setCurrentScreen, subscribeAppilotsDebugTraces, useAppilotsContext, useResolvedRegistry } from './chunk-AKAMWKO6.mjs';
6
6
  import * as React8 from 'react';
7
- import React8__default, { createContext, useRef, useEffect, useCallback, useSyncExternalStore, useMemo, useContext, useState } from 'react';
8
- import { StyleSheet, Dimensions, Platform, View, Animated, Text, TouchableOpacity, ActivityIndicator, useColorScheme, FlatList, Image, TextInput, KeyboardAvoidingView, Modal, Linking } from 'react-native';
7
+ import React8__default, { createContext, useRef, useEffect, useState, useCallback, useSyncExternalStore, useMemo, useContext } from 'react';
8
+ import { StyleSheet, Dimensions, Platform, View, Animated, Text, TouchableOpacity, AccessibilityInfo, ActivityIndicator, useColorScheme, useWindowDimensions, FlatList, Image, TextInput, KeyboardAvoidingView, Modal, Linking } from 'react-native';
9
9
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
10
10
  import Svg, { Path } from 'react-native-svg';
11
11
 
@@ -116,13 +116,15 @@ var PT_BR_CONFIRM_FALLBACK = {
116
116
  confirmDestructiveApprove: "Sim, executar",
117
117
  confirmApprove: "Confirmar",
118
118
  confirmCancel: "Cancelar",
119
- confirmThisAction: "esta a\xE7\xE3o"
119
+ confirmThisAction: "esta a\xE7\xE3o",
120
+ confirmExpired: "Esta confirma\xE7\xE3o expirou. Pe\xE7a de novo ao assistente para continuar."
120
121
  };
121
122
  function deriveConfirmCardContent(action, palette = DEFAULT_BREADCRUMB_PALETTE, strings) {
122
123
  const payload = action.payload ?? {};
123
124
  const isDestructive = isDestructiveAction(action);
124
125
  const copy = { ...PT_BR_CONFIRM_FALLBACK, ...strings ?? {} };
125
- const actionName = typeof payload.actionDescription === "string" && payload.actionDescription || copy.confirmThisAction;
126
+ const described = typeof payload.actionDescription === "string" ? payload.actionDescription : "";
127
+ const actionName = described || copy.confirmThisAction;
126
128
  const message = typeof payload.message === "string" && payload.message || copy.confirmMessage.replace("{action}", actionName);
127
129
  const title = typeof payload.title === "string" && payload.title || (isDestructive ? copy.confirmDestructiveTitle : copy.confirmTitle);
128
130
  const confirmLabel = typeof payload.confirmLabel === "string" && payload.confirmLabel || (isDestructive ? copy.confirmDestructiveApprove : copy.confirmApprove);
@@ -133,6 +135,10 @@ function deriveConfirmCardContent(action, palette = DEFAULT_BREADCRUMB_PALETTE,
133
135
  message,
134
136
  confirmLabel,
135
137
  cancelLabel,
138
+ // A bare "Confirm" heard out of context says nothing about what it does.
139
+ confirmA11yLabel: described ? `${confirmLabel}: ${described}` : confirmLabel,
140
+ cancelA11yLabel: described ? `${cancelLabel}: ${described}` : cancelLabel,
141
+ expiredMessage: copy.confirmExpired ?? PT_BR_CONFIRM_FALLBACK.confirmExpired,
136
142
  glyph: isDestructive ? "\u26A0" : "?",
137
143
  actionColor: isDestructive ? palette.error : palette.primary,
138
144
  cardBackground: palette.surface,
@@ -142,6 +148,79 @@ function deriveConfirmCardContent(action, palette = DEFAULT_BREADCRUMB_PALETTE,
142
148
  cancelTextColor: palette.textSecondary
143
149
  };
144
150
  }
151
+ var TARGET_ANNOUNCEMENT = {
152
+ "pt-BR": {
153
+ press: "Tocando em {label}",
154
+ toggle: "Alternando {label}",
155
+ focus: "Selecionando {label}",
156
+ fill: "Preenchendo {label}"
157
+ },
158
+ en: {
159
+ press: "Tapping {label}",
160
+ toggle: "Toggling {label}",
161
+ focus: "Selecting {label}",
162
+ fill: "Filling in {label}"
163
+ },
164
+ es: {
165
+ press: "Tocando {label}",
166
+ toggle: "Cambiando {label}",
167
+ focus: "Seleccionando {label}",
168
+ fill: "Rellenando {label}"
169
+ },
170
+ fr: {
171
+ press: "Appui sur {label}",
172
+ toggle: "Bascule de {label}",
173
+ focus: "S\xE9lection de {label}",
174
+ fill: "Saisie de {label}"
175
+ }
176
+ };
177
+ function targetAnnouncement(verb, label, locale = "pt-BR") {
178
+ return (TARGET_ANNOUNCEMENT[locale] ?? TARGET_ANNOUNCEMENT["pt-BR"])[verb].replace(
179
+ "{label}",
180
+ label
181
+ );
182
+ }
183
+ function executorNamesTarget(action) {
184
+ if (action.type === "form_fill") return true;
185
+ if (action.type !== "ui_interaction") return false;
186
+ const interaction = action.payload?.action;
187
+ return !["scroll", "scroll_to"].includes(String(interaction ?? "press"));
188
+ }
189
+ function actionAnnouncement(action, locale, named) {
190
+ const { state, label } = describeAction(action, locale);
191
+ if (state === "failed") return label;
192
+ const executorNames = executorNamesTarget(action);
193
+ if (state === "running") return executorNames ? null : label;
194
+ if (state === "success") return executorNames && !named ? label : null;
195
+ return null;
196
+ }
197
+ var reduceMotion = false;
198
+ var listeners = /* @__PURE__ */ new Set();
199
+ var subscription;
200
+ function set(value) {
201
+ if (value === reduceMotion) return;
202
+ reduceMotion = value;
203
+ for (const listener of listeners) listener();
204
+ }
205
+ function subscribe(onChange) {
206
+ listeners.add(onChange);
207
+ if (!subscription) {
208
+ subscription = AccessibilityInfo.addEventListener("reduceMotionChanged", set);
209
+ AccessibilityInfo.isReduceMotionEnabled().then(set, () => {
210
+ });
211
+ }
212
+ return () => {
213
+ listeners.delete(onChange);
214
+ if (listeners.size === 0) {
215
+ subscription?.remove();
216
+ subscription = void 0;
217
+ }
218
+ };
219
+ }
220
+ var get = () => reduceMotion;
221
+ function useReduceMotion() {
222
+ return useSyncExternalStore(subscribe, get, get);
223
+ }
145
224
 
146
225
  // src/components/ActionBreadcrumb.tsx
147
226
  function ActionBreadcrumb({
@@ -154,7 +233,8 @@ function ActionBreadcrumb({
154
233
  locale,
155
234
  requireApprovalFor,
156
235
  onApprove,
157
- onReject
236
+ onReject,
237
+ onConfirmCardGone
158
238
  }) {
159
239
  if (actions.length === 0) return null;
160
240
  const resolvedPalette = {
@@ -176,7 +256,8 @@ function ActionBreadcrumb({
176
256
  locale,
177
257
  needsApproval: requireApprovalFor?.(action) ?? false,
178
258
  onApprove,
179
- onReject
259
+ onReject,
260
+ onConfirmCardGone
180
261
  }
181
262
  )));
182
263
  }
@@ -191,7 +272,8 @@ function BreadcrumbRow(props) {
191
272
  fontFamily: props.fontFamily,
192
273
  confirmStrings: props.confirmStrings,
193
274
  onApprove: props.onApprove,
194
- onReject: props.onReject
275
+ onReject: props.onReject,
276
+ onGone: props.onConfirmCardGone
195
277
  }
196
278
  );
197
279
  }
@@ -213,40 +295,39 @@ function RegularBreadcrumbRow({
213
295
  const fade = useRef(new Animated.Value(0)).current;
214
296
  const pulse = useRef(new Animated.Value(1)).current;
215
297
  const lastState = useRef(state);
298
+ const reduceMotion2 = useReduceMotion();
216
299
  useEffect(() => {
300
+ if (reduceMotion2) {
301
+ fade.setValue(1);
302
+ return;
303
+ }
217
304
  Animated.timing(fade, {
218
305
  toValue: 1,
219
306
  duration: 180,
220
307
  useNativeDriver: true
221
308
  }).start();
222
- }, [fade]);
309
+ }, [fade, reduceMotion2]);
223
310
  useEffect(() => {
224
311
  if (lastState.current === state) return;
225
312
  lastState.current = state;
313
+ if (reduceMotion2) return;
226
314
  pulse.setValue(0.4);
227
315
  Animated.timing(pulse, {
228
316
  toValue: 1,
229
317
  duration: 220,
230
318
  useNativeDriver: true
231
319
  }).start();
232
- }, [state, pulse]);
320
+ }, [state, pulse, reduceMotion2]);
233
321
  const isDestructive = isDestructiveAction(action);
234
- return /* @__PURE__ */ React8__default.createElement(Animated.View, { style: [styles.row, { opacity: fade }] }, /* @__PURE__ */ React8__default.createElement(
235
- Animated.View,
322
+ return /* @__PURE__ */ React8__default.createElement(Animated.View, { style: [styles.row, { opacity: fade }] }, /* @__PURE__ */ React8__default.createElement(Animated.View, { style: [styles.labelGroup, { opacity: pulse }] }, /* @__PURE__ */ React8__default.createElement(StateIcon, { state, tone, primaryColor }), /* @__PURE__ */ React8__default.createElement(
323
+ Text,
236
324
  {
237
- style: { opacity: pulse, flexDirection: "row", alignItems: "flex-start", flex: 1 }
325
+ style: [styles.label, { color: state === "failed" ? tone.color : textColor, fontFamily }],
326
+ accessibilityLabel: `${state}: ${label}`
238
327
  },
239
- /* @__PURE__ */ React8__default.createElement(StateIcon, { state, tone, primaryColor }),
240
- /* @__PURE__ */ React8__default.createElement(
241
- Text,
242
- {
243
- style: [styles.label, { color: state === "failed" ? tone.color : textColor, fontFamily }],
244
- accessibilityLabel: `${state}: ${label}`
245
- },
246
- label,
247
- isDestructive && /* @__PURE__ */ React8__default.createElement(Text, { style: [styles.destructiveTag, { color: palette.error }] }, " \u26A0 destrutiva")
248
- )
249
- ), needsApproval && action.status === "pending" && onApprove && onReject && /* @__PURE__ */ React8__default.createElement(View, { style: styles.approvalButtons }, /* @__PURE__ */ React8__default.createElement(
328
+ label,
329
+ isDestructive && /* @__PURE__ */ React8__default.createElement(Text, { style: [styles.destructiveTag, { color: palette.error }] }, " \u26A0 destrutiva")
330
+ )), needsApproval && action.status === "pending" && onApprove && onReject && /* @__PURE__ */ React8__default.createElement(View, { style: styles.approvalButtons }, /* @__PURE__ */ React8__default.createElement(
250
331
  TouchableOpacity,
251
332
  {
252
333
  onPress: () => onReject(action.id),
@@ -268,13 +349,15 @@ function RegularBreadcrumbRow({
268
349
  /* @__PURE__ */ React8__default.createElement(Text, { style: styles.approveBtnText }, "Aprovar")
269
350
  )));
270
351
  }
352
+ var MIN_TOUCH_TARGET = 44;
271
353
  function ConfirmCard({
272
354
  action,
273
355
  palette,
274
356
  fontFamily,
275
357
  confirmStrings,
276
358
  onApprove,
277
- onReject
359
+ onReject,
360
+ onGone
278
361
  }) {
279
362
  const {
280
363
  isDestructive,
@@ -282,6 +365,9 @@ function ConfirmCard({
282
365
  message,
283
366
  confirmLabel,
284
367
  cancelLabel,
368
+ confirmA11yLabel,
369
+ cancelA11yLabel,
370
+ expiredMessage,
285
371
  glyph,
286
372
  actionColor,
287
373
  cardBackground,
@@ -290,6 +376,23 @@ function ConfirmCard({
290
376
  cancelBackground,
291
377
  cancelTextColor
292
378
  } = deriveConfirmCardContent(action, palette, confirmStrings);
379
+ const summaryRef = useRef(null);
380
+ const expiresAt = confirmExpiresAt(action);
381
+ const [expired, setExpired] = useState(() => expiresAt !== void 0 && expiresAt <= Date.now());
382
+ const onGoneRef = useRef(onGone);
383
+ onGoneRef.current = onGone;
384
+ useEffect(() => {
385
+ if (summaryRef.current) AccessibilityInfo.sendAccessibilityEvent(summaryRef.current, "focus");
386
+ return () => onGoneRef.current?.();
387
+ }, []);
388
+ useEffect(() => {
389
+ if (expiresAt === void 0 || expired) return;
390
+ const timer = setTimeout(() => setExpired(true), expiresAt - Date.now());
391
+ return () => clearTimeout(timer);
392
+ }, [expiresAt, expired]);
393
+ useEffect(() => {
394
+ if (expired) AccessibilityInfo.announceForAccessibility(expiredMessage);
395
+ }, [expired, expiredMessage]);
293
396
  return /* @__PURE__ */ React8__default.createElement(
294
397
  View,
295
398
  {
@@ -297,26 +400,46 @@ function ConfirmCard({
297
400
  confirmCardStyles.card,
298
401
  { backgroundColor: cardBackground, borderColor: cardBorderColor }
299
402
  ],
300
- testID: "assistant-confirm-card"
403
+ testID: "assistant-confirm-card",
404
+ onAccessibilityEscape: () => onReject?.(action.id)
301
405
  },
302
- /* @__PURE__ */ React8__default.createElement(View, { style: confirmCardStyles.headerRow }, /* @__PURE__ */ React8__default.createElement(Text, { style: [confirmCardStyles.glyph, { color: actionColor }] }, glyph), /* @__PURE__ */ React8__default.createElement(
406
+ /* @__PURE__ */ React8__default.createElement(
407
+ View,
408
+ {
409
+ ref: summaryRef,
410
+ accessible: true,
411
+ accessibilityLabel: `${title}. ${message}`,
412
+ accessibilityLiveRegion: "polite"
413
+ },
414
+ /* @__PURE__ */ React8__default.createElement(View, { style: confirmCardStyles.headerRow }, /* @__PURE__ */ React8__default.createElement(Text, { style: [confirmCardStyles.glyph, { color: actionColor }] }, glyph), /* @__PURE__ */ React8__default.createElement(
415
+ Text,
416
+ {
417
+ style: [
418
+ confirmCardStyles.title,
419
+ { color: isDestructive ? palette.error : palette.text, fontFamily }
420
+ ]
421
+ },
422
+ title
423
+ )),
424
+ /* @__PURE__ */ React8__default.createElement(Text, { style: [confirmCardStyles.message, { color: messageColor, fontFamily }] }, message)
425
+ ),
426
+ expired && /* @__PURE__ */ React8__default.createElement(
303
427
  Text,
304
428
  {
305
- style: [
306
- confirmCardStyles.title,
307
- { color: isDestructive ? palette.error : palette.text, fontFamily }
308
- ]
429
+ style: [confirmCardStyles.expired, { color: palette.error, fontFamily }],
430
+ accessibilityLiveRegion: "assertive",
431
+ testID: "assistant-confirm-expired"
309
432
  },
310
- title
311
- )),
312
- /* @__PURE__ */ React8__default.createElement(Text, { style: [confirmCardStyles.message, { color: messageColor, fontFamily }] }, message),
433
+ expiredMessage
434
+ ),
313
435
  /* @__PURE__ */ React8__default.createElement(View, { style: confirmCardStyles.buttons }, /* @__PURE__ */ React8__default.createElement(
314
436
  TouchableOpacity,
315
437
  {
316
438
  onPress: () => onReject?.(action.id),
317
439
  style: [confirmCardStyles.btn, { backgroundColor: cancelBackground }],
318
440
  activeOpacity: 0.7,
319
- accessibilityLabel: cancelLabel,
441
+ accessibilityRole: "button",
442
+ accessibilityLabel: cancelA11yLabel,
320
443
  testID: "assistant-confirm-reject"
321
444
  },
322
445
  /* @__PURE__ */ React8__default.createElement(Text, { style: [confirmCardStyles.cancelText, { color: cancelTextColor, fontFamily }] }, cancelLabel)
@@ -324,13 +447,17 @@ function ConfirmCard({
324
447
  TouchableOpacity,
325
448
  {
326
449
  onPress: () => onApprove?.(action.id),
450
+ disabled: expired,
327
451
  style: [
328
452
  confirmCardStyles.btn,
329
453
  confirmCardStyles.confirmBtn,
330
- { backgroundColor: actionColor }
454
+ { backgroundColor: actionColor },
455
+ expired && confirmCardStyles.disabledBtn
331
456
  ],
332
457
  activeOpacity: 0.7,
333
- accessibilityLabel: confirmLabel,
458
+ accessibilityRole: "button",
459
+ accessibilityState: { disabled: expired },
460
+ accessibilityLabel: confirmA11yLabel,
334
461
  testID: "assistant-confirm-approve"
335
462
  },
336
463
  /* @__PURE__ */ React8__default.createElement(Text, { style: [confirmCardStyles.confirmText, { fontFamily }] }, confirmLabel)
@@ -367,18 +494,33 @@ var confirmCardStyles = StyleSheet.create({
367
494
  // color set inline — theme-driven (textSecondary).
368
495
  marginBottom: 12
369
496
  },
497
+ // Wraps: on one line the two buttons ran off the LEFT edge at iOS AX5
498
+ // and Cancel was unreachable — on the one card that gates something
499
+ // irreversible.
370
500
  buttons: {
371
501
  flexDirection: "row",
502
+ flexWrap: "wrap",
372
503
  gap: 8,
373
504
  justifyContent: "flex-end"
374
505
  },
375
506
  btn: {
507
+ minHeight: MIN_TOUCH_TARGET,
508
+ maxWidth: "100%",
509
+ justifyContent: "center",
376
510
  paddingHorizontal: 14,
377
511
  paddingVertical: 8,
378
512
  borderRadius: 10,
379
513
  minWidth: 96,
380
514
  alignItems: "center"
381
515
  },
516
+ disabledBtn: {
517
+ opacity: 0.4
518
+ },
519
+ expired: {
520
+ fontSize: 13,
521
+ fontWeight: "600",
522
+ marginBottom: 12
523
+ },
382
524
  cancelText: {
383
525
  fontSize: 13,
384
526
  fontWeight: "600"
@@ -398,7 +540,11 @@ function StateIcon({
398
540
  tone,
399
541
  primaryColor
400
542
  }) {
401
- return /* @__PURE__ */ React8__default.createElement(View, { style: [styles.iconCircle, { backgroundColor: tone.iconBg }] }, state === "running" ? /* @__PURE__ */ React8__default.createElement(ActivityIndicator, { size: "small", color: primaryColor }) : /* @__PURE__ */ React8__default.createElement(Text, { style: [styles.iconGlyph, { color: tone.color }] }, iconGlyphFor(state)));
543
+ return /* @__PURE__ */ React8__default.createElement(View, { style: [styles.iconCircle, { backgroundColor: tone.iconBg }] }, state === "running" ? /* @__PURE__ */ React8__default.createElement(ActivityIndicator, { size: "small", color: primaryColor }) : (
544
+ // Decorative, inside a fixed circle: capped so it cannot clip. The
545
+ // state is also in the row's accessibilityLabel.
546
+ /* @__PURE__ */ React8__default.createElement(Text, { maxFontSizeMultiplier: 1.3, style: [styles.iconGlyph, { color: tone.color }] }, iconGlyphFor(state))
547
+ ));
402
548
  }
403
549
  var styles = StyleSheet.create({
404
550
  container: {
@@ -406,12 +552,26 @@ var styles = StyleSheet.create({
406
552
  marginBottom: 8,
407
553
  paddingVertical: 4
408
554
  },
555
+ // Wraps so Approve/Reject drop below the label at large text sizes. On
556
+ // one line they squeezed the label to zero width (iOS AX5): the user was
557
+ // asked to approve an action they could not read.
409
558
  row: {
410
559
  flexDirection: "row",
560
+ flexWrap: "wrap",
561
+ gap: 8,
411
562
  alignItems: "center",
412
563
  paddingVertical: 6,
413
564
  paddingHorizontal: 4
414
565
  },
566
+ labelGroup: {
567
+ flexDirection: "row",
568
+ alignItems: "flex-start",
569
+ flexGrow: 1,
570
+ flexShrink: 1,
571
+ flexBasis: 0,
572
+ // What forces the wrap: the label keeps at least half the row.
573
+ minWidth: "50%"
574
+ },
415
575
  iconCircle: {
416
576
  width: 22,
417
577
  height: 22,
@@ -439,9 +599,11 @@ var styles = StyleSheet.create({
439
599
  approvalButtons: {
440
600
  flexDirection: "row",
441
601
  gap: 6,
442
- marginLeft: 8
602
+ marginLeft: "auto"
443
603
  },
444
604
  btn: {
605
+ minHeight: MIN_TOUCH_TARGET,
606
+ justifyContent: "center",
445
607
  paddingHorizontal: 10,
446
608
  paddingVertical: 5,
447
609
  borderRadius: 8
@@ -505,6 +667,7 @@ var APPILOTS_SKIP_PROPS = {
505
667
  };
506
668
 
507
669
  // src/components/AgentPill.tsx
670
+ var PILL_MAX_FONT_SCALE = 1.5;
508
671
  function toneFor(state, colors) {
509
672
  switch (state) {
510
673
  case "success":
@@ -519,6 +682,7 @@ function AgentPill({
519
682
  label,
520
683
  state,
521
684
  onExpand,
685
+ expandRef,
522
686
  onStop,
523
687
  position,
524
688
  insets,
@@ -527,15 +691,20 @@ function AgentPill({
527
691
  strings
528
692
  }) {
529
693
  const atTop = position === "top-left" || position === "top-right";
694
+ const reduceMotion2 = useReduceMotion();
530
695
  const enter = useRef(new Animated.Value(0)).current;
531
696
  useEffect(() => {
697
+ if (reduceMotion2) {
698
+ enter.setValue(1);
699
+ return;
700
+ }
532
701
  Animated.spring(enter, {
533
702
  toValue: 1,
534
703
  useNativeDriver: true,
535
704
  tension: 70,
536
705
  friction: 12
537
706
  }).start();
538
- }, [enter]);
707
+ }, [enter, reduceMotion2]);
539
708
  const translateY = enter.interpolate({
540
709
  inputRange: [0, 1],
541
710
  outputRange: [atTop ? -80 : 80, 0]
@@ -556,6 +725,7 @@ function AgentPill({
556
725
  TouchableOpacity,
557
726
  {
558
727
  ...APPILOTS_SKIP_PROPS,
728
+ ref: expandRef,
559
729
  testID: "appilots-agent-pill",
560
730
  accessibilityRole: "button",
561
731
  accessibilityLabel: strings.expandA11y,
@@ -568,7 +738,8 @@ function AgentPill({
568
738
  Text,
569
739
  {
570
740
  testID: "appilots-agent-pill-label",
571
- numberOfLines: 1,
741
+ numberOfLines: 2,
742
+ maxFontSizeMultiplier: PILL_MAX_FONT_SCALE,
572
743
  style: [styles2.label, { color: colors.text, fontFamily }]
573
744
  },
574
745
  label
@@ -640,15 +811,21 @@ function SpotlightOverlay({
640
811
  fontFamily,
641
812
  window: win
642
813
  }) {
643
- const pulse = useRef(new Animated.Value(0)).current;
814
+ const reduceMotion2 = useReduceMotion();
815
+ const pulse = useRef(new Animated.Value(reduceMotion2 ? 1 : 0)).current;
644
816
  useEffect(() => {
817
+ if (reduceMotion2) {
818
+ pulse.stopAnimation();
819
+ pulse.setValue(1);
820
+ return;
821
+ }
645
822
  pulse.setValue(0);
646
823
  Animated.timing(pulse, {
647
824
  toValue: 1,
648
825
  duration: 220,
649
826
  useNativeDriver: true
650
827
  }).start();
651
- }, [seq, pulse]);
828
+ }, [seq, pulse, reduceMotion2]);
652
829
  if (!isRectOnScreen(rect, win)) return null;
653
830
  const frame = {
654
831
  left: rect.x - PADDING,
@@ -739,11 +916,11 @@ var NEVER = () => () => {
739
916
  };
740
917
  var NULL_TARGET = () => null;
741
918
  function useSpotlight(enabled) {
742
- const subscribe = useCallback(
919
+ const subscribe2 = useCallback(
743
920
  (onChange) => enabled ? subscribeSpotlight(onChange) : NEVER(),
744
921
  [enabled]
745
922
  );
746
- return useSyncExternalStore(subscribe, enabled ? getSpotlightTarget : NULL_TARGET, NULL_TARGET);
923
+ return useSyncExternalStore(subscribe2, enabled ? getSpotlightTarget : NULL_TARGET, NULL_TARGET);
747
924
  }
748
925
 
749
926
  // src/components/pillStatus.ts
@@ -763,12 +940,74 @@ function resolvePillStatus(actions, busy, strings) {
763
940
  }
764
941
  return { label: strings.working, state: "running" };
765
942
  }
766
-
767
- // src/components/speech.ts
768
- function isSpeechHost(value) {
769
- if (!value || typeof value !== "object") return false;
770
- const candidate = value;
771
- return typeof candidate.start === "function" && typeof candidate.stop === "function" && typeof candidate.cancel === "function";
943
+ var STREAMING_PREFIX = "streaming_";
944
+ function announce(message) {
945
+ if (Platform.OS === "ios" && AccessibilityInfo.announceForAccessibilityWithOptions) {
946
+ AccessibilityInfo.announceForAccessibilityWithOptions(message, { queue: true });
947
+ } else {
948
+ AccessibilityInfo.announceForAccessibility(message);
949
+ }
950
+ }
951
+ var speakable = (content) => content.replace(/[*`#]+/g, "").trim();
952
+ function useChatAnnouncements({
953
+ messages,
954
+ actions,
955
+ awaitingApproval,
956
+ acting,
957
+ locale,
958
+ strings
959
+ }) {
960
+ const seenMessages = useRef(null);
961
+ if (!seenMessages.current) seenMessages.current = new Set(messages.map((m) => m.id));
962
+ const seenActions = useRef(null);
963
+ if (!seenActions.current) seenActions.current = new Map(actions.map((a) => [a.id, a.status]));
964
+ const prev = useRef({ awaitingApproval, acting });
965
+ const namedSinceSettle = useRef(false);
966
+ const localeRef = useRef(locale);
967
+ localeRef.current = locale;
968
+ useEffect(
969
+ () => subscribeSpotlightAnnouncement((label, verb) => {
970
+ namedSinceSettle.current = true;
971
+ announce(targetAnnouncement(verb, label, localeRef.current));
972
+ }),
973
+ []
974
+ );
975
+ useEffect(() => {
976
+ const out = [];
977
+ for (const message of messages) {
978
+ if (message.role === "user" || message.id.startsWith(STREAMING_PREFIX)) continue;
979
+ if (seenMessages.current.has(message.id)) continue;
980
+ seenMessages.current.add(message.id);
981
+ const text = speakable(message.content);
982
+ if (text) out.push(text);
983
+ }
984
+ for (const action of actions) {
985
+ if (action.type === "confirm") continue;
986
+ if (seenActions.current.get(action.id) === action.status) continue;
987
+ seenActions.current.set(action.id, action.status);
988
+ const line = actionAnnouncement(action, locale, namedSinceSettle.current);
989
+ if (action.status === "completed" || action.status === "failed") {
990
+ namedSinceSettle.current = false;
991
+ }
992
+ if (line) out.push(line);
993
+ }
994
+ if (awaitingApproval && !prev.current.awaitingApproval) out.push(strings.a11yAwaitingApproval);
995
+ if (prev.current.acting && !acting) out.push(strings.a11yTurnDone);
996
+ prev.current = { awaitingApproval, acting };
997
+ for (const line of out) announce(line);
998
+ }, [messages, actions, awaitingApproval, acting, locale, strings]);
999
+ }
1000
+ function useStillWorking(active, template) {
1001
+ const [seconds, setSeconds] = useState(0);
1002
+ useEffect(() => {
1003
+ setSeconds(0);
1004
+ if (!active) return;
1005
+ return startStillWorkingTimer((elapsed) => {
1006
+ setSeconds(elapsed);
1007
+ AccessibilityInfo.announceForAccessibility(template.replace("{seconds}", String(elapsed)));
1008
+ });
1009
+ }, [active, template]);
1010
+ return seconds;
772
1011
  }
773
1012
  function useVoiceInput({
774
1013
  host,
@@ -841,6 +1080,15 @@ function useVoiceInput({
841
1080
  });
842
1081
  return dispose;
843
1082
  }, [host, state]);
1083
+ useEffect(() => {
1084
+ if (!host?.onEnd) return;
1085
+ if (state !== "listening") return;
1086
+ return host.onEnd((error) => {
1087
+ if (!mounted.current) return;
1088
+ setPartial("");
1089
+ setState(error ? "error" : "idle");
1090
+ });
1091
+ }, [host, state]);
844
1092
  return { state, partial, start, stop, cancel };
845
1093
  }
846
1094
 
@@ -894,7 +1142,7 @@ function resolveExperience(requested, caps) {
894
1142
  field: "input",
895
1143
  from: input.join("+"),
896
1144
  to: input.filter((mode) => mode !== "voice").join("+") || "text",
897
- reason: 'input:"voice" needs a SpeechHost passed to <AppilotsChat speech={...} />. The SDK ships no speech engine of its own \u2014 see docs/experience.md.'
1145
+ reason: 'input:"voice" needs a SpeechHost passed to <AppilotsChat speech={...} />. The SDK ships no speech engine of its own \u2014 see https://docs.appilots.com/docs/guides/voice-input.'
898
1146
  });
899
1147
  input = input.filter((mode) => mode !== "voice");
900
1148
  }
@@ -1290,9 +1538,13 @@ function isImageSource(value) {
1290
1538
  return typeof value === "number" || /^https?:\/\//i.test(value.trim());
1291
1539
  }
1292
1540
  var { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get("window");
1541
+ var A11Y_FOCUS_DELAY_MS = 500;
1293
1542
  var CHAT_HORIZONTAL_PADDING = 16;
1294
1543
  var BUBBLE_HORIZONTAL_PADDING = 14;
1295
1544
  var ASSISTANT_CONTENT_WIDTH = SCREEN_WIDTH - CHAT_HORIZONTAL_PADDING * 2 - BUBBLE_HORIZONTAL_PADDING * 2;
1545
+ var MIN_TOUCH_TARGET2 = 44;
1546
+ var LARGE_TEXT_FONT_SCALE = 1.6;
1547
+ var INPUT_MAX_HEIGHT = 120;
1296
1548
  function AppilotsChat(props) {
1297
1549
  const { degraded } = useAppilotsContext();
1298
1550
  if (degraded) return null;
@@ -1402,6 +1654,7 @@ function AppilotsChatInner({
1402
1654
  return void 0;
1403
1655
  }, [messages]);
1404
1656
  const insets = useSafeAreaInsetsOrDefault();
1657
+ const { fontScale } = useWindowDimensions();
1405
1658
  const [internalVisible, setInternalVisible] = useState(mode === "inline");
1406
1659
  const [inputText, setInputText] = useState("");
1407
1660
  const flatListRef = useRef(null);
@@ -1439,9 +1692,19 @@ function AppilotsChatInner({
1439
1692
  },
1440
1693
  [autoExecute]
1441
1694
  );
1442
- const awaitingUser = pendingActions.some((action) => requireApproval(action)) || escalation?.status === "pending" || escalation?.status === "active";
1695
+ const awaitingApproval = pendingActions.some((action) => requireApproval(action));
1696
+ const awaitingUser = awaitingApproval || escalation?.status === "pending" || escalation?.status === "active";
1443
1697
  const busy = isLoading || executingActions.length > 0;
1698
+ const stillWorkingSeconds = useStillWorking(busy && !awaitingUser, strings.stillWorking);
1444
1699
  const acting = busy && hasStartedActing;
1700
+ useChatAnnouncements({
1701
+ messages,
1702
+ actions: allActions,
1703
+ awaitingApproval,
1704
+ acting,
1705
+ locale: resolvedLocale,
1706
+ strings
1707
+ });
1445
1708
  const hostModalOpen = useHostModalOpen(
1446
1709
  usesPill(presence) && (busy || wantsSheet || unreadResult)
1447
1710
  );
@@ -1516,6 +1779,19 @@ function AppilotsChatInner({
1516
1779
  friction: 11
1517
1780
  }).start();
1518
1781
  }, [isVisible, slideAnim, mode]);
1782
+ const titleRef = useRef(null);
1783
+ const launcherRef = useRef(null);
1784
+ const pillRef = useRef(null);
1785
+ const wasVisibleRef = useRef(isVisible);
1786
+ useEffect(() => {
1787
+ if (mode === "inline" || wasVisibleRef.current === isVisible) return;
1788
+ wasVisibleRef.current = isVisible;
1789
+ const timer = setTimeout(() => {
1790
+ const target = isVisible ? titleRef.current : pillRef.current ?? launcherRef.current;
1791
+ if (target) AccessibilityInfo.sendAccessibilityEvent(target, "focus");
1792
+ }, A11Y_FOCUS_DELAY_MS);
1793
+ return () => clearTimeout(timer);
1794
+ }, [isVisible, mode]);
1519
1795
  useEffect(() => {
1520
1796
  if (messages.length > 0) {
1521
1797
  setTimeout(() => flatListRef.current?.scrollToEnd({ animated: true }), 100);
@@ -1658,6 +1934,10 @@ function AppilotsChatInner({
1658
1934
  },
1659
1935
  [theme, lastAssistantMessageId, escalation, requestHuman, strings]
1660
1936
  );
1937
+ const composerRef = useRef(null);
1938
+ const returnFocusToComposer = useCallback(() => {
1939
+ if (composerRef.current) AccessibilityInfo.sendAccessibilityEvent(composerRef.current, "focus");
1940
+ }, []);
1661
1941
  const renderBreadcrumb = useCallback(
1662
1942
  (actions) => /* @__PURE__ */ React8__default.createElement(
1663
1943
  ActionBreadcrumb,
@@ -1674,7 +1954,8 @@ function AppilotsChatInner({
1674
1954
  confirmDestructiveApprove: strings.confirmDestructiveApprove,
1675
1955
  confirmApprove: strings.confirmApprove,
1676
1956
  confirmCancel: strings.confirmCancel,
1677
- confirmThisAction: strings.confirmThisAction
1957
+ confirmThisAction: strings.confirmThisAction,
1958
+ confirmExpired: strings.confirmExpired
1678
1959
  },
1679
1960
  palette: {
1680
1961
  textSecondary: theme.colors.textSecondary,
@@ -1686,7 +1967,8 @@ function AppilotsChatInner({
1686
1967
  },
1687
1968
  requireApprovalFor: requireApproval,
1688
1969
  onApprove: approveAction,
1689
- onReject: rejectAction
1970
+ onReject: rejectAction,
1971
+ onConfirmCardGone: returnFocusToComposer
1690
1972
  }
1691
1973
  ),
1692
1974
  [
@@ -1703,7 +1985,8 @@ function AppilotsChatInner({
1703
1985
  resolvedLocale,
1704
1986
  requireApproval,
1705
1987
  approveAction,
1706
- rejectAction
1988
+ rejectAction,
1989
+ returnFocusToComposer
1707
1990
  ]
1708
1991
  );
1709
1992
  const resolvedHeaderTitle = resolvedTitle ?? t("chatTitle");
@@ -1746,6 +2029,7 @@ function AppilotsChatInner({
1746
2029
  mode === "fullscreen" && styles5.chatContainerFullscreen,
1747
2030
  mode === "sidebar" && styles5.chatContainerSidebar,
1748
2031
  mode === "inline" && styles5.chatContainerInline,
2032
+ mode === "bubble" && fontScale >= LARGE_TEXT_FONT_SCALE && styles5.chatContainerFullscreen,
1749
2033
  {
1750
2034
  backgroundColor: theme.colors.background,
1751
2035
  borderTopLeftRadius: theme.radii.lg,
@@ -1756,6 +2040,8 @@ function AppilotsChatInner({
1756
2040
  /* @__PURE__ */ React8__default.createElement(View, { style: [styles5.header, { borderBottomColor: theme.colors.border }] }, /* @__PURE__ */ React8__default.createElement(View, { style: styles5.headerLeft }, renderAvatar(), /* @__PURE__ */ React8__default.createElement(
1757
2041
  Text,
1758
2042
  {
2043
+ ref: titleRef,
2044
+ accessibilityRole: "header",
1759
2045
  style: [
1760
2046
  styles5.headerTitle,
1761
2047
  {
@@ -1882,8 +2168,7 @@ function AppilotsChatInner({
1882
2168
  color: theme.colors.textSecondary,
1883
2169
  fontFamily: theme.typography.fontFamily
1884
2170
  }
1885
- ],
1886
- numberOfLines: 2
2171
+ ]
1887
2172
  },
1888
2173
  resolvedEmptySubtitle
1889
2174
  ), showSuggestedPrompts && suggestedPrompts.length > 0 && /* @__PURE__ */ React8__default.createElement(View, { style: styles5.chipsContainer, testID: "assistant-suggested-prompts" }, suggestedPrompts.map((prompt) => /* @__PURE__ */ React8__default.createElement(
@@ -1912,8 +2197,7 @@ function AppilotsChatInner({
1912
2197
  fontFamily: theme.typography.fontFamily,
1913
2198
  fontSize: theme.typography.fontSizeSm
1914
2199
  }
1915
- ],
1916
- numberOfLines: 1
2200
+ ]
1917
2201
  },
1918
2202
  prompt.text
1919
2203
  )
@@ -1935,16 +2219,17 @@ function AppilotsChatInner({
1935
2219
  { color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily }
1936
2220
  ]
1937
2221
  },
1938
- strings[loadingStatusKey] ?? strings.thinking
2222
+ stillWorkingSeconds ? strings.stillWorking.replace("{seconds}", String(stillWorkingSeconds)) : strings[loadingStatusKey] ?? strings.thinking
1939
2223
  )
1940
2224
  ) : null
1941
2225
  }
1942
2226
  ),
1943
- mission && !["completed", "cancelled"].includes(mission.status) && /* @__PURE__ */ React8__default.createElement(View, { style: { padding: 12, gap: 8 }, testID: "appilots-mission-controls" }, /* @__PURE__ */ React8__default.createElement(Text, { style: { color: theme.colors.text }, testID: "appilots-mission-progress" }, mission.progressText, mission.status === "paused" ? ` \xB7 ${strings.missionPaused}` : ""), /* @__PURE__ */ React8__default.createElement(View, { style: { flexDirection: "row", gap: 12 } }, mission.status === "running" ? /* @__PURE__ */ React8__default.createElement(
2227
+ mission && !["completed", "cancelled"].includes(mission.status) && /* @__PURE__ */ React8__default.createElement(View, { style: { padding: 12, gap: 8 }, testID: "appilots-mission-controls" }, /* @__PURE__ */ React8__default.createElement(Text, { style: { color: theme.colors.text }, testID: "appilots-mission-progress" }, mission.progressText, mission.status === "paused" ? ` \xB7 ${strings.missionPaused}` : ""), /* @__PURE__ */ React8__default.createElement(View, { style: styles5.missionButtons }, mission.status === "running" ? /* @__PURE__ */ React8__default.createElement(
1944
2228
  TouchableOpacity,
1945
2229
  {
1946
2230
  onPress: () => void pauseMission(),
1947
2231
  testID: "appilots-mission-pause",
2232
+ style: styles5.missionButton,
1948
2233
  accessibilityRole: "button"
1949
2234
  },
1950
2235
  /* @__PURE__ */ React8__default.createElement(Text, { style: { color: theme.colors.text } }, strings.missionPause)
@@ -1954,6 +2239,7 @@ function AppilotsChatInner({
1954
2239
  onPress: () => void resumeMission(),
1955
2240
  disabled: isLoading,
1956
2241
  testID: "appilots-mission-resume",
2242
+ style: styles5.missionButton,
1957
2243
  accessibilityRole: "button"
1958
2244
  },
1959
2245
  /* @__PURE__ */ React8__default.createElement(Text, { style: { color: theme.colors.text } }, strings.missionResume)
@@ -1962,6 +2248,7 @@ function AppilotsChatInner({
1962
2248
  {
1963
2249
  onPress: () => void cancelMission(),
1964
2250
  testID: "appilots-mission-cancel",
2251
+ style: styles5.missionButton,
1965
2252
  accessibilityRole: "button"
1966
2253
  },
1967
2254
  /* @__PURE__ */ React8__default.createElement(Text, { style: { color: theme.colors.text } }, strings.missionCancel)
@@ -1980,6 +2267,7 @@ function AppilotsChatInner({
1980
2267
  /* @__PURE__ */ React8__default.createElement(
1981
2268
  TextInput,
1982
2269
  {
2270
+ ref: composerRef,
1983
2271
  style: [
1984
2272
  styles5.input,
1985
2273
  {
@@ -1988,7 +2276,12 @@ function AppilotsChatInner({
1988
2276
  borderRadius: theme.radii.md,
1989
2277
  fontFamily: theme.typography.fontFamily,
1990
2278
  fontSize: theme.typography.fontSizeBase,
1991
- lineHeight: theme.typography.fontSizeBase * 1.35
2279
+ lineHeight: theme.typography.fontSizeBase * 1.35,
2280
+ // The 120pt cap is ~5 lines at default size and barely one at
2281
+ // iOS AX5, where even the placeholder was cut. It grows with
2282
+ // the text, but never past a quarter of the screen, so the
2283
+ // keyboard still leaves room for the transcript.
2284
+ maxHeight: Math.min(INPUT_MAX_HEIGHT * fontScale, SCREEN_HEIGHT * 0.25)
1992
2285
  }
1993
2286
  ],
1994
2287
  placeholder: resolvedPlaceholder,
@@ -2083,6 +2376,7 @@ function AppilotsChatInner({
2083
2376
  TouchableOpacity,
2084
2377
  {
2085
2378
  ...APPILOTS_SKIP_PROPS,
2379
+ ref: launcherRef,
2086
2380
  onPress: openChat,
2087
2381
  activeOpacity: 0.8,
2088
2382
  accessibilityRole: "button",
@@ -2096,6 +2390,7 @@ function AppilotsChatInner({
2096
2390
  TouchableOpacity,
2097
2391
  {
2098
2392
  ...APPILOTS_SKIP_PROPS,
2393
+ ref: launcherRef,
2099
2394
  style: [
2100
2395
  styles5.fab,
2101
2396
  fabPositionStyle,
@@ -2148,6 +2443,7 @@ function AppilotsChatInner({
2148
2443
  },
2149
2444
  strings: { expandA11y: strings.pillExpandA11y, stopA11y: strings.pillStopA11y },
2150
2445
  onExpand: openChat,
2446
+ expandRef: pillRef,
2151
2447
  onStop: busy ? cancelMessage : void 0
2152
2448
  }
2153
2449
  ), /* @__PURE__ */ React8__default.createElement(SheetHost, { useModal: presence === "sheet", visible: isVisible, onRequestClose: closeChat }, /* @__PURE__ */ React8__default.createElement(
@@ -2155,6 +2451,7 @@ function AppilotsChatInner({
2155
2451
  {
2156
2452
  ...APPILOTS_SKIP_PROPS,
2157
2453
  pointerEvents: presence === "sheet" ? "auto" : "box-none",
2454
+ onAccessibilityEscape: closeChat,
2158
2455
  style: [
2159
2456
  mode === "fullscreen" ? styles5.modalContainerFullscreen : styles5.modalContainer,
2160
2457
  mode === "sidebar" && styles5.modalContainerSidebar,
@@ -2205,6 +2502,7 @@ function SheetHost({
2205
2502
  {
2206
2503
  ...APPILOTS_SKIP_PROPS,
2207
2504
  pointerEvents: "box-none",
2505
+ accessibilityViewIsModal: true,
2208
2506
  style: [StyleSheet.absoluteFill, styles5.sheetOverlay]
2209
2507
  },
2210
2508
  children
@@ -2303,8 +2601,13 @@ var styles5 = StyleSheet.create({
2303
2601
  maxHeight: "100%",
2304
2602
  flex: 1
2305
2603
  },
2604
+ // Wraps instead of squeezing: at large text sizes the title alone is
2605
+ // wider than the row, and without the wrap it pushed Clear and ✕ off the
2606
+ // right edge — the chat could not be closed (measured at iOS AX5).
2306
2607
  header: {
2307
2608
  flexDirection: "row",
2609
+ flexWrap: "wrap",
2610
+ rowGap: 4,
2308
2611
  alignItems: "center",
2309
2612
  justifyContent: "space-between",
2310
2613
  paddingHorizontal: 16,
@@ -2314,7 +2617,8 @@ var styles5 = StyleSheet.create({
2314
2617
  headerLeft: {
2315
2618
  flexDirection: "row",
2316
2619
  alignItems: "center",
2317
- gap: 8
2620
+ gap: 8,
2621
+ flexShrink: 1
2318
2622
  },
2319
2623
  headerDot: {
2320
2624
  width: 8,
@@ -2333,24 +2637,30 @@ var styles5 = StyleSheet.create({
2333
2637
  justifyContent: "center"
2334
2638
  },
2335
2639
  headerTitle: {
2336
- fontWeight: "600"
2640
+ fontWeight: "600",
2641
+ flexShrink: 1
2337
2642
  },
2338
2643
  headerActions: {
2339
2644
  flexDirection: "row",
2340
2645
  alignItems: "center",
2341
- gap: 12
2646
+ gap: 4,
2647
+ // Stays right-aligned when the header wraps it onto its own line.
2648
+ marginLeft: "auto"
2342
2649
  },
2343
2650
  headerButton: {
2651
+ minWidth: MIN_TOUCH_TARGET2,
2652
+ minHeight: MIN_TOUCH_TARGET2,
2344
2653
  paddingHorizontal: 8,
2345
- paddingVertical: 4
2654
+ alignItems: "center",
2655
+ justifyContent: "center"
2346
2656
  },
2347
2657
  headerButtonText: {
2348
2658
  fontSize: 13
2349
2659
  },
2350
2660
  closeButton: {
2351
- width: 28,
2352
- height: 28,
2353
- borderRadius: 14,
2661
+ minWidth: MIN_TOUCH_TARGET2,
2662
+ minHeight: MIN_TOUCH_TARGET2,
2663
+ borderRadius: MIN_TOUCH_TARGET2 / 2,
2354
2664
  alignItems: "center",
2355
2665
  justifyContent: "center"
2356
2666
  },
@@ -2432,6 +2742,8 @@ var styles5 = StyleSheet.create({
2432
2742
  maxWidth: 320
2433
2743
  },
2434
2744
  chip: {
2745
+ minHeight: MIN_TOUCH_TARGET2,
2746
+ justifyContent: "center",
2435
2747
  paddingHorizontal: 16,
2436
2748
  paddingVertical: 10,
2437
2749
  borderWidth: 1,
@@ -2494,16 +2806,16 @@ var styles5 = StyleSheet.create({
2494
2806
  },
2495
2807
  input: {
2496
2808
  flex: 1,
2497
- minHeight: 38,
2498
- maxHeight: 120,
2809
+ minHeight: MIN_TOUCH_TARGET2,
2810
+ maxHeight: INPUT_MAX_HEIGHT,
2499
2811
  paddingHorizontal: 16,
2500
2812
  paddingTop: Platform.OS === "ios" ? 9 : 8,
2501
2813
  paddingBottom: Platform.OS === "ios" ? 9 : 8,
2502
2814
  textAlignVertical: "top"
2503
2815
  },
2504
2816
  sendButton: {
2505
- width: 38,
2506
- height: 38,
2817
+ minWidth: MIN_TOUCH_TARGET2,
2818
+ minHeight: MIN_TOUCH_TARGET2,
2507
2819
  alignItems: "center",
2508
2820
  justifyContent: "center"
2509
2821
  },
@@ -2512,6 +2824,15 @@ var styles5 = StyleSheet.create({
2512
2824
  fontSize: 18,
2513
2825
  fontWeight: "700"
2514
2826
  },
2827
+ missionButtons: {
2828
+ flexDirection: "row",
2829
+ flexWrap: "wrap",
2830
+ columnGap: 12
2831
+ },
2832
+ missionButton: {
2833
+ minHeight: MIN_TOUCH_TARGET2,
2834
+ justifyContent: "center"
2835
+ },
2515
2836
  poweredByContainer: {
2516
2837
  alignItems: "center",
2517
2838
  paddingVertical: 4