@mistriwork/mistri 1.0.0-rc.12 → 1.0.0-rc.13

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 (221) hide show
  1. package/README.md +4 -3
  2. package/dist/client/assets/LayoutBuilder-MPzkVU2e.js.map +1 -1
  3. package/dist/client/assets/index-D2dHI6hR.js.map +1 -1
  4. package/dist/client/assets/shell-TreN_uLs.js.map +1 -1
  5. package/dist/docs/access-model.md +1 -1
  6. package/dist/docs/build-and-deploy.md +0 -1
  7. package/dist/docs/cli.md +1 -1
  8. package/dist/docs/glossary.md +3 -3
  9. package/dist/docs/query.md +9 -21
  10. package/dist/docs/status.md +3 -3
  11. package/dist/embed/react/chunks/LayoutBuilder-Cbxa_wve.js.map +1 -1
  12. package/dist/embed/react/chunks/app-0UZ1Re1k.js.map +1 -1
  13. package/dist/embed/react/chunks/page-Bx4o1kpQ.js.map +1 -1
  14. package/dist/embed/react/chunks/palette-Bb7Lf6eo.js.map +1 -1
  15. package/dist/embed/react/chunks/shell-DZRACiB9.js.map +1 -1
  16. package/dist/embed/react/chunks/write-BQO4CFc9.js.map +1 -1
  17. package/dist/embed/react/index.js +1 -1
  18. package/dist/server/build/dockerfile.js +1 -1
  19. package/dist/server/cli/usage.js +2 -2
  20. package/dist/ui/index.js +2 -2
  21. package/dist/ui/page.js +2 -2
  22. package/dist/ui/{shared-N4Ej8yRD.js → shared-1PD7TOTJ.js} +5 -5
  23. package/dist/ui/{shared-N4Ej8yRD.js.map → shared-1PD7TOTJ.js.map} +1 -1
  24. package/dist/ui/{shared-D5m86-4o.js → shared-1gMnOu9J.js} +6 -6
  25. package/dist/ui/{shared-D5m86-4o.js.map → shared-1gMnOu9J.js.map} +1 -1
  26. package/dist/ui/{shared-Ctaj8mR2.js → shared-5RQMi4BR.js} +3 -3
  27. package/dist/ui/{shared-Ctaj8mR2.js.map → shared-5RQMi4BR.js.map} +1 -1
  28. package/dist/ui/{shared-DpLeIATZ.js → shared-B-BUF9fx.js} +2 -2
  29. package/dist/ui/{shared-DpLeIATZ.js.map → shared-B-BUF9fx.js.map} +1 -1
  30. package/dist/ui/{shared-DWYnjItP.js → shared-B-MhiPIN.js} +9 -9
  31. package/dist/ui/{shared-DWYnjItP.js.map → shared-B-MhiPIN.js.map} +1 -1
  32. package/dist/ui/{shared-0fNuhSbG.js → shared-B07qIMlE.js} +5 -5
  33. package/dist/ui/{shared-0fNuhSbG.js.map → shared-B07qIMlE.js.map} +1 -1
  34. package/dist/ui/{shared-lYtMadRU.js → shared-B1SAS_J-.js} +3 -3
  35. package/dist/ui/{shared-lYtMadRU.js.map → shared-B1SAS_J-.js.map} +1 -1
  36. package/dist/ui/{shared-CpI0Y8M5.js → shared-B4jgU8h9.js} +6 -6
  37. package/dist/ui/{shared-CpI0Y8M5.js.map → shared-B4jgU8h9.js.map} +1 -1
  38. package/dist/ui/{shared-Cxu1c35l.js → shared-B5aBCUi7.js} +30 -30
  39. package/dist/ui/{shared-Cxu1c35l.js.map → shared-B5aBCUi7.js.map} +1 -1
  40. package/dist/ui/{shared-CAe-US1q.js → shared-B9H7_Pji.js} +5 -5
  41. package/dist/ui/{shared-CAe-US1q.js.map → shared-B9H7_Pji.js.map} +1 -1
  42. package/dist/ui/{shared-Q2bgBaYo.js → shared-BAXr20S-.js} +4 -4
  43. package/dist/ui/{shared-Q2bgBaYo.js.map → shared-BAXr20S-.js.map} +1 -1
  44. package/dist/ui/{shared-CyEefFFb.js → shared-BDDUzH-f.js} +19 -19
  45. package/dist/ui/{shared-CyEefFFb.js.map → shared-BDDUzH-f.js.map} +1 -1
  46. package/dist/ui/{shared-BdhPI6dq.js → shared-BGmkeDeW.js} +3 -3
  47. package/dist/ui/{shared-BdhPI6dq.js.map → shared-BGmkeDeW.js.map} +1 -1
  48. package/dist/ui/{shared-DONIpq2q.js → shared-BHCQ60yN.js} +2 -2
  49. package/dist/ui/{shared-DONIpq2q.js.map → shared-BHCQ60yN.js.map} +1 -1
  50. package/dist/ui/{shared-CNmaAv64.js → shared-BHsjawAG.js} +8 -8
  51. package/dist/ui/{shared-CNmaAv64.js.map → shared-BHsjawAG.js.map} +1 -1
  52. package/dist/ui/{shared-DgjU9kOy.js → shared-BIQuK_nq.js} +4 -4
  53. package/dist/ui/{shared-DgjU9kOy.js.map → shared-BIQuK_nq.js.map} +1 -1
  54. package/dist/ui/{shared-icSQAj21.js → shared-BOe4W2FI.js} +3 -3
  55. package/dist/ui/{shared-icSQAj21.js.map → shared-BOe4W2FI.js.map} +1 -1
  56. package/dist/ui/{shared-BlxoOJSk.js → shared-BPpC08ip.js} +3 -3
  57. package/dist/ui/{shared-BlxoOJSk.js.map → shared-BPpC08ip.js.map} +1 -1
  58. package/dist/ui/{shared-BtiFL37J.js → shared-BdA3LPCB.js} +2 -2
  59. package/dist/ui/{shared-BtiFL37J.js.map → shared-BdA3LPCB.js.map} +1 -1
  60. package/dist/ui/{shared-CvR94vn4.js → shared-Be3nbWsd.js} +11 -11
  61. package/dist/ui/{shared-CvR94vn4.js.map → shared-Be3nbWsd.js.map} +1 -1
  62. package/dist/ui/{shared-Bl_nF_IH.js → shared-Bf30ZLoS.js} +8 -4
  63. package/dist/ui/shared-Bf30ZLoS.js.map +1 -0
  64. package/dist/ui/{shared-DijrJ_V-.js → shared-BguP_BaR.js} +7 -7
  65. package/dist/ui/{shared-DijrJ_V-.js.map → shared-BguP_BaR.js.map} +1 -1
  66. package/dist/ui/{shared-DPAVoWY0.js → shared-BoLbc2WD.js} +17 -2
  67. package/dist/ui/{shared-DPAVoWY0.js.map → shared-BoLbc2WD.js.map} +1 -1
  68. package/dist/ui/{shared-BkdBTYwx.js → shared-BttyOW4W.js} +3 -3
  69. package/dist/ui/{shared-BkdBTYwx.js.map → shared-BttyOW4W.js.map} +1 -1
  70. package/dist/ui/{shared-CQWLz8uT.js → shared-BumxsBlA.js} +10 -10
  71. package/dist/ui/{shared-CQWLz8uT.js.map → shared-BumxsBlA.js.map} +1 -1
  72. package/dist/ui/{shared-Dzu7tFiC.js → shared-Bw73ktBk.js} +3 -3
  73. package/dist/ui/{shared-Dzu7tFiC.js.map → shared-Bw73ktBk.js.map} +1 -1
  74. package/dist/ui/{shared-BhAYErSx.js → shared-C1B0GH0e.js} +4 -4
  75. package/dist/ui/{shared-BhAYErSx.js.map → shared-C1B0GH0e.js.map} +1 -1
  76. package/dist/ui/{shared-BQJQcGgj.js → shared-C2FsyYr7.js} +4 -4
  77. package/dist/ui/{shared-BQJQcGgj.js.map → shared-C2FsyYr7.js.map} +1 -1
  78. package/dist/ui/{shared-B9lw2Ryh.js → shared-C2RU1qjv.js} +4 -4
  79. package/dist/ui/{shared-B9lw2Ryh.js.map → shared-C2RU1qjv.js.map} +1 -1
  80. package/dist/ui/{shared-DQYNI9wO.js → shared-C42f07hJ.js} +5 -5
  81. package/dist/ui/{shared-DQYNI9wO.js.map → shared-C42f07hJ.js.map} +1 -1
  82. package/dist/ui/{shared-CqCZ8wMY.js → shared-C4LkPCU4.js} +3 -3
  83. package/dist/ui/{shared-CqCZ8wMY.js.map → shared-C4LkPCU4.js.map} +1 -1
  84. package/dist/ui/{shared-BhtpgOpa.js → shared-C4wSfz6k.js} +9 -9
  85. package/dist/ui/{shared-BhtpgOpa.js.map → shared-C4wSfz6k.js.map} +1 -1
  86. package/dist/ui/{shared-BHc2Izkj.js → shared-C9DZtSFH.js} +2 -2
  87. package/dist/ui/{shared-BHc2Izkj.js.map → shared-C9DZtSFH.js.map} +1 -1
  88. package/dist/ui/{shared-DW1YuiNS.js → shared-CA52ClGo.js} +2 -2
  89. package/dist/ui/{shared-DW1YuiNS.js.map → shared-CA52ClGo.js.map} +1 -1
  90. package/dist/ui/{shared-BbSbilkh.js → shared-CBciTwm6.js} +9 -9
  91. package/dist/ui/{shared-BbSbilkh.js.map → shared-CBciTwm6.js.map} +1 -1
  92. package/dist/ui/{shared-C-J32eBn.js → shared-CL3RLKDA.js} +16 -16
  93. package/dist/ui/{shared-C-J32eBn.js.map → shared-CL3RLKDA.js.map} +1 -1
  94. package/dist/ui/{shared-Ck8S4OxW.js → shared-CLq3-_hW.js} +3 -3
  95. package/dist/ui/{shared-Ck8S4OxW.js.map → shared-CLq3-_hW.js.map} +1 -1
  96. package/dist/ui/{shared-Q5WefT7G.js → shared-CMjK-Ucd.js} +3 -3
  97. package/dist/ui/{shared-Q5WefT7G.js.map → shared-CMjK-Ucd.js.map} +1 -1
  98. package/dist/ui/{shared-D3MAuEA3.js → shared-CPjpSFGh.js} +2 -2
  99. package/dist/ui/{shared-D3MAuEA3.js.map → shared-CPjpSFGh.js.map} +1 -1
  100. package/dist/ui/{shared-VshXGwXP.js → shared-CSPhENq3.js} +2 -2
  101. package/dist/ui/{shared-VshXGwXP.js.map → shared-CSPhENq3.js.map} +1 -1
  102. package/dist/ui/{shared-DJrJaE73.js → shared-CZJE4Bmp.js} +3 -3
  103. package/dist/ui/{shared-DJrJaE73.js.map → shared-CZJE4Bmp.js.map} +1 -1
  104. package/dist/ui/{shared-D7ONOwI7.js → shared-C_BhUL2I.js} +7 -7
  105. package/dist/ui/{shared-D7ONOwI7.js.map → shared-C_BhUL2I.js.map} +1 -1
  106. package/dist/ui/{shared-j-2peBIB.js → shared-CbeujwdU.js} +4 -4
  107. package/dist/ui/{shared-j-2peBIB.js.map → shared-CbeujwdU.js.map} +1 -1
  108. package/dist/ui/{shared-D5GrOEa2.js → shared-Cbf-o4Dt.js} +4 -4
  109. package/dist/ui/{shared-D5GrOEa2.js.map → shared-Cbf-o4Dt.js.map} +1 -1
  110. package/dist/ui/{shared-HwiPIajr.js → shared-CdNmeNHh.js} +3 -3
  111. package/dist/ui/{shared-HwiPIajr.js.map → shared-CdNmeNHh.js.map} +1 -1
  112. package/dist/ui/{shared-ZCFt-CPb.js → shared-CdQOMD8J.js} +3 -3
  113. package/dist/ui/{shared-ZCFt-CPb.js.map → shared-CdQOMD8J.js.map} +1 -1
  114. package/dist/ui/{shared-ClhXSGGK.js → shared-CdiPYlTh.js} +2 -2
  115. package/dist/ui/{shared-ClhXSGGK.js.map → shared-CdiPYlTh.js.map} +1 -1
  116. package/dist/ui/{shared-DA8_pVRZ.js → shared-CepLAhwm.js} +2 -2
  117. package/dist/ui/{shared-DA8_pVRZ.js.map → shared-CepLAhwm.js.map} +1 -1
  118. package/dist/ui/{shared-BrYKjrnM.js → shared-Ch82h_Fu.js} +6 -6
  119. package/dist/ui/{shared-BrYKjrnM.js.map → shared-Ch82h_Fu.js.map} +1 -1
  120. package/dist/ui/{shared-C4UrE8Fb.js → shared-Chp88all.js} +3 -3
  121. package/dist/ui/{shared-C4UrE8Fb.js.map → shared-Chp88all.js.map} +1 -1
  122. package/dist/ui/{shared-CWa80DTO.js → shared-Ci16s2rN.js} +9 -9
  123. package/dist/ui/{shared-CWa80DTO.js.map → shared-Ci16s2rN.js.map} +1 -1
  124. package/dist/ui/{shared-tksTLD6Q.js → shared-CiqNkoE1.js} +5 -5
  125. package/dist/ui/{shared-tksTLD6Q.js.map → shared-CiqNkoE1.js.map} +1 -1
  126. package/dist/ui/{shared-yt7DjSrq.js → shared-CpMhOq8F.js} +2 -2
  127. package/dist/ui/{shared-yt7DjSrq.js.map → shared-CpMhOq8F.js.map} +1 -1
  128. package/dist/ui/{shared-Dm0z5WUl.js → shared-CrWv5JKH.js} +6 -6
  129. package/dist/ui/{shared-Dm0z5WUl.js.map → shared-CrWv5JKH.js.map} +1 -1
  130. package/dist/ui/{shared-CQL3FGHc.js → shared-Csfi3Pti.js} +2 -2
  131. package/dist/ui/{shared-CQL3FGHc.js.map → shared-Csfi3Pti.js.map} +1 -1
  132. package/dist/ui/{shared-BKNRbm-V.js → shared-CuZt26rm.js} +2 -2
  133. package/dist/ui/{shared-BKNRbm-V.js.map → shared-CuZt26rm.js.map} +1 -1
  134. package/dist/ui/{shared-Guq3upiR.js → shared-Cws_z-TS.js} +2 -2
  135. package/dist/ui/{shared-Guq3upiR.js.map → shared-Cws_z-TS.js.map} +1 -1
  136. package/dist/ui/{shared-Bw_qZeIj.js → shared-D1KWuCg6.js} +2 -2
  137. package/dist/ui/{shared-Bw_qZeIj.js.map → shared-D1KWuCg6.js.map} +1 -1
  138. package/dist/ui/{shared-CuCZKwXW.js → shared-D66ju_Cb.js} +23 -23
  139. package/dist/ui/{shared-CuCZKwXW.js.map → shared-D66ju_Cb.js.map} +1 -1
  140. package/dist/ui/{shared-DYfX7NSM.js → shared-DGaQZXnw.js} +2 -2
  141. package/dist/ui/{shared-DYfX7NSM.js.map → shared-DGaQZXnw.js.map} +1 -1
  142. package/dist/ui/{shared-B3Hp8tOv.js → shared-DHy41sET.js} +6 -6
  143. package/dist/ui/{shared-B3Hp8tOv.js.map → shared-DHy41sET.js.map} +1 -1
  144. package/dist/ui/{shared-CXyd7IKi.js → shared-DKEOMQ6l.js} +2 -2
  145. package/dist/ui/{shared-CXyd7IKi.js.map → shared-DKEOMQ6l.js.map} +1 -1
  146. package/dist/ui/{shared-Ccmc8YO0.js → shared-DSUICuaJ.js} +33 -33
  147. package/dist/ui/{shared-Ccmc8YO0.js.map → shared-DSUICuaJ.js.map} +1 -1
  148. package/dist/ui/{shared-CL2iw_js.js → shared-DUPMRoO0.js} +13 -13
  149. package/dist/ui/{shared-CL2iw_js.js.map → shared-DUPMRoO0.js.map} +1 -1
  150. package/dist/ui/{shared-Dtp4eRul.js → shared-DVfdm6ah.js} +2 -2
  151. package/dist/ui/{shared-Dtp4eRul.js.map → shared-DVfdm6ah.js.map} +1 -1
  152. package/dist/ui/{shared-BJ5Ex32G.js → shared-DXM7d7lJ.js} +3 -3
  153. package/dist/ui/{shared-BJ5Ex32G.js.map → shared-DXM7d7lJ.js.map} +1 -1
  154. package/dist/ui/{shared-BFr6ld0e.js → shared-DXbMlcev.js} +2 -2
  155. package/dist/ui/{shared-BFr6ld0e.js.map → shared-DXbMlcev.js.map} +1 -1
  156. package/dist/ui/{shared-BUp3jv-N.js → shared-D_SkPtJ9.js} +6 -6
  157. package/dist/ui/{shared-BUp3jv-N.js.map → shared-D_SkPtJ9.js.map} +1 -1
  158. package/dist/ui/{shared-CdNomVzH.js → shared-Dd21k2ic.js} +4 -4
  159. package/dist/ui/{shared-CdNomVzH.js.map → shared-Dd21k2ic.js.map} +1 -1
  160. package/dist/ui/{shared-gRMbxmpu.js → shared-Dh0nHtLZ.js} +8 -8
  161. package/dist/ui/{shared-gRMbxmpu.js.map → shared-Dh0nHtLZ.js.map} +1 -1
  162. package/dist/ui/{shared-BKGJgjog.js → shared-DkOdSpSK.js} +3 -3
  163. package/dist/ui/{shared-BKGJgjog.js.map → shared-DkOdSpSK.js.map} +1 -1
  164. package/dist/ui/{shared-BMsh72Uq.js → shared-Dkph5ckT.js} +2 -2
  165. package/dist/ui/{shared-BMsh72Uq.js.map → shared-Dkph5ckT.js.map} +1 -1
  166. package/dist/ui/{shared-Be1HpDOB.js → shared-Dl1_Nx--.js} +3 -3
  167. package/dist/ui/{shared-Be1HpDOB.js.map → shared-Dl1_Nx--.js.map} +1 -1
  168. package/dist/ui/{shared-BQIZWOoJ.js → shared-Dm0IGbAf.js} +2 -2
  169. package/dist/ui/{shared-BQIZWOoJ.js.map → shared-Dm0IGbAf.js.map} +1 -1
  170. package/dist/ui/{shared-DSUtcU5S.js → shared-DnIkTpPr.js} +5 -5
  171. package/dist/ui/{shared-DSUtcU5S.js.map → shared-DnIkTpPr.js.map} +1 -1
  172. package/dist/ui/{shared-DsxGbYlV.js → shared-Docl_0tq.js} +3 -3
  173. package/dist/ui/{shared-DsxGbYlV.js.map → shared-Docl_0tq.js.map} +1 -1
  174. package/dist/ui/{shared-DZskrV26.js → shared-DprrHC72.js} +2 -2
  175. package/dist/ui/{shared-DZskrV26.js.map → shared-DprrHC72.js.map} +1 -1
  176. package/dist/ui/{shared-Bmw43Hwf.js → shared-DyKsJTZs.js} +2 -2
  177. package/dist/ui/{shared-Bmw43Hwf.js.map → shared-DyKsJTZs.js.map} +1 -1
  178. package/dist/ui/{shared-BLG88Pya.js → shared-FO7vfI3F.js} +4 -4
  179. package/dist/ui/{shared-BLG88Pya.js.map → shared-FO7vfI3F.js.map} +1 -1
  180. package/dist/ui/{shared-DLSd80D1.js → shared-HalAVHjv.js} +3 -3
  181. package/dist/ui/{shared-DLSd80D1.js.map → shared-HalAVHjv.js.map} +1 -1
  182. package/dist/ui/{shared-D1mMBLdj.js → shared-JvcI-LWp.js} +15 -15
  183. package/dist/ui/{shared-D1mMBLdj.js.map → shared-JvcI-LWp.js.map} +1 -1
  184. package/dist/ui/{shared-C6Hv_4O0.js → shared-K7Ym27yg.js} +2 -2
  185. package/dist/ui/{shared-C6Hv_4O0.js.map → shared-K7Ym27yg.js.map} +1 -1
  186. package/dist/ui/{shared-B39ln7oi.js → shared-SzlnwfJO.js} +3 -3
  187. package/dist/ui/{shared-B39ln7oi.js.map → shared-SzlnwfJO.js.map} +1 -1
  188. package/dist/ui/{shared-DgPn_Jll.js → shared-TrGbd1FY.js} +3 -3
  189. package/dist/ui/{shared-DgPn_Jll.js.map → shared-TrGbd1FY.js.map} +1 -1
  190. package/dist/ui/{shared-DrFb-XtA.js → shared-WhbuObEW.js} +2 -2
  191. package/dist/ui/{shared-DrFb-XtA.js.map → shared-WhbuObEW.js.map} +1 -1
  192. package/dist/ui/{shared-DiFQljcq.js → shared-XcSKDp6N.js} +11 -11
  193. package/dist/ui/{shared-DiFQljcq.js.map → shared-XcSKDp6N.js.map} +1 -1
  194. package/dist/ui/{shared-CMAg58rY.js → shared-Z4uBJU6I.js} +7 -6
  195. package/dist/ui/shared-Z4uBJU6I.js.map +1 -0
  196. package/dist/ui/{shared-BnUptuUp.js → shared-bAgeiRa5.js} +2 -2
  197. package/dist/ui/{shared-BnUptuUp.js.map → shared-bAgeiRa5.js.map} +1 -1
  198. package/dist/ui/{shared-BxMvSjey.js → shared-cgXG7OgZ.js} +5 -5
  199. package/dist/ui/{shared-BxMvSjey.js.map → shared-cgXG7OgZ.js.map} +1 -1
  200. package/dist/ui/{shared-u4CG2kQJ.js → shared-eZ0SB_zm.js} +2 -2
  201. package/dist/ui/{shared-u4CG2kQJ.js.map → shared-eZ0SB_zm.js.map} +1 -1
  202. package/dist/ui/{shared-L90bWW8e.js → shared-frKFb79C.js} +2 -2
  203. package/dist/ui/{shared-L90bWW8e.js.map → shared-frKFb79C.js.map} +1 -1
  204. package/dist/ui/{shared-Dd34nhkw.js → shared-hAayNSRA.js} +3 -3
  205. package/dist/ui/{shared-Dd34nhkw.js.map → shared-hAayNSRA.js.map} +1 -1
  206. package/dist/ui/{shared-DzC64cpp.js → shared-j_J16p2B.js} +2 -2
  207. package/dist/ui/{shared-DzC64cpp.js.map → shared-j_J16p2B.js.map} +1 -1
  208. package/dist/ui/{shared-DX7HRIV-.js → shared-tqpl5N3g.js} +6 -6
  209. package/dist/ui/{shared-DX7HRIV-.js.map → shared-tqpl5N3g.js.map} +1 -1
  210. package/dist/ui/{shared-BS5dBMpA.js → shared-y-6_ZDts.js} +2 -2
  211. package/dist/ui/{shared-BS5dBMpA.js.map → shared-y-6_ZDts.js.map} +1 -1
  212. package/dist/ui/{shared-Cv5v_MP1.js → shared-zG6e1Rhz.js} +13 -13
  213. package/dist/ui/{shared-Cv5v_MP1.js.map → shared-zG6e1Rhz.js.map} +1 -1
  214. package/dist/ui/testing.js +1 -1
  215. package/package.json +4 -4
  216. package/dist/docs/cli-browser.md +0 -232
  217. package/dist/docs/database-browser.md +0 -115
  218. package/dist/docs/keyboard.md +0 -63
  219. package/dist/docs/mcp.md +0 -86
  220. package/dist/ui/shared-Bl_nF_IH.js.map +0 -1
  221. package/dist/ui/shared-CMAg58rY.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"shared-BkdBTYwx.js","names":["SERIES","HEX"],"sources":["../../../engine/src/shared/themeColor.ts","../../../engine/src/shared/themePartsCheck.ts","../../../engine/src/shared/theme.ts","../../src/shared/themeBuild.ts","../../src/shared/themeSpec.ts","../../../engine/src/shared/chartSeparation.ts","../../src/shared/themeCheck.ts","../../src/shared/tenantBranding.ts","../../src/client/copy/dashboards/components/BrandingForm.ts","../../src/client/components/BrandingForm.tsx"],"sourcesContent":["/**\n * The colour arithmetic a theme is measured with: a bounded evaluator of the colours a palette is written in, mixing\n * and compositing as the page does, and WCAG contrast. Apart from theme.ts, which judges whole themes, because the\n * client draws with one result of it (heatTop, a matrix's strongest tint) on every first paint, and theme.ts with its\n * recipes, faces and checks is several kilobytes no first paint should carry.\n */\nimport type { ThemePalette } from '../../schema/theme.js';\n\nexport type Color = readonly [number, number, number, number];\n/** A deliberately bounded color evaluator, not a CSS parser. Fractional sRGB\n channels stay unrounded through compositing and luminance calculation. */\nexport function diagnosticColor(value: string, depth = 0): Color | undefined {\n if (depth > 8) return undefined;\n value = value.trim().toLowerCase();\n if (value === 'transparent') return [0, 0, 0, 0];\n if (value === 'white') return [1, 1, 1, 1];\n if (value === 'black') return [0, 0, 0, 1];\n if (/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value)) {\n const hex =\n value.length < 6\n ? value\n .slice(1)\n .split('')\n .map((char) => char + char)\n .join('')\n : value.slice(1);\n return [\n parseInt(hex.slice(0, 2), 16) / 255,\n parseInt(hex.slice(2, 4), 16) / 255,\n parseInt(hex.slice(4, 6), 16) / 255,\n hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1,\n ];\n }\n // The two expressions emitted by effectiveThemePalette. The inner legacy\n // color can be unknown; that remains unmeasured rather than guessed.\n const mix = /^color-mix\\(in srgb, (.+) (62|14)%, (white|transparent)\\)$/.exec(value);\n const color = mix && diagnosticColor(mix[1]!, depth + 1);\n return color ? mixColor(color, diagnosticColor(mix![3]!)!, Number(mix![2]) / 100) : undefined;\n}\n\nexport function mixColor(a: Color, b: Color, weight: number): Color {\n const alpha = a[3] * weight + b[3] * (1 - weight);\n const channel = (index: 0 | 1 | 2) => (alpha ? (a[index] * a[3] * weight + b[index] * b[3] * (1 - weight)) / alpha : 0);\n return [channel(0), channel(1), channel(2), alpha];\n}\n\nexport function over(color: Color, ground: Color): Color {\n const alpha = color[3] + ground[3] * (1 - color[3]);\n const channel = (index: 0 | 1 | 2) => (alpha ? (color[index] * color[3] + ground[index] * ground[3] * (1 - color[3])) / alpha : 0);\n return [channel(0), channel(1), channel(2), alpha];\n}\n\nexport function measuredContrast(foreground: Color, background: Color): number | undefined {\n if (background[3] !== 1) return undefined; // No known opaque ground.\n const luminance = (color: Color) =>\n color\n .slice(0, 3)\n .map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4))\n .reduce((total, value, index) => total + value * [0.2126, 0.7152, 0.0722][index]!, 0);\n const a = luminance(over(foreground, background)),\n b = luminance(background);\n return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n}\n\n/** A composited colour as the page paints it, `#rrggbb` (alpha dropped: a ground is opaque, an ink composited on one). */\nexport function colorHex(color: Color): string {\n return `#${color\n .slice(0, 3)\n .map((value) =>\n Math.round(Math.min(1, Math.max(0, value)) * 255)\n .toString(16)\n .padStart(2, '0'),\n )\n .join('')}`;\n}\n\n/** The strongest a heat tint is drawn (a matrix's heaviest cell), as a share of the accent over the panel. */\nexport const HEAT_TOP = 0.62;\n/** Below this a heat scale stops reading as one: the palette's heaviest cell is drawn here and `check` says why. */\nconst HEAT_FLOOR = 0.2;\n\n/**\n * How strong a heat tint may be drawn under a palette: the strongest share of the accent over the panel, at most\n * HEAT_TOP, on which the ink still reads at 4.5:1. A matrix's cells say their figure (and, folded, their column) in\n * the ink on that tint, so the scale stops where the words would fade: 62% under Lumen's blue, about half under\n * Mono's black or a dark mode's light accent, where the ink sits near the accent. Unmeasurable colors keep HEAT_TOP.\n */\nexport function heatTop(palette: Pick<ThemePalette, 'accent' | 'surface' | 'ink'>): number {\n const accent = palette.accent && diagnosticColor(palette.accent),\n surface = palette.surface && diagnosticColor(palette.surface),\n ink = palette.ink && diagnosticColor(palette.ink);\n if (!accent || !surface || !ink) return HEAT_TOP;\n for (let step = Math.round(HEAT_TOP * 100); step > HEAT_FLOOR * 100; step--) {\n const ratio = measuredContrast(ink, over(mixColor(accent, [0, 0, 0, 0], step / 100), surface));\n if (ratio === undefined || ratio >= 4.5) return step / 100;\n }\n return HEAT_FLOOR;\n}\n\n/** What a command form's field edge at rest is drawn to: 3:1 with room to spare, short of a line that shouts. */\nconst FIELD_EDGE = 3.5;\n/** Past this a control's edge is a line drawn to be seen (Mono's ink), not a field's quiet boundary. */\nconst FIELD_EDGE_CEILING = 4.5;\n\n/**\n * A command form's field edge at rest (formDress.css, `--field-edge`): a clean outline at 3:1 on what the field\n * stands on (WCAG 1.4.11). Where the look's control edge (`lineControl`) already reads at up to 4.5:1 on its surface\n * and its page, it is that edge, so a form's field and a record's inline edit wear one line (Lumen, Sage). A stronger\n * one — Mono's ink, a theme's near-black — is mixed toward the surface to the lightest that still clears FIELD_EDGE on\n * both: a box of solid ink round every answer read as a form shouting. Undefined keeps the look's own edge (the sheet\n * falls back to `--line-control`), and so does a color this cannot measure.\n */\nexport function fieldEdge(palette: Pick<ThemePalette, 'lineControl' | 'surface' | 'ground'>): string | undefined {\n const edge = palette.lineControl && diagnosticColor(palette.lineControl),\n surface = palette.surface && diagnosticColor(palette.surface),\n ground = palette.ground ? diagnosticColor(palette.ground) : surface;\n if (!edge || !surface || !ground) return undefined;\n const least = (color: Color) => Math.min(measuredContrast(color, surface) ?? 0, measuredContrast(color, ground) ?? 0);\n if (least(edge) <= FIELD_EDGE_CEILING) return undefined;\n for (let step = 1; step < 100; step++) {\n const mixed = mixColor(edge, surface, step / 100);\n if (least(mixed) >= FIELD_EDGE) return colorHex(mixed);\n }\n return undefined;\n}\n","/**\n * What a theme may say beyond the recipe menu, checked: its patterns (`surface`), its parts' styles (`parts`) and\n * its icons' weight (`type.icons`), for the spec (themeSpec.ts) and for a compiled theme read back (theme.ts\n * parseThemes) alike. A module apart from themeParts.ts, which the first paint wears and these messages are no\n * weight of.\n */\nimport type { ThemeIconWeight, ThemePart, ThemePartStyle, ThemePattern, ThemeSurface } from '../../schema/theme.js';\nimport { PALETTE_TOKENS, rolePath } from './themeTokens.js';\nimport { PART_CASES, PART_FACES, PART_LIFTS, PART_PROPERTIES, PART_RADII, PART_WEIGHTS, THEME_PARTS } from './themeParts.js';\n\n/** The patterns, drawn by surfacesLook.css; each at a strength of 1 (omitted), 2 or 3. */\nexport const PATTERN_KINDS = ['paper', 'grain', 'grid', 'dots', 'lines', 'scanlines'] as const;\n/** How much of the line color each strength draws a pattern in (surfacesLook.css says the same three). */\nconst PATTERN_STRENGTH = { 1: 0.4, 2: 0.7, 3: 1 } as const;\n/* Paper is two families of fibers crossing, twice the ink of one family of lines at a strength: at 40% its low end\n read as a mesh over the page, not a sheet of paper. Its own steps are half again lighter. */\nconst PAPER_STRENGTH = { 1: 0.2, 2: 0.45, 3: 0.7 } as const;\n/** How much of the line color a pattern of this kind and strength is drawn in. */\nexport const patternStrength = (pattern: ThemePattern): number =>\n (pattern.kind === 'paper' ? PAPER_STRENGTH : PATTERN_STRENGTH)[pattern.strength ?? 1];\n/** Phosphor's weights a theme may pick for the icons: thin fades at 16px, duotone says a second tone no role names. */\nexport const ICON_WEIGHTS: readonly ThemeIconWeight[] = ['regular', 'light', 'bold', 'fill'];\n\n/** The color roles a part may name, by their paths: every color but the widths and the shadow's ink. */\nconst PART_COLOR_PATHS: readonly string[] = Object.keys(PALETTE_TOKENS)\n .filter((role) => role !== 'shadow' && role !== 'rulesWeight' && role !== 'rulesControl')\n .map(rolePath);\n/** A role's path back to the role it names: `nav.onCurrent` is `navOnCurrent`, `chart[2]` is `chart3`. */\nexport function pathRole(path: string): string {\n const series = /^chart\\[(\\d)\\]$/.exec(path);\n if (series) return `chart${Number(series[1]) + 1}`;\n return path.replace(/\\.(\\w)/, (_, letter: string) => letter.toUpperCase());\n}\n\nfunction record(raw: unknown, where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${where} must be an object.`);\n return raw as Record<string, unknown>;\n}\n\n/** `surface`: a pattern behind the page, one behind the menu, each of the six at a strength; omitted strength is 1. */\nexport function surfaceOf(raw: unknown, where: string): ThemeSurface {\n const surface = record(raw, where);\n const out: ThemeSurface = {};\n for (const [place, value] of Object.entries(surface)) {\n if (place !== 'ground' && place !== 'nav') throw new Error(`${where} has no \"${place}\". It takes: ground, nav.`);\n const pattern = record(value, `${where}.${place}`);\n for (const key of Object.keys(pattern))\n if (key !== 'kind' && key !== 'strength')\n throw new Error(\n `${where}.${place} has no \"${key}\". It takes: kind, strength. A pattern is one of the platform's, drawn in the theme's line color.`,\n );\n if (!(PATTERN_KINDS as readonly unknown[]).includes(pattern.kind))\n throw new Error(`${where}.${place}.kind must be one of ${PATTERN_KINDS.join(', ')}.`);\n if (pattern.strength !== undefined && pattern.strength !== 1 && pattern.strength !== 2 && pattern.strength !== 3)\n throw new Error(`${where}.${place}.strength must be 1, 2 or 3.`);\n out[place] = { kind: pattern.kind as ThemePattern['kind'], strength: (pattern.strength ?? 1) as 1 | 2 | 3 };\n }\n return out;\n}\n\n/** `parts`: each a published part, saying only what its menus hold. */\nexport function partsOf(raw: unknown, where: string): Partial<Record<ThemePart, ThemePartStyle>> {\n const parts = record(raw, where);\n const out: Partial<Record<ThemePart, ThemePartStyle>> = {};\n for (const [part, value] of Object.entries(parts)) {\n if (!(THEME_PARTS as readonly string[]).includes(part))\n throw new Error(`${where} has no \"${part}\". The parts are: ${THEME_PARTS.join(', ')}.`);\n const at = `${where}.${part}`;\n const style = record(value, at);\n for (const [property, said] of Object.entries(style)) {\n if (!(PART_PROPERTIES as readonly string[]).includes(property))\n throw new Error(\n `${at} has no \"${property}\". It takes: ${PART_PROPERTIES.join(', ')}. A part's size, spacing, order and visibility are the App's, never its theme's.`,\n );\n const menu = (why: string, values: readonly unknown[]) => {\n if (!values.includes(said)) throw new Error(`${at}.${property} must be one of ${values.join(', ')}${why}.`);\n };\n if (property === 'ground' || property === 'ink' || property === 'edge') {\n if (said === 'nav.currentInk') throw new Error(`${at}.${property} names nav.currentInk, which is now nav.onCurrent.`);\n if (typeof said !== 'string' || !PART_COLOR_PATHS.includes(said))\n throw new Error(`${at}.${property} must be a color role by its path, like nav.ground or chart[2], never a hex.`);\n } else if (property === 'radius') menu(\": a corner the theme's shape already says, or square\", PART_RADII);\n else if (property === 'lift') menu('', PART_LIFTS);\n else if (property === 'face') menu(\": a face the theme's type already says\", PART_FACES);\n else if (property === 'weight') menu('', PART_WEIGHTS);\n else menu('', PART_CASES);\n }\n out[part as ThemePart] = { ...style } as ThemePartStyle;\n }\n return out;\n}\n\n/** `type.icons`: one of Phosphor's weights the icons are drawn in. */\nexport function iconsOf(raw: unknown, where: string): ThemeIconWeight {\n if (!(ICON_WEIGHTS as readonly unknown[]).includes(raw))\n throw new Error(\n `${where} must be one of ${ICON_WEIGHTS.join(', ')}: thin draws a 1px stroke that fades at the 16px an icon is drawn at, and duotone a second tone no color role says.`,\n );\n return raw as ThemeIconWeight;\n}\n","import type { ThemeDefinition, ThemePalette, ThemePaletteVariant, ThemeRecipes } from '../../schema/theme.js';\nimport {\n effectiveThemeDefinition,\n effectiveThemePalette,\n parseScheme,\n THEME_PRESETS,\n themeModes,\n withInks,\n type AppAccent,\n} from './themePresets.js';\nimport { colorHex, diagnosticColor, HEAT_TOP, heatTop, measuredContrast, mixColor, over, type Color } from './themeColor.js';\nimport { DENSITIES, ELEVATIONS, LIFTS, MOTIONS, RECIPE_MENU, RECIPES_RENAMED, RHYTHMS } from './themeRecipes.js';\nimport { families } from './themeFaces.js';\nimport { iconsOf, partsOf, pathRole, patternStrength, surfaceOf } from './themePartsCheck.js';\nimport { TYPE_MOVED, typeOf } from './themeType.js';\nimport {\n BORDER_MENU,\n CONTROL_HEIGHT,\n controlHeightFits,\n cornerFits,\n FONT_TOKENS,\n PALETTE_TOKENS,\n RADIUS_MAX,\n RADIUS_TOKENS,\n rolePath,\n ROUND,\n ROUND_CORNERS,\n THEME_BASES,\n} from './themeTokens.js';\nexport { FONT_TOKENS, PALETTE_TOKENS, RADIUS_TOKENS, shapeTokens, THEME_BASES } from './themeTokens.js';\nexport type { ThemeBase, ThemeDefinition, ThemePalette, ThemePaletteVariant } from '../../schema/theme.js';\n\nfunction object(raw: unknown, where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${where} must be an object.`);\n return raw as Record<string, unknown>;\n}\n\nfunction strings(raw: unknown, keys: readonly string[], valid: (value: string) => boolean, where: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (!keys.includes(key)) throw new Error(`${where} has no \"${key}\". It takes: ${keys.join(', ')}.`);\n if (typeof value !== 'string' || !valid(value.trim())) throw new Error(`${where}.${key} has an invalid value.`);\n out[key] = value.trim();\n }\n return out;\n}\n\n/** The roles' names before they were the spec's own (0.9.2), each to the one that replaced it. */\nconst OLD_ROLES: Readonly<Record<string, string>> = {\n background: 'ground',\n subtle: 'raised',\n border: 'line',\n borderStrong: 'lineStrong',\n borderControl: 'lineControl',\n text: 'ink',\n textMuted: 'inkMuted',\n textSubtle: 'inkSubtle',\n positive: 'statusGood',\n warning: 'statusWarning',\n negative: 'statusCritical',\n key: 'statusInfo',\n navSurface: 'navGround',\n navText: 'navInk',\n navBorder: 'navLine',\n navCurrentText: 'navOnCurrent',\n navCurrentInk: 'navOnCurrent',\n sectionRule: 'rulesSection',\n headRule: 'rulesHead',\n ruleWeight: 'rulesWeight',\n};\n\n/** A palette's roles: opaque six-digit hex colors, but for `rulesWeight` and `rulesControl`, widths. */\nconst RULE_WEIGHTS = ['1px', '2px', '3px'] as const;\nconst CONTROL_WEIGHTS = ['0px', '1px', '2px'] as const;\nfunction paletteStrings(raw: unknown, where: string): Record<string, string> {\n const colors = Object.keys(PALETTE_TOKENS).filter((key) => key !== 'rulesWeight' && key !== 'rulesControl');\n const { rulesWeight, rulesControl, ...rest } = object(raw, where);\n for (const key of Object.keys(rest))\n if (Object.hasOwn(OLD_ROLES, key)) throw new Error(`${where}.${key} is now ${where}.${OLD_ROLES[key]}.`);\n const result = strings(rest, colors, (color) => /^#[0-9a-f]{6}$/i.test(color), where);\n if (rulesWeight !== undefined) {\n if (typeof rulesWeight !== 'string' || !(RULE_WEIGHTS as readonly string[]).includes(rulesWeight.trim())) {\n throw new Error(`${where}.rulesWeight must be one of ${RULE_WEIGHTS.join(', ')}.`);\n }\n result.rulesWeight = rulesWeight.trim();\n }\n if (rulesControl !== undefined) {\n if (typeof rulesControl !== 'string' || !(CONTROL_WEIGHTS as readonly string[]).includes(rulesControl.trim())) {\n throw new Error(`${where}.rulesControl must be one of ${CONTROL_WEIGHTS.join(', ')}.`);\n }\n result.rulesControl = rulesControl.trim();\n }\n return result;\n}\n\nexport function parseThemes(raw: unknown, where: string, key = 'themes'): ThemeDefinition[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw) || raw.length > 20) throw new Error(`${where}.${key} must be an array of at most 20 themes.`);\n const used = new Set<string>(['default', ...THEME_BASES]);\n return raw.map((value, index): ThemeDefinition => {\n const path = `${where}.${key}[${index}]`;\n const theme = object(value, path);\n for (const key of Object.keys(theme)) {\n if (key === 'scale' || key === 'headingWeight') throw new Error(`${path}.${key} is now ${path}.${TYPE_MOVED[`type.${key}`]}.`);\n if (\n ![\n 'id',\n 'label',\n 'base',\n 'light',\n 'dark',\n 'palettes',\n 'defaultPalette',\n 'scheme',\n 'fonts',\n 'radius',\n 'border',\n 'height',\n ...STRUCTURE_KEYS,\n ].includes(key)\n )\n throw new Error(`${path} has no \"${key}\".`);\n }\n if (typeof theme.id !== 'string' || !/^[a-z][a-z0-9-]{0,47}$/.test(theme.id) || used.has(theme.id)) {\n throw new Error(`${path}.id must be a unique slug, not a built-in theme name.`);\n }\n used.add(theme.id);\n if (typeof theme.label !== 'string' || !theme.label.trim() || theme.label.trim().length > 60)\n throw new Error(`${path}.label must have 1 to 60 characters.`);\n if (theme.base !== undefined && (typeof theme.base !== 'string' || !(THEME_BASES as readonly string[]).includes(theme.base)))\n throw new Error(`${path}.base must be one of ${THEME_BASES.join(', ')}.`);\n const palette = (mode: 'light' | 'dark'): ThemePalette => {\n const result = paletteStrings(theme[mode], `${path}.${mode}`);\n for (const required of ['surface', 'ink', 'accent']) {\n if (!result[required]) throw new Error(`${path}.${mode}.${required} is required (a six-digit hex color).`);\n }\n return { ...result, surface: result.surface!, ink: result.ink!, accent: result.accent! };\n };\n const scheme = parseScheme(theme.scheme, path);\n const dark = palette('dark');\n /* One set of colors worn in both slots (themeBuild.ts): a light slot of its own is the colors a dark-only theme lacks. */\n const light = scheme === 'dark-only' ? dark : palette('light');\n if (scheme === 'dark-only' && theme.light !== undefined && JSON.stringify(palette('light')) !== JSON.stringify(dark))\n throw new Error(`${path}.light: a dark-only theme has no light colors; name dark alone, or drop scheme: 'dark-only'.`);\n const palettes = parsePaletteVariants(theme.palettes, path);\n if (\n theme.defaultPalette !== undefined &&\n (typeof theme.defaultPalette !== 'string' ||\n (theme.defaultPalette !== 'default' && !palettes?.some((item) => item.id === theme.defaultPalette)))\n ) {\n throw new Error(`${path}.defaultPalette must be \"default\" or one of this theme's palettes.`);\n }\n return {\n id: theme.id,\n label: theme.label.trim(),\n base: (theme.base ?? 'mono') as ThemeDefinition['base'],\n light,\n dark,\n ...(scheme === undefined ? {} : { scheme }),\n ...(palettes === undefined ? {} : { palettes }),\n ...(theme.defaultPalette === undefined ? {} : { defaultPalette: theme.defaultPalette as string }),\n ...(theme.fonts === undefined\n ? {}\n : {\n fonts: strings(\n theme.fonts,\n Object.keys(FONT_TOKENS),\n (font) => font.length > 0 && font.length <= 200 && /^[\\w\\s'\",.-]+$/.test(font),\n `${path}.fonts`,\n ),\n }),\n ...(theme.radius === undefined ? {} : { radius: radii(theme.radius, `${path}.radius`) }),\n ...(theme.border === undefined ? {} : { border: borders(theme.border, `${path}.border`) }),\n ...(theme.height === undefined ? {} : { height: heights(theme.height, `${path}.height`) }),\n ...structure(theme, path),\n };\n });\n}\n\n/** A compiled theme's corners, each within its ceiling, a control's and a pill's `round` as ROUND. */\nfunction radii(raw: unknown, where: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (!Object.hasOwn(RADIUS_TOKENS, key))\n throw new Error(`${where} has no \"${key}\". It takes: ${Object.keys(RADIUS_TOKENS).join(', ')}.`);\n const fits = cornerFits(value, RADIUS_MAX[key as keyof typeof RADIUS_MAX]) || (ROUND_CORNERS.has(key) && value === ROUND);\n if (!fits) throw new Error(`${where}.${key} has an invalid value.`);\n out[key] = value as string;\n }\n return out;\n}\n/** A compiled theme's panel edge and focus ring, each from its menu. */\nfunction borders(raw: unknown, where: string): NonNullable<ThemeDefinition['border']> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (key !== 'panel' && key !== 'focus') throw new Error(`${where} has no \"${key}\". It takes: panel, focus.`);\n if (!(BORDER_MENU[key] as readonly unknown[]).includes(value)) throw new Error(`${where}.${key} has an invalid value.`);\n out[key] = value as string;\n }\n return out as NonNullable<ThemeDefinition['border']>;\n}\n\n/** A compiled theme's control height: whole pixels from CONTROL_HEIGHT.min to .max. */\nfunction heights(raw: unknown, where: string): NonNullable<ThemeDefinition['height']> {\n const out: { control?: string } = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (key !== 'control') throw new Error(`${where} has no \"${key}\". It takes: control.`);\n if (!controlHeightFits(value))\n throw new Error(`${where}.control must be a length from ${CONTROL_HEIGHT.min}px to ${CONTROL_HEIGHT.max}px.`);\n out.control = value as string;\n }\n return out;\n}\n\n/**\n * What a theme's shape says that `check` warns of (theme-radius): a panel squarer than the controls it holds, its\n * corner cutting under theirs. A round control is a pill, not a corner, so it is never measured against a panel.\n */\nexport function themeShapeWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n const control = theme.radius?.control,\n panel = theme.radius?.panel;\n if (!control || !panel || control === ROUND || parseFloat(panel) >= parseFloat(control)) return [];\n return [\n `ui.customThemes[${index}] (\"${theme.id}\") shape.radius.panel ${panel} is squarer than shape.radius.control ${control}: a panel holds controls, so give its corner at least theirs.`,\n ];\n });\n}\n\n/**\n * What a theme's shadow says that `check` warns of (theme-shadow, the contract's C2): a shadow lighter than the ground\n * it falls on, which draws a glow round a card or a menu rather than lifting it off the page — the edge the lift was\n * for is lost. The theme still wears.\n */\nexport function themeShadowWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n const worn = effectiveThemeDefinition(theme, 'default');\n return themeModes(theme).flatMap((mode) => {\n const shadow = worn[mode].shadow,\n ground = worn[mode].ground ?? worn[mode].surface;\n if (!shadow || !/^#[0-9a-f]{6}$/i.test(shadow) || !/^#[0-9a-f]{6}$/i.test(ground) || luminance(shadow) <= luminance(ground))\n return [];\n return [\n `ui.customThemes[${index}] (\"${theme.id}\") colors.${mode}.shadow ${shadow} is lighter than its ground ${ground}: a shadow lighter than the page is a glow, not a lift, so give it a color darker than the ground.`,\n ];\n });\n });\n}\n\n/**\n * What a banded table's parts say that `check` warns of (theme-band, the contract's C13): a `status` or `tag` part\n * given an opaque ground. Part rules live in the `theme` cascade layer, above the `look` layer's band, so the chip\n * keeps its own ground on every other row and shows as a pale column. The author's color is not touched: a\n * translucent ground lets the band through, and so does the default.\n */\nexport function themeBandWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n if (theme.recipes?.table !== 'banded') return [];\n const worn = effectiveThemeDefinition(theme, 'default');\n return (['status', 'tag'] as const).flatMap((part) => {\n const role = theme.parts?.[part]?.ground;\n if (!role) return [];\n const key = pathRole(role) as keyof ThemePalette;\n const opaque = themeModes(theme).filter((mode) => {\n const value = worn[mode][key];\n const color = value ? diagnosticColor(value) : undefined;\n return color !== undefined && color[3] >= 1;\n });\n if (!opaque.length) return [];\n return [\n `ui.customThemes[${index}] (\"${theme.id}\") parts.${part}.ground ${role} is opaque (${opaque.join(', ')}) on a banded table: a ${part} cell will not take the band and shows as a pale column on every other row, so give it a translucent ground or leave it out for the look's.`,\n ];\n });\n });\n}\n\n/** The stack a role's face reads when the role names none: the titles' the heading face, the rest the interface's. */\nconst ROLE_FACE: Record<string, 'heading' | 'ui'> = { display: 'heading', title: 'heading', heading: 'heading', section: 'heading' };\n\n/**\n * What a theme's type says that `check` warns of (theme-type, the contract's C8): a role heavier than 700 whose face\n * leads with a family the theme does not ship at that weight. design.md keeps titles at 700 or under for this: a\n * heavier weight a family has no file for is drawn by the browser as a smeared 700, or resolves a book face to its\n * Black cut, a masthead where a title was meant.\n */\nexport function themeTypeWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n const worn = effectiveThemeDefinition(theme, 'default');\n return Object.entries(worn.type?.roles ?? {}).flatMap(([role, said]) => {\n /* A line under 1.0 sets its descenders into the next: a title that wraps (a long name, a phone) collides with\n itself. Allowed down to 0.85 for a short large line, so it warns rather than refuses. */\n const leading =\n said.leading !== undefined && said.leading < 1\n ? [\n `ui.customThemes[${index}] (\"${theme.id}\") type.roles.${role}.leading ${said.leading} is under 1.0: a ${role} that wraps sets its descenders into the next line, so keep it at 1.0 or over unless it is one short line.`,\n ]\n : [];\n const weight = said.weight;\n if (weight === undefined || weight <= 700) return leading;\n const face = said.face ?? ROLE_FACE[role] ?? 'ui';\n const stack = (face === 'heading' ? (worn.fonts?.heading ?? worn.fonts?.ui) : worn.fonts?.[face]) ?? '';\n const lead = families(stack)[0];\n const shipped = (worn.faces ?? []).some((file) => {\n const [low, high = low] = file.weight.split(' ').map(Number);\n return file.family.toLowerCase() === lead?.toLowerCase() && weight >= low! && weight <= high!;\n });\n if (shipped) return leading;\n return [\n ...leading,\n `ui.customThemes[${index}] (\"${theme.id}\") type.roles.${role}.weight ${weight} is past 700 in a face the theme does not ship at that weight${lead ? ` (${lead})` : ''}: the browser draws a smeared 700 or a Black cut, so ship the face at ${weight} in type.faces or keep the weight at 700 or under.`,\n ];\n });\n });\n}\n\nconst STRUCTURE_KEYS = ['recipes', 'elevation', 'type', 'density', 'motion', 'rhythm', 'lifts', 'surface', 'parts', 'icons'] as const;\n/** A compiled theme's structure, as `compileTheme` writes it: every value from the menu. */\nfunction structure(theme: Record<string, unknown>, path: string): Partial<ThemeDefinition> {\n const out: Partial<ThemeDefinition> = {};\n if (theme.recipes !== undefined) {\n const recipes = object(theme.recipes, `${path}.recipes`);\n for (const [part, value] of Object.entries(recipes)) {\n if (TYPE_MOVED[`recipes.${part}`]) throw new Error(`${path}.recipes.${part} is now ${path}.${TYPE_MOVED[`recipes.${part}`]}.`);\n if (Object.hasOwn(RECIPES_RENAMED, part)) throw new Error(`${path}.recipes.${part} is now ${path}.recipes.${RECIPES_RENAMED[part]}.`);\n const menu = (RECIPE_MENU as Record<string, Record<string, unknown>>)[part];\n if (!menu || typeof value !== 'string' || !Object.hasOwn(menu, value)) throw new Error(`${path}.recipes.${part} is not on the menu.`);\n }\n out.recipes = { ...recipes } as ThemeDefinition['recipes'];\n }\n if (theme.elevation !== undefined) {\n if (typeof theme.elevation !== 'string' || !Object.hasOwn(ELEVATIONS, theme.elevation))\n throw new Error(`${path}.elevation is not on the menu.`);\n out.elevation = theme.elevation as ThemeDefinition['elevation'];\n }\n if (theme.type !== undefined) {\n const type = object(theme.type, `${path}.type`);\n for (const key of Object.keys(type))\n if (key !== 'base' && key !== 'roles') throw new Error(`${path}.type has no \"${key}\". It takes: base, roles.`);\n out.type = typeOf(type.base, type.roles, `${path}.type`);\n }\n if (theme.density !== undefined) {\n if (!(DENSITIES as readonly unknown[]).includes(theme.density))\n throw new Error(`${path}.density must be one of ${DENSITIES.join(', ')}.`);\n out.density = theme.density as ThemeDefinition['density'];\n }\n if (theme.lifts !== undefined) {\n const lifts = object(theme.lifts, `${path}.lifts`);\n for (const [key, step] of Object.entries(lifts))\n if ((key !== 'card' && key !== 'float') || typeof step !== 'string' || step === 'look' || !Object.hasOwn(LIFTS, step))\n throw new Error(`${path}.lifts.${key} is not on the menu.`);\n out.lifts = { ...lifts } as ThemeDefinition['lifts'];\n }\n if (theme.motion !== undefined) {\n if (typeof theme.motion !== 'string' || !Object.hasOwn(MOTIONS, theme.motion))\n throw new Error(`${path}.motion must be one of ${Object.keys(MOTIONS).join(', ')}.`);\n out.motion = theme.motion as ThemeDefinition['motion'];\n }\n if (theme.rhythm !== undefined) {\n if (typeof theme.rhythm !== 'string' || !Object.hasOwn(RHYTHMS, theme.rhythm))\n throw new Error(`${path}.rhythm must be one of ${Object.keys(RHYTHMS).join(', ')}.`);\n out.rhythm = theme.rhythm as ThemeDefinition['rhythm'];\n }\n if (theme.surface !== undefined) out.surface = surfaceOf(theme.surface, `${path}.surface`);\n if (theme.parts !== undefined) out.parts = partsOf(theme.parts, `${path}.parts`);\n if (theme.icons !== undefined) out.icons = iconsOf(theme.icons, `${path}.icons`);\n return out;\n}\n\nfunction parsePaletteVariants(raw: unknown, where: string): ThemePaletteVariant[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw) || raw.length > 20) throw new Error(`${where}.palettes must be an array of at most 20 palettes.`);\n const used = new Set(['default']);\n return raw.map((value, index) => {\n const path = `${where}.palettes[${index}]`,\n variant = object(value, path);\n for (const key of Object.keys(variant)) {\n if (!['id', 'label', 'light', 'dark'].includes(key)) throw new Error(`${path} has no \"${key}\".`);\n }\n if (typeof variant.id !== 'string' || !/^[a-z][a-z0-9-]{0,47}$/.test(variant.id) || used.has(variant.id)) {\n throw new Error(`${path}.id must be a unique slug, not \"default\".`);\n }\n used.add(variant.id);\n if (typeof variant.label !== 'string' || !variant.label.trim() || variant.label.trim().length > 60)\n throw new Error(`${path}.label must have 1 to 60 characters.`);\n const palette = (mode: 'light' | 'dark') => paletteStrings(variant[mode], `${path}.${mode}`);\n return { id: variant.id, label: variant.label.trim(), light: palette('light'), dark: palette('dark') };\n });\n}\n\n/** WCAG relative luminance, for the opaque hex colors accepted by the contract. */\nfunction luminance(hex: string): number {\n const channels = [1, 3, 5]\n .map((start) => parseInt(hex.slice(start, start + 2), 16) / 255)\n .map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4));\n return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722;\n}\n\nexport function contrastRatio(a: string, b: string): number {\n const left = luminance(a),\n right = luminance(b);\n return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);\n}\n\n/**\n * The least luminance ratio a field's fill may be from the ground and the surface it sits on, when the field draws\n * no edge. 1.08:1 is Lintel's Hearth (a warm #eeede7 well on a #f7f6f2 page, 1.085, and on white, 1.173): the faintest\n * field we ship that still reads as a well, and the faintest a calibrated laptop screen shows in daylight. Below it\n * (white on #fcfbf8, 1.03) the field is gone. WCAG 1.4.11 asks 3:1 of a control's boundary unless it is identified\n * some other way; the label stands over the field and the focus ring draws a 2px edge, which is that other way.\n */\nexport const EDGELESS_FIELD_DELTA = 1.08;\n\n/** What startup checks in each mode, and the ratio each pair must reach: text\n at 4.5:1 (WCAG 1.4.3) — the meaning colors on the page as well as on a\n panel, since a status word sits on both, and the page is where Meridian's\n red read at 4.4:1 while its panels passed — and a form control's edge at\n 3:1 (1.4.11) on the field it frames and on the page around it. Section and\n table hairlines are decoration, and are not checked. */\nconst CONTRAST_PAIRS: ReadonlyArray<readonly [keyof ThemePalette, keyof ThemePalette, number, string?]> = [\n ['ink', 'surface', 4.5],\n ['inkMuted', 'surface', 4.5],\n ['inkSubtle', 'surface', 4.5],\n ['ink', 'ground', 4.5],\n ['ink', 'raised', 4.5],\n /* A field's own fill (`colors.field`, the surface where it names none): the words typed in it, the faint ink of its\n placeholder and the muted ink of its hint stand on it, and its edge. Skipped where the fill is the surface, which\n the pairs above already measure. */\n ['ink', 'field', 4.5, 'the words in a field'],\n ['inkMuted', 'field', 4.5, \"a field's muted words\"],\n ['inkSubtle', 'field', 4.5, \"a field's placeholder\"],\n ['lineControl', 'field', 3, \"a control's edge on a field's fill\"],\n /* Every ink is drawn where the text it is for is drawn, not only on the panel it was picked on: a caption under a\n page's title and a count beside a tab stand on the page (TC13, a theme check passed and its pages failed). */\n ['inkMuted', 'ground', 4.5, 'a caption on the page'],\n ['inkSubtle', 'ground', 4.5, 'a faint caption on the page'],\n ['link', 'ground', 4.5],\n ['link', 'raised', 4.5],\n ['navAccent', 'navGround', 4.5, \"the menu's current entry\"],\n /* The accent is a fill, and is read as text in its ink (`accentInk`, the accent itself where a theme names none and\n it reads); the words drawn on a fill are `onAccent`. A bright brand accent can be a good fill and a poor text. */\n ['accentInk', 'surface', 4.5],\n // A quiet button's word and a current tab are the accent's ink on the page, and on a raised panel or a hovered row.\n ['accentInk', 'ground', 4.5, 'an action in the accent on the page'],\n ['accentInk', 'raised', 4.5, 'an action in the accent on a raised panel or a hovered row'],\n ['onAccent', 'accent', 4.5, 'the words on an accent fill'],\n ['onPrimary', 'primary', 4.5, \"the words on a form's primary action\"],\n ['link', 'surface', 4.5],\n ['navInk', 'navGround', 4.5, \"the menu's entries\"],\n ['navMuted', 'navGround', 4.5, \"the menu's group headings, counts and icons\"],\n ...(['statusGood', 'statusWarning', 'statusCritical', 'statusInfo'] as const).flatMap(\n (role) =>\n [\n [role, 'surface', 4.5],\n [role, 'ground', 4.5],\n [role, 'raised', 4.5, 'a status word in a hovered row or on a raised panel'],\n ] as const,\n ),\n ['lineControl', 'surface', 3],\n ['lineControl', 'ground', 3],\n /* A field stands on a raised panel too, and a list's sort and filter controls on its head: a control's edge that\n passed on the panel and the page read 2.94:1 there. */\n ['lineControl', 'raised', 3, \"a control's edge on a raised panel or a table's head\"],\n // The focus ring is a graphic the keyboard reader follows (1.4.11): on the field, the page and the subtle panel it can sit on.\n ['focus', 'surface', 3],\n ['focus', 'ground', 3],\n ['focus', 'raised', 3],\n /* A chart's first four series are graphics a reader has to tell from the\n card they sit on; past four, a chart is already a legend's job. */\n ...(['chart1', 'chart2', 'chart3', 'chart4'] as const).map((series) => [series, 'surface', 3] as const),\n];\n\nexport { HEAT_TOP, heatTop };\n\nconst SERIES = ['chart1', 'chart2', 'chart3', 'chart4', 'chart5', 'chart6', 'chart7', 'chart8'] as const;\n\n/**\n * The colours a platform component mixes from a theme's series and its ink rather than wears as a token: no pair of\n * tokens is what it draws, so each is measured as drawn, and named by the series it comes from. A board card's\n * category chip (TriageBoard.css: 45% of its series in the ink, except under the `strip` board, which says it in words, on a 13% tint of it over the card), a person's\n * initials on their avatar (features.css: 45% in the ink, on a 15% tint over a 7% ink wash), and under the `icon`\n * stat recipe, a stat card's mark (lumenLook.css: 80% in the ink on a 12% tint, a graphic at 3:1). `check` holds a\n * theme to each, and `grade` names the series behind a reading of one.\n */\nexport function componentInks(\n palette: ThemePalette,\n recipes: Partial<ThemeRecipes> = {},\n): Array<{ role: keyof ThemePalette; what: string; fg: string; bg: string; needs: number }> {\n const color = (key: keyof ThemePalette) => (palette[key] ? diagnosticColor(palette[key]!) : undefined);\n const ink = color('ink'),\n surface = color('surface');\n if (!ink || !surface) return [];\n const clear: Color = [0, 0, 0, 0];\n const out: Array<{ role: keyof ThemePalette; what: string; fg: Color; bg: Color; needs: number }> = [];\n SERIES.forEach((role, index) => {\n const series = color(role);\n if (!series) return;\n if (recipes.board !== 'strip')\n out.push({\n role,\n what: \"a board card's category chip\",\n fg: mixColor(series, ink, 0.45),\n bg: over(mixColor(series, clear, 0.13), surface),\n needs: 4.5,\n });\n if (index < 7)\n out.push({\n role,\n what: \"an avatar's initials\",\n fg: mixColor(series, ink, 0.45),\n bg: mixColor(series, mixColor(ink, surface, 0.07), 0.15),\n needs: 4.5,\n });\n if (recipes.stat === 'icon' && index < 4)\n out.push({\n role,\n what: \"a stat card's mark\",\n fg: mixColor(series, ink, 0.8),\n bg: mixColor(series, surface, 0.12),\n needs: 3,\n });\n });\n /* The eighth avatar is the second and third series half and half: said by the second. */\n const second = color('chart2'),\n third = color('chart3');\n if (second && third) {\n const slot = mixColor(second, third, 0.5);\n out.push({\n role: 'chart2',\n what: \"the eighth avatar's initials (chart[1] and chart[2] half and half)\",\n fg: mixColor(slot, ink, 0.45),\n bg: mixColor(slot, mixColor(ink, surface, 0.07), 0.15),\n needs: 4.5,\n });\n }\n return out.map((item) => ({ ...item, fg: colorHex(over(item.fg, item.bg)), bg: colorHex(item.bg) }));\n}\n\n/**\n * The pairs a role is read in (CONTRAST_PAIRS), measured with `value` drawn in its place: what an author wrote for a\n * colour the page wears as another (a link written as the accent is drawn in the accent's ink), so the colour as\n * written is held to the rule its role is.\n */\nexport function rolePairs(\n palette: ThemePalette,\n role: keyof ThemePalette,\n value: string,\n): Array<{ background: keyof ThemePalette; needs: number; what?: string; ratio: number }> {\n const foreground = diagnosticColor(value);\n if (!foreground) return [];\n return CONTRAST_PAIRS.flatMap(([ink, background, needs, what]) => {\n const ground = palette[background];\n if (ink !== role || !ground || ((background === 'ground' || background === 'field') && ground === palette.surface)) return [];\n const backgroundColor = diagnosticColor(ground);\n const ratio = backgroundColor && measuredContrast(foreground, backgroundColor);\n return ratio === undefined || !ratio ? [] : [{ background, needs, ...(what ? { what } : {}), ratio }];\n });\n}\n\n/** One failed check: which theme, palette and mode, the role it is about, and the sentence for it. */\nexport interface ThemeFinding {\n themeId: string;\n paletteId: string;\n mode: 'light' | 'dark';\n /** The role (`accent`, `navMuted`, `chart3`); rolePath gives the spec's name for it (`nav.muted`, `chart[2]`). */\n token: keyof ThemePalette;\n /** What the check measures, in a few words: \"accent on surface\", \"chart2 beside chart3 for deuteranopia\". */\n check: string;\n ratio: number;\n needs: number;\n message: string;\n}\n\nexport function themeWarnings(themes: readonly ThemeDefinition[], own: { accent?: AppAccent } = {}): string[] {\n return themeFindings(themes, own).map((finding) => finding.message);\n}\n\n/** Every failed check of the themes; with `all`, every check measured, passed or not (a fix reads what it reaches). */\nexport function themeFindings(themes: readonly ThemeDefinition[], own: { accent?: AppAccent } = {}, all = false): ThemeFinding[] {\n return themes.flatMap((theme) =>\n ['default', ...(theme.palettes ?? []).map((item) => item.id)].flatMap((id) =>\n themeModes(theme).flatMap((mode) => {\n const definition = effectiveThemeDefinition(theme, id);\n const palette = withInks(effectiveThemePalette(definition[mode], mode === 'dark', own));\n const name = id === 'default' ? theme.label : `${theme.label} / ${theme.palettes!.find((item) => item.id === id)!.label}`;\n const at = { themeId: theme.id, paletteId: id, mode };\n return CONTRAST_PAIRS.flatMap(([foreground, background, needs, what]): ThemeFinding[] => {\n const a = palette[foreground],\n b = palette[background];\n // A page the colour of its panels is one ground, and one warning; a field the colour of its panel the same.\n if (!a || !b || ((background === 'ground' || background === 'field') && b === palette.surface)) return [];\n // With no edge there is no edge to measure: the fill's own distance from its grounds is (stateFindings).\n if (background === 'field' && foreground === 'lineControl' && palette.rulesControl === '0px') return [];\n const foregroundColor = diagnosticColor(a),\n backgroundColor = diagnosticColor(b);\n if (!foregroundColor || !backgroundColor) return [];\n const ratio = measuredContrast(foregroundColor, backgroundColor);\n if (ratio === undefined || (!all && ratio >= needs)) return [];\n const rule =\n needs === 4.5\n ? 'normal text needs 4.5:1'\n : foreground.startsWith('chart')\n ? 'a chart series needs 3:1'\n : foreground === 'focus'\n ? 'the focus ring needs 3:1'\n : \"a form control's edge needs 3:1\";\n return [\n {\n ...at,\n token: foreground,\n check: `${foreground} on ${background}${what ? ` (${what})` : ''}`,\n ratio,\n needs,\n message: `Theme \"${name}\" (${mode}): ${what ? `${what}, ` : ''}${rolePath(foreground)}/${rolePath(background)} contrast is ${ratio.toFixed(2)}:1; ${rule}.`,\n },\n ];\n }).concat(stateFindings(definition, name, at, palette, all));\n }),\n ),\n );\n}\n\n/** Readers can select any offered look, so App-wide overrides are checked on\n built-ins too. Without overrides retain the registered-theme warning scope. */\nexport function appThemeWarnings(\n themes: readonly ThemeDefinition[],\n own: { accent?: AppAccent; radius?: string; font?: string } = {},\n): string[] {\n const warnings = themeWarnings(own.accent ? [...THEME_PRESETS, ...themes] : themes, own);\n const accents =\n typeof own.accent === 'object'\n ? [\n ['accent.light', own.accent.light],\n ['accent.dark', own.accent.dark],\n ]\n : [['accent', own.accent]];\n for (const [key, value] of accents) {\n if (value && /\\burl\\s*\\(/i.test(value)) warnings.push(`App ui.theme.${key}: a CSS image URL is not a color; use a CSS color value.`);\n else if (value && !diagnosticColor(value))\n warnings.push(\n `App ui.theme.${key}: contrast was not checked for this legacy CSS color; use a hex color for numeric contrast diagnostics.`,\n );\n }\n if (own.radius) {\n const diagnostic = radiusDiagnostic(own.radius);\n if (diagnostic) warnings.push(`App ui.theme.radius: ${diagnostic}`);\n }\n return warnings;\n}\n\n/** Recognize literal border-radius syntax only. Expressions remain compatible;\n an unmeasured diagnostic is not a declaration that the browser rejects them. */\nfunction radiusDiagnostic(raw: string): string | undefined {\n const value = raw.trim().toLowerCase();\n if (/^(?:initial|inherit|unset|revert|revert-layer)$/.test(value)) return undefined;\n const unmeasured = 'validity was not checked for this legacy CSS radius expression; verify its computed border-radius in the browser.';\n if (/[()\\\\]/.test(value)) return unmeasured;\n const groups = value.split('/').map((group) => group.trim().split(/\\s+/));\n if (groups.length > 2 || groups.some((group) => group.length > 4 || !group[0])) return unmeasured;\n for (const token of groups.flat()) {\n const literal = /^([+-]?(?:\\d*\\.\\d+|\\d+)(?:e[+-]?\\d+)?)([a-z]+|%)?$/.exec(token);\n if (!literal) {\n if (/^[a-z-]+$/.test(token)) return 'an unrecognized bare keyword is not a CSS radius; use nonnegative lengths or percentages.';\n return unmeasured;\n }\n const number = Number(literal[1]);\n const unit = literal[2];\n // Unknown/future units are not rejected by this deliberately bounded check.\n if (\n unit &&\n !/^(?:%|px|cm|mm|q|in|pt|pc|em|rem|ex|rex|cap|rcap|ch|rch|ic|ric|lh|rlh|(?:s|l|d)?v(?:w|h|i|b|min|max)|cq(?:w|h|i|b|min|max))$/.test(\n unit,\n )\n )\n return unmeasured;\n if (number < 0) return 'a negative radius is not valid CSS; use a nonnegative CSS length.';\n if (!unit && number !== 0) return 'a nonzero unitless radius is not valid CSS; include a CSS length unit or percentage.';\n }\n return undefined;\n}\n\nfunction stateFindings(\n theme: ThemeDefinition,\n name: string,\n at: Pick<ThemeFinding, 'themeId' | 'paletteId' | 'mode'>,\n palette: ThemePalette,\n all: boolean,\n): ThemeFinding[] {\n const result: ThemeFinding[] = [];\n const mode = at.mode;\n const recipes = theme.recipes ?? {};\n const color = (key: keyof ThemePalette) => (palette[key] ? diagnosticColor(palette[key]!) : undefined);\n const check = (\n label: string,\n foreground: Color | undefined,\n background: Color | undefined,\n needs = 4.5,\n token?: keyof ThemePalette,\n said?: string,\n ) => {\n if (!foreground || !background) return;\n const ratio = measuredContrast(foreground, background);\n if (ratio === undefined || (!all && ratio >= needs)) return;\n const rule = said ?? (needs === 4.5 ? 'normal text needs 4.5:1' : 'the focus ring needs 3:1');\n result.push({\n ...at,\n token: token ?? (/^[a-zA-Z0-9]+/.exec(label)![0] as keyof ThemePalette),\n check: label,\n ratio,\n needs,\n message: `Theme \"${name}\" (${mode}): ${label.replace(/^\\w+/, rolePath).replace(/\\/(\\w+)$/, (_, role: string) => `/${rolePath(role)}`)} contrast is ${ratio.toFixed(2)}:1; ${rule}.`,\n });\n };\n const surface = color('surface'),\n subtle = color('raised'),\n text = color('ink');\n /* A field with no edge (shape.border.control 0px) is told from the page by its fill alone, so the fill must stand\n apart from both grounds it sits on (EDGELESS_FIELD_DELTA says how far). A field that names no fill is the surface,\n which is no distance from the surface at all. */\n if (palette.rulesControl === '0px')\n for (const ground of ['ground', 'surface'] as const) {\n if (ground === 'ground' && (!palette.ground || palette.ground === palette.surface)) continue;\n check(\n `field/${ground}`,\n color('field') ?? surface,\n color(ground),\n EDGELESS_FIELD_DELTA,\n 'field',\n `a field with no edge (shape.border.control 0px) must stand ${EDGELESS_FIELD_DELTA}:1 apart from the ${ground} by its fill, so give colors.field a fill that does or keep a 1px edge`,\n );\n }\n /* The hover and row ground (--raised) carries every kind of text a row\n holds, not only the full ink the pair list checks there: a muted date, a\n faint count, a link. */\n for (const key of ['inkMuted', 'inkSubtle'] as const) check(`${key}/raised`, color(key), subtle);\n /* A tone chip (features.css .tone-*) writes 78% of the meaning colour into\n the text ink, on a 12% tint of it over the panel. Neither end of that is\n a palette colour, so no pair above could see it. */\n for (const role of ['statusGood', 'statusWarning', 'statusCritical', 'statusInfo'] as const) {\n const ink = color(role);\n if (!ink || !text || !surface) continue;\n check(`${role} tone chip`, mixColor(ink, text, 0.78), over(mixColor(ink, [0, 0, 0, 0], 0.12), surface));\n }\n /* A series is drawn as words and marks too, mixed into the ink (componentInks): measured as each is drawn. */\n for (const item of componentInks(palette, recipes))\n check(\n `${item.role} as ${item.what}`,\n diagnosticColor(item.fg),\n diagnosticColor(item.bg),\n item.needs,\n item.role,\n item.needs === 3 ? 'a mark needs 3:1' : undefined,\n );\n /* A danger banner (a page's errors, pending edits) writes the danger colour\n on a 10% tint of it mixed into the panel. */\n const danger = color('statusCritical');\n if (danger && surface) check('statusCritical/danger banner', danger, mixColor(danger, surface, 0.1));\n /* A matrix's heaviest cell says its figure in the ink on the accent's tint, drawn no stronger than heatTop allows:\n only a palette whose ink is too near its accent for even the floor's tint is short here. */\n const accent = color('accent');\n if (accent && surface)\n check(\n \"ink/accent's heat tint (a matrix's heaviest cell)\",\n text,\n over(mixColor(accent, [0, 0, 0, 0], heatTop(palette)), surface),\n 4.5,\n 'accent',\n );\n /* A printed page is white paper whatever the page's ground (style.css's print layer), so a light palette's accent\n and links are read there too: one drawn for a dark \"light\" mode reads on its panel and vanishes on paper (C12).\n A theme that says it is dark has no light palette to be read as paper: its one set of colors is judged as dark. */\n if (mode === 'light' && theme.scheme !== 'dark-only')\n for (const key of ['accentInk', 'link'] as const) {\n /* Darker than its panel, a color reads better on paper than there: the panel's own pair already says it. */\n const value = palette[key];\n const hex = (css: string | undefined) => css !== undefined && /^#[0-9a-f]{6}$/i.test(css);\n if (!hex(value) || !hex(palette.surface) || contrastRatio(value!, '#ffffff') < contrastRatio(palette.surface!, '#ffffff'))\n check(`${key} on printed paper`, color(key), [1, 1, 1, 1], 4.5, key);\n }\n const soft = color('accentSoft');\n for (const ground of ['surface', 'ground'] as const) {\n const base = color(ground);\n if (ground === 'ground' && palette.ground === palette.surface) continue;\n // Selected menu items and pressed controls use accent ink on this tint.\n check(`accentInk/accentSoft over ${ground}`, color('accentInk'), soft && base ? over(soft, base) : undefined);\n }\n // A chosen row, a chosen card and a current step keep their words in the ink on that tint.\n check('ink/accentSoft over surface (a chosen row)', text, soft && surface ? over(soft, surface) : undefined);\n const nav = color('navGround'),\n navInk = color('navInk');\n // visualThemes.css topbar item hover/current and expanded group overlays.\n const navGround = (weight: number) => (nav && navInk ? over(mixColor(navInk, [0, 0, 0, 0], weight), nav) : undefined);\n check('navAccent/navigation hover', color('navAccent'), navGround(0.05));\n // The current entry's block, when a theme names one: its words on it (structure.css).\n if (palette.navCurrent) check('navOnCurrent/navCurrent', color('navOnCurrent') ?? navInk, color('navCurrent'));\n check('navInk/navigation expanded', navInk, navGround(0.07));\n /* The pill menu marks its current entry with the accent's tint, laid on the menu's own ground, in the menu's\n accent (lumenLook.css --current-ground): a pair neither the menu's nor the page's checks see. A theme that\n names its own block (nav.current) is measured on that instead. */\n if (recipes.nav === 'pill' && !palette.navCurrent)\n check(\n \"navAccent/accentSoft over navGround (the pill menu's current entry)\",\n color('navAccent'),\n soft && nav ? over(soft, nav) : undefined,\n );\n /* The search is a field on the surface in the page's ink (themeFamilies.css), a pair the page's checks read; the\n dark bar alone draws it on an 8% ink tint of its own ground in its muted ink (brandLook.css). */\n if (recipes.bar === 'dark') check('navMuted/navigation search', color('navMuted'), navGround(0.08));\n /* The dark bar draws its keys — New, the customer chooser — on the same tint in the menu's ink (brandLook.css), and\n a phone's menu, search and New marks in its muted ink on its ground (visualThemes.css, structure.css): until\n 0.9.6 those stayed the page's keys and greys, about 1.5:1 on a green menu, and no pair here read them. */\n if (recipes.bar === 'dark') check('navInk/navigation bar button', navInk, navGround(0.08));\n if (recipes.table === 'tinted') {\n const accent = color('accent'),\n surface = color('surface');\n const hover = accent && surface ? mixColor(accent, surface, 0.04) : undefined;\n check('ink/tinted row hover', color('ink'), hover);\n check('link/tinted row hover', color('link'), hover);\n }\n /* A banded table lays every other row on the band (brandLook.css): whatever a row says is said on it. */\n if (recipes.table === 'banded' && palette.band)\n for (const key of [\n 'ink',\n 'inkMuted',\n 'inkSubtle',\n 'link',\n 'accentInk',\n 'statusGood',\n 'statusWarning',\n 'statusCritical',\n 'statusInfo',\n ] as const)\n check(`${key}/band`, color(key), color('band'), 4.5, 'band');\n /* A pattern is the theme's line color at its strength over the ground it is drawn on (surfacesLook.css): the words\n on the page, or in the menu, are measured where a line crosses under them, the darkest point of it. */\n const patterned = (place: 'ground' | 'nav') => {\n const pattern = theme.surface?.[place];\n if (!pattern) return;\n const line = color(place === 'nav' ? 'navLine' : 'line') ?? color('line');\n const ground = place === 'nav' ? color('navGround') : (color('ground') ?? surface);\n if (!line || !ground) return;\n const under = over(mixColor(line, [line[0], line[1], line[2], 0], patternStrength(pattern)), ground);\n const where = place === 'nav' ? `the menu's ${pattern.kind} pattern` : `the page's ${pattern.kind} pattern`;\n const inks = place === 'nav' ? (['navInk', 'navMuted'] as const) : (['ink', 'inkMuted', 'inkSubtle', 'link', 'accentInk'] as const);\n for (const key of inks) check(`${key} on ${where}`, color(key), under, 4.5, place === 'nav' ? 'navLine' : 'line');\n };\n patterned('ground');\n patterned('nav');\n /* A part's words on its ground (theme-part): its own ink on its own ground, or the one it is drawn on unsaid. */\n for (const [part, style] of Object.entries(theme.parts ?? {})) {\n if (!style || (style.ground === undefined && style.ink === undefined)) continue;\n const ink = style.ink ? pathRole(style.ink) : partInk(part);\n const ground = style.ground ? pathRole(style.ground) : partGround(part, palette);\n if (!ink || !ground) continue;\n const said = (role: string) => rolePath(role);\n const label = `parts.${part}: ${ink} on ${ground}`;\n const foreground = color(ink as keyof ThemePalette),\n background = color(ground as keyof ThemePalette);\n if (!foreground || !background) continue;\n const ratio = measuredContrast(foreground, background);\n if (ratio === undefined || (!all && ratio >= 4.5)) continue;\n result.push({\n ...at,\n token: (style.ground ? ground : ink) as keyof ThemePalette,\n check: label,\n ratio,\n needs: 4.5,\n message: `Theme \"${name}\" (${mode}): parts.${part}: ${said(ink)} on ${said(ground)} contrast is ${ratio.toFixed(2)}:1; normal text needs 4.5:1.`,\n });\n }\n return result;\n}\n\n/** The ink a part's words are drawn in when its style says none: the menu's for the menu's parts, the link's for a\n link; a primary button's is the ink drawn on the accent fill. */\nfunction partInk(part: string): string | undefined {\n if (part === 'button.primary') return 'onAccent';\n if (/^(?:nav|bar|logo)\\b/.test(part)) return 'navInk';\n return part === 'link' ? 'link' : 'ink';\n}\n/** The ground a part is drawn on when its style says none. */\nfunction partGround(part: string, palette: ThemePalette): string {\n if (/^(?:nav|bar|logo)\\b/.test(part)) return 'navGround';\n if (part === 'button.primary') return 'accent';\n return (part === 'ground' || part === 'page.title') && palette.ground ? 'ground' : 'surface';\n}\n","/**\n * `buildTheme`: a spec already known to be good, assembled into the\n * ThemeDefinition every surface wears — the colour roles, the colours a theme\n * leaves unsaid, a built-in's colours under an author's. The built-in looks\n * are built with it ahead of time (scripts/theme-presets.mjs); an App's or a\n * tenant's theme is checked by themeSpec.ts's `compileTheme` and then built\n * here, on the server or in the branding form, never at first paint.\n */\nimport { mergeType } from '@mistriwork/engine/shared/themeType.js';\nimport type { ThemeColorsOver, ThemeDefinition, ThemeDensity, ThemePalette, ThemeSpec } from '@mistriwork/engine/schema/theme.js';\nimport { DEFAULT_RECIPES } from '@mistriwork/engine/shared/themeRecipes.js';\nimport { ROUND } from '@mistriwork/engine/shared/themeTokens.js';\n\n/* ---------- colors: names by purpose, and the roles they fill ---------- */\n\n/** A spec color's path and the role it fills in a compiled palette: the same name, flattened (`nav.ground` is\n `navGround`), and worn as the custom property of that name (PALETTE_TOKENS). */\nexport const COLOR_ROLES: ReadonlyArray<readonly [string, keyof ThemePalette]> = [\n ['ground', 'ground'],\n ['surface', 'surface'],\n ['field', 'field'],\n ['raised', 'raised'],\n ['ink', 'ink'],\n ['inkMuted', 'inkMuted'],\n ['inkSubtle', 'inkSubtle'],\n ['line', 'line'],\n ['lineStrong', 'lineStrong'],\n ['lineControl', 'lineControl'],\n ['accent', 'accent'],\n ['onAccent', 'onAccent'],\n ['accentInk', 'accentInk'],\n ['accentSoft', 'accentSoft'],\n ['primary', 'primary'],\n ['onPrimary', 'onPrimary'],\n ['link', 'link'],\n ['focus', 'focus'],\n ['status.good', 'statusGood'],\n ['status.warning', 'statusWarning'],\n ['status.critical', 'statusCritical'],\n ['status.info', 'statusInfo'],\n ['nav.ground', 'navGround'],\n ['nav.ink', 'navInk'],\n ['nav.muted', 'navMuted'],\n ['nav.line', 'navLine'],\n ['nav.accent', 'navAccent'],\n ['nav.current', 'navCurrent'],\n ['nav.onCurrent', 'navOnCurrent'],\n ['rules.section', 'rulesSection'],\n ['rules.head', 'rulesHead'],\n ['shadow', 'shadow'],\n ['band', 'band'],\n ...[1, 2, 3, 4, 5, 6, 7, 8].map((n) => [`chart[${n - 1}]`, `chart${n}` as keyof ThemePalette] as const),\n];\n\n/* ---------- the colors a theme leaves unsaid ---------- */\n\nfunction channels(value: string): [number, number, number] {\n return [1, 3, 5].map((start) => parseInt(value.slice(start, start + 2), 16)) as [number, number, number];\n}\n/** `weight` of `a` over `b`, in sRGB, as hex: the derivations below are colors an author could have written. */\nexport function mixHex(a: string, b: string, weight: number): string {\n const x = channels(a),\n y = channels(b);\n return `#${x\n .map((value, index) =>\n Math.round(value * weight + y[index]! * (1 - weight))\n .toString(16)\n .padStart(2, '0'),\n )\n .join('')}`;\n}\n\n/** What a theme that extends nothing starts from where it says nothing: meaning colors that read on white and on\n near-black, and Lumen's chart series after the theme's own accent, spaced for color-blind readers. */\nconst STARTING = {\n light: {\n statusGood: '#117a38',\n statusWarning: '#a84d07',\n statusCritical: '#c81e1e',\n statusInfo: '#0369a1',\n chart: ['#2557e0', '#9a6cf7', '#0e9a6c', '#d97706', '#e0428a', '#0e8fb0', '#a3510b', '#94a3b8'],\n },\n dark: {\n statusGood: '#4ade80',\n statusWarning: '#fbbf24',\n statusCritical: '#f87171',\n statusInfo: '#7dd3fc',\n chart: ['#3559e6', '#9a78ec', '#12a678', '#c88317', '#e0559b', '#1597b9', '#d26b3b', '#6b7694'],\n },\n} as const;\n\n/** Every role, from the four a theme must name. Mixes toward the surface keep a derived ink readable on it; the validator measures them anyway. */\nfunction derive(given: Partial<ThemePalette>, mode: 'light' | 'dark'): ThemePalette {\n const surface = given.surface!,\n ink = given.ink!,\n accent = given.accent!;\n const start = STARTING[mode];\n const palette: ThemePalette = {\n ground: given.ground,\n surface,\n raised: mixHex(ink, surface, 0.04),\n line: mixHex(ink, surface, 0.16),\n lineStrong: mixHex(ink, surface, 0.56),\n lineControl: mixHex(ink, surface, 0.52),\n ink,\n inkMuted: mixHex(ink, surface, 0.82),\n inkSubtle: mixHex(ink, surface, 0.7),\n accent,\n accentSoft: mixHex(accent, surface, 0.12),\n link: accent,\n focus: accent,\n statusGood: start.statusGood,\n statusWarning: start.statusWarning,\n statusCritical: start.statusCritical,\n statusInfo: start.statusInfo,\n navGround: given.surface,\n navInk: ink,\n navMuted: undefined,\n navLine: undefined,\n navAccent: undefined,\n chart1: accent,\n ...Object.fromEntries(start.chart.slice(1).map((color, index) => [`chart${index + 2}`, color])),\n };\n const merged = { ...palette, ...given };\n merged.navGround = given.navGround ?? merged.surface;\n merged.navInk = given.navInk ?? merged.ink;\n merged.navMuted = given.navMuted ?? merged.inkSubtle;\n merged.navLine = given.navLine ?? merged.line;\n merged.navAccent = given.navAccent ?? merged.accent;\n return merged;\n}\n\n/** A built-in's colors under an author's: an accent said without its tint, link, focus, menu accent or first\n series takes them along where the built-in had them equal to its own accent — they were that accent, not\n a color of their own. */\nfunction overlay(base: ThemePalette, over: Partial<ThemePalette>): ThemePalette {\n const merged: ThemePalette = { ...base, ...over };\n if (over.accent !== undefined && over.accent !== base.accent) {\n const surface = merged.surface;\n if (over.accentSoft === undefined) merged.accentSoft = mixHex(over.accent, surface, 0.12);\n /* The inks a base chose for its own accent read on that fill, not this one: they are chosen again where worn. */\n if (over.onAccent === undefined) delete merged.onAccent;\n if (over.accentInk === undefined) delete merged.accentInk;\n if (over.focus === undefined) merged.focus = over.accent;\n for (const role of ['link', 'navAccent'] as const)\n if (over[role] === undefined && base[role] === base.accent) merged[role] = over.accent;\n /* The first series and the metric sparkline are the brand's colour in every look, whether or not the look drew\n its own first hue (Mono's accent is the ink, its first series a blue): a theme that names an accent and no\n chart wears it, so a chart is never the look's colour beside the App's. */\n if (over.chart1 === undefined) merged.chart1 = over.accent;\n }\n /* A form's primary action was the built-in's ink or its accent, not a color of its own: a theme that says another\n of that color and no `primary` takes it along, and the words on it are chosen again where worn. */\n if (over.primary === undefined && base.primary !== undefined)\n for (const role of ['ink', 'accent'] as const)\n if (over[role] !== undefined && over[role] !== base[role] && base.primary === base[role]) {\n merged.primary = over[role];\n if (over.onPrimary === undefined) delete merged.onPrimary;\n }\n if (over.primary !== undefined && over.primary !== base.primary && over.onPrimary === undefined) delete merged.onPrimary;\n return merged;\n}\n\n/** One mode's colours, by purpose, to roles, as written. */\nfunction rolesOf(colors: ThemeColorsOver | undefined): Partial<ThemePalette> {\n const out: Partial<Record<keyof ThemePalette, string>> = {};\n if (!colors) return {};\n const source = colors as Record<string, unknown>;\n for (const [path, role] of COLOR_ROLES) {\n if (path.startsWith('chart[')) continue;\n const [group, key] = path.split('.') as [string, string | undefined];\n const value = key === undefined ? source[group] : (source[group] as Record<string, unknown> | undefined)?.[key];\n if (typeof value === 'string') out[role] = value.trim().toLowerCase();\n }\n colors.chart?.forEach((value, index) => {\n if (typeof value === 'string') out[`chart${index + 1}` as keyof ThemePalette] = value.trim().toLowerCase();\n });\n return out as unknown as Partial<ThemePalette>;\n}\n\n/** A spec that is known to be good — a built-in, or one `compileTheme` has checked — as the definition it compiles\n to. `base` is the built-in it extends, resolved by the caller. */\nexport function buildTheme(spec: ThemeSpec, base?: ThemeDefinition): ThemeDefinition {\n const colors = (spec.colors ?? {}) as { light?: ThemeColorsOver; dark?: ThemeColorsOver };\n /* A rule's and a control's width are shape, said once, and worn as each mode's palette roles they always were. */\n const border = spec.shape?.border;\n const widths: Partial<ThemePalette> = {\n ...(border?.rule ? { rulesWeight: border.rule } : {}),\n ...(border?.control ? { rulesControl: border.control } : {}),\n };\n /* A dark-only theme has one set of colors, worn in both slots: nothing downstream has a light mode to be missing. */\n const darkOnly = spec.scheme === 'dark-only';\n const mode = (name: 'light' | 'dark'): ThemePalette => {\n const given = { ...rolesOf(colors[name]), ...widths };\n return base ? overlay(base[name], given) : derive(given, name);\n };\n /* `round` is a pill's corner: a length past any box's (themeTokens.ts ROUND). */\n const radius = Object.fromEntries(\n Object.entries(spec.shape?.radius ?? {}).map(([key, length]) => [key, length === 'round' ? ROUND : length]),\n );\n const ownBorder = {\n ...(border?.panel ? { panel: border.panel } : {}),\n ...(border?.focus ? { focus: border.focus } : {}),\n };\n const height = spec.shape?.height?.control ? { control: spec.shape.height.control } : undefined;\n const palettes = spec.palettes?.map((variant) => ({\n id: variant.id,\n label: variant.label.trim(),\n light: rolesOf(darkOnly ? variant.dark : variant.light),\n dark: rolesOf(variant.dark),\n }));\n const font = spec.type?.font;\n const fonts = typeof font === 'string' ? { ui: font, data: font, heading: font } : font;\n const density = spec.density ?? base?.density;\n const motion = spec.motion ?? base?.motion;\n const rhythm = spec.rhythm ?? base?.rhythm;\n const type = mergeType(base?.type, spec.type && { base: spec.type.base, roles: spec.type.roles });\n const steps = typeof spec.shape?.elevation === 'object' ? spec.shape.elevation : undefined;\n const lifts = base?.lifts || steps ? { ...base?.lifts, ...steps } : undefined;\n /* A face is a theme's own file: a built-in ships none (Lumen's Inter is its sheet's); a theme value it extends (a\n package's look) may, and its faces stand under this theme's own, which win at a family, weight and style. */\n const own = spec.type?.faces?.map((face) => ({\n family: face.family,\n src: face.src,\n weight: String(face.weight ?? 400),\n style: face.style ?? ('normal' as const),\n ...(face.subset === false ? { subset: false as const } : {}),\n }));\n const cut = (face: { family: string; weight: string; style: string }) => `${face.family.toLowerCase()} ${face.weight} ${face.style}`;\n const faces = base?.faces?.length ? [...base.faces.filter((face) => !own?.some((mine) => cut(mine) === cut(face))), ...(own ?? [])] : own;\n /* A theme's stylesheet is its own, or the one it extends: a built-in has none. */\n const stylesheet = spec.stylesheet ? { src: spec.stylesheet } : base?.stylesheet;\n /* A pattern says its strength (1 when omitted); a part's style lies over the one it extends, property by property. */\n const patterns = Object.entries(spec.surface ?? {}).map(([place, pattern]) => [\n place,\n { kind: pattern!.kind, strength: pattern!.strength ?? 1 },\n ]);\n const surface = base?.surface || patterns.length ? { ...base?.surface, ...Object.fromEntries(patterns) } : undefined;\n let parts: ThemeDefinition['parts'] = base?.parts ? { ...base.parts } : undefined;\n for (const [part, style] of Object.entries(spec.parts ?? {}))\n parts = { ...parts, [part]: { ...parts?.[part as keyof typeof parts], ...style } };\n const icons = spec.type?.icons ?? base?.icons;\n return {\n id: spec.id,\n label: spec.label.trim(),\n ...(base?.base ? { base: base.base } : {}),\n light: mode(darkOnly ? 'dark' : 'light'),\n dark: mode('dark'),\n ...((spec.scheme ?? base?.scheme) ? { scheme: (spec.scheme ?? base?.scheme)! } : {}),\n ...(palettes === undefined ? (base?.palettes && spec.colors === undefined ? { palettes: base.palettes } : {}) : { palettes }),\n ...(spec.defaultPalette === undefined ? {} : { defaultPalette: spec.defaultPalette }),\n fonts: { ...base?.fonts, ...fonts },\n ...(faces?.length ? { faces } : {}),\n radius: { control: '4px', field: '4px', ...base?.radius, ...radius },\n ...(base?.border || Object.keys(ownBorder).length ? { border: { ...base?.border, ...ownBorder } } : {}),\n ...(base?.height || height ? { height: { ...base?.height, ...height } } : {}),\n recipes: { ...DEFAULT_RECIPES, ...base?.recipes, ...spec.recipes },\n /* A preset is the look's own drawing of its lifts; `{ card, float }` steps over the base's, which stays. */\n elevation: (typeof spec.shape?.elevation === 'string' ? spec.shape.elevation : undefined) ?? base?.elevation ?? 'hairline',\n ...(lifts ? { lifts } : {}),\n ...(type ? { type } : {}),\n ...(density ? { density: density as ThemeDensity } : {}),\n ...(motion ? { motion } : {}),\n ...(rhythm ? { rhythm } : {}),\n /* The theme's own suggestion only: a built-in suggests none, so there is nothing to inherit. */\n ...(spec.shell?.placement ? { shell: { placement: spec.shell.placement } } : {}),\n ...(surface ? { surface } : {}),\n ...(parts ? { parts } : {}),\n ...(icons ? { icons } : {}),\n ...(stylesheet ? { stylesheet } : {}),\n };\n}\n","/**\n * `defineTheme`: a theme by purpose — colors for what they are for, a shape, a\n * type, a density and a pick from a closed menu of treatments — compiled to the\n * ThemeDefinition every surface already wears. The built-in looks are written in\n * it (themePresets.ts), so the menu is exactly what they do and an App's theme\n * can only ask for what one of them already draws. There is no CSS in a theme:\n * each recipe is a `data-look-<part>` attribute the shipped sheets answer.\n *\n * This module checks; themeBuild.ts builds. The built-in looks are built there\n * without these checks, because the browser wears them at first paint and the\n * checks' messages are weight no reader needs; test/theme-spec.test.ts holds\n * every built-in to them. An App's or a tenant's theme comes through here.\n */\nimport type {\n ThemeColorsOver,\n ThemeDefinition,\n ThemeDensity,\n ThemePalette,\n ThemePaletteVariant,\n ThemeSpec,\n} from '@mistriwork/engine/schema/theme.js';\nimport { buildTheme, COLOR_ROLES } from './themeBuild.js';\nimport {\n BORDER_MENU,\n CONTROL_HEIGHT,\n controlHeightFits,\n cornerFits,\n RADIUS_MAX,\n ROUND_CORNERS,\n rolePath,\n} from '@mistriwork/engine/shared/themeTokens.js';\nimport {\n DENSITIES,\n ELEVATIONS,\n LIFTS,\n MOTIONS,\n RECIPE_MENU,\n RECIPES_RENAMED,\n RHYTHMS,\n RECIPE_PARTS,\n type ResolvedRecipes,\n} from '@mistriwork/engine/shared/themeRecipes.js';\nimport { TYPE_MOVED, typeOf } from '@mistriwork/engine/shared/themeType.js';\nimport { parseScheme, THEME_PRESETS, themePreset } from '@mistriwork/engine/shared/themePresets.js';\nimport { iconsOf, partsOf, surfaceOf } from '@mistriwork/engine/shared/themePartsCheck.js';\n\n/* ---------- colors: names by purpose, and the roles they fill ---------- */\n\n/** The spec's name for a role, for a message an author can act on: `status.critical`, not `statusCritical`. */\nexport function colorPath(role: string): string {\n return rolePath(role);\n}\n\nconst HEX = /^#[0-9a-f]{6}$/i;\nconst GROUPS = {\n status: ['good', 'warning', 'critical', 'info'],\n nav: ['ground', 'ink', 'muted', 'line', 'accent', 'current', 'onCurrent'],\n rules: ['section', 'head'],\n} as const;\nconst FLAT = [\n 'ground',\n 'surface',\n 'field',\n 'raised',\n 'ink',\n 'inkMuted',\n 'inkSubtle',\n 'line',\n 'lineStrong',\n 'lineControl',\n 'accent',\n 'onAccent',\n 'accentInk',\n 'accentSoft',\n 'primary',\n 'onPrimary',\n 'link',\n 'focus',\n 'shadow',\n 'band',\n];\n\n/** The names a color went by before the spec's were the only ones (0.9.2): the palette's, which the sheets and the\n compiled theme used. There is one name per color now, and a theme that says an old one is told the new. */\nconst OLD_NAMES: Readonly<Record<string, string>> = {\n background: 'ground',\n subtle: 'raised',\n border: 'line',\n borderStrong: 'lineStrong',\n borderControl: 'lineControl',\n text: 'ink',\n textMuted: 'inkMuted',\n textSubtle: 'inkSubtle',\n positive: 'status.good',\n warning: 'status.warning',\n negative: 'status.critical',\n key: 'status.info',\n navSurface: 'nav.ground',\n navText: 'nav.ink',\n navMuted: 'nav.muted',\n navBorder: 'nav.line',\n navAccent: 'nav.accent',\n navCurrent: 'nav.current',\n navCurrentText: 'nav.onCurrent',\n sectionRule: 'rules.section',\n headRule: 'rules.head',\n ruleWeight: 'rules.weight',\n};\n/** The widths that were colors' neighbors until 0.9.4: a rule's and a control's edge are shape, said once for both modes. */\nconst MOVED_TO_SHAPE: Readonly<Record<string, string>> = { weight: 'shape.border.rule', control: 'shape.border.control' };\nfunction renamed(colors: Record<string, unknown>, where: string) {\n for (const key of Object.keys(colors))\n if (Object.hasOwn(OLD_NAMES, key)) throw new Error(`${where}.${key} is now ${where}.${OLD_NAMES[key]}.`);\n /* Renamed before 1.0: words on a fill are `on<Fill>` (onAccent), and `<color>Ink` is a color used as text (accentInk). */\n const nav = colors.nav;\n if (nav && typeof nav === 'object' && Object.hasOwn(nav, 'currentInk'))\n throw new Error(`${where}.nav.currentInk is now ${where}.nav.onCurrent: the words on nav.current.`);\n const rules = colors.rules;\n if (rules && typeof rules === 'object')\n for (const key of Object.keys(rules))\n if (Object.hasOwn(MOVED_TO_SHAPE, key)) throw new Error(`${where}.rules.${key} is now ${MOVED_TO_SHAPE[key]}.`);\n}\n\nfunction record(raw: unknown, where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${where} must be an object.`);\n return raw as Record<string, unknown>;\n}\nfunction only(raw: Record<string, unknown>, keys: readonly string[], where: string) {\n for (const key of Object.keys(raw)) if (!keys.includes(key)) throw new Error(`${where} has no \"${key}\". It takes: ${keys.join(', ')}.`);\n}\nfunction hex(value: unknown, where: string): string {\n if (typeof value !== 'string' || !HEX.test(value.trim())) throw new Error(`${where} must be a six-digit hex color like #1d4ed8.`);\n return value.trim().toLowerCase();\n}\n\n/** One mode's colors, by purpose, to roles. `required` names the four a theme that extends nothing must say. */\nfunction colorsToRoles(raw: unknown, where: string, required: boolean): Partial<ThemePalette> {\n if (raw === undefined && !required) return {};\n const colors = record(raw, where);\n renamed(colors, where);\n only(colors, [...FLAT, 'status', 'nav', 'rules', 'chart'], where);\n const out: Partial<Record<keyof ThemePalette, string>> = {};\n for (const [path, role] of COLOR_ROLES) {\n const [group, key] = path.split('.') as [string, string | undefined];\n if (path.startsWith('chart[')) continue;\n if (key === undefined) {\n if (colors[group] !== undefined) out[role] = hex(colors[group], `${where}.${group}`);\n continue;\n }\n const inner = colors[group];\n if (inner === undefined) continue;\n const values = record(inner, `${where}.${group}`);\n only(values, GROUPS[group as keyof typeof GROUPS], `${where}.${group}`);\n if (values[key] === undefined) continue;\n out[role] = hex(values[key], `${where}.${path}`);\n }\n if (colors.chart !== undefined) {\n if (!Array.isArray(colors.chart) || colors.chart.length > 8) throw new Error(`${where}.chart must be at most eight colors.`);\n colors.chart.forEach((value, index) => {\n if (value === null && !required) return;\n out[`chart${index + 1}` as keyof ThemePalette] = hex(value, `${where}.chart[${index}]`);\n });\n }\n if (required)\n for (const key of ['ground', 'surface', 'ink', 'accent'])\n if (colors[key] === undefined) throw new Error(`${where}.${key} is required.`);\n /* The words on a fill are said with the fill: on their own they would sit on whichever color a look chose. */\n if (colors.onPrimary !== undefined && colors.primary === undefined)\n throw new Error(`${where}.onPrimary is the words on ${where}.primary: name primary beside it.`);\n return out as Partial<ThemePalette>;\n}\n\n/* ---------- the spec ---------- */\n\nconst THEME_ID = /^[a-z][a-z0-9-]{0,47}$/;\n\nconst FONT = /^[\\w\\s'\",.-]+$/;\nconst SPEC_KEYS = [\n 'kind',\n 'id',\n 'label',\n 'extends',\n 'colors',\n 'palettes',\n 'defaultPalette',\n 'scheme',\n 'shape',\n 'type',\n 'density',\n 'motion',\n 'rhythm',\n 'recipes',\n 'shell',\n 'surface',\n 'parts',\n 'stylesheet',\n 'contract',\n 'from',\n];\n\n/** The theme contract this mistri reads (docs: theme-contract). A package says the one it was written to. */\nconst THEME_CONTRACT = 1;\n/* A stylesheet beside the theme, downward only: `./theme.css`, `./looks/ledger.css`. The server holds the resolved\n file to the theme's directory as well (appBrand.ts), so a link out of it is refused there. */\nconst SHEET_SRC = /^\\.\\/(?:[\\w.-]+\\/)*[\\w.-]+\\.css$/;\n\n/**\n * The directory a theme's files are read from (its faces and its stylesheet), where it is not the App's: a package's,\n * from its `from`. Kept here rather than on the definition, which is sent to the browser and written into a build:\n * a path on the machine is neither's to carry. Keyed by the face or the stylesheet record, so one inherited from a\n * package's theme is read from the package.\n */\nconst FILE_ROOTS = new WeakMap<object, string>();\nexport function themeFileRoot(file: object): string | undefined {\n return FILE_ROOTS.get(file);\n}\n/** The theme a compiled theme extends when that is a theme value (`extends: ledger`), for the validator. */\nconst PARENTS = new WeakMap<ThemeDefinition, ThemeDefinition>();\nfunction themeParent(theme: ThemeDefinition): ThemeDefinition | undefined {\n return PARENTS.get(theme);\n}\n\n/** A theme spec checked and compiled. `builtIn` resolves `extends`; `reserved` are ids it may not take. */\nexport function compileTheme(\n raw: unknown,\n where: string,\n builtIn: (id: string) => ThemeDefinition | undefined,\n reserved: ReadonlySet<string> = new Set(),\n options: ThemeCompileOptions = {},\n): ThemeDefinition {\n const spec = record(raw, where) as Record<string, unknown> & Partial<ThemeSpec>;\n // The key was `palette` until 1.0; `palettes` and `ui.theme.palette` name variations, so the colors moved out of their way.\n if ('palette' in spec) throw new Error(`${where}.palette is now colors: { light, dark }.`);\n only(spec, SPEC_KEYS, where);\n if (spec.kind !== undefined && spec.kind !== 'theme') throw new Error(`${where}.kind must be \"theme\" (write it with defineTheme).`);\n if (typeof spec.id !== 'string' || !THEME_ID.test(spec.id) || reserved.has(spec.id))\n throw new Error(`${where}.id must be a unique lowercase slug, not a built-in look's name.`);\n if (typeof spec.label !== 'string' || !spec.label.trim() || spec.label.trim().length > 60)\n throw new Error(`${where}.label must have 1 to 60 characters.`);\n /* A package's theme says the contract it was written to, and where its files are (theme-contract). */\n if (spec.contract !== undefined && spec.contract !== THEME_CONTRACT)\n throw new Error(\n `${where}.contract is ${JSON.stringify(spec.contract)}: this mistri reads theme contract ${THEME_CONTRACT}, and a theme written to another is refused. Use the package's release for contract ${THEME_CONTRACT}, or upgrade mistri.`,\n );\n let root: string | undefined;\n if (spec.from !== undefined) {\n if (typeof spec.from !== 'string' || !spec.from.startsWith('file://'))\n throw new Error(`${where}.from must be the theme module's import.meta.url, a file: URL.`);\n if (spec.contract === undefined)\n throw new Error(\n `${where}.from names a package's directory, and a package says the contract it was written to: contract: ${THEME_CONTRACT}.`,\n );\n if (!options.faces) throw new Error(`${where}.from reads files from a package, and this theme has no server to read them.`);\n root = decodeURIComponent(new URL('.', spec.from).pathname).replace(/\\/$/, '');\n }\n let base: ThemeDefinition | undefined;\n if (spec.extends !== undefined && typeof spec.extends === 'object' && spec.extends !== null) {\n /* A theme value (a package's look): itself extending a built-in or nothing, so a chain is two deep at most. */\n const parent = spec.extends as unknown as Record<string, unknown>;\n if (parent.kind !== 'theme') throw new Error(`${where}.extends must name a built-in look or be a theme written with defineTheme.`);\n if (parent.extends !== undefined && typeof parent.extends !== 'string')\n throw new Error(\n `${where}.extends is a theme that extends another theme; a theme extends a built-in look or one theme that extends one, no deeper.`,\n );\n base = compileTheme(parent, `${where}.extends`, builtIn, new Set(['default']), options);\n } else if (spec.extends !== undefined) {\n base = typeof spec.extends === 'string' ? builtIn(spec.extends) : undefined;\n if (!base) throw new Error(`${where}.extends must name a built-in look.`);\n }\n const colors = spec.colors === undefined && base ? {} : record(spec.colors, `${where}.colors`);\n only(colors, ['light', 'dark'], `${where}.colors`);\n const darkOnly = parseScheme(spec.scheme, where) === 'dark-only';\n if (darkOnly && colors.light !== undefined)\n throw new Error(`${where}.colors.light: a dark-only theme has no light colors; name colors.dark alone, or drop scheme: 'dark-only'.`);\n for (const name of darkOnly ? (['dark'] as const) : (['light', 'dark'] as const))\n colorsToRoles(colors[name], `${where}.colors.${name}`, !base);\n\n const palettes = spec.palettes === undefined ? undefined : variants(spec.palettes, `${where}.palettes`, darkOnly);\n if (\n spec.defaultPalette !== undefined &&\n (typeof spec.defaultPalette !== 'string' ||\n (spec.defaultPalette !== 'default' && !palettes?.some((item) => item.id === spec.defaultPalette)))\n )\n throw new Error(`${where}.defaultPalette must be \"default\" or one of this theme's palettes.`);\n\n const shape = spec.shape === undefined ? {} : record(spec.shape, `${where}.shape`);\n only(shape, ['radius', 'elevation', 'border', 'height'], `${where}.shape`);\n if (shape.radius !== undefined) radiiOf(shape.radius, `${where}.shape.radius`);\n if (shape.border !== undefined) bordersOf(shape.border, `${where}.shape.border`);\n if (shape.height !== undefined) {\n const height = record(shape.height, `${where}.shape.height`);\n only(height, ['control'], `${where}.shape.height`);\n if (height.control !== undefined && !controlHeightFits(height.control))\n throw new Error(\n `${where}.shape.height.control must be a length from ${CONTROL_HEIGHT.min}px to ${CONTROL_HEIGHT.max}px, in whole pixels.`,\n );\n }\n if (shape.elevation !== undefined && typeof shape.elevation === 'object') liftsOf(shape.elevation, `${where}.shape.elevation`);\n else if (shape.elevation !== undefined && !Object.hasOwn(ELEVATIONS, shape.elevation as string))\n throw new Error(`${where}.shape.elevation must be one of ${Object.keys(ELEVATIONS).join(', ')}, or { card, float }.`);\n\n const type = spec.type === undefined ? {} : record(spec.type, `${where}.type`);\n for (const key of ['scale', 'headingWeight'])\n if (key in type) throw new Error(`${where}.type.${key} is now ${where}.${TYPE_MOVED[`type.${key}`]}.`);\n only(type, ['font', 'faces', 'base', 'roles', 'icons'], `${where}.type`);\n if (type.icons !== undefined) iconsOf(type.icons, `${where}.type.icons`);\n if (type.font !== undefined) fontsOf(type.font, `${where}.type.font`);\n if (type.faces !== undefined) {\n /* A face is a file in the App's directory, read and served by the server that loaded the App (appFonts.ts):\n a tenant's saved theme or a host page's has no directory to read it from. */\n if (!options.faces)\n throw new Error(`${where}.type.faces ships font files from an App's directory, and this theme has none to read them from.`);\n facesOf(type.faces, `${where}.type.faces`);\n }\n typeOf(type.base, type.roles, `${where}.type`);\n if (spec.density !== undefined && !DENSITIES.includes(spec.density as ThemeDensity))\n throw new Error(`${where}.density must be one of ${DENSITIES.join(', ')}.`);\n if (spec.motion !== undefined && !Object.hasOwn(MOTIONS, spec.motion as string))\n throw new Error(`${where}.motion must be one of ${Object.keys(MOTIONS).join(', ')}.`);\n if (spec.rhythm !== undefined && !Object.hasOwn(RHYTHMS, spec.rhythm as string))\n throw new Error(`${where}.rhythm must be one of ${Object.keys(RHYTHMS).join(', ')}.`);\n\n recipesOf(spec.recipes, `${where}.recipes`);\n if (spec.shell !== undefined) {\n const shell = record(spec.shell, `${where}.shell`);\n only(shell, ['placement'], `${where}.shell`);\n if (shell.placement !== undefined && shell.placement !== 'side' && shell.placement !== 'top')\n throw new Error(`${where}.shell.placement must be \"side\" or \"top\".`);\n }\n if (spec.surface !== undefined) surfaceOf(spec.surface, `${where}.surface`);\n if (spec.parts !== undefined) partsOf(spec.parts, `${where}.parts`);\n if (spec.stylesheet !== undefined) {\n /* Read, checked against its allowlist and served by the server that loaded the App (appBrand.ts). */\n if (!options.faces) throw new Error(`${where}.stylesheet is a file in an App's directory, and this theme has none to read it from.`);\n if (typeof spec.stylesheet !== 'string' || !SHEET_SRC.test(spec.stylesheet) || spec.stylesheet.split('/').includes('..'))\n throw new Error(`${where}.stylesheet must be a .css file beside the theme, written like \"./theme.css\".`);\n }\n const built = buildTheme(spec as ThemeSpec, base);\n if (base && typeof spec.extends === 'object') PARENTS.set(built, base);\n /* The files this theme names itself are read from its package, when it is one; inherited ones keep their own. */\n if (root) {\n for (const face of built.faces ?? []) if (!base?.faces?.includes(face)) FILE_ROOTS.set(face, root);\n if (built.stylesheet && built.stylesheet !== base?.stylesheet) FILE_ROOTS.set(built.stylesheet, root);\n }\n return built;\n}\n\nfunction variants(raw: unknown, where: string, darkOnly = false): ThemePaletteVariant[] {\n if (!Array.isArray(raw) || raw.length > 20) throw new Error(`${where} must be at most 20 palettes.`);\n const used = new Set(['default']);\n return raw.map((value, index) => {\n const path = `${where}[${index}]`,\n variant = record(value, path);\n only(variant, ['id', 'label', 'light', 'dark'], path);\n if (typeof variant.id !== 'string' || !THEME_ID.test(variant.id) || used.has(variant.id))\n throw new Error(`${path}.id must be a unique slug, not \"default\".`);\n used.add(variant.id);\n if (typeof variant.label !== 'string' || !variant.label.trim() || variant.label.trim().length > 60)\n throw new Error(`${path}.label must have 1 to 60 characters.`);\n if (darkOnly && variant.light !== undefined)\n throw new Error(`${path}.light: a dark-only theme has no light colors; name the palette's dark alone.`);\n return {\n id: variant.id,\n label: variant.label.trim(),\n light: colorsToRoles(variant.light, `${path}.light`, false),\n dark: colorsToRoles(variant.dark, `${path}.dark`, false),\n };\n });\n}\n\nfunction radiiOf(raw: unknown, where: string): Record<string, string> {\n if (typeof raw === 'string')\n throw new Error(\n `${where} is { control, field, pill, panel }, each a length (a control's and a pill's may be 'round'); one length is not accepted. Write { control: '${raw}', field: '${raw}' }.`,\n );\n const value = record(raw, where);\n only(value, Object.keys(RADIUS_MAX), where);\n for (const [key, length] of Object.entries(value)) {\n const max = RADIUS_MAX[key as keyof typeof RADIUS_MAX];\n const round = ROUND_CORNERS.has(key);\n if (!(cornerFits(length, max) || (round && length === 'round')))\n throw new Error(`${where}.${key} must be a length from 0px to ${max}px${round ? \", or 'round'\" : ''}.`);\n }\n return value as Record<string, string>;\n}\n\n/** `{ card, float }`: a step for each, from the platform's lifts (the attribute's `look` is not one an author says). */\nfunction liftsOf(raw: unknown, where: string): void {\n const value = record(raw, where);\n only(value, ['card', 'float'], where);\n const steps = Object.keys(LIFTS).slice(1);\n for (const [key, step] of Object.entries(value))\n if (!steps.includes(step as string)) throw new Error(`${where}.${key} must be one of ${steps.join(', ')}.`);\n}\n\nfunction bordersOf(raw: unknown, where: string): void {\n const value = record(raw, where);\n only(value, Object.keys(BORDER_MENU), where);\n for (const [key, width] of Object.entries(value)) {\n const menu = BORDER_MENU[key as keyof typeof BORDER_MENU] as readonly string[];\n if (!menu.includes(width as string)) throw new Error(`${where}.${key} must be ${menu.slice(0, -1).join(', ')} or ${menu.at(-1)}.`);\n }\n}\n\nfunction fontsOf(raw: unknown, where: string): Partial<Record<'ui' | 'data' | 'heading' | 'code', string>> {\n const ok = (font: unknown): font is string => typeof font === 'string' && font.length > 0 && font.length <= 200 && FONT.test(font);\n if (typeof raw === 'string') {\n if (!ok(raw)) throw new Error(`${where} must be a CSS font-family list (letters, digits, spaces, quotes, commas).`);\n return { ui: raw, data: raw, heading: raw };\n }\n const value = record(raw, where);\n only(value, ['ui', 'data', 'heading', 'code'], where);\n for (const [key, font] of Object.entries(value))\n if (!ok(font)) throw new Error(`${where}.${key} must be a CSS font-family list (letters, digits, spaces, quotes, commas).`);\n return value as Record<string, string>;\n}\n\n/** What a compile allows beyond the spec's own rules. */\ninterface ThemeCompileOptions {\n /** `type.faces`: only an App's own theme, whose files the server reads from its directory. */\n faces?: boolean;\n}\n\n/* A family name as an `@font-face` and a stack both say it, quoted: no quote, comma or semicolon to close either. */\nconst FACE_FAMILY = /^[A-Za-z0-9][A-Za-z0-9 _-]{0,59}$/;\n/* Relative to the App's directory, downward only: `./fonts/x.woff2`. The server holds the resolved file to the\n directory as well (appFonts.ts), so a link out of it is refused there. */\nconst FACE_SRC = /^\\.\\/(?:[\\w.-]+\\/)*[\\w.-]+\\.(?:woff2|woff)$/;\nconst FACE_WEIGHT = /^[1-9]00(?: [1-9]00)?$/;\n/** At most this many faces: a regular, a bold and their italics in each of three families. */\nconst MAX_FACES = 12;\n\nfunction facesOf(raw: unknown, where: string) {\n if (!Array.isArray(raw) || raw.length > MAX_FACES) throw new Error(`${where} must be at most ${MAX_FACES} faces.`);\n const seen = new Set<string>();\n raw.forEach((value, index) => {\n const path = `${where}[${index}]`,\n face = record(value, path);\n only(face, ['family', 'src', 'weight', 'style', 'subset'], path);\n if (typeof face.family !== 'string' || !FACE_FAMILY.test(face.family))\n throw new Error(`${path}.family must be a family name of letters, digits, spaces, hyphens or underscores (at most 60).`);\n if (typeof face.src !== 'string' || !FACE_SRC.test(face.src) || face.src.split('/').includes('..'))\n throw new Error(`${path}.src must be a .woff2 or .woff file inside the App's directory, written like \"./fonts/face.woff2\".`);\n if (\n face.weight !== undefined &&\n !(typeof face.weight === 'number' && Number.isInteger(face.weight) && face.weight >= 100 && face.weight <= 900) &&\n !(typeof face.weight === 'string' && FACE_WEIGHT.test(face.weight))\n )\n throw new Error(`${path}.weight must be 100 to 900, or a variable file's range like \"100 900\".`);\n if (face.style !== undefined && face.style !== 'normal' && face.style !== 'italic')\n throw new Error(`${path}.style must be \"normal\" or \"italic\".`);\n // Latin is the default cut; the one other choice is the whole file.\n if (face.subset !== undefined && face.subset !== false)\n throw new Error(`${path}.subset must be false (ship the file whole), or left out to cut it to Latin.`);\n const key = `${face.family.toLowerCase()} ${String(face.weight ?? 400)} ${String(face.style ?? 'normal')}`;\n if (seen.has(key)) throw new Error(`${path} repeats ${face.family} at that weight and style.`);\n seen.add(key);\n });\n}\n\nfunction recipesOf(raw: unknown, where: string): Partial<ResolvedRecipes> {\n if (raw === undefined) return {};\n const value = record(raw, where);\n for (const [part, pick] of Object.entries(value)) {\n const moved = TYPE_MOVED[`recipes.${part}`];\n if (moved) throw new Error(`${where}.${part} is now ${where.replace(/\\.recipes$/, '')}.${moved}.`);\n if (Object.hasOwn(RECIPES_RENAMED, part)) throw new Error(`${where}.${part} is now ${where}.${RECIPES_RENAMED[part]}.`);\n const menu = (RECIPE_MENU as Record<string, Record<string, unknown>>)[part];\n if (!menu) throw new Error(`${where} has no part \"${part}\". The parts are: ${RECIPE_PARTS.join(', ')}.`);\n if (typeof pick !== 'string' || !Object.hasOwn(menu, pick))\n throw new Error(`${where}.${part} is not on the menu: pick one of ${Object.keys(menu).join(', ')}.`);\n }\n return value as Partial<ResolvedRecipes>;\n}\n\n/** A compiled theme's colors back into the spec's words, for the reference and the round-trip test. */\nexport function rolesToColors(palette: Partial<ThemePalette>): ThemeColorsOver {\n const out: Record<string, unknown> = {};\n for (const [path, role] of COLOR_ROLES) {\n const value = palette[role];\n if (value === undefined) continue;\n const chart = /^chart\\[(\\d)\\]$/.exec(path);\n if (chart) {\n const series = (out.chart ??= []) as (string | null)[];\n while (series.length < Number(chart[1])) series.push(null);\n series[Number(chart[1])] = value;\n } else if (path.includes('.')) {\n const [group, key] = path.split('.') as [string, string];\n ((out[group] ??= {}) as Record<string, string>)[key] = value;\n } else out[path] = value;\n }\n return out as ThemeColorsOver;\n}\n\nconst BUILT_IN_IDS: ReadonlySet<string> = new Set(['default', ...THEME_PRESETS.map((look) => look.id)]);\n\n/** Which built-in each compiled App theme extends, so the validator holds against its author only the colours they set. */\nconst EXTENDED = new WeakMap<ThemeDefinition, ThemeDefinition | null>();\n\n/** An App's `defineTheme` result, checked and compiled over the built-in it extends. */\nexport function compileAppTheme(\n raw: unknown,\n where: string,\n taken: ReadonlySet<string> = new Set(),\n options: ThemeCompileOptions = {},\n): ThemeDefinition {\n let extended: ThemeDefinition | null = null;\n const theme = compileTheme(\n raw,\n where,\n (id) => {\n extended = BUILT_IN_IDS.has(id) ? (themePreset(id) ?? null) : null;\n return extended ?? undefined;\n },\n new Set([...BUILT_IN_IDS, ...taken]),\n options,\n );\n EXTENDED.set(theme, themeParent(theme) ?? extended);\n /* The spec as written, for what a check suggests in its words and where `check --fix` writes (themeFix.ts). */\n /* The ids taken then, copied: the caller goes on to add this theme's own, which a variation must still be allowed. */\n SPECS.set(theme, { raw: raw as ThemeSpec, where, taken: new Set(taken), options });\n return theme;\n}\n\n/** Each compiled App theme's spec and how it was compiled, so a fix can be tried by compiling the spec again. */\nconst SPECS = new WeakMap<ThemeDefinition, { raw: ThemeSpec; where: string; taken: ReadonlySet<string>; options: ThemeCompileOptions }>();\n\n/** The spec a compiled App theme came from, and how to compile a variation of it; undefined for a theme not compiled here. */\nexport function themeSpecOf(\n theme: ThemeDefinition,\n): { raw: ThemeSpec; where: string; taken: ReadonlySet<string>; options: ThemeCompileOptions } | undefined {\n return SPECS.get(theme);\n}\n\n/** The built-in a compiled App theme extends: `null` for one that extends nothing, `undefined` for a theme not compiled here. */\nexport function themeExtends(theme: ThemeDefinition): ThemeDefinition | null | undefined {\n return EXTENDED.get(theme);\n}\n","/**\n * Chart series apart for color-blind readers: the one measure `mistri check` holds an App's chart to\n * (themeCheck.ts) and the built-in looks order their series by under each palette (lookPalettes.ts).\n */\n/** Machado, Oliveira and Fernandes (2009) at full severity, in linear sRGB: the\n two red-green deficiencies, about one man in twelve between them. Tritanopia\n is rare enough (about 1 in 10,000) that it is not held against a palette. */\nconst DEFICIENCIES = {\n protanopia: [\n [0.152286, 1.052583, -0.204868],\n [0.114503, 0.786281, 0.099216],\n [-0.003882, -0.048116, 1.051998],\n ],\n deuteranopia: [\n [0.367322, 0.860646, -0.227968],\n [0.280085, 0.672501, 0.047413],\n [-0.01182, 0.04294, 0.968881],\n ],\n} as const;\n/** Adjacent series, in OKLab ΔE ×100: at least 8 under each deficiency, and 10 to normal vision. */\nconst CHART_SEPARATION = { deficient: 8, normal: 10 } as const;\n\nexport function linear(hex: string): [number, number, number] {\n return [1, 3, 5].map((start) => {\n const value = parseInt(hex.slice(start, start + 2), 16) / 255;\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n}\nexport function oklab([r, g, b]: readonly number[]): [number, number, number] {\n const l = Math.cbrt(0.4122214708 * r! + 0.5363325363 * g! + 0.0514459929 * b!),\n m = Math.cbrt(0.2119034982 * r! + 0.6806995451 * g! + 0.1073969566 * b!),\n s = Math.cbrt(0.0883024619 * r! + 0.2817188376 * g! + 0.6299787005 * b!);\n return [\n 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n ];\n}\nfunction simulate(color: readonly number[], matrix: readonly (readonly number[])[]): number[] {\n return matrix.map((row) => Math.min(1, Math.max(0, row[0]! * color[0]! + row[1]! * color[1]! + row[2]! * color[2]!)));\n}\n/** OKLab distance ×100 between two hex colors, as seen with the deficiency named or with normal vision. */\nfunction chartDistance(a: string, b: string, deficiency?: keyof typeof DEFICIENCIES): number {\n const see = (hex: string) => oklab(deficiency ? simulate(linear(hex), DEFICIENCIES[deficiency]) : linear(hex));\n const x = see(a),\n y = see(b);\n return 100 * Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]);\n}\n\n/** The first reader two series are too alike for, how far apart they are to them and what they need; undefined when\n every reader tells them apart. The one measure: the check says it, and a suggested chart is proved by it. */\nexport function chartPairFailure(a: string, b: string): { vision: string; distance: number; needs: number } | undefined {\n const tests: Array<[string, number, number]> = [\n ['normal vision', chartDistance(a, b), CHART_SEPARATION.normal],\n ...(Object.keys(DEFICIENCIES) as (keyof typeof DEFICIENCIES)[]).map(\n (kind) => [kind, chartDistance(a, b, kind), CHART_SEPARATION.deficient] as [string, number, number],\n ),\n ];\n const failed = tests.find(([, distance, needs]) => distance < needs);\n return failed && { vision: failed[0], distance: failed[1], needs: failed[2] };\n}\n\n/** Whether two series stay apart for every reader the chart check holds a palette to. */\nexport function chartsApart(a: string, b: string): boolean {\n return !chartPairFailure(a, b);\n}\n","/**\n * The one validator a theme meets, wherever it comes from: `mistri check`\n * and `build` run it over an App's `defineTheme` themes, startup over its\n * accent on the looks it offers, and a tenant's branding save over the theme\n * that tenant would wear (tenantBranding.ts). It adds to the contrast checks\n * (theme.ts) the chart's color-blind separation, and says each failure by the\n * color's own name in the spec (`status.critical`, `chart[2]`).\n */\nimport type { ThemeDefinition, ThemePalette } from '@mistriwork/engine/schema/theme.js';\nimport { rolePairs, themeFindings, type ThemeFinding } from '@mistriwork/engine/shared/theme.js';\nimport {\n effectiveThemeDefinition,\n effectiveThemePalette,\n themeModes,\n withInks,\n type AppAccent,\n} from '@mistriwork/engine/shared/themePresets.js';\nimport { COLOR_ROLES } from './themeBuild.js';\nimport { colorPath, themeSpecOf } from './themeSpec.js';\n\nconst ROLES = new Set<string>(COLOR_ROLES.map(([, role]) => role));\n\n/* ---------- chart series apart for color-blind readers ---------- */\n\n/* The measure lives beside the looks (engine chartSeparation.ts), which order their own series by it. */\nexport { chartPairFailure, chartsApart, linear, oklab } from '@mistriwork/engine/shared/chartSeparation.js';\nimport { chartPairFailure } from '@mistriwork/engine/shared/chartSeparation.js';\n\nconst HEX = /^#[0-9a-f]{6}$/i;\nconst SERIES = [1, 2, 3, 4, 5, 6, 7, 8].map((n) => `chart${n}` as keyof ThemePalette);\n\n/** Each adjacent pair of series one of `authored` names (a built-in's own pairs are its own). */\nfunction chartFindings(\n theme: ThemeDefinition,\n own: { accent?: AppAccent },\n authored: (paletteId: string, mode: 'light' | 'dark', role: keyof ThemePalette) => boolean,\n): ThemeFinding[] {\n const findings: ThemeFinding[] = [];\n for (const paletteId of ['default', ...(theme.palettes ?? []).map((item) => item.id)])\n for (const mode of themeModes(theme)) {\n const palette = effectiveThemePalette(effectiveThemeDefinition(theme, paletteId)[mode], mode === 'dark', own);\n const name =\n paletteId === 'default' ? theme.label : `${theme.label} / ${theme.palettes!.find((item) => item.id === paletteId)!.label}`;\n for (let index = 0; index < SERIES.length - 1; index++) {\n const left = SERIES[index]!,\n right = SERIES[index + 1]!;\n const a = palette[left],\n b = palette[right];\n if (!a || !b || !HEX.test(a) || !HEX.test(b)) continue;\n if (!authored(paletteId, mode, left) && !authored(paletteId, mode, right)) continue;\n const token = authored(paletteId, mode, right) ? right : left;\n const failed = chartPairFailure(a, b);\n if (!failed) continue;\n const { vision, distance, needs } = failed;\n findings.push({\n themeId: theme.id,\n paletteId,\n mode,\n token,\n check: `${left} beside ${right} for ${vision}`,\n ratio: distance,\n needs,\n message: `Theme \"${name}\" (${mode}): ${colorPath(left)} and ${colorPath(right)} are ${distance.toFixed(1)} apart for ${vision}; adjacent chart series need ${needs}.`,\n });\n }\n }\n return findings;\n}\n\n/* ---------- the colours an author wrote, as written ---------- */\n\n/** A colour measured as the author wrote it, and what the page wears in its place. */\ntype WrittenFinding = ThemeFinding & { written?: string; wornAs?: string };\n\n/** The inks a page wears as the accent's text ink when they are written the same as the accent (themePresets.ts\n effectiveThemePalette): a link, the ring, the menu's current mark, the first series. */\nconst FOLLOWERS = ['link', 'focus', 'navAccent', 'chart1'] as const;\n\n/**\n * An ink the author wrote that the page does not wear as written — a link, a ring, a menu mark or a first series\n * written the same as the accent is drawn in the accent's text ink instead — held to its role's pairs as written:\n * a value in the spec is one the reader sees or one check refuses, never one dropped in silence. A link written as an\n * amber accent read 2.0:1 and passed (T-3). Only a theme whose spec is at hand (compileAppTheme) is read; an App's\n * own `ui.theme.accent` replaces these on purpose and is checked as the accent.\n */\nfunction writtenFindings(theme: ThemeDefinition, own: { accent?: AppAccent }): WrittenFinding[] {\n const raw = themeSpecOf(theme)?.raw as\n | { colors?: Record<string, unknown>; palettes?: ReadonlyArray<Record<string, unknown>> }\n | undefined;\n if (!raw || own.accent) return [];\n const findings: WrittenFinding[] = [];\n const darkOnly = theme.scheme === 'dark-only';\n for (const [index, paletteId] of ['default', ...(theme.palettes ?? []).map((item) => item.id)].entries())\n for (const mode of themeModes(theme)) {\n const source = (index === 0 ? raw.colors : raw.palettes?.[index - 1])?.[darkOnly ? 'dark' : mode] as\n | Record<string, unknown>\n | undefined;\n if (!source) continue;\n const worn = withInks(effectiveThemePalette(effectiveThemeDefinition(theme, paletteId)[mode], mode === 'dark', own) as ThemePalette);\n const name = index === 0 ? theme.label : `${theme.label} / ${theme.palettes![index - 1]!.label}`;\n for (const role of FOLLOWERS) {\n const path = colorPath(role);\n const group = /^(\\w+)\\.(\\w+)$/.exec(path);\n const said =\n role === 'chart1'\n ? (source.chart as unknown[] | undefined)?.[0]\n : group\n ? (source[group[1]!] as Record<string, unknown> | undefined)?.[group[2]!]\n : source[role];\n if (typeof said !== 'string' || !HEX.test(said.trim()) || said.trim().toLowerCase() === worn[role]?.toLowerCase()) continue;\n const written = said.trim().toLowerCase();\n for (const pair of rolePairs(worn, role, written)) {\n if (pair.ratio >= pair.needs) continue;\n findings.push({\n themeId: theme.id,\n paletteId,\n mode,\n token: role,\n check: `${role} as written on ${pair.background}${pair.what ? ` (${pair.what})` : ''}`,\n ratio: pair.ratio,\n needs: pair.needs,\n written,\n wornAs: worn[role],\n message: `Theme \"${name}\" (${mode}): ${path} as written is ${written}, ${pair.ratio.toFixed(2)}:1 on ${colorPath(pair.background)}; it needs ${pair.needs}:1.`,\n });\n }\n }\n }\n return findings;\n}\n\n/* ---------- the validator ---------- */\n\nexport interface ThemeProblem {\n themeId: string;\n paletteId: string;\n mode: 'light' | 'dark';\n /** The color by its name in the spec: `accent`, `status.critical`, `chart[2]`, `nav.muted`. */\n token: string;\n /** The same color by its role (`accent`, `negative`, `chart3`, `navMuted`): what themeFix.ts varies. */\n role: keyof ThemePalette;\n /** What was measured, by roles: `accent/accentSoft over surface`, `chart2 beside chart3 for deuteranopia`. */\n check: string;\n ratio: number;\n needs: number;\n /** What failed, said by the check: \"accent on surface 3.10:1, needs 4.5:1\". */\n reason: string;\n /** The colour as the author wrote it, where the page wears another in its place (a link written as the accent). */\n written?: string;\n /** The whole sentence, naming theme, mode, token and reason. */\n message: string;\n}\n\n/**\n * Every failure of a theme: contrast of text and controls in light and dark\n * and every palette, and its chart's color-blind separation. `authored` says\n * which colors the author set, so a series kept from a built-in is not held\n * against the author; omitted, every color is the author's.\n */\nexport function checkTheme(\n theme: ThemeDefinition,\n options: {\n own?: { accent?: AppAccent };\n authored?: (paletteId: string, mode: 'light' | 'dark', role: keyof ThemePalette) => boolean;\n } = {},\n): ThemeProblem[] {\n const own = options.own ?? {};\n const authored = options.authored ?? (() => true);\n const all: WrittenFinding[] = [...themeFindings([theme], own), ...chartFindings(theme, own, authored), ...writtenFindings(theme, own)];\n return all.map((finding) => {\n const name =\n finding.paletteId === 'default'\n ? theme.label\n : `${theme.label} / ${theme.palettes!.find((item) => item.id === finding.paletteId)!.label}`;\n const check = finding.check.replace(/\\b[a-z][a-zA-Z0-9]*\\b/g, (word) => (ROLES.has(word) ? colorPath(word) : word));\n const reason = check.includes(' for ')\n ? `${check} are ${finding.ratio.toFixed(1)} apart; adjacent series need ${finding.needs}`\n : `${check} is ${finding.ratio.toFixed(2)}:1; it needs ${finding.needs}:1`;\n return {\n themeId: finding.themeId,\n paletteId: finding.paletteId,\n mode: finding.mode,\n token: colorPath(finding.token),\n role: finding.token,\n check: finding.check,\n ratio: finding.ratio,\n needs: finding.needs,\n reason,\n ...(finding.written ? { written: finding.written } : {}),\n message: `Theme \"${name}\" (${finding.mode}), ${colorPath(finding.token)}: ${reason}.`,\n };\n });\n}\n\n/** The roles a compiled theme takes from its author rather than the built-in it extends. */\nexport function authoredRoles(theme: ThemeDefinition, base: ThemeDefinition | undefined) {\n return (paletteId: string, mode: 'light' | 'dark', role: keyof ThemePalette): boolean => {\n if (!base) return true;\n const ours = effectiveThemeDefinition(theme, paletteId)[mode][role];\n const theirs = effectiveThemeDefinition(base, base.palettes?.some((item) => item.id === paletteId) ? paletteId : 'default')[mode][role];\n return ours !== theirs;\n };\n}\n","/**\n * A tenant's branding: what an App lets each of its customers say about how the\n * App looks to their people — a name, a logo, an accent, one of the App's\n * palettes or themes, a corner and a density — and never the grounds, the ink\n * or the recipes. The App declares the scope (`branding: { themes, allow }`);\n * a tenant admin saves a value in it; the server keeps it in the platform's\n * branding store and dresses that tenant's pages with it from the first paint.\n *\n * Shared by the server (which parses, validates and serves) and the Branding\n * block (which previews and says the refusal before the save does). The one\n * validator is themeCheck.ts; this file turns its findings into the fixed\n * sentences a person can act on.\n */\nimport type { ThemeDefinition, ThemeDensity, ThemePalette } from '@mistriwork/engine/schema/theme.js';\nimport { chartsApart, checkTheme, type ThemeProblem } from './themeCheck.js';\nimport { mixHex } from './themeBuild.js';\nimport { DENSITIES } from '@mistriwork/engine/shared/themeRecipes.js';\nimport { effectiveThemeDefinition, themePreset } from '@mistriwork/engine/shared/themePresets.js';\nimport { compileAppTheme } from './themeSpec.js';\nimport type { AppAuth, AppUi } from './appModel.js';\nimport { canonicalLocale } from '@mistriwork/engine/shared/appLocale.js';\nimport { audienceThemes as audienceThemeIds, resolveTheme, type ThemeAudiences } from '@mistriwork/engine/shared/themeResolve.js';\n\n/** What a tenant may change, when the App allows it. `preset` (the theme it starts from) needs more than one theme to\n * pick from, and names it as `ui.theme.preset` does. */\nconst BRANDING_KEYS = ['logo', 'name', 'accent', 'palette', 'radius', 'density', 'preset', 'locale'] as const;\nexport type BrandingKey = (typeof BRANDING_KEYS)[number];\n\n/** The App's declaration, parsed: its themes compiled (the first is the tenants' default), and the keys a tenant may set. */\nexport interface BrandingDeclaration {\n readonly themes: readonly ThemeDefinition[];\n readonly allow: readonly BrandingKey[];\n /** The tenant roles whose members may save it: a tenant's admins, by the App's own names for them. */\n readonly editors: readonly string[];\n /** `ui.theme.for`, and each theme it dresses an audience in, by id: a role's theme wins over the tenant's pick\n (themeResolve.ts), and the tenant's accent, name, logo, corner and density lay over it as over its own. */\n readonly audiences?: ThemeAudiences;\n readonly audienceThemes?: readonly ThemeDefinition[];\n}\n\n/** A tenant's saved branding. Every field is optional; an absent one is the App's. */\nexport interface TenantBrandingValue {\n /** The name the tenant's people see in the shell, the title and email. */\n readonly name?: string;\n /** The logo's content hash and served type (brandingLogo.ts); the bytes live in the branding store. */\n readonly logo?: { readonly hash: string; readonly type: string };\n /** The logo drawn on a dark bar or by night, where the light one would not read. Allowed with `logo`; the light logo\n stands where this is none. */\n readonly logoDark?: { readonly hash: string; readonly type: string };\n /** One color for both grounds (mixed toward white on the dark one), or one for each. */\n readonly accent?: { readonly light: string; readonly dark: string };\n /** One of the theme's palettes. */\n readonly palette?: string;\n /** A control's corner, 0px to 8px. */\n readonly radius?: string;\n readonly density?: ThemeDensity;\n /** One of `branding.themes`, by id. */\n readonly preset?: string;\n /** The tenant's language and figures for its people whose own row names none (shared/appLocale.ts `resolveLocale`). */\n readonly locale?: string;\n}\n\nconst HEX = /^#[0-9a-f]{6}$/;\nconst isPlain = (raw: unknown): raw is Record<string, unknown> =>\n Boolean(raw) && typeof raw === 'object' && !Array.isArray(raw) && [Object.prototype, null].includes(Object.getPrototypeOf(raw));\n\n/**\n * The App's `branding` block, checked against the App it sits in: a tenant App\n * (`auth.rows: 'tenant'`), keys from the closed list, editors that are tenant\n * roles, and themes written with defineTheme or named as built-ins. Without\n * `themes` the tenants' one theme is the App's own look, its accent included.\n */\nexport function parseBrandingDeclaration(raw: unknown, app: { ui: AppUi; auth?: AppAuth }, where: string): BrandingDeclaration {\n const at = `${where}: branding`;\n if (!isPlain(raw)) throw new Error(`${at} must be an object: { allow: [...], editors: [...], themes?: [...] }.`);\n for (const key of Object.keys(raw))\n if (!['allow', 'editors', 'themes'].includes(key))\n throw new Error(`${at} has an unsupported field \"${key}\". It takes allow, editors and themes.`);\n if (app.auth?.rows !== 'tenant')\n throw new Error(`${at}: branding is chosen per tenant, so it needs a tenant App (auth.rows: 'tenant'). An App's own look is ui.theme.`);\n const custom = app.ui.customThemes ?? [];\n const taken = new Set(custom.map((theme) => theme.id));\n let themes: ThemeDefinition[];\n if (raw.themes === undefined) themes = [ownLook(app.ui)];\n else {\n if (!Array.isArray(raw.themes) || !raw.themes.length || raw.themes.length > 6)\n throw new Error(`${at}.themes must list one to six themes: defineTheme(...) results or built-in names.`);\n themes = raw.themes.map((spec: unknown, index: number) => {\n const here = `${at}.themes[${index}]`;\n if (typeof spec === 'string') {\n const preset = themePreset(spec);\n if (!preset) throw new Error(`${here} \"${spec}\" is not a built-in theme.`);\n return preset;\n }\n if (!isPlain(spec) || spec.kind !== 'theme')\n throw new Error(`${here} must be a theme written with defineTheme(...) or a built-in's name.`);\n /* The App's own theme, offered to its tenants too, is the one ui compiled, not a second copy. */\n const same = custom.find((theme) => theme.id === spec.id);\n if (same) return same;\n const theme = compileAppTheme(spec, here, taken);\n taken.add(theme.id);\n return theme;\n });\n if (new Set(themes.map((theme) => theme.id)).size !== themes.length) throw new Error(`${at}.themes names a theme twice.`);\n }\n if (!Array.isArray(raw.allow) || !raw.allow.length)\n throw new Error(`${at}.allow must list what a tenant may change: ${BRANDING_KEYS.join(', ')}.`);\n const allow: BrandingKey[] = [];\n for (const key of raw.allow) {\n if (key === 'theme') throw new Error(`${at}.allow has \"theme\", which is now \"preset\": which of branding.themes a tenant starts from.`);\n if (typeof key !== 'string' || !(BRANDING_KEYS as readonly string[]).includes(key))\n throw new Error(\n `${at}.allow has \"${String(key)}\"; a tenant may change only ${BRANDING_KEYS.join(', ')}. Grounds, ink and recipes stay the App's.`,\n );\n if (allow.includes(key as BrandingKey)) throw new Error(`${at}.allow names \"${key}\" twice.`);\n allow.push(key as BrandingKey);\n }\n if (allow.includes('preset') && themes.length < 2)\n throw new Error(`${at}.allow has \"preset\", which needs more than one theme in branding.themes to pick from.`);\n if (allow.includes('palette') && !themes.some((theme) => theme.palettes?.length))\n throw new Error(`${at}.allow has \"palette\", and no theme offered has a palette to pick.`);\n const roles = app.auth?.roles ?? {};\n if (!Array.isArray(raw.editors) || !raw.editors.length)\n throw new Error(`${at}.editors must name the tenant roles whose members may change it (a tenant's admins).`);\n const editors: string[] = [];\n for (const role of raw.editors) {\n const declared = typeof role === 'string' ? (roles as Record<string, { realm?: string }>)[role] : undefined;\n /* A role names no realm when it is the App's own, a tenant's; a platform-realm role is the provider's staff. */\n if (!declared || (declared.realm !== undefined && declared.realm !== 'tenant'))\n throw new Error(`${at}.editors has \"${String(role)}\", which is not a tenant role of auth.roles.`);\n if (!editors.includes(role as string)) editors.push(role as string);\n }\n const audiences = app.ui.theme?.for;\n const audienceThemes = audienceThemeIds(audiences).flatMap((id) => {\n const theme = custom.find((item) => item.id === id) ?? themePreset(id);\n return theme ? [theme] : [];\n });\n return Object.freeze({\n themes: Object.freeze(themes),\n allow: Object.freeze(allow),\n editors: Object.freeze(editors),\n ...(audiences ? { audiences, audienceThemes: Object.freeze(audienceThemes) } : {}),\n });\n}\n\n/**\n * A tenant's branding as one audience wears it: where `ui.theme.for` dresses a signed-out visitor or one of the\n * account's roles (themeResolve.ts), that theme replaces the tenant's pick and its palette, and the tenant's accent,\n * name, logo, corner and density lay over it; otherwise the tenant's own, as saved.\n */\nexport function audienceBranding(\n declaration: BrandingDeclaration,\n value: TenantBrandingValue,\n audience: { public?: boolean; roles?: readonly string[] },\n): { declaration: BrandingDeclaration; value: TenantBrandingValue } {\n const themes = declaration.audienceThemes ?? [];\n const worn = resolveTheme({ ...audience, for: declaration.audiences, offered: (id) => themes.some((theme) => theme.id === id) });\n if (worn.source !== 'public' && worn.source !== 'role') return { declaration, value };\n const { palette: _palette, ...rest } = value;\n return { declaration: { ...declaration, themes: themes.filter((theme) => theme.id === worn.id) }, value: { ...rest, preset: worn.id } };\n}\n\n/** The App's look as one theme: its preset or its own theme, with its accent laid over as a tenant's would be. */\nfunction ownLook(ui: AppUi): ThemeDefinition {\n const preset = ui.theme?.preset;\n const base = ui.customThemes?.find((theme) => theme.id === preset) ?? themePreset(preset) ?? themePreset('workshop')!;\n const accent = ui.theme?.accent;\n const value: TenantBrandingValue = {\n ...(ui.theme?.palette && ui.theme.palette !== 'default' ? { palette: ui.theme.palette } : {}),\n ...(typeof accent === 'string' && HEX.test(accent.toLowerCase())\n ? { accent: { light: accent.toLowerCase(), dark: mixHex(accent.toLowerCase(), '#ffffff', 0.62) } }\n : typeof accent === 'object' && HEX.test(accent.light.toLowerCase()) && HEX.test(accent.dark.toLowerCase())\n ? { accent: { light: accent.light.toLowerCase(), dark: accent.dark.toLowerCase() } }\n : {}),\n };\n if (!value.accent && !value.palette) return base;\n const theme = tenantTheme({ themes: [base], allow: [], editors: [] }, value);\n return { ...theme, id: base.id, label: base.label };\n}\nconst RADIUS = /^(?:[0-7]|8)px$/;\nconst SLUG = /^[a-z][a-z0-9-]{0,47}$/;\n\n/** Why a save was refused: the field and a sentence that says what to do. */\nexport interface BrandingRefusal {\n readonly field: BrandingKey;\n readonly message: string;\n}\n\nexport class BrandingRefused extends Error {\n readonly refusals: readonly BrandingRefusal[];\n constructor(refusals: readonly BrandingRefusal[]) {\n super(refusals.map((refusal) => refusal.message).join(' '));\n this.name = 'BrandingRefused';\n this.refusals = refusals;\n }\n}\n\nfunction plain(raw: unknown): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || ![Object.prototype, null].includes(Object.getPrototypeOf(raw)))\n throw new BrandingRefused([{ field: 'name', message: 'Branding is a set of named fields.' }]);\n return raw as Record<string, unknown>;\n}\n\n/**\n * A tenant's value checked field by field against the App's declaration: a key\n * the App does not allow, or a value outside its type, is refused with the field\n * named. Colours are lowercase six-digit hex; nothing here reaches CSS before it\n * has matched one of these patterns.\n */\nexport function parseTenantBranding(raw: unknown, declaration: BrandingDeclaration): TenantBrandingValue {\n const input = plain(raw);\n const refusals: BrandingRefusal[] = [];\n const allowed = new Set<string>(declaration.allow);\n const out: { -readonly [K in keyof TenantBrandingValue]: TenantBrandingValue[K] } = {};\n for (const [key, value] of Object.entries(input)) {\n if (value === undefined || value === null) continue;\n if (\n !(BRANDING_KEYS as readonly string[]).includes(key === 'logoDark' ? 'logo' : key) ||\n !allowed.has(key === 'logoDark' ? 'logo' : key)\n ) {\n refusals.push({ field: 'name', message: `This App does not let a customer change \"${key}\".` });\n continue;\n }\n switch (key as BrandingKey | 'logoDark') {\n case 'name':\n // eslint-disable-next-line no-control-regex -- a name is one line of text\n if (typeof value !== 'string' || !value.trim() || value.trim().length > 80 || /[\\u0000-\\u001f\\u007f<>]/.test(value))\n refusals.push({ field: 'name', message: 'The name must be one line of at most 80 characters, without < or >.' });\n else out.name = value.trim();\n break;\n case 'logo':\n case 'logoDark': {\n const logo = typeof value === 'object' ? (value as Record<string, unknown>) : {};\n if (\n typeof logo.hash !== 'string' ||\n !/^[0-9a-f]{64}$/.test(logo.hash) ||\n typeof logo.type !== 'string' ||\n !/^image\\/(png|jpeg|webp|gif|svg\\+xml)$/.test(logo.type)\n )\n refusals.push({\n field: 'logo',\n message:\n key === 'logo'\n ? 'Upload the logo again: the saved one was not recognized.'\n : 'Upload the dark logo again: the saved one was not recognized.',\n });\n else out[key as 'logo' | 'logoDark'] = { hash: logo.hash, type: logo.type };\n break;\n }\n case 'accent': {\n /* One color is for the light ground; the dark one is derived from it once the theme it will stand in is\n known, below. */\n const one = typeof value === 'string' ? value.trim().toLowerCase() : '';\n const pair = HEX.test(one) ? { light: one, dark: one } : typeof value === 'object' ? (value as Record<string, unknown>) : {};\n const light = typeof pair.light === 'string' ? pair.light.trim().toLowerCase() : '';\n const dark = typeof pair.dark === 'string' ? pair.dark.trim().toLowerCase() : '';\n if (!HEX.test(light) || !HEX.test(dark)) refusals.push({ field: 'accent', message: 'The accent must be a color like #1d4ed8.' });\n else out.accent = { light, dark };\n break;\n }\n case 'palette':\n if (typeof value !== 'string' || !SLUG.test(value))\n refusals.push({ field: 'palette', message: 'Pick one of the palettes offered.' });\n else out.palette = value;\n break;\n case 'radius':\n if (typeof value !== 'string' || !RADIUS.test(value))\n refusals.push({ field: 'radius', message: 'The corner must be between 0px and 8px.' });\n else out.radius = value;\n break;\n case 'density':\n if (!(DENSITIES as readonly unknown[]).includes(value))\n refusals.push({ field: 'density', message: 'Pick compact, comfortable or spacious.' });\n else out.density = value as ThemeDensity;\n break;\n case 'preset':\n if (typeof value !== 'string' || !declaration.themes.some((theme) => theme.id === value))\n refusals.push({ field: 'preset', message: 'Pick one of the themes offered.' });\n else out.preset = value;\n break;\n case 'locale': {\n const tag = canonicalLocale(value);\n if (!tag) refusals.push({ field: 'locale', message: 'Pick a language like \"en-US\" or \"es-MX\".' });\n else out.locale = tag;\n break;\n }\n }\n }\n if (out.accent && typeof input.accent === 'string') out.accent = accentFor(declaration, out, out.accent.light);\n if (out.palette !== undefined) {\n const theme = themeOf(declaration, out.preset);\n if (out.palette !== 'default' && !theme.palettes?.some((palette) => palette.id === out.palette))\n refusals.push({ field: 'palette', message: 'Pick one of the palettes offered.' });\n }\n if (refusals.length) throw new BrandingRefused(refusals);\n return out;\n}\n\nfunction themeOf(declaration: BrandingDeclaration, id: string | undefined): ThemeDefinition {\n return declaration.themes.find((theme) => theme.id === id) ?? declaration.themes[0]!;\n}\n\n/** The roles an accent stands for where the theme already wore its accent: the link and the menu's current room. */\nconst FOLLOWS_ACCENT = ['link', 'navAccent'] as const;\n\n/**\n * The theme a tenant's people wear: the App's chosen theme and palette, with the\n * tenant's accent laid over the roles that were that accent, and its corner and\n * density. Grounds, ink, lines and recipes are the App's and stay so.\n */\nexport function tenantTheme(declaration: BrandingDeclaration, value: TenantBrandingValue): ThemeDefinition {\n const chosen = themeOf(declaration, value.preset);\n const base = effectiveThemeDefinition(chosen, value.palette ?? chosen.defaultPalette);\n const mode = (name: 'light' | 'dark'): ThemePalette => {\n const palette = { ...base[name] };\n const accent = value.accent?.[name];\n if (!accent) return palette;\n for (const role of FOLLOWS_ACCENT) if (palette[role] === palette.accent) palette[role] = accent;\n /* The first chart series is the brand's, whatever the theme drew it in: Lumen's navy accent beside its blue\n first series left a tenant's teal on the bar and its charts blue, and the form says the accent colors them.\n Unless that would set it beside the second series for a color-blind reader: then the theme's own first\n series stays, as it did, and an accent that was already the first series is still held to the check. */\n if (palette.chart1 === palette.accent || !palette.chart2 || chartsApart(accent, palette.chart2)) palette.chart1 = accent;\n palette.accent = accent;\n /* The words a look chose for its own accent fill (Workshop's ink on its orange) read on that fill, not this one:\n they are derived again for the tenant's, as for an App's accent (themePresets.ts, overlayPalette). */\n delete palette.onAccent;\n /* A tenant picks one color, and it must read as text on the page: `accentInk` said, so the check holds the pick\n to it where an App's theme, which names its inks, is given the one that reads. */\n palette.accentInk = accent;\n palette.focus = accent;\n /* The selected item's tint: as faint as the built-ins draw theirs (Lumen's is about 8% on white), so the accent still reads on it. */\n palette.accentSoft = mixHex(accent, palette.surface, name === 'light' ? 0.08 : 0.2);\n return palette;\n };\n const { palettes: _palettes, defaultPalette: _default, ...rest } = base;\n return {\n ...rest,\n id: 'tenant',\n label: value.name ?? chosen.label,\n light: mode('light'),\n dark: mode('dark'),\n ...(value.radius ? { radius: { control: value.radius, field: value.radius } } : {}),\n ...(value.density ? { density: value.density } : {}),\n };\n}\n\n/**\n * The one validator over the theme a tenant would wear, holding against the\n * tenant only the colors its accent changed, said in a fixed sentence per\n * kind of failure — the person saving picked a color, not a token.\n */\nexport function brandingProblems(declaration: BrandingDeclaration, value: TenantBrandingValue): BrandingRefusal[] {\n const problems = wornFindings(declaration, value);\n const seen = new Set<string>();\n const refusals: BrandingRefusal[] = [];\n for (const [problem, worn] of problems) {\n const message = worn ? sentence(problem).replace(/\\.$/, ` in ${worn}, which some of your people wear.`) : sentence(problem);\n if (seen.has(message)) continue;\n seen.add(message);\n refusals.push({ field: 'accent', message });\n }\n return refusals;\n}\n\n/** The findings on every theme the tenant's accent is worn over: its own pick, and each theme an audience wears\n (ui.theme.for), which the tenant did not pick and so is named beside a failure there. */\nfunction wornFindings(declaration: BrandingDeclaration, value: TenantBrandingValue): (readonly [ThemeProblem, string])[] {\n const { palette: _palette, ...over } = value;\n return [\n ...accentFindings(declaration, value).map((problem) => [problem, ''] as const),\n ...(declaration.audienceThemes ?? [])\n .filter((theme) => theme.id !== themeOf(declaration, value.preset).id)\n .flatMap((theme) =>\n accentFindings({ ...declaration, themes: [theme] }, { ...over, preset: theme.id }).map(\n (problem) => [problem, theme.label] as const,\n ),\n ),\n ];\n}\n\n/** The validator's findings against the colors the tenant's accent changed. */\nfunction accentFindings(declaration: BrandingDeclaration, value: TenantBrandingValue): ThemeProblem[] {\n if (!value.accent) return [];\n const chosen = themeOf(declaration, value.preset);\n const before = effectiveThemeDefinition(chosen, value.palette ?? chosen.defaultPalette);\n const after = tenantTheme(declaration, value);\n const changed = (_palette: string, mode: 'light' | 'dark', role: keyof ThemePalette) => before[mode][role] !== after[mode][role];\n return checkTheme({ ...after, palettes: undefined }, { authored: changed });\n}\n\n/**\n * One brand color for both grounds. The dark theme's accent starts where an\n * App's own does, the color mixed 62% toward white, and where that still reads\n * too dark there — a deep brand blue such as #1d4ed8 on its own tint, in Lumen\n * or Sage by night — it is lifted in OKLCH lightness, its hue and as much of\n * its chroma as sRGB holds kept, until the validator passes it: the brand\n * stays recognisably the brand rather than the save being refused. A color no\n * lift makes readable keeps the plain mix, and the validator says why.\n */\nexport function accentFor(\n declaration: BrandingDeclaration,\n value: TenantBrandingValue,\n light: string,\n): { readonly light: string; readonly dark: string } {\n const start = mixHex(light, '#ffffff', 0.62);\n const darkOk = (dark: string) => !wornFindings(declaration, { ...value, accent: { light, dark } }).some(([p]) => p.mode === 'dark');\n if (darkOk(start)) return { light, dark: start };\n const [l, c, h] = oklch(start);\n for (let lift = l + 0.02; lift <= 0.96; lift += 0.02) {\n const dark = fromOklch(lift, c, h);\n if (darkOk(dark)) return { light, dark };\n }\n return { light, dark: start };\n}\n\n/* OKLab (Björn Ottosson), polar: lightness 0 to 1, chroma, hue in radians. */\nfunction oklch(hex: string): [number, number, number] {\n const [r, g, b] = [1, 3, 5].map((at) => {\n const v = parseInt(hex.slice(at, at + 2), 16) / 255;\n return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n const lms = [\n Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b),\n Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b),\n Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b),\n ] as const;\n const L = 0.2104542553 * lms[0] + 0.793617785 * lms[1] - 0.0040720468 * lms[2];\n const A = 1.9779984951 * lms[0] - 2.428592205 * lms[1] + 0.4505937099 * lms[2];\n const B = 0.0259040371 * lms[0] + 0.7827717662 * lms[1] - 0.808675766 * lms[2];\n return [L, Math.hypot(A, B), Math.atan2(B, A)];\n}\n\n/** A lightness, chroma and hue as hex, the chroma eased down until the color is one sRGB can show. */\nfunction fromOklch(l: number, c: number, h: number): string {\n for (let chroma = c; ; chroma = Math.max(0, chroma - 0.005)) {\n const a = chroma * Math.cos(h),\n b = chroma * Math.sin(h);\n const lms = [\n l + 0.3963377774 * a + 0.2158037573 * b,\n l - 0.1055613458 * a - 0.0638541728 * b,\n l - 0.0894841775 * a - 1.291485548 * b,\n ].map((v) => v ** 3) as [number, number, number];\n const rgb = [\n 4.0767416621 * lms[0] - 3.3077115913 * lms[1] + 0.2309699292 * lms[2],\n -1.2684380046 * lms[0] + 2.6097574011 * lms[1] - 0.3413193965 * lms[2],\n -0.0041960863 * lms[0] - 0.7034186147 * lms[1] + 1.707614701 * lms[2],\n ];\n if (chroma === 0 || rgb.every((v) => v >= -1e-4 && v <= 1 + 1e-4))\n return `#${rgb\n .map((v) => {\n const x = Math.min(1, Math.max(0, v));\n return Math.round((x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055) * 255)\n .toString(16)\n .padStart(2, '0');\n })\n .join('')}`;\n }\n}\n\n/** A finding as the person who picked the color would say it. */\nfunction sentence(problem: ThemeProblem): string {\n const light = problem.mode === 'light';\n const ground = light ? 'white' : 'the dark background';\n if (problem.token.startsWith('chart'))\n return `This accent is too close to the next chart color for color-blind readers in the ${problem.mode} theme. Pick a color further from it.`;\n if (problem.token === 'nav.accent') return `This accent is too ${light ? 'light' : 'dark'} on the menu in the ${problem.mode} theme.`;\n if (problem.token === 'focus' || /focus ring/.test(problem.reason))\n return `This accent is too ${light ? 'light' : 'dark'} on ${ground} for the keyboard focus ring.`;\n if (/selected tab|accentSoft/.test(problem.reason))\n return `This accent is too ${light ? 'light' : 'dark'} to read on its own tint (a selected item) in the ${problem.mode} theme.`;\n if (problem.token === 'link') return `This accent is too ${light ? 'light' : 'dark'} on ${ground} for links.`;\n return `This accent is too ${light ? 'light' : 'dark'} on ${ground} for buttons.`;\n}\n\n/** Everything a save must clear: the value's shape, then the validator. Throws BrandingRefused. */\nexport function validateTenantBranding(raw: unknown, declaration: BrandingDeclaration): TenantBrandingValue {\n const value = parseTenantBranding(raw, declaration);\n const problems = brandingProblems(declaration, value);\n if (problems.length) throw new BrandingRefused(problems);\n return value;\n}\n","/** What BrandingForm says, in the dashboards room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '@mistriwork/kit/client/messages.js';\n\nexport const say = defineMessages({\n loading: 'Loading branding…',\n couldNotLoad: 'Branding could not be loaded.',\n readOnly: \"Only this organization's admins can change its branding.\",\n name: 'Name',\n nameHint: 'What your people see in the bar, the tab and email.',\n logo: 'Logo',\n logoHint: 'PNG, JPEG, WebP or GIF up to 256 KB, or an SVG up to 64 KB with its text as outlines. Drawn 96 by 24 pixels.',\n chooseLogo: 'Choose a file',\n removeLogo: 'Remove',\n noLogo: 'No logo: the name stands alone.',\n darkLogo: 'For a dark bar',\n darkLogoHint: 'Optional. Drawn on a dark bar and by night; the logo above stands where there is none.',\n noDarkLogo: 'None: the logo above is used.',\n accent: 'Accent',\n accentHint: 'Buttons, links, the current page and the first chart series.',\n accentColor: 'Accent color',\n palette: 'Palette',\n theme: 'Theme',\n corner: 'Corners',\n square: 'Square',\n density: 'Density',\n compact: 'Compact',\n comfortable: 'Comfortable',\n spacious: 'Spacious',\n appDefault: 'The App’s',\n save: 'Save',\n saving: 'Saving…',\n reset: 'Undo changes',\n saved: 'Saved. Your people see it on their next page.',\n couldNotSave: 'The branding could not be saved.',\n preview: 'Preview',\n previewShell: 'The bar',\n previewRecord: 'A record',\n previewStat: 'A figure',\n previewEmail: 'An email',\n sampleRecord: 'Acme Corporation',\n sampleStatus: 'Active',\n sampleAction: 'Assign',\n sampleStat: 'Open requests',\n sampleNav: 'Requests',\n sampleOther: 'Accounts',\n sampleEmailLine: 'Your request #1042 has an update.',\n sampleEmailButton: 'View request',\n});\n","/**\n * A tenant's own Branding form: what the App's `branding.allow` lets its admins\n * change, beside a live preview of what their people will see — the bar, a\n * record's head, a figure and an email's header. The preview and the refusals\n * are the save's own (shared/tenantBranding.ts): what this form says before\n * Save is what the server would say after it. Members who are not the App's\n * `branding.editors` see the same form, read-only.\n *\n * Every color reaches the page through a custom property set from a value\n * that already matched `#rrggbb`; a logo is drawn from its own object URL\n * while unsaved, and from the App's logo address once saved.\n */\nimport { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent } from 'react';\nimport type { ThemeDefinition, ThemeDensity } from '@mistriwork/engine/schema/theme.js';\nimport {\n accentFor,\n brandingProblems,\n tenantTheme,\n type BrandingKey,\n type BrandingRefusal,\n type TenantBrandingValue,\n} from '../../shared/tenantBranding.js';\nimport { api } from '@mistriwork/kit/client/core.js';\nimport { served } from '@mistriwork/kit/client/addressMount.js';\nimport { activeApp } from '../appState.js';\nimport { Button } from '@mistriwork/ui/Button.js';\nimport { Dropdown } from '@mistriwork/kit/client/components/Dropdown.js';\nimport { Field } from '@mistriwork/ui/Field.js';\nimport './BrandingForm.css';\nimport { say } from '../copy/dashboards/components/BrandingForm.js';\nimport { shellRoot } from '@mistriwork/kit/client/shellHost.js';\n\ninterface BrandingReply {\n branding: TenantBrandingValue;\n revision: number;\n editor: boolean;\n allow: BrandingKey[];\n themes: ThemeDefinition[];\n error?: string;\n errors?: BrandingRefusal[];\n}\n\nconst HEX = /^#[0-9a-f]{6}$/;\nconst RADII = ['0px', '2px', '4px', '6px', '8px'];\nconst LOGO_ADDRESS = /^\\/api\\/branding\\/logo\\/[0-9a-f]{64}$/;\n\n/** The draft as the save takes it: a key the form leaves empty is the App's. */\ntype Draft = { -readonly [K in keyof TenantBrandingValue]?: TenantBrandingValue[K] };\n\nfunction dark(): boolean {\n const root = shellRoot().dataset.theme;\n return root === 'dark' || (!root && window.matchMedia?.('(prefers-color-scheme: dark)').matches === true);\n}\n\nexport function BrandingForm() {\n const [reply, setReply] = useState<BrandingReply | null>(null);\n const [failed, setFailed] = useState('');\n const [draft, setDraft] = useState<Draft>({});\n /* An upload not yet saved: its bytes for the save, its object URL for the preview; null removes the saved logo. */\n const [upload, setUpload] = useState<{ base64: string; url: string } | null | undefined>(undefined);\n /* The same for the dark logo, which belongs to a logo: removing the logo takes it too. */\n const [darkUpload, setDarkUpload] = useState<{ base64: string; url: string } | null | undefined>(undefined);\n const [phase, setPhase] = useState<'idle' | 'saving'>('idle');\n const [message, setMessage] = useState('');\n const [refused, setRefused] = useState<BrandingRefusal[]>([]);\n const file = useRef<HTMLInputElement>(null);\n const darkFile = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n let alive = true;\n void (async () => {\n try {\n const got = (await api('/api/branding', {})) as BrandingReply;\n if (!alive) return;\n if (got.error) setFailed(got.error);\n else {\n setReply(got);\n setDraft({ ...got.branding });\n }\n } catch (error) {\n if (alive) setFailed(error instanceof Error ? error.message : say('couldNotLoad'));\n }\n })();\n return () => {\n alive = false;\n };\n }, []);\n useEffect(() => () => (upload ? URL.revokeObjectURL(upload.url) : undefined), [upload]);\n useEffect(() => () => (darkUpload ? URL.revokeObjectURL(darkUpload.url) : undefined), [darkUpload]);\n\n const declaration = useMemo(() => (reply ? { themes: reply.themes, allow: reply.allow, editors: [] } : undefined), [reply]);\n /* What the save would judge: the draft without the color a reader is still typing. */\n const candidate = useMemo<TenantBrandingValue>(() => {\n const { accent, ...rest } = draft;\n return accent && HEX.test(accent.light) && HEX.test(accent.dark) ? { ...rest, accent } : rest;\n }, [draft]);\n const theme = useMemo(() => (declaration ? tenantTheme(declaration, candidate) : undefined), [declaration, candidate]);\n const problems = useMemo(() => (declaration ? brandingProblems(declaration, candidate) : []), [declaration, candidate]);\n\n if (failed)\n return (\n <p className=\"branding-form-note\" role=\"alert\">\n {failed}\n </p>\n );\n if (!reply || !declaration || !theme)\n return (\n <p className=\"branding-form-note\" role=\"status\">\n {say('loading')}\n </p>\n );\n\n const allowed = (key: BrandingKey) => reply.allow.includes(key);\n const editor = reply.editor;\n const app = activeApp();\n const name = draft.name?.trim() || app?.name || '';\n const savedLogo = reply.branding.logo ? `/api/branding/logo/${reply.branding.logo.hash}` : undefined;\n const logo = upload === null ? undefined : (upload?.url ?? (savedLogo && LOGO_ADDRESS.test(savedLogo) ? served(savedLogo) : undefined));\n const savedDark = reply.branding.logoDark ? `/api/branding/logo/${reply.branding.logoDark.hash}` : undefined;\n const darkLogo =\n !logo || darkUpload === null\n ? undefined\n : (darkUpload?.url ?? (savedDark && LOGO_ADDRESS.test(savedDark) ? served(savedDark) : undefined));\n const chosen = reply.themes.find((item) => item.id === draft.preset) ?? reply.themes[0]!;\n const palette = theme[dark() ? 'dark' : 'light'];\n const problemFor = (field: BrandingKey) =>\n [...refused.filter((item) => item.field === field), ...(field === 'accent' ? problems : [])].map((item) => item.message)[0] ?? '';\n const set = (patch: Draft) => {\n setDraft((current) => ({ ...current, ...patch }));\n setMessage('');\n setRefused([]);\n };\n const accentText = draft.accent?.light ?? '';\n /* One accent for both grounds, the dark one derived as the save derives it, in the theme the draft wears. */\n const pairFor = (light: string) => accentFor(declaration!, candidate, light);\n const preview = {\n '--bp-ground': palette.ground,\n '--bp-surface': palette.surface,\n '--bp-ink': palette.ink,\n '--bp-muted': palette.inkMuted,\n '--bp-line': palette.line,\n '--bp-accent': palette.accent,\n '--bp-accent-soft': palette.accentSoft,\n '--bp-nav': palette.navGround ?? palette.surface,\n '--bp-nav-ink': palette.navInk ?? palette.ink,\n '--bp-nav-accent': palette.navAccent ?? palette.accent,\n '--bp-chart': palette.chart1 ?? palette.accent,\n '--bp-radius': draft.radius ?? theme.radius?.control ?? '4px',\n } as CSSProperties;\n\n const pick = async (event: ChangeEvent<HTMLInputElement>, into: typeof setUpload) => {\n const chosenFile = event.target.files?.[0];\n event.target.value = '';\n if (!chosenFile) return;\n const bytes = new Uint8Array(await chosenFile.arrayBuffer());\n let binary = '';\n for (let index = 0; index < bytes.length; index += 0x8000) binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));\n into({ base64: btoa(binary), url: URL.createObjectURL(chosenFile) });\n setMessage('');\n setRefused([]);\n };\n const save = async () => {\n if (phase === 'saving' || !editor || problems.length) return;\n setPhase('saving');\n setMessage('');\n try {\n const { logo: _logo, logoDark: _logoDark, ...value } = draft;\n const got = (await api('/api/branding/save', {\n branding:\n value.accent && !(HEX.test(value.accent.light) && HEX.test(value.accent.dark)) ? { ...value, accent: value.accent.light } : value,\n ...(upload === undefined ? {} : { logo: upload === null ? null : upload.base64 }),\n ...(darkUpload === undefined ? {} : { darkLogo: darkUpload === null ? null : darkUpload.base64 }),\n })) as BrandingReply;\n if (got.error) {\n setRefused(got.errors ?? [{ field: 'name', message: got.error }]);\n setMessage(got.error);\n } else {\n setReply(got);\n setDraft({ ...got.branding });\n setUpload(undefined);\n setDarkUpload(undefined);\n setMessage(say('saved'));\n }\n } catch (error) {\n setMessage(error instanceof Error ? error.message : say('couldNotSave'));\n } finally {\n setPhase('idle');\n }\n };\n const disabled = !editor || phase === 'saving';\n\n return (\n <div className=\"branding-form\">\n <form\n className=\"branding-form-fields\"\n onSubmit={(event) => {\n event.preventDefault();\n void save();\n }}\n noValidate\n >\n {!editor && <p className=\"branding-form-note\">{say('readOnly')}</p>}\n {allowed('name') && (\n <Field label={say('name')} hint={say('nameHint')} problem={problemFor('name')} size=\"compact\">\n <input\n className=\"field-control\"\n name=\"name\"\n value={draft.name ?? ''}\n placeholder={app?.name}\n maxLength={80}\n disabled={disabled}\n onChange={(event) => set({ name: event.target.value || undefined })}\n />\n </Field>\n )}\n {allowed('logo') && (\n <Field label={say('logo')} hint={say('logoHint')} problem={problemFor('logo')} size=\"compact\" group>\n {(control) => (\n <div className=\"branding-form-logo\" {...control} role=\"group\">\n <span className=\"branding-form-logo-box\">{logo ? <img src={logo} alt=\"\" width={96} height={24} /> : say('noLogo')}</span>\n <input\n ref={file}\n type=\"file\"\n hidden\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/svg+xml\"\n onChange={(event) => void pick(event, setUpload)}\n />\n <Button intent=\"secondary\" size=\"compact\" disabled={disabled} onClick={() => file.current?.click()}>\n {say('chooseLogo')}\n </Button>\n {logo && (\n <Button\n intent=\"quiet\"\n size=\"compact\"\n disabled={disabled}\n onClick={() => setUpload(reply.branding.logo ? null : undefined)}\n >\n {say('removeLogo')}\n </Button>\n )}\n </div>\n )}\n </Field>\n )}\n {allowed('logo') && logo && (\n <Field label={say('darkLogo')} hint={say('darkLogoHint')} size=\"compact\" group>\n {(control) => (\n <div className=\"branding-form-logo\" {...control} role=\"group\">\n <span className=\"branding-form-logo-box branding-form-logo-box--dark\">\n {darkLogo ? <img src={darkLogo} alt=\"\" width={96} height={24} /> : say('noDarkLogo')}\n </span>\n <input\n ref={darkFile}\n type=\"file\"\n hidden\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/svg+xml\"\n onChange={(event) => void pick(event, setDarkUpload)}\n />\n <Button intent=\"secondary\" size=\"compact\" disabled={disabled} onClick={() => darkFile.current?.click()}>\n {say('chooseLogo')}\n </Button>\n {darkLogo && (\n <Button\n intent=\"quiet\"\n size=\"compact\"\n disabled={disabled}\n onClick={() => setDarkUpload(reply.branding.logoDark ? null : undefined)}\n >\n {say('removeLogo')}\n </Button>\n )}\n </div>\n )}\n </Field>\n )}\n {allowed('accent') && (\n <Field label={say('accent')} hint={say('accentHint')} problem={problemFor('accent')} size=\"compact\" group>\n {(control) => (\n <div className=\"branding-form-accent\" {...control} role=\"group\">\n <input\n type=\"color\"\n aria-label={say('accentColor')}\n value={HEX.test(accentText) ? accentText : palette.accent}\n disabled={disabled}\n onChange={(event) => set({ accent: pairFor(event.target.value) })}\n />\n <input\n className=\"field-control\"\n aria-label={say('accentColor')}\n value={accentText}\n placeholder={chosen.light.accent}\n spellCheck={false}\n maxLength={7}\n disabled={disabled}\n onChange={(event) => {\n const typed = event.target.value.trim().toLowerCase();\n set({ accent: typed ? (HEX.test(typed) ? pairFor(typed) : { light: typed, dark: typed }) : undefined });\n }}\n />\n </div>\n )}\n </Field>\n )}\n {allowed('preset') && (\n <Field label={say('theme')} problem={problemFor('preset')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('theme')}\n disabled={disabled}\n value={draft.preset ?? reply.themes[0]!.id}\n items={reply.themes.map((item) => ({ value: item.id, label: item.label }))}\n onChange={(next) => set({ preset: next, palette: undefined })}\n />\n </Field>\n )}\n {allowed('palette') && chosen.palettes?.length ? (\n <Field label={say('palette')} problem={problemFor('palette')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('palette')}\n disabled={disabled}\n value={draft.palette ?? chosen.defaultPalette ?? 'default'}\n items={[{ value: 'default', label: chosen.label }, ...chosen.palettes.map((item) => ({ value: item.id, label: item.label }))]}\n onChange={(next) => set({ palette: next })}\n />\n </Field>\n ) : null}\n {allowed('radius') && (\n <Field label={say('corner')} problem={problemFor('radius')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('corner')}\n disabled={disabled}\n value={draft.radius ?? ''}\n items={[\n { value: '', label: say('appDefault') },\n ...RADII.map((radius) => ({ value: radius, label: radius === '0px' ? say('square') : radius })),\n ]}\n onChange={(next) => set({ radius: next || undefined })}\n />\n </Field>\n )}\n {allowed('density') && (\n <Field label={say('density')} problem={problemFor('density')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('density')}\n disabled={disabled}\n value={draft.density ?? ''}\n items={[\n { value: '', label: say('appDefault') },\n { value: 'compact', label: say('compact') },\n { value: 'comfortable', label: say('comfortable') },\n { value: 'spacious', label: say('spacious') },\n ]}\n onChange={(next) => set({ density: (next || undefined) as ThemeDensity | undefined })}\n />\n </Field>\n )}\n {editor && (\n <footer className=\"form-actions\">\n <p className=\"branding-form-status\" role=\"status\">\n {message}\n </p>\n <Button\n intent=\"secondary\"\n size=\"compact\"\n disabled={phase === 'saving'}\n onClick={() => {\n setDraft({ ...reply.branding });\n setUpload(undefined);\n setMessage('');\n setRefused([]);\n }}\n >\n {say('reset')}\n </Button>\n <Button intent=\"primary\" size=\"compact\" type=\"submit\" disabled={phase === 'saving' || problems.length > 0}>\n {phase === 'saving' ? say('saving') : say('save')}\n </Button>\n </footer>\n )}\n </form>\n <section className=\"branding-preview\" aria-label={say('preview')} style={preview}>\n <figure>\n <figcaption>{say('previewShell')}</figcaption>\n <div className=\"branding-preview-shell\">\n <span className=\"branding-preview-mark\">\n {logo ? <img src={dark() && darkLogo ? darkLogo : logo} alt=\"\" width={96} height={24} /> : null}\n </span>\n <strong>{name}</strong>\n <span className=\"branding-preview-nav\" data-current>\n {say('sampleNav')}\n </span>\n <span className=\"branding-preview-nav\">{say('sampleOther')}</span>\n </div>\n </figure>\n <figure>\n <figcaption>{say('previewRecord')}</figcaption>\n <div className=\"branding-preview-record\">\n <strong>{say('sampleRecord')}</strong>\n <span className=\"branding-preview-pill\">{say('sampleStatus')}</span>\n <span className=\"branding-preview-button\">{say('sampleAction')}</span>\n </div>\n </figure>\n <figure>\n <figcaption>{say('previewStat')}</figcaption>\n <div className=\"branding-preview-stat\">\n <span>{say('sampleStat')}</span>\n <strong>128</strong>\n <span className=\"branding-preview-bars\" aria-hidden=\"true\">\n {[40, 64, 52, 80, 70].map((height, index) => (\n <i key={index} style={{ height: `${height}%` }} />\n ))}\n </span>\n </div>\n </figure>\n <figure>\n <figcaption>{say('previewEmail')}</figcaption>\n <div className=\"branding-preview-email\">\n <div className=\"branding-preview-email-head\">\n {logo ? <img src={logo} alt=\"\" width={96} height={24} /> : <strong>{name}</strong>}\n </div>\n <p>{say('sampleEmailLine')}</p>\n <span className=\"branding-preview-button\">{say('sampleEmailButton')}</span>\n </div>\n </figure>\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shared-BttyOW4W.js","names":["SERIES","HEX"],"sources":["../../../engine/src/shared/themeColor.ts","../../../engine/src/shared/themePartsCheck.ts","../../../engine/src/shared/theme.ts","../../src/shared/themeBuild.ts","../../src/shared/themeSpec.ts","../../../engine/src/shared/chartSeparation.ts","../../src/shared/themeCheck.ts","../../src/shared/tenantBranding.ts","../../src/client/copy/dashboards/components/BrandingForm.ts","../../src/client/components/BrandingForm.tsx"],"sourcesContent":["/**\n * The colour arithmetic a theme is measured with: a bounded evaluator of the colours a palette is written in, mixing\n * and compositing as the page does, and WCAG contrast. Apart from theme.ts, which judges whole themes, because the\n * client draws with one result of it (heatTop, a matrix's strongest tint) on every first paint, and theme.ts with its\n * recipes, faces and checks is several kilobytes no first paint should carry.\n */\nimport type { ThemePalette } from '../../schema/theme.js';\n\nexport type Color = readonly [number, number, number, number];\n/** A deliberately bounded color evaluator, not a CSS parser. Fractional sRGB\n channels stay unrounded through compositing and luminance calculation. */\nexport function diagnosticColor(value: string, depth = 0): Color | undefined {\n if (depth > 8) return undefined;\n value = value.trim().toLowerCase();\n if (value === 'transparent') return [0, 0, 0, 0];\n if (value === 'white') return [1, 1, 1, 1];\n if (value === 'black') return [0, 0, 0, 1];\n if (/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value)) {\n const hex =\n value.length < 6\n ? value\n .slice(1)\n .split('')\n .map((char) => char + char)\n .join('')\n : value.slice(1);\n return [\n parseInt(hex.slice(0, 2), 16) / 255,\n parseInt(hex.slice(2, 4), 16) / 255,\n parseInt(hex.slice(4, 6), 16) / 255,\n hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1,\n ];\n }\n // The two expressions emitted by effectiveThemePalette. The inner legacy\n // color can be unknown; that remains unmeasured rather than guessed.\n const mix = /^color-mix\\(in srgb, (.+) (62|14)%, (white|transparent)\\)$/.exec(value);\n const color = mix && diagnosticColor(mix[1]!, depth + 1);\n return color ? mixColor(color, diagnosticColor(mix![3]!)!, Number(mix![2]) / 100) : undefined;\n}\n\nexport function mixColor(a: Color, b: Color, weight: number): Color {\n const alpha = a[3] * weight + b[3] * (1 - weight);\n const channel = (index: 0 | 1 | 2) => (alpha ? (a[index] * a[3] * weight + b[index] * b[3] * (1 - weight)) / alpha : 0);\n return [channel(0), channel(1), channel(2), alpha];\n}\n\nexport function over(color: Color, ground: Color): Color {\n const alpha = color[3] + ground[3] * (1 - color[3]);\n const channel = (index: 0 | 1 | 2) => (alpha ? (color[index] * color[3] + ground[index] * ground[3] * (1 - color[3])) / alpha : 0);\n return [channel(0), channel(1), channel(2), alpha];\n}\n\nexport function measuredContrast(foreground: Color, background: Color): number | undefined {\n if (background[3] !== 1) return undefined; // No known opaque ground.\n const luminance = (color: Color) =>\n color\n .slice(0, 3)\n .map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4))\n .reduce((total, value, index) => total + value * [0.2126, 0.7152, 0.0722][index]!, 0);\n const a = luminance(over(foreground, background)),\n b = luminance(background);\n return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n}\n\n/** A composited colour as the page paints it, `#rrggbb` (alpha dropped: a ground is opaque, an ink composited on one). */\nexport function colorHex(color: Color): string {\n return `#${color\n .slice(0, 3)\n .map((value) =>\n Math.round(Math.min(1, Math.max(0, value)) * 255)\n .toString(16)\n .padStart(2, '0'),\n )\n .join('')}`;\n}\n\n/** The strongest a heat tint is drawn (a matrix's heaviest cell), as a share of the accent over the panel. */\nexport const HEAT_TOP = 0.62;\n/** Below this a heat scale stops reading as one: the palette's heaviest cell is drawn here and `check` says why. */\nconst HEAT_FLOOR = 0.2;\n\n/**\n * How strong a heat tint may be drawn under a palette: the strongest share of the accent over the panel, at most\n * HEAT_TOP, on which the ink still reads at 4.5:1. A matrix's cells say their figure (and, folded, their column) in\n * the ink on that tint, so the scale stops where the words would fade: 62% under Lumen's blue, about half under\n * Mono's black or a dark mode's light accent, where the ink sits near the accent. Unmeasurable colors keep HEAT_TOP.\n */\nexport function heatTop(palette: Pick<ThemePalette, 'accent' | 'surface' | 'ink'>): number {\n const accent = palette.accent && diagnosticColor(palette.accent),\n surface = palette.surface && diagnosticColor(palette.surface),\n ink = palette.ink && diagnosticColor(palette.ink);\n if (!accent || !surface || !ink) return HEAT_TOP;\n for (let step = Math.round(HEAT_TOP * 100); step > HEAT_FLOOR * 100; step--) {\n const ratio = measuredContrast(ink, over(mixColor(accent, [0, 0, 0, 0], step / 100), surface));\n if (ratio === undefined || ratio >= 4.5) return step / 100;\n }\n return HEAT_FLOOR;\n}\n\n/** What a command form's field edge at rest is drawn to: 3:1 with room to spare, short of a line that shouts. */\nconst FIELD_EDGE = 3.5;\n/** Past this a control's edge is a line drawn to be seen (Mono's ink), not a field's quiet boundary. */\nconst FIELD_EDGE_CEILING = 4.5;\n\n/**\n * A command form's field edge at rest (formDress.css, `--field-edge`): a clean outline at 3:1 on what the field\n * stands on (WCAG 1.4.11). Where the look's control edge (`lineControl`) already reads at up to 4.5:1 on its surface\n * and its page, it is that edge, so a form's field and a record's inline edit wear one line (Lumen, Sage). A stronger\n * one — Mono's ink, a theme's near-black — is mixed toward the surface to the lightest that still clears FIELD_EDGE on\n * both: a box of solid ink round every answer read as a form shouting. Undefined keeps the look's own edge (the sheet\n * falls back to `--line-control`), and so does a color this cannot measure.\n */\nexport function fieldEdge(palette: Pick<ThemePalette, 'lineControl' | 'surface' | 'ground'>): string | undefined {\n const edge = palette.lineControl && diagnosticColor(palette.lineControl),\n surface = palette.surface && diagnosticColor(palette.surface),\n ground = palette.ground ? diagnosticColor(palette.ground) : surface;\n if (!edge || !surface || !ground) return undefined;\n const least = (color: Color) => Math.min(measuredContrast(color, surface) ?? 0, measuredContrast(color, ground) ?? 0);\n if (least(edge) <= FIELD_EDGE_CEILING) return undefined;\n for (let step = 1; step < 100; step++) {\n const mixed = mixColor(edge, surface, step / 100);\n if (least(mixed) >= FIELD_EDGE) return colorHex(mixed);\n }\n return undefined;\n}\n","/**\n * What a theme may say beyond the recipe menu, checked: its patterns (`surface`), its parts' styles (`parts`) and\n * its icons' weight (`type.icons`), for the spec (themeSpec.ts) and for a compiled theme read back (theme.ts\n * parseThemes) alike. A module apart from themeParts.ts, which the first paint wears and these messages are no\n * weight of.\n */\nimport type { ThemeIconWeight, ThemePart, ThemePartStyle, ThemePattern, ThemeSurface } from '../../schema/theme.js';\nimport { PALETTE_TOKENS, rolePath } from './themeTokens.js';\nimport { PART_CASES, PART_FACES, PART_LIFTS, PART_PROPERTIES, PART_RADII, PART_WEIGHTS, THEME_PARTS } from './themeParts.js';\n\n/** The patterns, drawn by surfacesLook.css; each at a strength of 1 (omitted), 2 or 3. */\nexport const PATTERN_KINDS = ['paper', 'grain', 'grid', 'dots', 'lines', 'scanlines'] as const;\n/** How much of the line color each strength draws a pattern in (surfacesLook.css says the same three). */\nconst PATTERN_STRENGTH = { 1: 0.4, 2: 0.7, 3: 1 } as const;\n/* Paper is two families of fibers crossing, twice the ink of one family of lines at a strength: at 40% its low end\n read as a mesh over the page, not a sheet of paper. Its own steps are half again lighter. */\nconst PAPER_STRENGTH = { 1: 0.2, 2: 0.45, 3: 0.7 } as const;\n/** How much of the line color a pattern of this kind and strength is drawn in. */\nexport const patternStrength = (pattern: ThemePattern): number =>\n (pattern.kind === 'paper' ? PAPER_STRENGTH : PATTERN_STRENGTH)[pattern.strength ?? 1];\n/** Phosphor's weights a theme may pick for the icons: thin fades at 16px, duotone says a second tone no role names. */\nexport const ICON_WEIGHTS: readonly ThemeIconWeight[] = ['regular', 'light', 'bold', 'fill'];\n\n/** The color roles a part may name, by their paths: every color but the widths and the shadow's ink. */\nconst PART_COLOR_PATHS: readonly string[] = Object.keys(PALETTE_TOKENS)\n .filter((role) => role !== 'shadow' && role !== 'rulesWeight' && role !== 'rulesControl')\n .map(rolePath);\n/** A role's path back to the role it names: `nav.onCurrent` is `navOnCurrent`, `chart[2]` is `chart3`. */\nexport function pathRole(path: string): string {\n const series = /^chart\\[(\\d)\\]$/.exec(path);\n if (series) return `chart${Number(series[1]) + 1}`;\n return path.replace(/\\.(\\w)/, (_, letter: string) => letter.toUpperCase());\n}\n\nfunction record(raw: unknown, where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${where} must be an object.`);\n return raw as Record<string, unknown>;\n}\n\n/** `surface`: a pattern behind the page, one behind the menu, each of the six at a strength; omitted strength is 1. */\nexport function surfaceOf(raw: unknown, where: string): ThemeSurface {\n const surface = record(raw, where);\n const out: ThemeSurface = {};\n for (const [place, value] of Object.entries(surface)) {\n if (place !== 'ground' && place !== 'nav') throw new Error(`${where} has no \"${place}\". It takes: ground, nav.`);\n const pattern = record(value, `${where}.${place}`);\n for (const key of Object.keys(pattern))\n if (key !== 'kind' && key !== 'strength')\n throw new Error(\n `${where}.${place} has no \"${key}\". It takes: kind, strength. A pattern is one of the platform's, drawn in the theme's line color.`,\n );\n if (!(PATTERN_KINDS as readonly unknown[]).includes(pattern.kind))\n throw new Error(`${where}.${place}.kind must be one of ${PATTERN_KINDS.join(', ')}.`);\n if (pattern.strength !== undefined && pattern.strength !== 1 && pattern.strength !== 2 && pattern.strength !== 3)\n throw new Error(`${where}.${place}.strength must be 1, 2 or 3.`);\n out[place] = { kind: pattern.kind as ThemePattern['kind'], strength: (pattern.strength ?? 1) as 1 | 2 | 3 };\n }\n return out;\n}\n\n/** `parts`: each a published part, saying only what its menus hold. */\nexport function partsOf(raw: unknown, where: string): Partial<Record<ThemePart, ThemePartStyle>> {\n const parts = record(raw, where);\n const out: Partial<Record<ThemePart, ThemePartStyle>> = {};\n for (const [part, value] of Object.entries(parts)) {\n if (!(THEME_PARTS as readonly string[]).includes(part))\n throw new Error(`${where} has no \"${part}\". The parts are: ${THEME_PARTS.join(', ')}.`);\n const at = `${where}.${part}`;\n const style = record(value, at);\n for (const [property, said] of Object.entries(style)) {\n if (!(PART_PROPERTIES as readonly string[]).includes(property))\n throw new Error(\n `${at} has no \"${property}\". It takes: ${PART_PROPERTIES.join(', ')}. A part's size, spacing, order and visibility are the App's, never its theme's.`,\n );\n const menu = (why: string, values: readonly unknown[]) => {\n if (!values.includes(said)) throw new Error(`${at}.${property} must be one of ${values.join(', ')}${why}.`);\n };\n if (property === 'ground' || property === 'ink' || property === 'edge') {\n if (said === 'nav.currentInk') throw new Error(`${at}.${property} names nav.currentInk, which is now nav.onCurrent.`);\n if (typeof said !== 'string' || !PART_COLOR_PATHS.includes(said))\n throw new Error(`${at}.${property} must be a color role by its path, like nav.ground or chart[2], never a hex.`);\n } else if (property === 'radius') menu(\": a corner the theme's shape already says, or square\", PART_RADII);\n else if (property === 'lift') menu('', PART_LIFTS);\n else if (property === 'face') menu(\": a face the theme's type already says\", PART_FACES);\n else if (property === 'weight') menu('', PART_WEIGHTS);\n else menu('', PART_CASES);\n }\n out[part as ThemePart] = { ...style } as ThemePartStyle;\n }\n return out;\n}\n\n/** `type.icons`: one of Phosphor's weights the icons are drawn in. */\nexport function iconsOf(raw: unknown, where: string): ThemeIconWeight {\n if (!(ICON_WEIGHTS as readonly unknown[]).includes(raw))\n throw new Error(\n `${where} must be one of ${ICON_WEIGHTS.join(', ')}: thin draws a 1px stroke that fades at the 16px an icon is drawn at, and duotone a second tone no color role says.`,\n );\n return raw as ThemeIconWeight;\n}\n","import type { ThemeDefinition, ThemePalette, ThemePaletteVariant, ThemeRecipes } from '../../schema/theme.js';\nimport {\n effectiveThemeDefinition,\n effectiveThemePalette,\n parseScheme,\n THEME_PRESETS,\n themeModes,\n withInks,\n type AppAccent,\n} from './themePresets.js';\nimport { colorHex, diagnosticColor, HEAT_TOP, heatTop, measuredContrast, mixColor, over, type Color } from './themeColor.js';\nimport { DENSITIES, ELEVATIONS, LIFTS, MOTIONS, RECIPE_MENU, RECIPES_RENAMED, RHYTHMS } from './themeRecipes.js';\nimport { families } from './themeFaces.js';\nimport { iconsOf, partsOf, pathRole, patternStrength, surfaceOf } from './themePartsCheck.js';\nimport { TYPE_MOVED, typeOf } from './themeType.js';\nimport {\n BORDER_MENU,\n CONTROL_HEIGHT,\n controlHeightFits,\n cornerFits,\n FONT_TOKENS,\n PALETTE_TOKENS,\n RADIUS_MAX,\n RADIUS_TOKENS,\n rolePath,\n ROUND,\n ROUND_CORNERS,\n THEME_BASES,\n} from './themeTokens.js';\nexport { FONT_TOKENS, PALETTE_TOKENS, RADIUS_TOKENS, shapeTokens, THEME_BASES } from './themeTokens.js';\nexport type { ThemeBase, ThemeDefinition, ThemePalette, ThemePaletteVariant } from '../../schema/theme.js';\n\nfunction object(raw: unknown, where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${where} must be an object.`);\n return raw as Record<string, unknown>;\n}\n\nfunction strings(raw: unknown, keys: readonly string[], valid: (value: string) => boolean, where: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (!keys.includes(key)) throw new Error(`${where} has no \"${key}\". It takes: ${keys.join(', ')}.`);\n if (typeof value !== 'string' || !valid(value.trim())) throw new Error(`${where}.${key} has an invalid value.`);\n out[key] = value.trim();\n }\n return out;\n}\n\n/** The roles' names before they were the spec's own (0.9.2), each to the one that replaced it. */\nconst OLD_ROLES: Readonly<Record<string, string>> = {\n background: 'ground',\n subtle: 'raised',\n border: 'line',\n borderStrong: 'lineStrong',\n borderControl: 'lineControl',\n text: 'ink',\n textMuted: 'inkMuted',\n textSubtle: 'inkSubtle',\n positive: 'statusGood',\n warning: 'statusWarning',\n negative: 'statusCritical',\n key: 'statusInfo',\n navSurface: 'navGround',\n navText: 'navInk',\n navBorder: 'navLine',\n navCurrentText: 'navOnCurrent',\n navCurrentInk: 'navOnCurrent',\n sectionRule: 'rulesSection',\n headRule: 'rulesHead',\n ruleWeight: 'rulesWeight',\n};\n\n/** A palette's roles: opaque six-digit hex colors, but for `rulesWeight` and `rulesControl`, widths. */\nconst RULE_WEIGHTS = ['1px', '2px', '3px'] as const;\nconst CONTROL_WEIGHTS = ['0px', '1px', '2px'] as const;\nfunction paletteStrings(raw: unknown, where: string): Record<string, string> {\n const colors = Object.keys(PALETTE_TOKENS).filter((key) => key !== 'rulesWeight' && key !== 'rulesControl');\n const { rulesWeight, rulesControl, ...rest } = object(raw, where);\n for (const key of Object.keys(rest))\n if (Object.hasOwn(OLD_ROLES, key)) throw new Error(`${where}.${key} is now ${where}.${OLD_ROLES[key]}.`);\n const result = strings(rest, colors, (color) => /^#[0-9a-f]{6}$/i.test(color), where);\n if (rulesWeight !== undefined) {\n if (typeof rulesWeight !== 'string' || !(RULE_WEIGHTS as readonly string[]).includes(rulesWeight.trim())) {\n throw new Error(`${where}.rulesWeight must be one of ${RULE_WEIGHTS.join(', ')}.`);\n }\n result.rulesWeight = rulesWeight.trim();\n }\n if (rulesControl !== undefined) {\n if (typeof rulesControl !== 'string' || !(CONTROL_WEIGHTS as readonly string[]).includes(rulesControl.trim())) {\n throw new Error(`${where}.rulesControl must be one of ${CONTROL_WEIGHTS.join(', ')}.`);\n }\n result.rulesControl = rulesControl.trim();\n }\n return result;\n}\n\nexport function parseThemes(raw: unknown, where: string, key = 'themes'): ThemeDefinition[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw) || raw.length > 20) throw new Error(`${where}.${key} must be an array of at most 20 themes.`);\n const used = new Set<string>(['default', ...THEME_BASES]);\n return raw.map((value, index): ThemeDefinition => {\n const path = `${where}.${key}[${index}]`;\n const theme = object(value, path);\n for (const key of Object.keys(theme)) {\n if (key === 'scale' || key === 'headingWeight') throw new Error(`${path}.${key} is now ${path}.${TYPE_MOVED[`type.${key}`]}.`);\n if (\n ![\n 'id',\n 'label',\n 'base',\n 'light',\n 'dark',\n 'palettes',\n 'defaultPalette',\n 'scheme',\n 'fonts',\n 'radius',\n 'border',\n 'height',\n ...STRUCTURE_KEYS,\n ].includes(key)\n )\n throw new Error(`${path} has no \"${key}\".`);\n }\n if (typeof theme.id !== 'string' || !/^[a-z][a-z0-9-]{0,47}$/.test(theme.id) || used.has(theme.id)) {\n throw new Error(`${path}.id must be a unique slug, not a built-in theme name.`);\n }\n used.add(theme.id);\n if (typeof theme.label !== 'string' || !theme.label.trim() || theme.label.trim().length > 60)\n throw new Error(`${path}.label must have 1 to 60 characters.`);\n if (theme.base !== undefined && (typeof theme.base !== 'string' || !(THEME_BASES as readonly string[]).includes(theme.base)))\n throw new Error(`${path}.base must be one of ${THEME_BASES.join(', ')}.`);\n const palette = (mode: 'light' | 'dark'): ThemePalette => {\n const result = paletteStrings(theme[mode], `${path}.${mode}`);\n for (const required of ['surface', 'ink', 'accent']) {\n if (!result[required]) throw new Error(`${path}.${mode}.${required} is required (a six-digit hex color).`);\n }\n return { ...result, surface: result.surface!, ink: result.ink!, accent: result.accent! };\n };\n const scheme = parseScheme(theme.scheme, path);\n const dark = palette('dark');\n /* One set of colors worn in both slots (themeBuild.ts): a light slot of its own is the colors a dark-only theme lacks. */\n const light = scheme === 'dark-only' ? dark : palette('light');\n if (scheme === 'dark-only' && theme.light !== undefined && JSON.stringify(palette('light')) !== JSON.stringify(dark))\n throw new Error(`${path}.light: a dark-only theme has no light colors; name dark alone, or drop scheme: 'dark-only'.`);\n const palettes = parsePaletteVariants(theme.palettes, path);\n if (\n theme.defaultPalette !== undefined &&\n (typeof theme.defaultPalette !== 'string' ||\n (theme.defaultPalette !== 'default' && !palettes?.some((item) => item.id === theme.defaultPalette)))\n ) {\n throw new Error(`${path}.defaultPalette must be \"default\" or one of this theme's palettes.`);\n }\n return {\n id: theme.id,\n label: theme.label.trim(),\n base: (theme.base ?? 'mono') as ThemeDefinition['base'],\n light,\n dark,\n ...(scheme === undefined ? {} : { scheme }),\n ...(palettes === undefined ? {} : { palettes }),\n ...(theme.defaultPalette === undefined ? {} : { defaultPalette: theme.defaultPalette as string }),\n ...(theme.fonts === undefined\n ? {}\n : {\n fonts: strings(\n theme.fonts,\n Object.keys(FONT_TOKENS),\n (font) => font.length > 0 && font.length <= 200 && /^[\\w\\s'\",.-]+$/.test(font),\n `${path}.fonts`,\n ),\n }),\n ...(theme.radius === undefined ? {} : { radius: radii(theme.radius, `${path}.radius`) }),\n ...(theme.border === undefined ? {} : { border: borders(theme.border, `${path}.border`) }),\n ...(theme.height === undefined ? {} : { height: heights(theme.height, `${path}.height`) }),\n ...structure(theme, path),\n };\n });\n}\n\n/** A compiled theme's corners, each within its ceiling, a control's and a pill's `round` as ROUND. */\nfunction radii(raw: unknown, where: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (!Object.hasOwn(RADIUS_TOKENS, key))\n throw new Error(`${where} has no \"${key}\". It takes: ${Object.keys(RADIUS_TOKENS).join(', ')}.`);\n const fits = cornerFits(value, RADIUS_MAX[key as keyof typeof RADIUS_MAX]) || (ROUND_CORNERS.has(key) && value === ROUND);\n if (!fits) throw new Error(`${where}.${key} has an invalid value.`);\n out[key] = value as string;\n }\n return out;\n}\n/** A compiled theme's panel edge and focus ring, each from its menu. */\nfunction borders(raw: unknown, where: string): NonNullable<ThemeDefinition['border']> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (key !== 'panel' && key !== 'focus') throw new Error(`${where} has no \"${key}\". It takes: panel, focus.`);\n if (!(BORDER_MENU[key] as readonly unknown[]).includes(value)) throw new Error(`${where}.${key} has an invalid value.`);\n out[key] = value as string;\n }\n return out as NonNullable<ThemeDefinition['border']>;\n}\n\n/** A compiled theme's control height: whole pixels from CONTROL_HEIGHT.min to .max. */\nfunction heights(raw: unknown, where: string): NonNullable<ThemeDefinition['height']> {\n const out: { control?: string } = {};\n for (const [key, value] of Object.entries(object(raw, where))) {\n if (key !== 'control') throw new Error(`${where} has no \"${key}\". It takes: control.`);\n if (!controlHeightFits(value))\n throw new Error(`${where}.control must be a length from ${CONTROL_HEIGHT.min}px to ${CONTROL_HEIGHT.max}px.`);\n out.control = value as string;\n }\n return out;\n}\n\n/**\n * What a theme's shape says that `check` warns of (theme-radius): a panel squarer than the controls it holds, its\n * corner cutting under theirs. A round control is a pill, not a corner, so it is never measured against a panel.\n */\nexport function themeShapeWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n const control = theme.radius?.control,\n panel = theme.radius?.panel;\n if (!control || !panel || control === ROUND || parseFloat(panel) >= parseFloat(control)) return [];\n return [\n `ui.customThemes[${index}] (\"${theme.id}\") shape.radius.panel ${panel} is squarer than shape.radius.control ${control}: a panel holds controls, so give its corner at least theirs.`,\n ];\n });\n}\n\n/**\n * What a theme's shadow says that `check` warns of (theme-shadow, the contract's C2): a shadow lighter than the ground\n * it falls on, which draws a glow round a card or a menu rather than lifting it off the page — the edge the lift was\n * for is lost. The theme still wears.\n */\nexport function themeShadowWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n const worn = effectiveThemeDefinition(theme, 'default');\n return themeModes(theme).flatMap((mode) => {\n const shadow = worn[mode].shadow,\n ground = worn[mode].ground ?? worn[mode].surface;\n if (!shadow || !/^#[0-9a-f]{6}$/i.test(shadow) || !/^#[0-9a-f]{6}$/i.test(ground) || luminance(shadow) <= luminance(ground))\n return [];\n return [\n `ui.customThemes[${index}] (\"${theme.id}\") colors.${mode}.shadow ${shadow} is lighter than its ground ${ground}: a shadow lighter than the page is a glow, not a lift, so give it a color darker than the ground.`,\n ];\n });\n });\n}\n\n/**\n * What a banded table's parts say that `check` warns of (theme-band, the contract's C13): a `status` or `tag` part\n * given an opaque ground. Part rules live in the `theme` cascade layer, above the `look` layer's band, so the chip\n * keeps its own ground on every other row and shows as a pale column. The author's color is not touched: a\n * translucent ground lets the band through, and so does the default.\n */\nexport function themeBandWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n if (theme.recipes?.table !== 'banded') return [];\n const worn = effectiveThemeDefinition(theme, 'default');\n return (['status', 'tag'] as const).flatMap((part) => {\n const role = theme.parts?.[part]?.ground;\n if (!role) return [];\n const key = pathRole(role) as keyof ThemePalette;\n const opaque = themeModes(theme).filter((mode) => {\n const value = worn[mode][key];\n const color = value ? diagnosticColor(value) : undefined;\n return color !== undefined && color[3] >= 1;\n });\n if (!opaque.length) return [];\n return [\n `ui.customThemes[${index}] (\"${theme.id}\") parts.${part}.ground ${role} is opaque (${opaque.join(', ')}) on a banded table: a ${part} cell will not take the band and shows as a pale column on every other row, so give it a translucent ground or leave it out for the look's.`,\n ];\n });\n });\n}\n\n/** The stack a role's face reads when the role names none: the titles' the heading face, the rest the interface's. */\nconst ROLE_FACE: Record<string, 'heading' | 'ui'> = { display: 'heading', title: 'heading', heading: 'heading', section: 'heading' };\n\n/**\n * What a theme's type says that `check` warns of (theme-type, the contract's C8): a role heavier than 700 whose face\n * leads with a family the theme does not ship at that weight. design.md keeps titles at 700 or under for this: a\n * heavier weight a family has no file for is drawn by the browser as a smeared 700, or resolves a book face to its\n * Black cut, a masthead where a title was meant.\n */\nexport function themeTypeWarnings(themes: readonly ThemeDefinition[]): string[] {\n return themes.flatMap((theme, index) => {\n const worn = effectiveThemeDefinition(theme, 'default');\n return Object.entries(worn.type?.roles ?? {}).flatMap(([role, said]) => {\n /* A line under 1.0 sets its descenders into the next: a title that wraps (a long name, a phone) collides with\n itself. Allowed down to 0.85 for a short large line, so it warns rather than refuses. */\n const leading =\n said.leading !== undefined && said.leading < 1\n ? [\n `ui.customThemes[${index}] (\"${theme.id}\") type.roles.${role}.leading ${said.leading} is under 1.0: a ${role} that wraps sets its descenders into the next line, so keep it at 1.0 or over unless it is one short line.`,\n ]\n : [];\n const weight = said.weight;\n if (weight === undefined || weight <= 700) return leading;\n const face = said.face ?? ROLE_FACE[role] ?? 'ui';\n const stack = (face === 'heading' ? (worn.fonts?.heading ?? worn.fonts?.ui) : worn.fonts?.[face]) ?? '';\n const lead = families(stack)[0];\n const shipped = (worn.faces ?? []).some((file) => {\n const [low, high = low] = file.weight.split(' ').map(Number);\n return file.family.toLowerCase() === lead?.toLowerCase() && weight >= low! && weight <= high!;\n });\n if (shipped) return leading;\n return [\n ...leading,\n `ui.customThemes[${index}] (\"${theme.id}\") type.roles.${role}.weight ${weight} is past 700 in a face the theme does not ship at that weight${lead ? ` (${lead})` : ''}: the browser draws a smeared 700 or a Black cut, so ship the face at ${weight} in type.faces or keep the weight at 700 or under.`,\n ];\n });\n });\n}\n\nconst STRUCTURE_KEYS = ['recipes', 'elevation', 'type', 'density', 'motion', 'rhythm', 'lifts', 'surface', 'parts', 'icons'] as const;\n/** A compiled theme's structure, as `compileTheme` writes it: every value from the menu. */\nfunction structure(theme: Record<string, unknown>, path: string): Partial<ThemeDefinition> {\n const out: Partial<ThemeDefinition> = {};\n if (theme.recipes !== undefined) {\n const recipes = object(theme.recipes, `${path}.recipes`);\n for (const [part, value] of Object.entries(recipes)) {\n if (TYPE_MOVED[`recipes.${part}`]) throw new Error(`${path}.recipes.${part} is now ${path}.${TYPE_MOVED[`recipes.${part}`]}.`);\n if (Object.hasOwn(RECIPES_RENAMED, part)) throw new Error(`${path}.recipes.${part} is now ${path}.recipes.${RECIPES_RENAMED[part]}.`);\n const menu = (RECIPE_MENU as Record<string, Record<string, unknown>>)[part];\n if (!menu || typeof value !== 'string' || !Object.hasOwn(menu, value)) throw new Error(`${path}.recipes.${part} is not on the menu.`);\n }\n out.recipes = { ...recipes } as ThemeDefinition['recipes'];\n }\n if (theme.elevation !== undefined) {\n if (typeof theme.elevation !== 'string' || !Object.hasOwn(ELEVATIONS, theme.elevation))\n throw new Error(`${path}.elevation is not on the menu.`);\n out.elevation = theme.elevation as ThemeDefinition['elevation'];\n }\n if (theme.type !== undefined) {\n const type = object(theme.type, `${path}.type`);\n for (const key of Object.keys(type))\n if (key !== 'base' && key !== 'roles') throw new Error(`${path}.type has no \"${key}\". It takes: base, roles.`);\n out.type = typeOf(type.base, type.roles, `${path}.type`);\n }\n if (theme.density !== undefined) {\n if (!(DENSITIES as readonly unknown[]).includes(theme.density))\n throw new Error(`${path}.density must be one of ${DENSITIES.join(', ')}.`);\n out.density = theme.density as ThemeDefinition['density'];\n }\n if (theme.lifts !== undefined) {\n const lifts = object(theme.lifts, `${path}.lifts`);\n for (const [key, step] of Object.entries(lifts))\n if ((key !== 'card' && key !== 'float') || typeof step !== 'string' || step === 'look' || !Object.hasOwn(LIFTS, step))\n throw new Error(`${path}.lifts.${key} is not on the menu.`);\n out.lifts = { ...lifts } as ThemeDefinition['lifts'];\n }\n if (theme.motion !== undefined) {\n if (typeof theme.motion !== 'string' || !Object.hasOwn(MOTIONS, theme.motion))\n throw new Error(`${path}.motion must be one of ${Object.keys(MOTIONS).join(', ')}.`);\n out.motion = theme.motion as ThemeDefinition['motion'];\n }\n if (theme.rhythm !== undefined) {\n if (typeof theme.rhythm !== 'string' || !Object.hasOwn(RHYTHMS, theme.rhythm))\n throw new Error(`${path}.rhythm must be one of ${Object.keys(RHYTHMS).join(', ')}.`);\n out.rhythm = theme.rhythm as ThemeDefinition['rhythm'];\n }\n if (theme.surface !== undefined) out.surface = surfaceOf(theme.surface, `${path}.surface`);\n if (theme.parts !== undefined) out.parts = partsOf(theme.parts, `${path}.parts`);\n if (theme.icons !== undefined) out.icons = iconsOf(theme.icons, `${path}.icons`);\n return out;\n}\n\nfunction parsePaletteVariants(raw: unknown, where: string): ThemePaletteVariant[] | undefined {\n if (raw === undefined) return undefined;\n if (!Array.isArray(raw) || raw.length > 20) throw new Error(`${where}.palettes must be an array of at most 20 palettes.`);\n const used = new Set(['default']);\n return raw.map((value, index) => {\n const path = `${where}.palettes[${index}]`,\n variant = object(value, path);\n for (const key of Object.keys(variant)) {\n if (!['id', 'label', 'light', 'dark'].includes(key)) throw new Error(`${path} has no \"${key}\".`);\n }\n if (typeof variant.id !== 'string' || !/^[a-z][a-z0-9-]{0,47}$/.test(variant.id) || used.has(variant.id)) {\n throw new Error(`${path}.id must be a unique slug, not \"default\".`);\n }\n used.add(variant.id);\n if (typeof variant.label !== 'string' || !variant.label.trim() || variant.label.trim().length > 60)\n throw new Error(`${path}.label must have 1 to 60 characters.`);\n const palette = (mode: 'light' | 'dark') => paletteStrings(variant[mode], `${path}.${mode}`);\n return { id: variant.id, label: variant.label.trim(), light: palette('light'), dark: palette('dark') };\n });\n}\n\n/** WCAG relative luminance, for the opaque hex colors accepted by the contract. */\nfunction luminance(hex: string): number {\n const channels = [1, 3, 5]\n .map((start) => parseInt(hex.slice(start, start + 2), 16) / 255)\n .map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4));\n return channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722;\n}\n\nexport function contrastRatio(a: string, b: string): number {\n const left = luminance(a),\n right = luminance(b);\n return (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05);\n}\n\n/**\n * The least luminance ratio a field's fill may be from the ground and the surface it sits on, when the field draws\n * no edge. 1.08:1 is Lintel's Hearth (a warm #eeede7 well on a #f7f6f2 page, 1.085, and on white, 1.173): the faintest\n * field we ship that still reads as a well, and the faintest a calibrated laptop screen shows in daylight. Below it\n * (white on #fcfbf8, 1.03) the field is gone. WCAG 1.4.11 asks 3:1 of a control's boundary unless it is identified\n * some other way; the label stands over the field and the focus ring draws a 2px edge, which is that other way.\n */\nexport const EDGELESS_FIELD_DELTA = 1.08;\n\n/** What startup checks in each mode, and the ratio each pair must reach: text\n at 4.5:1 (WCAG 1.4.3) — the meaning colors on the page as well as on a\n panel, since a status word sits on both, and the page is where Meridian's\n red read at 4.4:1 while its panels passed — and a form control's edge at\n 3:1 (1.4.11) on the field it frames and on the page around it. Section and\n table hairlines are decoration, and are not checked. */\nconst CONTRAST_PAIRS: ReadonlyArray<readonly [keyof ThemePalette, keyof ThemePalette, number, string?]> = [\n ['ink', 'surface', 4.5],\n ['inkMuted', 'surface', 4.5],\n ['inkSubtle', 'surface', 4.5],\n ['ink', 'ground', 4.5],\n ['ink', 'raised', 4.5],\n /* A field's own fill (`colors.field`, the surface where it names none): the words typed in it, the faint ink of its\n placeholder and the muted ink of its hint stand on it, and its edge. Skipped where the fill is the surface, which\n the pairs above already measure. */\n ['ink', 'field', 4.5, 'the words in a field'],\n ['inkMuted', 'field', 4.5, \"a field's muted words\"],\n ['inkSubtle', 'field', 4.5, \"a field's placeholder\"],\n ['lineControl', 'field', 3, \"a control's edge on a field's fill\"],\n /* Every ink is drawn where the text it is for is drawn, not only on the panel it was picked on: a caption under a\n page's title and a count beside a tab stand on the page (TC13, a theme check passed and its pages failed). */\n ['inkMuted', 'ground', 4.5, 'a caption on the page'],\n ['inkSubtle', 'ground', 4.5, 'a faint caption on the page'],\n ['link', 'ground', 4.5],\n ['link', 'raised', 4.5],\n ['navAccent', 'navGround', 4.5, \"the menu's current entry\"],\n /* The accent is a fill, and is read as text in its ink (`accentInk`, the accent itself where a theme names none and\n it reads); the words drawn on a fill are `onAccent`. A bright brand accent can be a good fill and a poor text. */\n ['accentInk', 'surface', 4.5],\n // A quiet button's word and a current tab are the accent's ink on the page, and on a raised panel or a hovered row.\n ['accentInk', 'ground', 4.5, 'an action in the accent on the page'],\n ['accentInk', 'raised', 4.5, 'an action in the accent on a raised panel or a hovered row'],\n ['onAccent', 'accent', 4.5, 'the words on an accent fill'],\n ['onPrimary', 'primary', 4.5, \"the words on a form's primary action\"],\n ['link', 'surface', 4.5],\n ['navInk', 'navGround', 4.5, \"the menu's entries\"],\n ['navMuted', 'navGround', 4.5, \"the menu's group headings, counts and icons\"],\n ...(['statusGood', 'statusWarning', 'statusCritical', 'statusInfo'] as const).flatMap(\n (role) =>\n [\n [role, 'surface', 4.5],\n [role, 'ground', 4.5],\n [role, 'raised', 4.5, 'a status word in a hovered row or on a raised panel'],\n ] as const,\n ),\n ['lineControl', 'surface', 3],\n ['lineControl', 'ground', 3],\n /* A field stands on a raised panel too, and a list's sort and filter controls on its head: a control's edge that\n passed on the panel and the page read 2.94:1 there. */\n ['lineControl', 'raised', 3, \"a control's edge on a raised panel or a table's head\"],\n // The focus ring is a graphic the keyboard reader follows (1.4.11): on the field, the page and the subtle panel it can sit on.\n ['focus', 'surface', 3],\n ['focus', 'ground', 3],\n ['focus', 'raised', 3],\n /* A chart's first four series are graphics a reader has to tell from the\n card they sit on; past four, a chart is already a legend's job. */\n ...(['chart1', 'chart2', 'chart3', 'chart4'] as const).map((series) => [series, 'surface', 3] as const),\n];\n\nexport { HEAT_TOP, heatTop };\n\nconst SERIES = ['chart1', 'chart2', 'chart3', 'chart4', 'chart5', 'chart6', 'chart7', 'chart8'] as const;\n\n/**\n * The colours a platform component mixes from a theme's series and its ink rather than wears as a token: no pair of\n * tokens is what it draws, so each is measured as drawn, and named by the series it comes from. A board card's\n * category chip (TriageBoard.css: 45% of its series in the ink, except under the `strip` board, which says it in words, on a 13% tint of it over the card), a person's\n * initials on their avatar (features.css: 45% in the ink, on a 15% tint over a 7% ink wash), and under the `icon`\n * stat recipe, a stat card's mark (lumenLook.css: 80% in the ink on a 12% tint, a graphic at 3:1). `check` holds a\n * theme to each, and `grade` names the series behind a reading of one.\n */\nexport function componentInks(\n palette: ThemePalette,\n recipes: Partial<ThemeRecipes> = {},\n): Array<{ role: keyof ThemePalette; what: string; fg: string; bg: string; needs: number }> {\n const color = (key: keyof ThemePalette) => (palette[key] ? diagnosticColor(palette[key]!) : undefined);\n const ink = color('ink'),\n surface = color('surface');\n if (!ink || !surface) return [];\n const clear: Color = [0, 0, 0, 0];\n const out: Array<{ role: keyof ThemePalette; what: string; fg: Color; bg: Color; needs: number }> = [];\n SERIES.forEach((role, index) => {\n const series = color(role);\n if (!series) return;\n if (recipes.board !== 'strip')\n out.push({\n role,\n what: \"a board card's category chip\",\n fg: mixColor(series, ink, 0.45),\n bg: over(mixColor(series, clear, 0.13), surface),\n needs: 4.5,\n });\n if (index < 7)\n out.push({\n role,\n what: \"an avatar's initials\",\n fg: mixColor(series, ink, 0.45),\n bg: mixColor(series, mixColor(ink, surface, 0.07), 0.15),\n needs: 4.5,\n });\n if (recipes.stat === 'icon' && index < 4)\n out.push({\n role,\n what: \"a stat card's mark\",\n fg: mixColor(series, ink, 0.8),\n bg: mixColor(series, surface, 0.12),\n needs: 3,\n });\n });\n /* The eighth avatar is the second and third series half and half: said by the second. */\n const second = color('chart2'),\n third = color('chart3');\n if (second && third) {\n const slot = mixColor(second, third, 0.5);\n out.push({\n role: 'chart2',\n what: \"the eighth avatar's initials (chart[1] and chart[2] half and half)\",\n fg: mixColor(slot, ink, 0.45),\n bg: mixColor(slot, mixColor(ink, surface, 0.07), 0.15),\n needs: 4.5,\n });\n }\n return out.map((item) => ({ ...item, fg: colorHex(over(item.fg, item.bg)), bg: colorHex(item.bg) }));\n}\n\n/**\n * The pairs a role is read in (CONTRAST_PAIRS), measured with `value` drawn in its place: what an author wrote for a\n * colour the page wears as another (a link written as the accent is drawn in the accent's ink), so the colour as\n * written is held to the rule its role is.\n */\nexport function rolePairs(\n palette: ThemePalette,\n role: keyof ThemePalette,\n value: string,\n): Array<{ background: keyof ThemePalette; needs: number; what?: string; ratio: number }> {\n const foreground = diagnosticColor(value);\n if (!foreground) return [];\n return CONTRAST_PAIRS.flatMap(([ink, background, needs, what]) => {\n const ground = palette[background];\n if (ink !== role || !ground || ((background === 'ground' || background === 'field') && ground === palette.surface)) return [];\n const backgroundColor = diagnosticColor(ground);\n const ratio = backgroundColor && measuredContrast(foreground, backgroundColor);\n return ratio === undefined || !ratio ? [] : [{ background, needs, ...(what ? { what } : {}), ratio }];\n });\n}\n\n/** One failed check: which theme, palette and mode, the role it is about, and the sentence for it. */\nexport interface ThemeFinding {\n themeId: string;\n paletteId: string;\n mode: 'light' | 'dark';\n /** The role (`accent`, `navMuted`, `chart3`); rolePath gives the spec's name for it (`nav.muted`, `chart[2]`). */\n token: keyof ThemePalette;\n /** What the check measures, in a few words: \"accent on surface\", \"chart2 beside chart3 for deuteranopia\". */\n check: string;\n ratio: number;\n needs: number;\n message: string;\n}\n\nexport function themeWarnings(themes: readonly ThemeDefinition[], own: { accent?: AppAccent } = {}): string[] {\n return themeFindings(themes, own).map((finding) => finding.message);\n}\n\n/** Every failed check of the themes; with `all`, every check measured, passed or not (a fix reads what it reaches). */\nexport function themeFindings(themes: readonly ThemeDefinition[], own: { accent?: AppAccent } = {}, all = false): ThemeFinding[] {\n return themes.flatMap((theme) =>\n ['default', ...(theme.palettes ?? []).map((item) => item.id)].flatMap((id) =>\n themeModes(theme).flatMap((mode) => {\n const definition = effectiveThemeDefinition(theme, id);\n const palette = withInks(effectiveThemePalette(definition[mode], mode === 'dark', own));\n const name = id === 'default' ? theme.label : `${theme.label} / ${theme.palettes!.find((item) => item.id === id)!.label}`;\n const at = { themeId: theme.id, paletteId: id, mode };\n return CONTRAST_PAIRS.flatMap(([foreground, background, needs, what]): ThemeFinding[] => {\n const a = palette[foreground],\n b = palette[background];\n // A page the colour of its panels is one ground, and one warning; a field the colour of its panel the same.\n if (!a || !b || ((background === 'ground' || background === 'field') && b === palette.surface)) return [];\n // With no edge there is no edge to measure: the fill's own distance from its grounds is (stateFindings).\n if (background === 'field' && foreground === 'lineControl' && palette.rulesControl === '0px') return [];\n const foregroundColor = diagnosticColor(a),\n backgroundColor = diagnosticColor(b);\n if (!foregroundColor || !backgroundColor) return [];\n const ratio = measuredContrast(foregroundColor, backgroundColor);\n if (ratio === undefined || (!all && ratio >= needs)) return [];\n const rule =\n needs === 4.5\n ? 'normal text needs 4.5:1'\n : foreground.startsWith('chart')\n ? 'a chart series needs 3:1'\n : foreground === 'focus'\n ? 'the focus ring needs 3:1'\n : \"a form control's edge needs 3:1\";\n return [\n {\n ...at,\n token: foreground,\n check: `${foreground} on ${background}${what ? ` (${what})` : ''}`,\n ratio,\n needs,\n message: `Theme \"${name}\" (${mode}): ${what ? `${what}, ` : ''}${rolePath(foreground)}/${rolePath(background)} contrast is ${ratio.toFixed(2)}:1; ${rule}.`,\n },\n ];\n }).concat(stateFindings(definition, name, at, palette, all));\n }),\n ),\n );\n}\n\n/** Readers can select any offered look, so App-wide overrides are checked on\n built-ins too. Without overrides retain the registered-theme warning scope. */\nexport function appThemeWarnings(\n themes: readonly ThemeDefinition[],\n own: { accent?: AppAccent; radius?: string; font?: string } = {},\n): string[] {\n const warnings = themeWarnings(own.accent ? [...THEME_PRESETS, ...themes] : themes, own);\n const accents =\n typeof own.accent === 'object'\n ? [\n ['accent.light', own.accent.light],\n ['accent.dark', own.accent.dark],\n ]\n : [['accent', own.accent]];\n for (const [key, value] of accents) {\n if (value && /\\burl\\s*\\(/i.test(value)) warnings.push(`App ui.theme.${key}: a CSS image URL is not a color; use a CSS color value.`);\n else if (value && !diagnosticColor(value))\n warnings.push(\n `App ui.theme.${key}: contrast was not checked for this legacy CSS color; use a hex color for numeric contrast diagnostics.`,\n );\n }\n if (own.radius) {\n const diagnostic = radiusDiagnostic(own.radius);\n if (diagnostic) warnings.push(`App ui.theme.radius: ${diagnostic}`);\n }\n return warnings;\n}\n\n/** Recognize literal border-radius syntax only. Expressions remain compatible;\n an unmeasured diagnostic is not a declaration that the browser rejects them. */\nfunction radiusDiagnostic(raw: string): string | undefined {\n const value = raw.trim().toLowerCase();\n if (/^(?:initial|inherit|unset|revert|revert-layer)$/.test(value)) return undefined;\n const unmeasured = 'validity was not checked for this legacy CSS radius expression; verify its computed border-radius in the browser.';\n if (/[()\\\\]/.test(value)) return unmeasured;\n const groups = value.split('/').map((group) => group.trim().split(/\\s+/));\n if (groups.length > 2 || groups.some((group) => group.length > 4 || !group[0])) return unmeasured;\n for (const token of groups.flat()) {\n const literal = /^([+-]?(?:\\d*\\.\\d+|\\d+)(?:e[+-]?\\d+)?)([a-z]+|%)?$/.exec(token);\n if (!literal) {\n if (/^[a-z-]+$/.test(token)) return 'an unrecognized bare keyword is not a CSS radius; use nonnegative lengths or percentages.';\n return unmeasured;\n }\n const number = Number(literal[1]);\n const unit = literal[2];\n // Unknown/future units are not rejected by this deliberately bounded check.\n if (\n unit &&\n !/^(?:%|px|cm|mm|q|in|pt|pc|em|rem|ex|rex|cap|rcap|ch|rch|ic|ric|lh|rlh|(?:s|l|d)?v(?:w|h|i|b|min|max)|cq(?:w|h|i|b|min|max))$/.test(\n unit,\n )\n )\n return unmeasured;\n if (number < 0) return 'a negative radius is not valid CSS; use a nonnegative CSS length.';\n if (!unit && number !== 0) return 'a nonzero unitless radius is not valid CSS; include a CSS length unit or percentage.';\n }\n return undefined;\n}\n\nfunction stateFindings(\n theme: ThemeDefinition,\n name: string,\n at: Pick<ThemeFinding, 'themeId' | 'paletteId' | 'mode'>,\n palette: ThemePalette,\n all: boolean,\n): ThemeFinding[] {\n const result: ThemeFinding[] = [];\n const mode = at.mode;\n const recipes = theme.recipes ?? {};\n const color = (key: keyof ThemePalette) => (palette[key] ? diagnosticColor(palette[key]!) : undefined);\n const check = (\n label: string,\n foreground: Color | undefined,\n background: Color | undefined,\n needs = 4.5,\n token?: keyof ThemePalette,\n said?: string,\n ) => {\n if (!foreground || !background) return;\n const ratio = measuredContrast(foreground, background);\n if (ratio === undefined || (!all && ratio >= needs)) return;\n const rule = said ?? (needs === 4.5 ? 'normal text needs 4.5:1' : 'the focus ring needs 3:1');\n result.push({\n ...at,\n token: token ?? (/^[a-zA-Z0-9]+/.exec(label)![0] as keyof ThemePalette),\n check: label,\n ratio,\n needs,\n message: `Theme \"${name}\" (${mode}): ${label.replace(/^\\w+/, rolePath).replace(/\\/(\\w+)$/, (_, role: string) => `/${rolePath(role)}`)} contrast is ${ratio.toFixed(2)}:1; ${rule}.`,\n });\n };\n const surface = color('surface'),\n subtle = color('raised'),\n text = color('ink');\n /* A field with no edge (shape.border.control 0px) is told from the page by its fill alone, so the fill must stand\n apart from both grounds it sits on (EDGELESS_FIELD_DELTA says how far). A field that names no fill is the surface,\n which is no distance from the surface at all. */\n if (palette.rulesControl === '0px')\n for (const ground of ['ground', 'surface'] as const) {\n if (ground === 'ground' && (!palette.ground || palette.ground === palette.surface)) continue;\n check(\n `field/${ground}`,\n color('field') ?? surface,\n color(ground),\n EDGELESS_FIELD_DELTA,\n 'field',\n `a field with no edge (shape.border.control 0px) must stand ${EDGELESS_FIELD_DELTA}:1 apart from the ${ground} by its fill, so give colors.field a fill that does or keep a 1px edge`,\n );\n }\n /* The hover and row ground (--raised) carries every kind of text a row\n holds, not only the full ink the pair list checks there: a muted date, a\n faint count, a link. */\n for (const key of ['inkMuted', 'inkSubtle'] as const) check(`${key}/raised`, color(key), subtle);\n /* A tone chip (features.css .tone-*) writes 78% of the meaning colour into\n the text ink, on a 12% tint of it over the panel. Neither end of that is\n a palette colour, so no pair above could see it. */\n for (const role of ['statusGood', 'statusWarning', 'statusCritical', 'statusInfo'] as const) {\n const ink = color(role);\n if (!ink || !text || !surface) continue;\n check(`${role} tone chip`, mixColor(ink, text, 0.78), over(mixColor(ink, [0, 0, 0, 0], 0.12), surface));\n }\n /* A series is drawn as words and marks too, mixed into the ink (componentInks): measured as each is drawn. */\n for (const item of componentInks(palette, recipes))\n check(\n `${item.role} as ${item.what}`,\n diagnosticColor(item.fg),\n diagnosticColor(item.bg),\n item.needs,\n item.role,\n item.needs === 3 ? 'a mark needs 3:1' : undefined,\n );\n /* A danger banner (a page's errors, pending edits) writes the danger colour\n on a 10% tint of it mixed into the panel. */\n const danger = color('statusCritical');\n if (danger && surface) check('statusCritical/danger banner', danger, mixColor(danger, surface, 0.1));\n /* A matrix's heaviest cell says its figure in the ink on the accent's tint, drawn no stronger than heatTop allows:\n only a palette whose ink is too near its accent for even the floor's tint is short here. */\n const accent = color('accent');\n if (accent && surface)\n check(\n \"ink/accent's heat tint (a matrix's heaviest cell)\",\n text,\n over(mixColor(accent, [0, 0, 0, 0], heatTop(palette)), surface),\n 4.5,\n 'accent',\n );\n /* A printed page is white paper whatever the page's ground (style.css's print layer), so a light palette's accent\n and links are read there too: one drawn for a dark \"light\" mode reads on its panel and vanishes on paper (C12).\n A theme that says it is dark has no light palette to be read as paper: its one set of colors is judged as dark. */\n if (mode === 'light' && theme.scheme !== 'dark-only')\n for (const key of ['accentInk', 'link'] as const) {\n /* Darker than its panel, a color reads better on paper than there: the panel's own pair already says it. */\n const value = palette[key];\n const hex = (css: string | undefined) => css !== undefined && /^#[0-9a-f]{6}$/i.test(css);\n if (!hex(value) || !hex(palette.surface) || contrastRatio(value!, '#ffffff') < contrastRatio(palette.surface!, '#ffffff'))\n check(`${key} on printed paper`, color(key), [1, 1, 1, 1], 4.5, key);\n }\n const soft = color('accentSoft');\n for (const ground of ['surface', 'ground'] as const) {\n const base = color(ground);\n if (ground === 'ground' && palette.ground === palette.surface) continue;\n // Selected menu items and pressed controls use accent ink on this tint.\n check(`accentInk/accentSoft over ${ground}`, color('accentInk'), soft && base ? over(soft, base) : undefined);\n }\n // A chosen row, a chosen card and a current step keep their words in the ink on that tint.\n check('ink/accentSoft over surface (a chosen row)', text, soft && surface ? over(soft, surface) : undefined);\n const nav = color('navGround'),\n navInk = color('navInk');\n // visualThemes.css topbar item hover/current and expanded group overlays.\n const navGround = (weight: number) => (nav && navInk ? over(mixColor(navInk, [0, 0, 0, 0], weight), nav) : undefined);\n check('navAccent/navigation hover', color('navAccent'), navGround(0.05));\n // The current entry's block, when a theme names one: its words on it (structure.css).\n if (palette.navCurrent) check('navOnCurrent/navCurrent', color('navOnCurrent') ?? navInk, color('navCurrent'));\n check('navInk/navigation expanded', navInk, navGround(0.07));\n /* The pill menu marks its current entry with the accent's tint, laid on the menu's own ground, in the menu's\n accent (lumenLook.css --current-ground): a pair neither the menu's nor the page's checks see. A theme that\n names its own block (nav.current) is measured on that instead. */\n if (recipes.nav === 'pill' && !palette.navCurrent)\n check(\n \"navAccent/accentSoft over navGround (the pill menu's current entry)\",\n color('navAccent'),\n soft && nav ? over(soft, nav) : undefined,\n );\n /* The search is a field on the surface in the page's ink (themeFamilies.css), a pair the page's checks read; the\n dark bar alone draws it on an 8% ink tint of its own ground in its muted ink (brandLook.css). */\n if (recipes.bar === 'dark') check('navMuted/navigation search', color('navMuted'), navGround(0.08));\n /* The dark bar draws its keys — New, the customer chooser — on the same tint in the menu's ink (brandLook.css), and\n a phone's menu, search and New marks in its muted ink on its ground (visualThemes.css, structure.css): until\n 0.9.6 those stayed the page's keys and greys, about 1.5:1 on a green menu, and no pair here read them. */\n if (recipes.bar === 'dark') check('navInk/navigation bar button', navInk, navGround(0.08));\n if (recipes.table === 'tinted') {\n const accent = color('accent'),\n surface = color('surface');\n const hover = accent && surface ? mixColor(accent, surface, 0.04) : undefined;\n check('ink/tinted row hover', color('ink'), hover);\n check('link/tinted row hover', color('link'), hover);\n }\n /* A banded table lays every other row on the band (brandLook.css): whatever a row says is said on it. */\n if (recipes.table === 'banded' && palette.band)\n for (const key of [\n 'ink',\n 'inkMuted',\n 'inkSubtle',\n 'link',\n 'accentInk',\n 'statusGood',\n 'statusWarning',\n 'statusCritical',\n 'statusInfo',\n ] as const)\n check(`${key}/band`, color(key), color('band'), 4.5, 'band');\n /* A pattern is the theme's line color at its strength over the ground it is drawn on (surfacesLook.css): the words\n on the page, or in the menu, are measured where a line crosses under them, the darkest point of it. */\n const patterned = (place: 'ground' | 'nav') => {\n const pattern = theme.surface?.[place];\n if (!pattern) return;\n const line = color(place === 'nav' ? 'navLine' : 'line') ?? color('line');\n const ground = place === 'nav' ? color('navGround') : (color('ground') ?? surface);\n if (!line || !ground) return;\n const under = over(mixColor(line, [line[0], line[1], line[2], 0], patternStrength(pattern)), ground);\n const where = place === 'nav' ? `the menu's ${pattern.kind} pattern` : `the page's ${pattern.kind} pattern`;\n const inks = place === 'nav' ? (['navInk', 'navMuted'] as const) : (['ink', 'inkMuted', 'inkSubtle', 'link', 'accentInk'] as const);\n for (const key of inks) check(`${key} on ${where}`, color(key), under, 4.5, place === 'nav' ? 'navLine' : 'line');\n };\n patterned('ground');\n patterned('nav');\n /* A part's words on its ground (theme-part): its own ink on its own ground, or the one it is drawn on unsaid. */\n for (const [part, style] of Object.entries(theme.parts ?? {})) {\n if (!style || (style.ground === undefined && style.ink === undefined)) continue;\n const ink = style.ink ? pathRole(style.ink) : partInk(part);\n const ground = style.ground ? pathRole(style.ground) : partGround(part, palette);\n if (!ink || !ground) continue;\n const said = (role: string) => rolePath(role);\n const label = `parts.${part}: ${ink} on ${ground}`;\n const foreground = color(ink as keyof ThemePalette),\n background = color(ground as keyof ThemePalette);\n if (!foreground || !background) continue;\n const ratio = measuredContrast(foreground, background);\n if (ratio === undefined || (!all && ratio >= 4.5)) continue;\n result.push({\n ...at,\n token: (style.ground ? ground : ink) as keyof ThemePalette,\n check: label,\n ratio,\n needs: 4.5,\n message: `Theme \"${name}\" (${mode}): parts.${part}: ${said(ink)} on ${said(ground)} contrast is ${ratio.toFixed(2)}:1; normal text needs 4.5:1.`,\n });\n }\n return result;\n}\n\n/** The ink a part's words are drawn in when its style says none: the menu's for the menu's parts, the link's for a\n link; a primary button's is the ink drawn on the accent fill. */\nfunction partInk(part: string): string | undefined {\n if (part === 'button.primary') return 'onAccent';\n if (/^(?:nav|bar|logo)\\b/.test(part)) return 'navInk';\n return part === 'link' ? 'link' : 'ink';\n}\n/** The ground a part is drawn on when its style says none. */\nfunction partGround(part: string, palette: ThemePalette): string {\n if (/^(?:nav|bar|logo)\\b/.test(part)) return 'navGround';\n if (part === 'button.primary') return 'accent';\n return (part === 'ground' || part === 'page.title') && palette.ground ? 'ground' : 'surface';\n}\n","/**\n * `buildTheme`: a spec already known to be good, assembled into the\n * ThemeDefinition every surface wears — the colour roles, the colours a theme\n * leaves unsaid, a built-in's colours under an author's. The built-in looks\n * are built with it ahead of time (scripts/theme-presets.mjs); an App's or a\n * tenant's theme is checked by themeSpec.ts's `compileTheme` and then built\n * here, on the server or in the branding form, never at first paint.\n */\nimport { mergeType } from '@mistriwork/engine/shared/themeType.js';\nimport type { ThemeColorsOver, ThemeDefinition, ThemeDensity, ThemePalette, ThemeSpec } from '@mistriwork/engine/schema/theme.js';\nimport { DEFAULT_RECIPES } from '@mistriwork/engine/shared/themeRecipes.js';\nimport { ROUND } from '@mistriwork/engine/shared/themeTokens.js';\n\n/* ---------- colors: names by purpose, and the roles they fill ---------- */\n\n/** A spec color's path and the role it fills in a compiled palette: the same name, flattened (`nav.ground` is\n `navGround`), and worn as the custom property of that name (PALETTE_TOKENS). */\nexport const COLOR_ROLES: ReadonlyArray<readonly [string, keyof ThemePalette]> = [\n ['ground', 'ground'],\n ['surface', 'surface'],\n ['field', 'field'],\n ['raised', 'raised'],\n ['ink', 'ink'],\n ['inkMuted', 'inkMuted'],\n ['inkSubtle', 'inkSubtle'],\n ['line', 'line'],\n ['lineStrong', 'lineStrong'],\n ['lineControl', 'lineControl'],\n ['accent', 'accent'],\n ['onAccent', 'onAccent'],\n ['accentInk', 'accentInk'],\n ['accentSoft', 'accentSoft'],\n ['primary', 'primary'],\n ['onPrimary', 'onPrimary'],\n ['link', 'link'],\n ['focus', 'focus'],\n ['status.good', 'statusGood'],\n ['status.warning', 'statusWarning'],\n ['status.critical', 'statusCritical'],\n ['status.info', 'statusInfo'],\n ['nav.ground', 'navGround'],\n ['nav.ink', 'navInk'],\n ['nav.muted', 'navMuted'],\n ['nav.line', 'navLine'],\n ['nav.accent', 'navAccent'],\n ['nav.current', 'navCurrent'],\n ['nav.onCurrent', 'navOnCurrent'],\n ['rules.section', 'rulesSection'],\n ['rules.head', 'rulesHead'],\n ['shadow', 'shadow'],\n ['band', 'band'],\n ...[1, 2, 3, 4, 5, 6, 7, 8].map((n) => [`chart[${n - 1}]`, `chart${n}` as keyof ThemePalette] as const),\n];\n\n/* ---------- the colors a theme leaves unsaid ---------- */\n\nfunction channels(value: string): [number, number, number] {\n return [1, 3, 5].map((start) => parseInt(value.slice(start, start + 2), 16)) as [number, number, number];\n}\n/** `weight` of `a` over `b`, in sRGB, as hex: the derivations below are colors an author could have written. */\nexport function mixHex(a: string, b: string, weight: number): string {\n const x = channels(a),\n y = channels(b);\n return `#${x\n .map((value, index) =>\n Math.round(value * weight + y[index]! * (1 - weight))\n .toString(16)\n .padStart(2, '0'),\n )\n .join('')}`;\n}\n\n/** What a theme that extends nothing starts from where it says nothing: meaning colors that read on white and on\n near-black, and Lumen's chart series after the theme's own accent, spaced for color-blind readers. */\nconst STARTING = {\n light: {\n statusGood: '#117a38',\n statusWarning: '#a84d07',\n statusCritical: '#c81e1e',\n statusInfo: '#0369a1',\n chart: ['#2557e0', '#9a6cf7', '#0e9a6c', '#d97706', '#e0428a', '#0e8fb0', '#a3510b', '#94a3b8'],\n },\n dark: {\n statusGood: '#4ade80',\n statusWarning: '#fbbf24',\n statusCritical: '#f87171',\n statusInfo: '#7dd3fc',\n chart: ['#3559e6', '#9a78ec', '#12a678', '#c88317', '#e0559b', '#1597b9', '#d26b3b', '#6b7694'],\n },\n} as const;\n\n/** Every role, from the four a theme must name. Mixes toward the surface keep a derived ink readable on it; the validator measures them anyway. */\nfunction derive(given: Partial<ThemePalette>, mode: 'light' | 'dark'): ThemePalette {\n const surface = given.surface!,\n ink = given.ink!,\n accent = given.accent!;\n const start = STARTING[mode];\n const palette: ThemePalette = {\n ground: given.ground,\n surface,\n raised: mixHex(ink, surface, 0.04),\n line: mixHex(ink, surface, 0.16),\n lineStrong: mixHex(ink, surface, 0.56),\n lineControl: mixHex(ink, surface, 0.52),\n ink,\n inkMuted: mixHex(ink, surface, 0.82),\n inkSubtle: mixHex(ink, surface, 0.7),\n accent,\n accentSoft: mixHex(accent, surface, 0.12),\n link: accent,\n focus: accent,\n statusGood: start.statusGood,\n statusWarning: start.statusWarning,\n statusCritical: start.statusCritical,\n statusInfo: start.statusInfo,\n navGround: given.surface,\n navInk: ink,\n navMuted: undefined,\n navLine: undefined,\n navAccent: undefined,\n chart1: accent,\n ...Object.fromEntries(start.chart.slice(1).map((color, index) => [`chart${index + 2}`, color])),\n };\n const merged = { ...palette, ...given };\n merged.navGround = given.navGround ?? merged.surface;\n merged.navInk = given.navInk ?? merged.ink;\n merged.navMuted = given.navMuted ?? merged.inkSubtle;\n merged.navLine = given.navLine ?? merged.line;\n merged.navAccent = given.navAccent ?? merged.accent;\n return merged;\n}\n\n/** A built-in's colors under an author's: an accent said without its tint, link, focus, menu accent or first\n series takes them along where the built-in had them equal to its own accent — they were that accent, not\n a color of their own. */\nfunction overlay(base: ThemePalette, over: Partial<ThemePalette>): ThemePalette {\n const merged: ThemePalette = { ...base, ...over };\n if (over.accent !== undefined && over.accent !== base.accent) {\n const surface = merged.surface;\n if (over.accentSoft === undefined) merged.accentSoft = mixHex(over.accent, surface, 0.12);\n /* The inks a base chose for its own accent read on that fill, not this one: they are chosen again where worn. */\n if (over.onAccent === undefined) delete merged.onAccent;\n if (over.accentInk === undefined) delete merged.accentInk;\n if (over.focus === undefined) merged.focus = over.accent;\n for (const role of ['link', 'navAccent'] as const)\n if (over[role] === undefined && base[role] === base.accent) merged[role] = over.accent;\n /* The first series and the metric sparkline are the brand's colour in every look, whether or not the look drew\n its own first hue (Mono's accent is the ink, its first series a blue): a theme that names an accent and no\n chart wears it, so a chart is never the look's colour beside the App's. */\n if (over.chart1 === undefined) merged.chart1 = over.accent;\n }\n /* A form's primary action was the built-in's ink or its accent, not a color of its own: a theme that says another\n of that color and no `primary` takes it along, and the words on it are chosen again where worn. */\n if (over.primary === undefined && base.primary !== undefined)\n for (const role of ['ink', 'accent'] as const)\n if (over[role] !== undefined && over[role] !== base[role] && base.primary === base[role]) {\n merged.primary = over[role];\n if (over.onPrimary === undefined) delete merged.onPrimary;\n }\n if (over.primary !== undefined && over.primary !== base.primary && over.onPrimary === undefined) delete merged.onPrimary;\n return merged;\n}\n\n/** One mode's colours, by purpose, to roles, as written. */\nfunction rolesOf(colors: ThemeColorsOver | undefined): Partial<ThemePalette> {\n const out: Partial<Record<keyof ThemePalette, string>> = {};\n if (!colors) return {};\n const source = colors as Record<string, unknown>;\n for (const [path, role] of COLOR_ROLES) {\n if (path.startsWith('chart[')) continue;\n const [group, key] = path.split('.') as [string, string | undefined];\n const value = key === undefined ? source[group] : (source[group] as Record<string, unknown> | undefined)?.[key];\n if (typeof value === 'string') out[role] = value.trim().toLowerCase();\n }\n colors.chart?.forEach((value, index) => {\n if (typeof value === 'string') out[`chart${index + 1}` as keyof ThemePalette] = value.trim().toLowerCase();\n });\n return out as unknown as Partial<ThemePalette>;\n}\n\n/** A spec that is known to be good — a built-in, or one `compileTheme` has checked — as the definition it compiles\n to. `base` is the built-in it extends, resolved by the caller. */\nexport function buildTheme(spec: ThemeSpec, base?: ThemeDefinition): ThemeDefinition {\n const colors = (spec.colors ?? {}) as { light?: ThemeColorsOver; dark?: ThemeColorsOver };\n /* A rule's and a control's width are shape, said once, and worn as each mode's palette roles they always were. */\n const border = spec.shape?.border;\n const widths: Partial<ThemePalette> = {\n ...(border?.rule ? { rulesWeight: border.rule } : {}),\n ...(border?.control ? { rulesControl: border.control } : {}),\n };\n /* A dark-only theme has one set of colors, worn in both slots: nothing downstream has a light mode to be missing. */\n const darkOnly = spec.scheme === 'dark-only';\n const mode = (name: 'light' | 'dark'): ThemePalette => {\n const given = { ...rolesOf(colors[name]), ...widths };\n return base ? overlay(base[name], given) : derive(given, name);\n };\n /* `round` is a pill's corner: a length past any box's (themeTokens.ts ROUND). */\n const radius = Object.fromEntries(\n Object.entries(spec.shape?.radius ?? {}).map(([key, length]) => [key, length === 'round' ? ROUND : length]),\n );\n const ownBorder = {\n ...(border?.panel ? { panel: border.panel } : {}),\n ...(border?.focus ? { focus: border.focus } : {}),\n };\n const height = spec.shape?.height?.control ? { control: spec.shape.height.control } : undefined;\n const palettes = spec.palettes?.map((variant) => ({\n id: variant.id,\n label: variant.label.trim(),\n light: rolesOf(darkOnly ? variant.dark : variant.light),\n dark: rolesOf(variant.dark),\n }));\n const font = spec.type?.font;\n const fonts = typeof font === 'string' ? { ui: font, data: font, heading: font } : font;\n const density = spec.density ?? base?.density;\n const motion = spec.motion ?? base?.motion;\n const rhythm = spec.rhythm ?? base?.rhythm;\n const type = mergeType(base?.type, spec.type && { base: spec.type.base, roles: spec.type.roles });\n const steps = typeof spec.shape?.elevation === 'object' ? spec.shape.elevation : undefined;\n const lifts = base?.lifts || steps ? { ...base?.lifts, ...steps } : undefined;\n /* A face is a theme's own file: a built-in ships none (Lumen's Inter is its sheet's); a theme value it extends (a\n package's look) may, and its faces stand under this theme's own, which win at a family, weight and style. */\n const own = spec.type?.faces?.map((face) => ({\n family: face.family,\n src: face.src,\n weight: String(face.weight ?? 400),\n style: face.style ?? ('normal' as const),\n ...(face.subset === false ? { subset: false as const } : {}),\n }));\n const cut = (face: { family: string; weight: string; style: string }) => `${face.family.toLowerCase()} ${face.weight} ${face.style}`;\n const faces = base?.faces?.length ? [...base.faces.filter((face) => !own?.some((mine) => cut(mine) === cut(face))), ...(own ?? [])] : own;\n /* A theme's stylesheet is its own, or the one it extends: a built-in has none. */\n const stylesheet = spec.stylesheet ? { src: spec.stylesheet } : base?.stylesheet;\n /* A pattern says its strength (1 when omitted); a part's style lies over the one it extends, property by property. */\n const patterns = Object.entries(spec.surface ?? {}).map(([place, pattern]) => [\n place,\n { kind: pattern!.kind, strength: pattern!.strength ?? 1 },\n ]);\n const surface = base?.surface || patterns.length ? { ...base?.surface, ...Object.fromEntries(patterns) } : undefined;\n let parts: ThemeDefinition['parts'] = base?.parts ? { ...base.parts } : undefined;\n for (const [part, style] of Object.entries(spec.parts ?? {}))\n parts = { ...parts, [part]: { ...parts?.[part as keyof typeof parts], ...style } };\n const icons = spec.type?.icons ?? base?.icons;\n return {\n id: spec.id,\n label: spec.label.trim(),\n ...(base?.base ? { base: base.base } : {}),\n light: mode(darkOnly ? 'dark' : 'light'),\n dark: mode('dark'),\n ...((spec.scheme ?? base?.scheme) ? { scheme: (spec.scheme ?? base?.scheme)! } : {}),\n ...(palettes === undefined ? (base?.palettes && spec.colors === undefined ? { palettes: base.palettes } : {}) : { palettes }),\n ...(spec.defaultPalette === undefined ? {} : { defaultPalette: spec.defaultPalette }),\n fonts: { ...base?.fonts, ...fonts },\n ...(faces?.length ? { faces } : {}),\n radius: { control: '4px', field: '4px', ...base?.radius, ...radius },\n ...(base?.border || Object.keys(ownBorder).length ? { border: { ...base?.border, ...ownBorder } } : {}),\n ...(base?.height || height ? { height: { ...base?.height, ...height } } : {}),\n recipes: { ...DEFAULT_RECIPES, ...base?.recipes, ...spec.recipes },\n /* A preset is the look's own drawing of its lifts; `{ card, float }` steps over the base's, which stays. */\n elevation: (typeof spec.shape?.elevation === 'string' ? spec.shape.elevation : undefined) ?? base?.elevation ?? 'hairline',\n ...(lifts ? { lifts } : {}),\n ...(type ? { type } : {}),\n ...(density ? { density: density as ThemeDensity } : {}),\n ...(motion ? { motion } : {}),\n ...(rhythm ? { rhythm } : {}),\n /* The theme's own suggestion only: a built-in suggests none, so there is nothing to inherit. */\n ...(spec.shell?.placement ? { shell: { placement: spec.shell.placement } } : {}),\n ...(surface ? { surface } : {}),\n ...(parts ? { parts } : {}),\n ...(icons ? { icons } : {}),\n ...(stylesheet ? { stylesheet } : {}),\n };\n}\n","/**\n * `defineTheme`: a theme by purpose — colors for what they are for, a shape, a\n * type, a density and a pick from a closed menu of treatments — compiled to the\n * ThemeDefinition every surface already wears. The built-in looks are written in\n * it (themePresets.ts), so the menu is exactly what they do and an App's theme\n * can only ask for what one of them already draws. There is no CSS in a theme:\n * each recipe is a `data-look-<part>` attribute the shipped sheets answer.\n *\n * This module checks; themeBuild.ts builds. The built-in looks are built there\n * without these checks, because the browser wears them at first paint and the\n * checks' messages are weight no reader needs; test/theme-spec.test.ts holds\n * every built-in to them. An App's or a tenant's theme comes through here.\n */\nimport type {\n ThemeColorsOver,\n ThemeDefinition,\n ThemeDensity,\n ThemePalette,\n ThemePaletteVariant,\n ThemeSpec,\n} from '@mistriwork/engine/schema/theme.js';\nimport { buildTheme, COLOR_ROLES } from './themeBuild.js';\nimport {\n BORDER_MENU,\n CONTROL_HEIGHT,\n controlHeightFits,\n cornerFits,\n RADIUS_MAX,\n ROUND_CORNERS,\n rolePath,\n} from '@mistriwork/engine/shared/themeTokens.js';\nimport {\n DENSITIES,\n ELEVATIONS,\n LIFTS,\n MOTIONS,\n RECIPE_MENU,\n RECIPES_RENAMED,\n RHYTHMS,\n RECIPE_PARTS,\n type ResolvedRecipes,\n} from '@mistriwork/engine/shared/themeRecipes.js';\nimport { TYPE_MOVED, typeOf } from '@mistriwork/engine/shared/themeType.js';\nimport { parseScheme, THEME_PRESETS, themePreset } from '@mistriwork/engine/shared/themePresets.js';\nimport { iconsOf, partsOf, surfaceOf } from '@mistriwork/engine/shared/themePartsCheck.js';\n\n/* ---------- colors: names by purpose, and the roles they fill ---------- */\n\n/** The spec's name for a role, for a message an author can act on: `status.critical`, not `statusCritical`. */\nexport function colorPath(role: string): string {\n return rolePath(role);\n}\n\nconst HEX = /^#[0-9a-f]{6}$/i;\nconst GROUPS = {\n status: ['good', 'warning', 'critical', 'info'],\n nav: ['ground', 'ink', 'muted', 'line', 'accent', 'current', 'onCurrent'],\n rules: ['section', 'head'],\n} as const;\nconst FLAT = [\n 'ground',\n 'surface',\n 'field',\n 'raised',\n 'ink',\n 'inkMuted',\n 'inkSubtle',\n 'line',\n 'lineStrong',\n 'lineControl',\n 'accent',\n 'onAccent',\n 'accentInk',\n 'accentSoft',\n 'primary',\n 'onPrimary',\n 'link',\n 'focus',\n 'shadow',\n 'band',\n];\n\n/** The names a color went by before the spec's were the only ones (0.9.2): the palette's, which the sheets and the\n compiled theme used. There is one name per color now, and a theme that says an old one is told the new. */\nconst OLD_NAMES: Readonly<Record<string, string>> = {\n background: 'ground',\n subtle: 'raised',\n border: 'line',\n borderStrong: 'lineStrong',\n borderControl: 'lineControl',\n text: 'ink',\n textMuted: 'inkMuted',\n textSubtle: 'inkSubtle',\n positive: 'status.good',\n warning: 'status.warning',\n negative: 'status.critical',\n key: 'status.info',\n navSurface: 'nav.ground',\n navText: 'nav.ink',\n navMuted: 'nav.muted',\n navBorder: 'nav.line',\n navAccent: 'nav.accent',\n navCurrent: 'nav.current',\n navCurrentText: 'nav.onCurrent',\n sectionRule: 'rules.section',\n headRule: 'rules.head',\n ruleWeight: 'rules.weight',\n};\n/** The widths that were colors' neighbors until 0.9.4: a rule's and a control's edge are shape, said once for both modes. */\nconst MOVED_TO_SHAPE: Readonly<Record<string, string>> = { weight: 'shape.border.rule', control: 'shape.border.control' };\nfunction renamed(colors: Record<string, unknown>, where: string) {\n for (const key of Object.keys(colors))\n if (Object.hasOwn(OLD_NAMES, key)) throw new Error(`${where}.${key} is now ${where}.${OLD_NAMES[key]}.`);\n /* Renamed before 1.0: words on a fill are `on<Fill>` (onAccent), and `<color>Ink` is a color used as text (accentInk). */\n const nav = colors.nav;\n if (nav && typeof nav === 'object' && Object.hasOwn(nav, 'currentInk'))\n throw new Error(`${where}.nav.currentInk is now ${where}.nav.onCurrent: the words on nav.current.`);\n const rules = colors.rules;\n if (rules && typeof rules === 'object')\n for (const key of Object.keys(rules))\n if (Object.hasOwn(MOVED_TO_SHAPE, key)) throw new Error(`${where}.rules.${key} is now ${MOVED_TO_SHAPE[key]}.`);\n}\n\nfunction record(raw: unknown, where: string): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${where} must be an object.`);\n return raw as Record<string, unknown>;\n}\nfunction only(raw: Record<string, unknown>, keys: readonly string[], where: string) {\n for (const key of Object.keys(raw)) if (!keys.includes(key)) throw new Error(`${where} has no \"${key}\". It takes: ${keys.join(', ')}.`);\n}\nfunction hex(value: unknown, where: string): string {\n if (typeof value !== 'string' || !HEX.test(value.trim())) throw new Error(`${where} must be a six-digit hex color like #1d4ed8.`);\n return value.trim().toLowerCase();\n}\n\n/** One mode's colors, by purpose, to roles. `required` names the four a theme that extends nothing must say. */\nfunction colorsToRoles(raw: unknown, where: string, required: boolean): Partial<ThemePalette> {\n if (raw === undefined && !required) return {};\n const colors = record(raw, where);\n renamed(colors, where);\n only(colors, [...FLAT, 'status', 'nav', 'rules', 'chart'], where);\n const out: Partial<Record<keyof ThemePalette, string>> = {};\n for (const [path, role] of COLOR_ROLES) {\n const [group, key] = path.split('.') as [string, string | undefined];\n if (path.startsWith('chart[')) continue;\n if (key === undefined) {\n if (colors[group] !== undefined) out[role] = hex(colors[group], `${where}.${group}`);\n continue;\n }\n const inner = colors[group];\n if (inner === undefined) continue;\n const values = record(inner, `${where}.${group}`);\n only(values, GROUPS[group as keyof typeof GROUPS], `${where}.${group}`);\n if (values[key] === undefined) continue;\n out[role] = hex(values[key], `${where}.${path}`);\n }\n if (colors.chart !== undefined) {\n if (!Array.isArray(colors.chart) || colors.chart.length > 8) throw new Error(`${where}.chart must be at most eight colors.`);\n colors.chart.forEach((value, index) => {\n if (value === null && !required) return;\n out[`chart${index + 1}` as keyof ThemePalette] = hex(value, `${where}.chart[${index}]`);\n });\n }\n if (required)\n for (const key of ['ground', 'surface', 'ink', 'accent'])\n if (colors[key] === undefined) throw new Error(`${where}.${key} is required.`);\n /* The words on a fill are said with the fill: on their own they would sit on whichever color a look chose. */\n if (colors.onPrimary !== undefined && colors.primary === undefined)\n throw new Error(`${where}.onPrimary is the words on ${where}.primary: name primary beside it.`);\n return out as Partial<ThemePalette>;\n}\n\n/* ---------- the spec ---------- */\n\nconst THEME_ID = /^[a-z][a-z0-9-]{0,47}$/;\n\nconst FONT = /^[\\w\\s'\",.-]+$/;\nconst SPEC_KEYS = [\n 'kind',\n 'id',\n 'label',\n 'extends',\n 'colors',\n 'palettes',\n 'defaultPalette',\n 'scheme',\n 'shape',\n 'type',\n 'density',\n 'motion',\n 'rhythm',\n 'recipes',\n 'shell',\n 'surface',\n 'parts',\n 'stylesheet',\n 'contract',\n 'from',\n];\n\n/** The theme contract this mistri reads (docs: theme-contract). A package says the one it was written to. */\nconst THEME_CONTRACT = 1;\n/* A stylesheet beside the theme, downward only: `./theme.css`, `./looks/ledger.css`. The server holds the resolved\n file to the theme's directory as well (appBrand.ts), so a link out of it is refused there. */\nconst SHEET_SRC = /^\\.\\/(?:[\\w.-]+\\/)*[\\w.-]+\\.css$/;\n\n/**\n * The directory a theme's files are read from (its faces and its stylesheet), where it is not the App's: a package's,\n * from its `from`. Kept here rather than on the definition, which is sent to the browser and written into a build:\n * a path on the machine is neither's to carry. Keyed by the face or the stylesheet record, so one inherited from a\n * package's theme is read from the package.\n */\nconst FILE_ROOTS = new WeakMap<object, string>();\nexport function themeFileRoot(file: object): string | undefined {\n return FILE_ROOTS.get(file);\n}\n/** The theme a compiled theme extends when that is a theme value (`extends: ledger`), for the validator. */\nconst PARENTS = new WeakMap<ThemeDefinition, ThemeDefinition>();\nfunction themeParent(theme: ThemeDefinition): ThemeDefinition | undefined {\n return PARENTS.get(theme);\n}\n\n/** A theme spec checked and compiled. `builtIn` resolves `extends`; `reserved` are ids it may not take. */\nexport function compileTheme(\n raw: unknown,\n where: string,\n builtIn: (id: string) => ThemeDefinition | undefined,\n reserved: ReadonlySet<string> = new Set(),\n options: ThemeCompileOptions = {},\n): ThemeDefinition {\n const spec = record(raw, where) as Record<string, unknown> & Partial<ThemeSpec>;\n // The key was `palette` until 1.0; `palettes` and `ui.theme.palette` name variations, so the colors moved out of their way.\n if ('palette' in spec) throw new Error(`${where}.palette is now colors: { light, dark }.`);\n only(spec, SPEC_KEYS, where);\n if (spec.kind !== undefined && spec.kind !== 'theme') throw new Error(`${where}.kind must be \"theme\" (write it with defineTheme).`);\n if (typeof spec.id !== 'string' || !THEME_ID.test(spec.id) || reserved.has(spec.id))\n throw new Error(`${where}.id must be a unique lowercase slug, not a built-in look's name.`);\n if (typeof spec.label !== 'string' || !spec.label.trim() || spec.label.trim().length > 60)\n throw new Error(`${where}.label must have 1 to 60 characters.`);\n /* A package's theme says the contract it was written to, and where its files are (theme-contract). */\n if (spec.contract !== undefined && spec.contract !== THEME_CONTRACT)\n throw new Error(\n `${where}.contract is ${JSON.stringify(spec.contract)}: this mistri reads theme contract ${THEME_CONTRACT}, and a theme written to another is refused. Use the package's release for contract ${THEME_CONTRACT}, or upgrade mistri.`,\n );\n let root: string | undefined;\n if (spec.from !== undefined) {\n if (typeof spec.from !== 'string' || !spec.from.startsWith('file://'))\n throw new Error(`${where}.from must be the theme module's import.meta.url, a file: URL.`);\n if (spec.contract === undefined)\n throw new Error(\n `${where}.from names a package's directory, and a package says the contract it was written to: contract: ${THEME_CONTRACT}.`,\n );\n if (!options.faces) throw new Error(`${where}.from reads files from a package, and this theme has no server to read them.`);\n root = decodeURIComponent(new URL('.', spec.from).pathname).replace(/\\/$/, '');\n }\n let base: ThemeDefinition | undefined;\n if (spec.extends !== undefined && typeof spec.extends === 'object' && spec.extends !== null) {\n /* A theme value (a package's look): itself extending a built-in or nothing, so a chain is two deep at most. */\n const parent = spec.extends as unknown as Record<string, unknown>;\n if (parent.kind !== 'theme') throw new Error(`${where}.extends must name a built-in look or be a theme written with defineTheme.`);\n if (parent.extends !== undefined && typeof parent.extends !== 'string')\n throw new Error(\n `${where}.extends is a theme that extends another theme; a theme extends a built-in look or one theme that extends one, no deeper.`,\n );\n base = compileTheme(parent, `${where}.extends`, builtIn, new Set(['default']), options);\n } else if (spec.extends !== undefined) {\n base = typeof spec.extends === 'string' ? builtIn(spec.extends) : undefined;\n if (!base) throw new Error(`${where}.extends must name a built-in look.`);\n }\n const colors = spec.colors === undefined && base ? {} : record(spec.colors, `${where}.colors`);\n only(colors, ['light', 'dark'], `${where}.colors`);\n const darkOnly = parseScheme(spec.scheme, where) === 'dark-only';\n if (darkOnly && colors.light !== undefined)\n throw new Error(`${where}.colors.light: a dark-only theme has no light colors; name colors.dark alone, or drop scheme: 'dark-only'.`);\n for (const name of darkOnly ? (['dark'] as const) : (['light', 'dark'] as const))\n colorsToRoles(colors[name], `${where}.colors.${name}`, !base);\n\n const palettes = spec.palettes === undefined ? undefined : variants(spec.palettes, `${where}.palettes`, darkOnly);\n if (\n spec.defaultPalette !== undefined &&\n (typeof spec.defaultPalette !== 'string' ||\n (spec.defaultPalette !== 'default' && !palettes?.some((item) => item.id === spec.defaultPalette)))\n )\n throw new Error(`${where}.defaultPalette must be \"default\" or one of this theme's palettes.`);\n\n const shape = spec.shape === undefined ? {} : record(spec.shape, `${where}.shape`);\n only(shape, ['radius', 'elevation', 'border', 'height'], `${where}.shape`);\n if (shape.radius !== undefined) radiiOf(shape.radius, `${where}.shape.radius`);\n if (shape.border !== undefined) bordersOf(shape.border, `${where}.shape.border`);\n if (shape.height !== undefined) {\n const height = record(shape.height, `${where}.shape.height`);\n only(height, ['control'], `${where}.shape.height`);\n if (height.control !== undefined && !controlHeightFits(height.control))\n throw new Error(\n `${where}.shape.height.control must be a length from ${CONTROL_HEIGHT.min}px to ${CONTROL_HEIGHT.max}px, in whole pixels.`,\n );\n }\n if (shape.elevation !== undefined && typeof shape.elevation === 'object') liftsOf(shape.elevation, `${where}.shape.elevation`);\n else if (shape.elevation !== undefined && !Object.hasOwn(ELEVATIONS, shape.elevation as string))\n throw new Error(`${where}.shape.elevation must be one of ${Object.keys(ELEVATIONS).join(', ')}, or { card, float }.`);\n\n const type = spec.type === undefined ? {} : record(spec.type, `${where}.type`);\n for (const key of ['scale', 'headingWeight'])\n if (key in type) throw new Error(`${where}.type.${key} is now ${where}.${TYPE_MOVED[`type.${key}`]}.`);\n only(type, ['font', 'faces', 'base', 'roles', 'icons'], `${where}.type`);\n if (type.icons !== undefined) iconsOf(type.icons, `${where}.type.icons`);\n if (type.font !== undefined) fontsOf(type.font, `${where}.type.font`);\n if (type.faces !== undefined) {\n /* A face is a file in the App's directory, read and served by the server that loaded the App (appFonts.ts):\n a tenant's saved theme or a host page's has no directory to read it from. */\n if (!options.faces)\n throw new Error(`${where}.type.faces ships font files from an App's directory, and this theme has none to read them from.`);\n facesOf(type.faces, `${where}.type.faces`);\n }\n typeOf(type.base, type.roles, `${where}.type`);\n if (spec.density !== undefined && !DENSITIES.includes(spec.density as ThemeDensity))\n throw new Error(`${where}.density must be one of ${DENSITIES.join(', ')}.`);\n if (spec.motion !== undefined && !Object.hasOwn(MOTIONS, spec.motion as string))\n throw new Error(`${where}.motion must be one of ${Object.keys(MOTIONS).join(', ')}.`);\n if (spec.rhythm !== undefined && !Object.hasOwn(RHYTHMS, spec.rhythm as string))\n throw new Error(`${where}.rhythm must be one of ${Object.keys(RHYTHMS).join(', ')}.`);\n\n recipesOf(spec.recipes, `${where}.recipes`);\n if (spec.shell !== undefined) {\n const shell = record(spec.shell, `${where}.shell`);\n only(shell, ['placement'], `${where}.shell`);\n if (shell.placement !== undefined && shell.placement !== 'side' && shell.placement !== 'top')\n throw new Error(`${where}.shell.placement must be \"side\" or \"top\".`);\n }\n if (spec.surface !== undefined) surfaceOf(spec.surface, `${where}.surface`);\n if (spec.parts !== undefined) partsOf(spec.parts, `${where}.parts`);\n if (spec.stylesheet !== undefined) {\n /* Read, checked against its allowlist and served by the server that loaded the App (appBrand.ts). */\n if (!options.faces) throw new Error(`${where}.stylesheet is a file in an App's directory, and this theme has none to read it from.`);\n if (typeof spec.stylesheet !== 'string' || !SHEET_SRC.test(spec.stylesheet) || spec.stylesheet.split('/').includes('..'))\n throw new Error(`${where}.stylesheet must be a .css file beside the theme, written like \"./theme.css\".`);\n }\n const built = buildTheme(spec as ThemeSpec, base);\n if (base && typeof spec.extends === 'object') PARENTS.set(built, base);\n /* The files this theme names itself are read from its package, when it is one; inherited ones keep their own. */\n if (root) {\n for (const face of built.faces ?? []) if (!base?.faces?.includes(face)) FILE_ROOTS.set(face, root);\n if (built.stylesheet && built.stylesheet !== base?.stylesheet) FILE_ROOTS.set(built.stylesheet, root);\n }\n return built;\n}\n\nfunction variants(raw: unknown, where: string, darkOnly = false): ThemePaletteVariant[] {\n if (!Array.isArray(raw) || raw.length > 20) throw new Error(`${where} must be at most 20 palettes.`);\n const used = new Set(['default']);\n return raw.map((value, index) => {\n const path = `${where}[${index}]`,\n variant = record(value, path);\n only(variant, ['id', 'label', 'light', 'dark'], path);\n if (typeof variant.id !== 'string' || !THEME_ID.test(variant.id) || used.has(variant.id))\n throw new Error(`${path}.id must be a unique slug, not \"default\".`);\n used.add(variant.id);\n if (typeof variant.label !== 'string' || !variant.label.trim() || variant.label.trim().length > 60)\n throw new Error(`${path}.label must have 1 to 60 characters.`);\n if (darkOnly && variant.light !== undefined)\n throw new Error(`${path}.light: a dark-only theme has no light colors; name the palette's dark alone.`);\n return {\n id: variant.id,\n label: variant.label.trim(),\n light: colorsToRoles(variant.light, `${path}.light`, false),\n dark: colorsToRoles(variant.dark, `${path}.dark`, false),\n };\n });\n}\n\nfunction radiiOf(raw: unknown, where: string): Record<string, string> {\n if (typeof raw === 'string')\n throw new Error(\n `${where} is { control, field, pill, panel }, each a length (a control's and a pill's may be 'round'); one length is not accepted. Write { control: '${raw}', field: '${raw}' }.`,\n );\n const value = record(raw, where);\n only(value, Object.keys(RADIUS_MAX), where);\n for (const [key, length] of Object.entries(value)) {\n const max = RADIUS_MAX[key as keyof typeof RADIUS_MAX];\n const round = ROUND_CORNERS.has(key);\n if (!(cornerFits(length, max) || (round && length === 'round')))\n throw new Error(`${where}.${key} must be a length from 0px to ${max}px${round ? \", or 'round'\" : ''}.`);\n }\n return value as Record<string, string>;\n}\n\n/** `{ card, float }`: a step for each, from the platform's lifts (the attribute's `look` is not one an author says). */\nfunction liftsOf(raw: unknown, where: string): void {\n const value = record(raw, where);\n only(value, ['card', 'float'], where);\n const steps = Object.keys(LIFTS).slice(1);\n for (const [key, step] of Object.entries(value))\n if (!steps.includes(step as string)) throw new Error(`${where}.${key} must be one of ${steps.join(', ')}.`);\n}\n\nfunction bordersOf(raw: unknown, where: string): void {\n const value = record(raw, where);\n only(value, Object.keys(BORDER_MENU), where);\n for (const [key, width] of Object.entries(value)) {\n const menu = BORDER_MENU[key as keyof typeof BORDER_MENU] as readonly string[];\n if (!menu.includes(width as string)) throw new Error(`${where}.${key} must be ${menu.slice(0, -1).join(', ')} or ${menu.at(-1)}.`);\n }\n}\n\nfunction fontsOf(raw: unknown, where: string): Partial<Record<'ui' | 'data' | 'heading' | 'code', string>> {\n const ok = (font: unknown): font is string => typeof font === 'string' && font.length > 0 && font.length <= 200 && FONT.test(font);\n if (typeof raw === 'string') {\n if (!ok(raw)) throw new Error(`${where} must be a CSS font-family list (letters, digits, spaces, quotes, commas).`);\n return { ui: raw, data: raw, heading: raw };\n }\n const value = record(raw, where);\n only(value, ['ui', 'data', 'heading', 'code'], where);\n for (const [key, font] of Object.entries(value))\n if (!ok(font)) throw new Error(`${where}.${key} must be a CSS font-family list (letters, digits, spaces, quotes, commas).`);\n return value as Record<string, string>;\n}\n\n/** What a compile allows beyond the spec's own rules. */\ninterface ThemeCompileOptions {\n /** `type.faces`: only an App's own theme, whose files the server reads from its directory. */\n faces?: boolean;\n}\n\n/* A family name as an `@font-face` and a stack both say it, quoted: no quote, comma or semicolon to close either. */\nconst FACE_FAMILY = /^[A-Za-z0-9][A-Za-z0-9 _-]{0,59}$/;\n/* Relative to the App's directory, downward only: `./fonts/x.woff2`. The server holds the resolved file to the\n directory as well (appFonts.ts), so a link out of it is refused there. */\nconst FACE_SRC = /^\\.\\/(?:[\\w.-]+\\/)*[\\w.-]+\\.(?:woff2|woff)$/;\nconst FACE_WEIGHT = /^[1-9]00(?: [1-9]00)?$/;\n/** At most this many faces: a regular, a bold and their italics in each of three families. */\nconst MAX_FACES = 12;\n\nfunction facesOf(raw: unknown, where: string) {\n if (!Array.isArray(raw) || raw.length > MAX_FACES) throw new Error(`${where} must be at most ${MAX_FACES} faces.`);\n const seen = new Set<string>();\n raw.forEach((value, index) => {\n const path = `${where}[${index}]`,\n face = record(value, path);\n only(face, ['family', 'src', 'weight', 'style', 'subset'], path);\n if (typeof face.family !== 'string' || !FACE_FAMILY.test(face.family))\n throw new Error(`${path}.family must be a family name of letters, digits, spaces, hyphens or underscores (at most 60).`);\n if (typeof face.src !== 'string' || !FACE_SRC.test(face.src) || face.src.split('/').includes('..'))\n throw new Error(`${path}.src must be a .woff2 or .woff file inside the App's directory, written like \"./fonts/face.woff2\".`);\n if (\n face.weight !== undefined &&\n !(typeof face.weight === 'number' && Number.isInteger(face.weight) && face.weight >= 100 && face.weight <= 900) &&\n !(typeof face.weight === 'string' && FACE_WEIGHT.test(face.weight))\n )\n throw new Error(`${path}.weight must be 100 to 900, or a variable file's range like \"100 900\".`);\n if (face.style !== undefined && face.style !== 'normal' && face.style !== 'italic')\n throw new Error(`${path}.style must be \"normal\" or \"italic\".`);\n // Latin is the default cut; the one other choice is the whole file.\n if (face.subset !== undefined && face.subset !== false)\n throw new Error(`${path}.subset must be false (ship the file whole), or left out to cut it to Latin.`);\n const key = `${face.family.toLowerCase()} ${String(face.weight ?? 400)} ${String(face.style ?? 'normal')}`;\n if (seen.has(key)) throw new Error(`${path} repeats ${face.family} at that weight and style.`);\n seen.add(key);\n });\n}\n\nfunction recipesOf(raw: unknown, where: string): Partial<ResolvedRecipes> {\n if (raw === undefined) return {};\n const value = record(raw, where);\n for (const [part, pick] of Object.entries(value)) {\n const moved = TYPE_MOVED[`recipes.${part}`];\n if (moved) throw new Error(`${where}.${part} is now ${where.replace(/\\.recipes$/, '')}.${moved}.`);\n if (Object.hasOwn(RECIPES_RENAMED, part)) throw new Error(`${where}.${part} is now ${where}.${RECIPES_RENAMED[part]}.`);\n const menu = (RECIPE_MENU as Record<string, Record<string, unknown>>)[part];\n if (!menu) throw new Error(`${where} has no part \"${part}\". The parts are: ${RECIPE_PARTS.join(', ')}.`);\n if (typeof pick !== 'string' || !Object.hasOwn(menu, pick))\n throw new Error(`${where}.${part} is not on the menu: pick one of ${Object.keys(menu).join(', ')}.`);\n }\n return value as Partial<ResolvedRecipes>;\n}\n\n/** A compiled theme's colors back into the spec's words, for the reference and the round-trip test. */\nexport function rolesToColors(palette: Partial<ThemePalette>): ThemeColorsOver {\n const out: Record<string, unknown> = {};\n for (const [path, role] of COLOR_ROLES) {\n const value = palette[role];\n if (value === undefined) continue;\n const chart = /^chart\\[(\\d)\\]$/.exec(path);\n if (chart) {\n const series = (out.chart ??= []) as (string | null)[];\n while (series.length < Number(chart[1])) series.push(null);\n series[Number(chart[1])] = value;\n } else if (path.includes('.')) {\n const [group, key] = path.split('.') as [string, string];\n ((out[group] ??= {}) as Record<string, string>)[key] = value;\n } else out[path] = value;\n }\n return out as ThemeColorsOver;\n}\n\nconst BUILT_IN_IDS: ReadonlySet<string> = new Set(['default', ...THEME_PRESETS.map((look) => look.id)]);\n\n/** Which built-in each compiled App theme extends, so the validator holds against its author only the colours they set. */\nconst EXTENDED = new WeakMap<ThemeDefinition, ThemeDefinition | null>();\n\n/** An App's `defineTheme` result, checked and compiled over the built-in it extends. */\nexport function compileAppTheme(\n raw: unknown,\n where: string,\n taken: ReadonlySet<string> = new Set(),\n options: ThemeCompileOptions = {},\n): ThemeDefinition {\n let extended: ThemeDefinition | null = null;\n const theme = compileTheme(\n raw,\n where,\n (id) => {\n extended = BUILT_IN_IDS.has(id) ? (themePreset(id) ?? null) : null;\n return extended ?? undefined;\n },\n new Set([...BUILT_IN_IDS, ...taken]),\n options,\n );\n EXTENDED.set(theme, themeParent(theme) ?? extended);\n /* The spec as written, for what a check suggests in its words and where `check --fix` writes (themeFix.ts). */\n /* The ids taken then, copied: the caller goes on to add this theme's own, which a variation must still be allowed. */\n SPECS.set(theme, { raw: raw as ThemeSpec, where, taken: new Set(taken), options });\n return theme;\n}\n\n/** Each compiled App theme's spec and how it was compiled, so a fix can be tried by compiling the spec again. */\nconst SPECS = new WeakMap<ThemeDefinition, { raw: ThemeSpec; where: string; taken: ReadonlySet<string>; options: ThemeCompileOptions }>();\n\n/** The spec a compiled App theme came from, and how to compile a variation of it; undefined for a theme not compiled here. */\nexport function themeSpecOf(\n theme: ThemeDefinition,\n): { raw: ThemeSpec; where: string; taken: ReadonlySet<string>; options: ThemeCompileOptions } | undefined {\n return SPECS.get(theme);\n}\n\n/** The built-in a compiled App theme extends: `null` for one that extends nothing, `undefined` for a theme not compiled here. */\nexport function themeExtends(theme: ThemeDefinition): ThemeDefinition | null | undefined {\n return EXTENDED.get(theme);\n}\n","/**\n * Chart series apart for color-blind readers: the one measure `mistri check` holds an App's chart to\n * (themeCheck.ts) and the built-in looks order their series by under each palette (lookPalettes.ts).\n */\n/** Machado, Oliveira and Fernandes (2009) at full severity, in linear sRGB: the\n two red-green deficiencies, about one man in twelve between them. Tritanopia\n is rare enough (about 1 in 10,000) that it is not held against a palette. */\nconst DEFICIENCIES = {\n protanopia: [\n [0.152286, 1.052583, -0.204868],\n [0.114503, 0.786281, 0.099216],\n [-0.003882, -0.048116, 1.051998],\n ],\n deuteranopia: [\n [0.367322, 0.860646, -0.227968],\n [0.280085, 0.672501, 0.047413],\n [-0.01182, 0.04294, 0.968881],\n ],\n} as const;\n/** Adjacent series, in OKLab ΔE ×100: at least 8 under each deficiency, and 10 to normal vision. */\nconst CHART_SEPARATION = { deficient: 8, normal: 10 } as const;\n\nexport function linear(hex: string): [number, number, number] {\n return [1, 3, 5].map((start) => {\n const value = parseInt(hex.slice(start, start + 2), 16) / 255;\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n}\nexport function oklab([r, g, b]: readonly number[]): [number, number, number] {\n const l = Math.cbrt(0.4122214708 * r! + 0.5363325363 * g! + 0.0514459929 * b!),\n m = Math.cbrt(0.2119034982 * r! + 0.6806995451 * g! + 0.1073969566 * b!),\n s = Math.cbrt(0.0883024619 * r! + 0.2817188376 * g! + 0.6299787005 * b!);\n return [\n 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n ];\n}\nfunction simulate(color: readonly number[], matrix: readonly (readonly number[])[]): number[] {\n return matrix.map((row) => Math.min(1, Math.max(0, row[0]! * color[0]! + row[1]! * color[1]! + row[2]! * color[2]!)));\n}\n/** OKLab distance ×100 between two hex colors, as seen with the deficiency named or with normal vision. */\nfunction chartDistance(a: string, b: string, deficiency?: keyof typeof DEFICIENCIES): number {\n const see = (hex: string) => oklab(deficiency ? simulate(linear(hex), DEFICIENCIES[deficiency]) : linear(hex));\n const x = see(a),\n y = see(b);\n return 100 * Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]);\n}\n\n/** The first reader two series are too alike for, how far apart they are to them and what they need; undefined when\n every reader tells them apart. The one measure: the check says it, and a suggested chart is proved by it. */\nexport function chartPairFailure(a: string, b: string): { vision: string; distance: number; needs: number } | undefined {\n const tests: Array<[string, number, number]> = [\n ['normal vision', chartDistance(a, b), CHART_SEPARATION.normal],\n ...(Object.keys(DEFICIENCIES) as (keyof typeof DEFICIENCIES)[]).map(\n (kind) => [kind, chartDistance(a, b, kind), CHART_SEPARATION.deficient] as [string, number, number],\n ),\n ];\n const failed = tests.find(([, distance, needs]) => distance < needs);\n return failed && { vision: failed[0], distance: failed[1], needs: failed[2] };\n}\n\n/** Whether two series stay apart for every reader the chart check holds a palette to. */\nexport function chartsApart(a: string, b: string): boolean {\n return !chartPairFailure(a, b);\n}\n","/**\n * The one validator a theme meets, wherever it comes from: `mistri check`\n * and `build` run it over an App's `defineTheme` themes, startup over its\n * accent on the looks it offers, and a tenant's branding save over the theme\n * that tenant would wear (tenantBranding.ts). It adds to the contrast checks\n * (theme.ts) the chart's color-blind separation, and says each failure by the\n * color's own name in the spec (`status.critical`, `chart[2]`).\n */\nimport type { ThemeDefinition, ThemePalette } from '@mistriwork/engine/schema/theme.js';\nimport { rolePairs, themeFindings, type ThemeFinding } from '@mistriwork/engine/shared/theme.js';\nimport {\n effectiveThemeDefinition,\n effectiveThemePalette,\n themeModes,\n withInks,\n type AppAccent,\n} from '@mistriwork/engine/shared/themePresets.js';\nimport { COLOR_ROLES } from './themeBuild.js';\nimport { colorPath, themeSpecOf } from './themeSpec.js';\n\nconst ROLES = new Set<string>(COLOR_ROLES.map(([, role]) => role));\n\n/* ---------- chart series apart for color-blind readers ---------- */\n\n/* The measure lives beside the looks (engine chartSeparation.ts), which order their own series by it. */\nexport { chartPairFailure, chartsApart, linear, oklab } from '@mistriwork/engine/shared/chartSeparation.js';\nimport { chartPairFailure } from '@mistriwork/engine/shared/chartSeparation.js';\n\nconst HEX = /^#[0-9a-f]{6}$/i;\nconst SERIES = [1, 2, 3, 4, 5, 6, 7, 8].map((n) => `chart${n}` as keyof ThemePalette);\n\n/** Each adjacent pair of series one of `authored` names (a built-in's own pairs are its own). */\nfunction chartFindings(\n theme: ThemeDefinition,\n own: { accent?: AppAccent },\n authored: (paletteId: string, mode: 'light' | 'dark', role: keyof ThemePalette) => boolean,\n): ThemeFinding[] {\n const findings: ThemeFinding[] = [];\n for (const paletteId of ['default', ...(theme.palettes ?? []).map((item) => item.id)])\n for (const mode of themeModes(theme)) {\n const palette = effectiveThemePalette(effectiveThemeDefinition(theme, paletteId)[mode], mode === 'dark', own);\n const name =\n paletteId === 'default' ? theme.label : `${theme.label} / ${theme.palettes!.find((item) => item.id === paletteId)!.label}`;\n for (let index = 0; index < SERIES.length - 1; index++) {\n const left = SERIES[index]!,\n right = SERIES[index + 1]!;\n const a = palette[left],\n b = palette[right];\n if (!a || !b || !HEX.test(a) || !HEX.test(b)) continue;\n if (!authored(paletteId, mode, left) && !authored(paletteId, mode, right)) continue;\n const token = authored(paletteId, mode, right) ? right : left;\n const failed = chartPairFailure(a, b);\n if (!failed) continue;\n const { vision, distance, needs } = failed;\n findings.push({\n themeId: theme.id,\n paletteId,\n mode,\n token,\n check: `${left} beside ${right} for ${vision}`,\n ratio: distance,\n needs,\n message: `Theme \"${name}\" (${mode}): ${colorPath(left)} and ${colorPath(right)} are ${distance.toFixed(1)} apart for ${vision}; adjacent chart series need ${needs}.`,\n });\n }\n }\n return findings;\n}\n\n/* ---------- the colours an author wrote, as written ---------- */\n\n/** A colour measured as the author wrote it, and what the page wears in its place. */\ntype WrittenFinding = ThemeFinding & { written?: string; wornAs?: string };\n\n/** The inks a page wears as the accent's text ink when they are written the same as the accent (themePresets.ts\n effectiveThemePalette): a link, the ring, the menu's current mark, the first series. */\nconst FOLLOWERS = ['link', 'focus', 'navAccent', 'chart1'] as const;\n\n/**\n * An ink the author wrote that the page does not wear as written — a link, a ring, a menu mark or a first series\n * written the same as the accent is drawn in the accent's text ink instead — held to its role's pairs as written:\n * a value in the spec is one the reader sees or one check refuses, never one dropped in silence. A link written as an\n * amber accent read 2.0:1 and passed (T-3). Only a theme whose spec is at hand (compileAppTheme) is read; an App's\n * own `ui.theme.accent` replaces these on purpose and is checked as the accent.\n */\nfunction writtenFindings(theme: ThemeDefinition, own: { accent?: AppAccent }): WrittenFinding[] {\n const raw = themeSpecOf(theme)?.raw as\n | { colors?: Record<string, unknown>; palettes?: ReadonlyArray<Record<string, unknown>> }\n | undefined;\n if (!raw || own.accent) return [];\n const findings: WrittenFinding[] = [];\n const darkOnly = theme.scheme === 'dark-only';\n for (const [index, paletteId] of ['default', ...(theme.palettes ?? []).map((item) => item.id)].entries())\n for (const mode of themeModes(theme)) {\n const source = (index === 0 ? raw.colors : raw.palettes?.[index - 1])?.[darkOnly ? 'dark' : mode] as\n | Record<string, unknown>\n | undefined;\n if (!source) continue;\n const worn = withInks(effectiveThemePalette(effectiveThemeDefinition(theme, paletteId)[mode], mode === 'dark', own) as ThemePalette);\n const name = index === 0 ? theme.label : `${theme.label} / ${theme.palettes![index - 1]!.label}`;\n for (const role of FOLLOWERS) {\n const path = colorPath(role);\n const group = /^(\\w+)\\.(\\w+)$/.exec(path);\n const said =\n role === 'chart1'\n ? (source.chart as unknown[] | undefined)?.[0]\n : group\n ? (source[group[1]!] as Record<string, unknown> | undefined)?.[group[2]!]\n : source[role];\n if (typeof said !== 'string' || !HEX.test(said.trim()) || said.trim().toLowerCase() === worn[role]?.toLowerCase()) continue;\n const written = said.trim().toLowerCase();\n for (const pair of rolePairs(worn, role, written)) {\n if (pair.ratio >= pair.needs) continue;\n findings.push({\n themeId: theme.id,\n paletteId,\n mode,\n token: role,\n check: `${role} as written on ${pair.background}${pair.what ? ` (${pair.what})` : ''}`,\n ratio: pair.ratio,\n needs: pair.needs,\n written,\n wornAs: worn[role],\n message: `Theme \"${name}\" (${mode}): ${path} as written is ${written}, ${pair.ratio.toFixed(2)}:1 on ${colorPath(pair.background)}; it needs ${pair.needs}:1.`,\n });\n }\n }\n }\n return findings;\n}\n\n/* ---------- the validator ---------- */\n\nexport interface ThemeProblem {\n themeId: string;\n paletteId: string;\n mode: 'light' | 'dark';\n /** The color by its name in the spec: `accent`, `status.critical`, `chart[2]`, `nav.muted`. */\n token: string;\n /** The same color by its role (`accent`, `negative`, `chart3`, `navMuted`): what themeFix.ts varies. */\n role: keyof ThemePalette;\n /** What was measured, by roles: `accent/accentSoft over surface`, `chart2 beside chart3 for deuteranopia`. */\n check: string;\n ratio: number;\n needs: number;\n /** What failed, said by the check: \"accent on surface 3.10:1, needs 4.5:1\". */\n reason: string;\n /** The colour as the author wrote it, where the page wears another in its place (a link written as the accent). */\n written?: string;\n /** The whole sentence, naming theme, mode, token and reason. */\n message: string;\n}\n\n/**\n * Every failure of a theme: contrast of text and controls in light and dark\n * and every palette, and its chart's color-blind separation. `authored` says\n * which colors the author set, so a series kept from a built-in is not held\n * against the author; omitted, every color is the author's.\n */\nexport function checkTheme(\n theme: ThemeDefinition,\n options: {\n own?: { accent?: AppAccent };\n authored?: (paletteId: string, mode: 'light' | 'dark', role: keyof ThemePalette) => boolean;\n } = {},\n): ThemeProblem[] {\n const own = options.own ?? {};\n const authored = options.authored ?? (() => true);\n const all: WrittenFinding[] = [...themeFindings([theme], own), ...chartFindings(theme, own, authored), ...writtenFindings(theme, own)];\n return all.map((finding) => {\n const name =\n finding.paletteId === 'default'\n ? theme.label\n : `${theme.label} / ${theme.palettes!.find((item) => item.id === finding.paletteId)!.label}`;\n const check = finding.check.replace(/\\b[a-z][a-zA-Z0-9]*\\b/g, (word) => (ROLES.has(word) ? colorPath(word) : word));\n const reason = check.includes(' for ')\n ? `${check} are ${finding.ratio.toFixed(1)} apart; adjacent series need ${finding.needs}`\n : `${check} is ${finding.ratio.toFixed(2)}:1; it needs ${finding.needs}:1`;\n return {\n themeId: finding.themeId,\n paletteId: finding.paletteId,\n mode: finding.mode,\n token: colorPath(finding.token),\n role: finding.token,\n check: finding.check,\n ratio: finding.ratio,\n needs: finding.needs,\n reason,\n ...(finding.written ? { written: finding.written } : {}),\n message: `Theme \"${name}\" (${finding.mode}), ${colorPath(finding.token)}: ${reason}.`,\n };\n });\n}\n\n/** The roles a compiled theme takes from its author rather than the built-in it extends. */\nexport function authoredRoles(theme: ThemeDefinition, base: ThemeDefinition | undefined) {\n return (paletteId: string, mode: 'light' | 'dark', role: keyof ThemePalette): boolean => {\n if (!base) return true;\n const ours = effectiveThemeDefinition(theme, paletteId)[mode][role];\n const theirs = effectiveThemeDefinition(base, base.palettes?.some((item) => item.id === paletteId) ? paletteId : 'default')[mode][role];\n return ours !== theirs;\n };\n}\n","/**\n * A tenant's branding: what an App lets each of its customers say about how the\n * App looks to their people — a name, a logo, an accent, one of the App's\n * palettes or themes, a corner and a density — and never the grounds, the ink\n * or the recipes. The App declares the scope (`branding: { themes, allow }`);\n * a tenant admin saves a value in it; the server keeps it in the platform's\n * branding store and dresses that tenant's pages with it from the first paint.\n *\n * Shared by the server (which parses, validates and serves) and the Branding\n * block (which previews and says the refusal before the save does). The one\n * validator is themeCheck.ts; this file turns its findings into the fixed\n * sentences a person can act on.\n */\nimport type { ThemeDefinition, ThemeDensity, ThemePalette } from '@mistriwork/engine/schema/theme.js';\nimport { chartsApart, checkTheme, type ThemeProblem } from './themeCheck.js';\nimport { mixHex } from './themeBuild.js';\nimport { DENSITIES } from '@mistriwork/engine/shared/themeRecipes.js';\nimport { effectiveThemeDefinition, themePreset } from '@mistriwork/engine/shared/themePresets.js';\nimport { compileAppTheme } from './themeSpec.js';\nimport type { AppAuth, AppUi } from './appModel.js';\nimport { canonicalLocale } from '@mistriwork/engine/shared/appLocale.js';\nimport { audienceThemes as audienceThemeIds, resolveTheme, type ThemeAudiences } from '@mistriwork/engine/shared/themeResolve.js';\n\n/** What a tenant may change, when the App allows it. `preset` (the theme it starts from) needs more than one theme to\n * pick from, and names it as `ui.theme.preset` does. */\nconst BRANDING_KEYS = ['logo', 'name', 'accent', 'palette', 'radius', 'density', 'preset', 'locale'] as const;\nexport type BrandingKey = (typeof BRANDING_KEYS)[number];\n\n/** The App's declaration, parsed: its themes compiled (the first is the tenants' default), and the keys a tenant may set. */\nexport interface BrandingDeclaration {\n readonly themes: readonly ThemeDefinition[];\n readonly allow: readonly BrandingKey[];\n /** The tenant roles whose members may save it: a tenant's admins, by the App's own names for them. */\n readonly editors: readonly string[];\n /** `ui.theme.for`, and each theme it dresses an audience in, by id: a role's theme wins over the tenant's pick\n (themeResolve.ts), and the tenant's accent, name, logo, corner and density lay over it as over its own. */\n readonly audiences?: ThemeAudiences;\n readonly audienceThemes?: readonly ThemeDefinition[];\n}\n\n/** A tenant's saved branding. Every field is optional; an absent one is the App's. */\nexport interface TenantBrandingValue {\n /** The name the tenant's people see in the shell, the title and email. */\n readonly name?: string;\n /** The logo's content hash and served type (brandingLogo.ts); the bytes live in the branding store. */\n readonly logo?: { readonly hash: string; readonly type: string };\n /** The logo drawn on a dark bar or by night, where the light one would not read. Allowed with `logo`; the light logo\n stands where this is none. */\n readonly logoDark?: { readonly hash: string; readonly type: string };\n /** One color for both grounds (mixed toward white on the dark one), or one for each. */\n readonly accent?: { readonly light: string; readonly dark: string };\n /** One of the theme's palettes. */\n readonly palette?: string;\n /** A control's corner, 0px to 8px. */\n readonly radius?: string;\n readonly density?: ThemeDensity;\n /** One of `branding.themes`, by id. */\n readonly preset?: string;\n /** The tenant's language and figures for its people whose own row names none (shared/appLocale.ts `resolveLocale`). */\n readonly locale?: string;\n}\n\nconst HEX = /^#[0-9a-f]{6}$/;\nconst isPlain = (raw: unknown): raw is Record<string, unknown> =>\n Boolean(raw) && typeof raw === 'object' && !Array.isArray(raw) && [Object.prototype, null].includes(Object.getPrototypeOf(raw));\n\n/**\n * The App's `branding` block, checked against the App it sits in: a tenant App\n * (`auth.rows: 'tenant'`), keys from the closed list, editors that are tenant\n * roles, and themes written with defineTheme or named as built-ins. Without\n * `themes` the tenants' one theme is the App's own look, its accent included.\n */\nexport function parseBrandingDeclaration(raw: unknown, app: { ui: AppUi; auth?: AppAuth }, where: string): BrandingDeclaration {\n const at = `${where}: branding`;\n if (!isPlain(raw)) throw new Error(`${at} must be an object: { allow: [...], editors: [...], themes?: [...] }.`);\n for (const key of Object.keys(raw))\n if (!['allow', 'editors', 'themes'].includes(key))\n throw new Error(`${at} has an unsupported field \"${key}\". It takes allow, editors and themes.`);\n if (app.auth?.rows !== 'tenant')\n throw new Error(`${at}: branding is chosen per tenant, so it needs a tenant App (auth.rows: 'tenant'). An App's own look is ui.theme.`);\n const custom = app.ui.customThemes ?? [];\n const taken = new Set(custom.map((theme) => theme.id));\n let themes: ThemeDefinition[];\n if (raw.themes === undefined) themes = [ownLook(app.ui)];\n else {\n if (!Array.isArray(raw.themes) || !raw.themes.length || raw.themes.length > 6)\n throw new Error(`${at}.themes must list one to six themes: defineTheme(...) results or built-in names.`);\n themes = raw.themes.map((spec: unknown, index: number) => {\n const here = `${at}.themes[${index}]`;\n if (typeof spec === 'string') {\n const preset = themePreset(spec);\n if (!preset) throw new Error(`${here} \"${spec}\" is not a built-in theme.`);\n return preset;\n }\n if (!isPlain(spec) || spec.kind !== 'theme')\n throw new Error(`${here} must be a theme written with defineTheme(...) or a built-in's name.`);\n /* The App's own theme, offered to its tenants too, is the one ui compiled, not a second copy. */\n const same = custom.find((theme) => theme.id === spec.id);\n if (same) return same;\n const theme = compileAppTheme(spec, here, taken);\n taken.add(theme.id);\n return theme;\n });\n if (new Set(themes.map((theme) => theme.id)).size !== themes.length) throw new Error(`${at}.themes names a theme twice.`);\n }\n if (!Array.isArray(raw.allow) || !raw.allow.length)\n throw new Error(`${at}.allow must list what a tenant may change: ${BRANDING_KEYS.join(', ')}.`);\n const allow: BrandingKey[] = [];\n for (const key of raw.allow) {\n if (key === 'theme') throw new Error(`${at}.allow has \"theme\", which is now \"preset\": which of branding.themes a tenant starts from.`);\n if (typeof key !== 'string' || !(BRANDING_KEYS as readonly string[]).includes(key))\n throw new Error(\n `${at}.allow has \"${String(key)}\"; a tenant may change only ${BRANDING_KEYS.join(', ')}. Grounds, ink and recipes stay the App's.`,\n );\n if (allow.includes(key as BrandingKey)) throw new Error(`${at}.allow names \"${key}\" twice.`);\n allow.push(key as BrandingKey);\n }\n if (allow.includes('preset') && themes.length < 2)\n throw new Error(`${at}.allow has \"preset\", which needs more than one theme in branding.themes to pick from.`);\n if (allow.includes('palette') && !themes.some((theme) => theme.palettes?.length))\n throw new Error(`${at}.allow has \"palette\", and no theme offered has a palette to pick.`);\n const roles = app.auth?.roles ?? {};\n if (!Array.isArray(raw.editors) || !raw.editors.length)\n throw new Error(`${at}.editors must name the tenant roles whose members may change it (a tenant's admins).`);\n const editors: string[] = [];\n for (const role of raw.editors) {\n const declared = typeof role === 'string' ? (roles as Record<string, { realm?: string }>)[role] : undefined;\n /* A role names no realm when it is the App's own, a tenant's; a platform-realm role is the provider's staff. */\n if (!declared || (declared.realm !== undefined && declared.realm !== 'tenant'))\n throw new Error(`${at}.editors has \"${String(role)}\", which is not a tenant role of auth.roles.`);\n if (!editors.includes(role as string)) editors.push(role as string);\n }\n const audiences = app.ui.theme?.for;\n const audienceThemes = audienceThemeIds(audiences).flatMap((id) => {\n const theme = custom.find((item) => item.id === id) ?? themePreset(id);\n return theme ? [theme] : [];\n });\n return Object.freeze({\n themes: Object.freeze(themes),\n allow: Object.freeze(allow),\n editors: Object.freeze(editors),\n ...(audiences ? { audiences, audienceThemes: Object.freeze(audienceThemes) } : {}),\n });\n}\n\n/**\n * A tenant's branding as one audience wears it: where `ui.theme.for` dresses a signed-out visitor or one of the\n * account's roles (themeResolve.ts), that theme replaces the tenant's pick and its palette, and the tenant's accent,\n * name, logo, corner and density lay over it; otherwise the tenant's own, as saved.\n */\nexport function audienceBranding(\n declaration: BrandingDeclaration,\n value: TenantBrandingValue,\n audience: { public?: boolean; roles?: readonly string[] },\n): { declaration: BrandingDeclaration; value: TenantBrandingValue } {\n const themes = declaration.audienceThemes ?? [];\n const worn = resolveTheme({ ...audience, for: declaration.audiences, offered: (id) => themes.some((theme) => theme.id === id) });\n if (worn.source !== 'public' && worn.source !== 'role') return { declaration, value };\n const { palette: _palette, ...rest } = value;\n return { declaration: { ...declaration, themes: themes.filter((theme) => theme.id === worn.id) }, value: { ...rest, preset: worn.id } };\n}\n\n/** The App's look as one theme: its preset or its own theme, with its accent laid over as a tenant's would be. */\nfunction ownLook(ui: AppUi): ThemeDefinition {\n const preset = ui.theme?.preset;\n const base = ui.customThemes?.find((theme) => theme.id === preset) ?? themePreset(preset) ?? themePreset('workshop')!;\n const accent = ui.theme?.accent;\n const value: TenantBrandingValue = {\n ...(ui.theme?.palette && ui.theme.palette !== 'default' ? { palette: ui.theme.palette } : {}),\n ...(typeof accent === 'string' && HEX.test(accent.toLowerCase())\n ? { accent: { light: accent.toLowerCase(), dark: mixHex(accent.toLowerCase(), '#ffffff', 0.62) } }\n : typeof accent === 'object' && HEX.test(accent.light.toLowerCase()) && HEX.test(accent.dark.toLowerCase())\n ? { accent: { light: accent.light.toLowerCase(), dark: accent.dark.toLowerCase() } }\n : {}),\n };\n if (!value.accent && !value.palette) return base;\n const theme = tenantTheme({ themes: [base], allow: [], editors: [] }, value);\n return { ...theme, id: base.id, label: base.label };\n}\nconst RADIUS = /^(?:[0-7]|8)px$/;\nconst SLUG = /^[a-z][a-z0-9-]{0,47}$/;\n\n/** Why a save was refused: the field and a sentence that says what to do. */\nexport interface BrandingRefusal {\n readonly field: BrandingKey;\n readonly message: string;\n}\n\nexport class BrandingRefused extends Error {\n readonly refusals: readonly BrandingRefusal[];\n constructor(refusals: readonly BrandingRefusal[]) {\n super(refusals.map((refusal) => refusal.message).join(' '));\n this.name = 'BrandingRefused';\n this.refusals = refusals;\n }\n}\n\nfunction plain(raw: unknown): Record<string, unknown> {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw) || ![Object.prototype, null].includes(Object.getPrototypeOf(raw)))\n throw new BrandingRefused([{ field: 'name', message: 'Branding is a set of named fields.' }]);\n return raw as Record<string, unknown>;\n}\n\n/**\n * A tenant's value checked field by field against the App's declaration: a key\n * the App does not allow, or a value outside its type, is refused with the field\n * named. Colours are lowercase six-digit hex; nothing here reaches CSS before it\n * has matched one of these patterns.\n */\nexport function parseTenantBranding(raw: unknown, declaration: BrandingDeclaration): TenantBrandingValue {\n const input = plain(raw);\n const refusals: BrandingRefusal[] = [];\n const allowed = new Set<string>(declaration.allow);\n const out: { -readonly [K in keyof TenantBrandingValue]: TenantBrandingValue[K] } = {};\n for (const [key, value] of Object.entries(input)) {\n if (value === undefined || value === null) continue;\n if (\n !(BRANDING_KEYS as readonly string[]).includes(key === 'logoDark' ? 'logo' : key) ||\n !allowed.has(key === 'logoDark' ? 'logo' : key)\n ) {\n refusals.push({ field: 'name', message: `This App does not let a customer change \"${key}\".` });\n continue;\n }\n switch (key as BrandingKey | 'logoDark') {\n case 'name':\n // eslint-disable-next-line no-control-regex -- a name is one line of text\n if (typeof value !== 'string' || !value.trim() || value.trim().length > 80 || /[\\u0000-\\u001f\\u007f<>]/.test(value))\n refusals.push({ field: 'name', message: 'The name must be one line of at most 80 characters, without < or >.' });\n else out.name = value.trim();\n break;\n case 'logo':\n case 'logoDark': {\n const logo = typeof value === 'object' ? (value as Record<string, unknown>) : {};\n if (\n typeof logo.hash !== 'string' ||\n !/^[0-9a-f]{64}$/.test(logo.hash) ||\n typeof logo.type !== 'string' ||\n !/^image\\/(png|jpeg|webp|gif|svg\\+xml)$/.test(logo.type)\n )\n refusals.push({\n field: 'logo',\n message:\n key === 'logo'\n ? 'Upload the logo again: the saved one was not recognized.'\n : 'Upload the dark logo again: the saved one was not recognized.',\n });\n else out[key as 'logo' | 'logoDark'] = { hash: logo.hash, type: logo.type };\n break;\n }\n case 'accent': {\n /* One color is for the light ground; the dark one is derived from it once the theme it will stand in is\n known, below. */\n const one = typeof value === 'string' ? value.trim().toLowerCase() : '';\n const pair = HEX.test(one) ? { light: one, dark: one } : typeof value === 'object' ? (value as Record<string, unknown>) : {};\n const light = typeof pair.light === 'string' ? pair.light.trim().toLowerCase() : '';\n const dark = typeof pair.dark === 'string' ? pair.dark.trim().toLowerCase() : '';\n if (!HEX.test(light) || !HEX.test(dark)) refusals.push({ field: 'accent', message: 'The accent must be a color like #1d4ed8.' });\n else out.accent = { light, dark };\n break;\n }\n case 'palette':\n if (typeof value !== 'string' || !SLUG.test(value))\n refusals.push({ field: 'palette', message: 'Pick one of the palettes offered.' });\n else out.palette = value;\n break;\n case 'radius':\n if (typeof value !== 'string' || !RADIUS.test(value))\n refusals.push({ field: 'radius', message: 'The corner must be between 0px and 8px.' });\n else out.radius = value;\n break;\n case 'density':\n if (!(DENSITIES as readonly unknown[]).includes(value))\n refusals.push({ field: 'density', message: 'Pick compact, comfortable or spacious.' });\n else out.density = value as ThemeDensity;\n break;\n case 'preset':\n if (typeof value !== 'string' || !declaration.themes.some((theme) => theme.id === value))\n refusals.push({ field: 'preset', message: 'Pick one of the themes offered.' });\n else out.preset = value;\n break;\n case 'locale': {\n const tag = canonicalLocale(value);\n if (!tag) refusals.push({ field: 'locale', message: 'Pick a language like \"en-US\" or \"es-MX\".' });\n else out.locale = tag;\n break;\n }\n }\n }\n if (out.accent && typeof input.accent === 'string') out.accent = accentFor(declaration, out, out.accent.light);\n if (out.palette !== undefined) {\n const theme = themeOf(declaration, out.preset);\n if (out.palette !== 'default' && !theme.palettes?.some((palette) => palette.id === out.palette))\n refusals.push({ field: 'palette', message: 'Pick one of the palettes offered.' });\n }\n if (refusals.length) throw new BrandingRefused(refusals);\n return out;\n}\n\nfunction themeOf(declaration: BrandingDeclaration, id: string | undefined): ThemeDefinition {\n return declaration.themes.find((theme) => theme.id === id) ?? declaration.themes[0]!;\n}\n\n/** The roles an accent stands for where the theme already wore its accent: the link and the menu's current room. */\nconst FOLLOWS_ACCENT = ['link', 'navAccent'] as const;\n\n/**\n * The theme a tenant's people wear: the App's chosen theme and palette, with the\n * tenant's accent laid over the roles that were that accent, and its corner and\n * density. Grounds, ink, lines and recipes are the App's and stay so.\n */\nexport function tenantTheme(declaration: BrandingDeclaration, value: TenantBrandingValue): ThemeDefinition {\n const chosen = themeOf(declaration, value.preset);\n const base = effectiveThemeDefinition(chosen, value.palette ?? chosen.defaultPalette);\n const mode = (name: 'light' | 'dark'): ThemePalette => {\n const palette = { ...base[name] };\n const accent = value.accent?.[name];\n if (!accent) return palette;\n for (const role of FOLLOWS_ACCENT) if (palette[role] === palette.accent) palette[role] = accent;\n /* The first chart series is the brand's, whatever the theme drew it in: Lumen's navy accent beside its blue\n first series left a tenant's teal on the bar and its charts blue, and the form says the accent colors them.\n Unless that would set it beside the second series for a color-blind reader: then the theme's own first\n series stays, as it did, and an accent that was already the first series is still held to the check. */\n if (palette.chart1 === palette.accent || !palette.chart2 || chartsApart(accent, palette.chart2)) palette.chart1 = accent;\n palette.accent = accent;\n /* The words a look chose for its own accent fill (Workshop's ink on its orange) read on that fill, not this one:\n they are derived again for the tenant's, as for an App's accent (themePresets.ts, overlayPalette). */\n delete palette.onAccent;\n /* A tenant picks one color, and it must read as text on the page: `accentInk` said, so the check holds the pick\n to it where an App's theme, which names its inks, is given the one that reads. */\n palette.accentInk = accent;\n palette.focus = accent;\n /* The selected item's tint: as faint as the built-ins draw theirs (Lumen's is about 8% on white), so the accent still reads on it. */\n palette.accentSoft = mixHex(accent, palette.surface, name === 'light' ? 0.08 : 0.2);\n return palette;\n };\n const { palettes: _palettes, defaultPalette: _default, ...rest } = base;\n return {\n ...rest,\n id: 'tenant',\n label: value.name ?? chosen.label,\n light: mode('light'),\n dark: mode('dark'),\n ...(value.radius ? { radius: { control: value.radius, field: value.radius } } : {}),\n ...(value.density ? { density: value.density } : {}),\n };\n}\n\n/**\n * The one validator over the theme a tenant would wear, holding against the\n * tenant only the colors its accent changed, said in a fixed sentence per\n * kind of failure — the person saving picked a color, not a token.\n */\nexport function brandingProblems(declaration: BrandingDeclaration, value: TenantBrandingValue): BrandingRefusal[] {\n const problems = wornFindings(declaration, value);\n const seen = new Set<string>();\n const refusals: BrandingRefusal[] = [];\n for (const [problem, worn] of problems) {\n const message = worn ? sentence(problem).replace(/\\.$/, ` in ${worn}, which some of your people wear.`) : sentence(problem);\n if (seen.has(message)) continue;\n seen.add(message);\n refusals.push({ field: 'accent', message });\n }\n return refusals;\n}\n\n/** The findings on every theme the tenant's accent is worn over: its own pick, and each theme an audience wears\n (ui.theme.for), which the tenant did not pick and so is named beside a failure there. */\nfunction wornFindings(declaration: BrandingDeclaration, value: TenantBrandingValue): (readonly [ThemeProblem, string])[] {\n const { palette: _palette, ...over } = value;\n return [\n ...accentFindings(declaration, value).map((problem) => [problem, ''] as const),\n ...(declaration.audienceThemes ?? [])\n .filter((theme) => theme.id !== themeOf(declaration, value.preset).id)\n .flatMap((theme) =>\n accentFindings({ ...declaration, themes: [theme] }, { ...over, preset: theme.id }).map(\n (problem) => [problem, theme.label] as const,\n ),\n ),\n ];\n}\n\n/** The validator's findings against the colors the tenant's accent changed. */\nfunction accentFindings(declaration: BrandingDeclaration, value: TenantBrandingValue): ThemeProblem[] {\n if (!value.accent) return [];\n const chosen = themeOf(declaration, value.preset);\n const before = effectiveThemeDefinition(chosen, value.palette ?? chosen.defaultPalette);\n const after = tenantTheme(declaration, value);\n const changed = (_palette: string, mode: 'light' | 'dark', role: keyof ThemePalette) => before[mode][role] !== after[mode][role];\n return checkTheme({ ...after, palettes: undefined }, { authored: changed });\n}\n\n/**\n * One brand color for both grounds. The dark theme's accent starts where an\n * App's own does, the color mixed 62% toward white, and where that still reads\n * too dark there — a deep brand blue such as #1d4ed8 on its own tint, in Lumen\n * or Sage by night — it is lifted in OKLCH lightness, its hue and as much of\n * its chroma as sRGB holds kept, until the validator passes it: the brand\n * stays recognisably the brand rather than the save being refused. A color no\n * lift makes readable keeps the plain mix, and the validator says why.\n */\nexport function accentFor(\n declaration: BrandingDeclaration,\n value: TenantBrandingValue,\n light: string,\n): { readonly light: string; readonly dark: string } {\n const start = mixHex(light, '#ffffff', 0.62);\n const darkOk = (dark: string) => !wornFindings(declaration, { ...value, accent: { light, dark } }).some(([p]) => p.mode === 'dark');\n if (darkOk(start)) return { light, dark: start };\n const [l, c, h] = oklch(start);\n for (let lift = l + 0.02; lift <= 0.96; lift += 0.02) {\n const dark = fromOklch(lift, c, h);\n if (darkOk(dark)) return { light, dark };\n }\n return { light, dark: start };\n}\n\n/* OKLab (Björn Ottosson), polar: lightness 0 to 1, chroma, hue in radians. */\nfunction oklch(hex: string): [number, number, number] {\n const [r, g, b] = [1, 3, 5].map((at) => {\n const v = parseInt(hex.slice(at, at + 2), 16) / 255;\n return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n const lms = [\n Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b),\n Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b),\n Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b),\n ] as const;\n const L = 0.2104542553 * lms[0] + 0.793617785 * lms[1] - 0.0040720468 * lms[2];\n const A = 1.9779984951 * lms[0] - 2.428592205 * lms[1] + 0.4505937099 * lms[2];\n const B = 0.0259040371 * lms[0] + 0.7827717662 * lms[1] - 0.808675766 * lms[2];\n return [L, Math.hypot(A, B), Math.atan2(B, A)];\n}\n\n/** A lightness, chroma and hue as hex, the chroma eased down until the color is one sRGB can show. */\nfunction fromOklch(l: number, c: number, h: number): string {\n for (let chroma = c; ; chroma = Math.max(0, chroma - 0.005)) {\n const a = chroma * Math.cos(h),\n b = chroma * Math.sin(h);\n const lms = [\n l + 0.3963377774 * a + 0.2158037573 * b,\n l - 0.1055613458 * a - 0.0638541728 * b,\n l - 0.0894841775 * a - 1.291485548 * b,\n ].map((v) => v ** 3) as [number, number, number];\n const rgb = [\n 4.0767416621 * lms[0] - 3.3077115913 * lms[1] + 0.2309699292 * lms[2],\n -1.2684380046 * lms[0] + 2.6097574011 * lms[1] - 0.3413193965 * lms[2],\n -0.0041960863 * lms[0] - 0.7034186147 * lms[1] + 1.707614701 * lms[2],\n ];\n if (chroma === 0 || rgb.every((v) => v >= -1e-4 && v <= 1 + 1e-4))\n return `#${rgb\n .map((v) => {\n const x = Math.min(1, Math.max(0, v));\n return Math.round((x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055) * 255)\n .toString(16)\n .padStart(2, '0');\n })\n .join('')}`;\n }\n}\n\n/** A finding as the person who picked the color would say it. */\nfunction sentence(problem: ThemeProblem): string {\n const light = problem.mode === 'light';\n const ground = light ? 'white' : 'the dark background';\n if (problem.token.startsWith('chart'))\n return `This accent is too close to the next chart color for color-blind readers in the ${problem.mode} theme. Pick a color further from it.`;\n if (problem.token === 'nav.accent') return `This accent is too ${light ? 'light' : 'dark'} on the menu in the ${problem.mode} theme.`;\n if (problem.token === 'focus' || /focus ring/.test(problem.reason))\n return `This accent is too ${light ? 'light' : 'dark'} on ${ground} for the keyboard focus ring.`;\n if (/selected tab|accentSoft/.test(problem.reason))\n return `This accent is too ${light ? 'light' : 'dark'} to read on its own tint (a selected item) in the ${problem.mode} theme.`;\n if (problem.token === 'link') return `This accent is too ${light ? 'light' : 'dark'} on ${ground} for links.`;\n return `This accent is too ${light ? 'light' : 'dark'} on ${ground} for buttons.`;\n}\n\n/** Everything a save must clear: the value's shape, then the validator. Throws BrandingRefused. */\nexport function validateTenantBranding(raw: unknown, declaration: BrandingDeclaration): TenantBrandingValue {\n const value = parseTenantBranding(raw, declaration);\n const problems = brandingProblems(declaration, value);\n if (problems.length) throw new BrandingRefused(problems);\n return value;\n}\n","/** What BrandingForm says, in the dashboards room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '@mistriwork/kit/client/messages.js';\n\nexport const say = defineMessages({\n loading: 'Loading branding…',\n couldNotLoad: 'Branding could not be loaded.',\n readOnly: \"Only this organization's admins can change its branding.\",\n name: 'Name',\n nameHint: 'What your people see in the bar, the tab and email.',\n logo: 'Logo',\n logoHint: 'PNG, JPEG, WebP or GIF up to 256 KB, or an SVG up to 64 KB with its text as outlines. Drawn 96 by 24 pixels.',\n chooseLogo: 'Choose a file',\n removeLogo: 'Remove',\n noLogo: 'No logo: the name stands alone.',\n darkLogo: 'For a dark bar',\n darkLogoHint: 'Optional. Drawn on a dark bar and by night; the logo above stands where there is none.',\n noDarkLogo: 'None: the logo above is used.',\n accent: 'Accent',\n accentHint: 'Buttons, links, the current page and the first chart series.',\n accentColor: 'Accent color',\n palette: 'Palette',\n theme: 'Theme',\n corner: 'Corners',\n square: 'Square',\n density: 'Density',\n compact: 'Compact',\n comfortable: 'Comfortable',\n spacious: 'Spacious',\n appDefault: 'The App’s',\n save: 'Save',\n saving: 'Saving…',\n reset: 'Undo changes',\n saved: 'Saved. Your people see it on their next page.',\n couldNotSave: 'The branding could not be saved.',\n preview: 'Preview',\n previewShell: 'The bar',\n previewRecord: 'A record',\n previewStat: 'A figure',\n previewEmail: 'An email',\n sampleRecord: 'Acme Corporation',\n sampleStatus: 'Active',\n sampleAction: 'Assign',\n sampleStat: 'Open requests',\n sampleNav: 'Requests',\n sampleOther: 'Accounts',\n sampleEmailLine: 'Your request #1042 has an update.',\n sampleEmailButton: 'View request',\n});\n","/**\n * A tenant's own Branding form: what the App's `branding.allow` lets its admins\n * change, beside a live preview of what their people will see — the bar, a\n * record's head, a figure and an email's header. The preview and the refusals\n * are the save's own (shared/tenantBranding.ts): what this form says before\n * Save is what the server would say after it. Members who are not the App's\n * `branding.editors` see the same form, read-only.\n *\n * Every color reaches the page through a custom property set from a value\n * that already matched `#rrggbb`; a logo is drawn from its own object URL\n * while unsaved, and from the App's logo address once saved.\n */\nimport { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent } from 'react';\nimport type { ThemeDefinition, ThemeDensity } from '@mistriwork/engine/schema/theme.js';\nimport {\n accentFor,\n brandingProblems,\n tenantTheme,\n type BrandingKey,\n type BrandingRefusal,\n type TenantBrandingValue,\n} from '../../shared/tenantBranding.js';\nimport { api } from '@mistriwork/kit/client/core.js';\nimport { served } from '@mistriwork/kit/client/addressMount.js';\nimport { activeApp } from '../appState.js';\nimport { Button } from '@mistriwork/ui/Button.js';\nimport { Dropdown } from '@mistriwork/kit/client/components/Dropdown.js';\nimport { Field } from '@mistriwork/ui/Field.js';\nimport './BrandingForm.css';\nimport { say } from '../copy/dashboards/components/BrandingForm.js';\nimport { shellRoot } from '@mistriwork/kit/client/shellHost.js';\n\ninterface BrandingReply {\n branding: TenantBrandingValue;\n revision: number;\n editor: boolean;\n allow: BrandingKey[];\n themes: ThemeDefinition[];\n error?: string;\n errors?: BrandingRefusal[];\n}\n\nconst HEX = /^#[0-9a-f]{6}$/;\nconst RADII = ['0px', '2px', '4px', '6px', '8px'];\nconst LOGO_ADDRESS = /^\\/api\\/branding\\/logo\\/[0-9a-f]{64}$/;\n\n/** The draft as the save takes it: a key the form leaves empty is the App's. */\ntype Draft = { -readonly [K in keyof TenantBrandingValue]?: TenantBrandingValue[K] };\n\nfunction dark(): boolean {\n const root = shellRoot().dataset.theme;\n return root === 'dark' || (!root && window.matchMedia?.('(prefers-color-scheme: dark)').matches === true);\n}\n\nexport function BrandingForm() {\n const [reply, setReply] = useState<BrandingReply | null>(null);\n const [failed, setFailed] = useState('');\n const [draft, setDraft] = useState<Draft>({});\n /* An upload not yet saved: its bytes for the save, its object URL for the preview; null removes the saved logo. */\n const [upload, setUpload] = useState<{ base64: string; url: string } | null | undefined>(undefined);\n /* The same for the dark logo, which belongs to a logo: removing the logo takes it too. */\n const [darkUpload, setDarkUpload] = useState<{ base64: string; url: string } | null | undefined>(undefined);\n const [phase, setPhase] = useState<'idle' | 'saving'>('idle');\n const [message, setMessage] = useState('');\n const [refused, setRefused] = useState<BrandingRefusal[]>([]);\n const file = useRef<HTMLInputElement>(null);\n const darkFile = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n let alive = true;\n void (async () => {\n try {\n const got = (await api('/api/branding', {})) as BrandingReply;\n if (!alive) return;\n if (got.error) setFailed(got.error);\n else {\n setReply(got);\n setDraft({ ...got.branding });\n }\n } catch (error) {\n if (alive) setFailed(error instanceof Error ? error.message : say('couldNotLoad'));\n }\n })();\n return () => {\n alive = false;\n };\n }, []);\n useEffect(() => () => (upload ? URL.revokeObjectURL(upload.url) : undefined), [upload]);\n useEffect(() => () => (darkUpload ? URL.revokeObjectURL(darkUpload.url) : undefined), [darkUpload]);\n\n const declaration = useMemo(() => (reply ? { themes: reply.themes, allow: reply.allow, editors: [] } : undefined), [reply]);\n /* What the save would judge: the draft without the color a reader is still typing. */\n const candidate = useMemo<TenantBrandingValue>(() => {\n const { accent, ...rest } = draft;\n return accent && HEX.test(accent.light) && HEX.test(accent.dark) ? { ...rest, accent } : rest;\n }, [draft]);\n const theme = useMemo(() => (declaration ? tenantTheme(declaration, candidate) : undefined), [declaration, candidate]);\n const problems = useMemo(() => (declaration ? brandingProblems(declaration, candidate) : []), [declaration, candidate]);\n\n if (failed)\n return (\n <p className=\"branding-form-note\" role=\"alert\">\n {failed}\n </p>\n );\n if (!reply || !declaration || !theme)\n return (\n <p className=\"branding-form-note\" role=\"status\">\n {say('loading')}\n </p>\n );\n\n const allowed = (key: BrandingKey) => reply.allow.includes(key);\n const editor = reply.editor;\n const app = activeApp();\n const name = draft.name?.trim() || app?.name || '';\n const savedLogo = reply.branding.logo ? `/api/branding/logo/${reply.branding.logo.hash}` : undefined;\n const logo = upload === null ? undefined : (upload?.url ?? (savedLogo && LOGO_ADDRESS.test(savedLogo) ? served(savedLogo) : undefined));\n const savedDark = reply.branding.logoDark ? `/api/branding/logo/${reply.branding.logoDark.hash}` : undefined;\n const darkLogo =\n !logo || darkUpload === null\n ? undefined\n : (darkUpload?.url ?? (savedDark && LOGO_ADDRESS.test(savedDark) ? served(savedDark) : undefined));\n const chosen = reply.themes.find((item) => item.id === draft.preset) ?? reply.themes[0]!;\n const palette = theme[dark() ? 'dark' : 'light'];\n const problemFor = (field: BrandingKey) =>\n [...refused.filter((item) => item.field === field), ...(field === 'accent' ? problems : [])].map((item) => item.message)[0] ?? '';\n const set = (patch: Draft) => {\n setDraft((current) => ({ ...current, ...patch }));\n setMessage('');\n setRefused([]);\n };\n const accentText = draft.accent?.light ?? '';\n /* One accent for both grounds, the dark one derived as the save derives it, in the theme the draft wears. */\n const pairFor = (light: string) => accentFor(declaration!, candidate, light);\n const preview = {\n '--bp-ground': palette.ground,\n '--bp-surface': palette.surface,\n '--bp-ink': palette.ink,\n '--bp-muted': palette.inkMuted,\n '--bp-line': palette.line,\n '--bp-accent': palette.accent,\n '--bp-accent-soft': palette.accentSoft,\n '--bp-nav': palette.navGround ?? palette.surface,\n '--bp-nav-ink': palette.navInk ?? palette.ink,\n '--bp-nav-accent': palette.navAccent ?? palette.accent,\n '--bp-chart': palette.chart1 ?? palette.accent,\n '--bp-radius': draft.radius ?? theme.radius?.control ?? '4px',\n } as CSSProperties;\n\n const pick = async (event: ChangeEvent<HTMLInputElement>, into: typeof setUpload) => {\n const chosenFile = event.target.files?.[0];\n event.target.value = '';\n if (!chosenFile) return;\n const bytes = new Uint8Array(await chosenFile.arrayBuffer());\n let binary = '';\n for (let index = 0; index < bytes.length; index += 0x8000) binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));\n into({ base64: btoa(binary), url: URL.createObjectURL(chosenFile) });\n setMessage('');\n setRefused([]);\n };\n const save = async () => {\n if (phase === 'saving' || !editor || problems.length) return;\n setPhase('saving');\n setMessage('');\n try {\n const { logo: _logo, logoDark: _logoDark, ...value } = draft;\n const got = (await api('/api/branding/save', {\n branding:\n value.accent && !(HEX.test(value.accent.light) && HEX.test(value.accent.dark)) ? { ...value, accent: value.accent.light } : value,\n ...(upload === undefined ? {} : { logo: upload === null ? null : upload.base64 }),\n ...(darkUpload === undefined ? {} : { darkLogo: darkUpload === null ? null : darkUpload.base64 }),\n })) as BrandingReply;\n if (got.error) {\n setRefused(got.errors ?? [{ field: 'name', message: got.error }]);\n setMessage(got.error);\n } else {\n setReply(got);\n setDraft({ ...got.branding });\n setUpload(undefined);\n setDarkUpload(undefined);\n setMessage(say('saved'));\n }\n } catch (error) {\n setMessage(error instanceof Error ? error.message : say('couldNotSave'));\n } finally {\n setPhase('idle');\n }\n };\n const disabled = !editor || phase === 'saving';\n\n return (\n <div className=\"branding-form\">\n <form\n className=\"branding-form-fields\"\n onSubmit={(event) => {\n event.preventDefault();\n void save();\n }}\n noValidate\n >\n {!editor && <p className=\"branding-form-note\">{say('readOnly')}</p>}\n {allowed('name') && (\n <Field label={say('name')} hint={say('nameHint')} problem={problemFor('name')} size=\"compact\">\n <input\n className=\"field-control\"\n name=\"name\"\n value={draft.name ?? ''}\n placeholder={app?.name}\n maxLength={80}\n disabled={disabled}\n onChange={(event) => set({ name: event.target.value || undefined })}\n />\n </Field>\n )}\n {allowed('logo') && (\n <Field label={say('logo')} hint={say('logoHint')} problem={problemFor('logo')} size=\"compact\" group>\n {(control) => (\n <div className=\"branding-form-logo\" {...control} role=\"group\">\n <span className=\"branding-form-logo-box\">{logo ? <img src={logo} alt=\"\" width={96} height={24} /> : say('noLogo')}</span>\n <input\n ref={file}\n type=\"file\"\n hidden\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/svg+xml\"\n onChange={(event) => void pick(event, setUpload)}\n />\n <Button intent=\"secondary\" size=\"compact\" disabled={disabled} onClick={() => file.current?.click()}>\n {say('chooseLogo')}\n </Button>\n {logo && (\n <Button\n intent=\"quiet\"\n size=\"compact\"\n disabled={disabled}\n onClick={() => setUpload(reply.branding.logo ? null : undefined)}\n >\n {say('removeLogo')}\n </Button>\n )}\n </div>\n )}\n </Field>\n )}\n {allowed('logo') && logo && (\n <Field label={say('darkLogo')} hint={say('darkLogoHint')} size=\"compact\" group>\n {(control) => (\n <div className=\"branding-form-logo\" {...control} role=\"group\">\n <span className=\"branding-form-logo-box branding-form-logo-box--dark\">\n {darkLogo ? <img src={darkLogo} alt=\"\" width={96} height={24} /> : say('noDarkLogo')}\n </span>\n <input\n ref={darkFile}\n type=\"file\"\n hidden\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/svg+xml\"\n onChange={(event) => void pick(event, setDarkUpload)}\n />\n <Button intent=\"secondary\" size=\"compact\" disabled={disabled} onClick={() => darkFile.current?.click()}>\n {say('chooseLogo')}\n </Button>\n {darkLogo && (\n <Button\n intent=\"quiet\"\n size=\"compact\"\n disabled={disabled}\n onClick={() => setDarkUpload(reply.branding.logoDark ? null : undefined)}\n >\n {say('removeLogo')}\n </Button>\n )}\n </div>\n )}\n </Field>\n )}\n {allowed('accent') && (\n <Field label={say('accent')} hint={say('accentHint')} problem={problemFor('accent')} size=\"compact\" group>\n {(control) => (\n <div className=\"branding-form-accent\" {...control} role=\"group\">\n <input\n type=\"color\"\n aria-label={say('accentColor')}\n value={HEX.test(accentText) ? accentText : palette.accent}\n disabled={disabled}\n onChange={(event) => set({ accent: pairFor(event.target.value) })}\n />\n <input\n className=\"field-control\"\n aria-label={say('accentColor')}\n value={accentText}\n placeholder={chosen.light.accent}\n spellCheck={false}\n maxLength={7}\n disabled={disabled}\n onChange={(event) => {\n const typed = event.target.value.trim().toLowerCase();\n set({ accent: typed ? (HEX.test(typed) ? pairFor(typed) : { light: typed, dark: typed }) : undefined });\n }}\n />\n </div>\n )}\n </Field>\n )}\n {allowed('preset') && (\n <Field label={say('theme')} problem={problemFor('preset')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('theme')}\n disabled={disabled}\n value={draft.preset ?? reply.themes[0]!.id}\n items={reply.themes.map((item) => ({ value: item.id, label: item.label }))}\n onChange={(next) => set({ preset: next, palette: undefined })}\n />\n </Field>\n )}\n {allowed('palette') && chosen.palettes?.length ? (\n <Field label={say('palette')} problem={problemFor('palette')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('palette')}\n disabled={disabled}\n value={draft.palette ?? chosen.defaultPalette ?? 'default'}\n items={[{ value: 'default', label: chosen.label }, ...chosen.palettes.map((item) => ({ value: item.id, label: item.label }))]}\n onChange={(next) => set({ palette: next })}\n />\n </Field>\n ) : null}\n {allowed('radius') && (\n <Field label={say('corner')} problem={problemFor('radius')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('corner')}\n disabled={disabled}\n value={draft.radius ?? ''}\n items={[\n { value: '', label: say('appDefault') },\n ...RADII.map((radius) => ({ value: radius, label: radius === '0px' ? say('square') : radius })),\n ]}\n onChange={(next) => set({ radius: next || undefined })}\n />\n </Field>\n )}\n {allowed('density') && (\n <Field label={say('density')} problem={problemFor('density')} size=\"compact\" group>\n <Dropdown\n ariaLabel={say('density')}\n disabled={disabled}\n value={draft.density ?? ''}\n items={[\n { value: '', label: say('appDefault') },\n { value: 'compact', label: say('compact') },\n { value: 'comfortable', label: say('comfortable') },\n { value: 'spacious', label: say('spacious') },\n ]}\n onChange={(next) => set({ density: (next || undefined) as ThemeDensity | undefined })}\n />\n </Field>\n )}\n {editor && (\n <footer className=\"form-actions\">\n <p className=\"branding-form-status\" role=\"status\">\n {message}\n </p>\n <Button\n intent=\"secondary\"\n size=\"compact\"\n disabled={phase === 'saving'}\n onClick={() => {\n setDraft({ ...reply.branding });\n setUpload(undefined);\n setMessage('');\n setRefused([]);\n }}\n >\n {say('reset')}\n </Button>\n <Button intent=\"primary\" size=\"compact\" type=\"submit\" disabled={phase === 'saving' || problems.length > 0}>\n {phase === 'saving' ? say('saving') : say('save')}\n </Button>\n </footer>\n )}\n </form>\n <section className=\"branding-preview\" aria-label={say('preview')} style={preview}>\n <figure>\n <figcaption>{say('previewShell')}</figcaption>\n <div className=\"branding-preview-shell\">\n <span className=\"branding-preview-mark\">\n {logo ? <img src={dark() && darkLogo ? darkLogo : logo} alt=\"\" width={96} height={24} /> : null}\n </span>\n <strong>{name}</strong>\n <span className=\"branding-preview-nav\" data-current>\n {say('sampleNav')}\n </span>\n <span className=\"branding-preview-nav\">{say('sampleOther')}</span>\n </div>\n </figure>\n <figure>\n <figcaption>{say('previewRecord')}</figcaption>\n <div className=\"branding-preview-record\">\n <strong>{say('sampleRecord')}</strong>\n <span className=\"branding-preview-pill\">{say('sampleStatus')}</span>\n <span className=\"branding-preview-button\">{say('sampleAction')}</span>\n </div>\n </figure>\n <figure>\n <figcaption>{say('previewStat')}</figcaption>\n <div className=\"branding-preview-stat\">\n <span>{say('sampleStat')}</span>\n <strong>128</strong>\n <span className=\"branding-preview-bars\" aria-hidden=\"true\">\n {[40, 64, 52, 80, 70].map((height, index) => (\n <i key={index} style={{ height: `${height}%` }} />\n ))}\n </span>\n </div>\n </figure>\n <figure>\n <figcaption>{say('previewEmail')}</figcaption>\n <div className=\"branding-preview-email\">\n <div className=\"branding-preview-email-head\">\n {logo ? <img src={logo} alt=\"\" width={96} height={24} /> : <strong>{name}</strong>}\n </div>\n <p>{say('sampleEmailLine')}</p>\n <span className=\"branding-preview-button\">{say('sampleEmailButton')}</span>\n </div>\n </figure>\n </section>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAWA,SAAgB,gBAAgB,OAAe,QAAQ,GAAsB;CAC3E,IAAI,QAAQ,GAAG,OAAO,KAAA;CACtB,QAAQ,MAAM,KAAK,CAAC,CAAC,YAAY;CACjC,IAAI,UAAU,eAAe,OAAO;EAAC;EAAG;EAAG;EAAG;CAAC;CAC/C,IAAI,UAAU,SAAS,OAAO;EAAC;EAAG;EAAG;EAAG;CAAC;CACzC,IAAI,UAAU,SAAS,OAAO;EAAC;EAAG;EAAG;EAAG;CAAC;CACzC,IAAI,+CAA+C,KAAK,KAAK,GAAG;EAC9D,MAAM,MACJ,MAAM,SAAS,IACX,MACG,MAAM,CAAC,CAAC,CACR,MAAM,EAAE,CAAC,CACT,KAAK,SAAS,OAAO,IAAI,CAAC,CAC1B,KAAK,EAAE,IACV,MAAM,MAAM,CAAC;EACnB,OAAO;GACL,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,IAAI;GAChC,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,IAAI;GAChC,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,IAAI;GAChC,IAAI,WAAW,IAAI,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,IAAI,MAAM;EAC3D;CACF;CAGA,MAAM,MAAM,6DAA6D,KAAK,KAAK;CACnF,MAAM,QAAQ,OAAO,gBAAgB,IAAI,IAAK,QAAQ,CAAC;CACvD,OAAO,QAAQ,SAAS,OAAO,gBAAgB,IAAK,EAAG,GAAI,OAAO,IAAK,EAAE,IAAI,GAAG,IAAI,KAAA;AACtF;AAEA,SAAgB,SAAS,GAAU,GAAU,QAAuB;CAClE,MAAM,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,IAAI;CAC1C,MAAM,WAAW,UAAsB,SAAS,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,IAAI,WAAW,QAAQ;CACrH,OAAO;EAAC,QAAQ,CAAC;EAAG,QAAQ,CAAC;EAAG,QAAQ,CAAC;EAAG;CAAK;AACnD;AAEA,SAAgB,KAAK,OAAc,QAAsB;CACvD,MAAM,QAAQ,MAAM,KAAK,OAAO,MAAM,IAAI,MAAM;CAChD,MAAM,WAAW,UAAsB,SAAS,MAAM,SAAS,MAAM,KAAK,OAAO,SAAS,OAAO,MAAM,IAAI,MAAM,OAAO,QAAQ;CAChI,OAAO;EAAC,QAAQ,CAAC;EAAG,QAAQ,CAAC;EAAG,QAAQ,CAAC;EAAG;CAAK;AACnD;AAEA,SAAgB,iBAAiB,YAAmB,YAAuC;CACzF,IAAI,WAAW,OAAO,GAAG,OAAO,KAAA;CAChC,MAAM,aAAa,UACjB,MACG,MAAM,GAAG,CAAC,CAAC,CACX,KAAK,UAAW,SAAS,SAAU,QAAQ,UAAU,QAAQ,QAAS,UAAU,GAAI,CAAC,CACrF,QAAQ,OAAO,OAAO,UAAU,QAAQ,QAAQ;EAAC;EAAQ;EAAQ;CAAM,CAAC,CAAC,QAAS,CAAC;CACxF,MAAM,IAAI,UAAU,KAAK,YAAY,UAAU,CAAC,GAC9C,IAAI,UAAU,UAAU;CAC1B,QAAQ,KAAK,IAAI,GAAG,CAAC,IAAI,QAAS,KAAK,IAAI,GAAG,CAAC,IAAI;AACrD;;AAGA,SAAgB,SAAS,OAAsB;CAC7C,OAAO,IAAI,MACR,MAAM,GAAG,CAAC,CAAC,CACX,KAAK,UACJ,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAC9C,SAAS,EAAE,CAAC,CACZ,SAAS,GAAG,GAAG,CACpB,CAAC,CACA,KAAK,EAAE;AACZ;;AAGA,IAAa,WAAW;;AAExB,IAAM,aAAa;;;;;;;AAQnB,SAAgB,QAAQ,SAAmE;CACzF,MAAM,SAAS,QAAQ,UAAU,gBAAgB,QAAQ,MAAM,GAC7D,UAAU,QAAQ,WAAW,gBAAgB,QAAQ,OAAO,GAC5D,MAAM,QAAQ,OAAO,gBAAgB,QAAQ,GAAG;CAClD,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,KAAK,OAAO;CACxC,KAAK,IAAI,OAAO,KAAK,MAAM,WAAW,GAAG,GAAG,OAAO,aAAa,KAAK,QAAQ;EAC3E,MAAM,QAAQ,iBAAiB,KAAK,KAAK,SAAS,QAAQ;GAAC;GAAG;GAAG;GAAG;EAAC,GAAG,OAAO,GAAG,GAAG,OAAO,CAAC;EAC7F,IAAI,UAAU,KAAA,KAAa,SAAS,KAAK,OAAO,OAAO;CACzD;CACA,OAAO;AACT;;;ACpFA,IAAM,mBAAmB;CAAE,GAAG;CAAK,GAAG;CAAK,GAAG;AAAE;AAGhD,IAAM,iBAAiB;CAAE,GAAG;CAAK,GAAG;CAAM,GAAG;AAAI;;AAEjD,IAAa,mBAAmB,aAC7B,QAAQ,SAAS,UAAU,iBAAiB,iBAAA,CAAkB,QAAQ,YAAY;AAKzC,OAAO,KAAK,cAAc,CAAC,CACpE,QAAQ,SAAS,SAAS,YAAY,SAAS,iBAAiB,SAAS,cAAc,CAAC,CACxF,IAAI,QAAQ;;AAEf,SAAgB,SAAS,MAAsB;CAC7C,MAAM,SAAS,kBAAkB,KAAK,IAAI;CAC1C,IAAI,QAAQ,OAAO,QAAQ,OAAO,OAAO,EAAE,IAAI;CAC/C,OAAO,KAAK,QAAQ,WAAW,GAAG,WAAmB,OAAO,YAAY,CAAC;AAC3E;;;ACsWA,SAAS,UAAU,KAAqB;CACtC,MAAM,WAAW;EAAC;EAAG;EAAG;CAAC,CAAC,CACvB,KAAK,UAAU,SAAS,IAAI,MAAM,OAAO,QAAQ,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC,CAC/D,KAAK,UAAW,SAAS,SAAU,QAAQ,UAAU,QAAQ,QAAS,UAAU,GAAI;CACvF,OAAO,SAAS,KAAM,QAAS,SAAS,KAAM,QAAS,SAAS,KAAM;AACxE;AAEA,SAAgB,cAAc,GAAW,GAAmB;CAC1D,MAAM,OAAO,UAAU,CAAC,GACtB,QAAQ,UAAU,CAAC;CACrB,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,QAAS,KAAK,IAAI,MAAM,KAAK,IAAI;AACnE;;;;;;;;AASA,IAAa,uBAAuB;;;;;;;AAQpC,IAAM,iBAAoG;CACxG;EAAC;EAAO;EAAW;CAAG;CACtB;EAAC;EAAY;EAAW;CAAG;CAC3B;EAAC;EAAa;EAAW;CAAG;CAC5B;EAAC;EAAO;EAAU;CAAG;CACrB;EAAC;EAAO;EAAU;CAAG;CAIrB;EAAC;EAAO;EAAS;EAAK;CAAsB;CAC5C;EAAC;EAAY;EAAS;EAAK;CAAuB;CAClD;EAAC;EAAa;EAAS;EAAK;CAAuB;CACnD;EAAC;EAAe;EAAS;EAAG;CAAoC;CAGhE;EAAC;EAAY;EAAU;EAAK;CAAuB;CACnD;EAAC;EAAa;EAAU;EAAK;CAA6B;CAC1D;EAAC;EAAQ;EAAU;CAAG;CACtB;EAAC;EAAQ;EAAU;CAAG;CACtB;EAAC;EAAa;EAAa;EAAK;CAA0B;CAG1D;EAAC;EAAa;EAAW;CAAG;CAE5B;EAAC;EAAa;EAAU;EAAK;CAAqC;CAClE;EAAC;EAAa;EAAU;EAAK;CAA4D;CACzF;EAAC;EAAY;EAAU;EAAK;CAA6B;CACzD;EAAC;EAAa;EAAW;EAAK;CAAsC;CACpE;EAAC;EAAQ;EAAW;CAAG;CACvB;EAAC;EAAU;EAAa;EAAK;CAAoB;CACjD;EAAC;EAAY;EAAa;EAAK;CAA6C;CAC5E,GAAI;EAAC;EAAc;EAAiB;EAAkB;CAAY,CAAC,CAAW,SAC3E,SACC;EACE;GAAC;GAAM;GAAW;EAAG;EACrB;GAAC;GAAM;GAAU;EAAG;EACpB;GAAC;GAAM;GAAU;GAAK;EAAqD;CAC7E,CACJ;CACA;EAAC;EAAe;EAAW;CAAC;CAC5B;EAAC;EAAe;EAAU;CAAC;CAG3B;EAAC;EAAe;EAAU;EAAG;CAAsD;CAEnF;EAAC;EAAS;EAAW;CAAC;CACtB;EAAC;EAAS;EAAU;CAAC;CACrB;EAAC;EAAS;EAAU;CAAC;CAGrB,GAAI;EAAC;EAAU;EAAU;EAAU;CAAQ,CAAC,CAAW,KAAK,WAAW;EAAC;EAAQ;EAAW;CAAC,CAAU;AACxG;AAIA,IAAMA,WAAS;CAAC;CAAU;CAAU;CAAU;CAAU;CAAU;CAAU;CAAU;AAAQ;;;;;;;;;AAU9F,SAAgB,cACd,SACA,UAAiC,CAAC,GACwD;CAC1F,MAAM,SAAS,QAA6B,QAAQ,OAAO,gBAAgB,QAAQ,IAAK,IAAI,KAAA;CAC5F,MAAM,MAAM,MAAM,KAAK,GACrB,UAAU,MAAM,SAAS;CAC3B,IAAI,CAAC,OAAO,CAAC,SAAS,OAAO,CAAC;CAC9B,MAAM,QAAe;EAAC;EAAG;EAAG;EAAG;CAAC;CAChC,MAAM,MAA8F,CAAC;CACrG,SAAO,SAAS,MAAM,UAAU;EAC9B,MAAM,SAAS,MAAM,IAAI;EACzB,IAAI,CAAC,QAAQ;EACb,IAAI,QAAQ,UAAU,SACpB,IAAI,KAAK;GACP;GACA,MAAM;GACN,IAAI,SAAS,QAAQ,KAAK,GAAI;GAC9B,IAAI,KAAK,SAAS,QAAQ,OAAO,GAAI,GAAG,OAAO;GAC/C,OAAO;EACT,CAAC;EACH,IAAI,QAAQ,GACV,IAAI,KAAK;GACP;GACA,MAAM;GACN,IAAI,SAAS,QAAQ,KAAK,GAAI;GAC9B,IAAI,SAAS,QAAQ,SAAS,KAAK,SAAS,GAAI,GAAG,GAAI;GACvD,OAAO;EACT,CAAC;EACH,IAAI,QAAQ,SAAS,UAAU,QAAQ,GACrC,IAAI,KAAK;GACP;GACA,MAAM;GACN,IAAI,SAAS,QAAQ,KAAK,EAAG;GAC7B,IAAI,SAAS,QAAQ,SAAS,GAAI;GAClC,OAAO;EACT,CAAC;CACL,CAAC;CAED,MAAM,SAAS,MAAM,QAAQ,GAC3B,QAAQ,MAAM,QAAQ;CACxB,IAAI,UAAU,OAAO;EACnB,MAAM,OAAO,SAAS,QAAQ,OAAO,EAAG;EACxC,IAAI,KAAK;GACP,MAAM;GACN,MAAM;GACN,IAAI,SAAS,MAAM,KAAK,GAAI;GAC5B,IAAI,SAAS,MAAM,SAAS,KAAK,SAAS,GAAI,GAAG,GAAI;GACrD,OAAO;EACT,CAAC;CACH;CACA,OAAO,IAAI,KAAK,UAAU;EAAE,GAAG;EAAM,IAAI,SAAS,KAAK,KAAK,IAAI,KAAK,EAAE,CAAC;EAAG,IAAI,SAAS,KAAK,EAAE;CAAE,EAAE;AACrG;;;;;;AAOA,SAAgB,UACd,SACA,MACA,OACwF;CACxF,MAAM,aAAa,gBAAgB,KAAK;CACxC,IAAI,CAAC,YAAY,OAAO,CAAC;CACzB,OAAO,eAAe,SAAS,CAAC,KAAK,YAAY,OAAO,UAAU;EAChE,MAAM,SAAS,QAAQ;EACvB,IAAI,QAAQ,QAAQ,CAAC,WAAY,eAAe,YAAY,eAAe,YAAY,WAAW,QAAQ,SAAU,OAAO,CAAC;EAC5H,MAAM,kBAAkB,gBAAgB,MAAM;EAC9C,MAAM,QAAQ,mBAAmB,iBAAiB,YAAY,eAAe;EAC7E,OAAO,UAAU,KAAA,KAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;GAAE;GAAY;GAAO,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;GAAI;EAAM,CAAC;CACtG,CAAC;AACH;;AAqBA,SAAgB,cAAc,QAAoC,MAA8B,CAAC,GAAG,MAAM,OAAuB;CAC/H,OAAO,OAAO,SAAS,UACrB,CAAC,WAAW,IAAI,MAAM,YAAY,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,OACrE,WAAW,KAAK,CAAC,CAAC,SAAS,SAAS;EAClC,MAAM,aAAa,yBAAyB,OAAO,EAAE;EACrD,MAAM,UAAU,SAAS,sBAAsB,WAAW,OAAO,SAAS,QAAQ,GAAG,CAAC;EACtF,MAAM,OAAO,OAAO,YAAY,MAAM,QAAQ,GAAG,MAAM,MAAM,KAAK,MAAM,SAAU,MAAM,SAAS,KAAK,OAAO,EAAE,CAAC,CAAE;EAClH,MAAM,KAAK;GAAE,SAAS,MAAM;GAAI,WAAW;GAAI;EAAK;EACpD,OAAO,eAAe,SAAS,CAAC,YAAY,YAAY,OAAO,UAA0B;GACvF,MAAM,IAAI,QAAQ,aAChB,IAAI,QAAQ;GAEd,IAAI,CAAC,KAAK,CAAC,MAAO,eAAe,YAAY,eAAe,YAAY,MAAM,QAAQ,SAAU,OAAO,CAAC;GAExG,IAAI,eAAe,WAAW,eAAe,iBAAiB,QAAQ,iBAAiB,OAAO,OAAO,CAAC;GACtG,MAAM,kBAAkB,gBAAgB,CAAC,GACvC,kBAAkB,gBAAgB,CAAC;GACrC,IAAI,CAAC,mBAAmB,CAAC,iBAAiB,OAAO,CAAC;GAClD,MAAM,QAAQ,iBAAiB,iBAAiB,eAAe;GAC/D,IAAI,UAAU,KAAA,KAAc,CAAC,OAAO,SAAS,OAAQ,OAAO,CAAC;GAC7D,MAAM,OACJ,UAAU,MACN,4BACA,WAAW,WAAW,OAAO,IAC3B,6BACA,eAAe,UACb,6BACA;GACV,OAAO,CACL;IACE,GAAG;IACH,OAAO;IACP,OAAO,GAAG,WAAW,MAAM,aAAa,OAAO,KAAK,KAAK,KAAK;IAC9D;IACA;IACA,SAAS,UAAU,KAAK,KAAK,KAAK,KAAK,OAAO,GAAG,KAAK,MAAM,KAAK,SAAS,UAAU,EAAE,GAAG,SAAS,UAAU,EAAE,eAAe,MAAM,QAAQ,CAAC,EAAE,MAAM,KAAK;GAC3J,CACF;EACF,CAAC,CAAC,CAAC,OAAO,cAAc,YAAY,MAAM,IAAI,SAAS,GAAG,CAAC;CAC7D,CAAC,CACH,CACF;AACF;AA6DA,SAAS,cACP,OACA,MACA,IACA,SACA,KACgB;CAChB,MAAM,SAAyB,CAAC;CAChC,MAAM,OAAO,GAAG;CAChB,MAAM,UAAU,MAAM,WAAW,CAAC;CAClC,MAAM,SAAS,QAA6B,QAAQ,OAAO,gBAAgB,QAAQ,IAAK,IAAI,KAAA;CAC5F,MAAM,SACJ,OACA,YACA,YACA,QAAQ,KACR,OACA,SACG;EACH,IAAI,CAAC,cAAc,CAAC,YAAY;EAChC,MAAM,QAAQ,iBAAiB,YAAY,UAAU;EACrD,IAAI,UAAU,KAAA,KAAc,CAAC,OAAO,SAAS,OAAQ;EACrD,MAAM,OAAO,SAAS,UAAU,MAAM,4BAA4B;EAClE,OAAO,KAAK;GACV,GAAG;GACH,OAAO,SAAU,gBAAgB,KAAK,KAAK,CAAC,CAAE;GAC9C,OAAO;GACP;GACA;GACA,SAAS,UAAU,KAAK,KAAK,KAAK,KAAK,MAAM,QAAQ,QAAQ,QAAQ,CAAC,CAAC,QAAQ,aAAa,GAAG,SAAiB,IAAI,SAAS,IAAI,GAAG,EAAE,eAAe,MAAM,QAAQ,CAAC,EAAE,MAAM,KAAK;EACnL,CAAC;CACH;CACA,MAAM,UAAU,MAAM,SAAS,GAC7B,SAAS,MAAM,QAAQ,GACvB,OAAO,MAAM,KAAK;CAIpB,IAAI,QAAQ,iBAAiB,OAC3B,KAAK,MAAM,UAAU,CAAC,UAAU,SAAS,GAAY;EACnD,IAAI,WAAW,aAAa,CAAC,QAAQ,UAAU,QAAQ,WAAW,QAAQ,UAAU;EACpF,MACE,SAAS,UACT,MAAM,OAAO,KAAK,SAClB,MAAM,MAAM,GACZ,sBACA,SACA,8DAA8D,qBAAqB,oBAAoB,OAAO,uEAChH;CACF;CAIF,KAAK,MAAM,OAAO,CAAC,YAAY,WAAW,GAAY,MAAM,GAAG,IAAI,UAAU,MAAM,GAAG,GAAG,MAAM;CAI/F,KAAK,MAAM,QAAQ;EAAC;EAAc;EAAiB;EAAkB;CAAY,GAAY;EAC3F,MAAM,MAAM,MAAM,IAAI;EACtB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS;EAC/B,MAAM,GAAG,KAAK,aAAa,SAAS,KAAK,MAAM,GAAI,GAAG,KAAK,SAAS,KAAK;GAAC;GAAG;GAAG;GAAG;EAAC,GAAG,GAAI,GAAG,OAAO,CAAC;CACxG;CAEA,KAAK,MAAM,QAAQ,cAAc,SAAS,OAAO,GAC/C,MACE,GAAG,KAAK,KAAK,MAAM,KAAK,QACxB,gBAAgB,KAAK,EAAE,GACvB,gBAAgB,KAAK,EAAE,GACvB,KAAK,OACL,KAAK,MACL,KAAK,UAAU,IAAI,qBAAqB,KAAA,CAC1C;CAGF,MAAM,SAAS,MAAM,gBAAgB;CACrC,IAAI,UAAU,SAAS,MAAM,gCAAgC,QAAQ,SAAS,QAAQ,SAAS,EAAG,CAAC;CAGnG,MAAM,SAAS,MAAM,QAAQ;CAC7B,IAAI,UAAU,SACZ,MACE,qDACA,MACA,KAAK,SAAS,QAAQ;EAAC;EAAG;EAAG;EAAG;CAAC,GAAG,QAAQ,OAAO,CAAC,GAAG,OAAO,GAC9D,KACA,QACF;CAIF,IAAI,SAAS,WAAW,MAAM,WAAW,aACvC,KAAK,MAAM,OAAO,CAAC,aAAa,MAAM,GAAY;EAEhD,MAAM,QAAQ,QAAQ;EACtB,MAAM,OAAO,QAA4B,QAAQ,KAAA,KAAa,kBAAkB,KAAK,GAAG;EACxF,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,QAAQ,OAAO,KAAK,cAAc,OAAQ,SAAS,IAAI,cAAc,QAAQ,SAAU,SAAS,GACtH,MAAM,GAAG,IAAI,oBAAoB,MAAM,GAAG,GAAG;GAAC;GAAG;GAAG;GAAG;EAAC,GAAG,KAAK,GAAG;CACvE;CACF,MAAM,OAAO,MAAM,YAAY;CAC/B,KAAK,MAAM,UAAU,CAAC,WAAW,QAAQ,GAAY;EACnD,MAAM,OAAO,MAAM,MAAM;EACzB,IAAI,WAAW,YAAY,QAAQ,WAAW,QAAQ,SAAS;EAE/D,MAAM,6BAA6B,UAAU,MAAM,WAAW,GAAG,QAAQ,OAAO,KAAK,MAAM,IAAI,IAAI,KAAA,CAAS;CAC9G;CAEA,MAAM,8CAA8C,MAAM,QAAQ,UAAU,KAAK,MAAM,OAAO,IAAI,KAAA,CAAS;CAC3G,MAAM,MAAM,MAAM,WAAW,GAC3B,SAAS,MAAM,QAAQ;CAEzB,MAAM,aAAa,WAAoB,OAAO,SAAS,KAAK,SAAS,QAAQ;EAAC;EAAG;EAAG;EAAG;CAAC,GAAG,MAAM,GAAG,GAAG,IAAI,KAAA;CAC3G,MAAM,8BAA8B,MAAM,WAAW,GAAG,UAAU,GAAI,CAAC;CAEvE,IAAI,QAAQ,YAAY,MAAM,2BAA2B,MAAM,cAAc,KAAK,QAAQ,MAAM,YAAY,CAAC;CAC7G,MAAM,8BAA8B,QAAQ,UAAU,GAAI,CAAC;CAI3D,IAAI,QAAQ,QAAQ,UAAU,CAAC,QAAQ,YACrC,MACE,uEACA,MAAM,WAAW,GACjB,QAAQ,MAAM,KAAK,MAAM,GAAG,IAAI,KAAA,CAClC;CAGF,IAAI,QAAQ,QAAQ,QAAQ,MAAM,8BAA8B,MAAM,UAAU,GAAG,UAAU,GAAI,CAAC;CAIlG,IAAI,QAAQ,QAAQ,QAAQ,MAAM,gCAAgC,QAAQ,UAAU,GAAI,CAAC;CACzF,IAAI,QAAQ,UAAU,UAAU;EAC9B,MAAM,SAAS,MAAM,QAAQ,GAC3B,UAAU,MAAM,SAAS;EAC3B,MAAM,QAAQ,UAAU,UAAU,SAAS,QAAQ,SAAS,GAAI,IAAI,KAAA;EACpE,MAAM,wBAAwB,MAAM,KAAK,GAAG,KAAK;EACjD,MAAM,yBAAyB,MAAM,MAAM,GAAG,KAAK;CACrD;CAEA,IAAI,QAAQ,UAAU,YAAY,QAAQ,MACxC,KAAK,MAAM,OAAO;EAChB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,GACE,MAAM,GAAG,IAAI,QAAQ,MAAM,GAAG,GAAG,MAAM,MAAM,GAAG,KAAK,MAAM;CAG/D,MAAM,aAAa,UAA4B;EAC7C,MAAM,UAAU,MAAM,UAAU;EAChC,IAAI,CAAC,SAAS;EACd,MAAM,OAAO,MAAM,UAAU,QAAQ,YAAY,MAAM,KAAK,MAAM,MAAM;EACxE,MAAM,SAAS,UAAU,QAAQ,MAAM,WAAW,IAAK,MAAM,QAAQ,KAAK;EAC1E,IAAI,CAAC,QAAQ,CAAC,QAAQ;EACtB,MAAM,QAAQ,KAAK,SAAS,MAAM;GAAC,KAAK;GAAI,KAAK;GAAI,KAAK;GAAI;EAAC,GAAG,gBAAgB,OAAO,CAAC,GAAG,MAAM;EACnG,MAAM,QAAQ,UAAU,QAAQ,cAAc,QAAQ,KAAK,YAAY,cAAc,QAAQ,KAAK;EAClG,MAAM,OAAO,UAAU,QAAS,CAAC,UAAU,UAAU,IAAe;GAAC;GAAO;GAAY;GAAa;GAAQ;EAAW;EACxH,KAAK,MAAM,OAAO,MAAM,MAAM,GAAG,IAAI,MAAM,SAAS,MAAM,GAAG,GAAG,OAAO,KAAK,UAAU,QAAQ,YAAY,MAAM;CAClH;CACA,UAAU,QAAQ;CAClB,UAAU,KAAK;CAEf,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,SAAS,CAAC,CAAC,GAAG;EAC7D,IAAI,CAAC,SAAU,MAAM,WAAW,KAAA,KAAa,MAAM,QAAQ,KAAA,GAAY;EACvE,MAAM,MAAM,MAAM,MAAM,SAAS,MAAM,GAAG,IAAI,QAAQ,IAAI;EAC1D,MAAM,SAAS,MAAM,SAAS,SAAS,MAAM,MAAM,IAAI,WAAW,MAAM,OAAO;EAC/E,IAAI,CAAC,OAAO,CAAC,QAAQ;EACrB,MAAM,QAAQ,SAAiB,SAAS,IAAI;EAC5C,MAAM,QAAQ,SAAS,KAAK,IAAI,IAAI,MAAM;EAC1C,MAAM,aAAa,MAAM,GAAyB,GAChD,aAAa,MAAM,MAA4B;EACjD,IAAI,CAAC,cAAc,CAAC,YAAY;EAChC,MAAM,QAAQ,iBAAiB,YAAY,UAAU;EACrD,IAAI,UAAU,KAAA,KAAc,CAAC,OAAO,SAAS,KAAM;EACnD,OAAO,KAAK;GACV,GAAG;GACH,OAAQ,MAAM,SAAS,SAAS;GAChC,OAAO;GACP;GACA,OAAO;GACP,SAAS,UAAU,KAAK,KAAK,KAAK,WAAW,KAAK,IAAI,KAAK,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,eAAe,MAAM,QAAQ,CAAC,EAAE;EACrH,CAAC;CACH;CACA,OAAO;AACT;;;AAIA,SAAS,QAAQ,MAAkC;CACjD,IAAI,SAAS,kBAAkB,OAAO;CACtC,IAAI,sBAAsB,KAAK,IAAI,GAAG,OAAO;CAC7C,OAAO,SAAS,SAAS,SAAS;AACpC;;AAEA,SAAS,WAAW,MAAc,SAA+B;CAC/D,IAAI,sBAAsB,KAAK,IAAI,GAAG,OAAO;CAC7C,IAAI,SAAS,kBAAkB,OAAO;CACtC,QAAQ,SAAS,YAAY,SAAS,iBAAiB,QAAQ,SAAS,WAAW;AACrF;;;;ACn2BA,IAAa,cAAoE;CAC/E,CAAC,UAAU,QAAQ;CACnB,CAAC,WAAW,SAAS;CACrB,CAAC,SAAS,OAAO;CACjB,CAAC,UAAU,QAAQ;CACnB,CAAC,OAAO,KAAK;CACb,CAAC,YAAY,UAAU;CACvB,CAAC,aAAa,WAAW;CACzB,CAAC,QAAQ,MAAM;CACf,CAAC,cAAc,YAAY;CAC3B,CAAC,eAAe,aAAa;CAC7B,CAAC,UAAU,QAAQ;CACnB,CAAC,YAAY,UAAU;CACvB,CAAC,aAAa,WAAW;CACzB,CAAC,cAAc,YAAY;CAC3B,CAAC,WAAW,SAAS;CACrB,CAAC,aAAa,WAAW;CACzB,CAAC,QAAQ,MAAM;CACf,CAAC,SAAS,OAAO;CACjB,CAAC,eAAe,YAAY;CAC5B,CAAC,kBAAkB,eAAe;CAClC,CAAC,mBAAmB,gBAAgB;CACpC,CAAC,eAAe,YAAY;CAC5B,CAAC,cAAc,WAAW;CAC1B,CAAC,WAAW,QAAQ;CACpB,CAAC,aAAa,UAAU;CACxB,CAAC,YAAY,SAAS;CACtB,CAAC,cAAc,WAAW;CAC1B,CAAC,eAAe,YAAY;CAC5B,CAAC,iBAAiB,cAAc;CAChC,CAAC,iBAAiB,cAAc;CAChC,CAAC,cAAc,WAAW;CAC1B,CAAC,UAAU,QAAQ;CACnB,CAAC,QAAQ,MAAM;CACf,GAAG;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,SAAS,IAAI,EAAE,IAAI,QAAQ,GAAyB,CAAU;AACxG;AAIA,SAAS,SAAS,OAAyC;CACzD,OAAO;EAAC;EAAG;EAAG;CAAC,CAAC,CAAC,KAAK,UAAU,SAAS,MAAM,MAAM,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC;AAC7E;;AAEA,SAAgB,OAAO,GAAW,GAAW,QAAwB;CACnE,MAAM,IAAI,SAAS,CAAC,GAClB,IAAI,SAAS,CAAC;CAChB,OAAO,IAAI,EACR,KAAK,OAAO,UACX,KAAK,MAAM,QAAQ,SAAS,EAAE,UAAW,IAAI,OAAO,CAAC,CAClD,SAAS,EAAE,CAAC,CACZ,SAAS,GAAG,GAAG,CACpB,CAAC,CACA,KAAK,EAAE;AACZ;;;ACrBA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,SAAS,IAAI;AACtB;AA2bkD,CAAY,GAAG,cAAc,KAAK,SAAS,KAAK,EAAE,CAAC;;AA+BrG,IAAM,wBAAQ,IAAI,QAAsH;;AAGxI,SAAgB,YACd,OACyG;CACzG,OAAO,MAAM,IAAI,KAAK;AACxB;;;;;;;;;AC7gBA,IAAM,eAAe;CACnB,YAAY;EACV;GAAC;GAAU;GAAU;EAAS;EAC9B;GAAC;GAAU;GAAU;EAAQ;EAC7B;GAAC;GAAW;GAAW;EAAQ;CACjC;CACA,cAAc;EACZ;GAAC;GAAU;GAAU;EAAS;EAC9B;GAAC;GAAU;GAAU;EAAQ;EAC7B;GAAC;GAAU;GAAS;EAAQ;CAC9B;AACF;;AAEA,IAAM,mBAAmB;CAAE,WAAW;CAAG,QAAQ;AAAG;AAEpD,SAAgB,OAAO,KAAuC;CAC5D,OAAO;EAAC;EAAG;EAAG;CAAC,CAAC,CAAC,KAAK,UAAU;EAC9B,MAAM,QAAQ,SAAS,IAAI,MAAM,OAAO,QAAQ,CAAC,GAAG,EAAE,IAAI;EAC1D,OAAO,SAAS,SAAU,QAAQ,UAAU,QAAQ,QAAS,UAAU;CACzE,CAAC;AACH;AACA,SAAgB,MAAM,CAAC,GAAG,GAAG,IAAiD;CAC5E,MAAM,IAAI,KAAK,KAAK,cAAe,IAAK,cAAe,IAAK,cAAe,CAAE,GAC3E,IAAI,KAAK,KAAK,cAAe,IAAK,cAAe,IAAK,cAAe,CAAE,GACvE,IAAI,KAAK,KAAK,cAAe,IAAK,cAAe,IAAK,cAAe,CAAE;CACzE,OAAO;EACL,cAAe,IAAI,aAAc,IAAI,cAAe;EACpD,eAAe,IAAI,cAAc,IAAI,cAAe;EACpD,cAAe,IAAI,cAAe,IAAI,aAAc;CACtD;AACF;AACA,SAAS,SAAS,OAA0B,QAAkD;CAC5F,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,KAAM,MAAM,KAAM,IAAI,KAAM,MAAM,KAAM,IAAI,KAAM,MAAM,EAAG,CAAC,CAAC;AACtH;;AAEA,SAAS,cAAc,GAAW,GAAW,YAAgD;CAC3F,MAAM,OAAO,QAAgB,MAAM,aAAa,SAAS,OAAO,GAAG,GAAG,aAAa,WAAW,IAAI,OAAO,GAAG,CAAC;CAC7G,MAAM,IAAI,IAAI,CAAC,GACb,IAAI,IAAI,CAAC;CACX,OAAO,MAAM,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;AAC/D;;;AAIA,SAAgB,iBAAiB,GAAW,GAA4E;CAOtH,MAAM,SAAS,CALb;EAAC;EAAiB,cAAc,GAAG,CAAC;EAAG,iBAAiB;CAAM,GAC9D,GAAI,OAAO,KAAK,YAAY,CAAC,CAAmC,KAC7D,SAAS;EAAC;EAAM,cAAc,GAAG,GAAG,IAAI;EAAG,iBAAiB;CAAS,CACxE,CAEa,CAAA,CAAM,MAAM,GAAG,UAAU,WAAW,WAAW,KAAK;CACnE,OAAO,UAAU;EAAE,QAAQ,OAAO;EAAI,UAAU,OAAO;EAAI,OAAO,OAAO;CAAG;AAC9E;;AAGA,SAAgB,YAAY,GAAW,GAAoB;CACzD,OAAO,CAAC,iBAAiB,GAAG,CAAC;AAC/B;;AC7CA,IAAM,QAAQ,IAAI,IAAY,YAAY,KAAK,GAAG,UAAU,IAAI,CAAC;AAQjE,IAAMC,QAAM;AACZ,IAAM,SAAS;CAAC;CAAG;CAAG;CAAG;CAAG;CAAG;CAAG;CAAG;AAAC,CAAC,CAAC,KAAK,MAAM,QAAQ,GAAyB;;AAGpF,SAAS,cACP,OACA,KACA,UACgB;CAChB,MAAM,WAA2B,CAAC;CAClC,KAAK,MAAM,aAAa,CAAC,WAAW,IAAI,MAAM,YAAY,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,EAAE,CAAC,GAClF,KAAK,MAAM,QAAQ,WAAW,KAAK,GAAG;EACpC,MAAM,UAAU,sBAAsB,yBAAyB,OAAO,SAAS,CAAC,CAAC,OAAO,SAAS,QAAQ,GAAG;EAC5G,MAAM,OACJ,cAAc,YAAY,MAAM,QAAQ,GAAG,MAAM,MAAM,KAAK,MAAM,SAAU,MAAM,SAAS,KAAK,OAAO,SAAS,CAAC,CAAE;EACrH,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG,SAAS;GACtD,MAAM,OAAO,OAAO,QAClB,QAAQ,OAAO,QAAQ;GACzB,MAAM,IAAI,QAAQ,OAChB,IAAI,QAAQ;GACd,IAAI,CAAC,KAAK,CAAC,KAAK,CAACA,MAAI,KAAK,CAAC,KAAK,CAACA,MAAI,KAAK,CAAC,GAAG;GAC9C,IAAI,CAAC,SAAS,WAAW,MAAM,IAAI,KAAK,CAAC,SAAS,WAAW,MAAM,KAAK,GAAG;GAC3E,MAAM,QAAQ,SAAS,WAAW,MAAM,KAAK,IAAI,QAAQ;GACzD,MAAM,SAAS,iBAAiB,GAAG,CAAC;GACpC,IAAI,CAAC,QAAQ;GACb,MAAM,EAAE,QAAQ,UAAU,UAAU;GACpC,SAAS,KAAK;IACZ,SAAS,MAAM;IACf;IACA;IACA;IACA,OAAO,GAAG,KAAK,UAAU,MAAM,OAAO;IACtC,OAAO;IACP;IACA,SAAS,UAAU,KAAK,KAAK,KAAK,KAAK,UAAU,IAAI,EAAE,OAAO,UAAU,KAAK,EAAE,OAAO,SAAS,QAAQ,CAAC,EAAE,aAAa,OAAO,+BAA+B,MAAM;GACrK,CAAC;EACH;CACF;CACF,OAAO;AACT;;;AASA,IAAM,YAAY;CAAC;CAAQ;CAAS;CAAa;AAAQ;;;;;;;;AASzD,SAAS,gBAAgB,OAAwB,KAA+C;CAC9F,MAAM,MAAM,YAAY,KAAK,CAAC,EAAE;CAGhC,IAAI,CAAC,OAAO,IAAI,QAAQ,OAAO,CAAC;CAChC,MAAM,WAA6B,CAAC;CACpC,MAAM,WAAW,MAAM,WAAW;CAClC,KAAK,MAAM,CAAC,OAAO,cAAc,CAAC,WAAW,IAAI,MAAM,YAAY,CAAC,EAAA,CAAG,KAAK,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,GACrG,KAAK,MAAM,QAAQ,WAAW,KAAK,GAAG;EACpC,MAAM,UAAU,UAAU,IAAI,IAAI,SAAS,IAAI,WAAW,QAAQ,GAAA,GAAM,WAAW,SAAS;EAG5F,IAAI,CAAC,QAAQ;EACb,MAAM,OAAO,SAAS,sBAAsB,yBAAyB,OAAO,SAAS,CAAC,CAAC,OAAO,SAAS,QAAQ,GAAG,CAAiB;EACnI,MAAM,OAAO,UAAU,IAAI,MAAM,QAAQ,GAAG,MAAM,MAAM,KAAK,MAAM,SAAU,QAAQ,EAAE,CAAE;EACzF,KAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,OAAO,UAAU,IAAI;GAC3B,MAAM,QAAQ,iBAAiB,KAAK,IAAI;GACxC,MAAM,OACJ,SAAS,WACJ,OAAO,QAAkC,KAC1C,QACG,OAAO,MAAM,GAAI,GAA2C,MAAM,MACnE,OAAO;GACf,IAAI,OAAO,SAAS,YAAY,CAACA,MAAI,KAAK,KAAK,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,YAAY,MAAM,KAAK,KAAK,EAAE,YAAY,GAAG;GACnH,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,YAAY;GACxC,KAAK,MAAM,QAAQ,UAAU,MAAM,MAAM,OAAO,GAAG;IACjD,IAAI,KAAK,SAAS,KAAK,OAAO;IAC9B,SAAS,KAAK;KACZ,SAAS,MAAM;KACf;KACA;KACA,OAAO;KACP,OAAO,GAAG,KAAK,iBAAiB,KAAK,aAAa,KAAK,OAAO,KAAK,KAAK,KAAK,KAAK;KAClF,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ;KACA,QAAQ,KAAK;KACb,SAAS,UAAU,KAAK,KAAK,KAAK,KAAK,KAAK,iBAAiB,QAAQ,IAAI,KAAK,MAAM,QAAQ,CAAC,EAAE,QAAQ,UAAU,KAAK,UAAU,EAAE,aAAa,KAAK,MAAM;IAC5J,CAAC;GACH;EACF;CACF;CACF,OAAO;AACT;;;;;;;AA8BA,SAAgB,WACd,OACA,UAGI,CAAC,GACW;CAChB,MAAM,MAAM,QAAQ,OAAO,CAAC;CAC5B,MAAM,WAAW,QAAQ,mBAAmB;CAE5C,OAAO;EADwB,GAAG,cAAc,CAAC,KAAK,GAAG,GAAG;EAAG,GAAG,cAAc,OAAO,KAAK,QAAQ;EAAG,GAAG,gBAAgB,OAAO,GAAG;CAC7H,CAAA,CAAI,KAAK,YAAY;EAC1B,MAAM,OACJ,QAAQ,cAAc,YAClB,MAAM,QACN,GAAG,MAAM,MAAM,KAAK,MAAM,SAAU,MAAM,SAAS,KAAK,OAAO,QAAQ,SAAS,CAAC,CAAE;EACzF,MAAM,QAAQ,QAAQ,MAAM,QAAQ,2BAA2B,SAAU,MAAM,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,IAAK;EAClH,MAAM,SAAS,MAAM,SAAS,OAAO,IACjC,GAAG,MAAM,OAAO,QAAQ,MAAM,QAAQ,CAAC,EAAE,+BAA+B,QAAQ,UAChF,GAAG,MAAM,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAE,eAAe,QAAQ,MAAM;EACzE,OAAO;GACL,SAAS,QAAQ;GACjB,WAAW,QAAQ;GACnB,MAAM,QAAQ;GACd,OAAO,UAAU,QAAQ,KAAK;GAC9B,MAAM,QAAQ;GACd,OAAO,QAAQ;GACf,OAAO,QAAQ;GACf,OAAO,QAAQ;GACf;GACA,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;GACtD,SAAS,UAAU,KAAK,KAAK,QAAQ,KAAK,KAAK,UAAU,QAAQ,KAAK,EAAE,IAAI,OAAO;EACrF;CACF,CAAC;AACH;;AC0GA,SAAS,QAAQ,aAAkC,IAAyC;CAC1F,OAAO,YAAY,OAAO,MAAM,UAAU,MAAM,OAAO,EAAE,KAAK,YAAY,OAAO;AACnF;;AAGA,IAAM,iBAAiB,CAAC,QAAQ,WAAW;;;;;;AAO3C,SAAgB,YAAY,aAAkC,OAA6C;CACzG,MAAM,SAAS,QAAQ,aAAa,MAAM,MAAM;CAChD,MAAM,OAAO,yBAAyB,QAAQ,MAAM,WAAW,OAAO,cAAc;CACpF,MAAM,QAAQ,SAAyC;EACrD,MAAM,UAAU,EAAE,GAAG,KAAK,MAAM;EAChC,MAAM,SAAS,MAAM,SAAS;EAC9B,IAAI,CAAC,QAAQ,OAAO;EACpB,KAAK,MAAM,QAAQ,gBAAgB,IAAI,QAAQ,UAAU,QAAQ,QAAQ,QAAQ,QAAQ;EAKzF,IAAI,QAAQ,WAAW,QAAQ,UAAU,CAAC,QAAQ,UAAU,YAAY,QAAQ,QAAQ,MAAM,GAAG,QAAQ,SAAS;EAClH,QAAQ,SAAS;EAGjB,OAAO,QAAQ;EAGf,QAAQ,YAAY;EACpB,QAAQ,QAAQ;EAEhB,QAAQ,aAAa,OAAO,QAAQ,QAAQ,SAAS,SAAS,UAAU,MAAO,EAAG;EAClF,OAAO;CACT;CACA,MAAM,EAAE,UAAU,WAAW,gBAAgB,UAAU,GAAG,SAAS;CACnE,OAAO;EACL,GAAG;EACH,IAAI;EACJ,OAAO,MAAM,QAAQ,OAAO;EAC5B,OAAO,KAAK,OAAO;EACnB,MAAM,KAAK,MAAM;EACjB,GAAI,MAAM,SAAS,EAAE,QAAQ;GAAE,SAAS,MAAM;GAAQ,OAAO,MAAM;EAAO,EAAE,IAAI,CAAC;EACjF,GAAI,MAAM,UAAU,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;CACpD;AACF;;;;;;AAOA,SAAgB,iBAAiB,aAAkC,OAA+C;CAChH,MAAM,WAAW,aAAa,aAAa,KAAK;CAChD,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,WAA8B,CAAC;CACrC,KAAK,MAAM,CAAC,SAAS,SAAS,UAAU;EACtC,MAAM,UAAU,OAAO,SAAS,OAAO,CAAC,CAAC,QAAQ,OAAO,OAAO,KAAK,kCAAkC,IAAI,SAAS,OAAO;EAC1H,IAAI,KAAK,IAAI,OAAO,GAAG;EACvB,KAAK,IAAI,OAAO;EAChB,SAAS,KAAK;GAAE,OAAO;GAAU;EAAQ,CAAC;CAC5C;CACA,OAAO;AACT;;;AAIA,SAAS,aAAa,aAAkC,OAAiE;CACvH,MAAM,EAAE,SAAS,UAAU,GAAG,SAAS;CACvC,OAAO,CACL,GAAG,eAAe,aAAa,KAAK,CAAC,CAAC,KAAK,YAAY,CAAC,SAAS,EAAE,CAAU,GAC7E,IAAI,YAAY,kBAAkB,CAAC,EAAA,CAChC,QAAQ,UAAU,MAAM,OAAO,QAAQ,aAAa,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,CACrE,SAAS,UACR,eAAe;EAAE,GAAG;EAAa,QAAQ,CAAC,KAAK;CAAE,GAAG;EAAE,GAAG;EAAM,QAAQ,MAAM;CAAG,CAAC,CAAC,CAAC,KAChF,YAAY,CAAC,SAAS,MAAM,KAAK,CACpC,CACF,CACJ;AACF;;AAGA,SAAS,eAAe,aAAkC,OAA4C;CACpG,IAAI,CAAC,MAAM,QAAQ,OAAO,CAAC;CAC3B,MAAM,SAAS,QAAQ,aAAa,MAAM,MAAM;CAChD,MAAM,SAAS,yBAAyB,QAAQ,MAAM,WAAW,OAAO,cAAc;CACtF,MAAM,QAAQ,YAAY,aAAa,KAAK;CAC5C,MAAM,WAAW,UAAkB,MAAwB,SAA6B,OAAO,KAAK,CAAC,UAAU,MAAM,KAAK,CAAC;CAC3H,OAAO,WAAW;EAAE,GAAG;EAAO,UAAU,KAAA;CAAU,GAAG,EAAE,UAAU,QAAQ,CAAC;AAC5E;;;;;;;;;;AAWA,SAAgB,UACd,aACA,OACA,OACmD;CACnD,MAAM,QAAQ,OAAO,OAAO,WAAW,GAAI;CAC3C,MAAM,UAAU,SAAiB,CAAC,aAAa,aAAa;EAAE,GAAG;EAAO,QAAQ;GAAE;GAAO;EAAK;CAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,MAAM;CAClI,IAAI,OAAO,KAAK,GAAG,OAAO;EAAE;EAAO,MAAM;CAAM;CAC/C,MAAM,CAAC,GAAG,GAAG,KAAK,MAAM,KAAK;CAC7B,KAAK,IAAI,OAAO,IAAI,KAAM,QAAQ,KAAM,QAAQ,KAAM;EACpD,MAAM,OAAO,UAAU,MAAM,GAAG,CAAC;EACjC,IAAI,OAAO,IAAI,GAAG,OAAO;GAAE;GAAO;EAAK;CACzC;CACA,OAAO;EAAE;EAAO,MAAM;CAAM;AAC9B;AAGA,SAAS,MAAM,KAAuC;CACpD,MAAM,CAAC,GAAG,GAAG,KAAK;EAAC;EAAG;EAAG;CAAC,CAAC,CAAC,KAAK,OAAO;EACtC,MAAM,IAAI,SAAS,IAAI,MAAM,IAAI,KAAK,CAAC,GAAG,EAAE,IAAI;EAChD,OAAO,KAAK,SAAU,IAAI,UAAU,IAAI,QAAS,UAAU;CAC7D,CAAC;CACD,MAAM,MAAM;EACV,KAAK,KAAK,cAAe,IAAI,cAAe,IAAI,cAAe,CAAC;EAChE,KAAK,KAAK,cAAe,IAAI,cAAe,IAAI,cAAe,CAAC;EAChE,KAAK,KAAK,cAAe,IAAI,cAAe,IAAI,cAAe,CAAC;CAClE;CACA,MAAM,IAAI,cAAe,IAAI,KAAK,aAAc,IAAI,KAAK,cAAe,IAAI;CAC5E,MAAM,IAAI,eAAe,IAAI,KAAK,cAAc,IAAI,KAAK,cAAe,IAAI;CAC5E,MAAM,IAAI,cAAe,IAAI,KAAK,cAAe,IAAI,KAAK,aAAc,IAAI;CAC5E,OAAO;EAAC;EAAG,KAAK,MAAM,GAAG,CAAC;EAAG,KAAK,MAAM,GAAG,CAAC;CAAC;AAC/C;;AAGA,SAAS,UAAU,GAAW,GAAW,GAAmB;CAC1D,KAAK,IAAI,SAAS,IAAK,SAAS,KAAK,IAAI,GAAG,SAAS,IAAK,GAAG;EAC3D,MAAM,IAAI,SAAS,KAAK,IAAI,CAAC,GAC3B,IAAI,SAAS,KAAK,IAAI,CAAC;EACzB,MAAM,MAAM;GACV,IAAI,cAAe,IAAI,cAAe;GACtC,IAAI,cAAe,IAAI,cAAe;GACtC,IAAI,cAAe,IAAI,cAAc;EACvC,CAAC,CAAC,KAAK,MAAM,KAAK,CAAC;EACnB,MAAM,MAAM;GACV,eAAe,IAAI,KAAK,eAAe,IAAI,KAAK,cAAe,IAAI;GACnE,gBAAgB,IAAI,KAAK,eAAe,IAAI,KAAK,cAAe,IAAI;GACpE,eAAgB,IAAI,KAAK,cAAe,IAAI,KAAK,cAAc,IAAI;EACrE;EACA,IAAI,WAAW,KAAK,IAAI,OAAO,MAAM,KAAK,SAAS,KAAK,MAAQ,GAC9D,OAAO,IAAI,IACR,KAAK,MAAM;GACV,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;GACpC,OAAO,KAAK,OAAO,KAAK,WAAY,QAAQ,IAAI,QAAQ,MAAM,IAAI,OAAO,QAAS,GAAG,CAAC,CACnF,SAAS,EAAE,CAAC,CACZ,SAAS,GAAG,GAAG;EACpB,CAAC,CAAC,CACD,KAAK,EAAE;CACd;AACF;;AAGA,SAAS,SAAS,SAA+B;CAC/C,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,SAAS,QAAQ,UAAU;CACjC,IAAI,QAAQ,MAAM,WAAW,OAAO,GAClC,OAAO,mFAAmF,QAAQ,KAAK;CACzG,IAAI,QAAQ,UAAU,cAAc,OAAO,sBAAsB,QAAQ,UAAU,OAAO,sBAAsB,QAAQ,KAAK;CAC7H,IAAI,QAAQ,UAAU,WAAW,aAAa,KAAK,QAAQ,MAAM,GAC/D,OAAO,sBAAsB,QAAQ,UAAU,OAAO,MAAM,OAAO;CACrE,IAAI,0BAA0B,KAAK,QAAQ,MAAM,GAC/C,OAAO,sBAAsB,QAAQ,UAAU,OAAO,oDAAoD,QAAQ,KAAK;CACzH,IAAI,QAAQ,UAAU,QAAQ,OAAO,sBAAsB,QAAQ,UAAU,OAAO,MAAM,OAAO;CACjG,OAAO,sBAAsB,QAAQ,UAAU,OAAO,MAAM,OAAO;AACrE;;;ACtdA,IAAa,MAAM,eAAe;CAChC,SAAS;CACT,cAAc;CACd,UAAU;CACV,MAAM;CACN,UAAU;CACV,MAAM;CACN,UAAU;CACV,YAAY;CACZ,YAAY;CACZ,QAAQ;CACR,UAAU;CACV,cAAc;CACd,YAAY;CACZ,QAAQ;CACR,YAAY;CACZ,aAAa;CACb,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,aAAa;CACb,UAAU;CACV,YAAY;CACZ,MAAM;CACN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,cAAc;CACd,SAAS;CACT,cAAc;CACd,eAAe;CACf,aAAa;CACb,cAAc;CACd,cAAc;CACd,cAAc;CACd,cAAc;CACd,YAAY;CACZ,WAAW;CACX,aAAa;CACb,iBAAiB;CACjB,mBAAmB;AACrB,CAAC;;;;;;;;;;;;;;ACLD,IAAM,MAAM;AACZ,IAAM,QAAQ;CAAC;CAAO;CAAO;CAAO;CAAO;AAAK;AAChD,IAAM,eAAe;AAKrB,SAAS,OAAgB;CACvB,MAAM,OAAO,UAAU,CAAC,CAAC,QAAQ;CACjC,OAAO,SAAS,UAAW,CAAC,QAAQ,OAAO,aAAa,8BAA8B,CAAC,CAAC,YAAY;AACtG;AAEA,SAAgB,eAAe;CAC7B,MAAM,CAAC,OAAO,YAAY,SAA+B,IAAI;CAC7D,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,OAAO,YAAY,SAAgB,CAAC,CAAC;CAE5C,MAAM,CAAC,QAAQ,aAAa,SAA6D,KAAA,CAAS;CAElG,MAAM,CAAC,YAAY,iBAAiB,SAA6D,KAAA,CAAS;CAC1G,MAAM,CAAC,OAAO,YAAY,SAA4B,MAAM;CAC5D,MAAM,CAAC,SAAS,cAAc,SAAS,EAAE;CACzC,MAAM,CAAC,SAAS,cAAc,SAA4B,CAAC,CAAC;CAC5D,MAAM,OAAO,OAAyB,IAAI;CAC1C,MAAM,WAAW,OAAyB,IAAI;CAE9C,gBAAgB;EACd,IAAI,QAAQ;EACZ,CAAM,YAAY;GAChB,IAAI;IACF,MAAM,MAAO,MAAM,IAAI,iBAAiB,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO;IACZ,IAAI,IAAI,OAAO,UAAU,IAAI,KAAK;SAC7B;KACH,SAAS,GAAG;KACZ,SAAS,EAAE,GAAG,IAAI,SAAS,CAAC;IAC9B;GACF,SAAS,OAAO;IACd,IAAI,OAAO,UAAU,iBAAiB,QAAQ,MAAM,UAAU,IAAI,cAAc,CAAC;GACnF;EACF,EAAA,CAAG;EACH,aAAa;GACX,QAAQ;EACV;CACF,GAAG,CAAC,CAAC;CACL,sBAAuB,SAAS,IAAI,gBAAgB,OAAO,GAAG,IAAI,KAAA,GAAY,CAAC,MAAM,CAAC;CACtF,sBAAuB,aAAa,IAAI,gBAAgB,WAAW,GAAG,IAAI,KAAA,GAAY,CAAC,UAAU,CAAC;CAElG,MAAM,cAAc,cAAe,QAAQ;EAAE,QAAQ,MAAM;EAAQ,OAAO,MAAM;EAAO,SAAS,CAAC;CAAE,IAAI,KAAA,GAAY,CAAC,KAAK,CAAC;CAE1H,MAAM,YAAY,cAAmC;EACnD,MAAM,EAAE,QAAQ,GAAG,SAAS;EAC5B,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,KAAK,IAAI,KAAK,OAAO,IAAI,IAAI;GAAE,GAAG;GAAM;EAAO,IAAI;CAC3F,GAAG,CAAC,KAAK,CAAC;CACV,MAAM,QAAQ,cAAe,cAAc,YAAY,aAAa,SAAS,IAAI,KAAA,GAAY,CAAC,aAAa,SAAS,CAAC;CACrH,MAAM,WAAW,cAAe,cAAc,iBAAiB,aAAa,SAAS,IAAI,CAAC,GAAI,CAAC,aAAa,SAAS,CAAC;CAEtH,IAAI,QACF,OACE,oBAAC,KAAD;EAAG,WAAU;EAAqB,MAAK;EACpC,UAAA;CACA,CAAA;CAEP,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,OAC7B,OACE,oBAAC,KAAD;EAAG,WAAU;EAAqB,MAAK;EACpC,UAAA,IAAI,SAAS;CACb,CAAA;CAGP,MAAM,WAAW,QAAqB,MAAM,MAAM,SAAS,GAAG;CAC9D,MAAM,SAAS,MAAM;CACrB,MAAM,MAAM,UAAU;CACtB,MAAM,OAAO,MAAM,MAAM,KAAK,KAAK,KAAK,QAAQ;CAChD,MAAM,YAAY,MAAM,SAAS,OAAO,sBAAsB,MAAM,SAAS,KAAK,SAAS,KAAA;CAC3F,MAAM,OAAO,WAAW,OAAO,KAAA,IAAa,QAAQ,QAAQ,aAAa,aAAa,KAAK,SAAS,IAAI,OAAO,SAAS,IAAI,KAAA;CAC5H,MAAM,YAAY,MAAM,SAAS,WAAW,sBAAsB,MAAM,SAAS,SAAS,SAAS,KAAA;CACnG,MAAM,WACJ,CAAC,QAAQ,eAAe,OACpB,KAAA,IACC,YAAY,QAAQ,aAAa,aAAa,KAAK,SAAS,IAAI,OAAO,SAAS,IAAI,KAAA;CAC3F,MAAM,SAAS,MAAM,OAAO,MAAM,SAAS,KAAK,OAAO,MAAM,MAAM,KAAK,MAAM,OAAO;CACrF,MAAM,UAAU,MAAM,KAAK,IAAI,SAAS;CACxC,MAAM,cAAc,UAClB,CAAC,GAAG,QAAQ,QAAQ,SAAS,KAAK,UAAU,KAAK,GAAG,GAAI,UAAU,WAAW,WAAW,CAAC,CAAE,CAAC,CAAC,KAAK,SAAS,KAAK,OAAO,CAAC,CAAC,MAAM;CACjI,MAAM,OAAO,UAAiB;EAC5B,UAAU,aAAa;GAAE,GAAG;GAAS,GAAG;EAAM,EAAE;EAChD,WAAW,EAAE;EACb,WAAW,CAAC,CAAC;CACf;CACA,MAAM,aAAa,MAAM,QAAQ,SAAS;CAE1C,MAAM,WAAW,UAAkB,UAAU,aAAc,WAAW,KAAK;CAC3E,MAAM,UAAU;EACd,eAAe,QAAQ;EACvB,gBAAgB,QAAQ;EACxB,YAAY,QAAQ;EACpB,cAAc,QAAQ;EACtB,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,oBAAoB,QAAQ;EAC5B,YAAY,QAAQ,aAAa,QAAQ;EACzC,gBAAgB,QAAQ,UAAU,QAAQ;EAC1C,mBAAmB,QAAQ,aAAa,QAAQ;EAChD,cAAc,QAAQ,UAAU,QAAQ;EACxC,eAAe,MAAM,UAAU,MAAM,QAAQ,WAAW;CAC1D;CAEA,MAAM,OAAO,OAAO,OAAsC,SAA2B;EACnF,MAAM,aAAa,MAAM,OAAO,QAAQ;EACxC,MAAM,OAAO,QAAQ;EACrB,IAAI,CAAC,YAAY;EACjB,MAAM,QAAQ,IAAI,WAAW,MAAM,WAAW,YAAY,CAAC;EAC3D,IAAI,SAAS;EACb,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,OAAQ,UAAU,OAAO,aAAa,GAAG,MAAM,SAAS,OAAO,QAAQ,KAAM,CAAC;EACjI,KAAK;GAAE,QAAQ,KAAK,MAAM;GAAG,KAAK,IAAI,gBAAgB,UAAU;EAAE,CAAC;EACnE,WAAW,EAAE;EACb,WAAW,CAAC,CAAC;CACf;CACA,MAAM,OAAO,YAAY;EACvB,IAAI,UAAU,YAAY,CAAC,UAAU,SAAS,QAAQ;EACtD,SAAS,QAAQ;EACjB,WAAW,EAAE;EACb,IAAI;GACF,MAAM,EAAE,MAAM,OAAO,UAAU,WAAW,GAAG,UAAU;GACvD,MAAM,MAAO,MAAM,IAAI,sBAAsB;IAC3C,UACE,MAAM,UAAU,EAAE,IAAI,KAAK,MAAM,OAAO,KAAK,KAAK,IAAI,KAAK,MAAM,OAAO,IAAI,KAAK;KAAE,GAAG;KAAO,QAAQ,MAAM,OAAO;IAAM,IAAI;IAC9H,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,WAAW,OAAO,OAAO,OAAO,OAAO;IAC/E,GAAI,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,eAAe,OAAO,OAAO,WAAW,OAAO;GACjG,CAAC;GACD,IAAI,IAAI,OAAO;IACb,WAAW,IAAI,UAAU,CAAC;KAAE,OAAO;KAAQ,SAAS,IAAI;IAAM,CAAC,CAAC;IAChE,WAAW,IAAI,KAAK;GACtB,OAAO;IACL,SAAS,GAAG;IACZ,SAAS,EAAE,GAAG,IAAI,SAAS,CAAC;IAC5B,UAAU,KAAA,CAAS;IACnB,cAAc,KAAA,CAAS;IACvB,WAAW,IAAI,OAAO,CAAC;GACzB;EACF,SAAS,OAAO;GACd,WAAW,iBAAiB,QAAQ,MAAM,UAAU,IAAI,cAAc,CAAC;EACzE,UAAU;GACR,SAAS,MAAM;EACjB;CACF;CACA,MAAM,WAAW,CAAC,UAAU,UAAU;CAEtC,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,QAAD;GACE,WAAU;GACV,WAAW,UAAU;IACnB,MAAM,eAAe;IACrB,KAAU;GACZ;GACA,YAAA;GANF,UAAA;IAQG,CAAC,UAAU,oBAAC,KAAD;KAAG,WAAU;KAAsB,UAAA,IAAI,UAAU;IAAK,CAAA;IACjE,QAAQ,MAAM,KACb,oBAAC,OAAD;KAAO,OAAO,IAAI,MAAM;KAAG,MAAM,IAAI,UAAU;KAAG,SAAS,WAAW,MAAM;KAAG,MAAK;KAClF,UAAA,oBAAC,SAAD;MACE,WAAU;MACV,MAAK;MACL,OAAO,MAAM,QAAQ;MACrB,aAAa,KAAK;MAClB,WAAW;MACD;MACV,WAAW,UAAU,IAAI,EAAE,MAAM,MAAM,OAAO,SAAS,KAAA,EAAU,CAAC;KACnE,CAAA;IACI,CAAA;IAER,QAAQ,MAAM,KACb,oBAAC,OAAD;KAAO,OAAO,IAAI,MAAM;KAAG,MAAM,IAAI,UAAU;KAAG,SAAS,WAAW,MAAM;KAAG,MAAK;KAAU,OAAA;KAC1F,WAAA,YACA,qBAAC,OAAD;MAAK,WAAU;MAAqB,GAAI;MAAS,MAAK;MAAtD,UAAA;OACE,oBAAC,QAAD;QAAM,WAAU;QAA0B,UAAA,OAAO,oBAAC,OAAD;SAAK,KAAK;SAAM,KAAI;SAAG,OAAO;SAAI,QAAQ;QAAK,CAAA,IAAI,IAAI,QAAQ;OAAQ,CAAA;OACxH,oBAAC,SAAD;QACE,KAAK;QACL,MAAK;QACL,QAAA;QACA,QAAO;QACP,WAAW,UAAU,KAAK,KAAK,OAAO,SAAS;OAChD,CAAA;OACD,oBAAC,QAAD;QAAQ,QAAO;QAAY,MAAK;QAAoB;QAAU,eAAe,KAAK,SAAS,MAAM;QAC9F,UAAA,IAAI,YAAY;OACX,CAAA;OACP,QACC,oBAAC,QAAD;QACE,QAAO;QACP,MAAK;QACK;QACV,eAAe,UAAU,MAAM,SAAS,OAAO,OAAO,KAAA,CAAS;QAE9D,UAAA,IAAI,YAAY;OACX,CAAA;MAEP;;IAEF,CAAA;IAER,QAAQ,MAAM,KAAK,QAClB,oBAAC,OAAD;KAAO,OAAO,IAAI,UAAU;KAAG,MAAM,IAAI,cAAc;KAAG,MAAK;KAAU,OAAA;KACrE,WAAA,YACA,qBAAC,OAAD;MAAK,WAAU;MAAqB,GAAI;MAAS,MAAK;MAAtD,UAAA;OACE,oBAAC,QAAD;QAAM,WAAU;QACb,UAAA,WAAW,oBAAC,OAAD;SAAK,KAAK;SAAU,KAAI;SAAG,OAAO;SAAI,QAAQ;QAAK,CAAA,IAAI,IAAI,YAAY;OAC/E,CAAA;OACN,oBAAC,SAAD;QACE,KAAK;QACL,MAAK;QACL,QAAA;QACA,QAAO;QACP,WAAW,UAAU,KAAK,KAAK,OAAO,aAAa;OACpD,CAAA;OACD,oBAAC,QAAD;QAAQ,QAAO;QAAY,MAAK;QAAoB;QAAU,eAAe,SAAS,SAAS,MAAM;QAClG,UAAA,IAAI,YAAY;OACX,CAAA;OACP,YACC,oBAAC,QAAD;QACE,QAAO;QACP,MAAK;QACK;QACV,eAAe,cAAc,MAAM,SAAS,WAAW,OAAO,KAAA,CAAS;QAEtE,UAAA,IAAI,YAAY;OACX,CAAA;MAEP;;IAEF,CAAA;IAER,QAAQ,QAAQ,KACf,oBAAC,OAAD;KAAO,OAAO,IAAI,QAAQ;KAAG,MAAM,IAAI,YAAY;KAAG,SAAS,WAAW,QAAQ;KAAG,MAAK;KAAU,OAAA;KAChG,WAAA,YACA,qBAAC,OAAD;MAAK,WAAU;MAAuB,GAAI;MAAS,MAAK;MAAxD,UAAA,CACE,oBAAC,SAAD;OACE,MAAK;OACL,cAAY,IAAI,aAAa;OAC7B,OAAO,IAAI,KAAK,UAAU,IAAI,aAAa,QAAQ;OACzC;OACV,WAAW,UAAU,IAAI,EAAE,QAAQ,QAAQ,MAAM,OAAO,KAAK,EAAE,CAAC;MACjE,CAAA,GACD,oBAAC,SAAD;OACE,WAAU;OACV,cAAY,IAAI,aAAa;OAC7B,OAAO;OACP,aAAa,OAAO,MAAM;OAC1B,YAAY;OACZ,WAAW;OACD;OACV,WAAW,UAAU;QACnB,MAAM,QAAQ,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC,YAAY;QACpD,IAAI,EAAE,QAAQ,QAAS,IAAI,KAAK,KAAK,IAAI,QAAQ,KAAK,IAAI;SAAE,OAAO;SAAO,MAAM;QAAM,IAAK,KAAA,EAAU,CAAC;OACxG;MACD,CAAA,CACE;;IAEF,CAAA;IAER,QAAQ,QAAQ,KACf,oBAAC,OAAD;KAAO,OAAO,IAAI,OAAO;KAAG,SAAS,WAAW,QAAQ;KAAG,MAAK;KAAU,OAAA;KACxE,UAAA,oBAAC,UAAD;MACE,WAAW,IAAI,OAAO;MACZ;MACV,OAAO,MAAM,UAAU,MAAM,OAAO,EAAE,CAAE;MACxC,OAAO,MAAM,OAAO,KAAK,UAAU;OAAE,OAAO,KAAK;OAAI,OAAO,KAAK;MAAM,EAAE;MACzE,WAAW,SAAS,IAAI;OAAE,QAAQ;OAAM,SAAS,KAAA;MAAU,CAAC;KAC7D,CAAA;IACI,CAAA;IAER,QAAQ,SAAS,KAAK,OAAO,UAAU,SACtC,oBAAC,OAAD;KAAO,OAAO,IAAI,SAAS;KAAG,SAAS,WAAW,SAAS;KAAG,MAAK;KAAU,OAAA;KAC3E,UAAA,oBAAC,UAAD;MACE,WAAW,IAAI,SAAS;MACd;MACV,OAAO,MAAM,WAAW,OAAO,kBAAkB;MACjD,OAAO,CAAC;OAAE,OAAO;OAAW,OAAO,OAAO;MAAM,GAAG,GAAG,OAAO,SAAS,KAAK,UAAU;OAAE,OAAO,KAAK;OAAI,OAAO,KAAK;MAAM,EAAE,CAAC;MAC5H,WAAW,SAAS,IAAI,EAAE,SAAS,KAAK,CAAC;KAC1C,CAAA;IACI,CAAA,IACL;IACH,QAAQ,QAAQ,KACf,oBAAC,OAAD;KAAO,OAAO,IAAI,QAAQ;KAAG,SAAS,WAAW,QAAQ;KAAG,MAAK;KAAU,OAAA;KACzE,UAAA,oBAAC,UAAD;MACE,WAAW,IAAI,QAAQ;MACb;MACV,OAAO,MAAM,UAAU;MACvB,OAAO,CACL;OAAE,OAAO;OAAI,OAAO,IAAI,YAAY;MAAE,GACtC,GAAG,MAAM,KAAK,YAAY;OAAE,OAAO;OAAQ,OAAO,WAAW,QAAQ,IAAI,QAAQ,IAAI;MAAO,EAAE,CAChG;MACA,WAAW,SAAS,IAAI,EAAE,QAAQ,QAAQ,KAAA,EAAU,CAAC;KACtD,CAAA;IACI,CAAA;IAER,QAAQ,SAAS,KAChB,oBAAC,OAAD;KAAO,OAAO,IAAI,SAAS;KAAG,SAAS,WAAW,SAAS;KAAG,MAAK;KAAU,OAAA;KAC3E,UAAA,oBAAC,UAAD;MACE,WAAW,IAAI,SAAS;MACd;MACV,OAAO,MAAM,WAAW;MACxB,OAAO;OACL;QAAE,OAAO;QAAI,OAAO,IAAI,YAAY;OAAE;OACtC;QAAE,OAAO;QAAW,OAAO,IAAI,SAAS;OAAE;OAC1C;QAAE,OAAO;QAAe,OAAO,IAAI,aAAa;OAAE;OAClD;QAAE,OAAO;QAAY,OAAO,IAAI,UAAU;OAAE;MAC9C;MACA,WAAW,SAAS,IAAI,EAAE,SAAU,QAAQ,KAAA,EAAuC,CAAC;KACrF,CAAA;IACI,CAAA;IAER,UACC,qBAAC,UAAD;KAAQ,WAAU;KAAlB,UAAA;MACE,oBAAC,KAAD;OAAG,WAAU;OAAuB,MAAK;OACtC,UAAA;MACA,CAAA;MACH,oBAAC,QAAD;OACE,QAAO;OACP,MAAK;OACL,UAAU,UAAU;OACpB,eAAe;QACb,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC;QAC9B,UAAU,KAAA,CAAS;QACnB,WAAW,EAAE;QACb,WAAW,CAAC,CAAC;OACf;OAEC,UAAA,IAAI,OAAO;MACN,CAAA;MACR,oBAAC,QAAD;OAAQ,QAAO;OAAU,MAAK;OAAU,MAAK;OAAS,UAAU,UAAU,YAAY,SAAS,SAAS;OACrG,UAAA,UAAU,WAAW,IAAI,QAAQ,IAAI,IAAI,MAAM;MAC1C,CAAA;KACF;;GAEN;EACN,CAAA,GAAA,qBAAC,WAAD;GAAS,WAAU;GAAmB,cAAY,IAAI,SAAS;GAAG,OAAO;GAAzE,UAAA;IACE,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAa,IAAI,cAAc,EAAc,CAAA,GAC7C,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,oBAAC,QAAD;OAAM,WAAU;OACb,UAAA,OAAO,oBAAC,OAAD;QAAK,KAAK,KAAK,KAAK,WAAW,WAAW;QAAM,KAAI;QAAG,OAAO;QAAI,QAAQ;OAAK,CAAA,IAAI;MACvF,CAAA;MACN,oBAAC,UAAD,EAAA,UAAS,KAAa,CAAA;MACtB,oBAAC,QAAD;OAAM,WAAU;OAAuB,gBAAA;OACpC,UAAA,IAAI,WAAW;MACZ,CAAA;MACN,oBAAC,QAAD;OAAM,WAAU;OAAwB,UAAA,IAAI,aAAa;MAAQ,CAAA;KAC9D;IACC,CAAA,CAAA,EAAA,CAAA;IACR,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAa,IAAI,eAAe,EAAc,CAAA,GAC9C,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,oBAAC,UAAD,EAAA,UAAS,IAAI,cAAc,EAAU,CAAA;MACrC,oBAAC,QAAD;OAAM,WAAU;OAAyB,UAAA,IAAI,cAAc;MAAQ,CAAA;MACnE,oBAAC,QAAD;OAAM,WAAU;OAA2B,UAAA,IAAI,cAAc;MAAQ,CAAA;KAClE;IACC,CAAA,CAAA,EAAA,CAAA;IACR,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAa,IAAI,aAAa,EAAc,CAAA,GAC5C,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,oBAAC,QAAD,EAAA,UAAO,IAAI,YAAY,EAAQ,CAAA;MAC/B,oBAAC,UAAD,EAAA,UAAQ,MAAW,CAAA;MACnB,oBAAC,QAAD;OAAM,WAAU;OAAwB,eAAY;OACjD,UAAA;QAAC;QAAI;QAAI;QAAI;QAAI;OAAE,CAAC,CAAC,KAAK,QAAQ,UACjC,oBAAC,KAAD,EAAe,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,EAAI,GAAzC,KAAyC,CAClD;MACG,CAAA;KACH;IACC,CAAA,CAAA,EAAA,CAAA;IACR,qBAAC,UAAD,EAAA,UAAA,CACE,oBAAC,cAAD,EAAA,UAAa,IAAI,cAAc,EAAc,CAAA,GAC7C,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,oBAAC,OAAD;OAAK,WAAU;OACZ,UAAA,OAAO,oBAAC,OAAD;QAAK,KAAK;QAAM,KAAI;QAAG,OAAO;QAAI,QAAQ;OAAK,CAAA,IAAI,oBAAC,UAAD,EAAA,UAAS,KAAa,CAAA;MAC9E,CAAA;MACL,oBAAC,KAAD,EAAA,UAAI,IAAI,iBAAiB,EAAK,CAAA;MAC9B,oBAAC,QAAD;OAAM,WAAU;OAA2B,UAAA,IAAI,mBAAmB;MAAQ,CAAA;KACvE;IACC,CAAA,CAAA,EAAA,CAAA;GACD;EACN,CAAA,CAAA;;AAET"}