@rubytech/create-maxy-code 0.1.489 → 0.1.491

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +1716 -0
  3. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +103 -0
  4. package/payload/platform/lib/storage-broker/dist/audit.d.ts +29 -1
  5. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  6. package/payload/platform/lib/storage-broker/dist/audit.js +25 -2
  7. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  8. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -0
  9. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  10. package/payload/platform/lib/storage-broker/dist/cf-exec.js +22 -0
  11. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  12. package/payload/platform/lib/storage-broker/src/audit.ts +59 -1
  13. package/payload/platform/lib/storage-broker/src/cf-exec.ts +48 -0
  14. package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +34 -5
  15. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +28 -8
  16. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +133 -35
  17. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +95 -0
  18. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +141 -9
  19. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +83 -0
  20. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +48 -0
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +62 -0
  22. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +103 -0
  23. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +211 -0
  24. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +168 -0
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +138 -0
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +111 -4
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +14 -13
  28. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +52 -14
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +70 -7
  30. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +74 -0
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +91 -0
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +23 -0
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +45 -0
  34. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +59 -4
  35. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +58 -9
  36. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +358 -16
  37. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -1
  38. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +45 -0
  39. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +216 -20
  40. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +5 -122
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  42. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  43. package/payload/platform/templates/agents/passive/IDENTITY.md +6 -0
  44. package/payload/platform/templates/agents/public/IDENTITY.md +6 -0
  45. package/payload/platform/templates/specialists/agents/citation-auditor.md +6 -0
  46. package/payload/platform/templates/specialists/agents/coding-assistant.md +6 -0
  47. package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +6 -0
  48. package/payload/platform/templates/specialists/agents/content-producer.md +6 -0
  49. package/payload/platform/templates/specialists/agents/data-manager.md +6 -0
  50. package/payload/platform/templates/specialists/agents/database-operator.md +6 -0
  51. package/payload/platform/templates/specialists/agents/librarian.md +6 -0
  52. package/payload/platform/templates/specialists/agents/personal-assistant.md +6 -0
  53. package/payload/platform/templates/specialists/agents/project-manager.md +6 -0
  54. package/payload/platform/templates/specialists/agents/public-session-reviewer.md +6 -0
  55. package/payload/platform/templates/specialists/agents/research-assistant.md +7 -1
  56. package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +6 -0
  57. package/payload/premium-plugins/writer-craft/agents/writer-craft--manuscript-reviewer.md +6 -0
  58. package/payload/server/{chunk-UFXRUKXO.js → chunk-KZFE3MOY.js} +49 -3
  59. package/payload/server/public/activity.html +5 -6
  60. package/payload/server/public/assets/AdminLoginScreens-DgVFsHtd.js +1 -0
  61. package/payload/server/public/assets/AdminShell-BwNqwb4k.js +2 -0
  62. package/payload/server/public/assets/activity-2DL2DwLr.js +1 -0
  63. package/payload/server/public/assets/admin-COe0dT2U.js +1 -0
  64. package/payload/server/public/assets/{arc-D-QzyndF.js → arc-CFMJyMm-.js} +1 -1
  65. package/payload/server/public/assets/architecture-YZFGNWBL-Cypt5qHL.js +1 -0
  66. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-W7f0Us4S.js → architectureDiagram-Q4EWVU46-CiSSgdL8.js} +1 -1
  67. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CfPS4FXg.js → blockDiagram-DXYQGD6D-_sXtIW5c.js} +1 -1
  68. package/payload/server/public/assets/{bot-DnuyRsLS.js → bot-CpBuDQlF.js} +1 -1
  69. package/payload/server/public/assets/{browser-q4EFqKvR.js → browser-CSApuZrv.js} +1 -1
  70. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Dq60cLVI.js → c4Diagram-AHTNJAMY-Bh7OAMXs.js} +1 -1
  71. package/payload/server/public/assets/{calendar-C-q9kWmA.js → calendar-DlvwZnS7.js} +1 -1
  72. package/payload/server/public/assets/channel-BMyXE4zc.js +1 -0
  73. package/payload/server/public/assets/chat-DWvYv9jq.js +1 -0
  74. package/payload/server/public/assets/chevron-down-Dnuuw6Q2.js +1 -0
  75. package/payload/server/public/assets/chevron-left-CC4zmWOr.js +1 -0
  76. package/payload/server/public/assets/chevron-right-CG_lHTx_.js +1 -0
  77. package/payload/server/public/assets/{chunk-2KRD3SAO-CXDjuDN0.js → chunk-2KRD3SAO-D4fktAwW.js} +1 -1
  78. package/payload/server/public/assets/{chunk-336JU56O-926ox0tj.js → chunk-336JU56O-CcQoKH2G.js} +2 -2
  79. package/payload/server/public/assets/chunk-426QAEUC-Xu6JKP4d.js +1 -0
  80. package/payload/server/public/assets/{chunk-4BX2VUAB-C2nrPCfN.js → chunk-4BX2VUAB-C13Wz3zS.js} +1 -1
  81. package/payload/server/public/assets/{chunk-4TB4RGXK-CuW3FOR6.js → chunk-4TB4RGXK-BL48D7gN.js} +1 -1
  82. package/payload/server/public/assets/{chunk-55IACEB6-B4_-In90.js → chunk-55IACEB6-TmOKdy8a.js} +1 -1
  83. package/payload/server/public/assets/{chunk-5FUZZQ4R-DnYGr9CP.js → chunk-5FUZZQ4R-DZ_jSF6N.js} +1 -1
  84. package/payload/server/public/assets/{chunk-5PVQY5BW-BYCOi9bf.js → chunk-5PVQY5BW-De4G1vfV.js} +1 -1
  85. package/payload/server/public/assets/{chunk-67CJDMHE-CBxsC4JP.js → chunk-67CJDMHE-Cif9Uu5C.js} +1 -1
  86. package/payload/server/public/assets/{chunk-7N4EOEYR-CAACaqFr.js → chunk-7N4EOEYR-WjTHCXnd.js} +1 -1
  87. package/payload/server/public/assets/{chunk-AA7GKIK3-BoamZdS6.js → chunk-AA7GKIK3-CwCfdQmb.js} +1 -1
  88. package/payload/server/public/assets/{chunk-BSJP7CBP-CHk82eFy.js → chunk-BSJP7CBP-9A67yXwT.js} +1 -1
  89. package/payload/server/public/assets/{chunk-CIAEETIT-DdeW9zxl.js → chunk-CIAEETIT-DPMiWSBi.js} +1 -1
  90. package/payload/server/public/assets/{chunk-EDXVE4YY-DJqSRv2w.js → chunk-EDXVE4YY-C2f_Qvx-.js} +1 -1
  91. package/payload/server/public/assets/{chunk-ENJZ2VHE-BRi4BwM5.js → chunk-ENJZ2VHE-B-Q9EbSQ.js} +1 -1
  92. package/payload/server/public/assets/{chunk-FMBD7UC4-OJp2CtUE.js → chunk-FMBD7UC4-Bmt_ZRlV.js} +1 -1
  93. package/payload/server/public/assets/{chunk-FOC6F5B3-BUAQ6spG.js → chunk-FOC6F5B3-DfGJGxpX.js} +1 -1
  94. package/payload/server/public/assets/{chunk-ICPOFSXX-DeRggD-Q.js → chunk-ICPOFSXX-Wyym1Q8K.js} +2 -2
  95. package/payload/server/public/assets/{chunk-K5T4RW27-EHiXZ8Py.js → chunk-K5T4RW27-DMpoaqw2.js} +1 -1
  96. package/payload/server/public/assets/{chunk-KGLVRYIC-tOajPcpn.js → chunk-KGLVRYIC-DYUFgri2.js} +1 -1
  97. package/payload/server/public/assets/{chunk-LIHQZDEY-uwO6oHIU.js → chunk-LIHQZDEY-x3LmeKmx.js} +1 -1
  98. package/payload/server/public/assets/{chunk-ORNJ4GCN-Dpa1_BCA.js → chunk-ORNJ4GCN-BeuNAWkW.js} +1 -1
  99. package/payload/server/public/assets/{chunk-OYMX7WX6-ChdDqaQL.js → chunk-OYMX7WX6-BUWVAfQA.js} +1 -1
  100. package/payload/server/public/assets/chunk-QZHKN3VN-7_kS3RBy.js +1 -0
  101. package/payload/server/public/assets/{chunk-U2HBQHQK-5JgWosPv.js → chunk-U2HBQHQK-DKS5adQo.js} +1 -1
  102. package/payload/server/public/assets/{chunk-X2U36JSP-BGJqysB2.js → chunk-X2U36JSP-CT_UM4IN.js} +1 -1
  103. package/payload/server/public/assets/{chunk-XPW4576I-GERE2ekx.js → chunk-XPW4576I-ByFnN4JO.js} +1 -1
  104. package/payload/server/public/assets/{chunk-YZCP3GAM-BskqX1V5.js → chunk-YZCP3GAM-DCetC20J.js} +1 -1
  105. package/payload/server/public/assets/{chunk-ZZ45TVLE-DG1iY-L9.js → chunk-ZZ45TVLE-Bh1QIv8N.js} +1 -1
  106. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DJqbNcuC.js +1 -0
  107. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CGglqfQA.js +1 -0
  108. package/payload/server/public/assets/clone-BiTMTzp_.js +1 -0
  109. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-C8Rl4ykh.js → cose-bilkent-S5V4N54A-DgLEaApT.js} +1 -1
  110. package/payload/server/public/assets/{dagre-KV5264BT-C0YrJPF3.js → dagre-KV5264BT-BfkUPrEV.js} +1 -1
  111. package/payload/server/public/assets/{dagre-DTlv-FFy.js → dagre-unhMIyVo.js} +1 -1
  112. package/payload/server/public/assets/data-CJbC_cVy.js +1 -0
  113. package/payload/server/public/assets/{diagram-5BDNPKRD-Dfk5oepI.js → diagram-5BDNPKRD-BjjwcTD9.js} +1 -1
  114. package/payload/server/public/assets/{diagram-G4DWMVQ6-QRj55DiB.js → diagram-G4DWMVQ6-BbgWtVSS.js} +1 -1
  115. package/payload/server/public/assets/{diagram-MMDJMWI5-BAPKsohU.js → diagram-MMDJMWI5-DyAnkWGy.js} +1 -1
  116. package/payload/server/public/assets/{diagram-TYMM5635-C3iZXPkC.js → diagram-TYMM5635-SAMMoLxq.js} +1 -1
  117. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DIbI7g_5.js → erDiagram-SMLLAGMA-Cvo-5-fD.js} +1 -1
  118. package/payload/server/public/assets/{flatten-DlAJeN0L.js → flatten-BXzaHgtL.js} +1 -1
  119. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-l3eJJr8R.js → flowDiagram-DWJPFMVM-y3o2Ycld.js} +1 -1
  120. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-aaEzHxIq.js → ganttDiagram-T4ZO3ILL-BEm_CQaK.js} +1 -1
  121. package/payload/server/public/assets/gitGraph-7Q5UKJZL-C_-XjxLd.js +1 -0
  122. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Di-1eg_m.js → gitGraphDiagram-UUTBAWPF-CqmOxS7F.js} +1 -1
  123. package/payload/server/public/assets/{graph-Cyh6FWaV.js → graph-kaxS9a_7.js} +2 -2
  124. package/payload/server/public/assets/{graph-labels-CUQtXFKW.js → graph-labels-GgejRtgV.js} +1 -1
  125. package/payload/server/public/assets/{graphlib-D5awC2yp.js → graphlib-_HfbQcBf.js} +1 -1
  126. package/payload/server/public/assets/info-OMHHGYJF-BH8yDCfL.js +1 -0
  127. package/payload/server/public/assets/infoDiagram-42DDH7IO-DyDXyJ6U.js +2 -0
  128. package/payload/server/public/assets/{isEmpty-Dh1dF-tM.js → isEmpty-uB1dEu_Z.js} +1 -1
  129. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CLOd_cNf.js → ishikawaDiagram-UXIWVN3A-C_d0vcZC.js} +1 -1
  130. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-cGyfW6Tg.js → journeyDiagram-VCZTEJTY-DsjRBqlW.js} +1 -1
  131. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CRTKWPlh.js → kanban-definition-6JOO6SKY-BwtK6LS-.js} +1 -1
  132. package/payload/server/public/assets/{line-B8Brm0TD.js → line-szBCFOLJ.js} +1 -1
  133. package/payload/server/public/assets/{linear-BHnSKXBe.js → linear-X3O7lwK3.js} +1 -1
  134. package/payload/server/public/assets/{maximize-2-k3Fu5hdP.js → maximize-2-CPq3nwZA.js} +1 -1
  135. package/payload/server/public/assets/{mermaid-parser.core-BtNpdQct.js → mermaid-parser.core-BavvMAhU.js} +2 -2
  136. package/payload/server/public/assets/{mermaid.core-BY87anQZ.js → mermaid.core-Br2F9Hdm.js} +3 -3
  137. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-rUSv9qN1.js → mindmap-definition-QFDTVHPH-4qlwXoJ-.js} +1 -1
  138. package/payload/server/public/assets/operator-CldXXilj.js +1 -0
  139. package/payload/server/public/assets/{ordinal-Bnl5GL-x.js → ordinal-CZY9laxg.js} +1 -1
  140. package/payload/server/public/assets/packet-4T2RLAQJ-Bajcns4r.js +1 -0
  141. package/payload/server/public/assets/{page-Cno8NNMk.js → page-BE6DkJP9.js} +1 -1
  142. package/payload/server/public/assets/{page-DhwPG5Zf.js → page-BIwdWXnv.js} +3 -3
  143. package/payload/server/public/assets/{pdf-render-CrpAi4Ba.js → pdf-render-H884L3EI.js} +1 -1
  144. package/payload/server/public/assets/pie-ZZUOXDRM-D7hjM5w1.js +1 -0
  145. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Cjn9IP-C.js → pieDiagram-DEJITSTG-BbXryIg0.js} +1 -1
  146. package/payload/server/public/assets/{public-BJ5Oo2Fw.js → public-BKQEQGPE.js} +1 -1
  147. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BkSHkSsn.js → quadrantDiagram-34T5L4WZ-BHLjcvh3.js} +1 -1
  148. package/payload/server/public/assets/radar-PYXPWWZC-B46d5x7a.js +1 -0
  149. package/payload/server/public/assets/{reduce-Bp5oNw1n.js → reduce-BGQzAenn.js} +1 -1
  150. package/payload/server/public/assets/{requirementDiagram-MS252O5E-hLwgyB-F.js → requirementDiagram-MS252O5E-BXaVO0lk.js} +1 -1
  151. package/payload/server/public/assets/{rotate-ccw-D-ov5jM4.js → rotate-ccw-DieZzl10.js} +1 -1
  152. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D6Dl1RIa.js → sankeyDiagram-XADWPNL6-CmN1s17Q.js} +1 -1
  153. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-j0QBqkbL.js → sequenceDiagram-FGHM5R23-DC08e5NY.js} +1 -1
  154. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-TvEBNsuV.js → stateDiagram-FHFEXIEX-DnoQMuOW.js} +1 -1
  155. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CrstH34p.js +1 -0
  156. package/payload/server/public/assets/{tasks-DHMxTZVD.js → tasks-DYyCGz8W.js} +1 -1
  157. package/payload/server/public/assets/{time-entry-format-EmV5lz6o.js → time-entry-format-CC_9jFuR.js} +1 -1
  158. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BfivOP-j.js → timeline-definition-GMOUNBTQ-eTn3uRaH.js} +1 -1
  159. package/payload/server/public/assets/treeView-SZITEDCU-3XQh5xQ1.js +1 -0
  160. package/payload/server/public/assets/treemap-W4RFUUIX-DwR-_l5z.js +1 -0
  161. package/payload/server/public/assets/{triangle-alert-CivyPfAL.js → triangle-alert-7mQfCQWP.js} +1 -1
  162. package/payload/server/public/assets/{useCopyFeedback-hbnPS-r9.js → useCopyFeedback-os7N0yCx.js} +1 -1
  163. package/payload/server/public/assets/useSubAccountSwitcher-Bgfqig1V.css +1 -0
  164. package/payload/server/public/assets/{useVoiceRecorder-QN9_GqB_.js → useVoiceRecorder-ZFwt5i6d.js} +2 -2
  165. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Bvfw9aFJ.js → vennDiagram-DHZGUBPP-DNx6o_ZQ.js} +1 -1
  166. package/payload/server/public/assets/wardley-RL74JXVD-DZp9oCGv.js +1 -0
  167. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C9h04G6b.js → wardleyDiagram-NUSXRM2D-BZOchqF3.js} +1 -1
  168. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BMvi2m2q.js → xychartDiagram-5P7HB3ND-ZBEGH6ZI.js} +1 -1
  169. package/payload/server/public/browser.html +4 -4
  170. package/payload/server/public/calendar.html +6 -6
  171. package/payload/server/public/chat.html +14 -14
  172. package/payload/server/public/data.html +11 -11
  173. package/payload/server/public/graph.html +10 -11
  174. package/payload/server/public/index.html +16 -17
  175. package/payload/server/public/operator.html +15 -16
  176. package/payload/server/public/public.html +14 -14
  177. package/payload/server/public/tasks.html +6 -6
  178. package/payload/server/server.js +1488 -895
  179. package/payload/server/{src-YJF3UJNR.js → src-UYDFMWF4.js} +1 -1
  180. package/payload/platform/lib/active-rules/package-lock.json +0 -12
  181. package/payload/server/public/assets/AdminLoginScreens-Dic_fPt6.js +0 -1
  182. package/payload/server/public/assets/AdminShell-JN7rZ8aG.js +0 -2
  183. package/payload/server/public/assets/Checkbox-CB74kINA.js +0 -1
  184. package/payload/server/public/assets/activity-DbfpHg3i.js +0 -1
  185. package/payload/server/public/assets/admin-DvQPTxqe.js +0 -1
  186. package/payload/server/public/assets/architecture-YZFGNWBL-CGvD4r9F.js +0 -1
  187. package/payload/server/public/assets/channel-Bpg-ADC0.js +0 -1
  188. package/payload/server/public/assets/chat-DcefIV9q.js +0 -1
  189. package/payload/server/public/assets/chevron-down-Blma_CKl.js +0 -1
  190. package/payload/server/public/assets/chevron-left-xeYDxFiC.js +0 -1
  191. package/payload/server/public/assets/chevron-right-BonbtjqJ.js +0 -1
  192. package/payload/server/public/assets/chunk-426QAEUC-1poG7Nd3.js +0 -1
  193. package/payload/server/public/assets/chunk-QZHKN3VN-GN1tIO8J.js +0 -1
  194. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DMGVq8Jv.js +0 -1
  195. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfddT8UV.js +0 -1
  196. package/payload/server/public/assets/clone-Cc9vFIEk.js +0 -1
  197. package/payload/server/public/assets/data-Bu-5SUmR.js +0 -1
  198. package/payload/server/public/assets/gitGraph-7Q5UKJZL-DtjXE0UI.js +0 -1
  199. package/payload/server/public/assets/info-OMHHGYJF-CmArhGYf.js +0 -1
  200. package/payload/server/public/assets/infoDiagram-42DDH7IO-utWz7oZy.js +0 -2
  201. package/payload/server/public/assets/operator-8NVpNLeD.js +0 -1
  202. package/payload/server/public/assets/packet-4T2RLAQJ-BTfK0Zw4.js +0 -1
  203. package/payload/server/public/assets/pie-ZZUOXDRM-BvtPeMQY.js +0 -1
  204. package/payload/server/public/assets/radar-PYXPWWZC-D2UXvQgn.js +0 -1
  205. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ur4eM6rb.js +0 -1
  206. package/payload/server/public/assets/treeView-SZITEDCU-0dE7FvM2.js +0 -1
  207. package/payload/server/public/assets/treemap-W4RFUUIX-BrwlXpr5.js +0 -1
  208. package/payload/server/public/assets/useSubAccountSwitcher-kwhG-aJj.css +0 -1
  209. package/payload/server/public/assets/wardley-RL74JXVD-CO9HdfJ-.js +0 -1
  210. /package/payload/server/public/assets/{_baseFor-BBkmqPEk.js → _baseFor-CMWvz0uA.js} +0 -0
  211. /package/payload/server/public/assets/{array-Bjtb2C_5.js → array-Cny_NT6T.js} +0 -0
  212. /package/payload/server/public/assets/{cytoscape.esm-B1dQ664k.js → cytoscape.esm-De0CCmB2.js} +0 -0
  213. /package/payload/server/public/assets/{defaultLocale-iwYA7ne8.js → defaultLocale-BahRht8U.js} +0 -0
  214. /package/payload/server/public/assets/{dist-CLmrZbY7.js → dist-C9D_xz81.js} +0 -0
  215. /package/payload/server/public/assets/{init-CxnBAgcf.js → init-DoIvjoJ_.js} +0 -0
  216. /package/payload/server/public/assets/{katex-DojHoLIr.js → katex-Bfn1OZEl.js} +0 -0
  217. /package/payload/server/public/assets/{path-JZiojYzQ.js → path-CTezU8L5.js} +0 -0
  218. /package/payload/server/public/assets/{preload-helper-CT1Z6Pdu.js → preload-helper-D_sPnjeL.js} +0 -0
  219. /package/payload/server/public/assets/{rough.esm-D5NinLFK.js → rough.esm-BJo4pvXu.js} +0 -0
  220. /package/payload/server/public/assets/{src-DQ0MSD6q.js → src-BTKk8KAG.js} +0 -0
  221. /package/payload/server/public/assets/{useSubAccountSwitcher-DotTrvX3.js → useSubAccountSwitcher-Q941cZYV.js} +0 -0
@@ -1,7 +1,8 @@
1
1
  import type { Handler, Logger, PortalEnv } from './_lib/types'
2
2
  import { readSessionCookie, resolveSession } from './_lib/session'
3
3
  import { q } from './_lib/log'
4
- import { authorizeKey, ownerPrefix } from './_lib/authorize'
4
+ import { authorizeKey } from './_lib/authorize'
5
+ import { checkTarget, uploadKey } from './_lib/target'
5
6
 
6
7
  const MAX_FILENAME = 200
7
8
 
@@ -14,9 +15,28 @@ const MAX_FILENAME = 200
14
15
  // isolate, and the two must move independently.
15
16
  const MAX_UPLOAD_BYTES = 25 * 1024 * 1024
16
17
 
18
+ /**
19
+ * The per-part ceiling (Task 1910).
20
+ *
21
+ * Identical to MAX_UPLOAD_BYTES and for exactly the same reason: a part is read
22
+ * wholly into the Function's memory just as a single-shot body is. Parts do not
23
+ * relax the isolate bound, they let a large file respect it repeatedly.
24
+ */
25
+ export const MAX_PART_BYTES = 25 * 1024 * 1024
26
+
27
+ /**
28
+ * The whole-file ceiling.
29
+ *
30
+ * Not an isolate limit — no single request ever carries this much. It bounds
31
+ * what one client may push through one portal in one go, so a mistaken drag of
32
+ * a disk image is refused before it consumes an account's storage.
33
+ */
34
+ export const MAX_TOTAL_BYTES = 1024 * 1024 * 1024
35
+
17
36
  export async function processUpload(
18
37
  sessionId: string,
19
38
  filename: string,
39
+ targetPath: string,
20
40
  bytes: Uint8Array,
21
41
  env: PortalEnv,
22
42
  log: Logger,
@@ -41,7 +61,22 @@ export async function processUpload(
41
61
  return { status: 400, payload: { ok: false, error: 'filename required' } }
42
62
  }
43
63
 
44
- const key = `${ownerPrefix(owner)}${filename}`
64
+ // The target is resolved BEFORE any R2 call, the same ordering rule the owner
65
+ // gate follows: a denied caller must never be able to cause a store write.
66
+ const target = await checkTarget(env.DB, session.accountId, session.folders, targetPath)
67
+ if (!target.ok) {
68
+ log(
69
+ `[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
70
+ `targetPath=${q(targetPath)} result=denied-${target.reason}`,
71
+ )
72
+ return { status: 403, payload: { ok: false, error: 'target not allowed' } }
73
+ }
74
+ log(
75
+ `[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
76
+ `targetPath=${q(targetPath)} result=allowed`,
77
+ )
78
+
79
+ const key = uploadKey(owner, targetPath, filename)
45
80
  // The key is derived from the session owner, then re-checked through the same
46
81
  // gate the other routes use — a filename carrying '..' must not escape.
47
82
  if (!authorizeKey(owner, key)) {
@@ -63,7 +98,8 @@ export async function processUpload(
63
98
  }
64
99
 
65
100
  log(
66
- `[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} filename=${q(filename)} bytes=${bytes.byteLength}`,
101
+ `[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} filename=${q(filename)} ` +
102
+ `targetPath=${q(targetPath)} bytes=${bytes.byteLength}`,
67
103
  )
68
104
 
69
105
  try {
@@ -85,17 +121,15 @@ export async function processUpload(
85
121
  // person their upload failed. Resetting `ingested` is the load-bearing
86
122
  // part: without it a replacement of an already-swept file would never be
87
123
  // ingested, and nothing would ever say so.
88
- await env.DB.prepare(
89
- `INSERT INTO manifest (fileId, ownerId, filename, objectKey, size, uploadedAt, ingested)
90
- VALUES (?, ?, ?, ?, ?, ?, ?)
91
- ON CONFLICT (objectKey) DO UPDATE SET
92
- fileId = excluded.fileId,
93
- size = excluded.size,
94
- uploadedAt = excluded.uploadedAt,
95
- ingested = 0`,
96
- )
97
- .bind(fileId, owner, filename, key, bytes.byteLength, new Date(startedMs).toISOString(), 0)
98
- .run()
124
+ await writeManifestRow(env, {
125
+ fileId,
126
+ owner,
127
+ filename,
128
+ key,
129
+ size: bytes.byteLength,
130
+ at: new Date(startedMs).toISOString(),
131
+ targetPath,
132
+ })
99
133
  log(`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} result=ok`)
100
134
  } catch (err) {
101
135
  log(
@@ -118,29 +152,337 @@ export async function processUpload(
118
152
  return { status: 200, payload: { ok: true, fileId, objectPresent, rowPresent } }
119
153
  }
120
154
 
155
+ /** The manifest upsert, shared by the single-shot and multipart paths so the
156
+ * two can never drift on what a stored row means. */
157
+ async function writeManifestRow(
158
+ env: PortalEnv,
159
+ row: { fileId: string; owner: string; filename: string; key: string; size: number; at: string; targetPath: string },
160
+ ): Promise<void> {
161
+ await env.DB.prepare(
162
+ `INSERT INTO manifest (fileId, ownerId, filename, objectKey, size, uploadedAt, ingested, targetPath)
163
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
164
+ ON CONFLICT (objectKey) DO UPDATE SET
165
+ fileId = excluded.fileId,
166
+ size = excluded.size,
167
+ uploadedAt = excluded.uploadedAt,
168
+ targetPath = excluded.targetPath,
169
+ ingested = 0,
170
+ devicePath = '',
171
+ deleted = 0`,
172
+ )
173
+ .bind(row.fileId, row.owner, row.filename, row.key, row.size, row.at, 0, row.targetPath)
174
+ .run()
175
+ }
176
+
177
+ /**
178
+ * Open a multipart session.
179
+ *
180
+ * The declared size is checked BEFORE the session is opened, so an over-cap
181
+ * declaration never leaves an orphan multipart upload accruing storage that
182
+ * nothing would ever clean up.
183
+ */
184
+ export async function processMultipartCreate(
185
+ sessionId: string,
186
+ filename: string,
187
+ targetPath: string,
188
+ declaredSize: number,
189
+ env: PortalEnv,
190
+ log: Logger,
191
+ newId: () => string,
192
+ now: () => number,
193
+ ): Promise<Handler> {
194
+ const startedMs = now()
195
+ const session = await resolveSession(env.DB, sessionId, startedMs)
196
+ if (!session) {
197
+ log('[data-portal] op=mp-create owner=none result=denied')
198
+ return { status: 401, payload: { ok: false, error: 'denied' } }
199
+ }
200
+ const owner = session.ownerId
201
+ const uploadId = newId()
202
+
203
+ if (!filename || filename.length > MAX_FILENAME) {
204
+ log(`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
205
+ return { status: 400, payload: { ok: false, error: 'filename required' } }
206
+ }
207
+ if (!Number.isFinite(declaredSize) || declaredSize < 0 || declaredSize > MAX_TOTAL_BYTES) {
208
+ log(
209
+ `[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} ` +
210
+ `declared=${declaredSize} result=too-large limit=${MAX_TOTAL_BYTES}`,
211
+ )
212
+ return {
213
+ status: 413,
214
+ payload: { ok: false, error: `file is over the ${MAX_TOTAL_BYTES}-byte limit` },
215
+ }
216
+ }
217
+
218
+ const target = await checkTarget(env.DB, session.accountId, session.folders, targetPath)
219
+ if (!target.ok) {
220
+ log(
221
+ `[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
222
+ `targetPath=${q(targetPath)} result=denied-${target.reason}`,
223
+ )
224
+ return { status: 403, payload: { ok: false, error: 'target not allowed' } }
225
+ }
226
+
227
+ const key = uploadKey(owner, targetPath, filename)
228
+ if (!authorizeKey(owner, key)) {
229
+ log(`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
230
+ return { status: 400, payload: { ok: false, error: 'invalid filename' } }
231
+ }
232
+
233
+ const mp = await env.BUCKET.createMultipartUpload(key)
234
+ log(
235
+ `[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} key=${q(key)} ` +
236
+ `declared=${declaredSize} parts=${Math.ceil(declaredSize / MAX_PART_BYTES)} result=ok`,
237
+ )
238
+ return { status: 200, payload: { ok: true, uploadId, key, r2UploadId: mp.uploadId } }
239
+ }
240
+
241
+ /** One part. The key arrives from the client on every request, so it is
242
+ * re-authorized on every request — a session that may write `alice/` must not
243
+ * be able to append a part to `bob/`. */
244
+ export async function processMultipartPart(
245
+ sessionId: string,
246
+ key: string,
247
+ r2UploadId: string,
248
+ partNumber: number,
249
+ bytes: Uint8Array,
250
+ env: PortalEnv,
251
+ log: Logger,
252
+ ): Promise<Handler> {
253
+ const session = await resolveSession(env.DB, sessionId, Date.now())
254
+ if (!session) {
255
+ log('[data-portal] op=r2-put owner=none result=denied')
256
+ return { status: 401, payload: { ok: false, error: 'denied' } }
257
+ }
258
+ if (!authorizeKey(session.ownerId, key)) {
259
+ log(`[data-portal] op=r2-put owner=${q(session.ownerId)} result=denied reason=not-owner`)
260
+ return { status: 403, payload: { ok: false, error: 'denied' } }
261
+ }
262
+ if (bytes.byteLength > MAX_PART_BYTES) {
263
+ log(
264
+ `[data-portal] op=r2-put owner=${q(session.ownerId)} part=${partNumber} ` +
265
+ `bytes=${bytes.byteLength} result=too-large limit=${MAX_PART_BYTES}`,
266
+ )
267
+ return { status: 413, payload: { ok: false, error: 'part too large' } }
268
+ }
269
+
270
+ try {
271
+ const mp = env.BUCKET.resumeMultipartUpload(key, r2UploadId)
272
+ const part = await mp.uploadPart(partNumber, bytes)
273
+ log(
274
+ `[data-portal] op=r2-put owner=${q(session.ownerId)} key=${q(key)} ` +
275
+ `part=${partNumber} bytes=${bytes.byteLength} result=ok`,
276
+ )
277
+ return { status: 200, payload: { ok: true, partNumber, etag: part.etag } }
278
+ } catch (err) {
279
+ log(
280
+ `[data-portal] op=r2-put owner=${q(session.ownerId)} key=${q(key)} part=${partNumber} ` +
281
+ `result=failed err="${(err as Error).message}"`,
282
+ )
283
+ return { status: 502, payload: { ok: false, error: 'store failed' } }
284
+ }
285
+ }
286
+
287
+ export interface CompletedPart {
288
+ partNumber: number
289
+ etag: string
290
+ size?: number
291
+ }
292
+
293
+ /**
294
+ * Assemble the parts and record the row.
295
+ *
296
+ * The declared size at create time is a claim; the summed part sizes are the
297
+ * measurement, so the cap is enforced again here. Over the cap the session is
298
+ * ABORTED rather than completed: assembling first and deleting after would
299
+ * store the oversized object, however briefly, and a failure between the two
300
+ * would leave it there permanently.
301
+ */
302
+ export async function processMultipartComplete(
303
+ sessionId: string,
304
+ key: string,
305
+ r2UploadId: string,
306
+ targetPath: string,
307
+ parts: CompletedPart[],
308
+ env: PortalEnv,
309
+ log: Logger,
310
+ newId: () => string,
311
+ now: () => number,
312
+ ): Promise<Handler> {
313
+ const startedMs = now()
314
+ const session = await resolveSession(env.DB, sessionId, startedMs)
315
+ if (!session) {
316
+ log('[data-portal] op=mp-complete owner=none result=denied')
317
+ return { status: 401, payload: { ok: false, error: 'denied' } }
318
+ }
319
+ const owner = session.ownerId
320
+ const uploadId = newId()
321
+ if (!authorizeKey(owner, key)) {
322
+ log(`[data-portal] op=mp-complete uploadId=${uploadId} owner=${q(owner)} result=denied reason=not-owner`)
323
+ return { status: 403, payload: { ok: false, error: 'denied' } }
324
+ }
325
+
326
+ const mp = env.BUCKET.resumeMultipartUpload(key, r2UploadId)
327
+ const declaredTotal = parts.reduce((n, p) => n + (p.size ?? 0), 0)
328
+ if (declaredTotal > MAX_TOTAL_BYTES) {
329
+ await mp.abort().catch(() => {})
330
+ log(
331
+ `[data-portal] op=mp-complete uploadId=${uploadId} owner=${q(owner)} ` +
332
+ `bytes=${declaredTotal} result=too-large limit=${MAX_TOTAL_BYTES} aborted=true`,
333
+ )
334
+ return { status: 413, payload: { ok: false, error: 'file too large' } }
335
+ }
336
+
337
+ let size = declaredTotal
338
+ try {
339
+ const done = await mp.complete(parts.map((p) => ({ partNumber: p.partNumber, etag: p.etag })))
340
+ if (done && typeof done.size === 'number') size = done.size
341
+ log(`[data-portal] op=r2-put uploadId=${uploadId} key=${q(key)} parts=${parts.length} result=ok`)
342
+ } catch (err) {
343
+ log(
344
+ `[data-portal] op=r2-put uploadId=${uploadId} key=${q(key)} ` +
345
+ `result=failed err="${(err as Error).message}"`,
346
+ )
347
+ return { status: 502, payload: { ok: false, error: 'store failed' } }
348
+ }
349
+
350
+ const fileId = newId()
351
+ const filename = key.split('/').pop() ?? ''
352
+ try {
353
+ await writeManifestRow(env, {
354
+ fileId, owner, filename, key, size,
355
+ at: new Date(startedMs).toISOString(),
356
+ targetPath,
357
+ })
358
+ log(`[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} result=ok`)
359
+ } catch (err) {
360
+ log(
361
+ `[data-portal] op=manifest-write uploadId=${uploadId} rowId=${fileId} ` +
362
+ `result=failed err="${(err as Error).message}"`,
363
+ )
364
+ return { status: 502, payload: { ok: false, error: 'manifest failed' } }
365
+ }
366
+
367
+ const objectPresent = (await env.BUCKET.head(key)) !== null
368
+ const rowPresent =
369
+ (await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?').bind(key).first()) !==
370
+ null
371
+ log(
372
+ `[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} ` +
373
+ `objectPresent=${objectPresent} rowPresent=${rowPresent}`,
374
+ )
375
+ return { status: 200, payload: { ok: true, fileId, objectPresent, rowPresent } }
376
+ }
377
+
121
378
  interface PagesContext {
122
379
  request: Request
123
380
  env: PortalEnv
124
381
  }
125
382
 
383
+ /**
384
+ * The multipart stages, routed off an `action` field.
385
+ *
386
+ * No action at all is today's single-shot upload, so the common case — a
387
+ * document well under one part — stays exactly one request. Only a file that
388
+ * needs slicing pays for three round trips.
389
+ */
390
+ async function onMultipart(context: PagesContext, form: FormData, action: string): Promise<Response> {
391
+ const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
392
+ const log = (line: string) => console.log(line)
393
+ const newId = () => crypto.randomUUID()
394
+
395
+ if (action === 'create') {
396
+ const file = form.get('filename')
397
+ const { status, payload } = await processMultipartCreate(
398
+ sessionId,
399
+ typeof file === 'string' ? file : '',
400
+ String(form.get('targetPath') ?? ''),
401
+ Number(form.get('size') ?? NaN),
402
+ context.env,
403
+ log,
404
+ newId,
405
+ () => Date.now(),
406
+ )
407
+ return Response.json(payload, { status })
408
+ }
409
+
410
+ if (action === 'part') {
411
+ const part = form.get('part')
412
+ if (!(part instanceof File)) {
413
+ return Response.json({ ok: false, error: 'part required' }, { status: 400 })
414
+ }
415
+ const { status, payload } = await processMultipartPart(
416
+ sessionId,
417
+ String(form.get('key') ?? ''),
418
+ String(form.get('r2UploadId') ?? ''),
419
+ Number(form.get('partNumber') ?? NaN),
420
+ new Uint8Array(await part.arrayBuffer()),
421
+ context.env,
422
+ log,
423
+ )
424
+ return Response.json(payload, { status })
425
+ }
426
+
427
+ let parts: CompletedPart[]
428
+ try {
429
+ parts = JSON.parse(String(form.get('parts') ?? '[]')) as CompletedPart[]
430
+ if (!Array.isArray(parts)) throw new Error('parts must be an array')
431
+ } catch {
432
+ return Response.json({ ok: false, error: 'invalid parts' }, { status: 400 })
433
+ }
434
+ const { status, payload } = await processMultipartComplete(
435
+ sessionId,
436
+ String(form.get('key') ?? ''),
437
+ String(form.get('r2UploadId') ?? ''),
438
+ String(form.get('targetPath') ?? ''),
439
+ parts,
440
+ context.env,
441
+ log,
442
+ newId,
443
+ () => Date.now(),
444
+ )
445
+ return Response.json(payload, { status })
446
+ }
447
+
126
448
  export async function onRequestPost(context: PagesContext): Promise<Response> {
127
449
  const sessionId = readSessionCookie(context.request.headers.get('cookie')) ?? ''
128
450
  let filename = ''
451
+ let targetPath = ''
129
452
  let bytes: Uint8Array
453
+
454
+ // Parsing the form is the only thing this catch may cover. A multipart stage
455
+ // dispatched from inside it would report its own failures as "invalid form",
456
+ // which is the silent-failure shape: one message standing in for every cause.
457
+ let form: FormData
458
+ try {
459
+ form = await context.request.formData()
460
+ } catch {
461
+ return Response.json({ ok: false, error: 'invalid form' }, { status: 400 })
462
+ }
463
+
464
+ const action = String(form.get('action') ?? '')
465
+ if (action === 'create' || action === 'part' || action === 'complete') {
466
+ return onMultipart(context, form, action)
467
+ }
468
+
130
469
  try {
131
- const form = await context.request.formData()
132
470
  const file = form.get('file')
133
471
  if (!(file instanceof File)) {
134
472
  return Response.json({ ok: false, error: 'file required' }, { status: 400 })
135
473
  }
136
474
  filename = file.name
475
+ // Absent means the default uploads area, which is always legal. An absent
476
+ // field and an empty one are the same request.
477
+ targetPath = String(form.get('targetPath') ?? '')
137
478
  bytes = new Uint8Array(await file.arrayBuffer())
138
479
  } catch {
139
- return Response.json({ ok: false, error: 'invalid form' }, { status: 400 })
480
+ return Response.json({ ok: false, error: 'invalid file' }, { status: 400 })
140
481
  }
141
482
  const { status, payload } = await processUpload(
142
483
  sessionId,
143
484
  filename,
485
+ targetPath,
144
486
  bytes,
145
487
  context.env,
146
488
  (line) => console.log(line),
@@ -72,10 +72,18 @@
72
72
  <div class="dp-modal-panel" role="dialog" aria-modal="true" aria-label="Upload a file">
73
73
  <button class="dp-modal-close" id="dp-modal-close" type="button" aria-label="Close">&times;</button>
74
74
  <h2 class="dp-subtitle">Upload a file</h2>
75
+ <!-- `multiple`: a person dragging three invoices in expects three
76
+ uploads, not the first one silently. -->
75
77
  <form id="dp-upload" class="dp-upload">
76
- <input class="dp-file" id="dp-file" type="file" required />
78
+ <input class="dp-file" id="dp-file" type="file" multiple required />
77
79
  <button class="dp-btn" type="submit">Upload</button>
78
80
  </form>
81
+ <!-- Names the folder the upload will land in, so the destination is
82
+ never a guess. Filled by portal.js from the open folder. -->
83
+ <p class="dp-upload-target" id="dp-upload-target"></p>
84
+ <div class="dp-progress" id="dp-progress" hidden>
85
+ <div class="dp-progress-bar" id="dp-progress-bar"></div>
86
+ </div>
79
87
  </div>
80
88
  </div>
81
89
  </section>
@@ -308,6 +308,51 @@ body {
308
308
  outline-offset: 3px;
309
309
  }
310
310
 
311
+ /* The open listing while a file is dragged over it: this drop targets the
312
+ folder currently open. Same dashed treatment as the upload area, so one
313
+ visual language means "release here". */
314
+ .dp-files.dp-dropping {
315
+ outline: 2px dashed var(--dp-accent);
316
+ outline-offset: 3px;
317
+ border-radius: 6px;
318
+ }
319
+
320
+ /* One folder row under the pointer. Filled rather than outlined, because the
321
+ question a client is asking mid-drag is "which of these rows will take it",
322
+ and an outline on a dense list is easy to misread by one row. */
323
+ .dp-file-row.dp-drop-target {
324
+ background: color-mix(in srgb, var(--dp-accent) 12%, transparent);
325
+ outline: 2px solid var(--dp-accent);
326
+ outline-offset: -2px;
327
+ border-radius: 6px;
328
+ }
329
+
330
+ /* Which folder the modal will upload into. Stated rather than implied: an
331
+ upload whose destination the client has to infer is the defect the
332
+ folder-targeted exchange exists to remove. */
333
+ .dp-upload-target {
334
+ margin: 0.5rem 0 0;
335
+ font-size: 0.85rem;
336
+ color: var(--dp-muted);
337
+ }
338
+
339
+ /* Per-file upload progress. Hidden between uploads via the `hidden` attribute,
340
+ which [hidden]{display:none!important} keeps authoritative. */
341
+ .dp-progress {
342
+ margin-top: 0.6rem;
343
+ height: 4px;
344
+ background: var(--dp-rule);
345
+ border-radius: 2px;
346
+ overflow: hidden;
347
+ }
348
+
349
+ .dp-progress-bar {
350
+ height: 100%;
351
+ width: 0;
352
+ background: var(--dp-accent);
353
+ transition: width 120ms linear;
354
+ }
355
+
311
356
  /* Pinned nav row. position:sticky keeps it in flow, so it reserves its own
312
357
  height and never overlaps the first file row (which position:fixed would). An
313
358
  opaque paper background hides the rows that scroll under it. Reuses existing