@kaeawc/auto-mobile 0.0.92 → 0.0.94

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 (257) hide show
  1. package/dist/build-info.json +12 -12
  2. package/dist/schemas/test-plan.schema.json +370 -52
  3. package/dist/src/ble-peripheral.py +153 -0
  4. package/dist/src/{chunk-r14wwk37.js → chunk-026y62r9.js} +12 -12
  5. package/dist/src/chunk-0jyfg947.js +4 -0
  6. package/dist/src/{chunk-950qcq1e.js → chunk-0zfpp1wh.js} +4 -4
  7. package/dist/src/chunk-13qxf7ym.js +7 -0
  8. package/dist/src/chunk-1scenrk7.js +4 -0
  9. package/dist/src/chunk-1vb5ph1k.js +4 -0
  10. package/dist/src/chunk-20g565q7.js +5 -0
  11. package/dist/src/chunk-21h6dhdm.js +5 -0
  12. package/dist/src/{chunk-2e6s75qv.js → chunk-25mfpthk.js} +8 -8
  13. package/dist/src/{chunk-9rkfyt6q.js → chunk-2b39zyxc.js} +3 -3
  14. package/dist/src/chunk-2gzd6wbd.js +4 -0
  15. package/dist/src/{chunk-eka89q3k.js → chunk-3dntffy9.js} +1 -1
  16. package/dist/src/chunk-3gfczcsn.js +4 -0
  17. package/dist/src/chunk-3h5jz1vb.js +9 -0
  18. package/dist/src/chunk-41zcy6b3.js +5 -0
  19. package/dist/src/chunk-47n9m2de.js +4 -0
  20. package/dist/src/chunk-4vxeq0vd.js +58 -0
  21. package/dist/src/chunk-59dx6qaf.js +4 -0
  22. package/dist/src/chunk-5sdf9y92.js +4 -0
  23. package/dist/src/chunk-5wk1t46v.js +5 -0
  24. package/dist/src/chunk-661c50hh.js +17 -0
  25. package/dist/src/chunk-68n6bjjd.js +21 -0
  26. package/dist/src/{chunk-zg67wjkh.js → chunk-69pmjdf2.js} +3 -3
  27. package/dist/src/chunk-6hrj3jt6.js +145 -0
  28. package/dist/src/{chunk-b4cxdzwy.js → chunk-6myr26wx.js} +8 -8
  29. package/dist/src/chunk-6n96k09n.js +4 -0
  30. package/dist/src/chunk-6vwrbkre.js +4 -0
  31. package/dist/src/chunk-6wk0yrab.js +4 -0
  32. package/dist/src/chunk-6yhm9een.js +4 -0
  33. package/dist/src/chunk-7k7gjg2t.js +7 -0
  34. package/dist/src/chunk-7n5fz68h.js +4 -0
  35. package/dist/src/chunk-8cvnpnbp.js +4 -0
  36. package/dist/src/chunk-8kzv7rk7.js +4 -0
  37. package/dist/src/chunk-8nj68t1v.js +167 -0
  38. package/dist/src/{chunk-r20pe1nv.js → chunk-96r3bpg7.js} +3 -3
  39. package/dist/src/chunk-97x887te.js +4 -0
  40. package/dist/src/{chunk-sz1a7p43.js → chunk-99a6tbvd.js} +1 -1
  41. package/dist/src/chunk-9p5d2994.js +5 -0
  42. package/dist/src/{chunk-w7zsh13a.js → chunk-9q265er0.js} +4 -4
  43. package/dist/src/chunk-9xgnrv5w.js +5 -0
  44. package/dist/src/chunk-9xgsmshb.js +5 -0
  45. package/dist/src/chunk-a02d1wgv.js +4 -0
  46. package/dist/src/{chunk-km0dy5vc.js → chunk-a9ps00a1.js} +10 -10
  47. package/dist/src/chunk-aewdveet.js +5 -0
  48. package/dist/src/chunk-b3tdtq7r.js +9 -0
  49. package/dist/src/chunk-bn7jx7a2.js +4 -0
  50. package/dist/src/chunk-bnyw27ja.js +4 -0
  51. package/dist/src/{chunk-s1zq65ny.js → chunk-bpwsg76p.js} +2 -2
  52. package/dist/src/chunk-bvranw3g.js +4 -0
  53. package/dist/src/chunk-c25a84zb.js +4 -0
  54. package/dist/src/chunk-c27yv1dx.js +5 -0
  55. package/dist/src/chunk-c3ewxm4x.js +5 -0
  56. package/dist/src/chunk-cb1ga9rd.js +7 -0
  57. package/dist/src/chunk-cn7fwy7k.js +4 -0
  58. package/dist/src/chunk-cnvy4a64.js +4 -0
  59. package/dist/src/chunk-cqhw4yr6.js +5 -0
  60. package/dist/src/{chunk-v90es00d.js → chunk-ctmfshjc.js} +2 -2
  61. package/dist/src/chunk-ctwmhz13.js +4 -0
  62. package/dist/src/{chunk-qv2918jt.js → chunk-d3jx9bz5.js} +2 -2
  63. package/dist/src/{chunk-wry6skrm.js → chunk-d6rb08ga.js} +1 -1
  64. package/dist/src/chunk-ddqhj4eg.js +5 -0
  65. package/dist/src/{chunk-d57432wq.js → chunk-ddrhs1c3.js} +2 -2
  66. package/dist/src/chunk-dnjnh9p6.js +4 -0
  67. package/dist/src/{chunk-ajsrz473.js → chunk-dw5rwgya.js} +3 -3
  68. package/dist/src/chunk-e0870khr.js +5 -0
  69. package/dist/src/chunk-ea3k10kj.js +4 -0
  70. package/dist/src/chunk-emv82m1w.js +7 -0
  71. package/dist/src/chunk-f3m6pdnx.js +4 -0
  72. package/dist/src/chunk-f84z32cb.js +4 -0
  73. package/dist/src/chunk-fpczgf0z.js +4 -0
  74. package/dist/src/chunk-g2js4c2v.js +971 -0
  75. package/dist/src/chunk-g3af2qbk.js +5 -0
  76. package/dist/src/chunk-gadtna7s.js +11 -0
  77. package/dist/src/{chunk-c9nk1r78.js → chunk-gcyx407v.js} +3 -3
  78. package/dist/src/{chunk-jdqvcq0a.js → chunk-gfpntwc1.js} +3 -3
  79. package/dist/src/chunk-gmnm5e3e.js +4 -0
  80. package/dist/src/chunk-gp6vb5gf.js +4 -0
  81. package/dist/src/chunk-gppgy7pa.js +4 -0
  82. package/dist/src/chunk-hegfz0ae.js +5 -0
  83. package/dist/src/chunk-hpzxdp1k.js +17 -0
  84. package/dist/src/{chunk-2v7tw1nj.js → chunk-j69g8b50.js} +3 -3
  85. package/dist/src/chunk-jvstjnqm.js +19 -0
  86. package/dist/src/chunk-jzqrv78s.js +4 -0
  87. package/dist/src/chunk-k2948b8d.js +4 -0
  88. package/dist/src/chunk-k4ftd40d.js +4 -0
  89. package/dist/src/chunk-kd9vfaxh.js +5 -0
  90. package/dist/src/{chunk-34zxqcmq.js → chunk-kdczmgwk.js} +3 -3
  91. package/dist/src/chunk-ke3wn14v.js +4 -0
  92. package/dist/src/chunk-kh5n7m6w.js +4 -0
  93. package/dist/src/{chunk-0dfnyfkh.js → chunk-khejfph4.js} +3 -3
  94. package/dist/src/chunk-kny2xemh.js +4 -0
  95. package/dist/src/chunk-knyzw9md.js +4 -0
  96. package/dist/src/chunk-mbzpt2hp.js +4 -0
  97. package/dist/src/chunk-mm8rcxad.js +4 -0
  98. package/dist/src/chunk-nc4516tx.js +4 -0
  99. package/dist/src/{chunk-stfzjr9y.js → chunk-ntfrqrqn.js} +5 -5
  100. package/dist/src/chunk-nz5h2j52.js +4 -0
  101. package/dist/src/chunk-p22s97qy.js +4 -0
  102. package/dist/src/chunk-p9kt1sgr.js +6 -0
  103. package/dist/src/{chunk-9y5bcy28.js → chunk-pf282r5g.js} +4 -4
  104. package/dist/src/chunk-pnd4mrxy.js +4 -0
  105. package/dist/src/chunk-pqqteyj6.js +4 -0
  106. package/dist/src/chunk-qj1tbnv2.js +8 -0
  107. package/dist/src/chunk-qnh9egwr.js +4 -0
  108. package/dist/src/chunk-qy3khhf8.js +8 -0
  109. package/dist/src/chunk-r0vp315h.js +11 -0
  110. package/dist/src/chunk-r4j43b9s.js +237 -0
  111. package/dist/src/{chunk-4pfftt07.js → chunk-r54cxxze.js} +3 -3
  112. package/dist/src/chunk-r6pvhj33.js +325 -0
  113. package/dist/src/chunk-rb7aa5gr.js +54 -0
  114. package/dist/src/chunk-rdj50kb4.js +4 -0
  115. package/dist/src/chunk-rnd4vcas.js +4 -0
  116. package/dist/src/chunk-s3y3zfcx.js +14 -0
  117. package/dist/src/chunk-s4g43k75.js +5 -0
  118. package/dist/src/chunk-sfqe223f.js +311 -0
  119. package/dist/src/chunk-sqdrxjw5.js +7 -0
  120. package/dist/src/{chunk-0w1spptt.js → chunk-t54k47em.js} +4 -4
  121. package/dist/src/chunk-tek4pnvv.js +26 -0
  122. package/dist/src/chunk-tn74y7af.js +4 -0
  123. package/dist/src/chunk-tvq0nzam.js +5 -0
  124. package/dist/src/chunk-tx4f0h9c.js +6 -0
  125. package/dist/src/chunk-vdqag3fj.js +5 -0
  126. package/dist/src/chunk-vz5zzs57.js +4 -0
  127. package/dist/src/chunk-w4s2vfpj.js +4 -0
  128. package/dist/src/chunk-w5yypt53.js +42 -0
  129. package/dist/src/{chunk-r0f1q783.js → chunk-w68v64vh.js} +3 -3
  130. package/dist/src/chunk-whwj0pdk.js +5 -0
  131. package/dist/src/chunk-wnmqt1dc.js +4 -0
  132. package/dist/src/{chunk-8en08r7q.js → chunk-wpe46vsa.js} +3 -3
  133. package/dist/src/chunk-wxdjeb4k.js +27 -0
  134. package/dist/src/chunk-xcqzwf9j.js +247 -0
  135. package/dist/src/chunk-xezekwwn.js +5 -0
  136. package/dist/src/chunk-xf19m15s.js +4 -0
  137. package/dist/src/chunk-xghbxha4.js +4 -0
  138. package/dist/src/{chunk-jhnbfj19.js → chunk-xn9m3xc2.js} +5 -5
  139. package/dist/src/{chunk-a4k9rt7z.js → chunk-yqfz4vts.js} +3 -3
  140. package/dist/src/chunk-z8tvs8nk.js +5 -0
  141. package/dist/src/chunk-zm2rehqb.js +5 -0
  142. package/dist/src/chunk-zs6dqm35.js +4 -0
  143. package/dist/src/daemon/processEntry.js +2 -2
  144. package/dist/src/index.js +4 -4
  145. package/dist/src/runtime/bunRuntimeGuard.js +2 -2
  146. package/dist/src/utils/workers/iosBundleExtractWorker.js +2 -2
  147. package/ios/CameraPosterInjection/Injection/CameraPosterInjection.m +466 -0
  148. package/package.json +4 -3
  149. package/schemas/test-plan.schema.json +370 -52
  150. package/dist/src/chunk-09442h7v.js +0 -4
  151. package/dist/src/chunk-0e4gm1kj.js +0 -5
  152. package/dist/src/chunk-0eb8576f.js +0 -4
  153. package/dist/src/chunk-0rjm4nx7.js +0 -5
  154. package/dist/src/chunk-1zmh2eak.js +0 -4
  155. package/dist/src/chunk-28k63ha3.js +0 -5
  156. package/dist/src/chunk-2d4wmp8b.js +0 -4
  157. package/dist/src/chunk-32jpxz2y.js +0 -323
  158. package/dist/src/chunk-3jmvqygr.js +0 -238
  159. package/dist/src/chunk-3swvfhqt.js +0 -5
  160. package/dist/src/chunk-44bt78m9.js +0 -5
  161. package/dist/src/chunk-488vt2bp.js +0 -4
  162. package/dist/src/chunk-4fgdhcwx.js +0 -5
  163. package/dist/src/chunk-4jp2se4w.js +0 -4
  164. package/dist/src/chunk-4vhr9e4t.js +0 -4
  165. package/dist/src/chunk-4xd6zs3y.js +0 -15
  166. package/dist/src/chunk-51567pg8.js +0 -4
  167. package/dist/src/chunk-5agqsfev.js +0 -4
  168. package/dist/src/chunk-5d706gmr.js +0 -4
  169. package/dist/src/chunk-5kd4rqk7.js +0 -5
  170. package/dist/src/chunk-61t538bm.js +0 -4
  171. package/dist/src/chunk-6d3mmqxt.js +0 -54
  172. package/dist/src/chunk-6hyt398x.js +0 -5
  173. package/dist/src/chunk-6mnr8kpm.js +0 -4
  174. package/dist/src/chunk-79q5xsdc.js +0 -42
  175. package/dist/src/chunk-7bdgand3.js +0 -4
  176. package/dist/src/chunk-84cp9dw5.js +0 -4
  177. package/dist/src/chunk-86f2h5ww.js +0 -5
  178. package/dist/src/chunk-8cghfzb2.js +0 -19
  179. package/dist/src/chunk-9tf2p9y9.js +0 -5
  180. package/dist/src/chunk-a2sc4jm0.js +0 -5
  181. package/dist/src/chunk-aa5ng3d1.js +0 -4
  182. package/dist/src/chunk-b3h0a6ca.js +0 -4
  183. package/dist/src/chunk-b6a737qt.js +0 -4
  184. package/dist/src/chunk-bn64x5z5.js +0 -5
  185. package/dist/src/chunk-bvh649za.js +0 -12
  186. package/dist/src/chunk-c9tyfg4j.js +0 -26
  187. package/dist/src/chunk-cfsdfqnm.js +0 -4
  188. package/dist/src/chunk-cxyf04jc.js +0 -7
  189. package/dist/src/chunk-da08tx5v.js +0 -4
  190. package/dist/src/chunk-f2tc08m2.js +0 -4
  191. package/dist/src/chunk-f8tnk179.js +0 -5
  192. package/dist/src/chunk-f9s1kkhx.js +0 -5
  193. package/dist/src/chunk-fbp8a4rx.js +0 -4
  194. package/dist/src/chunk-fd2z0sze.js +0 -4
  195. package/dist/src/chunk-fk927f4q.js +0 -54
  196. package/dist/src/chunk-fxmr3a0f.js +0 -4
  197. package/dist/src/chunk-gaqwhd44.js +0 -4
  198. package/dist/src/chunk-ggtd5x5a.js +0 -5
  199. package/dist/src/chunk-gndcfrm7.js +0 -4
  200. package/dist/src/chunk-h3n4adc6.js +0 -310
  201. package/dist/src/chunk-h5pjbb83.js +0 -4
  202. package/dist/src/chunk-hgnnh302.js +0 -4
  203. package/dist/src/chunk-hmke8nhx.js +0 -5
  204. package/dist/src/chunk-j2bx12hn.js +0 -4
  205. package/dist/src/chunk-j70y1a2g.js +0 -4
  206. package/dist/src/chunk-js1jpyxp.js +0 -6
  207. package/dist/src/chunk-kbjq0qvb.js +0 -4
  208. package/dist/src/chunk-kfwx57xd.js +0 -4
  209. package/dist/src/chunk-kzwctna2.js +0 -4
  210. package/dist/src/chunk-m9ehyehy.js +0 -4
  211. package/dist/src/chunk-mct91p1f.js +0 -4
  212. package/dist/src/chunk-mf9spd4g.js +0 -5
  213. package/dist/src/chunk-mgrcansb.js +0 -4
  214. package/dist/src/chunk-mqc1d0eb.js +0 -6
  215. package/dist/src/chunk-n1zmpk0z.js +0 -4
  216. package/dist/src/chunk-ncyjf63g.js +0 -4
  217. package/dist/src/chunk-nr64d21q.js +0 -973
  218. package/dist/src/chunk-nwm2pq5v.js +0 -168
  219. package/dist/src/chunk-pm06b05d.js +0 -9
  220. package/dist/src/chunk-q50s00ry.js +0 -4
  221. package/dist/src/chunk-qwye1958.js +0 -14
  222. package/dist/src/chunk-r8z9pfve.js +0 -5
  223. package/dist/src/chunk-rdpgkjf6.js +0 -4
  224. package/dist/src/chunk-rkr0dks9.js +0 -5
  225. package/dist/src/chunk-rn84t3sf.js +0 -11
  226. package/dist/src/chunk-s123kn7n.js +0 -4
  227. package/dist/src/chunk-s73qn3fx.js +0 -4
  228. package/dist/src/chunk-sjn0nj4p.js +0 -5
  229. package/dist/src/chunk-sns0c35v.js +0 -7
  230. package/dist/src/chunk-t4dr5c8v.js +0 -8
  231. package/dist/src/chunk-tc8k9c1j.js +0 -4
  232. package/dist/src/chunk-teznhase.js +0 -4
  233. package/dist/src/chunk-ty9xgmz4.js +0 -4
  234. package/dist/src/chunk-tyazf3sk.js +0 -145
  235. package/dist/src/chunk-v6npjrjm.js +0 -5
  236. package/dist/src/chunk-vbvtzkwz.js +0 -5
  237. package/dist/src/chunk-vefd8qee.js +0 -8
  238. package/dist/src/chunk-vs9r72ta.js +0 -17
  239. package/dist/src/chunk-w2mwa0jk.js +0 -4
  240. package/dist/src/chunk-w88k0bgr.js +0 -4
  241. package/dist/src/chunk-wzdt0enk.js +0 -4
  242. package/dist/src/chunk-xhhv854c.js +0 -4
  243. package/dist/src/chunk-y58270hd.js +0 -4
  244. package/dist/src/chunk-yk0hmbex.js +0 -4
  245. package/dist/src/chunk-ympy4fdz.js +0 -28
  246. package/dist/src/chunk-yw436axy.js +0 -4
  247. package/dist/src/chunk-z04jrpjy.js +0 -5
  248. package/dist/src/chunk-z2xznczw.js +0 -4
  249. package/dist/src/chunk-z4xnrgps.js +0 -4
  250. package/dist/src/chunk-z7csyszv.js +0 -4
  251. package/dist/src/chunk-z8t0v07j.js +0 -11
  252. package/dist/src/chunk-z916af47.js +0 -228
  253. package/dist/src/chunk-zcr8he8q.js +0 -5
  254. package/dist/src/chunk-zd9vmkzn.js +0 -9
  255. package/dist/src/chunk-zj939v0h.js +0 -5
  256. package/dist/src/chunk-zkdjwtc1.js +0 -4
  257. package/dist/src/chunk-zkv5kzt6.js +0 -4
@@ -0,0 +1,971 @@
1
+ // @bun
2
+ import{W,c,cr,TE,kE,JO,Is,Lf,Et,IE,by,IK,Yu,ka,Wae,rM}from"./chunk-w5yypt53.js";import"./chunk-r0vp315h.js";import"./chunk-5wk1t46v.js";import{bu,Zh,Su,qV,VV,EC,xv,cL,j5,K5,IC,G5,dL,WV,Y5,J5}from"./chunk-7k7gjg2t.js";import{Qe,Ab}from"./chunk-sfqe223f.js";import{cm,dm,jG,Jf,Ey,uS,pS,Iy,rI}from"./chunk-r4j43b9s.js";import{xC,uL,kT}from"./chunk-9q265er0.js";import{t}from"./chunk-2b39zyxc.js";var ie='## Destinations of device requests\n\nA `request` action with `via: "device"` is checked on the device before it connects. It is refused\nwith `blocked_destination` when the URL names `localhost` (or a `*.localhost` name), or when the\ndestination resolves to a loopback, unspecified or link-local address. On Android this also covers\nthe emulator\'s addresses for the host machine (`10.0.2.2` and the other `10.0.2.x` aliases), and\nports 3000-3010 and 8765 on a private address are refused too. No `adb reverse` is made for a\nprototype, so a device request cannot reach a server on the host machine through `localhost`: use\n`via: "host"` for a host-local server, or call it by an address the device can reach that is not\nloopback. On iOS, call a Mac-local service by the Mac\'s LAN address.\n\n## Mock backends (no server code)\n\nBuild a backend-driven prototype without running a server. `defineBackend {backendId, backend}`\nregisters an in-process backend for this daemon session (`backendId` is lowercase letters, digits\nand hyphens); a `request` with `via: "host"` to `https://<backendId>.mock.automobile.invalid/<path>`\nis answered by it, with no network, DNS lookup or egress policy. The host must be connected, so it\ndoes not apply to `via: "device"` requests or saved prototypes. `backendLog {backendId, since?}` lists the requests it\nanswered and `clearBackend {backendId}` removes it; defining the same id again replaces it and resets\nits data. Requests to a backend that is not defined fail with `network`.\n\n```json\n{"action":"defineBackend","backendId":"shop","backend":{\n "collections":{"products":[{"id":1,"title":"Mug","price":12},{"id":2,"title":"Tote","price":20}]},\n "routes":[{"method":"GET","path":"/greeting/:name","body":{"text":"Hello {params.name}"}}],\n "latencyMs":{"min":200,"max":600},"faults":[{"path":"/products","method":"POST","status":500,"every":3}],\n "seed":7}}\n```\n\nA `show` can define backends itself: `mock` is `{backendId: definition}` (the same definition shape),\nor `mockPath` names an absolute local JSON file holding that object (up to 2 MiB; the path never\nreaches the device). They are defined, all or none, once the spec is accepted and before it is\nsent, replacing backends of the same id, and a saved prototype does not carry them.\n\n- **Collections** are REST over `{name: [items]}`; every item needs an `id` (a number, or a non-empty\n string other than `.` and `..`). `GET /products?title=Mug`, `q=mug` (case-insensitive substring over\n string and number members), `_sort=price` or `-price`, `_page` and `_limit` (an\n `x-total-count` header reports the total), or `after=<id>&size=<n>` for `{items, next}` cursor paging.\n `GET /products/1`, `POST` (201; the id is generated when absent, 409 if it exists), `PATCH` (merge),\n `PUT` (replace) and `DELETE`. Writes persist for the session; a backend holds at most 1 MiB.\n- **Routes** are canned responses that win over collections. Paths are literal segments and\n `:name` parameters. A body string that is exactly `{params.x}`, `{query.x}` or `{body:/pointer}`\n takes that value\'s type; inside longer text it is inserted as text; an unbound one is `null` or empty.\n- **Latency and faults** replay identically for the same `seed`: `latencyMs {min,max}` delays every\n answer, and a fault `{path, method?, status, every | rate}` fails every nth matching request or\n that fraction of them with `{error:"mock fault"}`.\n- **Actors** play the other side (at most 16, each up to 64 steps). An actor fires on a matching\n successful request (`on: {route: "POST /messages/:id"}`) or every N ms (`every` with `maxRuns`),\n after an optional `afterMs` lead, and runs `do` in order: `{append: collection, value}`,\n `{update: collection, id, value}` (merge), `{remove: collection, id}`, each with an optional\n `afterMs` after the previous step. `value` and `id` bind `{params.x}`, `{query.x}` and\n `{body:/pointer}` from the triggering request. A failed step is skipped and the run goes on;\n redefining or clearing the backend cancels pending steps. Poll the collection to see the result.\n `publish`, `advance` and `notify` steps are not supported yet.\n- A request\'s `timeoutMs` applies to the mock too, and a response over the 262144-byte cap fails\n with `too_large`.\n- **Rooms** serve realtime channels with `via: "host"`: a WebSocket to\n `wss://<backendId>.mock.automobile.invalid/rooms/general` or an SSE stream to the `https://` form\n joins the first room whose `path` (`/rooms/:roomId`) matches; no match is `handshake_failed`\n with status 404. A room `{path, onConnect?, echo?, relay?, timeline?}` sends its `onConnect` frames\n to each new connection, `timeline [{afterMs, frame}]` frames that long after each connection\n opened, returns a send to its sender with `echo`, and passes it to the room\'s other connections\n with `relay`. Frames bind `{params.x}` and `{query.x}` from the channel URL; a string frame is sent\n as written, other JSON as JSON text.\n- **Broadcasts** `{collection: {room, frame, on?}}` send a successful collection write (`POST` by\n default; `on` takes `POST`, `PUT`, `PATCH`) to a room. `{item}` and `{item:/pointer}` bind the\n stored item, so `{"messages":{"room":"/rooms/{item:/roomId}","frame":{"message":"{item}"}}}` makes a\n message one device posts appear on every device in that room.\n- `publish {backendId, room, frame}` speaks as the other side to every connection in `room` and\n returns `delivered`. `backendLog` lists room frames (`{room, from, data}`, `from` is `client`,\n `publish` or `broadcast`) beside requests. Redefining or clearing a backend closes its channels\n with code 1001.\n\n## Value formatters (#14227)\n\nUse top-level `derive: {priceText: {of: {data: "/price"}, format: {currency: {code: "USD"}}}}`\nand read `{derived:priceText}` in text node text, contentDescription, rich-text spans,\nbutton/FAB labels, topAppBar title, badge text, dialog title/text, and snackbar text.\nOther text-kind fields (request url/headers/body, check messages, and\nselect/segmented/menu/searchBar labels) validate unknown names but show tokens literally on devices.\nThere are at most 32 names (state-key syntax); unknown reads fail validation.\n`of` chooses exactly one data pointer, state key or literal `{value: number|string}`.\nInside a repeat, `repeat.derive` supports at most 8 names, with `of: {item: "field"}`\nor a literal value; the resulting strings bind as `{item.priceText}` using your alias.\nNames cannot collide with original row fields; derived fields cannot reference each other.\nHost validation checks field references; device-dependent key and emit-name value checks wait for the renderer.\n\n`format` has exactly one key with an object: `number {minFraction?,maxFraction?,grouping?}`,\n`compact {style?:short|long}`, `currency {code:USD}`, `percent {minFraction?,maxFraction?}`,\n`date {style?:short|medium|long}`, `time {style?:short|medium|long}`, or\n`relative {style?:narrow|short|long}`. Fraction digits are integers 0..20;\nmin cannot exceed max. Grouping true/default respects locale minimum grouping digits;\nfalse disables grouping entirely. Compact, currency, and percent respect locale minimum grouping digits.\nCompact defaults short, date medium,\ntime short, relative long. Currency codes are three uppercase letters and use CLDR minor units.\nNumber kinds require finite numbers; percent inputs are ratios (0.256 means 25.6%).\nDate/time/relative accept integer epoch milliseconds, YYYY-MM-DD (midnight UTC),\nor ISO date-times with seconds and Z or an explicit +hh:mm offset; fractional seconds work.\nInvalid or missing inputs display empty text. Optional entry `locale` (BCP 47, <=35 characters)\nand `timeZone` (IANA id, <=128) override device defaults.\n\nRelative time uses an injected clock snapshot, truncates toward zero, and picks the largest\nelapsed unit: seconds, minutes (60 s), hours (60 min), days (24 h), months (30 d), years (365 d).\nUnder one second uses localized now. Exact English fixtures normalize U+202F/U+00A0 to spaces;\nforeign currency symbols, narrow relative text, now wording and non-English CLDR data can vary\nby engine and use structural comparisons. Set locale, zone and clock for deterministic previews.\nBoth derive maps require `prototype_format_v1`; use literal strings with older renderers.\n`dateTime`, `duration`, `plural`, and `fileSize` are unsupported follow-ups.\n',de='### Timers\n\nTimer actions and `onAppear` require `prototype_timers_v1`.\nThe root and nodes declaring `visibleWhen` may set `onAppear` to 1..32 ordinary actions.\nThey run on first show if visible, and on each hidden-to-visible edge; visible-to-visible\nand a same-id show without reset do not run them again. Repeat placeholders bind as in onTap.\nEach list is atomic, validates like a tap, and emits change for a net state change, without\nrestarting idle TTL. A handler runs at most once per interaction, bounded by the action-depth limit.\nFor a splash, put `onAppear:[{type:"startTimer",id:"splash",afterMs:2000,\nactions:[{type:"setState",key:"loading",value:false}]}]` on the root and use\n`visibleWhen:{key:"loading",equals:false}` on content. Hiding a node does not cancel its timer.\nStart a timer from any action list:\n`{type:"startTimer",id:"tick",afterMs:100,everyMs:1000,maxRuns:10,actions:[{type:"emit",name:"tick"}]}`.\n`afterMs` is required; it and optional `everyMs` are integers 100..3600000 ms.\n`maxRuns` is 1..10000 total fires, including the first, and requires `everyMs`.\nWithout maxRuns, repeats continue until cancelled or dismissed. Actions contain 1..32 entries,\nwith the same validation and nesting limit as request follow-ups. IDs are literal strings of\n1..64 characters, never repeat-bound; at most eight distinct startTimer ids per spec.\n`{type:"cancelTimer",id:"tick"}` removes a timer; unknown ids are no-ops. The host warns\non show and patch if no startTimer declares the cancelled id; devices do not validate that advisory.\n\nStarting a live id replaces its schedule and resets runs. Each fire is one atomic interaction:\ninvalid planned writes drop the whole fire but count the run; a net state change emits one change.\nSelf-cancellation stops repeats, and a replacement started during a fire wins. Repeats schedule\nfrom planned fire time without drift. Timers pause while hidden or inactive, retaining remaining\ntime (screenshot capture does not pause); dismiss, teardown and any replacing show cancel them.\nPatch preserves them. Fires never restart idle TTL.\n\n`prototype {action:"simulate",id,advanceTimeMs:1000}` advances without waiting (integer\n1..3600000), requiring the same capability. It returns `fired`; paused timers fire zero times.\nDue timers run chronologically, ties by start order, including timers started during advancement.\nExactly 10000 due fires succeed. If another fire is still due, advance stops at the last fire with\nan error and fired=10000, keeping pending timers for the next call. Real scheduling continues from the shifted clock.\nInspect reports `timers:[{id,nextFireInMs,runs}]`, ordered by remaining time then start order;\nremaining time is nonnegative and frozen while paused. A capable device yields an empty array\nwhen no timers are live.\n',le='## Strings and semantics\n\nTop-level `strings` requires `prototype_strings_v1`: `{default: {title: "Cart", items: {one: "{#} item", other: "{#} items"}}, fr: {title: "Panier"}}`.\nUse `{t:title}` or `{t:items|2}` in display text, spans, semantics.label or semantics.hint.\nCounts are non-negative integer literals (1\u20139 digits) or `{data:/pointer}`; state-key counts are unsupported.\nPipeline: data substitution, then translation, then state binding. Translation values may contain state bindings; translated text is never scanned again for translations. `{#}` inserts the decimal count in a plural form. Invalid runtime counts use other.\nshow `locale` (locale tag, max 35 characters) overrides device preferred locales for that show; omitted follows device locales, which re-resolve live.\nEach requested locale tries its full tag then shorter prefixes, deduplicated, then default. Tags match case-insensitively with underscores normalized to hyphens. Lookup is per key; the supplying table language decides plurals (default uses the first requested language, else en).\nInteger plural groups: ja/zh/ko/vi/th/id/ms/lo/km/my use other; fr/pt/hi/bn/fa/gu/kn/am use one for 0\u20131; ru/uk/be and pl use one/few/many; cs/sk use one/few/other; ar uses zero/one/two/few/many/other; remaining languages use one at 1, other otherwise. Missing forms use other; million-multiple Romance many is outside v1.\nLimits: 16 tables, 512 keys per table, 2048 characters per nonempty value; default and every plural other form are required. Keys use letters/underscore followed by letters/digits/underscore/dot/hyphen, at most 64 characters. Values cannot contain translation, data or platform placeholders.\nWith an explicit locale, `platformNotes` construct `strings.fallback` (platform all) lists up to 20 referenced keys reaching default. Advisory lint reports missing-translation and unknown-translation-key.\nPseudo-localisation transforms each table value and plural form while preserving keys, tags, category names and bindings; strings is opaque to node walking. Node translation placeholders stay intact; semantics label and hint are display text.\n\nEvery node accepts `semantics {heading?,liveRegion?: "polite"|"assertive",group?,label?,hint?,traits?: ["button"|"image"|"selected"]}`, requiring `prototype_semantics_v1`. At least one field is required; traits are unique (1\u20133). Label/hint are nonempty and accept translations, data and state bindings. Label conflicts with contentDescription.\nHeading enables screen-reader navigation; empty-heading lint flags headings without text/label. Group contains child traversal. Android maps heading/liveRegion/traversal group, label to contentDescription, hint to onClickLabel for tap actions else stateDescription, and traits to Role.Button/Role.Image/selected. iOS maps header/button/image/selected traits, contain grouping, label/hint, and live-region change announcements (not initial render; assertive has high speech priority).\nAndroid observe exposes heading as the string "true" and live-region as polite/assertive; iOS snapshots currently expose no SwiftUI traits. Logical RTL swipe sides are outside this capability.\n',ce='## User-testing sessions\n\nCall `prototype {action:"testSessionBegin", id, goal?, cohort?}` after showing a prototype.\nThe host records accepted events without consuming `awaitEvent`. A goal is a screen label\nsuch as `checkout/done`; reaching it marks completion. Cohort is a free label, with no\ncross-session aggregation. `testSessionEnd {id}` stops recording and returns JSON plus\nMarkdown. Dismissal or session release also ends recording. `testSessionReport {id}`\nreads the latest session for this scope, or pass `testSessionId` for a stored one.\nOnly one session may be active per device and prototype.\n\nReports include screen visits, time, abandonment, validation failures and pager changes.\nLabels use the deepest stack\'s top screen as `stack/screen` (ties use the lowest stack id),\nor `(root)`. Text values, payloads, context and state snapshots are never stored;\nnon-secure changes retain only keys, string lengths or value types and validity.\nSecure fields are excluded. Up to 5,000 sanitized events are retained per session; extra\nevents are counted as dropped and the report is marked truncated. The newest 50 ended\nsessions are kept per device/prototype.\n\nTo record dead taps, show with `window.deadTaps: true` on a device advertising\n`prototype_dead_tap_v1`; an older agent is refused. Dead taps are single-pointer taps\ninside the prototype window that no interactive node handled, excluding drags, scrolling\nand app taps outside the window. Their payload has only screen ids and coordinates,\nrounded to one decimal in window-local dp (points on iOS). Reports include counts,\nup to 200 points per screen and a 48-unit grid. There is no screenshot overlay yet.\n\nStack transition patterns: `sharedAxisX`, `sharedAxisY`, `fadeThrough` and `zoom` (and a push/replace\n`source`) require `prototype_stack_patterns_v1`; zoom needs a source naming exactly one shared key inside the target stack\'s screens.\nWith `prototype_zoom_transition_v1`, zoom morphs source-to-root on Android and uses native\ninteractive zoom on iOS 18+. Older agents keep fadeThrough (Android) or platform push (iOS 17);\nplatformNotes reports the fallback. The new capability reports rendering, not spec acceptance.\nReduced motion crossfades and motion off snaps; composed iOS patterns pop non-interactively.\n',Y='### Data actions\n\nA tap can change `/data` on the device without the agent answering it. Data actions write a\nmember of `data`; each `path` is `/data` plus a non-empty data pointer (the same syntax and\n`MAX_PROTOTYPE_DATA_POINTER_LENGTH` as `{data: pointer}`), so the whole document cannot be\nwritten:\n\n- `{type: "setData", path, value}` creates or replaces an object member, or replaces an existing\n array element. A missing parent is an error.\n- `{type: "appendData", path, value}` appends to the existing array at `path`.\n- `{type: "prependData", path, value}` inserts one row at the start; list maxItems trims from the end.\n- `{type: "removeData", path}` removes an existing member or element; later elements shift down.\n\n`value` uses the emit.context binding forms except `page`: `{value: json}` is the literal,\n`{state: key}` reads state, `{data: pointer}` reads data, and `{item: field}` and\n`{index: true}` read the repeat row (repeat only). A written value must be data: strings, booleans,\nfinite numbers, arrays and objects, without null. Inside a repeat, a whole `{index}` segment of\n`path`, as in `/data/cart/{index}`, is bound to the current row; `{index}` anywhere else, or\n`{alias.field}`, is rejected.\n\nThe host validates pointer syntax and `{index}` use, rejects a literal value data cannot hold, and\nrejects an `appendData` or `prependData` whose target resolves in the spec\'s own `data` to something other than an\narray. A target that only exists after earlier writes is checked by the device. A write that would\nleave `data` over `MAX_PROTOTYPE_DATA_WRITE_BYTES` (or over a depth or array limit), or whose\ntarget is missing or not an array, changes nothing.\nActions run in order, so a later action sees an earlier write.\n\nEach write is a `change` event with `name` the written `/data/...` path and `payload`\n`{op: "set"|"append"|"remove", value?, revision}`; `awaitEvent` with `kind: "change"` returns it.\n`revision` is the device revision after the write, the same counter as a patch result\'s. The host\napplies each write to its own copy of the spec and to `status` data, so both stay current. The\nagent\'s `/data` patches and a device\'s writes never silently lose each other: once the device\nhas written a prototype\'s data, a patch with `/data` ops needs `baseRevision` (the latest\n`revision` the agent has seen) and is refused with `conflict: {revision, state, data}` when a write\nnewer than that revision touched an overlapping path (`data` lists those paths). A `show` makes its\nown `data` authoritative again and clears the history.\n\nData actions require `prototype_data_actions_v1`. A show or patch result that contains one is\nrefused on a device without it, before anything is shown or sent.\n\n\n### Realtime channels\n\nThe validated contract requires `prototype_platform_realtime_v1`; current Android and iOS\nruntimes do not advertise it. A root `channels` map holds at most four named channels and\nrequires `requires.platform:["realtime"]`. Each channel declares `kind:"websocket"|"sse"`\nand `url` (ws/wss or http/https respectively), using request URL placeholders and length rules.\nOptional fields: `protocols` (WebSocket only, up to four), `headers` (up to 16, request rules\nplus refusing Sec-WebSocket-* and Last-Event-ID), `via:"host"|"device"` (host default),\n`connect:"onShow"|"manual"` (onShow default), declared string `statusKey`,\n`decode:"json"|"text"` (json default), `reconnect`, `routes`, `onOpen/onClose/onError`.\nDevice routing additionally requires `prototype_platform_realtime_device_v1`.\nReconnect fields are integer `maxAttempts:0..20`, `initialDelayMs>=100`,\n`maxDelayMs<=60000`, with initial <= max. Up to 16 routes use `when:{pointer,equals}`,\n`{pointer,exists:boolean}`, `{event:SSE-name}`, or omit when. Their actions are state/data\nwrites; request and navigation are refused. In channel callbacks and platform handlers,\n`{event:"/message"}` reads decoded event data and `{event:""}` reads the whole event.\nCalls use `{type:"platform",call:"realtime.connect"|"realtime.disconnect",args:{channel:"chat"}}`\nand `realtime.send` with `args:{channel:"chat",body:{value:json}}` (WebSocket only).\nRead live counters with `{platform:"/realtime/chat/framesIn"}`; state keys are status, attempt,\nframesIn, framesDropped, closeCode, closeReason and error. Events are realtime.status and realtime.frame.\nThe same capability gates `appendData.maxItems` and `upsertData {path,key,value,maxItems?}`.\nUpsert replaces the first row whose key pointer matches the incoming value, otherwise appends;\nmissing incoming identity is an error. maxItems is 1..2000 and drops oldest rows before limits apply.\nFrame/send budgets are 65536/16384 bytes, with eight open channels per device and a 50 ms flush budget.\nTransport, lifecycle and routing execution remain unavailable. Flush-window coalescing is an open\ndecision; no coalescing behavior is implemented.\n',pe="\n### Adaptive panes and navigation\n\n`adaptive` needs `prototype_adaptive_v1` and a closed `kind`:\n- `listDetail`: `list` and `detail` node slots, `selectionKey` naming a declared string state key. Empty string means no selection; selecting pushes detail in compact layout and Back clears the key.\n- `navigationSuite`: 1\u20138 unique `items: [{value,label}]`, `stateKey` naming a declared string whose value matches an item, and a `content` node slot. Item taps write that value. Use conditions on the key to choose content.\n- `supporting`: `main` and `supporting` node slots, `openKey` naming a declared boolean. Close clears the key.\n\nWidths are native per platform. Android Material adaptive scaffolds split panes at expanded and use bar/rail/drawer at compact/medium/expanded. iOS horizontalSizeClass regular uses split/sidebar/inspector, compact uses stack/bottom bar/sheet. There is no shared breakpoint or `minWidth`; `platformNotes` reports the different rules on show. iOS draws Material-styled containers under every design until native platform containers are available.\n",he='## Platform namespaces\n\nChoose styling in this order:\n1. Portable node/window fields express behavior on both platforms: use `button.variant: "outlined"`\n for an outlined button, or `style.cornerRadius` for an authored corner.\n2. Portable theme tokens express a shared visual system: set `theme.colors.primary` once rather\n than repeating a color on each button; use theme typography and corner tokens for shared styles.\n3. `theme.design: "platform"` chooses native platform treatment for covered controls (the default);\n `"material"` keeps the shared Material appearance. Check design fallback notes for custom controls.\n4. Use `ios`/`android` only for semantics with no portable meaning, such as\n `ios: {buttonStyle: "glass"}` or `android: {predictiveBack: {style: "none"}}` on a stack.\n\nNamespaces are closed objects on nodes, window and theme; each field has a contract `why`.\nOwn-platform blocks require `prototype_platform_namespace_v1`, except the four node glass fields,\nwhich require only `prototype_glass_v1` on iOS; mixing another iOS key requires both.\nForeign blocks are accepted and\nignored with paths in `platformNotes`; unknown fields are rejected even in foreign blocks.\niOS glass buttons need 26 and otherwise use bordered buttons; hover needs 17 and is render-only\n(no pointer input). Custom tab bars, app bars and sheets cannot apply `tab.role`,\n`bottomAccessory` (node-only), `navigationTitle` or `sheet.sizing`; these are reported as ignored.\nWindow/theme namespace fields likewise have no applicable node and are reported as ignored.\nAndroid `indication: "ripple"|"none"` controls the node\'s clickable feedback; motion-off still wins.\nStack `predictiveBack.style` uses the existing pop for `default`, horizontal movement with fade\nfor `crossActivity` within the prototype activity, or snaps without predictive scrubbing for `none`.\nUse notes to confirm what the current renderer can apply before relying on a namespace field.',ue='## Platform features\n\nA spec reaches a platform feature through a module. Declare every module it uses in a root `requires: {platform: [<module>], permissions?: [{name, purpose}], background?: [...]}`; a call, handler or read of an undeclared module fails validation with a path, and a call\'s permissions must appear in `requires.permissions`. The host reads `requires` before show and refuses a device that lacks a module\'s capability, before anything is drawn.\n\n- **Call.** `{type:"platform", call:"<module>.<op>", args, into?, statusKey?, timeoutMs?, onSuccess?, onError?}` in any action list. `args` are strict per call; values are `{value}`, `{state}`, `{data}`, `{platform}`, `{item}`/`{index}` in a repeat, or `{event}` in a handler. `into` (a `/data` path) and `statusKey` (a declared string state key that moves loading, then success or error) behave as for `request`. A failure writes one of unsupported, unavailable, permission_denied, invalid_args, cancelled, timeout, too_many_in_flight or platform_error to `<into>__meta.error`. Each finished call is an emit event `prototype_platform` (`{callId, call, outcome, error?, durationMs}`).\n- **Handler.** A root `platform: {on: {"<module>.<event>": [actions]}}`. Inside a handler only, `{event: "/pointer"}` reads the event\'s data. Handlers run atomically like a tap and may use `push`, `pop`, data actions and further calls. A delivered event is an emit event `prototype_platform_event` (`{event, data, truncated?}`).\n- **Read.** `/platform/<module>/<key>` is device-owned, read-only JSON: `{platform:/device/os}` in text, `{platform: "/device/widthClass", equals: "expanded"}` as a condition, `{platform: pointer}` as a value. It is not patchable or stored and is not part of the `/data` revision log.\n- **Names.** Authored emit names cannot start with `prototype_`.\n\nEach module needs `prototype_platform_v1` plus its own capability; a device without either refuses show, patch and `replaceNode` and names the capability. The table below is generated from the contract, so it lists every module, call, event and key a spec may name; a device can run only the modules whose capabilities it advertises:\n',me=`Example: a podcast posts a "new episode" notification, and tapping it deep-links to the episode through a handler \`push\`.
3
+
4
+ \`\`\`json
5
+ {
6
+ "id": "podcast",
7
+ "window": {"placement": {"type": "fullscreen"}},
8
+ "state": {"postStatus": "idle"},
9
+ "data": {"opened": {"title": ""}},
10
+ "requires": {
11
+ "platform": ["notifications"],
12
+ "permissions": [{"name": "notifications", "purpose": "Tell you when a new episode is out"}]
13
+ },
14
+ "notifications": {"channels": [{"id": "episodes", "name": "New episodes", "importance": "default"}]},
15
+ "platform": {"on": {"notifications.opened": [
16
+ {"type": "setData", "path": "/data/opened/title", "value": {"event": "/data/title"}},
17
+ {"type": "push", "stack": "nav", "screen": "episode"}
18
+ ]}},
19
+ "root": {
20
+ "type": "stack", "id": "nav", "start": "home",
21
+ "screens": {
22
+ "home": {"type": "button", "label": "Simulate release", "onTap": [{"type": "platform", "call": "notifications.post", "statusKey": "postStatus", "args": {
23
+ "id": {"value": "ep-42"}, "channel": {"value": "episodes"},
24
+ "title": {"value": "New episode"}, "body": {"value": "42: Shipping on Fridays"},
25
+ "data": {"value": {"title": "42: Shipping on Fridays"}}
26
+ }}]},
27
+ "episode": {"type": "text", "text": "Now playing {data:/opened/title}"}
28
+ }
29
+ }
30
+ }
31
+ \`\`\`
32
+
33
+ Drive it for real with \`systemTray\` (find and tap the notification), then \`awaitEvent\` for \`prototype_platform_event\` and \`observe\` the episode screen.
34
+
35
+ `,ye='### Notifications\n\n`notifications.post|cancel|setBadge|schedule` need `requires.platform:["notifications"]` and\n`prototype_platform_notifications_v1`; all but `cancel` also need the `notifications` permission. Declare channels in a root block:\n`"notifications": {"channels": [{"id": "messages", "name": "Messages", "importance": "high"}]}`\n(1 to 8 channels, unique ids; `importance` is min, low, default or high, and iOS maps min and low to\npassive, default and high to active; optional `description` and `showBadge`). The block needs\n`notifications` in `requires.platform`.\n\n- `post` takes `id`, `channel`, `title` and optional `body`, `largeImage`, `style`, `group`,\n `badge`, `data` and up to 3 `actions`. A literal `channel` must name a declared channel, and\n action ids are unique. Posting an `id` that is already shown updates it in place.\n- `schedule` takes the `post` arguments plus `delaySeconds` (1 to 604800) and also needs\n `prototype_platform_notifications_scheduled_v1`; its firing is the `notifications.scheduledFired`\n event. Delivery is inexact.\n- A `notifications.push` handler receives a simulated server push and also needs\n `prototype_platform_push_v1`; read `{event: "/type"}` and `{event: "/data/..."}` there.\n- `cancel` takes exactly one of `id`, `group` or `all: true`.\n- Taps, action buttons and inline replies arrive at `platform.on` handlers\n (`notifications.opened|action|reply|dismissed`); read `{event: "/notificationId"}`,\n `"/actionId"`, `"/reply"` and `"/data"` there.\n- `/platform/notifications/channels/<id>/blocked` is true when the user turned a declared channel\n off; a post to it fails with `unavailable`. A read of an undeclared channel fails validation.',fe='### Media actions\n\n`mediaLibrary.pick` opens the system photo picker and `camera.capture` opens the system camera.\nNeither needs `requires.permissions`. Declare the module in `requires.platform`. A pick needs\n`prototype_platform_mediaLibrary_v1`. A capture needs `prototype_platform_camera_v1` and\n`prototype_platform_camera_capture_v1`. An iOS Simulator has the picker but no system camera, so\nit advertises `mediaLibrary` without the capture capability.\n\n- `pick` takes `media` (`images`, `videos` or `imagesAndVideos`) and optional `maxItems` (1 to 64,\n default 1). With `maxItems` 1, `into` receives one media item. With more, it receives an array.\n Android clamps `maxItems` to the system picker\'s limit.\n- `capture` takes `media` (`image` or `video`) and optional `maxDurationMs` (1 to 600000, video only).\n- A media item is `{asset: "media:\u2026", kind, mimeType, width?, height?, durationMs?, bytes, source,\n createdAt, thumbnail?}`; `source` is `library` for a pick and `camera` for a capture. Bind it to an image with `{data: "/pointer"}`; that needs\n `prototype_platform_media_items_v1`.\n- Cancelling leaves `into` untouched and runs `onError` with `cancelled`. A device without a camera\n fails with `unavailable` (detail `missing_usage_description` when the iOS plist key is missing), and\n storage failures are `platform_error` (detail `media_limit` or `io`). Only one system-UI call can run\n per prototype at a time; a second fails with `too_many_in_flight`.\n- Current runtimes advertise neither module, so the host refuses such a spec before show.\n `platformMode` and simulated outcomes through `prototype simulate` are not accepted yet.\n\n```json\n{\n "id": "photo-draft",\n "window": {"placement": {"type": "fullscreen"}},\n "state": {"pickStatus": "idle", "camStatus": "idle"},\n "data": {"draft": {}},\n "requires": {"platform": ["mediaLibrary", "camera"]},\n "root": {\n "type": "column",\n "children": [\n {"type": "text", "text": "Picker: {pickStatus}, camera: {camStatus}"},\n {"type": "button", "label": "Choose photos", "onTap": [{"type": "platform", "call": "mediaLibrary.pick", "args": {"media": {"value": "imagesAndVideos"}, "maxItems": {"value": 4}}, "into": "/data/draft/picked", "statusKey": "pickStatus"}]},\n {"type": "button", "label": "Take photo", "onTap": [{"type": "platform", "call": "camera.capture", "args": {"media": {"value": "image"}}, "into": "/data/draft/photo", "statusKey": "camStatus"}]}\n ]\n }\n}\n```';var vt=["standard","expressive"].flatMap((e)=>["spatial","effects"].map((a)=>{let o=["fast","default","slow"].map((n)=>W.motionTokens[e][a][n]);return`| ${e} ${a} | ${o.map((n)=>n.dampingRatio).join(" / ")} | ${o.map((n)=>n.stiffness).join(" / ")} |`})).join(`
36
+ `),ge=`## Expressive and layout motion
37
+
38
+ (needs \`prototype_motion_v3\`, Android only) \`motion: "expressive"\` uses Material expressive
39
+ springs for visibility, container size, press feedback and \`animate\` with \`easing: "spring"\`.
40
+ Spatial changes use the spatial spring; color and alpha use the effects spring. Standard remains
41
+ the default. Under \`animate\`, border color/width, cornerRadius, elevation, shadowColor, padding
42
+ and dp width/height also tween; fill/wrap/token, text, font and gradient swaps stay instant.
43
+ Node \`press: {scale?, alpha?}\` needs at least one field: scale 0.85\u20131, alpha 0.5\u20131, on a node
44
+ with a tap/long-press/double-tap or an inherently pressable control. Its scale overrides style
45
+ \`pressScale\`; drawing changes without shrinking the touch target or reported bounds.
46
+ With motion none or Android animator scale 0, controls settle instantly and ripples are disabled.
47
+ The host refuses these features against the iOS agent until #14684.
48
+
49
+ Text nodes accept \`contentTransition: "numeric" | "crossfade" | "none"\` (default \`none\`). Non-\`none\` requires \`prototype_content_transition_v1\`. Numeric changes roll only changed characters up for an increase and down for a decrease; formatting is stripped to digits, the first decimal point and a leading minus. Missing or equal numeric values crossfade; identical text stays still. Spans crossfade as a whole. Roll uses a spring (Android damping 0.8, stiffness 380; iOS duration 0.35 s, bounce 0.15); fades take 200 ms. Accessibility and observe expose the new text immediately. Android reduced motion uses fades and motion off snaps; iOS content transitions snap under reduced motion or motion off.
50
+
51
+ \`animate.tempo: "fast" | "default" | "slow"\` requires \`prototype_motion_tokens_v1\`
52
+ and cannot be combined with \`durationMs\` or \`easing\`. The contract's \`motionTokens\`
53
+ table selects a physical spring by scheme (\`motion: "standard"\` by default, or
54
+ \`"expressive"\`), property role and tempo. \`easing: "spring"\` selects the default
55
+ tempo of that scheme. Spatial properties are offset, scale, rotation, cornerRadius,
56
+ elevation, shadowColor, padding, border width and dp width/height; alpha, background,
57
+ color and border color use critically damped effects springs.
58
+
59
+ | Scheme / role | Damping ratio (fast / default / slow) | Stiffness (fast / default / slow) |
60
+ | --- | --- | --- |
61
+ ${vt}
62
+
63
+ Compose uses \`spring(dampingRatio, stiffness)\`. SwiftUI uses
64
+ \`interpolatingSpring(mass: 1, stiffness: k, damping: 2 * dampingRatio * sqrt(k))\`,
65
+ so both renderers use the same physical spring. Reduced motion snaps spatial
66
+ changes and retains effects; motion off snaps everything. Existing motion v2/v3
67
+ capability requirements still apply; iOS advertises the token capability but
68
+ continues to refuse those broader features until its corresponding capabilities
69
+ are available.
70
+
71
+ ## Attention loops
72
+
73
+ (Android only, requires \`prototype_loops_v1\`) Any node accepts
74
+ \`attentionLoop: {effect: "pulse" | "spin", periodMs: 400\u20134000, iterations?: 1\u201320 | "infinite"}\`.
75
+ The period and finite count are integers; omitted iterations means infinite. Pulse scales
76
+ 1 \u2192 1.08 \u2192 1 per period; spin turns 0 \u2192 360 degrees. Drawing changes without changing
77
+ layout, reported bounds or touch targets. A pulse node must omit \`style.scale\`; a spin node
78
+ must omit \`style.rotation\`, including every \`styleWhen\` style. At most eight expanded nodes
79
+ may carry loops, including repeat instances; shimmer does not consume that limit.
80
+ Finite loops settle at identity and contribute to settling motion while running. Infinite
81
+ loops and shimmer contribute only to the intentional loop count. Reduced motion, motion
82
+ none and animator scale 0 leave loops at rest. On Android each observe metadata read renews
83
+ a 1500 ms capture hold: attention loops draw identity and shimmer draws no band during it,
84
+ so captures within the hold are identical when the rest of the scene is unchanged.
85
+ `;var H='## Starter templates\n\nThe repository ships eight validated multi-screen specs, each a `stack` with a `testTag` on every\ninteractive node. In a checkout, show one with `specPath` (an absolute path) from\n`test/fixtures/prototype-spec/valid/`, or copy its shape when writing a spec:\n\n- `template-onboarding.json`: welcome, sign-up form with `checks`, a button gated by\n `enabledWhen {valid}`, `emit` with `requireValid`, done.\n- `template-feed-detail.json`: a `repeat` over `{path: "/posts"}` inside a refreshable `scroll`; a\n tap stores the row in state and `push`es the detail screen.\n- `template-tabbed-home.json`: a `pager` driven by a `bottomNav {pager}`, plus a pushed screen.\n- `template-checkout.json`: quantity stepper, form with `select` and `datePicker`, and a\n `bottomSheet` confirmation.\n- `template-settings.json`: `listItem` switches, `radioGroup`, `slider`, `dialog`, `snackbar` and\n a `storage` block.\n- `template-search.json`: docked `searchBar` whose child lists `/results` and shows an empty\n state with `{data, empty: true}`.\n- `template-chat.json`: reversed `scroll` with `stickToEnd`, quick-reply chips that\n `appendData`, and a composer that `emit`s `message_sent` with the draft as context (Android only).\n- `template-dashboard.json`: `chart` line, bar and ring over `/data`, and a `startTimer` clock.\n';var ve=`${he}
86
+
87
+ ## Gestures
88
+
89
+ \`onLongPress\` and \`onDoubleTap\` run their action lists exactly like \`onTap\`, with no new
90
+ event kind. A node with either list is interactive even without \`onTap\`. All three may coexist;
91
+ a node with only \`onLongPress\` and/or \`onDoubleTap\` does not consume single taps:
92
+ the nearest ancestor's \`onTap\` runs. The single-tap delay applies only to nodes declaring
93
+ \`onDoubleTap\`; \`onTap\` with only \`onLongPress\` fires immediately.
94
+ Repeat item and component prop placeholders bind in these lists just as they do in \`onTap\`.
95
+ Use \`tapOn\` with \`action: "longPress"|"doubleTap"\` and \`layer: "prototype"\` to drive them
96
+ by coordinate gesture; no new MCP tool is needed. Long-press uses the platform default threshold
97
+ (about 0.5 s on iOS versus the Android long-press timeout, about 0.4 s); the double-tap window is
98
+ the system's. The host refuses unsupported agents before sending a show.
99
+ Each declared list is also a named accessibility action on both platforms, so TalkBack and
100
+ VoiceOver users reach it without the touch gesture: \`Long press\` and \`Double tap\` (fixed English
101
+ names). A single \`onTap\` is the default activation and adds none.
102
+
103
+ ### Swipe actions
104
+
105
+ A \`row\` or \`listItem\` may declare \`swipeActions: {start?, end?, fullSwipe?}\` (needs
106
+ \`prototype_swipe_actions_v1\`). \`start\` is revealed by swiping right and \`end\` by swiping left
107
+ (mirrored in right-to-left layouts is not done: sides are physical). Each side holds 1-3 entries
108
+ \`{label, icon?, role?, color?, onTap}\`: \`label\` is the button text and its accessibility name,
109
+ \`icon\` a built-in icon name (an SF Symbol on iOS), \`role\` \`default\` (primary colours) or
110
+ \`destructive\` (error colours), \`color\` a hex or theme role that overrides the background, and
111
+ \`onTap\` 1-32 actions. The first entry of a side sits at the screen edge. A release past 40% of
112
+ the revealed width, or a fling past the open velocity, leaves the row open; a drag beyond the
113
+ revealed width resists (rubber-band). With \`fullSwipe: true\` a drag past 60% of the row (or a hard
114
+ fling) runs the swiped side's first entry and closes the row. Only one row is open at a time, and a
115
+ tap on an open row closes it instead of running its \`onTap\`. Revealed buttons get the test tag
116
+ \`<rowTestTag>.swipe.<index>\`, counting \`start\` entries first and then \`end\`; they exist only while
117
+ the row is open. Every entry is also an accessibility action (TalkBack custom action, VoiceOver
118
+ action) on the row, so assistive technology can run it without swiping. The reduced profile snaps row settling; spec
119
+ \`motion: "none"\` snaps instead of animating. Put swipeActions on repeated rows; a row inside a
120
+ horizontal \`pager\` or a \`carousel\` is refused because their drag would compete.
121
+
122
+ ### Pull-to-refresh
123
+
124
+ A vertical \`scroll\` may declare \`refresh: {stateKey, onRefresh}\` (needs
125
+ \`prototype_refresh_v1\`). \`stateKey\` names a boolean state key: the platform's refresh indicator
126
+ shows while it is true. A pull sets the key to true and runs \`onRefresh\` like a tap; the prototype
127
+ (an \`onRefresh\` \`setState\`, or the agent through \`patch\` on \`/state/<key>\` with a
128
+ \`baseRevision\`) sets it back to false to hide the indicator. Declare the key in \`state\` as a
129
+ boolean; a horizontal scroll with \`refresh\` is rejected. The pull is a swipe down from the top of
130
+ the scroll (\`swipeOn\` with the prototype layer) and \`observe\` reports the scroll as usual.
131
+
132
+ ### Chat lists
133
+
134
+ A vertical \`scroll\` can be a conversation view (needs \`prototype_chat_scroll_v1\`, Android only for
135
+ now): \`reverse: true\` anchors it at the end of its content, so the newest row sits at the bottom;
136
+ \`stickToEnd: true\` follows rows appended while it is at the end; \`atEndKey\` names a declared
137
+ boolean state key the runtime keeps true while it is at the end (show a "new messages" pill with
138
+ \`visibleWhen {key, equals: false}\`); \`onStartReached\` runs each time the start is reached, to load
139
+ older history. \`scrollTo {scroll, to: "start" | "end" | {key: <repeat.key value>}, animated?}\` moves
140
+ the scroll whose \`id\` is \`scroll\` (exactly one); \`{key}\` scrolls to that repeat row. A composer is a
141
+ row with a \`textField\` (\`imeAction: "send"\`, \`onSubmit\`) and a send \`iconButton\`, with
142
+ \`safeAreaPadding {edges: ["bottom"], types: ["ime", "systemBars"]}\`. Rows off screen are absent from
143
+ \`observe\`; \`swipeOn lookFor\` still finds them.
144
+ `,we=`# Prototype authoring guide
145
+
146
+ Use the \`prototype\` tool with \`action: "show"\` and a full \`spec\`. A show with the id of the
147
+ prototype already on screen replaces it. Use show for a new screen or a window, theme or root
148
+ change; patch for content (\`/data\`), state (\`/state\`) or one node by id (\`replaceNode\`). A large or script-generated spec can be written to a local JSON file and
149
+ shown with \`specPath\` (an absolute path the daemon reads) in place of \`spec\`; give exactly one of
150
+ the two. The spec is
151
+ \`{id, window, theme?, data?, state?, motion?, root}\`. State values are flat scalars (string, finite
152
+ number, boolean). State keys match \`[A-Za-z_][A-Za-z0-9_]{0,63}\`. Read
153
+ \`automobile:prototype/spec-schema\` for the JSON Schema of every node, action and field; the tool
154
+ definition lists only the spec's top-level fields unless AUTOMOBILE_PROTOTYPE_FULL_SCHEMA=1 inlines it.
155
+
156
+ Screen navigation: \`{type:"stack", id, start, screens:{home: node, detail: node}}\` starts with
157
+ path [start] and renders only the top screen. Screen names and id are non-empty; start names an
158
+ existing screen. Actions \`push {stack, screen}\`, \`pop {stack}\`, \`popToRoot {stack}\`, and
159
+ \`replace {stack, screen}\` append, remove the top, reset to [start], and swap the top respectively.
160
+ Push is ignored at depth 32; pop at depth 1 does nothing. \`screen_changed\` has name = stack id
161
+ and payload \`{screen, depth}\`, emitted only when the top screen or depth changes. Requires
162
+ \`prototype_stack_v1\`; every screen counts toward node limits, including hidden screens.
163
+ Routes (requires \`prototype_routes_v1\`; older devices refuse them):
164
+ keep \`screens\` as bare nodes and optionally add \`routes:{screen:{params?,onEnter?,onBack?}}\`.
165
+ Declare up to 16 params per screen as string|number|boolean; names match [A-Za-z_][A-Za-z0-9_]*.
166
+ Push/replace accept a flat \`params\` map of scalars or {state}/{data}/{item}/{index:true} bindings;
167
+ all declared params are required, literals must match their types, and extra names are refused.
168
+ \`setStack {stack,path:[{screen,params?}]}\` takes 1-32 entries and replaces the whole path.
169
+ Each back-stack entry keeps its own params, so Back shows the previous entry as it was.
170
+ In a screen and its lifecycle actions, \`{route.name}\` binds the top entry's params in the same
171
+ fields as repeat placeholders, condition operands and whole /data pointer segments.
172
+ onEnter/onBack each take 1-32 actions with normal nesting limits. onEnter runs after the
173
+ \`screen_changed\` event whenever the screen becomes top, and once on show (a same-id show skips it
174
+ for stacks whose path it kept). onBack replaces the pop
175
+ for system Back, the iOS edge swipe and a plain \`pop\`; \`pop {force:true}\` always pops, so a
176
+ dialog confirm that leaves uses force (a direct same-stack pop in onBack needs it too).
177
+ Storage key \`stack:<id>\` persists the path with params; a stored path whose screens or params no
178
+ longer fit is dropped and the stack starts at \`start\`. Status and inspect keep \`path:string[]\`
179
+ and add parallel \`params:object[]\`; screen_changed payloads add \`params\` when the top has any.
180
+ Android Back and the iOS edge swipe pop a stack above depth 1. A stack's optional
181
+ \`transition: "platform" | "slide" | "fade" | "none"\` selects screen navigation animation
182
+ (default \`platform\`); \`push\` and \`replace\` can override it with their own \`transition\`.
183
+ Any explicit value needs \`prototype_stack_transitions_v1\`; pop/popToRoot take no override.
184
+ Transitions snap under \`motion: "none"\` or Android animator/transition scale 0, and fade under
185
+ \`motion: "reduced"\` or iOS Reduce Motion. A stack's
186
+ \`transition\` accepts only those navigation values and no longer animates the stack's own visibility
187
+ (earlier specs using \`expand\`, \`scale\`, \`slideStart\`, \`slideEnd\` or \`blurReplace\` on a stack
188
+ are now refused): wrap the stack in a \`box\` with \`visibleWhen\` and a visibility transition for
189
+ that. Each entry's scroll position is restored on pop. A stack cannot appear inside a repeat template.
190
+
191
+ On Android, \`show replaceOthers: false\` retains other prototype windows, up to four distinct ids
192
+ (requires \`prototype_multi_surface_v1\`); a fifth is refused listing the shown ids. A same-id show
193
+ with false replaces only that prototype; omitted or true replaces other ids. \`dismiss {id}\` removes
194
+ one, \`dismiss {all: true}\` removes all, and \`status\` lists every shown id. \`inspect\` reconciles
195
+ the full list. iOS refuses false and shows one prototype at a time.
196
+
197
+ Minimal spec:
198
+
199
+ \`\`\`json
200
+ {
201
+ "id": "demo",
202
+ "window": { "placement": { "type": "fullscreen" } },
203
+ "state": { "name": "Ada" },
204
+ "root": { "type": "text", "text": "Hello {name}" }
205
+ }
206
+ \`\`\`
207
+
208
+ A \`{key}\` placeholder in a \`text\` node reads the state value of that key.
209
+
210
+ ## Text field options
211
+
212
+ \`textField\` binds an initialized string state key. Optional \`label\` is a nonempty floating
213
+ label; \`variant\` is \`filled\` (default) or \`outlined\`; nonempty \`supportingText\` appears below
214
+ the field. \`keyboardType\` is \`text\` (default), \`number\`, \`decimal\`, \`email\`, \`phone\`, \`url\`,
215
+ or \`password\`. \`secure\` masks text; \`multiline\` enables multiple lines. \`minLines\` and
216
+ \`maxLines\` are integers in 1..20, permitted only with \`multiline: true\`; \`minLines\` cannot
217
+ exceed \`maxLines\`. Multiline text cannot use \`secure: true\` or \`keyboardType: "password"\`.
218
+ \`maxLength\` is an integer in 1..10000 and shows a character counter; the initial bound
219
+ value cannot exceed it (UTF-16 length).
220
+
221
+ All these options require \`prototype_text_field_options_v1\`. A device without the capability
222
+ is refused before uploads or show; nothing is shown. Update CtrlProxy, or relaunch the iOS
223
+ app with \`launchApp prototype: true\`, or drop the textField options.
224
+
225
+ \`textField.errorWhen\` accepts any condition and forces error styling immediately, independently of checks/validateOn. Optional nonempty \`errorText\` (max 256 UTF-16 units) requires errorWhen. Caption precedence while forced: errorText, currently shown check message, supportingText, then no caption. Both options require \`prototype_string_conditions_v1\`. They do not affect change-event validity, \`{valid}\` or \`requireValid\`, which remain checks-only. Fields without checks do not track validation blur.
226
+
227
+ A \`textField\` may also declare \`trailingIcon: {icon, label, onTap?}\` (a built-in icon, its
228
+ nonempty accessible \`label\` and optional 1-32 \`onTap\` actions, for example a clear button that
229
+ \`setState\`s the field's key to an empty string; without \`onTap\` it is decoration),
230
+ \`imeAction\` (\`done\`, \`next\`, \`search\`, \`send\` or \`go\`: the keyboard's action key) and
231
+ \`onSubmit\` (1-32 actions run when that key is pressed; without \`imeAction\` the key is
232
+ \`done\`). Multiline text has no IME action, so \`imeAction\` and \`onSubmit\` are refused with
233
+ \`multiline: true\`. These need \`prototype_text_field_actions_v1\`, with the same refusal.
234
+ \`leadingIcon\` is a built-in icon name drawn before the text (decoration, no actions; iOS maps it to
235
+ an SF Symbol) and needs \`prototype_text_field_leading_icon_v1\`, with the same refusal.
236
+
237
+ Open question: a \`secure\` field's value still lives in state and in \`change\` events/\`inspect\`;
238
+ this slice does not redact it.
239
+
240
+ ## Search field and results
241
+
242
+ \`searchBar.stateKey\` names an initialized string query. Editing uses the same state write
243
+ and \`change\` event as a text field; keyboard search runs \`onSubmit\`. A visible search bar
244
+ is an editable text target for \`sendKeys\` and makes the window focusable for the keyboard.
245
+ \`placeholder\` follows text-field rules. \`leadingIcon\` and each trailing action's required
246
+ \`icon\` use the icon catalog; its required nonempty \`label\` is the accessibility label,
247
+ and \`onTap\` runs ordinary actions.
248
+ \`activeKey\` names an initialized boolean: true shows the optional single \`child\`.
249
+ A child requires activeKey; activeKey without a child is allowed.
250
+ \`variant: "bar"\` (default) expands to fill its parent when active; \`"docked"\` stays in
251
+ place and draws results directly underneath. Results are ordinary nodes inside the prototype
252
+ window, so \`observe\` sees every row; use a column of literal rows rather than a lazy list.
253
+ Child nodes and submit/trailing actions participate in all normal validation and template walks;
254
+ repeat placeholders bind in the placeholder, trailing labels and actions as in other controls.
255
+ Show and patches producing a search bar require \`prototype_search_bar_v1\` and are refused
256
+ before device requests when it is absent. The Android and iOS implementations ship in later passes.
257
+ iOS uses a drawn field with SF Symbol icons, not SwiftUI \`.searchable\`; these differences
258
+ are reported in \`platformNotes\`.
259
+
260
+ ## Loading, empty and error states
261
+
262
+ State-key \`contains\`/\`startsWith\` compare UTF-16 substrings/prefixes, case-sensitive with no Unicode normalization. Literal operands are nonempty strings up to 256 UTF-16 units; repeat placeholders always bind as text (a numeric item renders as its text), and an empty substituted operand holds for every string. \`lengthGt\`/\`lengthLt\` compare UTF-16 length strictly against literal integers 0..100000 (\`\uD83D\uDE00\` has length 2). Missing or non-string state values fail all four. They are valid only with \`key\`, wherever a condition is accepted, and require \`prototype_string_conditions_v1\`.
263
+
264
+ (needs \`prototype_loading_states_v1\`) \`{data: "/feed", empty: true}\` holds when the value is
265
+ missing, \`[]\`, \`{}\` or \`""\`; \`empty: false\` is its inverse. It replaces the comparison, so
266
+ it cannot be combined with another comparison, and it applies to data
267
+ conditions only. Style \`shimmer: true\` on a \`box\`, \`row\`, \`column\` or \`card\` sweeps an
268
+ animated highlight over its background and reads "Loading" to accessibility; it stays at its rest pose under
269
+ \`motion: "reduced"\`, iOS Reduce Motion, \`motion: "none"\` or Android animator/transition scale 0. The canonical trio uses a request's \`statusKey\`:
270
+
271
+ - **Loading:** a column with \`visibleWhen: {key: "feedStatus", equals: "loading"}\` that repeats a
272
+ few literal placeholder rows styled \`{shimmer: true, height: {dp: 56}, background:
273
+ "surfaceVariant", cornerRadius: "medium"}\`.
274
+ - **Empty:** \`visibleWhen: {all: [{key: "feedStatus", equals: "success"}, {data: "/feed",
275
+ empty: true}]}\`, with the "No posts yet" copy.
276
+ - **Error:** \`visibleWhen: {key: "feedStatus", equals: "error"}\`, with a retry button that runs
277
+ the request again.
278
+
279
+ The rows themselves repeat over \`{path: "/feed"}\`. The iOS agent draws shimmer too.
280
+
281
+ ## Style effects
282
+
283
+ (needs \`prototype_effects_v1\`; no connected device advertises it yet, so the host refuses a spec
284
+ with \`style.effects\` until a renderer ships) \`style.effects\` is 1 to 4 paint-only looks drawn in
285
+ order inside the node's own bounds: \`noise\`, \`gradientMesh\`, \`shimmer\`, \`glow\`,
286
+ \`vignette\`, \`blur\`, \`pixelate\`, \`chromaticAberration\`, \`wave\` and \`ripple\`, each with typed,
287
+ bounded params. Shader source is never accepted. Any effect may loop with \`animate:
288
+ {periodMs: 400-20000}\` and start at \`phase\` 0-1; a spec has at most 64 effects and 8 animated ones
289
+ across its expanded \`repeat\` tree, counting every \`styleWhen\` style's list too.
290
+
291
+ ## Scroll-driven motion
292
+
293
+ (needs \`prototype_scroll_motion_v1\`) A node under a scroll, pager, carousel or repeat takes
294
+ \`scrollTransition: {axis: "horizontal" | "vertical", edge: {scale?, alpha?, rotation?}}\`.
295
+ It interpolates from the edge values to identity at the viewport centre. Any node can take
296
+ \`scrollLinked: {source: "feed", range: [0, 160], alpha: [1, 0], offsetY: [0, -40]}\`:
297
+ \`source\` names a scroll in the same screen; the input endpoints increase, and each output
298
+ is a two-number range. Outputs are \`alpha\`, \`scale\`, \`offsetX\`, \`offsetY\` and \`blur\`.
299
+ They interpolate linearly and clamp outside the input range. At most 16 bindings are allowed
300
+ across the expanded tree; each field counts once per instance. These effects change drawing,
301
+ not layout, hit targets or observed bounds. \`motion: "reduced"\` keeps alpha only;
302
+ \`motion: "none"\` keeps all scroll-driven outputs. Android blur requires Android 12 or later.
303
+
304
+ ## Style effects
305
+
306
+ (needs \`prototype_effects_v1\`; no connected device advertises it yet, so the host refuses a spec
307
+ with \`style.effects\` until a renderer ships) \`style.effects\` is 1 to 4 paint-only looks drawn in
308
+ order inside the node's own bounds: \`noise\`, \`gradientMesh\`, \`shimmer\`, \`glow\`,
309
+ \`vignette\`, \`blur\`, \`pixelate\`, \`chromaticAberration\`, \`wave\` and \`ripple\`, each with typed,
310
+ bounded params. Shader source is never accepted. Any effect may loop with \`animate:
311
+ {periodMs: 400-20000}\` and start at \`phase\` 0-1; a spec has at most 64 effects and 8 animated ones
312
+ across its expanded \`repeat\` tree, counting every \`styleWhen\` style's list too.
313
+
314
+ ## Motion profiles and shared elements
315
+
316
+ \`motion\` is \`standard\` (default), \`reduced\` or \`none\`. Explicit \`motion: "reduced"\`
317
+ requires \`prototype_reduced_motion_v1\`. iOS Reduce Motion selects the reduced profile;
318
+ Android animator scale or transition scale 0 selects off. Spec \`none\` or system off wins over
319
+ reduced. Reduced replaces spatial animation (slides, scale, container size, presses, swipe-row
320
+ settling, pager scrolling and shared morphs) with a crossfade or snap with fade. Shimmer and
321
+ other ambient/attention loops stop at rest and report no loop; color, alpha and fades still animate.
322
+ Off makes every change instant.
323
+
324
+ \`shared: {key, as: "element" | "bounds"}\` requires \`prototype_shared_element_v2\` and
325
+ \`visibleWhen\` on the same node (zoom sources exempt). Element shares identical content (a hero image); bounds shares
326
+ a container with changing content and crossfades both sides. \`sharedKey\` remains the
327
+ \`prototype_shared_element_v1\` bounds alias; a node cannot set both. Literal v2 keys allow
328
+ letters, digits, underscore and hyphen, starting with a letter or underscore (1-64 characters).
329
+ A spec has at most 16 resolved keys, including unmatched feed items. Literal keys pair exactly two nodes, neither nested in the
330
+ other. Inside repeat children, use \`shared: {key: "story-{item.id}", as: "bounds"}\` with
331
+ \`repeat.key: "id"\`: placeholders must name that same field; index and constant keys are refused.
332
+ Each item resolves a distinct key, used at most twice; unmatched feed items remain ordinary cards.
333
+ The pair must use the same as value and cannot cross a dialog or bottomSheet boundary.
334
+ Choose visibility conditions so the pair is not visible together. Full motion morphs geometry;
335
+ reduced uses ordinary visibility crossfades and off swaps instantly.
336
+
337
+ ## One-shot keyframes
338
+
339
+ Every node supports \`keyframes: {trigger: {key: "liked"}, durationMs: 600,
340
+ tracks: {scale: [[0,1],[0.35,1.3],[1,1]]}}\` with \`prototype_keyframes_v1\`
341
+ (iOS 17+). Alternatively trigger with \`{data: "/liked"}\`, relative to data.
342
+ It plays once on each value change, never on first show; another change restarts it.
343
+ Use 100\u20133000 ms and 1\u20137 tracks: scale, scaleX, scaleY, rotation (degrees), offsetX,
344
+ offsetY (dp), alpha. Each track has 2\u20138 [fraction,value] points, strictly increasing
345
+ from 0 to 1, or \`{points, curve: "linear" | "standard" | "spring"}\` (default standard).
346
+ Start and end at rest (scale/alpha 1, rotation/offsets 0); scales must be positive,
347
+ alpha 0\u20131. At most 16 expanded nodes may carry keyframes. These are draw transforms:
348
+ layout and observed semantics stay unchanged. Reduced keeps alpha only; none disables playback.
349
+
350
+ ## Motion v2
351
+
352
+ (needs \`prototype_motion_v2\`) A node's \`animate: {durationMs?, easing?}\` (0-2000 ms, default
353
+ 300; \`standard\`, \`emphasized\`, \`linear\` or \`spring\`) animates changes to its \`alpha\`,
354
+ \`offset\`, \`scale\`, \`rotation\`, \`background\` and \`color\`, typically from \`styleWhen\`; without
355
+ it they are instant. Style \`scale\` (0-4) and \`rotation\` (degrees, -360..360) draw about the
356
+ node's centre without moving siblings. \`transition\` also takes \`slideStart\` / \`slideEnd\`
357
+ (slide in from that edge, mirrored right to left: drawers) and \`scale\`. An \`image\` with
358
+ \`zoomable: {maxScale}\` (1-8) pinches, pans when zoomed and toggles 1x/2x on a double tap, with
359
+ accessibility zoom actions; it cannot also have \`onDoubleTap\`. Everything snaps under
360
+ \`motion: "none"\` or Android animator/transition scale 0. In the reduced profile spatial changes
361
+ snap or crossfade; color and alpha still animate. The iOS agent does not support Motion v2 yet (#14684).
362
+
363
+ ## Icon effects
364
+
365
+ (needs \`prototype_icon_effects_v1\`, iOS 17+) An icon accepts \`effect: {type, trigger?, repeat?}\`.
366
+ Types: bounce, pulse, scale, variableColor, wiggle, rotate, breathe, replace, drawOn.
367
+ Use \`trigger: {key: "flag"}\` or \`{data: "/revision"}\` for one play per value change;
368
+ omit both controls for one play on appearance. \`repeat\` is \`"infinite"\` or 1\u201320 and
369
+ cannot coexist with trigger. Repeats count as loops and share the eight animated-effect budget.
370
+ Scale holds while its trigger is truthy; replace follows icon name changes. Reduced/off rest,
371
+ except reduced replace crossfades. Android approximates effects with scale/alpha/rotation and
372
+ crossfades. iOS 17 uses bounce for wiggle, none for rotate and pulse for breathe; drawOn needs
373
+ 26 and otherwise uses replace. \`platformNotes\` reports approximations/fallbacks with node paths.
374
+
375
+ ${ge}
376
+
377
+ ## Choreography
378
+
379
+ (needs \`prototype_choreography_v1\`) On nodes other than stacks, \`transition\` also takes \`{type, delayMs?}\` (\`delayMs\` 0-600 ms, the wait
380
+ before the enter starts) besides the plain names, and \`blurReplace\`: a blur-and-fade enter and exit
381
+ (iOS \`.blurReplace\`; Android 12 and later blur, Android 11 and earlier fade only). A \`box\`, \`row\` or \`column\` takes \`stagger: {ms, order?}\` (\`ms\` 0-100 per child;
382
+ \`order\` \`forward\` (default), \`reverse\` or \`center\`): its direct children with \`visibleWhen\`
383
+ enter one after another in layout order, each \`ms\` apart, including the children a \`repeat\` makes.
384
+ A child's own \`transition.delayMs\` adds to its stagger wait, and no child may wait more than 600 ms
385
+ in all (a literal \`repeat\` is counted; a data-bound one is clamped on the device). Delays are zero
386
+ under \`motion: "none"\` or Android animator/transition scale 0. In the reduced profile spatial
387
+ transitions become fades. Delays apply to enters only. The iOS agent does not
388
+ support it yet.
389
+
390
+ ## Lazy lists
391
+
392
+ \`list {repeat:{items:{path},as,key},children:[row],axis?,header?,footer?,spacing?,contentPadding?,onEndReached?,endThreshold?,refresh?,maxItems?}\`
393
+ is a lazy scrolling collection (needs \`prototype_lazy_list_v1\` and \`prototype_data_model_v1\`).
394
+ The path resolves to at most ${rM} objects; \`key\` is required, projected and unique. Empty arrays
395
+ are valid. Axis defaults to vertical; spacing/padding are nonnegative dp. One row template and
396
+ optional header/footer each count once toward node/image limits, while per-item binding and text
397
+ budgets still apply. Header/footer are ordinary nodes outside the row scope. Never nest lists,
398
+ put a list in a repeat, or put a same-axis scroll above/below a list. Rows cannot nest repeats/pagers.
399
+ Only composed rows are in \`observe\`; use \`swipeOn\` to reach more. Row keys preserve identity.
400
+ \`onEndReached\` fires for nonempty rows within \`endThreshold\` of the end (1\u201350, default 3), once
401
+ per \`(rowCount,lastRowKey)\` signature; empty/no-change pages never refire. Signature state survives
402
+ recomposition and resets on show. \`endThreshold\` needs \`onEndReached\`. Vertical \`refresh\` uses
403
+ scroll.refresh semantics and a declared boolean key (also needs \`prototype_refresh_v1\`).
404
+ \`maxItems\` (1\u2013${rM}) requires initial data to fit; appending trims oldest rows atomically before
405
+ final data limits. The smallest cap among lists sharing the exact data path wins.
406
+ \`request.append:{from,to}\` (same lazy-list capability) extracts a response array from an object
407
+ member at \`from\`, extends the existing \`/data/...\` array \`to\`, and writes the remaining response
408
+ to optional \`into\`. These paths must not overlap. Any failure rolls back all writes and returns
409
+ \`invalid_response\`. Extend events precede into/meta/status/request/onSuccess events; change
410
+ payloads are \`{op:"append"|"extend"|"prepend",value,maxItems?,revision}\`. \`prependData {path,value}\` resolves one row and sends writer op \`prepend\` with \`value:[row]\`;
411
+ it requires the lazy-list, data-actions and data-model capabilities. It uses the smallest matching
412
+ list cap and trims from the end, keeping the first maxItems rows. A missing event value means the
413
+ page exceeded the byte budget and the host stops tracking the spec.
414
+ Renderers keep the first visible row key and offset across writes, even at the top; new rows sit
415
+ above it. Headers/footers are not anchors; a trimmed key uses the renderer default. Each stack
416
+ entry restores its list key/offset after push/pop; a missing key falls back to the nearest surviving
417
+ index or top. Prepend, anchoring and per-stack-entry list scroll restore are not device-verified
418
+ until the acceptance run; benchmark qualification remains unverified.
419
+
420
+ ## Charts
421
+
422
+ \`{type: "chart", variant, series, yRange?, xLabels?, goal?}\` draws a \`line\`, \`area\`, \`bar\` or
423
+ \`ring\` chart (needs \`prototype_chart_v1\`). \`series\` holds 1-4 \`{path, x?, y, label, color?}\`
424
+ entries: \`path\` is a \`/data\` pointer to an array of objects (empty is valid, at most ${Wae} items as
425
+ for any data array), \`y\` names each point's numeric member and \`x\` its optional label member;
426
+ \`color\` is a hex or theme role, or a \`{light, dark}\` pair. Points are drawn in array order, so
427
+ \`appendData\`, a \`request\` \`into\` or a \`/data\` patch re-renders the chart. \`yRange {min, max}\`
428
+ fixes the axis (max above min); otherwise it spans the values and the goal, and bars rise from zero.
429
+ \`xLabels\` is \`none\` (default), \`ends\` or \`all\` of the first series' x labels. \`goal\` draws a
430
+ dashed reference line; a \`ring\` takes exactly one series and a positive \`goal\`, and draws that
431
+ series' latest value against it. The chart is one accessible node labelled with its summary, such as
432
+ "Line chart, Heart rate, 24 points, min 58, max 141, latest 72" or "Ring chart, Steps, 6500 of
433
+ 10000"; \`observe\` reports it, and an authored \`contentDescription\` replaces it. A literal path that
434
+ does not resolve to an array, a point that is not an object, or a point whose \`y\` is not a number
435
+ fails at \`<path>.series[i].path\` or \`.y\`. The iOS agent draws charts with Swift Charts.
436
+
437
+ ## Rich text
438
+
439
+ A text node has exactly one of \`text\` or \`spans\`; plain \`text: ""\` remains valid.
440
+ Spans are strict \`{text, style?, onTap?}\` objects with nonempty, untrimmed text; whitespace is valid.
441
+ Span style accepts only color, fontWeight, fontStyle, textDecoration, textSize, letterSpacing,
442
+ fontFamily and background, with the same ranges as node style; background is a colour, never a gradient.
443
+ There are no span ids, testTags, visibility conditions, nested spans or URLs.
444
+ Each span uses the same state \`{key}\`, data \`{data:/pointer}\`, repeat \`{item.field}\` / \`{index}\`
445
+ and component \`{props.x}\` binding as node text; span actions bind like node onTap actions.
446
+ The resolved texts concatenate in order without separators: observe and accessibility show the
447
+ full plain text (an explicit node contentDescription still overrides its accessible label).
448
+ Spans do not count as nodes and require \`prototype_rich_text_v1\`; older devices are refused before show.
449
+ A tappable span is part of the text node, not a separate accessibility node, and runs actions without opening URLs.
450
+ On Android, activate it with \`tapOn {accessibilityLink:"<span text>"}\` or
451
+ \`tapOn {text:"<full plain text>", subtext:{text:"<span text>"}}\` without \`layer\`:
452
+ these selectors act across windows because tapOn refuses layer with accessibilityLink/subtext (#9305).
453
+ Use \`tapAt\` on the span bounds as the fallback.
454
+ On iOS, links are drawn as links and tap-through is by \`tapAt\` only; show reports the
455
+ \`rich-text-links\` platform note for tappable spans because there are no separate accessibility elements or semantic links.
456
+
457
+ ### Linkify
458
+
459
+ \`linkify: {kinds, style?, onTap}\` on a text node with \`text\` (not \`spans\`) turns @mentions,
460
+ #hashtags and URLs in the resolved text (for example \`{item.body}\`) into links. \`kinds\` lists 1-3 of
461
+ \`mention\`, \`hashtag\`, \`url\`; \`style\` takes the span style fields. A match starts at the start of
462
+ the text or after whitespace: a mention is \`@\` + 1-30 of \`[A-Za-z0-9_]\` (\`ana@example.com\` is not
463
+ one), a hashtag \`#\` + up to 100 letters, digits or \`_\` (not all digits), a URL \`http(s)://\` up to
464
+ whitespace without trailing punctuation. Inside \`onTap\` only, \`{match: "text"}\` (the tapped
465
+ \`@ana\`) and \`{match: "kind"}\` work as a \`setState\` value, a \`setData\`/\`appendData\` value or an emit
466
+ context entry: \`[{type:"setState",key:"profileHandle",value:{match:"text"}},{type:"push",stack:"nav",screen:"profile"}]\`.
467
+ Repeat bindings are refused inside linkify \`onTap\`. URL taps never open a browser. Requires
468
+ \`prototype_linkify_v1\`.
469
+
470
+ ## Saved prototypes
471
+
472
+ Use \`save\` with \`spec\`, optional \`assets\`, \`label\` (defaults to \`spec.id\`) and
473
+ \`icon\` (an asset id uploaded in \`assets\` in that same call). Android needs
474
+ \`prototype_saved_v1\`. On iOS this is simulator only and needs no prototype agent:
475
+ the viewer template is resolved from the verified release asset, or from
476
+ \`AUTOMOBILE_IOS_PROTOTYPE_VIEWER\` (build it with \`scripts/ios/prototype-viewer-build.sh\`).
477
+ Omit \`icon\` for the default app icon. Pinning returns \`pinned:"unsupported"\`; the iOS
478
+ app icon is already on the home screen. \`listSaved\` on iOS returns bundle ids accepted by
479
+ \`deleteSaved\`, with \`pinned:false\` and no \`savedAt\` (the save time cannot be recovered).
480
+ \`deleteSaved\` also accepts the authored id; it removes the iOS app icon, so
481
+ \`removeIcon:true\` is ignored with a warning.
482
+
483
+ On Android or an iOS simulator with the ProtoViewer app installed, \`save\` without \`pin\` or \`icon\` imports
484
+ the prototype into the ProtoViewer library instead, and does not need CtrlProxy or
485
+ \`prototype_saved_v1\`. It returns \`viewer\` \`{app, libraryId, label}\` (plus \`display\` {key, role}, the panel it landed on, on an iPhone Duo); every save is a new
486
+ library entry, which \`listSaved\` and \`deleteSaved\` do not see. ProtoViewer treats the entry
487
+ as imported: its requests stay off until the user allows network in ProtoViewer, and it is not
488
+ attached to this host (no \`awaitEvent\`, \`patch\` or \`inspect\`). \`{action:"open",id}\` with
489
+ that \`libraryId\` launches it on Android or an iOS simulator. It needs ProtoViewer installed.
490
+ If ProtoViewer is absent, too old for this route, or its inbox cannot be used, \`save\`
491
+ keeps the platform save path. A timeout after delivery may still have saved the entry; check
492
+ ProtoViewer before retrying.
493
+
494
+ ## Fullscreen host chrome
495
+
496
+ \`window.chrome\` is \`"none"\` (default) or \`"bar"\`, fullscreen only, on a device advertising
497
+ \`prototype_window_chrome_v1\`. With \`none\` there is no host dismiss bar: content starts at the
498
+ very top of the screen, including under the status bar. A \`topAppBar\` or top content needs
499
+ \`safeAreaPadding\` on the top edge; relative sheet detents use the full window height.
500
+ A host-owned two-finger long press (about 1 s) anywhere on the window reveals a dismiss pill for
501
+ 5 s; tapping it dismisses with reason \`user\`. Agent dismiss, a replacing show, authored dismiss,
502
+ the session's five-minute idle TTL and last-client disconnect still work.
503
+ \`window.transition\` (auto|none|fade|slide|scale) and \`repeat.stagger\` (0-100 ms per instance, capped at 600 ms) require \`prototype_window_motion_v1\` on Android.
504
+ \`window.persistence: "device"\` defaults to \`bar\` and keeps its existing persistence behaviour.
505
+ The device implementations ship separately: without the capability a device keeps the bar,
506
+ explicit \`none\` is refused, and omitted chrome or \`bar\` succeeds. A successful fullscreen
507
+ show and subsequent host-local status report the effective value as \`lastResult.chrome\`;
508
+ it is absent for sheet/floating windows, dismiss, failed show and adopted prototypes.
509
+
510
+ ## Frosted backdrop
511
+
512
+ \`window.backdrop\` is \`{material, blur?, tint?}\` and blurs what is behind the window on any
513
+ placement (not the prototype's own content). \`material\` is one of \`ultraThin\`, \`thin\`,
514
+ \`regular\`, \`thick\` or \`glass\` (Liquid Glass, iOS 26+ only). \`blur\` is an optional whole radius in
515
+ dp (points on iOS), 0-150. \`tint\` is one hex or role colour, not a light/dark pair. The host refuses
516
+ the show on a device without \`prototype_backdrop_v1\`. Where the platform cannot blur, the device
517
+ draws a tonal fill (\`tint\` at 85% alpha, else the \`surfaceContainer\` role at 85%) and \`lastResult.backdrop\` is
518
+ \`fallback\`; otherwise it is \`blur\`.
519
+
520
+
521
+ ## Tooltip and drawer
522
+
523
+ \`tooltip\` requires \`prototype_tooltip_v1\`, boolean \`openWhen: {key, equals}\`, nonblank \`text\`, and exactly one of \`anchorTo\` (a unique node id in the same spec, other than itself) or \`anchor\` (the existing element selector or bounds anchor). Element anchors are resolved by the host to a target rect; native renderers hoist the tooltip as an overlay, not ordinary anchored content. IDs are literal: repeated targets with multiple instances and duplicate component-instance ids are ambiguous and rejected; a repeat with no items supplies no target.
524
+
525
+ Optional \`caret\` is \`auto\` (default), \`top\`, \`bottom\`, \`start\` or \`end\`: it names the bubble edge pointing at the target, so \`top\` places the bubble below the target. Auto chooses a side with room; auto and explicit sides flip to the opposite side when needed to stay inside the window. Optional strict \`spotlight: {padding?, cornerRadius?}\` draws the scrim role with a rounded hole around the target: integer dp 0..64, defaults 8 and 12 respectively. Without spotlight there is no scrim. Tapping outside the bubble on the spotlight scrim, or tapping the bubble without \`onTap\`, writes \`!equals\` to the open key, then runs optional \`onDismiss\` (1..32 actions). Bubble and scrim tags are \`<testTag>\` and \`<testTag>.close\`. Use one boolean per step for a coach-mark tour; a step's \`onTap\` uses \`setState\` to close its key and open the next step's key. Numeric equality is not supported by \`openWhen\`.
526
+
527
+ \`drawer\` requires \`prototype_drawer_v1\`, \`edge: "start"|"end"\`, boolean \`openWhen: {key, equals}\` and a \`child\` node. Start/end follow layout direction. The renderer fixes panel width; there is no width field. Optional \`scrim\` accepts the same color value (including mode pairs) as bottomSheet and defaults to the scrim role. Optional \`dismissOnSwipe\` defaults to true, as for bottomSheet. Both nodes accept the common node fields, including \`onTap\`, but no long-press or double-tap handlers.
528
+
529
+ ## Menu and select
530
+
531
+ \`menu\` and \`select\` require \`prototype_menus_v1\`; the host refuses them before showing or uploading assets when the connected renderer lacks it.
532
+
533
+ A \`menu\` requires \`openWhen: {key, equals}\` with a boolean equality and 1\u201316 strict \`items\`. Each item is \`{label, icon?, trailingText?, enabled?, onTap?}\` or exactly \`{type:"divider"}\`. Labels and trailing text are nonempty, icons use the existing icon names, and \`onTap\` has 1\u201332 existing actions. A disabled item is dimmed, cannot be tapped and does not close the menu. Dividers are neither focusable nor tappable. The elevated surface is anchored to its parent\u2019s bottom end and clamped inside the window; it opens above its anchor when it does not fit below. An enabled item or outside tap writes \`!equals\` to the open key; item actions run after closing.
534
+
535
+ A \`select\` requires a string \`stateKey\` and 2\u201332 \`{value,label}\` options with unique nonempty values and nonempty labels; its optional label is nonempty. It shows the current option\u2019s label, or empty text when no option matches, and a trailing expand indicator. Expansion is local view state. Choosing an option writes the string key, emits the same \`change\` payload as \`radioGroup\`, and collapses the list. Tapping the field again or outside also collapses it. Both nodes support repeat templates and reusable \`use\` components: state keys, menu item labels/trailing text/actions and select labels/option labels bind placeholders.
536
+
537
+ Rendering stays inside the prototype window on both platforms so \`observe\` and \`tapOn\` reach the normal hierarchy. Android uses an in-window surface rather than a separate DropdownMenu/Popup window. iOS uses an in-window list rather than system SwiftUI Menu/Picker popups; successful iOS shows report this substitution in \`platformNotes\`. Menu icons use the existing SF Symbols mapping on iOS.
538
+
539
+ The select field and menu surface carry the node \`testTag\`. Select option rows use \`<testTag>.<value>\`; menu item rows use \`<testTag>.<index>\`, with zero-based positions including dividers. Android adds row tags only when \`testTag\` is set; iOS falls back to \`(testTag ?? id).<value>\` or \`(testTag ?? id).<index>\`.
540
+
541
+ `,be="## Lists: repeat\n\nWrapping containers require `prototype_flow_grid_v1`. A `flow` is a wrapping row:\n`style.spacing` is the item gap, and `style.runSpacing` is the line gap (finite dp >= 0,\nwith the same bounds as spacing). Optional `maxLines` is an integer >= 1; overflow is\nclipped visually, while all children stay in the hierarchy and count toward node/depth budgets.\nA `grid` requires `columns`: an integer 1..12 or `{minWidth: dp}` with finite dp > 0.\nSpacing applies to both axes unless runSpacing overrides the row gap. runSpacing is meaningful\nonly on flow/grid. Grid is always non-lazy and never scrolls itself; wrap it in `scroll`.\nChildren are placed row-major; a span that does not fit the row starts a new row. Optional\n`span` is an integer >= 1 on any node shape, but is valid only on a direct grid child;\nit must not exceed fixed columns. Adaptive runtime spans are clamped to the resolved count.\n\nA `carousel` (`prototype_carousel_v1`) is a horizontally scrolling row of items, one per child\n(at least one), that snaps to items and may declare `repeat` like `row`. `itemWidth` (dp, > 0)\nis required. `variant` is `multiBrowse` (default; items change width as they scroll, Android\nMaterial), `hero` (centered large item) or `uncontained` (fixed-width items). `itemSpacing`\nis dp >= 0. iOS draws every variant as fixed-width snapping items.\nA pager's optional `stateKey` binds a declared nonnegative integer number key both ways\n(requires `prototype_pager_state_key_v1`). Swipes write the key and emit `page_changed`\nand `change`; state writes and patches move the pager, clamped to its page range, animated\nunless `motion: \"none\"`. Initial/replacing-show state wins over retained pages. Use the\nsame key on a tabBar/bottomNav for simple shared selection, and read it in `styleWhen`\non a row of dot boxes outside the pager. Two pagers cannot share a key; binding inside\nrepeat templates is refused. `setPage` writes the same selection.\nA pager may also declare\n`indicator: \"dots\"` (default `none`): page dots with the current page filled.\n`indicatorPlacement` is `over` or `below` and applies only with `indicator: \"dots\"`.\nWhen omitted, Android defaults to `over` and iOS to `below`. `below` places a row under\nhorizontal pages or a column beside vertical pages; `over` overlays the bottom or end edge.\nBoth fields need the same capability. A carousel has no page index, event or `setPage` target; use a\n`pager` when the page matters.\n\nA `box`, `row`, `column`, `flow`, `grid` or `carousel` may declare `repeat: {items, as}`. Its `children` are the template and\nare instantiated once per item, in order, as siblings inside the same container (the container\nitself is not repeated). For eagerly composed rows use a `column` with `repeat`; for lazy rows use the `list` node.\n\n```json\n{\n \"type\": \"column\",\n \"repeat\": {\n \"items\": [\n { \"name\": \"Alpha\", \"tag\": \"a\" },\n { \"name\": \"Beta\", \"tag\": \"b\" }\n ],\n \"as\": \"item\"\n },\n \"children\": [\n {\n \"type\": \"text\",\n \"text\": \"{index}: {item.name}\",\n \"onTap\": [{ \"type\": \"setState\", \"key\": \"picked\", \"value\": \"{item.tag}\" }]\n }\n ]\n}\n```\n\nGrammar:\n\n- `{index}` is the zero-based position; `{<as>.<field>}` reads a field of the current item.\n- Literal items are objects of scalar fields (no nesting); `as` is a state-key-shaped identifier.\n A state key cannot hold a list.\n- Placeholders are bound in a template child's `text`, the label/title/text fields of components\n (button, fab, segmentedButton options, topAppBar, dialog, menu item labels/trailingText, select labels/options, snackbar), state condition operands\n (`equals`/`notEquals` strings), `setState.value` and `emit.name`. `emit.payload` is not bound.\n Anything else, including a `{key}` state placeholder and other braces, is left untouched. A show or patch lists a placeholder\n in such a field as `placeholderFindings` (for example `chip.label` or `badge.text`).\n (`equals`/`notEquals`/`contains`/`startsWith` strings, including inside `errorWhen`) and `textField.errorText`, `setState.value` and `emit.name`. `emit.payload` is not bound.\n Anything else, including a `{key}` state placeholder and other braces, is left untouched.\n- State keys bind too: `stateKey`, `hourKey`/`minuteKey`, a list item's trailing `stateKey`,\n `openWhen.key`, condition `key`s and the `key` of `setState`, `toggle`, `increment` and\n `decrement`, so `\"key\": \"liked_{item.id}\"` gives each row its own state. Every bound key must\n be a literal state key and, like any key, be initialised in `state` with the type it binds.\n- A string that is exactly one placeholder keeps the item field's own type (so\n `equals: \"{item.id}\"` matches a numeric state value when `id` is a number); inside a longer\n string it renders as text.\n\nRejected (same paths in TypeScript and Kotlin):\n\n- a placeholder naming a field missing from any item (`Unknown repeat field \"nme\"`)\n- a template that contains another `repeat` or a `pager`\n- `repeat` on a leaf node\n- an `emit.name` that binds to an empty string for any item\n- a state key that binds to an invalid key for some item (fails at `<container>.repeat.items[i]`),\n or a state-key placeholder outside every template\n- an expanded tree that exceeds the node or image limit (fails at the container's `repeat`);\n depth is unaffected because instances are siblings\n",ke="### Emit context\n\nUse a stable `emit.name` such as `pick` and `context: {id: {item: \"id\"}}` to report a\nrepeat item's id, instead of encoding the id into the event name. `emit.payload` stays literal\nand uninterpolated; context is resolved on the device when the emit action runs, after earlier\nactions in the same atomic `onTap` list. For example, setState then emit reads the new state.\n\nEach nonempty context name has a strict binding object with exactly one key:\n\n- `{state: key}` reads state; repeat placeholders such as `liked_{item.id}` bind to concrete keys.\n- `{item: field}` reads a typed repeat field; `{index: true}` reads its zero-based numeric index.\n Both require a repeat template. The field must exist in every item; data-path repeats use the\n same scalar projection as text placeholders. Expansion replaces these bindings with `{value: ...}`.\n- `{page: pagerId}` reads an existing pager's current numeric page.\n- `{data: pointer}` reads any JSON value via the existing data pointer syntax and\n `MAX_PROTOTYPE_DATA_POINTER_LENGTH`; it also requires `prototype_data_model_v1`.\n- `{value: json}` is literal JSON, subject to the emit payload depth limit.\n\nMissing state keys and data paths resolve to null. `MAX_PROTOTYPE_EMIT_CONTEXT_ENTRIES` bounds the\nnumber of entries. The resolved object's compact JSON UTF-8 bytes\nare bounded by `MAX_PROTOTYPE_EMIT_PAYLOAD_BYTES`; this runtime count includes multibyte strings\nand compact JSON numbers, rather than the payload validator's conservative numeric cost.\nIf over budget, sort entries by ascending name (byte-wise UTF-8 order), keep the longest prefix\nthat fits and omit all remaining entries. An oversized first entry produces `{}`. The tap succeeds.\n\nA show or patch replacement using `emit.context` requires `prototype_emit_context_v1`; older\ndevices are refused before upload, display or patch dispatch. The `prototype_event` wire has `context` next to the unchanged\n`payload` only if the emit declared context (including an empty object). It has\n`contextTruncated: true` only when truncated; otherwise the field is omitted, never false.\n`awaitEvent` and host status preserve these fields.\n\n",Se='### Storage\n\nA show resets `state` and `data` to the spec\'s values, so what the user typed or a data action\nchanged is lost on the next show, and saved prototypes keep the spec but no user data. A root-level\n`storage` block keeps chosen values on the device, for example\n`"storage": {"version": 1, "keys": ["state:darkMode", "data:/cart"]}`.\n\n`version` is an integer from 1; `keys` holds 1 to `MAX_PROTOTYPE_STORAGE_KEYS` unique entries.\n`state:<key>` names a key declared in `state`. `data:<pointer>` is an RFC 6901 pointer to a member of\n`data` (the pointer is relative to `data`: `data:/cart` is `/data/cart`, the path a data action\nuses; the whole document is refused). Listing a pointer and its ancestor or descendant is rejected,\nand the values the spec itself holds for the listed keys must fit `MAX_PROTOTYPE_STORAGE_BYTES`\n(counted as the UTF-8 bytes of each storage key plus its compact JSON value).\n\nThe device saves each listed key, keyed by the spec `id`, after every write: a user edit, a device\ndata action or a request response. A write that would take the stored total over\n`MAX_PROTOTYPE_STORAGE_BYTES` still applies on screen but is not stored (`skippedKeys` in inspect).\nStored values survive a dismiss, a process restart and a saved prototype\'s relaunch.\n\nPrecedence on the next show of the same `id` (the adopted decision of #12522):\n\n- Stored values replace the spec\'s values for the listed keys. The spec\'s `state` is otherwise still\n authoritative.\n- A different `storage.version` than the values were stored under discards all of them: the spec\'s\n values stand. Bump `version` when you change what a key means.\n- A stored key the spec no longer lists is dropped, and a show without a `storage` block drops every\n stored value of that id.\n- A stored value that no longer fits the spec is dropped and the spec\'s value stands: a `state` value\n whose type (string, number, boolean) differs from the spec\'s, a state key the spec no longer\n declares, a `data` value of another JSON kind than the spec\'s value at that pointer, or one that\n cannot be written there (missing parent, a data limit).\n- Restoring happens before the first draw and emits no `change` event. After a show, read the real\n values with inspect: the show result and the host\'s cached copy carry the spec\'s values.\n- A host `patch` against a stored key wins: it applies under the usual `baseRevision` conflict rules\n and the device then stores the new value.\n\n`inspect` reports `storage: {version, keys: [{key, bytes}], totalBytes, skippedKeys?}` per prototype\nfor the keys that currently hold a stored value, or omits it when the device does not report it.\n`{action: "clearStorage", id}` wipes the stored values of an id (the next show starts from the spec;\nthe result has `cleared`, the number of keys removed). `deleteSaved` also clears that id\'s storage.\n`storage`, `clearStorage` and the report need `prototype_storage_v1`: a spec with `storage` is refused\nbefore anything is shown, and `clearStorage` before anything is sent, on a device without it.\n\n\n',xe='### Sharing\n\n`{type:"platform", call:"share.open", args:{text:{value:"Hi {name}"}}}` requires\n`requires.platform:["share"]` and `prototype_platform_share_v1`. Current runtimes do not\nadvertise it; the host refuses the spec before show. Arguments are value bindings: at least one\nof `text|url|images|files|contact`, plus optional `subject`. URLs use literal http(s) schemes; prototype deep links are unavailable. Images accept up to 10 uploaded IDs\n(or `{asset:id}` / `{data:"/pointer"}` holding an uploaded ID), files up to 4\n`{asset,name,mimeType}` entries. File names have no leading dot, separators or controls and\nfit 128 UTF-8 bytes; MIME types are type/subtype tokens. Contact fields are `name` (required),\n`phone,email,org,url,note`, serialized as vCard 3.0. Strings take ordinary placeholders and\nfit 65536 UTF-8 bytes. `into` receives `{outcome:"shared"|"unknown",target?,items}`;\nits text summary keeps 2048 Unicode scalars. `statusKey,onSuccess,onError` use platform-call\nsemantics. Real system sheets, cancellation callbacks and queued fake outcomes are owed;\n`platformMode` and `prototype simulate` are not accepted yet.\n\n### Requests\n\n`{type: "request", url, method?, headers?, query?, body?, into?, statusKey?, via?, timeoutMs?, onSuccess?, onError?}`\ncalls an HTTP endpoint from a tap and writes the answer into `/data`, so a prototype can load and\npost real content without the agent answering each tap. It requires `prototype_network_v1`\n(`prototype_network_device_v1` as well for `via: "device"`); a show or patch result containing\none is refused on a device without them.\n\n- `url` is an absolute `http` or `https` URL (at most 2048 characters). Host and device requests\n apply an egress policy: local destinations and daemon ports on private addresses are not permitted.\n It takes the text placeholders of any text field (`{key}` state, `{data:/pointer}`, repeat `{item.field}` / `{index}`). Without a\n placeholder it must parse as an http(s) URL; with one, it must start with a literal `http://` or\n `https://`. (`{state:key}` is not a placeholder: write `{key}`.)\n- `method` is GET (default), POST, PUT, PATCH, DELETE or HEAD. A GET or HEAD cannot have a `body`.\n- `headers` and `query` (up to 16 entries each) map a name to a literal string or to a value\n binding: `{state: key}`, `{data: pointer}`, `{value: scalar}`, `{item: field}` / `{index: true}`\n (repeat only). A bound value must be a string, number or boolean. `query` entries are set on the URL\n in order (a key already in the URL is replaced). Literal header and query strings take the same\n text placeholders as `url`.\n- `body` is a value binding, so a literal body is `{value: json}`; `{state: key}` sends the state\n value and `{data: pointer}` the data subtree. A non-string body is sent as JSON with\n `Content-Type: application/json` unless a header sets one; a string body is sent as is.\n- `into` is a `/data/<pointer>` path with the data-action rules (a whole `{index}` segment inside a\n repeat); it must name an object member, not an array element, and its parent must exist. Without\n `into` nothing is written to data.\n- `statusKey` names a declared **string** state key. The device sets it to `"loading"` when the\n request starts and to `"success"` or `"error"` when it ends, so `visibleWhen` can show a spinner\n (`{key: "feedStatus", equals: "loading"}`) or an error. Do not bind it to a control with fixed options.\n- `via` is `"host"` (default: the host runs the call and the device sends only the resolved request\n frame) or `"device"` (the device makes the call itself and cannot use `localhost` for the\n host machine, see below).\n- `timeoutMs` is optional, 1 to `MAX_PROTOTYPE_REQUEST_TIMEOUT_MS`; the default is\n `DEFAULT_PROTOTYPE_REQUEST_TIMEOUT_MS`.\n- `onSuccess` and `onError` are action lists (1 to 32) run after the request ends, for example\n `setState`, `push` or another `request`. Requests nest up to `MAX_PROTOTYPE_ACTION_DEPTH` levels.\n\n**Response.** A 2xx response is parsed: JSON is written to `into` exactly as `setData` would write it,\nanything else as a string, an empty body (and a HEAD) as `""`. The response must be data (no null,\nwithin the data limits) or the request fails with `invalid_response`. Metadata goes to `<into>__meta`,\na sibling whose key is the last segment of `into` plus `__meta` (`/data/feed` writes\n`/data/feed__meta`): `{status?, headers?, durationMs, error?: {code, message}}`. `status` and\n`headers` (names lower-cased) are present once a response arrived; `error` is present for every\nfailure. A non-2xx status leaves `into` untouched, writes the metadata with\n`error: {code: "http_error", message: "HTTP 404"}` and counts as an error. If the metadata would not fit\nthe data write limit, `headers` is dropped. Other `error.code` values: `timeout`, `too_large`,\n`connection_refused`, `blocked_destination` (the destination is not permitted), `cancelled`, `invalid_url`, `invalid_request`, `network`, `too_many_in_flight`,\n`invalid_response`.\n\n**Order.** On completion the device writes `into`, then `<into>__meta`, then `statusKey`, then sends the\nrequest event, then runs `onSuccess` or `onError`. Each `/data` write is a `change` event as for data\nactions. The request event is an `emit` with `name` `prototype_request` (reserved: an authored\n`emit` cannot use it) and `payload`\n`{requestId, method, url, via, into?, outcome: "success"|"error", status?, durationMs, error?: {code, message}}`,\nwhere `url` is the resolved URL cut to 2048 characters. Await it with\n`awaitEvent {kind: "emit", eventName: "prototype_request"}`.\n\n**Limits.** A response body over `MAX_PROTOTYPE_RESPONSE_BYTES` fails with `too_large` (the value never\nexceeds `MAX_PROTOTYPE_DATA_WRITE_BYTES`, so an accepted response fits one `/data` write). At most\n`MAX_PROTOTYPE_REQUESTS_IN_FLIGHT` requests run per device; another fails with `too_many_in_flight`.\nA request to an `into` that already has one in flight cancels the first: the latest request wins. The\ncancelled one writes nothing, does not touch `statusKey`, runs no follow-up and reports a\n`cancelled` event. Dismissing the prototype cancels all its requests.\n\n',Te="## Data\n\n`data` is agent-owned, read-only content: nested objects, arrays, strings, finite numbers and\nbooleans, without null. `state` remains the user-owned scalar map; actions only write declared\nstate keys. A full show replaces data; a capability-gated patch can change data. See the generated Limits table.\nThe data byte limit uses the `emit.payload` cost: every number costs 32 bytes regardless of\nits digits, so numeric-heavy data reaches the limit before its serialized size does.\n\n`MAX_PROTOTYPE_REPEAT_PATH_CELLS` bounds all path-bound repeats after component expansion,\nincluding empty templates. An instantiated object item costs one cell per projected scalar member,\nwith a minimum of one cell. Literal repeats do not count. Containers are checked in pre-order;\na crossing container fails at `repeat.items.path` with `Repeat path cell limit exceeded`, after\nits three path-source checks and before template projection.\n\nPointers are nonempty RFC 6901 paths relative to data, beginning with `/`; decode `~1` as `/`\nand `~0` as `~`. Object keys are exact own members; array indices are `0` or a positive decimal\nwithout leading zeros. An empty key uses a trailing slash. Invalid escapes and overlong pointers\nare rejected; a missing text path renders empty.\n\n```json\n{\n \"id\": \"orders\",\n \"window\": { \"placement\": { \"type\": \"fullscreen\" } },\n \"data\": { \"orders\": [{ \"id\": 7, \"title\": \"Latte\", \"total\": 4.5 }], \"cartCount\": 1 },\n \"state\": { \"liked_7\": false },\n \"root\": {\n \"type\": \"column\",\n \"repeat\": { \"items\": { \"path\": \"/orders\" }, \"as\": \"item\", \"key\": \"id\" },\n \"children\": [{ \"type\": \"text\", \"text\": \"{item.title} ({data:/cartCount})\",\n \"visibleWhen\": { \"data\": \"/cartCount\", \"gt\": 0 },\n \"onTap\": [{ \"type\": \"toggle\", \"key\": \"liked_{item.id}\" }] }]\n }\n}\n```\n\n- `repeat.items: {path}` must resolve to an array of objects (empty is valid). Only scalar members\n with field-shaped names are projected into items. `repeat.key` names a field every item has;\n its rendered values must be unique, for literal lists too.\n- `{data:/pointer}` binds only the text-kind fields repeat already binds: text, button/FAB labels,\n segmented option labels, app bar title/navigation/action labels, dialog title/text/button labels,\n menu item labels/trailing text, select labels/option labels, and snackbar text/action label. Missing, object or array values become empty text. Other braces\n stay literal. A data string is inserted as plain text before repeat and state binding, so\n `{item.x}`, `{index}` or `{key}` inside it bind afterwards exactly like authored text.\n Data values cannot be escaped; inserted strings are never scanned again for data placeholders.\n Pointers are untouched by component props.\n- `MAX_PROTOTYPE_SUBSTITUTED_TEXT_LENGTH` (16384) bounds the sum of replacement UTF-16\n lengths per bindable field at each stage: data in text-kind fields, then repeat in all bindable\n fields for every item, including repeat placeholders inserted by data. Literal characters and\n unresolved repeat placeholders contribute zero. State placeholders are not validated because\n state can change after show. Exceeding the cap fails at the authored field path with\n `Substituted text length limit exceeded`.\n `MAX_PROTOTYPE_SUBSTITUTED_TEXT_TOTAL` (4194304) also bounds rebuilt text per validation: each\n recognized data stage and each repeat item charges authored stage length plus replacement length,\n including literal units and placeholder tokens. Inside a repeat, a data-resolved field is also\n charged once per item, even without repeat placeholders. Purely literal fields and state\n placeholders are not charged. Crossing the total fails at that field with `Substituted text total limit exceeded`.\n During state interpolation, a replacement that would exceed the per-call substitution limit stays\n as written, and later placeholders may still substitute.\n- A condition uses exactly one of `key`, `data`, `platform`, `valid`, `all`, `any`, `not`. A data condition has exactly\n one comparison: `equals` (same scalar type and value), `notEquals` (its inverse, including missing\n or non-scalar), `gt` or `lt` (numbers only, otherwise false), or `empty` (data only). `equals`/`notEquals` operands are\n literals; repeat placeholders there are rejected. Key forms also accept `contains`, `startsWith`, `lengthGt` or `lengthLt` (see string conditions). `bottomSheet.openWhen` remains boolean state.\n\nThe host validates data and repeat paths; the device resolves bindings once per show before\nrepeat expansion. All data uses require `prototype_data_model_v1`; an older device is refused\nbefore uploads or show, with an update/relaunch hint. `status` reports the last successful show's\ndata next to state; `inspect` reports and adopts device data. Absent data is omitted; dismiss\nclears it. Events carry data only as the value of a device data write (see Data actions).\n",_e=`## Reuse: components
542
+
543
+ A top-level \`components\` map names node templates; a \`use\` node places one anywhere a node goes.
544
+ The host expands every \`use\` before validating and sending, so devices only see plain nodes and
545
+ every limit counts the expanded tree.
546
+
547
+ \`\`\`json
548
+ {
549
+ "id": "feed",
550
+ "window": { "placement": { "type": "fullscreen" } },
551
+ "state": { "liked_a": false, "liked_b": true },
552
+ "components": {
553
+ "postCard": {
554
+ "root": {
555
+ "type": "card",
556
+ "children": [
557
+ { "type": "text", "text": "{props.name}" },
558
+ { "type": "button", "label": "Like", "onTap": [{ "type": "toggle", "key": "{props.likeKey}" }] }
559
+ ]
560
+ }
561
+ }
562
+ },
563
+ "root": {
564
+ "type": "column",
565
+ "children": [
566
+ { "type": "use", "component": "postCard", "props": { "name": "Alexey", "likeKey": "liked_a" } },
567
+ { "type": "use", "component": "postCard", "props": { "name": "Bea", "likeKey": "liked_b" } }
568
+ ]
569
+ }
570
+ }
571
+ \`\`\`
572
+
573
+ - A \`use\` node has only \`type\`, \`component\` and \`props\`; props are scalars (string, number,
574
+ boolean). \`{props.<field>}\` binds in the same fields as repeat placeholders, state keys included,
575
+ and a string that is exactly one placeholder keeps the prop's type.
576
+ - A component may contain \`repeat\` and other \`use\` nodes. A \`use\` inside a repeat template may
577
+ pass item placeholders as props (\`"likeKey": "liked_{item.id}"\`); the repeat binds them later.
578
+ - Rejected: an unknown component, a missing or unused prop, a non-scalar prop, a cycle (including
579
+ a component that uses itself) and \`use\` nesting deeper than its limit (see Limits).
580
+ - An error inside an expansion names each \`use\` it went through, for example
581
+ \`root.children[2] (use postCard) \u2192 components.postCard.root.children[1].label\`.
582
+ `,Ae=`## Style polish
583
+
584
+ Glass: style.surface "glass" and strict style.glass {variant?:regular|clear,tint?:colour,interactive?:boolean} need prototype_glass_v1 (iOS 26+ only today; older iOS and Android refuse it). glass requires surface "glass" in the same style, including styleWhen; cornerRadius shapes it, absent means capsule. ios.glassGroup {spacing:0..200} is for column/row/box/flow/grid/list/listItem; descendant glass shared keys morph in their nearest group, must be unique there, cannot collide with visibility-pair keys, and require repeat.key templates under repeat (including the repeated node). ios.glassUnion (ASCII id, 1..64), ios.glassTransition matchedGeometry|materialize|identity need a base glass surface; ios.backgroundExtension:true is image-only. All four ios glass keys are node-only, invalid in window.ios/theme.ios. All four need only the glass gate on iOS; other ios keys still need the unadvertised platform namespace gate; Android ignores ios fields with platformNotes paths. Android rendering is deferred, with real blur required at API 31+ and no glass below API 31. scroll.edgeEffect soft|hard|none needs prototype_scroll_edge_effect_v1: iOS 26 native; older iOS shares fadeEdges (24/8 dp defaults, authored lengths win, none disables only the edge effect and its default fallback; explicit fadeEdges still apply). System accessibility settings control glass appearance; assert structure/bounds.
585
+
586
+ Layered style.shadows (1-3 strict {color,radius,spread?,offset?,inner?}, radius 0-1000 dp, spread and offset axes -1000..1000; omitted spread/offset/inner mean 0/{x:0,y:0}/false), style.cornerStyle circular|continuous (default circular), style.border {width,color? ,gradient?} with exactly one of color or gradient, scroll.fadeEdges {top?,bottom?,start?,end?} (0-1000 dp, at least one), and image.filter {saturation?,tint?} (saturation 0-2, at least one) require prototype_style_polish_v2. shadows excludes elevation and shadowColor. Fades appear only where scrolling remains; start/end mirror in RTL. Colour pairs in shadows and tint, and pairs or role names in border gradient stops also require prototype_theme_modes_v1.
587
+
588
+ Rendering order: transform, press scale, size, alpha, drop shadows (shadows/elevation before the clip), clip (cornerRadius + cornerStyle), background, gradient, shimmer, border (color or gradient), inner shadows.
589
+
590
+ ## Light and dark mode
591
+
592
+ Theme design: \`theme.design\` accepts \`material\` or \`platform\` (default). Explicit platform
593
+ requires \`prototype_design_platform_v1\`; omission silently keeps today's drawing on older renderers.
594
+ Android platform look is Material. On iOS button, slider, datePicker and timePicker already use
595
+ native controls; layout/content nodes are neutral and other nodes retain Material drawing.
596
+ Explicit platform requests report \`design.fallback\` notes for those nodes (first 20 paths, plus
597
+ \`total\` beyond); omitted design and material do not. See the contract's \`designCoverage\` table.
598
+
599
+ A shown prototype has exactly one resolved mode, light or dark. Roles, per-mode values and the
600
+ scheme all read that one mode, on Android and on iOS.
601
+
602
+ Authoring, in order of preference:
603
+
604
+ - **Roles.** Use a colour role name instead of a hex value wherever a colour goes. A role already
605
+ has a light and a dark value, so nothing else is needed.
606
+ - **\`{light, dark}\` pairs.** For a brand literal that needs a hand-picked dark value, a colour
607
+ field, gradient stop or scrim takes \`{"light": "#FFFFFF", "dark": "surfaceContainer"}\`: both
608
+ sides required, each a hex value or a role. An image takes \`{"light": assetId, "dark": assetId}\`;
609
+ upload both assets.
610
+ - **Theme maps.** \`theme.colors.<role>\` overrides a role in both modes. \`theme.colors.light\` and
611
+ \`theme.colors.dark\` are \`{<role>: hex}\` maps applied after the flat overrides, for the resolved
612
+ mode only.
613
+ - **Scrims.** In a scrim slot (\`window.placement.scrim\`, a bottomSheet \`scrim\`) only the \`scrim\`
614
+ role is drawn translucent, at 0.4 alpha. Any other role is drawn unchanged, so it covers what is
615
+ behind it.
616
+
617
+ Pairs, theme maps and role names in gradient stops and scrims need a device advertising
618
+ \`prototype_theme_modes_v1\`; a show that uses them on an older device is refused.
619
+
620
+ How the mode is resolved. The first step that applies decides, and is reported as \`source\`:
621
+
622
+ 1. \`theme.mode\` \`light\` or \`dark\` (\`explicit\`).
623
+ 2. The luminance of the flat \`theme.colors.background\` override, else \`theme.colors.surface\`
624
+ (\`roleLuminance\`). The per-mode maps never take part.
625
+ 3. The first opaque hex background on the root's leading chain (\`authoredBackground\`). A role or
626
+ a pair depends on the mode, so it cannot decide it.
627
+ 4. The system setting: the show's \`appearance\` when it is \`light\` or \`dark\` (\`override\`), else
628
+ the device's own setting (\`system\`).
629
+
630
+ \`theme.mode: "system"\` asks for the system setting outright and skips steps 2 and 3.
631
+
632
+ Checking both modes without touching the device: \`show\` takes \`appearance: "device" | "light" |
633
+ "dark"\` (default \`device\`).
634
+
635
+ \`\`\`json
636
+ { "action": "show", "appearance": "dark", "spec": { "id": "demo", "window": { "placement": { "type": "fullscreen" } }, "root": { "type": "text", "text": "Hello" } } }
637
+ \`\`\`
638
+
639
+ - The override replaces the system setting only (step 4). It does not beat an explicit
640
+ \`theme.mode\`, a flat \`background\`/\`surface\` override or an opaque authored background; the
641
+ reported \`source\` says which step won.
642
+ - It belongs to that show: a later show of the same id without \`appearance\` follows the device
643
+ again. The device and the app behind the prototype are not changed.
644
+ - \`light\` and \`dark\` need a device advertising \`prototype_appearance_v1\` and are refused, with
645
+ nothing shown, on one that does not. \`device\` works everywhere.
646
+
647
+ To test a spec: show it with \`appearance: "dark"\`, \`observe\`, then show it again with
648
+ \`appearance: "light"\` and \`observe\`. To put the app behind the prototype into the same mode as
649
+ well, set the device with \`displayConfig\` and restore the \`previous\` value it returns when done.
650
+
651
+ What the device reports (\`prototype_appearance_v1\`; absent on a device without it):
652
+
653
+ - A successful show returns \`lastResult.appearance: {mode, source, deviceDark}\`, and \`inspect\`
654
+ carries the same object on each prototype; after a show crossed by an \`appearance_changed\`
655
+ event, the status entry has no appearance until a later event or \`inspect\` states it.
656
+ \`deviceDark\` is the device's own setting whatever
657
+ decided \`mode\`. Only a device answer states it, and only for the moment it answered: the show
658
+ result and \`inspect\`. \`status\` is host-local, so it reports \`mode\` and \`source\` (kept current by
659
+ \`appearance_changed\` events) and never \`deviceDark\`, because the device can flip while a pinned
660
+ mode stays put and then no event arrives. Call \`inspect\` when you need it now.
661
+ - While a prototype is shown it follows the device live. Any change of the resolved mode (the
662
+ device flipping, or prototype state changing a background the mode is inferred from) sends one
663
+ \`appearance_changed\` event with a null \`name\` and the payload \`{mode, source}\`, and refreshes
664
+ \`status\`. Wait for it with \`awaitEvent\` and \`kind: "appearance_changed"\`. Nothing is sent when
665
+ the mode stays the same, nor for the show itself, whose result already carries the mode.
666
+
667
+ Input and state changes (text input, toggles, sliders etc.) arrive as kind \`emit\` with \`eventName\` \`change\` (there is no separate \`change\` kind), with payload \`{key,value}\` for one changed key or \`{keys,values}\` for several.
668
+ `,Pe=`## Runtime cost
669
+
670
+ What each construct costs on a device, so a large prototype can pick the cheap form. This is
671
+ guidance read from the renderer code, not measurement: the Android renderer (Jetpack Compose) and
672
+ the iOS agent (SwiftUI) were read, and no statement below has been measured on a device. The only
673
+ measurement behind it is epic #12127: a prototype of several hundred nodes on a debug build of the
674
+ Android renderer dropped frames on tab switches, section collapses and page swipes. It predates
675
+ the fixes noted below and gives no per-construct cost. A row marked *being changed* describes
676
+ today's behaviour; the issue it names is open and the row will shrink when it lands.
677
+
678
+ What one state change costs:
679
+
680
+ - **Android.** Every state write publishes one new snapshot. The render tree is mapped once per
681
+ snapshot: every node is visited, hidden ones included, and its \`visibleWhen\`, \`styleWhen\` and
682
+ \`{key}\` text are evaluated against the new state. A node whose result equals the previous one
683
+ is reused as the same object, so only the nodes that changed recompose (#12162). The mapping is
684
+ proportional to the node count; the recomposition to the nodes that changed.
685
+ - **Android, validation.** A state write is validated by re-checking the new state and the nodes
686
+ that bind a key, not by re-encoding and re-validating the whole spec (#12162). The whole spec is
687
+ checked once, on the first write after a \`show\` or a replace. A tap with several
688
+ \`setState\`, \`toggle\` or \`increment\` actions validates each of them in turn before anything
689
+ is applied.
690
+ - **iOS.** Every node view reads the prototype's state, so a state change re-evaluates the body of
691
+ each node on screen and SwiftUI's diffing decides what is redrawn. Each write also re-validates
692
+ the whole tree (every node, in several passes) and re-encodes the state to check the size cap;
693
+ iOS has no equivalent of the Android state-only validation.
694
+ - A tap's action list sends one \`change\` event with the final state, however many keys it wrote.
695
+
696
+ Authoring choices:
697
+
698
+ - **Writes.** Each \`setState\` in a tap is its own validation and its own state snapshot on
699
+ Android. Prefer one key that several nodes read (a selected tab index that \`visibleWhen\` and
700
+ \`styleWhen\` compare against) over several keys set together. Whether snapshots published in
701
+ one frame are mapped separately depends on frame timing; one key avoids the question.
702
+ - **\`styleWhen\` over two \`visibleWhen\` siblings.** On Android a node with \`visibleWhen\` is
703
+ wrapped in an animation container while motion is on; iOS keeps an equivalent container. Two
704
+ siblings with opposite \`visibleWhen\` conditions that cross-fade cost two such wrappers; one
705
+ node whose look changes with \`styleWhen\` costs none, because the style is merged into the
706
+ node's own modifiers.
707
+ - **\`visibleWhen\` transition.** The default transition (no \`transition\`, or \`expand\`) animates
708
+ the node's size, which re-measures its siblings while it runs. Use \`transition: "fade"\`,
709
+ \`"slide"\` or \`"none"\` unless the size change is the point. *Being changed (#12136):* every
710
+ \`box\`, \`row\`, \`column\`, \`flow\` and \`grid\` also animates its own size whenever its children change while
711
+ motion is on, so a size change below a container is animated by that container whatever
712
+ transition the child chose. iOS does the same per container, from a layout signature computed
713
+ from the container's children on each state change.
714
+ - **Hidden subtrees.** A node hidden by \`visibleWhen\` costs nothing at rest on either platform:
715
+ its subtree is not composed (Android) or not built (iOS). It is built in the frame in which it
716
+ becomes visible, so a large subtree behind \`visibleWhen\` can make that one frame slow; keep
717
+ such subtrees small, or split the prototype. *Being changed (#12133):* on Android a hidden
718
+ subtree is disposed when hidden and rebuilt when shown, and a pager page other than the current
719
+ one is not composed until it is swiped to.
720
+ - **\`pressScale\`.** On Android it adds an animated press state and a graphics layer to each
721
+ tappable node that sets it, so put it on buttons, not on every row. On iOS every node with
722
+ \`onTap\`, \`onLongPress\` or \`onDoubleTap\` already has the press gesture, a minimum touch target and a scale effect whether or
723
+ not it sets \`pressScale\`, so \`pressScale\` adds nothing there.
724
+ - **Images.** On Android each \`image\` node is a sub-composition that asks the image cache for a
725
+ bitmap sized to the node, so many small images in a list cost more than one strip image. Decode
726
+ is cached and runs off the main thread. Further per-recomposition work in images and icons is
727
+ *being changed (#12138)*. The iOS image path was not checked.
728
+ - **Accessibility nodes.** On Android a decorative \`icon\` or \`image\` (not tappable,
729
+ no \`testTag\`, no \`contentDescription\`) reports no node, except inside a tappable node with
730
+ no text of its own and no \`contentDescription\`, where icons and images keep their node and label
731
+ because they are the only way to find it. A tappable \`box\`,
732
+ \`row\`, \`column\`, \`flow\`, \`grid\` or \`card\` whose only text is one \`text\` node is one node
733
+ labelled by that text when its descendants have no independent tags, actions, descriptions,
734
+ controls or visibility conditions. Other non-tappable text, icon and image nodes are never labelled
735
+ with their kind; empty tappable ones keep the kind as their only selector handle. An unlabelled,
736
+ non-tappable layout container (\`box\`, \`row\`, \`column\`, \`flow\`, \`grid\`, \`scroll\`, \`pager\`, \`card\`,
737
+ \`divider\`, \`spacer\`) with no text, \`testTag\`, content description or state also reports
738
+ no node. Every text, tagged node and tappable node stays targetable. Give a \`testTag\` only to
739
+ nodes you will look up. Every accessibility node is work whenever \`observe\` or a screen reader
740
+ reads the tree.
741
+ - **Reserved \`testTag\` prefix.** A \`testTag\` cannot start with \`automobile-\`: the host's own dismiss
742
+ controls use \`automobile-prototype-dismiss\`, and a spec that takes one is rejected with the node's path.
743
+ \`tapOn\` keeps its point off those controls, or fails naming one that covers the whole target.
744
+ - **Long lists.** A \`scroll\` node builds all of its children up front on both platforms, so a
745
+ long list inside \`scroll\` pays for every row when it is shown. *Being changed on Android
746
+ (#12139).* \`repeat\` expands to real nodes that count against the node limit.
747
+ - **\`motion: "none"\`.** It removes the visibility animations, size animations and press springs
748
+ (changes become instant) and a hidden \`visibleWhen\` node is simply absent, so it is the right
749
+ choice for automated checks. Motion is also off when Android animator or transition scale is 0. The reduced profile
750
+ keeps fades and color/alpha animation while removing spatial movement and ambient loops.
751
+ - **Layout direction.** Set \`window.layoutDirection\` to \`rtl\` or \`ltr\` on iOS to override the prototype layout without changing the system locale or translating text. Omit it to follow the host. Requires \`prototype_layout_direction_v1\`; Android refuses specs that set it.
752
+ - **Window format.** On Android an opaque fullscreen prototype is still composited as a
753
+ translucent layer, whatever the spec says. *Being changed (#12140).*
754
+ - **Accessibility events.** The Android accessibility service resolves an event's window type once
755
+ per window rather than once per event (#12160), so a prototype that changes a lot no longer
756
+ pays that lookup on every change event. Throttling the events a prototype's own window emits is
757
+ *being changed (#12134)*.
758
+ - **Size.** The node count and the spec bytes are both limited (see Limits) and both drive the
759
+ per-change work above. No tested budget exists yet; for a prototype of several hundred nodes
760
+ every rule above matters, so split it well before it nears the limits.
761
+ `,Ee='## Common validation errors\n\n| Message | Fix |\n| --- | --- |\n| `Unknown prototype icon name` | Look the name up with `automobile:prototype/icons?query=<word>`. |\n| `Expected object` / `Invalid data value` | Data must be an object of nested JSON values without null or nonfinite numbers. |\n| `Data depth limit exceeded` / `Data array limit exceeded` / `Data byte limit exceeded` | Reduce data to the generated limits. |\n| `Invalid pointer value` | Use a nonempty RFC 6901 pointer within the pointer limit. |\n| `Repeat path does not resolve to an array` / `Repeat path resolves to more than 128 items` / `Repeat path item <i> is not an object` | Point to an array of objects within the repeat item limit. |\n| `Duplicate repeat key "x" (items <i> and <j>)` | Give every item a unique rendered identity. |\n| `Bound state key "x" is invalid (repeat item <i>)` | A path-bound key must form a valid state key; error is at repeat.items.path. |\n| `Requires key or data or platform` | Supply one condition source with exactly one comparison. |\n| `Repeat path cell limit exceeded` | Reduce path-bound repeats or the scalar members projected per item; see `MAX_PROTOTYPE_REPEAT_PATH_CELLS`. |\n| `Substituted text total limit exceeded` | Reduce bindable fields, replacement lengths or repeat items; see `MAX_PROTOTYPE_SUBSTITUTED_TEXT_TOTAL`. |\n| `Repeat placeholder in a data condition operand is not supported` | Use a literal operand; state conditions still bind repeat operands. |\n| `Unknown repeat field "x"` | Every item needs the field the placeholder names. |\n| `Missing component prop "x"` / `Unused component prop` | Pass exactly the props the component\'s `{props.*}` placeholders name. |\n| `Expanded node limit exceeded` | Fewer items or a smaller template; ordinary repeats count every instance; lazy list templates count once. |\n| `maxRuns requires everyMs` | Supply everyMs or remove maxRuns. |\n| `Too many distinct timer ids` | Use at most eight distinct startTimer ids, including nested actions and components. |\n| a `stateKey` rejected | The key must be initialised in `state` with the type the node binds (string, boolean, number). |\n| an unknown property | Nodes and actions are strict: only the fields listed above are accepted. |\n\nPaths in errors (for example `root.children[0].text`) point at the offending value in the spec.\n',Oe=`### Patch (#11675)
762
+
763
+ Use \`show\` for a new screen or a window, theme or root change. Use \`patch\` for
764
+ content (\`/data\`), state (\`/state\`) or one node by id (\`replaceNode\`). Android CtrlProxy advertises
765
+ \`prototype_patch_v1\` and applies patches in place; the iOS prototype agent advertises it too
766
+ (a relaunched app loads the current agent), so an older agent still needs the whole spec with \`show\`.
767
+
768
+ \`{action:"patch", id, ops, baseRevision?}\` sends a \`patch_prototype\` frame
769
+ \`{requestId, id, baseRevision?, ops}\` only to a device advertising
770
+ \`prototype_patch_v1\`. Android and iOS use \`prototype_result\` replies. Success
771
+ carries \`revision\`; a state conflict carries
772
+ \`{success:false,error,conflict:{revision,state:{key:scalar}}}\`. Revisions are
773
+ non-negative safe integers; conflict values are strings, booleans or finite
774
+ numbers with valid state keys. Malformed reply fields are dropped. Status and
775
+ inspect entries carry \`revision\` (the newest from a landed patch or a device
776
+ /data write, absent until one arrives) and \`lastAction: "patch"\` once a patch lands.
777
+
778
+ Ops apply in order, completely or not at all. \`add\`, \`replace\`, \`remove\` and
779
+ \`test\` address \`/data\` or \`/data\` followed by an RFC 6901 JSON pointer: \`~0\`
780
+ escapes \`~\`, \`~1\` escapes \`/\`; array indices are canonical non-negative decimal
781
+ integers, and \`-\` appends for \`add\` only. Parent paths must exist. \`replace\`,
782
+ \`remove\` and \`test\` require the addressed value to exist. \`add\`, \`replace\` and
783
+ \`test\` carry \`value\`; \`remove\` does not. State ops address exactly
784
+ \`/state/<key>\`, where keys match \`[A-Za-z_][A-Za-z0-9_]{0,63}\` and values are
785
+ strings, booleans or finite numbers.
786
+
787
+ \`replaceNode\` carries \`{op:"replaceNode",id,node}\`. The id must identify exactly
788
+ one node in the tree, cannot be the root or inside a repeat, and the replacement
789
+ must keep that id. Replacement nodes cannot contain \`use\` nodes or element
790
+ anchors. Inline the component, use a bounds anchor, or use \`show\`.
791
+
792
+ The agent writes data; the user writes state. Data-only patches and patches
793
+ containing only data ops and \`replaceNode\` ignore \`baseRevision\`, until the device has
794
+ written the prototype's data with a data action (\`prototype_data_actions_v1\`): from then on a
795
+ \`/data\` op also needs \`baseRevision\` and conflicts with newer overlapping device writes (see
796
+ Data actions). Any state op, including \`test\`, requires \`baseRevision\`. A device supporting the capability
797
+ must refuse the whole patch with \`conflict\` if the user changed a touched state
798
+ key since that revision, returning the current revision and those keys' values.
799
+
800
+ There are at most ${IE} ops, the frame is capped at ${ka/1048576} MiB, and the resulting whole
801
+ spec must satisfy every spec/data limit, including node count and depth. The
802
+ host tracks a cloned copy of each id's device spec from its last acknowledged show,
803
+ with components expanded and anchors resolved. It applies ops to a copy and
804
+ runs the full validator before sending. Errors preserve deterministic paths.
805
+ The host validates state ops against the agent-declared state and leaves value
806
+ comparison for \`/state\` \`test\` to the device; its copy cannot know user edits.
807
+
808
+ Patches are FIFO per device, with one in flight at a time. A pending show makes
809
+ patch refuse as busy. Shows are not delayed by patches. If a dispatched show
810
+ and an acknowledged patch overlap, their device-side order is unknown: the host
811
+ forgets its spec, warns, and requires a further successful show before patching.
812
+ A failed dispatched show, dismissed event, successful dismiss, session release,
813
+ device removal or unbinding also ends tracking. Inspect retains each reported tracked id
814
+ and drops missing ids; adopted siblings do not end its tracking. A fifth-surface refusal
815
+ with \`replaceOthers: false\` also retains the acknowledged specs. A transport failure after
816
+ patch dispatch is indeterminate and requires a new show. An atomic device
817
+ refusal leaves the tracked copy unchanged. A successful patch refreshes shown
818
+ status data without changing \`lastResult\`, timestamps or event snapshots.
819
+ `,V='## Checks\n\nDevices advertising `prototype_checks_v1` evaluate declarative checks locally. The host refuses\nshow and patch results using this feature on older devices. Inputs `textField`, `slider`,\n`checkbox`, `switch`, `radioGroup`, `datePicker` and `timePicker` accept `checks` (1\u20138 entries)\nand `validateOn: "change" | "blur" | "submit"` (omitted means blur). Each check contains a\nnonempty `message` and exactly one rule. `validateOn` controls error display, never validity.\nThe first failing message is displayed.\n\n- `required: true`: trimmed nonempty text, true for checkbox/switch, nonempty selection for\n radioGroup/datePicker, present number for slider/timePicker.\n- `minLength` / `maxLength`: nonnegative integer UTF-16 lengths, textField only.\n- `min` / `max`: finite numeric inclusive bounds, slider and pickers only. Text fields hold\n strings and have no numeric subtype. Date state remains YYYY-MM-DD; bounds are UTC epoch\n milliseconds. Time pickers retain hourKey/minuteKey; checks apply independently to both keys.\n- `format`: textField only, one of email, phone, digits, decimal, url.\n- `pattern`: textField only, the restricted grammar below, at most 128 UTF-16 units.\n\nEmpty string, null or missing value passes all rules except required. Whitespace is only empty\nfor required. Pattern and format matching use at most 4096 UTF-16 units. Required and length checks inspect the complete value.\nEvery validity reference must name declared state with at least one check. Multiple checked\ncontrols sharing a key must all pass. Numeric/length lower bounds must not exceed upper bounds.\n\nFormats use these exact whole-value expressions (ASCII digits):\n\n- email: `[^\\s@]+@[^\\s@]+\\.[^\\s@]+` (Unicode names allowed).\n- phone: optional leading +, then 7\u201315 characters from ASCII digits, space, hyphen and parentheses;\n at least seven digits. The leading + is outside the character count. Punctuation need not balance.\n- digits: `[0-9]+`.\n- decimal: `-?[0-9]+(\\.[0-9]+)?` (no exponent or leading +).\n- url: `https?://[^\\s/?#]+([/?#]\\S*)?` (lowercase scheme, Unicode hosts allowed).\n\nFormat whitespace follows JavaScript whitespace (including NBSP); no format trims its value.\nPatterns use ASCII `\\d`, `\\w`, `\\s` and their complements `\\D`, `\\W`, `\\S`; `\\w` is\n`[A-Za-z0-9_]` and `\\s` is space, tab, CR, LF, FF, VT. Dot matches one Unicode scalar except\nCR, LF, U+2028 and U+2029. Matching consumes the entire value, even with a final newline.\nAllowed: literals, dot, classes `[abc]` / `[^abc]`, literal ranges, those six shorthand escapes,\nescaped metacharacters, anchors ^ $, groups `(\u2026)` / `(?:\u2026)`, alternation |, and quantifiers\n`? * + {n} {n,} {n,m}`. Every written bound is <=100 and n<=m; open upper bounds are unbounded.\nReject backreferences, lookahead/behind, named groups, lazy/possessive quantifiers, Unicode\nproperties, inline flags, word boundaries `\\b` / `\\B`, and every other escape (including\n`\\n`, `\\x`, `\\u`; use literal characters). Empty classes are rejected. A group repeated more\nthan once cannot contain alternation or any variable quantifier (maximum > minimum), transitively.\nAt most two variable quantifiers are allowed in the entire pattern: ? * + {n,} and {n,m} when\nm>n. Fixed {n} quantifiers inside repeated groups remain allowed.\nThus `(a|b)+`, `(a+)+`, `(a*)*` and `((ab)+)+` are rejected. Optional groups remain allowed.\nThese exact rules and error substrings are pinned in the language-neutral checks fixtures.\n\nConditions `{valid: "email"}` and `{valid: ["email", "password"]}` mean all referenced keys\nare valid, including fields whose errors are not yet shown. They compose with all/any/not in\nvisibleWhen, styleWhen and enabledWhen. enabledWhen is supported on the seven input types plus\nbutton, iconButton, fab, chip and listItem; false disables the control and skips its onTap.\n`emit` may specify `requireValid: ["email", "password"]` (1\u20138 keys). Any failing key aborts the\nwhole action list atomically, shows errors on every listed field, and sends\n`{kind:"validation_failed", keys:[\u2026]}` naming only the failing keys. `change` events carry\n`validity: {key:boolean}` for checked keys. Both event kinds retain the usual event envelope.\n';var wt={device:{tools:["rotate","displayConfig","setPosture","windowMode"],note:"orientation, window size, posture and windowing mode update `/platform/device`; `device.changed` fires only when a size class, posture or idiom changes (on iOS, only width class or idiom), so a rotation that keeps the size class sends no event"},realtime:{tools:[],note:"the channel's server drives frames and status; no device tool"},haptics:{tools:[],note:"assert the call's `prototype_platform` outcome"},sound:{tools:[],note:"assert the call's `prototype_platform` outcome"},clipboard:{tools:["clipboard"],note:"set the clipboard before `paste`/`hasText`; read it after `copy`"},share:{tools:["observe","tapOn"],note:"drive the system share sheet"},battery:{tools:["setDeviceState"],note:"`power.battery` level and charging (Android)"},power:{tools:["setDeviceState"],note:"`power.batterySaver` and `power.thermalStatus` (Android)"},audio:{tools:["mediaSession"],note:"stream volume (Android)"},connectivity:{tools:["setDeviceState"],note:"`connectivity` toggles and `networkCondition`"},biometric:{tools:["biometricAuth","setDeviceState"],note:"match or fail a prompt; `biometrics.enrollment` on an iOS Simulator"},settings:{tools:["observe","pressButton"],note:"confirm the settings screen, then go back"},contacts:{tools:["observe","tapOn"],note:"pick in the system picker"},calendar:{tools:["observe","tapOn"],note:"save or cancel in the system editor"},links:{tools:["observe"],note:"confirm the opened app or browser"},mediaLibrary:{tools:["putAppFile","setAppPermissions"],note:"stage photos in the `media_library` domain; grant or revoke access"},camera:{tools:["setDeviceState","setAppPermissions"],note:"`cameraPoster` sets what the emulated camera sees"},auth:{tools:["observe","tapOn"],note:"complete the provider's sign-in screen"},location:{tools:["setDeviceState","setAppPermissions","systemAlerts"],note:"`location` fix or route; grant access or answer the prompt"},commerce:{tools:["observe","tapOn"],note:"complete the store purchase sheet"},secureStorage:{tools:[],note:"read `/platform/secureStorage/locked`; no device tool"},sensors:{tools:["setSensor","shake"],note:"emulator sensor values and a shake gesture"},map:{tools:["tapOn","swipeOn"],note:"tap markers and pan the map"},webview:{tools:["observe","tapOn"],note:"page content is in the accessibility tree; tap a link to navigate"},audioRecorder:{tools:["injectAudioInput","setAppPermissions"],note:"feed known audio into the microphone"},notifications:{tools:["systemTray","setAppPermissions"],note:"open, act on, reply to or dismiss a posted notification; `notificationsEnabled` blocks the whole app, while a single channel is blocked only in system Settings (`observe`, `tapOn`)"},permissions:{tools:["setAppPermissions","systemAlerts"],note:"grant or revoke before reading `/platform/permissions`; answer the prompt a `request` raises"},media:{tools:["mediaSession"],note:"headset transport keys and session state (Android)"},cast:{tools:[],note:"needs a real receiver on the network; no device tool"}};function Re(e=wt){return["To test a module for real, drive the device with these tools, then assert with `awaitEvent` and `observe`:","","| Module | Tools | What they drive |","| --- | --- | --- |",...Object.entries(e).map(([o,n])=>{let s=n.tools.map((r)=>`\`${r}\``).join(", ")||"-";return`| \`${o}\` | ${s} | ${n.note} |`})].join(`
820
+ `)}var y=W.definitions;function bt(e){let o=e.min!==void 0||e.max!==void 0?` ${e.min??0}..${e.max??"n"}`:"";return`${E(e.item??{kind:"value"})}[]${o}`}function E(e){switch(e.kind){case"enum":return(e.values??[]).join("|");case"ref":return e.name??"ref";case"boundKey":return"key";case"strings":return"strings";case"screenMap":return`screen-name \u2192 node (${e.min}..${e.max} entries)`;case"array":return bt(e);case"choice":return(e.options??[]).map(E).join(" or ");default:return e.kind}}function K(e,a){return`${e}${a.optional?"":"*"}: ${E(a.rule)}`}function kt(e){let a=Object.values(e),[o,...n]=a,s=new Map;for(let[r,i]of Object.entries(o?.fields??{}))if(n.every((p)=>JSON.stringify(p.fields?.[r])===JSON.stringify(i))&&r!=="type")s.set(r,i);return s}function $e(e,a){return Object.entries(e).map(([o,n])=>{let s=Object.entries(n.fields??{}).filter(([r])=>r!=="type"&&!a.has(r)).map(([r,i])=>K(r,i));return`- \`${o}\`${s.length>0?`: ${s.join(", ")}`:""}`})}var g=(e)=>e.map((a)=>`\`${a}\``).join(", ");function P(e,a){return(y[e]?.fields?.[a]?.rule.values??[]).map(String)}function St(){let e=Object.entries(W.limits).map(([n,s])=>`| \`${n}\` | ${s} |`),a=["onTap","onLongPress","onDoubleTap","onAppear"],o=y.repeat.fields?.items.rule??{};for(let n of a){let s=y.node.variants?.box.fields?.[n].rule??{};e.push(`| actions per \`${n}\` | ${s.min}..${s.max} |`)}return e.push(`| \`repeat.items\` | ${o.min}..${o.max} |`),e.push(`| \`use\` nesting (host-expanded components) | ${JO} |`),["| Limit | Value |","| --- | --- |",...e].join(`
821
+ `)}function xt(){let e=y.node.variants??{},a=kt(e);return["## Nodes","","`*` marks a required field. Fields shared by every node:","",[...a].map(([n,s])=>K(n,s)).join(", "),"",...$e(e,a),"",`Pager options require \`${kE}\` whenever any is explicitly present (including default values): \`axis\` is horizontal (default) or vertical; \`pageSpacing\` is the dp gap (default 0); \`peek: {start?, end?}\` leaves neighbouring pages visible along the main axis (default 0 on each side, \`{}\` is a no-op); \`pageSize\` is fill (default) or \`{dp}\` for fixed-size pages. Distances are finite, spacing and peek are 0..${W.limits.MAX_PROTOTYPE_PAGER_DP}, and fixed page size is 1e-6..${W.limits.MAX_PROTOTYPE_PAGER_DP} (\`MAX_PROTOTYPE_PAGER_DP\`). Objects are strict. On iOS dp means points. A vertical pager cannot have any vertical scroll ancestor, including a scroll with omitted axis; a horizontal scroll ancestor is allowed. Every visible page (including partly visible peek and fixed-size pages) is exposed to accessibility, observe and tapOn; Android marks the current page selected only for pagers with positive peek or a fixed pageSize; iOS marks it selected on the options path (a pager with any non-default option) and not on the legacy paged pager; plain pagers expose only the current page on both. Pages fully off-screen are not exposed.`,"Bar scrolling requires `prototype_bar_scroll_v1` when `onScroll` or `scroll` is explicit. `topAppBar.onScroll` defaults to pinned; enterAlways hides and reveals with scroll direction; exitUntilCollapsed collapses medium/large titles to the small height. `bottomNav` and `tabBar` default to none; hide slides away and minimize draws icons only. Set `scroll` to exactly one scroll id in the same screen for an active behavior. Hidden bars leave hit testing and observation; partial bars expose drawn bounds. Reduced motion snaps between states. iOS uses custom Material bars; native platform bars are deferred.","The current page is the first fully visible page (fixed-size pages snap at Start; fill uses the usual settled page). page_changed, {page}/{pageCount}, setPage and tabBar/bottomNav pager bindings keep their meaning. Fully off-screen retained neighbours stay outside the semantics tree; stable keys and neighbour retention are unchanged. Indicator and carousel remain deferred."].join(`
822
+ `)}function Tt(){let e=y.action.variants??{};return["## Actions","","Used in `onTap`, `onLongPress`, `onDoubleTap`, `onAppear` and searchBar `onSubmit` lists.","",`Gesture fields are available only on ${g(Object.entries(y.node.variants??{}).filter(([,a])=>a.fields?.onLongPress!==void 0).map(([a])=>a))}; they require \`prototype_gestures_v1\`.`,"",...$e(e,new Map)].join(`
823
+ `)}function _t(){let e=y.colorRole.values??[];return["## Theme and style","",`Theme: \`{${Object.keys(y.theme.fields??{}).join(", ")}}\`; mode is ${g(P("theme","mode"))}.`,"",`Material colour roles (a colour field takes a hex value or one of these): ${g(e)}.`,"",`Per-mode values, refused unless the device advertises \`${TE}\`: every colour field (style colours, \`border.color\`, gradient stops, \`window.placement.scrim\`, a bottomSheet or drawer \`scrim\`) also takes \`{light, dark}\`, each a hex value or a role, both required; gradient stops and scrims take a role name too. \`theme.colors.light\` and \`theme.colors.dark\` are \`{<role>: hex}\` maps applied after the flat role overrides for the resolved mode. \`image.asset\` and a tabBar or bottomNav item \`image\` take \`{light: assetId, dark: assetId}\`: upload both, and a pair naming two different assets counts as two images. Prefer a role name, which already follows the mode; use a pair for a brand literal that needs a hand-picked dark value.`,"",`Text style roles (\`style.textStyle\`): ${g(P("style","textStyle"))}.`,"",`Corner shapes: ${g(P("themeShapes","corner"))}. Font families: ${g(P("themeTypography","fontFamily"))}.`,"",`Style properties: ${g(Object.keys(y.style.fields??{}))}.`].join(`
824
+ `)}function At(){return["## Icons","",`Icon names are a closed set of ${Ie().length} Material names; variants: ${g(W.definitions.iconVariant.values)}.`,`Look names up with \`${uL}?query=<substring>\` (at most ${kT} matches per read).`].join(`
825
+ `)}function Pt(){return["## Embedding in an app","",`Read \`${Ey}?id=<id>\` (optional \`platform=android|ios\`, \`deviceId\`) for a shown or saved prototype as app code: the spec, its assets, a Jetpack Compose or SwiftUI host file that renders it with the published renderer library, the library coordinate to add, and the capabilities the spec needs. Asset bytes for saved prototypes come from \`${uS}?id=<id>&assetId=<asset>\`.`,"","For one file to paste instead, read `automobile:prototype/<id>/compose` (Kotlin) or `automobile:prototype/<id>/swiftui` (Swift) for a shown prototype: the same host file with the spec inlined as a string constant, a header naming the library coordinate, required capabilities and each asset's URI (assets are not inlined), and a preview sized to `frame` (default `pixel-9` / `iphone-16`). `revision` picks a kept revision as for the web page.","",`To graduate a prototype to screens the app owns, read \`${pS}?id=<id>\` (same parameters). It returns a one-shot Compose or SwiftUI file with no renderer dependency: layout, text, buttons, text fields, lists and stack navigation become real code, every other node becomes a \`TODO(eject)\` stub that keeps its \`testTag\` and children, and \`remaining\` lists each node, action and binding left to do. It is not round-trip.`].join(`
826
+ `)}function Ot(){return["## Web preview and alternatives","",`Read \`automobile:prototype/<id>/web\` for a shown prototype as one self-contained HTML page: the prototype in a device frame with rotate, zoom, a variant switcher and its revision history. Optional \`frame\` (a device frame id; default the showing platform's), \`revision\` (default the latest) and \`variant\`. Revision 1 is the first acknowledged show; each later show of the id and each landed patch adds one; device state edits do not. The last ${jG} revisions per id are kept. Subscribe to the URI without parameters to hear every new revision. The \`exportPrototypeWeb\` tool writes the same page to an HTML file and returns its path and sha256.`,"","To present design alternatives side by side, show each under its own id with `variant: {group, label}` using one `group` (host-side only; the device shows whatever was shown last). The web page of any id in the group offers every alternative by `label`. Describe them in chat and ask the user which they prefer."].join(`
827
+ `)}function Rt(){let e=y.placement.variants??{};return["## Window","",`Placement types: ${g(Object.keys(e))}. Window fields: ${Object.entries(y.window.fields??{}).map(([a,o])=>K(a,o)).join(", ")}.`].join(`
828
+ `)}function Ie(){return W.definitions.iconName.values}function Ne(e){let a=e.trim().toLowerCase();if(a==="")return{total:0,names:[]};let o=Ie().filter((n)=>n.includes(a));return{total:o.length,names:o.slice(0,kT)}}function $t(e,a){return`${e}${a.optional?"?":""}: ${E(a.value)}`}function It(e=Yu){return["| Module | Capabilities | Calls | Events | `/platform` state |","| --- | --- | --- | --- | --- |",...Object.entries(e).filter(([,o])=>o.placeholder!==!0).map(([o,n])=>{let s=Object.entries(n.calls).map(([p,u])=>{let w=Object.entries(u.args).map(([v,A])=>$t(v,A)),b=[...u.permissions.map((v)=>`permission ${v}`),...u.background.map((v)=>`background ${v}`),...u.capability?[`capability \`${u.capability}\``]:[]];return`\`${o}.${p}\`(${w.join(", ")})${b.length>0?` needs ${b.join(", ")}`:""}`}),r=Object.keys(n.events).map((p)=>`\`${o}.${p}\``),i=Object.keys(n.state??{}).map((p)=>`\`/${o}/${Nt(o,p)}${p}\``),l=[n.capability,n.deviceCapability,...Object.values(n.backends??{}).map((p)=>p.capability)].filter(Boolean);return`| \`${o}\` | ${g(l)} | ${s.join("<br>")||"-"} | ${r.join(", ")||"-"} | ${i.join(", ")||"-"} |`})].join(`
829
+ `)}function Nt(e,a){if(e==="media")return"players/<id>/";return e==="notifications"&&IK.has(a)?"channels/<id>/":""}function jt(){return["## Platform differences","",`iOS draws SF Symbols: ${Object.keys(WV.mapped).length} of ${W.definitions.iconName.values.length} contract names are mapped in schemas/prototype-ios-icon-symbols.json. Unmapped names draw a placeholder symbol. Agents advertise prototype_icon_sf_symbols_v1; older agents should be relaunched with a current agent.`,'With no variant, iOS draws the name\'s default SF Symbol, which is often the outline form; pass `variant: "filled"` on an `icon` node for the closest match to Android where the table has a filled form.',"","- `icon.name`: unmapped names draw a placeholder; a successful show lists affected paths under `platformNotes`. Material parity remains a future option behind `prototype_icon_set_material_v1`.","- `icon.variant`: filled/outlined map where a form exists; rounded/sharp/twoTone and forms absent from the table draw the default symbol. A successful show lists affected paths under `platformNotes`. Material parity remains a future option behind `prototype_icon_variant_v1`.",...J5.filter(({construct:e})=>!e.startsWith("icon.")).map(({construct:e,platform:a,effect:o,capability:n,detail:s})=>`- \`${e}\`: ${a}, ${o}. ${s} A successful show lists affected paths under \`platformNotes\`${n?`; lifted by \`${n}\``:""}.`),...Y5.map(({construct:e,detail:a})=>`- \`${e}\`: ${a}`)].join(`
830
+ `)}function je(){return[we,le,H,`## Limits
831
+
832
+ Generated from \`schemas/prototype-spec-contract.json\`.
833
+
834
+ `+St(),Rt(),xt(),Tt(),ve,be,Te,Y,Se,de,pe,ce,xe,ke,V,Oe,_e,_t(),Ae,V,ie,At(),jt(),Pt(),Ot(),`${ue}
835
+
836
+ ${It()}
837
+
838
+ ${Re()}
839
+
840
+ ${me}
841
+
842
+ ${ye}
843
+
844
+ ${fe}`,Pe,Ee].join(`
845
+
846
+ `)}import{createHash as qt}from"crypto";function h(e,a){return`"${Ce(e,a).join("")}"`}function Ce(e,a){return[...e.toWellFormed()].map((o)=>{if(o==='"'||o==="\\"||o==="$"&&a==="kotlin")return`\\${o}`;let n=o.codePointAt(0)??0;if(n>=32&&n<127)return o;if(a==="swift")return`\\u{${n.toString(16)}}`;return[...Array.from({length:o.length},(s,r)=>o.charCodeAt(r))].map((s)=>`\\u${s.toString(16).padStart(4,"0")}`).join("")})}function O(e){return h(e,"kotlin")}function x(e){return h(e,"swift")}var Dt=16384;function Mt(e){let a=[],o="";for(let n of Ce(e,"kotlin")){if(o.length+n.length>Dt)a.push(`"${o}"`),o="";o+=n}return[...a,`"${o}"`]}function Ct(e){let a=Mt(e);if(a.length===1)return`private const val SPEC = ${a[0]}`;return`private val SPEC =
847
+ listOf(
848
+ ${a.map((o)=>` ${o},`).join(`
849
+ `)}
850
+ )
851
+ .joinToString("")`}function Ue(e){return e?e.header.map((a)=>a?`// ${a}`:"//").join(`
852
+ `)+`
853
+
854
+ `:""}function Le(e){return e.slice(e.lastIndexOf("/")+1)}function R(e){return e.kind==="font"}function De(e){if(e.length===0)return" emptyMap()";return`
855
+ listOfNotNull(
856
+ ${e.map((o)=>` read(${O(`$DIRECTORY/${Le(o.path)}`).replace("\\$DIRECTORY","$DIRECTORY")})?.let { ${O(o.id)} to it },`).join(`
857
+ `)}
858
+ )
859
+ .toMap()`}function I(e){let a=e.assets.filter((r)=>!R(r)),o=e.assets.filter(R),n=e.storage?"remember(context) { PrototypeStorage.fileBacked(context) }":"PrototypeStorage.None",{inline:s}=e;return`${Ue(s)}package prototypes
860
+
861
+ import android.content.Context
862
+ import androidx.compose.runtime.Composable
863
+ import androidx.compose.runtime.remember
864
+ import androidx.compose.ui.Modifier
865
+ import androidx.compose.ui.platform.LocalContext
866
+ ${s?`import androidx.compose.ui.tooling.preview.Preview
867
+ `:""}import dev.jasonpearson.automobile.protocol.PrototypeEvent
868
+ import dev.jasonpearson.automobile.prototype.AutoMobilePrototype
869
+ import dev.jasonpearson.automobile.prototype.PrototypeAssets
870
+ import dev.jasonpearson.automobile.prototype.PrototypeDismissReason
871
+ import dev.jasonpearson.automobile.prototype.PrototypeNetwork
872
+ import dev.jasonpearson.automobile.prototype.PrototypeStorage
873
+
874
+ /** The app assets directory holding this prototype's spec.json and asset files. */
875
+ private const val DIRECTORY = ${O(`prototypes/${e.directory}`)}
876
+ ${s?`
877
+ /** The prototype spec JSON, compiled in. */
878
+ ${Ct(s.spec)}
879
+ `:""}
880
+ /**
881
+ * The ${e.directory} prototype, drawn by the AutoMobile prototype renderer. Generated by
882
+ * ${s?`${s.generatedBy}: the spec is inlined above and the assets live under
883
+ * assets/prototypes/${e.directory}.`:`automobile:prototype/native: the spec and assets live under assets/prototypes/${e.directory}.`}
884
+ */
885
+ @Composable
886
+ fun ${e.typeName}(
887
+ modifier: Modifier = Modifier,
888
+ network: PrototypeNetwork = remember { PrototypeNetwork() },
889
+ onEvent: (PrototypeEvent) -> Unit = {},
890
+ onDismiss: (PrototypeDismissReason) -> Unit = {},
891
+ ) {
892
+ val context = LocalContext.current
893
+ val read = { path: String -> context.readAsset(path) }
894
+ ${s?" val spec = SPEC":` // An empty spec (missing file) shows the renderer's load-error view instead of crashing.
895
+ val spec = remember(context) { read("$DIRECTORY/spec.json")?.decodeToString().orEmpty() }`}
896
+ val assets =
897
+ remember(context) {
898
+ PrototypeAssets(
899
+ images =${De(a)},
900
+ fonts =${De(o)},
901
+ )
902
+ }
903
+ AutoMobilePrototype(
904
+ spec = spec,
905
+ modifier = modifier,
906
+ assets = assets,
907
+ storage = ${n},
908
+ network = network,
909
+ onEvent = onEvent,
910
+ onDismiss = onDismiss,
911
+ )
912
+ }
913
+
914
+ /** A bundled file's bytes, or null when the app does not ship it. */
915
+ private fun Context.readAsset(path: String): ByteArray? = runCatching {
916
+ assets.open(path).use { it.readBytes() }
917
+ }
918
+ .getOrNull()
919
+ ${s?Ut(e.typeName,s):""}`}function Ut(e,a){let{name:o,widthDp:n,heightDp:s}=a.preview;return`
920
+ @Preview(name = ${O(o)}, widthDp = ${Math.round(n)}, heightDp = ${Math.round(s)})
921
+ @Composable
922
+ private fun ${e}Preview() {
923
+ ${e}()
924
+ }
925
+ `}function Me(e){if(e.length===0)return"[:]";return`[${e.map((o)=>`${x(o.id)}: resource(${x(Le(o.path))})`).join(", ")}]`}function N(e){let a=e.assets.filter((s)=>!R(s)),o=e.assets.filter(R),{inline:n}=e;return`${Ue(n)}import AutoMobilePrototype
926
+ import Foundation
927
+ import SwiftUI
928
+
929
+ /// The ${e.directory} prototype, drawn by the AutoMobile prototype renderer. Generated by
930
+ ${n?`/// ${n.generatedBy}: the spec is inlined below and the assets live in
931
+ /// Prototypes/${e.directory}.`:`/// automobile:prototype/native: the spec and assets live in Prototypes/${e.directory}.`}
932
+ struct ${e.typeName}: View {
933
+ var network: PrototypeNetworkConfiguration = .default
934
+ var onEvent: @MainActor (PrototypeEvent) -> Void = { _ in }
935
+ var onDismiss: @MainActor (PrototypeDismissReason) -> Void = { _ in }
936
+
937
+ // Read once per process, not on every body evaluation.
938
+ private static let spec = ${n?`Data(${x(n.spec)}.utf8)`:'resource("spec.json")'}
939
+ private static let assets = PrototypeAssets(images: ${Me(a)}, fonts: ${Me(o)})
940
+
941
+ var body: some View {
942
+ AutoMobilePrototypeView(
943
+ spec: Self.spec,
944
+ assets: Self.assets,
945
+ storage: ${e.storage?".fileBacked":".none"},
946
+ network: network,
947
+ onEvent: onEvent,
948
+ onDismiss: onDismiss
949
+ )
950
+ }
951
+
952
+ /// Bundled file bytes; a missing spec shows the renderer's load-error view instead of a blank screen.
953
+ private static func resource(_ name: String) -> Data {
954
+ let url = Bundle.main.bundleURL
955
+ .appendingPathComponent(${x(`Prototypes/${e.directory}`)})
956
+ .appendingPathComponent(name)
957
+ return (try? Data(contentsOf: url)) ?? Data()
958
+ }
959
+ }
960
+ ${n?Lt(e.typeName,n):""}`}function Lt(e,a){let{name:o,widthDp:n,heightDp:s}=a.preview;return`
961
+ #Preview(${x(o)}) {
962
+ ${e}()
963
+ .frame(width: ${n}, height: ${s})
964
+ }
965
+ `}var Ft="dev.jasonpearson.auto-mobile:auto-mobile-prototype-compose",Gt="https://github.com/kaeawc/auto-mobile",zt="AutoMobilePrototype",Bt={"image/png":"png","image/jpeg":"jpg","image/webp":"webp","font/ttf":"ttf","font/otf":"otf","application/vnd.automobile.agsl":"agsl"};function J(e,a,o=Ey){if(e===void 0||e.length===0)throw new t(`${o} needs ${a}.`);return e}function Wt(e){if(e===void 0||e==="android"||e==="ios")return e;throw new t(`Unsupported platform "${e}"; use android or ios.`)}function qe(e){return qt("sha256").update(e,"utf8").digest("hex").slice(0,8)}function Yt(e){return e.replace(/[^A-Za-z0-9_-]/g,"_")}var j=64;function D(e){let a=Yt(e);if(a===e&&a.length<=j)return a;return`${a.slice(0,j-9)}-${qe(e)}`}function T(e){return D(e)}function _(e){let o=e.split(/[^A-Za-z0-9]+/).filter((s)=>s.length>0).map((s)=>s[0].toUpperCase()+s.slice(1)).join(""),n=/^[A-Za-z]/.test(o)?o:`P${o}`;if(/^[A-Za-z][A-Za-z0-9]*$/.test(e)&&n.length<=j)return`${n}Prototype`;return`${n.slice(0,j-8)}Prototype${qe(e)}`}function X(e){let{images:a,fonts:o}=Ab(e);return[...[...a].sort().map((n)=>({id:n,kind:"image"})),...[...o].sort().map((n)=>({id:n,kind:"font"}))]}async function Fe(e,a,o){return(await e.savedPrototypes(a)).find((s)=>s.id===o)}function Ge(e,a){let o=e.devicesShowing(a);if(o.length===1)return o[0];if(o.length===0)throw new t(`No device is showing prototype "${a}". Show it first, or pass deviceId for a saved prototype.`);throw new t(`Prototype "${a}" is shown on ${o.length} devices (${o.join(", ")}); pass deviceId.`)}async function Ht(e,a,o){let n=o??Ge(e,a),s=await Fe(e,n,a),r=e.shownSpec(n,a);if(r.kind==="known")return{deviceId:n,spec:r.spec,saved:s};if(r.kind==="busy")throw new t(`A prototype show is in flight on ${n}; read ${Ey} again once it finishes.`);if(s)return{deviceId:n,spec:s.spec,saved:s};throw new t(`Unknown prototype "${a}" on ${n}: it is neither shown nor saved there.`)}async function Vt(e,a,o){let n=a??await e.platformOf(o.deviceId)??(o.saved?"android":void 0);if(!n)throw new t(`Cannot tell the platform of ${o.deviceId}; pass platform=android or platform=ios.`);return n}function M(e,a,o){return e==="android"?`app/src/main/assets/prototypes/${a}/${o}`:`Prototypes/${a}/${o}`}function Kt(e,a,o){let n=new URLSearchParams({id:e,assetId:a,deviceId:o});return`${uS}?${n.toString()}`}function Jt(e,a,o){let n=T(a),s=new Map((o.saved?.assets??[]).map((r)=>[r.id,r]));return X(o.spec).map(({id:r,kind:i})=>{let l=s.get(r),p=l?.mimeType?`.${Bt[l.mimeType]}`:"";return{id:r,kind:i,...l?.mimeType?{mimeType:l.mimeType}:{},path:M(e,n,`${D(r)}${p}`),uri:Kt(a,r,o.deviceId),available:l?.bytes!==void 0}})}function Z(e,a){return e==="android"?{gradle:`${Ft}:${a}`}:{swiftPackage:{url:Gt,from:a,product:zt}}}function Xt(e,a,o){let n=[];if(a.includes("prototype_network_v1"))n.push(e==="android"?'`request` actions fail until the app passes PrototypeNetwork(device = ...); host-routed requests (omitted or via:"host") then use it unless a hostHandler takes them over.':'`request` actions use the renderer\'s URLSession client by default; pass PrototypeNetworkConfiguration(hostHandler:) to take over host-routed requests (omitted or via:"host").');let s=o.filter((r)=>!r.available).map((r)=>r.id);if(s.length>0)n.push(`The host has no bytes for asset(s) ${s.join(", ")} (only saved prototypes keep them); add those files yourself.`);if(e==="ios")n.push("Add the Prototypes folder to the app target as a folder reference so Bundle.main finds files by subdirectory.");return n}function Zt(e,a,o,n){let s=T(a),r=_(a),i={id:a,directory:s,typeName:r,assets:n,storage:o.storage!==void 0},l={path:M(e,s,"spec.json"),content:JSON.stringify(o,null,2)};return e==="android"?[l,{path:`app/src/main/java/prototypes/${r}.kt`,content:I(i)}]:[l,{path:`${r}.swift`,content:N(i)}]}function Q(e){let a=new Map;for(let o of e){let n=`${o.kind}:${o.id.normalize("NFC")}`,s=a.get(n);if(s!==void 0)throw new t(`Asset ids ${JSON.stringify(s)} and ${JSON.stringify(o.id)} are the same string in Swift (canonically equivalent); rename one before exporting for iOS.`);a.set(n,o.id)}}function ee(e,a){if(dm(e).includes("prototype_custom_shaders_v1"))throw new t(a==="ios"?"Custom AGSL shaders are Android only; remove style.shader before exporting for iOS.":"Native prototype export does not support style.shader yet; the Compose renderer has no custom shader runtime. Remove style.shader before exporting.")}async function te(e,a,o=Ey){let n=J(e.id,"id",o),s=Wt(e.platform),r=await Ht(a,n,e.deviceId),i=await Vt(a,s,r);return{id:n,platform:i,resolved:r}}async function ze(e,a){let{id:o,platform:n,resolved:s}=await te(e,a);ee(s.spec,n);let r=a.libraryVersion(),i=dm(s.spec),l=Jt(n,o,s);if(n==="ios")Q(l);return{id:o,platform:n,library:Z(n,r),requiredCapabilities:i,minimumLibraryVersion:r,files:Zt(n,o,s.spec,l),assets:l,notes:Xt(n,i,l)}}async function Be(e,a){let o=J(e.id,"id"),n=J(e.assetId,"assetId"),s=e.deviceId??Ge(a,o),r=(await Fe(a,s,o))?.assets.find((i)=>i.id===n);if(!r?.bytes||!r.mimeType)throw new t(`No bytes for asset "${n}" of prototype "${o}" on ${s}: the host keeps asset bytes only for saved prototypes. Supply the file yourself.`);return{mimeType:r.mimeType,bytes:r.bytes}}var Ye=W.definitions;function Qt(e,a){let o=c(e);return o?.kind==="ref"&&o.name===a}function ea(e,a,o){let n=c(a);return n?Object.entries(c(e)??{}).flatMap(([s,r])=>(s in n)?k(c(r)?.rule,n[s],`${o}.${s}`):[]):[]}function ta(e,a,o){if(!Array.isArray(a))return[];return Qt(e,"action")?[{path:o,actions:a}]:a.flatMap((n,s)=>k(e,n,`${o}[${s}]`))}var We={ref:(e,a,o)=>e.name==="node"||e.name==="action"?[]:k(Ye[String(e.name)],a,o),array:(e,a,o)=>ta(e.item,a,o),map:(e,a,o)=>Object.entries(c(a)??{}).flatMap(([n,s])=>k(e.item,s,`${o}[${JSON.stringify(n)}]`)),object:(e,a,o)=>ea(e.fields,a,o),tagged:(e,a,o)=>k(c(e.variants)?.[String(c(a)?.type)],a,o),choice:(e,a,o)=>Array.isArray(e.options)?e.options.flatMap((n)=>k(n,a,o)):[]};function k(e,a,o){let n=c(e),s=n?.kind,r=typeof s==="string"&&Object.hasOwn(We,s)?We[s]:void 0;return n&&r?r(n,a,o):[]}function He(e,a){let o=k(Ye.node,e,a).filter((n)=>n.path!==`${a}.onTap`);return[...new Map(o.map((n)=>[n.path,n])).values()]}var Ve={box:{compose:"Box",swiftUi:"ZStack"},row:{compose:"Row",swiftUi:"HStack"},column:{compose:"Column",swiftUi:"VStack"},flow:"stub",grid:"stub",carousel:"stub",text:{compose:"Text",swiftUi:"Text"},image:"stub",icon:"stub",spacer:{compose:"Spacer",swiftUi:"Spacer"},textField:{compose:"TextField / OutlinedTextField",swiftUi:"TextField / SecureField"},searchBar:"stub",switch:"stub",checkbox:"stub",button:{compose:"Button (by variant)",swiftUi:"Button"},pasteButton:"stub",radioGroup:"stub",select:"stub",listItem:{compose:"ListItem",swiftUi:"HStack"},slider:"stub",chip:"stub",card:{compose:"Card",swiftUi:"VStack"},iconButton:"stub",fab:"stub",segmentedButton:"stub",topAppBar:"stub",divider:{compose:"HorizontalDivider / VerticalDivider",swiftUi:"Divider"},badge:"stub",progress:"stub",chart:"stub",dialog:"stub",menu:"stub",snackbar:"stub",timePicker:"stub",datePicker:"stub",list:{compose:"LazyColumn / LazyRow",swiftUi:"ScrollView + LazyVStack / LazyHStack"},scroll:{compose:"Column / Row + scroll modifier",swiftUi:"ScrollView"},pager:"stub",stack:{compose:"when over a back stack",swiftUi:"switch over a back stack"},tabBar:"stub",bottomNav:"stub",bottomSheet:"stub",tooltip:"stub",adaptive:"stub",drawer:"stub",videoPlayer:"stub",map:"stub",castButton:"stub",cameraPreview:"stub",webView:"stub"};function Ke(e){return typeof e==="string"&&Object.hasOwn(Ve,e)&&Ve[e]!=="stub"}var aa=/\{[^{}]+\}/,oa=new Set(["push","replace"]),na=new Set(["pop","popToRoot"]);function sa(e,a){let o=String(e?.type);if(typeof e?.stack!=="string")return;let n=h(e.stack,a);if(oa.has(o)&&typeof e.screen==="string")return`ui.${o}(${n}, ${h(e.screen,a)})`;return na.has(o)?`ui.${o}(${n})`:void 0}class ae{writer;node;path;index;constructor(e,a,o,n){this.writer=e;this.node=a;this.path=o;this.index=n}get language(){return this.writer.dialect.language}string(e){let a=this.node[e];return typeof a==="string"?a:void 0}text(e){let a=this.index===void 0?e:e.replaceAll("{index}","");if(aa.test(a))this.writer.todos.push({path:this.path,kind:"binding",type:e});if(this.index===void 0||!e.includes("{index}"))return h(e,this.language);let o=this.writer.dialect.indexed(this.index),[n,...s]=e.split("{index}");return s.reduce((r,i)=>i.length>0?`${r} + ${o} + ${h(i,this.language)}`:`${r} + ${o}`,h(n,this.language))}testTag(){let e=this.string("testTag");return e===void 0?void 0:this.text(e)}actions(e){let a=this.node[e];if(!Array.isArray(a))return[];return a.map((o,n)=>this.action(c(o),`${this.path}.${e}[${n}]`))}action(e,a){let o=sa(e,this.language);if(o!==void 0)return o;let n=String(e?.type);return this.writer.todos.push({path:a,kind:"action",type:n}),`/* TODO(eject): ${n.replace(/[^A-Za-z0-9_]/g,"")} action */`}children(e="children"){let a=this.node[e];if(Array.isArray(a))return a.flatMap((o,n)=>this.writer.emit(o,`${this.path}.${e}[${n}]`,this.index));return this.writer.emit(a,`${this.path}.${e}`,this.index)}row(e){let a=this.node.children,o=Array.isArray(a)?a[0]:void 0;return this.writer.emit(o,`${this.path}.children[0]`,e)}rowCount(){let e=c(this.node.repeat),a=c(e?.items)?.path,o=typeof a==="string"?Et(this.writer.data,a):[];return this.writer.todos.push({path:`${this.path}.repeat`,kind:"binding",type:String(a)}),Array.isArray(o)?o.length:0}stackScreens(){let e=this.string("id")??"",a=c(this.node.screens)??{},o=this.string("start")??Object.keys(a)[0]??"";return this.writer.stackStarts.set(e,o),Object.entries(a).map(([n,s])=>[n,this.writer.screen(e,n,s,`${this.path}.screens[${JSON.stringify(n)}]`)])}literal(e){return h(e,this.language)}}var ra=" ";function ia(e){return e.map((a)=>a.length>0?`${ra}${a}`:a)}function U(e){let a=(o)=>o.startsWith("/*");return[e.filter((o)=>!a(o)).join("; "),...e.filter(a)].filter((o)=>o.length>0).join(" ")}function d(e,a,o="}"){return[e,...ia(a),o]}function C(e){let o=e.split(/[^A-Za-z0-9]+/).filter((n)=>n.length>0).map((n)=>n[0].toUpperCase()+n.slice(1)).join("");return/^[A-Za-z]/.test(o)?o:`S${o}`}class oe{dialect;data;prefix;todos=[];screens=[];stackStarts=new Map;scaffolded=new Set;screenNames=new Set;constructor(e,a,o){this.dialect=e;this.data=a;this.prefix=o}emit(e,a,o){let n=c(e);if(!n)return[];let s=new ae(this,n,a,o),r=n.type;if(Ke(r)){this.scaffolded.add(r);let l=this.dialect.emitters[r](n,s);return[...this.untranslated(n,a),...l]}this.todos.push({path:a,kind:"node",type:String(r)});let i=cr(n).flatMap(({value:l,suffix:p})=>this.emit(l,`${a}${p}`,o));return[...this.untranslated(n,a),...this.dialect.todoStub(n,s,i)]}untranslated(e,a){return[...this.untranslatedActions(e,a),...this.untranslatedRepeat(e,a)]}untranslatedActions(e,a){let o=He(e,a);this.todos.push(...o.flatMap((s)=>s.actions.map((r,i)=>({path:`${s.path}[${i}]`,kind:"action",type:String(c(r)?.type)}))));let n=o.map((s)=>s.path.slice(a.length+1));return n.length===0?[]:[`// TODO(eject): ${n.join(", ")} actions at ${a} are not translated.`]}untranslatedRepeat(e,a){let o=c(e.repeat);if(!o||e.type==="list")return[];let n=c(o.items)?.path;return this.todos.push({path:`${a}.repeat`,kind:"binding",type:typeof n==="string"?n:"repeat"}),[`// TODO(eject): repeat at ${a} is not translated; its children are written once.`]}screen(e,a,o,n){let s=`${this.prefix}${C(e)}${C(a)}Screen`;for(let i=2;this.screenNames.has(s);i+=1)s=`${this.prefix}${C(e)}${C(a)}Screen${i}`;this.screenNames.add(s);let r=this.screens.length;return this.screens.push({name:s,body:[]}),this.screens[r]={name:s,body:this.emit(o,n)},s}}function f(e,a="",o=!0){let n=["Modifier",a],s=e.testTag();if(s!==void 0)n.push(`.testTag(${s})`);let r=o?e.actions("onTap"):[];if(r.length>0)n.push(`.clickable { ${U(r)} }`);return n.join("")}function L(e,a=""){return(o,n)=>d(`${e}(modifier = ${f(n)}${a}) {`,n.children())}function da(e){return(Array.isArray(e.spans)?e.spans:[]).map((o)=>typeof o==="object"&&o!==null&&("text"in o)?String(o.text):"").join("")}function la(e,a){return a.text(a.string("text")??da(e))}var ca={filled:"Button",tonal:"FilledTonalButton",elevated:"ElevatedButton",outlined:"OutlinedButton",text:"TextButton"},pa=(e,a)=>[`${ca[a.string("variant")??"filled"]??"Button"}(`,...d(" onClick = {",a.actions("onTap").map((o)=>` ${o}`)," },"),` modifier = ${f(a,"",!1)},`,...d(") {",[`Text(text = ${a.text(a.string("label")??"")})`])];function q(e,a,o){let n=e.string(o);return n===void 0?[]:[` ${a} = { Text(text = ${e.text(n)}) },`]}var ha=(e,a)=>{let o=a.text(a.string("stateKey")??""),n=e.secure===!0||e.keyboardType==="password";return[`${e.variant==="filled"?"TextField":"OutlinedTextField"}(`,` value = ui.text(${o}),`,` onValueChange = { ui.setText(${o}, it) },`,` modifier = ${f(a)},`,...q(a,"label","label"),...q(a,"placeholder","placeholder"),...q(a,"supportingText","supportingText"),...e.multiline===!0?[]:[" singleLine = true,"],...n?[" visualTransformation = PasswordVisualTransformation(),"]:[],")"]},ua=(e,a)=>{let o=e.axis==="horizontal"?"LazyRow":"LazyColumn",n=a.children("header"),s=a.children("footer");return d(`${o}(modifier = ${f(a)}) {`,[...n.length>0?d("item {",n):[],...d(`items(${a.rowCount()}) { index ->`,a.row("index")),...s.length>0?d("item {",s):[]])},ma=(e,a)=>["ListItem(",` headlineContent = { Text(text = ${a.text(a.string("headline")??"")}) },`,...q(a,"supportingContent","supporting"),` modifier = ${f(a)},`,")"],ya=(e,a)=>{let o=a.literal(a.string("id")??""),n=a.stackScreens().map(([s,r])=>`${a.literal(s)} -> ${r}(ui)`);return d(`Box(modifier = ${f(a)}) {`,d(`when (ui.top(${o})) {`,n))},fa=(e,a)=>{let o=e.axis==="horizontal";return d(`${o?"Row":"Column"}(modifier = ${f(a,`${o?".horizontalScroll":".verticalScroll"}(rememberScrollState())`)}) {`,a.children("child"))},Je={language:"kotlin",emitters:{box:L("Box"),row:L("Row",", verticalAlignment = Alignment.CenterVertically"),column:L("Column"),card:L("Card"),scroll:fa,spacer:(e,a)=>[`Spacer(modifier = ${f(a,".size(8.dp)")})`],divider:(e,a)=>[`${e.orientation==="vertical"?"VerticalDivider":"HorizontalDivider"}(modifier = ${f(a)})`],text:(e,a)=>[`Text(text = ${la(e,a)}, modifier = ${f(a)})`],button:pa,textField:ha,list:ua,listItem:ma,stack:ya},todoStub:(e,a,o)=>[`// TODO(eject): ${String(e.type)} at ${a.path} has no scaffold mapping; build it by hand.`,...d(`Column(modifier = ${f(a)}) {`,[`Text(text = ${a.literal(`TODO: ${String(e.type)}`)})`,...o])],indexed:(e)=>e},Xe=["androidx.compose.foundation.*","androidx.compose.foundation.layout.*","androidx.compose.foundation.lazy.*","androidx.compose.material3.*","androidx.compose.runtime.*","androidx.compose.ui.Alignment","androidx.compose.ui.Modifier","androidx.compose.ui.graphics.Color","androidx.compose.ui.platform.testTag","androidx.compose.ui.text.input.PasswordVisualTransformation","androidx.compose.ui.unit.dp"];var at=Object.keys(W.definitions.themeModeColors.fields);function Ze(...e){let a=new Map;for(let o of e){let n=c(o)??{};for(let s of at){let r=n[s];if(typeof r==="string"&&cm(r))a.set(s,r)}}return a}function ot(e){let a=c(e),o=c(a?.colors),n=a?.mode==="light"||a?.mode==="dark"?a.mode:void 0;return{...n?{mode:n}:{},light:Ze(o,o?.light),dark:Ze(o,o?.dark),seeded:o?.seed!==void 0||o?.source!==void 0}}function nt(e){return cm(e)??{r:0,g:0,b:0,a:1}}function F(e){return Math.round(e).toString(16).padStart(2,"0").toUpperCase()}function ga(e){let{r:a,g:o,b:n,a:s}=nt(e);return`Color(0x${F(s*255)}${F(a)}${F(o)}${F(n)})`}function et(e,a,o){let n=[...o].map(([s,r])=>` ${s} = ${ga(r)},`);return n.length===0?[`private val ${e} = ${a}()`]:[`private val ${e} = ${a}(`,...n,")"]}function va(e){if(e==="dark")return"true";return e==="light"?"false":"isSystemInDarkTheme()"}function st(e,a){return[...["@Composable",`fun ${a}(content: @Composable () -> Unit) {`,` val dark = ${va(e.mode)}`,` MaterialTheme(colorScheme = if (dark) ${a}Dark else ${a}Light, content = content)`,"}",""],...et(`${a}Light`,"lightColorScheme",e.light),...et(`${a}Dark`,"darkColorScheme",e.dark)]}function tt(e){let{r:a,g:o,b:n,a:s}=nt(e),r=(i)=>(i/255).toFixed(3);return`UIColor(red: ${r(a)}, green: ${r(o)}, blue: ${r(n)}, alpha: ${s.toFixed(3)})`}function rt(e,a){let n=at.filter((s)=>e.light.has(s)||e.dark.has(s)).map((s)=>{let r=e.light.get(s)??e.dark.get(s)??"#000000",i=e.dark.get(s)??r;return` static let ${s} = Color(UIColor { $0.userInterfaceStyle == .dark ? ${tt(i)} : ${tt(r)} })`});return[`enum ${a} {`,...n,"}"]}function m(e,a=!0){let o=[],n=e.testTag();if(n!==void 0)o.push(`.accessibilityIdentifier(${n})`);let s=a?e.actions("onTap"):[];if(s.length>0)o.push(`.onTapGesture { ${U(s)} }`);return o}function G(e,a=[]){return(o,n)=>[...d(`${e} {`,n.children()),...a,...m(n)]}function wa(e){return(Array.isArray(e.spans)?e.spans:[]).map((o)=>typeof o==="object"&&o!==null&&("text"in o)?String(o.text):"").join("")}var ba={filled:".borderedProminent",tonal:".bordered",elevated:".bordered",outlined:".bordered",text:".borderless"},Sa=(e,a)=>[...d("Button {",a.actions("onTap"),"} label: {"),` Text(verbatim: ${a.text(a.string("label")??"")})`,"}",`.buttonStyle(${ba[a.string("variant")??"filled"]??".borderedProminent"})`,...m(a,!1)];function xa(e,a){let o=`ui.text(${a.text(a.string("stateKey")??"")})`,n=a.string("placeholder"),s=n===void 0?"":`, prompt: Text(verbatim: ${a.text(n)})`,r=`Text(verbatim: ${a.text(a.string("label")??n??"")})`;if(e.secure===!0||e.keyboardType==="password")return`SecureField(text: ${o}${s}) { ${r} }`;let i=e.multiline===!0?", axis: .vertical":"";return`TextField(text: ${o}${s}${i}) { ${r} }`}var Ta=(e,a)=>{let o=a.string("supportingText"),n=[xa(e,a),".textFieldStyle(.roundedBorder)"];if(o===void 0)return[...n,...m(a)];return[...d("VStack(alignment: .leading) {",[...n,`Text(verbatim: ${a.text(o)})`," .font(.caption)"]),...m(a)]},_a=(e,a)=>{let o=e.axis==="horizontal",n=d(`ForEach(0..<${a.rowCount()}, id: \\.self) { index in`,a.row("index")),s=d(`${o?"LazyHStack":"LazyVStack(alignment: .leading)"} {`,[...a.children("header"),...n,...a.children("footer")]);return[...d(`ScrollView(${o?".horizontal":".vertical"}) {`,s),...m(a)]},Aa=(e,a)=>{let o=a.string("supporting"),n=[`Text(verbatim: ${a.text(a.string("headline")??"")})`,...o===void 0?[]:[`Text(verbatim: ${a.text(o)})`," .font(.subheadline)"," .foregroundStyle(.secondary)"]];return[...d("HStack {",[...d("VStack(alignment: .leading) {",n),"Spacer()"]),".contentShape(Rectangle())",...m(a)]},Pa=(e,a)=>{let o=a.stackScreens().flatMap(([r,i])=>[`case ${a.literal(r)}:`,` ${i}(ui: ui)`]),s=[`switch ui.top(${a.literal(a.string("id")??"")}) {`,...o,"default:"," EmptyView()","}"];return[...d("Group {",s),...m(a)]},Ea=(e,a)=>[...d(`ScrollView(${e.axis==="horizontal"?".horizontal":".vertical"}) {`,a.children("child")),...m(a)],it={language:"swift",emitters:{box:G("ZStack"),row:G("HStack"),column:G("VStack(alignment: .leading)"),card:G("VStack(alignment: .leading)",[".padding()",".background(Color(UIColor.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 12))"]),scroll:Ea,spacer:(e,a)=>["Spacer()",".frame(width: 8, height: 8)",...m(a)],divider:(e,a)=>["Divider()",...m(a)],text:(e,a)=>[`Text(verbatim: ${a.text(a.string("text")??wa(e))})`,...m(a)],button:Sa,textField:Ta,list:_a,listItem:Aa,stack:Pa},todoStub:(e,a,o)=>[`// TODO(eject): ${String(e.type)} at ${a.path} has no scaffold mapping; build it by hand.`,...d("VStack(alignment: .leading) {",[`Text(verbatim: ${a.literal(`TODO: ${String(e.type)}`)})`,...o]),...m(a)],indexed:(e)=>`String(${e})`};function Oa(e){let a=Is(e),o=a.success?c(a.spec):void 0;if(!o)throw new t(`Cannot eject prototype "${String(e.id)}": its components do not expand. Show it again to see the error.`);return o}function Ra(e){return Object.entries(c(e.state)??{}).filter((a)=>typeof a[1]==="string")}function $a(e,a,o){let n=new URLSearchParams({id:e,platform:a}).toString();return[`// Generated once by ${pS}?${n}.`,"// This file is yours: edit it freely. It is never regenerated or synced back to the spec.",`// ${o} TODO(eject) item(s) remain; search for TODO(eject).`]}function Ia(e,a,o){let s=new oe(a==="android"?Je:it,o.data,""),r=s.emit(o.root,"root");return{writer:s,root:r,typeName:_(e),theme:ot(o.theme),texts:Ra(o),header:$a(e,a,s.todos.length)}}function dt(e){return`mapOf<String, String>(${e.map(([o,n])=>`${h(o,"kotlin")} to ${h(n,"kotlin")}`).join(", ")})`}function Na(e){return d(`class ${e.typeName}State {`,[`private val starts: Map<String, String> = ${dt([...e.writer.stackStarts])}`,"private val stacks = mutableStateMapOf<String, List<String>>()",`private val texts = mutableStateMapOf<String, String>().apply { putAll(${dt(e.texts)}) }`,"","fun top(stack: String): String = path(stack).last()","fun push(stack: String, screen: String) { stacks[stack] = path(stack) + screen }","fun pop(stack: String) { val p = path(stack); if (p.size > 1) stacks[stack] = p.dropLast(1) }",'fun popToRoot(stack: String) { stacks[stack] = listOf(starts[stack] ?: "") }',"fun replace(stack: String, screen: String) { stacks[stack] = path(stack).dropLast(1) + screen }",'fun text(key: String): String = texts[key] ?: ""',"fun setText(key: String, value: String) { texts[key] = value }","",'private fun path(stack: String): List<String> = stacks[stack] ?: listOf(starts[stack] ?: "")'])}function ja(e){let a=e.typeName,o=e.writer.screens.flatMap((s)=>["","@Composable",...d(`private fun ${s.name}(ui: ${a}State) {`,s.body)]);return`${[...e.header,"package prototypes.eject","",...Xe.map((s)=>`import ${s}`),"","@Composable",...d(`fun ${a}() {`,[`val ui = remember { ${a}State() }`,...d(`${a}Theme {`,e.root)]),...o,"",...Na(e),"",...st(e.theme,`${a}Theme`)].join(`
966
+ `)}
967
+ `}function lt(e){if(e.length===0)return"[:]";return`Dictionary([${e.map(([o,n])=>`(${h(o,"swift")}, ${h(n,"swift")})`).join(", ")}], uniquingKeysWith: { first, _ in first })`}function Da(e){return d(`final class ${e.typeName}State: ObservableObject {`,["@Published private var stacks: [String: [String]] = [:]","@Published private var texts: [String: String]","private let starts: [String: String]","",...d("init() {",[`starts = ${lt([...e.writer.stackStarts])}`,`texts = ${lt(e.texts)}`]),"",'func top(_ stack: String) -> String { path(stack).last ?? "" }',"func push(_ stack: String, _ screen: String) { stacks[stack] = path(stack) + [screen] }","func pop(_ stack: String) { let p = path(stack); if p.count > 1 { stacks[stack] = Array(p.dropLast()) } }",'func popToRoot(_ stack: String) { stacks[stack] = [starts[stack] ?? ""] }',"func replace(_ stack: String, _ screen: String) { stacks[stack] = Array(path(stack).dropLast()) + [screen] }",...d("func text(_ key: String) -> Binding<String> {",['Binding(get: { self.texts[key] ?? "" }, set: { self.texts[key] = $0 })']),"",'private func path(_ stack: String) -> [String] { stacks[stack] ?? [starts[stack] ?? ""] }'])}function Ma(e){return[...e.theme.light.has("primary")||e.theme.dark.has("primary")?[`.tint(${e.typeName}Theme.primary)`]:[],...e.theme.mode?[`.preferredColorScheme(.${e.theme.mode})`]:[]]}function Ca(e){let a=e.typeName,o=e.writer.screens.flatMap((s)=>["",...d(`private struct ${s.name}: View {`,[`@ObservedObject var ui: ${a}State`,"",...d("var body: some View {",d("Group {",s.body))])]);return`${[...e.header,"import SwiftUI","import UIKit","",...d(`struct ${a}: View {`,[`@StateObject private var ui = ${a}State()`,"",...d("var body: some View {",[...d("Group {",e.root),...Ma(e)])]),...o,"",...Da(e),"",...rt(e.theme,`${a}Theme`)].join(`
968
+ `)}
969
+ `}function Ua(e){let a=["One-shot scaffold, not round-trip: re-reading this resource never merges with code you changed.","Only layout, text, buttons, text fields, lists and stack navigation are translated; styles, conditions, state other than text, and data bindings are TODOs.","Every testTag is kept (Modifier.testTag / .accessibilityIdentifier), so flows recorded on the prototype can target the ejected screens."];if(e.theme.seeded)a.push("The spec seeds its color scheme (theme.colors.seed or source); only explicit color roles are exported.");return a}async function ct(e,a){let{id:o,platform:n,resolved:s}=await te(e,a,pS),r=Ia(o,n,Oa(s.spec)),i=n==="android"?{path:`app/src/main/java/prototypes/eject/${r.typeName}.kt`,content:ja(r)}:{path:`${r.typeName}.swift`,content:Ca(r)};return{id:o,platform:n,files:[i],scaffolded:[...r.writer.scaffolded].sort(),remaining:r.writer.todos,notes:Ua(r)}}import{createHash as La}from"crypto";var qa={compose:{platform:"android",mimeType:rI.compose,render:I},swiftui:{platform:"ios",mimeType:rI.swiftui,render:N}};function Fa(e,a,o){let n=T(a);return X(o).map(({id:s,kind:r})=>({id:s,kind:r,path:M(e,n,D(s)),uri:`${uS}?${new URLSearchParams({id:a,assetId:s}).toString()}`}))}function Ga(e){if("gradle"in e)return`Library (Gradle): implementation("${e.gradle}")`;let{url:a,from:o,product:n}=e.swiftPackage;return`Library (SwiftPM): .package(url: "${a}", from: "${o}"), product "${n}"`}function za(e,a,o,n){let{spec:s}=n,r=dm(s),i=o.length===0?["Assets: none"]:["Assets (not inlined; add each file at its path, a saved prototype serves its bytes):",...o.map((l)=>` ${l.kind} ${l.path} <- ${l.uri}`)];return[`Generated by ${e} (revision ${n.revision}, frame ${n.frame.id}).`,Ga(Z(a,n.version)),`Required capabilities: ${r.length===0?"none":r.join(", ")}`,...i]}function Ba(e){let{width:a,height:o}=xv(e,EC(e)[0]);return{name:e.label,widthDp:a,heightDp:o}}function pt(e,a,o,n){return qV(()=>Wa(e,a,o,n))}function Wa(e,a,o,n){let{platform:s,mimeType:r,render:i}=qa[a],l=IC(o.mcpSessionId),p=e.revision(o.id,K5(o.revision),l),u=Su(o.frame??Zh(s));ee(p.spec,s);let w=Fa(s,o.id,p.spec);if(s==="ios")Q(w);let b=Iy(o.id,a),v=i({id:o.id,directory:T(o.id),typeName:_(o.id),assets:w,storage:p.spec.storage!==void 0,inline:{spec:JSON.stringify(p.spec),generatedBy:b,header:za(b,s,w,{spec:p.spec,revision:p.revision,frame:u,version:n}),preview:Ba(u)}}),{deviceRevision:A,mcpSessionId:re}=p;return{text:v,mimeType:r,meta:{revision:p.revision,...A===void 0?{}:{deviceRevision:A},...re===void 0?{}:{mcpSessionId:re},revisions:e.history(o.id,l).map((gt)=>gt.revision),frame:u.id,sha256:La("sha256").update(v,"utf8").digest("hex"),rendererVersion:n}}}import{createHash as Xa}from"crypto";var ut=W.definitions;function B(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function mt(e,a,o){if(a)e.forEach((n)=>S(n,a,o))}var Ya={chip:(e)=>typeof e.stateKey==="string"?["filled/check"]:[],segmentedButton:()=>["filled/check"],select:()=>["filled/expand_more"],datePicker:()=>["filled/arrow_drop_down","filled/chevron_left","filled/chevron_right"]},ne=(e,a,o)=>{if(B(e))mt(Object.values(e),a.item,o)},Ha={ref:(e,a,o)=>Va(e,a.name,o),array:(e,a,o)=>{if(Array.isArray(e))mt(e,a.item,o)},map:ne,record:ne,screenMap:ne,object:(e,a,o)=>Ka(e,a,o),tagged:(e,a,o)=>{let n=B(e)?String(e.type):"",s=a.variants?.[n];if(s){for(let r of Ya[n]?.(e)??[])o.keys.add(r);S(e,s,o)}},choice:(e,a,o)=>a.options?.forEach((n)=>S(e,n,o))};function S(e,a,o){if(e!==void 0&&e!==null)Ha[a.kind]?.(e,a,o)}function Va(e,a,o){if(a==="iconName"){if(typeof e==="string"&&by.has(e))o.keys.add(`filled/${e}`);return}let n=ut[a];if(n)S(e,n,o)}var ht=(e,a)=>e?.kind==="ref"&&e.name===a;function Ka(e,a,o){if(!B(e))return;let n=Object.entries(a.fields??{}),s=n.find(([,i])=>ht(i?.rule,"iconVariant"))?.[0],r=s?cL(e[s]):"filled";if(s)o.variants.add(r);for(let[i,l]of n){let p=e[i];if(ht(l?.rule,"iconName")&&typeof p==="string"){if(by.has(p))o.keys.add(`${r}/${p}`)}else if(l)S(p,l.rule,o)}}function z(e,a){if(typeof e==="string"){if(by.has(e))a.add(e)}else if(Array.isArray(e))e.forEach((o)=>z(o,a));else if(B(e))Object.values(e).forEach((o)=>z(o,a))}function Ja(e){let a={keys:new Set,variants:new Set(["filled"])};S(Lf(e),ut.spec,a);let o=new Set;z(e.state,o),z(e.data,o);for(let n of o)for(let s of a.variants)a.keys.add(`${s}/${n}`);return[...a.keys].sort()}function yt(e,a){let o={},n=[];for(let s of Ja(e)){let[r,i]=s.split("/"),l=a.glyph(i,cL(r));if(l)o[s]=l.map((p)=>bu(p)).join("");else n.push(s)}return{glyphs:o,iconMissing:n}}var Za="automobile-prototype-embed",Qa=1;function eo(e,a=VV()){let{glyphs:o,iconMissing:n}=yt(e,a);return{format:Za,version:Qa,requires:dm(e),spec:e,...Object.keys(o).length>0?{glyphs:o}:{},...n.length>0?{iconMissing:n}:{}}}function ft(e,a,o,n){let s=Jf(e,a.id),r=G5(e,a,s),i=eo(r.revision.spec,n),l=JSON.stringify(i),{deviceRevision:p,mcpSessionId:u}=r.revision,w=IC(a.mcpSessionId);return{text:l,envelope:i,meta:{revision:r.revision.revision,...p===void 0?{}:{deviceRevision:p},...u===void 0?{}:{mcpSessionId:u},revisions:e.history(r.id,w).map((b)=>b.revision),variants:s,sha256:Xa("sha256").update(l).digest("hex"),rendererVersion:o}}}function es(){return new j5}function se(e,a,o){let n=new URLSearchParams(Object.entries(o).filter((r)=>r[1]!==void 0).sort(([r],[i])=>r.localeCompare(i))).toString(),s=Iy(e,a);return n?`${s}?${n}`:s}async function ts(e,a){let o=await dL(e.revisions,a,e.rendererVersion(),e.images),{frame:n,revision:s,variant:r,mcpSessionId:i}=a;return{uri:se(a.id,"web",{frame:n,revision:s,variant:r,mcpSessionId:i}),mimeType:"text/html",text:o.html,_meta:{...o.meta}}}function as(e,a){let o=ft(e.revisions,a,e.rendererVersion()),{revision:n,variant:s,mcpSessionId:r}=a;return{uri:se(a.id,"embed",{revision:n,variant:s,mcpSessionId:r}),mimeType:"application/json",text:o.text,_meta:{...o.meta}}}function os(e,a,o){let n=pt(e.revisions,a,o,e.rendererVersion()),{frame:s,revision:r,mcpSessionId:i}=o;return{uri:se(o.id,a,{frame:s,revision:r,mcpSessionId:i}),mimeType:n.mimeType,text:n.text,_meta:{...n.meta}}}function ns(){return{uri:xC,mimeType:"text/markdown",text:je()}}function ss(e){let{total:a,names:o}=Ne(e);return{uri:`${uL}?query=${Qe(e)}`,mimeType:"application/json",text:JSON.stringify({query:e,total:a,truncated:a>o.length,names:o})}}async function rs(e,a){let o=await ze(e,a),n=new URLSearchParams(e).toString();return{uri:n?`${Ey}?${n}`:Ey,mimeType:"application/json",text:JSON.stringify(o,null,2)}}async function is(e,a){let o=await ct(e,a),n=new URLSearchParams(e).toString();return{uri:n?`${pS}?${n}`:pS,mimeType:"application/json",text:JSON.stringify(o,null,2)}}async function ds(e,a){let o=await Be(e,a);return{uri:`${uS}?${new URLSearchParams(e).toString()}`,mimeType:o.mimeType,blob:Buffer.from(o.bytes).toString("base64")}}export{es as createPrototypeWebImageEncoder,is as readEject,as as readEmbed,ns as readGuide,ss as readIcons,rs as readNative,ds as readNativeAsset,os as readSource,ts as readWeb};
970
+
971
+ //# debugId=E282F6B1AA18F11864756E2164756E21