your-ai-workflow-firebase-os 2.0.1 → 2.0.3

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 (183) hide show
  1. package/dist/AccessNotice-BZ5Gt1nP.js +40 -0
  2. package/dist/AccessNotice-CN7kc70o.cjs +1 -0
  3. package/dist/{AddressField-R-HvndkG.js → AddressField-9Sqn8TvC.js} +3 -3
  4. package/dist/{AddressField-BkzFOo2t.cjs → AddressField-B7wtV8yA.cjs} +1 -1
  5. package/dist/Assets-DBMwZ3bN.cjs +1 -0
  6. package/dist/Assets-DT-NU_WO.js +296 -0
  7. package/dist/Attachments-Bl9_dfsu.js +46 -0
  8. package/dist/Attachments-UVK9SRXf.cjs +1 -0
  9. package/dist/Billing-0RIHayHy.cjs +1 -0
  10. package/dist/Billing-FNE2r3_d.js +379 -0
  11. package/dist/BookingCalendar-BP5kiv3d.cjs +1 -0
  12. package/dist/BookingCalendar-DZ2fEFja.js +309 -0
  13. package/dist/Button-4lzOE31p.js +48 -0
  14. package/dist/Button-5DlB2MCi.cjs +1 -0
  15. package/dist/CalendarView-BimoXy_k.cjs +1 -0
  16. package/dist/CalendarView-D-ykjpvI.js +664 -0
  17. package/dist/ConfirmModal-Di9GR-vV.cjs +1 -0
  18. package/dist/ConfirmModal-gKQEpd4d.js +113 -0
  19. package/dist/ContactPopup-BXnHrRwl.js +1114 -0
  20. package/dist/ContactPopup-lxmRMoIa.cjs +1 -0
  21. package/dist/CopyLink-BpEQWmfI.cjs +1 -0
  22. package/dist/CopyLink-CBAVfcm1.js +43 -0
  23. package/dist/DashboardNav-CXa_ci-_.cjs +1 -0
  24. package/dist/DashboardNav-Dmn7PVSp.js +886 -0
  25. package/dist/EmailAdmin-CjN_rvxT.js +318 -0
  26. package/dist/EmailAdmin-lAEBdY1s.cjs +1 -0
  27. package/dist/EventCard-46EquygT.cjs +1 -0
  28. package/dist/EventCard-B6xD-mk3.js +97 -0
  29. package/dist/FileViewerPopup-CX-J75TM.js +1283 -0
  30. package/dist/FileViewerPopup-D8-MmIoS.cjs +3 -0
  31. package/dist/FormNote-BivRNA3J.js +50 -0
  32. package/dist/FormNote-BtdTGpml.cjs +1 -0
  33. package/dist/FormsAdmin-B5PBCG2W.js +812 -0
  34. package/dist/FormsAdmin-BQCULvi5.cjs +1 -0
  35. package/dist/Inbox-BPk2ZkE5.js +779 -0
  36. package/dist/Inbox-DNrcBx0o.cjs +3 -0
  37. package/dist/Loader-Bu89fsbB.cjs +1 -0
  38. package/dist/Loader-D4gwgBOq.js +32 -0
  39. package/dist/ManagePlanButton-B0dwqZBd.cjs +1 -0
  40. package/dist/ManagePlanButton-BSrOHYze.js +29 -0
  41. package/dist/MyBookings-6qr0GgzO.cjs +1 -0
  42. package/dist/MyBookings-BQjGM2LQ.js +113 -0
  43. package/dist/PageTitle-CZhgmmDT.cjs +3 -0
  44. package/dist/PageTitle-D93YCiN8.js +1606 -0
  45. package/dist/PagesAdmin-BcHIcqM5.cjs +34 -0
  46. package/dist/PagesAdmin-D69P9nRb.js +5802 -0
  47. package/dist/PagesAdmin-DNwZOKjA.cjs +1 -0
  48. package/dist/PagesAdmin-HUITEcHx.js +3 -0
  49. package/dist/PaymentsAdmin-C2iCI9qC.js +1291 -0
  50. package/dist/PaymentsAdmin-CoXi2vCm.cjs +2 -0
  51. package/dist/{PhoneInputField-B3P_vUDW.cjs → PhoneInputField-BEwYZMB7.cjs} +1 -1
  52. package/dist/{PhoneInputField-BlYHb5e0.js → PhoneInputField-BG0L2djE.js} +2 -2
  53. package/dist/PhoneInputField-DfOtCIH_.js +2 -0
  54. package/dist/{PhoneInputField-CdNTvWXQ.cjs → PhoneInputField-jhWvto8C.cjs} +1 -1
  55. package/dist/Popup-DHYGFl2-.cjs +1 -0
  56. package/dist/Popup-xROLytgr.js +142 -0
  57. package/dist/ProviderMark-BBfaM5Yl.js +52 -0
  58. package/dist/ProviderMark-xLkUN3Fx.cjs +1 -0
  59. package/dist/PublishApp-Bm-6r3Td.js +595 -0
  60. package/dist/PublishApp-D4XJUkJa.cjs +2 -0
  61. package/dist/Said-CoC15awL.js +94 -0
  62. package/dist/Said-IqCNofdh.cjs +1 -0
  63. package/dist/ScheduleAdmin-BKfQkrR8.js +1853 -0
  64. package/dist/ScheduleAdmin-DwK36GAZ.cjs +1 -0
  65. package/dist/SchedulePicker--urIH2Eb.js +112 -0
  66. package/dist/SchedulePicker-nmqzUnUH.cjs +1 -0
  67. package/dist/Setup-CHLgd-ag.js +849 -0
  68. package/dist/Setup-CaDranTm.cjs +1374 -0
  69. package/dist/SetupPayments-CVIwh1eQ.js +411 -0
  70. package/dist/SetupPayments-HNXWA25X.cjs +1 -0
  71. package/dist/SignInAdmin-C94-A-0w.cjs +4 -0
  72. package/dist/SignInAdmin-Yxgv_8U4.js +766 -0
  73. package/dist/SupportBar-CDT0HwdP.cjs +1 -0
  74. package/dist/SupportBar-CN_JGC-9.js +117 -0
  75. package/dist/SystemTabContent-L3vGGF3P.cjs +1 -0
  76. package/dist/SystemTabContent-QdxxLILE.js +35 -0
  77. package/dist/TemplateDrive-DV8zNjYe.cjs +1 -0
  78. package/dist/TemplateDrive-NJY_AAvV.js +1197 -0
  79. package/dist/ThemeAdmin-BzFjJkLg.js +269 -0
  80. package/dist/ThemeAdmin-DVnPNxe7.cjs +1 -0
  81. package/dist/ThemeAdmin-DY7Vc4Sj.cjs +3 -0
  82. package/dist/ThemeAdmin-DnC-D2N3.js +2 -0
  83. package/dist/{ContactPopup-9J_YeJrb.js → TimeZoneField-DkI1Bkcz.js} +3382 -5513
  84. package/dist/TimeZoneField-qs7NPR30.cjs +12 -0
  85. package/dist/TransactionsAdmin-BsZ-oQWi.js +582 -0
  86. package/dist/TransactionsAdmin-CTSTiPld.cjs +1 -0
  87. package/dist/Trouble-9es_AI4F.cjs +1 -0
  88. package/dist/Trouble-oWt9fMsm.js +164 -0
  89. package/dist/Users-CTitWXyd.cjs +1 -0
  90. package/dist/Users-DJjQTWJF.js +1207 -0
  91. package/dist/aiModels-DIP9fn8T.cjs +1 -0
  92. package/dist/{aiModels-BHlEfuRJ.js → aiModels-DnDhNGup.js} +29 -61
  93. package/dist/arrivalRoads-BJ-35vkk.js +228 -0
  94. package/dist/arrivalRoads-qoU-B4jt.cjs +4 -0
  95. package/dist/{bake-D0gLVNTL.cjs → bake-n2wyFw0B.cjs} +1 -1
  96. package/dist/{bake-OErV2MLy.js → bake-naZeMu98.js} +14 -14
  97. package/dist/bookingPages-C7a3srPG.js +102 -0
  98. package/dist/bookingPages-DvbFbD9X.cjs +1 -0
  99. package/dist/components/CustomSelect.d.ts +7 -1
  100. package/dist/components/RegistrationFields.d.ts +6 -1
  101. package/dist/components/Reveal.d.ts +12 -0
  102. package/dist/components/ScrollToTop.d.ts +4 -0
  103. package/dist/contentFallback-C9MZjdq7.cjs +1 -0
  104. package/dist/contentFallback-CEDY0Pwp.js +96 -0
  105. package/dist/contentKey-Bwry4lDa.js +13 -0
  106. package/dist/contentKey-DJGYYnAE.cjs +1 -0
  107. package/dist/{dist-_zWHDDrc.js → dist-DUPvBnOA.js} +1 -1
  108. package/dist/email-BfnZnMwD.cjs +1 -0
  109. package/dist/email-Ddj8Dlu6.js +58 -0
  110. package/dist/{fileBytes-B0qxHYQL.cjs → fileBytes-BJPaiX9Y.cjs} +1 -1
  111. package/dist/{fileBytes-uhq3pGCS.js → fileBytes-bjgvCfWu.js} +2 -2
  112. package/dist/forms-Bvx9HTM6.js +75 -0
  113. package/dist/forms-CmNeTETW.cjs +1 -0
  114. package/dist/lib/ThemeContext.d.ts +0 -14
  115. package/dist/lib/bareImgWatch.d.ts +6 -1
  116. package/dist/lib/localEdits.d.ts +2 -0
  117. package/dist/lib/pageDesignLines.d.ts +1 -1
  118. package/dist/lib/paymentProducts.d.ts +8 -0
  119. package/dist/lib/payments.d.ts +11 -1
  120. package/dist/lib/people.d.ts +9 -0
  121. package/dist/lib/recordsKey.d.ts +24 -0
  122. package/dist/{lucide-react-DiyP8FA_.js → lucide-react-CA7GD7Nv.js} +8 -8
  123. package/dist/notices-B7iIrIEg.js +29 -0
  124. package/dist/notices-upMg8UVh.cjs +1 -0
  125. package/dist/openOnArrival-Buwqb86D.cjs +1 -0
  126. package/dist/openOnArrival-S2K6v-lB.js +35 -0
  127. package/dist/pageFiles--Ih60ITP.js +45 -0
  128. package/dist/pageFiles-UtBmlOcD.cjs +1 -0
  129. package/dist/pages/PagesAdmin.d.ts +69 -7
  130. package/dist/{pdf-DLuvpvHg.js → pdf-DRXcPGkr.js} +1 -1
  131. package/dist/{pdf.worker.min-C0gkd2Ec.js → pdf.worker.min-CfKZezeM.js} +1 -1
  132. package/dist/people-2jZE_pdI.js +102 -0
  133. package/dist/people-BpVvIr9-.cjs +1 -0
  134. package/dist/personName-CFw3pxuu.js +18 -0
  135. package/dist/personName-CbzN_i57.cjs +1 -0
  136. package/dist/publishSteps-C1cDFzzB.js +88 -0
  137. package/dist/publishSteps-SSm8afDw.cjs +1 -0
  138. package/dist/recordLock-DHVvI9bP.js +61 -0
  139. package/dist/recordLock-DWeIxiwh.cjs +1 -0
  140. package/dist/recordsKey-B7QrxdeU.cjs +1 -0
  141. package/dist/recordsKey-D5-wC0Aw.js +141 -0
  142. package/dist/registrationFields-CLeR2lQJ.js +181 -0
  143. package/dist/registrationFields-v4pOoRdn.cjs +1 -0
  144. package/dist/renderer.cjs.js +1 -1
  145. package/dist/renderer.es.js +1 -1
  146. package/dist/roles-D6VaHWfV.js +147 -0
  147. package/dist/roles-DQUFFBQe.cjs +1 -0
  148. package/dist/scheduleData-ATN8w5hI.js +727 -0
  149. package/dist/scheduleData-Cltj0ABZ.cjs +1 -0
  150. package/dist/server.cjs.js +6 -6
  151. package/dist/server.es.js +716 -715
  152. package/dist/serviceStatus-TQyZO_Y_.cjs +1 -0
  153. package/dist/serviceStatus-vS8AI7SZ.js +125 -0
  154. package/dist/tabAudience-BqpN6dsR.cjs +1 -0
  155. package/dist/tabAudience-CRyMvv0v.js +34 -0
  156. package/dist/transactions-BqMQDRx3.js +343 -0
  157. package/dist/transactions-CufFrdAM.cjs +1 -0
  158. package/dist/useIsPhone-BVGx_X3e.js +13 -0
  159. package/dist/useIsPhone-o63tNneA.cjs +1 -0
  160. package/dist/{validate-DzSJWuyL.js → validate-CmC_tLK-.js} +433 -351
  161. package/dist/validate-DZMwqSrb.cjs +103 -0
  162. package/dist/vite/validate.d.ts +14 -0
  163. package/dist/vite.cjs.js +1 -1
  164. package/dist/vite.es.js +1 -1
  165. package/dist/your-ai-workflow-firebase-os.cjs.js +15 -1431
  166. package/dist/your-ai-workflow-firebase-os.css +1 -1
  167. package/dist/your-ai-workflow-firebase-os.es.js +11695 -35911
  168. package/package.json +1 -1
  169. package/src/index.css +122 -2
  170. package/dist/ContactPopup-DF8qQCoY.cjs +0 -12
  171. package/dist/PhoneInputField-Btx07xvM.js +0 -2
  172. package/dist/aiModels-BIhoTNH0.cjs +0 -1
  173. package/dist/validate-e48WXJdP.cjs +0 -103
  174. /package/dist/{address-DI1YaCI4.cjs → address-Bsb8COq2.cjs} +0 -0
  175. /package/dist/{address-Ca1h_UZX.js → address-C2bHNxJU.js} +0 -0
  176. /package/dist/{dist-BLGlDv1e.cjs → dist-E4jTFPs7.cjs} +0 -0
  177. /package/dist/{lucide-react-BYniZ3lQ.cjs → lucide-react-BwV6L6Is.cjs} +0 -0
  178. /package/dist/{mediaKind-dk396a03.cjs → mediaKind-D0MWyv6b.cjs} +0 -0
  179. /package/dist/{mediaKind-De2Cx_kB.js → mediaKind-DmKMsjeY.js} +0 -0
  180. /package/dist/{pdf-CC5g01eC.cjs → pdf-jLjjhvjp.cjs} +0 -0
  181. /package/dist/{pdf.worker.min-pI_P-OCl.cjs → pdf.worker.min-bLgNWd7w.cjs} +0 -0
  182. /package/dist/{verifiedAccount-C1iCxopN.js → verifiedAccount-DRbRFJ6H.js} +0 -0
  183. /package/dist/{verifiedAccount-D3OeaOBo.cjs → verifiedAccount-DScyKk96.cjs} +0 -0
@@ -259,6 +259,9 @@ var D = (e) => E(y(e)), O = /\b(?:useAiChat|askAi|useAiEnabled|useAiModels)\b|ge
259
259
  "case-query": "breaks",
260
260
  "own-publish": "breaks",
261
261
  "hook-answer": "breaks",
262
+ "notify-body": "breaks",
263
+ "popup-open": "breaks",
264
+ "hand-by-field": "breaks",
262
265
  "not-a-case": "breaks",
263
266
  "save-args": "breaks",
264
267
  "own-write": "breaks",
@@ -336,12 +339,12 @@ var M = (e, t, n = 0) => {
336
339
  }, V = (e) => {
337
340
  let t = e.match(/^(['"`])([^'"`]*)\1$/);
338
341
  return t ? t[2] : null;
339
- }, H = (e) => /export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e) || /export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e), ne = (e) => /export\s+default\b/.test(e), U = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""), re = [
342
+ }, H = (e) => /export\s+(?:const|let|var|function|class)\s+\w*Config\b/.test(e) || /export\s*\{[^}]*\b\w*Config\b[^}]*\}/.test(e), U = (e) => /export\s+default\b/.test(e), W = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""), G = [
340
343
  "board",
341
344
  "table",
342
345
  "calendar",
343
346
  "drive"
344
- ], ie = [
347
+ ], ne = [
345
348
  "text",
346
349
  "email",
347
350
  "tel",
@@ -351,62 +354,62 @@ var M = (e, t, n = 0) => {
351
354
  "select",
352
355
  "checkbox",
353
356
  "address"
354
- ], ae = (e) => {
357
+ ], re = (e) => {
355
358
  for (let t of e.match(/\{[^{}]*\}/g) || []) {
356
359
  if (!/\btype:\s*['"`](?:text|textarea)['"`]/.test(t) || !/\baddress(?:es)?\b/i.test(t) || /\b(?:e-?mail|url|web|website|ip|wallet|crypto)\b/i.test(t)) continue;
357
360
  let e = t.match(/label:\s*['"`]([^'"`]+)['"`]/);
358
361
  return e ? e[1] : "that question";
359
362
  }
360
363
  return null;
361
- }, oe = (e, t) => `${e}: "${t}" asks for an address and draws a plain box. Make it { type: 'address' } — the app looks real addresses up as somebody types and fills in the town, the region, the postcode and the country, each named the way their own country names it. The answer is still the one readable line under the question's own name, with the parts beside it.`, se = [
364
+ }, ie = (e, t) => `${e}: "${t}" asks for an address and draws a plain box. Make it { type: 'address' } — the app looks real addresses up as somebody types and fills in the town, the region, the postcode and the country, each named the way their own country names it. The answer is still the one readable line under the question's own name, with the parts beside it.`, ae = [
362
365
  /\b(?:street|address\s*line|addressline)\b/i,
363
366
  /\b(?:postcode|postal\s*code|post\s*code|zip(?:\s*code)?|pincode)\b/i,
364
367
  /\b(?:town|city)\b/i,
365
368
  /\b(?:county|province|region|state)\b/i,
366
369
  /\bcountry\b/i
367
- ], ce = (e) => {
370
+ ], oe = (e) => {
368
371
  if (/<AddressField\b/.test(e)) return null;
369
372
  let t = [
370
373
  ...e.matchAll(/(?:label|placeholder|aria-label|name|id)=\{?\s*["'`]([^"'`]{1,60})["'`]/g),
371
374
  ...e.matchAll(/(?:label|placeholder|name):\s*["'`]([^"'`]{1,60})["'`]/g),
372
375
  ...e.matchAll(/\b(?:const|let)\s*\[\s*([A-Za-z_$][\w$]*)/g)
373
- ].map((e) => e[1]).join(" | "), n = se.filter((e) => e.test(t));
376
+ ].map((e) => e[1]).join(" | "), n = ae.filter((e) => e.test(t));
374
377
  return n.length < 2 ? null : t.split(" | ").find((e) => n.some((t) => t.test(e))) || "that box";
375
- }, le = (e, t) => `${e}: this page asks where somebody is with boxes of its own ("${t}" is one of them). An address is ONE question in this app: import { AddressField, EMPTY_ADDRESS, composeAddress } from 'your-ai-workflow-firebase-os', start it on EMPTY_ADDRESS, and draw <AddressField id="delivery" label="Delivery address" value={address} onChange={setAddress} required />. It looks real addresses up as somebody types and names the town, the region and the postcode the way their own country names them. What you save, email or pass into a payment is composeAddress(address) — the one readable line — and a part on its own is address.postcode.`, ue = (e, t) => `${e}: this page asks where somebody is with boxes of its own ("${t}" is one of them), before a payment. Where a purchase is going is asked with who is paying: delete those boxes and pass address to the one <BuyerDetails address onReady={setBuyer} /> from 'your-ai-workflow-firebase-os'. It draws the name, the email and ONE address box that looks real addresses up as somebody types — never an <AddressField> or boxes of your own beside it. What they gave is buyer.address (the one readable line, already in buyer.metadata) and a part on its own is buyer.addressParts.postcode (contract rule 9).`, de = /\b(?:full\s*name|your\s*name|first\s*name|last\s*name|surname|e-?mail|phone|telephone|mobile)\b/i, fe = /\b(?:autoComplete=\{?\s*["'`](?:name|given-name|family-name|email|tel)["'`]|type=\{?\s*["'`](?:email|tel)["'`])/, W = /<(?:BuyerDetails|Checkout|Basket|BuyButton|Plans)\b/, pe = /<(?:PayButton|BuyButton|BuyerDetails|Checkout|Basket|Plans)\b/, me = (e) => {
376
- if (!W.test(e)) return null;
378
+ }, se = (e, t) => `${e}: this page asks where somebody is with boxes of its own ("${t}" is one of them). An address is ONE question in this app: import { AddressField, EMPTY_ADDRESS, composeAddress } from 'your-ai-workflow-firebase-os', start it on EMPTY_ADDRESS, and draw <AddressField id="delivery" label="Delivery address" value={address} onChange={setAddress} required />. It looks real addresses up as somebody types and names the town, the region and the postcode the way their own country names them. What you save, email or pass into a payment is composeAddress(address) — the one readable line — and a part on its own is address.postcode.`, ce = (e, t) => `${e}: this page asks where somebody is with boxes of its own ("${t}" is one of them), before a payment. Where a purchase is going is asked with who is paying: delete those boxes and pass address to the one <BuyerDetails address onReady={setBuyer} /> from 'your-ai-workflow-firebase-os'. It draws the name, the email and ONE address box that looks real addresses up as somebody types — never an <AddressField> or boxes of your own beside it. What they gave is buyer.address (the one readable line, already in buyer.metadata) and a part on its own is buyer.addressParts.postcode (contract rule 9).`, le = /\b(?:full\s*name|your\s*name|first\s*name|last\s*name|surname|e-?mail|phone|telephone|mobile)\b/i, ue = /\b(?:autoComplete=\{?\s*["'`](?:name|given-name|family-name|email|tel)["'`]|type=\{?\s*["'`](?:email|tel)["'`])/, de = /<(?:BuyerDetails|Checkout|Basket|BuyButton|Plans)\b/, fe = /<(?:PayButton|BuyButton|BuyerDetails|Checkout|Basket|Plans)\b/, pe = (e) => {
379
+ if (!de.test(e)) return null;
377
380
  if (/<AddressField\b/.test(e)) return "an <AddressField>";
378
381
  if (/<PhoneInputField\b/.test(e)) return "a phone box";
379
382
  for (let t of e.matchAll(/<(?:Input|input)\b/g)) {
380
- let n = t.index ?? 0, r = e.indexOf("/>", n), i = e.slice(n, r === -1 ? n + 600 : Math.min(r + 2, n + 600)), a = [...i.matchAll(/(?:label|placeholder|aria-label)=\{?\s*["'`]([^"'`]{1,60})["'`]/g)].map((e) => e[1]).find((e) => de.test(e));
383
+ let n = t.index ?? 0, r = e.indexOf("/>", n), i = e.slice(n, r === -1 ? n + 600 : Math.min(r + 2, n + 600)), a = [...i.matchAll(/(?:label|placeholder|aria-label)=\{?\s*["'`]([^"'`]{1,60})["'`]/g)].map((e) => e[1]).find((e) => le.test(e));
381
384
  if (a) return `a "${a}" box`;
382
- if (fe.test(i)) return "a name, email or phone box";
385
+ if (ue.test(i)) return "a name, email or phone box";
383
386
  }
384
387
  return null;
385
- }, he = (e, t) => `${e}: this page has <BuyerDetails> and also draws ${t} of its own, so the buyer is asked the same thing twice. <BuyerDetails> draws the name and the email itself, and the phone and the address when you pass phone / address to it — labels and examples included. Delete your own box, and any "delivery details" or "shipping" step around it, and keep the ONE <BuyerDetails address onReady={setBuyer} />. What they gave is on the buyer onReady hands you: buyer.name, buyer.email, buyer.phone, buyer.address, and buyer.addressParts.postcode for a delivery charge (contract rule 9).`, ge = /\b(?:phone|telephone|mobile|whats\s?app)\b/i, _e = /\b(?:code|otp|pin|digits?)\b/i, G = /\b(?:e-?mail|url|web|website|ip|wallet|crypto)\b/i, K = (e) => [...e.matchAll(/<(?:Input|input|TextArea|textarea)\b/g)].map((t) => {
388
+ }, me = (e, t) => `${e}: this page has <BuyerDetails> and also draws ${t} of its own, so the buyer is asked the same thing twice. <BuyerDetails> draws the name and the email itself, and the phone and the address when you pass phone / address to it — labels and examples included. Delete your own box, and any "delivery details" or "shipping" step around it, and keep the ONE <BuyerDetails address onReady={setBuyer} />. What they gave is on the buyer onReady hands you: buyer.name, buyer.email, buyer.phone, buyer.address, and buyer.addressParts.postcode for a delivery charge (contract rule 9).`, he = /\b(?:phone|telephone|mobile|whats\s?app)\b/i, ge = /\b(?:code|otp|pin|digits?)\b/i, K = /\b(?:e-?mail|url|web|website|ip|wallet|crypto)\b/i, q = (e) => [...e.matchAll(/<(?:Input|input|TextArea|textarea)\b/g)].map((t) => {
386
389
  let n = t.index ?? 0, r = e.indexOf("/>", n), i = e.slice(n, r === -1 ? n + 600 : Math.min(r + 2, n + 600));
387
390
  return {
388
391
  tag: i,
389
392
  words: [...i.matchAll(/(?:label|placeholder|aria-label|name|autoComplete)=\{?\s*["'`]([^"'`]{1,60})["'`]/g)].map((e) => e[1])
390
393
  };
391
- }), ve = (e) => {
394
+ }), _e = (e) => {
392
395
  if (/<AddressField\b/.test(e)) return null;
393
- let t = K(e).filter(({ tag: e }) => !/\btype=\{?\s*["'`](?:email|url)["'`]/.test(e));
396
+ let t = q(e).filter(({ tag: e }) => !/\btype=\{?\s*["'`](?:email|url)["'`]/.test(e));
394
397
  for (let { words: e } of t) {
395
- let t = e.find((e) => /\baddress(?:es)?\b/i.test(e) && !G.test(e));
398
+ let t = e.find((e) => /\baddress(?:es)?\b/i.test(e) && !K.test(e));
396
399
  if (t) return t;
397
400
  }
398
- let n = t.flatMap((e) => e.words), r = se.filter((e) => n.some((t) => e.test(t)));
401
+ let n = t.flatMap((e) => e.words), r = ae.filter((e) => n.some((t) => e.test(t)));
399
402
  return r.length < 2 ? null : n.find((e) => r.some((t) => t.test(e))) || null;
400
- }, ye = (e) => {
401
- if (W.test(e)) return null;
402
- for (let { tag: t, words: n } of K(e)) {
403
- if (n.some((e) => _e.test(e))) continue;
404
- let e = n.find((e) => ge.test(e));
403
+ }, ve = (e) => {
404
+ if (de.test(e)) return null;
405
+ for (let { tag: t, words: n } of q(e)) {
406
+ if (n.some((e) => ge.test(e))) continue;
407
+ let e = n.find((e) => he.test(e));
405
408
  if (e) return `"${e}"`;
406
409
  if (/\b(?:type|autoComplete)=\{?\s*["'`]tel/.test(t)) return "type=\"tel\"";
407
410
  }
408
411
  return null;
409
- }, be = (e, t) => `${e}: this page asks for a phone number in a box of its own (${t}). A phone number is the app's own phone box — the one every form in the app draws, with its country-code picker. Somebody sending theirs in is a question on the page's form: { name: 'phone', type: 'tel', label: 'Phone' } in its *Form.config.ts, shown with <ContactPopup>. A number the page keeps on a record of its own is import { PhoneInputField } from 'your-ai-workflow-firebase-os' and <PhoneInputField field={{ label: 'Phone', placeholder: '20 7946 0000' }} value={phone} onChange={setPhone} /> in place of your box; what it hands back is the whole number with its + and country code (contract rule 23).`, xe = (e, t) => `${e}: this page asks for a phone number in a box of its own (${t}) before a payment. Who is paying is asked with <BuyerDetails phone onReady={setBuyer} /> from 'your-ai-workflow-firebase-os': it draws the name, the email and the app's own phone box itself. Delete your box; the number is buyer.phone, already in buyer.metadata (contract rule 9).`, Se = new Set([
412
+ }, ye = (e, t) => `${e}: this page asks for a phone number in a box of its own (${t}). A phone number is the app's own phone box — the one every form in the app draws, with its country-code picker. Somebody sending theirs in is a question on the page's form: { name: 'phone', type: 'tel', label: 'Phone' } in its *Form.config.ts, shown with <ContactPopup>. A number the page keeps on a record of its own is import { PhoneInputField } from 'your-ai-workflow-firebase-os' and <PhoneInputField field={{ label: 'Phone', placeholder: '20 7946 0000' }} value={phone} onChange={setPhone} /> in place of your box; what it hands back is the whole number with its + and country code (contract rule 23).`, be = (e, t) => `${e}: this page asks for a phone number in a box of its own (${t}) before a payment. Who is paying is asked with <BuyerDetails phone onReady={setBuyer} /> from 'your-ai-workflow-firebase-os': it draws the name, the email and the app's own phone box itself. Delete your box; the number is buyer.phone, already in buyer.metadata (contract rule 9).`, xe = new Set([
410
413
  "uid",
411
414
  "id",
412
415
  "email",
@@ -423,7 +426,7 @@ var M = (e, t, n = 0) => {
423
426
  "requestedRole",
424
427
  "requestedRoleAt",
425
428
  "finishedAt"
426
- ]), Ce = {
429
+ ]), Se = {
427
430
  board: {
428
431
  call: "actions.openInlineEditor(categoryId)",
429
432
  token: "openInlineEditor"
@@ -440,10 +443,10 @@ var M = (e, t, n = 0) => {
440
443
  call: "the openUploadModal() prop",
441
444
  token: "openUploadModal"
442
445
  }
443
- }, we = /<(?:video|audio|embed|object)[\s/>]|<iframe[^>]*(?:youtube|youtu\.be|vimeo|firebasestorage|\.pdf)/i, Te = (e, t) => {
446
+ }, Ce = /<(?:video|audio|embed|object)[\s/>]|<iframe[^>]*(?:youtube|youtu\.be|vimeo|firebasestorage|\.pdf)/i, we = (e, t) => {
444
447
  let n = [...new Set(t.map((e) => e.toLowerCase().startsWith("<iframe") ? "an <iframe> of a video or a file" : `a bare ${e.replace(/[\s/>]+$/, "")}>`))];
445
448
  return `${e}: ${P(n)} ${n.length > 1 ? "are" : "is"} drawn in the browser's own chrome — play a video or a recording with <VideoPlayer src={…} /> and open a file with <FileViewer src={…} />, or <FileViewerPopup open={…} onClose={…} src={…} /> behind a button, all imported from 'your-ai-workflow-firebase-os'.`;
446
- }, Ee = /<img[\s/>]/i, De = /(?:background(?:-i|I)mage\s*[:=]\s*[^;\n}]*|\bbg-\[)\s*(?:'|"|`|\{)?\s*url\(\s*['"`]?\s*(?:https?:|data:image|\/|\$\{|[\w./-]+\.(?:png|jpe?g|webp|avif|gif|svg))/i, Oe = (e) => `${e}: a picture hung off a background holds no space while it loads, cannot wait its turn below the fold, says nothing to somebody who cannot see it, and always downloads the full-size file however small the box is — a 300-pixel tile fetching a 1600-pixel photograph. Draw it with <Picture src={…} alt="…" className="absolute inset-0" /> imported from 'your-ai-workflow-firebase-os' behind whatever sits on top of it, or put it in a box with the picture inside. A background colour or a gradient is fine and is not this.`, ke = /type\s*=\s*(?:["']number["']|\{\s*["']number["']\s*\})/, q = /\bNumber\s*\(|\bparseInt\s*\(|\bparseFloat\s*\(|\bvalueAsNumber\b|\+\s*[A-Za-z_$][\w$]*\.target\.value/, J = /(?:\|\||\?\?)\s*0\b/, Y = (e, t) => {
449
+ }, Te = /<img[\s/>]/i, Ee = /(?:background(?:-i|I)mage\s*[:=]\s*[^;\n}]*|\bbg-\[)\s*(?:'|"|`|\{)?\s*url\(\s*['"`]?\s*(?:https?:|data:image|\/|\$\{|[\w./-]+\.(?:png|jpe?g|webp|avif|gif|svg))/i, De = (e) => `${e}: a picture hung off a background holds no space while it loads, cannot wait its turn below the fold, says nothing to somebody who cannot see it, and always downloads the full-size file however small the box is — a 300-pixel tile fetching a 1600-pixel photograph. Draw it with <Picture src={…} alt="…" className="absolute inset-0" /> imported from 'your-ai-workflow-firebase-os' behind whatever sits on top of it, or put it in a box with the picture inside. A background colour or a gradient is fine and is not this.`, J = /type\s*=\s*(?:["']number["']|\{\s*["']number["']\s*\})/, Oe = /\bNumber\s*\(|\bparseInt\s*\(|\bparseFloat\s*\(|\bvalueAsNumber\b|\+\s*[A-Za-z_$][\w$]*\.target\.value/, ke = /(?:\|\||\?\?)\s*0\b/, Ae = (e, t) => {
447
450
  let n = 0;
448
451
  for (let r = t; r < e.length; r++) {
449
452
  let i = e[r];
@@ -452,7 +455,7 @@ var M = (e, t, n = 0) => {
452
455
  else if (i === ">" && n === 0) return e.slice(t, r + 1);
453
456
  }
454
457
  return e.slice(t);
455
- }, Ae = (e, t) => {
458
+ }, je = (e, t) => {
456
459
  let n = e.search(RegExp(`\\b${t}\\s*=\\s*\\{`));
457
460
  if (n < 0) return "";
458
461
  let r = e.indexOf("{", n), i = 0;
@@ -460,41 +463,41 @@ var M = (e, t, n = 0) => {
460
463
  else if (e[t] === "}" && --i === 0) return e.slice(r + 1, t);
461
464
  return e.slice(r + 1);
462
465
  };
463
- function je(e) {
466
+ function Me(e) {
464
467
  let t = [];
465
468
  for (let n of e.matchAll(/<[iI]nput\b/g)) {
466
- let r = Y(e, n.index ?? 0);
467
- if (!ke.test(r)) continue;
468
- let i = Ae(r, "onChange"), a = Ae(r, "value");
469
- (q.test(i) || q.test(a) || J.test(a)) && t.push(r.replace(/\s+/g, " ").slice(0, 96).trim());
469
+ let r = Ae(e, n.index ?? 0);
470
+ if (!J.test(r)) continue;
471
+ let i = je(r, "onChange"), a = je(r, "value");
472
+ (Oe.test(i) || Oe.test(a) || ke.test(a)) && t.push(r.replace(/\s+/g, " ").slice(0, 96).trim());
470
473
  }
471
474
  return t;
472
475
  }
473
- var Me = (e, t) => `${e}: "${t[0]}…" turns what is being typed into a number on every keystroke, and Number('') is 0 — so deleting the last digit puts a 0 straight back and the box can never be emptied. Hold what was typed as TEXT (useState('') — empty, not 0) with value={qty} onChange={e => setQty(e.target.value)}, and make it a number only where it is used: const count = Number(qty) || 0. Same for value={x || 0} and value={Number(x)}. Where a number has to stay between limits, clamp it in onBlur when the box is left, never while it is being typed in.${F(t.length)}`, Ne = /<textarea[\s/>]/, Pe = (e) => `${e}: a bare <textarea> is a box of one fixed height — what is typed past its last line is cut off and scrolls inside the box. Replace every one with <TextArea label="…" placeholder="…" value={…} onChange={e => …} /> imported from 'your-ai-workflow-firebase-os': the same props, and it grows a line at a time as the person types, so nothing they write is ever hidden.`, Fe = /<input\b[^>]*\btype\s*=\s*(?:\{\s*)?['"`]color['"`]/, Ie = /\b(?:bg|text|border|ring|fill|stroke)-\[\$\{/;
476
+ var Ne = (e, t) => `${e}: "${t[0]}…" turns what is being typed into a number on every keystroke, and Number('') is 0 — so deleting the last digit puts a 0 straight back and the box can never be emptied. Hold what was typed as TEXT (useState('') — empty, not 0) with value={qty} onChange={e => setQty(e.target.value)}, and make it a number only where it is used: const count = Number(qty) || 0. Same for value={x || 0} and value={Number(x)}. Where a number has to stay between limits, clamp it in onBlur when the box is left, never while it is being typed in.${F(t.length)}`, Pe = /<textarea[\s/>]/, Fe = (e) => `${e}: a bare <textarea> is a box of one fixed height — what is typed past its last line is cut off and scrolls inside the box. Replace every one with <TextArea label="…" placeholder="…" value={…} onChange={e => …} /> imported from 'your-ai-workflow-firebase-os': the same props, and it grows a line at a time as the person types, so nothing they write is ever hidden.`, Y = /<input\b[^>]*\btype\s*=\s*(?:\{\s*)?['"`]color['"`]/, Ie = /\b(?:bg|text|border|ring|fill|stroke)-\[\$\{/;
474
477
  function Le(e) {
475
478
  let t = [];
476
- Fe.test(e) && t.push("<input type=\"color\">");
479
+ Y.test(e) && t.push("<input type=\"color\">");
477
480
  let n = e.match(Ie);
478
481
  return n && t.push(`${n[0]}…}]`), t;
479
482
  }
480
- var Re = /\b(isMain|isPrimary|isCover|mainIndex|mainImageIndex|coverIndex|primaryIndex|mainImageId|mainPictureId)\b/, ze = (e) => /<UploadBox\b/.test(e) && !/<UploadBox\b[^>]*\bsortable\b/.test(e) ? e.match(Re)?.[1] ?? null : null, Be = (e, t) => `${e}: ${t} keeps which picture is the MAIN one beside the list of pictures, and the two fall out of step — remove the main picture and it points at the wrong one, or at none. The set is the app's own upload box with one more word: <UploadBox multiple max={10} sortable value={pictures} onChange={…} label="Photos" />. Its tiles drag into order under a mouse and a finger, the FIRST picture is the main one and says so, and every other tile has "Make main". Keep ONE ordered list of links, read pictures[0] as the main picture everywhere, and delete ${t} and any move-left / move-right or star buttons of your own.`, Ve = "(?:qty|quantit(?:y|ies)|stock|inventory|onhand|instock|guests|seats|adults|children|kids|tickets|nights|rooms|units|portions|servings|copies|howmany)", X = (e) => RegExp(`\\b[\\w.]*${Ve}[\\w.]*(?<!\\.length)(?:\\s*(?:\\?\\?|\\|\\|)\\s*\\d+)?\\s*\\)?\\s*\\${e}\\s*1\\b|\\bset\\w*${Ve}\\w*\\s*\\(\\s*\\(?\\s*\\w+\\s*\\)?\\s*=>[^;\\n]{0,48}?\\${e}\\s*1\\b`, "i"), He = X("+"), Z = X("-"), Ue = (e) => {
483
+ var Re = /\b(isMain|isPrimary|isCover|mainIndex|mainImageIndex|coverIndex|primaryIndex|mainImageId|mainPictureId)\b/, ze = (e) => /<UploadBox\b/.test(e) && !/<UploadBox\b[^>]*\bsortable\b/.test(e) ? e.match(Re)?.[1] ?? null : null, Be = (e, t) => `${e}: ${t} keeps which picture is the MAIN one beside the list of pictures, and the two fall out of step — remove the main picture and it points at the wrong one, or at none. The set is the app's own upload box with one more word: <UploadBox multiple max={10} sortable value={pictures} onChange={…} label="Photos" />. Its tiles drag into order under a mouse and a finger, the FIRST picture is the main one and says so, and every other tile has "Make main". Keep ONE ordered list of links, read pictures[0] as the main picture everywhere, and delete ${t} and any move-left / move-right or star buttons of your own.`, Ve = "(?:qty|quantit(?:y|ies)|stock|inventory|onhand|instock|guests|seats|adults|children|kids|tickets|nights|rooms|units|portions|servings|copies|howmany)", He = (e) => RegExp(`\\b[\\w.]*${Ve}[\\w.]*(?<!\\.length)(?:\\s*(?:\\?\\?|\\|\\|)\\s*\\d+)?\\s*\\)?\\s*\\${e}\\s*1\\b|\\bset\\w*${Ve}\\w*\\s*\\(\\s*\\(?\\s*\\w+\\s*\\)?\\s*=>[^;\\n]{0,48}?\\${e}\\s*1\\b`, "i"), Ue = He("+"), We = He("-"), Ge = (e) => {
481
484
  if (/<NumberStepper\b/.test(e) || !/<button\b/i.test(e)) return null;
482
- let t = e.match(He);
483
- return t && Z.test(e) ? t[0].replace(/\s+/g, " ").trim() : null;
484
- }, We = (e, t) => `${e}: "${t}" — a count taken up and down by a − and a + this page drew itself: small under a thumb, nowhere to type a bigger number, and every press a write of its own. It is the app's own piece, imported from 'your-ai-workflow-firebase-os': <NumberStepper label="Stock" name={variant.name} value={variant.stock} min={0} onChange={(stock) => …} onSettled={(stock, from) => …} />. onChange fires on every press and is for the SCREEN (set the page's own state); onSettled fires ONCE when they have stopped, with what it is now and what it was — put the save there, and the history line if the page keeps one, so five presses are one write. Give it max wherever there is a most (what is in stock). Delete the two buttons, the number between them and their handlers; change nothing else on the page.`, Ge = /\bfrom\s+['"](recharts|chart\.js|react-chartjs-2|victory|d3(?:-[\w-]+)?|@nivo\/[\w-]+|apexcharts|react-apexcharts|echarts|echarts-for-react|@visx\/[\w-]+|chartist|plotly\.js|react-plotly\.js)['"]/, Ke = /\b(?:width|height)\s*:\s*`\$\{[^`]{1,120}\}%`/, qe = (e) => {
485
- let t = e.match(Ge);
485
+ let t = e.match(Ue);
486
+ return t && We.test(e) ? t[0].replace(/\s+/g, " ").trim() : null;
487
+ }, Ke = (e, t) => `${e}: "${t}" — a count taken up and down by a − and a + this page drew itself: small under a thumb, nowhere to type a bigger number, and every press a write of its own. It is the app's own piece, imported from 'your-ai-workflow-firebase-os': <NumberStepper label="Stock" name={variant.name} value={variant.stock} min={0} onChange={(stock) => …} onSettled={(stock, from) => …} />. onChange fires on every press and is for the SCREEN (set the page's own state); onSettled fires ONCE when they have stopped, with what it is now and what it was — put the save there, and the history line if the page keeps one, so five presses are one write. Give it max wherever there is a most (what is in stock). Delete the two buttons, the number between them and their handlers; change nothing else on the page.`, qe = /\bfrom\s+['"](recharts|chart\.js|react-chartjs-2|victory|d3(?:-[\w-]+)?|@nivo\/[\w-]+|apexcharts|react-apexcharts|echarts|echarts-for-react|@visx\/[\w-]+|chartist|plotly\.js|react-plotly\.js)['"]/, Je = /\b(?:width|height)\s*:\s*`\$\{[^`]{1,120}\}%`/, Ye = (e) => {
488
+ let t = e.match(qe);
486
489
  if (t) return t[1];
487
490
  if (/<Bars\b/.test(e) || !/\buseTallies\s*\(/.test(e)) return null;
488
- let n = e.match(Ke);
491
+ let n = e.match(Je);
489
492
  return n ? n[0].replace(/\s+/g, " ") : null;
490
- }, Je = (e, t) => `${e}: "${t}" — a chart this page drew for itself: its colours are not the owner's theme, and a count is one running total, so there are no dates to draw it over. Numbers side by side are the app's own piece, imported from 'your-ai-workflow-firebase-os': <Bars items={[{ label: 'Views', value: counts.of(product.id).views }, { label: 'Added to bag', value: counts.of(product.id).added }]} size="sm" /> — one bar for each number, the number at its tip, format={money} for prices. Delete the chart, its import and whatever worked its lengths out; change nothing else on the page.`, Ye = /\b(?:window\s*\.\s*)?location\s*\.\s*(?:href\s*=(?!=)|assign\s*\(|replace\s*\()\s*['"`]\/(?!\/)[^'"`\s]*/, Xe = (e, t) => `${e}: "${t.replace(/\s+/g, " ")}…" goes to a page of this app by setting the browser's address, which throws the running app away and loads it again — whatever was open or half-typed is gone, and inside a preview the address is not there at all. Go there the app's way: const navigate = useNavigate() from 'react-router-dom' and navigate('/that-address') in the press, or <Link to="/that-address"> round the words. Keep the address, the words and everything else exactly as they are.`, Ze = /\b(?:UIDs?|user(?:'s)? ids?|document ids?|doc ids?|record ids?)\b/i, Qe = /\b(?:label|placeholder|title|aria-label|hint|helper)\s*=\s*(["'])([^"']{0,80})\1|>\s*([^<>{}=;()]{2,80}?)\s*[<{]/g, $e = (e) => {
493
+ }, Xe = (e, t) => `${e}: "${t}" — a chart this page drew for itself: its colours are not the owner's theme, and a count is one running total, so there are no dates to draw it over. Numbers side by side are the app's own piece, imported from 'your-ai-workflow-firebase-os': <Bars items={[{ label: 'Views', value: counts.of(product.id).views }, { label: 'Added to bag', value: counts.of(product.id).added }]} size="sm" /> — one bar for each number, the number at its tip, format={money} for prices. Delete the chart, its import and whatever worked its lengths out; change nothing else on the page.`, Ze = /\b(?:window\s*\.\s*)?location\s*\.\s*(?:href\s*=(?!=)|assign\s*\(|replace\s*\()\s*['"`]\/(?!\/)[^'"`\s]*/, Qe = (e, t) => `${e}: "${t.replace(/\s+/g, " ")}…" goes to a page of this app by setting the browser's address, which throws the running app away and loads it again — whatever was open or half-typed is gone, and inside a preview the address is not there at all. Go there the app's way: const navigate = useNavigate() from 'react-router-dom' and navigate('/that-address') in the press, or <Link to="/that-address"> round the words. Keep the address, the words and everything else exactly as they are.`, X = /\b(?:UIDs?|user(?:'s)? ids?|document ids?|doc ids?|record ids?)\b/i, $e = /\b(?:label|placeholder|title|aria-label|hint|helper)\s*=\s*(["'])([^"']{0,80})\1|>\s*([^<>{}=;()]{2,80}?)\s*[<{]/g, et = (e) => {
491
494
  let t = [];
492
- for (let n of e.matchAll(Qe)) {
495
+ for (let n of e.matchAll($e)) {
493
496
  let e = (n[2] ?? n[3] ?? "").trim();
494
- e && Ze.test(e) && !t.includes(e) && t.push(e);
497
+ e && X.test(e) && !t.includes(e) && t.push(e);
495
498
  }
496
499
  return t;
497
- }, et = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} puts an id in front of people — nobody knows one, nobody can type one, and it means nothing to whoever reads it. A person is CHOSEN by name: a <CustomSelect> of people from 'your-ai-workflow-firebase-os' (my team read from user_profiles on a page for the team, usePeople('<role>') on a page whose entries are sent to people), the pick saved as the uid AND the name on the record, and only the NAME ever drawn. A record is pointed at by pressing it, never by typing its id. Delete every box that asks for an id and every id shown on the page; change nothing else.`, tt = (e, t) => `${e}: ${P(t)} — a colour that is DATA (a product's shade) picked or drawn by hand.${t.some((e) => e.startsWith("<input")) ? " The browser draws its colour well in its own chrome, a grey rectangle no theme class reaches, with nowhere to type a hex." : ""}${t.some((e) => !e.startsWith("<input")) ? " A class built from a value does not exist when Tailwind compiles the page, so every chip it draws is blank." : ""} Both are the app's own pieces, imported from 'your-ai-workflow-firebase-os': pick with <SwatchField label="Shades" multiple max={6} value={variant.shades} onChange={(shades) => …} /> — it hands back clean hex strings, a list with multiple and one string without — and show with <Swatch color={shade} name={variant.name} selected={…} onClick={…} />. Keep each shade as that one string on the record. This is not the hard-coded colour rule 8 forbids: that is about the DESIGN's colours, which stay the theme's.`, nt = /\bfit\s*=\s*(?:["']contain["']|\{\s*["'`]contain["'`]\s*\})/, rt = /(?:^|[\s"'`{:])(?:aspect-[\w[]|h-(?:\d|\[|full|screen|px\b)|inset-0\b)/, it = /(?:^|[\s"'`{:])(?:aspect-[\w[]|h-(?:\d|\[|full|screen))/, at = /(?:^|[\s"'`{:])w-auto\b/, ot = /\bsrc\s*=\s*\{[^}]*\[\s*(?:\d+|[A-Za-z_$][\w$]*)\s*\]/, st = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=[^;\n]*\[\s*(?:\d+|[A-Za-z_$][\w$]*)\s*\]/g, ct = /\bsrc\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}/, lt = /logo|brand|partner|sponsor|client|badge|avatar|rounded-full/i, ut = (e, t) => {
500
+ }, tt = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} puts an id in front of people — nobody knows one, nobody can type one, and it means nothing to whoever reads it. A person is CHOSEN by name: a <CustomSelect> of people from 'your-ai-workflow-firebase-os' (my team read from user_profiles on a page for the team, usePeople('<role>') on a page whose entries are sent to people), the pick saved as the uid AND the name on the record, and only the NAME ever drawn. A record is pointed at by pressing it, never by typing its id. Delete every box that asks for an id and every id shown on the page; change nothing else.`, nt = (e, t) => `${e}: ${P(t)} — a colour that is DATA (a product's shade) picked or drawn by hand.${t.some((e) => e.startsWith("<input")) ? " The browser draws its colour well in its own chrome, a grey rectangle no theme class reaches, with nowhere to type a hex." : ""}${t.some((e) => !e.startsWith("<input")) ? " A class built from a value does not exist when Tailwind compiles the page, so every chip it draws is blank." : ""} Both are the app's own pieces, imported from 'your-ai-workflow-firebase-os': pick with <SwatchField label="Shades" multiple max={6} value={variant.shades} onChange={(shades) => …} /> — it hands back clean hex strings, a list with multiple and one string without — and show with <Swatch color={shade} name={variant.name} selected={…} onClick={…} />. Keep each shade as that one string on the record. This is not the hard-coded colour rule 8 forbids: that is about the DESIGN's colours, which stay the theme's.`, rt = /\bfit\s*=\s*(?:["']contain["']|\{\s*["'`]contain["'`]\s*\})/, it = /(?:^|[\s"'`{:])(?:aspect-[\w[]|h-(?:\d|\[|full|screen|px\b)|inset-0\b)/, at = /(?:^|[\s"'`{:])(?:aspect-[\w[]|h-(?:\d|\[|full|screen))/, ot = /(?:^|[\s"'`{:])w-auto\b/, st = /\bsrc\s*=\s*\{[^}]*\[\s*(?:\d+|[A-Za-z_$][\w$]*)\s*\]/, ct = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=[^;\n]*\[\s*(?:\d+|[A-Za-z_$][\w$]*)\s*\]/g, lt = /\bsrc\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}/, ut = /logo|brand|partner|sponsor|client|badge|avatar|rounded-full/i, dt = (e, t) => {
498
501
  let n = 0;
499
502
  for (let r = t - 1; r >= 0; r--) {
500
503
  let t = e[r];
@@ -509,18 +512,18 @@ var Re = /\b(isMain|isPrimary|isCover|mainIndex|mainImageIndex|coverIndex|primar
509
512
  }
510
513
  return !1;
511
514
  };
512
- function dt(e) {
515
+ function ft(e) {
513
516
  let t = [], n = /* @__PURE__ */ new Set();
514
- for (let t of e.matchAll(st)) n.add(t[1]);
517
+ for (let t of e.matchAll(ct)) n.add(t[1]);
515
518
  for (let r of e.matchAll(/<Picture\b/g)) {
516
- let i = r.index ?? 0, a = Y(e, i);
517
- if (!/\bsrc\s*=\s*\{/.test(a) || lt.test(a)) continue;
518
- let o = ct.exec(a)?.[1], s = ot.test(a) || !!o && n.has(o) || ut(e, i), c = nt.test(a) && rt.test(a), l = !nt.test(a) && it.test(a) && !at.test(a) && s;
519
+ let i = r.index ?? 0, a = Ae(e, i);
520
+ if (!/\bsrc\s*=\s*\{/.test(a) || ut.test(a)) continue;
521
+ let o = lt.exec(a)?.[1], s = st.test(a) || !!o && n.has(o) || dt(e, i), c = rt.test(a) && it.test(a), l = !rt.test(a) && at.test(a) && !ot.test(a) && s;
519
522
  (c || l) && t.push(a.replace(/\s+/g, " ").slice(0, 96).trim());
520
523
  }
521
524
  return t;
522
525
  }
523
- var ft = (e, t) => `${e}: "${t[0]}…" frames each picture in a box of a shape you chose — the picture cropped to it, or sat small inside it with the box's tint showing round it — where in a gallery, a shop, a portfolio or any grid or row of pictures every frame is cut to its OWN picture's shape and the picture fills it exactly. Take the aspect-…/h-… off it (and out of any box shaped round it), take off fit="contain" if it has one, and give it only its width and rounding — <Picture src={p.photo} alt={p.name} className="w-full rounded-2xl" /> — and it takes the picture's own shape, held open before the picture lands, with nothing cropped. A row of thumbnails is the one place a height goes, with the width left to the picture (className="h-20 w-auto shrink-0"). Lay pictures of different shapes out as columns that keep their own heights (columns-2 md:columns-3 gap-6, every tile break-inside-avoid mb-6), never rows of equal boxes.${F(t.length)}`, pt = /\[[^\][]*?\.\.\.\s*([A-Za-z_$][\w$]*)(?![\w$.(])[^\][]*?\.\.\.\s*\1(?![\w$])[^\][]*\]/g, mt = /\b([A-Za-z_$][\w$]*)\.concat\(\s*\1(?![\w$])[^)]*\)/g, ht = "(?:[A-Za-z_$][\\w$]*)?(?:[Pp]roduct|[Ii]tem|[Pp]hoto|[Pp]icture|[Ii]mage|[Gg]aller|[Cc]ard|[Ee]ntr|[Rr]ecord|[Ll]isting|[Ww]ork|[Pp]roject|[Pp]ost|[Ll]ook|[Pp]iece|[Cc]ollection)[\\w$]*", gt = RegExp(`(?:\\bArray\\.from\\(\\s*\\{\\s*length\\s*:[^}]*\\}\\s*,|(?:\\[\\s*\\.\\.\\.\\s*Array\\([^)]*\\)\\s*\\]|\\bArray\\([^)]*\\)(?:\\.fill\\([^)]*\\))?)\\s*\\.map\\()\\s*\\(?[^)=]*\\)?\\s*=>\\s*[({\\s.]*(${ht})\\s*\\[[^\\]]*%\\s*\\1\\.length\\s*\\]`, "g"), _t = (e, t) => {
526
+ var pt = (e, t) => `${e}: "${t[0]}…" frames each picture in a box of a shape you chose — the picture cropped to it, or sat small inside it with the box's tint showing round it — where in a gallery, a shop, a portfolio or any grid or row of pictures every frame is cut to its OWN picture's shape and the picture fills it exactly. Take the aspect-…/h-… off it (and out of any box shaped round it), take off fit="contain" if it has one, and give it only its width and rounding — <Picture src={p.photo} alt={p.name} className="w-full rounded-2xl" /> — and it takes the picture's own shape, held open before the picture lands, with nothing cropped. A row of thumbnails is the one place a height goes, with the width left to the picture (className="h-20 w-auto shrink-0"). Lay pictures of different shapes out as columns that keep their own heights (columns-2 md:columns-3 gap-6, every tile break-inside-avoid mb-6), never rows of equal boxes.${F(t.length)}`, mt = /\[[^\][]*?\.\.\.\s*([A-Za-z_$][\w$]*)(?![\w$.(])[^\][]*?\.\.\.\s*\1(?![\w$])[^\][]*\]/g, ht = /\b([A-Za-z_$][\w$]*)\.concat\(\s*\1(?![\w$])[^)]*\)/g, gt = "(?:[A-Za-z_$][\\w$]*)?(?:[Pp]roduct|[Ii]tem|[Pp]hoto|[Pp]icture|[Ii]mage|[Gg]aller|[Cc]ard|[Ee]ntr|[Rr]ecord|[Ll]isting|[Ww]ork|[Pp]roject|[Pp]ost|[Ll]ook|[Pp]iece|[Cc]ollection)[\\w$]*", _t = RegExp(`(?:\\bArray\\.from\\(\\s*\\{\\s*length\\s*:[^}]*\\}\\s*,|(?:\\[\\s*\\.\\.\\.\\s*Array\\([^)]*\\)\\s*\\]|\\bArray\\([^)]*\\)(?:\\.fill\\([^)]*\\))?)\\s*\\.map\\()\\s*\\(?[^)=]*\\)?\\s*=>\\s*[({\\s.]*(${gt})\\s*\\[[^\\]]*%\\s*\\1\\.length\\s*\\]`, "g"), vt = (e, t) => {
524
527
  let n = 0;
525
528
  for (let r = t; r < e.length; r++) {
526
529
  let t = e[r];
@@ -528,23 +531,23 @@ var ft = (e, t) => `${e}: "${t[0]}…" frames each picture in a box of a shape y
528
531
  else if ((t === "]" || t === "}") && --n === 0) return t === "]" ? r : -1;
529
532
  }
530
533
  return -1;
531
- }, vt = (e) => {
534
+ }, yt = (e) => {
532
535
  let t = [], n = 0, r = -1;
533
536
  for (let i = 0; i < e.length; i++) {
534
537
  let a = e[i];
535
538
  a === "{" || a === "[" ? (n === 0 && a === "{" && (r = i), n++) : (a === "}" || a === "]") && (n--, n === 0 && a === "}" && r >= 0 && (t.push(e.slice(r, i + 1)), r = -1));
536
539
  }
537
540
  return t;
538
- }, yt = /https?:\/\/|\.(?:jpe?g|png|webp|avif|gif)\b/i, bt = /\b(?:id|_id|key|sku)\s*:\s*(?:'[^']*'|"[^"]*"|`[^`]*`|[\w$.-]+)\s*,?/g;
539
- function xt(e) {
541
+ }, bt = /https?:\/\/|\.(?:jpe?g|png|webp|avif|gif)\b/i, xt = /\b(?:id|_id|key|sku)\s*:\s*(?:'[^']*'|"[^"]*"|`[^`]*`|[\w$.-]+)\s*,?/g;
542
+ function St(e) {
540
543
  let t = [];
541
544
  for (let n of e.matchAll(/\[\s*\{/g)) {
542
- let r = n.index ?? 0, i = _t(e, r);
545
+ let r = n.index ?? 0, i = vt(e, r);
543
546
  if (i < 0) continue;
544
547
  let a = /* @__PURE__ */ new Set();
545
- for (let n of vt(e.slice(r + 1, i))) {
546
- if (!yt.test(n)) continue;
547
- let e = n.replace(bt, "").replace(/\s+/g, "");
548
+ for (let n of yt(e.slice(r + 1, i))) {
549
+ if (!bt.test(n)) continue;
550
+ let e = n.replace(xt, "").replace(/\s+/g, "");
548
551
  if (a.has(e)) {
549
552
  let e = /\b(?:name|title)\s*:\s*(['"`])([^'"`]{1,40})\1/.exec(n);
550
553
  t.push(e ? e[2] : n.replace(/\s+/g, " ").slice(0, 60));
@@ -554,44 +557,44 @@ function xt(e) {
554
557
  }
555
558
  return t;
556
559
  }
557
- function St(e) {
560
+ function Ct(e) {
558
561
  let t = [
559
- pt,
560
562
  mt,
561
- gt
563
+ ht,
564
+ _t
562
565
  ].flatMap((t) => M(e, t)).map((e) => e.length > 64 ? `${e.slice(0, 64)}…` : e);
563
- return [...new Set([...t, ...xt(e)])];
566
+ return [...new Set([...t, ...St(e)])];
564
567
  }
565
- var Ct = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} ${t.length > 1 ? "show" : "shows"} the same thing again to fill out the page — every product, picture and entry appears ONCE (rule 15). Take the repeat out and let the layout fit however many there are: three products are three cards, in fewer columns or centred, never one of them shown twice. A customer who meets the same product twice sees a shop padded out, and wonders which one to buy.`, wt = /className\s*=\s*(?:\{\s*)?(?:`|"|')(?=[^`"']*\bfixed\b)(?=[^`"']*\binset-0\b)(?![^`"']*(?:pointer-events-none|(?:^|[\s:])-z-))[^`"']*(?:`|"|')/g;
566
- function Tt(e) {
567
- return [...e.matchAll(wt)].length;
568
+ var wt = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} ${t.length > 1 ? "show" : "shows"} the same thing again to fill out the page — every product, picture and entry appears ONCE (rule 15). Take the repeat out and let the layout fit however many there are: three products are three cards, in fewer columns or centred, never one of them shown twice. A customer who meets the same product twice sees a shop padded out, and wonders which one to buy.`, Tt = /className\s*=\s*(?:\{\s*)?(?:`|"|')(?=[^`"']*\bfixed\b)(?=[^`"']*\binset-0\b)(?![^`"']*(?:pointer-events-none|(?:^|[\s:])-z-))[^`"']*(?:`|"|')/g;
569
+ function Et(e) {
570
+ return [...e.matchAll(Tt)].length;
568
571
  }
569
- var Et = (e, t) => `${e}: ${t > 1 ? `${t} windows here are` : "a window here is"} drawn by hand — a \`fixed inset-0\` layer of the page's own. Nobody can click out of one, Escape does nothing, the page behind keeps scrolling, and anything else the page draws on top (a confirmation, a second window) lands underneath it. ${F(t)}Replace ${t > 1 ? "each" : "it"} with <Popup open={…} onClose={() => …} title="…"> … </Popup> imported from 'your-ai-workflow-firebase-os' — the same state that opens it now, what is inside it unchanged; side="right" for a basket that slides in from the edge, size="lg" for a product, footer={…} for its one main button; a picture opened large out of a set is <Lightbox pictures={photos} index={at} onIndex={setAt} onClose={() => setAt(null)} /> — and delete the layer, its backdrop, its own close button and the AnimatePresence round it: Popup closes on a click outside, on Escape and on its ✕, and eases in and out by itself (rule 30).`, Dt = /\b(?:set|show|hide|add|push|use)?[A-Za-z]*(?:[Tt]oast|[Ss]nackbar)[A-Za-z]*\b/g, Ot = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} a confirmation this page draws for itself — and one drawn by a page lands UNDER the window that was pressed, so nobody sees it: somebody presses Add to bag, sees nothing happen, presses again, and finds five in their basket. Delete it — the state, the timer and what it draws — and at the moment the thing is done call say('Added to your bag', { action: { label: 'View bag', onClick: () => … } }) imported from 'your-ai-workflow-firebase-os': the app's own brief line, above every window, gone by itself. Let the button that was pressed say it too for a moment ("Added"), and keep the number it changed somewhere that is always on screen (rule 30).`, kt = /['"`]pub_([A-Za-z0-9_]+)['"`]/g, At = /\b(?:setDoc|addDoc|updateDoc|deleteDoc|writeBatch|runTransaction)\s*\(/;
570
- function jt(e) {
571
- if (!At.test(e)) return [];
572
+ var Dt = (e, t) => `${e}: ${t > 1 ? `${t} windows here are` : "a window here is"} drawn by hand — a \`fixed inset-0\` layer of the page's own. Nobody can click out of one, Escape does nothing, the page behind keeps scrolling, and anything else the page draws on top (a confirmation, a second window) lands underneath it. ${F(t)}Replace ${t > 1 ? "each" : "it"} with <Popup open={…} onClose={() => …} title="…"> … </Popup> imported from 'your-ai-workflow-firebase-os' — the same state that opens it now, what is inside it unchanged; side="right" for a basket that slides in from the edge, size="lg" for a product, footer={…} for its one main button; a picture opened large out of a set is <Lightbox pictures={photos} index={at} onIndex={setAt} onClose={() => setAt(null)} /> — and delete the layer, its backdrop, its own close button and the AnimatePresence round it: Popup closes on a click outside, on Escape and on its ✕, and eases in and out by itself (rule 30).`, Ot = /\b(?:set|show|hide|add|push|use)?[A-Za-z]*(?:[Tt]oast|[Ss]nackbar)[A-Za-z]*\b/g, kt = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} a confirmation this page draws for itself — and one drawn by a page lands UNDER the window that was pressed, so nobody sees it: somebody presses Add to bag, sees nothing happen, presses again, and finds five in their basket. Delete it — the state, the timer and what it draws — and at the moment the thing is done call say('Added to your bag', { action: { label: 'View bag', onClick: () => … } }) imported from 'your-ai-workflow-firebase-os': the app's own brief line, above every window, gone by itself. Let the button that was pressed say it too for a moment ("Added"), and keep the number it changed somewhere that is always on screen (rule 30).`, At = /['"`]pub_([A-Za-z0-9_]+)['"`]/g, jt = /\b(?:setDoc|addDoc|updateDoc|deleteDoc|writeBatch|runTransaction)\s*\(/;
573
+ function Mt(e) {
574
+ if (!jt.test(e)) return [];
572
575
  let t = /* @__PURE__ */ new Set();
573
- for (let n of e.matchAll(kt)) t.add(n[1]);
576
+ for (let n of e.matchAll(At)) t.add(n[1]);
574
577
  return [...t].filter((t) => t !== "bookings" && t !== "files" && RegExp(`['"\`](?:mem|admin|role_[A-Za-z0-9]+)_${t}['"\`]`).test(e));
575
578
  }
576
- var Mt = (e, t) => RegExp(`['"\`]((?:mem|admin|role_[A-Za-z0-9]+)_${t})['"\`]`).exec(e)?.[1] || `mem_${t}`, Nt = (e, t, n) => {
577
- let r = Mt(t, n[0]);
579
+ var Nt = (e, t) => RegExp(`['"\`]((?:mem|admin|role_[A-Za-z0-9]+)_${t})['"\`]`).exec(e)?.[1] || `mem_${t}`, Pt = (e, t, n) => {
580
+ let r = Nt(t, n[0]);
578
581
  return `${e}: this page writes the public copy in ${P(n.map((e) => `pub_${e}`))} itself, and a copy written by a page is how the page that reads it comes to show every thing TWICE: the page's own list holds things the server does not — a record drawn at once under a made-up id while its save is on its way, a list remembered in localStorage, a copy made with addDoc — and a loop that copies "whatever has no copy yet" puts each into pub_${n[0]} as a thing of its own, beside the real one that lands a moment later. The copy is the app's to keep. Delete every write to pub_${n[0]} from this file — on a save, on a delete, in the catch-up effect, where an order is applied — with the helper that strips fields for it, and say once at the top of the component: usePublicCopy('${r}') imported from 'your-ai-workflow-firebase-os', adding { leaveOut: ['…'] } for the fields that helper stripped other than who made the record. The app then keeps pub_${n[0]} matching ${r} while this page is open — each record whole, once, under its own id, only once the server has it — and clears out the repeats already there. Write ${r} exactly as now, never draw a new record under an id made up in the page, and change nothing else (rule 11).`;
579
- }, Pt = /\bactions\s*\??\.\s*(?:records|loading|error|saveRecord|updateRecord|deleteRecord|collection|user)\b/, Ft = (e, t, n) => {
582
+ }, Ft = /\bactions\s*\??\.\s*(?:records|loading|error|saveRecord|updateRecord|deleteRecord|collection|user)\b/, It = (e, t, n) => {
580
583
  let r = n.match(/['"](pub_[a-z0-9_]+)['"]/)?.[1], i = r ? `'${r}'` : "'pub_<the other page's collection>' — the sentence that built this page names it";
581
584
  return `${e}: ${t.replace(/\s+/g, "")} is read on a page anyone can open, and such a page is handed no actions at all — actions?.records is empty for ever here, so the page shows nothing to every visitor. What it shows is the pub_ copy the other page's app keeps, read by NAME: import { db } from 'your-ai-workflow-firebase-os' and onSnapshot(collection(db, ${i}), …) from 'firebase/firestore' at the top of the component, a loading state until the first answer lands, and those records handed to everything below that asked for actions.records (useStock(records) included). Delete every actions. read from this file (rule 11).`;
582
- }, It = (e) => /\buseStock\s*\(/.test(e) && /\b(?:saveRecord|updateRecord)\b/.test(e) && !/\busePublicCopy\s*\(/.test(e), Lt = (e) => `${e}: this page keeps the products a shop sells — useStock() says so — and keeps NO public copy of them: a shop anyone can open reads only a pub_ collection, and nothing writes one, so it shows no products at all. Say once at the top of the component usePublicCopy(actions.collection) imported from 'your-ai-workflow-firebase-os' (actions.collection is this page's own collection; add { leaveOut: ['…'] } for what is the owner's own business) — the app then keeps the pub_ copy while this page is open, whole, once, under each record's own id. Write nothing to pub_ yourself, and change nothing else (rule 11).`, Rt = /<(?:motion\.)?h1[\s>]/, zt = /\bfrom\s+['"`]\.\.?\/[^'"`]*['"`]/, Bt = /<(?:div|section|main|article|header)[\s>]/, Vt = (e) => ne(e) && Bt.test(e) && !Rt.test(e) && !zt.test(e), Ht = (e) => `${e}: this is a page of the website and it has no <h1> — nothing on it says what page this is. A page anyone can open gets no heading from the app, so its content starts with ONE <h1>: the page's own name, or my line for it, sized to suit the page, never as big as it will go — a calm one (text-2xl md:text-4xl font-semibold tracking-tight text-foreground) on a page that is about its things, a display line only on a page that opens on a big picture or a band of colour — with at most one quiet line under it and clear room over it (pt-10 md:pt-16), never pressed up under the menu. SectionHeading is an <h2> — right for the sections below, never the page's title. Add the <h1> at the top and change nothing else (rule 30).`, Ut = RegExp("className\\s*=\\s*(?:\\{\\s*)?(?:`|\"|')([^`\"']*\\bgrid\\b[^`\"']*)(?:`|\"|')\\s*\\}?\\s*>\\s*\\{\\s*(" + ht + ")\\s*\\.(?:slice\\([^)]*\\)\\s*\\.)?map\\(", "g");
583
- function Wt(e) {
585
+ }, Lt = (e) => /\buseStock\s*\(/.test(e) && /\b(?:saveRecord|updateRecord)\b/.test(e) && !/\busePublicCopy\s*\(/.test(e), Rt = (e) => `${e}: this page keeps the products a shop sells — useStock() says so — and keeps NO public copy of them: a shop anyone can open reads only a pub_ collection, and nothing writes one, so it shows no products at all. Say once at the top of the component usePublicCopy(actions.collection) imported from 'your-ai-workflow-firebase-os' (actions.collection is this page's own collection; add { leaveOut: ['…'] } for what is the owner's own business) — the app then keeps the pub_ copy while this page is open, whole, once, under each record's own id. Write nothing to pub_ yourself, and change nothing else (rule 11).`, zt = /<(?:motion\.)?h1[\s>]/, Bt = /\bfrom\s+['"`]\.\.?\/[^'"`]*['"`]/, Vt = /<(?:div|section|main|article|header)[\s>]/, Ht = (e) => U(e) && Vt.test(e) && !zt.test(e) && !Bt.test(e), Ut = (e) => `${e}: this is a page of the website and it has no <h1> — nothing on it says what page this is. A page anyone can open gets no heading from the app, so its content starts with ONE <h1>: the page's own name, or my line for it, sized to suit the page, never as big as it will go — a calm one (text-2xl md:text-4xl font-semibold tracking-tight text-foreground) on a page that is about its things, a display line only on a page that opens on a big picture or a band of colour — with at most one quiet line under it and clear room over it (pt-10 md:pt-16), never pressed up under the menu. SectionHeading is an <h2> — right for the sections below, never the page's title. Add the <h1> at the top and change nothing else (rule 30).`, Wt = RegExp("className\\s*=\\s*(?:\\{\\s*)?(?:`|\"|')([^`\"']*\\bgrid\\b[^`\"']*)(?:`|\"|')\\s*\\}?\\s*>\\s*\\{\\s*(" + gt + ")\\s*\\.(?:slice\\([^)]*\\)\\s*\\.)?map\\(", "g");
586
+ function Gt(e) {
584
587
  let t = [];
585
- for (let n of e.matchAll(Ut)) {
588
+ for (let n of e.matchAll(Wt)) {
586
589
  let r = n[1];
587
590
  if (!(/(?:^|\s)grid-cols-1(?:\s|$)/.test(r) || !/(?:^|\s)grid-cols-\d/.test(r))) continue;
588
- let i = (n.index ?? 0) + n[0].length - 1, a = Zi(e, i);
591
+ let i = (n.index ?? 0) + n[0].length - 1, a = Q(e, i);
589
592
  a < 0 || !/<Picture\b/.test(e.slice(i, a)) || t.push(`{${n[2]}.map(…)}`);
590
593
  }
591
594
  return [...new Set(t)];
592
595
  }
593
- var Gt = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} laid out ONE to a row on a phone, so every card is the whole width of the screen and a handful of things is a long scroll — and on a wide screen the cards are as oversized. A set of cards is two across on a phone and more as the screen widens: className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6" (pictures of mixed shapes: "columns-2 md:columns-3 xl:columns-4 gap-4", each tile break-inside-avoid mb-4). Change that one className and nothing else: every card keeps everything it holds, exactly as it looks now — its picture, its words, its swatches or choices, its buttons — and nothing new goes round the grid (rule 30).`, Kt = /\bdraggable(?=[\s/>=])(?!\s*=\s*\{\s*false\s*\})|\bonDragStart\s*=/, qt = /\bfrom\s+['"`](react-beautiful-dnd|@hello-pangea\/dnd|@dnd-kit\/[^'"`]+|react-dnd(?:-[^'"`]+)?|sortablejs|react-sortablejs|react-draggable)['"`]/g, Jt = (e, t) => `${e}: ${t.length ? `${P(t)} ${t.length > 1 ? "are" : "is"} not part of this app — the import resolves in a preview and fails when the app is published — and dragging here is already a piece of the app.` : "this page drags things with the browser's own drag and drop (draggable, onDragStart, onDrop) — nothing moves under a FINGER, a keyboard cannot reach it, and a picture inside the card starts a drag of its own and cancels the card's."} Both kinds of dragging are imported from 'your-ai-workflow-firebase-os'. Things moved BETWEEN stages — orders, leads, applications — are <Pipeline stages={[{ id: 'new', label: 'New' }, …]} items={orders} stageOf={(o) => o.stage} onMove={(o, stage) => updateDoc(doc(db, '…', o.id), { stage })} onOpen={(o) => setChosen(o)} card={(o) => <>…what one card says…</>} />. A set put IN ORDER — pictures, lessons, rows — is <Sortable items={pictures} onReorder={(next) => save(next)} className="grid grid-cols-3 gap-3" item={(p) => …} />. Delete every drag handler, the dragged / drag-over state and any "move left / move right" buttons added to make up for them: the pieces work under a mouse, a finger and the arrow keys, show where the thing will land, and put it back with a sentence when the save fails (rule 31).`;
594
- function Yt(e) {
596
+ var Kt = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} laid out ONE to a row on a phone, so every card is the whole width of the screen and a handful of things is a long scroll — and on a wide screen the cards are as oversized. A set of cards is two across on a phone and more as the screen widens: className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6" (pictures of mixed shapes: "columns-2 md:columns-3 xl:columns-4 gap-4", each tile break-inside-avoid mb-4). Change that one className and nothing else: every card keeps everything it holds, exactly as it looks now — its picture, its words, its swatches or choices, its buttons — and nothing new goes round the grid (rule 30).`, qt = /\bdraggable(?=[\s/>=])(?!\s*=\s*\{\s*false\s*\})|\bonDragStart\s*=/, Jt = /\bfrom\s+['"`](react-beautiful-dnd|@hello-pangea\/dnd|@dnd-kit\/[^'"`]+|react-dnd(?:-[^'"`]+)?|sortablejs|react-sortablejs|react-draggable)['"`]/g, Yt = (e, t) => `${e}: ${t.length ? `${P(t)} ${t.length > 1 ? "are" : "is"} not part of this app — the import resolves in a preview and fails when the app is published — and dragging here is already a piece of the app.` : "this page drags things with the browser's own drag and drop (draggable, onDragStart, onDrop) — nothing moves under a FINGER, a keyboard cannot reach it, and a picture inside the card starts a drag of its own and cancels the card's."} Both kinds of dragging are imported from 'your-ai-workflow-firebase-os'. Things moved BETWEEN stages — orders, leads, applications — are <Pipeline stages={[{ id: 'new', label: 'New' }, …]} items={orders} stageOf={(o) => o.stage} onMove={(o, stage) => updateDoc(doc(db, '…', o.id), { stage })} onOpen={(o) => setChosen(o)} card={(o) => <>…what one card says…</>} />. A set put IN ORDER — pictures, lessons, rows — is <Sortable items={pictures} onReorder={(next) => save(next)} className="grid grid-cols-3 gap-3" item={(p) => …} />. Delete every drag handler, the dragged / drag-over state and any "move left / move right" buttons added to make up for them: the pieces work under a mouse, a finger and the arrow keys, show where the thing will land, and put it back with a sentence when the save fails (rule 31).`;
597
+ function Xt(e) {
595
598
  let t = 0;
596
599
  for (let n of e.matchAll(/<table[\s>]/g)) {
597
600
  let r = e.indexOf("</table>", n.index ?? 0);
@@ -599,12 +602,12 @@ function Yt(e) {
599
602
  }
600
603
  return t;
601
604
  }
602
- var Xt = (e, t) => `${e}: ${t > 1 ? `${t} lists of records here are each` : "a list of records here is"} drawn as a <table> — on a phone that is a strip of small print to drag sideways, with the button that matters off the edge, and its rows cannot be reached by a keyboard or sorted. Replace ${t > 1 ? "each" : "it"} with <DataTable rows={orders} loading={loading} columns={[{ key: 'number', label: 'Order', value: (o) => o.number }, { key: 'total', label: 'Total', value: (o) => o.total, cell: (o) => money(o.total), align: 'right' }]} onOpen={(o) => setChosen(o)} actions={(o) => …} search="Search orders" empty="No orders yet" /> imported from 'your-ai-workflow-firebase-os': a table whose headings sort on a wide screen, one readable block a record on a phone, soft rows while it loads. \`value\` is the plain value a column sorts and searches by, \`cell\` what it draws when that is more than the value, and \`phone: false\` leaves a column off a phone. Delete the <table>, its overflow-x-auto wrapper and any sorting or searching written for it (rule 31).`, Zt = /\bconst\s*\[\s*((?:[A-Za-z_$][\w$]*)?[sS]tep(?:Index|Idx|Number|No|Id)?)\s*,\s*[A-Za-z_$][\w$]*\s*\]\s*=\s*useState\b/g;
603
- function Qt(e) {
604
- return /\buseSteps\s*\(|\.get\(\s*['"`][\w-]*step[\w-]*['"`]\s*\)/i.test(e) ? [] : M(e, Zt, 1);
605
+ var Zt = (e, t) => `${e}: ${t > 1 ? `${t} lists of records here are each` : "a list of records here is"} drawn as a <table> — on a phone that is a strip of small print to drag sideways, with the button that matters off the edge, and its rows cannot be reached by a keyboard or sorted. Replace ${t > 1 ? "each" : "it"} with <DataTable rows={orders} loading={loading} columns={[{ key: 'number', label: 'Order', value: (o) => o.number }, { key: 'total', label: 'Total', value: (o) => o.total, cell: (o) => money(o.total), align: 'right' }]} onOpen={(o) => setChosen(o)} actions={(o) => …} search="Search orders" empty="No orders yet" /> imported from 'your-ai-workflow-firebase-os': a table whose headings sort on a wide screen, one readable block a record on a phone, soft rows while it loads. \`value\` is the plain value a column sorts and searches by, \`cell\` what it draws when that is more than the value, and \`phone: false\` leaves a column off a phone. Delete the <table>, its overflow-x-auto wrapper and any sorting or searching written for it (rule 31).`, Qt = /\bconst\s*\[\s*((?:[A-Za-z_$][\w$]*)?[sS]tep(?:Index|Idx|Number|No|Id)?)\s*,\s*[A-Za-z_$][\w$]*\s*\]\s*=\s*useState\b/g;
606
+ function $t(e) {
607
+ return /\buseSteps\s*\(|\.get\(\s*['"`][\w-]*step[\w-]*['"`]\s*\)/i.test(e) ? [] : M(e, Qt, 1);
605
608
  }
606
- var $t = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "keep" : "keeps"} the step somebody is on in useState, so RELOADING the page starts them again at step one, the browser's Back button leaves the page instead of going back a step, and nobody can be sent a link to the step they are stuck on. Replace it with const flow = useSteps([{ id: 'details', label: 'Details' }, { id: 'pictures', label: 'Pictures' }, { id: 'review', label: 'Review' }]) imported from 'your-ai-workflow-firebase-os' — flow.step is the step on screen and it lives in the web address (?step=pictures, beside the ?product=… already there), flow.next() and flow.back() move and the browser's Back button follows, flow.reset() once the thing is sent — and draw the line across the top with <StepBar {...flow.bar} /> in place of numbered circles of your own. What was typed on each step is still useVisitorState (rules 16, 19 and 31).`, en = /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+)/g, tn = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} put on the page with dangerouslySetInnerHTML — whatever anybody managed to type into that record then RUNS as code on every visitor's screen, and takes their session with it. Long writing somebody typed — a post, a description, a method — is <Article text={${t[0]}} /> imported from 'your-ai-workflow-firebase-os': a blank line is a paragraph, "# " a heading, "- " a list, **bold**, [words](link), and a picture on a line of its own as ![what it shows](link) — and it never reads HTML, so nothing typed can run. A short line is plain {text} (rule 31).`;
607
- function nn(e) {
609
+ var en = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "keep" : "keeps"} the step somebody is on in useState, so RELOADING the page starts them again at step one, the browser's Back button leaves the page instead of going back a step, and nobody can be sent a link to the step they are stuck on. Replace it with const flow = useSteps([{ id: 'details', label: 'Details' }, { id: 'pictures', label: 'Pictures' }, { id: 'review', label: 'Review' }]) imported from 'your-ai-workflow-firebase-os' — flow.step is the step on screen and it lives in the web address (?step=pictures, beside the ?product=… already there), flow.next() and flow.back() move and the browser's Back button follows, flow.reset() once the thing is sent — and draw the line across the top with <StepBar {...flow.bar} /> in place of numbered circles of your own. What was typed on each step is still useVisitorState (rules 16, 19 and 31).`, tn = /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+)/g, nn = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} put on the page with dangerouslySetInnerHTML — whatever anybody managed to type into that record then RUNS as code on every visitor's screen, and takes their session with it. Long writing somebody typed — a post, a description, a method — is <Article text={${t[0]}} /> imported from 'your-ai-workflow-firebase-os': a blank line is a paragraph, "# " a heading, "- " a list, **bold**, [words](link), and a picture on a line of its own as ![what it shows](link) — and it never reads HTML, so nothing typed can run. A short line is plain {text} (rule 31).`;
610
+ function rn(e) {
608
611
  let t = /* @__PURE__ */ new Set();
609
612
  for (let n of e.matchAll(/\b(?:function|const)\s+((?:format|to|as)(?:Currency|Price|Money|Amount|Usd|Eur|Gbp)|currencyFormat(?:ter)?|priceFormat(?:ter)?)\b/g)) t.add(`${n[1]}()`);
610
613
  /style\s*:\s*['"`]currency['"`]/.test(e) && t.add("Intl.NumberFormat(…, { style: 'currency' })");
@@ -620,8 +623,8 @@ function nn(e) {
620
623
  }
621
624
  return [...t];
622
625
  }
623
- var rn = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} ${t.length > 1 ? "write" : "writes"} a price out by hand. .toFixed throws on a price that was saved as text or is not there yet — and the whole page goes blank; a "$" typed in front is wrong the day the <PayButton> beside it charges in euros; and 1250 reads "$1250.00" while the Transactions screen says "$1,250.00" for the same order. Every price, total and fee is money(amount) imported from 'your-ai-workflow-firebase-os' — money(25) is "$25.00", money(1250, 'eur') is "€1,250.00", and something that is not a number is a quiet dash — with the SAME currency word the page passes to its PayButton. Delete the formatter of your own and every typed currency sign.${F(t.length)} (rule 31)`;
624
- function an(e, t) {
626
+ var an = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} ${t.length > 1 ? "write" : "writes"} a price out by hand. .toFixed throws on a price that was saved as text or is not there yet — and the whole page goes blank; a "$" typed in front is wrong the day the <PayButton> beside it charges in euros; and 1250 reads "$1250.00" while the Transactions screen says "$1,250.00" for the same order. Every price, total and fee is money(amount) imported from 'your-ai-workflow-firebase-os' — money(25) is "$25.00", money(1250, 'eur') is "€1,250.00", and something that is not a number is a quiet dash — with the SAME currency word the page passes to its PayButton. Delete the formatter of your own and every typed currency sign.${F(t.length)} (rule 31)`;
627
+ function Z(e, t) {
625
628
  let n = [], r = RegExp(`<((?:motion\\.)?(?:${t}))(?=[\\s/>])`, "g");
626
629
  for (let t of e.matchAll(r)) {
627
630
  let r = (t.index ?? 0) + t[0].length, i = 0, a = "";
@@ -651,7 +654,7 @@ function an(e, t) {
651
654
  var on = /onClick\s*=\s*\{\s*\(?\s*([A-Za-z_$][\w$]*)\s*\)?\s*=>\s*\{?\s*\1\s*\.\s*(?:stopPropagation|preventDefault)\s*\(\s*\)\s*;?\s*\}?\s*\}/;
652
655
  function sn(e) {
653
656
  let t = /* @__PURE__ */ new Set();
654
- for (let { tag: n, attrs: r } of an(e, "div|span|li|tr|td|p|h[1-6]|section|article|figure|header|footer|img|label")) /\bonClick\s*=/.test(r) && (/\brole\s*=/.test(r) || on.test(r) || n === "label" && /\bhtmlFor\s*=/.test(r) || t.add(`<${n} onClick>`));
657
+ for (let { tag: n, attrs: r } of Z(e, "div|span|li|tr|td|p|h[1-6]|section|article|figure|header|footer|img|label")) /\bonClick\s*=/.test(r) && (/\brole\s*=/.test(r) || on.test(r) || n === "label" && /\bhtmlFor\s*=/.test(r) || t.add(`<${n} onClick>`));
655
658
  return [...t];
656
659
  }
657
660
  var cn = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} pressed with a mouse and by nothing else — a keyboard never reaches ${t.length > 1 ? "them" : "it"} and a screen reader does not know ${t.length > 1 ? "they" : "it"} can be pressed. Whatever is pressed is a <button type="button" className="text-left …"> (a <Link to="…"> where it goes to another page), with the same classes and the same onClick. Where a whole card is pressable AND has buttons of its own inside it — a button cannot sit in a button — keep the element and give it role="button" tabIndex={0} onKeyDown={(e) => { if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); open(); } }}. A row of records that opens is <DataTable onOpen={…}>, and a card in a pipeline is <Pipeline onOpen={…}>, which do this themselves.${F(t.length)} (rule 30)`, ln = "(?![©®™])\\p{Extended_Pictographic}", un = "✕✖✗✘✓✔←→↑↓↗⟳↻⟲▶◀▼▲▸◂▾▴‹›»«★☆⌄", dn = RegExp(`^(?:${ln}|[${un}ו]|[\\uFE0F\\u200D\\s])+$`, "u"), fn = RegExp(`${ln}|[${un}×]`, "u"), pn = RegExp(`^([${un}])\\uFE0F?\\s+\\S`, "u"), mn = RegExp(`\\S\\s+([${un}])\\uFE0F?$`, "u");
@@ -676,7 +679,7 @@ function hn(e) {
676
679
  var gn = (e, t) => `${e}: ${P(t.map((e) => `"${e}"`))} ${t.length > 1 ? "are characters" : "is a character"} standing in for an icon. A character is drawn by the visitor's own phone — in ITS colours and ITS style, never the theme's, a different drawing on every device — and it is the first thing that makes a page look home-made. An icon is a lucide-react icon: import { X, Check, ArrowRight, ArrowLeft, ShoppingBag, Camera, Package, Star } from 'lucide-react' and draw <X className="w-4 h-4" /> for ✕, <Check /> for ✓, <ArrowRight /> and <ArrowLeft /> for → and ←, and the icon OF the thing for an emoji (🛍️ is <ShoppingBag />, 📷 is <Camera />, 📦 is <Package />) — beside the same words, inside the same button. A turning ⟳ is <Loader /> from 'your-ai-workflow-firebase-os'. And where rule 27 would not have put an icon at all, take it out and leave the words.${F(t.length)} (rule 30)`, _n = /(?:window\s*\.\s*)?(?:local|session)Storage\s*(?:\.\s*(?:get|set|remove)Item|\[)/g, vn = (e) => `${(e.split("/").pop() || "").replace(/\.[jt]sx?$/, "").replace(/Content$/, "").toLowerCase() || "page"}-draft`, yn = (e, t) => `${e}: ${P(t)} — this page keeps what somebody is half-way through in the browser's storage itself. Written by hand it comes apart the same way every time: the whole form is saved on every change, so the EMPTY form is stored the moment the page opens and a draft is offered back to somebody who has typed nothing; Discard empties the form, the next save puts the empty one straight back, and the message returns for ever. It also reads storage while the page is being drawn, so the first paint disagrees with the second, and it throws outright in a private window. The app keeps it instead: const { value, setValue, clear, ready } = useVisitorState('${vn(e)}', {…the empty form…}), imported from 'your-ai-workflow-firebase-os'. Write it with setValue as they type — never from an effect that runs on load — draw the empty form until ready turns true, and clear() the moment the save lands AND the moment they discard it. Offer a draft back only while ready is true and what is in it is not the empty form: nothing is kept until something is really typed, and what was discarded stays discarded. Take every line that touches storage out of this file (rule 19).`, bn = /\banimate-spin\b(?![-[])/, xn = (e) => `${e}: this page sets something spinning itself (animate-spin) — a ring of its own, a character, an icon — so its loading looks like no other screen of the app, and keeps turning for somebody who asked their device for less motion. Loading is the app's: <Loader /> for a ring in a line (size="sm" | "md" | "lg"), <Loader block label="Loading orders" /> in the space a list will take, <Skeleton className="h-24" /> — one per card, row or number still to come — wherever the shape is known, and <Button isLoading={saving}> for a button that is sending; all imported from 'your-ai-workflow-firebase-os'. Replace every animate-spin with the one that fits and delete the element it was on (rule 30).`, Sn = /(?<![\w-])text-\[(?:(?:[0-9]|1[01])(?:\.\d+)?px|0?\.(?:[0-6]\d*|7[0-4]\d*)r?em)\]/g, Cn = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} smaller than small print — on a phone nobody reads it, and it fails every accessibility check the owner's clients may run. The smallest words on any page are text-xs (12px), and anything somebody has to READ — a name, a price, a note, a status — is text-sm or larger. Change every one, and where that makes a card too full, say less on the card and the rest in its <Popup> — never shrink the words to fit.${F(t.length)} (rule 30)`;
677
680
  function wn(e, t) {
678
681
  let n = /* @__PURE__ */ new Set();
679
- for (let { end: r } of an(e, "Button|button")) {
682
+ for (let { end: r } of Z(e, "Button|button")) {
680
683
  if (e[r - 1] === "/") continue;
681
684
  let i = e.slice(r + 1, r + 80).match(/^\s*([^<>{}]{1,40}?)\s*<\/(?:motion\.)?(?:Button|button)\s*>/);
682
685
  i && t.test(i[1].trim()) && n.add(i[1].trim());
@@ -758,18 +761,18 @@ function Un(e) {
758
761
  }
759
762
  var Wn = (e, t) => `${e}: ${P(t)} — the lines of an order are keyed by their id, and an item's id is its PRODUCT's: the same thing bought in two colours or sizes is two lines with ONE key, so React draws one of them twice and drops the other, and the console fills with "Encountered two children with the same key". Key each line by its place in the list instead — items.map((item, i) => <div key={i}>…) — the lines of an order never move (rule 9).`, Gn = /function TutorialPopup\(\{ onClose \}[\s\S]*?\n\}\n/;
760
763
  function Kn(e, t) {
761
- let n = [], r = M(t, qt, 1);
762
- (r.length || Kt.test(t)) && n.push(["own-drag", Jt(e, r)]);
763
- let i = Yt(t);
764
- i && n.push(["mapped-table", Xt(e, i)]);
765
- let a = Qt(t);
766
- a.length && n.push(["step-state", $t(e, a)]);
764
+ let n = [], r = M(t, Jt, 1);
765
+ (r.length || qt.test(t)) && n.push(["own-drag", Yt(e, r)]);
766
+ let i = Xt(t);
767
+ i && n.push(["mapped-table", Zt(e, i)]);
768
+ let a = $t(t);
769
+ a.length && n.push(["step-state", en(e, a)]);
767
770
  let o = M(t, _n);
768
771
  o.length && n.push(["own-draft", yn(e, o)]);
769
- let s = M(t, en, 1);
770
- s.length && n.push(["raw-html", tn(e, s)]);
771
- let c = nn(t);
772
- c.length && n.push(["own-money", rn(e, c)]);
772
+ let s = M(t, tn, 1);
773
+ s.length && n.push(["raw-html", nn(e, s)]);
774
+ let c = rn(t);
775
+ c.length && n.push(["own-money", an(e, c)]);
773
776
  let l = sn(t);
774
777
  l.length && n.push(["dead-click", cn(e, l)]);
775
778
  let u = hn(t);
@@ -830,29 +833,29 @@ function _r(e) {
830
833
  });
831
834
  }, f = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Set();
832
835
  for (let { file: t } of e) if (R(t)) for (let e of p(I(t))) g.add(e);
833
- let _ = Ya(e), v = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Set();
836
+ let _ = ao(e), v = /* @__PURE__ */ new Map(), y = /* @__PURE__ */ new Set();
834
837
  for (let { file: t, source: n } of e) {
835
838
  if (!L(t)) continue;
836
839
  let e = I(t), r = h(e);
837
840
  if (r !== "tab" && r !== "page") continue;
838
- let i = z(n), a = B(i, r === "tab" ? "tabName" : "pageName")[0], o = a === void 0 ? null : V(a), s = [r === "tab" ? l(e) : u(e), ...o ? [U(o), U(o).replace(/_/g, "")] : []];
841
+ let i = z(n), a = B(i, r === "tab" ? "tabName" : "pageName")[0], o = a === void 0 ? null : V(a), s = [r === "tab" ? l(e) : u(e), ...o ? [W(o), W(o).replace(/_/g, "")] : []];
839
842
  if (r === "page") for (let e of s) y.add(e);
840
843
  let c = B(i, "template")[0], d = c === void 0 ? null : V(c);
841
- if (!(!d || !re.includes(d))) for (let e of s) v.set(e, d);
844
+ if (!(!d || !G.includes(d))) for (let e of s) v.set(e, d);
842
845
  }
843
846
  for (let { file: t, source: n } of e) {
844
847
  let r = I(t), i = z(n);
845
848
  if (L(t)) {
846
849
  let e = h(r);
847
850
  if (e === "shipped") {
848
- if (r === "theme.config.ts") for (let e of yi(n)) c("brand-gone", bi(t, e));
851
+ if (r === "theme.config.ts") for (let e of bi(n)) c("brand-gone", xi(t, e));
849
852
  continue;
850
853
  }
851
854
  if (e === "other") {
852
855
  s("config-name", `${t}: rename it to somethingTab.config.ts (a dashboard page), somethingPage.config.ts (a page for anyone), somethingForm.config.ts (a form) or somethingRegistration.config.ts (what people are asked when they register).`);
853
856
  continue;
854
857
  }
855
- if (!H(i) && !ne(i) && s("config-export", `${t}: export one const named ${m(r)}Config.`), e === "tab" || e === "page") {
858
+ if (!H(i) && !U(i) && s("config-export", `${t}: export one const named ${m(r)}Config.`), e === "tab" || e === "page") {
856
859
  let { first: n, count: a } = gr(i), o = n === void 0 ? null : V(n);
857
860
  if (a === 0 || o === null || !o.startsWith("/")) s("route", `${t}: route must be one string starting with /, e.g. route: '/onboarding'. A page has exactly one web address.`);
858
861
  else if (o.includes(":") || o.includes("*")) {
@@ -885,152 +888,158 @@ function _r(e) {
885
888
  if (e === "form") {
886
889
  let e = d(r), n = B(i, "systemName")[0];
887
890
  n !== void 0 && V(n) !== e && s("system-name", `${t}: systemName must be '${e}'.`);
888
- let a = ae(i);
889
- a && c("plain-address", oe(t, a));
891
+ let a = re(i);
892
+ a && c("plain-address", ie(t, a));
890
893
  }
891
894
  if (e === "registration") {
892
895
  let e = [...B(i, "key"), ...B(i, "name")].map(V).filter((e) => e !== null);
893
896
  e.length === 0 && s("registration-key", `${t}: every question needs a key — fields: [{ key: 'company', label: 'Company name', type: 'text', required: true, editable: true }].`);
894
- let n = e.filter((e) => Se.has(e));
897
+ let n = e.filter((e) => xe.has(e));
895
898
  n.length && s("registration-reserved", `${t}: '${n[0]}' is a key the app already keeps on every profile, and the security rules refuse a write that touches it — rename that question's key to something of its own, like '${n[0]}Given'.`);
896
- let r = B(i, "type").map(V).find((e) => e !== null && !ie.includes(e));
897
- r && s("registration-type", `${t}: type '${r}' is not one a registration question can be — use ${ie.join(", ")}.`), /['"`]select['"`]/.test(i) && !/\boptions\s*:/.test(i) && s("registration-options", `${t}: a select question needs options — options: ['Sole trader', 'Limited company'].`);
898
- let a = ae(i);
899
- a && c("plain-address", oe(t, a));
899
+ let r = B(i, "type").map(V).find((e) => e !== null && !ne.includes(e));
900
+ r && s("registration-type", `${t}: type '${r}' is not one a registration question can be — use ${ne.join(", ")}.`), /['"`]select['"`]/.test(i) && !/\boptions\s*:/.test(i) && s("registration-options", `${t}: a select question needs options — options: ['Sole trader', 'Limited company'].`);
901
+ let a = re(i);
902
+ a && c("plain-address", ie(t, a));
900
903
  }
901
904
  continue;
902
905
  }
903
906
  if (R(t)) {
904
- ne(i) || s("default-export", `${t}: add export default.`);
907
+ U(i) || s("default-export", `${t}: add export default.`);
905
908
  let n = p(r).map((e) => v.get(e)).find(Boolean), a = /<form\b/.test(i) || /\bonSubmit\b/.test(i);
906
909
  if (a && n) {
907
- let e = Ce[n];
910
+ let e = Se[n];
908
911
  i.includes(e.token) || c("preset-form", `${t}: this page is a ${n} and already has its own add and edit dialog, themed for light and dark — open it with ${e.call} instead of building a form here.`);
909
912
  } else a && !/ContactPopup/.test(i) && c("hand-form", `${t}: forms are *Form.config.ts files shown with <ContactPopup formId="…" />; a hand-written form never reaches the Inbox.`);
910
913
  let o = i.match(/['"`](?:(?:mem|user|admin|pub|case)_notifications|user_notices)['"`]/);
911
914
  o && s("bell-collection", `${t}: ${o[0]} is the bell's own collection and pages never touch it — delete that code and send the notification with notify({ toUid | toRole, title, body, link }) imported from 'your-ai-workflow-firebase-os'. If this page is about notices of its own, keep them somewhere else (user_page_notices).`);
912
915
  let l = i.match(/['"`](?:chat_threads|chat_files|mem_chat_notes|mem_chat_state)['"`]/);
913
916
  l && s("chat-collection", `${t}: ${l[0]} is not a collection any page may touch — delete that code. This app has no chat: where people should be able to write to the owner, use the built-in form, <ContactPopup formId="…" />, and what they send arrives in the Inbox. If this page keeps messages of its own, keep them somewhere else (mem_page_messages).`);
914
- let u = i.match(ji);
917
+ let u = i.match(Vi);
915
918
  u && s("chat-call", `${t}: ${u[0]} is not part of this app — the chat is gone, and nothing is exported under that name. Delete it, and where people should be able to write to the owner use the built-in form, <ContactPopup formId="…" />; what they send arrives in the Inbox.`), /['"`]user_phoneclaims['"`]/.test(i) && s("phone-claims", `${t}: user_phoneclaims is how the app keeps a two-step phone number to one account, and pages never touch it — keep this page's own records somewhere else (user_page_phones).`), /<style[\s>]/.test(i) && s("style-tag", `${t}: a <style> tag is global CSS and restyles every page of the app — delete it and style with theme classes (bg-background, text-foreground, glass-panel, btn-primary).`);
916
919
  let d = lr(i);
917
920
  d && c("screen-cap", `${t}: ${d} makes the page exactly one screen tall and strands everything below it on a phone — the page already scrolls, so let it be as long as it needs and drop the height and any overflow-hidden around it.`);
918
921
  let f = ir(i);
919
922
  f && c("narrow-page", ar(t, f));
920
- let m = pe.test(i), h = ve(i), g = h ? null : ce(i);
921
- h ? c("address-box", (m ? ue : le)(t, h)) : g && c("own-address", (m ? ue : le)(t, g));
922
- let b = ye(i);
923
- b && c("own-phone", (m ? xe : be)(t, b));
924
- let x = me(i);
925
- x && c("buyer-twice", he(t, x));
923
+ let m = fe.test(i), h = _e(i), g = h ? null : oe(i);
924
+ h ? c("address-box", (m ? ce : se)(t, h)) : g && c("own-address", (m ? ce : se)(t, g));
925
+ let b = ve(i);
926
+ b && c("own-phone", (m ? be : ye)(t, b));
927
+ let x = pe(i);
928
+ x && c("buyer-twice", me(t, x));
926
929
  let S = M(i, nr);
927
930
  S.length && c("owner-words", tr(t, S));
928
931
  let C = Xn(i);
929
932
  C.length && c("demo-data", Zn(t, C));
930
933
  let w = M(i, Qn);
931
934
  w.length && c("colour", er(t, w)), /<select[\s/>]/.test(i) && c("native-select", `${t}: a native <select> is drawn by the browser in its own chrome and no theme class can restyle it — replace it with <CustomSelect value={…} options={[{ value, label }]} onChange={(v) => …} /> imported from 'your-ai-workflow-firebase-os' (onChange receives the chosen value string, not an event).`);
932
- let T = Ei(i);
933
- T.length && s("unknown-import", Di(t, T));
934
- let E = ki(i);
935
- E && c("notify-as-toast", Ai(t, E)), Ne.test(i) && c("bare-textarea", Pe(t));
936
- let D = Le(i);
937
- D.length && c("own-swatch", tt(t, D));
938
- let O = ze(i);
939
- O && c("own-picture-set", Be(t, O));
940
- let k = Br(i) ? null : Ue(i);
941
- k && c("own-stepper", We(t, k));
942
- let A = qe(i);
943
- A && c("own-chart", Je(t, A));
944
- let j = i.match(Ye);
945
- j && c("full-reload", Xe(t, j[0]));
946
- let P = $e(i);
947
- P.length && c("typed-id", et(t, P));
948
- let F = M(i, we);
949
- F.length && c("media-tag", Te(t, F)), Ee.test(i) && s("bare-img", qn(t));
950
- let I = dt(i);
951
- I.length && c("boxed-picture", ft(t, I));
952
- let L = St(i);
953
- L.length && c("repeated-items", Ct(t, L));
954
- let R = Tt(i);
955
- R && c("own-popup", Et(t, R));
956
- let z = [...new Set(M(i, Dt))];
957
- z.length && c("own-toast", Ot(t, z));
935
+ let T = ki(i);
936
+ T.length && s("unknown-import", Ai(t, T));
937
+ let E = Mi(i);
938
+ E && c("notify-as-toast", Bi(t, E));
939
+ let D = Ni(i);
940
+ D && c("notify-body", Pi(t, D));
941
+ let O = Fi(i);
942
+ O !== null && c("popup-open", Ii(t, O));
943
+ let k = Ri(i);
944
+ k && c("hand-by-field", zi(t, k)), Pe.test(i) && c("bare-textarea", Fe(t));
945
+ let A = Le(i);
946
+ A.length && c("own-swatch", nt(t, A));
947
+ let j = ze(i);
948
+ j && c("own-picture-set", Be(t, j));
949
+ let P = Br(i) ? null : Ge(i);
950
+ P && c("own-stepper", Ke(t, P));
951
+ let F = Ye(i);
952
+ F && c("own-chart", Xe(t, F));
953
+ let I = i.match(Ze);
954
+ I && c("full-reload", Qe(t, I[0]));
955
+ let L = et(i);
956
+ L.length && c("typed-id", tt(t, L));
957
+ let R = M(i, Ce);
958
+ R.length && c("media-tag", we(t, R)), Te.test(i) && s("bare-img", qn(t));
959
+ let z = ft(i);
960
+ z.length && c("boxed-picture", pt(t, z));
961
+ let B = Ct(i);
962
+ B.length && c("repeated-items", wt(t, B));
963
+ let V = Et(i);
964
+ V && c("own-popup", Dt(t, V));
965
+ let H = [...new Set(M(i, Ot))];
966
+ H.length && c("own-toast", kt(t, H));
958
967
  for (let [e, n] of Kn(t, i)) c(e, n);
959
- let B = eo(t, i, _);
960
- B.length && c("leads-nowhere", to(t, B)), De.test(i) && s("background-picture", Oe(t));
961
- let V = je(i);
962
- V.length && s("stuck-zero", Me(t, V));
963
- let H = M(i, Q);
964
- H.length && c("billing-code", Vi(t, H));
965
- let U = M(N(i, Q), so);
966
- U.length && c("payment-code", co(t, U));
967
- let re = M(i, lo);
968
- re.length && c("email-code", _o(t, re));
969
- let ie = M(i, uo);
970
- ie.length && s("storage-code", fo(t, ie)), ho.test(i) && c("file-input", go(t));
971
- let ae = M(i, po);
972
- ae.length && s("browser-dialog", mo(t, ae));
973
- let oe = te(i);
974
- oe && s("ai-code", ee(t, oe)), Or(i) && c("case-query", xi(t));
975
- for (let e of fi(i)) c("hook-answer", pi(t, e));
976
- let se = Yr(i);
977
- se.length && c("not-a-case", Xr(t, se));
978
- let de = $r(i);
979
- de.length && c("save-args", ei(t, de));
980
- let fe = hi(i);
981
- fe && c("own-publish", gi(t, fe));
982
- let W = M(i, Hi);
983
- W.length && c(W.some((e) => !e.startsWith("runTransaction")) ? "booking-code" : "own-transaction", Ui(t, W));
984
- let ge = i.match(Wi);
985
- ge && c("invented-hours", Gi(t, ge[0]));
986
- let _e = qi(i);
987
- _e.length && c("stale-times", Ji(t, _e));
988
- let G = p(r).some((e) => y.has(e)) || Fi(i);
989
- if (G) {
990
- mr(i) && c("no-gutter", hr(t)), Vt(i) && c("no-page-title", Ht(t));
991
- let e = Wt(i);
992
- e.length && c("one-per-row", Gt(t, e));
993
- let n = p(r).some((e) => v.has(e)) ? null : i.match(Pt);
994
- n && c("no-actions", Ft(t, n[0], i));
995
- let a = N(i, Hi), o = M(a, wr);
968
+ let W = uo(t, i, _);
969
+ W.length && c("leads-nowhere", fo(t, W)), Ee.test(i) && s("background-picture", De(t));
970
+ let G = Me(i);
971
+ G.length && s("stuck-zero", Ne(t, G));
972
+ let ne = M(i, Zi);
973
+ ne.length && c("billing-code", Qi(t, ne));
974
+ let re = M(N(i, Zi), vo);
975
+ re.length && c("payment-code", yo(t, re));
976
+ let ie = M(i, bo);
977
+ ie.length && c("email-code", Do(t, ie));
978
+ let ae = M(i, xo);
979
+ ae.length && s("storage-code", So(t, ae)), To.test(i) && c("file-input", Eo(t));
980
+ let le = M(i, Co);
981
+ le.length && s("browser-dialog", wo(t, le));
982
+ let ue = te(i);
983
+ ue && s("ai-code", ee(t, ue)), Or(i) && c("case-query", Si(t));
984
+ for (let e of pi(i)) c("hook-answer", mi(t, e));
985
+ let de = Yr(i);
986
+ de.length && c("not-a-case", Xr(t, de));
987
+ let he = $r(i);
988
+ he.length && c("save-args", ei(t, he));
989
+ let ge = gi(i);
990
+ ge && c("own-publish", _i(t, ge));
991
+ let K = M(i, $i);
992
+ K.length && c(K.some((e) => !e.startsWith("runTransaction")) ? "booking-code" : "own-transaction", ea(t, K));
993
+ let q = i.match(ta);
994
+ q && c("invented-hours", na(t, q[0]));
995
+ let xe = ia(i);
996
+ xe.length && c("stale-times", aa(t, xe));
997
+ let J = p(r).some((e) => y.has(e)) || Gi(i);
998
+ if (J) {
999
+ mr(i) && c("no-gutter", hr(t)), Ht(i) && c("no-page-title", Ut(t));
1000
+ let e = Gt(i);
1001
+ e.length && c("one-per-row", Kt(t, e));
1002
+ let n = p(r).some((e) => v.has(e)) ? null : i.match(Ft);
1003
+ n && c("no-actions", It(t, n[0], i));
1004
+ let a = N(i, $i), o = M(a, wr);
996
1005
  o.length && c("private-collection", Tr(t, o));
997
- let s = Ri.test(a);
998
- s && c("own-tally", Bi(t));
999
- let l = zi(M(a, Ii, 1), s);
1000
- l.length && c("public-write", Li(t, l));
1006
+ let s = Ji.test(a);
1007
+ s && c("own-tally", Xi(t));
1008
+ let l = Yi(M(a, Ki, 1), s);
1009
+ l.length && c("public-write", qi(t, l));
1001
1010
  }
1002
- let K = Sr(i);
1003
- if (!G && xr(i) && K.size === 1 && !K.has("case") && c("role-curtain", Cr(t)), !G && !n && /\bactions\b/.test(i)) {
1011
+ let Oe = Sr(i);
1012
+ if (!J && xr(i) && Oe.size === 1 && !Oe.has("case") && c("role-curtain", Cr(t)), !J && !n && /\bactions\b/.test(i)) {
1004
1013
  let e = oi(i);
1005
1014
  e.length && c("own-write", si(t, e));
1006
1015
  }
1007
- let Se = G ? null : fa(i);
1008
- Se && c("whole-inbox", oo(t, Se));
1009
- let ke = jr(i);
1010
- ke && c("public-copy", Mr(t, ke.token));
1011
- let q = jt(i);
1012
- q.length && c("own-copy", Nt(t, i, q)), !G && It(i) && c("no-copy", Lt(t));
1013
- let J = Br(i), Y = Ir(i);
1014
- if (J ? c("own-basket", zr.test(i) ? Vr(t, J, /<Checkout\b/.test(i)) : Hr(t, J)) : Y ? c("own-checkout", Lr(t, Y)) : Ur(i) && c("own-purchase", Wr(t)), !J && !Y) for (let n of Wa(i, e)) n.why === "flag" ? c("order-flag", io(n.config, t)) : n.why === "asks" ? c("order-asks", Ga(n.config, t)) : c("buyer-form", ao(n.config, t));
1015
- let Ae = Kr(i);
1016
- Ae && c("own-return", qr(t, Ae));
1017
- for (let n of Ja(t, i, e)) n.why === "typed" ? c("no-road-in", no(n.target, t, n.formId)) : c("own-road", ro(n, t));
1018
- let Fe = Qi(i);
1019
- Fe.length && c("swallowed-write", aa(t, Fe));
1020
- let Ie = ra(i);
1021
- Ie.length && c("swallowed-block", ia(t, Ie));
1022
- let Re = la(i);
1023
- Re.length && c("raw-fault", ua(t, Re));
1024
- let Ve = ga(i);
1025
- Ve.length && c("made-up-history", Ta(t, Ve));
1026
- let X = !G && Oa(i);
1027
- X && c("own-stock", Ha(t)), !X && ja(i) && c("own-left", Va(t));
1028
- let He = La(i);
1029
- He.length && c("joined-variants", Ra(t, He)), za(i) && c("zoom-is-a-gallery", Ba(t));
1030
- let Z = G || X ? null : ya(i);
1031
- Z && c("signed-by-admin", Ua(t, Z.collection, Z.signs));
1032
- let Ge = Ca(i).filter((e) => !Z || e.startsWith("a profile read"));
1033
- Ge.length && c("own-byline", wa(t, Ge));
1016
+ let ke = J ? null : Sa(i);
1017
+ ke && c("whole-inbox", _o(t, ke));
1018
+ let Ae = jr(i);
1019
+ Ae && c("public-copy", Mr(t, Ae.token));
1020
+ let je = Mt(i);
1021
+ je.length && c("own-copy", Pt(t, i, je)), !J && Lt(i) && c("no-copy", Rt(t));
1022
+ let Y = Br(i), Ie = Ir(i);
1023
+ if (Y ? c("own-basket", zr.test(i) ? Vr(t, Y, /<Checkout\b/.test(i)) : Hr(t, Y)) : Ie ? c("own-checkout", Lr(t, Ie)) : Ur(i) && c("own-purchase", Wr(t)), !Y && !Ie) for (let n of eo(i, e)) n.why === "flag" ? c("order-flag", ho(n.config, t)) : n.why === "asks" ? c("order-asks", to(n.config, t)) : c("buyer-form", go(n.config, t));
1024
+ let Re = Kr(i);
1025
+ Re && c("own-return", qr(t, Re));
1026
+ for (let n of io(t, i, e)) n.why === "typed" ? c("no-road-in", po(n.target, t, n.formId)) : c("own-road", mo(n, t));
1027
+ let Ve = ca(i);
1028
+ Ve.length && c("swallowed-write", ha(t, Ve));
1029
+ let He = pa(i);
1030
+ He.length && c("swallowed-block", ma(t, He));
1031
+ let Ue = ya(i);
1032
+ Ue.length && c("raw-fault", ba(t, Ue));
1033
+ let We = Ea(i);
1034
+ We.length && c("made-up-history", Fa(t, We));
1035
+ let qe = !J && Ra(i);
1036
+ qe && c("own-stock", Qa(t)), !qe && Va(i) && c("own-left", Za(t));
1037
+ let Je = qa(i);
1038
+ Je.length && c("joined-variants", Ja(t, Je)), Ya(i) && c("zoom-is-a-gallery", Xa(t));
1039
+ let X = J || qe ? null : ka(i);
1040
+ X && c("signed-by-admin", $a(t, X.collection, X.signs));
1041
+ let $e = Na(i).filter((e) => !X || e.startsWith("a profile read"));
1042
+ $e.length && c("own-byline", Pa(t, $e));
1034
1043
  continue;
1035
1044
  }
1036
1045
  if (t === a || t === o) {
@@ -1043,63 +1052,69 @@ function _r(e) {
1043
1052
  r.length && c("demo-data", Zn(t, r));
1044
1053
  let l = M(i, Qn);
1045
1054
  l.length && c("colour", er(t, l)), /<select[\s/>]/.test(i) && c("native-select", `${t}: a native <select> is drawn by the browser in its own chrome and no theme class can restyle it — replace it with <CustomSelect value={…} options={[{ value, label }]} onChange={(v) => …} /> imported from 'your-ai-workflow-firebase-os' (onChange receives the chosen value string, not an event).`);
1046
- let u = Ei(i);
1047
- u.length && s("unknown-import", Di(t, u));
1048
- let d = ki(i);
1049
- d && c("notify-as-toast", Ai(t, d)), Ne.test(i) && c("bare-textarea", Pe(t));
1050
- let f = Le(i);
1051
- f.length && c("own-swatch", tt(t, f));
1052
- let p = ze(i);
1053
- p && c("own-picture-set", Be(t, p));
1054
- let m = Br(i) ? null : Ue(i);
1055
- m && c("own-stepper", We(t, m));
1056
- let h = qe(i);
1057
- h && c("own-chart", Je(t, h));
1058
- let g = i.match(Ye);
1059
- g && c("full-reload", Xe(t, g[0]));
1060
- let v = $e(i);
1061
- v.length && c("typed-id", et(t, v));
1062
- let y = M(i, we);
1063
- y.length && c("media-tag", Te(t, y)), Ee.test(i) && s("bare-img", qn(t));
1064
- let b = dt(i);
1065
- b.length && c("boxed-picture", ft(t, b));
1066
- let x = St(i);
1067
- x.length && c("repeated-items", Ct(t, x));
1068
- let S = N(i, Gn), C = Tt(S);
1069
- C && c("own-popup", Et(t, C));
1070
- let w = [...new Set(M(i, Dt))];
1071
- w.length && c("own-toast", Ot(t, w));
1072
- let T = Wt(i);
1073
- T.length && c("one-per-row", Gt(t, T));
1074
- for (let [e, n] of Kn(t, S)) c(e, n);
1075
- let E = eo(t, S, _);
1076
- E.length && c("leads-nowhere", to(t, E)), De.test(i) && s("background-picture", Oe(t));
1077
- let D = je(i);
1078
- D.length && s("stuck-zero", Me(t, D));
1079
- let O = M(i, Q);
1080
- O.length && c("billing-code", Vi(t, O));
1081
- let k = M(N(i, Q), so);
1082
- k.length && c("payment-code", co(t, k));
1083
- let A = M(i, lo);
1084
- A.length && c("email-code", _o(t, A));
1085
- let j = M(i, uo);
1086
- j.length && s("storage-code", fo(t, j)), ho.test(i) && c("file-input", go(t));
1087
- let P = M(i, po);
1088
- P.length && s("browser-dialog", mo(t, P));
1089
- let F = te(i);
1090
- F && s("ai-code", ee(t, F));
1091
- let I = M(i, Hi);
1092
- I.length && c(I.some((e) => !e.startsWith("runTransaction")) ? "booking-code" : "own-transaction", Ui(t, I));
1093
- let L = i.match(Wi);
1094
- L && c("invented-hours", Gi(t, L[0]));
1095
- let R = qi(i);
1096
- R.length && c("stale-times", Ji(t, R));
1097
- let z = N(i, Hi), B = M(z, wr);
1098
- B.length && c("private-collection", Tr(t, B));
1099
- let V = Ri.test(z);
1100
- V && c("own-tally", Bi(t));
1101
- let H = zi(M(z, Ii, 1), V);
1102
- H.length && c("public-write", Li(t, H));
1055
+ let u = ki(i);
1056
+ u.length && s("unknown-import", Ai(t, u));
1057
+ let d = Mi(i);
1058
+ d && c("notify-as-toast", Bi(t, d));
1059
+ let f = Ni(i);
1060
+ f && c("notify-body", Pi(t, f));
1061
+ let p = Fi(i);
1062
+ p !== null && c("popup-open", Ii(t, p));
1063
+ let m = Ri(i);
1064
+ m && c("hand-by-field", zi(t, m)), Pe.test(i) && c("bare-textarea", Fe(t));
1065
+ let h = Le(i);
1066
+ h.length && c("own-swatch", nt(t, h));
1067
+ let g = ze(i);
1068
+ g && c("own-picture-set", Be(t, g));
1069
+ let v = Br(i) ? null : Ge(i);
1070
+ v && c("own-stepper", Ke(t, v));
1071
+ let y = Ye(i);
1072
+ y && c("own-chart", Xe(t, y));
1073
+ let b = i.match(Ze);
1074
+ b && c("full-reload", Qe(t, b[0]));
1075
+ let x = et(i);
1076
+ x.length && c("typed-id", tt(t, x));
1077
+ let S = M(i, Ce);
1078
+ S.length && c("media-tag", we(t, S)), Te.test(i) && s("bare-img", qn(t));
1079
+ let C = ft(i);
1080
+ C.length && c("boxed-picture", pt(t, C));
1081
+ let w = Ct(i);
1082
+ w.length && c("repeated-items", wt(t, w));
1083
+ let T = N(i, Gn), E = Et(T);
1084
+ E && c("own-popup", Dt(t, E));
1085
+ let D = [...new Set(M(i, Ot))];
1086
+ D.length && c("own-toast", kt(t, D));
1087
+ let O = Gt(i);
1088
+ O.length && c("one-per-row", Kt(t, O));
1089
+ for (let [e, n] of Kn(t, T)) c(e, n);
1090
+ let k = uo(t, T, _);
1091
+ k.length && c("leads-nowhere", fo(t, k)), Ee.test(i) && s("background-picture", De(t));
1092
+ let A = Me(i);
1093
+ A.length && s("stuck-zero", Ne(t, A));
1094
+ let j = M(i, Zi);
1095
+ j.length && c("billing-code", Qi(t, j));
1096
+ let P = M(N(i, Zi), vo);
1097
+ P.length && c("payment-code", yo(t, P));
1098
+ let F = M(i, bo);
1099
+ F.length && c("email-code", Do(t, F));
1100
+ let I = M(i, xo);
1101
+ I.length && s("storage-code", So(t, I)), To.test(i) && c("file-input", Eo(t));
1102
+ let L = M(i, Co);
1103
+ L.length && s("browser-dialog", wo(t, L));
1104
+ let R = te(i);
1105
+ R && s("ai-code", ee(t, R));
1106
+ let z = M(i, $i);
1107
+ z.length && c(z.some((e) => !e.startsWith("runTransaction")) ? "booking-code" : "own-transaction", ea(t, z));
1108
+ let B = i.match(ta);
1109
+ B && c("invented-hours", na(t, B[0]));
1110
+ let V = ia(i);
1111
+ V.length && c("stale-times", aa(t, V));
1112
+ let H = N(i, $i), U = M(H, wr);
1113
+ U.length && c("private-collection", Tr(t, U));
1114
+ let W = Ji.test(H);
1115
+ W && c("own-tally", Xi(t));
1116
+ let G = Yi(M(H, Ki, 1), W);
1117
+ G.length && c("public-write", qi(t, G));
1103
1118
  }
1104
1119
  }
1105
1120
  return r.sort((e, t) => Number(t.level === "error") - Number(e.level === "error")), {
@@ -1172,34 +1187,70 @@ var vr = (e) => RegExp(`(?:${e})\\s*(?:&&|\\?)\\s*\\(?\\s*<\\s*(?:Card|SectionHe
1172
1187
  "loading",
1173
1188
  "people",
1174
1189
  "error"
1190
+ ],
1191
+ useVisitorState: [
1192
+ "value",
1193
+ "setValue",
1194
+ "clear",
1195
+ "ready"
1196
+ ],
1197
+ useAuth: [
1198
+ "user",
1199
+ "userRole",
1200
+ "accessLevel",
1201
+ "isInternal",
1202
+ "isAdmin",
1203
+ "loading",
1204
+ "signOut",
1205
+ "registerUser",
1206
+ "userInactive",
1207
+ "roleResolved",
1208
+ "roleConfirmed",
1209
+ "requestedRole",
1210
+ "profile",
1211
+ "profileReady",
1212
+ "refreshUser",
1213
+ "activeOrg",
1214
+ "activeWorkspace",
1215
+ "checkOrg",
1216
+ "workspaceAccess",
1217
+ "userWorkspaces",
1218
+ "switchWorkspace",
1219
+ "createWorkspace",
1220
+ "createOrg"
1175
1221
  ]
1176
- }, li = "(useCases|usePublished|useCase|usePeople)", ui = new RegExp(String.raw`\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*${li}\b`, "g"), di = new RegExp(String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${li}\b`, "g"), fi = (e) => {
1222
+ }, li = {
1223
+ useVisitorState: "Its answer comes apart both ways — const { value, setValue, clear } = useVisitorState('draft', {}) or const [draft, setDraft] = useVisitorState('draft', {}) — and there is no state, setState or data on it: read under those names it is undefined, so what was typed is never kept and never shown.",
1224
+ useAuth: "Who is signed in is user — const { user, userRole } = useAuth(), then user?.uid and user?.email — and there is no uid, email or name on the answer itself: read under those names it is undefined, so every record is saved with no owner and every notify({ toUid }) reaches nobody."
1225
+ }, ui = "(useCases|usePublished|useCase|usePeople|useVisitorState|useAuth)", di = new RegExp(String.raw`\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*${ui}\b`, "g"), fi = new RegExp(String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${ui}\b`, "g"), pi = (e) => {
1177
1226
  let t = /* @__PURE__ */ new Map(), n = (e, n) => {
1178
1227
  !n || ci[e].includes(n) || t.set(e, (t.get(e) || /* @__PURE__ */ new Set()).add(n));
1179
1228
  };
1180
- for (let t of e.matchAll(ui)) for (let e of t[1].split(",")) {
1229
+ for (let t of e.matchAll(di)) for (let e of t[1].split(",")) {
1181
1230
  let r = e.split(/[:=]/)[0].trim();
1182
1231
  r && !r.startsWith("...") && n(t[2], r);
1183
1232
  }
1184
- for (let t of e.matchAll(di)) for (let r of e.matchAll(RegExp(`\\b${t[1].replace(/\$/g, "\\$")}\\s*\\??\\.\\s*([A-Za-z_$][\\w$]*)`, "g"))) n(t[2], r[1]);
1233
+ for (let t of e.matchAll(fi)) for (let r of e.matchAll(RegExp(`\\b${t[1].replace(/\$/g, "\\$")}\\s*\\??\\.\\s*([A-Za-z_$][\\w$]*)`, "g"))) n(t[2], r[1]);
1185
1234
  return [...t].map(([e, t]) => ({
1186
1235
  hook: e,
1187
1236
  read: [...t]
1188
1237
  }));
1189
- }, pi = (e, { hook: t, read: n }) => {
1190
- let r = ci[t], i = r[1], a = i === "people" ? "people" : i === "record" ? "entry" : "entries", o = i === "record" ? "so the page draws nothing whatever has arrived and shows its not-found line to everybody" : "so the list is empty whatever has arrived and the page shows its empty line to everybody";
1191
- return `${e}: ${t}() hands back { ${r.join(", ")} } and nothing else — this page reads ${n.map((e) => `\`${e}\``).join(" and ")} from it, which ${n.length === 1 ? "is" : "are"} never there, ${o}. Write const { ${r.join(", ")} } = ${t}(…) and read the ${a} from \`${i}\`. Change nothing else on the page.`;
1192
- }, mi = /(?:access|editors)\s*:\s*\[[^\]]*['"`]everyone['"`]|(?:access|editors)\s*:\s*arrayUnion\s*\(\s*['"`]everyone['"`]|arrayUnion\s*\(\s*['"`]everyone['"`]|arrayRemove\s*\(\s*['"`]everyone['"`]/, hi = (e) => {
1193
- let t = mi.exec(e);
1238
+ }, mi = (e, { hook: t, read: n }) => {
1239
+ let r = ci[t], i = li[t];
1240
+ if (i) return `${e}: ${t}() hands back { ${t === "useAuth" ? "user, userRole, isAdmin, loading, …" : r.join(", ")} } and nothing else — this page reads ${n.map((e) => `\`${e}\``).join(" and ")} from it, which ${n.length === 1 ? "is" : "are"} never there. ${i} Change nothing else on the page.`;
1241
+ let a = r[1], o = a === "people" ? "people" : a === "record" ? "entry" : "entries", s = a === "record" ? "so the page draws nothing whatever has arrived and shows its not-found line to everybody" : "so the list is empty whatever has arrived and the page shows its empty line to everybody";
1242
+ return `${e}: ${t}() hands back { ${r.join(", ")} } and nothing else — this page reads ${n.map((e) => `\`${e}\``).join(" and ")} from it, which ${n.length === 1 ? "is" : "are"} never there, ${s}. Write const { ${r.join(", ")} } = ${t}(…) and read the ${o} from \`${a}\`. Change nothing else on the page.`;
1243
+ }, hi = /(?:access|editors)\s*:\s*\[[^\]]*['"`]everyone['"`]|(?:access|editors)\s*:\s*arrayUnion\s*\(\s*['"`]everyone['"`]|arrayUnion\s*\(\s*['"`]everyone['"`]|arrayRemove\s*\(\s*['"`]everyone['"`]/, gi = (e) => {
1244
+ let t = hi.exec(e);
1194
1245
  return t ? /editors/.test(t[0]) ? "editors" : "access" : "";
1195
- }, gi = (e, t) => t === "editors" ? `${e}: this page writes 'everyone' onto a record's editors list. That would mean anybody signed in may rewrite or delete anybody's — the security rules refuse the write outright, so nothing is saved and the person is told nothing. 'everyone' belongs on \`access\` and nowhere else, and it is publish('case_…', id) that puts it there. Take the word out of editors; leave the author's own key on it.` : `${e}: this page writes 'everyone' into a record's access list by hand. Putting it there as the record is created publishes it the moment it is saved — there is no draft, and the owner asked for one. Use the two calls from 'your-ai-workflow-firebase-os': save it with withAccess(data, { uid }) and nothing else, then publish('case_…', id) from the button the person presses when they are ready, unpublish(…) to take it down, isPublished(record) to know which it is.`, _i = /^data:|placehold|placeholder|picsum|unsplash|dummyimage|loremflickr|example\.com|your[-_]?logo|logo[-_]?here/i, vi = (e, t) => {
1246
+ }, _i = (e, t) => t === "editors" ? `${e}: this page writes 'everyone' onto a record's editors list. That would mean anybody signed in may rewrite or delete anybody's — the security rules refuse the write outright, so nothing is saved and the person is told nothing. 'everyone' belongs on \`access\` and nowhere else, and it is publish('case_…', id) that puts it there. Take the word out of editors; leave the author's own key on it.` : `${e}: this page writes 'everyone' into a record's access list by hand. Putting it there as the record is created publishes it the moment it is saved — there is no draft, and the owner asked for one. Use the two calls from 'your-ai-workflow-firebase-os': save it with withAccess(data, { uid }) and nothing else, then publish('case_…', id) from the button the person presses when they are ready, unpublish(…) to take it down, isPublished(record) to know which it is.`, vi = /^data:|placehold|placeholder|picsum|unsplash|dummyimage|loremflickr|example\.com|your[-_]?logo|logo[-_]?here/i, yi = (e, t) => {
1196
1247
  let n = RegExp(`\\b${t}\\s*:\\s*(['"\`])([\\s\\S]*?)\\1`).exec(e);
1197
1248
  return n ? n[2].trim() : null;
1198
- }, yi = (e) => {
1199
- let t = ["logoUrl", "faviconUrl"], n = new Map(t.map((t) => [t, vi(e, t)])), r = /DO NOT EDIT: logoUrl/.test(e), i = [];
1249
+ }, bi = (e) => {
1250
+ let t = ["logoUrl", "faviconUrl"], n = new Map(t.map((t) => [t, yi(e, t)])), r = /DO NOT EDIT: logoUrl/.test(e), i = [];
1200
1251
  for (let e of t) {
1201
1252
  let t = n.get(e) ?? null, a = n.get(e === "logoUrl" ? "faviconUrl" : "logoUrl");
1202
- t ? _i.test(t) && i.push({
1253
+ t ? vi.test(t) && i.push({
1203
1254
  key: e,
1204
1255
  why: "stand-in"
1205
1256
  }) : (r || a) && i.push({
@@ -1208,10 +1259,10 @@ var vr = (e) => RegExp(`(?:${e})\\s*(?:&&|\\?)\\s*\\(?\\s*<\\s*(?:Card|SectionHe
1208
1259
  });
1209
1260
  }
1210
1261
  return i;
1211
- }, bi = (e, { key: t, why: n }) => {
1262
+ }, xi = (e, { key: t, why: n }) => {
1212
1263
  let r = t === "logoUrl" ? "logo, the picture in the top bar" : "favicon, the picture in the browser tab";
1213
1264
  return n === "stand-in" ? `${e}: \`${t}\` points at a stand-in rather than at my ${r}. That value is my brand and is never yours to choose: put back the link that was there. If you do not have it, say so and stop — I upload it in the "Design it with your assistant" box on my /theme screen, and the sentence that box gives me carries the link for you to paste in. Never a placeholder, never a picture you generated, never a link I did not give you.` : `${e}: \`${t}\` is empty — my ${r}, is gone from my app. This file ships with it set and there is exactly one thing that may ever change it: a link in a message of mine. Put the old value back, character for character (its own history in this file is where it is). If it cannot be recovered, say so plainly and change nothing: I upload it again in the "Design it with your assistant" box on my /theme screen, and the sentence that box gives me carries the link. Never fill it with a placeholder, a picture you generated, or any link I did not give you.`;
1214
- }, xi = (e) => `${e}: a plain query on a case_ collection is refused for everybody but an admin — the security rules prove a list by the key it names, and only useCases() asks for it that way. Read it with useCases('case_tickets') from 'your-ai-workflow-firebase-os' instead ({ loading, records, error }); addDoc(collection(db, '…'), withAccess(data, { uid })) is still how an entry is created.`, Si = /\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?\{([^}]*)\}\s*from\s*['"]your-ai-workflow-firebase-os['"]/g, Ci = {
1265
+ }, Si = (e) => `${e}: a plain query on a case_ collection is refused for everybody but an admin — the security rules prove a list by the key it names, and only useCases() asks for it that way. Read it with useCases('case_tickets') from 'your-ai-workflow-firebase-os' instead ({ loading, records, error }); addDoc(collection(db, '…'), withAccess(data, { uid })) is still how an entry is created.`, Ci = /\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?\{([^}]*)\}\s*from\s*['"]your-ai-workflow-firebase-os['"]/g, wi = {
1215
1266
  useFirestore: "db — import { db } from 'your-ai-workflow-firebase-os', with collection(db, '…') from 'firebase/firestore'",
1216
1267
  useDb: "db — import { db } from 'your-ai-workflow-firebase-os'",
1217
1268
  firestore: "db — import { db } from 'your-ai-workflow-firebase-os'",
@@ -1233,26 +1284,57 @@ var vr = (e) => RegExp(`(?:${e})\\s*(?:&&|\\?)\\s*\\(?\\s*<\\s*(?:Card|SectionHe
1233
1284
  formatCurrency: "money(amount)",
1234
1285
  Image: "<Picture>",
1235
1286
  Img: "<Picture>"
1236
- }, wi = (e) => {
1287
+ }, Ti = new Set([
1288
+ "collection",
1289
+ "onSnapshot",
1290
+ "doc",
1291
+ "query",
1292
+ "where",
1293
+ "orderBy",
1294
+ "limit",
1295
+ "startAfter",
1296
+ "addDoc",
1297
+ "setDoc",
1298
+ "updateDoc",
1299
+ "deleteDoc",
1300
+ "getDoc",
1301
+ "getDocs",
1302
+ "serverTimestamp",
1303
+ "arrayUnion",
1304
+ "arrayRemove",
1305
+ "increment",
1306
+ "Timestamp",
1307
+ "writeBatch",
1308
+ "runTransaction"
1309
+ ]), Ei = (e) => wi[e] || (Ti.has(e) ? `${e} from 'firebase/firestore' — import { ${e} } from 'firebase/firestore'; only db comes from the package` : null) || Di(e), Di = (e) => {
1237
1310
  let t = e.toLowerCase();
1238
1311
  return k.find((e) => e.toLowerCase() === t) || k.filter((e) => e.length > 3 && (t.includes(e.toLowerCase()) || e.toLowerCase().includes(t))).sort((t, n) => Math.abs(t.length - e.length) - Math.abs(n.length - e.length))[0] || null;
1239
- }, Ti = /^(?:ChatThread|openChat|useAiChat|askAi|useAiEnabled|useAiModels)$/, Ei = (e) => {
1312
+ }, Oi = /^(?:ChatThread|openChat|useAiChat|askAi|useAiEnabled|useAiModels)$/, ki = (e) => {
1240
1313
  let t = /* @__PURE__ */ new Set();
1241
- for (let n of z(e).matchAll(Si)) for (let e of n[1].split(",")) {
1314
+ for (let n of z(e).matchAll(Ci)) for (let e of n[1].split(",")) {
1242
1315
  let n = e.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
1243
- n && /^[A-Za-z_$][\w$]*$/.test(n) && !Ti.test(n) && !k.includes(n) && t.add(n);
1316
+ n && /^[A-Za-z_$][\w$]*$/.test(n) && !Oi.test(n) && !k.includes(n) && t.add(n);
1244
1317
  }
1245
1318
  return [...t];
1246
- }, Di = (e, t) => `${e}: ${P(t.map((e) => {
1247
- let t = Ci[e] || wi(e);
1319
+ }, Ai = (e, t) => `${e}: ${P(t.map((e) => {
1320
+ let t = Ei(e);
1248
1321
  return t ? `${e} (what is there: ${t})` : e;
1249
- }))} ${t.length > 1 ? "are" : "is"} imported from 'your-ai-workflow-firebase-os', and the package has nothing by ${t.length > 1 ? "those names" : "that name"} — the import fails, and because every page is loaded through registry.ts the WHOLE app stops loading, not only this page. Never guess a name: import only what the app's instructions name, spelled as they spell it. Take ${t.length > 1 ? "each one" : "it"} out of the import and use what is there in its place; if nothing the instructions name does the job, write it in this file from React and 'firebase/firestore' rather than importing a piece that does not exist. (If the instructions DO name it, the installed package is older than they are: update it.)`, Oi = /\bnotify\s*\(\s*\{((?:[^{}]|\{[^{}]*\})*)\}\s*\)/g, ki = (e) => {
1250
- for (let t of z(e).matchAll(Oi)) if (!/\bto(?:Uid|Role)\b|\.\.\./.test(t[1])) return t[0].replace(/\s+/g, " ").slice(0, 80);
1322
+ }))} ${t.length > 1 ? "are" : "is"} imported from 'your-ai-workflow-firebase-os', and the package has nothing by ${t.length > 1 ? "those names" : "that name"} — the import fails, and because every page is loaded through registry.ts the WHOLE app stops loading, not only this page. Never guess a name: import only what the app's instructions name, spelled as they spell it. Take ${t.length > 1 ? "each one" : "it"} out of the import and use what is there in its place; if nothing the instructions name does the job, write it in this file from React and 'firebase/firestore' rather than importing a piece that does not exist. (If the instructions DO name it, the installed package is older than they are: update it.)`, ji = /\bnotify\s*\(\s*\{((?:[^{}]|\{[^{}]*\})*)\}\s*\)/g, Mi = (e) => {
1323
+ for (let t of z(e).matchAll(ji)) if (!/\bto(?:Uid|Role)\b|\.\.\./.test(t[1])) return t[0].replace(/\s+/g, " ").slice(0, 80);
1324
+ return null;
1325
+ }, Ni = (e) => {
1326
+ for (let t of z(e).matchAll(ji)) if (/\bto(?:Uid|Role)\b|\.\.\./.test(t[1]) && /\bmessage\s*:/.test(t[1]) && !/\bbody\s*:/.test(t[1])) return t[0].replace(/\s+/g, " ").slice(0, 80);
1327
+ return null;
1328
+ }, Pi = (e, t) => `${e}: "${t}" — notify() takes { toUid or toRole, title, body, link }, and message is not one of its words: the bell shows title in bold with body under it, so this notification arrives with nothing to read. Put the one-line headline in title and the rest in body, and change nothing else about it.`, Fi = (e) => {
1329
+ for (let t of Z(z(e), "Popup")) if (!(/\.\.\./.test(t.attrs) || /\bopen\b/.test(t.attrs))) return t.attrs.replace(/\s+/g, " ").trim().slice(0, 60);
1251
1330
  return null;
1252
- }, Ai = (e, t) => `${e}: "${t}" — notify() is the BELL: a notification sent to somebody else, and it needs who it is for (toUid for one person, toRole for an audience) and a title. Called like this it sends nothing and shows nothing, so nobody is ever told their save went through. To answer the person who just pressed something it is say('Saved') imported from 'your-ai-workflow-firebase-os' — one brief line, no options needed. Change every notify({ … }) that has no toUid or toRole into say('the same words'), and keep notify() only where somebody ELSE has to hear about it.`, ji = /<ChatThread\b|\bopenChat\s*\(/, Mi = /\buseVisitorState\b/, Ni = /\btally\s*\(/, Pi = /\b(?:saveRecord|updateRecord|deleteRecord|usePublicCopy|useStockFromOrders|useTallies|logHistory|useCases|userRole)\b/, Fi = (e) => (Mi.test(e) || Ni.test(e)) && !Pi.test(e), Ii = /\b(updateDoc|setDoc|addDoc|deleteDoc|writeBatch)\s*\(/, Li = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "write" : "writes"} the database from a page anyone can open, and a visitor is not signed in — the rules refuse every write from here with "Missing or insufficient permissions", a pub_ copy included: the page that owns the data writes the copy, this page only reads it. Delete ${t.length > 1 ? "every one of those writes" : "the write"}. What a visitor does goes through the app's own form — <ContactPopup formId="…" isInline bare extra={{ … }} onSubmitted={…} /> from a *Form.config.ts, with what the page worked out (the items, their options, the total) in extra — a <PayButton>, or a schedule question; the page that owns the data reads the entry while an admin has it open and applies it once, to its own record and the pub_ copy together.`, Ri = /\bincrement\s*\(/, zi = (e, t) => t ? e.filter((e) => e !== "updateDoc" && e !== "setDoc") : e, Bi = (e) => `${e}: increment( counts what visitors do by writing the database from a page anyone can open, and a visitor is not signed in — the rules refuse that write for every real visitor, so the number only ever moves for the owner testing their own page while signed in. Delete the write and the increment import. Count with the app's own piece, imported from 'your-ai-workflow-firebase-os': tally('products', product.id, 'views') where the thing is opened — the word for what is counted, the record's own id, and one of views, added, clicks, saves, shares or downloads. It counts a visitor once a visit, leaves the team's own looks out, never throws and never holds the press up, so it is called and not awaited. A shop's <Basket tally="products"> counts every add itself. The page for the team reads them with useTallies('products') — counts.of(product.id).views. Never keep a count on the record, and never in a pub_ collection.`, Q = /fetch\(\s*['"`]\/api\/payments\/portal|cancel_at_period_end|cancelSubscription|subscriptions?\.(?:cancel|del)\(/, Vi = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} a hand-rolled billing flow — the app already has one. Cancel, resume, plan switches, card changes and invoices are ALL Stripe's own portal: render <ManagePlanButton /> imported from 'your-ai-workflow-firebase-os' (buyers also reach it on the built-in /billing page) and delete this code.`, Hi = /['"`](?:mem_bookings|pub_bookings|sys_schedules|sys_bookingpages)['"`]|runTransaction\s*\(/, Ui = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} hand-rolled booking code — bookings go through the app's own 'schedule' form question and useBookings() — never the collections. Delete it: a visitor books with a *Form.config.ts field { type: 'schedule', schedules: […], look: '…' } shown with <ContactPopup formId="…" isInline />, and a team page reads the bookings with useBookings() and cancels one with cancelBooking(id), both imported from 'your-ai-workflow-firebase-os'.`, Wi = /\b(?:mon|tue|wed|thu|fri|sat|sun)\b\s*:\s*\[\s*\{\s*from\s*:/, Gi = (e, t) => `${e}: ${t.trim().replace(/\s+/g, " ")} is somebody's working hours typed into the page — they go stale the moment that person edits their week. Read the team's hours with useTeamHours() from 'your-ai-workflow-firebase-os' — { loading, schedules }, each with memberName, timeZone, days and blocked — and turn them into times with availableSlots / availableSlotsFor / togetherSlots from the same package, passing the booked times as taken. Never write anybody's hours or times into a page.`, Ki = /\b(availableSlots|availableSlotsFor|togetherSlots|slotsFor)\s*\(/g;
1253
- function qi(e) {
1331
+ }, Ii = (e, t) => `${e}: a <Popup${t ? ` ${t}…` : ""}> here has no open prop — a window is drawn only while open is true, so this one never appears, and nothing says why. Give it the same state that decides whether it is on screen — <Popup open={!!chosen} onClose={() => setChosen(null)} title="…"> — and keep it rendered rather than mounted only while that state is set, so it can ease in and out (rule 30).`, Li = /\b(?:assign(?:ed|ee)(?:To|Uid|Id)|assignedUserId|handedTo|heldBy|holderUid)\s*:/, Ri = (e) => {
1332
+ let t = z(e), n = t.match(/\buseCases\s*(?:<[^>]*>)?\s*\(\s*['"]([^'"]+)['"]/);
1333
+ return !n || /\bsendTo\s*\(/.test(t) || /\bactions\s*\.\s*sendTo\b/.test(t) ? null : Li.test(t) ? n[1] : null;
1334
+ }, zi = (e, t) => `${e}: an entry here is handed to somebody by writing who holds it onto the record, and sendTo() is never called — the entry's own lists do not change, so the person it was handed to is never sent it and sees nothing, whatever the record says. Handing one over is sendTo('${t}', id, { people: [uid] }) — the collection this page reads with useCases — with the person picked from usePeople(…), and takeBack('${t}', id, { people: [uid] }) to take it off them; keep saving their uid and their name on the entry as well, so the page can say who holds it. Change nothing else on the page.`, Bi = (e, t) => `${e}: "${t}" — notify() is the BELL: a notification sent to somebody else, and it needs who it is for (toUid for one person, toRole for an audience) and a title. Called like this it sends nothing and shows nothing, so nobody is ever told their save went through. To answer the person who just pressed something it is say('Saved') imported from 'your-ai-workflow-firebase-os' — one brief line, no options needed. Change every notify({ … }) that has no toUid or toRole into say('the same words'), and keep notify() only where somebody ELSE has to hear about it.`, Vi = /<ChatThread\b|\bopenChat\s*\(/, Hi = /\buseVisitorState\b/, Ui = /\btally\s*\(/, Wi = /\b(?:saveRecord|updateRecord|deleteRecord|usePublicCopy|useStockFromOrders|useTallies|logHistory|useCases|userRole)\b/, Gi = (e) => (Hi.test(e) || Ui.test(e)) && !Wi.test(e), Ki = /\b(updateDoc|setDoc|addDoc|deleteDoc|writeBatch)\s*\(/, qi = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "write" : "writes"} the database from a page anyone can open, and a visitor is not signed in — the rules refuse every write from here with "Missing or insufficient permissions", a pub_ copy included: the page that owns the data writes the copy, this page only reads it. Delete ${t.length > 1 ? "every one of those writes" : "the write"}. What a visitor does goes through the app's own form — <ContactPopup formId="…" isInline bare extra={{ … }} onSubmitted={…} /> from a *Form.config.ts, with what the page worked out (the items, their options, the total) in extra — a <PayButton>, or a schedule question; the page that owns the data reads the entry while an admin has it open and applies it once, to its own record and the pub_ copy together.`, Ji = /\bincrement\s*\(/, Yi = (e, t) => t ? e.filter((e) => e !== "updateDoc" && e !== "setDoc") : e, Xi = (e) => `${e}: increment( counts what visitors do by writing the database from a page anyone can open, and a visitor is not signed in — the rules refuse that write for every real visitor, so the number only ever moves for the owner testing their own page while signed in. Delete the write and the increment import. Count with the app's own piece, imported from 'your-ai-workflow-firebase-os': tally('products', product.id, 'views') where the thing is opened — the word for what is counted, the record's own id, and one of views, added, clicks, saves, shares or downloads. It counts a visitor once a visit, leaves the team's own looks out, never throws and never holds the press up, so it is called and not awaited. A shop's <Basket tally="products"> counts every add itself. The page for the team reads them with useTallies('products') — counts.of(product.id).views. Never keep a count on the record, and never in a pub_ collection.`, Zi = /fetch\(\s*['"`]\/api\/payments\/portal|cancel_at_period_end|cancelSubscription|subscriptions?\.(?:cancel|del)\(/, Qi = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} a hand-rolled billing flow — the app already has one. Cancel, resume, plan switches, card changes and invoices are ALL Stripe's own portal: render <ManagePlanButton /> imported from 'your-ai-workflow-firebase-os' (buyers also reach it on the built-in /billing page) and delete this code.`, $i = /['"`](?:mem_bookings|pub_bookings|sys_schedules|sys_bookingpages)['"`]|runTransaction\s*\(/, ea = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} hand-rolled booking code — bookings go through the app's own 'schedule' form question and useBookings() — never the collections. Delete it: a visitor books with a *Form.config.ts field { type: 'schedule', schedules: […], look: '…' } shown with <ContactPopup formId="…" isInline />, and a team page reads the bookings with useBookings() and cancels one with cancelBooking(id), both imported from 'your-ai-workflow-firebase-os'.`, ta = /\b(?:mon|tue|wed|thu|fri|sat|sun)\b\s*:\s*\[\s*\{\s*from\s*:/, na = (e, t) => `${e}: ${t.trim().replace(/\s+/g, " ")} is somebody's working hours typed into the page — they go stale the moment that person edits their week. Read the team's hours with useTeamHours() from 'your-ai-workflow-firebase-os' — { loading, schedules }, each with memberName, timeZone, days and blocked — and turn them into times with availableSlots / availableSlotsFor / togetherSlots from the same package, passing the booked times as taken. Never write anybody's hours or times into a page.`, ra = /\b(availableSlots|availableSlotsFor|togetherSlots|slotsFor)\s*\(/g;
1335
+ function ia(e) {
1254
1336
  let t = [];
1255
- for (let n of e.matchAll(Ki)) {
1337
+ for (let n of e.matchAll(ra)) {
1256
1338
  let r = (n.index ?? 0) + n[0].length - 1, i = 0, a = -1;
1257
1339
  for (let t = r; t < e.length; t++) if (e[t] === "(") i++;
1258
1340
  else if (e[t] === ")" && --i === 0) {
@@ -1265,30 +1347,30 @@ function qi(e) {
1265
1347
  }
1266
1348
  return t;
1267
1349
  }
1268
- var Ji = (e, t) => {
1350
+ var aa = (e, t) => {
1269
1351
  let n = t[0].trim().replace(/\s+/g, " ");
1270
1352
  return `${e}: ${n.length > 80 ? `${n.slice(0, 80)}…` : n} works out free times without the booked ones, so the page offers hours somebody has already booked. Read the booked times with useTakenSlots(memberIds, from) from 'your-ai-workflow-firebase-os' — memberIds the people the page shows, from ONE Date kept in state — and pass them in: availableSlotsFor(schedules, { now: new Date(), taken }). A time booked on ANY booking link, page or schedule of that person is taken on all of them.${F(t.length)}`;
1271
- }, Yi = /\b(setDoc|updateDoc|addDoc|deleteDoc)\s*\(/g, Xi = (e) => !/\bthrow\b/.test(e) && !/\bset[A-Z]\w*\s*\(/.test(e) && !/\b(notify|toast|emailPerson)\s*\(/.test(e), Zi = (e, t) => {
1353
+ }, oa = /\b(setDoc|updateDoc|addDoc|deleteDoc)\s*\(/g, sa = (e) => !/\bthrow\b/.test(e) && !/\bset[A-Z]\w*\s*\(/.test(e) && !/\b(notify|toast|emailPerson)\s*\(/.test(e), Q = (e, t) => {
1272
1354
  let n = 0;
1273
1355
  for (let r = t; r < e.length; r++) if (e[r] === "(") n++;
1274
1356
  else if (e[r] === ")" && --n === 0) return r;
1275
1357
  return -1;
1276
1358
  };
1277
- function Qi(e) {
1359
+ function ca(e) {
1278
1360
  let t = [];
1279
- for (let n of e.matchAll(Yi)) {
1280
- let r = Zi(e, (n.index ?? 0) + n[0].length - 1);
1361
+ for (let n of e.matchAll(oa)) {
1362
+ let r = Q(e, (n.index ?? 0) + n[0].length - 1);
1281
1363
  if (r === -1) continue;
1282
1364
  let i = e.slice(r + 1).match(/^\s*\.catch\s*\(/);
1283
1365
  if (!i) continue;
1284
- let a = r + 1 + i[0].length - 1, o = Zi(e, a);
1366
+ let a = r + 1 + i[0].length - 1, o = Q(e, a);
1285
1367
  if (o === -1) continue;
1286
1368
  let s = e.slice(a + 1, o);
1287
- Xi(s) && t.push(`${n[1]}(…).catch(${s.replace(/\s+/g, " ").trim()})`);
1369
+ sa(s) && t.push(`${n[1]}(…).catch(${s.replace(/\s+/g, " ").trim()})`);
1288
1370
  }
1289
1371
  return t;
1290
1372
  }
1291
- var $i = (e) => e.replace(/console\s*\.\s*\w+\s*\([^()]*\)/g, "").replace(/[\s;]/g, "") === "", ea = (e, t) => {
1373
+ var la = (e) => e.replace(/console\s*\.\s*\w+\s*\([^()]*\)/g, "").replace(/[\s;]/g, "") === "", ua = (e, t) => {
1292
1374
  let n = 0;
1293
1375
  for (let r = t; r < e.length; r++) {
1294
1376
  let t = e[r];
@@ -1310,16 +1392,16 @@ var $i = (e) => e.replace(/console\s*\.\s*\w+\s*\([^()]*\)/g, "").replace(/[\s;]
1310
1392
  else if (t === "}" && --n === 0) return r;
1311
1393
  }
1312
1394
  return -1;
1313
- }, ta = /\btry\s*\{/g, na = /^\s*catch\s*(?:\([^()]*\)\s*)?\{/;
1314
- function ra(e) {
1395
+ }, da = /\btry\s*\{/g, fa = /^\s*catch\s*(?:\([^()]*\)\s*)?\{/;
1396
+ function pa(e) {
1315
1397
  let t = [];
1316
- for (let n of e.matchAll(ta)) {
1317
- let r = (n.index ?? 0) + n[0].length - 1, i = ea(e, r);
1398
+ for (let n of e.matchAll(da)) {
1399
+ let r = (n.index ?? 0) + n[0].length - 1, i = ua(e, r);
1318
1400
  if (i === -1) continue;
1319
- let a = e.slice(i + 1).match(na);
1401
+ let a = e.slice(i + 1).match(fa);
1320
1402
  if (!a) continue;
1321
- let o = i + 1 + a[0].length - 1, s = ea(e, o);
1322
- if (s === -1 || !$i(e.slice(o + 1, s))) continue;
1403
+ let o = i + 1 + a[0].length - 1, s = ua(e, o);
1404
+ if (s === -1 || !la(e.slice(o + 1, s))) continue;
1323
1405
  let c = e.slice(r + 1, i);
1324
1406
  if (/\btry\s*\{/.test(c)) continue;
1325
1407
  let l = c.match(/\b(setDoc|updateDoc|addDoc|deleteDoc)\s*\(/);
@@ -1327,33 +1409,33 @@ function ra(e) {
1327
1409
  }
1328
1410
  return t;
1329
1411
  }
1330
- var ia = (e, t) => `${e}: ${t[0]} throws the failure away — the write is awaited inside a try whose catch tells nobody, so the page carries on as though it landed and whatever it says next ("Saved", a tick, a row it adds to a list) it says whether Firestore took the write or refused it. A refusal is the ordinary case here, not a freak one: the rules refuse a write whose collection prefix is not that person's, so a trail the TEAM appends to in an admin_ collection records the admins and silently records nobody else (contract rule 11). In the catch, say plainly that it did not save and leave what they typed on the screen so they can send it again (contract rule 19).${F(t.length)}`, aa = (e, t) => `${e}: ${t[0].length > 72 ? `${t[0].slice(0, 72)}…` : t[0]} throws the failure away, so the page carries on as though the write landed — and whatever it says next ("Saved", a tick, a box it clears) it says whether Firestore took the write or refused it. That is the one thing a page must never do: it teaches the owner to trust a number that is not there. Await the write with nothing chained to it, say "Saved" only after it resolves, and in the catch say plainly that it did not save and leave what they typed on the screen so they can send it again (contract rule 19).${F(t.length)}`, oa = String.raw`(?:(?:e|err|error|ex|caught)\s*\??\.\s*(?:message|code)\b|String\(\s*(?:e|err|error)\s*\))`, sa = new RegExp(String.raw`\bset[A-Z]\w*\(\s*[^;)]{0,90}?` + oa, ""), ca = new RegExp(String.raw`[^=]>\s*\{[^{}]{0,90}?` + oa + String.raw`[^{}]{0,90}?\}`, ""), la = (e) => {
1412
+ var ma = (e, t) => `${e}: ${t[0]} throws the failure away — the write is awaited inside a try whose catch tells nobody, so the page carries on as though it landed and whatever it says next ("Saved", a tick, a row it adds to a list) it says whether Firestore took the write or refused it. A refusal is the ordinary case here, not a freak one: the rules refuse a write whose collection prefix is not that person's, so a trail the TEAM appends to in an admin_ collection records the admins and silently records nobody else (contract rule 11). In the catch, say plainly that it did not save and leave what they typed on the screen so they can send it again (contract rule 19).${F(t.length)}`, ha = (e, t) => `${e}: ${t[0].length > 72 ? `${t[0].slice(0, 72)}…` : t[0]} throws the failure away, so the page carries on as though the write landed — and whatever it says next ("Saved", a tick, a box it clears) it says whether Firestore took the write or refused it. That is the one thing a page must never do: it teaches the owner to trust a number that is not there. Await the write with nothing chained to it, say "Saved" only after it resolves, and in the catch say plainly that it did not save and leave what they typed on the screen so they can send it again (contract rule 19).${F(t.length)}`, ga = String.raw`(?:(?:e|err|error|ex|caught)\s*\??\.\s*(?:message|code)\b|String\(\s*(?:e|err|error)\s*\))`, _a = new RegExp(String.raw`\bset[A-Z]\w*\(\s*[^;)]{0,90}?` + ga, ""), va = new RegExp(String.raw`[^=]>\s*\{[^{}]{0,90}?` + ga + String.raw`[^{}]{0,90}?\}`, ""), ya = (e) => {
1331
1413
  let t = [];
1332
- for (let n of [sa, ca]) for (let r of e.matchAll(new RegExp(n.source, "g"))) {
1414
+ for (let n of [_a, va]) for (let r of e.matchAll(new RegExp(n.source, "g"))) {
1333
1415
  let n = r.index ?? 0, i = e.slice(Math.max(0, n - 60), n);
1334
1416
  /console\.\w+\($/.test(i.trimEnd()) || /console\.\w+\([^)]*$/.test(i) || t.push(r[0].trim());
1335
1417
  }
1336
1418
  return t;
1337
- }, ua = (e, t) => `${e}: ${t[0].length > 72 ? `${t[0].slice(0, 72)}…` : t[0]} puts what you CAUGHT on the screen. Those words are the library talking to a developer — "Missing or insufficient permissions.", "FirebaseError: unavailable", permission-denied — and to the person using the app they mean nothing, read as though they broke something, and leave them with nowhere to go. Say what it means and what to do next instead: troubleSentence(e, 'That could not be saved.'), imported from 'your-ai-workflow-firebase-os', which words a lost connection, a sign-in that ran out, a refusal and a busy app each in their own sentence and falls back to yours for anything else. Put it where the work was, leave what they typed on screen, and give them the button again (contract rule 25).${F(t.length)}`, da = /['"`](user_submissions|user_requests)['"`]/, fa = (e) => {
1338
- let t = e.match(da);
1419
+ }, ba = (e, t) => `${e}: ${t[0].length > 72 ? `${t[0].slice(0, 72)}…` : t[0]} puts what you CAUGHT on the screen. Those words are the library talking to a developer — "Missing or insufficient permissions.", "FirebaseError: unavailable", permission-denied — and to the person using the app they mean nothing, read as though they broke something, and leave them with nowhere to go. Say what it means and what to do next instead: troubleSentence(e, 'That could not be saved.'), imported from 'your-ai-workflow-firebase-os', which words a lost connection, a sign-in that ran out, a refusal and a busy app each in their own sentence and falls back to yours for anything else. Put it where the work was, leave what they typed on screen, and give them the button again (contract rule 25).${F(t.length)}`, xa = /['"`](user_submissions|user_requests)['"`]/, Sa = (e) => {
1420
+ let t = e.match(xa);
1339
1421
  return !t || /\bformId\b/.test(e) ? null : t[1];
1340
- }, pa = /\b(?:user|currentUser)\s*[?!]?\s*\.\s*(?:email|displayName|photoURL)\b/, ma = /\b([A-Za-z_$][\w$]*)\s*:\s*(?:new\s+Date\s*\(\s*)?Date\.now\(\)\s*-(?!=)/g, ha = (e, t) => {
1422
+ }, Ca = /\b(?:user|currentUser)\s*[?!]?\s*\.\s*(?:email|displayName|photoURL)\b/, wa = /\b([A-Za-z_$][\w$]*)\s*:\s*(?:new\s+Date\s*\(\s*)?Date\.now\(\)\s*-(?!=)/g, Ta = (e, t) => {
1341
1423
  let n = 0;
1342
1424
  for (let r = t; r >= 0; r--) if (e[r] === "}") n++;
1343
1425
  else if (e[r] === "{" && n-- === 0) return r;
1344
1426
  return -1;
1345
1427
  };
1346
- function ga(e) {
1428
+ function Ea(e) {
1347
1429
  let t = /* @__PURE__ */ new Set();
1348
- for (let n of e.matchAll(ma)) {
1349
- let r = ha(e, n.index ?? 0);
1430
+ for (let n of e.matchAll(wa)) {
1431
+ let r = Ta(e, n.index ?? 0);
1350
1432
  if (r === -1) continue;
1351
- let i = ea(e, r), a = e.slice(r, i === -1 ? void 0 : i + 1).match(pa);
1433
+ let i = ua(e, r), a = e.slice(r, i === -1 ? void 0 : i + 1).match(Ca);
1352
1434
  a && t.add(`${n[1]}: Date.now() - … beside ${a[0].replace(/\s+/g, "")}`);
1353
1435
  }
1354
1436
  return [...t];
1355
1437
  }
1356
- var _a = (e, t, n) => {
1438
+ var Da = (e, t, n) => {
1357
1439
  let r = 0;
1358
1440
  for (let i = t + 1; i < n; i++) {
1359
1441
  let t = e[i];
@@ -1362,29 +1444,29 @@ var _a = (e, t, n) => {
1362
1444
  else if (t === "," && r === 0) return i;
1363
1445
  }
1364
1446
  return n;
1365
- }, va = (e, t) => {
1447
+ }, Oa = (e, t) => {
1366
1448
  let n = t.replace(/\$/g, "\\$"), r = new RegExp(String.raw`\b(?:(?:const|let|var)\s+${n}\s*=\s*(?:useCallback\s*\(\s*)?(?:async\s+)?(?:function\b[^{]*|(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*(?::\s*[^={]+)?=>\s*\(?\s*)|function\s+${n}\s*\([^()]*\)[^{]*)\{`).exec(e);
1367
1449
  if (!r) return "";
1368
- let i = ea(e, r.index + r[0].length - 1);
1450
+ let i = ua(e, r.index + r[0].length - 1);
1369
1451
  return i === -1 ? "" : e.slice(r.index, i + 1);
1370
1452
  };
1371
- function ya(e) {
1453
+ function ka(e) {
1372
1454
  for (let t of e.matchAll(/\bonSnapshot\s*\(/g)) {
1373
- let n = (t.index ?? 0) + t[0].length - 1, r = Zi(e, n);
1455
+ let n = (t.index ?? 0) + t[0].length - 1, r = Q(e, n);
1374
1456
  if (r === -1) continue;
1375
- let i = _a(e, n, r), a = e.slice(n + 1, i).trim();
1457
+ let i = Da(e, n, r), a = e.slice(n + 1, i).trim();
1376
1458
  if (/^[A-Za-z_$][\w$]*$/.test(a)) {
1377
1459
  let t = new RegExp(String.raw`\b(?:const|let|var)\s+${a.replace(/\$/g, "\\$")}\s*=\s*([^;]{0,300})`).exec(e);
1378
1460
  a = t ? t[1] : "";
1379
1461
  }
1380
- let o = a.match(da);
1462
+ let o = a.match(xa);
1381
1463
  if (!o) continue;
1382
1464
  let s = e.slice(i, r);
1383
1465
  if (/\bsubmitter(?:Name|Email)\b/.test(s)) continue;
1384
- let c = /* @__PURE__ */ new Set(), l = s.match(pa);
1466
+ let c = /* @__PURE__ */ new Set(), l = s.match(Ca);
1385
1467
  l && c.add(l[0].replace(/\s+/g, ""));
1386
1468
  for (let [, t] of s.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
1387
- let n = va(e, t).match(pa);
1469
+ let n = Oa(e, t).match(Ca);
1388
1470
  n && c.add(`${n[0].replace(/\s+/g, "")} (inside ${t})`);
1389
1471
  }
1390
1472
  if (c.size) return {
@@ -1394,27 +1476,27 @@ function ya(e) {
1394
1476
  }
1395
1477
  return null;
1396
1478
  }
1397
- var ba = /\bdoc\s*\(\s*[\w.]+\s*,\s*['"`]user_?(?:profiles|s)['"`]\s*,\s*([^,)]+)\)/g, xa = /^(?:auth\s*\.\s*)?(?:user|currentUser|me|viewer)\s*[?!]?\s*\.\s*uid$|^uid$/, Sa = /\b(createdBy|updatedBy|addedBy|editedBy|modifiedBy|lastEditedBy|lastModifiedBy|createdByName|updatedByName)\s*:\s*[^,\n}]*\b(?:user|currentUser)\s*[?!]?\s*\.\s*(?:email|displayName|uid)\b/g;
1398
- function Ca(e) {
1479
+ var Aa = /\bdoc\s*\(\s*[\w.]+\s*,\s*['"`]user_?(?:profiles|s)['"`]\s*,\s*([^,)]+)\)/g, ja = /^(?:auth\s*\.\s*)?(?:user|currentUser|me|viewer)\s*[?!]?\s*\.\s*uid$|^uid$/, Ma = /\b(createdBy|updatedBy|addedBy|editedBy|modifiedBy|lastEditedBy|lastModifiedBy|createdByName|updatedByName)\s*:\s*[^,\n}]*\b(?:user|currentUser)\s*[?!]?\s*\.\s*(?:email|displayName|uid)\b/g;
1480
+ function Na(e) {
1399
1481
  let t = /* @__PURE__ */ new Set();
1400
- for (let n of e.matchAll(ba)) {
1482
+ for (let n of e.matchAll(Aa)) {
1401
1483
  let e = n[1].replace(/\s+/g, "");
1402
- xa.test(e) || t.add(`a profile read for ${e}`);
1484
+ ja.test(e) || t.add(`a profile read for ${e}`);
1403
1485
  }
1404
- for (let n of e.matchAll(Sa)) t.add(`${n[1]} signed by hand`);
1486
+ for (let n of e.matchAll(Ma)) t.add(`${n[1]} signed by hand`);
1405
1487
  return [...t];
1406
1488
  }
1407
- var wa = (e, t) => `${e}: ${P(t)} — the page works out WHO MADE a record for itself. It is already on the record: actions.saveRecord stamps creatorName, creatorAvatar, creatorEmail and createdAt on everything it saves, and actions.updateRecord stamps editedName, editedAvatar and editedAt on every change. Draw them with <MadeBy record={item} /> and <MadeBy record={item} edited /> from 'your-ai-workflow-firebase-os', and delete the rest: reading another person's user_profiles document is "Missing or insufficient permissions" for a role the owner invented and for every member — the card draws for the admin who built the page and for nobody it was built for — and on a card made from an Inbox entry it names the admin instead of the sender; a createdBy: user.email written by hand is an address where a name was wanted. There is no users collection. A person somebody PICKED (an assignee) is saved as a uid AND a name on the record, and the name is what the page draws. The one name a page passes itself is a change made for somebody else: actions.updateRecord(id, { stock, editedName: order.submitterName }) (contract rules 29 and 31).`, Ta = (e, t) => `${e}: ${P(t)} — a history line signed with whoever is LOOKING at the page, at a time made up by counting back from now. Lines like that are invented after the fact: every one of them says the owner, or whoever opens the page next, did something they never did, at a time nobody did it. Delete the code that makes them, and any copy of them the page kept in the browser. A history holds only the lines written at the moment something is done, by the save that does it, each signed by the person who did it — and while there are none, it says so (contract rule 29). On a page for the team that is two pieces of the app: logHistory('<the records' collection>', record.id, 'what was done', { from, to }) straight after the save — it signs the line itself — and <History collection="…" of={record.id} /> to draw them.${F(t.length)}`, Ea = /['"`]user_(?:submissions|requests)['"`]/, Da = /\bstock\b[^\n;]{0,120}\b(?:quantity|qty)\b/i, Oa = (e) => Ea.test(e) && Da.test(e) && !/\buseStock(?:FromOrders)?\b/.test(e), ka = /<Basket\b|<Checkout\b|<BuyButton\b|usePublicCopy\(|useTallies\(/, Aa = /\b(?:[A-Za-z_$][\w$]*(?:\??\.?\[[^\]\n]*\])*\??\.)+stock\b(?!SetAt)/, ja = (e) => ka.test(e) && Aa.test(e) && !/\buseStock(?:FromOrders)?\s*\(/.test(e), Ma = String.raw`(?:\b\w*[vV]ariants\s*\??|\(\s*[\w.?]*[vV]ariants\s*(?:\|\||\?\?)\s*\[\]\s*\))\s*\.\s*`, Na = String.raw`\s*\(\s*\(?\s*\w+\s*(?::\s*[\w<>[\]|. ]+)?,\s*(\w+)\s*(?::\s*[\w<>[\]|. ]+)?\)?\s*=>`, Pa = RegExp(`${Ma}reduce${Na}[^;]{0,200}?(?:\\b\\1\\s*\\??\\.\\s*stock\\b|\\.left\\s*\\([^)]*\\b\\1\\s*\\??\\.\\s*id\\b)`, "g"), Fa = RegExp(`${Ma}flatMap\\s*\\(\\s*\\(?\\s*(\\w+)\\s*(?::\\s*[\\w<>[\\]|. ]+)?\\)?\\s*=>\\s*\\(?\\s*\\1\\s*\\??\\.\\s*(?:shades|colou?rs|swatch(?:es)?)\\b`, "g"), Ia = (e, t) => {
1489
+ var Pa = (e, t) => `${e}: ${P(t)} — the page works out WHO MADE a record for itself. It is already on the record: actions.saveRecord stamps creatorName, creatorAvatar, creatorEmail and createdAt on everything it saves, and actions.updateRecord stamps editedName, editedAvatar and editedAt on every change. Draw them with <MadeBy record={item} /> and <MadeBy record={item} edited /> from 'your-ai-workflow-firebase-os', and delete the rest: reading another person's user_profiles document is "Missing or insufficient permissions" for a role the owner invented and for every member — the card draws for the admin who built the page and for nobody it was built for — and on a card made from an Inbox entry it names the admin instead of the sender; a createdBy: user.email written by hand is an address where a name was wanted. There is no users collection. A person somebody PICKED (an assignee) is saved as a uid AND a name on the record, and the name is what the page draws. The one name a page passes itself is a change made for somebody else: actions.updateRecord(id, { stock, editedName: order.submitterName }) (contract rules 29 and 31).`, Fa = (e, t) => `${e}: ${P(t)} — a history line signed with whoever is LOOKING at the page, at a time made up by counting back from now. Lines like that are invented after the fact: every one of them says the owner, or whoever opens the page next, did something they never did, at a time nobody did it. Delete the code that makes them, and any copy of them the page kept in the browser. A history holds only the lines written at the moment something is done, by the save that does it, each signed by the person who did it — and while there are none, it says so (contract rule 29). On a page for the team that is two pieces of the app: logHistory('<the records' collection>', record.id, 'what was done', { from, to }) straight after the save — it signs the line itself — and <History collection="…" of={record.id} /> to draw them.${F(t.length)}`, Ia = /['"`]user_(?:submissions|requests)['"`]/, La = /\bstock\b[^\n;]{0,120}\b(?:quantity|qty)\b/i, Ra = (e) => Ia.test(e) && La.test(e) && !/\buseStock(?:FromOrders)?\b/.test(e), za = /<Basket\b|<Checkout\b|<BuyButton\b|usePublicCopy\(|useTallies\(/, Ba = /\b(?:[A-Za-z_$][\w$]*(?:\??\.?\[[^\]\n]*\])*\??\.)+stock\b(?!SetAt)/, Va = (e) => za.test(e) && Ba.test(e) && !/\buseStock(?:FromOrders)?\s*\(/.test(e), Ha = String.raw`(?:\b\w*[vV]ariants\s*\??|\(\s*[\w.?]*[vV]ariants\s*(?:\|\||\?\?)\s*\[\]\s*\))\s*\.\s*`, Ua = String.raw`\s*\(\s*\(?\s*\w+\s*(?::\s*[\w<>[\]|. ]+)?,\s*(\w+)\s*(?::\s*[\w<>[\]|. ]+)?\)?\s*=>`, Wa = RegExp(`${Ha}reduce${Ua}[^;]{0,200}?(?:\\b\\1\\s*\\??\\.\\s*stock\\b|\\.left\\s*\\([^)]*\\b\\1\\s*\\??\\.\\s*id\\b)`, "g"), Ga = RegExp(`${Ha}flatMap\\s*\\(\\s*\\(?\\s*(\\w+)\\s*(?::\\s*[\\w<>[\\]|. ]+)?\\)?\\s*=>\\s*\\(?\\s*\\1\\s*\\??\\.\\s*(?:shades|colou?rs|swatch(?:es)?)\\b`, "g"), Ka = (e, t) => {
1408
1490
  for (let n of e.matchAll(t)) {
1409
1491
  let t = e.slice(Math.max(0, (n.index ?? 0) - 160), n.index);
1410
1492
  if (!/\.(?:reduce|flatMap)\s*\(|\bfor\s*\(/.test(t)) return !0;
1411
1493
  }
1412
1494
  return !1;
1413
- }, La = (e) => [...Ia(e, Pa) ? ["stock"] : [], ...Ia(e, Fa) ? ["shades"] : []], Ra = (e, t) => `${e}: this page draws a thing's variants as one — it ${[...t.includes("stock") ? ["adds its variants' stock up into one number (variants.reduce(… .stock …)), to show on its card or to save as the whole product's stock"] : [], ...t.includes("shades") ? ["runs every variant's shades together into one row of swatches (variants.flatMap((v) => v.shades)) that nobody can press"] : []].join(", and ")}. A thing with variants is shown as ONE variant at a time, the chosen one, its card included: every card keeps its own chosen variant (its first until another is pressed), each variant is one <Swatch color={variant.shades[0]} name={variant.name} selected={variant.id === chosen.id} onClick={() => choose(variant)} /> ON the card (its press never opens the card too), and pressing one swaps that card's picture and stock for that variant's right there, without opening anything. The stock shown is the chosen variant's own — stock.left(product, variant.id) where the page uses useStock — never the variants added up, and nothing keeps a product-wide stock beside theirs. A total over the whole shop is fine. Change nothing else on the page.`, za = (e) => {
1495
+ }, qa = (e) => [...Ka(e, Wa) ? ["stock"] : [], ...Ka(e, Ga) ? ["shades"] : []], Ja = (e, t) => `${e}: this page draws a thing's variants as one — it ${[...t.includes("stock") ? ["adds its variants' stock up into one number (variants.reduce(… .stock …)), to show on its card or to save as the whole product's stock"] : [], ...t.includes("shades") ? ["runs every variant's shades together into one row of swatches (variants.flatMap((v) => v.shades)) that nobody can press"] : []].join(", and ")}. A thing with variants is shown as ONE variant at a time, the chosen one, its card included: every card keeps its own chosen variant (its first until another is pressed), each variant is one <Swatch color={variant.shades[0]} name={variant.name} selected={variant.id === chosen.id} onClick={() => choose(variant)} /> ON the card (its press never opens the card too), and pressing one swaps that card's picture and stock for that variant's right there, without opening anything. The stock shown is the chosen variant's own — stock.left(product, variant.id) where the page uses useStock — never the variants added up, and nothing keeps a product-wide stock beside theirs. A total over the whole shop is fine. Change nothing else on the page.`, Ya = (e) => {
1414
1496
  if (!/\bcursor-zoom-in\b|<ZoomIn\b/.test(e)) return !1;
1415
- let t = an(e, "Lightbox");
1497
+ let t = Z(e, "Lightbox");
1416
1498
  return t.length > 0 && t.every((e) => !/(?:^|\s)zoom(?:\s|=|\/|$)/.test(e.attrs));
1417
- }, Ba = (e) => `${e}: this page shows a picture as something to ZOOM (a zoom-in cursor or a magnifier) and then opens it in <Lightbox> without zoom — a gallery: the same picture a little bigger with arrows through the others. Viewing and zooming are two things. Viewing is on the page: the small pictures swap the big one IN PLACE (never open a window). Zooming is pressing the big one: <Lightbox pictures={pictures} index={at} onIndex={setAt} onClose={() => setAt(null)} zoom /> opens it whole and magnifies the spot pressed in it — the close-up then follows the mouse, a finger drags and pinches it, one more press shows it whole again. Add zoom to the <Lightbox>, make the small pictures swap the big one, and change nothing else on the page.`, Va = (e) => `${e}: this page reads a stock number straight off a record — product.stock, or a variant's — to show it or to decide whether one can still be bought. That number is the count my team last SET, not what is left: it is right until the first sale and wrong for every visitor from then on, because a sale writes nothing to the record. What is left is the app's to work out: const stock = useStock(records) from 'your-ai-workflow-firebase-os' over the records this page shows, and stock.left(product) or stock.left(product, variant.id) is the number to draw and the number that decides — null where a thing keeps no count. Never product.stock or variants[i].stock in what is shown or compared.`, Ha = (e) => `${e}: this page takes the stock of what was sold by itself — it reads the shop's orders out of user_submissions and counts each line's quantity off a stock number. Written by hand that goes wrong silently: stock taken twice after a reload, taken for a basket nobody paid for, or "Missing or insufficient permissions" for everybody who is not an admin. Nothing is taken off the record at all any more: the record keeps the count my team last set (stock, or variants[i].stock, each variant with an id of its own), the app dates it when it is saved, and what is LEFT is that count minus what Stripe says was paid for since — const stock = useStock(records) from 'your-ai-workflow-firebase-os', and stock.left(product) or stock.left(product, variant.id) is the number, on this page and on the shop alike, for everybody, with nobody signed in. Delete the listener, the useSales() join, the decrement and the list of applied entries.`, Ua = (e, t, n) => `${e}: it applies entries from ${t} — things somebody else SENT — and writes them up signed with ${P(n)}. That code runs in the browser of the admin who has the page open, so every line names that admin, whoever actually placed the order or sent the entry. Take the person off the entry instead: submitterName and submitterEmail when they were signed in, else the name and email they typed into the form, else "A visitor" — and the time off the entry's submittedAt, not the moment the page got round to it. A helper that writes a line takes the person as an argument rather than reading user itself (contract rule 29) — the app's own logHistory(collection, id, what, { by: { name }, at }) does exactly that.`, Wa = (e, t) => {
1499
+ }, Xa = (e) => `${e}: this page shows a picture as something to ZOOM (a zoom-in cursor or a magnifier) and then opens it in <Lightbox> without zoom — a gallery: the same picture a little bigger with arrows through the others. Viewing and zooming are two things. Viewing is on the page: the small pictures swap the big one IN PLACE (never open a window). Zooming is pressing the big one: <Lightbox pictures={pictures} index={at} onIndex={setAt} onClose={() => setAt(null)} zoom /> opens it whole and magnifies the spot pressed in it — the close-up then follows the mouse, a finger drags and pinches it, one more press shows it whole again. Add zoom to the <Lightbox>, make the small pictures swap the big one, and change nothing else on the page.`, Za = (e) => `${e}: this page reads a stock number straight off a record — product.stock, or a variant's — to show it or to decide whether one can still be bought. That number is the count my team last SET, not what is left: it is right until the first sale and wrong for every visitor from then on, because a sale writes nothing to the record. What is left is the app's to work out: const stock = useStock(records) from 'your-ai-workflow-firebase-os' over the records this page shows, and stock.left(product) or stock.left(product, variant.id) is the number to draw and the number that decides — null where a thing keeps no count. Never product.stock or variants[i].stock in what is shown or compared.`, Qa = (e) => `${e}: this page takes the stock of what was sold by itself — it reads the shop's orders out of user_submissions and counts each line's quantity off a stock number. Written by hand that goes wrong silently: stock taken twice after a reload, taken for a basket nobody paid for, or "Missing or insufficient permissions" for everybody who is not an admin. Nothing is taken off the record at all any more: the record keeps the count my team last set (stock, or variants[i].stock, each variant with an id of its own), the app dates it when it is saved, and what is LEFT is that count minus what Stripe says was paid for since — const stock = useStock(records) from 'your-ai-workflow-firebase-os', and stock.left(product) or stock.left(product, variant.id) is the number, on this page and on the shop alike, for everybody, with nobody signed in. Delete the listener, the useSales() join, the decrement and the list of applied entries.`, $a = (e, t, n) => `${e}: it applies entries from ${t} — things somebody else SENT — and writes them up signed with ${P(n)}. That code runs in the browser of the admin who has the page open, so every line names that admin, whoever actually placed the order or sent the entry. Take the person off the entry instead: submitterName and submitterEmail when they were signed in, else the name and email they typed into the form, else "A visitor" — and the time off the entry's submittedAt, not the moment the page got round to it. A helper that writes a line takes the person as an argument rather than reading user itself (contract rule 29) — the app's own logHistory(collection, id, what, { by: { name }, at }) does exactly that.`, eo = (e, t) => {
1418
1500
  let n = /<(?:PayButton|BuyButton)\b/.test(e), r = [], i = /* @__PURE__ */ new Set();
1419
1501
  for (let a of e.matchAll(/<ContactPopup\b/g)) {
1420
1502
  let o = a.index ?? 0, s = e.indexOf("/>", o), c = e.slice(o, s === -1 ? o + 800 : s + 2), l = c.match(/formId=\{?\s*['"`]([^'"`]+)['"`]/)?.[1];
@@ -1440,14 +1522,14 @@ var wa = (e, t) => `${e}: ${P(t)} — the page works out WHO MADE a record for i
1440
1522
  }));
1441
1523
  }
1442
1524
  return r;
1443
- }, Ga = (e, t) => `${e}: this is the order ${t} sends back beside its <PayButton>, and an order asks the buyer NOTHING (fields: []) — who is paying is their ACCOUNT. Put <BuyerDetails onReady={setBuyer} phone address /> from 'your-ai-workflow-firebase-os' before the form, take every name, email, phone and address question out of the form, spread { ...buyer.metadata } into the button's metadata, and carry buyer.name, buyer.email, buyer.phone and buyer.address in the form's extra with the items. Asked through the form, the buyer has no account: their order can never be shown back to them, and a page built on these entries — an Orders page each buyer sees their own on — has nobody to send it to (contract rule 9).`, Ka = /(?:\bto|\bhref|\borderPath)=\{?\s*['"`]\/([A-Za-z0-9_-]+)[?'"`]|\bnavigate\(\s*['"`]\/([A-Za-z0-9_-]+)[?'"`]/g, $ = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, ""), qa = /\baddDoc\s*\(\s*collection\s*\(\s*db\s*,\s*['"`]case_|\bsetDoc\s*\(\s*doc\s*\(\s*collection\s*\(\s*db\s*,\s*['"`]case_/, Ja = (e, t, n) => {
1525
+ }, to = (e, t) => `${e}: this is the order ${t} sends back beside its <PayButton>, and an order asks the buyer NOTHING (fields: []) — who is paying is their ACCOUNT. Put <BuyerDetails onReady={setBuyer} phone address /> from 'your-ai-workflow-firebase-os' before the form, take every name, email, phone and address question out of the form, spread { ...buyer.metadata } into the button's metadata, and carry buyer.name, buyer.email, buyer.phone and buyer.address in the form's extra with the items. Asked through the form, the buyer has no account: their order can never be shown back to them, and a page built on these entries — an Orders page each buyer sees their own on — has nobody to send it to (contract rule 9).`, no = /(?:\bto|\bhref|\borderPath)=\{?\s*['"`]\/([A-Za-z0-9_-]+)[?'"`]|\bnavigate\(\s*['"`]\/([A-Za-z0-9_-]+)[?'"`]/g, $ = (e) => e.toLowerCase().replace(/[^a-z0-9]+/g, ""), ro = /\baddDoc\s*\(\s*collection\s*\(\s*db\s*,\s*['"`]case_|\bsetDoc\s*\(\s*doc\s*\(\s*collection\s*\(\s*db\s*,\s*['"`]case_/, io = (e, t, n) => {
1444
1526
  let r = (e) => [...t.matchAll(RegExp(`<${e}\\b`, "g"))].map((e) => {
1445
1527
  let n = e.index ?? 0, r = t.indexOf("/>", n);
1446
1528
  return t.slice(n, r === -1 ? n + 800 : r + 2);
1447
1529
  }), i = [...r("Basket"), ...r("Checkout")].find((e) => /\bformId=/.test(e)), a = (i ?? r("ContactPopup").find((e) => /\bextra=/.test(e)))?.match(/formId=\{?\s*['"`]([^'"`]+)['"`]/)?.[1];
1448
1530
  if (!a) return [];
1449
1531
  let o = [], s = /* @__PURE__ */ new Set(), c = [];
1450
- for (let e of t.matchAll(Ka)) {
1532
+ for (let e of t.matchAll(no)) {
1451
1533
  let t = $(e[1] || e[2] || "");
1452
1534
  if (!t || s.has(t)) continue;
1453
1535
  s.add(t);
@@ -1466,12 +1548,12 @@ var wa = (e, t) => `${e}: ${P(t)} — the page works out WHO MADE a record for i
1466
1548
  }
1467
1549
  };
1468
1550
  }, u = (e) => {
1469
- let t = e.match(da)?.[1], n = RegExp(`['"\`]${a.replace(/[^A-Za-z0-9_-]/g, "")}['"\`]`).test(e);
1551
+ let t = e.match(xa)?.[1], n = RegExp(`['"\`]${a.replace(/[^A-Za-z0-9_-]/g, "")}['"\`]`).test(e);
1470
1552
  return t && n && !/\bappliedEntries\b/.test(e) ? t : void 0;
1471
1553
  };
1472
1554
  for (let e of c) {
1473
1555
  let { theirs: t, gap: n } = l(e);
1474
- !/\b(?:usePublished|publish)\s*\(/.test(t) && /\buseCases\s*\(/.test(t) && qa.test(t) && o.push({
1556
+ !/\b(?:usePublished|publish)\s*\(/.test(t) && /\buseCases\s*\(/.test(t) && ro.test(t) && o.push({
1475
1557
  ...n,
1476
1558
  why: "typed",
1477
1559
  inbox: ""
@@ -1493,7 +1575,7 @@ var wa = (e, t) => `${e}: ${P(t)} — the page works out WHO MADE a record for i
1493
1575
  });
1494
1576
  }
1495
1577
  return o;
1496
- }, Ya = (e) => {
1578
+ }, ao = (e) => {
1497
1579
  let t = [], n = /* @__PURE__ */ new Set(), r = (e, r, i) => {
1498
1580
  let a = `${$(e)}|${r}`;
1499
1581
  !e || n.has(a) || (n.add(a), t.push({
@@ -1517,17 +1599,17 @@ var wa = (e, t) => `${e}: ${P(t)} — the page works out WHO MADE a record for i
1517
1599
  !e || e === "Dashboard" || r(e.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), `/${e.toLowerCase()}`, t);
1518
1600
  }
1519
1601
  return t;
1520
- }, Xa = (e) => e.replace(/<[^>]*>/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/&\w+;/g, " ").replace(/\s+/g, " ").trim(), Za = (e, t) => {
1602
+ }, oo = (e) => e.replace(/<[^>]*>/g, " ").replace(/\{[^{}]*\}/g, " ").replace(/&\w+;/g, " ").replace(/\s+/g, " ").trim(), so = (e, t) => {
1521
1603
  let n = e.slice(0, t);
1522
1604
  return (n.match(/<Link\b/g) || []).length > (n.match(/<\/Link>/g) || []).length;
1523
- }, Qa = /\bhref\s*=\s*(?:\{\s*)?['"`]\s*#?\s*['"`]/, $a = /\bonClick\s*=\s*\{\s*\(\s*\)\s*=>\s*\{\s*\}\s*\}/;
1524
- function eo(e, t, n) {
1605
+ }, co = /\bhref\s*=\s*(?:\{\s*)?['"`]\s*#?\s*['"`]/, lo = /\bonClick\s*=\s*\{\s*\(\s*\)\s*=>\s*\{\s*\}\s*\}/;
1606
+ function uo(e, t, n) {
1525
1607
  let r = [], i = /* @__PURE__ */ new Set();
1526
- for (let { tag: a, attrs: o, at: s, end: c } of an(t, "button|Button|a")) {
1527
- if (/\bonClick\s*=/.test(o) && !$a.test(o) || /\bdisabled\b|\{\.\.\./.test(o) || a !== "a" && /type\s*=\s*['"`]submit['"`]/.test(o) || a === "a" && /\bhref\s*=/.test(o) && !Qa.test(o) || Za(t, s)) continue;
1608
+ for (let { tag: a, attrs: o, at: s, end: c } of Z(t, "button|Button|a")) {
1609
+ if (/\bonClick\s*=/.test(o) && !lo.test(o) || /\bdisabled\b|\{\.\.\./.test(o) || a !== "a" && /type\s*=\s*['"`]submit['"`]/.test(o) || a === "a" && /\bhref\s*=/.test(o) && !co.test(o) || so(t, s)) continue;
1528
1610
  let l = t.indexOf(`</${a.replace("motion.", "")}>`, c);
1529
1611
  if (l === -1) continue;
1530
- let u = Xa(t.slice(c + 1, l));
1612
+ let u = oo(t.slice(c + 1, l));
1531
1613
  if (!u || u.length > 40) continue;
1532
1614
  let d = ` ${u.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `, f = n.find((t) => t.file !== e && t.words.length > 2 && d.includes(` ${t.words} `));
1533
1615
  !f || i.has(u) || (i.add(u), r.push({
@@ -1537,7 +1619,7 @@ function eo(e, t, n) {
1537
1619
  }
1538
1620
  return r;
1539
1621
  }
1540
- var to = (e, t) => `${e}: ${P(t.map((e) => `"${e.label}"`))} ${t.length > 1 ? "name pages" : "names a page"} this app HAS — ${P([...new Set(t.map((e) => e.route))])} — and ${t.length > 1 ? "none of them leads" : "it leads"} anywhere: no link, no handler, nothing happens when it is pressed. This is what a page built BEFORE that one looks like — the button was written when there was nowhere to send it. Wrap exactly the words and classes that are there in <Link to="${t[0].route}" className="…"> (or navigate('${t[0].route}') where a press has to do something first), and change nothing else about it: not its words, not its look, not where it sits.${F(t.length)} (rule 7)`, no = (e, t, n) => `${e}: ${t} sends what is made there as a form entry ('${n}') and links here — and the app itself lets each of those entries into this page's case_ collection, sent to the person who made it and to the team. So this page only READS them with useCases(): delete the form, button or function that writes a case_ entry by hand. An entry typed in here was paid for by nobody and belongs to nobody, and one sent to a whole role ('role:client') is shown to every person marked with it. While there are no entries yet, show a short empty line — never made-up ones (contract rule 11).`, ro = (e, t) => `${e.target}: it fetches the '${e.formId}' entries out of ${e.inbox} itself — what ${t} sends — and what is in there is not this page's to read. ${e.paid ? "<Checkout> saves each one BEFORE the money is taken, so that list holds every basket somebody walked away from, and this page draws each of them as an order. " : ""}An entry there carries uid and submittedAt — never userId, customerName or createdAt — so a list filtered on a guessed key is empty for the very person who ordered, and nobody but an admin may read the rest of it at all. The app already lets each ${e.paid ? "PAID " : ""}one into this page's own collection, sent to the person who made it and to the team: read them with const { loading, records, error } = useCases('${e.reads}') — the name this page's own sentence gave, if it gave another — and with nothing else. Delete the ${e.inbox} query, and any COPY of the entries kept in a collection of its own for somebody to work from: a stage, a note, who it was handed to are saved ON the entry (updateDoc on it), and handing one to a single person is sendTo('${e.reads}', id, { people: [uid] }) with the person picked from usePeople('internal') — a copy is a second order the person who made it never sees change. Never keep the list in localStorage either. Change nothing about how the page looks (contract rule 11).`, io = (e, t) => `${e}: this form is given extra={…} — an order going back to the page that keeps the record (on ${t}) — it is a payment's, and its entries are half of a transaction, not mail. Add transaction: true to its config, and they land on the owner's Transactions screen beside the payment instead of in the Inbox.`, ao = (e, t) => `${e}: this form stands on a page with a <PayButton> (on ${t}) — the questions asked before paying. Who is paying is not a form, it is their ACCOUNT: replace it with <BuyerDetails onReady={setBuyer} /> from 'your-ai-workflow-firebase-os' (pass phone / address when the page asks for them), show the <PayButton> once onReady has fired with { ...buyer.metadata } spread into its metadata, and delete the form's config. A form here lands every buyer in the Inbox as mail and rings the owner's bell with "New form submission" instead of "New payment" (contract rule 9).`, oo = (e, t) => `${e}: it reads ${t} without ever looking at formId, so it takes in every form in the app — the contact form, a newsletter box, a form on somebody else's page — not just this page's own. Read the entry's formId and skip the ones that are not this page's form (const FORM_ID = 'quote'; if (s.formId !== FORM_ID) return;), unless the owner asked for the whole Inbox in so many words. A booking is in neither collection: a time somebody takes is on the calendar, and useBookings() is the only way to read it.`, so = /loadStripe|@stripe\/stripe-js|new Stripe\(|fetch\(\s*['"`]\/api\/payments/, co = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} hand-rolled payment code that is not connected to the owner's Stripe account — delete it and take money with <PayButton amount={25} productName="…" /> for a one-off or <PayButton priceId="price_…" mode="subscription" /> for a subscription, imported from 'your-ai-workflow-firebase-os' (read purchases with useMyPayments(); an admins' page reads every sale with useSales()).`, lo = /api\.resend\.com|from\s+['"]resend['"]|fetch\(\s*['"`]\/api\/email/, uo = /from\s+['"]firebase\/storage['"]|\buploadBytes(?:Resumable)?\s*\(|\bgetDownloadURL\s*\(|\bgetStorage\s*\(|\bref\s*\(\s*storage\b/, fo = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} upload code written by hand, and the file it writes is guarded by a different rule than the record that links to it — delete it and take the file with <UploadBox value={photo} onChange={setPhoto} label="Product photo" /> imported from 'your-ai-workflow-firebase-os' (add \`shared\` when the picture is also shown on a page this one's people are not the audience for; a file attached while filling in a form is the form's own \`file\` question instead).`, po = /\b(?:window|globalThis|self)\s*\.\s*(?:confirm|alert|prompt)\b|(?<![\w$.]|function\s)(?:confirm|alert|prompt)\((?![^()]*\)\s*\{)/, mo = (e, t) => `${e}: ${P(t.map((e) => e.replace(/\($/, "()")))} ${t.length > 1 ? "open" : "opens"} the browser's own pop-up, drawn by the browser and not the app, over the owner's themed page — delete ${t.length > 1 ? "every one" : "it"}. A delete asks first in <ConfirmModal isOpen={!!toDelete} title="Delete this?" message="It cannot be undone." confirmText="Delete" onConfirm={removeIt} onCancel={() => setToDelete(null)} /> imported from 'your-ai-workflow-firebase-os' (on a from-scratch page actions.confirmDelete(id) opens it for you, and a drive's deleteFile(file) already asks); anything to tell somebody is words you styled on the page.`, ho = /<input[^>]*type\s*=\s*\{?['"]file['"]\}?/i, go = (e) => `${e}: an <input type="file"> uploads nothing on its own — draw the app's own box instead, <UploadBox value={photo} onChange={setPhoto} label="Product photo" /> imported from 'your-ai-workflow-firebase-os', which uploads as the file is picked or dropped and hands back the link to save on the record (on a form it is the form's own \`file\` question, and on a drive page it is openUploadModal()).`, _o = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} hand-rolled email code — delete it and send with emailOwner('new_submission', {…}) or emailOwner('heads_up', { title, summary }) to reach the owner, emailPerson('submission_received', collection, docId) to reach the person a record names, or — on a page only the owner's team can open — emailAnyone(to, subject, message), all imported from 'your-ai-workflow-firebase-os'. A public page never names a recipient or writes an email body.`, vo = [
1622
+ var fo = (e, t) => `${e}: ${P(t.map((e) => `"${e.label}"`))} ${t.length > 1 ? "name pages" : "names a page"} this app HAS — ${P([...new Set(t.map((e) => e.route))])} — and ${t.length > 1 ? "none of them leads" : "it leads"} anywhere: no link, no handler, nothing happens when it is pressed. This is what a page built BEFORE that one looks like — the button was written when there was nowhere to send it. Wrap exactly the words and classes that are there in <Link to="${t[0].route}" className="…"> (or navigate('${t[0].route}') where a press has to do something first), and change nothing else about it: not its words, not its look, not where it sits.${F(t.length)} (rule 7)`, po = (e, t, n) => `${e}: ${t} sends what is made there as a form entry ('${n}') and links here — and the app itself lets each of those entries into this page's case_ collection, sent to the person who made it and to the team. So this page only READS them with useCases(): delete the form, button or function that writes a case_ entry by hand. An entry typed in here was paid for by nobody and belongs to nobody, and one sent to a whole role ('role:client') is shown to every person marked with it. While there are no entries yet, show a short empty line — never made-up ones (contract rule 11).`, mo = (e, t) => `${e.target}: it fetches the '${e.formId}' entries out of ${e.inbox} itself — what ${t} sends — and what is in there is not this page's to read. ${e.paid ? "<Checkout> saves each one BEFORE the money is taken, so that list holds every basket somebody walked away from, and this page draws each of them as an order. " : ""}An entry there carries uid and submittedAt — never userId, customerName or createdAt — so a list filtered on a guessed key is empty for the very person who ordered, and nobody but an admin may read the rest of it at all. The app already lets each ${e.paid ? "PAID " : ""}one into this page's own collection, sent to the person who made it and to the team: read them with const { loading, records, error } = useCases('${e.reads}') — the name this page's own sentence gave, if it gave another — and with nothing else. Delete the ${e.inbox} query, and any COPY of the entries kept in a collection of its own for somebody to work from: a stage, a note, who it was handed to are saved ON the entry (updateDoc on it), and handing one to a single person is sendTo('${e.reads}', id, { people: [uid] }) with the person picked from usePeople('internal') — a copy is a second order the person who made it never sees change. Never keep the list in localStorage either. Change nothing about how the page looks (contract rule 11).`, ho = (e, t) => `${e}: this form is given extra={…} — an order going back to the page that keeps the record (on ${t}) — it is a payment's, and its entries are half of a transaction, not mail. Add transaction: true to its config, and they land on the owner's Transactions screen beside the payment instead of in the Inbox.`, go = (e, t) => `${e}: this form stands on a page with a <PayButton> (on ${t}) — the questions asked before paying. Who is paying is not a form, it is their ACCOUNT: replace it with <BuyerDetails onReady={setBuyer} /> from 'your-ai-workflow-firebase-os' (pass phone / address when the page asks for them), show the <PayButton> once onReady has fired with { ...buyer.metadata } spread into its metadata, and delete the form's config. A form here lands every buyer in the Inbox as mail and rings the owner's bell with "New form submission" instead of "New payment" (contract rule 9).`, _o = (e, t) => `${e}: it reads ${t} without ever looking at formId, so it takes in every form in the app — the contact form, a newsletter box, a form on somebody else's page — not just this page's own. Read the entry's formId and skip the ones that are not this page's form (const FORM_ID = 'quote'; if (s.formId !== FORM_ID) return;), unless the owner asked for the whole Inbox in so many words. A booking is in neither collection: a time somebody takes is on the calendar, and useBookings() is the only way to read it.`, vo = /loadStripe|@stripe\/stripe-js|new Stripe\(|fetch\(\s*['"`]\/api\/payments/, yo = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} hand-rolled payment code that is not connected to the owner's Stripe account — delete it and take money with <PayButton amount={25} productName="…" /> for a one-off or <PayButton priceId="price_…" mode="subscription" /> for a subscription, imported from 'your-ai-workflow-firebase-os' (read purchases with useMyPayments(); an admins' page reads every sale with useSales()).`, bo = /api\.resend\.com|from\s+['"]resend['"]|fetch\(\s*['"`]\/api\/email/, xo = /from\s+['"]firebase\/storage['"]|\buploadBytes(?:Resumable)?\s*\(|\bgetDownloadURL\s*\(|\bgetStorage\s*\(|\bref\s*\(\s*storage\b/, So = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} upload code written by hand, and the file it writes is guarded by a different rule than the record that links to it — delete it and take the file with <UploadBox value={photo} onChange={setPhoto} label="Product photo" /> imported from 'your-ai-workflow-firebase-os' (add \`shared\` when the picture is also shown on a page this one's people are not the audience for; a file attached while filling in a form is the form's own \`file\` question instead).`, Co = /\b(?:window|globalThis|self)\s*\.\s*(?:confirm|alert|prompt)\b|(?<![\w$.]|function\s)(?:confirm|alert|prompt)\((?![^()]*\)\s*\{)/, wo = (e, t) => `${e}: ${P(t.map((e) => e.replace(/\($/, "()")))} ${t.length > 1 ? "open" : "opens"} the browser's own pop-up, drawn by the browser and not the app, over the owner's themed page — delete ${t.length > 1 ? "every one" : "it"}. A delete asks first in <ConfirmModal isOpen={!!toDelete} title="Delete this?" message="It cannot be undone." confirmText="Delete" onConfirm={removeIt} onCancel={() => setToDelete(null)} /> imported from 'your-ai-workflow-firebase-os' (on a from-scratch page actions.confirmDelete(id) opens it for you, and a drive's deleteFile(file) already asks); anything to tell somebody is words you styled on the page.`, To = /<input[^>]*type\s*=\s*\{?['"]file['"]\}?/i, Eo = (e) => `${e}: an <input type="file"> uploads nothing on its own — draw the app's own box instead, <UploadBox value={photo} onChange={setPhoto} label="Product photo" /> imported from 'your-ai-workflow-firebase-os', which uploads as the file is picked or dropped and hands back the link to save on the record (on a form it is the form's own \`file\` question, and on a drive page it is openUploadModal()).`, Do = (e, t) => `${e}: ${P(t)} ${t.length > 1 ? "are" : "is"} hand-rolled email code — delete it and send with emailOwner('new_submission', {…}) or emailOwner('heads_up', { title, summary }) to reach the owner, emailPerson('submission_received', collection, docId) to reach the person a record names, or — on a page only the owner's team can open — emailAnyone(to, subject, message), all imported from 'your-ai-workflow-firebase-os'. A public page never names a recipient or writes an email body.`, Oo = [
1541
1623
  "--bg-color",
1542
1624
  "--bg-secondary-color",
1543
1625
  "--fg-color",
@@ -1555,10 +1637,10 @@ var to = (e, t) => `${e}: ${P(t.map((e) => `"${e.label}"`))} ${t.length > 1 ? "n
1555
1637
  "--notification-icon-color",
1556
1638
  "--selection-bg",
1557
1639
  "--selection-text"
1558
- ], yo = (e, t) => {
1640
+ ], ko = (e, t) => {
1559
1641
  let n = [], r = t.replace(/\/\*[\s\S]*?\*\//g, "");
1560
1642
  for (let [, t, i] of r.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
1561
- let r = t.trim(), a = vo.filter((e) => RegExp(`${e}\\s*:[^;}]*!important`).test(i));
1643
+ let r = t.trim(), a = Oo.filter((e) => RegExp(`${e}\\s*:[^;}]*!important`).test(i));
1562
1644
  if (a.length) {
1563
1645
  n.push(`${e}: \`${a[0]}\` is set with \`!important\` here, and the app writes that colour itself from the owner's theme — forcing it freezes it, so their Theme screen stops working. Delete the \`!important\` and set the colour in configs/theme.config.ts instead, in background, backgroundSecondary, foreground, accent or buttonText; every other colour is derived from those five.`);
1564
1646
  continue;
@@ -1578,11 +1660,11 @@ var to = (e, t) => `${e}: ${P(t.map((e) => `"${e.label}"`))} ${t.length > 1 ? "n
1578
1660
  }
1579
1661
  return n;
1580
1662
  };
1581
- function bo(e, t) {
1663
+ function Ao(e, t) {
1582
1664
  return _r(e.map((e) => ({
1583
1665
  file: e.file,
1584
1666
  source: t(e.abs)
1585
1667
  })));
1586
1668
  }
1587
1669
  //#endregion
1588
- export { l as C, b as S, E as T, _, j as a, p as b, a as c, s as d, h as f, d as g, C as h, bo as i, o as l, m, yo as n, r as o, y as p, _r as r, n as s, z as t, i as u, v, D as w, u as x, w as y };
1670
+ export { l as C, b as S, E as T, _, j as a, p as b, a as c, s as d, h as f, d as g, C as h, Ao as i, o as l, m, ko as n, r as o, y as p, _r as r, n as s, z as t, i as u, v, D as w, u as x, w as y };