@btndprojects/loam-ui-native 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/LICENSE +21 -0
  2. package/dist/accordion/index.js +3 -0
  3. package/dist/adaptive/index.js +2 -0
  4. package/dist/alert/index.js +3 -0
  5. package/dist/analyzer/index.js +6 -0
  6. package/dist/announcement/index.js +6 -0
  7. package/dist/audio-player/index.js +3 -0
  8. package/dist/avatar/index.js +3 -0
  9. package/dist/avatar-group/index.js +3 -0
  10. package/dist/badge/index.js +5 -0
  11. package/dist/banner/index.js +2 -0
  12. package/dist/board/index.js +7 -0
  13. package/dist/button/index.js +5 -0
  14. package/dist/camera-permission/index.js +13 -0
  15. package/dist/capture-result/index.js +18 -0
  16. package/dist/card/index.js +4 -0
  17. package/dist/card-form/index.js +10 -0
  18. package/dist/carousel/index.js +2 -0
  19. package/dist/chart/index.js +6 -0
  20. package/dist/charts/index.js +7 -0
  21. package/dist/checkbox/index.js +7 -0
  22. package/dist/checklist/index.js +10 -0
  23. package/dist/checkout/index.js +13 -0
  24. package/dist/chip/index.js +7 -0
  25. package/dist/chunk-223Q2T2Q.js +229 -0
  26. package/dist/chunk-2A5NIQAK.js +254 -0
  27. package/dist/chunk-2GK6PHGE.js +363 -0
  28. package/dist/chunk-2WUTXHMU.js +1 -0
  29. package/dist/chunk-2ZZ7TDJ4.js +324 -0
  30. package/dist/chunk-35OFI3JG.js +499 -0
  31. package/dist/chunk-3ARE4TES.js +195 -0
  32. package/dist/chunk-3ECPOSKO.js +1 -0
  33. package/dist/chunk-3ENTMPQY.js +229 -0
  34. package/dist/chunk-3HJH6FF4.js +98 -0
  35. package/dist/chunk-3NMD74ZU.js +4 -0
  36. package/dist/chunk-3WPW4KUN.js +182 -0
  37. package/dist/chunk-47FWHGUN.js +394 -0
  38. package/dist/chunk-4BXMXBJO.js +141 -0
  39. package/dist/chunk-4DS5ZWVC.js +83 -0
  40. package/dist/chunk-4GBOGOTV.js +295 -0
  41. package/dist/chunk-4SZYY6AN.js +99 -0
  42. package/dist/chunk-4YIP3CWS.js +237 -0
  43. package/dist/chunk-4YQOR7JM.js +204 -0
  44. package/dist/chunk-4ZAHCY44.js +177 -0
  45. package/dist/chunk-52X2VYTD.js +228 -0
  46. package/dist/chunk-5P26LBDL.js +1 -0
  47. package/dist/chunk-5XLRYDAX.js +1 -0
  48. package/dist/chunk-5YNTNN5A.js +177 -0
  49. package/dist/chunk-66SG2H23.js +129 -0
  50. package/dist/chunk-6HT2LP62.js +504 -0
  51. package/dist/chunk-6N33LFQA.js +35 -0
  52. package/dist/chunk-6X6PQ443.js +432 -0
  53. package/dist/chunk-7CFF4HXS.js +126 -0
  54. package/dist/chunk-7O2FMUPT.js +755 -0
  55. package/dist/chunk-AI4DWZCD.js +162 -0
  56. package/dist/chunk-AWJOCGFZ.js +213 -0
  57. package/dist/chunk-AZRX6SAC.js +285 -0
  58. package/dist/chunk-B54QE2FN.js +1 -0
  59. package/dist/chunk-BC475SJH.js +1 -0
  60. package/dist/chunk-BEDBFJND.js +202 -0
  61. package/dist/chunk-BTTFCMKG.js +166 -0
  62. package/dist/chunk-BV3B2HYZ.js +1635 -0
  63. package/dist/chunk-C2NTXVWM.js +98 -0
  64. package/dist/chunk-CDRZHT42.js +424 -0
  65. package/dist/chunk-CJ5EOYZE.js +62 -0
  66. package/dist/chunk-CUMUNPXY.js +128 -0
  67. package/dist/chunk-CUS3ZSCU.js +1 -0
  68. package/dist/chunk-CYRBHQQZ.js +411 -0
  69. package/dist/chunk-DHWADYZX.js +144 -0
  70. package/dist/chunk-DKCSPATL.js +1 -0
  71. package/dist/chunk-DM5QUKKO.js +326 -0
  72. package/dist/chunk-DSJBXLUG.js +845 -0
  73. package/dist/chunk-DYBH2I6W.js +1 -0
  74. package/dist/chunk-DZ255O2F.js +1 -0
  75. package/dist/chunk-E35BPSGW.js +1 -0
  76. package/dist/chunk-EJBDRVIH.js +166 -0
  77. package/dist/chunk-F43NP42L.js +1 -0
  78. package/dist/chunk-F5XNPWS7.js +1024 -0
  79. package/dist/chunk-FIF2M24N.js +1521 -0
  80. package/dist/chunk-FOKG57CR.js +993 -0
  81. package/dist/chunk-G3HOYSF7.js +1 -0
  82. package/dist/chunk-GDKZTW34.js +395 -0
  83. package/dist/chunk-GL7B2DOW.js +134 -0
  84. package/dist/chunk-GNNJ4GCC.js +471 -0
  85. package/dist/chunk-GNZ56EUI.js +1 -0
  86. package/dist/chunk-H6NIOQHK.js +346 -0
  87. package/dist/chunk-H6PC7RDA.js +1 -0
  88. package/dist/chunk-HPPW424V.js +334 -0
  89. package/dist/chunk-HQOQIBJI.js +1 -0
  90. package/dist/chunk-HU2EQNRZ.js +44 -0
  91. package/dist/chunk-IIQTAY3J.js +715 -0
  92. package/dist/chunk-ITTYJZWX.js +271 -0
  93. package/dist/chunk-IVNRBPGC.js +1018 -0
  94. package/dist/chunk-J4KLDGI4.js +1 -0
  95. package/dist/chunk-J7Q7VCVC.js +254 -0
  96. package/dist/chunk-JDEOITQ2.js +1 -0
  97. package/dist/chunk-JNH2TYIN.js +96 -0
  98. package/dist/chunk-JRLTGLN4.js +1 -0
  99. package/dist/chunk-JTRJJLKL.js +1 -0
  100. package/dist/chunk-JUAMLEHX.js +379 -0
  101. package/dist/chunk-JXJB2X6M.js +131 -0
  102. package/dist/chunk-KMI5ML5I.js +262 -0
  103. package/dist/chunk-KTAC4MGE.js +4 -0
  104. package/dist/chunk-LAECSR4H.js +178 -0
  105. package/dist/chunk-LK4BCSGX.js +1 -0
  106. package/dist/chunk-LOE5YNN3.js +470 -0
  107. package/dist/chunk-LRJNDEIA.js +302 -0
  108. package/dist/chunk-LSS54GLJ.js +116 -0
  109. package/dist/chunk-LZLHELAG.js +137 -0
  110. package/dist/chunk-M2LUXGKZ.js +1 -0
  111. package/dist/chunk-MOUJOP55.js +425 -0
  112. package/dist/chunk-N32RGOA5.js +159 -0
  113. package/dist/chunk-N5ZNE2YE.js +1 -0
  114. package/dist/chunk-NBNCBML6.js +1 -0
  115. package/dist/chunk-NKB7BZYK.js +167 -0
  116. package/dist/chunk-NQYBUO3K.js +1795 -0
  117. package/dist/chunk-NY2U3PBI.js +59 -0
  118. package/dist/chunk-O4AEHWII.js +464 -0
  119. package/dist/chunk-O7AE6DAF.js +146 -0
  120. package/dist/chunk-OETCUXQP.js +134 -0
  121. package/dist/chunk-OOPPQH27.js +179 -0
  122. package/dist/chunk-OPXFZ4Y5.js +204 -0
  123. package/dist/chunk-P23EQNRQ.js +338 -0
  124. package/dist/chunk-PJXUJYS6.js +248 -0
  125. package/dist/chunk-PNWUWOBF.js +1 -0
  126. package/dist/chunk-PUPM2YGF.js +376 -0
  127. package/dist/chunk-PZXZJBMP.js +620 -0
  128. package/dist/chunk-Q72LJOVE.js +169 -0
  129. package/dist/chunk-QCDXZBC5.js +247 -0
  130. package/dist/chunk-QDYDBCFP.js +197 -0
  131. package/dist/chunk-QJJ6MADV.js +274 -0
  132. package/dist/chunk-QKXCPTFS.js +349 -0
  133. package/dist/chunk-QWCMU5B2.js +860 -0
  134. package/dist/chunk-RGQWWKLA.js +634 -0
  135. package/dist/chunk-RIBDXGUG.js +108 -0
  136. package/dist/chunk-RJQOOHT2.js +214 -0
  137. package/dist/chunk-RNGLIHFA.js +191 -0
  138. package/dist/chunk-RQ3HASCC.js +6 -0
  139. package/dist/chunk-RSOJT5YW.js +124 -0
  140. package/dist/chunk-RTIKSY6A.js +1 -0
  141. package/dist/chunk-RUZHTGKB.js +237 -0
  142. package/dist/chunk-RW6ZL2FQ.js +284 -0
  143. package/dist/chunk-S7F6XLTK.js +540 -0
  144. package/dist/chunk-SBMNWWBV.js +155 -0
  145. package/dist/chunk-SDTQPRRB.js +1 -0
  146. package/dist/chunk-SYNOJOQO.js +377 -0
  147. package/dist/chunk-T753XT36.js +1 -0
  148. package/dist/chunk-TKQYWZ5V.js +196 -0
  149. package/dist/chunk-TKVFVF5D.js +95 -0
  150. package/dist/chunk-TOXAGRDQ.js +144 -0
  151. package/dist/chunk-TUTI5HOZ.js +112 -0
  152. package/dist/chunk-U7TFM2JA.js +140 -0
  153. package/dist/chunk-UC2WGUP3.js +627 -0
  154. package/dist/chunk-UWFLYMAR.js +1126 -0
  155. package/dist/chunk-UWSZB7BJ.js +1 -0
  156. package/dist/chunk-UYG5L34B.js +87 -0
  157. package/dist/chunk-VBOXAEPI.js +336 -0
  158. package/dist/chunk-VCDBXW5H.js +188 -0
  159. package/dist/chunk-VCXPHHDS.js +1 -0
  160. package/dist/chunk-VEAYTRPX.js +414 -0
  161. package/dist/chunk-VFCUIDRT.js +1 -0
  162. package/dist/chunk-VQHPQEUH.js +273 -0
  163. package/dist/chunk-VXDZFJGP.js +874 -0
  164. package/dist/chunk-WS56BL3D.js +1 -0
  165. package/dist/chunk-WSW6ZFAQ.js +98 -0
  166. package/dist/chunk-WVKKJA6U.js +870 -0
  167. package/dist/chunk-XNUVB5OL.js +375 -0
  168. package/dist/chunk-XOSCY2CW.js +242 -0
  169. package/dist/chunk-XXXEFXY5.js +99 -0
  170. package/dist/chunk-Y5CSSPV4.js +1 -0
  171. package/dist/chunk-Y6DU4J47.js +1 -0
  172. package/dist/chunk-YC6WORFL.js +535 -0
  173. package/dist/chunk-YDGVJETP.js +60 -0
  174. package/dist/chunk-YF4NSSMT.js +366 -0
  175. package/dist/chunk-YG3ZJHGU.js +223 -0
  176. package/dist/chunk-YIJ55TBC.js +1 -0
  177. package/dist/chunk-YKZ4RZZF.js +1 -0
  178. package/dist/chunk-YLLCXTMC.js +37 -0
  179. package/dist/chunk-YTA74UZR.js +1 -0
  180. package/dist/chunk-Z7H7VSFZ.js +323 -0
  181. package/dist/chunk-ZK2H4O5P.js +1 -0
  182. package/dist/chunk-ZVFE5QGU.js +524 -0
  183. package/dist/coachmark/index.js +4 -0
  184. package/dist/code-block/index.js +4 -0
  185. package/dist/color-field/index.js +7 -0
  186. package/dist/commerce/index.js +20 -0
  187. package/dist/connectivity/index.js +2 -0
  188. package/dist/context-menu/index.js +5 -0
  189. package/dist/copy-button/index.js +3 -0
  190. package/dist/data-grid/index.js +24 -0
  191. package/dist/date-range-picker/index.js +13 -0
  192. package/dist/description-list/index.js +2 -0
  193. package/dist/dialog/index.js +3 -0
  194. package/dist/empty-state/index.js +3 -0
  195. package/dist/error-summary/index.js +3 -0
  196. package/dist/fab/index.js +2 -0
  197. package/dist/file-upload/index.js +9 -0
  198. package/dist/filter-bar/index.js +8 -0
  199. package/dist/flow-run/index.js +14 -0
  200. package/dist/form/index.js +3 -0
  201. package/dist/gallery/index.js +2 -0
  202. package/dist/header/index.js +5 -0
  203. package/dist/hold-to-confirm/index.js +4 -0
  204. package/dist/index.d.ts +22883 -0
  205. package/dist/index.js +158 -0
  206. package/dist/infinite-list/index.js +2 -0
  207. package/dist/inline-edit/index.js +5 -0
  208. package/dist/input/index.js +5 -0
  209. package/dist/interactions/index.js +2 -0
  210. package/dist/invoice/index.js +10 -0
  211. package/dist/kbd/index.js +1 -0
  212. package/dist/keyboard/index.js +3 -0
  213. package/dist/list-row/index.js +7 -0
  214. package/dist/log-viewer/index.js +2 -0
  215. package/dist/map-view/index.js +3 -0
  216. package/dist/markdown/index.js +6 -0
  217. package/dist/metric/index.js +4 -0
  218. package/dist/money/index.js +3 -0
  219. package/dist/nav-drawer/index.js +2 -0
  220. package/dist/navigation/index.js +2 -0
  221. package/dist/notification-center/index.js +16 -0
  222. package/dist/number-roll/index.js +3 -0
  223. package/dist/one-handed/index.js +5 -0
  224. package/dist/os-surfaces/index.js +3 -0
  225. package/dist/otp/index.js +7 -0
  226. package/dist/page-header/index.js +1 -0
  227. package/dist/payment-method/index.js +12 -0
  228. package/dist/payment-status/index.js +16 -0
  229. package/dist/permission/index.js +2 -0
  230. package/dist/picker/index.js +7 -0
  231. package/dist/progress/index.js +5 -0
  232. package/dist/pull-to-refresh/index.js +4 -0
  233. package/dist/radio-group/index.js +6 -0
  234. package/dist/rating/index.js +2 -0
  235. package/dist/receivables/index.js +20 -0
  236. package/dist/refund/index.js +16 -0
  237. package/dist/save-status/index.js +3 -0
  238. package/dist/scroll-area/index.js +1 -0
  239. package/dist/search-bar/index.js +5 -0
  240. package/dist/segmented-control/index.js +5 -0
  241. package/dist/sheet/index.js +4 -0
  242. package/dist/skeleton/index.js +2 -0
  243. package/dist/slider/index.js +5 -0
  244. package/dist/spinner/index.js +2 -0
  245. package/dist/split-button/index.js +7 -0
  246. package/dist/spotlight/index.js +2 -0
  247. package/dist/stage-timeline/index.js +4 -0
  248. package/dist/status-dot/index.js +2 -0
  249. package/dist/stepper/index.js +6 -0
  250. package/dist/streaming-text/index.js +3 -0
  251. package/dist/survey/index.js +19 -0
  252. package/dist/switch/index.js +5 -0
  253. package/dist/system-state/index.js +4 -0
  254. package/dist/tab-bar/index.js +2 -0
  255. package/dist/tabs/index.js +2 -0
  256. package/dist/textarea/index.js +6 -0
  257. package/dist/thinking/index.js +2 -0
  258. package/dist/timeline/index.js +4 -0
  259. package/dist/toast/index.js +2 -0
  260. package/dist/toolbar/index.js +2 -0
  261. package/dist/tour/index.js +11 -0
  262. package/dist/truncation-notice/index.js +2 -0
  263. package/dist/update-notice/index.js +11 -0
  264. package/dist/video-player/index.js +2 -0
  265. package/dist/viewfinder/index.js +5 -0
  266. package/dist/widget/index.js +4 -0
  267. package/dist/wizard/index.js +6 -0
  268. package/package.json +76 -3
  269. package/README.md +0 -3
@@ -0,0 +1,1635 @@
1
+ import { camInk, GlassIconButton, decorativeProps, untabbableProps, camMeta, pressedProps } from './chunk-RW6ZL2FQ.js';
2
+ import { useReducedMotion } from './chunk-6N33LFQA.js';
3
+ import { Icon } from './chunk-TKVFVF5D.js';
4
+ import * as React3 from 'react';
5
+ import { Animated, View, Text, Pressable, Easing, Dimensions, ScrollView } from 'react-native';
6
+ import Svg2, { Path, Circle, Defs, LinearGradient, Stop, Rect, Line } from 'react-native-svg';
7
+ import { createStyleSheet, useLoamTheme, useStyles } from '@btndprojects/loam-theme-native';
8
+ import { TRACK_BAND_BOTTOM, EXPERT, CONTINUOUS_NOTICE, confidenceTone, TRACKING_MOTION, confidenceLabel, trackCountLabel, trackTone, chipRoom, chipAnchor, chipSlot, visibleChips, MAX_CHIPS, trackingSummaryLabel, readBatchLabel } from '@btndprojects/loam-analyzer';
9
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
+
11
+ var RING_R = 46;
12
+ var RING_C = 2 * Math.PI * RING_R;
13
+ var RING_STROKE = 6;
14
+ function CountdownRing({
15
+ value,
16
+ label = "Auto-capturing"
17
+ }) {
18
+ const theme = useLoamTheme();
19
+ const reduced = useReducedMotion();
20
+ const { styles } = useStyles(controlSheet);
21
+ const pct2 = Math.min(100, Math.max(0, value));
22
+ const still = reduced || theme.reduceMotion;
23
+ return /* @__PURE__ */ jsx(
24
+ View,
25
+ {
26
+ accessibilityRole: "progressbar",
27
+ role: "progressbar",
28
+ accessibilityLabel: label,
29
+ accessibilityValue: { min: 0, max: 100, now: Math.round(pct2) },
30
+ style: styles.ringRoot,
31
+ ...{
32
+ "aria-valuenow": Math.round(pct2),
33
+ "aria-valuemin": 0,
34
+ "aria-valuemax": 100
35
+ },
36
+ children: still ? /* @__PURE__ */ jsx(Text, { style: styles.ringNumber, children: String(Math.round(pct2)) }) : /* @__PURE__ */ jsx(Svg2, { width: "100%", height: "100%", viewBox: "0 0 100 100", children: /* @__PURE__ */ jsx(
37
+ Circle,
38
+ {
39
+ cx: 50,
40
+ cy: 50,
41
+ r: RING_R,
42
+ stroke: theme.color.good,
43
+ strokeWidth: RING_STROKE,
44
+ strokeLinecap: "round",
45
+ strokeDasharray: RING_C,
46
+ strokeDashoffset: RING_C * (1 - pct2 / 100),
47
+ fill: "none",
48
+ transform: "rotate(-90 50 50)"
49
+ }
50
+ ) })
51
+ }
52
+ );
53
+ }
54
+ function ShutterButton({
55
+ enabled = true,
56
+ mode = "manual",
57
+ platform = "web",
58
+ countdown = null,
59
+ blockedToast,
60
+ label = "Capture",
61
+ onPress,
62
+ onBlocked
63
+ }) {
64
+ const theme = useLoamTheme();
65
+ const { styles } = useStyles(controlSheet, { platform, enabled });
66
+ const glyphColour = enabled ? theme.media.onInk : camInk(theme);
67
+ if (mode === "continuous") return null;
68
+ return /* @__PURE__ */ jsxs(
69
+ Pressable,
70
+ {
71
+ accessibilityRole: "button",
72
+ accessibilityLabel: label,
73
+ accessibilityState: { disabled: !enabled },
74
+ testID: enabled ? "loam-shutter-open" : "loam-shutter-blocked",
75
+ style: styles.shutter,
76
+ onPress: () => {
77
+ if (!enabled) {
78
+ onBlocked?.(blockedToast ?? "");
79
+ return;
80
+ }
81
+ onPress?.();
82
+ },
83
+ children: [
84
+ /* @__PURE__ */ jsx(View, { style: styles.shutterFill, children: /* @__PURE__ */ jsx(
85
+ Icon,
86
+ {
87
+ name: enabled ? "circle" : "lock",
88
+ size: enabled ? theme.icon.lg : theme.icon.md,
89
+ color: glyphColour
90
+ }
91
+ ) }),
92
+ typeof countdown === "number" ? /* @__PURE__ */ jsx(CountdownRing, { value: countdown }) : null
93
+ ]
94
+ }
95
+ );
96
+ }
97
+ function ContinuousNotice({
98
+ text = CONTINUOUS_NOTICE
99
+ }) {
100
+ const theme = useLoamTheme();
101
+ const { styles } = useStyles(controlSheet);
102
+ const ink = camInk(theme);
103
+ return /* @__PURE__ */ jsxs(View, { style: styles.notice, children: [
104
+ /* @__PURE__ */ jsx(Icon, { name: "barcode", size: theme.icon.sm, color: ink }),
105
+ /* @__PURE__ */ jsx(Text, { style: [styles.noticeText, { color: ink }], children: text })
106
+ ] });
107
+ }
108
+ function TorchToggle({
109
+ on = false,
110
+ available = true,
111
+ label = "Torch",
112
+ onChange
113
+ }) {
114
+ if (!available) return null;
115
+ return /* @__PURE__ */ jsx(
116
+ GlassIconButton,
117
+ {
118
+ icon: "flash",
119
+ label,
120
+ on,
121
+ onPress: () => onChange?.(!on)
122
+ }
123
+ );
124
+ }
125
+ function GridToggle({
126
+ on = false,
127
+ label = "Grid",
128
+ onChange
129
+ }) {
130
+ return /* @__PURE__ */ jsx(GlassIconButton, { icon: "grid", label, on, onPress: () => onChange?.(!on) });
131
+ }
132
+ function FlipButton({
133
+ label = "Flip camera",
134
+ onFlip
135
+ }) {
136
+ return /* @__PURE__ */ jsx(GlassIconButton, { icon: "flip", label, onPress: onFlip });
137
+ }
138
+ function LensStop({
139
+ stop,
140
+ on,
141
+ onPick
142
+ }) {
143
+ const theme = useLoamTheme();
144
+ const { styles } = useStyles(controlSheet, { on });
145
+ return /* @__PURE__ */ jsx(
146
+ Pressable,
147
+ {
148
+ accessibilityRole: "button",
149
+ accessibilityLabel: `${stop}x`,
150
+ accessibilityState: { selected: on },
151
+ style: styles.lensStop,
152
+ hitSlop: Math.max(0, (theme.hit.lg - theme.ctl.md) / 2),
153
+ onPress: onPick,
154
+ ...pressedProps(on),
155
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.lensLabel, { color: on ? theme.media.onInk : camInk(theme) }], children: `${stop}x` })
156
+ }
157
+ );
158
+ }
159
+ function LensPicker({
160
+ stops = [2, 1, 0.5],
161
+ value = 1,
162
+ label = "Zoom",
163
+ onChange
164
+ }) {
165
+ const { styles } = useStyles(controlSheet);
166
+ return /* @__PURE__ */ jsx(View, { role: "group", accessibilityLabel: label, style: styles.lensColumn, children: stops.map((stop) => /* @__PURE__ */ jsx(LensStop, { stop, on: stop === value, onPick: () => onChange?.(stop) }, stop)) });
167
+ }
168
+ var controlSheet = createStyleSheet((theme) => ({
169
+ ringRoot: {
170
+ pointerEvents: "none",
171
+ position: "absolute",
172
+ top: theme.space[0],
173
+ bottom: theme.space[0],
174
+ left: theme.space[0],
175
+ right: theme.space[0],
176
+ alignItems: "center",
177
+ justifyContent: "center"
178
+ },
179
+ ringNumber: {
180
+ fontFamily: theme.font.sans,
181
+ fontSize: theme.fontSize[13],
182
+ fontVariant: ["tabular-nums"],
183
+ color: theme.color.good
184
+ },
185
+ shutter: {
186
+ flexGrow: 0,
187
+ flexShrink: 0,
188
+ alignItems: "center",
189
+ justifyContent: "center",
190
+ borderWidth: theme.borderWidth.strong,
191
+ variants: {
192
+ platform: {
193
+ ios: { width: theme.space[76], height: theme.space[76], borderRadius: theme.radius.full },
194
+ android: {
195
+ width: theme.space[72],
196
+ height: theme.space[72],
197
+ borderRadius: theme.radius["2xl"]
198
+ },
199
+ web: {
200
+ width: theme.space[68],
201
+ height: theme.space[68],
202
+ borderRadius: theme.radius["2xl"]
203
+ }
204
+ },
205
+ enabled: {
206
+ true: { borderColor: theme.media.ink, opacity: theme.opacity[100] },
207
+ // Dimmed and inert. Still reachable, still named, still able to say why.
208
+ false: { borderColor: theme.white.a8, opacity: theme.state.disabledOpacity }
209
+ }
210
+ }
211
+ },
212
+ shutterFill: {
213
+ width: "100%",
214
+ height: "100%",
215
+ alignItems: "center",
216
+ justifyContent: "center",
217
+ borderRadius: theme.radius.full,
218
+ variants: {
219
+ enabled: {
220
+ true: { backgroundColor: theme.media.ink },
221
+ false: {
222
+ backgroundColor: theme.black.a12,
223
+ borderWidth: theme.borderWidth.base,
224
+ borderColor: theme.white.a5
225
+ }
226
+ }
227
+ }
228
+ },
229
+ notice: {
230
+ flexDirection: "row",
231
+ alignItems: "center",
232
+ justifyContent: "center",
233
+ gap: theme.space[8],
234
+ minHeight: theme.ctl.lg,
235
+ // A sentence in a control-sized box: it takes the width it is given rather
236
+ // than hugging its own text, and the padding stays modest so the words get
237
+ // that width instead of the box.
238
+ alignSelf: "stretch",
239
+ paddingHorizontal: theme.space[12],
240
+ paddingVertical: theme.space[8],
241
+ borderRadius: theme.radius.sm,
242
+ backgroundColor: theme.black.a12,
243
+ borderWidth: theme.borderWidth.base,
244
+ borderColor: theme.white.a5
245
+ },
246
+ noticeText: {
247
+ fontFamily: theme.font.sans,
248
+ fontSize: theme.fontSize[13],
249
+ lineHeight: theme.fontSize[18],
250
+ textAlign: "center",
251
+ flexShrink: 1
252
+ },
253
+ lensColumn: {
254
+ flexDirection: "row",
255
+ alignItems: "center",
256
+ alignSelf: "center",
257
+ gap: theme.space[2],
258
+ padding: theme.space[4],
259
+ borderRadius: theme.radius.full,
260
+ backgroundColor: theme.black.a12,
261
+ borderWidth: theme.borderWidth.base,
262
+ borderColor: theme.white.a5
263
+ },
264
+ lensStop: {
265
+ width: theme.ctl.md,
266
+ height: theme.ctl.md,
267
+ flexGrow: 0,
268
+ flexShrink: 0,
269
+ alignItems: "center",
270
+ justifyContent: "center",
271
+ borderRadius: theme.radius.full,
272
+ variants: {
273
+ on: {
274
+ true: { backgroundColor: theme.media.ink },
275
+ false: {}
276
+ }
277
+ }
278
+ },
279
+ lensLabel: {
280
+ fontFamily: theme.font.sans,
281
+ fontSize: theme.fontSize[11],
282
+ fontWeight: theme.fontWeight.medium,
283
+ fontVariant: ["tabular-nums"]
284
+ }
285
+ }));
286
+ function ScanLine({ active = true, band, travel }) {
287
+ const theme = useLoamTheme();
288
+ const reduced = useReducedMotion();
289
+ const { styles } = useStyles(overlaySheet);
290
+ const height = band ?? theme.space[80];
291
+ const thickness = theme.borderWidth.heavy;
292
+ const distance = travel ?? height - thickness;
293
+ const offset = React3.useRef(new Animated.Value(0)).current;
294
+ const still = reduced || theme.reduceMotion;
295
+ React3.useEffect(() => {
296
+ if (!active || still) {
297
+ offset.setValue(0);
298
+ return;
299
+ }
300
+ const half = theme.loop.beacon;
301
+ const leg = (to) => Animated.timing(offset, {
302
+ toValue: to,
303
+ duration: half,
304
+ easing: Easing.bezier(...theme.easing.inOut),
305
+ useNativeDriver: true
306
+ });
307
+ const loop = Animated.loop(Animated.sequence([leg(1), leg(0)]));
308
+ loop.start();
309
+ return () => loop.stop();
310
+ }, [active, still, offset, theme.loop.beacon, theme.easing.inOut]);
311
+ if (!active) return null;
312
+ const translateY = still ? distance / 2 : offset.interpolate({ inputRange: [0, 1], outputRange: [0, distance] });
313
+ return /* @__PURE__ */ jsx(
314
+ View,
315
+ {
316
+ accessible: false,
317
+ testID: still ? "loam-scan-band-static" : "loam-scan-band",
318
+ style: [styles.scanBand, { height }],
319
+ ...decorativeProps(),
320
+ children: /* @__PURE__ */ jsx(
321
+ Animated.View,
322
+ {
323
+ style: [styles.scanLine, { height: thickness, transform: [{ translateY }] }],
324
+ children: /* @__PURE__ */ jsxs(Svg2, { width: "100%", height: thickness, children: [
325
+ /* @__PURE__ */ jsx(Defs, { children: /* @__PURE__ */ jsxs(LinearGradient, { id: "loam-cam-scan", x1: "0", y1: "0", x2: "1", y2: "0", children: [
326
+ /* @__PURE__ */ jsx(Stop, { offset: "0", stopColor: theme.color.good, stopOpacity: theme.opacity[0] }),
327
+ /* @__PURE__ */ jsx(Stop, { offset: "0.5", stopColor: theme.color.good, stopOpacity: theme.opacity[100] }),
328
+ /* @__PURE__ */ jsx(Stop, { offset: "1", stopColor: theme.color.good, stopOpacity: theme.opacity[0] })
329
+ ] }) }),
330
+ /* @__PURE__ */ jsx(Rect, { x: "0", y: "0", width: "100%", height: thickness, fill: "url(#loam-cam-scan)" })
331
+ ] })
332
+ }
333
+ )
334
+ }
335
+ );
336
+ }
337
+ var GUIDE_BOX = {
338
+ quad: { width: "78%", heightFlex: 62, hole: false },
339
+ circle: { width: "62%", square: true, hole: true },
340
+ line: { width: "84%", hole: true },
341
+ silhouette: { width: "86%", heightFlex: 38, hole: false }
342
+ };
343
+ function Corners({ colour }) {
344
+ const { styles } = useStyles(overlaySheet);
345
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
346
+ /* @__PURE__ */ jsx(View, { style: [styles.cornerTopStart, { borderColor: colour }] }),
347
+ /* @__PURE__ */ jsx(View, { style: [styles.cornerTopEnd, { borderColor: colour }] }),
348
+ /* @__PURE__ */ jsx(View, { style: [styles.cornerBottomStart, { borderColor: colour }] }),
349
+ /* @__PURE__ */ jsx(View, { style: [styles.cornerBottomEnd, { borderColor: colour }] })
350
+ ] });
351
+ }
352
+ function GuideOverlay({
353
+ kind,
354
+ pass = false,
355
+ dimmed = false,
356
+ hint,
357
+ scanning = true
358
+ }) {
359
+ const theme = useLoamTheme();
360
+ const { styles } = useStyles(overlaySheet, { pass, dimmed });
361
+ const snap = React3.useRef(new Animated.Value(pass ? 1 : 0)).current;
362
+ React3.useEffect(() => {
363
+ Animated.timing(snap, {
364
+ toValue: pass ? 1 : 0,
365
+ duration: theme.motion.swap.duration,
366
+ easing: Easing.bezier(...theme.motion.swap.easing),
367
+ useNativeDriver: true
368
+ }).start();
369
+ }, [pass, snap, theme.motion.swap.duration, theme.motion.swap.easing]);
370
+ if (kind === "none") return null;
371
+ const box = GUIDE_BOX[kind];
372
+ const colour = pass ? theme.color.good : theme.white.a12;
373
+ const scrim = box.hole && kind !== "circle" ? theme.black.a9 : void 0;
374
+ const reach = Dimensions.get("screen").height;
375
+ const bandGrow = box.heightFlex === void 0 ? null : { flexGrow: (100 - box.heightFlex) / 2 };
376
+ const scale = snap.interpolate({ inputRange: [0, 1], outputRange: [1, 1.015] });
377
+ return /* @__PURE__ */ jsxs(
378
+ View,
379
+ {
380
+ accessible: false,
381
+ testID: `loam-guide-${kind}-${pass ? "pass" : "wait"}`,
382
+ style: [styles.guideRoot, box.hole ? styles.guideRootBleed : null],
383
+ ...decorativeProps(),
384
+ children: [
385
+ /* @__PURE__ */ jsx(View, { style: [styles.scrimBand, bandGrow, scrim ? { backgroundColor: scrim } : null] }),
386
+ /* @__PURE__ */ jsxs(
387
+ View,
388
+ {
389
+ style: [
390
+ styles.guideRow,
391
+ box.heightFlex === void 0 ? null : { flexGrow: box.heightFlex, flexShrink: 1 }
392
+ ],
393
+ children: [
394
+ /* @__PURE__ */ jsx(View, { style: [styles.scrimSide, scrim ? { backgroundColor: scrim } : null] }),
395
+ /* @__PURE__ */ jsxs(
396
+ Animated.View,
397
+ {
398
+ style: [
399
+ styles.guideBox,
400
+ { width: box.width, transform: [{ scale }] },
401
+ box.square ? styles.guideSquare : null,
402
+ // A share of the band's height, so the box must take the row's
403
+ // height rather than its (absolutely placed) corners' zero.
404
+ box.heightFlex === void 0 ? null : styles.guideStretch
405
+ ],
406
+ children: [
407
+ hint ? /* @__PURE__ */ jsx(View, { style: styles.hintWrap, children: /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: styles.hint, children: hint }) }) : null,
408
+ kind === "quad" ? /* @__PURE__ */ jsx(Corners, { colour }) : null,
409
+ kind === "circle" ? /* @__PURE__ */ jsxs(Fragment, { children: [
410
+ /* @__PURE__ */ jsx(
411
+ View,
412
+ {
413
+ testID: "loam-guide-hole-ring",
414
+ style: [
415
+ styles.holeRing,
416
+ {
417
+ borderColor: theme.black.a9,
418
+ borderWidth: reach,
419
+ // Past half the ring's size, so it clamps to a true circle
420
+ // and the inner edge (radius minus border) is the guide's.
421
+ // `radius.full` (999) is less than the border, which left
422
+ // the hole square.
423
+ borderRadius: reach * 2,
424
+ top: -reach,
425
+ left: -reach,
426
+ right: -reach,
427
+ bottom: -reach
428
+ }
429
+ ]
430
+ }
431
+ ),
432
+ /* @__PURE__ */ jsx(View, { style: [styles.circleGuide, { borderColor: colour }] })
433
+ ] }) : null,
434
+ kind === "line" ? /* @__PURE__ */ jsx(View, { style: [styles.lineGuide, { borderColor: colour }], children: /* @__PURE__ */ jsx(ScanLine, { active: scanning }) }) : null,
435
+ kind === "silhouette" ? /* @__PURE__ */ jsxs(Svg2, { viewBox: "0 0 100 44", width: "100%", height: "100%", preserveAspectRatio: "xMidYMid meet", children: [
436
+ /* @__PURE__ */ jsx(
437
+ Path,
438
+ {
439
+ d: "M4 32 L10 20 Q14 12 24 10 L64 10 Q76 11 82 20 L94 26 Q97 27 97 31 L97 36 Q97 38 94 38 L6 38 Q3 38 3 35 Z",
440
+ stroke: colour,
441
+ strokeWidth: 1,
442
+ strokeDasharray: "5 3",
443
+ strokeLinejoin: "round",
444
+ fill: "none"
445
+ }
446
+ ),
447
+ /* @__PURE__ */ jsx(Circle, { cx: 26, cy: 38, r: 6, stroke: colour, strokeWidth: 1, strokeDasharray: "4 3", fill: "none" }),
448
+ /* @__PURE__ */ jsx(Circle, { cx: 76, cy: 38, r: 6, stroke: colour, strokeWidth: 1, strokeDasharray: "4 3", fill: "none" })
449
+ ] }) : null
450
+ ]
451
+ }
452
+ ),
453
+ /* @__PURE__ */ jsx(View, { style: [styles.scrimSide, scrim ? { backgroundColor: scrim } : null] })
454
+ ]
455
+ }
456
+ ),
457
+ /* @__PURE__ */ jsx(View, { style: [styles.scrimBand, bandGrow, scrim ? { backgroundColor: scrim } : null] })
458
+ ]
459
+ }
460
+ );
461
+ }
462
+ function GridOverlay({ visible = false }) {
463
+ const { styles } = useStyles(overlaySheet);
464
+ if (!visible) return null;
465
+ return /* @__PURE__ */ jsxs(View, { accessible: false, testID: "loam-grid-overlay", style: styles.gridRoot, ...decorativeProps(), children: [
466
+ /* @__PURE__ */ jsx(View, { style: styles.gridV1 }),
467
+ /* @__PURE__ */ jsx(View, { style: styles.gridV2 }),
468
+ /* @__PURE__ */ jsx(View, { style: styles.gridH1 }),
469
+ /* @__PURE__ */ jsx(View, { style: styles.gridH2 })
470
+ ] });
471
+ }
472
+ function FocusPulse({ point }) {
473
+ const theme = useLoamTheme();
474
+ const reduced = useReducedMotion();
475
+ const { styles } = useStyles(overlaySheet);
476
+ const phase = React3.useRef(new Animated.Value(0)).current;
477
+ const still = reduced || theme.reduceMotion;
478
+ React3.useEffect(() => {
479
+ if (still) {
480
+ phase.setValue(1);
481
+ return;
482
+ }
483
+ Animated.timing(phase, {
484
+ toValue: 1,
485
+ duration: theme.duration.slower,
486
+ easing: Easing.bezier(...theme.easing.out),
487
+ useNativeDriver: true
488
+ }).start();
489
+ }, [phase, still, theme.duration.slower, theme.easing.out]);
490
+ const opacity = still ? theme.opacity[100] : phase.interpolate({ inputRange: [0, 0.3, 1], outputRange: [0, 1, 0] });
491
+ const scale = still ? 1 : phase.interpolate({ inputRange: [0, 0.3, 1], outputRange: [1.015, 1, 1] });
492
+ return /* @__PURE__ */ jsx(
493
+ Animated.View,
494
+ {
495
+ accessible: false,
496
+ testID: "loam-focus-indicator",
497
+ style: [
498
+ styles.focusBox,
499
+ {
500
+ left: `${point.x * 100}%`,
501
+ top: `${point.y * 100}%`,
502
+ borderColor: theme.color.warn,
503
+ opacity,
504
+ transform: [{ scale }]
505
+ }
506
+ ],
507
+ ...decorativeProps(),
508
+ children: /* @__PURE__ */ jsxs(Svg2, { viewBox: "0 0 24 24", width: theme.icon.lg, height: theme.icon.lg, children: [
509
+ /* @__PURE__ */ jsx(Line, { x1: 12, y1: 6, x2: 12, y2: 18, stroke: theme.color.warn, strokeWidth: 1 }),
510
+ /* @__PURE__ */ jsx(Line, { x1: 6, y1: 12, x2: 18, y2: 12, stroke: theme.color.warn, strokeWidth: 1 })
511
+ ] })
512
+ }
513
+ );
514
+ }
515
+ function FocusIndicator({
516
+ point = null,
517
+ pulseKey = 0
518
+ }) {
519
+ if (!point) return null;
520
+ return /* @__PURE__ */ jsx(FocusPulse, { point }, pulseKey);
521
+ }
522
+ function FocusTapZone({ onFocusPoint }) {
523
+ const { styles } = useStyles(overlaySheet);
524
+ const size = React3.useRef({ width: 0, height: 0 });
525
+ const onLayout = (e) => {
526
+ size.current = {
527
+ width: e.nativeEvent.layout.width,
528
+ height: e.nativeEvent.layout.height
529
+ };
530
+ };
531
+ return /* @__PURE__ */ jsx(
532
+ Pressable,
533
+ {
534
+ accessible: false,
535
+ importantForAccessibility: "no-hide-descendants",
536
+ testID: "loam-focus-zone",
537
+ ...decorativeProps(),
538
+ ...untabbableProps(),
539
+ onLayout,
540
+ style: styles.tapZone,
541
+ onPress: (e) => {
542
+ if (!onFocusPoint) return;
543
+ const { width, height } = size.current;
544
+ if (width === 0 || height === 0) return;
545
+ const { locationX, locationY } = e.nativeEvent;
546
+ onFocusPoint({
547
+ x: Math.min(1, Math.max(0, locationX / width)),
548
+ y: Math.min(1, Math.max(0, locationY / height))
549
+ });
550
+ }
551
+ }
552
+ );
553
+ }
554
+ var overlaySheet = createStyleSheet((theme) => ({
555
+ scanBand: {
556
+ width: "100%",
557
+ overflow: "hidden",
558
+ // In the style object, not as a prop: the prop form is deprecated in
559
+ // react-native-web and warns on every render.
560
+ pointerEvents: "none"
561
+ },
562
+ scanLine: {
563
+ position: "absolute",
564
+ left: theme.space[0],
565
+ right: theme.space[0],
566
+ top: theme.space[0]
567
+ },
568
+ guideRoot: {
569
+ pointerEvents: "none",
570
+ position: "absolute",
571
+ top: theme.space[0],
572
+ bottom: theme.space[0],
573
+ left: theme.space[0],
574
+ right: theme.space[0],
575
+ flexDirection: "column",
576
+ variants: {
577
+ // The guide has done its job once a tracker locks; the boxes are the
578
+ // thing to look at (spec E.4).
579
+ dimmed: { true: { opacity: theme.opacity[20] }, false: {} },
580
+ pass: { true: {}, false: {} }
581
+ }
582
+ },
583
+ scrimBand: {
584
+ flexGrow: 1,
585
+ flexShrink: 1,
586
+ flexBasis: theme.space[0]
587
+ },
588
+ // The row's own bands. `alignSelf: 'stretch'` is the load-bearing line: the
589
+ // row centres its children, and a band with no content and no stretch is
590
+ // zero points tall — which left the circle's "hole" a full-width strip.
591
+ scrimSide: {
592
+ flexGrow: 1,
593
+ flexShrink: 1,
594
+ flexBasis: theme.space[0],
595
+ alignSelf: "stretch"
596
+ },
597
+ holeRing: {
598
+ position: "absolute"
599
+ },
600
+ guideRootBleed: {
601
+ top: "-100%",
602
+ bottom: "-100%"
603
+ },
604
+ guideRow: {
605
+ flexDirection: "row",
606
+ alignItems: "center",
607
+ justifyContent: "center"
608
+ },
609
+ guideBox: {
610
+ alignItems: "center",
611
+ justifyContent: "center"
612
+ },
613
+ guideStretch: {
614
+ alignSelf: "stretch"
615
+ },
616
+ guideSquare: {
617
+ aspectRatio: 1
618
+ },
619
+ circleGuide: {
620
+ width: "100%",
621
+ height: "100%",
622
+ borderRadius: theme.radius.full,
623
+ borderWidth: theme.borderWidth.strong
624
+ },
625
+ lineGuide: {
626
+ width: "100%",
627
+ borderRadius: theme.radius.sm,
628
+ borderWidth: theme.borderWidth.strong,
629
+ overflow: "hidden"
630
+ },
631
+ hintWrap: {
632
+ position: "absolute",
633
+ bottom: "100%",
634
+ marginBottom: theme.space[8],
635
+ alignItems: "center"
636
+ },
637
+ // An instruction, so the sans face at full ink — mono with wide tracking is
638
+ // the metadata treatment (the feed label), and it read as a caption here.
639
+ hint: {
640
+ fontFamily: theme.font.sans,
641
+ fontSize: theme.fontSize[12],
642
+ color: theme.media.ink,
643
+ paddingHorizontal: theme.space[12],
644
+ paddingVertical: theme.space[4],
645
+ borderRadius: theme.radius.full,
646
+ backgroundColor: theme.black.a12,
647
+ borderWidth: theme.borderWidth.base,
648
+ borderColor: theme.white.a5,
649
+ overflow: "hidden"
650
+ },
651
+ cornerTopStart: {
652
+ position: "absolute",
653
+ top: theme.space[0],
654
+ left: theme.space[0],
655
+ width: theme.space[32],
656
+ height: theme.space[32],
657
+ borderTopWidth: theme.borderWidth.strong,
658
+ borderLeftWidth: theme.borderWidth.strong,
659
+ borderTopLeftRadius: theme.radius.sm
660
+ },
661
+ cornerTopEnd: {
662
+ position: "absolute",
663
+ top: theme.space[0],
664
+ right: theme.space[0],
665
+ width: theme.space[32],
666
+ height: theme.space[32],
667
+ borderTopWidth: theme.borderWidth.strong,
668
+ borderRightWidth: theme.borderWidth.strong,
669
+ borderTopRightRadius: theme.radius.sm
670
+ },
671
+ cornerBottomStart: {
672
+ position: "absolute",
673
+ bottom: theme.space[0],
674
+ left: theme.space[0],
675
+ width: theme.space[32],
676
+ height: theme.space[32],
677
+ borderBottomWidth: theme.borderWidth.strong,
678
+ borderLeftWidth: theme.borderWidth.strong,
679
+ borderBottomLeftRadius: theme.radius.sm
680
+ },
681
+ cornerBottomEnd: {
682
+ position: "absolute",
683
+ bottom: theme.space[0],
684
+ right: theme.space[0],
685
+ width: theme.space[32],
686
+ height: theme.space[32],
687
+ borderBottomWidth: theme.borderWidth.strong,
688
+ borderRightWidth: theme.borderWidth.strong,
689
+ borderBottomRightRadius: theme.radius.sm
690
+ },
691
+ gridRoot: {
692
+ pointerEvents: "none",
693
+ position: "absolute",
694
+ top: theme.space[0],
695
+ bottom: theme.space[0],
696
+ left: theme.space[0],
697
+ right: theme.space[0]
698
+ },
699
+ gridV1: {
700
+ position: "absolute",
701
+ top: theme.space[0],
702
+ bottom: theme.space[0],
703
+ left: "33.333%",
704
+ width: theme.borderWidth.hair,
705
+ backgroundColor: theme.white.a5
706
+ },
707
+ gridV2: {
708
+ position: "absolute",
709
+ top: theme.space[0],
710
+ bottom: theme.space[0],
711
+ left: "66.666%",
712
+ width: theme.borderWidth.hair,
713
+ backgroundColor: theme.white.a5
714
+ },
715
+ gridH1: {
716
+ position: "absolute",
717
+ left: theme.space[0],
718
+ right: theme.space[0],
719
+ top: "33.333%",
720
+ height: theme.borderWidth.hair,
721
+ backgroundColor: theme.white.a5
722
+ },
723
+ gridH2: {
724
+ position: "absolute",
725
+ left: theme.space[0],
726
+ right: theme.space[0],
727
+ top: "66.666%",
728
+ height: theme.borderWidth.hair,
729
+ backgroundColor: theme.white.a5
730
+ },
731
+ focusBox: {
732
+ pointerEvents: "none",
733
+ position: "absolute",
734
+ width: theme.space[72],
735
+ height: theme.space[72],
736
+ marginLeft: -theme.space[36],
737
+ marginTop: -theme.space[36],
738
+ alignItems: "center",
739
+ justifyContent: "center",
740
+ borderRadius: theme.radius.sm,
741
+ borderWidth: theme.borderWidth.strong
742
+ },
743
+ tapZone: {
744
+ position: "absolute",
745
+ top: theme.space[0],
746
+ bottom: theme.space[0],
747
+ left: theme.space[0],
748
+ right: theme.space[0]
749
+ }
750
+ }));
751
+ var AnimatedPath = Animated.createAnimatedComponent(Path);
752
+ var SEVERITY_DASH = {
753
+ none: "dotted",
754
+ minor: "dashed",
755
+ moderate: "solid"
756
+ };
757
+ function toneStroke(theme, tone) {
758
+ switch (tone) {
759
+ case "good":
760
+ return theme.color.good;
761
+ case "warn":
762
+ return theme.color.warn;
763
+ case "danger":
764
+ return theme.color.danger;
765
+ default:
766
+ return theme.white.a12;
767
+ }
768
+ }
769
+ function TrackChip({
770
+ label,
771
+ conf,
772
+ slot = "above",
773
+ anchor = "start",
774
+ room = 100,
775
+ staggerIndex = 0,
776
+ frozen = false
777
+ }) {
778
+ const theme = useLoamTheme();
779
+ const reduced = useReducedMotion();
780
+ const tone = confidenceTone(conf);
781
+ const { styles } = useStyles(trackingSheet, { slot, tone, anchor });
782
+ const still = reduced || theme.reduceMotion || frozen;
783
+ const enter = React3.useRef(new Animated.Value(still ? 1 : 0)).current;
784
+ const delay = React3.useRef(staggerIndex * TRACKING_MOTION.chipStagger).current;
785
+ React3.useEffect(() => {
786
+ if (still) {
787
+ enter.setValue(1);
788
+ return;
789
+ }
790
+ const anim = Animated.timing(enter, {
791
+ toValue: 1,
792
+ delay,
793
+ duration: theme.motion.enter.duration,
794
+ easing: Easing.bezier(...theme.motion.enter.easing),
795
+ useNativeDriver: true
796
+ });
797
+ anim.start();
798
+ return () => anim.stop();
799
+ }, [still, enter, delay, theme.motion.enter.duration, theme.motion.enter.easing]);
800
+ return /* @__PURE__ */ jsx(View, { style: [styles.chipLane, { width: `${room}%` }], ...decorativeProps(), children: /* @__PURE__ */ jsxs(
801
+ Animated.View,
802
+ {
803
+ accessible: false,
804
+ testID: `loam-track-chip-${slot}`,
805
+ style: [styles.chip, { opacity: enter }],
806
+ children: [
807
+ /* @__PURE__ */ jsx(View, { style: styles.chipDot }),
808
+ /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: styles.chipLabel, children: label }),
809
+ /* @__PURE__ */ jsx(Text, { style: styles.chipConf, children: confidenceLabel(conf) })
810
+ ]
811
+ }
812
+ ) });
813
+ }
814
+ function TrackCountPill({ count, label }) {
815
+ const { styles } = useStyles(trackingSheet);
816
+ if (count <= 0) return null;
817
+ return /* @__PURE__ */ jsx(View, { accessible: false, testID: "loam-track-count", style: styles.countPill, ...decorativeProps(), children: /* @__PURE__ */ jsx(Text, { numberOfLines: 1, style: styles.countText, children: label ?? trackCountLabel(count) }) });
818
+ }
819
+ var OUTLINE_PATH = {
820
+ /** A leafy blob — the plant profile's subject. */
821
+ blob: "M50 6 C72 6 92 22 94 46 C96 70 78 94 50 94 C22 94 4 70 6 46 C8 22 28 6 50 6 Z",
822
+ /** A car in three-quarter view — the panel profile's subject. */
823
+ car: "M6 66 L14 40 Q19 26 34 23 L70 23 Q84 25 90 40 L94 54 Q96 58 96 64 L96 74 Q96 78 90 78 L10 78 Q4 78 4 72 Z"
824
+ };
825
+ function pct(v) {
826
+ return v.interpolate({ inputRange: [0, 100], outputRange: ["0%", "100%"] });
827
+ }
828
+ function TrackedBox({
829
+ object,
830
+ locked,
831
+ tone,
832
+ chip = true,
833
+ chipIndex = 0,
834
+ ease = TRACKING_MOTION.ease,
835
+ frozen = false
836
+ }) {
837
+ const theme = useLoamTheme();
838
+ const reduced = useReducedMotion();
839
+ const [x, y, w, h] = object.box;
840
+ const isLocked = locked ?? object.locked ?? false;
841
+ const severity = object.severity;
842
+ const resolved = tone ?? trackTone({ ...object, locked: isLocked });
843
+ const dash = severity ? SEVERITY_DASH[severity] : "solid";
844
+ const { styles } = useStyles(trackingSheet, { dash });
845
+ const stroke = toneStroke(theme, resolved);
846
+ const still = reduced || theme.reduceMotion || frozen;
847
+ const geo = React3.useRef({
848
+ x: new Animated.Value(x),
849
+ y: new Animated.Value(y),
850
+ w: new Animated.Value(w),
851
+ h: new Animated.Value(h)
852
+ }).current;
853
+ const lock = React3.useRef(new Animated.Value(1)).current;
854
+ const ants = React3.useRef(new Animated.Value(0)).current;
855
+ const last = React3.useRef(object.box);
856
+ React3.useEffect(() => {
857
+ const to = { x, y, w, h };
858
+ const keys = ["x", "y", "w", "h"];
859
+ const [px, py, pw, ph] = last.current;
860
+ const moved = px !== x || py !== y || pw !== w || ph !== h;
861
+ last.current = [x, y, w, h];
862
+ if (still) {
863
+ for (const k of keys) geo[k].setValue(to[k]);
864
+ return;
865
+ }
866
+ if (!moved) return;
867
+ const anim = Animated.parallel(
868
+ keys.map(
869
+ (k) => Animated.timing(geo[k], {
870
+ toValue: to[k],
871
+ duration: ease,
872
+ easing: Easing.bezier(...theme.easing.out),
873
+ useNativeDriver: false
874
+ })
875
+ )
876
+ );
877
+ anim.start();
878
+ return () => anim.stop();
879
+ }, [x, y, w, h, still, ease, geo, last, theme.easing.out]);
880
+ React3.useEffect(() => {
881
+ if (!isLocked || still) {
882
+ lock.setValue(1);
883
+ return;
884
+ }
885
+ lock.setValue(0);
886
+ const anim = Animated.timing(lock, {
887
+ toValue: 1,
888
+ duration: TRACKING_MOTION.lock,
889
+ easing: Easing.bezier(...theme.easing.out),
890
+ useNativeDriver: true
891
+ });
892
+ anim.start();
893
+ return () => anim.stop();
894
+ }, [isLocked, still, lock, theme.easing.out]);
895
+ React3.useEffect(() => {
896
+ if (!object.outline || still) {
897
+ ants.setValue(0);
898
+ return;
899
+ }
900
+ const loop = Animated.loop(
901
+ Animated.timing(ants, {
902
+ toValue: 1,
903
+ duration: TRACKING_MOTION.ants,
904
+ easing: Easing.linear,
905
+ useNativeDriver: false
906
+ })
907
+ );
908
+ loop.start();
909
+ return () => loop.stop();
910
+ }, [object.outline, still, ants]);
911
+ const dashUnit = theme.space[4];
912
+ const lockScale = lock.interpolate({
913
+ inputRange: [0, 1],
914
+ outputRange: [TRACKING_MOTION.lockScale, 1]
915
+ });
916
+ return /* @__PURE__ */ jsxs(
917
+ Animated.View,
918
+ {
919
+ accessible: false,
920
+ testID: `loam-track-box-${object.id}`,
921
+ style: [styles.box, { left: pct(geo.x), top: pct(geo.y), width: pct(geo.w), height: pct(geo.h) }],
922
+ ...decorativeProps(),
923
+ children: [
924
+ /* @__PURE__ */ jsxs(Animated.View, { style: [styles.bracketLayer, { transform: [{ scale: lockScale }] }], children: [
925
+ /* @__PURE__ */ jsx(View, { style: [styles.armTopStart, { borderColor: stroke }] }),
926
+ /* @__PURE__ */ jsx(View, { style: [styles.armTopEnd, { borderColor: stroke }] }),
927
+ /* @__PURE__ */ jsx(View, { style: [styles.armBottomStart, { borderColor: stroke }] }),
928
+ /* @__PURE__ */ jsx(View, { style: [styles.armBottomEnd, { borderColor: stroke }] })
929
+ ] }),
930
+ object.outline ? (
931
+ // A wrapping `View`, not `style` on the `Svg`: the outline has to be an
932
+ // absolutely positioned layer over the box, and a `View` is the thing
933
+ // this library positions.
934
+ /* @__PURE__ */ jsx(View, { testID: "loam-track-outline", style: styles.outline, children: /* @__PURE__ */ jsx(Svg2, { viewBox: "0 0 100 100", width: "100%", height: "100%", preserveAspectRatio: "none", children: /* @__PURE__ */ jsx(
935
+ AnimatedPath,
936
+ {
937
+ d: OUTLINE_PATH[object.outline],
938
+ stroke,
939
+ strokeWidth: 1,
940
+ strokeLinejoin: "round",
941
+ vectorEffect: "non-scaling-stroke",
942
+ fill: "none",
943
+ strokeDasharray: [dashUnit, dashUnit],
944
+ strokeDashoffset: ants.interpolate({
945
+ inputRange: [0, 1],
946
+ outputRange: [0, theme.space[8]]
947
+ })
948
+ }
949
+ ) }) })
950
+ ) : null,
951
+ isLocked ? (
952
+ // The lock's non-colour channel. Inside the box's trailing-top corner,
953
+ // where it cannot collide with a chip in any of the three slots.
954
+ /* @__PURE__ */ jsx(View, { testID: "loam-track-lock", style: styles.lockGlyph, children: /* @__PURE__ */ jsx(Icon, { name: "lock", size: theme.icon.xs, color: stroke }) })
955
+ ) : null,
956
+ chip ? /* @__PURE__ */ jsx(
957
+ TrackChip,
958
+ {
959
+ label: object.label,
960
+ conf: object.conf,
961
+ slot: chipSlot(object.box),
962
+ anchor: chipAnchor(object.box),
963
+ room: chipRoom(object.box),
964
+ staggerIndex: chipIndex,
965
+ frozen
966
+ }
967
+ ) : null
968
+ ]
969
+ }
970
+ );
971
+ }
972
+ function TrackingOverlay({
973
+ objects = [],
974
+ maxChips = MAX_CHIPS,
975
+ ease = TRACKING_MOTION.ease,
976
+ frozen = false,
977
+ dimmed = false,
978
+ safeAreaBottom = 0,
979
+ label
980
+ }) {
981
+ const { styles } = useStyles(trackingSheet, { dimmed });
982
+ const [framePx, setFramePx] = React3.useState(0);
983
+ const { chips, overflow } = visibleChips([...objects], maxChips, framePx || void 0);
984
+ const chipIndex = new Map(chips.map((o, i) => [o.id, i]));
985
+ const name = label === void 0 ? trackingSummaryLabel(objects) : label;
986
+ return /* @__PURE__ */ jsxs(
987
+ View,
988
+ {
989
+ testID: "loam-tracking-overlay",
990
+ accessible: name !== null,
991
+ accessibilityRole: name === null ? void 0 : "image",
992
+ accessibilityLabel: name ?? void 0,
993
+ style: styles.overlayRoot,
994
+ onLayout: (e) => setFramePx(e.nativeEvent.layout.width),
995
+ ...name === null ? decorativeProps() : {},
996
+ children: [
997
+ objects.map((o) => /* @__PURE__ */ jsx(
998
+ TrackedBox,
999
+ {
1000
+ object: o,
1001
+ chip: chipIndex.has(o.id),
1002
+ chipIndex: chipIndex.get(o.id) ?? 0,
1003
+ ease,
1004
+ frozen
1005
+ },
1006
+ o.id
1007
+ )),
1008
+ overflow > 0 ? /* @__PURE__ */ jsx(View, { style: [styles.countRow, { marginTop: -safeAreaBottom }], children: /* @__PURE__ */ jsx(TrackCountPill, { count: objects.length }) }) : null
1009
+ ]
1010
+ }
1011
+ );
1012
+ }
1013
+ var trackingSheet = createStyleSheet((theme) => ({
1014
+ overlayRoot: {
1015
+ pointerEvents: "none",
1016
+ position: "absolute",
1017
+ top: theme.space[0],
1018
+ bottom: theme.space[0],
1019
+ left: theme.space[0],
1020
+ right: theme.space[0],
1021
+ variants: {
1022
+ // `analysing` fades the layer back; every state inside keeps its own
1023
+ // channels, so the opacity is carrying nothing but distance.
1024
+ dimmed: { true: { opacity: theme.opacity[48] }, false: {} }
1025
+ }
1026
+ },
1027
+ box: {
1028
+ pointerEvents: "none",
1029
+ position: "absolute"
1030
+ },
1031
+ bracketLayer: {
1032
+ position: "absolute",
1033
+ top: theme.space[0],
1034
+ bottom: theme.space[0],
1035
+ left: theme.space[0],
1036
+ right: theme.space[0]
1037
+ },
1038
+ outline: {
1039
+ position: "absolute",
1040
+ top: theme.space[0],
1041
+ bottom: theme.space[0],
1042
+ left: theme.space[0],
1043
+ right: theme.space[0]
1044
+ },
1045
+ // Arm length is a share of the box, so a 6% box gets small brackets and a
1046
+ // 60% box gets large ones rather than every box getting 32pt.
1047
+ armTopStart: {
1048
+ position: "absolute",
1049
+ top: theme.space[0],
1050
+ left: theme.space[0],
1051
+ width: "25%",
1052
+ height: "25%",
1053
+ maxWidth: theme.space[32],
1054
+ maxHeight: theme.space[32],
1055
+ borderTopWidth: theme.borderWidth.strong,
1056
+ borderLeftWidth: theme.borderWidth.strong,
1057
+ borderTopLeftRadius: theme.radius.xs,
1058
+ variants: {
1059
+ dash: {
1060
+ dotted: { borderStyle: "dotted" },
1061
+ dashed: { borderStyle: "dashed" },
1062
+ solid: { borderStyle: "solid" }
1063
+ }
1064
+ }
1065
+ },
1066
+ armTopEnd: {
1067
+ position: "absolute",
1068
+ top: theme.space[0],
1069
+ right: theme.space[0],
1070
+ width: "25%",
1071
+ height: "25%",
1072
+ maxWidth: theme.space[32],
1073
+ maxHeight: theme.space[32],
1074
+ borderTopWidth: theme.borderWidth.strong,
1075
+ borderRightWidth: theme.borderWidth.strong,
1076
+ borderTopRightRadius: theme.radius.xs,
1077
+ variants: {
1078
+ dash: {
1079
+ dotted: { borderStyle: "dotted" },
1080
+ dashed: { borderStyle: "dashed" },
1081
+ solid: { borderStyle: "solid" }
1082
+ }
1083
+ }
1084
+ },
1085
+ armBottomStart: {
1086
+ position: "absolute",
1087
+ bottom: theme.space[0],
1088
+ left: theme.space[0],
1089
+ width: "25%",
1090
+ height: "25%",
1091
+ maxWidth: theme.space[32],
1092
+ maxHeight: theme.space[32],
1093
+ borderBottomWidth: theme.borderWidth.strong,
1094
+ borderLeftWidth: theme.borderWidth.strong,
1095
+ borderBottomLeftRadius: theme.radius.xs,
1096
+ variants: {
1097
+ dash: {
1098
+ dotted: { borderStyle: "dotted" },
1099
+ dashed: { borderStyle: "dashed" },
1100
+ solid: { borderStyle: "solid" }
1101
+ }
1102
+ }
1103
+ },
1104
+ armBottomEnd: {
1105
+ position: "absolute",
1106
+ bottom: theme.space[0],
1107
+ right: theme.space[0],
1108
+ width: "25%",
1109
+ height: "25%",
1110
+ maxWidth: theme.space[32],
1111
+ maxHeight: theme.space[32],
1112
+ borderBottomWidth: theme.borderWidth.strong,
1113
+ borderRightWidth: theme.borderWidth.strong,
1114
+ borderBottomRightRadius: theme.radius.xs,
1115
+ variants: {
1116
+ dash: {
1117
+ dotted: { borderStyle: "dotted" },
1118
+ dashed: { borderStyle: "dashed" },
1119
+ solid: { borderStyle: "solid" }
1120
+ }
1121
+ }
1122
+ },
1123
+ lockGlyph: {
1124
+ position: "absolute",
1125
+ top: theme.space[0],
1126
+ right: theme.space[0],
1127
+ margin: theme.space[2]
1128
+ },
1129
+ // The lane the chip hugs its content in. An absolute box shrink-wraps to its
1130
+ // containing block, so a chip positioned straight on a 60px wheel box was
1131
+ // capped at 60px: the label collapsed to nothing and the percentage spilled
1132
+ // past the plate. The lane anchors at the edge `chipAnchor()` picks and is as
1133
+ // wide as `chipRoom()` — up to the frame edge, never past a menu's width — and
1134
+ // the chip sizes to its own label inside it.
1135
+ chipLane: {
1136
+ pointerEvents: "none",
1137
+ position: "absolute",
1138
+ maxWidth: theme.panel.xs,
1139
+ flexDirection: "row",
1140
+ variants: {
1141
+ anchor: {
1142
+ start: { left: theme.space[0], justifyContent: "flex-start" },
1143
+ end: { right: theme.space[0], justifyContent: "flex-end" }
1144
+ },
1145
+ slot: {
1146
+ above: { bottom: "100%", marginBottom: theme.space[4] },
1147
+ "inside-top": { top: theme.space[0], marginTop: theme.space[4] },
1148
+ below: { top: "100%", marginTop: theme.space[4] }
1149
+ }
1150
+ }
1151
+ },
1152
+ chip: {
1153
+ flexShrink: 1,
1154
+ flexDirection: "row",
1155
+ alignItems: "center",
1156
+ gap: theme.space[6],
1157
+ paddingHorizontal: theme.space[8],
1158
+ paddingVertical: theme.space[2],
1159
+ borderRadius: theme.radius.full,
1160
+ // The same plate `viewfinder-chrome.tsx` defines, written out rather than
1161
+ // spread from `camGlass()`: a spread of a function's return type collapses
1162
+ // Unistyles' variant inference for the whole sheet, and the `slot` and
1163
+ // `tone` variants below are what place and colour this chip.
1164
+ backgroundColor: theme.black.a12,
1165
+ borderWidth: theme.borderWidth.base,
1166
+ borderColor: theme.white.a5,
1167
+ variants: {
1168
+ tone: { good: {}, warn: {}, danger: {} }
1169
+ }
1170
+ },
1171
+ chipDot: {
1172
+ width: theme.space[6],
1173
+ height: theme.space[6],
1174
+ flexGrow: 0,
1175
+ flexShrink: 0,
1176
+ borderRadius: theme.radius.full,
1177
+ variants: {
1178
+ tone: {
1179
+ good: { backgroundColor: theme.color.good },
1180
+ warn: { backgroundColor: theme.color.warn },
1181
+ danger: { backgroundColor: theme.color.danger }
1182
+ },
1183
+ slot: { above: {}, "inside-top": {}, below: {} }
1184
+ }
1185
+ },
1186
+ chipLabel: {
1187
+ fontFamily: theme.font.sans,
1188
+ fontSize: theme.fontSize[11],
1189
+ color: camInk(theme),
1190
+ flexShrink: 1
1191
+ },
1192
+ chipConf: {
1193
+ fontFamily: theme.font.mono,
1194
+ fontSize: theme.fontSize[11],
1195
+ color: camInk(theme),
1196
+ flexGrow: 0,
1197
+ flexShrink: 0,
1198
+ fontVariant: ["tabular-nums"]
1199
+ },
1200
+ countRow: {
1201
+ position: "absolute",
1202
+ top: `${TRACK_BAND_BOTTOM}%`,
1203
+ left: theme.space[0],
1204
+ right: theme.space[0],
1205
+ alignItems: "center"
1206
+ },
1207
+ countPill: {
1208
+ flexDirection: "row",
1209
+ alignItems: "center",
1210
+ paddingHorizontal: theme.space[12],
1211
+ paddingVertical: theme.space[4],
1212
+ borderRadius: theme.radius.full,
1213
+ backgroundColor: theme.black.a12,
1214
+ borderWidth: theme.borderWidth.base,
1215
+ borderColor: theme.white.a5
1216
+ },
1217
+ countText: {
1218
+ fontFamily: theme.font.mono,
1219
+ fontSize: theme.fontSize[11],
1220
+ color: camInk(theme),
1221
+ fontVariant: ["tabular-nums"]
1222
+ }
1223
+ }));
1224
+ function SequenceStrip({
1225
+ total = 6,
1226
+ shots = 0,
1227
+ current = 0,
1228
+ angles,
1229
+ mode = "angles",
1230
+ readLabel,
1231
+ onRedo,
1232
+ onRead
1233
+ }) {
1234
+ const theme = useLoamTheme();
1235
+ const { styles } = useStyles(sequenceSheet);
1236
+ const batch = mode === "batch";
1237
+ const count = batch ? Math.max(3, shots + 1) : total;
1238
+ const cells = Array.from({ length: count }, (_, i) => i);
1239
+ return /* @__PURE__ */ jsxs(View, { role: "group", accessibilityLabel: batch ? "Captured pages" : "Capture sequence", style: styles.strip, children: [
1240
+ /* @__PURE__ */ jsx(
1241
+ ScrollView,
1242
+ {
1243
+ horizontal: true,
1244
+ showsHorizontalScrollIndicator: false,
1245
+ contentContainerStyle: styles.railContent,
1246
+ style: styles.rail,
1247
+ children: cells.map((i) => {
1248
+ const done = i < shots;
1249
+ const isCurrent = !done && i === current;
1250
+ const name = batch ? `Page ${i + 1}` : angles?.[i]?.label ?? `Angle ${i + 1}`;
1251
+ const label = done ? `${name} \u2014 captured, tap to redo` : name;
1252
+ if (done) {
1253
+ return /* @__PURE__ */ jsx(
1254
+ Pressable,
1255
+ {
1256
+ accessibilityRole: "button",
1257
+ accessibilityLabel: label,
1258
+ style: styles.cellDone,
1259
+ hitSlop: (theme.hit.lg - theme.space[32]) / 2,
1260
+ onPress: () => onRedo?.(i),
1261
+ children: /* @__PURE__ */ jsx(
1262
+ Icon,
1263
+ {
1264
+ name: "check",
1265
+ size: theme.icon.sm,
1266
+ color: theme.color.goodInk,
1267
+ strokeWidth: theme.icon.strokeBold
1268
+ }
1269
+ )
1270
+ },
1271
+ i
1272
+ );
1273
+ }
1274
+ return /* @__PURE__ */ jsx(
1275
+ View,
1276
+ {
1277
+ accessible: true,
1278
+ accessibilityLabel: label,
1279
+ style: [styles.cellPending, isCurrent ? styles.cellCurrent : null],
1280
+ children: /* @__PURE__ */ jsx(Text, { style: styles.cellNumber, accessible: false, children: String(i + 1) })
1281
+ },
1282
+ i
1283
+ );
1284
+ })
1285
+ }
1286
+ ),
1287
+ batch && shots > 0 ? (
1288
+ // Pinned: outside the scroller, so eleven pages cannot push it off the
1289
+ // end of the rail.
1290
+ /* @__PURE__ */ jsx(
1291
+ Pressable,
1292
+ {
1293
+ accessibilityRole: "button",
1294
+ style: styles.pinned,
1295
+ hitSlop: (theme.hit.lg - theme.space[32]) / 2,
1296
+ onPress: onRead,
1297
+ children: /* @__PURE__ */ jsx(Text, { style: styles.pinnedLabel, children: readLabel ?? readBatchLabel(shots) })
1298
+ }
1299
+ )
1300
+ ) : null
1301
+ ] });
1302
+ }
1303
+ var EV_STOPS = Array.from(
1304
+ { length: 13 },
1305
+ (_, i) => Math.round((i - 6) / 3 * 100) / 100
1306
+ );
1307
+ function evLabel(v) {
1308
+ if (v === 0) return "0";
1309
+ return `${v > 0 ? "+" : ""}${v.toFixed(1)}`;
1310
+ }
1311
+ var GROUP = {
1312
+ ev: EXPERT[1]?.[0] ?? "EV compensation",
1313
+ torch: EXPERT[3]?.[0] ?? "Torch level",
1314
+ format: EXPERT[4]?.[0] ?? "Resolution and format",
1315
+ ae: EXPERT[0]?.[0] ?? "Exposure lock",
1316
+ af: EXPERT[2]?.[0] ?? "Focus lock",
1317
+ gridLevel: EXPERT[5]?.[0] ?? "Grid and level"
1318
+ };
1319
+ function Stop2({
1320
+ on,
1321
+ label,
1322
+ onPick
1323
+ }) {
1324
+ const theme = useLoamTheme();
1325
+ const { styles } = useStyles(sequenceSheet, { on });
1326
+ return /* @__PURE__ */ jsx(
1327
+ Pressable,
1328
+ {
1329
+ accessibilityRole: "button",
1330
+ accessibilityLabel: label,
1331
+ accessibilityState: { selected: on },
1332
+ style: styles.stop,
1333
+ onPress: onPick,
1334
+ ...pressedProps(on),
1335
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.stopLabel, { color: on ? theme.media.onInk : camInk(theme) }], children: label })
1336
+ }
1337
+ );
1338
+ }
1339
+ function ExpertPanel({
1340
+ platform = "ios",
1341
+ values = {},
1342
+ onChange,
1343
+ onClose
1344
+ }) {
1345
+ const theme = useLoamTheme();
1346
+ const { styles } = useStyles(sequenceSheet);
1347
+ const {
1348
+ ev = 0,
1349
+ torch = "off",
1350
+ format = "pipeline",
1351
+ aeLock = false,
1352
+ afLock = false,
1353
+ level = false,
1354
+ grid = false
1355
+ } = values;
1356
+ const toggles = [
1357
+ [GROUP.ae, aeLock, "aeLock"],
1358
+ [GROUP.af, afLock, "afLock"],
1359
+ ["Level", level, "level"],
1360
+ ["Grid", grid, "grid"]
1361
+ ];
1362
+ return /* @__PURE__ */ jsxs(View, { role: "group", accessibilityLabel: "Expert controls", style: styles.panel, children: [
1363
+ /* @__PURE__ */ jsxs(View, { style: styles.panelHead, children: [
1364
+ /* @__PURE__ */ jsx(Text, { style: [styles.panelTitle, { color: camInk(theme) }], children: "Expert" }),
1365
+ /* @__PURE__ */ jsx(View, { style: styles.panelHeadEnd, children: onClose ? /* @__PURE__ */ jsx(GlassIconButton, { icon: "x", label: "Close expert controls", onPress: onClose }) : null })
1366
+ ] }),
1367
+ /* @__PURE__ */ jsxs(View, { role: "group", accessibilityLabel: GROUP.ev, style: styles.group, children: [
1368
+ /* @__PURE__ */ jsx(Text, { style: [styles.groupLabel, camMeta(theme)], children: GROUP.ev.toUpperCase() }),
1369
+ /* @__PURE__ */ jsx(
1370
+ ScrollView,
1371
+ {
1372
+ horizontal: true,
1373
+ showsHorizontalScrollIndicator: false,
1374
+ contentContainerStyle: styles.stopRow,
1375
+ children: EV_STOPS.map((stop) => /* @__PURE__ */ jsx(
1376
+ Stop2,
1377
+ {
1378
+ on: stop === ev,
1379
+ label: evLabel(stop),
1380
+ onPick: () => onChange?.({ control: "ev", value: stop })
1381
+ },
1382
+ stop
1383
+ ))
1384
+ }
1385
+ )
1386
+ ] }),
1387
+ /* @__PURE__ */ jsxs(View, { role: "group", accessibilityLabel: GROUP.torch, style: styles.group, children: [
1388
+ /* @__PURE__ */ jsx(Text, { style: [styles.groupLabel, camMeta(theme)], children: GROUP.torch.toUpperCase() }),
1389
+ /* @__PURE__ */ jsx(View, { style: styles.stopRow, children: ["off", "low", "high"].map((lvl) => /* @__PURE__ */ jsx(
1390
+ Stop2,
1391
+ {
1392
+ on: lvl === torch,
1393
+ label: lvl,
1394
+ onPick: () => onChange?.({ control: "torch", value: lvl })
1395
+ },
1396
+ lvl
1397
+ )) })
1398
+ ] }),
1399
+ /* @__PURE__ */ jsxs(View, { role: "group", accessibilityLabel: GROUP.format, style: styles.group, children: [
1400
+ /* @__PURE__ */ jsx(Text, { style: [styles.groupLabel, camMeta(theme)], children: GROUP.format.toUpperCase() }),
1401
+ /* @__PURE__ */ jsx(View, { style: styles.stopRow, children: ["pipeline", "full"].map((f) => /* @__PURE__ */ jsx(
1402
+ Stop2,
1403
+ {
1404
+ on: f === format,
1405
+ label: f === "pipeline" ? "Pipeline-native" : "Full sensor",
1406
+ onPick: () => onChange?.({ control: "format", value: f })
1407
+ },
1408
+ f
1409
+ )) })
1410
+ ] }),
1411
+ /* @__PURE__ */ jsx(
1412
+ View,
1413
+ {
1414
+ role: "group",
1415
+ accessibilityLabel: GROUP.gridLevel,
1416
+ style: styles.toggleRow,
1417
+ testID: `loam-expert-${platform}`,
1418
+ children: toggles.map(([label, on, control]) => /* @__PURE__ */ jsx(
1419
+ Stop2,
1420
+ {
1421
+ on,
1422
+ label,
1423
+ onPick: () => onChange?.({ control, value: !on })
1424
+ },
1425
+ control
1426
+ ))
1427
+ }
1428
+ )
1429
+ ] });
1430
+ }
1431
+ var sequenceSheet = createStyleSheet((theme) => ({
1432
+ strip: {
1433
+ flexDirection: "row",
1434
+ alignItems: "center",
1435
+ gap: theme.space[8]
1436
+ },
1437
+ rail: {
1438
+ flexGrow: 1,
1439
+ flexShrink: 1,
1440
+ flexBasis: theme.space[0]
1441
+ },
1442
+ railContent: {
1443
+ flexDirection: "row",
1444
+ alignItems: "center",
1445
+ gap: theme.space[6]
1446
+ },
1447
+ cellDone: {
1448
+ width: theme.space[32],
1449
+ height: theme.space[32],
1450
+ flexGrow: 0,
1451
+ flexShrink: 0,
1452
+ alignItems: "center",
1453
+ justifyContent: "center",
1454
+ borderRadius: theme.radius.sm,
1455
+ backgroundColor: theme.color.good,
1456
+ borderWidth: theme.borderWidth.base,
1457
+ borderColor: theme.white.a5
1458
+ },
1459
+ cellPending: {
1460
+ width: theme.space[32],
1461
+ height: theme.space[32],
1462
+ flexGrow: 0,
1463
+ flexShrink: 0,
1464
+ alignItems: "center",
1465
+ justifyContent: "center",
1466
+ borderRadius: theme.radius.sm,
1467
+ backgroundColor: theme.black.a12,
1468
+ borderWidth: theme.borderWidth.base,
1469
+ borderColor: theme.white.a5
1470
+ },
1471
+ cellCurrent: {
1472
+ borderWidth: theme.borderWidth.strong,
1473
+ borderColor: theme.media.ink
1474
+ },
1475
+ cellNumber: {
1476
+ fontFamily: theme.font.sans,
1477
+ fontSize: theme.fontSize[11],
1478
+ fontVariant: ["tabular-nums"],
1479
+ color: theme.media.ink
1480
+ },
1481
+ pinned: {
1482
+ flexGrow: 0,
1483
+ flexShrink: 0,
1484
+ height: theme.space[32],
1485
+ alignItems: "center",
1486
+ justifyContent: "center",
1487
+ paddingHorizontal: theme.space[12],
1488
+ borderRadius: theme.radius.sm,
1489
+ backgroundColor: theme.media.ink
1490
+ },
1491
+ pinnedLabel: {
1492
+ fontFamily: theme.font.sans,
1493
+ fontSize: theme.fontSize[12],
1494
+ fontWeight: theme.fontWeight.medium,
1495
+ color: theme.media.onInk
1496
+ },
1497
+ panel: {
1498
+ flexDirection: "column",
1499
+ gap: theme.space[16],
1500
+ padding: theme.space[16],
1501
+ borderRadius: theme.radius.md,
1502
+ backgroundColor: theme.black.a12,
1503
+ borderWidth: theme.borderWidth.base,
1504
+ borderColor: theme.white.a5
1505
+ },
1506
+ panelHead: {
1507
+ flexDirection: "row",
1508
+ alignItems: "center",
1509
+ gap: theme.space[12]
1510
+ },
1511
+ panelHeadEnd: {
1512
+ marginStart: "auto"
1513
+ },
1514
+ panelTitle: {
1515
+ fontFamily: theme.font.sans,
1516
+ fontSize: theme.fontSize[13]
1517
+ },
1518
+ group: {
1519
+ flexDirection: "column",
1520
+ gap: theme.space[8]
1521
+ },
1522
+ groupLabel: {
1523
+ fontSize: theme.fontSize[10]
1524
+ },
1525
+ stopRow: {
1526
+ flexDirection: "row",
1527
+ alignItems: "center",
1528
+ gap: theme.space[8]
1529
+ },
1530
+ toggleRow: {
1531
+ flexDirection: "row",
1532
+ flexWrap: "wrap",
1533
+ alignItems: "center",
1534
+ gap: theme.space[8]
1535
+ },
1536
+ stop: {
1537
+ height: theme.ctl.lg,
1538
+ minWidth: theme.ctl.lg,
1539
+ flexGrow: 0,
1540
+ flexShrink: 0,
1541
+ alignItems: "center",
1542
+ justifyContent: "center",
1543
+ paddingHorizontal: theme.space[12],
1544
+ borderRadius: theme.radius.sm,
1545
+ borderWidth: theme.borderWidth.base,
1546
+ borderColor: theme.white.a5,
1547
+ variants: {
1548
+ on: {
1549
+ true: { backgroundColor: theme.media.ink, borderColor: theme.media.ink },
1550
+ false: { backgroundColor: theme.black.a12 }
1551
+ }
1552
+ }
1553
+ },
1554
+ stopLabel: {
1555
+ fontFamily: theme.font.sans,
1556
+ fontSize: theme.fontSize[11],
1557
+ fontVariant: ["tabular-nums"]
1558
+ }
1559
+ }));
1560
+ function ShutterTray({
1561
+ platform = "ios",
1562
+ start,
1563
+ primary,
1564
+ end,
1565
+ stack = false
1566
+ }) {
1567
+ const { styles } = useStyles(traySheet);
1568
+ const secondaries = /* @__PURE__ */ jsxs(Fragment, { children: [
1569
+ /* @__PURE__ */ jsx(View, { style: styles.slotStart, children: start }),
1570
+ /* @__PURE__ */ jsx(View, { style: styles.slotPrimary, children: stack ? null : primary }),
1571
+ /* @__PURE__ */ jsx(View, { style: styles.slotEnd, children: end })
1572
+ ] });
1573
+ if (!stack) {
1574
+ return /* @__PURE__ */ jsx(View, { style: styles.tray, testID: `loam-tray-${platform}`, children: secondaries });
1575
+ }
1576
+ return /* @__PURE__ */ jsxs(View, { style: styles.stackedTray, testID: `loam-tray-${platform}`, children: [
1577
+ /* @__PURE__ */ jsx(View, { style: styles.stackedPrimary, children: primary }),
1578
+ /* @__PURE__ */ jsx(View, { style: styles.stackedRow, children: secondaries })
1579
+ ] });
1580
+ }
1581
+ var traySheet = createStyleSheet((theme) => ({
1582
+ // Tight on the block axis: the shutter is the tray's height, and padding
1583
+ // above and below it is preview the user no longer sees.
1584
+ tray: {
1585
+ flexDirection: "row",
1586
+ alignItems: "center",
1587
+ justifyContent: "space-between",
1588
+ gap: theme.space[16],
1589
+ paddingHorizontal: theme.space[16],
1590
+ paddingVertical: theme.space[4]
1591
+ },
1592
+ stackedTray: {
1593
+ flexDirection: "column",
1594
+ gap: theme.space[8],
1595
+ paddingHorizontal: theme.space[16],
1596
+ paddingVertical: theme.space[4]
1597
+ },
1598
+ stackedPrimary: {
1599
+ flexDirection: "row",
1600
+ alignItems: "center",
1601
+ justifyContent: "center"
1602
+ },
1603
+ stackedRow: {
1604
+ flexDirection: "row",
1605
+ alignItems: "center",
1606
+ justifyContent: "space-between",
1607
+ gap: theme.space[16]
1608
+ },
1609
+ slotStart: {
1610
+ width: theme.space[48],
1611
+ flexGrow: 0,
1612
+ flexShrink: 0,
1613
+ flexDirection: "row",
1614
+ alignItems: "center",
1615
+ justifyContent: "flex-start"
1616
+ },
1617
+ slotPrimary: {
1618
+ flexGrow: 1,
1619
+ flexShrink: 1,
1620
+ flexBasis: theme.space[0],
1621
+ flexDirection: "row",
1622
+ alignItems: "center",
1623
+ justifyContent: "center"
1624
+ },
1625
+ slotEnd: {
1626
+ width: theme.space[48],
1627
+ flexGrow: 0,
1628
+ flexShrink: 0,
1629
+ flexDirection: "row",
1630
+ alignItems: "center",
1631
+ justifyContent: "flex-end"
1632
+ }
1633
+ }));
1634
+
1635
+ export { ContinuousNotice, CountdownRing, EV_STOPS, ExpertPanel, FlipButton, FocusIndicator, FocusTapZone, GridOverlay, GridToggle, GuideOverlay, LensPicker, ScanLine, SequenceStrip, ShutterButton, ShutterTray, TorchToggle, TrackChip, TrackCountPill, TrackedBox, TrackingOverlay };