@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.
- package/README.md +4 -3
- package/dist/client/assets/LayoutBuilder-MPzkVU2e.js.map +1 -1
- package/dist/client/assets/index-D2dHI6hR.js.map +1 -1
- package/dist/client/assets/shell-TreN_uLs.js.map +1 -1
- package/dist/docs/access-model.md +1 -1
- package/dist/docs/build-and-deploy.md +0 -1
- package/dist/docs/cli.md +1 -1
- package/dist/docs/glossary.md +3 -3
- package/dist/docs/query.md +9 -21
- package/dist/docs/status.md +3 -3
- package/dist/embed/react/chunks/LayoutBuilder-Cbxa_wve.js.map +1 -1
- package/dist/embed/react/chunks/app-0UZ1Re1k.js.map +1 -1
- package/dist/embed/react/chunks/page-Bx4o1kpQ.js.map +1 -1
- package/dist/embed/react/chunks/palette-Bb7Lf6eo.js.map +1 -1
- package/dist/embed/react/chunks/shell-DZRACiB9.js.map +1 -1
- package/dist/embed/react/chunks/write-BQO4CFc9.js.map +1 -1
- package/dist/embed/react/index.js +1 -1
- package/dist/server/build/dockerfile.js +1 -1
- package/dist/server/cli/usage.js +2 -2
- package/dist/ui/index.js +2 -2
- package/dist/ui/page.js +2 -2
- package/dist/ui/{shared-N4Ej8yRD.js → shared-1PD7TOTJ.js} +5 -5
- package/dist/ui/{shared-N4Ej8yRD.js.map → shared-1PD7TOTJ.js.map} +1 -1
- package/dist/ui/{shared-D5m86-4o.js → shared-1gMnOu9J.js} +6 -6
- package/dist/ui/{shared-D5m86-4o.js.map → shared-1gMnOu9J.js.map} +1 -1
- package/dist/ui/{shared-Ctaj8mR2.js → shared-5RQMi4BR.js} +3 -3
- package/dist/ui/{shared-Ctaj8mR2.js.map → shared-5RQMi4BR.js.map} +1 -1
- package/dist/ui/{shared-DpLeIATZ.js → shared-B-BUF9fx.js} +2 -2
- package/dist/ui/{shared-DpLeIATZ.js.map → shared-B-BUF9fx.js.map} +1 -1
- package/dist/ui/{shared-DWYnjItP.js → shared-B-MhiPIN.js} +9 -9
- package/dist/ui/{shared-DWYnjItP.js.map → shared-B-MhiPIN.js.map} +1 -1
- package/dist/ui/{shared-0fNuhSbG.js → shared-B07qIMlE.js} +5 -5
- package/dist/ui/{shared-0fNuhSbG.js.map → shared-B07qIMlE.js.map} +1 -1
- package/dist/ui/{shared-lYtMadRU.js → shared-B1SAS_J-.js} +3 -3
- package/dist/ui/{shared-lYtMadRU.js.map → shared-B1SAS_J-.js.map} +1 -1
- package/dist/ui/{shared-CpI0Y8M5.js → shared-B4jgU8h9.js} +6 -6
- package/dist/ui/{shared-CpI0Y8M5.js.map → shared-B4jgU8h9.js.map} +1 -1
- package/dist/ui/{shared-Cxu1c35l.js → shared-B5aBCUi7.js} +30 -30
- package/dist/ui/{shared-Cxu1c35l.js.map → shared-B5aBCUi7.js.map} +1 -1
- package/dist/ui/{shared-CAe-US1q.js → shared-B9H7_Pji.js} +5 -5
- package/dist/ui/{shared-CAe-US1q.js.map → shared-B9H7_Pji.js.map} +1 -1
- package/dist/ui/{shared-Q2bgBaYo.js → shared-BAXr20S-.js} +4 -4
- package/dist/ui/{shared-Q2bgBaYo.js.map → shared-BAXr20S-.js.map} +1 -1
- package/dist/ui/{shared-CyEefFFb.js → shared-BDDUzH-f.js} +19 -19
- package/dist/ui/{shared-CyEefFFb.js.map → shared-BDDUzH-f.js.map} +1 -1
- package/dist/ui/{shared-BdhPI6dq.js → shared-BGmkeDeW.js} +3 -3
- package/dist/ui/{shared-BdhPI6dq.js.map → shared-BGmkeDeW.js.map} +1 -1
- package/dist/ui/{shared-DONIpq2q.js → shared-BHCQ60yN.js} +2 -2
- package/dist/ui/{shared-DONIpq2q.js.map → shared-BHCQ60yN.js.map} +1 -1
- package/dist/ui/{shared-CNmaAv64.js → shared-BHsjawAG.js} +8 -8
- package/dist/ui/{shared-CNmaAv64.js.map → shared-BHsjawAG.js.map} +1 -1
- package/dist/ui/{shared-DgjU9kOy.js → shared-BIQuK_nq.js} +4 -4
- package/dist/ui/{shared-DgjU9kOy.js.map → shared-BIQuK_nq.js.map} +1 -1
- package/dist/ui/{shared-icSQAj21.js → shared-BOe4W2FI.js} +3 -3
- package/dist/ui/{shared-icSQAj21.js.map → shared-BOe4W2FI.js.map} +1 -1
- package/dist/ui/{shared-BlxoOJSk.js → shared-BPpC08ip.js} +3 -3
- package/dist/ui/{shared-BlxoOJSk.js.map → shared-BPpC08ip.js.map} +1 -1
- package/dist/ui/{shared-BtiFL37J.js → shared-BdA3LPCB.js} +2 -2
- package/dist/ui/{shared-BtiFL37J.js.map → shared-BdA3LPCB.js.map} +1 -1
- package/dist/ui/{shared-CvR94vn4.js → shared-Be3nbWsd.js} +11 -11
- package/dist/ui/{shared-CvR94vn4.js.map → shared-Be3nbWsd.js.map} +1 -1
- package/dist/ui/{shared-Bl_nF_IH.js → shared-Bf30ZLoS.js} +8 -4
- package/dist/ui/shared-Bf30ZLoS.js.map +1 -0
- package/dist/ui/{shared-DijrJ_V-.js → shared-BguP_BaR.js} +7 -7
- package/dist/ui/{shared-DijrJ_V-.js.map → shared-BguP_BaR.js.map} +1 -1
- package/dist/ui/{shared-DPAVoWY0.js → shared-BoLbc2WD.js} +17 -2
- package/dist/ui/{shared-DPAVoWY0.js.map → shared-BoLbc2WD.js.map} +1 -1
- package/dist/ui/{shared-BkdBTYwx.js → shared-BttyOW4W.js} +3 -3
- package/dist/ui/{shared-BkdBTYwx.js.map → shared-BttyOW4W.js.map} +1 -1
- package/dist/ui/{shared-CQWLz8uT.js → shared-BumxsBlA.js} +10 -10
- package/dist/ui/{shared-CQWLz8uT.js.map → shared-BumxsBlA.js.map} +1 -1
- package/dist/ui/{shared-Dzu7tFiC.js → shared-Bw73ktBk.js} +3 -3
- package/dist/ui/{shared-Dzu7tFiC.js.map → shared-Bw73ktBk.js.map} +1 -1
- package/dist/ui/{shared-BhAYErSx.js → shared-C1B0GH0e.js} +4 -4
- package/dist/ui/{shared-BhAYErSx.js.map → shared-C1B0GH0e.js.map} +1 -1
- package/dist/ui/{shared-BQJQcGgj.js → shared-C2FsyYr7.js} +4 -4
- package/dist/ui/{shared-BQJQcGgj.js.map → shared-C2FsyYr7.js.map} +1 -1
- package/dist/ui/{shared-B9lw2Ryh.js → shared-C2RU1qjv.js} +4 -4
- package/dist/ui/{shared-B9lw2Ryh.js.map → shared-C2RU1qjv.js.map} +1 -1
- package/dist/ui/{shared-DQYNI9wO.js → shared-C42f07hJ.js} +5 -5
- package/dist/ui/{shared-DQYNI9wO.js.map → shared-C42f07hJ.js.map} +1 -1
- package/dist/ui/{shared-CqCZ8wMY.js → shared-C4LkPCU4.js} +3 -3
- package/dist/ui/{shared-CqCZ8wMY.js.map → shared-C4LkPCU4.js.map} +1 -1
- package/dist/ui/{shared-BhtpgOpa.js → shared-C4wSfz6k.js} +9 -9
- package/dist/ui/{shared-BhtpgOpa.js.map → shared-C4wSfz6k.js.map} +1 -1
- package/dist/ui/{shared-BHc2Izkj.js → shared-C9DZtSFH.js} +2 -2
- package/dist/ui/{shared-BHc2Izkj.js.map → shared-C9DZtSFH.js.map} +1 -1
- package/dist/ui/{shared-DW1YuiNS.js → shared-CA52ClGo.js} +2 -2
- package/dist/ui/{shared-DW1YuiNS.js.map → shared-CA52ClGo.js.map} +1 -1
- package/dist/ui/{shared-BbSbilkh.js → shared-CBciTwm6.js} +9 -9
- package/dist/ui/{shared-BbSbilkh.js.map → shared-CBciTwm6.js.map} +1 -1
- package/dist/ui/{shared-C-J32eBn.js → shared-CL3RLKDA.js} +16 -16
- package/dist/ui/{shared-C-J32eBn.js.map → shared-CL3RLKDA.js.map} +1 -1
- package/dist/ui/{shared-Ck8S4OxW.js → shared-CLq3-_hW.js} +3 -3
- package/dist/ui/{shared-Ck8S4OxW.js.map → shared-CLq3-_hW.js.map} +1 -1
- package/dist/ui/{shared-Q5WefT7G.js → shared-CMjK-Ucd.js} +3 -3
- package/dist/ui/{shared-Q5WefT7G.js.map → shared-CMjK-Ucd.js.map} +1 -1
- package/dist/ui/{shared-D3MAuEA3.js → shared-CPjpSFGh.js} +2 -2
- package/dist/ui/{shared-D3MAuEA3.js.map → shared-CPjpSFGh.js.map} +1 -1
- package/dist/ui/{shared-VshXGwXP.js → shared-CSPhENq3.js} +2 -2
- package/dist/ui/{shared-VshXGwXP.js.map → shared-CSPhENq3.js.map} +1 -1
- package/dist/ui/{shared-DJrJaE73.js → shared-CZJE4Bmp.js} +3 -3
- package/dist/ui/{shared-DJrJaE73.js.map → shared-CZJE4Bmp.js.map} +1 -1
- package/dist/ui/{shared-D7ONOwI7.js → shared-C_BhUL2I.js} +7 -7
- package/dist/ui/{shared-D7ONOwI7.js.map → shared-C_BhUL2I.js.map} +1 -1
- package/dist/ui/{shared-j-2peBIB.js → shared-CbeujwdU.js} +4 -4
- package/dist/ui/{shared-j-2peBIB.js.map → shared-CbeujwdU.js.map} +1 -1
- package/dist/ui/{shared-D5GrOEa2.js → shared-Cbf-o4Dt.js} +4 -4
- package/dist/ui/{shared-D5GrOEa2.js.map → shared-Cbf-o4Dt.js.map} +1 -1
- package/dist/ui/{shared-HwiPIajr.js → shared-CdNmeNHh.js} +3 -3
- package/dist/ui/{shared-HwiPIajr.js.map → shared-CdNmeNHh.js.map} +1 -1
- package/dist/ui/{shared-ZCFt-CPb.js → shared-CdQOMD8J.js} +3 -3
- package/dist/ui/{shared-ZCFt-CPb.js.map → shared-CdQOMD8J.js.map} +1 -1
- package/dist/ui/{shared-ClhXSGGK.js → shared-CdiPYlTh.js} +2 -2
- package/dist/ui/{shared-ClhXSGGK.js.map → shared-CdiPYlTh.js.map} +1 -1
- package/dist/ui/{shared-DA8_pVRZ.js → shared-CepLAhwm.js} +2 -2
- package/dist/ui/{shared-DA8_pVRZ.js.map → shared-CepLAhwm.js.map} +1 -1
- package/dist/ui/{shared-BrYKjrnM.js → shared-Ch82h_Fu.js} +6 -6
- package/dist/ui/{shared-BrYKjrnM.js.map → shared-Ch82h_Fu.js.map} +1 -1
- package/dist/ui/{shared-C4UrE8Fb.js → shared-Chp88all.js} +3 -3
- package/dist/ui/{shared-C4UrE8Fb.js.map → shared-Chp88all.js.map} +1 -1
- package/dist/ui/{shared-CWa80DTO.js → shared-Ci16s2rN.js} +9 -9
- package/dist/ui/{shared-CWa80DTO.js.map → shared-Ci16s2rN.js.map} +1 -1
- package/dist/ui/{shared-tksTLD6Q.js → shared-CiqNkoE1.js} +5 -5
- package/dist/ui/{shared-tksTLD6Q.js.map → shared-CiqNkoE1.js.map} +1 -1
- package/dist/ui/{shared-yt7DjSrq.js → shared-CpMhOq8F.js} +2 -2
- package/dist/ui/{shared-yt7DjSrq.js.map → shared-CpMhOq8F.js.map} +1 -1
- package/dist/ui/{shared-Dm0z5WUl.js → shared-CrWv5JKH.js} +6 -6
- package/dist/ui/{shared-Dm0z5WUl.js.map → shared-CrWv5JKH.js.map} +1 -1
- package/dist/ui/{shared-CQL3FGHc.js → shared-Csfi3Pti.js} +2 -2
- package/dist/ui/{shared-CQL3FGHc.js.map → shared-Csfi3Pti.js.map} +1 -1
- package/dist/ui/{shared-BKNRbm-V.js → shared-CuZt26rm.js} +2 -2
- package/dist/ui/{shared-BKNRbm-V.js.map → shared-CuZt26rm.js.map} +1 -1
- package/dist/ui/{shared-Guq3upiR.js → shared-Cws_z-TS.js} +2 -2
- package/dist/ui/{shared-Guq3upiR.js.map → shared-Cws_z-TS.js.map} +1 -1
- package/dist/ui/{shared-Bw_qZeIj.js → shared-D1KWuCg6.js} +2 -2
- package/dist/ui/{shared-Bw_qZeIj.js.map → shared-D1KWuCg6.js.map} +1 -1
- package/dist/ui/{shared-CuCZKwXW.js → shared-D66ju_Cb.js} +23 -23
- package/dist/ui/{shared-CuCZKwXW.js.map → shared-D66ju_Cb.js.map} +1 -1
- package/dist/ui/{shared-DYfX7NSM.js → shared-DGaQZXnw.js} +2 -2
- package/dist/ui/{shared-DYfX7NSM.js.map → shared-DGaQZXnw.js.map} +1 -1
- package/dist/ui/{shared-B3Hp8tOv.js → shared-DHy41sET.js} +6 -6
- package/dist/ui/{shared-B3Hp8tOv.js.map → shared-DHy41sET.js.map} +1 -1
- package/dist/ui/{shared-CXyd7IKi.js → shared-DKEOMQ6l.js} +2 -2
- package/dist/ui/{shared-CXyd7IKi.js.map → shared-DKEOMQ6l.js.map} +1 -1
- package/dist/ui/{shared-Ccmc8YO0.js → shared-DSUICuaJ.js} +33 -33
- package/dist/ui/{shared-Ccmc8YO0.js.map → shared-DSUICuaJ.js.map} +1 -1
- package/dist/ui/{shared-CL2iw_js.js → shared-DUPMRoO0.js} +13 -13
- package/dist/ui/{shared-CL2iw_js.js.map → shared-DUPMRoO0.js.map} +1 -1
- package/dist/ui/{shared-Dtp4eRul.js → shared-DVfdm6ah.js} +2 -2
- package/dist/ui/{shared-Dtp4eRul.js.map → shared-DVfdm6ah.js.map} +1 -1
- package/dist/ui/{shared-BJ5Ex32G.js → shared-DXM7d7lJ.js} +3 -3
- package/dist/ui/{shared-BJ5Ex32G.js.map → shared-DXM7d7lJ.js.map} +1 -1
- package/dist/ui/{shared-BFr6ld0e.js → shared-DXbMlcev.js} +2 -2
- package/dist/ui/{shared-BFr6ld0e.js.map → shared-DXbMlcev.js.map} +1 -1
- package/dist/ui/{shared-BUp3jv-N.js → shared-D_SkPtJ9.js} +6 -6
- package/dist/ui/{shared-BUp3jv-N.js.map → shared-D_SkPtJ9.js.map} +1 -1
- package/dist/ui/{shared-CdNomVzH.js → shared-Dd21k2ic.js} +4 -4
- package/dist/ui/{shared-CdNomVzH.js.map → shared-Dd21k2ic.js.map} +1 -1
- package/dist/ui/{shared-gRMbxmpu.js → shared-Dh0nHtLZ.js} +8 -8
- package/dist/ui/{shared-gRMbxmpu.js.map → shared-Dh0nHtLZ.js.map} +1 -1
- package/dist/ui/{shared-BKGJgjog.js → shared-DkOdSpSK.js} +3 -3
- package/dist/ui/{shared-BKGJgjog.js.map → shared-DkOdSpSK.js.map} +1 -1
- package/dist/ui/{shared-BMsh72Uq.js → shared-Dkph5ckT.js} +2 -2
- package/dist/ui/{shared-BMsh72Uq.js.map → shared-Dkph5ckT.js.map} +1 -1
- package/dist/ui/{shared-Be1HpDOB.js → shared-Dl1_Nx--.js} +3 -3
- package/dist/ui/{shared-Be1HpDOB.js.map → shared-Dl1_Nx--.js.map} +1 -1
- package/dist/ui/{shared-BQIZWOoJ.js → shared-Dm0IGbAf.js} +2 -2
- package/dist/ui/{shared-BQIZWOoJ.js.map → shared-Dm0IGbAf.js.map} +1 -1
- package/dist/ui/{shared-DSUtcU5S.js → shared-DnIkTpPr.js} +5 -5
- package/dist/ui/{shared-DSUtcU5S.js.map → shared-DnIkTpPr.js.map} +1 -1
- package/dist/ui/{shared-DsxGbYlV.js → shared-Docl_0tq.js} +3 -3
- package/dist/ui/{shared-DsxGbYlV.js.map → shared-Docl_0tq.js.map} +1 -1
- package/dist/ui/{shared-DZskrV26.js → shared-DprrHC72.js} +2 -2
- package/dist/ui/{shared-DZskrV26.js.map → shared-DprrHC72.js.map} +1 -1
- package/dist/ui/{shared-Bmw43Hwf.js → shared-DyKsJTZs.js} +2 -2
- package/dist/ui/{shared-Bmw43Hwf.js.map → shared-DyKsJTZs.js.map} +1 -1
- package/dist/ui/{shared-BLG88Pya.js → shared-FO7vfI3F.js} +4 -4
- package/dist/ui/{shared-BLG88Pya.js.map → shared-FO7vfI3F.js.map} +1 -1
- package/dist/ui/{shared-DLSd80D1.js → shared-HalAVHjv.js} +3 -3
- package/dist/ui/{shared-DLSd80D1.js.map → shared-HalAVHjv.js.map} +1 -1
- package/dist/ui/{shared-D1mMBLdj.js → shared-JvcI-LWp.js} +15 -15
- package/dist/ui/{shared-D1mMBLdj.js.map → shared-JvcI-LWp.js.map} +1 -1
- package/dist/ui/{shared-C6Hv_4O0.js → shared-K7Ym27yg.js} +2 -2
- package/dist/ui/{shared-C6Hv_4O0.js.map → shared-K7Ym27yg.js.map} +1 -1
- package/dist/ui/{shared-B39ln7oi.js → shared-SzlnwfJO.js} +3 -3
- package/dist/ui/{shared-B39ln7oi.js.map → shared-SzlnwfJO.js.map} +1 -1
- package/dist/ui/{shared-DgPn_Jll.js → shared-TrGbd1FY.js} +3 -3
- package/dist/ui/{shared-DgPn_Jll.js.map → shared-TrGbd1FY.js.map} +1 -1
- package/dist/ui/{shared-DrFb-XtA.js → shared-WhbuObEW.js} +2 -2
- package/dist/ui/{shared-DrFb-XtA.js.map → shared-WhbuObEW.js.map} +1 -1
- package/dist/ui/{shared-DiFQljcq.js → shared-XcSKDp6N.js} +11 -11
- package/dist/ui/{shared-DiFQljcq.js.map → shared-XcSKDp6N.js.map} +1 -1
- package/dist/ui/{shared-CMAg58rY.js → shared-Z4uBJU6I.js} +7 -6
- package/dist/ui/shared-Z4uBJU6I.js.map +1 -0
- package/dist/ui/{shared-BnUptuUp.js → shared-bAgeiRa5.js} +2 -2
- package/dist/ui/{shared-BnUptuUp.js.map → shared-bAgeiRa5.js.map} +1 -1
- package/dist/ui/{shared-BxMvSjey.js → shared-cgXG7OgZ.js} +5 -5
- package/dist/ui/{shared-BxMvSjey.js.map → shared-cgXG7OgZ.js.map} +1 -1
- package/dist/ui/{shared-u4CG2kQJ.js → shared-eZ0SB_zm.js} +2 -2
- package/dist/ui/{shared-u4CG2kQJ.js.map → shared-eZ0SB_zm.js.map} +1 -1
- package/dist/ui/{shared-L90bWW8e.js → shared-frKFb79C.js} +2 -2
- package/dist/ui/{shared-L90bWW8e.js.map → shared-frKFb79C.js.map} +1 -1
- package/dist/ui/{shared-Dd34nhkw.js → shared-hAayNSRA.js} +3 -3
- package/dist/ui/{shared-Dd34nhkw.js.map → shared-hAayNSRA.js.map} +1 -1
- package/dist/ui/{shared-DzC64cpp.js → shared-j_J16p2B.js} +2 -2
- package/dist/ui/{shared-DzC64cpp.js.map → shared-j_J16p2B.js.map} +1 -1
- package/dist/ui/{shared-DX7HRIV-.js → shared-tqpl5N3g.js} +6 -6
- package/dist/ui/{shared-DX7HRIV-.js.map → shared-tqpl5N3g.js.map} +1 -1
- package/dist/ui/{shared-BS5dBMpA.js → shared-y-6_ZDts.js} +2 -2
- package/dist/ui/{shared-BS5dBMpA.js.map → shared-y-6_ZDts.js.map} +1 -1
- package/dist/ui/{shared-Cv5v_MP1.js → shared-zG6e1Rhz.js} +13 -13
- package/dist/ui/{shared-Cv5v_MP1.js.map → shared-zG6e1Rhz.js.map} +1 -1
- package/dist/ui/testing.js +1 -1
- package/package.json +4 -4
- package/dist/docs/cli-browser.md +0 -232
- package/dist/docs/database-browser.md +0 -115
- package/dist/docs/keyboard.md +0 -63
- package/dist/docs/mcp.md +0 -86
- package/dist/ui/shared-Bl_nF_IH.js.map +0 -1
- package/dist/ui/shared-CMAg58rY.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-CvR94vn4.js","names":["say","say","say","say","say","PlacedFormSurface","say"],"sources":["../../src/shared/appBrandAddress.ts","../../src/client/copy/public/components/authRequest.ts","../../src/client/copy/public/components/SignIn.ts","../../src/client/components/SignIn.tsx","../../src/client/copy/public/signin.ts","../../src/client/copy/public/publicForm.ts","../../src/client/publicForm.js","../../src/client/copy/forms/components/FormDialog.ts","../../src/client/components/FormDialog.tsx"],"sourcesContent":["/**\n * Where an App's own logo pictures are served (server/app/appBrand.ts): `/{app}/_brand/<name>-<hash>.<ext>`, beside\n * its theme's faces at `/{app}/_fonts/`. The client draws a logo only from an address of this shape, so nothing the\n * payload carries can point the bar at another site.\n */\nexport const BRAND_SEGMENT = '_brand';\n\nconst ADDRESS = new RegExp(`^/[^/?#]+/${BRAND_SEGMENT}/[a-z0-9][a-z0-9-]*\\\\.(?:png|jpg|webp|gif|svg)$`);\n\n/** Whether `href` is one of an App's own logo addresses. */\nexport function isBrandAddress(href: unknown): href is string {\n return typeof href === 'string' && ADDRESS.test(href);\n}\n","/** What authRequest says, in the public pages 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 signInFailed: 'Sign-in failed ({status}).',\n});\n","/** What SignIn says, in the public pages 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 google: 'Google',\n anAccountAlreadyUsesThisEmail: 'An account already uses this email address. Sign in instead.',\n enterAnEmailAddress: 'Enter an email address, like name@example.com.',\n useAtLeastCharacters: 'Use at least {min} characters.',\n thatLinkHasExpired: 'That link has expired or was already used. Ask for a new one below.',\n yourPasswordIsChanged: 'Your password is changed. Sign in with the new one.',\n enterYourEmailAddress: 'Enter your email address.',\n chooseAPassword: 'Choose a password.',\n enterYourPassword: 'Enter your password.',\n createYourAccount: 'Create your {app} account',\n join: 'Join {app}',\n resetYourPassword: 'Reset your password',\n chooseANewPassword: 'Choose a new password',\n sign: 'Sign in to {app}',\n back: 'Back to {app}',\n yourAccountIsReady: 'Your account is ready. Sign in to continue.',\n yourName: 'Your name',\n youExampleCom: 'you@example.com',\n atLeastCharacters: 'At least {min} characters.',\n hide: 'Hide',\n show: 'Show',\n createAccount: 'Create account',\n forgotPassword: 'Forgot password?',\n ifAnAccountUses: 'If an account uses {sentTo}, a link to choose a new password is on its way. It works once, for an hour.',\n enterTheEmailAddress: 'Enter the email address you sign in with, and we will send a link to choose a new password.',\n sendResetLink: 'Send reset link',\n passwordResetsForAreMade: 'Password resets for {app} are made by its administrator. Ask them for a new password or a reset link.',\n newPassword: 'New password',\n confirmNewPassword: 'Confirm new password',\n changePassword: 'Change password',\n continue: 'Continue with',\n haveAnInvitationCreate: 'Have an invitation? Create your account',\n haveAnAccountSign: 'Have an account? Sign in',\n newHereCreateAnAccount: 'New here? Create an account',\n workEmail: 'Sign in with your work email',\n workEmailLede: 'Enter your work email, and we will send you to your organization to sign in.',\n findMyOrganization: 'Continue',\n noOrganizationSignsIn:\n 'No organization signs in to {app} with {domain} addresses. Use another way to sign in, or ask your administrator.',\n refusedNoRole: 'Your organization signed you in, and gives you no role in {app}. Ask your administrator for access.',\n refusedSignUpClosed: 'There is no {app} account for you yet. Ask your administrator to add you.',\n refusedOutsideDomain: \"That sign-in is for another organization's addresses. Use your own organization's sign-in.\",\n refusedNotLinked:\n 'An account already uses this email, and the sign-in did not confirm the address. Sign in the way you did before, or ask your administrator.',\n refusedOther: 'The sign-in did not finish. Try again, or use another way to sign in.',\n});\n","/**\n * The sign-in screen, for an app that signs people in.\n *\n * The server said 401 with what to draw: the app's name, which providers\n * it takes, whether anyone may sign up, and whether a forgotten password can\n * be reset by a link. Everything here talks to Better Auth's own routes under\n * /api/auth with plain fetch — a JSON body in, a session cookie out — so the\n * client carries no auth library. Wearing the app's dress (its accent, its\n * name), because the person is signing in to the app, not to the tool\n * underneath.\n */\nimport { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react';\nimport { useMutation } from '@tanstack/react-query';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { Button } from '@mistriwork/ui/Button.js';\nimport { Field } from '@mistriwork/ui/Field.js';\nimport { AuthRefusal, post, reload } from './authRequest.js';\nimport { say } from '../copy/public/components/SignIn.js';\nimport { navReplace, navState } from '@mistriwork/kit/client/shellHost.js';\n\n/* Account creation by invitation is an explicit choice, not every sign-in's first paint. */\nconst ClaimInvitation = onDemand(() => import('./ClaimInvitation.js'), 'ClaimInvitation');\n\nexport interface SignInInfo {\n app: string;\n look?: { logo?: string; icons?: Record<string, import('@mistriwork/engine/shared/iconGlyphs.js').IconGlyph> };\n providers: string[];\n /** `provider`: accounts are made at a first sign-in through an identity provider; email sign-up is closed. */\n signUp: 'open' | 'invite' | 'provider';\n /** An identity provider's button words, by how the providers list names it (`oidc:workforce`). */\n labels?: Record<string, string>;\n /** The App id an account-creating invitation is claimed into, when the tenant host offers claims. */\n invitationClaim?: string;\n /** `'link'`: a forgotten password is reset by a one-time link the server\n delivers. Absent: an administrator resets it, and the screen says so. */\n reset?: 'link';\n}\n\n/** Where the signed-out App lives beside this door — its landing or public pages — for the way back. */\nexport interface SignInHome {\n href: string;\n}\n\nconst NAMES: Record<string, string> = { github: 'GitHub', google: say('google') };\n/* The two providers the platform names itself get their mark at the button's start, drawn here and not\n from the App's icon payload: the sign-in screen is the one surface with no App payload yet. GitHub's is\n one path in the text colour, so every look and dark scheme reads it; Google's keeps its four colours, as\n its brand rules ask. An OpenID Connect provider is whatever its config calls it and gets no mark. */\nconst MARKS: Record<string, ReactNode> = {\n github: (\n <path\n fill=\"currentColor\"\n d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\n />\n ),\n google: (\n <>\n <path\n fill=\"#4285F4\"\n d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n />\n </>\n ),\n};\n/** Each tenant's own identity providers, of either kind: found by the domain of the email a person types, never listed. */\nconst TENANT_PROVIDERS = ['oidc:*', 'saml:*'];\n/** The provider id Better Auth knows a listed provider by: `oidc:workforce` is `workforce`, `saml:okta` is `okta`. */\nconst providerId = (provider: string) => (provider.startsWith('oidc:') || provider.startsWith('saml:') ? provider.slice(5) : provider);\n\n/* The codes a refused sign-in through a provider comes back with (auth/identityRoles.ts, and Better Auth's\n own). Only the code is read: the words beside it in the address are anyone's to write. */\nconst REFUSED: Record<string, (app: string) => string> = {\n no_role: (app) => say('refusedNoRole', { app }),\n sign_up_closed: (app) => say('refusedSignUpClosed', { app }),\n signup_disabled: (app) => say('refusedSignUpClosed', { app }),\n outside_domain: () => say('refusedOutsideDomain'),\n account_not_linked: () => say('refusedNotLinked'),\n};\n\n/** A refusal a provider's sign-in came back to this page with, said once and taken out of the address. */\nfunction arrivedRefusal(app: string): string {\n if (typeof window === 'undefined') return '';\n const url = new URL(window.location.href);\n const code = url.searchParams.get('error');\n if (!code || url.searchParams.get('reset') === '1') return '';\n for (const name of ['error', 'error_description']) url.searchParams.delete(name);\n navReplace(`${url.pathname}${url.search}${url.hash}`, navState());\n return (REFUSED[code] ?? (() => say('refusedOther')))(app);\n}\nconst MIN_PASSWORD = 8;\n\ntype Mode = 'in' | 'up' | 'claim' | 'forgot' | 'reset' | 'work';\ntype Problems = Partial<Record<'name' | 'email' | 'password' | 'confirm', string>>;\n\n/* A server refusal that is about one field is said beside that field. The\n rest — a wrong email or password, which must not say which — stays the\n form's one sentence. */\nconst FIELD_OF: Record<string, keyof Problems> = {\n USER_ALREADY_EXISTS: 'email',\n USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: 'email',\n INVALID_EMAIL: 'email',\n PASSWORD_TOO_SHORT: 'password',\n PASSWORD_TOO_LONG: 'password',\n};\nconst SAID_OF: Record<string, string> = {\n USER_ALREADY_EXISTS: say('anAccountAlreadyUsesThisEmail'),\n USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: say('anAccountAlreadyUsesThisEmail'),\n INVALID_EMAIL: say('enterAnEmailAddress'),\n PASSWORD_TOO_SHORT: say('useAtLeastCharacters', { min: MIN_PASSWORD }),\n};\n\nconst looksLikeEmail = (value: string) => /^[^\\s@]+@[^\\s@]+$/.test(value);\n\n/** What a reset link brought back to this page: its token, or that it was refused. */\nfunction arrivedReset(): { token?: string; failed?: true } | null {\n if (typeof window === 'undefined') return null;\n const params = new URLSearchParams(window.location.search);\n if (params.get('reset') !== '1') return null;\n const token = params.get('token');\n if (token) return { token };\n return params.get('error') ? { failed: true } : null;\n}\n\n/** The address a reset link comes back to: this page, marked, so a stray `?token=` elsewhere is never read as one. */\nfunction resetReturn(): string {\n const here = new URL(window.location.href);\n return `${here.origin}${here.pathname}?reset=1`;\n}\n\n/** Take the reset's words out of the address, so a reload or a shared link carries no token. */\nfunction forgetResetAddress(): void {\n const url = new URL(window.location.href);\n for (const name of ['reset', 'token', 'error']) url.searchParams.delete(name);\n navReplace(`${url.pathname}${url.search}${url.hash}`, navState());\n}\n\nexport function SignIn({\n info,\n home,\n start,\n lead,\n}: {\n info: SignInInfo;\n home?: SignInHome;\n /** `'up'`: open on creating an account, where the App lets anyone make one — a saved form's \"Create an account to track this\". */\n start?: 'up';\n /** Why this door is here, said above the form: \"Sign in to add your application to your account.\" */\n lead?: string;\n}) {\n const [arrived] = useState(arrivedReset);\n const [mode, setMode] = useState<Mode>(() =>\n arrived?.token\n ? 'reset'\n : arrived?.failed\n ? 'forgot'\n : start === 'up' && info.signUp === 'open' && info.providers.includes('email')\n ? 'up'\n : 'in',\n );\n const token = useRef(arrived?.token ?? '');\n // The address an invitation claim just made an account for, handed to sign-in.\n const [claimed, setClaimed] = useState<string | null>(null);\n // A sentence the sign-in form opens with: an account made, a password changed.\n const [notice, setNotice] = useState('');\n const first = useRef<HTMLInputElement>(null);\n const [revealed, setRevealed] = useState(false);\n const [problems, setProblems] = useState<Problems>({});\n const [formProblem, setFormProblem] = useState(() => (arrived?.failed ? say('thatLinkHasExpired') : arrivedRefusal(info.app)));\n const [sentTo, setSentTo] = useState('');\n const email = info.providers.includes('email');\n const flips = info.signUp === 'open' && email;\n const claims = email && typeof info.invitationClaim === 'string' && !!info.invitationClaim;\n const fields = {\n name: useRef<HTMLInputElement>(null),\n email: useRef<HTMLInputElement>(null),\n password: useRef<HTMLInputElement>(null),\n confirm: useRef<HTMLInputElement>(null),\n };\n\n useEffect(() => {\n if (arrived) forgetResetAddress();\n }, [arrived]);\n useEffect(() => {\n first.current?.focus();\n }, [mode]);\n\n const go = (next: Mode) => {\n setProblems({});\n setFormProblem('');\n setSentTo('');\n setNotice('');\n setRevealed(false);\n setMode(next);\n };\n\n /** Say what is wrong beside each field, and send the focus to the first. */\n const [fix, setFix] = useState<{ key: keyof Problems } | null>(null);\n useEffect(() => {\n if (fix) fields[fix.key].current?.focus();\n }, [fix]);\n const refuse = (found: Problems) => {\n setProblems(found);\n const at = (['name', 'email', 'password', 'confirm'] as const).find((key) => found[key]);\n // Once drawn, so the field is heard already saying it is invalid (docs/design.md, Access).\n if (at) setFix({ key: at });\n return !at;\n };\n /** A server refusal: beside its field when it names one, the form's sentence otherwise. */\n const refused = (error: unknown) => {\n const code = error instanceof AuthRefusal ? error.code : undefined;\n const field = code ? FIELD_OF[code] : undefined;\n if (field) {\n refuse({ [field]: SAID_OF[code!] ?? (error as Error).message });\n setFormProblem('');\n } else setFormProblem((error as Error).message);\n };\n\n const credentials = useMutation({\n mutationFn: async (form: { name: string; email: string; password: string }) =>\n mode === 'up'\n ? post('/api/auth/sign-up/email', form)\n : post('/api/auth/sign-in/email', { email: form.email, password: form.password, rememberMe: true }),\n onSuccess: reload,\n onError: refused,\n });\n\n const social = useMutation({\n /* A refusal comes back to this page, where it is said (arrivedRefusal). */\n mutationFn: ({ provider, hint }: { provider: string; hint?: string }) =>\n /* A SAML provider's sign-in names it and where to come back to, and nothing else (auth/samlSignIn.ts). */\n provider.startsWith('saml:')\n ? post('/api/auth/sign-in/sso', {\n providerId: providerId(provider),\n callbackURL: window.location.href,\n errorCallbackURL: window.location.href,\n })\n : post('/api/auth/sign-in/social', {\n provider: providerId(provider),\n callbackURL: window.location.href,\n errorCallbackURL: window.location.href,\n ...(hint ? { loginHint: hint } : {}),\n }),\n onSuccess: (data) => {\n if (typeof data.url === 'string') window.location.href = data.url;\n },\n onError: (error) => setFormProblem((error as Error).message),\n });\n\n /* \"Sign in with your work email\": the address's domain names its organization's provider, and the\n sign-in goes there with the address as a hint. */\n const realm = useMutation({\n mutationFn: async (address: string) => ({ address, found: await post('/api/auth/work-email', { email: address }) }),\n onSuccess: ({ address, found }) => {\n // The answer names the provider and its kind; the kind says which sign-in starts there.\n if (typeof found.provider === 'string')\n social.mutate({ provider: `${found.kind === 'saml' ? 'saml' : 'oidc'}:${found.provider}`, hint: address });\n else setFormProblem(say('noOrganizationSignsIn', { app: info.app, domain: address.slice(address.lastIndexOf('@') + 1) }));\n },\n onError: refused,\n });\n\n const recovery = useMutation({\n mutationFn: (address: string) => post('/api/auth/request-password-reset', { email: address, redirectTo: resetReturn() }),\n onSuccess: (_data, address) => setSentTo(address),\n onError: refused,\n });\n\n const choosing = useMutation({\n mutationFn: (password: string) => post('/api/auth/reset-password', { newPassword: password, token: token.current }),\n onSuccess: () => {\n token.current = '';\n go('in');\n setNotice(say('yourPasswordIsChanged'));\n },\n onError: (error) => {\n // A spent or expired token cannot be tried again: ask for a new link.\n if (error instanceof AuthRefusal && error.code === 'INVALID_TOKEN') {\n token.current = '';\n go('forgot');\n setFormProblem(say('thatLinkHasExpired'));\n } else refused(error);\n },\n });\n\n const submit = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (credentials.isPending) return;\n const form = new FormData(e.currentTarget);\n const values = {\n name: String(form.get('name') ?? '').trim(),\n email: String(form.get('email') ?? '').trim(),\n password: String(form.get('password') ?? ''),\n };\n setFormProblem('');\n const found: Problems = {};\n if (mode === 'up' && !values.name) found.name = 'Enter your name.';\n if (!values.email) found.email = say('enterYourEmailAddress');\n else if (!looksLikeEmail(values.email)) found.email = say('enterAnEmailAddress');\n if (!values.password) found.password = mode === 'up' ? say('chooseAPassword') : say('enterYourPassword');\n else if (mode === 'up' && values.password.length < MIN_PASSWORD) found.password = say('useAtLeastCharacters', { min: MIN_PASSWORD });\n if (!refuse(found)) return;\n credentials.mutate(mode === 'up' ? values : { ...values, name: '' });\n };\n\n const work = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (realm.isPending || social.isPending) return;\n const address = String(new FormData(e.currentTarget).get('email') ?? '').trim();\n setFormProblem('');\n if (!refuse(!address ? { email: say('enterYourEmailAddress') } : !looksLikeEmail(address) ? { email: say('enterAnEmailAddress') } : {}))\n return;\n realm.mutate(address);\n };\n\n const ask = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (recovery.isPending) return;\n const address = String(new FormData(e.currentTarget).get('email') ?? '').trim();\n setFormProblem('');\n if (!refuse(!address ? { email: say('enterYourEmailAddress') } : !looksLikeEmail(address) ? { email: say('enterAnEmailAddress') } : {}))\n return;\n recovery.mutate(address);\n };\n\n const choose = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (choosing.isPending) return;\n const form = new FormData(e.currentTarget);\n const password = String(form.get('password') ?? ''),\n confirm = String(form.get('confirm') ?? '');\n setFormProblem('');\n if (\n !refuse(\n password.length < MIN_PASSWORD\n ? { password: say('useAtLeastCharacters', { min: MIN_PASSWORD }) }\n : password !== confirm\n ? { confirm: say('common.thePasswordsDoNotMatch') }\n : {},\n )\n )\n return;\n choosing.mutate(password);\n };\n\n /** A field's control, joined to the field's own ref and the one that takes the focus first. */\n const bind = (key: keyof Problems, focusFirst: boolean) => (node: HTMLInputElement | null) => {\n fields[key].current = node;\n if (focusFirst) first.current = node;\n };\n const clear = (key: keyof Problems) => () => {\n if (problems[key]) setProblems((before) => ({ ...before, [key]: undefined }));\n };\n\n const title =\n mode === 'up'\n ? say('createYourAccount', { app: info.app })\n : mode === 'claim'\n ? say('join', { app: info.app })\n : mode === 'forgot'\n ? say('resetYourPassword')\n : mode === 'reset'\n ? say('chooseANewPassword')\n : mode === 'work'\n ? say('workEmail')\n : say('sign', { app: info.app });\n const pending = credentials.isPending || recovery.isPending || choosing.isPending || realm.isPending;\n const listed = info.providers.filter((provider) => provider !== 'email' && !TENANT_PROVIDERS.includes(provider));\n const organizations = info.providers.some((provider) => TENANT_PROVIDERS.includes(provider));\n\n return (\n <div className=\"signin\">\n {/* The way back to the App's public pages, when it has them: the door is not a dead end. */}\n {home && (\n <a className=\"signin-home\" href={home.href} aria-label={say('back', { app: info.app })}>\n <span aria-hidden=\"true\">←</span> {info.app}\n </a>\n )}\n <div className=\"signin-card\">\n {/* The App's name is in the bar above; the heading says what this screen is for, in it. */}\n <h1 className=\"signin-title\">{title}</h1>\n {mode === 'claim' && claims && (\n <ClaimInvitation\n appId={info.invitationClaim!}\n onClaimed={(address) => {\n setClaimed(address);\n go('in');\n setNotice(say('yourAccountIsReady'));\n }}\n onCancel={() => go('in')}\n />\n )}\n {notice && mode === 'in' && (\n <p className=\"signin-notice\" role=\"status\">\n {notice}\n </p>\n )}\n {lead && !notice && (mode === 'in' || mode === 'up') && <p className=\"signin-notice\">{lead}</p>}\n {email && (mode === 'in' || mode === 'up') && (\n <form className=\"signin-form\" onSubmit={submit} noValidate key={claimed ?? ''}>\n {mode === 'up' && (\n <Field label={say('common.name')} problem={problems.name}>\n <input\n ref={bind('name', true)}\n type=\"text\"\n name=\"name\"\n className=\"field-control signin-input\"\n autoComplete=\"name\"\n required\n placeholder={say('yourName')}\n onInput={clear('name')}\n />\n </Field>\n )}\n <Field label={say('common.email')} problem={problems.email}>\n <input\n ref={bind('email', mode !== 'up')}\n type=\"email\"\n name=\"email\"\n className=\"field-control signin-input\"\n autoComplete=\"email\"\n required\n placeholder={say('youExampleCom')}\n defaultValue={mode === 'in' ? (claimed ?? undefined) : undefined}\n onInput={clear('email')}\n />\n </Field>\n <Field\n label={say('common.password')}\n id=\"signin-password\"\n problem={problems.password}\n hint={mode === 'up' ? say('atLeastCharacters', { min: MIN_PASSWORD }) : undefined}\n >\n {(control) => (\n <span className=\"signin-secret\">\n <input\n {...control}\n ref={bind('password', false)}\n type={revealed ? 'text' : 'password'}\n name=\"password\"\n className=\"field-control signin-input\"\n autoComplete={mode === 'up' ? 'new-password' : 'current-password'}\n required\n onInput={clear('password')}\n />\n {/* Named \"Show\"/\"Hide\", not \"Show password\": the field's own name stays the only \"Password\" on the page.\n Beside the field, not drawn over its right end: two controls in one box, one tapped for the other. */}\n <Button\n intent=\"quiet\"\n className=\"signin-reveal\"\n aria-pressed={revealed}\n aria-controls=\"signin-password\"\n onClick={() => setRevealed((on) => !on)}\n >\n {revealed ? say('hide') : say('show')}\n </Button>\n </span>\n )}\n </Field>\n {/* The refusal stands with the fields it is about, above the button that was just pressed, in a line held\n even while empty: below the links it arrived after the button and pushed \"Create an account\" down. */}\n <p className=\"signin-problem signin-problem-held\" role=\"alert\">\n {formProblem && !credentials.isPending ? formProblem : null}\n </p>\n {/* Busy, not disabled: a disabled button drops the focus it holds, and a\n refused sign-in left the reader on the page itself. submit refuses a second press. */}\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" className=\"signin-submit\" busy={credentials.isPending}>\n {mode === 'up' ? say('createAccount') : say('common.signIn')}\n </Button>\n {mode === 'in' && (\n <Button intent=\"quiet\" className=\"signin-forgot\" onClick={() => go('forgot')}>\n {say('forgotPassword')}\n </Button>\n )}\n </form>\n )}\n {mode === 'forgot' &&\n (info.reset === 'link' ? (\n sentTo ? (\n <p className=\"signin-notice\" role=\"status\">\n {say('ifAnAccountUses', { sentTo })}\n </p>\n ) : (\n <form className=\"signin-form\" onSubmit={ask} noValidate>\n <p className=\"signin-lede\">{say('enterTheEmailAddress')}</p>\n <Field label={say('common.email')} problem={problems.email}>\n <input\n ref={bind('email', true)}\n type=\"email\"\n name=\"email\"\n className=\"field-control signin-input\"\n autoComplete=\"email\"\n required\n placeholder={say('youExampleCom')}\n onInput={clear('email')}\n />\n </Field>\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" busy={recovery.isPending}>\n {say('sendResetLink')}\n </Button>\n </form>\n )\n ) : (\n <p className=\"signin-lede\">{say('passwordResetsForAreMade', { app: info.app })}</p>\n ))}\n {mode === 'reset' && (\n <form className=\"signin-form\" onSubmit={choose} noValidate>\n <Field label={say('newPassword')} problem={problems.password} hint={say('atLeastCharacters', { min: MIN_PASSWORD })}>\n <input\n ref={bind('password', true)}\n type=\"password\"\n name=\"password\"\n className=\"field-control signin-input\"\n autoComplete=\"new-password\"\n required\n onInput={clear('password')}\n />\n </Field>\n <Field label={say('confirmNewPassword')} problem={problems.confirm}>\n <input\n ref={bind('confirm', false)}\n type=\"password\"\n name=\"confirm\"\n className=\"field-control signin-input\"\n autoComplete=\"new-password\"\n required\n onInput={clear('confirm')}\n />\n </Field>\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" busy={choosing.isPending}>\n {say('changePassword')}\n </Button>\n </form>\n )}\n {mode === 'work' && (\n <form className=\"signin-form\" onSubmit={work} noValidate>\n <p className=\"signin-lede\">{say('workEmailLede')}</p>\n <Field label={say('common.email')} problem={problems.email}>\n <input\n ref={bind('email', true)}\n type=\"email\"\n name=\"email\"\n className=\"field-control signin-input\"\n autoComplete=\"email\"\n required\n placeholder={say('youExampleCom')}\n onInput={clear('email')}\n />\n </Field>\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" busy={realm.isPending || social.isPending}>\n {say('findMyOrganization')}\n </Button>\n </form>\n )}\n {(mode === 'in' || mode === 'up') && (listed.length > 0 || organizations) && (\n <div className=\"signin-social-row\">\n {listed.map((provider) => (\n <Button\n key={provider}\n intent=\"secondary\"\n size=\"large\"\n block\n className=\"signin-social\"\n busy={social.isPending}\n onClick={() => social.mutate({ provider })}\n >\n {MARKS[provider] && (\n <svg className=\"signin-mark\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n {MARKS[provider]}\n </svg>\n )}\n {say('continue')} {info.labels?.[provider] ?? NAMES[provider] ?? providerId(provider)}\n </Button>\n ))}\n {organizations && (\n <Button intent=\"secondary\" size=\"large\" block className=\"signin-social\" onClick={() => go('work')}>\n {say('workEmail')}\n </Button>\n )}\n </div>\n )}\n {mode !== 'claim' && !(email && (mode === 'in' || mode === 'up')) && (\n <p className=\"signin-problem\" role=\"alert\" hidden={!formProblem || pending}>\n {formProblem}\n </p>\n )}\n {(mode === 'forgot' || mode === 'reset' || mode === 'work') && (\n <Button intent=\"quiet\" className=\"signin-flip\" onClick={() => go('in')}>\n {say('common.backToSign')}\n </Button>\n )}\n {claims && (mode === 'in' || mode === 'up') && (\n <Button\n intent=\"quiet\"\n className=\"signin-flip\"\n onClick={() => {\n setClaimed(null);\n go('claim');\n }}\n >\n {say('haveAnInvitationCreate')}\n </Button>\n )}\n {mode !== 'in' && mode !== 'up' ? null : flips ? (\n <Button intent=\"quiet\" className=\"signin-flip\" onClick={() => go(mode === 'up' ? 'in' : 'up')}>\n {mode === 'up' ? say('haveAnAccountSign') : say('newHereCreateAnAccount')}\n </Button>\n ) : null}\n </div>\n </div>\n );\n}\n","/** What signin says, in the public pages 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 signInApp: 'Sign in · {app}',\n});\n","/** What publicForm says, in the public pages 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 thereIsNoSuch: 'There is no such form here.',\n theFormCouldNotBeLoaded: 'The form could not be loaded ({status}).',\n checkYourApplicationName: 'Check your application · {name}',\n signInToAddYourApplication: 'Sign in to add your application to your account.',\n addYourApplication: 'Add your application · {name}',\n signInToAddTheApplication: 'Sign in to add the application you just submitted to your account.',\n});\n","/**\n * A public form, as the vanilla client reaches it: the address, the one\n * read, and the crossing into React. Boot asks `publicFormRoute` (publicRoutes.js) before it\n * asks who-am-I, because the answer decides whether there is a session to\n * look for at all — a public form is open to everyone, so an applicant is\n * never sent to sign in, and someone signed in submits it as the public too.\n */\nimport { createElement } from 'react';\nimport { applyBrandLogo, headBrandingMark } from './appBrand.js';\nimport { $ } from '@mistriwork/kit/client/core.js';\nimport { isMounted, mount } from '@mistriwork/kit/client/components/mount.js';\nimport { takeServerDrawing } from './serverDrawing.js';\nimport { FormSurface, formArrives, submitPublicForm } from './components/FormSurface.js';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { showSignIn } from './signin.js';\nimport { say } from './copy/public/publicForm.js';\nimport { served, shown } from '@mistriwork/kit/client/addressMount.js';\nimport { firstPaintRead } from '@mistriwork/kit/client/firstPaint.js';\nimport { setTabTitle, shellBody, shellQuery } from '@mistriwork/kit/client/shellHost.js';\nimport { fetchServer } from '@mistriwork/kit/client/transport.js';\n\n/* Fetched when one is shown: FormDialog brings this module into the App\n shell, whose first paint no saved form is ever on. */\nconst SavedFormSurface = onDemand(() => import('./components/SavedFormSurface.js'), 'SavedFormSurface');\n/* A saved form's status check and account screen, each its own page under the form's address. */\nconst FormStatusSurface = onDemand(() => import('./components/FormStatusSurface.js'), 'FormStatusSurface');\nconst FormClaimSurface = onDemand(() => import('./components/FormClaimSurface.js'), 'FormClaimSurface');\nconst PlacedFormSurface = onDemand(() => import('./components/PlacedFormSurface.js'), 'PlacedFormSurface');\n/** @type {typeof import('./publicPlacement.js') | undefined} */\nlet placements;\n\n/**\n * The App's own sign-in, drawn where a saved form's page stands; once there\n * is a session the page reloads to itself (SignIn.tsx), so the check or the\n * question it was on comes back signed in. The door's shape is who-am-I's.\n * @param {string} back the form's own address, the door's way back\n * @param {{ start?: 'up'; lead?: string }} opening\n */\nasync function signInHere(back, opening) {\n const res = await fetchServer('/api/whoami', { credentials: 'same-origin', signal: AbortSignal.timeout(15_000) });\n const me = await res.json();\n if (!me?.signIn) return;\n showSignIn(me.signIn, { href: back }, opening);\n}\n\n/**\n * The staff door, on a form served as the App's home: a quiet Sign in at the\n * top bar's end, as a public App's visitor has one (app.js, showAccount).\n * The App's root is the form there, so without it staff had to know to type\n * `/{app}/sign-in`. Not on the form's own address, `/{app}/form/{key}`: that\n * is a link handed to applicants, drawn before anyone is asked who they are,\n * and its saved flows offer their own sign-in where one is wanted.\n * @param {string} appId\n */\nfunction offerSignIn(appId) {\n const bar = shellQuery('.topbar');\n if (!bar || bar.querySelector('.public-sign-in')) return;\n const link = document.createElement('a');\n link.className = 'ghost account-sign-in public-sign-in';\n link.href = shown(`/${encodeURIComponent(appId)}/sign-in`);\n link.textContent = say('common.signIn');\n bar.append(link);\n}\n\n/**\n * Fetch one public form's presentation. `undefined` when the server has no\n * public forms at all, so this address is not one and boot carries on; a\n * throw, with the sentence to show, for a form this App does not serve to\n * everyone — a mistyped key, or a signed-in form's — or a read that failed.\n * Never a sign-in prompt.\n * @param {string} id\n * @param {{ appId?: string; fresh?: boolean }} [options]\n * @returns {Promise<{ app: { id: string; name: string; logo?: import('@mistriwork/engine/shared/appIcons.js').AppIconName; icons?: Record<string, import('@mistriwork/engine/shared/iconGlyphs.js').IconGlyph>; theme?: import('../shared/app.js').AppTheme; customThemes?: import('@mistriwork/engine/shared/theme.js').ThemeDefinition[] }; form: import('./components/FormSurface.js').FormPresentation; placement?: import('../shared/publicPlacement.js').PublicFormPlacement } | undefined>}\n */\nexport async function loadPublicForm(id, options = {}) {\n const retained = placements?.retainedPublicForm(id, options.appId);\n if (retained) return retained;\n /* The page arrived with it (firstPaint.ts): a form's page asks nothing. */\n const carried = options.fresh ? undefined : firstPaintRead('GET', `/api/forms/${encodeURIComponent(id)}`)?.body;\n if (carried && typeof carried === 'object' && 'form' in carried && !('placement' in carried)) {\n // What the form asks beyond its fields — the stepper and its sheets, the draft's logic, live lines — comes with\n // it, so its first frame is whole, and a form that asks none of it fetches nothing (FormSurface.tsx formArrives).\n await formArrives(/** @type {any} */ (carried).form);\n return /** @type {any} */ (carried);\n }\n /* A tenant's link carries its intake address (`?via=…`): the read carries it on, so a choice from a table is\n offered that tenant's rows (server tenant/tenantPublicChoices.ts). */\n const via = new URLSearchParams(window.location.search).get('via');\n const res = await fetchServer(`/api/forms/${encodeURIComponent(id)}${via ? `?${new URLSearchParams({ via })}` : ''}`, {\n credentials: 'omit',\n cache: 'no-store',\n signal: AbortSignal.timeout(15_000),\n });\n /** @type {Record<string, any>} */\n let data = {};\n try {\n data = await res.json();\n } catch {\n /* the status is the answer */\n }\n if (res.ok && data && typeof data === 'object' && 'form' in data) {\n if (data.form?.id !== id || (options.appId && data.app?.id !== options.appId)) throw new Error(say('thereIsNoSuch'));\n if ('placement' in data) {\n placements ??= await import('./publicPlacement.js');\n return placements.preparePublicPlacement(/** @type {any} */ (data));\n }\n if (data.form) await formArrives(data.form);\n return /** @type {any} */ (data);\n }\n if (res.status === 404 && data?.code !== 'no_such_form') return undefined;\n throw new Error(\n res.status === 404 || res.status === 401\n ? say('thereIsNoSuch')\n : String(data?.error || say('theFormCouldNotBeLoaded', { status: res.status })),\n );\n}\n\n/**\n * Draw the public form into the content pane, in the App's name. The shell\n * chrome is hidden by class; the brand shows the App, not the tool.\n * @param {NonNullable<Awaited<ReturnType<typeof loadPublicForm>>>} prepared\n * @param {'status' | 'claim'} [page] a saved form's status check or account screen, under its address\n * @param {{ signIn?: boolean }} [options] `signIn`: the form is the App's home, to someone signed out of an App that signs people in\n * @returns {boolean} whether the address is one this form serves\n */\nexport function showPublicForm(prepared, page, options = {}) {\n const { app, form } = prepared;\n /** @type {any} */\n const saved = form;\n if (page === 'status' && !saved.save?.statusCheck) return false;\n if (page === 'claim' && !saved.save?.link?.sameDevice) return false;\n shellBody().classList.add('app-mode', 'public-form-mode');\n const brand = shellQuery('.brand-name');\n if (brand) brand.textContent = app.name;\n applyBrandLogo(\n shellQuery('.brand-mark'),\n /** @type {any} */ (app).theme?.branding?.mark ?? headBrandingMark() ?? app.logo,\n app.name,\n app.icons,\n );\n // An intake form's page has no heading: its label is the tab's title (docs/design.md).\n setTabTitle(form.style === 'intake' ? form.label : `${form.label} · ${app.name}`);\n const home = $('home-button');\n if (home instanceof HTMLButtonElement) {\n home.disabled = true;\n home.removeAttribute('title');\n }\n if (options.signIn) offerSignIn(app.id);\n const host = $('content');\n if (!isMounted(host) && !(host.getAttribute('data-ssr') === 'form' && !page && !prepared.placement && !(form.save && form.steps)))\n host.replaceChildren();\n /* A tenant publishes its form with its intake address, `?via=…`; the\n submission carries it on, as it was, for the server to resolve. Nothing\n on this page reads or depends on it. */\n const via = new URLSearchParams(window.location.search).get('via') ?? undefined;\n // As the browser shows it (addressMount.js): every use is a link.\n const base = shown(`/${encodeURIComponent(app.id)}/form/${encodeURIComponent(form.id)}`);\n if (prepared.placement) {\n if (page) return false;\n mount(host, createElement(PlacedFormSurface, { prepared, review: Boolean(form.steps?.length) }));\n return true;\n }\n if (page === 'status') {\n setTabTitle(say('checkYourApplicationName', { name: app.name }));\n mount(\n host,\n createElement(FormStatusSurface, {\n form: saved,\n appName: app.name,\n onSignIn: () => void signInHere(`${base}/status`, { lead: say('signInToAddYourApplication') }),\n }),\n );\n return true;\n }\n if (page === 'claim') {\n setTabTitle(say('addYourApplication', { name: app.name }));\n const start = new URLSearchParams(window.location.search).get('account') === 'new' ? 'up' : undefined;\n mount(\n host,\n createElement(FormClaimSurface, {\n form,\n appName: app.name,\n ...(saved.save?.statusCheck ? { statusHref: `${base}/status` } : {}),\n onSignIn: () => void signInHere(base, { ...(start ? { start } : {}), lead: say('signInToAddTheApplication') }),\n }),\n );\n return true;\n }\n // A form saved on every step draws its own stepper, review and leave guard.\n if (form.save && form.steps) {\n mount(\n host,\n createElement(SavedFormSurface, {\n form: /** @type {import('./components/SavedFormSurface.js').SavedFormPresentation} */ (form),\n appName: app.name,\n base,\n }),\n );\n return true;\n }\n // With steps, the last one reviews every answer from this page's memory before the one submit.\n mount(\n host,\n createElement(FormSurface, {\n form,\n appName: app.name,\n review: Boolean(form.steps?.length),\n // A reference input's choices: what the App opens to a signed-out visitor, read as one.\n visitor: { app: app.id, ...(via ? { via } : {}) },\n submit: (submission, input, guard) => submitPublicForm(form.id, submission, input, via, guard),\n // Once received, a quiet way back to the App's home; none where this form is that home (the way back is itself).\n ...(options.signIn ? {} : { homeHref: shown(`/${encodeURIComponent(app.id)}`) }),\n }),\n // The server drew the form as it opens (server/publicRender.ts): taken over where it stands.\n takeServerDrawing(host, 'form'),\n );\n return true;\n}\n","/** What FormDialog says, in the forms 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 form: 'Form',\n thereIsNoSuch: 'There is no such form here.',\n});\n","/**\n * A public form over the record that sent you to it.\n *\n * A call to action on a headline — \"Inquire about this car\", \"Schedule a test\n * drive\" — used to navigate to the form's own page, which takes away the very\n * thing the form is about: the price, the photograph, the mileage. Opened\n * here, the record stays behind the dialog and the reader can look again\n * before they send.\n *\n * The form is the same one the page draws (FormSurface), loaded from the same\n * public endpoint, submitted the same way. What differs is where it sits and\n * how it starts: the action's `prefill` and `defaults` arrive as a query\n * string, exactly as they would in the address, so the two routes to the form\n * cannot drift apart.\n */\nimport { useQuery } from '@tanstack/react-query';\nimport { useId, useMemo } from 'react';\nimport { loadPublicForm } from '../publicForm.js';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { served } from '@mistriwork/kit/client/addressMount.js';\nimport { FormSurface, submitPublicForm } from './FormSurface.js';\nimport { Modal } from '@mistriwork/ui/Modal.js';\nimport './FormDialog.css';\nimport { say } from '../copy/forms/components/FormDialog.js';\n\nconst PlacedFormSurface = onDemand(() => import('./PlacedFormSurface.js'), 'PlacedFormSurface');\n\nexport function FormDialog({\n view,\n appId,\n prefill,\n onClose,\n}: {\n view: string;\n appId?: string;\n /** The action's starting values as a query string (\"stock_ref=ABC&message=…\"). */\n prefill: string;\n onClose: () => void;\n}) {\n const instance = useId();\n // Invalidation may ask again, but this opening owns one preparation. An\n // unanswered send is retained by the placed owner beyond this opening.\n const prepare = useMemo(() => {\n let answer: ReturnType<typeof loadPublicForm> | undefined;\n return () => (answer ??= loadPublicForm(view, { appId, fresh: true }));\n }, [appId, view]);\n const asked = useQuery({\n queryKey: ['public-form', window.location.origin, served('/'), appId, view, instance],\n staleTime: Infinity,\n gcTime: 0,\n retry: false,\n queryFn: async () => (await prepare()) ?? null,\n });\n const form = asked.data?.form;\n /* The App's name comes with the form, the way it does for the form's own\n page — reading it from the nav store instead would make this module part\n of a cycle (test/repo/runtime-cycles). */\n const appName = asked.data?.app.name ?? '';\n /* A tenant publishes its form with its intake address, `?via=…`; a\n submission carries it on, as it was, for the server to resolve — the same\n rule the form's own page follows (publicForm.js). */\n const via = new URLSearchParams(window.location.search).get('via') ?? undefined;\n return (\n <Modal className=\"form-dialog\" onClose={onClose} ariaLabel={form?.label ?? say('form')}>\n {asked.isPending && <p className=\"form-dialog-waiting\">{say('common.loading')}</p>}\n {asked.isError && <p className=\"form-dialog-failed\">{(asked.error as Error).message}</p>}\n {asked.isSuccess && !form && <p className=\"form-dialog-failed\">{say('thereIsNoSuch')}</p>}\n {form &&\n (asked.data?.placement ? (\n <PlacedFormSurface key={instance + appId + view} prepared={asked.data} prefill={prefill} onDismiss={onClose} />\n ) : (\n <FormSurface\n key={instance + appId + view}\n form={form}\n appName={appName}\n prefill={prefill}\n inDialog\n // A reference input's choices: what the App opens to a signed-out visitor, read as one (publicForm.js).\n visitor={{ app: asked.data!.app.id, ...(via ? { via } : {}) }}\n submit={(submission, input, guard) => submitPublicForm(form.id, submission, input, via, guard)}\n />\n ))}\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shared-Be3nbWsd.js","names":["say","say","say","say","say","PlacedFormSurface","say"],"sources":["../../src/shared/appBrandAddress.ts","../../src/client/copy/public/components/authRequest.ts","../../src/client/copy/public/components/SignIn.ts","../../src/client/components/SignIn.tsx","../../src/client/copy/public/signin.ts","../../src/client/copy/public/publicForm.ts","../../src/client/publicForm.js","../../src/client/copy/forms/components/FormDialog.ts","../../src/client/components/FormDialog.tsx"],"sourcesContent":["/**\n * Where an App's own logo pictures are served (server/app/appBrand.ts): `/{app}/_brand/<name>-<hash>.<ext>`, beside\n * its theme's faces at `/{app}/_fonts/`. The client draws a logo only from an address of this shape, so nothing the\n * payload carries can point the bar at another site.\n */\nexport const BRAND_SEGMENT = '_brand';\n\nconst ADDRESS = new RegExp(`^/[^/?#]+/${BRAND_SEGMENT}/[a-z0-9][a-z0-9-]*\\\\.(?:png|jpg|webp|gif|svg)$`);\n\n/** Whether `href` is one of an App's own logo addresses. */\nexport function isBrandAddress(href: unknown): href is string {\n return typeof href === 'string' && ADDRESS.test(href);\n}\n","/** What authRequest says, in the public pages 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 signInFailed: 'Sign-in failed ({status}).',\n});\n","/** What SignIn says, in the public pages 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 google: 'Google',\n anAccountAlreadyUsesThisEmail: 'An account already uses this email address. Sign in instead.',\n enterAnEmailAddress: 'Enter an email address, like name@example.com.',\n useAtLeastCharacters: 'Use at least {min} characters.',\n thatLinkHasExpired: 'That link has expired or was already used. Ask for a new one below.',\n yourPasswordIsChanged: 'Your password is changed. Sign in with the new one.',\n enterYourEmailAddress: 'Enter your email address.',\n chooseAPassword: 'Choose a password.',\n enterYourPassword: 'Enter your password.',\n createYourAccount: 'Create your {app} account',\n join: 'Join {app}',\n resetYourPassword: 'Reset your password',\n chooseANewPassword: 'Choose a new password',\n sign: 'Sign in to {app}',\n back: 'Back to {app}',\n yourAccountIsReady: 'Your account is ready. Sign in to continue.',\n yourName: 'Your name',\n youExampleCom: 'you@example.com',\n atLeastCharacters: 'At least {min} characters.',\n hide: 'Hide',\n show: 'Show',\n createAccount: 'Create account',\n forgotPassword: 'Forgot password?',\n ifAnAccountUses: 'If an account uses {sentTo}, a link to choose a new password is on its way. It works once, for an hour.',\n enterTheEmailAddress: 'Enter the email address you sign in with, and we will send a link to choose a new password.',\n sendResetLink: 'Send reset link',\n passwordResetsForAreMade: 'Password resets for {app} are made by its administrator. Ask them for a new password or a reset link.',\n newPassword: 'New password',\n confirmNewPassword: 'Confirm new password',\n changePassword: 'Change password',\n continue: 'Continue with',\n haveAnInvitationCreate: 'Have an invitation? Create your account',\n haveAnAccountSign: 'Have an account? Sign in',\n newHereCreateAnAccount: 'New here? Create an account',\n workEmail: 'Sign in with your work email',\n workEmailLede: 'Enter your work email, and we will send you to your organization to sign in.',\n findMyOrganization: 'Continue',\n noOrganizationSignsIn:\n 'No organization signs in to {app} with {domain} addresses. Use another way to sign in, or ask your administrator.',\n refusedNoRole: 'Your organization signed you in, and gives you no role in {app}. Ask your administrator for access.',\n refusedSignUpClosed: 'There is no {app} account for you yet. Ask your administrator to add you.',\n refusedOutsideDomain: \"That sign-in is for another organization's addresses. Use your own organization's sign-in.\",\n refusedNotLinked:\n 'An account already uses this email, and the sign-in did not confirm the address. Sign in the way you did before, or ask your administrator.',\n refusedOther: 'The sign-in did not finish. Try again, or use another way to sign in.',\n});\n","/**\n * The sign-in screen, for an app that signs people in.\n *\n * The server said 401 with what to draw: the app's name, which providers\n * it takes, whether anyone may sign up, and whether a forgotten password can\n * be reset by a link. Everything here talks to Better Auth's own routes under\n * /api/auth with plain fetch — a JSON body in, a session cookie out — so the\n * client carries no auth library. Wearing the app's dress (its accent, its\n * name), because the person is signing in to the app, not to the tool\n * underneath.\n */\nimport { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react';\nimport { useMutation } from '@tanstack/react-query';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { Button } from '@mistriwork/ui/Button.js';\nimport { Field } from '@mistriwork/ui/Field.js';\nimport { AuthRefusal, post, reload } from './authRequest.js';\nimport { say } from '../copy/public/components/SignIn.js';\nimport { navReplace, navState } from '@mistriwork/kit/client/shellHost.js';\n\n/* Account creation by invitation is an explicit choice, not every sign-in's first paint. */\nconst ClaimInvitation = onDemand(() => import('./ClaimInvitation.js'), 'ClaimInvitation');\n\nexport interface SignInInfo {\n app: string;\n look?: { logo?: string; icons?: Record<string, import('@mistriwork/engine/shared/iconGlyphs.js').IconGlyph> };\n providers: string[];\n /** `provider`: accounts are made at a first sign-in through an identity provider; email sign-up is closed. */\n signUp: 'open' | 'invite' | 'provider';\n /** An identity provider's button words, by how the providers list names it (`oidc:workforce`). */\n labels?: Record<string, string>;\n /** The App id an account-creating invitation is claimed into, when the tenant host offers claims. */\n invitationClaim?: string;\n /** `'link'`: a forgotten password is reset by a one-time link the server\n delivers. Absent: an administrator resets it, and the screen says so. */\n reset?: 'link';\n}\n\n/** Where the signed-out App lives beside this door — its landing or public pages — for the way back. */\nexport interface SignInHome {\n href: string;\n}\n\nconst NAMES: Record<string, string> = { github: 'GitHub', google: say('google') };\n/* The two providers the platform names itself get their mark at the button's start, drawn here and not\n from the App's icon payload: the sign-in screen is the one surface with no App payload yet. GitHub's is\n one path in the text colour, so every look and dark scheme reads it; Google's keeps its four colours, as\n its brand rules ask. An OpenID Connect provider is whatever its config calls it and gets no mark. */\nconst MARKS: Record<string, ReactNode> = {\n github: (\n <path\n fill=\"currentColor\"\n d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\n />\n ),\n google: (\n <>\n <path\n fill=\"#4285F4\"\n d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n />\n </>\n ),\n};\n/** Each tenant's own identity providers, of either kind: found by the domain of the email a person types, never listed. */\nconst TENANT_PROVIDERS = ['oidc:*', 'saml:*'];\n/** The provider id Better Auth knows a listed provider by: `oidc:workforce` is `workforce`, `saml:okta` is `okta`. */\nconst providerId = (provider: string) => (provider.startsWith('oidc:') || provider.startsWith('saml:') ? provider.slice(5) : provider);\n\n/* The codes a refused sign-in through a provider comes back with (auth/identityRoles.ts, and Better Auth's\n own). Only the code is read: the words beside it in the address are anyone's to write. */\nconst REFUSED: Record<string, (app: string) => string> = {\n no_role: (app) => say('refusedNoRole', { app }),\n sign_up_closed: (app) => say('refusedSignUpClosed', { app }),\n signup_disabled: (app) => say('refusedSignUpClosed', { app }),\n outside_domain: () => say('refusedOutsideDomain'),\n account_not_linked: () => say('refusedNotLinked'),\n};\n\n/** A refusal a provider's sign-in came back to this page with, said once and taken out of the address. */\nfunction arrivedRefusal(app: string): string {\n if (typeof window === 'undefined') return '';\n const url = new URL(window.location.href);\n const code = url.searchParams.get('error');\n if (!code || url.searchParams.get('reset') === '1') return '';\n for (const name of ['error', 'error_description']) url.searchParams.delete(name);\n navReplace(`${url.pathname}${url.search}${url.hash}`, navState());\n return (REFUSED[code] ?? (() => say('refusedOther')))(app);\n}\nconst MIN_PASSWORD = 8;\n\ntype Mode = 'in' | 'up' | 'claim' | 'forgot' | 'reset' | 'work';\ntype Problems = Partial<Record<'name' | 'email' | 'password' | 'confirm', string>>;\n\n/* A server refusal that is about one field is said beside that field. The\n rest — a wrong email or password, which must not say which — stays the\n form's one sentence. */\nconst FIELD_OF: Record<string, keyof Problems> = {\n USER_ALREADY_EXISTS: 'email',\n USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: 'email',\n INVALID_EMAIL: 'email',\n PASSWORD_TOO_SHORT: 'password',\n PASSWORD_TOO_LONG: 'password',\n};\nconst SAID_OF: Record<string, string> = {\n USER_ALREADY_EXISTS: say('anAccountAlreadyUsesThisEmail'),\n USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: say('anAccountAlreadyUsesThisEmail'),\n INVALID_EMAIL: say('enterAnEmailAddress'),\n PASSWORD_TOO_SHORT: say('useAtLeastCharacters', { min: MIN_PASSWORD }),\n};\n\nconst looksLikeEmail = (value: string) => /^[^\\s@]+@[^\\s@]+$/.test(value);\n\n/** What a reset link brought back to this page: its token, or that it was refused. */\nfunction arrivedReset(): { token?: string; failed?: true } | null {\n if (typeof window === 'undefined') return null;\n const params = new URLSearchParams(window.location.search);\n if (params.get('reset') !== '1') return null;\n const token = params.get('token');\n if (token) return { token };\n return params.get('error') ? { failed: true } : null;\n}\n\n/** The address a reset link comes back to: this page, marked, so a stray `?token=` elsewhere is never read as one. */\nfunction resetReturn(): string {\n const here = new URL(window.location.href);\n return `${here.origin}${here.pathname}?reset=1`;\n}\n\n/** Take the reset's words out of the address, so a reload or a shared link carries no token. */\nfunction forgetResetAddress(): void {\n const url = new URL(window.location.href);\n for (const name of ['reset', 'token', 'error']) url.searchParams.delete(name);\n navReplace(`${url.pathname}${url.search}${url.hash}`, navState());\n}\n\nexport function SignIn({\n info,\n home,\n start,\n lead,\n}: {\n info: SignInInfo;\n home?: SignInHome;\n /** `'up'`: open on creating an account, where the App lets anyone make one — a saved form's \"Create an account to track this\". */\n start?: 'up';\n /** Why this door is here, said above the form: \"Sign in to add your application to your account.\" */\n lead?: string;\n}) {\n const [arrived] = useState(arrivedReset);\n const [mode, setMode] = useState<Mode>(() =>\n arrived?.token\n ? 'reset'\n : arrived?.failed\n ? 'forgot'\n : start === 'up' && info.signUp === 'open' && info.providers.includes('email')\n ? 'up'\n : 'in',\n );\n const token = useRef(arrived?.token ?? '');\n // The address an invitation claim just made an account for, handed to sign-in.\n const [claimed, setClaimed] = useState<string | null>(null);\n // A sentence the sign-in form opens with: an account made, a password changed.\n const [notice, setNotice] = useState('');\n const first = useRef<HTMLInputElement>(null);\n const [revealed, setRevealed] = useState(false);\n const [problems, setProblems] = useState<Problems>({});\n const [formProblem, setFormProblem] = useState(() => (arrived?.failed ? say('thatLinkHasExpired') : arrivedRefusal(info.app)));\n const [sentTo, setSentTo] = useState('');\n const email = info.providers.includes('email');\n const flips = info.signUp === 'open' && email;\n const claims = email && typeof info.invitationClaim === 'string' && !!info.invitationClaim;\n const fields = {\n name: useRef<HTMLInputElement>(null),\n email: useRef<HTMLInputElement>(null),\n password: useRef<HTMLInputElement>(null),\n confirm: useRef<HTMLInputElement>(null),\n };\n\n useEffect(() => {\n if (arrived) forgetResetAddress();\n }, [arrived]);\n useEffect(() => {\n first.current?.focus();\n }, [mode]);\n\n const go = (next: Mode) => {\n setProblems({});\n setFormProblem('');\n setSentTo('');\n setNotice('');\n setRevealed(false);\n setMode(next);\n };\n\n /** Say what is wrong beside each field, and send the focus to the first. */\n const [fix, setFix] = useState<{ key: keyof Problems } | null>(null);\n useEffect(() => {\n if (fix) fields[fix.key].current?.focus();\n }, [fix]);\n const refuse = (found: Problems) => {\n setProblems(found);\n const at = (['name', 'email', 'password', 'confirm'] as const).find((key) => found[key]);\n // Once drawn, so the field is heard already saying it is invalid (docs/design.md, Access).\n if (at) setFix({ key: at });\n return !at;\n };\n /** A server refusal: beside its field when it names one, the form's sentence otherwise. */\n const refused = (error: unknown) => {\n const code = error instanceof AuthRefusal ? error.code : undefined;\n const field = code ? FIELD_OF[code] : undefined;\n if (field) {\n refuse({ [field]: SAID_OF[code!] ?? (error as Error).message });\n setFormProblem('');\n } else setFormProblem((error as Error).message);\n };\n\n const credentials = useMutation({\n mutationFn: async (form: { name: string; email: string; password: string }) =>\n mode === 'up'\n ? post('/api/auth/sign-up/email', form)\n : post('/api/auth/sign-in/email', { email: form.email, password: form.password, rememberMe: true }),\n onSuccess: reload,\n onError: refused,\n });\n\n const social = useMutation({\n /* A refusal comes back to this page, where it is said (arrivedRefusal). */\n mutationFn: ({ provider, hint }: { provider: string; hint?: string }) =>\n /* A SAML provider's sign-in names it and where to come back to, and nothing else (auth/samlSignIn.ts). */\n provider.startsWith('saml:')\n ? post('/api/auth/sign-in/sso', {\n providerId: providerId(provider),\n callbackURL: window.location.href,\n errorCallbackURL: window.location.href,\n })\n : post('/api/auth/sign-in/social', {\n provider: providerId(provider),\n callbackURL: window.location.href,\n errorCallbackURL: window.location.href,\n ...(hint ? { loginHint: hint } : {}),\n }),\n onSuccess: (data) => {\n if (typeof data.url === 'string') window.location.href = data.url;\n },\n onError: (error) => setFormProblem((error as Error).message),\n });\n\n /* \"Sign in with your work email\": the address's domain names its organization's provider, and the\n sign-in goes there with the address as a hint. */\n const realm = useMutation({\n mutationFn: async (address: string) => ({ address, found: await post('/api/auth/work-email', { email: address }) }),\n onSuccess: ({ address, found }) => {\n // The answer names the provider and its kind; the kind says which sign-in starts there.\n if (typeof found.provider === 'string')\n social.mutate({ provider: `${found.kind === 'saml' ? 'saml' : 'oidc'}:${found.provider}`, hint: address });\n else setFormProblem(say('noOrganizationSignsIn', { app: info.app, domain: address.slice(address.lastIndexOf('@') + 1) }));\n },\n onError: refused,\n });\n\n const recovery = useMutation({\n mutationFn: (address: string) => post('/api/auth/request-password-reset', { email: address, redirectTo: resetReturn() }),\n onSuccess: (_data, address) => setSentTo(address),\n onError: refused,\n });\n\n const choosing = useMutation({\n mutationFn: (password: string) => post('/api/auth/reset-password', { newPassword: password, token: token.current }),\n onSuccess: () => {\n token.current = '';\n go('in');\n setNotice(say('yourPasswordIsChanged'));\n },\n onError: (error) => {\n // A spent or expired token cannot be tried again: ask for a new link.\n if (error instanceof AuthRefusal && error.code === 'INVALID_TOKEN') {\n token.current = '';\n go('forgot');\n setFormProblem(say('thatLinkHasExpired'));\n } else refused(error);\n },\n });\n\n const submit = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (credentials.isPending) return;\n const form = new FormData(e.currentTarget);\n const values = {\n name: String(form.get('name') ?? '').trim(),\n email: String(form.get('email') ?? '').trim(),\n password: String(form.get('password') ?? ''),\n };\n setFormProblem('');\n const found: Problems = {};\n if (mode === 'up' && !values.name) found.name = 'Enter your name.';\n if (!values.email) found.email = say('enterYourEmailAddress');\n else if (!looksLikeEmail(values.email)) found.email = say('enterAnEmailAddress');\n if (!values.password) found.password = mode === 'up' ? say('chooseAPassword') : say('enterYourPassword');\n else if (mode === 'up' && values.password.length < MIN_PASSWORD) found.password = say('useAtLeastCharacters', { min: MIN_PASSWORD });\n if (!refuse(found)) return;\n credentials.mutate(mode === 'up' ? values : { ...values, name: '' });\n };\n\n const work = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (realm.isPending || social.isPending) return;\n const address = String(new FormData(e.currentTarget).get('email') ?? '').trim();\n setFormProblem('');\n if (!refuse(!address ? { email: say('enterYourEmailAddress') } : !looksLikeEmail(address) ? { email: say('enterAnEmailAddress') } : {}))\n return;\n realm.mutate(address);\n };\n\n const ask = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (recovery.isPending) return;\n const address = String(new FormData(e.currentTarget).get('email') ?? '').trim();\n setFormProblem('');\n if (!refuse(!address ? { email: say('enterYourEmailAddress') } : !looksLikeEmail(address) ? { email: say('enterAnEmailAddress') } : {}))\n return;\n recovery.mutate(address);\n };\n\n const choose = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n if (choosing.isPending) return;\n const form = new FormData(e.currentTarget);\n const password = String(form.get('password') ?? ''),\n confirm = String(form.get('confirm') ?? '');\n setFormProblem('');\n if (\n !refuse(\n password.length < MIN_PASSWORD\n ? { password: say('useAtLeastCharacters', { min: MIN_PASSWORD }) }\n : password !== confirm\n ? { confirm: say('common.thePasswordsDoNotMatch') }\n : {},\n )\n )\n return;\n choosing.mutate(password);\n };\n\n /** A field's control, joined to the field's own ref and the one that takes the focus first. */\n const bind = (key: keyof Problems, focusFirst: boolean) => (node: HTMLInputElement | null) => {\n fields[key].current = node;\n if (focusFirst) first.current = node;\n };\n const clear = (key: keyof Problems) => () => {\n if (problems[key]) setProblems((before) => ({ ...before, [key]: undefined }));\n };\n\n const title =\n mode === 'up'\n ? say('createYourAccount', { app: info.app })\n : mode === 'claim'\n ? say('join', { app: info.app })\n : mode === 'forgot'\n ? say('resetYourPassword')\n : mode === 'reset'\n ? say('chooseANewPassword')\n : mode === 'work'\n ? say('workEmail')\n : say('sign', { app: info.app });\n const pending = credentials.isPending || recovery.isPending || choosing.isPending || realm.isPending;\n const listed = info.providers.filter((provider) => provider !== 'email' && !TENANT_PROVIDERS.includes(provider));\n const organizations = info.providers.some((provider) => TENANT_PROVIDERS.includes(provider));\n\n return (\n <div className=\"signin\">\n {/* The way back to the App's public pages, when it has them: the door is not a dead end. */}\n {home && (\n <a className=\"signin-home\" href={home.href} aria-label={say('back', { app: info.app })}>\n <span aria-hidden=\"true\">←</span> {info.app}\n </a>\n )}\n <div className=\"signin-card\">\n {/* The App's name is in the bar above; the heading says what this screen is for, in it. */}\n <h1 className=\"signin-title\">{title}</h1>\n {mode === 'claim' && claims && (\n <ClaimInvitation\n appId={info.invitationClaim!}\n onClaimed={(address) => {\n setClaimed(address);\n go('in');\n setNotice(say('yourAccountIsReady'));\n }}\n onCancel={() => go('in')}\n />\n )}\n {notice && mode === 'in' && (\n <p className=\"signin-notice\" role=\"status\">\n {notice}\n </p>\n )}\n {lead && !notice && (mode === 'in' || mode === 'up') && <p className=\"signin-notice\">{lead}</p>}\n {email && (mode === 'in' || mode === 'up') && (\n <form className=\"signin-form\" onSubmit={submit} noValidate key={claimed ?? ''}>\n {mode === 'up' && (\n <Field label={say('common.name')} problem={problems.name}>\n <input\n ref={bind('name', true)}\n type=\"text\"\n name=\"name\"\n className=\"field-control signin-input\"\n autoComplete=\"name\"\n required\n placeholder={say('yourName')}\n onInput={clear('name')}\n />\n </Field>\n )}\n <Field label={say('common.email')} problem={problems.email}>\n <input\n ref={bind('email', mode !== 'up')}\n type=\"email\"\n name=\"email\"\n className=\"field-control signin-input\"\n autoComplete=\"email\"\n required\n placeholder={say('youExampleCom')}\n defaultValue={mode === 'in' ? (claimed ?? undefined) : undefined}\n onInput={clear('email')}\n />\n </Field>\n <Field\n label={say('common.password')}\n id=\"signin-password\"\n problem={problems.password}\n hint={mode === 'up' ? say('atLeastCharacters', { min: MIN_PASSWORD }) : undefined}\n >\n {(control) => (\n <span className=\"signin-secret\">\n <input\n {...control}\n ref={bind('password', false)}\n type={revealed ? 'text' : 'password'}\n name=\"password\"\n className=\"field-control signin-input\"\n autoComplete={mode === 'up' ? 'new-password' : 'current-password'}\n required\n onInput={clear('password')}\n />\n {/* Named \"Show\"/\"Hide\", not \"Show password\": the field's own name stays the only \"Password\" on the page.\n Beside the field, not drawn over its right end: two controls in one box, one tapped for the other. */}\n <Button\n intent=\"quiet\"\n className=\"signin-reveal\"\n aria-pressed={revealed}\n aria-controls=\"signin-password\"\n onClick={() => setRevealed((on) => !on)}\n >\n {revealed ? say('hide') : say('show')}\n </Button>\n </span>\n )}\n </Field>\n {/* The refusal stands with the fields it is about, above the button that was just pressed, in a line held\n even while empty: below the links it arrived after the button and pushed \"Create an account\" down. */}\n <p className=\"signin-problem signin-problem-held\" role=\"alert\">\n {formProblem && !credentials.isPending ? formProblem : null}\n </p>\n {/* Busy, not disabled: a disabled button drops the focus it holds, and a\n refused sign-in left the reader on the page itself. submit refuses a second press. */}\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" className=\"signin-submit\" busy={credentials.isPending}>\n {mode === 'up' ? say('createAccount') : say('common.signIn')}\n </Button>\n {mode === 'in' && (\n <Button intent=\"quiet\" className=\"signin-forgot\" onClick={() => go('forgot')}>\n {say('forgotPassword')}\n </Button>\n )}\n </form>\n )}\n {mode === 'forgot' &&\n (info.reset === 'link' ? (\n sentTo ? (\n <p className=\"signin-notice\" role=\"status\">\n {say('ifAnAccountUses', { sentTo })}\n </p>\n ) : (\n <form className=\"signin-form\" onSubmit={ask} noValidate>\n <p className=\"signin-lede\">{say('enterTheEmailAddress')}</p>\n <Field label={say('common.email')} problem={problems.email}>\n <input\n ref={bind('email', true)}\n type=\"email\"\n name=\"email\"\n className=\"field-control signin-input\"\n autoComplete=\"email\"\n required\n placeholder={say('youExampleCom')}\n onInput={clear('email')}\n />\n </Field>\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" busy={recovery.isPending}>\n {say('sendResetLink')}\n </Button>\n </form>\n )\n ) : (\n <p className=\"signin-lede\">{say('passwordResetsForAreMade', { app: info.app })}</p>\n ))}\n {mode === 'reset' && (\n <form className=\"signin-form\" onSubmit={choose} noValidate>\n <Field label={say('newPassword')} problem={problems.password} hint={say('atLeastCharacters', { min: MIN_PASSWORD })}>\n <input\n ref={bind('password', true)}\n type=\"password\"\n name=\"password\"\n className=\"field-control signin-input\"\n autoComplete=\"new-password\"\n required\n onInput={clear('password')}\n />\n </Field>\n <Field label={say('confirmNewPassword')} problem={problems.confirm}>\n <input\n ref={bind('confirm', false)}\n type=\"password\"\n name=\"confirm\"\n className=\"field-control signin-input\"\n autoComplete=\"new-password\"\n required\n onInput={clear('confirm')}\n />\n </Field>\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" busy={choosing.isPending}>\n {say('changePassword')}\n </Button>\n </form>\n )}\n {mode === 'work' && (\n <form className=\"signin-form\" onSubmit={work} noValidate>\n <p className=\"signin-lede\">{say('workEmailLede')}</p>\n <Field label={say('common.email')} problem={problems.email}>\n <input\n ref={bind('email', true)}\n type=\"email\"\n name=\"email\"\n className=\"field-control signin-input\"\n autoComplete=\"email\"\n required\n placeholder={say('youExampleCom')}\n onInput={clear('email')}\n />\n </Field>\n <Button intent=\"primary\" size=\"large\" block type=\"submit\" busy={realm.isPending || social.isPending}>\n {say('findMyOrganization')}\n </Button>\n </form>\n )}\n {(mode === 'in' || mode === 'up') && (listed.length > 0 || organizations) && (\n <div className=\"signin-social-row\">\n {listed.map((provider) => (\n <Button\n key={provider}\n intent=\"secondary\"\n size=\"large\"\n block\n className=\"signin-social\"\n busy={social.isPending}\n onClick={() => social.mutate({ provider })}\n >\n {MARKS[provider] && (\n <svg className=\"signin-mark\" viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n {MARKS[provider]}\n </svg>\n )}\n {say('continue')} {info.labels?.[provider] ?? NAMES[provider] ?? providerId(provider)}\n </Button>\n ))}\n {organizations && (\n <Button intent=\"secondary\" size=\"large\" block className=\"signin-social\" onClick={() => go('work')}>\n {say('workEmail')}\n </Button>\n )}\n </div>\n )}\n {mode !== 'claim' && !(email && (mode === 'in' || mode === 'up')) && (\n <p className=\"signin-problem\" role=\"alert\" hidden={!formProblem || pending}>\n {formProblem}\n </p>\n )}\n {(mode === 'forgot' || mode === 'reset' || mode === 'work') && (\n <Button intent=\"quiet\" className=\"signin-flip\" onClick={() => go('in')}>\n {say('common.backToSign')}\n </Button>\n )}\n {claims && (mode === 'in' || mode === 'up') && (\n <Button\n intent=\"quiet\"\n className=\"signin-flip\"\n onClick={() => {\n setClaimed(null);\n go('claim');\n }}\n >\n {say('haveAnInvitationCreate')}\n </Button>\n )}\n {mode !== 'in' && mode !== 'up' ? null : flips ? (\n <Button intent=\"quiet\" className=\"signin-flip\" onClick={() => go(mode === 'up' ? 'in' : 'up')}>\n {mode === 'up' ? say('haveAnAccountSign') : say('newHereCreateAnAccount')}\n </Button>\n ) : null}\n </div>\n </div>\n );\n}\n","/** What signin says, in the public pages 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 signInApp: 'Sign in · {app}',\n});\n","/** What publicForm says, in the public pages 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 thereIsNoSuch: 'There is no such form here.',\n theFormCouldNotBeLoaded: 'The form could not be loaded ({status}).',\n checkYourApplicationName: 'Check your application · {name}',\n signInToAddYourApplication: 'Sign in to add your application to your account.',\n addYourApplication: 'Add your application · {name}',\n signInToAddTheApplication: 'Sign in to add the application you just submitted to your account.',\n});\n","/**\n * A public form, as the vanilla client reaches it: the address, the one\n * read, and the crossing into React. Boot asks `publicFormRoute` (publicRoutes.js) before it\n * asks who-am-I, because the answer decides whether there is a session to\n * look for at all — a public form is open to everyone, so an applicant is\n * never sent to sign in, and someone signed in submits it as the public too.\n */\nimport { createElement } from 'react';\nimport { applyBrandLogo, headBrandingMark } from './appBrand.js';\nimport { $ } from '@mistriwork/kit/client/core.js';\nimport { isMounted, mount } from '@mistriwork/kit/client/components/mount.js';\nimport { takeServerDrawing } from './serverDrawing.js';\nimport { FormSurface, formArrives, submitPublicForm } from './components/FormSurface.js';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { showSignIn } from './signin.js';\nimport { say } from './copy/public/publicForm.js';\nimport { served, shown } from '@mistriwork/kit/client/addressMount.js';\nimport { firstPaintRead } from '@mistriwork/kit/client/firstPaint.js';\nimport { setTabTitle, shellBody, shellQuery } from '@mistriwork/kit/client/shellHost.js';\nimport { fetchServer } from '@mistriwork/kit/client/transport.js';\n\n/* Fetched when one is shown: FormDialog brings this module into the App\n shell, whose first paint no saved form is ever on. */\nconst SavedFormSurface = onDemand(() => import('./components/SavedFormSurface.js'), 'SavedFormSurface');\n/* A saved form's status check and account screen, each its own page under the form's address. */\nconst FormStatusSurface = onDemand(() => import('./components/FormStatusSurface.js'), 'FormStatusSurface');\nconst FormClaimSurface = onDemand(() => import('./components/FormClaimSurface.js'), 'FormClaimSurface');\nconst PlacedFormSurface = onDemand(() => import('./components/PlacedFormSurface.js'), 'PlacedFormSurface');\n/** @type {typeof import('./publicPlacement.js') | undefined} */\nlet placements;\n\n/**\n * The App's own sign-in, drawn where a saved form's page stands; once there\n * is a session the page reloads to itself (SignIn.tsx), so the check or the\n * question it was on comes back signed in. The door's shape is who-am-I's.\n * @param {string} back the form's own address, the door's way back\n * @param {{ start?: 'up'; lead?: string }} opening\n */\nasync function signInHere(back, opening) {\n const res = await fetchServer('/api/whoami', { credentials: 'same-origin', signal: AbortSignal.timeout(15_000) });\n const me = await res.json();\n if (!me?.signIn) return;\n showSignIn(me.signIn, { href: back }, opening);\n}\n\n/**\n * The staff door, on a form served as the App's home: a quiet Sign in at the\n * top bar's end, as a public App's visitor has one (app.js, showAccount).\n * The App's root is the form there, so without it staff had to know to type\n * `/{app}/sign-in`. Not on the form's own address, `/{app}/form/{key}`: that\n * is a link handed to applicants, drawn before anyone is asked who they are,\n * and its saved flows offer their own sign-in where one is wanted.\n * @param {string} appId\n */\nfunction offerSignIn(appId) {\n const bar = shellQuery('.topbar');\n if (!bar || bar.querySelector('.public-sign-in')) return;\n const link = document.createElement('a');\n link.className = 'ghost account-sign-in public-sign-in';\n link.href = shown(`/${encodeURIComponent(appId)}/sign-in`);\n link.textContent = say('common.signIn');\n bar.append(link);\n}\n\n/**\n * Fetch one public form's presentation. `undefined` when the server has no\n * public forms at all, so this address is not one and boot carries on; a\n * throw, with the sentence to show, for a form this App does not serve to\n * everyone — a mistyped key, or a signed-in form's — or a read that failed.\n * Never a sign-in prompt.\n * @param {string} id\n * @param {{ appId?: string; fresh?: boolean }} [options]\n * @returns {Promise<{ app: { id: string; name: string; logo?: import('@mistriwork/engine/shared/appIcons.js').AppIconName; icons?: Record<string, import('@mistriwork/engine/shared/iconGlyphs.js').IconGlyph>; theme?: import('../shared/app.js').AppTheme; customThemes?: import('@mistriwork/engine/shared/theme.js').ThemeDefinition[] }; form: import('./components/FormSurface.js').FormPresentation; placement?: import('../shared/publicPlacement.js').PublicFormPlacement } | undefined>}\n */\nexport async function loadPublicForm(id, options = {}) {\n const retained = placements?.retainedPublicForm(id, options.appId);\n if (retained) return retained;\n /* The page arrived with it (firstPaint.ts): a form's page asks nothing. */\n const carried = options.fresh ? undefined : firstPaintRead('GET', `/api/forms/${encodeURIComponent(id)}`)?.body;\n if (carried && typeof carried === 'object' && 'form' in carried && !('placement' in carried)) {\n // What the form asks beyond its fields — the stepper and its sheets, the draft's logic, live lines — comes with\n // it, so its first frame is whole, and a form that asks none of it fetches nothing (FormSurface.tsx formArrives).\n await formArrives(/** @type {any} */ (carried).form);\n return /** @type {any} */ (carried);\n }\n /* A tenant's link carries its intake address (`?via=…`): the read carries it on, so a choice from a table is\n offered that tenant's rows (server tenant/tenantPublicChoices.ts). */\n const via = new URLSearchParams(window.location.search).get('via');\n const res = await fetchServer(`/api/forms/${encodeURIComponent(id)}${via ? `?${new URLSearchParams({ via })}` : ''}`, {\n credentials: 'omit',\n cache: 'no-store',\n signal: AbortSignal.timeout(15_000),\n });\n /** @type {Record<string, any>} */\n let data = {};\n try {\n data = await res.json();\n } catch {\n /* the status is the answer */\n }\n if (res.ok && data && typeof data === 'object' && 'form' in data) {\n if (data.form?.id !== id || (options.appId && data.app?.id !== options.appId)) throw new Error(say('thereIsNoSuch'));\n if ('placement' in data) {\n placements ??= await import('./publicPlacement.js');\n return placements.preparePublicPlacement(/** @type {any} */ (data));\n }\n if (data.form) await formArrives(data.form);\n return /** @type {any} */ (data);\n }\n if (res.status === 404 && data?.code !== 'no_such_form') return undefined;\n throw new Error(\n res.status === 404 || res.status === 401\n ? say('thereIsNoSuch')\n : String(data?.error || say('theFormCouldNotBeLoaded', { status: res.status })),\n );\n}\n\n/**\n * Draw the public form into the content pane, in the App's name. The shell\n * chrome is hidden by class; the brand shows the App, not the tool.\n * @param {NonNullable<Awaited<ReturnType<typeof loadPublicForm>>>} prepared\n * @param {'status' | 'claim'} [page] a saved form's status check or account screen, under its address\n * @param {{ signIn?: boolean }} [options] `signIn`: the form is the App's home, to someone signed out of an App that signs people in\n * @returns {boolean} whether the address is one this form serves\n */\nexport function showPublicForm(prepared, page, options = {}) {\n const { app, form } = prepared;\n /** @type {any} */\n const saved = form;\n if (page === 'status' && !saved.save?.statusCheck) return false;\n if (page === 'claim' && !saved.save?.link?.sameDevice) return false;\n shellBody().classList.add('app-mode', 'public-form-mode');\n const brand = shellQuery('.brand-name');\n if (brand) brand.textContent = app.name;\n applyBrandLogo(\n shellQuery('.brand-mark'),\n /** @type {any} */ (app).theme?.branding?.mark ?? headBrandingMark() ?? app.logo,\n app.name,\n app.icons,\n );\n // An intake form's page has no heading: its label is the tab's title (docs/design.md).\n setTabTitle(form.style === 'intake' ? form.label : `${form.label} · ${app.name}`);\n const home = $('home-button');\n if (home instanceof HTMLButtonElement) {\n home.disabled = true;\n home.removeAttribute('title');\n }\n if (options.signIn) offerSignIn(app.id);\n const host = $('content');\n if (!isMounted(host) && !(host.getAttribute('data-ssr') === 'form' && !page && !prepared.placement && !(form.save && form.steps)))\n host.replaceChildren();\n /* A tenant publishes its form with its intake address, `?via=…`; the\n submission carries it on, as it was, for the server to resolve. Nothing\n on this page reads or depends on it. */\n const via = new URLSearchParams(window.location.search).get('via') ?? undefined;\n // As the browser shows it (addressMount.js): every use is a link.\n const base = shown(`/${encodeURIComponent(app.id)}/form/${encodeURIComponent(form.id)}`);\n if (prepared.placement) {\n if (page) return false;\n mount(host, createElement(PlacedFormSurface, { prepared, review: Boolean(form.steps?.length) }));\n return true;\n }\n if (page === 'status') {\n setTabTitle(say('checkYourApplicationName', { name: app.name }));\n mount(\n host,\n createElement(FormStatusSurface, {\n form: saved,\n appName: app.name,\n onSignIn: () => void signInHere(`${base}/status`, { lead: say('signInToAddYourApplication') }),\n }),\n );\n return true;\n }\n if (page === 'claim') {\n setTabTitle(say('addYourApplication', { name: app.name }));\n const start = new URLSearchParams(window.location.search).get('account') === 'new' ? 'up' : undefined;\n mount(\n host,\n createElement(FormClaimSurface, {\n form,\n appName: app.name,\n ...(saved.save?.statusCheck ? { statusHref: `${base}/status` } : {}),\n onSignIn: () => void signInHere(base, { ...(start ? { start } : {}), lead: say('signInToAddTheApplication') }),\n }),\n );\n return true;\n }\n // A form saved on every step draws its own stepper, review and leave guard.\n if (form.save && form.steps) {\n mount(\n host,\n createElement(SavedFormSurface, {\n form: /** @type {import('./components/SavedFormSurface.js').SavedFormPresentation} */ (form),\n appName: app.name,\n base,\n }),\n );\n return true;\n }\n // With steps, the last one reviews every answer from this page's memory before the one submit.\n mount(\n host,\n createElement(FormSurface, {\n form,\n appName: app.name,\n review: Boolean(form.steps?.length),\n // A reference input's choices: what the App opens to a signed-out visitor, read as one.\n visitor: { app: app.id, ...(via ? { via } : {}) },\n submit: (submission, input, guard) => submitPublicForm(form.id, submission, input, via, guard),\n // Once received, a quiet way back to the App's home; none where this form is that home (the way back is itself).\n ...(options.signIn ? {} : { homeHref: shown(`/${encodeURIComponent(app.id)}`) }),\n }),\n // The server drew the form as it opens (server/publicRender.ts): taken over where it stands.\n takeServerDrawing(host, 'form'),\n );\n return true;\n}\n","/** What FormDialog says, in the forms 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 form: 'Form',\n thereIsNoSuch: 'There is no such form here.',\n});\n","/**\n * A public form over the record that sent you to it.\n *\n * A call to action on a headline — \"Inquire about this car\", \"Schedule a test\n * drive\" — used to navigate to the form's own page, which takes away the very\n * thing the form is about: the price, the photograph, the mileage. Opened\n * here, the record stays behind the dialog and the reader can look again\n * before they send.\n *\n * The form is the same one the page draws (FormSurface), loaded from the same\n * public endpoint, submitted the same way. What differs is where it sits and\n * how it starts: the action's `prefill` and `defaults` arrive as a query\n * string, exactly as they would in the address, so the two routes to the form\n * cannot drift apart.\n */\nimport { useQuery } from '@tanstack/react-query';\nimport { useId, useMemo } from 'react';\nimport { loadPublicForm } from '../publicForm.js';\nimport { onDemand } from '@mistriwork/kit/client/components/onDemand.js';\nimport { served } from '@mistriwork/kit/client/addressMount.js';\nimport { FormSurface, submitPublicForm } from './FormSurface.js';\nimport { Modal } from '@mistriwork/ui/Modal.js';\nimport './FormDialog.css';\nimport { say } from '../copy/forms/components/FormDialog.js';\n\nconst PlacedFormSurface = onDemand(() => import('./PlacedFormSurface.js'), 'PlacedFormSurface');\n\nexport function FormDialog({\n view,\n appId,\n prefill,\n onClose,\n}: {\n view: string;\n appId?: string;\n /** The action's starting values as a query string (\"stock_ref=ABC&message=…\"). */\n prefill: string;\n onClose: () => void;\n}) {\n const instance = useId();\n // Invalidation may ask again, but this opening owns one preparation. An\n // unanswered send is retained by the placed owner beyond this opening.\n const prepare = useMemo(() => {\n let answer: ReturnType<typeof loadPublicForm> | undefined;\n return () => (answer ??= loadPublicForm(view, { appId, fresh: true }));\n }, [appId, view]);\n const asked = useQuery({\n queryKey: ['public-form', window.location.origin, served('/'), appId, view, instance],\n staleTime: Infinity,\n gcTime: 0,\n retry: false,\n queryFn: async () => (await prepare()) ?? null,\n });\n const form = asked.data?.form;\n /* The App's name comes with the form, the way it does for the form's own\n page — reading it from the nav store instead would make this module part\n of a cycle (test/repo/runtime-cycles). */\n const appName = asked.data?.app.name ?? '';\n /* A tenant publishes its form with its intake address, `?via=…`; a\n submission carries it on, as it was, for the server to resolve — the same\n rule the form's own page follows (publicForm.js). */\n const via = new URLSearchParams(window.location.search).get('via') ?? undefined;\n return (\n <Modal className=\"form-dialog\" onClose={onClose} ariaLabel={form?.label ?? say('form')}>\n {asked.isPending && <p className=\"form-dialog-waiting\">{say('common.loading')}</p>}\n {asked.isError && <p className=\"form-dialog-failed\">{(asked.error as Error).message}</p>}\n {asked.isSuccess && !form && <p className=\"form-dialog-failed\">{say('thereIsNoSuch')}</p>}\n {form &&\n (asked.data?.placement ? (\n <PlacedFormSurface key={instance + appId + view} prepared={asked.data} prefill={prefill} onDismiss={onClose} />\n ) : (\n <FormSurface\n key={instance + appId + view}\n form={form}\n appName={appName}\n prefill={prefill}\n inDialog\n // A reference input's choices: what the App opens to a signed-out visitor, read as one (publicForm.js).\n visitor={{ app: asked.data!.app.id, ...(via ? { via } : {}) }}\n submit={(submission, input, guard) => submitPublicForm(form.id, submission, input, via, guard)}\n />\n ))}\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { D as subscribeSessionIdentity } from "./shared-poc11xxo.js";
|
|
2
2
|
import { gt as defineMessages, rn as shell } from "./shared-DJZjSi3J.js";
|
|
3
|
-
import { L as state, d as findTable, h as loadJson, i as api, ot as appRequestScope, st as ownedBy, ut as connectionGeneration } from "./shared-
|
|
3
|
+
import { L as state, d as findTable, h as loadJson, i as api, ot as appRequestScope, st as ownedBy, ut as connectionGeneration } from "./shared-BoLbc2WD.js";
|
|
4
4
|
import { u as resourceOf } from "./shared-DpVb48sM.js";
|
|
5
|
-
import { d as whenBody } from "./shared-
|
|
5
|
+
import { d as whenBody } from "./shared-CZJE4Bmp.js";
|
|
6
6
|
defineMessages({
|
|
7
7
|
theServerDidNotConfirm: "The server did not confirm the change within 30 seconds. It may still complete.",
|
|
8
8
|
noDatabaseConnectionIsSelected: "No database connection is selected."
|
|
@@ -21,7 +21,11 @@ function mirrorFor(connection) {
|
|
|
21
21
|
if (!mirrors.has(connection)) mirrors.set(connection, loadJson(storeKey(connection), {}));
|
|
22
22
|
return mirrors.get(connection);
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Pages saved for the connection in hand, read from the local mirror.
|
|
26
|
+
*
|
|
27
|
+
* @param {*} [tableId] only the pages based on this table
|
|
28
|
+
*/
|
|
25
29
|
function savedPages(tableId) {
|
|
26
30
|
const all = mirrorFor(state.activeConnection);
|
|
27
31
|
const pages = Object.values(all);
|
|
@@ -246,4 +250,4 @@ var DATE_UNITS = [
|
|
|
246
250
|
Object.freeze({ "\0": "undefined" });
|
|
247
251
|
export { pagesFor as a, rootLabel as c, sourceOf as d, pageToken as i, rootMatches as l, MEASURE_FUNCTIONS as n, headerOf as o, pageById as r, pinnedBy as s, DATE_UNITS as t, sectionClass as u };
|
|
248
252
|
|
|
249
|
-
//# sourceMappingURL=shared-
|
|
253
|
+
//# sourceMappingURL=shared-Bf30ZLoS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-Bf30ZLoS.js","names":["say","say","say"],"sources":["../../../kit/src/client/copy/record/savedPages.ts","../../../kit/src/client/savedPages.js","../../../kit/src/client/copy/record/page.ts","../../../kit/src/client/copy/record/components/pageModel.ts","../../../kit/src/client/components/pageModel.ts","../../../kit/src/client/page.js","../../../engine/src/shared/breakdown.ts"],"sourcesContent":["/** What savedPages says, in the record room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n theServerDidNotConfirm: 'The server did not confirm the change within 30 seconds. It may still complete.',\n noDatabaseConnectionIsSelected: 'No database connection is selected.',\n});\n","/**\n * The pages saved for a connection, as a synchronous mirror of the server's\n * file: what the record page's page list reads (page.js) in either shell, and\n * what the Workbench's page builder (pagebuilder.js) writes. An App reads\n * none — its pages are its definition — and a public visitor has none.\n */\nimport { appRequestScope, ownedBy } from './requestIdentity.js';\nimport { api, loadJson, saveJson, state } from './core.js';\nimport { sessionIdentity, subscribeSessionIdentity } from './sessionIdentity.js';\nimport { shell } from './shellMode.js';\nimport { say } from './copy/record/savedPages.js';\n\nconst KEY = 'mistri.pages.saved';\n\n/**\n * Told when a page is saved or deleted, so the record-page cache and the menu\n * can be rebuilt. A callback rather than an import: page.js reads the saved\n * pages from here, and an import back made a cycle. page.js registers itself.\n * @type {(() => void) | null}\n */\nlet pagesChanged = null;\n/** @param {() => void} fn */\nexport function whenSavedPagesChange(fn) {\n pagesChanged = fn;\n}\n\nconst storeKey = (connection) => `${KEY}.${connection ?? ''}`;\n/** @type {Map<string, Record<string, import('@mistriwork/engine/shared/page.js').PageDef>>} */\nconst mirrors = new Map();\nconst loads = new Map();\n\n/* Another person's pages are not this one's: a change of who is signed in forgets the mirror and any read in flight. */\nsubscribeSessionIdentity(() => {\n mirrors.clear();\n loads.clear();\n});\n\nfunction mirrorFor(connection) {\n if (!mirrors.has(connection)) mirrors.set(connection, loadJson(storeKey(connection), {}));\n return mirrors.get(connection);\n}\n\nfunction updateMirror(connection, pages) {\n mirrors.set(connection, pages);\n // Disk acknowledgement is authoritative; unavailable browser storage is only a cache miss.\n saveJson(storeKey(connection), pages);\n}\n\nasync function writePage(id, page, connection) {\n let timer;\n try {\n await Promise.race([\n api('/api/pages/save', { id, page, ...ownedBy(connection) }),\n new Promise((_resolve, reject) => {\n timer = setTimeout(() => reject(new Error(say('theServerDidNotConfirm'))), 30_000);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Pages saved for the connection in hand, read from the local mirror.\n *\n * @param {*} [tableId] only the pages based on this table\n */\nexport function savedPages(tableId) {\n const all = mirrorFor(state.activeConnection);\n const pages = Object.values(all);\n return tableId ? pages.filter((p) => p.base === tableId) : pages;\n}\n\n/**\n * Pull this connection's saved pages from the server into the mirror.\n *\n * The same arrangement record layouts have, for the same reason: the mirror\n * is what makes reads synchronous, the file is what makes the work outlive\n * this browser. A failure is silent — the mirror already holds whatever this\n * browser saved.\n */\nexport async function loadSavedPages() {\n // A public visitor has none to read (the shell's publicVisitor), and saved pages are\n // the database browser's: an App's pages are its definition.\n if (shell().publicVisitor() || appRequestScope() !== false) return;\n const owner = sessionIdentity();\n const connection = state.activeConnection;\n const before = mirrorFor(connection);\n const load = Symbol();\n loads.set(connection, load);\n try {\n const { pages } = await api('/api/pages/saved');\n // A late read must not undo an acknowledged write or a newer read.\n if (\n owner === sessionIdentity() &&\n pages &&\n typeof pages === 'object' &&\n !Array.isArray(pages) &&\n mirrors.get(connection) === before &&\n loads.get(connection) === load\n )\n updateMirror(connection, pages);\n } catch {\n /* Server-side pages unavailable; the mirror stands. */\n }\n}\n\n/**\n * @param {*} page\n * @param {string | null} [connection] the connection written to; the one in hand, unless given\n */\nexport async function savePage(page, connection = state.activeConnection) {\n const owner = sessionIdentity();\n if (!connection) throw new Error(say('noDatabaseConnectionIsSelected'));\n const saved = structuredClone(page);\n await writePage(saved.id, saved, connection);\n if (owner !== sessionIdentity()) return;\n updateMirror(connection, { ...mirrorFor(connection), [saved.id]: saved });\n /* Also tells the menu to rebuild: it was registered once at boot, so a page\n built afterwards was reachable only from the link you were already on,\n and a deleted one stayed listed as a command that opened nothing. */\n if (state.activeConnection === connection) pagesChanged?.();\n}\n\n/**\n * @param {*} id\n * @param {string | null} [connection] the connection written to; the one in hand, unless given\n */\nexport async function deletePage(id, connection = state.activeConnection) {\n const owner = sessionIdentity();\n if (!connection) throw new Error(say('noDatabaseConnectionIsSelected'));\n await writePage(id, null, connection);\n if (owner !== sessionIdentity()) return;\n const all = { ...mirrorFor(connection) };\n delete all[id];\n updateMirror(connection, all);\n if (state.activeConnection === connection) pagesChanged?.();\n}\n","/** What page says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../messages.js';\n\nexport const say = defineMessages({\n theConnectionOrPage: 'The connection or page definitions changed while loading.',\n thereIsNoPage: 'There is no page called \"{id}\".',\n});\n","/** What pageModel says, in the record page room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n savedOnThisServer: 'saved on this server, for this connection',\n fromTheAppOrConnection: 'from the app or connection configuration',\n builtFromTheSchema: 'built from the schema',\n});\n","/**\n * What a page is made of, before anything is drawn: the shapes the server\n * hands back, and the small rules the page and its sections share.\n */\nimport { ownedBy } from '../requestIdentity.js';\nimport { api } from '../core.js';\nimport { whenBody } from '@mistriwork/engine/shared/recordReads.js';\nimport type { TableMeta } from '@mistriwork/engine/shared/schema.js';\nimport type { FigureFormat } from '@mistriwork/engine/shared/model.js';\nimport type { RelatedCreate } from '@mistriwork/engine/schema/model.js';\nimport type { ResolvedWorkflowStatus } from '@mistriwork/engine/shared/page.js';\nimport type { HttpViewDefinition, HttpBreakdownDefinition } from '@mistriwork/engine/shared/resourceHttp.js';\nimport { say } from '../copy/record/components/pageModel.js';\n\nexport interface PageSection {\n sizing?: import('@mistriwork/engine/shared/tableSizing.js').TableSizing;\n kind: import('@mistriwork/engine/shared/pageModel.js').PageSection['kind'];\n /** A chart's shape, height and goal, as a dashboard chart's. */\n shape?: 'line' | 'bar' | 'donut' | 'stacked';\n /** A bar chart's or donut's category order (shared/pageModel.ts, PageChart.sort). */\n sort?: string;\n height?: number;\n target?: number;\n /** A note's words, mark and one way on. */\n text?: string;\n icon?: import('@mistriwork/engine/shared/appIcons.js').AppIconName;\n action?: import('@mistriwork/engine/shared/appLinks.js').AppLinkAction;\n /** An embed: the key of the list view this related list draws. */\n view?: string;\n /** A custom section: the author's component, the record's fields it reads, its named reads and props. */\n component?: import('@mistriwork/engine/shared/appComponents.js').ComponentDecl;\n record?: string[];\n reads?: Record<string, import('@mistriwork/engine/shared/appComponents.js').PageCustomRead>;\n aggregates?: Record<string, import('@mistriwork/engine/shared/appComponents.js').PageCustomTotal>;\n props?: Record<string, unknown>;\n region?: 'main' | 'aside';\n /** The section strip's tab this section is drawn in. */\n tab?: string;\n title?: string;\n /** The tab and title in the source catalog's words, where the reader's differ (server apps.ts keepAddresses): what an address calls its tab. */\n addressWords?: { tab?: string; title?: string };\n from?: string;\n of?: string;\n path?: string;\n by?: string;\n /** A timeline's streams, as the page model keeps them. */\n streams?: Array<{\n from: string;\n path: string;\n at: string;\n show: string;\n detail?: string;\n by?: string;\n filter?: string;\n side?: string;\n /** Every row of the stream in this tone, and drawn as one line: in place of the section's rules. */\n tone?: import('@mistriwork/engine/shared/pageModel.js').TimelineTone;\n compact?: true;\n }>;\n /** A timeline read as a conversation: oldest first, its composer under the rows, rows tinted or drawn as one line by a column's value. */\n newest?: 'last';\n composer?: import('@mistriwork/engine/shared/pageModel.js').TimelineComposer;\n tone?: Record<string, Record<string, import('@mistriwork/engine/shared/pageModel.js').TimelineTone>>;\n compact?: Record<string, string[]>;\n /** The other party's rows, drawn against the far edge: asked by the server as each stream's `side` read (SectionPlan). */\n side?: string;\n filter?: string;\n fn?: string;\n column?: string;\n /** A metric's figure said as a dashboard's: `percent` reads it as 0–100, and a unit word follows it. */\n format?: FigureFormat;\n unit?: string;\n paths?: string[];\n names?: boolean;\n /** A note: quiet small print, or a callout on a ground of its own. */\n variant?: 'quiet' | 'callout';\n /** Properties edited in place, through an action where the reader has no write grant. */\n edit?: true;\n presentation?:\n | 'summary'\n | 'gallery'\n | 'headline'\n | 'facts'\n | 'table'\n | 'properties'\n | 'card'\n | 'header'\n | import('@mistriwork/engine/shared/appCards.js').AppCardsList\n | import('@mistriwork/engine/shared/appComponents.js').CustomListPresentation;\n /** An embedded list view's board or calendar, held to the record (shared/pageModel.ts, PageList.lanes). */\n lanes?: import('@mistriwork/engine/shared/pageModel.js').PageList['lanes'];\n actions?: import('@mistriwork/engine/shared/pageModel.js').PageHeadlineAction[];\n /** A record header's status column, fact words, filled verb and destructive verbs (shared/pageModel.ts). */\n status?: string;\n factLabels?: Record<string, import('@mistriwork/engine/shared/appCards.js').FactLabel>;\n primary?: string;\n destructive?: string[];\n headerActions?: string[];\n /** A headline's second figure, its figure's earlier value, and small print. */\n subfigure?: string;\n was?: string;\n note?: string;\n /** A similar list: rows of the page's own table sharing the record's `same` values, `near` its numbers. */\n similar?: import('@mistriwork/engine/shared/pageModel.js').PageSimilar;\n workflow?: ResolvedWorkflowStatus;\n create?: RelatedCreate<string>;\n empty?: import('@mistriwork/engine/shared/listPresentations.js').AppListEmpty;\n /** Authored `empty: 'hide' | 'collapse'`: the list is left out, or said in one line, while it has no rows. */\n emptyMode?: 'hide' | 'collapse';\n /** The rows under a header for each record they share (shared/pageModel.ts, PageGroup); `limit` is then the rows to a group. */\n group?: import('@mistriwork/engine/shared/pageModel.js').PageGroup;\n /** Columns summed in the list's footer, over every row it matches. */\n totals?: string[];\n /** A ⋯ at the end of each row with the row's own verbs. */\n rowActions?: true;\n /** A related list's authored `create: false`, and the record's verbs it draws in its head (shared/pageModel.ts). */\n noCreate?: true;\n listActions?: string[];\n columns?: string[] | number;\n limit?: number;\n width?: string;\n offsetLeft?: string;\n offsetRight?: string;\n when?: string;\n hidden?: boolean;\n}\n\nexport interface PageDef {\n id: string;\n name: string;\n base: string;\n source: 'saved' | 'config' | 'suggested';\n sections: PageSection[];\n listTabs?: boolean;\n empty?: 'hide' | 'collapse';\n /** Its archetype (shared/pageLayouts.ts): which regions it has and their order. Absent, or 'record', is the record's own. */\n layout?: import('@mistriwork/engine/shared/pageLayouts.js').PageLayoutWord;\n error?: string;\n}\n\nexport type ViewDef = HttpViewDefinition;\n\nexport interface SectionPlan {\n index: number;\n error?: string;\n empty?: boolean;\n view?: ViewDef;\n query?: string;\n viewText?: string;\n counts?: boolean;\n /** A related list's footer sums: one row measured over every related row, a column's or a rollup's. */\n totals?: HttpBreakdownDefinition;\n /** A grouped list's groups: one row for each record its rows share, with their counts and subtotals. */\n groups?: HttpBreakdownDefinition;\n breakdown?: HttpBreakdownDefinition;\n streams?: Array<{\n error?: string;\n empty?: boolean;\n view?: ViewDef;\n from: string;\n at: string;\n show: string;\n detail?: string;\n by?: string;\n /** A timeline's `side`: the stream's key columns, and its read narrowed to the other party's rows. */\n key?: string[];\n side?: { error?: string; empty?: boolean; view?: ViewDef };\n }>;\n /** A custom section's reads, each planned as a list, by name. */\n reads?: Record<string, { error?: string; empty?: boolean; view?: ViewDef }>;\n /** A custom section's aggregate reads, each planned as a breakdown, by name. */\n aggregates?: Record<string, { error?: string; empty?: boolean; breakdown?: HttpBreakdownDefinition }>;\n}\n\nexport interface PageEntry {\n kind: 'page';\n pageId: string;\n table?: string;\n key?: Record<string, unknown>;\n label?: string;\n tab?: string;\n rootBody?: string;\n chooserQuery?: string;\n}\n\n/** A key, when there is no label to show instead. */\nexport function rootLabel(key: Record<string, unknown>): string {\n return Object.values(key).join(' · ');\n}\n\n/**\n * Whether the root record satisfies a condition written in the query\n * language — the `when` of a section or an action. One bounded query,\n * key and condition composed the way every fold is.\n */\nexport async function rootMatches(base: TableMeta, key: Record<string, unknown>, when: string, connection?: string): Promise<boolean> {\n try {\n /* Built as the server builds it for the page's compose (shared/recordReads.ts). */\n const data = await api('/api/run', { ...whenBody(base, key, when), ...(connection ? ownedBy(connection) : {}) });\n return Boolean(data.rows?.length);\n } catch {\n return false;\n }\n}\n\n/**\n * The column values every row of a list already satisfies: a page's list is\n * \"the children of this record\", one equality on the child's reference\n * column that is deliberately not shown, so it has to be put back before\n * anything reads the row's key. One group only, bare `=` on the view's own\n * base.\n */\nexport function pinnedBy(view: ViewDef | undefined): Record<string, unknown> {\n const groups = view?.filter?.groups ?? [];\n if (groups.length !== 1) return {};\n const pinned: Record<string, unknown> = {};\n for (const condition of groups[0]) {\n if (condition.op !== '=' || typeof condition.path !== 'string') continue;\n if (condition.path.includes('.')) continue;\n pinned[condition.path] = condition.value;\n }\n return pinned;\n}\n\n/** The classes a section wears, on the skeleton and on the settled box alike. */\nexport function sectionClass(section: PageSection): string {\n return (\n `page-section page-${section.kind} width-${section.width ?? (section.kind === 'metric' ? 'auto' : 'full')}` +\n `${section.offsetLeft ? ` offset-l-${section.offsetLeft}` : ''}` +\n `${section.offsetRight ? ` offset-r-${section.offsetRight}` : ''}`\n );\n}\n\n/** Where a page came from, which is the same question as who may change it. */\nexport function sourceOf(page: { source: string }): string {\n return page.source === 'saved'\n ? say('savedOnThisServer')\n : page.source === 'config'\n ? say('fromTheAppOrConnection')\n : say('builtFromTheSchema');\n}\n\n/** The page's record header, if it declares one, with the index its plan and label are keyed by. */\nexport function headerOf(page: PageDef): { section: PageSection; index: number } | undefined {\n const index = page.sections.findIndex((section) => section.kind === 'fields' && section.presentation === 'header' && !section.hidden);\n return index === -1 ? undefined : { section: page.sections[index], index };\n}\n","/**\n * Pages, as the vanilla client reaches them.\n *\n * The page is a component (components/Page.tsx) and the room around it —\n * which definition a link names, the root, the probe, the plan, the crumb —\n * is components/PageRoom.tsx. What stays here is the registry of pages every\n * other module asks: the config's, the suggested one every table has, and\n * the ones this browser built; and how a page is spelled in a link.\n */\nimport { api, findTable, state } from './core.js';\nimport { savedPages, whenSavedPagesChange } from './savedPages.js';\nimport { shell } from './shellMode.js';\nimport { appRequestScope } from './requestIdentity.js';\nimport { resourceOf } from './components/appRooms.js';\nimport { connectionGeneration } from './connectionTransition.js';\nimport { say } from './copy/record/page.js';\n\nexport { rootMatches } from './components/pageModel.js';\n\n/* ---------- lookup ---------- */\n\n/** @typedef {{connection: string | null | undefined, schema: import('@mistriwork/engine/shared/schema.js').DatabaseSchema | null, pages?: import('@mistriwork/engine/shared/page.js').PageDef[], pending?: Promise<import('@mistriwork/engine/shared/page.js').PageDef[]>}} PageCache */\n/** @type {PageCache | undefined} */\nlet cache;\n\n// Cache identity is also the load generation: invalidation retires every reader of it.\nfunction pageOwner() {\n if (!cache || cache.connection !== state.activeConnection || cache.schema !== state.schema) {\n cache = { connection: state.activeConnection, schema: state.schema };\n }\n return cache;\n}\n\n/** @param {PageCache} owner */\nfunction assertCurrent(owner) {\n if (cache !== owner || owner.connection !== state.activeConnection || owner.schema !== state.schema) {\n throw new Error(say('theConnectionOrPage'));\n }\n}\n\n/* The configured pages asked for at start beside the schema and the App,\n not after them: adopted by the first cache of the same connection in the\n same connection generation, and dropped with the cache otherwise. The\n answer is the server's for this connection either way; asking it a round\n trip earlier changes only when it arrives. */\n/** @type {{ connection: string | null | undefined, generation: number, promise: Promise<{ pages?: import('@mistriwork/engine/shared/page.js').PageDef[] }> } | undefined} */\nlet early;\nexport function prefetchPages() {\n const promise = api('/api/pages');\n promise.catch(() => {});\n early = { connection: state.activeConnection, generation: connectionGeneration(), promise };\n}\n\n/** @param {PageCache} owner */\nasync function configuredPages(owner) {\n assertCurrent(owner);\n if (owner.pages) return owner.pages;\n if (!owner.pending) {\n const adopted =\n early && early.connection === owner.connection && early.generation === connectionGeneration() ? early.promise : undefined;\n early = undefined;\n owner.pending = (adopted ?? api('/api/pages'))\n .then((data) => {\n assertCurrent(owner);\n /* A document is a page put on paper, reached from its record as a verb (documents.ts): never a\n page a record opens through, nor one offered beside the record's own. */\n return (owner.pages = (data.pages ?? []).filter((page) => page.document === undefined));\n })\n .finally(() => {\n owner.pending = undefined;\n });\n }\n return owner.pending;\n}\n\n// Saved layouts belong to the standalone browser, including while an App's\n// payload is loading. Never even read an App's stale/malformed local mirror.\nconst appPagesOnly = () => Boolean(shell().app()) || appRequestScope() !== false;\nconst browserPages = () => (appPagesOnly() ? [] : savedPages());\n\n/* App records use their authored page, or a suggestion only when none exists.\n The standalone browser retains its own saved-page precedence. The walk asks\n it through the shell, which names it at boot (app.js, `resolvePage`). */\n/** @param {string} tableId */\nexport async function recordPageId(tableId) {\n const pages = await pagesFor(tableId);\n const built = new Set(browserPages().map((p) => p.id));\n const named = resourceOf(shell().app(), tableId)?.page;\n const [first] = pages;\n const chosen = first && built.has(first.id) ? first : (named && pages.find((p) => p.id === named)) || first;\n if (appPagesOnly() && chosen?.error) throw new Error(chosen.error);\n return chosen && !chosen.error ? chosen.id : `suggested:${tableId}`;\n}\n\n/** Every page for this connection; standalone Workbench includes its saved pages first. */\nexport async function pagesFor(tableId) {\n const owner = pageOwner();\n const pages = await configuredPages(owner);\n assertCurrent(owner);\n const all = [...browserPages(), ...pages];\n return tableId ? all.filter((p) => p.base === tableId) : all;\n}\n\n/**\n * Told when the set of pages changes, so the menu can be rebuilt. A\n * callback rather than an import: the builder announces, and whoever cares\n * listens — an import the other way made a cycle.\n */\nlet listener = null;\n\nexport function whenPagesChange(fn) {\n listener = fn;\n}\n\nexport function forgetPages() {\n cache = undefined;\n early = undefined;\n listener?.();\n}\nwhenSavedPagesChange(forgetPages);\n\nexport async function pageById(id, tableId) {\n const owner = pageOwner();\n const suggested = async (base) => {\n assertCurrent(owner);\n const data = await api(`/api/pages/suggested?table=${encodeURIComponent(base)}`);\n assertCurrent(owner);\n return data.page;\n };\n const mine = browserPages().find((p) => p.id === id);\n if (mine) return mine;\n if (id?.startsWith('suggested:')) {\n const base = id.slice('suggested:'.length);\n if (appPagesOnly()) {\n const authored = await recordPageId(base);\n assertCurrent(owner);\n if (authored !== id) return pageById(authored, base);\n }\n return suggested(base);\n }\n const pages = await pagesFor();\n assertCurrent(owner);\n const found = pages.find((p) => p.id === id);\n if (found) return found;\n\n /* A link may name the page rather than identify it — `@customer-overview`\n rather than `@saved:customer:1787509869158`. Ids are tried first. */\n const named = pages.filter((p) => pageSlug(p) === id);\n const mineFirst = named.find((p) => !tableId || p.base === tableId) ?? named[0];\n if (mineFirst) return mineFirst;\n\n /* Legacy bare table links use the App's page, or the browser's suggestion. */\n const asTable = findTable(id);\n if (asTable) {\n if (appPagesOnly()) {\n const authored = await recordPageId(asTable.id);\n assertCurrent(owner);\n return pageById(authored, asTable.id);\n }\n return suggested(asTable.id);\n }\n if (tableId) {\n if (appPagesOnly()) throw new Error(say('thereIsNoPage', { id }));\n return suggested(tableId);\n }\n throw new Error(say('thereIsNoPage', { id }));\n}\n\n/**\n * A page's name as it is written in a link: the readable half of an id\n * that would otherwise carry a timestamp into every pasted ticket.\n */\nfunction pageSlug(page) {\n return String(page?.name ?? '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * The token a page is written as in the hash: its slug where that names it\n * unambiguously, its id otherwise. Synchronous, because the address bar is\n * stamped during a render.\n */\nexport function pageToken(pageId) {\n if (!pageId) return pageId;\n if (pageId.startsWith('suggested:')) return pageId.slice('suggested:'.length);\n const known = [...browserPages(), ...(pageOwner().pages ?? [])];\n const page = known.find((p) => p.id === pageId);\n if (!page) return pageId;\n const slug = pageSlug(page);\n /* Ambiguity keeps the id: two pages called \"Overview\" would otherwise both\n answer to `@overview`. A table of the same name counts too. */\n const clashes = known.filter((p) => pageSlug(p) === slug).length > 1 || Boolean(findTable(slug));\n return slug && !clashes ? slug : pageId;\n}\n","/**\n * Counting things by something: GROUP BY, as a shape rather than as text.\n *\n * This was the last real hole in the query language. \"How many invoices per\n * month\", \"which countries have the most customers\", \"what does each status\n * add up to\" are the questions anybody asks of a database in the first ten\n * minutes, and until now the only way to ask them here was to drop into `sql`\n * — which means an agent leaves the checked path exactly where the question\n * gets interesting, and a person leaves the tool.\n *\n * A breakdown is deliberately *not* a view with a `groupBy` bolted on. A view\n * promises one row per base record, and every part of it — the correlated\n * aggregates, the LEFT JOIN rule, the row-click that opens a record — leans on\n * that promise. Grouping breaks it: the rows that come back are not records\n * and cannot be opened, and a column that is neither a key nor an aggregate is\n * a bug on two engines and a silent wrong answer on the third. So it is a\n * sibling type with its own compiler, sharing the parts that are genuinely the\n * same: path resolution, the join planner, and `buildWhere`.\n *\n * invoice count by status\n * invoice sum total by month invoice_date\n * invoice count, avg total by customer_id.country_code.name sort count desc\n *\n * Two rules the shape enforces, both of which SQL leaves to the author:\n *\n * - **Every selected expression is a key or a measure.** There is nowhere to\n * put a third thing, so `SELECT status, name, COUNT(*) … GROUP BY status`\n * — which a lenient engine would answer with an arbitrary `name` — cannot be\n * written.\n * - **The result is bounded like every other read.** A breakdown over a\n * high-cardinality column is a million-row answer, and `sort count desc\n * limit 20` is what somebody meant anyway.\n */\nimport { buildWhere, type Condition, type DateUnit, type Filter, type SqlStyle } from './filter.js';\nimport { clampLimit, clampOffset, countStatement } from './reads.js';\nimport { aggregateInput, columnKind, findTable, labelPath, type DatabaseSchema, type ForeignKey } from './schema.js';\nimport { JoinPlan, PathError, sqlLiteral, type ViewFilter } from './view.js';\nimport { ScopedRelations, type RowScope } from './rowScope.js';\nimport { readsOf } from './query.js';\nimport { isFixedScopeCatalog } from './scopeCatalog.js';\nimport { viewPaths, withViewComputed } from './viewLabels.js';\nimport { tableAtPrefix, tryResolvePath, type ViewDef } from './viewPaths.js';\nimport type { ComputedPath } from './rollups.js';\nimport { withViewReferences } from './declaredReferences.js';\nimport { declaredScale, exactSqliteSum, sumsExactly } from './decimals.js';\nimport { LABEL_SEGMENT, labelPrefix } from './recordLabel.js';\n\n/** What a measure can do. `count` is the only one that needs no column. `median` is PostgreSQL's\n `percentile_cont(0.5)`: SQLite has no median aggregate, so it is refused there. */\nexport type MeasureFn = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'median';\n\nexport const MEASURE_FUNCTIONS: MeasureFn[] = ['count', 'sum', 'avg', 'min', 'max', 'median'];\n\n/** The arithmetic a computed measure joins its terms with. SQL's precedence: `*` and `/` before `+` and `-`. */\nexport type MeasureOp = '+' | '-' | '*' | '/';\nexport const MEASURE_OPS: readonly MeasureOp[] = ['+', '-', '*', '/'];\n\n/** Why a dialect cannot answer a measure function, or undefined when it can. */\nexport function measureRefusal(fn: MeasureFn, dialect: string | undefined): string | undefined {\n if (fn !== 'median') return undefined;\n if (dialect === 'sqlite')\n return '`median` is not available on SQLite: it has no median aggregate (no percentile_cont). Use `avg`, or a PostgreSQL source.';\n return undefined;\n}\n\n/** The periods a date key can be cut into. */\nexport const DATE_UNITS: DateUnit[] = ['day', 'week', 'month', 'quarter', 'year', 'weekday', 'hour'];\n\n/**\n * One thing to group by: a path, optionally cut down to a period.\n *\n * The path is a reference path like a view's, so \"customers by country name\"\n * is `customer … by country_code.name` and needs no join written by hand.\n */\nexport interface BreakdownKey {\n path: string;\n /** Cut a date or timestamp down to a period. Refused on anything else. */\n bucket?: DateUnit;\n alias?: string;\n}\n\n/** One aggregate: a function and the column it reads. */\nexport interface MeasureTerm {\n fn: MeasureFn;\n /** The column to measure. Absent means `COUNT(*)`, and is only valid there. */\n path?: string;\n /** COUNT DISTINCT. Meaningless on the others and refused there. */\n distinct?: boolean;\n /**\n * `count when [status = open]`: only the rows that hold these conditions are counted, in the same statement as the\n * other measures (`COUNT(CASE WHEN … THEN 1 END)`), so a funnel's stages are one snapshot and can never disagree.\n * Only a bare `count` takes it. `when` is the conditions as written; `where` is what they parsed to, which is what\n * runs: the parser fills it, and a request's own is held to the same shape a filter is (resourceReadOperations.ts).\n */\n when?: string;\n where?: ViewFilter;\n}\n\n/**\n * A measure: one aggregate, or a computed measure — aggregates and numbers\n * joined by arithmetic, `sum paid / sum total`, `sum total - sum cost`. A\n * ratio is the `/` case, not a function of its own. A division never\n * truncates and never fails: its divisor is read as a decimal and a zero\n * one answers null.\n */\nexport interface Measure extends MeasureTerm {\n alias?: string;\n /** The terms after the first, each with the operator before it. */\n steps?: { op: MeasureOp; term: MeasureTerm | number }[];\n}\n\nconst termText = (t: MeasureTerm | number): string =>\n typeof t === 'number'\n ? String(t)\n : [t.fn, ...(t.distinct ? ['distinct'] : []), ...(t.path ? [t.path] : []), ...(t.when ? ['when', `[${t.when}]`] : [])].join(' ');\n\n/** A measure as it is written: `sum paid / sum total as rate`. */\nexport function measureToText(m: Measure): string {\n return [termText(m), ...(m.steps ?? []).flatMap(({ op, term }) => [op, termText(term)]), ...(m.alias ? ['as', m.alias] : [])].join(' ');\n}\n\nconst termName = (t: MeasureTerm | number, path = typeof t === 'number' ? undefined : t.path): string =>\n typeof t === 'number' ? String(t) : t.fn === 'count' ? (path ? `count(${path})` : 'count') : `${t.fn}(${path ?? ''})`;\n\n/** The name a measure answers under when it has no alias: `sum(total)`, `sum(paid) / sum(total)`. */\nexport function measureName(m: Measure, path = m.path): string {\n return m.alias?.trim() || [termName(m, path), ...(m.steps ?? []).flatMap(({ op, term }) => [op, termName(term)])].join(' ');\n}\n\n/** Every column a measure reads. */\nexport function measurePaths(m: Measure): string[] {\n return [m, ...(m.steps ?? []).map(({ term }) => term)].flatMap((t) => (typeof t !== 'number' && t.path ? [t.path] : []));\n}\n\n/** Every `when` among a breakdown's measures, in order: what a reader's grants and the stitched-read check must also hold. */\nexport const measureConditions = (measures: readonly Measure[] | undefined): ViewFilter[] =>\n (measures ?? []).flatMap((m) => (m.where?.groups.length ? [m.where] : []));\n\n/** The paths a filter's conditions walk, and the paths they compare against. */\nexport const filterPaths = (filter: ViewFilter | undefined): string[] =>\n (filter?.groups ?? []).flatMap((group) => group.flatMap((c) => [c.path, ...(c.other === undefined ? [] : [c.other])]));\n\nexport interface BreakdownOrder {\n /** A key's or a measure's output name. Breakdowns sort by what they show. */\n name: string;\n direction: 'asc' | 'desc';\n /** On a key: this entry only moves the empty group (a null key) after every other, whatever the entries after it\n say; it orders nothing else, so `direction` is not read. The groups of a grouped list use it to draw \"No sprint\"\n last under any sort — done in the query because the groups are paged, so no page could place it by itself. */\n emptyLast?: boolean;\n}\n\nexport interface BreakdownDef {\n base: string;\n by: BreakdownKey[];\n /** `table.column` of text columns an App declares dates (declaredDates.ts), which this catalog reads as TEXT:\n a period bucket over one is allowed. Carried by the App's resource facade, never taken from a request. */\n dateColumns?: string[];\n measures: Measure[];\n filter?: ViewFilter;\n orderBy?: BreakdownOrder[];\n limit?: number;\n /** The breakdown written down, when it came from text. */\n text?: string;\n /** The ISO weekday a `week` bucket's weeks start on, 1 (Monday) to 7 (Sunday); Monday when unsaid. An App's\n server sets it from `ui.locale` (weekStart.ts), whatever a request carried. */\n weekStart?: number;\n /**\n * The rollups its paths name (rollups.ts), by path — a measure `sum\n * on_hand`, a key, a condition. Filled by the server from the App's\n * catalog (withBreakdownComputed), never taken from a request, and read\n * by the compiler where the adapter's catalog does not carry them.\n */\n computed?: Record<string, ComputedPath>;\n /** The computed columns its paths name (computed.ts), pinned the same way. */\n expressions?: ViewDef['expressions'];\n /** The record tags its conditions ask (`tags has vpn`, tagLists.ts), pinned the same way: a filter rail's\n counts under a tags filter ask them where the adapter's catalog has no tags. */\n tagged?: ViewDef['tagged'];\n /** The App's declared references it may walk, as a view carries them (ViewDef.references); never a request's. */\n references?: ForeignKey[];\n /**\n * The keys that are references, each with the path of the name of the\n * record it points at (`customer_id` → `customer_id.name`): a group of\n * `customer_id` is a customer, and is said by the customer's name, as a\n * grid's cell says it. Each such key answers that name in a `label` column\n * after the measures. Filled by the server from the catalog\n * (withBreakdownComputed), never taken from a request, as are `labels`,\n * the record-name templates a `$label` name needs (a view's `labels`).\n */\n names?: Record<string, string>;\n labels?: Record<string, string>;\n}\n\nexport interface CompiledMeasure {\n name: string;\n /** A key, a measure, or a key's record name (`of` names the key). */\n kind: 'key' | 'measure' | 'label';\n of?: string;\n fn?: MeasureFn;\n /** The path this came from, for the interface to label it with. */\n path?: string;\n /** What the values are, as far as the schema knows. */\n type: string;\n bucket?: DateUnit;\n}\n\ninterface CompiledBreakdown {\n text: string;\n params: unknown[];\n columns: CompiledMeasure[];\n limit: number;\n}\n\n/** How many group keys are worth having. */\nconst MAX_KEYS = 3;\n\n/**\n * Is this column something a date bucket can be taken of?\n *\n * Read off the declared type rather than the values, because the answer has\n * to be the same before and after the table has any rows in it. SQLite's\n * `DATE` is a convention rather than a type, which is exactly why the check is\n * on the declaration: a column somebody called `DATE` is one they intend to\n * hold dates, and `strftime` over text that is not one returns null — a group\n * of its own rather than an error, which is the honest outcome.\n */\nconst isDateish = (type: string): boolean => /date|time/i.test(type ?? '');\n\n/**\n * Compile a breakdown to one parameterised SELECT.\n *\n * The order of work matches `compileView`'s and for the same reason: keys,\n * then measures, then the filter, then the sort — so aliases are handed out in\n * first-use order and the same breakdown always compiles to the same text.\n */\nexport function compileBreakdown(\n schema: DatabaseSchema,\n def: BreakdownDef,\n style: SqlStyle,\n offset: number | unknown = 0,\n scope?: RowScope,\n): CompiledBreakdown {\n /* A breakdown compiles to the same statement against the same catalog, and an App's enter asks for each of its\n rail's and dashboard's breakdowns several times — against the reader's pruned catalog and the full one for\n the access check (columnGrants.ts), under the reader's scope to run it, and again for the SQL pane — each a\n full compile (a field report: a large tenant App spent a twelfth of an enter's CPU here). Kept only against a catalog nothing can change (fixedCatalog, below), for a definition\n whose text says all of it (breakdownText), and only where compiling read neither the clock nor the reader\n (query.ts readsOf): a statement that wrote a request's instant, day or email is that request's, and is never\n handed to another. */\n const text = typeof offset === 'number' && fixedCatalog(schema) ? breakdownText(def) : undefined;\n if (text === undefined) {\n breakdownsCompiled++;\n return compileBreakdownFresh(schema, def, style, offset, scope);\n }\n let byStyle = breakdownsKept.get(schema);\n if (!byStyle) breakdownsKept.set(schema, (byStyle = new WeakMap()));\n let kept = byStyle.get(style);\n if (!kept) byStyle.set(style, (kept = new Map()));\n const key = `${scope ? scopeOrdinal(scope) : 0}\\0${offset as number}\\0${text}`;\n let found = kept.get(key);\n if (found) {\n kept.delete(key);\n kept.set(key, found);\n } else {\n breakdownsCompiled++;\n const read = readsOf(() => compileBreakdownFresh(schema, def, style, offset, scope));\n if (read.clock !== 0 || read.reader) return read.value;\n found = read.value;\n kept.set(key, found);\n if (kept.size > BREAKDOWNS_KEPT) kept.delete(kept.keys().next().value!);\n }\n // Each caller's own arrays: a driver or a caller may add to what it is handed.\n return { ...found, params: [...found.params], columns: found.columns.map((column) => ({ ...column })) };\n}\n/* A catalog nothing can change: a captured scope catalog (scopeCatalog.ts), or whatever the server says it froze\n all the way down (evidence.ts captureTenantData, which registers itself here: a shared module cannot import it). */\nlet alsoFixed: (schema: DatabaseSchema) => boolean = () => false;\n/** Who says a catalog cannot change, beside the scope catalog's own captures. */\nexport function keepBreakdownsAgainst(fixed: (schema: DatabaseSchema) => boolean): void {\n alsoFixed = fixed;\n}\nconst fixedCatalog = (schema: DatabaseSchema) => isFixedScopeCatalog(schema) || alsoFixed(schema);\n\n/* A definition's text, where the text says all of it: plain records, arrays, strings, booleans and finite numbers.\n A property left undefined (a key with no `bucket`, a list with no `orderBy` — most of them, as a reader's snapshot\n carries them) is said as one, never dropped, so the text of a definition that has the key and one that does not\n still differ; anything else JSON would say alike — an undefined in a list, a non-finite number, an object that is\n not a plain record, a function, a symbol or a BigInt — is no text, and is compiled afresh. */\nconst UNDEFINED = Object.freeze({ '\\u0000': 'undefined' });\nfunction breakdownText(def: BreakdownDef): string | undefined {\n let plain = true;\n const text = JSON.stringify(def, function (this: Record<string, unknown>, key, item: unknown) {\n // The value as held, not as its toJSON says it: a Date is said as text, and is not the text it says.\n const held = this[key];\n if (held === undefined) {\n if (Array.isArray(this)) plain = false;\n return UNDEFINED;\n }\n // JSON drops a function or symbol (two definitions apart only by one would share a text) and throws on a BigInt.\n if (typeof held === 'function' || typeof held === 'symbol' || typeof held === 'bigint') {\n plain = false;\n return null;\n }\n if (\n (typeof held === 'number' && !Number.isFinite(held)) ||\n (held !== null && typeof held === 'object' && !Array.isArray(held) && ![null, Object.prototype].includes(Object.getPrototypeOf(held)))\n )\n plain = false;\n return item;\n });\n return plain ? text : undefined;\n}\n\nconst breakdownsKept = new WeakMap<DatabaseSchema, WeakMap<SqlStyle, Map<string, CompiledBreakdown>>>();\nconst BREAKDOWNS_KEPT = 256;\nlet breakdownsCompiled = 0;\n/** For tests: how many breakdowns were compiled rather than found. */\nexport function breakdownsCompiledSoFar(): number {\n return breakdownsCompiled;\n}\nconst scopeOrdinals = new WeakMap<RowScope, number>();\nlet scopesSeen = 0;\nconst scopeOrdinal = (scope: RowScope): number => {\n let ordinal = scopeOrdinals.get(scope);\n if (ordinal === undefined) scopeOrdinals.set(scope, (ordinal = ++scopesSeen));\n return ordinal;\n};\n\nfunction compileBreakdownFresh(\n schema: DatabaseSchema,\n def: BreakdownDef,\n style: SqlStyle,\n offset: number | unknown,\n scope: RowScope | undefined,\n): CompiledBreakdown {\n // The App's soft keys, where this catalog is the database's own (declaredReferences.ts).\n schema = withViewReferences(schema, def.references);\n const base = findTable(schema, def.base);\n if (!base) {\n throw new PathError(`No table called \"${def.base}\" in this database, so there is nothing to break down.`, def.base, def.base, 0);\n }\n /* No `by` is one group: the whole answer as a single row —\n \"invoice sum total\" is a number, not a mistake. The app home's metric\n cards ride on exactly this, and it reads the same in the workbench. */\n if (def.by.length > MAX_KEYS) {\n /* A cap rather than a warning. Three keys is already a table nobody reads\n across; the fourth is how a breakdown becomes the base table again, one\n row per record, with the count column always saying 1. */\n throw new PathError(\n `A breakdown groups by at most ${MAX_KEYS} things; this one names ${def.by.length}.`,\n def.by[MAX_KEYS].path,\n def.by[MAX_KEYS].path,\n 0,\n );\n }\n if (!def.measures?.length) {\n throw new PathError('A breakdown needs something to measure — `count`, or `sum total`.', '', '', 0);\n }\n\n const relations = new ScopedRelations(schema, style, scope);\n const baseRelation = relations.table(base.id);\n const plan = new JoinPlan(schema, base, style, relations.table);\n /* The view compiler's paths, so a rollup — the base's or through a\n reference — is the same correlated subquery here as in a list, its\n child rows read through this statement's relations (a row scope's\n CTE). A key, a measure and a condition may each name one. */\n const paths = viewPaths(\n schema,\n {\n id: '',\n name: '',\n base: def.base,\n ...(def.computed ? { computed: def.computed } : {}),\n ...(def.expressions ? { expressions: def.expressions } : {}),\n ...(def.tagged ? { tagged: def.tagged } : {}),\n ...(def.labels ? { labels: def.labels } : {}),\n } as ViewDef,\n plan,\n style,\n relations,\n );\n const qualify = (path: string): string => paths.qualify(path);\n const membership = paths.membership;\n const exists = paths.exists;\n\n const seen = new Map<string, number>();\n /** Output names are unique or the result set loses a column silently. */\n const nameFor = (wanted: string): string => {\n const count = seen.get(wanted) ?? 0;\n seen.set(wanted, count + 1);\n return count ? `${wanted}_${count + 1}` : wanted;\n };\n\n const columns: CompiledMeasure[] = [];\n const select: string[] = [];\n const grouping: string[] = [];\n\n for (const key of def.by ?? []) {\n const resolved = paths.resolve(key.path);\n let sql = qualify(key.path);\n if (key.bucket) {\n // A request's word, looked up in the dialect's own table: one of the four, never anything else.\n if (!DATE_UNITS.includes(key.bucket)) {\n throw new PathError(\n `\"${String(key.bucket).slice(0, 40)}\" is not a period. Use one of: ${DATE_UNITS.join(', ')}.`,\n key.path,\n key.path,\n 0,\n );\n }\n if (!isDateish(resolved.column.type) && !def.dateColumns?.includes(`${resolved.table.id}.${resolved.column.name}`)) {\n throw new PathError(\n `\"${key.path}\" is ${resolved.column.type || 'untyped'}, so it cannot be grouped by ${key.bucket}. ` +\n 'Group by it directly, or pick a date column.',\n key.path,\n key.path,\n resolved.hops.length,\n );\n }\n sql = style.dateBucket(sql, key.bucket, def.weekStart);\n }\n\n const name = nameFor(key.alias?.trim() || (key.bucket ? `${key.bucket}(${resolved.path})` : resolved.path));\n select.push(`${sql} AS ${style.quote(name)}`);\n /* Grouped by the *expression*, not by the output name. Postgres\n would take the alias; SQLite would too — but a bucketed key aliased to\n `month(invoice_date)` and grouped by that name is one identifier away\n from grouping by a column that happens to share it. The expression is\n never ambiguous.\n\n A rollup is the exception: its expression is a correlated subquery,\n and an engine can fail to see two copies of one as\n the same thing, so it refuses the SELECT list's copy as ungrouped.\n Grouped by its position in the SELECT list instead — the one place it\n is written, which every engine groups by and nothing can shadow. */\n grouping.push(def.computed && Object.hasOwn(def.computed, key.path) ? String(select.length) : sql);\n columns.push({\n name,\n kind: 'key',\n path: resolved.path,\n /* A bucket makes text out of whatever it was, and the interface should\n render `2026-08` as the label it is rather than as a date it is not. */\n type: key.bucket ? 'text' : resolved.column.type,\n bucket: key.bucket,\n });\n }\n\n /* A measure said as text whose order is a number: an exact SQLite sum, by its output name. */\n const orderedBy = new Map<string, string>();\n /* One aggregate's SQL, and what its values are. `exact` asks for a decimal column's sum as exact text where the\n engine would add floats (decimals.ts): a measure on its own, never a term of arithmetic, which reads a number. */\n const termSql = (term: MeasureTerm, exact = false): { sql: string; path?: string; type: string; order?: string } => {\n /* The function is written into the statement as a word, so it is one of\n the words: a structured def arrives from a request (`/api/breakdown`\n `def`), and anything else there would be SQL of the caller's own,\n outside every scope and grant this statement reads through. */\n if (typeof term !== 'object' || term === null || !MEASURE_FUNCTIONS.includes(term.fn)) {\n throw new PathError(`\"${String(term?.fn).slice(0, 40)}\" is not a measure. Use one of: ${MEASURE_FUNCTIONS.join(', ')}.`, '', '', 0);\n }\n if (term.fn === 'count') {\n if (term.distinct && !term.path) {\n throw new PathError('`count distinct` needs a column to count distinct values of.', '', '', 0);\n }\n /* COUNT(*) rather than COUNT(column) when no column is named: a column\n count skips nulls, which turns \"how many invoices\" into \"how many\n invoices have a total\" without saying so. Naming a column is how you\n ask the other question, and then the skipping is the point. */\n return {\n sql: term.path ? `COUNT(${term.distinct ? 'DISTINCT ' : ''}${qualify(term.path)})` : 'COUNT(*)',\n path: term.path,\n type: 'integer',\n };\n }\n if (!term.path) {\n throw new PathError(`\\`${term.fn}\\` needs a column: \\`${term.fn} total\\`.`, '', '', 0);\n }\n if (term.distinct) {\n throw new PathError(`\\`distinct\\` only means something with \\`count\\`, not with \\`${term.fn}\\`.`, term.path, term.path, 0);\n }\n const refused = measureRefusal(term.fn, style.dialect);\n if (refused) throw new PathError(refused, term.path, term.path, 0);\n const resolved = paths.resolve(term.path);\n const read = aggregateInput(qualify(term.path), resolved.column.type, style.dialect);\n /* SQLite adds a decimal column as floats, a cent off at large totals: summed in its smallest unit instead, and\n said with its places, as PostgreSQL's exact numeric says it. */\n if (exact && term.fn === 'sum' && style.dialect === 'sqlite' && sumsExactly(resolved.column.type)) {\n const sum = exactSqliteSum(read, declaredScale(resolved.column.type), resolved.path);\n return { sql: sum.text, path: resolved.path, type: resolved.column.type, order: sum.total };\n }\n return {\n sql: term.fn === 'median' ? `percentile_cont(0.5) WITHIN GROUP (ORDER BY ${read})` : `${term.fn.toUpperCase()}(${read})`,\n path: resolved.path,\n /* `avg` and `median` are fractions of whatever they read — an average\n of integers is not an integer, and saying otherwise makes the\n interface round a number the database did not. */\n type:\n term.fn === 'avg' || term.fn === 'median'\n ? 'numeric'\n : term.fn === 'sum' && columnKind(resolved.column.type) === 'boolean'\n ? 'integer'\n : resolved.column.type,\n };\n };\n\n /* A measure that counts only some rows (`count when [...]`) is written once the statement's own parameters are\n counted, below: its values are bound after the scope's and before the filter's, in the order the text reads. */\n const whens: { at: number; name: string; where: ViewFilter }[] = [];\n const asFilter = (filter: ViewFilter | undefined): Filter | undefined =>\n filter?.groups.length\n ? {\n groups: filter.groups.map((group) =>\n group.map((c): Condition =>\n c.any\n ? { column: c.path, op: c.op, any: c.any }\n : c.other === undefined\n ? { column: c.path, op: c.op, value: c.value }\n : { column: c.path, op: c.op, other: c.other },\n ),\n ),\n }\n : undefined;\n /* Every path a condition names is planned (its joins, its tags) before the scope prefix is frozen. */\n const planPaths = (conditions: Filter | undefined) => {\n for (const group of conditions?.groups ?? []) {\n for (const condition of group) {\n /* A tags condition (`has`) plans its link and tag relations here too, before the scope\n prefix is frozen, as a view's does: its EXISTS reads them through it. */\n if (condition.any) {\n exists(condition.any);\n continue;\n }\n qualify(condition.column);\n if (condition.other !== undefined) qualify(condition.other);\n }\n }\n };\n\n for (const measure of def.measures) {\n if (measure.where !== undefined || measure.when !== undefined) {\n if (measure.fn !== 'count' || measure.path || measure.distinct || measure.steps?.length || !measure.where) {\n throw new PathError('`when` only goes with a bare `count`: `count when [status = open]`.', '', '', 0);\n }\n const conditions = asFilter(measure.where);\n if (conditions) {\n planPaths(conditions);\n const name = nameFor(measureName(measure, undefined));\n whens.push({ at: select.length, name, where: measure.where });\n select.push('');\n columns.push({ name, kind: 'measure', fn: 'count', type: 'integer' });\n continue;\n }\n }\n const head = termSql(measure, !measure.steps?.length);\n if (!measure.steps?.length) {\n const name = nameFor(measureName(measure, head.path));\n select.push(`${head.sql} AS ${style.quote(name)}`);\n if (head.order) orderedBy.set(name, head.order);\n columns.push({ name, kind: 'measure', fn: measure.fn, path: head.path, type: head.type });\n continue;\n }\n /* A computed measure is written out in its own order: SQL's precedence\n is the grammar's. A divisor is made a decimal, so integer counts do\n not divide to zero on PostgreSQL and SQLite, and NULLIF makes a zero\n one an empty answer instead of an error. */\n const pieces = [head.sql];\n for (const { op, term } of measure.steps) {\n if (!MEASURE_OPS.includes(op)) throw new PathError(`\"${op}\" is not arithmetic a measure joins with: + - * or /.`, '', '', 0);\n if (typeof term === 'number' && !Number.isFinite(term)) throw new PathError(\"A computed measure's number must be finite.\", '', '', 0);\n const sql = typeof term === 'number' ? String(term) : termSql(term).sql;\n pieces.push(op, op === '/' ? `NULLIF(1.0 * ${sql}, 0)` : sql);\n }\n const name = nameFor(measureName(measure, head.path));\n select.push(`(${pieces.join(' ')}) AS ${style.quote(name)}`);\n columns.push({ name, kind: 'measure', type: 'numeric' });\n }\n\n /* A key that is a reference is said by its record's name, as a grid's cell\n is: the name through the same join the key's path walks, read through\n this statement's relations, so a record this reader's scope does not\n admit has no name and the group stays its key. Last, after the\n measures, so a reader that takes the key and the measure by position\n reads what it always did; grouped by position, since the key decides it. */\n for (const [index, key] of (def.by ?? []).entries()) {\n if (key.bucket || !def.names || !Object.hasOwn(def.names, key.path)) continue;\n const path = def.names[key.path];\n const name = nameFor(`${columns[index].name}.${LABEL_SEGMENT}`);\n select.push(`${qualify(path)} AS ${style.quote(name)}`);\n grouping.push(String(select.length));\n columns.push({ name, kind: 'label', of: columns[index].name, path, type: 'text' });\n }\n\n const conditions = asFilter(def.filter);\n planPaths(conditions);\n const joins = plan.clauses();\n const prefix = relations.finish();\n const whenParams: unknown[] = [];\n for (const { at, name, where: held } of whens) {\n const built = buildWhere(\n asFilter(held),\n style,\n prefix.params.length + whenParams.length + 1,\n qualify,\n qualify,\n membership,\n exists,\n false,\n paths.typeOf,\n );\n whenParams.push(...built.params);\n /* COUNT, not SUM: a count of no rows is 0, where a sum of none is null. */\n select[at] = `COUNT(CASE WHEN ${built.text.replace(/^ WHERE /, '')} THEN 1 END) AS ${style.quote(name)}`;\n }\n const where = buildWhere(\n conditions,\n style,\n prefix.params.length + whenParams.length + 1,\n qualify,\n qualify,\n membership,\n exists,\n false,\n paths.typeOf,\n );\n\n /* Sorted by output name, and only by output name.\n\n Every column of a breakdown is either a key or a measure, and both are in\n the SELECT list under a name — so there is nothing else it could sort by,\n and ORDER BY runs late enough to see the aliases on all three engines.\n Sorting by an expression instead would let `sort total desc` mean the\n ungrouped column on a statement where that column no longer exists. */\n const names = new Set(columns.map((c) => c.name));\n const order = (def.orderBy ?? []).map((o) => {\n if (!names.has(o.name)) {\n throw new PathError(`Nothing called \"${o.name}\" is in this breakdown. It shows: ${[...names].join(', ')}.`, o.name, o.name, 0);\n }\n if (o.emptyLast) return `(${style.quote(o.name)} IS NULL) ASC`;\n /* An exact sum is text; it sorts by the integer it says, never as text (\"9.00\" after \"10.00\"). */\n return `${orderedBy.get(o.name) ?? style.quote(o.name)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`;\n });\n\n const limit = clampLimit(def.limit);\n const skip = clampOffset(offset);\n const n = prefix.params.length + whenParams.length + where.params.length;\n\n const lines = [`SELECT ${select.join(',\\n ')}`, ` FROM ${baseRelation} ${style.quote(plan.baseAlias)}`, ...joins];\n if (where.text) lines.push(where.text);\n if (grouping.length) lines.push(` GROUP BY ${grouping.join(', ')}`);\n if (order.length) lines.push(` ORDER BY ${order.join(', ')}`);\n lines.push(` LIMIT ${style.placeholder(n + 1)} OFFSET ${style.placeholder(n + 2)}`);\n\n return { text: prefix.text + lines.join('\\n'), params: [...prefix.params, ...whenParams, ...where.params, limit, skip], columns, limit };\n}\n\n/**\n * The breakdown with the rollups its paths name, read from a catalog that\n * serves them (an App's) — and with any it arrived with dropped, as\n * withViewComputed does for a view: which rows a number sums is the\n * catalog's to say, never a request's.\n */\nexport function withBreakdownComputed(schema: DatabaseSchema, def: BreakdownDef): BreakdownDef {\n // A request's declared dates and names are dropped with its rollups: all are the catalog's to carry (appResources.ts).\n const { computed: _given, expressions: _written, tagged: _tagged, dateColumns: _dated, names: _names, labels: _labels, ...rest } = def;\n /* A key that is a reference to a record with a name: its groups are said\n by that name (compileBreakdown), the one a grid's cell reads (labelPath),\n through the same reference. */\n const names: Record<string, string> = {};\n const labels: Record<string, string> = {};\n for (const key of def.by ?? []) {\n if (typeof key?.path !== 'string' || key.bucket || Object.hasOwn(names, key.path)) continue;\n const target = tableAtPrefix(schema, def.base, key.path);\n const resolved = target && tryResolvePath(schema, def.base, key.path).resolved;\n if (!target || !resolved || resolved.rollup) continue;\n const naming = labelPath(schema, target);\n if (!naming) continue;\n names[key.path] = `${key.path}.${naming}`;\n const prefix = labelPrefix(names[key.path]);\n if (prefix !== undefined && target.labelTemplate) labels[prefix] = target.labelTemplate;\n }\n const named = Object.keys(names).length ? { ...rest, names, ...(Object.keys(labels).length ? { labels } : {}) } : rest;\n const paths = [\n ...(def.by ?? []).map((key) => key.path),\n ...(def.measures ?? []).flatMap(measurePaths),\n ...filterPaths(def.filter),\n ...measureConditions(def.measures).flatMap(filterPaths),\n ];\n /* The rollups and computed columns its paths name, as a view's are (withViewComputed). */\n const pinned = withViewComputed(schema, {\n id: '',\n name: '',\n base: def.base,\n columns: paths.filter((path): path is string => typeof path === 'string').map((path) => ({ path })),\n });\n return {\n ...named,\n ...(pinned.computed ? { computed: pinned.computed } : {}),\n ...(pinned.expressions ? { expressions: pinned.expressions } : {}),\n ...(pinned.tagged ? { tagged: pinned.tagged } : {}),\n };\n}\n\n/**\n * How many groups there are, ignoring the page.\n *\n * `SELECT COUNT(*) FROM (the grouped statement)` — the only honest way to\n * count groups, because the number is a property of the grouping rather than\n * of the base table. Without the limit and offset, which is the whole point of\n * asking.\n */\nexport function compileBreakdownCount(\n schema: DatabaseSchema,\n def: BreakdownDef,\n style: SqlStyle,\n scope?: RowScope,\n limit?: number,\n): { text: string; params: unknown[] } {\n const inner = compileBreakdown(schema, def, style, 0, scope);\n /* The LIMIT/OFFSET line is the last one and carries the last two\n parameters. Dropped by rebuilding rather than by string surgery would be\n nicer; it is the last line by construction, and the test says so. */\n const lines = inner.text.split('\\n');\n const body = lines.slice(0, -1).join('\\n');\n return {\n /* With a bound, at most `limit` groups are counted (countStatement). The\n grouping itself still reads what it groups; the bound keeps the count's\n answer, not the scan, from growing with the data. */\n text: limit === undefined ? `SELECT COUNT(*) AS n FROM (\\n${body}\\n) AS grouped` : countStatement(`(\\n${body}\\n) AS grouped`, limit),\n params: inner.params.slice(0, -2),\n };\n}\n\n/**\n * Render a breakdown as the SQL it will become, values inline.\n *\n * The exact counterpart of `explainView`, and here for the same reason: the\n * SQL pane is how somebody learns what a line of the language did on their\n * behalf, and a statement full of `?` teaches nothing about the filter that\n * produced it. It also has to be a statement that runs, because the pane\n * offers to open it in the SQL editor.\n */\nexport function explainBreakdown(schema: DatabaseSchema, def: BreakdownDef, style: SqlStyle, offset: number | unknown = 0): string {\n const compiled = compileBreakdown(schema, def, style, offset);\n let i = 0;\n return compiled.text.replace(/\\$\\d+|\\?/g, () => sqlLiteral(compiled.params[i++]));\n}\n\n/** A breakdown's groups said by name: key column → the key's value as text → the record's name. */\nexport type GroupNames = Record<string, Record<string, string>>;\n\n/**\n * The answer as its readers take it — the keys first and the measures after,\n * by position — with each reference key's names (compileBreakdown's `label`\n * columns) lifted out beside it: a chart reads its key and measure by index,\n * a board its count last, and a name among them would be read as either.\n */\nexport function withGroupNames<R extends { columns: string[]; rows: Record<string, unknown>[]; resolved: CompiledMeasure[] }>(\n result: R,\n): R & { names?: GroupNames } {\n const labels = result.resolved.filter((column) => column.kind === 'label' && column.of);\n if (!labels.length) return result;\n const names: GroupNames = {};\n for (const label of labels) {\n const said: Record<string, string> = {};\n for (const row of result.rows) {\n const key = row[label.of!],\n name = row[label.name];\n if (key !== null && key !== undefined && typeof name === 'string' && name !== '') said[String(key)] = name;\n }\n names[label.of!] = said;\n }\n const hidden = new Set(labels.map((label) => label.name));\n return {\n ...result,\n columns: result.columns.filter((name) => !hidden.has(name)),\n resolved: result.resolved.filter((column) => !hidden.has(column.name)),\n rows: result.rows.map((row) => Object.fromEntries(Object.entries(row).filter(([name]) => !hidden.has(name)))),\n names,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Q as Problem, it as useQuery } from "./shared-poc11xxo.js";
|
|
2
2
|
import { F as primaryKey, gt as defineMessages, i as UiSizeContext, o as useUiHost, t as ComponentStyleScope, yn as keepPreviousData } from "./shared-DJZjSi3J.js";
|
|
3
|
-
import { L as state, dt as subscribeAppPresentation, i as api, lt as appPresentation } from "./shared-
|
|
4
|
-
import { t as onDemand } from "./shared-
|
|
3
|
+
import { L as state, dt as subscribeAppPresentation, i as api, lt as appPresentation } from "./shared-BoLbc2WD.js";
|
|
4
|
+
import { t as onDemand } from "./shared-B-BUF9fx.js";
|
|
5
5
|
import { n as totalGroups, t as totalFigure } from "./shared-C-8votY-.js";
|
|
6
|
-
import { A as batchedBreakdown, K as picked, Z as breakdownText, at as windowed, tt as measureText } from "./shared-
|
|
7
|
-
import { g as pageBridge } from "./shared-
|
|
6
|
+
import { A as batchedBreakdown, K as picked, Z as breakdownText, at as windowed, tt as measureText } from "./shared-XcSKDp6N.js";
|
|
7
|
+
import { g as pageBridge } from "./shared-Chp88all.js";
|
|
8
8
|
import { n as pathEqualityFilter } from "./shared-_IMKuuvy.js";
|
|
9
|
-
import { loadComponent, t as rowsByPath } from "./shared-
|
|
10
|
-
import { n as groupsAsked } from "./shared-
|
|
9
|
+
import { loadComponent, t as rowsByPath } from "./shared-B-MhiPIN.js";
|
|
10
|
+
import { n as groupsAsked } from "./shared-CL3RLKDA.js";
|
|
11
11
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
12
12
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
// packages/mistri/src/client/components/sdk/componentKey.ts
|
|
@@ -406,4 +406,4 @@ function DashReads({ section, windowNow, picks, refresh, aside }) {
|
|
|
406
406
|
}
|
|
407
407
|
export { CustomBody, CustomDashSection, CustomPageBody, CustomPageSection };
|
|
408
408
|
|
|
409
|
-
//# sourceMappingURL=shared-
|
|
409
|
+
//# sourceMappingURL=shared-BguP_BaR.js.map
|