@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-BlxoOJSk.js","names":["percentOf","say","_Fragment","sayCopy"],"sources":["../../../kit/src/client/copy/dashboards/components/ChartShapes.ts","../../../kit/src/client/components/ChartShapes.tsx"],"sourcesContent":["/** What ChartShapes says, in the dashboards room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n scatterSummary: { one: '{count} dot: {x} across, {y} up', other: '{count} dots: {x} across, {y} up' },\n heatSummary: '{rows} by {columns}; highest {highest} at {where}',\n low: 'Low',\n high: 'High',\n waterfallSummary: '{count} changes to a total of {total}',\n total: 'Total',\n treemapSummary: { one: '{count} group; largest {largest}', other: '{count} groups; largest {largest}' },\n others: 'Others',\n});\n","/**\n * The dashboard's further chart shapes: a scatter for how two measures of the\n * same groups go together, a heat map for where a measure gathers across two keys\n * (weekday by hour), a waterfall for how rises and falls make a total, and a\n * treemap for how a whole divides when it has too many parts for a donut.\n *\n * A part on demand (ChartFigure.tsx): only a dashboard that draws one of these\n * fetches this module and its sheet. They keep Chart.tsx's grammar: one\n * series' color for one measure (a waterfall's falls in the second, which the\n * theme validator holds apart from the first for color-blind readers), a\n * categorical key in its vocabulary's order, levels in a gutter of their own,\n * words in the ink, a pill that names the point under the pointer, and every\n * value also a row of a table assistive technology reads; a treemap's tiles\n * are one hue in steps, not a colour each. Nothing moves (motion rule 8).\n */\nimport { useLayoutEffect, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';\nimport { axisMarks, periodTick } from './Chart.js';\nimport { orderGroups } from './dashboardModel.js';\nimport { formatPercent as percentOf } from '../locale.js';\nimport { say as sayCopy } from '../copy/dashboards/components/ChartShapes.js';\nimport { textMeasure } from './textMeasure.js';\nimport { niceScale } from '@mistriwork/engine/shared/niceTicks.js';\nimport './ChartShapes.css';\n\n/** How a figure is said; its `tick` is how an axis says a round level (Chart.tsx). */\ntype Say = ((value: unknown) => string) & { tick?: ((value: unknown) => string) & { steps?: readonly number[] } };\nconst tickSay = (say: Say) => say.tick ?? say;\n\nexport type ExtraShape = 'scatter' | 'heatmap' | 'waterfall' | 'treemap';\n\ninterface Answer {\n columns?: string[];\n rows?: Array<Record<string, unknown>>;\n}\n\ninterface ChartShapeProps {\n shape: ExtraShape;\n answer: Answer;\n /** The measure, as its reader says it. */\n say: Say;\n /** A scatter's `x` measure, as its reader says it. */\n sayX?: Say;\n /** A scatter's two measures by their words (\"count\", \"sum amount\"). */\n names?: { x: string; y: string };\n /** The first key's value as its reader says it, and the second's for a heat map. */\n label: (key: unknown) => string;\n seriesLabel?: (key: unknown) => string;\n vocabulary?: readonly string[];\n seriesVocabulary?: readonly string[];\n wide: number;\n high?: number;\n limit?: number;\n target?: number;\n}\n\nconst pct = (share: number) => percentOf(share, { maximumFractionDigits: 0 });\nconst f1 = (n: number) => n.toFixed(1);\n/** A period the grammar buckets by (\"2026-08\", \"2026-Q3\"), which keeps the calendar's order. */\nconst PERIOD = /^\\d{4}(-\\d{2}(-\\d{2})?|-Q[1-4])?$/;\n/** The width a tick's words take at the axis's 11px, budgeted as Chart.tsx budgets it. */\nconst tickWidth = (text: string) => text.length * 6.6 + 4;\nconst finite = (value: unknown) => Number.isFinite(Number(value)) && value !== null && value !== '';\n\n/** One key's values in a fixed order: numbers and periods ascending, a vocabulary's words in its order, the rest as read. */\nfunction keyOrder(keys: unknown[], vocabulary?: readonly string[]): string[] {\n const seen = [...new Set(keys.map((key) => (key == null ? '' : String(key))))];\n if (seen.every((key) => key !== '' && Number.isFinite(Number(key)))) {\n const sorted = seen.sort((a, b) => Number(a) - Number(b));\n /* A run of whole numbers — an hour or a weekday bucket — keeps its empty steps, so 8 AM with no moves is a\n pale column between 7 and 9, not a gap the eye cannot see. Spelled as the answer spells them (\"08\"). */\n const first = Number(sorted[0]),\n last = Number(sorted[sorted.length - 1]);\n if (sorted.every((key) => Number.isInteger(Number(key))) && last - first < 48) {\n const width = sorted[0].length;\n return Array.from({ length: last - first + 1 }, (_, i) => String(first + i).padStart(width, '0'));\n }\n return sorted;\n }\n if (seen.every((key) => PERIOD.test(key))) return seen.sort();\n const rank = new Map((vocabulary ?? []).map((word, at) => [word, at]));\n return seen\n .map((key, at) => ({ key, at, place: rank.get(key) ?? Number.POSITIVE_INFINITY }))\n .sort((a, b) => a.place - b.place || a.at - b.at)\n .map((entry) => entry.key);\n}\n\n/** The levels a scale is ruled at: its ends and its middle. */\n/** The levels a scale is ruled at: round ones covering it (shared/niceTicks.ts). */\nconst levelsOf = (min: number, max: number, steps?: readonly number[]) => niceScale(min, max, 2, steps);\n\n/** The table every drawing is also read as. */\nfunction DataTable({ head, rows }: { head?: string[]; rows: string[][] }) {\n return (\n <div className=\"visually-hidden\">\n <table className=\"chart-data\">\n {head && (\n <thead>\n <tr>\n {head.map((cell, i) => (\n <th key={i} scope=\"col\">\n {cell}\n </th>\n ))}\n </tr>\n </thead>\n )}\n <tbody>\n {rows.map((row, i) => (\n <tr key={i}>\n <th scope=\"row\">{row[0]}</th>\n {row.slice(1).map((cell, j) => (\n <td key={j}>{cell}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\n/** The pill over the pointed-at mark, as a bar chart's: kept inside the figure. */\nfunction Tip({ text, at, top, wide }: { text: string; at: number; top: number; wide: number }) {\n const width = tickWidth(text) + 12;\n const left = Math.min(Math.max(at - width / 2, 0), wide - width);\n return (\n <g className=\"chart-bar-tip\" aria-hidden=\"true\">\n <rect x={f1(left)} y={f1(Math.max(2, top))} width={f1(width)} height={20} />\n <text x={f1(left + width / 2)} y={f1(Math.max(2, top) + 14)} textAnchor=\"middle\">\n {text}\n </text>\n </g>\n );\n}\n\n/* ---------- scatter ---------- */\n\nfunction Scatter({ answer, say, sayX = say, names, label, wide, high }: ChartShapeProps) {\n const [key, xs, ys] = answer.columns ?? [];\n const dots = (answer.rows ?? [])\n .filter((row) => finite(row[xs]) && finite(row[ys]))\n .map((row) => ({ key: row[key], x: Number(row[xs]), y: Number(row[ys]) }));\n const [near, setNear] = useState<number | null>(null);\n if (!dots.length) return null;\n const W = wide;\n const H = high ? Math.max(160, Math.round(high)) : Math.max(200, Math.min(Math.round(W / 2.2), 320));\n const yScale = levelsOf(Math.min(0, ...dots.map((d) => d.y)), Math.max(...dots.map((d) => d.y)) || 1, say.tick?.steps),\n xScale = levelsOf(Math.min(0, ...dots.map((d) => d.x)), Math.max(...dots.map((d) => d.x)) || 1, sayX.tick?.steps);\n const [minX, maxX, minY, maxY] = [xScale.min, xScale.max, yScale.min, yScale.max];\n const yLevels = yScale.levels,\n xLevels = xScale.levels;\n const gutter = Math.max(...yLevels.map((v) => tickWidth(tickSay(say)(v)))) + 6;\n const PAD = { top: 28, right: Math.max(...xLevels.map((v) => tickWidth(tickSay(sayX)(v)))) / 2 + 4, bottom: 24, left: gutter };\n const x = (v: number) => PAD.left + ((v - minX) / (maxX - minX || 1)) * (W - PAD.left - PAD.right);\n const y = (v: number) => PAD.top + (1 - (v - minY) / (maxY - minY || 1)) * (H - PAD.top - PAD.bottom);\n const pick = (event: ReactPointerEvent<SVGSVGElement>) => {\n const box = event.currentTarget.getBoundingClientRect();\n if (!box.width) return;\n // screen-px: the pointer's ruler, mapped into viewBox units by the ratio.\n const px = ((event.clientX - box.left) / box.width) * W,\n py = ((event.clientY - box.top) / box.height) * H;\n let best = 0;\n dots.forEach((d, i) => {\n if (Math.hypot(x(d.x) - px, y(d.y) - py) < Math.hypot(x(dots[best].x) - px, y(dots[best].y) - py)) best = i;\n });\n setNear(best);\n };\n const active = near === null ? undefined : dots[near];\n return (\n <>\n <svg\n className=\"chart-scatter\"\n viewBox={`0 0 ${W} ${H}`}\n role=\"img\"\n aria-label={sayCopy('scatterSummary', { count: dots.length, x: names?.x ?? '', y: names?.y ?? '' })}\n onPointerMove={pick}\n onPointerLeave={() => setNear(null)}\n >\n {yLevels.map((v) => (\n <g key={`y${v}`}>\n <line className=\"chart-grid\" x1={PAD.left} x2={W - PAD.right} y1={f1(y(v))} y2={f1(y(v))} />\n <text className=\"chart-tick chart-y-tick\" x={f1(PAD.left - 6)} y={f1(y(v) + 4)} textAnchor=\"end\">\n {tickSay(say)(v)}\n </text>\n </g>\n ))}\n {xLevels.map((v) => (\n <text key={`x${v}`} className=\"chart-tick chart-x-tick\" x={f1(x(v))} y={H - 6} textAnchor=\"middle\">\n {tickSay(sayX)(v)}\n </text>\n ))}\n {dots.map((d, i) => (\n <circle key={i} className={`chart-scatter-dot${i === near ? ' is-active' : ''}`} cx={f1(x(d.x))} cy={f1(y(d.y))} r={4.5}>\n <title>{`${label(d.key)}: ${sayX(d.x)}, ${say(d.y)}`}</title>\n </circle>\n ))}\n {active && (\n <Tip text={`${label(active.key)} · ${sayX(active.x)} · ${say(active.y)}`} at={x(active.x)} top={y(active.y) - 30} wide={W} />\n )}\n </svg>\n <DataTable head={['', names?.x ?? '', names?.y ?? '']} rows={dots.map((d) => [label(d.key), sayX(d.x), say(d.y)])} />\n </>\n );\n}\n\n/* ---------- heat map ---------- */\n\n/** A cell's shade: one of five steps of the first series over the surface, so a step reads apart from the next. */\nconst HEAT_STEPS = [14, 30, 46, 64, 82];\n\n/**\n * Every how many columns a heat map names its column, so that every name it shows is whole: a row of hours at a\n * half width read \"10 …\", \"11 …\". Each name is measured at the head's 11px against a column's share of the grid,\n * after the row names' 8ch and the cells' 2px spacing; a name that is not shown stays for a reader.\n */\nfunction headEvery(names: readonly string[], wide: number, measure = textMeasure('400 11px system-ui, sans-serif')): number {\n if (!names.length || !wide) return 1;\n const column = (wide - 88) / names.length - 2;\n const widest = Math.max(...names.map((name) => (measure ? measure(name) : name.length * 6)));\n return column > 0 ? Math.max(1, Math.ceil((widest + 4) / column)) : 1;\n}\n\nfunction Heatmap({ answer, say, label, seriesLabel = String, vocabulary, seriesVocabulary, wide }: ChartShapeProps) {\n const [rowKey, colKey, value] = answer.columns ?? [];\n const rowsIn = answer.rows ?? [];\n const [near, setNear] = useState<{ row: string; col: string } | null>(null);\n if (!rowsIn.length) return null;\n const rowKeys = keyOrder(\n rowsIn.map((row) => row[rowKey]),\n vocabulary,\n );\n const colKeys = keyOrder(\n rowsIn.map((row) => row[colKey]),\n seriesVocabulary,\n );\n const cells = new Map<string, number>();\n for (const row of rowsIn) cells.set(`${row[rowKey] ?? ''}\\u0000${row[colKey] ?? ''}`, Number(row[value]) || 0);\n const values = [...cells.values()];\n const max = Math.max(...values, 0);\n const min = Math.min(...values.filter((v) => v > 0), max);\n let peak = { row: rowKeys[0], col: colKeys[0], value: -Infinity };\n for (const [at, v] of cells) if (v > peak.value) peak = { row: at.split('\\u0000')[0], col: at.split('\\u0000')[1], value: v };\n const step = (v: number) => (v <= 0 || max <= 0 ? -1 : Math.min(HEAT_STEPS.length - 1, Math.floor((v / max) * HEAT_STEPS.length - 1e-9)));\n const hovered = near ? cells.get(`${near.row}\\u0000${near.col}`) : undefined;\n const every = headEvery(\n colKeys.map((col) => seriesLabel(col)),\n wide,\n );\n const summary = sayCopy('heatSummary', {\n rows: rowKeys.length,\n columns: colKeys.length,\n highest: say(peak.value),\n where: `${label(peak.row)}, ${seriesLabel(peak.col)}`,\n });\n return (\n <div className=\"chart-heat\">\n {/* The grid may scroll in its own box (a phone's card): a keyboard reaches it, as a pointer does. */}\n <div className=\"chart-heat-scroll\" data-scrolls=\"\" role=\"region\" aria-label={summary} tabIndex={0}>\n <table\n className=\"chart-heat-grid\"\n style={{ '--heat-cols': colKeys.length } as CSSProperties}\n aria-label={summary}\n onPointerLeave={() => setNear(null)}\n >\n <thead>\n <tr>\n <td />\n {colKeys.map((col, i) => (\n <th key={col} scope=\"col\" className={`chart-heat-col${every > 1 && i === colKeys.length - 1 ? ' is-end' : ''}`}>\n {i % every === 0 ? seriesLabel(col) : <span className=\"visually-hidden\">{seriesLabel(col)}</span>}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rowKeys.map((row) => (\n <tr key={row}>\n <th scope=\"row\" className=\"chart-heat-row\">\n {label(row)}\n </th>\n {colKeys.map((col) => {\n const v = cells.get(`${row}\\u0000${col}`) ?? 0;\n const shade = step(v);\n return (\n <td\n key={col}\n className={`chart-heat-cell${near?.row === row && near.col === col ? ' is-active' : ''}`}\n style={shade < 0 ? undefined : ({ '--heat': `${HEAT_STEPS[shade]}%` } as CSSProperties)}\n onPointerEnter={() => setNear({ row, col })}\n >\n <span className=\"visually-hidden\">{say(v)}</span>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"chart-heat-foot\">\n <p className=\"chart-heat-said\" aria-hidden=\"true\">\n {near ? `${label(near.row)} · ${seriesLabel(near.col)} — ${say(hovered ?? 0)}` : ' '}\n </p>\n <span className=\"chart-heat-legend\" aria-hidden=\"true\">\n {sayCopy('low')} {say(min)}\n {HEAT_STEPS.map((shade) => (\n <span key={shade} className=\"chart-heat-key\" style={{ '--heat': `${shade}%` } as CSSProperties} />\n ))}\n {say(max)} {sayCopy('high')}\n </span>\n </div>\n </div>\n );\n}\n\n/* ---------- waterfall ---------- */\n\nfunction Waterfall({ answer, say, label, vocabulary, wide, high, limit, target }: ChartShapeProps) {\n const [key, value] = answer.columns ?? [];\n const rowsIn = answer.rows ?? [];\n const periods = rowsIn.length > 0 && rowsIn.every((row) => row[key] != null && PERIOD.test(String(row[key])));\n const ordered = (\n periods ? [...rowsIn].sort((a, b) => String(a[key]).localeCompare(String(b[key]))) : orderGroups(rowsIn, key, value, vocabulary)\n ).slice(0, limit);\n const [near, setNear] = useState<number | null>(null);\n if (!ordered.length) return null;\n let running = 0;\n const steps = ordered.map((row) => {\n const change = Number(row[value]) || 0;\n const from = running;\n running += change;\n return { key: row[key], change, from, to: running, total: false };\n });\n const bars = [...steps, { key: null, change: running, from: 0, to: running, total: true }];\n const names = bars.map((bar, i) =>\n bar.total ? sayCopy('total') : periods ? periodTick(String(bar.key), i ? String(bars[i - 1].key) : undefined) : label(bar.key),\n );\n const W = wide;\n const H = high ? Math.max(120, Math.round(high)) : Math.max(180, Math.min(Math.round(W / 3), 260));\n const goal = target !== undefined && Number.isFinite(target) ? target : undefined;\n const max = Math.max(0, goal ?? 0, ...bars.map((bar) => Math.max(bar.from, bar.to)));\n const min = Math.min(0, ...bars.map((bar) => Math.min(bar.from, bar.to)));\n const scale = levelsOf(min, max, say.tick?.steps);\n const levels = scale.levels;\n const span = scale.max - scale.min || 1;\n const PAD = { top: 30, right: 6, bottom: 24, left: Math.max(...levels.map((v) => tickWidth(tickSay(say)(v)))) + 6 };\n const band = (W - PAD.left - PAD.right) / bars.length;\n const barWide = Math.max(2, Math.min(48, band * 0.64));\n const mid = (i: number) => PAD.left + band * (i + 0.5);\n const y = (v: number) => PAD.top + (1 - (v - scale.min) / span) * (H - PAD.top - PAD.bottom);\n const marks = axisMarks(names, mid, { centered: true });\n const active = near ?? bars.length - 1;\n const said = (bar: (typeof bars)[number]) =>\n bar.total\n ? `${sayCopy('total')} · ${say(bar.to)}`\n : `${names[bars.indexOf(bar)]} · ${bar.change >= 0 ? '+' : '−'}${say(Math.abs(bar.change))}`;\n return (\n <>\n <svg\n className=\"chart-waterfall\"\n viewBox={`0 0 ${W} ${H}`}\n role=\"img\"\n aria-label={sayCopy('waterfallSummary', { count: steps.length, total: say(running) })}\n onPointerMove={(event) => {\n const box = event.currentTarget.getBoundingClientRect();\n if (!box.width) return;\n // screen-px: the pointer's ruler, mapped into viewBox units by the ratio.\n const px = ((event.clientX - box.left) / box.width) * W;\n setNear(Math.min(bars.length - 1, Math.max(0, Math.floor((px - PAD.left) / band))));\n }}\n onPointerLeave={() => setNear(null)}\n >\n {levels.map((v) => (\n <g key={`l${v}`}>\n <line className=\"chart-grid\" x1={PAD.left} x2={W - PAD.right} y1={f1(y(v))} y2={f1(y(v))} />\n <text className=\"chart-tick chart-y-tick\" x={f1(PAD.left - 6)} y={f1(y(v) + 4)} textAnchor=\"end\">\n {tickSay(say)(v)}\n </text>\n </g>\n ))}\n {bars.map((bar, i) => {\n const top = y(Math.max(bar.from, bar.to)),\n bottom = y(Math.min(bar.from, bar.to));\n const kind = bar.total ? 'is-total' : bar.change >= 0 ? 'is-rise' : 'is-fall';\n return (\n <g key={i}>\n {i > 0 && (\n <line\n className=\"chart-waterfall-join\"\n x1={f1(mid(i - 1) + barWide / 2)}\n x2={f1(mid(i) - barWide / 2)}\n y1={f1(y(bars[i - 1].to))}\n y2={f1(y(bars[i - 1].to))}\n />\n )}\n <rect\n className={`chart-waterfall-bar ${kind}${i === active ? ' is-active' : ''}`}\n x={f1(mid(i) - barWide / 2)}\n y={f1(top)}\n width={f1(barWide)}\n height={f1(Math.max(1, bottom - top))}\n >\n <title>{said(bar)}</title>\n </rect>\n </g>\n );\n })}\n {goal !== undefined && (\n <g className=\"chart-target\">\n <line className=\"chart-target-line\" x1={PAD.left} x2={W - PAD.right} y1={f1(y(goal))} y2={f1(y(goal))} />\n </g>\n )}\n {marks.map(({ index }) => (\n <text\n key={`m${index}`}\n className={`chart-tick chart-x-tick${index === active ? ' is-active' : ''}`}\n x={f1(Math.min(Math.max(mid(index), tickWidth(names[index]) / 2), W - tickWidth(names[index]) / 2))}\n y={H - 6}\n textAnchor=\"middle\"\n >\n {names[index]}\n </text>\n ))}\n <Tip text={said(bars[active])} at={mid(active)} top={y(Math.max(bars[active].from, bars[active].to)) - 26} wide={W} />\n </svg>\n <DataTable\n rows={bars.map((bar, i) => [names[i], bar.total ? say(bar.to) : `${bar.change >= 0 ? '+' : '−'}${say(Math.abs(bar.change))}`])}\n />\n </>\n );\n}\n\n/* ---------- treemap ---------- */\n\ninterface Tile {\n key: unknown;\n value: number;\n series: number;\n other?: boolean;\n}\ninterface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Tiles laid out squarified (Bruls, Huizing and van Wijk): each row of tiles\n * along the shorter side takes the next largest while that keeps its worst\n * aspect ratio from getting worse, so tiles stay near square and a size is\n * read as an area. In units of the box, which is drawn at any width.\n */\nexport function squarify(values: readonly number[], box: Box): Box[] {\n const out: Box[] = [];\n const total = values.reduce((sum, v) => sum + v, 0);\n if (total <= 0) return values.map(() => ({ x: box.x, y: box.y, w: 0, h: 0 }));\n const scale = (box.w * box.h) / total;\n let { x, y, w, h } = box;\n let at = 0;\n const worst = (row: number[], side: number) => {\n const sum = row.reduce((s, v) => s + v, 0) * scale;\n const most = Math.max(...row) * scale,\n least = Math.min(...row) * scale;\n return Math.max((side * side * most) / (sum * sum), (sum * sum) / (side * side * least));\n };\n while (at < values.length) {\n const side = Math.min(w, h);\n const row = [values[at]];\n while (at + row.length < values.length && worst([...row, values[at + row.length]], side) <= worst(row, side))\n row.push(values[at + row.length]);\n const area = row.reduce((s, v) => s + v, 0) * scale;\n if (w >= h) {\n const across = area / h;\n let down = y;\n for (const v of row) {\n const tall = (v * scale) / across;\n out.push({ x, y: down, w: across, h: tall });\n down += tall;\n }\n x += across;\n w -= across;\n } else {\n const tall = area / w;\n let along = x;\n for (const v of row) {\n const across = (v * scale) / tall;\n out.push({ x: along, y, w: across, h: tall });\n along += across;\n }\n y += tall;\n h -= tall;\n }\n at += row.length;\n }\n return out;\n}\n\n/* A tile's name: 500 12px on 1.25 lines, inside 8px by 6px of padding and a 2px border; its figure a 15px line under a 2px gap. */\nconst TILE_X = 20;\nconst TILE_Y = 16;\nconst TILE_VALUE = 17;\n\n/**\n * How a tile says its name, or null where it cannot say it whole: on one\n * line in 12px; else on two (three in a tall tile), broken between words;\n * else the same at 11px;\n * else not at all, and the tile's tooltip and the data table carry it.\n * Cut with an ellipsis, Depot's tiles read \"Slee…\" and \"Tires …\" — a name\n * that names nothing. `measure` gives a text's width at 12px.\n */\nexport function tileLabel(\n name: string,\n w: number,\n h: number,\n measure: (text: string) => number,\n): { size: 12 | 11; lines: number; value: boolean } | null {\n const room = w - TILE_X;\n const high = h - TILE_Y;\n for (const size of [12, 11] as const) {\n const width = (text: string) => (measure(text) * size) / 12;\n const line = size * 1.25;\n if (high < line) continue;\n const value = (lines: number) => high >= lines * line + TILE_VALUE;\n if (width(name) <= room) return { size, lines: 1, value: value(1) };\n if (high < 2 * line) continue;\n const words = name.split(/\\s+/).filter(Boolean);\n if (words.some((word) => width(word) > room)) continue;\n let lines = 1;\n let current = '';\n for (const word of words) {\n const next = current ? `${current} ${word}` : word;\n if (width(next) <= room) current = next;\n else {\n lines += 1;\n current = word;\n }\n }\n if (lines <= Math.min(3, Math.floor(high / line))) return { size, lines, value: value(lines) };\n }\n return null;\n}\n\n/** At most this many tiles; the rest are one tile in the quiet ink. */\nconst TILES = 8;\n\n/* One hue in steps, the largest deepest: a tile's size is read as its area, so a colour per tile said nothing more\n and five pastels made the treemap the loudest thing on its page. From 88% of the first series down to 24%, so the\n largest reads as the accent itself; a tile from DEEP_TILE up says its words in the ground's colour. */\nconst tileStep = (i: number, count: number) => Math.round(88 - (count > 1 ? (64 * i) / (count - 1) : 0));\nconst DEEP_TILE = 56;\n\ntype Rgb = readonly [number, number, number];\n/** The colours a tile is mixed from and said in, as the page resolves them. */\ninterface TileInks {\n series: Rgb;\n surface: Rgb;\n text: Rgb;\n ground: Rgb;\n}\n\n/** A colour as the page computes it: `rgb(…)`, or `color(srgb …)` in 0–1. */\nfunction rgbOf(said: string): Rgb | undefined {\n const srgb = /color\\(srgb\\s+([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)/.exec(said);\n if (srgb) return [Number(srgb[1]) * 255, Number(srgb[2]) * 255, Number(srgb[3]) * 255];\n const rgb = /rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)/.exec(said);\n return rgb ? [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])] : undefined;\n}\n\nfunction luminance([r, g, b]: Rgb): number {\n const lin = (c: number) => (c / 255 <= 0.04045 ? c / 255 / 12.92 : ((c / 255 + 0.055) / 1.055) ** 2.4);\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\nfunction contrast(a: Rgb, b: Rgb): number {\n const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);\n return (x + 0.05) / (y + 0.05);\n}\n\n/**\n * A tile's step and ink: the ground's colour where it reads on the step at 4.5:1, else the ink; where neither does\n * — a mid step of a deep hue, between the two — the step is lightened until the ink reads. Without the page's colours\n * (before the first paint), the plain threshold.\n */\nfunction tileInk(step: number, inks: TileInks | null): { step: number; deep: boolean } {\n if (!inks) return { step, deep: step >= DEEP_TILE };\n const mixed = (share: number): Rgb =>\n [0, 1, 2].map((c) => (inks.series[c] * share + inks.surface[c] * (100 - share)) / 100) as unknown as Rgb;\n if (contrast(mixed(step), inks.ground) >= 4.5) return { step, deep: true };\n let lighter = step;\n while (lighter > 0 && contrast(mixed(lighter), inks.text) < 4.5) lighter -= 2;\n return { step: Math.max(0, lighter), deep: false };\n}\n\nfunction Treemap({ answer, say, label, wide, high, limit }: ChartShapeProps) {\n const [key, value] = answer.columns ?? [];\n const groups = (answer.rows ?? [])\n .map((row) => ({ key: row[key], value: Number(row[value]) || 0 }))\n .filter((group) => group.value > 0)\n .sort((a, b) => b.value - a.value)\n .slice(0, limit ?? undefined);\n const [near, setNear] = useState<number | null>(null);\n /* The tiles' names are measured in the face they are set in, once it is known. */\n const map = useRef<HTMLDivElement>(null);\n const [face, setFace] = useState<string | null>(null);\n const [inks, setInks] = useState<TileInks | null>(null);\n useLayoutEffect(() => {\n const host = map.current;\n if (!host) return;\n setFace(getComputedStyle(host).fontFamily || null);\n /* Each colour resolved as the page has it, through a probe: the tokens may be any CSS colour, and a look sets them. */\n const probe = document.createElement('span');\n probe.hidden = true;\n host.append(probe);\n const read = (color: string) => {\n probe.style.color = color;\n return rgbOf(getComputedStyle(probe).color);\n };\n const [series, surface, text, ground] = ['var(--chart-1, var(--accent))', 'var(--surface)', 'var(--ink)', 'var(--ground)'].map(read);\n probe.remove();\n if (series && surface && text && ground) setInks({ series, surface, text, ground });\n }, []);\n if (!groups.length) return null;\n const measured = face ? textMeasure(`500 12px ${face}`) : undefined;\n const measure = measured ?? ((text: string) => text.length * 6.6);\n const kept: Tile[] = groups.slice(0, groups.length > TILES ? TILES - 1 : TILES).map((group, i) => ({ ...group, series: i + 1 }));\n const rest = groups.slice(kept.length);\n if (rest.length) kept.push({ key: null, value: rest.reduce((sum, group) => sum + group.value, 0), series: 0, other: true });\n const W = wide;\n const H = high ? Math.max(160, Math.round(high)) : Math.max(200, Math.min(Math.round(W / 2), 320));\n const boxes = squarify(\n kept.map((tile) => tile.value),\n { x: 0, y: 0, w: W, h: H },\n );\n const whole = kept.reduce((sum, tile) => sum + tile.value, 0);\n const name = (tile: Tile) => (tile.other ? `${sayCopy('others')} (${rest.length})` : label(tile.key));\n return (\n <>\n <div\n ref={map}\n className=\"chart-treemap\"\n style={{ aspectRatio: `${W} / ${H}` }}\n role=\"img\"\n aria-label={sayCopy('treemapSummary', { count: groups.length, largest: `${label(groups[0].key)} ${say(groups[0].value)}` })}\n onPointerLeave={() => setNear(null)}\n >\n {kept.map((tile, i) => {\n const box = boxes[i];\n const said = tileLabel(name(tile), box.w, box.h, measure);\n const ink = tile.other ? undefined : tileInk(tileStep(i, kept.length - (rest.length ? 1 : 0)), inks);\n return (\n <div\n key={i}\n className={`chart-tile${tile.other ? ' is-other' : ''}${ink?.deep ? ' is-deep' : ''}${said?.size === 11 ? ' is-small' : ''}${near === i ? ' is-active' : ''}`}\n style={\n {\n left: `${(box.x / W) * 100}%`,\n top: `${(box.y / H) * 100}%`,\n width: `${(box.w / W) * 100}%`,\n height: `${(box.h / H) * 100}%`,\n ...(ink ? { '--tile-step': `${ink.step}%` } : {}),\n } as CSSProperties\n }\n title={`${name(tile)}: ${say(tile.value)} (${pct((tile.value / whole) * 100)})`}\n onPointerEnter={() => setNear(i)}\n >\n {said && (\n <>\n <span className=\"chart-tile-name\">{name(tile)}</span>\n {said.value && <span className=\"chart-tile-value\">{say(tile.value)}</span>}\n </>\n )}\n </div>\n );\n })}\n </div>\n <p className=\"chart-heat-said\" aria-hidden=\"true\">\n {near === null ? ' ' : `${name(kept[near])} — ${say(kept[near].value)} · ${pct((kept[near].value / whole) * 100)}`}\n </p>\n <DataTable rows={kept.map((tile) => [name(tile), say(tile.value), pct((tile.value / whole) * 100)])} />\n </>\n );\n}\n\n/** The drawing for one of the further shapes, or nothing where the answer has nothing it can draw. */\nexport function ChartShape(props: ChartShapeProps) {\n const Shape = { scatter: Scatter, heatmap: Heatmap, waterfall: Waterfall, treemap: Treemap }[props.shape];\n return <Shape {...props} />;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shared-BPpC08ip.js","names":["percentOf","say","_Fragment","sayCopy"],"sources":["../../../kit/src/client/copy/dashboards/components/ChartShapes.ts","../../../kit/src/client/components/ChartShapes.tsx"],"sourcesContent":["/** What ChartShapes says, in the dashboards room (packages/kit/src/client/messages.ts says how the catalog is split). */\nimport { defineMessages } from '../../../messages.js';\n\nexport const say = defineMessages({\n scatterSummary: { one: '{count} dot: {x} across, {y} up', other: '{count} dots: {x} across, {y} up' },\n heatSummary: '{rows} by {columns}; highest {highest} at {where}',\n low: 'Low',\n high: 'High',\n waterfallSummary: '{count} changes to a total of {total}',\n total: 'Total',\n treemapSummary: { one: '{count} group; largest {largest}', other: '{count} groups; largest {largest}' },\n others: 'Others',\n});\n","/**\n * The dashboard's further chart shapes: a scatter for how two measures of the\n * same groups go together, a heat map for where a measure gathers across two keys\n * (weekday by hour), a waterfall for how rises and falls make a total, and a\n * treemap for how a whole divides when it has too many parts for a donut.\n *\n * A part on demand (ChartFigure.tsx): only a dashboard that draws one of these\n * fetches this module and its sheet. They keep Chart.tsx's grammar: one\n * series' color for one measure (a waterfall's falls in the second, which the\n * theme validator holds apart from the first for color-blind readers), a\n * categorical key in its vocabulary's order, levels in a gutter of their own,\n * words in the ink, a pill that names the point under the pointer, and every\n * value also a row of a table assistive technology reads; a treemap's tiles\n * are one hue in steps, not a colour each. Nothing moves (motion rule 8).\n */\nimport { useLayoutEffect, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';\nimport { axisMarks, periodTick } from './Chart.js';\nimport { orderGroups } from './dashboardModel.js';\nimport { formatPercent as percentOf } from '../locale.js';\nimport { say as sayCopy } from '../copy/dashboards/components/ChartShapes.js';\nimport { textMeasure } from './textMeasure.js';\nimport { niceScale } from '@mistriwork/engine/shared/niceTicks.js';\nimport './ChartShapes.css';\n\n/** How a figure is said; its `tick` is how an axis says a round level (Chart.tsx). */\ntype Say = ((value: unknown) => string) & { tick?: ((value: unknown) => string) & { steps?: readonly number[] } };\nconst tickSay = (say: Say) => say.tick ?? say;\n\nexport type ExtraShape = 'scatter' | 'heatmap' | 'waterfall' | 'treemap';\n\ninterface Answer {\n columns?: string[];\n rows?: Array<Record<string, unknown>>;\n}\n\ninterface ChartShapeProps {\n shape: ExtraShape;\n answer: Answer;\n /** The measure, as its reader says it. */\n say: Say;\n /** A scatter's `x` measure, as its reader says it. */\n sayX?: Say;\n /** A scatter's two measures by their words (\"count\", \"sum amount\"). */\n names?: { x: string; y: string };\n /** The first key's value as its reader says it, and the second's for a heat map. */\n label: (key: unknown) => string;\n seriesLabel?: (key: unknown) => string;\n vocabulary?: readonly string[];\n seriesVocabulary?: readonly string[];\n wide: number;\n high?: number;\n limit?: number;\n target?: number;\n}\n\nconst pct = (share: number) => percentOf(share, { maximumFractionDigits: 0 });\nconst f1 = (n: number) => n.toFixed(1);\n/** A period the grammar buckets by (\"2026-08\", \"2026-Q3\"), which keeps the calendar's order. */\nconst PERIOD = /^\\d{4}(-\\d{2}(-\\d{2})?|-Q[1-4])?$/;\n/** The width a tick's words take at the axis's 11px, budgeted as Chart.tsx budgets it. */\nconst tickWidth = (text: string) => text.length * 6.6 + 4;\nconst finite = (value: unknown) => Number.isFinite(Number(value)) && value !== null && value !== '';\n\n/** One key's values in a fixed order: numbers and periods ascending, a vocabulary's words in its order, the rest as read. */\nfunction keyOrder(keys: unknown[], vocabulary?: readonly string[]): string[] {\n const seen = [...new Set(keys.map((key) => (key == null ? '' : String(key))))];\n if (seen.every((key) => key !== '' && Number.isFinite(Number(key)))) {\n const sorted = seen.sort((a, b) => Number(a) - Number(b));\n /* A run of whole numbers — an hour or a weekday bucket — keeps its empty steps, so 8 AM with no moves is a\n pale column between 7 and 9, not a gap the eye cannot see. Spelled as the answer spells them (\"08\"). */\n const first = Number(sorted[0]),\n last = Number(sorted[sorted.length - 1]);\n if (sorted.every((key) => Number.isInteger(Number(key))) && last - first < 48) {\n const width = sorted[0].length;\n return Array.from({ length: last - first + 1 }, (_, i) => String(first + i).padStart(width, '0'));\n }\n return sorted;\n }\n if (seen.every((key) => PERIOD.test(key))) return seen.sort();\n const rank = new Map((vocabulary ?? []).map((word, at) => [word, at]));\n return seen\n .map((key, at) => ({ key, at, place: rank.get(key) ?? Number.POSITIVE_INFINITY }))\n .sort((a, b) => a.place - b.place || a.at - b.at)\n .map((entry) => entry.key);\n}\n\n/** The levels a scale is ruled at: its ends and its middle. */\n/** The levels a scale is ruled at: round ones covering it (shared/niceTicks.ts). */\nconst levelsOf = (min: number, max: number, steps?: readonly number[]) => niceScale(min, max, 2, steps);\n\n/** The table every drawing is also read as. */\nfunction DataTable({ head, rows }: { head?: string[]; rows: string[][] }) {\n return (\n <div className=\"visually-hidden\">\n <table className=\"chart-data\">\n {head && (\n <thead>\n <tr>\n {head.map((cell, i) => (\n <th key={i} scope=\"col\">\n {cell}\n </th>\n ))}\n </tr>\n </thead>\n )}\n <tbody>\n {rows.map((row, i) => (\n <tr key={i}>\n <th scope=\"row\">{row[0]}</th>\n {row.slice(1).map((cell, j) => (\n <td key={j}>{cell}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n\n/** The pill over the pointed-at mark, as a bar chart's: kept inside the figure. */\nfunction Tip({ text, at, top, wide }: { text: string; at: number; top: number; wide: number }) {\n const width = tickWidth(text) + 12;\n const left = Math.min(Math.max(at - width / 2, 0), wide - width);\n return (\n <g className=\"chart-bar-tip\" aria-hidden=\"true\">\n <rect x={f1(left)} y={f1(Math.max(2, top))} width={f1(width)} height={20} />\n <text x={f1(left + width / 2)} y={f1(Math.max(2, top) + 14)} textAnchor=\"middle\">\n {text}\n </text>\n </g>\n );\n}\n\n/* ---------- scatter ---------- */\n\nfunction Scatter({ answer, say, sayX = say, names, label, wide, high }: ChartShapeProps) {\n const [key, xs, ys] = answer.columns ?? [];\n const dots = (answer.rows ?? [])\n .filter((row) => finite(row[xs]) && finite(row[ys]))\n .map((row) => ({ key: row[key], x: Number(row[xs]), y: Number(row[ys]) }));\n const [near, setNear] = useState<number | null>(null);\n if (!dots.length) return null;\n const W = wide;\n const H = high ? Math.max(160, Math.round(high)) : Math.max(200, Math.min(Math.round(W / 2.2), 320));\n const yScale = levelsOf(Math.min(0, ...dots.map((d) => d.y)), Math.max(...dots.map((d) => d.y)) || 1, say.tick?.steps),\n xScale = levelsOf(Math.min(0, ...dots.map((d) => d.x)), Math.max(...dots.map((d) => d.x)) || 1, sayX.tick?.steps);\n const [minX, maxX, minY, maxY] = [xScale.min, xScale.max, yScale.min, yScale.max];\n const yLevels = yScale.levels,\n xLevels = xScale.levels;\n const gutter = Math.max(...yLevels.map((v) => tickWidth(tickSay(say)(v)))) + 6;\n const PAD = { top: 28, right: Math.max(...xLevels.map((v) => tickWidth(tickSay(sayX)(v)))) / 2 + 4, bottom: 24, left: gutter };\n const x = (v: number) => PAD.left + ((v - minX) / (maxX - minX || 1)) * (W - PAD.left - PAD.right);\n const y = (v: number) => PAD.top + (1 - (v - minY) / (maxY - minY || 1)) * (H - PAD.top - PAD.bottom);\n const pick = (event: ReactPointerEvent<SVGSVGElement>) => {\n const box = event.currentTarget.getBoundingClientRect();\n if (!box.width) return;\n // screen-px: the pointer's ruler, mapped into viewBox units by the ratio.\n const px = ((event.clientX - box.left) / box.width) * W,\n py = ((event.clientY - box.top) / box.height) * H;\n let best = 0;\n dots.forEach((d, i) => {\n if (Math.hypot(x(d.x) - px, y(d.y) - py) < Math.hypot(x(dots[best].x) - px, y(dots[best].y) - py)) best = i;\n });\n setNear(best);\n };\n const active = near === null ? undefined : dots[near];\n return (\n <>\n <svg\n className=\"chart-scatter\"\n viewBox={`0 0 ${W} ${H}`}\n role=\"img\"\n aria-label={sayCopy('scatterSummary', { count: dots.length, x: names?.x ?? '', y: names?.y ?? '' })}\n onPointerMove={pick}\n onPointerLeave={() => setNear(null)}\n >\n {yLevels.map((v) => (\n <g key={`y${v}`}>\n <line className=\"chart-grid\" x1={PAD.left} x2={W - PAD.right} y1={f1(y(v))} y2={f1(y(v))} />\n <text className=\"chart-tick chart-y-tick\" x={f1(PAD.left - 6)} y={f1(y(v) + 4)} textAnchor=\"end\">\n {tickSay(say)(v)}\n </text>\n </g>\n ))}\n {xLevels.map((v) => (\n <text key={`x${v}`} className=\"chart-tick chart-x-tick\" x={f1(x(v))} y={H - 6} textAnchor=\"middle\">\n {tickSay(sayX)(v)}\n </text>\n ))}\n {dots.map((d, i) => (\n <circle key={i} className={`chart-scatter-dot${i === near ? ' is-active' : ''}`} cx={f1(x(d.x))} cy={f1(y(d.y))} r={4.5}>\n <title>{`${label(d.key)}: ${sayX(d.x)}, ${say(d.y)}`}</title>\n </circle>\n ))}\n {active && (\n <Tip text={`${label(active.key)} · ${sayX(active.x)} · ${say(active.y)}`} at={x(active.x)} top={y(active.y) - 30} wide={W} />\n )}\n </svg>\n <DataTable head={['', names?.x ?? '', names?.y ?? '']} rows={dots.map((d) => [label(d.key), sayX(d.x), say(d.y)])} />\n </>\n );\n}\n\n/* ---------- heat map ---------- */\n\n/** A cell's shade: one of five steps of the first series over the surface, so a step reads apart from the next. */\nconst HEAT_STEPS = [14, 30, 46, 64, 82];\n\n/**\n * Every how many columns a heat map names its column, so that every name it shows is whole: a row of hours at a\n * half width read \"10 …\", \"11 …\". Each name is measured at the head's 11px against a column's share of the grid,\n * after the row names' 8ch and the cells' 2px spacing; a name that is not shown stays for a reader.\n */\nfunction headEvery(names: readonly string[], wide: number, measure = textMeasure('400 11px system-ui, sans-serif')): number {\n if (!names.length || !wide) return 1;\n const column = (wide - 88) / names.length - 2;\n const widest = Math.max(...names.map((name) => (measure ? measure(name) : name.length * 6)));\n return column > 0 ? Math.max(1, Math.ceil((widest + 4) / column)) : 1;\n}\n\nfunction Heatmap({ answer, say, label, seriesLabel = String, vocabulary, seriesVocabulary, wide }: ChartShapeProps) {\n const [rowKey, colKey, value] = answer.columns ?? [];\n const rowsIn = answer.rows ?? [];\n const [near, setNear] = useState<{ row: string; col: string } | null>(null);\n if (!rowsIn.length) return null;\n const rowKeys = keyOrder(\n rowsIn.map((row) => row[rowKey]),\n vocabulary,\n );\n const colKeys = keyOrder(\n rowsIn.map((row) => row[colKey]),\n seriesVocabulary,\n );\n const cells = new Map<string, number>();\n for (const row of rowsIn) cells.set(`${row[rowKey] ?? ''}\\u0000${row[colKey] ?? ''}`, Number(row[value]) || 0);\n const values = [...cells.values()];\n const max = Math.max(...values, 0);\n const min = Math.min(...values.filter((v) => v > 0), max);\n let peak = { row: rowKeys[0], col: colKeys[0], value: -Infinity };\n for (const [at, v] of cells) if (v > peak.value) peak = { row: at.split('\\u0000')[0], col: at.split('\\u0000')[1], value: v };\n const step = (v: number) => (v <= 0 || max <= 0 ? -1 : Math.min(HEAT_STEPS.length - 1, Math.floor((v / max) * HEAT_STEPS.length - 1e-9)));\n const hovered = near ? cells.get(`${near.row}\\u0000${near.col}`) : undefined;\n const every = headEvery(\n colKeys.map((col) => seriesLabel(col)),\n wide,\n );\n const summary = sayCopy('heatSummary', {\n rows: rowKeys.length,\n columns: colKeys.length,\n highest: say(peak.value),\n where: `${label(peak.row)}, ${seriesLabel(peak.col)}`,\n });\n return (\n <div className=\"chart-heat\">\n {/* The grid may scroll in its own box (a phone's card): a keyboard reaches it, as a pointer does. */}\n <div className=\"chart-heat-scroll\" data-scrolls=\"\" role=\"region\" aria-label={summary} tabIndex={0}>\n <table\n className=\"chart-heat-grid\"\n style={{ '--heat-cols': colKeys.length } as CSSProperties}\n aria-label={summary}\n onPointerLeave={() => setNear(null)}\n >\n <thead>\n <tr>\n <td />\n {colKeys.map((col, i) => (\n <th key={col} scope=\"col\" className={`chart-heat-col${every > 1 && i === colKeys.length - 1 ? ' is-end' : ''}`}>\n {i % every === 0 ? seriesLabel(col) : <span className=\"visually-hidden\">{seriesLabel(col)}</span>}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rowKeys.map((row) => (\n <tr key={row}>\n <th scope=\"row\" className=\"chart-heat-row\">\n {label(row)}\n </th>\n {colKeys.map((col) => {\n const v = cells.get(`${row}\\u0000${col}`) ?? 0;\n const shade = step(v);\n return (\n <td\n key={col}\n className={`chart-heat-cell${near?.row === row && near.col === col ? ' is-active' : ''}`}\n style={shade < 0 ? undefined : ({ '--heat': `${HEAT_STEPS[shade]}%` } as CSSProperties)}\n onPointerEnter={() => setNear({ row, col })}\n >\n <span className=\"visually-hidden\">{say(v)}</span>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"chart-heat-foot\">\n <p className=\"chart-heat-said\" aria-hidden=\"true\">\n {near ? `${label(near.row)} · ${seriesLabel(near.col)} — ${say(hovered ?? 0)}` : ' '}\n </p>\n <span className=\"chart-heat-legend\" aria-hidden=\"true\">\n {sayCopy('low')} {say(min)}\n {HEAT_STEPS.map((shade) => (\n <span key={shade} className=\"chart-heat-key\" style={{ '--heat': `${shade}%` } as CSSProperties} />\n ))}\n {say(max)} {sayCopy('high')}\n </span>\n </div>\n </div>\n );\n}\n\n/* ---------- waterfall ---------- */\n\nfunction Waterfall({ answer, say, label, vocabulary, wide, high, limit, target }: ChartShapeProps) {\n const [key, value] = answer.columns ?? [];\n const rowsIn = answer.rows ?? [];\n const periods = rowsIn.length > 0 && rowsIn.every((row) => row[key] != null && PERIOD.test(String(row[key])));\n const ordered = (\n periods ? [...rowsIn].sort((a, b) => String(a[key]).localeCompare(String(b[key]))) : orderGroups(rowsIn, key, value, vocabulary)\n ).slice(0, limit);\n const [near, setNear] = useState<number | null>(null);\n if (!ordered.length) return null;\n let running = 0;\n const steps = ordered.map((row) => {\n const change = Number(row[value]) || 0;\n const from = running;\n running += change;\n return { key: row[key], change, from, to: running, total: false };\n });\n const bars = [...steps, { key: null, change: running, from: 0, to: running, total: true }];\n const names = bars.map((bar, i) =>\n bar.total ? sayCopy('total') : periods ? periodTick(String(bar.key), i ? String(bars[i - 1].key) : undefined) : label(bar.key),\n );\n const W = wide;\n const H = high ? Math.max(120, Math.round(high)) : Math.max(180, Math.min(Math.round(W / 3), 260));\n const goal = target !== undefined && Number.isFinite(target) ? target : undefined;\n const max = Math.max(0, goal ?? 0, ...bars.map((bar) => Math.max(bar.from, bar.to)));\n const min = Math.min(0, ...bars.map((bar) => Math.min(bar.from, bar.to)));\n const scale = levelsOf(min, max, say.tick?.steps);\n const levels = scale.levels;\n const span = scale.max - scale.min || 1;\n const PAD = { top: 30, right: 6, bottom: 24, left: Math.max(...levels.map((v) => tickWidth(tickSay(say)(v)))) + 6 };\n const band = (W - PAD.left - PAD.right) / bars.length;\n const barWide = Math.max(2, Math.min(48, band * 0.64));\n const mid = (i: number) => PAD.left + band * (i + 0.5);\n const y = (v: number) => PAD.top + (1 - (v - scale.min) / span) * (H - PAD.top - PAD.bottom);\n const marks = axisMarks(names, mid, { centered: true });\n const active = near ?? bars.length - 1;\n const said = (bar: (typeof bars)[number]) =>\n bar.total\n ? `${sayCopy('total')} · ${say(bar.to)}`\n : `${names[bars.indexOf(bar)]} · ${bar.change >= 0 ? '+' : '−'}${say(Math.abs(bar.change))}`;\n return (\n <>\n <svg\n className=\"chart-waterfall\"\n viewBox={`0 0 ${W} ${H}`}\n role=\"img\"\n aria-label={sayCopy('waterfallSummary', { count: steps.length, total: say(running) })}\n onPointerMove={(event) => {\n const box = event.currentTarget.getBoundingClientRect();\n if (!box.width) return;\n // screen-px: the pointer's ruler, mapped into viewBox units by the ratio.\n const px = ((event.clientX - box.left) / box.width) * W;\n setNear(Math.min(bars.length - 1, Math.max(0, Math.floor((px - PAD.left) / band))));\n }}\n onPointerLeave={() => setNear(null)}\n >\n {levels.map((v) => (\n <g key={`l${v}`}>\n <line className=\"chart-grid\" x1={PAD.left} x2={W - PAD.right} y1={f1(y(v))} y2={f1(y(v))} />\n <text className=\"chart-tick chart-y-tick\" x={f1(PAD.left - 6)} y={f1(y(v) + 4)} textAnchor=\"end\">\n {tickSay(say)(v)}\n </text>\n </g>\n ))}\n {bars.map((bar, i) => {\n const top = y(Math.max(bar.from, bar.to)),\n bottom = y(Math.min(bar.from, bar.to));\n const kind = bar.total ? 'is-total' : bar.change >= 0 ? 'is-rise' : 'is-fall';\n return (\n <g key={i}>\n {i > 0 && (\n <line\n className=\"chart-waterfall-join\"\n x1={f1(mid(i - 1) + barWide / 2)}\n x2={f1(mid(i) - barWide / 2)}\n y1={f1(y(bars[i - 1].to))}\n y2={f1(y(bars[i - 1].to))}\n />\n )}\n <rect\n className={`chart-waterfall-bar ${kind}${i === active ? ' is-active' : ''}`}\n x={f1(mid(i) - barWide / 2)}\n y={f1(top)}\n width={f1(barWide)}\n height={f1(Math.max(1, bottom - top))}\n >\n <title>{said(bar)}</title>\n </rect>\n </g>\n );\n })}\n {goal !== undefined && (\n <g className=\"chart-target\">\n <line className=\"chart-target-line\" x1={PAD.left} x2={W - PAD.right} y1={f1(y(goal))} y2={f1(y(goal))} />\n </g>\n )}\n {marks.map(({ index }) => (\n <text\n key={`m${index}`}\n className={`chart-tick chart-x-tick${index === active ? ' is-active' : ''}`}\n x={f1(Math.min(Math.max(mid(index), tickWidth(names[index]) / 2), W - tickWidth(names[index]) / 2))}\n y={H - 6}\n textAnchor=\"middle\"\n >\n {names[index]}\n </text>\n ))}\n <Tip text={said(bars[active])} at={mid(active)} top={y(Math.max(bars[active].from, bars[active].to)) - 26} wide={W} />\n </svg>\n <DataTable\n rows={bars.map((bar, i) => [names[i], bar.total ? say(bar.to) : `${bar.change >= 0 ? '+' : '−'}${say(Math.abs(bar.change))}`])}\n />\n </>\n );\n}\n\n/* ---------- treemap ---------- */\n\ninterface Tile {\n key: unknown;\n value: number;\n series: number;\n other?: boolean;\n}\ninterface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Tiles laid out squarified (Bruls, Huizing and van Wijk): each row of tiles\n * along the shorter side takes the next largest while that keeps its worst\n * aspect ratio from getting worse, so tiles stay near square and a size is\n * read as an area. In units of the box, which is drawn at any width.\n */\nexport function squarify(values: readonly number[], box: Box): Box[] {\n const out: Box[] = [];\n const total = values.reduce((sum, v) => sum + v, 0);\n if (total <= 0) return values.map(() => ({ x: box.x, y: box.y, w: 0, h: 0 }));\n const scale = (box.w * box.h) / total;\n let { x, y, w, h } = box;\n let at = 0;\n const worst = (row: number[], side: number) => {\n const sum = row.reduce((s, v) => s + v, 0) * scale;\n const most = Math.max(...row) * scale,\n least = Math.min(...row) * scale;\n return Math.max((side * side * most) / (sum * sum), (sum * sum) / (side * side * least));\n };\n while (at < values.length) {\n const side = Math.min(w, h);\n const row = [values[at]];\n while (at + row.length < values.length && worst([...row, values[at + row.length]], side) <= worst(row, side))\n row.push(values[at + row.length]);\n const area = row.reduce((s, v) => s + v, 0) * scale;\n if (w >= h) {\n const across = area / h;\n let down = y;\n for (const v of row) {\n const tall = (v * scale) / across;\n out.push({ x, y: down, w: across, h: tall });\n down += tall;\n }\n x += across;\n w -= across;\n } else {\n const tall = area / w;\n let along = x;\n for (const v of row) {\n const across = (v * scale) / tall;\n out.push({ x: along, y, w: across, h: tall });\n along += across;\n }\n y += tall;\n h -= tall;\n }\n at += row.length;\n }\n return out;\n}\n\n/* A tile's name: 500 12px on 1.25 lines, inside 8px by 6px of padding and a 2px border; its figure a 15px line under a 2px gap. */\nconst TILE_X = 20;\nconst TILE_Y = 16;\nconst TILE_VALUE = 17;\n\n/**\n * How a tile says its name, or null where it cannot say it whole: on one\n * line in 12px; else on two (three in a tall tile), broken between words;\n * else the same at 11px;\n * else not at all, and the tile's tooltip and the data table carry it.\n * Cut with an ellipsis, Depot's tiles read \"Slee…\" and \"Tires …\" — a name\n * that names nothing. `measure` gives a text's width at 12px.\n */\nexport function tileLabel(\n name: string,\n w: number,\n h: number,\n measure: (text: string) => number,\n): { size: 12 | 11; lines: number; value: boolean } | null {\n const room = w - TILE_X;\n const high = h - TILE_Y;\n for (const size of [12, 11] as const) {\n const width = (text: string) => (measure(text) * size) / 12;\n const line = size * 1.25;\n if (high < line) continue;\n const value = (lines: number) => high >= lines * line + TILE_VALUE;\n if (width(name) <= room) return { size, lines: 1, value: value(1) };\n if (high < 2 * line) continue;\n const words = name.split(/\\s+/).filter(Boolean);\n if (words.some((word) => width(word) > room)) continue;\n let lines = 1;\n let current = '';\n for (const word of words) {\n const next = current ? `${current} ${word}` : word;\n if (width(next) <= room) current = next;\n else {\n lines += 1;\n current = word;\n }\n }\n if (lines <= Math.min(3, Math.floor(high / line))) return { size, lines, value: value(lines) };\n }\n return null;\n}\n\n/** At most this many tiles; the rest are one tile in the quiet ink. */\nconst TILES = 8;\n\n/* One hue in steps, the largest deepest: a tile's size is read as its area, so a colour per tile said nothing more\n and five pastels made the treemap the loudest thing on its page. From 88% of the first series down to 24%, so the\n largest reads as the accent itself; a tile from DEEP_TILE up says its words in the ground's colour. */\nconst tileStep = (i: number, count: number) => Math.round(88 - (count > 1 ? (64 * i) / (count - 1) : 0));\nconst DEEP_TILE = 56;\n\ntype Rgb = readonly [number, number, number];\n/** The colours a tile is mixed from and said in, as the page resolves them. */\ninterface TileInks {\n series: Rgb;\n surface: Rgb;\n text: Rgb;\n ground: Rgb;\n}\n\n/** A colour as the page computes it: `rgb(…)`, or `color(srgb …)` in 0–1. */\nfunction rgbOf(said: string): Rgb | undefined {\n const srgb = /color\\(srgb\\s+([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)/.exec(said);\n if (srgb) return [Number(srgb[1]) * 255, Number(srgb[2]) * 255, Number(srgb[3]) * 255];\n const rgb = /rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)/.exec(said);\n return rgb ? [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])] : undefined;\n}\n\nfunction luminance([r, g, b]: Rgb): number {\n const lin = (c: number) => (c / 255 <= 0.04045 ? c / 255 / 12.92 : ((c / 255 + 0.055) / 1.055) ** 2.4);\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\nfunction contrast(a: Rgb, b: Rgb): number {\n const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);\n return (x + 0.05) / (y + 0.05);\n}\n\n/**\n * A tile's step and ink: the ground's colour where it reads on the step at 4.5:1, else the ink; where neither does\n * — a mid step of a deep hue, between the two — the step is lightened until the ink reads. Without the page's colours\n * (before the first paint), the plain threshold.\n */\nfunction tileInk(step: number, inks: TileInks | null): { step: number; deep: boolean } {\n if (!inks) return { step, deep: step >= DEEP_TILE };\n const mixed = (share: number): Rgb =>\n [0, 1, 2].map((c) => (inks.series[c] * share + inks.surface[c] * (100 - share)) / 100) as unknown as Rgb;\n if (contrast(mixed(step), inks.ground) >= 4.5) return { step, deep: true };\n let lighter = step;\n while (lighter > 0 && contrast(mixed(lighter), inks.text) < 4.5) lighter -= 2;\n return { step: Math.max(0, lighter), deep: false };\n}\n\nfunction Treemap({ answer, say, label, wide, high, limit }: ChartShapeProps) {\n const [key, value] = answer.columns ?? [];\n const groups = (answer.rows ?? [])\n .map((row) => ({ key: row[key], value: Number(row[value]) || 0 }))\n .filter((group) => group.value > 0)\n .sort((a, b) => b.value - a.value)\n .slice(0, limit ?? undefined);\n const [near, setNear] = useState<number | null>(null);\n /* The tiles' names are measured in the face they are set in, once it is known. */\n const map = useRef<HTMLDivElement>(null);\n const [face, setFace] = useState<string | null>(null);\n const [inks, setInks] = useState<TileInks | null>(null);\n useLayoutEffect(() => {\n const host = map.current;\n if (!host) return;\n setFace(getComputedStyle(host).fontFamily || null);\n /* Each colour resolved as the page has it, through a probe: the tokens may be any CSS colour, and a look sets them. */\n const probe = document.createElement('span');\n probe.hidden = true;\n host.append(probe);\n const read = (color: string) => {\n probe.style.color = color;\n return rgbOf(getComputedStyle(probe).color);\n };\n const [series, surface, text, ground] = ['var(--chart-1, var(--accent))', 'var(--surface)', 'var(--ink)', 'var(--ground)'].map(read);\n probe.remove();\n if (series && surface && text && ground) setInks({ series, surface, text, ground });\n }, []);\n if (!groups.length) return null;\n const measured = face ? textMeasure(`500 12px ${face}`) : undefined;\n const measure = measured ?? ((text: string) => text.length * 6.6);\n const kept: Tile[] = groups.slice(0, groups.length > TILES ? TILES - 1 : TILES).map((group, i) => ({ ...group, series: i + 1 }));\n const rest = groups.slice(kept.length);\n if (rest.length) kept.push({ key: null, value: rest.reduce((sum, group) => sum + group.value, 0), series: 0, other: true });\n const W = wide;\n const H = high ? Math.max(160, Math.round(high)) : Math.max(200, Math.min(Math.round(W / 2), 320));\n const boxes = squarify(\n kept.map((tile) => tile.value),\n { x: 0, y: 0, w: W, h: H },\n );\n const whole = kept.reduce((sum, tile) => sum + tile.value, 0);\n const name = (tile: Tile) => (tile.other ? `${sayCopy('others')} (${rest.length})` : label(tile.key));\n return (\n <>\n <div\n ref={map}\n className=\"chart-treemap\"\n style={{ aspectRatio: `${W} / ${H}` }}\n role=\"img\"\n aria-label={sayCopy('treemapSummary', { count: groups.length, largest: `${label(groups[0].key)} ${say(groups[0].value)}` })}\n onPointerLeave={() => setNear(null)}\n >\n {kept.map((tile, i) => {\n const box = boxes[i];\n const said = tileLabel(name(tile), box.w, box.h, measure);\n const ink = tile.other ? undefined : tileInk(tileStep(i, kept.length - (rest.length ? 1 : 0)), inks);\n return (\n <div\n key={i}\n className={`chart-tile${tile.other ? ' is-other' : ''}${ink?.deep ? ' is-deep' : ''}${said?.size === 11 ? ' is-small' : ''}${near === i ? ' is-active' : ''}`}\n style={\n {\n left: `${(box.x / W) * 100}%`,\n top: `${(box.y / H) * 100}%`,\n width: `${(box.w / W) * 100}%`,\n height: `${(box.h / H) * 100}%`,\n ...(ink ? { '--tile-step': `${ink.step}%` } : {}),\n } as CSSProperties\n }\n title={`${name(tile)}: ${say(tile.value)} (${pct((tile.value / whole) * 100)})`}\n onPointerEnter={() => setNear(i)}\n >\n {said && (\n <>\n <span className=\"chart-tile-name\">{name(tile)}</span>\n {said.value && <span className=\"chart-tile-value\">{say(tile.value)}</span>}\n </>\n )}\n </div>\n );\n })}\n </div>\n <p className=\"chart-heat-said\" aria-hidden=\"true\">\n {near === null ? ' ' : `${name(kept[near])} — ${say(kept[near].value)} · ${pct((kept[near].value / whole) * 100)}`}\n </p>\n <DataTable rows={kept.map((tile) => [name(tile), say(tile.value), pct((tile.value / whole) * 100)])} />\n </>\n );\n}\n\n/** The drawing for one of the further shapes, or nothing where the answer has nothing it can draw. */\nexport function ChartShape(props: ChartShapeProps) {\n const Shape = { scatter: Scatter, heatmap: Heatmap, waterfall: Waterfall, treemap: Treemap }[props.shape];\n return <Shape {...props} />;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./shared-poc11xxo.js";
|
|
2
2
|
import { k as findTable } from "./shared-DJZjSi3J.js";
|
|
3
|
-
import { q as tryResolvePath } from "./shared-
|
|
3
|
+
import { q as tryResolvePath } from "./shared-BoLbc2WD.js";
|
|
4
4
|
import "./shared-CS6uipL4.js";
|
|
5
5
|
// packages/engine/src/shared/pagePaths.ts
|
|
6
6
|
/** The grouped record's columns a header says, as paths from the listed rows: read with each group's rows, so a header costs no read. */
|
|
@@ -39,4 +39,4 @@ function cardStem(paths) {
|
|
|
39
39
|
}
|
|
40
40
|
export { sharedPrefix as i, groupFactPaths as n, groupTable as r, cardStem as t };
|
|
41
41
|
|
|
42
|
-
//# sourceMappingURL=shared-
|
|
42
|
+
//# sourceMappingURL=shared-BdA3LPCB.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-BtiFL37J.js","names":[],"sources":["../../../engine/src/shared/pagePaths.ts"],"sourcesContent":["/*\n * A page against a schema: the reference path a section follows from the\n * root, the filter that pins it to one root row, and the list and metric\n * views a section runs. Pure; nothing fetches.\n */\nimport { columnOrRollup, findTable, labelColumn, primaryKey, type DatabaseSchema, type ForeignKey, type TableMeta } from './schema.js';\nimport { MAX_PATH_DEPTH, hopName, parseViewQuery, validateView, type ViewDef, type ViewFilter } from './view.js';\nimport { quoteValue, saidAt } from './query.js';\nimport { type PageMetric, type PageList, type PageDef, type PageCustom, DEFAULT_PAGE_LIST_LIMIT, GROUPS_DEFAULT } from './pageModel.js';\nimport type { PageCustomRead } from './appComponents.js';\nimport { cardsColumns } from './appCards.js';\nimport type { BreakdownDef } from './breakdown.js';\nimport { tryResolvePath } from './viewPaths.js';\n\n/* ---------- resolving against a schema ---------- */\n\n/**\n * A path from a table back to the root, one foreign key per hop.\n *\n * Returned rather than a boolean because the caller needs the same thing the\n * validator does: which columns to compare, and whether the last hop actually\n * lands on the root. Composite keys are carried whole — a path that resolved\n * only the first column of a two-column key would build a filter that matched\n * every row sharing that column.\n */\nexport interface ResolvedPath {\n hops: ForeignKey[];\n /** The table the path ends at. Must be the page's base for the path to be usable. */\n endsAt: string;\n}\n\n/**\n * Follow `a.b.c` from a table, hop by hop, and say where it lands.\n *\n * Each segment names a foreign key on the current table by its canonical\n * column(s), as in view paths (`a+b` for a composite), or by its constraint\n * name, which tells apart two references over the same columns; a dotted\n * constraint name is not a forward path segment.\n */\nexport function resolvePath(schema: DatabaseSchema, from: string, path: string): { path?: ResolvedPath; error?: string } {\n const segments = path\n .split('.')\n .map((s) => s.trim())\n .filter(Boolean);\n if (!segments.length) return { error: 'A path cannot be empty.' };\n if (segments.length > MAX_PATH_DEPTH) {\n return { error: `\"${path}\" follows more than ${MAX_PATH_DEPTH} references.` };\n }\n\n let current = from;\n const hops: ForeignKey[] = [];\n for (const segment of segments) {\n const table = findTable(schema, current);\n if (!table) return { error: `\"${current}\" is not a table on this connection.` };\n\n const outgoing = schema.foreignKeys.filter((fk) => fk.from.table === table.id);\n const matches = outgoing.filter((fk) => hopName(fk) === segment || fk.name === segment);\n if (matches.length > 1) return { error: `\"${segment}\" is ambiguous on ${table.name} — ${matches.length} foreign keys match.` };\n const hop = matches[0];\n if (!hop) {\n /* The commonest cause is a key the database does not declare: a text\n column holding another table's code. Said as that, since \"no\n reference\" beside a column the author can see reads as a typo. */\n const column = table.columns.some((c) => c.name === segment);\n return {\n error:\n `\"${table.name}\" has no reference called \"${segment}\".` +\n (column ? ` \"${segment}\" is a column of ${table.name}, but not a foreign key: paths follow declared foreign keys only.` : '') +\n (outgoing.length ? ` It points at: ${outgoing.map((fk) => hopName(fk)).join(', ')}.` : ' It points at nothing.') +\n ' See `mistri docs relationships`.',\n };\n }\n hops.push(hop);\n current = hop.to.table;\n }\n return { path: { hops, endsAt: current } };\n}\n\n/** Prefer column-based paths, retaining explicit names when columns alone\n * cannot identify the relationship. Never emit a dotted name as one hop. */\nexport function pageHop(schema: DatabaseSchema, fk: ForeignKey): string | undefined {\n return [...new Set([hopName(fk), fk.name])].find((name) => {\n const resolved = resolvePath(schema, fk.from.table, name).path;\n return resolved?.hops.length === 1 && resolved.hops[0] === fk;\n });\n}\n\n/**\n * The filter that ties a section to one root record.\n *\n * Built as a structured `ViewFilter` rather than as text, for the reason the\n * whole tool is built that way: a key value that happens to contain a quote\n * or the word `and` is a value, and text would make it grammar.\n *\n * The path is written into the column, so the compiler does the joining —\n * `invoice_id.customer_id` is a path it already knows how to walk, and the\n * one place that knows how to join is better than a second one here.\n */\nexport function rootFilter(path: string, key: Record<string, string | number>, resolved: ResolvedPath): ViewFilter {\n const last = resolved.hops[resolved.hops.length - 1];\n // The path's segments reach the view compiler as written: a constraint\n // name tells apart two references sharing the same columns, and replacing\n // it with a canonical hop would lose the choice.\n const prefix = path\n .split('.')\n .map((segment) => segment.trim())\n .filter(Boolean)\n .slice(0, -1)\n .join('.');\n /* Every column of the root's key, compared through the same path. A\n composite key compared on one column matches every row that shares it,\n which on `invoice_line` is most of the table. */\n /* A declared reference (a soft key) meets the root at another unique\n column than its key: `product_sku` holds a product's `sku`, and the\n page knows the product by `id`. Compared through the path instead, so\n the join the reference already is finds the root by its own key. */\n if (last.to.columns.some((column) => key[column] === undefined)) {\n const whole = path\n .split('.')\n .map((segment) => segment.trim())\n .filter(Boolean)\n .join('.');\n return { groups: [Object.entries(key).map(([column, value]) => ({ path: `${whole}.${column}`, op: '=' as const, value }))] };\n }\n const conditions = last.to.columns.map((rootColumn, i) => {\n const childColumn = last.from.columns[i] ?? last.from.columns[0];\n return {\n path: prefix ? `${prefix}.${childColumn}` : childColumn,\n op: '=' as const,\n value: key[rootColumn],\n };\n });\n return { groups: [conditions] };\n}\n\n/**\n * Resolve a section's `of` — the forward reference naming which record the\n * section is about.\n *\n * One hop, deliberately. `of: 'site_id.address_id'` is expressible in the\n * type and refused here: each extra hop is another row the planner must read\n * before any section can run, and nobody has yet asked for a section about\n * the address of the site of the record they are looking at. The error says\n * the limit rather than pretending the path is malformed.\n */\nexport function resolveOf(schema: DatabaseSchema, baseId: string, of: string): { fk?: ForeignKey; table?: TableMeta; error?: string } {\n const resolved = resolvePath(schema, baseId, of);\n if (resolved.error) return { error: resolved.error };\n if (resolved.path!.hops.length !== 1) {\n return { error: `\"of\" reaches one reference from ${baseId}; \"${of}\" is ${resolved.path!.hops.length} hops.` };\n }\n const fk = resolved.path!.hops[0];\n const table = findTable(schema, resolved.path!.endsAt);\n if (!table) return { error: `\"${of}\" leads to a table this connection does not have.` };\n return { fk, table };\n}\n\n/**\n * The keys of the records the page's `of` sections are about, by prefix.\n *\n * `null` says the root's reference is empty — a work order with no site —\n * which is different from \"unknown\": a section given null renders its empty\n * state instead of a filter that can never match.\n */\nexport type OfKeys = Record<string, Record<string, string | number> | null>;\n\n/**\n * The section's own narrowing, ANDed onto the root's.\n *\n * `filter` has been in the model and in the config parser since the start and\n * was never applied: a page saying `\"filter\": \"status = open\"` listed every\n * order, silently. Documented, accepted, ignored — the worst shape a bug can\n * take, because the file says the behaviour is there.\n *\n * Written in the query language and parsed against the *child*, which is what\n * \"recent open orders\" means: the condition is about the order, not about the\n * customer the page is on. Errors come back rather than throwing, so a\n * mistyped filter names its section instead of blanking the page.\n */\nfunction withSectionFilter(\n schema: DatabaseSchema,\n from: string,\n root: ViewFilter,\n filter: string | undefined,\n now?: Date,\n): { filter?: ViewFilter; error?: string } {\n if (!filter) return { filter: root };\n const parsed = parseViewQuery(schema, from, filter, now);\n if (parsed.errors.length) return { error: parsed.errors[0].message };\n\n const extra = parsed.filter.groups;\n if (!extra.length) return { filter: root };\n /* ANDed into every root group rather than appended as another group: groups\n are OR-ed, so appending would have widened the result instead of\n narrowing it — a filter that added rows. With one root group, which is\n what `rootFilter` always builds, this is the ordinary case of \"and also\".\n \n A filter with its own OR becomes a product, which is what AND-of-ORs\n means when both sides have alternatives. */\n const groups = [];\n for (const rootGroup of root.groups) {\n for (const extraGroup of extra) groups.push([...rootGroup, ...extraGroup]);\n }\n return { filter: { groups } };\n}\n\n/** The record's own values, read on the server from its row for the sections that match on them (a similar list). */\nexport type RootValues = Record<string, unknown>;\n\n/* The engines' number types, by the names each gives its columns. */\nconst NUMERIC_TYPE = /int|real|num|dec|float|double|money/i;\n\n/**\n * What is wrong with a list's match (`same`, `near`) against this schema, or\n * undefined. Said at startup (validatePage) and again when the list is\n * planned, in the same words, so a page that loads is a page whose matched\n * list can run. Each column is one of the listed table and of the page's own,\n * the record's value being read off the page's row.\n */\nexport function similarProblem(schema: DatabaseSchema, page: PageDef, section: PageList): string | undefined {\n const similar = section.similar;\n if (!similar) return undefined;\n const table = findTable(schema, section.from);\n if (!table) return `\"${section.from}\" is not a table on this connection.`;\n const base = findTable(schema, page.base);\n const near = Object.keys(similar.near ?? {});\n for (const column of [...similar.same, ...near]) {\n if (!table.columns.some((c) => c.name === column)) return `\"same\" names \"${column}\", which is not a column on ${table.name}.`;\n if (!base?.columns.some((c) => c.name === column)) {\n return `\"same\" names \"${column}\", which the page's own table, ${base?.name ?? page.base}, does not have: a row shares it with the record the page is on.`;\n }\n }\n for (const column of near) {\n const type = table.columns.find((c) => c.name === column)?.type ?? '';\n if (!NUMERIC_TYPE.test(type)) {\n return `\"near\" keeps \"${column}\" near the record's value, so it must be a number column; it is ${type || 'untyped'}.`;\n }\n }\n if (table.id === page.base && !primaryKey(table).length)\n return `A match leaves out the record the page is on, and ${table.name} has no primary key to know it by.`;\n return undefined;\n}\n\n/** Why `column` cannot be summed over `table`'s rows: not a column, rollup or computed column, or not a number. */\nfunction numberColumnProblem(table: TableMeta, column: string, option: string, verb: string): string | undefined {\n const computed = table.computed && Object.hasOwn(table.computed, column) ? table.computed[column] : undefined;\n const type =\n table.columns.find((c) => c.name === column)?.type ??\n (table.rollups && Object.hasOwn(table.rollups, column) ? table.rollups[column].type : undefined) ??\n (computed ? (computed.type === 'number' ? 'NUMERIC' : computed.type) : undefined);\n if (type === undefined) return `\"${option}\" names \"${column}\", which is not a column, rollup or computed column on ${table.name}.`;\n if (!NUMERIC_TYPE.test(type)) return `\"${option}\" ${verb} \"${column}\", so it must be a number; it is ${type || 'untyped'}.`;\n return undefined;\n}\n\n/**\n * What is wrong with a related list's footer of sums (`totals`) against this\n * schema, or undefined — said at startup and again when the list is planned.\n * A total is a number column of the rows, or one of their numeric rollups or\n * computed columns (`refunded`, the refunds of each order), through any\n * reference `whose` walks.\n */\nexport function relatedMatchProblem(schema: DatabaseSchema, page: PageDef, section: PageList): string | undefined {\n const table = findTable(schema, section.from);\n if (!table) return `\"${section.from}\" is not a table on this connection.`;\n void page;\n for (const column of section.totals ?? []) {\n const problem = numberColumnProblem(table, column, 'totals', 'sums');\n if (problem) return problem;\n }\n return undefined;\n}\n\n/** The grouped record's columns a header says, as paths from the listed rows: read with each group's rows, so a header costs no read. */\nexport function groupFactPaths(section: Pick<PageList, 'group'>): string[] {\n const group = section.group;\n return group?.show?.length ? group.show.map((column) => `${group.by}.${column}`) : [];\n}\n\n/** The table a group's header is a record of, or undefined where the groups are plain values (or the path does not resolve). */\nexport function groupTable(schema: DatabaseSchema, section: Pick<PageList, 'from' | 'group'>): TableMeta | undefined {\n if (!section.group) return undefined;\n const { resolved } = tryResolvePath(schema, section.from, section.group.by);\n if (!resolved) return undefined;\n const { table, column } = resolved;\n /* The declared key the column is the whole of: a leg of a composite key names no one record. */\n const fk = schema.foreignKeys.find((k) => k.from.table === table.id && k.from.columns.length === 1 && k.from.columns[0] === column.name);\n return fk ? findTable(schema, fk.to.table) : undefined;\n}\n\n/**\n * What is wrong with a grouped list against this schema, or undefined — said at startup and again when the list is\n * planned. The header is the record the rows point at through `by`; where `by` ends at a plain column the groups are\n * its values, and there is no record to take facts or an order from.\n */\nexport function groupProblem(schema: DatabaseSchema, page: PageDef, section: PageList): string | undefined {\n const group = section.group;\n if (!group) return undefined;\n const table = findTable(schema, section.from);\n if (!table) return `\"${section.from}\" is not a table on this connection.`;\n const found = tryResolvePath(schema, table.id, group.by);\n if (!found.resolved)\n return `\"group.by\" names \"${group.by}\", which is not a column of ${table.name} or a path from it. ${found.error?.message ?? ''}`.trim();\n for (const column of group.sum ?? []) {\n const problem = numberColumnProblem(table, column, 'group.sum', 'sums');\n if (problem) return problem;\n }\n const target = groupTable(schema, section);\n if (target && target.id === page.base && !section.of)\n return `\"group.by\" names \"${group.by}\", which is the record this page is on, so every row would fall in one group. Group by a record between them, such as the middle of the chain.`;\n if (!target) {\n if (group.show?.length)\n return `\"group.show\" says facts of the record a group is, and \"${group.by}\" is a plain column: its groups are its values. Leave \"show\" out, or group by a reference.`;\n if (group.sort)\n return `\"group.sort\" orders by a column of the record a group is, and \"${group.by}\" is a plain column: its groups are values, largest first. Leave \"sort\" out, or group by a reference.`;\n return undefined;\n }\n for (const name of group.show ?? []) {\n if (!target.columns.some((c) => c.name === name) && !(target.computed && Object.hasOwn(target.computed, name)))\n return `\"group.show\" names \"${name}\", which is not a column of ${target.name}, the record each group is.`;\n }\n if (group.sort && !target.columns.some((c) => c.name === group.sort!.column))\n return `\"group.sort\" names \"${group.sort.column}\", which is not a column of ${target.name}, the record each group is.`;\n return undefined;\n}\n\n/**\n * The groups of a grouped list: one row for each record (or value) the listed rows point at through `by`, with how\n * many rows it holds, its subtotals when the list has `totals`, and, when the groups are ordered by a column of the\n * grouped record, the largest of that column — the sort column depends on the key, so the maximum is exact. It is a\n * plain breakdown over the same rows the list reads, so it adds no SQL shape of its own and the reader's grants,\n * row policy and tenant scope narrow it as they narrow every breakdown.\n */\nexport function groupsView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n ofKeys?: OfKeys,\n now?: Date,\n): { breakdown?: BreakdownDef; empty?: boolean; error?: string } {\n const group = section.group;\n if (!group) return {};\n const problem = groupProblem(schema, page, section) ?? relatedMatchProblem(schema, page, section);\n if (problem) return { error: problem };\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true };\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n const table = findTable(schema, section.from)!;\n const narrowed = withSectionFilter(schema, table.id, rootFilter(section.path, root.key!, resolved.path!), section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n const sort = group.sort;\n return {\n breakdown: {\n base: table.id,\n by: [{ path: group.by }],\n measures: [\n { fn: 'count', alias: 'group_rows' },\n ...(sort ? [{ fn: 'max' as const, path: `${group.by}.${sort.column}`, alias: 'group_order' }] : []),\n ...(section.totals ?? []).map((column, i) => ({ fn: 'sum' as const, path: column, alias: `total_${i}` })),\n ...(group.sum ?? []).map((column, i) => ({ fn: 'sum' as const, path: column, alias: `group_sum_${i}` })),\n ],\n filter: narrowed.filter,\n /* The rows with no value are one group, and they come last whatever the order: the breakdown is paged, so only\n the query can keep them last on every page (a client could place them on the page they happen to fall on). */\n orderBy: [\n { name: group.by, direction: 'asc', emptyLast: true },\n sort ? { name: 'group_order', direction: sort.direction } : { name: 'group_rows', direction: 'desc' },\n ],\n limit: group.limit ?? GROUPS_DEFAULT,\n },\n };\n}\n\n/**\n * The sums a related list's footer says, as a breakdown with no key over the\n * rows the list's reference matches — one row, a `sum` a column, as a\n * narrowed page metric is measured — so the footer counts every row, not the\n * page of them the list has loaded, and a rollup of the rows sums as a column\n * does.\n */\nexport function totalsView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n ofKeys?: OfKeys,\n rootValues?: RootValues,\n): { breakdown?: BreakdownDef; empty?: boolean; error?: string } {\n if (!section.totals?.length) return {};\n /* A matched list sums the rows it shows: the same filter, built from the record's own values. */\n if (section.similar) {\n const problem = relatedMatchProblem(schema, page, section);\n if (problem) return { error: problem };\n const matched = similarView(schema, page, section, key, 0, undefined, rootValues);\n if (!matched.view) return matched.empty ? { empty: true } : { error: matched.error };\n return {\n breakdown: {\n base: matched.view.base,\n by: [],\n measures: section.totals.map((column, i) => ({ fn: 'sum' as const, path: column, alias: `total_${i}` })),\n filter: matched.view.filter,\n limit: 1,\n },\n };\n }\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true };\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n const problem = relatedMatchProblem(schema, page, section);\n if (problem) return { error: problem };\n const table = findTable(schema, section.from)!;\n return {\n breakdown: {\n base: table.id,\n by: [],\n measures: section.totals.map((column, i) => ({ fn: 'sum' as const, path: column, alias: `total_${i}` })),\n filter: rootFilter(section.path, root.key!, resolved.path!),\n limit: 1,\n },\n };\n}\n\n/**\n * A matched list: rows that share the record's values — equal on `same`,\n * within a fraction of the record's value on `near` — and, where they are\n * rows of the page's own table, the record itself left out; closest first on\n * the first `near` column, then the section's own sort, then the key, so equal\n * distances still come back in one order.\n *\n * Built from values the caller read from the root row on the server, never\n * from anything the client sent, and returned as an ordinary view, so the\n * reader's grants, row policy, the public read ceiling and tenant scope\n * narrow it exactly as they narrow any list. A value the record lacks — an\n * empty `same`, a `near` that is not a number — empties the section: a\n * \"similar\" list of everything, because this record's body type is blank,\n * would be a list that lies.\n */\nfunction similarView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n index: number,\n now: Date | undefined,\n rootValues: RootValues | undefined,\n): { view?: ViewDef; empty?: boolean; error?: string } {\n const problem = similarProblem(schema, page, section);\n if (problem) return { error: problem };\n if (!rootValues) return { error: 'The record\\'s values for \"same\" were not read before planning.' };\n const table = findTable(schema, section.from)!;\n const similar = section.similar!;\n const conditions: ViewFilter['groups'][number] = [];\n for (const column of similar.same) {\n const value = rootValues[column];\n if (value === null || value === undefined || (typeof value === 'string' && !value.trim())) return { empty: true };\n if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return { empty: true };\n conditions.push({ path: column, op: '=', value });\n }\n const closest: NonNullable<ViewDef['orderBy']> = [];\n for (const [column, fraction] of Object.entries(similar.near ?? {})) {\n const raw = rootValues[column];\n const value = typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() ? Number(raw) : Number.NaN;\n if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER) return { empty: true };\n let [low, high] = [value * (1 - fraction), value * (1 + fraction)].sort((a, b) => a - b);\n /* A whole-number column keeps whole-number bounds — the same rows, and a\n question the query bar can read back (\"price >= 22497\"). */\n if (/int/i.test(table.columns.find((c) => c.name === column)?.type ?? '')) [low, high] = [Math.ceil(low), Math.floor(high)];\n conditions.push({ path: column, op: '>=', value: low }, { path: column, op: '<=', value: high });\n if (!closest.length) closest.push({ path: column, direction: 'asc', near: value });\n }\n /* The record itself, left out as NOT (every key column equal): one OR-ed\n group per key column, so a composite key leaves out exactly one row. Rows of another table are not it. */\n const keyColumns = primaryKey(table);\n const own = table.id === page.base;\n if (own && keyColumns.some((column) => key[column] === undefined))\n return { error: `The record's key does not name ${table.name}'s ${keyColumns.join(', ')}.` };\n const groups = own ? keyColumns.map((column) => [...conditions, { path: column, op: '!=' as const, value: key[column] }]) : [conditions];\n const narrowed = withSectionFilter(schema, table.id, { groups }, section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n\n const cardColumns =\n typeof section.presentation === 'object'\n ? section.presentation.kind === 'custom'\n ? [...section.presentation.show]\n : cardsColumns(section.presentation)\n : [];\n const columns = [...new Set([...(section.columns?.length ? section.columns : defaultColumns(table, '')), ...cardColumns])].map((p) => ({\n path: p,\n }));\n const sorted = section.orderBy?.map((o) => ({ path: o.column, direction: o.direction })) ?? [];\n const tieBreak = keyColumns.filter((c) => !sorted.some((o) => o.path === c)).map((c) => ({ path: c, direction: 'asc' as const }));\n return {\n view: {\n id: `page:${page.id}:${index}`,\n name: section.title ?? table.name,\n base: table.id,\n columns,\n filter: narrowed.filter,\n orderBy: [...closest, ...sorted, ...tieBreak],\n limit: section.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n };\n}\n\n/**\n * What a list section needs to run, as a view the existing compiler can take.\n *\n * A page adds no query machinery of its own. Every section is a view — that\n * is the point of having built one — so a page is an arrangement of views\n * around a shared root rather than a second way to ask a database questions.\n */\nexport function listView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n index: number,\n now?: Date,\n ofKeys?: OfKeys,\n rootValues?: RootValues,\n): { view?: ViewDef; empty?: boolean; error?: string } {\n /* A similar list walks no path: it is matched on the record's own values. */\n if (section.similar) return similarView(schema, page, section, key, index, now, rootValues);\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true };\n\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n if (resolved.path!.endsAt !== root.table) {\n return {\n error: `\"${section.path}\" leads from ${section.from} to ` + `${resolved.path!.endsAt}, not to ${root.table}.`,\n };\n }\n\n const table = findTable(schema, section.from);\n if (!table) return { error: `\"${section.from}\" is not a table on this connection.` };\n /* Cards read their own columns (title, figure, badge, lines, summary)\n beside whatever the section shows. */\n const cardColumns =\n typeof section.presentation === 'object'\n ? section.presentation.kind === 'custom'\n ? [...section.presentation.show]\n : cardsColumns(section.presentation)\n : [];\n /* A current card is titled by its row's name, which its facts leave out. */\n const titled = section.presentation === 'card' ? [labelColumn(table)].filter((name): name is string => Boolean(name)) : [];\n const columns = [\n ...new Set([\n ...titled,\n ...(section.columns?.length ? section.columns : defaultColumns(table, section.path, resolved.path!)),\n ...cardColumns,\n ...groupFactPaths(section),\n ]),\n ].map((p) => ({ path: p }));\n\n const joined = rootFilter(section.path, root.key!, resolved.path!);\n const narrowed = withSectionFilter(schema, table.id, joined, section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n\n return {\n view: {\n id: `page:${page.id}:${index}`,\n name: section.title ?? table.name,\n base: table.id,\n columns,\n filter: narrowed.filter,\n orderBy: section.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),\n limit: section.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n };\n}\n\n/**\n * The record a section is actually about: the root, or — through `of` — a\n * record the root points at, whose key the planner has already read.\n */\nfunction sectionRoot(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageMetric | PageList,\n key: Record<string, string | number>,\n ofKeys: OfKeys | undefined,\n): { table?: string; key?: Record<string, string | number>; empty?: boolean; error?: string } {\n if (!section.of) return { table: page.base, key };\n const of = resolveOf(schema, page.base, section.of);\n if (of.error) return { error: of.error };\n const referenced = ofKeys?.[section.of];\n if (referenced === undefined) {\n /* The planner read the root row before building sections, so a missing\n entry is the caller's bug, not the page's. Said as what it is. */\n return { error: `The key for \"${section.of}\" was not resolved before planning.` };\n }\n if (referenced === null) return { empty: true };\n return { table: of.table!.id, key: referenced };\n}\n\n/**\n * What a metric section needs to run.\n *\n * Expressed as a one-row view over the *root* table with a single aggregate,\n * rather than as a count over the child: the aggregate machinery already\n * guarantees that two counts over different children cannot inflate each\n * other, and doing it any other way here would give up that guarantee for a\n * number nobody would think to check.\n */\n/**\n * How the query bar would say what a section shows — when it can say it.\n *\n * A count on a page is a number, and a number you cannot get behind is a\n * number you have to take on trust. Clicking one should open the rows it\n * counted, narrowed the same way — and for a long time it opened the child\n * table with no filter at all, because the plan carried no query and the\n * client fell back to the table's own name. The count said 3 and the grid\n * said 20.\n *\n * The query language has no dotted paths: `invoice_line invoice_id.customer_id\n * = 4` is not a thing it can parse, and a page two hops out produces exactly\n * that. So this answers `undefined` rather than something that will not run —\n * the caller opens the composer on the view itself in that case, which can\n * say it. The distinction is deliberate: it is better to be told the language\n * cannot express this than to be shown a filter that quietly is not the one.\n *\n * `is empty` for a null test, because that is the grammar's spelling; `in` is\n * left out, since a list needs bracket syntax the parser writes differently\n * from how it reads it.\n */\nconst QUERY_OPS: Partial<Record<string, string>> = {\n '=': '=',\n '!=': '!=',\n '<': '<',\n '<=': '<=',\n '>': '>',\n '>=': '>=',\n contains: 'contains',\n startsWith: 'startswith',\n endsWith: 'endswith',\n};\n\nexport function sectionQuery(view: ViewDef | undefined): string | undefined {\n if (!view?.base) return undefined;\n const groups = view.filter?.groups?.filter((g) => g.length) ?? [];\n if (!groups.length) return view.base;\n\n const parts: string[] = [];\n for (const group of groups) {\n const conditions: string[] = [];\n for (const c of group) {\n /* A path, not a column: the language filters the table in front of it\n and nothing further out. */\n if (!c.path || c.path.includes('.')) return undefined;\n if (c.op === 'isNull' || c.op === 'isNotNull') {\n conditions.push(`${c.path} is ${c.op === 'isNull' ? '' : 'not '}empty`);\n continue;\n }\n const op = QUERY_OPS[c.op];\n if (c.other !== undefined) {\n if (!op || c.other.includes('.')) return undefined;\n conditions.push(`${c.path} ${op} ${c.other}`);\n continue;\n }\n if (!op || c.value === undefined || c.value === null || Array.isArray(c.value)) return undefined;\n conditions.push(`${c.path} ${op} ${quoteValue(c.value)}`);\n }\n parts.push(conditions.join(' and '));\n }\n return `${view.base} ${parts.join(' or ')}`;\n}\n\nexport function metricView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageMetric,\n key: Record<string, string | number>,\n index: number,\n now?: Date,\n ofKeys?: OfKeys,\n): { view?: ViewDef; counts?: boolean; breakdown?: BreakdownDef; empty?: boolean; error?: string } {\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true, counts: section.fn === 'count' };\n\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n if (resolved.path!.endsAt !== root.table) {\n return {\n error: `\"${section.path}\" leads from ${section.from} to ` + `${resolved.path!.endsAt}, not to ${root.table}.`,\n };\n }\n if (section.fn !== 'count' && !section.column) {\n return { error: `\"${section.fn}\" needs a column to ${section.fn}.` };\n }\n\n const table = findTable(schema, section.from);\n if (!table) return { error: `\"${section.from}\" is not a table on this connection.` };\n if (section.column && !columnOrRollup(table, section.column)) {\n return { error: `\"${table.name}\" has no column called \"${section.column}\".` };\n }\n\n /* The record this section is about — the page's base, or the table `of`\n reaches. The aggregate view below anchors on it, so a sum \"of its site\"\n is a one-row view over site exactly as a sum over the root is one over\n the root. */\n const base = findTable(schema, root.table!);\n if (!base) return { error: `\"${root.table}\" is not a table on this connection.` };\n\n /**\n * A count is a filtered count of the child, not an aggregate on the root.\n *\n * It used to be an aggregate, with `via` taken from the *first* hop — which\n * is the right constraint only when there is one. For anything further out\n * the first hop names a key that does not point at the root at all: on a\n * customer page, `invoice_line via invoice_id.customer_id` handed the\n * aggregate `line_invoice_fk`, which joins lines to invoices and knows\n * nothing about customers. The number that came back was not the number\n * asked for, and looked like one.\n *\n * Counting the child directly is correct at any depth, and it is what the\n * list beside it already does — so a count and the list it summarises can\n * no longer disagree. The view returns one row and its `total`, which is\n * exact: a view only ever joins to-one, so counting it is counting the\n * table it started from.\n */\n const narrowed = withSectionFilter(schema, table.id, rootFilter(section.path, root.key!, resolved.path!), section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n /* The rows it measures, as the list beside it would show them: what a\n count reads its total from, and what any metric reopens as. */\n const rowsView: ViewDef = {\n id: `page:${page.id}:${index}`,\n name: section.title ?? table.name,\n base: table.id,\n columns: (primaryKey(table)[0] ? [primaryKey(table)[0]] : [table.columns[0].name]).map((path) => ({ path })),\n filter: narrowed.filter,\n limit: 1,\n };\n if (section.fn === 'count') return { counts: true, view: rowsView };\n\n /* A narrowed `sum`, `avg`, `min` or `max` — or one further out than one\n hop — is measured the way a count is counted: over the child's own rows,\n filtered by the path to the root and the section's query, as a\n breakdown with no key, which answers one row. That is correct at any\n depth for the same reason the count is (a path only joins to-one), and\n it is the same filter the list beside it reads, so the figure and the\n list cannot disagree. The aggregate compiler below reaches one hop and\n takes no filter; it was refusing both, which `mistri check` could not\n see coming. */\n /* A rollup of the child (rollups.ts) is not a column the one-hop\n aggregate below can read, so it is measured this way too. */\n const rollup = Boolean(section.column && !table.columns.some((c) => c.name === section.column));\n if (section.filter || resolved.path!.hops.length > 1 || rollup) {\n return {\n view: rowsView,\n breakdown: {\n base: table.id,\n by: [],\n measures: [{ fn: section.fn, path: section.column, alias: 'value' }],\n filter: narrowed.filter,\n limit: 1,\n },\n };\n }\n\n return {\n view: {\n id: `page:${page.id}:${index}`,\n name: section.title ?? `${section.fn} of ${table.name}`,\n base: base.id,\n columns: primaryKey(base).map((c) => ({ path: c })),\n aggregates: [\n {\n fn: section.fn,\n via: `${table.id}.${resolved.path!.hops[0].from.columns.join('+')}`,\n column: section.column,\n alias: 'value',\n },\n ],\n filter: { groups: [primaryKey(base).map((c) => ({ path: c, op: '=' as const, value: root.key![c] }))] },\n limit: 1,\n },\n };\n}\n\n/**\n * Columns worth showing in a list nobody has configured.\n *\n * The path back to the root is dropped: every row in the section has the same\n * value for it, so a column of forty identical numbers is the one column that\n * cannot tell two rows apart. The key and the label come first, because they\n * are what a row is called.\n */\nexport function defaultColumns(table: TableMeta, path: string, resolved?: ResolvedPath): string[] {\n /* The columns the first hop actually uses, not the segment that named it.\n \n A saved path segment can be a constraint name, so splitting the\n path for a column name found `address_country_fk`, matched nothing, and\n left `country_code` in the table: a column reading NZ on every row of a\n section that is *about* NZ. The one column that cannot tell two rows\n apart, kept in the one place it is guaranteed useless. */\n const pathHead = resolved ? resolved.hops[0].from.columns : [path.split('.')[0]];\n const key = primaryKey(table);\n const label = labelColumn(table);\n const ordered = [\n ...key,\n ...(label && !key.includes(label) ? [label] : []),\n ...table.columns.map((c) => c.name).filter((n) => !key.includes(n) && n !== label),\n ];\n return ordered.filter((name) => !pathHead.includes(name)).slice(0, 6);\n}\n\n/* ---------- a custom section's reads ---------- */\n\n/**\n * One declared read of a custom section, planned as the list it is: with\n * `whose`, exactly a related list (listView), the same join back to the\n * record; without, the resource's own rows narrowed by its query — the\n * dashboard's rows section, on a page. Either way the result is a view the\n * server compiles and authorizes on every run, like any section's.\n */\nexport function customReadView(\n schema: DatabaseSchema,\n page: PageDef,\n name: string,\n read: PageCustomRead,\n key: Record<string, string | number>,\n index: number,\n now?: Date,\n): { view?: ViewDef; empty?: boolean; error?: string } {\n if (read.path) {\n const built = listView(\n schema,\n page,\n {\n kind: 'list',\n from: read.from,\n path: read.path,\n columns: read.columns,\n filter: read.filter,\n orderBy: read.orderBy,\n limit: read.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n key,\n index,\n now,\n );\n return built.view ? { ...built, view: { ...built.view, id: `page:${page.id}:${index}:${name}` } } : built;\n }\n const table = findTable(schema, read.from);\n if (!table) return { error: `\"${read.from}\" is not a table on this connection.` };\n let filter: ViewFilter | undefined;\n if (read.filter) {\n const parsed = parseViewQuery(schema, table.id, read.filter, now);\n if (parsed.errors.length) return { error: parsed.errors[0].message };\n if (parsed.filter.groups.length) filter = parsed.filter;\n }\n return {\n view: {\n id: `page:${page.id}:${index}:${name}`,\n name,\n base: table.id,\n columns: read.columns.map((path) => ({ path })),\n ...(filter ? { filter } : {}),\n orderBy: read.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),\n limit: read.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n };\n}\n\n/**\n * What is wrong with a custom section against this schema, or undefined:\n * the record's fields judged as a fields section's, each read as the list it\n * is planned as — its `whose` leading home, its columns, order and query\n * through the compiler that will run them. Said at startup, so a misspelled\n * column is refused before a reader sees the section.\n */\nexport function customProblem(schema: DatabaseSchema, page: PageDef, section: PageCustom, index: number): string | undefined {\n if (section.record?.length) {\n const error = validateView(schema, {\n id: `page:${page.id}:${index}:record`,\n name: 'record',\n base: page.base,\n columns: section.record.map((path) => ({ path })),\n });\n if (error) return `\"record\": ${error}`;\n }\n for (const [name, read] of Object.entries(section.reads ?? {})) {\n const here = `rows \"${name}\"`;\n const table = findTable(schema, read.from);\n if (!table) return `${here}: \"${read.from}\" is not a table on this connection.`;\n if (read.path) {\n const resolved = resolvePath(schema, read.from, read.path);\n if (resolved.error) return `${here}: ${resolved.error}`;\n if (resolved.path!.endsAt !== page.base)\n return `${here}: \"${read.path}\" leads from ${read.from} to ${resolved.path!.endsAt}, not to ${page.base}.`;\n }\n if (read.filter) {\n const parsed = parseViewQuery(schema, table.id, read.filter);\n if (parsed.errors.length) return `${here}: ${saidAt(parsed.errors[0], read.filter.trim())}`;\n }\n const error = validateView(schema, {\n id: `page:${page.id}:${index}:${name}`,\n name,\n base: table.id,\n columns: read.columns.map((path) => ({ path })),\n orderBy: read.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),\n });\n if (error) return `${here}: ${error}`;\n }\n return undefined;\n}\n\n/* ---------- captions ---------- */\n\n/** The hop every path shares, where each is one field past it (`dealer_id.name`, `dealer_id.city` → `dealer_id`), or ''. */\nexport function sharedPrefix(paths: readonly string[] | undefined): string {\n if (!paths?.length) return '';\n const first = paths[0];\n if (!first.includes('.')) return '';\n const prefix = first.slice(0, first.lastIndexOf('.'));\n return paths.every((p) => p.startsWith(`${prefix}.`) && !p.slice(prefix.length + 1).includes('.')) ? prefix : '';\n}\n\n/**\n * The hop a card's facts are captioned without. A card's title is the record\n * (`bike_id`, the reference itself) and its facts are that record's own fields\n * (`bike_id.make`, `bike_id.serial_number`): read with the title, the paths\n * share no hop, and a fact that is the parent's label column was captioned as\n * the reference (\"Bike\" over a serial number). Its own name is what it is there.\n */\nexport function cardStem(paths: readonly string[] | undefined): string {\n const [title, ...facts] = paths ?? [];\n const stem = sharedPrefix(facts);\n /* Only where the title is that record: beside a title of its own, `dealer_id.name` is still \"Dealer\". */\n return stem && (title === stem || title.startsWith(`${stem}.`)) ? stem : sharedPrefix(paths);\n}\n"],"mappings":";;;;;;AAkRA,SAAgB,eAAe,SAA4C;CACzE,MAAM,QAAQ,QAAQ;CACtB,OAAO,OAAO,MAAM,SAAS,MAAM,KAAK,KAAK,WAAW,GAAG,MAAM,GAAG,GAAG,QAAQ,IAAI,CAAC;AACtF;;AAGA,SAAgB,WAAW,QAAwB,SAAkE;CACnH,IAAI,CAAC,QAAQ,OAAO,OAAO,KAAA;CAC3B,MAAM,EAAE,aAAa,eAAe,QAAQ,QAAQ,MAAM,QAAQ,MAAM,EAAE;CAC1E,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,MAAM,EAAE,OAAO,WAAW;CAE1B,MAAM,KAAK,OAAO,YAAY,MAAM,MAAM,EAAE,KAAK,UAAU,MAAM,MAAM,EAAE,KAAK,QAAQ,WAAW,KAAK,EAAE,KAAK,QAAQ,OAAO,OAAO,IAAI;CACvI,OAAO,KAAK,UAAU,QAAQ,GAAG,GAAG,KAAK,IAAI,KAAA;AAC/C;;AAsnBA,SAAgB,aAAa,OAA8C;CACzE,IAAI,CAAC,OAAO,QAAQ,OAAO;CAC3B,MAAM,QAAQ,MAAM;CACpB,IAAI,CAAC,MAAM,SAAS,GAAG,GAAG,OAAO;CACjC,MAAM,SAAS,MAAM,MAAM,GAAG,MAAM,YAAY,GAAG,CAAC;CACpD,OAAO,MAAM,OAAO,MAAM,EAAE,WAAW,GAAG,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,OAAO,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,IAAI,SAAS;AAChH;;;;;;;;AASA,SAAgB,SAAS,OAA8C;CACrE,MAAM,CAAC,OAAO,GAAG,SAAS,SAAS,CAAC;CACpC,MAAM,OAAO,aAAa,KAAK;CAE/B,OAAO,SAAS,UAAU,QAAQ,MAAM,WAAW,GAAG,KAAK,EAAE,KAAK,OAAO,aAAa,KAAK;AAC7F"}
|
|
1
|
+
{"version":3,"file":"shared-BdA3LPCB.js","names":[],"sources":["../../../engine/src/shared/pagePaths.ts"],"sourcesContent":["/*\n * A page against a schema: the reference path a section follows from the\n * root, the filter that pins it to one root row, and the list and metric\n * views a section runs. Pure; nothing fetches.\n */\nimport { columnOrRollup, findTable, labelColumn, primaryKey, type DatabaseSchema, type ForeignKey, type TableMeta } from './schema.js';\nimport { MAX_PATH_DEPTH, hopName, parseViewQuery, validateView, type ViewDef, type ViewFilter } from './view.js';\nimport { quoteValue, saidAt } from './query.js';\nimport { type PageMetric, type PageList, type PageDef, type PageCustom, DEFAULT_PAGE_LIST_LIMIT, GROUPS_DEFAULT } from './pageModel.js';\nimport type { PageCustomRead } from './appComponents.js';\nimport { cardsColumns } from './appCards.js';\nimport type { BreakdownDef } from './breakdown.js';\nimport { tryResolvePath } from './viewPaths.js';\n\n/* ---------- resolving against a schema ---------- */\n\n/**\n * A path from a table back to the root, one foreign key per hop.\n *\n * Returned rather than a boolean because the caller needs the same thing the\n * validator does: which columns to compare, and whether the last hop actually\n * lands on the root. Composite keys are carried whole — a path that resolved\n * only the first column of a two-column key would build a filter that matched\n * every row sharing that column.\n */\nexport interface ResolvedPath {\n hops: ForeignKey[];\n /** The table the path ends at. Must be the page's base for the path to be usable. */\n endsAt: string;\n}\n\n/**\n * Follow `a.b.c` from a table, hop by hop, and say where it lands.\n *\n * Each segment names a foreign key on the current table by its canonical\n * column(s), as in view paths (`a+b` for a composite), or by its constraint\n * name, which tells apart two references over the same columns; a dotted\n * constraint name is not a forward path segment.\n */\nexport function resolvePath(schema: DatabaseSchema, from: string, path: string): { path?: ResolvedPath; error?: string } {\n const segments = path\n .split('.')\n .map((s) => s.trim())\n .filter(Boolean);\n if (!segments.length) return { error: 'A path cannot be empty.' };\n if (segments.length > MAX_PATH_DEPTH) {\n return { error: `\"${path}\" follows more than ${MAX_PATH_DEPTH} references.` };\n }\n\n let current = from;\n const hops: ForeignKey[] = [];\n for (const segment of segments) {\n const table = findTable(schema, current);\n if (!table) return { error: `\"${current}\" is not a table on this connection.` };\n\n const outgoing = schema.foreignKeys.filter((fk) => fk.from.table === table.id);\n const matches = outgoing.filter((fk) => hopName(fk) === segment || fk.name === segment);\n if (matches.length > 1) return { error: `\"${segment}\" is ambiguous on ${table.name} — ${matches.length} foreign keys match.` };\n const hop = matches[0];\n if (!hop) {\n /* The commonest cause is a key the database does not declare: a text\n column holding another table's code. Said as that, since \"no\n reference\" beside a column the author can see reads as a typo. */\n const column = table.columns.some((c) => c.name === segment);\n return {\n error:\n `\"${table.name}\" has no reference called \"${segment}\".` +\n (column ? ` \"${segment}\" is a column of ${table.name}, but not a foreign key: paths follow declared foreign keys only.` : '') +\n (outgoing.length ? ` It points at: ${outgoing.map((fk) => hopName(fk)).join(', ')}.` : ' It points at nothing.') +\n ' See `mistri docs relationships`.',\n };\n }\n hops.push(hop);\n current = hop.to.table;\n }\n return { path: { hops, endsAt: current } };\n}\n\n/** Prefer column-based paths, retaining explicit names when columns alone\n * cannot identify the relationship. Never emit a dotted name as one hop. */\nexport function pageHop(schema: DatabaseSchema, fk: ForeignKey): string | undefined {\n return [...new Set([hopName(fk), fk.name])].find((name) => {\n const resolved = resolvePath(schema, fk.from.table, name).path;\n return resolved?.hops.length === 1 && resolved.hops[0] === fk;\n });\n}\n\n/**\n * The filter that ties a section to one root record.\n *\n * Built as a structured `ViewFilter` rather than as text, for the reason the\n * whole tool is built that way: a key value that happens to contain a quote\n * or the word `and` is a value, and text would make it grammar.\n *\n * The path is written into the column, so the compiler does the joining —\n * `invoice_id.customer_id` is a path it already knows how to walk, and the\n * one place that knows how to join is better than a second one here.\n */\nexport function rootFilter(path: string, key: Record<string, string | number>, resolved: ResolvedPath): ViewFilter {\n const last = resolved.hops[resolved.hops.length - 1];\n // The path's segments reach the view compiler as written: a constraint\n // name tells apart two references sharing the same columns, and replacing\n // it with a canonical hop would lose the choice.\n const prefix = path\n .split('.')\n .map((segment) => segment.trim())\n .filter(Boolean)\n .slice(0, -1)\n .join('.');\n /* Every column of the root's key, compared through the same path. A\n composite key compared on one column matches every row that shares it,\n which on `invoice_line` is most of the table. */\n /* A declared reference (a soft key) meets the root at another unique\n column than its key: `product_sku` holds a product's `sku`, and the\n page knows the product by `id`. Compared through the path instead, so\n the join the reference already is finds the root by its own key. */\n if (last.to.columns.some((column) => key[column] === undefined)) {\n const whole = path\n .split('.')\n .map((segment) => segment.trim())\n .filter(Boolean)\n .join('.');\n return { groups: [Object.entries(key).map(([column, value]) => ({ path: `${whole}.${column}`, op: '=' as const, value }))] };\n }\n const conditions = last.to.columns.map((rootColumn, i) => {\n const childColumn = last.from.columns[i] ?? last.from.columns[0];\n return {\n path: prefix ? `${prefix}.${childColumn}` : childColumn,\n op: '=' as const,\n value: key[rootColumn],\n };\n });\n return { groups: [conditions] };\n}\n\n/**\n * Resolve a section's `of` — the forward reference naming which record the\n * section is about.\n *\n * One hop, deliberately. `of: 'site_id.address_id'` is expressible in the\n * type and refused here: each extra hop is another row the planner must read\n * before any section can run, and nobody has yet asked for a section about\n * the address of the site of the record they are looking at. The error says\n * the limit rather than pretending the path is malformed.\n */\nexport function resolveOf(schema: DatabaseSchema, baseId: string, of: string): { fk?: ForeignKey; table?: TableMeta; error?: string } {\n const resolved = resolvePath(schema, baseId, of);\n if (resolved.error) return { error: resolved.error };\n if (resolved.path!.hops.length !== 1) {\n return { error: `\"of\" reaches one reference from ${baseId}; \"${of}\" is ${resolved.path!.hops.length} hops.` };\n }\n const fk = resolved.path!.hops[0];\n const table = findTable(schema, resolved.path!.endsAt);\n if (!table) return { error: `\"${of}\" leads to a table this connection does not have.` };\n return { fk, table };\n}\n\n/**\n * The keys of the records the page's `of` sections are about, by prefix.\n *\n * `null` says the root's reference is empty — a work order with no site —\n * which is different from \"unknown\": a section given null renders its empty\n * state instead of a filter that can never match.\n */\nexport type OfKeys = Record<string, Record<string, string | number> | null>;\n\n/**\n * The section's own narrowing, ANDed onto the root's.\n *\n * `filter` has been in the model and in the config parser since the start and\n * was never applied: a page saying `\"filter\": \"status = open\"` listed every\n * order, silently. Documented, accepted, ignored — the worst shape a bug can\n * take, because the file says the behaviour is there.\n *\n * Written in the query language and parsed against the *child*, which is what\n * \"recent open orders\" means: the condition is about the order, not about the\n * customer the page is on. Errors come back rather than throwing, so a\n * mistyped filter names its section instead of blanking the page.\n */\nfunction withSectionFilter(\n schema: DatabaseSchema,\n from: string,\n root: ViewFilter,\n filter: string | undefined,\n now?: Date,\n): { filter?: ViewFilter; error?: string } {\n if (!filter) return { filter: root };\n const parsed = parseViewQuery(schema, from, filter, now);\n if (parsed.errors.length) return { error: parsed.errors[0].message };\n\n const extra = parsed.filter.groups;\n if (!extra.length) return { filter: root };\n /* ANDed into every root group rather than appended as another group: groups\n are OR-ed, so appending would have widened the result instead of\n narrowing it — a filter that added rows. With one root group, which is\n what `rootFilter` always builds, this is the ordinary case of \"and also\".\n \n A filter with its own OR becomes a product, which is what AND-of-ORs\n means when both sides have alternatives. */\n const groups = [];\n for (const rootGroup of root.groups) {\n for (const extraGroup of extra) groups.push([...rootGroup, ...extraGroup]);\n }\n return { filter: { groups } };\n}\n\n/** The record's own values, read on the server from its row for the sections that match on them (a similar list). */\nexport type RootValues = Record<string, unknown>;\n\n/* The engines' number types, by the names each gives its columns. */\nconst NUMERIC_TYPE = /int|real|num|dec|float|double|money/i;\n\n/**\n * What is wrong with a list's match (`same`, `near`) against this schema, or\n * undefined. Said at startup (validatePage) and again when the list is\n * planned, in the same words, so a page that loads is a page whose matched\n * list can run. Each column is one of the listed table and of the page's own,\n * the record's value being read off the page's row.\n */\nexport function similarProblem(schema: DatabaseSchema, page: PageDef, section: PageList): string | undefined {\n const similar = section.similar;\n if (!similar) return undefined;\n const table = findTable(schema, section.from);\n if (!table) return `\"${section.from}\" is not a table on this connection.`;\n const base = findTable(schema, page.base);\n const near = Object.keys(similar.near ?? {});\n for (const column of [...similar.same, ...near]) {\n if (!table.columns.some((c) => c.name === column)) return `\"same\" names \"${column}\", which is not a column on ${table.name}.`;\n if (!base?.columns.some((c) => c.name === column)) {\n return `\"same\" names \"${column}\", which the page's own table, ${base?.name ?? page.base}, does not have: a row shares it with the record the page is on.`;\n }\n }\n for (const column of near) {\n const type = table.columns.find((c) => c.name === column)?.type ?? '';\n if (!NUMERIC_TYPE.test(type)) {\n return `\"near\" keeps \"${column}\" near the record's value, so it must be a number column; it is ${type || 'untyped'}.`;\n }\n }\n if (table.id === page.base && !primaryKey(table).length)\n return `A match leaves out the record the page is on, and ${table.name} has no primary key to know it by.`;\n return undefined;\n}\n\n/** Why `column` cannot be summed over `table`'s rows: not a column, rollup or computed column, or not a number. */\nfunction numberColumnProblem(table: TableMeta, column: string, option: string, verb: string): string | undefined {\n const computed = table.computed && Object.hasOwn(table.computed, column) ? table.computed[column] : undefined;\n const type =\n table.columns.find((c) => c.name === column)?.type ??\n (table.rollups && Object.hasOwn(table.rollups, column) ? table.rollups[column].type : undefined) ??\n (computed ? (computed.type === 'number' ? 'NUMERIC' : computed.type) : undefined);\n if (type === undefined) return `\"${option}\" names \"${column}\", which is not a column, rollup or computed column on ${table.name}.`;\n if (!NUMERIC_TYPE.test(type)) return `\"${option}\" ${verb} \"${column}\", so it must be a number; it is ${type || 'untyped'}.`;\n return undefined;\n}\n\n/**\n * What is wrong with a related list's footer of sums (`totals`) against this\n * schema, or undefined — said at startup and again when the list is planned.\n * A total is a number column of the rows, or one of their numeric rollups or\n * computed columns (`refunded`, the refunds of each order), through any\n * reference `whose` walks.\n */\nexport function relatedMatchProblem(schema: DatabaseSchema, page: PageDef, section: PageList): string | undefined {\n const table = findTable(schema, section.from);\n if (!table) return `\"${section.from}\" is not a table on this connection.`;\n void page;\n for (const column of section.totals ?? []) {\n const problem = numberColumnProblem(table, column, 'totals', 'sums');\n if (problem) return problem;\n }\n return undefined;\n}\n\n/** The grouped record's columns a header says, as paths from the listed rows: read with each group's rows, so a header costs no read. */\nexport function groupFactPaths(section: Pick<PageList, 'group'>): string[] {\n const group = section.group;\n return group?.show?.length ? group.show.map((column) => `${group.by}.${column}`) : [];\n}\n\n/** The table a group's header is a record of, or undefined where the groups are plain values (or the path does not resolve). */\nexport function groupTable(schema: DatabaseSchema, section: Pick<PageList, 'from' | 'group'>): TableMeta | undefined {\n if (!section.group) return undefined;\n const { resolved } = tryResolvePath(schema, section.from, section.group.by);\n if (!resolved) return undefined;\n const { table, column } = resolved;\n /* The declared key the column is the whole of: a leg of a composite key names no one record. */\n const fk = schema.foreignKeys.find((k) => k.from.table === table.id && k.from.columns.length === 1 && k.from.columns[0] === column.name);\n return fk ? findTable(schema, fk.to.table) : undefined;\n}\n\n/**\n * What is wrong with a grouped list against this schema, or undefined — said at startup and again when the list is\n * planned. The header is the record the rows point at through `by`; where `by` ends at a plain column the groups are\n * its values, and there is no record to take facts or an order from.\n */\nexport function groupProblem(schema: DatabaseSchema, page: PageDef, section: PageList): string | undefined {\n const group = section.group;\n if (!group) return undefined;\n const table = findTable(schema, section.from);\n if (!table) return `\"${section.from}\" is not a table on this connection.`;\n const found = tryResolvePath(schema, table.id, group.by);\n if (!found.resolved)\n return `\"group.by\" names \"${group.by}\", which is not a column of ${table.name} or a path from it. ${found.error?.message ?? ''}`.trim();\n for (const column of group.sum ?? []) {\n const problem = numberColumnProblem(table, column, 'group.sum', 'sums');\n if (problem) return problem;\n }\n const target = groupTable(schema, section);\n if (target && target.id === page.base && !section.of)\n return `\"group.by\" names \"${group.by}\", which is the record this page is on, so every row would fall in one group. Group by a record between them, such as the middle of the chain.`;\n if (!target) {\n if (group.show?.length)\n return `\"group.show\" says facts of the record a group is, and \"${group.by}\" is a plain column: its groups are its values. Leave \"show\" out, or group by a reference.`;\n if (group.sort)\n return `\"group.sort\" orders by a column of the record a group is, and \"${group.by}\" is a plain column: its groups are values, largest first. Leave \"sort\" out, or group by a reference.`;\n return undefined;\n }\n for (const name of group.show ?? []) {\n if (!target.columns.some((c) => c.name === name) && !(target.computed && Object.hasOwn(target.computed, name)))\n return `\"group.show\" names \"${name}\", which is not a column of ${target.name}, the record each group is.`;\n }\n if (group.sort && !target.columns.some((c) => c.name === group.sort!.column))\n return `\"group.sort\" names \"${group.sort.column}\", which is not a column of ${target.name}, the record each group is.`;\n return undefined;\n}\n\n/**\n * The groups of a grouped list: one row for each record (or value) the listed rows point at through `by`, with how\n * many rows it holds, its subtotals when the list has `totals`, and, when the groups are ordered by a column of the\n * grouped record, the largest of that column — the sort column depends on the key, so the maximum is exact. It is a\n * plain breakdown over the same rows the list reads, so it adds no SQL shape of its own and the reader's grants,\n * row policy and tenant scope narrow it as they narrow every breakdown.\n */\nexport function groupsView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n ofKeys?: OfKeys,\n now?: Date,\n): { breakdown?: BreakdownDef; empty?: boolean; error?: string } {\n const group = section.group;\n if (!group) return {};\n const problem = groupProblem(schema, page, section) ?? relatedMatchProblem(schema, page, section);\n if (problem) return { error: problem };\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true };\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n const table = findTable(schema, section.from)!;\n const narrowed = withSectionFilter(schema, table.id, rootFilter(section.path, root.key!, resolved.path!), section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n const sort = group.sort;\n return {\n breakdown: {\n base: table.id,\n by: [{ path: group.by }],\n measures: [\n { fn: 'count', alias: 'group_rows' },\n ...(sort ? [{ fn: 'max' as const, path: `${group.by}.${sort.column}`, alias: 'group_order' }] : []),\n ...(section.totals ?? []).map((column, i) => ({ fn: 'sum' as const, path: column, alias: `total_${i}` })),\n ...(group.sum ?? []).map((column, i) => ({ fn: 'sum' as const, path: column, alias: `group_sum_${i}` })),\n ],\n filter: narrowed.filter,\n /* The rows with no value are one group, and they come last whatever the order: the breakdown is paged, so only\n the query can keep them last on every page (a client could place them on the page they happen to fall on). */\n orderBy: [\n { name: group.by, direction: 'asc', emptyLast: true },\n sort ? { name: 'group_order', direction: sort.direction } : { name: 'group_rows', direction: 'desc' },\n ],\n limit: group.limit ?? GROUPS_DEFAULT,\n },\n };\n}\n\n/**\n * The sums a related list's footer says, as a breakdown with no key over the\n * rows the list's reference matches — one row, a `sum` a column, as a\n * narrowed page metric is measured — so the footer counts every row, not the\n * page of them the list has loaded, and a rollup of the rows sums as a column\n * does.\n */\nexport function totalsView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n ofKeys?: OfKeys,\n rootValues?: RootValues,\n): { breakdown?: BreakdownDef; empty?: boolean; error?: string } {\n if (!section.totals?.length) return {};\n /* A matched list sums the rows it shows: the same filter, built from the record's own values. */\n if (section.similar) {\n const problem = relatedMatchProblem(schema, page, section);\n if (problem) return { error: problem };\n const matched = similarView(schema, page, section, key, 0, undefined, rootValues);\n if (!matched.view) return matched.empty ? { empty: true } : { error: matched.error };\n return {\n breakdown: {\n base: matched.view.base,\n by: [],\n measures: section.totals.map((column, i) => ({ fn: 'sum' as const, path: column, alias: `total_${i}` })),\n filter: matched.view.filter,\n limit: 1,\n },\n };\n }\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true };\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n const problem = relatedMatchProblem(schema, page, section);\n if (problem) return { error: problem };\n const table = findTable(schema, section.from)!;\n return {\n breakdown: {\n base: table.id,\n by: [],\n measures: section.totals.map((column, i) => ({ fn: 'sum' as const, path: column, alias: `total_${i}` })),\n filter: rootFilter(section.path, root.key!, resolved.path!),\n limit: 1,\n },\n };\n}\n\n/**\n * A matched list: rows that share the record's values — equal on `same`,\n * within a fraction of the record's value on `near` — and, where they are\n * rows of the page's own table, the record itself left out; closest first on\n * the first `near` column, then the section's own sort, then the key, so equal\n * distances still come back in one order.\n *\n * Built from values the caller read from the root row on the server, never\n * from anything the client sent, and returned as an ordinary view, so the\n * reader's grants, row policy, the public read ceiling and tenant scope\n * narrow it exactly as they narrow any list. A value the record lacks — an\n * empty `same`, a `near` that is not a number — empties the section: a\n * \"similar\" list of everything, because this record's body type is blank,\n * would be a list that lies.\n */\nfunction similarView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n index: number,\n now: Date | undefined,\n rootValues: RootValues | undefined,\n): { view?: ViewDef; empty?: boolean; error?: string } {\n const problem = similarProblem(schema, page, section);\n if (problem) return { error: problem };\n if (!rootValues) return { error: 'The record\\'s values for \"same\" were not read before planning.' };\n const table = findTable(schema, section.from)!;\n const similar = section.similar!;\n const conditions: ViewFilter['groups'][number] = [];\n for (const column of similar.same) {\n const value = rootValues[column];\n if (value === null || value === undefined || (typeof value === 'string' && !value.trim())) return { empty: true };\n if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return { empty: true };\n conditions.push({ path: column, op: '=', value });\n }\n const closest: NonNullable<ViewDef['orderBy']> = [];\n for (const [column, fraction] of Object.entries(similar.near ?? {})) {\n const raw = rootValues[column];\n const value = typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() ? Number(raw) : Number.NaN;\n if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER) return { empty: true };\n let [low, high] = [value * (1 - fraction), value * (1 + fraction)].sort((a, b) => a - b);\n /* A whole-number column keeps whole-number bounds — the same rows, and a\n question the query bar can read back (\"price >= 22497\"). */\n if (/int/i.test(table.columns.find((c) => c.name === column)?.type ?? '')) [low, high] = [Math.ceil(low), Math.floor(high)];\n conditions.push({ path: column, op: '>=', value: low }, { path: column, op: '<=', value: high });\n if (!closest.length) closest.push({ path: column, direction: 'asc', near: value });\n }\n /* The record itself, left out as NOT (every key column equal): one OR-ed\n group per key column, so a composite key leaves out exactly one row. Rows of another table are not it. */\n const keyColumns = primaryKey(table);\n const own = table.id === page.base;\n if (own && keyColumns.some((column) => key[column] === undefined))\n return { error: `The record's key does not name ${table.name}'s ${keyColumns.join(', ')}.` };\n const groups = own ? keyColumns.map((column) => [...conditions, { path: column, op: '!=' as const, value: key[column] }]) : [conditions];\n const narrowed = withSectionFilter(schema, table.id, { groups }, section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n\n const cardColumns =\n typeof section.presentation === 'object'\n ? section.presentation.kind === 'custom'\n ? [...section.presentation.show]\n : cardsColumns(section.presentation)\n : [];\n const columns = [...new Set([...(section.columns?.length ? section.columns : defaultColumns(table, '')), ...cardColumns])].map((p) => ({\n path: p,\n }));\n const sorted = section.orderBy?.map((o) => ({ path: o.column, direction: o.direction })) ?? [];\n const tieBreak = keyColumns.filter((c) => !sorted.some((o) => o.path === c)).map((c) => ({ path: c, direction: 'asc' as const }));\n return {\n view: {\n id: `page:${page.id}:${index}`,\n name: section.title ?? table.name,\n base: table.id,\n columns,\n filter: narrowed.filter,\n orderBy: [...closest, ...sorted, ...tieBreak],\n limit: section.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n };\n}\n\n/**\n * What a list section needs to run, as a view the existing compiler can take.\n *\n * A page adds no query machinery of its own. Every section is a view — that\n * is the point of having built one — so a page is an arrangement of views\n * around a shared root rather than a second way to ask a database questions.\n */\nexport function listView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageList,\n key: Record<string, string | number>,\n index: number,\n now?: Date,\n ofKeys?: OfKeys,\n rootValues?: RootValues,\n): { view?: ViewDef; empty?: boolean; error?: string } {\n /* A similar list walks no path: it is matched on the record's own values. */\n if (section.similar) return similarView(schema, page, section, key, index, now, rootValues);\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true };\n\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n if (resolved.path!.endsAt !== root.table) {\n return {\n error: `\"${section.path}\" leads from ${section.from} to ` + `${resolved.path!.endsAt}, not to ${root.table}.`,\n };\n }\n\n const table = findTable(schema, section.from);\n if (!table) return { error: `\"${section.from}\" is not a table on this connection.` };\n /* Cards read their own columns (title, figure, badge, lines, summary)\n beside whatever the section shows. */\n const cardColumns =\n typeof section.presentation === 'object'\n ? section.presentation.kind === 'custom'\n ? [...section.presentation.show]\n : cardsColumns(section.presentation)\n : [];\n /* A current card is titled by its row's name, which its facts leave out. */\n const titled = section.presentation === 'card' ? [labelColumn(table)].filter((name): name is string => Boolean(name)) : [];\n const columns = [\n ...new Set([\n ...titled,\n ...(section.columns?.length ? section.columns : defaultColumns(table, section.path, resolved.path!)),\n ...cardColumns,\n ...groupFactPaths(section),\n ]),\n ].map((p) => ({ path: p }));\n\n const joined = rootFilter(section.path, root.key!, resolved.path!);\n const narrowed = withSectionFilter(schema, table.id, joined, section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n\n return {\n view: {\n id: `page:${page.id}:${index}`,\n name: section.title ?? table.name,\n base: table.id,\n columns,\n filter: narrowed.filter,\n orderBy: section.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),\n limit: section.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n };\n}\n\n/**\n * The record a section is actually about: the root, or — through `of` — a\n * record the root points at, whose key the planner has already read.\n */\nfunction sectionRoot(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageMetric | PageList,\n key: Record<string, string | number>,\n ofKeys: OfKeys | undefined,\n): { table?: string; key?: Record<string, string | number>; empty?: boolean; error?: string } {\n if (!section.of) return { table: page.base, key };\n const of = resolveOf(schema, page.base, section.of);\n if (of.error) return { error: of.error };\n const referenced = ofKeys?.[section.of];\n if (referenced === undefined) {\n /* The planner read the root row before building sections, so a missing\n entry is the caller's bug, not the page's. Said as what it is. */\n return { error: `The key for \"${section.of}\" was not resolved before planning.` };\n }\n if (referenced === null) return { empty: true };\n return { table: of.table!.id, key: referenced };\n}\n\n/**\n * What a metric section needs to run.\n *\n * Expressed as a one-row view over the *root* table with a single aggregate,\n * rather than as a count over the child: the aggregate machinery already\n * guarantees that two counts over different children cannot inflate each\n * other, and doing it any other way here would give up that guarantee for a\n * number nobody would think to check.\n */\n/**\n * How the query bar would say what a section shows — when it can say it.\n *\n * A count on a page is a number, and a number you cannot get behind is a\n * number you have to take on trust. Clicking one should open the rows it\n * counted, narrowed the same way — and for a long time it opened the child\n * table with no filter at all, because the plan carried no query and the\n * client fell back to the table's own name. The count said 3 and the grid\n * said 20.\n *\n * The query language has no dotted paths: `invoice_line invoice_id.customer_id\n * = 4` is not a thing it can parse, and a page two hops out produces exactly\n * that. So this answers `undefined` rather than something that will not run —\n * the caller opens the composer on the view itself in that case, which can\n * say it. The distinction is deliberate: it is better to be told the language\n * cannot express this than to be shown a filter that quietly is not the one.\n *\n * `is empty` for a null test, because that is the grammar's spelling; `in` is\n * left out, since a list needs bracket syntax the parser writes differently\n * from how it reads it.\n */\nconst QUERY_OPS: Partial<Record<string, string>> = {\n '=': '=',\n '!=': '!=',\n '<': '<',\n '<=': '<=',\n '>': '>',\n '>=': '>=',\n contains: 'contains',\n startsWith: 'startswith',\n endsWith: 'endswith',\n};\n\nexport function sectionQuery(view: ViewDef | undefined): string | undefined {\n if (!view?.base) return undefined;\n const groups = view.filter?.groups?.filter((g) => g.length) ?? [];\n if (!groups.length) return view.base;\n\n const parts: string[] = [];\n for (const group of groups) {\n const conditions: string[] = [];\n for (const c of group) {\n /* A path, not a column: the language filters the table in front of it\n and nothing further out. */\n if (!c.path || c.path.includes('.')) return undefined;\n if (c.op === 'isNull' || c.op === 'isNotNull') {\n conditions.push(`${c.path} is ${c.op === 'isNull' ? '' : 'not '}empty`);\n continue;\n }\n const op = QUERY_OPS[c.op];\n if (c.other !== undefined) {\n if (!op || c.other.includes('.')) return undefined;\n conditions.push(`${c.path} ${op} ${c.other}`);\n continue;\n }\n if (!op || c.value === undefined || c.value === null || Array.isArray(c.value)) return undefined;\n conditions.push(`${c.path} ${op} ${quoteValue(c.value)}`);\n }\n parts.push(conditions.join(' and '));\n }\n return `${view.base} ${parts.join(' or ')}`;\n}\n\nexport function metricView(\n schema: DatabaseSchema,\n page: PageDef,\n section: PageMetric,\n key: Record<string, string | number>,\n index: number,\n now?: Date,\n ofKeys?: OfKeys,\n): { view?: ViewDef; counts?: boolean; breakdown?: BreakdownDef; empty?: boolean; error?: string } {\n const root = sectionRoot(schema, page, section, key, ofKeys);\n if (root.error) return { error: root.error };\n if (root.empty) return { empty: true, counts: section.fn === 'count' };\n\n const resolved = resolvePath(schema, section.from, section.path);\n if (resolved.error) return { error: resolved.error };\n if (resolved.path!.endsAt !== root.table) {\n return {\n error: `\"${section.path}\" leads from ${section.from} to ` + `${resolved.path!.endsAt}, not to ${root.table}.`,\n };\n }\n if (section.fn !== 'count' && !section.column) {\n return { error: `\"${section.fn}\" needs a column to ${section.fn}.` };\n }\n\n const table = findTable(schema, section.from);\n if (!table) return { error: `\"${section.from}\" is not a table on this connection.` };\n if (section.column && !columnOrRollup(table, section.column)) {\n return { error: `\"${table.name}\" has no column called \"${section.column}\".` };\n }\n\n /* The record this section is about — the page's base, or the table `of`\n reaches. The aggregate view below anchors on it, so a sum \"of its site\"\n is a one-row view over site exactly as a sum over the root is one over\n the root. */\n const base = findTable(schema, root.table!);\n if (!base) return { error: `\"${root.table}\" is not a table on this connection.` };\n\n /**\n * A count is a filtered count of the child, not an aggregate on the root.\n *\n * It used to be an aggregate, with `via` taken from the *first* hop — which\n * is the right constraint only when there is one. For anything further out\n * the first hop names a key that does not point at the root at all: on a\n * customer page, `invoice_line via invoice_id.customer_id` handed the\n * aggregate `line_invoice_fk`, which joins lines to invoices and knows\n * nothing about customers. The number that came back was not the number\n * asked for, and looked like one.\n *\n * Counting the child directly is correct at any depth, and it is what the\n * list beside it already does — so a count and the list it summarises can\n * no longer disagree. The view returns one row and its `total`, which is\n * exact: a view only ever joins to-one, so counting it is counting the\n * table it started from.\n */\n const narrowed = withSectionFilter(schema, table.id, rootFilter(section.path, root.key!, resolved.path!), section.filter, now);\n if (narrowed.error) return { error: narrowed.error };\n /* The rows it measures, as the list beside it would show them: what a\n count reads its total from, and what any metric reopens as. */\n const rowsView: ViewDef = {\n id: `page:${page.id}:${index}`,\n name: section.title ?? table.name,\n base: table.id,\n columns: (primaryKey(table)[0] ? [primaryKey(table)[0]] : [table.columns[0].name]).map((path) => ({ path })),\n filter: narrowed.filter,\n limit: 1,\n };\n if (section.fn === 'count') return { counts: true, view: rowsView };\n\n /* A narrowed `sum`, `avg`, `min` or `max` — or one further out than one\n hop — is measured the way a count is counted: over the child's own rows,\n filtered by the path to the root and the section's query, as a\n breakdown with no key, which answers one row. That is correct at any\n depth for the same reason the count is (a path only joins to-one), and\n it is the same filter the list beside it reads, so the figure and the\n list cannot disagree. The aggregate compiler below reaches one hop and\n takes no filter; it was refusing both, which `mistri check` could not\n see coming. */\n /* A rollup of the child (rollups.ts) is not a column the one-hop\n aggregate below can read, so it is measured this way too. */\n const rollup = Boolean(section.column && !table.columns.some((c) => c.name === section.column));\n if (section.filter || resolved.path!.hops.length > 1 || rollup) {\n return {\n view: rowsView,\n breakdown: {\n base: table.id,\n by: [],\n measures: [{ fn: section.fn, path: section.column, alias: 'value' }],\n filter: narrowed.filter,\n limit: 1,\n },\n };\n }\n\n return {\n view: {\n id: `page:${page.id}:${index}`,\n name: section.title ?? `${section.fn} of ${table.name}`,\n base: base.id,\n columns: primaryKey(base).map((c) => ({ path: c })),\n aggregates: [\n {\n fn: section.fn,\n via: `${table.id}.${resolved.path!.hops[0].from.columns.join('+')}`,\n column: section.column,\n alias: 'value',\n },\n ],\n filter: { groups: [primaryKey(base).map((c) => ({ path: c, op: '=' as const, value: root.key![c] }))] },\n limit: 1,\n },\n };\n}\n\n/**\n * Columns worth showing in a list nobody has configured.\n *\n * The path back to the root is dropped: every row in the section has the same\n * value for it, so a column of forty identical numbers is the one column that\n * cannot tell two rows apart. The key and the label come first, because they\n * are what a row is called.\n */\nexport function defaultColumns(table: TableMeta, path: string, resolved?: ResolvedPath): string[] {\n /* The columns the first hop actually uses, not the segment that named it.\n \n A saved path segment can be a constraint name, so splitting the\n path for a column name found `address_country_fk`, matched nothing, and\n left `country_code` in the table: a column reading NZ on every row of a\n section that is *about* NZ. The one column that cannot tell two rows\n apart, kept in the one place it is guaranteed useless. */\n const pathHead = resolved ? resolved.hops[0].from.columns : [path.split('.')[0]];\n const key = primaryKey(table);\n const label = labelColumn(table);\n const ordered = [\n ...key,\n ...(label && !key.includes(label) ? [label] : []),\n ...table.columns.map((c) => c.name).filter((n) => !key.includes(n) && n !== label),\n ];\n return ordered.filter((name) => !pathHead.includes(name)).slice(0, 6);\n}\n\n/* ---------- a custom section's reads ---------- */\n\n/**\n * One declared read of a custom section, planned as the list it is: with\n * `whose`, exactly a related list (listView), the same join back to the\n * record; without, the resource's own rows narrowed by its query — the\n * dashboard's rows section, on a page. Either way the result is a view the\n * server compiles and authorizes on every run, like any section's.\n */\nexport function customReadView(\n schema: DatabaseSchema,\n page: PageDef,\n name: string,\n read: PageCustomRead,\n key: Record<string, string | number>,\n index: number,\n now?: Date,\n): { view?: ViewDef; empty?: boolean; error?: string } {\n if (read.path) {\n const built = listView(\n schema,\n page,\n {\n kind: 'list',\n from: read.from,\n path: read.path,\n columns: read.columns,\n filter: read.filter,\n orderBy: read.orderBy,\n limit: read.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n key,\n index,\n now,\n );\n return built.view ? { ...built, view: { ...built.view, id: `page:${page.id}:${index}:${name}` } } : built;\n }\n const table = findTable(schema, read.from);\n if (!table) return { error: `\"${read.from}\" is not a table on this connection.` };\n let filter: ViewFilter | undefined;\n if (read.filter) {\n const parsed = parseViewQuery(schema, table.id, read.filter, now);\n if (parsed.errors.length) return { error: parsed.errors[0].message };\n if (parsed.filter.groups.length) filter = parsed.filter;\n }\n return {\n view: {\n id: `page:${page.id}:${index}:${name}`,\n name,\n base: table.id,\n columns: read.columns.map((path) => ({ path })),\n ...(filter ? { filter } : {}),\n orderBy: read.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),\n limit: read.limit ?? DEFAULT_PAGE_LIST_LIMIT,\n },\n };\n}\n\n/**\n * What is wrong with a custom section against this schema, or undefined:\n * the record's fields judged as a fields section's, each read as the list it\n * is planned as — its `whose` leading home, its columns, order and query\n * through the compiler that will run them. Said at startup, so a misspelled\n * column is refused before a reader sees the section.\n */\nexport function customProblem(schema: DatabaseSchema, page: PageDef, section: PageCustom, index: number): string | undefined {\n if (section.record?.length) {\n const error = validateView(schema, {\n id: `page:${page.id}:${index}:record`,\n name: 'record',\n base: page.base,\n columns: section.record.map((path) => ({ path })),\n });\n if (error) return `\"record\": ${error}`;\n }\n for (const [name, read] of Object.entries(section.reads ?? {})) {\n const here = `rows \"${name}\"`;\n const table = findTable(schema, read.from);\n if (!table) return `${here}: \"${read.from}\" is not a table on this connection.`;\n if (read.path) {\n const resolved = resolvePath(schema, read.from, read.path);\n if (resolved.error) return `${here}: ${resolved.error}`;\n if (resolved.path!.endsAt !== page.base)\n return `${here}: \"${read.path}\" leads from ${read.from} to ${resolved.path!.endsAt}, not to ${page.base}.`;\n }\n if (read.filter) {\n const parsed = parseViewQuery(schema, table.id, read.filter);\n if (parsed.errors.length) return `${here}: ${saidAt(parsed.errors[0], read.filter.trim())}`;\n }\n const error = validateView(schema, {\n id: `page:${page.id}:${index}:${name}`,\n name,\n base: table.id,\n columns: read.columns.map((path) => ({ path })),\n orderBy: read.orderBy?.map((o) => ({ path: o.column, direction: o.direction })),\n });\n if (error) return `${here}: ${error}`;\n }\n return undefined;\n}\n\n/* ---------- captions ---------- */\n\n/** The hop every path shares, where each is one field past it (`dealer_id.name`, `dealer_id.city` → `dealer_id`), or ''. */\nexport function sharedPrefix(paths: readonly string[] | undefined): string {\n if (!paths?.length) return '';\n const first = paths[0];\n if (!first.includes('.')) return '';\n const prefix = first.slice(0, first.lastIndexOf('.'));\n return paths.every((p) => p.startsWith(`${prefix}.`) && !p.slice(prefix.length + 1).includes('.')) ? prefix : '';\n}\n\n/**\n * The hop a card's facts are captioned without. A card's title is the record\n * (`bike_id`, the reference itself) and its facts are that record's own fields\n * (`bike_id.make`, `bike_id.serial_number`): read with the title, the paths\n * share no hop, and a fact that is the parent's label column was captioned as\n * the reference (\"Bike\" over a serial number). Its own name is what it is there.\n */\nexport function cardStem(paths: readonly string[] | undefined): string {\n const [title, ...facts] = paths ?? [];\n const stem = sharedPrefix(facts);\n /* Only where the title is that record: beside a title of its own, `dealer_id.name` is still \"Dealer\". */\n return stem && (title === stem || title.startsWith(`${stem}.`)) ? stem : sharedPrefix(paths);\n}\n"],"mappings":";;;;;;AAkRA,SAAgB,eAAe,SAA4C;CACzE,MAAM,QAAQ,QAAQ;CACtB,OAAO,OAAO,MAAM,SAAS,MAAM,KAAK,KAAK,WAAW,GAAG,MAAM,GAAG,GAAG,QAAQ,IAAI,CAAC;AACtF;;AAGA,SAAgB,WAAW,QAAwB,SAAkE;CACnH,IAAI,CAAC,QAAQ,OAAO,OAAO,KAAA;CAC3B,MAAM,EAAE,aAAa,eAAe,QAAQ,QAAQ,MAAM,QAAQ,MAAM,EAAE;CAC1E,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,MAAM,EAAE,OAAO,WAAW;CAE1B,MAAM,KAAK,OAAO,YAAY,MAAM,MAAM,EAAE,KAAK,UAAU,MAAM,MAAM,EAAE,KAAK,QAAQ,WAAW,KAAK,EAAE,KAAK,QAAQ,OAAO,OAAO,IAAI;CACvI,OAAO,KAAK,UAAU,QAAQ,GAAG,GAAG,KAAK,IAAI,KAAA;AAC/C;;AAsnBA,SAAgB,aAAa,OAA8C;CACzE,IAAI,CAAC,OAAO,QAAQ,OAAO;CAC3B,MAAM,QAAQ,MAAM;CACpB,IAAI,CAAC,MAAM,SAAS,GAAG,GAAG,OAAO;CACjC,MAAM,SAAS,MAAM,MAAM,GAAG,MAAM,YAAY,GAAG,CAAC;CACpD,OAAO,MAAM,OAAO,MAAM,EAAE,WAAW,GAAG,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,OAAO,SAAS,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,IAAI,SAAS;AAChH;;;;;;;;AASA,SAAgB,SAAS,OAA8C;CACrE,MAAM,CAAC,OAAO,GAAG,SAAS,SAAS,CAAC;CACpC,MAAM,OAAO,aAAa,KAAK;CAE/B,OAAO,SAAS,UAAU,QAAQ,MAAM,WAAW,GAAG,KAAK,EAAE,KAAK,OAAO,aAAa,KAAK;AAC7F"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { d as firstPaintRead, it as useQuery, x as fetchServer } from "./shared-poc11xxo.js";
|
|
2
2
|
import { gt as defineMessages } from "./shared-DJZjSi3J.js";
|
|
3
|
-
import "./shared-
|
|
3
|
+
import "./shared-BoLbc2WD.js";
|
|
4
4
|
import { a as served } from "./shared-sZ8B7ZTw.js";
|
|
5
5
|
import { t as Modal } from "./shared-CRr4kJXB.js";
|
|
6
|
-
import { t as onDemand } from "./shared-
|
|
6
|
+
import { t as onDemand } from "./shared-B-BUF9fx.js";
|
|
7
7
|
import "./shared-C3MTHv9P.js";
|
|
8
8
|
import "./shared-CERG8O1B.js";
|
|
9
|
-
import { i as formArrives, s as submitPublicForm, t as FormSurface } from "./shared-
|
|
10
|
-
import "./shared-
|
|
9
|
+
import { i as formArrives, s as submitPublicForm, t as FormSurface } from "./shared-B4jgU8h9.js";
|
|
10
|
+
import "./shared-CL3RLKDA.js";
|
|
11
11
|
import { useId, useMemo } from "react";
|
|
12
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
import "react-dom/client";
|
|
@@ -74,10 +74,10 @@ var say$1 = defineMessages({
|
|
|
74
74
|
addYourApplication: "Add your application · {name}",
|
|
75
75
|
signInToAddTheApplication: "Sign in to add the application you just submitted to your account."
|
|
76
76
|
});
|
|
77
|
-
onDemand(() => import("./shared-
|
|
78
|
-
onDemand(() => import("./shared-
|
|
79
|
-
onDemand(() => import("./shared-
|
|
80
|
-
onDemand(() => import("./shared-
|
|
77
|
+
onDemand(() => import("./shared-5RQMi4BR.js"), "SavedFormSurface");
|
|
78
|
+
onDemand(() => import("./shared-K7Ym27yg.js"), "FormStatusSurface");
|
|
79
|
+
onDemand(() => import("./shared-CA52ClGo.js"), "FormClaimSurface");
|
|
80
|
+
onDemand(() => import("./shared-DXM7d7lJ.js"), "PlacedFormSurface");
|
|
81
81
|
/** @type {typeof import('./publicPlacement.js') | undefined} */
|
|
82
82
|
var placements;
|
|
83
83
|
/**
|
|
@@ -115,7 +115,7 @@ async function loadPublicForm(id, options = {}) {
|
|
|
115
115
|
if (res.ok && data && typeof data === "object" && "form" in data) {
|
|
116
116
|
if (data.form?.id !== id || options.appId && data.app?.id !== options.appId) throw new Error(say$1("thereIsNoSuch"));
|
|
117
117
|
if ("placement" in data) {
|
|
118
|
-
placements ??= await import("./shared-
|
|
118
|
+
placements ??= await import("./shared-Csfi3Pti.js");
|
|
119
119
|
return placements.preparePublicPlacement(data);
|
|
120
120
|
}
|
|
121
121
|
if (data.form) await formArrives(data.form);
|
|
@@ -146,7 +146,7 @@ var say = defineMessages({
|
|
|
146
146
|
* string, exactly as they would in the address, so the two routes to the form
|
|
147
147
|
* cannot drift apart.
|
|
148
148
|
*/
|
|
149
|
-
var PlacedFormSurface = onDemand(() => import("./shared-
|
|
149
|
+
var PlacedFormSurface = onDemand(() => import("./shared-DXM7d7lJ.js"), "PlacedFormSurface");
|
|
150
150
|
function FormDialog({ view, appId, prefill, onClose }) {
|
|
151
151
|
const instance = useId();
|
|
152
152
|
const prepare = useMemo(() => {
|
|
@@ -210,4 +210,4 @@ function FormDialog({ view, appId, prefill, onClose }) {
|
|
|
210
210
|
}
|
|
211
211
|
export { FormDialog };
|
|
212
212
|
|
|
213
|
-
//# sourceMappingURL=shared-
|
|
213
|
+
//# sourceMappingURL=shared-Be3nbWsd.js.map
|