create-yss-spec 3.5.0 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (882) hide show
  1. package/README.md +6 -2
  2. package/package.json +1 -1
  3. package/src/api/_sync-service.js +31 -6
  4. package/src/cli/args.js +2 -0
  5. package/src/cli/help.js +4 -1
  6. package/src/commands/attach.js +9 -3
  7. package/src/commands/diff.js +1 -0
  8. package/src/commands/doctor.js +1 -0
  9. package/src/commands/init.js +2 -1
  10. package/src/commands/sync.js +1 -0
  11. package/src/family-identity.js +1 -1
  12. package/src/template/asset-runtime.js +59 -49
  13. package/src/template/attach-planner.js +1 -1
  14. package/src/template/distribution-runtime.js +9 -9
  15. package/src/template/instance-runtime.js +9 -9
  16. package/src/template/layout-migration.js +160 -0
  17. package/src/template/migration-runtime.js +2 -2
  18. package/src/template/sync-planner.js +1 -1
  19. package/template/.agents/rules/yss-ai-skills.md +1 -1
  20. package/template/.agents/skills/.strategic-design-skills-manifest.json +7 -8
  21. package/template/.agents/skills/.yss-skills-manifest.json +2 -2
  22. package/template/.agents/skills/alibaba-java-code-style/SKILL.md +1 -1
  23. package/template/.agents/skills/code-review/SKILL.md +6 -6
  24. package/template/.agents/skills/code-review/references/yss-review-standards.md +1 -1
  25. package/template/.agents/skills/codebase-design/SKILL.md +4 -0
  26. package/template/{.codex/skills/improve-codebase-architecture/SKILL.md → .agents/skills/codebase-design/references/architecture-audit.md} +3 -9
  27. package/template/.agents/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  28. package/template/.agents/skills/frontend-commit/SKILL.md +4 -93
  29. package/template/.agents/skills/git-commit-core/SKILL.md +18 -0
  30. package/template/.agents/skills/i-have-adhd/SKILL.md +1 -1
  31. package/template/.agents/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  32. package/template/.agents/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  33. package/template/.agents/skills/implementation-repo-onboarding/SKILL.md +2 -2
  34. package/template/.agents/skills/java-backend-commit/SKILL.md +4 -96
  35. package/template/.agents/skills/llm-wiki/SKILL.md +12 -13
  36. package/template/.agents/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  37. package/template/.agents/skills/llm-wiki/references/compile.md +21 -114
  38. package/template/.agents/skills/llm-wiki/references/discover.md +3 -3
  39. package/template/.agents/skills/llm-wiki/references/ingest.md +8 -33
  40. package/template/.agents/skills/llm-wiki/references/lint.md +9 -46
  41. package/template/.agents/skills/llm-wiki/references/query.md +9 -18
  42. package/template/.agents/skills/llm-wiki/references/schema.md +56 -29
  43. package/template/.agents/skills/llm-wiki/references/transactions.md +48 -0
  44. package/template/.agents/skills/llm-wiki/references/writing.md +4 -4
  45. package/template/.agents/skills/llm-wiki/scripts/advise.mjs +46 -58
  46. package/template/.agents/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  47. package/template/.agents/skills/llm-wiki/scripts/core.mjs +262 -0
  48. package/template/.agents/skills/llm-wiki/scripts/extract.mjs +35 -18
  49. package/template/.agents/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  50. package/template/.agents/skills/llm-wiki/scripts/feedback.mjs +51 -0
  51. package/template/.agents/skills/llm-wiki/scripts/inventory.mjs +43 -171
  52. package/template/.agents/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  53. package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  54. package/template/.agents/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  55. package/template/.agents/skills/llm-wiki/scripts/migrate.mjs +51 -0
  56. package/template/.agents/skills/llm-wiki/scripts/query.mjs +149 -0
  57. package/template/.agents/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  58. package/template/.agents/skills/llm-wiki/scripts/sources.mjs +166 -0
  59. package/template/.agents/skills/llm-wiki/scripts/transaction.mjs +384 -0
  60. package/template/.agents/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  61. package/template/.agents/skills/prototype/LOGIC.md +1 -1
  62. package/template/.agents/skills/prototype/SKILL.md +5 -19
  63. package/template/.agents/skills/prototype-review/SKILL.md +4 -4
  64. package/template/.agents/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  65. package/template/.agents/skills/triage/SKILL.md +1 -1
  66. package/template/.agents/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  67. package/template/.agents/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  68. package/template/.agents/skills/yss-backend-spec-review/SKILL.md +1 -1
  69. package/template/.agents/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  70. package/template/.agents/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  71. package/template/.agents/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  72. package/template/.agents/skills/yss-design-system/SKILL.md +10 -10
  73. package/template/.agents/skills/yss-design-system/references/design-qa-theme.md +3 -3
  74. package/template/.agents/skills/yss-design-system/references/design-system.md +3 -3
  75. package/template/.agents/skills/yss-exception/SKILL.md +1 -1
  76. package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  77. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  78. package/template/.agents/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  79. package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  80. package/template/.agents/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  81. package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  82. package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  83. package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  84. package/template/.agents/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  85. package/template/.agents/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  86. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  87. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  88. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  89. package/template/.agents/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  90. package/template/.agents/skills/yss-openapi-draft-review/SKILL.md +2 -2
  91. package/template/.agents/skills/yss-openapi-governance/SKILL.md +7 -7
  92. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +17 -17
  93. package/template/.agents/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  94. package/template/.agents/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  95. package/template/.agents/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  96. package/template/.agents/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  97. package/template/.agents/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  98. package/template/.agents/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  99. package/template/.agents/skills/yss-product-lifecycle/references/state-model.md +4 -4
  100. package/template/.agents/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  101. package/template/.agents/skills/yss-prototype-stage/SKILL.md +7 -7
  102. package/template/.agents/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  103. package/template/.agents/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  104. package/template/.agents/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  105. package/template/.agents/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  106. package/template/.agents/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  107. package/template/.agents/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  108. package/template/.agents/skills/yss-repository/SKILL.md +2 -2
  109. package/template/.agents/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  110. package/template/.agents/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  111. package/template/.agents/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  112. package/template/.agents/skills/yss-research/SKILL.md +6 -2
  113. package/template/.agents/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  114. package/template/.agents/skills/yss-stage-decision/SKILL.md +5 -5
  115. package/template/.agents/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  116. package/template/.agents/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  117. package/template/.agents/skills/yss-tactical-design/SKILL.md +2 -2
  118. package/template/.agents/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  119. package/template/.agents/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  120. package/template/.agents/skills/yss-technical-design/SKILL.md +6 -6
  121. package/template/.agents/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  122. package/template/.agents/skills/yss-ui/SKILL.md +1 -1
  123. package/template/.agents/skills/yss-up-springboot3/SKILL.md +1 -1
  124. package/template/.agents/skills/yss-web-controller/SKILL.md +1 -1
  125. package/template/.agents/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  126. package/template/.codex/skills/alibaba-java-code-style/SKILL.md +1 -1
  127. package/template/.codex/skills/code-review/SKILL.md +6 -6
  128. package/template/.codex/skills/code-review/references/yss-review-standards.md +1 -1
  129. package/template/.codex/skills/codebase-design/SKILL.md +4 -0
  130. package/template/{.pi/skills/improve-codebase-architecture/SKILL.md → .codex/skills/codebase-design/references/architecture-audit.md} +3 -9
  131. package/template/.codex/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  132. package/template/.codex/skills/frontend-commit/SKILL.md +4 -93
  133. package/template/.codex/skills/git-commit-core/SKILL.md +18 -0
  134. package/template/.codex/skills/i-have-adhd/SKILL.md +1 -1
  135. package/template/.codex/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  136. package/template/.codex/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  137. package/template/.codex/skills/implementation-repo-onboarding/SKILL.md +2 -2
  138. package/template/.codex/skills/java-backend-commit/SKILL.md +4 -96
  139. package/template/.codex/skills/llm-wiki/SKILL.md +12 -13
  140. package/template/.codex/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  141. package/template/.codex/skills/llm-wiki/references/compile.md +21 -114
  142. package/template/.codex/skills/llm-wiki/references/discover.md +3 -3
  143. package/template/.codex/skills/llm-wiki/references/ingest.md +8 -33
  144. package/template/.codex/skills/llm-wiki/references/lint.md +9 -46
  145. package/template/.codex/skills/llm-wiki/references/query.md +9 -18
  146. package/template/.codex/skills/llm-wiki/references/schema.md +56 -29
  147. package/template/.codex/skills/llm-wiki/references/transactions.md +48 -0
  148. package/template/.codex/skills/llm-wiki/references/writing.md +4 -4
  149. package/template/.codex/skills/llm-wiki/scripts/advise.mjs +46 -58
  150. package/template/.codex/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  151. package/template/.codex/skills/llm-wiki/scripts/core.mjs +262 -0
  152. package/template/.codex/skills/llm-wiki/scripts/extract.mjs +35 -18
  153. package/template/.codex/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  154. package/template/.codex/skills/llm-wiki/scripts/feedback.mjs +51 -0
  155. package/template/.codex/skills/llm-wiki/scripts/inventory.mjs +43 -171
  156. package/template/.codex/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  157. package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  158. package/template/.codex/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  159. package/template/.codex/skills/llm-wiki/scripts/migrate.mjs +51 -0
  160. package/template/.codex/skills/llm-wiki/scripts/query.mjs +149 -0
  161. package/template/.codex/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  162. package/template/.codex/skills/llm-wiki/scripts/sources.mjs +166 -0
  163. package/template/.codex/skills/llm-wiki/scripts/transaction.mjs +384 -0
  164. package/template/.codex/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  165. package/template/.codex/skills/prototype/LOGIC.md +1 -1
  166. package/template/.codex/skills/prototype/SKILL.md +5 -19
  167. package/template/.codex/skills/prototype-review/SKILL.md +4 -4
  168. package/template/.codex/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  169. package/template/.codex/skills/triage/SKILL.md +1 -1
  170. package/template/.codex/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  171. package/template/.codex/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  172. package/template/.codex/skills/yss-backend-spec-review/SKILL.md +1 -1
  173. package/template/.codex/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  174. package/template/.codex/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  175. package/template/.codex/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  176. package/template/.codex/skills/yss-design-system/SKILL.md +10 -10
  177. package/template/.codex/skills/yss-design-system/references/design-qa-theme.md +3 -3
  178. package/template/.codex/skills/yss-design-system/references/design-system.md +3 -3
  179. package/template/.codex/skills/yss-exception/SKILL.md +1 -1
  180. package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  181. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  182. package/template/.codex/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  183. package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  184. package/template/.codex/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  185. package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  186. package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  187. package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  188. package/template/.codex/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  189. package/template/.codex/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  190. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  191. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  192. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  193. package/template/.codex/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  194. package/template/.codex/skills/yss-openapi-draft-review/SKILL.md +2 -2
  195. package/template/.codex/skills/yss-openapi-governance/SKILL.md +7 -7
  196. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +17 -17
  197. package/template/.codex/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  198. package/template/.codex/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  199. package/template/.codex/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  200. package/template/.codex/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  201. package/template/.codex/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  202. package/template/.codex/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  203. package/template/.codex/skills/yss-product-lifecycle/references/state-model.md +4 -4
  204. package/template/.codex/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  205. package/template/.codex/skills/yss-prototype-stage/SKILL.md +7 -7
  206. package/template/.codex/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  207. package/template/.codex/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  208. package/template/.codex/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  209. package/template/.codex/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  210. package/template/.codex/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  211. package/template/.codex/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  212. package/template/.codex/skills/yss-repository/SKILL.md +2 -2
  213. package/template/.codex/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  214. package/template/.codex/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  215. package/template/.codex/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  216. package/template/.codex/skills/yss-research/SKILL.md +6 -2
  217. package/template/.codex/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  218. package/template/.codex/skills/yss-stage-decision/SKILL.md +5 -5
  219. package/template/.codex/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  220. package/template/.codex/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  221. package/template/.codex/skills/yss-tactical-design/SKILL.md +2 -2
  222. package/template/.codex/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  223. package/template/.codex/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  224. package/template/.codex/skills/yss-technical-design/SKILL.md +6 -6
  225. package/template/.codex/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  226. package/template/.codex/skills/yss-ui/SKILL.md +1 -1
  227. package/template/.codex/skills/yss-up-springboot3/SKILL.md +1 -1
  228. package/template/.codex/skills/yss-web-controller/SKILL.md +1 -1
  229. package/template/.codex/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  230. package/template/.cursor/skills/alibaba-java-code-style/SKILL.md +1 -1
  231. package/template/.cursor/skills/code-review/SKILL.md +6 -6
  232. package/template/.cursor/skills/code-review/references/yss-review-standards.md +1 -1
  233. package/template/.cursor/skills/codebase-design/SKILL.md +4 -0
  234. package/template/.cursor/skills/{improve-codebase-architecture/SKILL.md → codebase-design/references/architecture-audit.md} +3 -9
  235. package/template/.cursor/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  236. package/template/.cursor/skills/frontend-commit/SKILL.md +4 -93
  237. package/template/.cursor/skills/git-commit-core/SKILL.md +18 -0
  238. package/template/.cursor/skills/i-have-adhd/SKILL.md +1 -1
  239. package/template/.cursor/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  240. package/template/.cursor/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  241. package/template/.cursor/skills/implementation-repo-onboarding/SKILL.md +2 -2
  242. package/template/.cursor/skills/java-backend-commit/SKILL.md +4 -96
  243. package/template/.cursor/skills/llm-wiki/SKILL.md +12 -13
  244. package/template/.cursor/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  245. package/template/.cursor/skills/llm-wiki/references/compile.md +21 -114
  246. package/template/.cursor/skills/llm-wiki/references/discover.md +3 -3
  247. package/template/.cursor/skills/llm-wiki/references/ingest.md +8 -33
  248. package/template/.cursor/skills/llm-wiki/references/lint.md +9 -46
  249. package/template/.cursor/skills/llm-wiki/references/query.md +9 -18
  250. package/template/.cursor/skills/llm-wiki/references/schema.md +56 -29
  251. package/template/.cursor/skills/llm-wiki/references/transactions.md +48 -0
  252. package/template/.cursor/skills/llm-wiki/references/writing.md +4 -4
  253. package/template/.cursor/skills/llm-wiki/scripts/advise.mjs +46 -58
  254. package/template/.cursor/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  255. package/template/.cursor/skills/llm-wiki/scripts/core.mjs +262 -0
  256. package/template/.cursor/skills/llm-wiki/scripts/extract.mjs +35 -18
  257. package/template/.cursor/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  258. package/template/.cursor/skills/llm-wiki/scripts/feedback.mjs +51 -0
  259. package/template/.cursor/skills/llm-wiki/scripts/inventory.mjs +43 -171
  260. package/template/.cursor/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  261. package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  262. package/template/.cursor/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  263. package/template/.cursor/skills/llm-wiki/scripts/migrate.mjs +51 -0
  264. package/template/.cursor/skills/llm-wiki/scripts/query.mjs +149 -0
  265. package/template/.cursor/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  266. package/template/.cursor/skills/llm-wiki/scripts/sources.mjs +166 -0
  267. package/template/.cursor/skills/llm-wiki/scripts/transaction.mjs +384 -0
  268. package/template/.cursor/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  269. package/template/.cursor/skills/prototype/LOGIC.md +1 -1
  270. package/template/.cursor/skills/prototype/SKILL.md +5 -19
  271. package/template/.cursor/skills/prototype-review/SKILL.md +4 -4
  272. package/template/.cursor/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  273. package/template/.cursor/skills/triage/SKILL.md +1 -1
  274. package/template/.cursor/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  275. package/template/.cursor/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  276. package/template/.cursor/skills/yss-backend-spec-review/SKILL.md +1 -1
  277. package/template/.cursor/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  278. package/template/.cursor/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  279. package/template/.cursor/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  280. package/template/.cursor/skills/yss-design-system/SKILL.md +10 -10
  281. package/template/.cursor/skills/yss-design-system/references/design-qa-theme.md +3 -3
  282. package/template/.cursor/skills/yss-design-system/references/design-system.md +3 -3
  283. package/template/.cursor/skills/yss-exception/SKILL.md +1 -1
  284. package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  285. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  286. package/template/.cursor/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  287. package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  288. package/template/.cursor/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  289. package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  290. package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  291. package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  292. package/template/.cursor/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  293. package/template/.cursor/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  294. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  295. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  296. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  297. package/template/.cursor/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  298. package/template/.cursor/skills/yss-openapi-draft-review/SKILL.md +2 -2
  299. package/template/.cursor/skills/yss-openapi-governance/SKILL.md +7 -7
  300. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +17 -17
  301. package/template/.cursor/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  302. package/template/.cursor/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  303. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  304. package/template/.cursor/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  305. package/template/.cursor/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  306. package/template/.cursor/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  307. package/template/.cursor/skills/yss-product-lifecycle/references/state-model.md +4 -4
  308. package/template/.cursor/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  309. package/template/.cursor/skills/yss-prototype-stage/SKILL.md +7 -7
  310. package/template/.cursor/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  311. package/template/.cursor/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  312. package/template/.cursor/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  313. package/template/.cursor/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  314. package/template/.cursor/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  315. package/template/.cursor/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  316. package/template/.cursor/skills/yss-repository/SKILL.md +2 -2
  317. package/template/.cursor/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  318. package/template/.cursor/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  319. package/template/.cursor/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  320. package/template/.cursor/skills/yss-research/SKILL.md +6 -2
  321. package/template/.cursor/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  322. package/template/.cursor/skills/yss-stage-decision/SKILL.md +5 -5
  323. package/template/.cursor/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  324. package/template/.cursor/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  325. package/template/.cursor/skills/yss-tactical-design/SKILL.md +2 -2
  326. package/template/.cursor/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  327. package/template/.cursor/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  328. package/template/.cursor/skills/yss-technical-design/SKILL.md +6 -6
  329. package/template/.cursor/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  330. package/template/.cursor/skills/yss-ui/SKILL.md +1 -1
  331. package/template/.cursor/skills/yss-up-springboot3/SKILL.md +1 -1
  332. package/template/.cursor/skills/yss-web-controller/SKILL.md +1 -1
  333. package/template/.cursor/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  334. package/template/.cursorrules +1 -1
  335. package/template/.pi/skills/alibaba-java-code-style/SKILL.md +1 -1
  336. package/template/.pi/skills/code-review/SKILL.md +6 -6
  337. package/template/.pi/skills/code-review/references/yss-review-standards.md +1 -1
  338. package/template/.pi/skills/codebase-design/SKILL.md +4 -0
  339. package/template/{.agents/skills/improve-codebase-architecture/SKILL.md → .pi/skills/codebase-design/references/architecture-audit.md} +3 -9
  340. package/template/.pi/skills/cross-repo-implementation-routing/SKILL.md +2 -2
  341. package/template/.pi/skills/frontend-commit/SKILL.md +4 -93
  342. package/template/.pi/skills/git-commit-core/SKILL.md +18 -0
  343. package/template/.pi/skills/i-have-adhd/SKILL.md +1 -1
  344. package/template/.pi/skills/i-have-adhd/references/yss-adaptation.md +1 -1
  345. package/template/.pi/skills/i-have-adhd/tests/verify-integration.mjs +2 -2
  346. package/template/.pi/skills/implementation-repo-onboarding/SKILL.md +2 -2
  347. package/template/.pi/skills/java-backend-commit/SKILL.md +4 -96
  348. package/template/.pi/skills/llm-wiki/SKILL.md +12 -13
  349. package/template/.pi/skills/llm-wiki/assets/CLAUDE.md.template +7 -1
  350. package/template/.pi/skills/llm-wiki/references/compile.md +21 -114
  351. package/template/.pi/skills/llm-wiki/references/discover.md +3 -3
  352. package/template/.pi/skills/llm-wiki/references/ingest.md +8 -33
  353. package/template/.pi/skills/llm-wiki/references/lint.md +9 -46
  354. package/template/.pi/skills/llm-wiki/references/query.md +9 -18
  355. package/template/.pi/skills/llm-wiki/references/schema.md +56 -29
  356. package/template/.pi/skills/llm-wiki/references/transactions.md +48 -0
  357. package/template/.pi/skills/llm-wiki/references/writing.md +4 -4
  358. package/template/.pi/skills/llm-wiki/scripts/advise.mjs +46 -58
  359. package/template/.pi/skills/llm-wiki/scripts/advise.test.mjs +11 -16
  360. package/template/.pi/skills/llm-wiki/scripts/core.mjs +262 -0
  361. package/template/.pi/skills/llm-wiki/scripts/extract.mjs +35 -18
  362. package/template/.pi/skills/llm-wiki/scripts/extract.test.mjs +5 -8
  363. package/template/.pi/skills/llm-wiki/scripts/feedback.mjs +51 -0
  364. package/template/.pi/skills/llm-wiki/scripts/inventory.mjs +43 -171
  365. package/template/.pi/skills/llm-wiki/scripts/inventory.test.mjs +25 -18
  366. package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.mjs +69 -92
  367. package/template/.pi/skills/llm-wiki/scripts/lint-wikilinks.test.mjs +25 -37
  368. package/template/.pi/skills/llm-wiki/scripts/migrate.mjs +51 -0
  369. package/template/.pi/skills/llm-wiki/scripts/query.mjs +149 -0
  370. package/template/.pi/skills/llm-wiki/scripts/regressions.test.mjs +101 -0
  371. package/template/.pi/skills/llm-wiki/scripts/sources.mjs +166 -0
  372. package/template/.pi/skills/llm-wiki/scripts/transaction.mjs +384 -0
  373. package/template/.pi/skills/llm-wiki/scripts/v2.test.mjs +354 -0
  374. package/template/.pi/skills/prototype/LOGIC.md +1 -1
  375. package/template/.pi/skills/prototype/SKILL.md +5 -19
  376. package/template/.pi/skills/prototype-review/SKILL.md +4 -4
  377. package/template/.pi/skills/setup-matt-pocock-skills/SKILL.md +11 -11
  378. package/template/.pi/skills/triage/SKILL.md +1 -1
  379. package/template/.pi/skills/yss-application/references/profiles/layered-mvc-service.md +1 -1
  380. package/template/.pi/skills/yss-application/references/profiles/mvc-data-analysis-v1.md +1 -1
  381. package/template/.pi/skills/yss-backend-spec-review/SKILL.md +1 -1
  382. package/template/.pi/skills/yss-backend-spec-review/references/check-inputs.md +1 -1
  383. package/template/.pi/skills/yss-ddd-scaffold-generator/SKILL.md +2 -2
  384. package/template/.pi/skills/yss-ddd-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  385. package/template/.pi/skills/yss-design-system/SKILL.md +10 -10
  386. package/template/.pi/skills/yss-design-system/references/design-qa-theme.md +3 -3
  387. package/template/.pi/skills/yss-design-system/references/design-system.md +3 -3
  388. package/template/.pi/skills/yss-exception/SKILL.md +1 -1
  389. package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +2 -2
  390. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/generate_scaffold.mjs +1 -1
  391. package/template/.pi/skills/yss-frontend-scaffold-generator/scripts/refresh_baseline.mjs +2 -2
  392. package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +11 -9
  393. package/template/.pi/skills/yss-implementation-contract-compiler/references/boundaries.md +3 -4
  394. package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +9 -9
  395. package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract-v2.md +3 -3
  396. package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +1 -1
  397. package/template/.pi/skills/yss-implementation-contract-compiler/references/strategic-handoff-routing.md +1 -1
  398. package/template/.pi/skills/yss-implementation-contract-compiler/references/yss-skill-execution-result.md +14 -0
  399. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/SKILL.md +2 -2
  400. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/scripts/data-analysis-profile.mjs +5 -5
  401. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/scripts/generate_scaffold.mjs +2 -2
  402. package/template/.pi/skills/yss-layered-mvc-scaffold-generator/scripts/scaffold-generator.test.mjs +42 -0
  403. package/template/.pi/skills/yss-openapi-draft-review/SKILL.md +2 -2
  404. package/template/.pi/skills/yss-openapi-governance/SKILL.md +7 -7
  405. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +17 -17
  406. package/template/.pi/skills/yss-product-lifecycle/references/external-input-questionnaire.md +19 -0
  407. package/template/.pi/skills/yss-product-lifecycle/references/matt-yss-adapter.md +10 -10
  408. package/template/.pi/skills/yss-product-lifecycle/references/orchestration-contract.yaml +85 -47
  409. package/template/.pi/skills/yss-product-lifecycle/references/orchestration.md +24 -16
  410. package/template/.pi/skills/yss-product-lifecycle/references/plan-requirements.md +10 -0
  411. package/template/.pi/skills/yss-product-lifecycle/references/request-triage.md +2 -0
  412. package/template/.pi/skills/yss-product-lifecycle/references/state-model.md +4 -4
  413. package/template/.pi/skills/yss-product-lifecycle/references/user-decisions.md +2 -2
  414. package/template/.pi/skills/yss-prototype-stage/SKILL.md +7 -7
  415. package/template/.pi/skills/yss-prototype-stage/references/existing-ui-entry.md +1 -1
  416. package/template/.pi/skills/yss-prototype-stage/scripts/build-antd-prototype.mjs +1 -1
  417. package/template/.pi/skills/yss-prototype-stage/scripts/offline-html.mjs +3 -3
  418. package/template/.pi/skills/yss-prototype-stage/tests/browser-smoke.mjs +2 -2
  419. package/template/.pi/skills/yss-prototype-stage/tests/run-scenarios.mjs +4 -4
  420. package/template/.pi/skills/yss-prototype-stage/tests/workbench-smoke.mjs +1 -1
  421. package/template/.pi/skills/yss-repository/SKILL.md +2 -2
  422. package/template/.pi/skills/yss-repository/references/profiles/layered-mvc-service.md +1 -1
  423. package/template/.pi/skills/yss-repository/references/profiles/mvc-data-analysis-v1.md +1 -1
  424. package/template/.pi/skills/yss-repository/tests/profile-routing.test.mjs +2 -2
  425. package/template/.pi/skills/yss-research/SKILL.md +6 -2
  426. package/template/.pi/skills/yss-skill-source-index-refresh/references/backend-component-platform-compatibility.md +1 -1
  427. package/template/.pi/skills/yss-stage-decision/SKILL.md +5 -5
  428. package/template/.pi/skills/yss-stage-decision/references/domain-strategy-contract.md +1 -1
  429. package/template/.pi/skills/yss-stage-decision/references/strategic-handoff-routing.md +1 -1
  430. package/template/.pi/skills/yss-tactical-design/SKILL.md +2 -2
  431. package/template/.pi/skills/yss-tactical-design/references/strategic-handoff-routing.md +1 -1
  432. package/template/.pi/skills/yss-tactical-design/references/tactical-design-contract.md +1 -1
  433. package/template/.pi/skills/yss-technical-design/SKILL.md +6 -6
  434. package/template/.pi/skills/yss-technical-design/references/technical-design-contract.md +1 -1
  435. package/template/.pi/skills/yss-ui/SKILL.md +1 -1
  436. package/template/.pi/skills/yss-up-springboot3/SKILL.md +1 -1
  437. package/template/.pi/skills/yss-web-controller/SKILL.md +1 -1
  438. package/template/.pi/skills/yss-web-controller/references/profiles/layered-mvc-service.md +1 -1
  439. package/template/{docs → .template-spec}/adr/README.md +1 -1
  440. package/template/{docs → .template-spec}/agents/README.md +2 -2
  441. package/template/{docs → .template-spec}/agents/backend-architecture-profiles.md +2 -2
  442. package/template/{docs → .template-spec}/agents/digital-human-roles.md +6 -6
  443. package/template/{docs → .template-spec}/agents/digital-human-roles.yaml +6 -6
  444. package/template/{docs → .template-spec}/agents/gitlab-workflow-skills.md +2 -2
  445. package/template/{docs → .template-spec}/agents/issue-tracker.md +1 -1
  446. package/template/{docs → .template-spec}/agents/skill-migrations.md +17 -2
  447. package/template/{docs → .template-spec}/agents/triage-labels.md +1 -1
  448. package/template/{docs → .template-spec}/agents/yss-skill-registry.yaml +12 -31
  449. package/template/{docs → .template-spec}/architecture/README.md +5 -36
  450. package/template/{docs → .template-spec}/architecture/templates/tech-design-template.md +3 -1
  451. package/template/{docs → .template-spec}/design/README.md +12 -11
  452. package/template/{docs → .template-spec}/design/design.md +19 -19
  453. package/template/{docs/design/templates/interaction-spec-template.md → .template-spec/design/templates/examples/model-publishing-interaction.example.md} +5 -3
  454. package/template/{docs/design/templates/state-matrix-template.md → .template-spec/design/templates/examples/model-publishing-state-matrix.example.md} +3 -1
  455. package/template/.template-spec/design/templates/interaction-spec-template.md +61 -0
  456. package/template/{docs → .template-spec}/design/templates/product-overview-design-template.md +11 -11
  457. package/template/{docs → .template-spec}/design/templates/prototype-evidence-template.yaml +4 -4
  458. package/template/{docs → .template-spec}/design/templates/prototype-review-checklist.md +1 -1
  459. package/template/.template-spec/design/templates/state-matrix-template.md +29 -0
  460. package/template/.template-spec/design/tokens/.design-md-projection.json +13 -0
  461. package/template/{docs → .template-spec}/engineering/backend-platforms.md +1 -1
  462. package/template/{docs → .template-spec}/plan/entry-review.md +8 -4
  463. package/template/{docs → .template-spec}/plan/templates/competitive-analysis-template.md +2 -2
  464. package/template/{docs → .template-spec}/plan/templates/plan-template.md +1 -1
  465. package/template/{docs → .template-spec}/process/contract-reading.md +25 -1
  466. package/template/{docs → .template-spec}/process/lifecycle-artifact-map.md +5 -5
  467. package/template/.template-spec/process/lifecycle-registry-baseline.json +255 -0
  468. package/template/{docs → .template-spec}/process/lifecycle-registry.yaml +25 -12
  469. package/template/{docs → .template-spec}/process/plan-migration.md +1 -1
  470. package/template/.template-spec/process/research-completion.md +23 -0
  471. package/template/{docs → .template-spec}/process/schemas/digital-human-task-package.schema.json +209 -6
  472. package/template/{docs → .template-spec}/process/schemas/frontend-implementation-evidence.schema.json +7 -15
  473. package/template/{docs → .template-spec}/process/schemas/lifecycle-checkpoint.schema.json +15 -0
  474. package/template/{docs → .template-spec}/process/schemas/lifecycle-registry.schema.json +1 -1
  475. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff-v3.schema.json +1 -1
  476. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff-v4.schema.json +1 -1
  477. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff-v5.schema.json +1 -1
  478. package/template/{docs → .template-spec}/process/schemas/strategic-design-handoff.schema.json +1 -1
  479. package/template/{docs → .template-spec}/process/stage-tracking.md +1 -1
  480. package/template/.template-spec/process/subagent-collaboration.md +21 -0
  481. package/template/{docs → .template-spec}/process/templates/frontend-implementation-verification-template.yaml +15 -1
  482. package/template/{docs → .template-spec}/templates/build-architecture-checklist-template.md +2 -2
  483. package/template/{docs → .template-spec}/templates/digital-human-runtime-profile-template.md +2 -2
  484. package/template/{docs → .template-spec}/templates/grok-bot-profile-template.md +1 -1
  485. package/template/{docs → .template-spec}/templates/local-parent-ticket-template.md +1 -1
  486. package/template/.template-spec/templates/release-note-template.md +29 -0
  487. package/template/.template-spec/templates/retro-report-template.md +24 -0
  488. package/template/{docs → .template-spec}/templates/subagent-task-package-template.md +3 -3
  489. package/template/.template-spec/templates/verification-record-template.md +64 -0
  490. package/template/{docs → .template-spec}/templates/vertical-slice-ticket-template.md +5 -10
  491. package/template/{docs → .template-spec}/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +1 -1
  492. package/template/{docs → .template-spec}/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +4 -4
  493. package/template/AGENTS.md +13 -13
  494. package/template/CONTEXT.md +8 -4
  495. package/template/README.md +33 -45
  496. package/template/scripts/backend-delivery +1 -1
  497. package/template/scripts/contract +5 -5
  498. package/template/scripts/lib/api-contract-decision.mjs +1 -1
  499. package/template/scripts/lib/approved-execution-context.mjs +5 -5
  500. package/template/scripts/lib/backend-delivery.mjs +4 -4
  501. package/template/scripts/lib/backend-platform.mjs +1 -1
  502. package/template/scripts/lib/backend-review.mjs +1 -1
  503. package/template/scripts/lib/backend-scaffold-prerequisites.mjs +1 -1
  504. package/template/scripts/lib/backend-standards-coverage.mjs +2 -2
  505. package/template/scripts/lib/context-reconciliation.mjs +1 -1
  506. package/template/scripts/lib/contract-views.mjs +66 -11
  507. package/template/scripts/lib/delivery-preflight.mjs +6 -6
  508. package/template/scripts/lib/design-md.mjs +2 -2
  509. package/template/scripts/lib/digital-human-roles.mjs +2 -2
  510. package/template/scripts/lib/execution-evidence.mjs +51 -0
  511. package/template/scripts/lib/existing-backend-architecture.mjs +1 -1
  512. package/template/scripts/lib/existing-ui-baseline.mjs +1 -1
  513. package/template/scripts/lib/frontend-delivery.mjs +2 -2
  514. package/template/scripts/lib/governance-layout.mjs +74 -0
  515. package/template/scripts/lib/harness-execution-scope.mjs +3 -3
  516. package/template/scripts/lib/implementation-contract-compiler.mjs +2 -0
  517. package/template/scripts/lib/lifecycle-context-query.mjs +26 -6
  518. package/template/scripts/lib/lifecycle-registry.mjs +46 -7
  519. package/template/scripts/lib/lifecycle-status.mjs +137 -0
  520. package/template/scripts/lib/lifecycle-transition.mjs +90 -11
  521. package/template/scripts/lib/maintenance-research.mjs +63 -0
  522. package/template/scripts/lib/openapi-draft-validation.mjs +1 -1
  523. package/template/scripts/lib/plan-spec-entry-view.mjs +48 -0
  524. package/template/scripts/lib/plan-spec-entry.mjs +3 -3
  525. package/template/scripts/lib/read-only-intake.mjs +132 -0
  526. package/template/scripts/lib/skill-registry.mjs +10 -9
  527. package/template/scripts/lib/skill-supply-chain.mjs +5 -1
  528. package/template/scripts/lib/slice-contract-preparation.mjs +3 -3
  529. package/template/scripts/lib/slice-contract.mjs +1 -1
  530. package/template/scripts/lib/slice-repositories.mjs +1 -1
  531. package/template/scripts/lib/slice-task-package.mjs +4 -4
  532. package/template/scripts/lib/stage-tracking-migration.mjs +2 -2
  533. package/template/scripts/lib/stage-tracking.mjs +5 -5
  534. package/template/scripts/lib/strategic-feedback.mjs +2 -2
  535. package/template/scripts/lib/strategic-handoff-routing.mjs +2 -2
  536. package/template/scripts/lib/strategic-handoff.mjs +15 -15
  537. package/template/scripts/lib/task-package-schema.mjs +2 -2
  538. package/template/scripts/lib/task-package.mjs +10 -3
  539. package/template/scripts/lib/technical-design-boundary.mjs +1 -1
  540. package/template/scripts/lib/user-decision.mjs +2 -1
  541. package/template/scripts/lifecycle-status +14 -0
  542. package/template/scripts/plan-spec-entry +21 -0
  543. package/template/scripts/prepare-read-only-intake +16 -0
  544. package/template/scripts/run-read-only-intake +14 -0
  545. package/template/scripts/strategic-consumer-entry +1 -1
  546. package/template/scripts/strategic-handoff +1 -1
  547. package/template/scripts/verify-digital-human-task-package +2 -2
  548. package/template/scripts/verify-frontend-implementation-evidence +25 -2
  549. package/template/scripts/verify-governance-layout +15 -0
  550. package/template/scripts/verify-lifecycle-checkpoint +1 -1
  551. package/template/scripts/verify-maintenance-research +13 -0
  552. package/template/scripts/verify-project-instance +2 -0
  553. package/template/scripts/verify-scaffold-architecture-decisions +1 -1
  554. package/template/scripts/verify-strategic-context-import +3 -3
  555. package/template/skills-lock.json +82 -139
  556. package/template.manifest.json +135 -46
  557. package/template.snapshot.json +5 -6
  558. package/template/.agents/skills/grill-with-docs/SKILL.md +0 -20
  559. package/template/.agents/skills/grill-with-docs/agents/openai.yaml +0 -5
  560. package/template/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  561. package/template/.agents/skills/prototype/UI.md +0 -112
  562. package/template/.agents/skills/setup-matt-pocock-skills/domain.md +0 -37
  563. package/template/.agents/skills/to-questionnaire/SKILL.md +0 -55
  564. package/template/.agents/skills/to-questionnaire/agents/openai.yaml +0 -5
  565. package/template/.agents/skills/wait-what/SKILL.md +0 -7
  566. package/template/.agents/skills/wait-what/agents/openai.yaml +0 -5
  567. package/template/.codex/skills/data-analytics/.app.json +0 -84
  568. package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +0 -66
  569. package/template/.codex/skills/data-analytics/.mcp.json +0 -30
  570. package/template/.codex/skills/data-analytics/AGENTS.md +0 -20
  571. package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +0 -27
  572. package/template/.codex/skills/data-analytics/README.md +0 -60
  573. package/template/.codex/skills/data-analytics/__yss_dotfile__.gitignore +0 -2
  574. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +0 -16
  575. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +0 -1
  576. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +0 -1
  577. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +0 -1
  578. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +0 -1
  579. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +0 -1
  580. package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +0 -1
  581. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +0 -16
  582. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +0 -1
  583. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +0 -1
  584. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +0 -1
  585. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +0 -1
  586. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +0 -1
  587. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +0 -1
  588. package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +0 -1
  589. package/template/.codex/skills/data-analytics/assets/datascience-small.svg +0 -5
  590. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +0 -16
  591. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +0 -1
  592. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +0 -1
  593. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +0 -1
  594. package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +0 -1
  595. package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
  596. package/template/.codex/skills/data-analytics/assets/datascience.svg +0 -10
  597. package/template/.codex/skills/data-analytics/mcp/server.cjs +0 -2964
  598. package/template/.codex/skills/data-analytics/package-lock.json +0 -3048
  599. package/template/.codex/skills/data-analytics/package.json +0 -43
  600. package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +0 -75
  601. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +0 -137
  602. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +0 -6
  603. package/template/.codex/skills/data-analytics/skills/analyze-data-quality/references/quality-checks.md +0 -29
  604. package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +0 -148
  605. package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +0 -6
  606. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +0 -18
  607. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +0 -24
  608. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +0 -71
  609. package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +0 -85
  610. package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +0 -207
  611. package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +0 -6
  612. package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +0 -70
  613. package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +0 -66
  614. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +0 -108
  615. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +0 -6
  616. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +0 -1
  617. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +0 -52
  618. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +0 -61
  619. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +0 -362
  620. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +0 -829
  621. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +0 -30
  622. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +0 -127
  623. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +0 -374
  624. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +0 -613
  625. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +0 -54
  626. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +0 -16
  627. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +0 -9
  628. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/tests/test_delivery_plan.py +0 -45
  629. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +0 -77
  630. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +0 -6
  631. package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +0 -2379
  632. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +0 -88
  633. package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +0 -6
  634. package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +0 -97
  635. package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +0 -59
  636. package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +0 -75
  637. package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +0 -103
  638. package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +0 -6
  639. package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +0 -68
  640. package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +0 -6
  641. package/template/.codex/skills/data-analytics/skills/index/SKILL.md +0 -251
  642. package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +0 -6
  643. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +0 -131
  644. package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +0 -6
  645. package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +0 -141
  646. package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +0 -6
  647. package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +0 -32
  648. package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +0 -106
  649. package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +0 -6
  650. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +0 -130
  651. package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +0 -6
  652. package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +0 -141
  653. package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +0 -6
  654. package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +0 -178
  655. package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +0 -9
  656. package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
  657. package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +0 -31
  658. package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +0 -466
  659. package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +0 -99
  660. package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +0 -197
  661. package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +0 -6
  662. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +0 -26
  663. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +0 -51
  664. package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +0 -66
  665. package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +0 -69
  666. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +0 -204
  667. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +0 -87
  668. package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +0 -497
  669. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +0 -74
  670. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +0 -65
  671. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +0 -160
  672. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +0 -75
  673. package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +0 -96
  674. package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +0 -263
  675. package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +0 -1462
  676. package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +0 -128
  677. package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +0 -101
  678. package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +0 -499
  679. package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +0 -978
  680. package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +0 -126
  681. package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +0 -6
  682. package/template/.codex/skills/data-analytics/skills/validate-data/references/validation-methods.md +0 -70
  683. package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +0 -157
  684. package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +0 -6
  685. package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +0 -774
  686. package/template/.codex/skills/data-analytics/src/DESIGN.md +0 -222
  687. package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +0 -3666
  688. package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +0 -136
  689. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +0 -54
  690. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +0 -96
  691. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +0 -1647
  692. package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +0 -245
  693. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +0 -462
  694. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +0 -107
  695. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +0 -164
  696. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +0 -192
  697. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +0 -203
  698. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +0 -619
  699. package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +0 -402
  700. package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
  701. package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +0 -370
  702. package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +0 -536
  703. package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +0 -681
  704. package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +0 -164
  705. package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +0 -21
  706. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +0 -472
  707. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +0 -75
  708. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +0 -142
  709. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +0 -142
  710. package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +0 -377
  711. package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +0 -3468
  712. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +0 -44
  713. package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +0 -639
  714. package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +0 -327
  715. package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +0 -225
  716. package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +0 -204
  717. package/template/.codex/skills/data-analytics/src/analytics-app-core.md +0 -85
  718. package/template/.codex/skills/data-analytics/src/codex-style-contract.md +0 -30
  719. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +0 -12
  720. package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +0 -1008
  721. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +0 -2117
  722. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +0 -122
  723. package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +0 -3588
  724. package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +0 -483
  725. package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +0 -32
  726. package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +0 -333
  727. package/template/.codex/skills/data-analytics/src/mcp-host.js +0 -153
  728. package/template/.codex/skills/data-analytics/src/recharts-config.js +0 -315
  729. package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +0 -294
  730. package/template/.codex/skills/data-analytics/src/sql-source-view.css +0 -33
  731. package/template/.codex/skills/data-analytics/src/sql-source-view.js +0 -157
  732. package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +0 -121
  733. package/template/.codex/skills/data-analytics/src/table-renderer.jsx +0 -28
  734. package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +0 -87
  735. package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +0 -105
  736. package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +0 -233
  737. package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +0 -1500
  738. package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +0 -219
  739. package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +0 -144
  740. package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +0 -11
  741. package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +0 -54
  742. package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +0 -321
  743. package/template/.codex/skills/data-analytics/tsconfig.json +0 -19
  744. package/template/.codex/skills/data-analytics/vite.config.ts +0 -46
  745. package/template/.codex/skills/grill-with-docs/SKILL.md +0 -20
  746. package/template/.codex/skills/grill-with-docs/agents/openai.yaml +0 -5
  747. package/template/.codex/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  748. package/template/.codex/skills/prototype/UI.md +0 -112
  749. package/template/.codex/skills/setup-matt-pocock-skills/domain.md +0 -37
  750. package/template/.codex/skills/to-questionnaire/SKILL.md +0 -55
  751. package/template/.codex/skills/to-questionnaire/agents/openai.yaml +0 -5
  752. package/template/.codex/skills/wait-what/SKILL.md +0 -7
  753. package/template/.codex/skills/wait-what/agents/openai.yaml +0 -5
  754. package/template/.cursor/skills/grill-with-docs/SKILL.md +0 -20
  755. package/template/.cursor/skills/grill-with-docs/agents/openai.yaml +0 -5
  756. package/template/.cursor/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  757. package/template/.cursor/skills/prototype/UI.md +0 -112
  758. package/template/.cursor/skills/setup-matt-pocock-skills/domain.md +0 -37
  759. package/template/.cursor/skills/to-questionnaire/SKILL.md +0 -55
  760. package/template/.cursor/skills/to-questionnaire/agents/openai.yaml +0 -5
  761. package/template/.cursor/skills/wait-what/SKILL.md +0 -7
  762. package/template/.cursor/skills/wait-what/agents/openai.yaml +0 -5
  763. package/template/.pi/skills/grill-with-docs/SKILL.md +0 -20
  764. package/template/.pi/skills/grill-with-docs/agents/openai.yaml +0 -5
  765. package/template/.pi/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
  766. package/template/.pi/skills/prototype/UI.md +0 -112
  767. package/template/.pi/skills/setup-matt-pocock-skills/domain.md +0 -37
  768. package/template/.pi/skills/to-questionnaire/SKILL.md +0 -55
  769. package/template/.pi/skills/to-questionnaire/agents/openai.yaml +0 -5
  770. package/template/.pi/skills/wait-what/SKILL.md +0 -7
  771. package/template/.pi/skills/wait-what/agents/openai.yaml +0 -5
  772. package/template/docs/agents/archify-integration.md +0 -19
  773. package/template/docs/agents/domain.md +0 -31
  774. package/template/docs/design/design-system-sync.yaml +0 -13
  775. package/template/docs/design/preview-dark.html +0 -178
  776. package/template/docs/design/preview.css +0 -730
  777. package/template/docs/design/preview.html +0 -178
  778. package/template/docs/design/preview.js +0 -71
  779. package/template/docs/design/tokens/.design-md-projection.json +0 -13
  780. package/template/docs/engineering/evidence/aliyun-artifact-resolution.json +0 -446
  781. package/template/docs/process/MATT-POCOCK-ENGINEERING-SKILLS.md +0 -13
  782. package/template/docs/process/github-workflows.md +0 -52
  783. package/template/docs/process/subagent-collaboration.md +0 -13
  784. package/template/docs/templates/release-note-template.md +0 -14
  785. package/template/docs/templates/retro-report-template.md +0 -16
  786. package/template/docs/templates/verification-record-template.md +0 -53
  787. /package/template/.agents/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  788. /package/template/.codex/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  789. /package/template/.cursor/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  790. /package/template/.pi/skills/{improve-codebase-architecture → codebase-design/references}/HTML-REPORT.md +0 -0
  791. /package/template/{docs → .template-spec}/api/templates/openapi-draft-review-checklist.md +0 -0
  792. /package/template/{docs → .template-spec}/api/templates/openapi-draft-validation-record-template.yaml +0 -0
  793. /package/template/{docs → .template-spec}/api/templates/openapi-freeze-record-template.md +0 -0
  794. /package/template/{docs → .template-spec}/api/templates/openapi-json-export-record-template.md +0 -0
  795. /package/template/{docs → .template-spec}/architecture/templates/architecture-deepening-template.md +0 -0
  796. /package/template/{docs → .template-spec}/architecture/templates/architecture-review-checklist.md +0 -0
  797. /package/template/{docs → .template-spec}/architecture/templates/business-architecture-template.md +0 -0
  798. /package/template/{docs → .template-spec}/architecture/templates/data-architecture-template.md +0 -0
  799. /package/template/{docs → .template-spec}/architecture/templates/engineering-baseline-review-template.md +0 -0
  800. /package/template/{docs → .template-spec}/architecture/templates/functional-architecture-template.md +0 -0
  801. /package/template/{docs → .template-spec}/architecture/templates/system-overview-design-template.md +0 -0
  802. /package/template/{docs → .template-spec}/design/diagrams/README.md +0 -0
  803. /package/template/{docs → .template-spec}/design/schemas/visual-baseline.schema.json +0 -0
  804. /package/template/{docs → .template-spec}/design/templates/prototype-confirmation-template.md +0 -0
  805. /package/template/{docs → .template-spec}/design/templates/visual-baseline-template.yaml +0 -0
  806. /package/template/{docs → .template-spec}/design/tokens/theme.json +0 -0
  807. /package/template/{docs → .template-spec}/design/tokens/tokens.compact.json +0 -0
  808. /package/template/{docs → .template-spec}/design/tokens/tokens.dark.json +0 -0
  809. /package/template/{docs → .template-spec}/design/tokens/tokens.default.json +0 -0
  810. /package/template/{docs → .template-spec}/design/tokens/variables.css +0 -0
  811. /package/template/{docs → .template-spec}/design/tokens/variables.dark.css +0 -0
  812. /package/template/{docs → .template-spec}/engineering/backend-platforms.json +0 -0
  813. /package/template/{docs → .template-spec}/plan/README.md +0 -0
  814. /package/template/{docs → .template-spec}/plan/templates/competitive-matrix-template.md +0 -0
  815. /package/template/{docs → .template-spec}/plan/templates/market-analysis-template.md +0 -0
  816. /package/template/{docs → .template-spec}/plan/templates/user-pain-points-template.md +0 -0
  817. /package/template/{docs → .template-spec}/process/delivery-preflight.md +0 -0
  818. /package/template/{docs → .template-spec}/process/document-writing.md +0 -0
  819. /package/template/{docs → .template-spec}/process/existing-backend-architecture.md +0 -0
  820. /package/template/{docs → .template-spec}/process/existing-ui-baseline.md +0 -0
  821. /package/template/{docs → .template-spec}/process/frontend-backend-delivery.md +0 -0
  822. /package/template/{docs → .template-spec}/process/harness-process-tailoring.md +0 -0
  823. /package/template/{docs → .template-spec}/process/implementation-repo-integration.md +0 -0
  824. /package/template/{docs/process/lifecycle-registry-baseline.json → .template-spec/process/lifecycle-registry-baseline-v1.json} +0 -0
  825. /package/template/{docs → .template-spec}/process/schemas/api-contract-decision-v2.schema.json +0 -0
  826. /package/template/{docs → .template-spec}/process/schemas/api-contract-decision.schema.json +0 -0
  827. /package/template/{docs → .template-spec}/process/schemas/backend-architecture-identity.schema.json +0 -0
  828. /package/template/{docs → .template-spec}/process/schemas/backend-delivery-verification.schema.json +0 -0
  829. /package/template/{docs → .template-spec}/process/schemas/backend-delivery.schema.json +0 -0
  830. /package/template/{docs → .template-spec}/process/schemas/context-reconciliation.schema.json +0 -0
  831. /package/template/{docs → .template-spec}/process/schemas/data-architecture-decision.schema.json +0 -0
  832. /package/template/{docs → .template-spec}/process/schemas/delivery-preflight-input.schema.json +0 -0
  833. /package/template/{docs → .template-spec}/process/schemas/delivery-preflight-result.schema.json +0 -0
  834. /package/template/{docs → .template-spec}/process/schemas/existing-ui-baseline.schema.json +0 -0
  835. /package/template/{docs → .template-spec}/process/schemas/frontend-delivery-acceptance-v1.schema.json +0 -0
  836. /package/template/{docs → .template-spec}/process/schemas/frontend-delivery-acceptance-v3.schema.json +0 -0
  837. /package/template/{docs → .template-spec}/process/schemas/frontend-delivery-acceptance.schema.json +0 -0
  838. /package/template/{docs → .template-spec}/process/schemas/frontend-strategic-preflight-v2.schema.json +0 -0
  839. /package/template/{docs → .template-spec}/process/schemas/frontend-strategic-preflight.schema.json +0 -0
  840. /package/template/{docs → .template-spec}/process/schemas/implementation-repository-preparation-result.schema.json +0 -0
  841. /package/template/{docs → .template-spec}/process/schemas/openapi-draft-validation-record.schema.json +0 -0
  842. /package/template/{docs → .template-spec}/process/schemas/project-scaffold-contract.schema.json +0 -0
  843. /package/template/{docs → .template-spec}/process/schemas/review-bundle.schema.json +0 -0
  844. /package/template/{docs → .template-spec}/process/schemas/scaffold-architecture-decisions.schema.json +0 -0
  845. /package/template/{docs → .template-spec}/process/schemas/slice-implementation-contract-v3.schema.json +0 -0
  846. /package/template/{docs → .template-spec}/process/schemas/stage-tracking.schema.json +0 -0
  847. /package/template/{docs → .template-spec}/process/schemas/strategic-consumer-feedback.schema.json +0 -0
  848. /package/template/{docs → .template-spec}/process/schemas/strategic-feedback-adjudication.schema.json +0 -0
  849. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-delivery-verification.schema.json +0 -0
  850. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-delivery.schema.json +0 -0
  851. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-domain-strategy-v3.schema.json +0 -0
  852. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-domain-strategy.schema.json +0 -0
  853. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-export-v2.schema.json +0 -0
  854. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-export.schema.json +0 -0
  855. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-import-receipt-v3.schema.json +0 -0
  856. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-import-receipt.schema.json +0 -0
  857. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-package.schema.json +0 -0
  858. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-stage-decision-package-v3.schema.json +0 -0
  859. /package/template/{docs → .template-spec}/process/schemas/strategic-handoff-stage-decision-package.schema.json +0 -0
  860. /package/template/{docs → .template-spec}/process/schemas/subagent-task-package.schema.json +0 -0
  861. /package/template/{docs → .template-spec}/process/schemas/user-decision.schema.json +0 -0
  862. /package/template/{docs → .template-spec}/process/strategic-handoff-package.md +0 -0
  863. /package/template/{docs → .template-spec}/process/templates/context-reconciliation-template.yaml +0 -0
  864. /package/template/{docs → .template-spec}/process/templates/frontend-implementation-plan-template.yaml +0 -0
  865. /package/template/{docs → .template-spec}/process/templates/lifecycle-checkpoint-template.yaml +0 -0
  866. /package/template/{docs → .template-spec}/process/templates/scaffold-architecture-decisions-template.yaml +0 -0
  867. /package/template/{docs → .template-spec}/process/templates/stage-checkpoint-template.md +0 -0
  868. /package/template/{docs → .template-spec}/process/templates/user-decision-template.yaml +0 -0
  869. /package/template/{docs → .template-spec}/templates/adr-template.md +0 -0
  870. /package/template/{docs → .template-spec}/templates/approval-record-template.yaml +0 -0
  871. /package/template/{docs → .template-spec}/templates/cross-repo-slice-template.md +0 -0
  872. /package/template/{docs → .template-spec}/templates/examples/lifecycle-writing-examples.md +0 -0
  873. /package/template/{docs → .template-spec}/templates/implementation-repo-registry-template.md +0 -0
  874. /package/template/{docs → .template-spec}/templates/openapi-spec-template.yaml +0 -0
  875. /package/template/{docs → .template-spec}/templates/review-bundle-template.yaml +0 -0
  876. /package/template/{docs → .template-spec}/templates/review-report-template.md +0 -0
  877. /package/template/{docs → .template-spec}/templates/spec-delta-template.md +0 -0
  878. /package/template/{docs → .template-spec}/templates/spec-template.md +0 -0
  879. /package/template/{docs → .template-spec}/user-guide/yss-ui-mcp.md +0 -0
  880. /package/template/{docs → .template-spec}/user-guide//346/212/200/346/234/257/350/256/276/350/256/241/347/224/250/346/210/267/346/214/207/345/215/227.md" +0 -0
  881. /package/template/{docs → .template-spec}/user-guide//350/256/276/345/244/207/345/200/237/347/224/250/350/264/257/347/251/277/346/241/210/344/276/213.md" +0 -0
  882. /package/template/{docs → .template-spec}/user-guide//351/230/266/346/256/265/346/216/245/345/205/245/344/270/216/344/272/247/347/211/251/345/244/215/347/224/250/344/275/277/347/224/250/346/211/213/345/206/214.md" +0 -0
@@ -1,3666 +0,0 @@
1
- import { ArrowDown, ArrowUp, Boxes, Camera, ChartArea, ChartBar, ChartColumn, ChartColumnStacked, ChartLine, ChartNoAxesColumn, ChartNoAxesCombined, ChartScatter, ChartSpline, Check, ChevronDown, ChevronLeft, ChevronRight, Copy, Database, Ellipsis, Expand, Filter as FunnelIcon, FileDown, FileText, Globe, Pencil, Presentation, RefreshCw, Table2, Tally3, Trash2, X, } from "lucide-react";
2
- import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { createPortal } from "react-dom";
4
- import { AnalyticsLayoutCanvas } from "./layout/AnalyticsLayoutCanvas";
5
- import { isChartType as sharedIsChartType } from "./charting/chart-compatibility";
6
- import { applyChartSpecOverride, chartEncoding, chartEncodingField, chartEncodingFields, chartEncodingLabel, chartHasEncodingSpec, chartSpecOverrideFromWidgetSpec, chartUsedFields, compatibleChartTypesFor, compatibleChartTypesForArtifactCard, withChartType } from "./charting/chart-app-helpers";
7
- import { ChartRenderer } from "./charting/ChartRenderer";
8
- import { RichMarkdown } from "./layout/RichMarkdown";
9
- import { copyElementAsImage, copyTextToClipboard, imageCopySuccessMessage, shouldOfferImageClipboardCopy, usePreparedImageExport } from "./imageExport";
10
- function resizeEditableTextarea(element) {
11
- if (!element)
12
- return;
13
- element.style.height = "auto";
14
- element.style.height = `${element.scrollHeight}px`;
15
- }
16
- function cloneSerializable(value) {
17
- return JSON.parse(JSON.stringify(value));
18
- }
19
- const DEFAULT_DASHBOARD_CARD_LAYOUT = "half";
20
- const DEFAULT_REPORT_CARD_LAYOUT = "full";
21
- const CARD_DRAG_CANCEL_SELECTOR = ".viz-card__no-drag, button, a, input, textarea, select, [contenteditable='true'], .rich-markdown-editor, [role='button'], [role='menu'], [role='menuitem'], [role='menuitemradio']";
22
- const CHART_FULLSCREEN_HEIGHT = 520;
23
- const TABLE_CARD_PAGE_SIZE = 15;
24
- const TABLE_COLUMN_DEFAULT_WIDTH = 144;
25
- const TABLE_COLUMN_DENSE_HORIZONTAL_PADDING = 16;
26
- const TABLE_COLUMN_HEADER_CHROME_WIDTH = 24;
27
- const TABLE_COLUMN_KEYBOARD_STEP = 24;
28
- const TABLE_COLUMN_MAX_WIDTH = 420;
29
- const TABLE_COLUMN_MIN_WIDTH = 88;
30
- const TABLE_COLUMN_NUMERIC_MAX_WIDTH = 136;
31
- const TABLE_COLUMN_SAMPLE_SIZE = 50;
32
- const TABLE_COLUMN_TEXT_MAX_WIDTH = 220;
33
- const TABLE_COLUMN_LONG_TEXT_MAX_WIDTH = 260;
34
- const EMPTY_ACCESS_ISSUES = [];
35
- const EMPTY_CARDS = [];
36
- const EMPTY_CHARTS = [];
37
- const EMPTY_FILTERS = [];
38
- const EMPTY_REPORT_BLOCKS = [];
39
- const EMPTY_TABLES = [];
40
- const KPI_COLUMN_BREAKPOINTS = [
41
- { minWidth: 760, columns: 3 },
42
- { minWidth: 520, columns: 2 },
43
- { minWidth: 392, columns: 2 }
44
- ];
45
- function getHeatmapFill(intensity) {
46
- const index = clamp(Math.floor(intensity * heatmapBlueScale.length), 0, heatmapBlueScale.length - 1);
47
- return heatmapBlueScale[index];
48
- }
49
- async function fetchJson(path, init) {
50
- const response = await fetch(path, { cache: "no-store", ...init });
51
- const text = await response.text();
52
- const payload = text ? JSON.parse(text) : {};
53
- if (!response.ok) {
54
- const message = typeof payload?.error === "string" ? payload.error : `Request failed: ${response.status}`;
55
- throw new Error(message);
56
- }
57
- return payload;
58
- }
59
- async function fetchOptionalJson(path, init) {
60
- try {
61
- return await fetchJson(path, init);
62
- }
63
- catch {
64
- return null;
65
- }
66
- }
67
- function asNumber(value) {
68
- if (typeof value === "number" && Number.isFinite(value))
69
- return value;
70
- if (typeof value === "string" && value.trim() !== "") {
71
- const numeric = Number(value.replace(/,/g, ""));
72
- if (Number.isFinite(numeric))
73
- return numeric;
74
- }
75
- return null;
76
- }
77
- function asFiniteNumber(value, fallback = 0) {
78
- if (typeof value === "number" && Number.isFinite(value))
79
- return value;
80
- if (typeof value === "string") {
81
- const numeric = Number(value);
82
- if (Number.isFinite(numeric))
83
- return numeric;
84
- }
85
- return fallback;
86
- }
87
- function clamp(value, min, max) {
88
- return Math.max(min, Math.min(max, value));
89
- }
90
- function formatValue(value, format = "compact") {
91
- const numeric = asNumber(value);
92
- if (numeric == null)
93
- return value == null ? "n/a" : String(value);
94
- if (format === "percent") {
95
- return new Intl.NumberFormat(undefined, {
96
- maximumFractionDigits: 1,
97
- style: "percent"
98
- }).format(numeric);
99
- }
100
- if (format === "currency") {
101
- return new Intl.NumberFormat(undefined, {
102
- currency: "USD",
103
- maximumFractionDigits: 2,
104
- notation: "compact",
105
- style: "currency"
106
- }).format(numeric);
107
- }
108
- if (format === "number") {
109
- return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(numeric);
110
- }
111
- return new Intl.NumberFormat(undefined, {
112
- maximumFractionDigits: 2,
113
- notation: "compact"
114
- }).format(numeric);
115
- }
116
- function cardLabel(card) {
117
- return cardMetrics(card)[0]?.label ?? card.id;
118
- }
119
- function rowMatchesFilter(row, filter) {
120
- if (!filter)
121
- return true;
122
- return Object.entries(filter).every(([field, expected]) => String(row[field] ?? "") === String(expected ?? ""));
123
- }
124
- function movementDirection(value) {
125
- if (typeof value !== "string")
126
- return null;
127
- const trimmed = value.trim();
128
- if (!trimmed)
129
- return null;
130
- if (/^[+↑]/.test(trimmed))
131
- return "positive";
132
- if (/^[-−↓]/.test(trimmed))
133
- return "negative";
134
- return null;
135
- }
136
- function formatMetricValue(value, metric) {
137
- if (value == null || value === "")
138
- return null;
139
- const numeric = asNumber(value);
140
- const rendered = formatValue(value, metric.format);
141
- if (!metric.signed || numeric == null || numeric === 0)
142
- return rendered;
143
- return `${numeric > 0 ? "+" : ""}${rendered}`;
144
- }
145
- function metricMovementDirection(value, signed = false) {
146
- if (!signed)
147
- return null;
148
- const numeric = asNumber(value);
149
- if (numeric != null) {
150
- if (numeric > 0)
151
- return "positive";
152
- if (numeric < 0)
153
- return "negative";
154
- return "neutral";
155
- }
156
- return movementDirection(value);
157
- }
158
- function cardMetrics(card) {
159
- return Array.isArray(card.metrics) ? card.metrics : [];
160
- }
161
- function MetricInfoIcon({ className }) {
162
- return (<svg aria-hidden="true" className={className} fill="none" height="21" viewBox="0 0 21 21" width="21" xmlns="http://www.w3.org/2000/svg">
163
- <path d="M10.6 9.70459C11.0142 9.70461 11.35 10.0404 11.35 10.4546V13.7876C11.35 14.2018 11.0142 14.5376 10.6 14.5376C10.1858 14.5376 9.84998 14.2018 9.84998 13.7876V10.4546C9.84998 10.0404 10.1858 9.70459 10.6 9.70459Z" fill="currentColor"/>
164
- <path d="M10.6 6.2876C11.1292 6.28762 11.558 6.71732 11.558 7.24658C11.5578 7.77569 11.1291 8.20457 10.6 8.20459C10.0708 8.20459 9.64215 7.7757 9.64197 7.24658C9.64197 6.71731 10.0707 6.2876 10.6 6.2876Z" fill="currentColor"/>
165
- <path clipRule="evenodd" d="M10.6 2.53955C14.9713 2.53955 18.515 6.08326 18.515 10.4546C18.515 14.8259 14.9713 18.3696 10.6 18.3696C6.22864 18.3696 2.68494 14.8259 2.68494 10.4546C2.68494 6.08326 6.22864 2.53955 10.6 2.53955ZM10.6 3.86963C6.96318 3.86963 4.01501 6.81779 4.01501 10.4546C4.01501 14.0914 6.96318 17.0396 10.6 17.0396C14.2368 17.0396 17.1849 14.0914 17.1849 10.4546C17.1849 6.81779 14.2368 3.86963 10.6 3.86963Z" fill="currentColor" fillRule="evenodd"/>
166
- </svg>);
167
- }
168
- function MetricDescriptionPopover({ description, label }) {
169
- const tooltipId = useId();
170
- const buttonRef = useRef(null);
171
- const popoverRef = useRef(null);
172
- const [isOpen, setIsOpen] = useState(false);
173
- const [popoverStyle, setPopoverStyle] = useState(null);
174
- const updatePopoverPosition = useCallback(() => {
175
- if (typeof window === "undefined")
176
- return;
177
- const anchor = buttonRef.current?.getBoundingClientRect();
178
- if (!anchor)
179
- return;
180
- const margin = 12;
181
- const gap = 8;
182
- const maxWidth = Math.max(120, Math.min(320, window.innerWidth - margin * 2));
183
- const popoverRect = popoverRef.current?.getBoundingClientRect();
184
- const width = Math.min(popoverRect?.width ?? maxWidth, maxWidth);
185
- const height = popoverRect?.height ?? 0;
186
- let left = anchor.left + anchor.width / 2 - width / 2;
187
- left = clamp(left, margin, window.innerWidth - margin - width);
188
- let top = anchor.bottom + gap;
189
- if (height && top + height > window.innerHeight - margin && anchor.top - height - gap >= margin) {
190
- top = anchor.top - height - gap;
191
- }
192
- else if (height) {
193
- top = clamp(top, margin, window.innerHeight - margin - height);
194
- }
195
- setPopoverStyle((current) => {
196
- if (current?.left === left && current?.top === top && current?.maxWidth === maxWidth)
197
- return current;
198
- return { left, maxWidth, top };
199
- });
200
- }, []);
201
- useEffect(() => {
202
- if (!isOpen) {
203
- setPopoverStyle(null);
204
- return;
205
- }
206
- updatePopoverPosition();
207
- const frame = window.requestAnimationFrame(updatePopoverPosition);
208
- window.addEventListener("resize", updatePopoverPosition);
209
- window.addEventListener("scroll", updatePopoverPosition, true);
210
- return () => {
211
- window.cancelAnimationFrame(frame);
212
- window.removeEventListener("resize", updatePopoverPosition);
213
- window.removeEventListener("scroll", updatePopoverPosition, true);
214
- };
215
- }, [isOpen, updatePopoverPosition]);
216
- const renderedPopoverStyle = popoverStyle
217
- ? { left: popoverStyle.left, maxWidth: popoverStyle.maxWidth, top: popoverStyle.top }
218
- : { left: 0, maxWidth: "min(320px, calc(100vw - 24px))", top: 0, visibility: "hidden" };
219
- return (<span className="kpi-info-wrap" onBlur={() => setIsOpen(false)} onFocus={() => setIsOpen(true)} onMouseEnter={() => setIsOpen(true)} onMouseLeave={() => setIsOpen(false)}>
220
- <button aria-describedby={isOpen ? tooltipId : undefined} aria-label={`${label}: ${description}`} className="kpi-info" ref={buttonRef} type="button">
221
- <MetricInfoIcon className="kpi-info-icon"/>
222
- </button>
223
- {isOpen && typeof document !== "undefined" ? createPortal(<span className="kpi-info-popover" id={tooltipId} ref={popoverRef} role="tooltip" style={renderedPopoverStyle}>
224
- {description}
225
- </span>, document.body) : null}
226
- </span>);
227
- }
228
- function tableColumnFormat(column) {
229
- if (column.format)
230
- return column.format;
231
- return column.type === "currency" || column.type === "number" || column.type === "percent"
232
- ? column.type
233
- : undefined;
234
- }
235
- function tableColumnLooksLikeMovement(column) {
236
- return column.movement === true || column.semantic === "movement" || column.role === "movement";
237
- }
238
- function tableCellMovementClass(column, value) {
239
- if (!tableColumnLooksLikeMovement(column))
240
- return "";
241
- const numeric = asNumber(value);
242
- const direction = numeric == null ? movementDirection(value) : numeric > 0 ? "positive" : numeric < 0 ? "negative" : "neutral";
243
- if (!direction || direction === "neutral")
244
- return "";
245
- return `table-cell-movement table-cell-movement-${direction}`;
246
- }
247
- function formatTableDate(value) {
248
- if (value == null || value === "")
249
- return "";
250
- if (value instanceof Date && !Number.isNaN(value.getTime())) {
251
- return new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(value);
252
- }
253
- if (typeof value !== "string")
254
- return String(value);
255
- const trimmed = value.trim();
256
- const monthMatch = trimmed.match(/^(\d{4})-(\d{2})$/);
257
- if (monthMatch) {
258
- const [, yearValue, monthValue] = monthMatch;
259
- const date = new Date(Date.UTC(Number(yearValue), Number(monthValue) - 1, 1));
260
- if (!Number.isNaN(date.getTime())) {
261
- return new Intl.DateTimeFormat(undefined, {
262
- month: "short",
263
- timeZone: "UTC",
264
- year: "numeric"
265
- }).format(date);
266
- }
267
- }
268
- const dayMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})(?:$|[T\s])/);
269
- if (dayMatch) {
270
- const [, yearValue, monthValue, dayValue] = dayMatch;
271
- const date = new Date(Date.UTC(Number(yearValue), Number(monthValue) - 1, Number(dayValue)));
272
- if (!Number.isNaN(date.getTime())) {
273
- return new Intl.DateTimeFormat(undefined, {
274
- day: "numeric",
275
- month: "short",
276
- timeZone: "UTC",
277
- year: "numeric"
278
- }).format(date);
279
- }
280
- }
281
- return trimmed;
282
- }
283
- function formatTableCellValue(column, value) {
284
- if (column.type === "date")
285
- return formatTableDate(value);
286
- const format = tableColumnFormat(column);
287
- const rendered = format ? formatValue(value, format) : String(value ?? "");
288
- if (!tableColumnLooksLikeMovement(column))
289
- return rendered;
290
- const numeric = asNumber(value);
291
- if (numeric == null || numeric === 0)
292
- return rendered;
293
- return `${numeric > 0 ? "+" : ""}${rendered}`;
294
- }
295
- function normalizedTableTextLength(value) {
296
- return String(value ?? "").replace(/\s+/g, " ").trim().length;
297
- }
298
- function percentileValue(values, percentile) {
299
- if (!values.length)
300
- return 0;
301
- const sorted = [...values].sort((a, b) => a - b);
302
- const index = clamp(Math.ceil(sorted.length * percentile) - 1, 0, sorted.length - 1);
303
- return sorted[index];
304
- }
305
- function isNumericTableColumn(column, rows) {
306
- if (tableColumnFormat(column))
307
- return true;
308
- let observed = false;
309
- for (const row of rows.slice(0, TABLE_COLUMN_SAMPLE_SIZE)) {
310
- const value = row[column.field];
311
- if (value == null || value === "")
312
- continue;
313
- observed = true;
314
- if (asNumber(value) == null)
315
- return false;
316
- }
317
- return observed;
318
- }
319
- function tableColumnDisplayText(column, row) {
320
- return formatTableCellValue(column, row[column.field]);
321
- }
322
- function estimateTableColumnWidth(column, rows, density) {
323
- const sampleRows = rows.slice(0, TABLE_COLUMN_SAMPLE_SIZE);
324
- const labelLength = normalizedTableTextLength(column.label);
325
- const cellLengths = sampleRows
326
- .map((row) => normalizedTableTextLength(tableColumnDisplayText(column, row)))
327
- .filter((length) => length > 0);
328
- const p90Length = percentileValue([labelLength, ...cellLengths], 0.9);
329
- const maxLength = Math.max(labelLength, ...cellLengths, 0);
330
- const columnKey = `${column.field} ${column.label}`.toLowerCase();
331
- const isLongTextColumn = /(comment|description|detail|explanation|insight|interpretation|note|reason|summary)/.test(columnKey)
332
- || maxLength > 34;
333
- const isNumericColumn = isNumericTableColumn(column, sampleRows);
334
- const isDateColumn = /(date|day|month|quarter|week)/.test(columnKey);
335
- const horizontalPadding = TABLE_COLUMN_HEADER_CHROME_WIDTH + (density === "dense" ? TABLE_COLUMN_DENSE_HORIZONTAL_PADDING : 0);
336
- const charWidth = isNumericColumn ? 7.2 : 7.4;
337
- const targetLength = isLongTextColumn
338
- ? Math.min(Math.max(labelLength + 4, p90Length), 44)
339
- : Math.max(labelLength, p90Length);
340
- const measuredWidth = Math.ceil(targetLength * charWidth + horizontalPadding);
341
- if (isNumericColumn) {
342
- const minWidth = tableColumnFormat(column) === "percent" ? 96 : 108;
343
- return clamp(measuredWidth, minWidth, TABLE_COLUMN_NUMERIC_MAX_WIDTH);
344
- }
345
- if (isDateColumn) {
346
- return clamp(measuredWidth, 116, 156);
347
- }
348
- if (isLongTextColumn) {
349
- return clamp(measuredWidth, 220, TABLE_COLUMN_LONG_TEXT_MAX_WIDTH);
350
- }
351
- return clamp(measuredWidth, density === "dense" ? 124 : 112, TABLE_COLUMN_TEXT_MAX_WIDTH);
352
- }
353
- function estimateTableColumnWidths(table, rows, density) {
354
- return Object.fromEntries(table.columns.map((column) => [
355
- column.field,
356
- estimateTableColumnWidth(column, rows, density)
357
- ]));
358
- }
359
- function formatDate(value) {
360
- if (!value)
361
- return "Unknown";
362
- const date = new Date(value);
363
- if (Number.isNaN(date.getTime()))
364
- return value;
365
- return new Intl.DateTimeFormat(undefined, {
366
- dateStyle: "medium",
367
- timeStyle: "short"
368
- }).format(date);
369
- }
370
- function snapshotStatusLabel(status) {
371
- if (!status || status === "ready")
372
- return null;
373
- if (status === "fixture")
374
- return "Fixture data";
375
- if (status === "partial")
376
- return null;
377
- return "Blocked snapshot";
378
- }
379
- function getRows(snapshot, dataset) {
380
- return snapshot?.datasets?.[dataset] ?? [];
381
- }
382
- function asArray(value) {
383
- return Array.isArray(value) ? value : [];
384
- }
385
- function asRecord(value) {
386
- return value && typeof value === "object" && !Array.isArray(value)
387
- ? value
388
- : {};
389
- }
390
- function normalizeManifest(rawManifest) {
391
- const manifest = asRecord(rawManifest);
392
- return {
393
- ...manifest,
394
- version: 1,
395
- title: typeof manifest.title === "string" ? manifest.title : "Data Analytics artifact",
396
- generatedAt: typeof manifest.generatedAt === "string" ? manifest.generatedAt : "",
397
- filters: asArray(manifest.filters).map((rawFilter) => {
398
- const filter = asRecord(rawFilter);
399
- return {
400
- ...filter,
401
- targets: asArray(filter.targets)
402
- };
403
- }),
404
- cards: asArray(manifest.cards).map((rawCard) => asRecord(rawCard)),
405
- charts: asArray(manifest.charts).map((rawChart) => {
406
- const chart = asRecord(rawChart);
407
- return {
408
- ...chart,
409
- referenceLines: asArray(chart.referenceLines)
410
- };
411
- }),
412
- tables: asArray(manifest.tables).map((rawTable) => {
413
- const table = asRecord(rawTable);
414
- return {
415
- ...table,
416
- columns: asArray(table.columns)
417
- };
418
- }),
419
- sources: asArray(manifest.sources).map((rawSource) => asRecord(rawSource)),
420
- blocks: asArray(manifest.blocks).map((rawBlock) => {
421
- const block = asRecord(rawBlock);
422
- return {
423
- ...block,
424
- cardIds: asArray(block.cardIds)
425
- };
426
- })
427
- };
428
- }
429
- function normalizeSnapshot(rawSnapshot) {
430
- const snapshot = asRecord(rawSnapshot);
431
- const datasets = {};
432
- const rawDatasets = asRecord(snapshot.datasets);
433
- for (const [dataset, rows] of Object.entries(rawDatasets)) {
434
- datasets[dataset] = asArray(rows).filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row));
435
- }
436
- return {
437
- ...snapshot,
438
- version: 1,
439
- generatedAt: typeof snapshot.generatedAt === "string" ? snapshot.generatedAt : "",
440
- datasets,
441
- accessIssues: asArray(snapshot.accessIssues)
442
- };
443
- }
444
- function filterTargets(filter) {
445
- const targets = [{ dataset: filter.dataset, field: filter.field }];
446
- for (const target of filter.targets ?? []) {
447
- targets.push({ dataset: target.dataset, field: target.field ?? filter.field });
448
- }
449
- const seen = new Set();
450
- return targets.filter((target) => {
451
- const key = `${target.dataset}\u0001${target.field}`;
452
- if (seen.has(key))
453
- return false;
454
- seen.add(key);
455
- return true;
456
- });
457
- }
458
- function filterFieldForDataset(filter, dataset) {
459
- return filterTargets(filter).find((target) => target.dataset === dataset)?.field ?? null;
460
- }
461
- function dashboardSurfaceDatasets(cards, charts, tables) {
462
- return new Set([...cards, ...charts, ...tables]
463
- .map((surface) => surface.dataset)
464
- .filter((dataset) => Boolean(dataset)));
465
- }
466
- function isGlobalFilter(filter, cards, charts, tables) {
467
- const requiredDatasets = dashboardSurfaceDatasets(cards, charts, tables);
468
- if (requiredDatasets.size <= 1)
469
- return true;
470
- const targetDatasets = new Set(filterTargets(filter).map((target) => target.dataset));
471
- return [...requiredDatasets].every((dataset) => targetDatasets.has(dataset));
472
- }
473
- function getGlobalFilters(filters, cards, charts, tables) {
474
- return filters.filter((filter) => isGlobalFilter(filter, cards, charts, tables));
475
- }
476
- function filterRowsForDataset(rows, dataset, filters, selectedFilters, usedFields = []) {
477
- const usedFieldSet = new Set(usedFields.filter(Boolean));
478
- const explicitAllFields = new Set(filters
479
- .map((filter) => {
480
- const field = filterFieldForDataset(filter, dataset);
481
- const selected = selectedFilters[filter.id] ?? filter.defaultValue ?? "all";
482
- return field && selected === "all" && rows.some((row) => String(row[field] ?? "") === "all")
483
- ? field
484
- : null;
485
- })
486
- .filter((field) => Boolean(field)));
487
- const filteredRows = rows.filter((row) => filters.every((filter) => {
488
- const field = filterFieldForDataset(filter, dataset);
489
- if (!field)
490
- return true;
491
- const selected = selectedFilters[filter.id] ?? filter.defaultValue ?? "all";
492
- if (selected === "all") {
493
- return !explicitAllFields.has(field) || String(row[field] ?? "") === "all";
494
- }
495
- return String(row[field] ?? "") === selected;
496
- }));
497
- const aggregateFields = Object.keys(rows[0] ?? {}).filter((field) => {
498
- if (usedFieldSet.has(field))
499
- return false;
500
- let hasAggregate = false;
501
- let hasBreakdown = false;
502
- for (const row of filteredRows) {
503
- const value = String(row[field] ?? "");
504
- if (value === "all")
505
- hasAggregate = true;
506
- else if (value)
507
- hasBreakdown = true;
508
- if (hasAggregate && hasBreakdown)
509
- return true;
510
- }
511
- return false;
512
- });
513
- if (!aggregateFields.length)
514
- return filteredRows;
515
- const aggregateRows = filteredRows.filter((row) => aggregateFields.every((field) => String(row[field] ?? "") === "all"));
516
- return aggregateRows.length ? aggregateRows : filteredRows;
517
- }
518
- function DashboardShell({ children, detailMode = false, isEditMode = false, surface }) {
519
- return (<main className={`dashboard-shell ${surface === "report" ? "report-shell" : ""} ${detailMode ? "chart-detail-shell" : ""} ${isEditMode ? "is-app-editing" : ""}`.trim()} data-edit-mode={isEditMode ? "true" : "false"}>
520
- {children}
521
- </main>);
522
- }
523
- function dashboardCardLayout(layout) {
524
- return layout ?? DEFAULT_DASHBOARD_CARD_LAYOUT;
525
- }
526
- function reportCardLayout(layout) {
527
- return layout ?? DEFAULT_REPORT_CARD_LAYOUT;
528
- }
529
- function contentLayoutKey(manifest) {
530
- if (!manifest)
531
- return null;
532
- const base = `${manifest.title}:${manifest.generatedAt}`;
533
- return `datascience-dashboard:content-layout:${base}`;
534
- }
535
- function chartTextKey(manifest) {
536
- if (!manifest)
537
- return null;
538
- const base = `${manifest.title}:${manifest.generatedAt}`;
539
- return `datascience-dashboard:chart-text:${base}`;
540
- }
541
- function chartTypeKey(manifest) {
542
- if (!manifest)
543
- return null;
544
- const base = `${manifest.title}:${manifest.generatedAt}`;
545
- return `datascience-${manifest.surface ?? "dashboard"}:chart-type:${base}`;
546
- }
547
- function chartSpecKey(manifest) {
548
- if (!manifest)
549
- return null;
550
- const base = `${manifest.title}:${manifest.generatedAt}`;
551
- return `datascience-${manifest.surface ?? "dashboard"}:chart-spec:${base}`;
552
- }
553
- function pageTitleTextKey(manifest) {
554
- if (!manifest)
555
- return null;
556
- const base = `${manifest.title}:${manifest.generatedAt}`;
557
- return `datascience-${manifest.surface ?? "dashboard"}:page-title:${base}`;
558
- }
559
- function tableTextKey(manifest) {
560
- if (!manifest)
561
- return null;
562
- const base = `${manifest.title}:${manifest.generatedAt}`;
563
- return `datascience-${manifest.surface ?? "dashboard"}:table-text:${base}`;
564
- }
565
- function storageKeyHash(value) {
566
- let hash = 2166136261;
567
- for (let index = 0; index < value.length; index += 1) {
568
- hash ^= value.charCodeAt(index);
569
- hash = Math.imul(hash, 16777619);
570
- }
571
- return (hash >>> 0).toString(36);
572
- }
573
- function blockTextKey(manifest) {
574
- if (!manifest)
575
- return null;
576
- const base = `${manifest.title}:${manifest.generatedAt}`;
577
- const blockSignature = (manifest.blocks ?? [])
578
- .map((block) => `${block.id}:${block.type}:${typeof block.body === "string" ? storageKeyHash(block.body) : ""}`)
579
- .join(",");
580
- return `datascience-report:block-text:${base}:${storageKeyHash(blockSignature)}`;
581
- }
582
- function deletedReportBlocksKey(manifest) {
583
- if (!manifest)
584
- return null;
585
- const base = `${manifest.title}:${manifest.generatedAt}`;
586
- const blockSignature = (manifest.blocks ?? [])
587
- .map((block) => block.id)
588
- .join(",");
589
- return `datascience-report:deleted-blocks:${base}:${blockSignature}`;
590
- }
591
- function tableColumnWidthKey(manifest) {
592
- if (!manifest)
593
- return null;
594
- const base = `${manifest.title}:${manifest.generatedAt}`;
595
- const surface = manifest.surface ?? "dashboard";
596
- const tableSignature = (manifest.tables ?? [])
597
- .map((table) => `${table.id}:${table.columns.map((column) => column.field).join("|")}`)
598
- .join(",");
599
- return `datascience-${surface}:table-column-widths:${base}:${tableSignature}`;
600
- }
601
- function reportContentLayoutKey(manifest) {
602
- if (!manifest)
603
- return null;
604
- const base = `${manifest.title}:${manifest.generatedAt}`;
605
- const blockSignature = (manifest.blocks ?? [])
606
- .map((block) => `${block.id}:${reportCardLayout(block.layout)}`)
607
- .join(",");
608
- return `datascience-report:content-layout:v3:${base}:${blockSignature}`;
609
- }
610
- function sourceForChart(chart, sources) {
611
- if (chart?.source && typeof chart.source === "object")
612
- return chart.source;
613
- return sources.find((source) => source.id === chart.sourceId) ?? null;
614
- }
615
- function sourceForTable(table, sources) {
616
- if (table?.source && typeof table.source === "object")
617
- return table.source;
618
- return sources.find((source) => source.id === table.sourceId) ?? null;
619
- }
620
- function sourceQueryFromChartSpec(chart) {
621
- return chart?.source?.query ?? null;
622
- }
623
- function sourceQueryFromSourceSpec(source) {
624
- if (!source)
625
- return null;
626
- return source.query ?? null;
627
- }
628
- function queryTextFromSourceQuery(sourceQuery) {
629
- return (sourceQuery?.sql ?? "").trim();
630
- }
631
- function accessIssueForChart(chart, issues) {
632
- return (issues.find((issue) => issue.dataset === chart.dataset) ??
633
- issues.find((issue) => issue.sourceId && issue.sourceId === chart.sourceId) ??
634
- null);
635
- }
636
- function activeFilterSummary(filters, selectedFilters) {
637
- const active = filters
638
- .map((filter) => {
639
- const value = selectedFilters[filter.id] ?? filter.defaultValue ?? "all";
640
- return value === "all" ? null : `${filter.label}: ${value}`;
641
- })
642
- .filter(Boolean);
643
- return active.length ? active.join(", ") : "None";
644
- }
645
- function stringListFromValue(value) {
646
- if (Array.isArray(value)) {
647
- return value
648
- .map((item) => {
649
- if (item && typeof item === "object") {
650
- const label = item.label ?? item.name ?? item.table ?? item.field ?? item.metric ?? item.id;
651
- const detail = item.description ?? item.definition ?? item.value ?? item.expression;
652
- return label && detail ? `${label}: ${detail}` : label ?? detail;
653
- }
654
- return item;
655
- })
656
- .map((item) => String(item ?? "").trim())
657
- .filter(Boolean);
658
- }
659
- if (value && typeof value === "object") {
660
- return Object.entries(value)
661
- .map(([key, item]) => `${key}: ${item}`)
662
- .map((item) => item.trim())
663
- .filter(Boolean);
664
- }
665
- const textValue = String(value ?? "").trim();
666
- return textValue ? [textValue] : [];
667
- }
668
- function firstStringList(objects, keys) {
669
- for (const object of objects) {
670
- if (!object)
671
- continue;
672
- for (const key of keys) {
673
- const values = stringListFromValue(object[key]);
674
- if (values.length)
675
- return values;
676
- }
677
- }
678
- return [];
679
- }
680
- function isLikelySourceTableName(value) {
681
- const tableName = String(value ?? "").trim().replace(/^[`"\[]|[`"\]]$/g, "");
682
- return /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){1,3}$/.test(tableName);
683
- }
684
- function sourceTableNamesFromMetadata(objects) {
685
- const values = firstStringList(objects, ["tables_used", "tablesUsed", "source_tables", "sourceTables", "tables"]);
686
- return values.filter(isLikelySourceTableName);
687
- }
688
- function extractTablesFromQuery(queryText) {
689
- const tables = [];
690
- const seen = new Set();
691
- const pattern = /\b(?:from|join)\s+([`"\[]?[\w.-]+(?:\.[\w.-]+){0,3}[`"\]]?)/gi;
692
- let match;
693
- while ((match = pattern.exec(queryText ?? ""))) {
694
- const table = match[1].replace(/^[`"\[]|[`"\]]$/g, "");
695
- const normalized = table.toLowerCase();
696
- if (!table || seen.has(normalized))
697
- continue;
698
- seen.add(normalized);
699
- tables.push(table);
700
- }
701
- return tables;
702
- }
703
- function sourceBuildDetails({ activeFilters, columns = [], dataset, metrics = [], source, sourceQuery, snapshot }) {
704
- const queryText = queryTextFromSourceQuery(sourceQueryFromSourceSpec(source)) || queryTextFromSourceQuery(sourceQuery) || "";
705
- const metadataObjects = [source, sourceQueryFromSourceSpec(source), sourceQuery].filter(Boolean);
706
- const explicitTables = sourceTableNamesFromMetadata(metadataObjects);
707
- const inferredTables = extractTablesFromQuery(queryText);
708
- const tables = explicitTables.length ? explicitTables : inferredTables;
709
- const filters = firstStringList(metadataObjects, [
710
- "filters",
711
- "filter_descriptions",
712
- "filterDescriptions",
713
- "filter_description",
714
- "filterDescription",
715
- ]);
716
- const metricDefinitions = firstStringList(metadataObjects, [
717
- "metric_definitions",
718
- "metricDefinitions",
719
- "metrics_definition",
720
- "metricDefinition",
721
- ]);
722
- const fallbackMetrics = metricDefinitions.length
723
- ? metricDefinitions
724
- : metrics.length
725
- ? metrics.map((metric) => `${metric.label}: displayed from ${metric.field}`)
726
- : columns
727
- .filter((column) => ["number", "percent", "currency"].includes(column.format ?? column.type ?? ""))
728
- .map((column) => `${column.label ?? column.field}: displayed from ${column.field}`)
729
- .slice(0, 6);
730
- const filterRows = filters.length ? filters : activeFilters && activeFilters !== "None" ? [activeFilters] : ["None declared"];
731
- const tableRows = tables.length ? tables : ["Not declared"];
732
- const snapshotValue = sourceQuery?.executed_at ?? sourceQueryFromSourceSpec(source)?.executed_at ?? snapshot?.generatedAt ?? "Not declared";
733
- const primarySource = tableRows[0] === "Not declared" ? dataset ?? "reviewed rows" : tableRows[0];
734
- const summary = `This block uses ${primarySource} for dataset ${dataset ?? "unknown"}.${filterRows[0] !== "None declared" ? ` Filters: ${filterRows.slice(0, 2).join("; ")}.` : ""} Use the source query below to inspect the exact logic.`;
735
- return {
736
- dataset: dataset ?? "Not declared",
737
- fields: columns
738
- .map((column) => column.field ?? column.key ?? column.label)
739
- .filter(Boolean),
740
- filters: filterRows,
741
- metricDefinitions: fallbackMetrics.length ? fallbackMetrics : ["Displayed directly from source columns"],
742
- snapshot: snapshotValue,
743
- summary,
744
- tables: tableRows
745
- };
746
- }
747
- function detailText(values, fallback = "Not declared") {
748
- const items = Array.isArray(values) ? values.filter(Boolean) : [values].filter(Boolean);
749
- return items.length ? items.join("; ") : fallback;
750
- }
751
- function detailList(values, fallback = "Not declared") {
752
- const items = Array.isArray(values) ? values.filter(Boolean) : [values].filter(Boolean);
753
- if (!items.length)
754
- return fallback;
755
- return (<div className="source-detail-list">
756
- {items.map((item, index) => <div className="source-detail-list-item" key={`${index}-${item}`}>
757
- {item}
758
- </div>)}
759
- </div>);
760
- }
761
- function compareTableValues(a, b, field, direction) {
762
- const aValue = a[field];
763
- const bValue = b[field];
764
- if (aValue == null && bValue == null)
765
- return 0;
766
- if (aValue == null)
767
- return direction === "asc" ? 1 : -1;
768
- if (bValue == null)
769
- return direction === "asc" ? -1 : 1;
770
- let result = 0;
771
- if (typeof aValue === "number" && typeof bValue === "number") {
772
- result = aValue - bValue;
773
- }
774
- else {
775
- result = String(aValue).localeCompare(String(bValue), undefined, {
776
- numeric: true,
777
- sensitivity: "base"
778
- });
779
- }
780
- return direction === "asc" ? result : -result;
781
- }
782
- function MetricBadge({ metric, row }) {
783
- const value = row[metric.field];
784
- const renderedValue = formatMetricValue(value, metric);
785
- if (!renderedValue)
786
- return null;
787
- const direction = metricMovementDirection(value, metric.signed === true);
788
- return (<div className={`metric-badge ${direction ?? "neutral"}`}>
789
- <span className="metric-badge-label">{metric.label}</span>
790
- <span className="metric-badge-value">{renderedValue}</span>
791
- </div>);
792
- }
793
- function KpiCard({ card, filters, selectedFilters, snapshot }) {
794
- const metrics = cardMetrics(card);
795
- const [primaryMetric, ...supportingMetrics] = metrics;
796
- const rows = filterRowsForDataset(getRows(snapshot, card.dataset), card.dataset, filters, selectedFilters, metrics.map((metric) => metric.field));
797
- const row = rows.find((candidate) => rowMatchesFilter(candidate, card.filter)) ?? {};
798
- const label = cardLabel(card);
799
- const description = card.description?.trim();
800
- return (<section className="kpi-card">
801
- <div className="kpi-label-row">
802
- <div className="kpi-label">{label}</div>
803
- {description ? <MetricDescriptionPopover description={description} label={label}/> : null}
804
- </div>
805
- <div className="kpi-value">{primaryMetric ? formatValue(row[primaryMetric.field], primaryMetric.format) : "—"}</div>
806
- {supportingMetrics.length ? (<div className="metric-badge-row">
807
- {supportingMetrics.map((metric) => (<MetricBadge key={`${metric.field}:${metric.label}`} metric={metric} row={row}/>))}
808
- </div>) : null}
809
- </section>);
810
- }
811
- function getKpiColumnCount(width, cardCount) {
812
- const breakpoint = KPI_COLUMN_BREAKPOINTS.find((item) => width >= item.minWidth);
813
- return Math.max(1, Math.min(cardCount, breakpoint?.columns ?? 1));
814
- }
815
- function useMeasuredElementSize() {
816
- const ref = useRef(null);
817
- const [size, setSize] = useState({ height: 0, width: 0 });
818
- useEffect(() => {
819
- const element = ref.current;
820
- if (!element)
821
- return;
822
- let frame = 0;
823
- const updateWidth = () => {
824
- window.cancelAnimationFrame(frame);
825
- frame = window.requestAnimationFrame(() => {
826
- const rect = element.getBoundingClientRect();
827
- setSize({
828
- height: Math.floor(rect.height),
829
- width: Math.floor(rect.width)
830
- });
831
- });
832
- };
833
- updateWidth();
834
- const observer = new ResizeObserver(updateWidth);
835
- observer.observe(element);
836
- window.addEventListener("resize", updateWidth);
837
- return () => {
838
- window.cancelAnimationFrame(frame);
839
- observer.disconnect();
840
- window.removeEventListener("resize", updateWidth);
841
- };
842
- }, []);
843
- return [ref, size];
844
- }
845
- function KpiStrip({ cards, filters, selectedFilters, snapshot }) {
846
- const [stripRef, stripSize] = useMeasuredElementSize();
847
- const fallbackWidth = typeof window === "undefined" ? 0 : window.innerWidth;
848
- const columns = getKpiColumnCount(stripSize.width || fallbackWidth, cards.length);
849
- const placeholderCount = (columns - (cards.length % columns)) % columns;
850
- const stripStyle = { "--kpi-columns": columns };
851
- if (!cards.length)
852
- return null;
853
- return (<section className={`kpi-strip kpi-columns-${columns}`} style={stripStyle} aria-label="Key metrics" ref={stripRef}>
854
- {cards.map((card) => (<KpiCard card={card} filters={filters} key={card.id} selectedFilters={selectedFilters} snapshot={snapshot}/>))}
855
- {Array.from({ length: placeholderCount }, (_, index) => (<div aria-hidden="true" className="kpi-card kpi-card-placeholder" key={`kpi-placeholder-${index}`}/>))}
856
- </section>);
857
- }
858
- const MENU_CLOSE_ANIMATION_MS = 100;
859
- const NARROW_FIXED_MENU_QUERY = "(max-width: 560px)";
860
- function useDashboardMenu(isOpen, onOpenChange) {
861
- const menuButtonRef = useRef(null);
862
- const menuRef = useRef(null);
863
- const isOpenRef = useRef(isOpen);
864
- const [shouldRenderMenu, setShouldRenderMenu] = useState(isOpen);
865
- const [menuMotionClass, setMenuMotionClass] = useState("opening");
866
- const [fixedMenuStyle, setFixedMenuStyle] = useState(undefined);
867
- useEffect(() => {
868
- isOpenRef.current = isOpen;
869
- }, [isOpen]);
870
- const updateFixedMenuStyle = useCallback(() => {
871
- if (typeof window === "undefined") {
872
- return;
873
- }
874
- if (!window.matchMedia(NARROW_FIXED_MENU_QUERY).matches) {
875
- setFixedMenuStyle(undefined);
876
- return;
877
- }
878
- const anchor = menuButtonRef.current?.getBoundingClientRect();
879
- if (!anchor) {
880
- return;
881
- }
882
- const menuSurface = menuRef.current?.matches?.('[role="menu"]')
883
- ? menuRef.current
884
- : menuRef.current?.querySelector?.('[role="menu"]');
885
- const margin = 12;
886
- const gap = 6;
887
- const surfaceHeight = menuSurface?.getBoundingClientRect?.().height ?? 0;
888
- let top = anchor.bottom + gap;
889
- if (surfaceHeight && top + surfaceHeight > window.innerHeight - margin && anchor.top - surfaceHeight - gap >= margin) {
890
- top = anchor.top - surfaceHeight - gap;
891
- }
892
- else if (surfaceHeight) {
893
- top = clamp(top, margin, window.innerHeight - margin - surfaceHeight);
894
- }
895
- else {
896
- top = clamp(top, margin, window.innerHeight - margin);
897
- }
898
- setFixedMenuStyle((current) => {
899
- if (current?.top === top)
900
- return current;
901
- return { top };
902
- });
903
- }, []);
904
- function menuItems() {
905
- return Array.from(menuRef.current?.querySelectorAll('[role="menuitem"], [role="menuitemradio"]') ?? []).filter((item) => !item.disabled);
906
- }
907
- function focusMenuItem(position) {
908
- window.requestAnimationFrame(() => {
909
- const items = menuItems();
910
- if (!items.length)
911
- return;
912
- const index = position === "first"
913
- ? 0
914
- : position === "last"
915
- ? items.length - 1
916
- : Math.max(0, Math.min(items.length - 1, position));
917
- items[index]?.focus();
918
- });
919
- }
920
- function openMenu() {
921
- setShouldRenderMenu(true);
922
- setMenuMotionClass("opening");
923
- onOpenChange(true);
924
- }
925
- function closeMenu() {
926
- onOpenChange(false);
927
- }
928
- function setMenuOpen(nextIsOpen) {
929
- if (nextIsOpen) {
930
- openMenu();
931
- return;
932
- }
933
- closeMenu();
934
- }
935
- function toggleMenu() {
936
- const nextIsOpen = !isOpenRef.current;
937
- setMenuOpen(nextIsOpen);
938
- return nextIsOpen;
939
- }
940
- useEffect(() => {
941
- if (isOpen) {
942
- setShouldRenderMenu(true);
943
- setMenuMotionClass("opening");
944
- return;
945
- }
946
- if (!shouldRenderMenu)
947
- return;
948
- setMenuMotionClass("closing");
949
- const closeTimer = window.setTimeout(() => {
950
- setShouldRenderMenu(false);
951
- }, MENU_CLOSE_ANIMATION_MS);
952
- return () => {
953
- window.clearTimeout(closeTimer);
954
- };
955
- }, [isOpen, shouldRenderMenu]);
956
- useLayoutEffect(() => {
957
- if (!shouldRenderMenu) {
958
- setFixedMenuStyle(undefined);
959
- return;
960
- }
961
- updateFixedMenuStyle();
962
- const frame = window.requestAnimationFrame(updateFixedMenuStyle);
963
- window.addEventListener("resize", updateFixedMenuStyle);
964
- window.addEventListener("scroll", updateFixedMenuStyle, true);
965
- return () => {
966
- window.cancelAnimationFrame(frame);
967
- window.removeEventListener("resize", updateFixedMenuStyle);
968
- window.removeEventListener("scroll", updateFixedMenuStyle, true);
969
- };
970
- }, [shouldRenderMenu, updateFixedMenuStyle]);
971
- useEffect(() => {
972
- if (!isOpen)
973
- return;
974
- function handlePointerDown(event) {
975
- const target = event.target;
976
- if (!menuRef.current?.contains(target) && !menuButtonRef.current?.contains(target)) {
977
- closeMenu();
978
- }
979
- }
980
- function handleKeyDown(event) {
981
- if (event.key === "Escape") {
982
- closeMenu();
983
- menuButtonRef.current?.focus();
984
- }
985
- }
986
- document.addEventListener("pointerdown", handlePointerDown);
987
- document.addEventListener("keydown", handleKeyDown);
988
- return () => {
989
- document.removeEventListener("pointerdown", handlePointerDown);
990
- document.removeEventListener("keydown", handleKeyDown);
991
- };
992
- }, [isOpen, onOpenChange]);
993
- return {
994
- closeMenu,
995
- fixedMenuStyle,
996
- handleMenuButtonKeyDown: (event) => {
997
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
998
- event.preventDefault();
999
- openMenu();
1000
- focusMenuItem(event.key === "ArrowDown" ? "first" : "last");
1001
- }
1002
- },
1003
- handleMenuKeyDown: (event) => {
1004
- if (event.key !== "ArrowDown" &&
1005
- event.key !== "ArrowUp" &&
1006
- event.key !== "Home" &&
1007
- event.key !== "End" &&
1008
- event.key !== "Escape") {
1009
- return;
1010
- }
1011
- event.preventDefault();
1012
- if (event.key === "Escape") {
1013
- closeMenu();
1014
- menuButtonRef.current?.focus();
1015
- return;
1016
- }
1017
- const items = menuItems();
1018
- if (!items.length)
1019
- return;
1020
- const activeIndex = items.findIndex((item) => item === document.activeElement);
1021
- if (event.key === "Home") {
1022
- focusMenuItem("first");
1023
- return;
1024
- }
1025
- if (event.key === "End") {
1026
- focusMenuItem("last");
1027
- return;
1028
- }
1029
- const offset = event.key === "ArrowDown" ? 1 : -1;
1030
- const nextIndex = activeIndex === -1
1031
- ? (event.key === "ArrowDown" ? 0 : items.length - 1)
1032
- : (activeIndex + offset + items.length) % items.length;
1033
- focusMenuItem(nextIndex);
1034
- },
1035
- menuButtonRef,
1036
- menuMotionClass,
1037
- menuRef,
1038
- toggleMenu,
1039
- shouldRenderMenu
1040
- };
1041
- }
1042
- function FilterMenu({ label, onChange, options, value }) {
1043
- const [isOpen, setIsOpen] = useState(false);
1044
- const { closeMenu, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isOpen, setIsOpen);
1045
- const selectedOption = options.find((option) => option.value === value) ?? options[0];
1046
- return (<div className="filter-menu" ref={menuRef}>
1047
- <button aria-expanded={isOpen} aria-haspopup="menu" className="filter-menu-button" onClick={toggleMenu} onKeyDown={handleMenuButtonKeyDown} ref={menuButtonRef} type="button">
1048
- <span className="filter-menu-label">{label}</span>
1049
- <span className="filter-menu-value">{selectedOption?.label ?? value}</span>
1050
- <ChevronDown aria-hidden="true" size={14} strokeWidth={2}/>
1051
- </button>
1052
- {shouldRenderMenu ? (<div className={`filter-menu-list menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu">
1053
- {options.map((option) => {
1054
- const isSelected = option.value === value;
1055
- return (<button aria-checked={isSelected} className="filter-menu-item" key={option.value} onClick={() => {
1056
- onChange(option.value);
1057
- closeMenu();
1058
- }} role="menuitemradio" type="button">
1059
- <span>{option.label}</span>
1060
- {isSelected ? <Check aria-hidden="true" size={14} strokeWidth={2}/> : null}
1061
- </button>);
1062
- })}
1063
- </div>) : null}
1064
- </div>);
1065
- }
1066
- function FilterToolbar({ filters, snapshot, selectedFilters, onChange }) {
1067
- if (!filters.length)
1068
- return null;
1069
- return (<div className="filter-toolbar" aria-label="Analytics filters">
1070
- <div className="filter-group">
1071
- {filters.map((filter) => {
1072
- const options = Array.from(new Set(getRows(snapshot, filter.dataset).map((row) => String(row[filter.field] ?? ""))))
1073
- .filter(Boolean)
1074
- .sort();
1075
- const filterOptions = [
1076
- ...(filter.includeAll === false ? [] : [{ label: "All", value: "all" }]),
1077
- ...options.map((option) => ({ label: option, value: option }))
1078
- ];
1079
- const value = selectedFilters[filter.id] ?? filter.defaultValue ?? "all";
1080
- return (<FilterMenu key={filter.id} label={filter.label} onChange={(nextValue) => onChange({ ...selectedFilters, [filter.id]: nextValue })} options={filterOptions} value={value}/>);
1081
- })}
1082
- </div>
1083
- </div>);
1084
- }
1085
- function PanelHeader({ action, children, title, subtitle, titleRowClassName, titleRowProps }) {
1086
- return (<div className="panel-header">
1087
- <div {...titleRowProps} className={`panel-title-row ${titleRowClassName ?? ""} ${titleRowProps?.className ?? ""}`.trim()}>
1088
- {children ?? (<div>
1089
- <h2>{title}</h2>
1090
- {subtitle ? <p>{subtitle}</p> : null}
1091
- </div>)}
1092
- </div>
1093
- {action}
1094
- </div>);
1095
- }
1096
- function EditablePageTitle({ ariaLabel, isEditMode, onChange, onRequestEditMode, placeholder, readOnly = false, title }) {
1097
- const [isEditing, setIsEditing] = useState(false);
1098
- const textareaRef = useRef(null);
1099
- const displayTitle = title.trim() ? title : placeholder;
1100
- useEffect(() => {
1101
- if (isEditing)
1102
- resizeEditableTextarea(textareaRef.current);
1103
- }, [isEditing, title]);
1104
- function startEditing() {
1105
- if (readOnly)
1106
- return;
1107
- if (!isEditMode)
1108
- onRequestEditMode();
1109
- setIsEditing(true);
1110
- }
1111
- if (readOnly) {
1112
- return (<div className="page-title-edit-target page-title-readonly">
1113
- <h1>{displayTitle}</h1>
1114
- </div>);
1115
- }
1116
- if (isEditing) {
1117
- return (<textarea aria-label={ariaLabel} autoFocus className="page-title-editor viz-card__no-drag" onBlur={() => setIsEditing(false)} onChange={(event) => {
1118
- resizeEditableTextarea(event.currentTarget);
1119
- onChange(event.currentTarget.value);
1120
- }} placeholder={placeholder} ref={textareaRef} rows={1} value={title}/>);
1121
- }
1122
- return (<div aria-label={ariaLabel} className="page-title-edit-target viz-card__no-drag" data-page-title-edit-mode={isEditMode ? "true" : "false"} onClick={() => {
1123
- if (isEditMode)
1124
- startEditing();
1125
- }} onDoubleClick={startEditing} onKeyDown={(event) => {
1126
- if (event.key === "Enter" || event.key === " ") {
1127
- event.preventDefault();
1128
- startEditing();
1129
- }
1130
- }} role="button" tabIndex={0}>
1131
- <h1>{displayTitle}</h1>
1132
- </div>);
1133
- }
1134
- function composeHeaderMarkdown(title, description) {
1135
- return description?.trim() ? `## ${title}\n\n${description}` : `## ${title}`;
1136
- }
1137
- function composeVisualHeaderMarkdown(title, description, headerMarkdown) {
1138
- if (headerMarkdown?.trim()) {
1139
- return headerMarkdown;
1140
- }
1141
- return composeHeaderMarkdown(title, description);
1142
- }
1143
- function composePageTitle(manifest) {
1144
- const fallbackTitle = manifest?.surface === "report" ? "Data Analytics Report" : "Data Analytics Dashboard";
1145
- return manifest?.title?.trim() || fallbackTitle;
1146
- }
1147
- function composeChartHeaderMarkdown(chart) {
1148
- return composeVisualHeaderMarkdown(chart.title, chart.showDescription ? chart.subtitle : undefined, chart.headerMarkdown);
1149
- }
1150
- function composeTableHeaderMarkdown(table) {
1151
- return composeVisualHeaderMarkdown(table.title, table.showDescription ? table.subtitle : undefined, table.headerMarkdown);
1152
- }
1153
- function reportBlockBodyMarkdown(block) {
1154
- return typeof block.body === "string" ? block.body : "";
1155
- }
1156
- function visibleString(value) {
1157
- return typeof value === "string" && value.trim() ? value : undefined;
1158
- }
1159
- function composeBlockMarkdown(block, textOverride) {
1160
- if (visibleString(textOverride?.bodyMarkdown))
1161
- return textOverride.bodyMarkdown;
1162
- return reportBlockBodyMarkdown(block);
1163
- }
1164
- function composeBlockHtml(block, textOverride) {
1165
- if (visibleString(textOverride?.html))
1166
- return textOverride.html;
1167
- return typeof block.body === "string" ? block.body : "";
1168
- }
1169
- const REPORT_HTML_BLOCK_CSP = [
1170
- "default-src 'none'",
1171
- "base-uri 'none'",
1172
- "connect-src 'none'",
1173
- "font-src data:",
1174
- "form-action 'none'",
1175
- "frame-src 'none'",
1176
- "img-src data: blob:",
1177
- "media-src data: blob:",
1178
- "object-src 'none'",
1179
- "script-src 'none'",
1180
- "style-src 'unsafe-inline'",
1181
- ].join("; ");
1182
- function sandboxedReportHtml(html) {
1183
- return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${REPORT_HTML_BLOCK_CSP}"><meta name="viewport" content="width=device-width, initial-scale=1"><base target="_blank"><style>html,body{margin:0;background:transparent;}img,svg,canvas,video{max-width:100%;height:auto;}</style></head><body>${html}</body></html>`;
1184
- }
1185
- function measureHtmlFrameHeight(frame) {
1186
- const documentElement = frame?.contentDocument?.documentElement;
1187
- const body = frame?.contentDocument?.body;
1188
- if (!documentElement && !body)
1189
- return 0;
1190
- return Math.ceil(Math.max(documentElement?.scrollHeight ?? 0, body?.scrollHeight ?? 0));
1191
- }
1192
- function markdownPlainText(markdown) {
1193
- return markdown
1194
- .replace(/^#{1,6}\s+/gm, "")
1195
- .replace(/\*\*([^*]+)\*\*/g, "$1")
1196
- .replace(/`([^`]+)`/g, "$1")
1197
- .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
1198
- .trim();
1199
- }
1200
- function markdownFirstLine(markdown, fallback) {
1201
- const first = markdownPlainText(markdown).split(/\r?\n/).find((line) => line.trim());
1202
- return first?.trim() || fallback;
1203
- }
1204
- function ChartBody({ accessIssue, chart, filters, isFullscreen = false, selectedFilters, snapshot }) {
1205
- const [visibleSeries, setVisibleSeries] = useState();
1206
- const rawRows = filterRowsForDataset(getRows(snapshot, chart.dataset), chart.dataset, filters, selectedFilters, chartUsedFields(chart));
1207
- if (accessIssue) {
1208
- return (<div className="permission-card">
1209
- <div className="permission-card-title">Missing data access</div>
1210
- <p>{accessIssue.message}</p>
1211
- {accessIssue.actionHref ? (<a href={accessIssue.actionHref} rel="noreferrer" target="_blank">
1212
- {accessIssue.actionLabel ?? "Request access"}
1213
- </a>) : accessIssue.actionLabel ? (<span>{accessIssue.actionLabel}</span>) : null}
1214
- </div>);
1215
- }
1216
- if (!rawRows.length) {
1217
- return <div className="empty-state">No rows match the selected filters.</div>;
1218
- }
1219
- return (<ChartRenderer chart={chart} height={isFullscreen ? CHART_FULLSCREEN_HEIGHT : undefined} onVisibleSeriesChange={setVisibleSeries} rows={rawRows} surface={isFullscreen ? "explorer" : "card"} visibleSeries={visibleSeries}/>);
1220
- }
1221
- function VizCard({ accessIssue, chart, children, isEditMode, isMenuOpen, layout, onCopyResult, onDeleteBlock, onRequestEditMode, onTextChange, onMenuOpenChange, onModalOpen, textOverride }) {
1222
- const cardRef = useRef(null);
1223
- const { closeMenu, fixedMenuStyle, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isMenuOpen, onMenuOpenChange);
1224
- const [imageExportState, setImageExportState] = useState({ status: "idle" });
1225
- const { getPreparedImageBlob, preparedImageExportStatus, prepareImageExport, resetPreparedImageExport } = usePreparedImageExport(cardRef);
1226
- const fallbackHeaderMarkdown = textOverride?.title || textOverride?.subtitle
1227
- ? composeHeaderMarkdown(textOverride.title ?? chart.title, textOverride.subtitle ?? chart.subtitle)
1228
- : composeChartHeaderMarkdown(chart);
1229
- const displayHeaderMarkdown = textOverride?.headerMarkdown ?? fallbackHeaderMarkdown;
1230
- const displayChart = {
1231
- ...chart,
1232
- subtitle: undefined,
1233
- title: markdownFirstLine(displayHeaderMarkdown, chart.title)
1234
- };
1235
- function prepareImageExportQuietly(force = false) {
1236
- if (accessIssue || !shouldOfferImageClipboardCopy())
1237
- return;
1238
- const prepared = prepareImageExport({ force });
1239
- if (prepared)
1240
- void prepared.promise.catch(() => undefined);
1241
- }
1242
- async function handleCopyAsImage() {
1243
- if (!cardRef.current)
1244
- return;
1245
- setImageExportState({ status: "loading" });
1246
- try {
1247
- const copyResult = await copyElementAsImage(cardRef.current, getPreparedImageBlob());
1248
- setImageExportState({ status: "idle" });
1249
- resetPreparedImageExport();
1250
- onCopyResult(imageCopySuccessMessage("Copied widget as image.", copyResult));
1251
- }
1252
- catch (error) {
1253
- setImageExportState({
1254
- error: error instanceof Error ? error.message : "Failed to copy image.",
1255
- status: "error"
1256
- });
1257
- onCopyResult(error instanceof Error ? error.message : "Failed to copy image.", true);
1258
- }
1259
- }
1260
- const menuItem = (label, icon, onClick, disabled = false, tone = "default", onPrepare) => (<button className={`viz-card-menu-item ${tone === "danger" ? "viz-card-menu-item-danger" : ""}`.trim()} disabled={disabled} key={label} onFocus={onPrepare} onClick={() => {
1261
- void onClick();
1262
- closeMenu();
1263
- }} onPointerEnter={onPrepare} role="menuitem" type="button">
1264
- <span aria-hidden="true" className="viz-card-menu-icon">
1265
- {icon}
1266
- </span>
1267
- <span>{label}</span>
1268
- </button>);
1269
- return (<section className={`panel chart-panel viz-card ${accessIssue ? "has-permission-issue" : ""}`} id={chart.id} ref={cardRef}>
1270
- <PanelHeader action={<div className="viz-card-actions" data-image-export-exclude="true" ref={menuRef}>
1271
- <button aria-expanded={isMenuOpen} aria-label={`Open options for ${displayChart.title}`} className="viz-card-menu-button viz-card__no-drag" onClick={(event) => {
1272
- event.stopPropagation();
1273
- const nextIsMenuOpen = toggleMenu();
1274
- if (nextIsMenuOpen) {
1275
- prepareImageExportQuietly(true);
1276
- }
1277
- else {
1278
- resetPreparedImageExport();
1279
- }
1280
- }} onFocus={() => prepareImageExportQuietly()} onKeyDown={handleMenuButtonKeyDown} onPointerEnter={() => prepareImageExportQuietly()} ref={menuButtonRef} type="button">
1281
- <Ellipsis aria-hidden="true" size={18} strokeWidth={2}/>
1282
- </button>
1283
- {shouldRenderMenu ? (<div className={`viz-card-menu viz-card__no-drag menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu" style={fixedMenuStyle}>
1284
- {menuItem("Edit chart", <ChartNoAxesCombined size={18} strokeWidth={2}/>, () => onModalOpen({ chart: displayChart, kind: "fullscreen" }))}
1285
- {menuItem("View data source", <Database size={18} strokeWidth={2}/>, () => onModalOpen({ chart: displayChart, kind: "source" }))}
1286
- {shouldOfferImageClipboardCopy()
1287
- ? menuItem("Copy as image", <Camera size={18} strokeWidth={2}/>, handleCopyAsImage, Boolean(accessIssue) ||
1288
- imageExportState.status === "loading" ||
1289
- preparedImageExportStatus === "pending", "default", prepareImageExportQuietly)
1290
- : null}
1291
- {onDeleteBlock ? menuItem("Delete", <Trash2 size={18} strokeWidth={2}/>, onDeleteBlock, false, "danger") : null}
1292
- </div>) : null}
1293
- </div>} subtitle={displayChart.subtitle} title={displayChart.title} titleRowClassName="viz-card__drag-handle">
1294
- <RichMarkdown ariaLabel={`Edit markdown header for ${chart.title}`} className="editable-cell-header" isEditMode={isEditMode} markdown={displayHeaderMarkdown} onMarkdownChange={(nextMarkdown) => onTextChange(chart.id, { headerMarkdown: nextMarkdown })} onRequestEditMode={onRequestEditMode} placeholder={composeChartHeaderMarkdown(chart)} variant="cellHeader"/>
1295
- </PanelHeader>
1296
- {children}
1297
- </section>);
1298
- }
1299
- function AccessIssueStrip({ issues }) {
1300
- if (!issues.length)
1301
- return null;
1302
- return (<section className="access-issue-strip" aria-label="Data access issues">
1303
- <div>
1304
- <strong>{issues.length === 1 ? "Data access blocker" : "Data access blockers"}</strong>
1305
- <p>Some report data could not load because the source query could not complete.</p>
1306
- </div>
1307
- <ul>
1308
- {issues.map((issue) => (<li key={issue.id}>
1309
- <span>{issue.scope ?? issue.dataset ?? issue.sourceId ?? issue.id}</span>
1310
- <span>{issue.message}</span>
1311
- {issue.actionHref ? (<a href={issue.actionHref} rel="noreferrer" target="_blank">
1312
- {issue.actionLabel ?? "Request access"}
1313
- </a>) : null}
1314
- </li>))}
1315
- </ul>
1316
- </section>);
1317
- }
1318
- const SQL_KEYWORDS = new Set([
1319
- "and",
1320
- "as",
1321
- "asc",
1322
- "by",
1323
- "case",
1324
- "desc",
1325
- "distinct",
1326
- "else",
1327
- "end",
1328
- "from",
1329
- "group",
1330
- "in",
1331
- "is",
1332
- "join",
1333
- "left",
1334
- "like",
1335
- "max",
1336
- "min",
1337
- "not",
1338
- "null",
1339
- "on",
1340
- "or",
1341
- "order",
1342
- "over",
1343
- "partition",
1344
- "right",
1345
- "select",
1346
- "sum",
1347
- "then",
1348
- "when",
1349
- "where",
1350
- "with"
1351
- ]);
1352
- const SQL_FUNCTIONS = new Set([
1353
- "avg",
1354
- "cast",
1355
- "coalesce",
1356
- "concat",
1357
- "count",
1358
- "current_date",
1359
- "date_trunc",
1360
- "dateadd",
1361
- "date_add",
1362
- "round",
1363
- "nullif"
1364
- ]);
1365
- const SQL_TOKEN_PATTERN = /(--[^\n]*|'(?:''|[^'])*'|<>|!=|<=|>=|\b\d+(?:\.\d+)?\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|[(),.;=*+\-/%<>])/g;
1366
- const SQL_FORMAT_LINE_STARTERS = new Set([
1367
- "from",
1368
- "group",
1369
- "having",
1370
- "limit",
1371
- "order",
1372
- "select",
1373
- "union",
1374
- "where",
1375
- "with"
1376
- ]);
1377
- const SQL_JOIN_MODIFIERS = new Set([
1378
- "cross",
1379
- "full",
1380
- "inner",
1381
- "join",
1382
- "left",
1383
- "outer",
1384
- "right"
1385
- ]);
1386
- function sqlFormatTokens(sql) {
1387
- const tokens = [];
1388
- let cursor = 0;
1389
- for (const match of sql.matchAll(SQL_TOKEN_PATTERN)) {
1390
- const token = match[0];
1391
- const index = match.index ?? 0;
1392
- const rawGap = sql.slice(cursor, index).trim();
1393
- if (rawGap)
1394
- tokens.push(...rawGap.split(/\s+/));
1395
- tokens.push(token);
1396
- cursor = index + token.length;
1397
- }
1398
- const tail = sql.slice(cursor).trim();
1399
- if (tail)
1400
- tokens.push(...tail.split(/\s+/));
1401
- return tokens;
1402
- }
1403
- function isSqlWord(token) {
1404
- return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(token);
1405
- }
1406
- function formatSqlForDisplay(sql) {
1407
- const tokens = sqlFormatTokens(sql.trim());
1408
- if (!tokens.length)
1409
- return "";
1410
- const lines = [];
1411
- let current = "";
1412
- let indent = 0;
1413
- const contextStack = [];
1414
- let previousToken = "";
1415
- function currentIndent(level = indent) {
1416
- return " ".repeat(Math.max(0, level));
1417
- }
1418
- function pushLine(nextIndent = indent) {
1419
- const text = current.trimEnd();
1420
- if (text.trim())
1421
- lines.push(text);
1422
- current = currentIndent(nextIndent);
1423
- }
1424
- function append(token, { tight = false } = {}) {
1425
- if (!current)
1426
- current = currentIndent();
1427
- const trimmed = current.trimEnd();
1428
- const needsSpace = !tight &&
1429
- trimmed &&
1430
- !trimmed.endsWith("(") &&
1431
- !trimmed.endsWith(".") &&
1432
- token !== "." &&
1433
- token !== "," &&
1434
- token !== ")" &&
1435
- token !== ";";
1436
- current = `${trimmed}${needsSpace ? " " : ""}${token}`;
1437
- previousToken = token;
1438
- }
1439
- for (let index = 0; index < tokens.length; index += 1) {
1440
- const token = tokens[index];
1441
- const lower = token.toLowerCase();
1442
- const previousLower = previousToken.toLowerCase();
1443
- const nextLower = tokens[index + 1]?.toLowerCase?.() ?? "";
1444
- if (token.startsWith("--")) {
1445
- if (current.trim())
1446
- pushLine();
1447
- append(token);
1448
- pushLine();
1449
- continue;
1450
- }
1451
- if (SQL_FORMAT_LINE_STARTERS.has(lower)) {
1452
- if (current.trim() && !current.trimEnd().endsWith("("))
1453
- pushLine();
1454
- append(lower.toUpperCase());
1455
- continue;
1456
- }
1457
- if (SQL_JOIN_MODIFIERS.has(lower)) {
1458
- if (!(lower === "join" && SQL_JOIN_MODIFIERS.has(previousLower)) && current.trim())
1459
- pushLine();
1460
- append(lower.toUpperCase());
1461
- continue;
1462
- }
1463
- if (token === "(") {
1464
- const functionCall = SQL_FUNCTIONS.has(previousLower) ||
1465
- (isSqlWord(previousToken) && !SQL_KEYWORDS.has(previousLower) && previousLower !== "as");
1466
- append(token, { tight: functionCall });
1467
- contextStack.push(functionCall ? "function" : "group");
1468
- if (!functionCall && (nextLower === "select" || previousLower === "as")) {
1469
- indent += 1;
1470
- pushLine();
1471
- }
1472
- else if (!functionCall) {
1473
- indent += 1;
1474
- }
1475
- continue;
1476
- }
1477
- if (token === ")") {
1478
- const context = contextStack.pop();
1479
- const nextIndent = Math.max(0, indent - 1);
1480
- if (context !== "function" && current.trim() && current.trim() !== currentIndent().trim())
1481
- pushLine(nextIndent);
1482
- indent = nextIndent;
1483
- append(token, { tight: true });
1484
- continue;
1485
- }
1486
- if (token === ",") {
1487
- append(token, { tight: true });
1488
- if (contextStack[contextStack.length - 1] !== "function")
1489
- pushLine();
1490
- continue;
1491
- }
1492
- if (token === ".") {
1493
- append(token, { tight: true });
1494
- continue;
1495
- }
1496
- if (token === ";") {
1497
- append(token, { tight: true });
1498
- pushLine();
1499
- continue;
1500
- }
1501
- append(SQL_KEYWORDS.has(lower) || SQL_FUNCTIONS.has(lower) ? lower.toUpperCase() : token);
1502
- }
1503
- if (current.trim())
1504
- pushLine();
1505
- return lines.join("\n");
1506
- }
1507
- function highlightSql(sql) {
1508
- const nodes = [];
1509
- let cursor = 0;
1510
- for (const match of sql.matchAll(SQL_TOKEN_PATTERN)) {
1511
- const token = match[0];
1512
- const index = match.index ?? 0;
1513
- if (index > cursor) {
1514
- nodes.push(sql.slice(cursor, index));
1515
- }
1516
- const lower = token.toLowerCase();
1517
- let className = "sql-token";
1518
- if (token.startsWith("--")) {
1519
- className += " comment";
1520
- }
1521
- else if (token.startsWith("'")) {
1522
- className += " string";
1523
- }
1524
- else if (/^\d/.test(token)) {
1525
- className += " number";
1526
- }
1527
- else if (SQL_KEYWORDS.has(lower)) {
1528
- className += " keyword";
1529
- }
1530
- else if (SQL_FUNCTIONS.has(lower)) {
1531
- className += " function";
1532
- }
1533
- else if (/^[(),.;=*+\-/%]$/.test(token)) {
1534
- className += " punctuation";
1535
- }
1536
- else {
1537
- className += " identifier";
1538
- }
1539
- nodes.push(<span className={className} key={`${index}-${token}`}>
1540
- {token}
1541
- </span>);
1542
- cursor = index + token.length;
1543
- }
1544
- if (cursor < sql.length) {
1545
- nodes.push(sql.slice(cursor));
1546
- }
1547
- return nodes;
1548
- }
1549
- const SOURCE_FETCH_TIMEOUT_MS = 10000;
1550
- function DataSourceDetails({ children, details, source, sourceQuery }) {
1551
- return (<div className="source-modal-sections">
1552
- <section className="source-modal-section">
1553
- <h3>Details</h3>
1554
- <dl className="details-grid source-details-grid">
1555
- <div>
1556
- <dt>Dataset</dt>
1557
- <dd>{details.dataset}</dd>
1558
- </div>
1559
- <div>
1560
- <dt>Tables used</dt>
1561
- <dd>{detailList(details.tables)}</dd>
1562
- </div>
1563
- <div>
1564
- <dt>Filters</dt>
1565
- <dd>{detailText(details.filters)}</dd>
1566
- </div>
1567
- <div>
1568
- <dt>Metric definitions</dt>
1569
- <dd>{detailList(details.metricDefinitions)}</dd>
1570
- </div>
1571
- <div>
1572
- <dt>Snapshot</dt>
1573
- <dd>{formatDate(details.snapshot)}</dd>
1574
- </div>
1575
- </dl>
1576
- </section>
1577
- <section className="source-modal-section">
1578
- <h3>Source query</h3>
1579
- <SourceQueryBlock source={source} sourceQuery={sourceQuery}/>
1580
- </section>
1581
- <section className="source-modal-section">
1582
- <h3>Data table</h3>
1583
- {children}
1584
- </section>
1585
- </div>);
1586
- }
1587
- function SourceQueryBlock({ source, sourceQuery }) {
1588
- const [sourceContent, setSourceContent] = useState({ status: "idle" });
1589
- const [copyStatus, setCopyStatus] = useState("idle");
1590
- const codeRef = useRef(null);
1591
- const inlineSourceText = queryTextFromSourceQuery(sourceQueryFromSourceSpec(source)) || queryTextFromSourceQuery(sourceQuery) || "";
1592
- const displaySourceText = sourceContent.status === "loaded" ? formatSqlForDisplay(sourceContent.text) : "";
1593
- useEffect(() => {
1594
- if (inlineSourceText) {
1595
- setSourceContent({ status: "loaded", text: inlineSourceText });
1596
- return;
1597
- }
1598
- if (!source?.path) {
1599
- setSourceContent({ status: "idle" });
1600
- return;
1601
- }
1602
- let cancelled = false;
1603
- const controller = new AbortController();
1604
- const timeout = window.setTimeout(() => controller.abort(), SOURCE_FETCH_TIMEOUT_MS);
1605
- setSourceContent({ status: "loading" });
1606
- fetch(`/api/source-file?path=${encodeURIComponent(source.path)}`, {
1607
- cache: "no-store",
1608
- signal: controller.signal
1609
- })
1610
- .then(async (response) => {
1611
- const text = await response.text();
1612
- if (!response.ok)
1613
- throw new Error(text || `Request failed: ${response.status}`);
1614
- return text;
1615
- })
1616
- .then((text) => {
1617
- if (!cancelled)
1618
- setSourceContent({ status: "loaded", text });
1619
- })
1620
- .catch((error) => {
1621
- if (!cancelled) {
1622
- const isAbort = error instanceof Error && error.name === "AbortError";
1623
- setSourceContent({
1624
- error: isAbort
1625
- ? "Source request timed out. Refresh the app and try again."
1626
- : error instanceof Error
1627
- ? error.message
1628
- : "Failed to load source",
1629
- status: "error"
1630
- });
1631
- }
1632
- })
1633
- .finally(() => {
1634
- window.clearTimeout(timeout);
1635
- });
1636
- return () => {
1637
- cancelled = true;
1638
- window.clearTimeout(timeout);
1639
- controller.abort();
1640
- };
1641
- }, [inlineSourceText, source?.path]);
1642
- useEffect(() => {
1643
- if (copyStatus === "idle")
1644
- return;
1645
- const timeout = window.setTimeout(() => setCopyStatus("idle"), 1800);
1646
- return () => window.clearTimeout(timeout);
1647
- }, [copyStatus]);
1648
- async function handleCopyQuery(text) {
1649
- try {
1650
- await copyTextToClipboard(text);
1651
- setCopyStatus("copied");
1652
- }
1653
- catch {
1654
- setCopyStatus("blocked");
1655
- }
1656
- }
1657
- if (sourceContent.status === "loading")
1658
- return <>Loading source...</>;
1659
- if (sourceContent.status === "error") {
1660
- return <span className="source-error">{sourceContent.error}</span>;
1661
- }
1662
- if (sourceContent.status === "loaded") {
1663
- return (<div className="source-query-shell">
1664
- {source?.path || source?.href || source?.label || sourceQuery?.id || sourceQuery?.engine ? (<div className="source-query-meta">
1665
- <span>
1666
- {source?.href ? (<a href={source.href} rel="noreferrer" target="_blank">
1667
- {source.path ?? source.label}
1668
- </a>) : (source?.path ?? source?.label ?? sourceQuery?.id ?? sourceQuery?.engine)}
1669
- </span>
1670
- <button className="source-query-copy" onClick={() => void handleCopyQuery(displaySourceText || sourceContent.text)} type="button">
1671
- <Copy aria-hidden="true" size={14} strokeWidth={2}/>
1672
- {copyStatus === "copied"
1673
- ? "Copied"
1674
- : copyStatus === "blocked"
1675
- ? "Copy failed"
1676
- : "Copy query"}
1677
- </button>
1678
- </div>) : null}
1679
- <pre className="source-query">
1680
- <code ref={codeRef}>{highlightSql(displaySourceText || sourceContent.text)}</code>
1681
- </pre>
1682
- </div>);
1683
- }
1684
- return <>No source query file mapped.</>;
1685
- }
1686
- function useModalScrollLock(enabled) {
1687
- useEffect(() => {
1688
- if (!enabled)
1689
- return undefined;
1690
- const previousOverflow = document.body.style.overflow;
1691
- const previousOverscrollBehavior = document.body.style.overscrollBehavior;
1692
- document.body.style.overflow = "hidden";
1693
- document.body.style.overscrollBehavior = "none";
1694
- return () => {
1695
- document.body.style.overflow = previousOverflow;
1696
- document.body.style.overscrollBehavior = previousOverscrollBehavior;
1697
- };
1698
- }, [enabled]);
1699
- }
1700
- function sourcePreviewColumns(rows, columns = []) {
1701
- if (columns.length)
1702
- return columns;
1703
- const fields = [];
1704
- const seen = new Set();
1705
- for (const row of rows) {
1706
- for (const field of Object.keys(row)) {
1707
- if (!seen.has(field)) {
1708
- seen.add(field);
1709
- fields.push(field);
1710
- }
1711
- }
1712
- }
1713
- return fields.map((field) => ({ field, label: field }));
1714
- }
1715
- function SourceDataTable({ columns = [], dataset, density = "dense", rows = [] }) {
1716
- const previewRows = useMemo(() => asArray(rows).filter((row) => row && typeof row === "object"), [rows]);
1717
- const previewDataset = dataset ?? "__source_preview_rows";
1718
- const table = useMemo(() => ({
1719
- columns: sourcePreviewColumns(previewRows, columns),
1720
- dataset: previewDataset,
1721
- id: `source-preview-${previewDataset}`
1722
- }), [columns, previewDataset, previewRows]);
1723
- const snapshot = useMemo(() => ({ datasets: { [previewDataset]: previewRows } }), [previewDataset, previewRows]);
1724
- const [columnWidths, setColumnWidths] = useState({});
1725
- useEffect(() => {
1726
- setColumnWidths({});
1727
- }, [table.id]);
1728
- return (<div className="source-data-table">
1729
- <TableContent allowColumnResize={false} columnWidths={columnWidths} density={density} filters={EMPTY_FILTERS} onColumnWidthsChange={(_tableId, nextWidths) => setColumnWidths(nextWidths)} selectedFilters={{}} snapshot={snapshot} table={table}/>
1730
- </div>);
1731
- }
1732
- function querySourceForChart(chart, sources) {
1733
- const directSource = sourceForChart(chart, sources);
1734
- return (directSource ??
1735
- sources.find((source) => {
1736
- const label = source.label?.toLowerCase() ?? "";
1737
- const path = source.path?.toLowerCase() ?? "";
1738
- return path.endsWith(".sql") || label.includes("sql") || label.includes("quer");
1739
- }) ??
1740
- null);
1741
- }
1742
- function chartMetricsForBuildDetails(chart) {
1743
- const metrics = [];
1744
- const seen = new Set();
1745
- function addMetric(field, label) {
1746
- if (!field || seen.has(field))
1747
- return;
1748
- seen.add(field);
1749
- metrics.push({ field, label: label || field });
1750
- }
1751
- addMetric(chart.encodings?.y?.field, chart.encodings?.y?.label);
1752
- for (const field of chart.encodings?.y?.fields ?? []) {
1753
- addMetric(field, field);
1754
- }
1755
- return metrics;
1756
- }
1757
- function ChartSourceModalDialog({ activeFilters, chart, manifest, onClose, rows = [], snapshot }) {
1758
- const dialogRef = useRef(null);
1759
- const source = querySourceForChart(chart, manifest?.sources ?? []);
1760
- const sourceQuery = sourceQueryFromSourceSpec(source);
1761
- const buildDetails = sourceBuildDetails({
1762
- activeFilters,
1763
- dataset: chart.dataset,
1764
- metrics: chartMetricsForBuildDetails(chart),
1765
- source,
1766
- sourceQuery,
1767
- snapshot
1768
- });
1769
- useModalScrollLock(true);
1770
- useEffect(() => {
1771
- const dialog = dialogRef.current;
1772
- if (dialog && !dialog.open) {
1773
- dialog.showModal();
1774
- }
1775
- }, []);
1776
- return (<dialog aria-labelledby="chart-source-modal-title" className="native-modal source-modal" onCancel={onClose} onClick={(event) => {
1777
- if (event.target === event.currentTarget) {
1778
- event.currentTarget.close();
1779
- }
1780
- }} onClose={onClose} ref={dialogRef}>
1781
- <section className="modal-panel source-modal-panel">
1782
- <div className="modal-header">
1783
- <div>
1784
- <h2 id="chart-source-modal-title">Data source</h2>
1785
- <p>{chart.title}</p>
1786
- </div>
1787
- <button aria-label="Close data source" className="modal-close-button" onClick={() => dialogRef.current?.close()} type="button">
1788
- <X aria-hidden="true" size={20} strokeWidth={2}/>
1789
- </button>
1790
- </div>
1791
- <DataSourceDetails details={buildDetails} source={source} sourceQuery={sourceQuery}>
1792
- <SourceDataTable dataset={chart.dataset} rows={rows}/>
1793
- </DataSourceDetails>
1794
- </section>
1795
- </dialog>);
1796
- }
1797
- function safeScriptJson(value) {
1798
- return JSON.stringify(value).replace(/</g, "\\u003c");
1799
- }
1800
- function chartWidgetDetailHtml(html, widgetInstanceId) {
1801
- const bridge = `<script>
1802
- window.openai = {
1803
- ...(window.openai || {}),
1804
- availableDisplayModes: ["modal"],
1805
- displayMode: "modal",
1806
- widgetInstanceId: ${safeScriptJson(widgetInstanceId)},
1807
- requestDisplayMode(request) {
1808
- const mode = typeof request === "string" ? request : request && request.mode;
1809
- window.parent.postMessage({ type: "datascience-chart-widget-display-mode", mode }, "*");
1810
- return Promise.resolve({ mode });
1811
- },
1812
- sendFollowUpMessage(payload) {
1813
- window.parent.postMessage(
1814
- {
1815
- originUrl: payload && payload.originUrl,
1816
- prompt: payload && payload.prompt,
1817
- type: "datascience-chart-widget-codex-prompt"
1818
- },
1819
- "*"
1820
- );
1821
- return Promise.resolve({ ok: true });
1822
- },
1823
- openCodexPrompt(payload) {
1824
- return this.sendFollowUpMessage(payload);
1825
- }
1826
- };
1827
- </script>`;
1828
- return html.includes("</head>") ? html.replace("</head>", `${bridge}</head>`) : `${bridge}${html}`;
1829
- }
1830
- function shouldUseHostedChartModal(packageInfo) {
1831
- return packageInfo?.hostedReadOnly === true || packageInfo?.deliveryMode === "site_creator";
1832
- }
1833
- function ChartDetailPage({ accessIssue, chart, filters, manifest, onChartSpecChange, onClose, packageInfo, rows, selectedFilters, snapshot }) {
1834
- const dialogRef = useRef(null);
1835
- const iframeRef = useRef(null);
1836
- const pendingPostTimersRef = useRef([]);
1837
- const [widgetHtml, setWidgetHtml] = useState(null);
1838
- const [widgetError, setWidgetError] = useState(null);
1839
- const [sourceQueryText, setSourceQueryText] = useState(null);
1840
- const source = querySourceForChart(chart, manifest?.sources ?? []);
1841
- const hostedChartModal = shouldUseHostedChartModal(packageInfo);
1842
- const widgetInstanceId = `report-chart-detail-${chart.id}`;
1843
- const widgetPayload = useMemo(() => chartWidgetPayload(chart, rows, source, snapshot, sourceQueryText), [chart, rows, source, snapshot, sourceQueryText]);
1844
- const widgetUrl = useMemo(() => `/api/inline-chart-widget?displayMode=modal&widgetInstanceId=${encodeURIComponent(widgetInstanceId)}`, [widgetInstanceId]);
1845
- const clearPendingPostTimers = useCallback(() => {
1846
- pendingPostTimersRef.current.forEach((timer) => window.clearTimeout(timer));
1847
- pendingPostTimersRef.current = [];
1848
- }, []);
1849
- const postWidgetPayload = useCallback(() => {
1850
- iframeRef.current?.contentWindow?.postMessage({
1851
- displayMode: "modal",
1852
- payload: widgetPayload,
1853
- targetWidgetInstanceId: widgetInstanceId
1854
- }, "*");
1855
- }, [widgetInstanceId, widgetPayload]);
1856
- const scheduleWidgetPayloadPosts = useCallback(() => {
1857
- clearPendingPostTimers();
1858
- pendingPostTimersRef.current = [0, 50, 150, 350].map((delay) => window.setTimeout(postWidgetPayload, delay));
1859
- }, [clearPendingPostTimers, postWidgetPayload]);
1860
- useEffect(() => {
1861
- const dialog = dialogRef.current;
1862
- if (dialog && !dialog.open) {
1863
- dialog.showModal();
1864
- }
1865
- }, []);
1866
- useEffect(() => {
1867
- let cancelled = false;
1868
- if (hostedChartModal) {
1869
- setWidgetHtml(null);
1870
- setWidgetError(null);
1871
- return () => {
1872
- cancelled = true;
1873
- };
1874
- }
1875
- setWidgetHtml(null);
1876
- setWidgetError(null);
1877
- void fetch(widgetUrl)
1878
- .then((response) => {
1879
- if (!response.ok)
1880
- throw new Error(`Shared chart detail failed to load (${response.status}).`);
1881
- return response.text();
1882
- })
1883
- .then((html) => {
1884
- if (!cancelled)
1885
- setWidgetHtml(chartWidgetDetailHtml(html, widgetInstanceId));
1886
- })
1887
- .catch((error) => {
1888
- if (!cancelled)
1889
- setWidgetError(error instanceof Error ? error.message : "Shared chart detail failed to load.");
1890
- });
1891
- return () => {
1892
- cancelled = true;
1893
- };
1894
- }, [hostedChartModal, widgetInstanceId, widgetUrl]);
1895
- useEffect(() => {
1896
- let cancelled = false;
1897
- if (hostedChartModal) {
1898
- setSourceQueryText(null);
1899
- return () => {
1900
- cancelled = true;
1901
- };
1902
- }
1903
- const inlineSourceText = queryTextFromSourceQuery(sourceQueryFromSourceSpec(source));
1904
- if (inlineSourceText) {
1905
- setSourceQueryText(inlineSourceText);
1906
- return () => {
1907
- cancelled = true;
1908
- };
1909
- }
1910
- if (!source?.path) {
1911
- setSourceQueryText(null);
1912
- return () => {
1913
- cancelled = true;
1914
- };
1915
- }
1916
- setSourceQueryText(null);
1917
- void fetch(`/api/source-file?path=${encodeURIComponent(source.path)}`, {
1918
- headers: { Accept: "text/plain" }
1919
- })
1920
- .then((response) => {
1921
- if (!response.ok)
1922
- return "";
1923
- return response.text();
1924
- })
1925
- .then((text) => {
1926
- if (!cancelled)
1927
- setSourceQueryText(text.trim() || null);
1928
- })
1929
- .catch(() => {
1930
- if (!cancelled)
1931
- setSourceQueryText(null);
1932
- });
1933
- return () => {
1934
- cancelled = true;
1935
- };
1936
- }, [hostedChartModal, source?.path, source?.query?.query]);
1937
- useEffect(() => {
1938
- if (hostedChartModal || !widgetHtml)
1939
- return;
1940
- scheduleWidgetPayloadPosts();
1941
- }, [hostedChartModal, scheduleWidgetPayloadPosts, widgetHtml]);
1942
- useEffect(() => clearPendingPostTimers, [clearPendingPostTimers]);
1943
- useEffect(() => {
1944
- if (hostedChartModal)
1945
- return undefined;
1946
- function handleWidgetMessage(event) {
1947
- const data = event.data;
1948
- if (!data || typeof data !== "object")
1949
- return;
1950
- if (data.type === "datascience-chart-widget-display-mode" && data.mode === "inline") {
1951
- onClose();
1952
- }
1953
- if (data.type === "datascience-chart-widget-spec-reset" && data.widgetInstanceId === widgetInstanceId) {
1954
- onChartSpecChange(chart.id, null);
1955
- }
1956
- if (data.type === "datascience-chart-widget-spec-change" && data.widgetInstanceId === widgetInstanceId) {
1957
- onChartSpecChange(chart.id, data.visualization_spec);
1958
- }
1959
- if (data.type === "datascience-chart-widget-codex-prompt" && typeof data.prompt === "string") {
1960
- sendCodexPromptToHost(data.prompt, null);
1961
- }
1962
- }
1963
- window.addEventListener("message", handleWidgetMessage);
1964
- return () => window.removeEventListener("message", handleWidgetMessage);
1965
- }, [chart.id, hostedChartModal, onChartSpecChange, onClose, widgetInstanceId]);
1966
- return (<dialog aria-label={`Edit ${chart.title}`} className={`native-modal chart-explore-modal ${hostedChartModal ? "chart-hosted-explore-modal" : ""}`.trim()} onCancel={onClose} onClick={(event) => {
1967
- if (event.target === event.currentTarget) {
1968
- event.currentTarget.close();
1969
- }
1970
- }} onClose={onClose} ref={dialogRef}>
1971
- <section className={`modal-panel chart-explore-panel ${hostedChartModal ? "chart-hosted-explore-panel" : ""}`.trim()}>
1972
- <section aria-label={`Edit ${chart.title}`} className={`chart-detail-page unified-chart-detail-page chart-explore-body ${hostedChartModal ? "chart-hosted-explore-body" : ""}`.trim()}>
1973
- {hostedChartModal ? (<ChartBody accessIssue={accessIssue} chart={chart} filters={filters} isFullscreen selectedFilters={selectedFilters} snapshot={snapshot}/>) : null}
1974
- {!hostedChartModal && !widgetHtml && !widgetError ? <div className="chart-detail-loading">Loading chart detail...</div> : null}
1975
- {widgetError ? <div className="empty-state error-state">{widgetError}</div> : null}
1976
- {!hostedChartModal ? (<iframe className="unified-chart-detail-frame" hidden={!widgetHtml || Boolean(widgetError)} onLoad={scheduleWidgetPayloadPosts} ref={iframeRef} srcDoc={widgetHtml ?? ""} title={`Edit ${chart.title}`}/>) : null}
1977
- </section>
1978
- </section>
1979
- </dialog>);
1980
- }
1981
- function chartWidgetColumnType(column, value) {
1982
- if (column?.type === "date")
1983
- return "date";
1984
- if (column?.format || column?.type === "currency" || column?.type === "number" || column?.type === "percent")
1985
- return "number";
1986
- if (typeof value === "number")
1987
- return "number";
1988
- if (typeof value === "string" && /^\d{4}-\d{2}(?:-\d{2})?(?:$|[T\s])/.test(value))
1989
- return "date";
1990
- return "text";
1991
- }
1992
- function chartWidgetEncodingColumn(chart, role, rows) {
1993
- const field = chartEncodingField(chart, role);
1994
- if (!field)
1995
- return null;
1996
- const encoding = chartEncoding(chart, role);
1997
- return {
1998
- key: field,
1999
- label: chartEncodingLabel(chart, role, field),
2000
- type: encoding.type === "temporal" ? "date" : encoding.type === "quantitative" ? "number" : chartWidgetColumnType(encoding, rows[0]?.[field]),
2001
- unit: encoding.unit
2002
- };
2003
- }
2004
- function chartWidgetSource(source, sourceQuery, queryText, snapshot) {
2005
- return {
2006
- id: source?.id,
2007
- label: source?.label,
2008
- path: source?.path,
2009
- href: source?.href,
2010
- query: {
2011
- engine: sourceQuery?.engine ?? source?.label ?? "report manifest",
2012
- executed_at: sourceQuery?.executed_at ?? snapshot?.generatedAt,
2013
- id: sourceQuery?.id ?? source?.path ?? source?.href,
2014
- url: sourceQuery?.url,
2015
- sql: queryText || "",
2016
- description: sourceQuery?.description,
2017
- language: sourceQuery?.language ?? (source?.path?.toLowerCase().endsWith(".sql") ? "SQL" : undefined),
2018
- tables_used: sourceQuery?.tables_used,
2019
- filters: sourceQuery?.filters,
2020
- metric_definitions: sourceQuery?.metric_definitions
2021
- }
2022
- };
2023
- }
2024
- function chartWidgetPayloadFromEncodings(chart, rows, source, snapshot, sourceQueryText) {
2025
- const xField = chartEncodingField(chart, "x");
2026
- const yField = chartEncodingField(chart, "y");
2027
- const yFields = chartEncodingFields(chart, "y");
2028
- const colorField = chartEncodingField(chart, "color");
2029
- const chartSourceQuery = sourceQueryFromChartSpec(chart);
2030
- const sourceSpecQuery = sourceQueryFromSourceSpec(source);
2031
- const sourceQuery = chartSourceQuery ?? sourceSpecQuery;
2032
- const queryText = queryTextFromSourceQuery(chartSourceQuery) ||
2033
- queryTextFromSourceQuery(sourceSpecQuery) ||
2034
- sourceQueryText?.trim();
2035
- const unit = chart.unit ?? chartEncoding(chart, "y").unit ?? (chart.valueFormat === "currency" ? "USD" : chart.valueFormat === "percent" ? "%" : undefined);
2036
- const settings = chartWidgetSettings(chart);
2037
- if (xField && yField) {
2038
- const columns = [
2039
- chartWidgetEncodingColumn(chart, "x", rows),
2040
- colorField ? chartWidgetEncodingColumn(chart, "color", rows) : null,
2041
- chartWidgetEncodingColumn(chart, "y", rows)
2042
- ].filter(Boolean);
2043
- return {
2044
- ok: true,
2045
- widget_type: "chart",
2046
- title: chart.title,
2047
- subtitle: chart.showDescription ? chart.subtitle : undefined,
2048
- source: chartWidgetSource(source, sourceQuery, queryText, snapshot),
2049
- result_table: {
2050
- columns,
2051
- row_count: rows.length,
2052
- rows: rows.map((row) => Object.fromEntries(columns.map((column) => [column.key, row[column.key]]))),
2053
- truncated: false
2054
- },
2055
- visualization_spec: {
2056
- version: "1",
2057
- intent: chart.intent ?? "custom",
2058
- visualization_type: chart.type,
2059
- encodings: {
2060
- x: { field: xField, type: chartEncoding(chart, "x").type ?? "nominal" },
2061
- y: { aggregate: chartEncoding(chart, "y").aggregate ?? "sum", field: yField, type: "quantitative", unit },
2062
- ...(colorField ? { color: { field: colorField, type: chartEncoding(chart, "color").type ?? "nominal" } } : {})
2063
- },
2064
- presentation: {
2065
- show_controls: true,
2066
- unit,
2067
- view_mode: "both"
2068
- },
2069
- settings
2070
- }
2071
- };
2072
- }
2073
- if (xField && yFields.length) {
2074
- const longRows = rows.flatMap((row) => yFields.map((field) => ({
2075
- [xField]: row[xField],
2076
- series: field,
2077
- value: row[field]
2078
- })));
2079
- return {
2080
- ok: true,
2081
- widget_type: "chart",
2082
- title: chart.title,
2083
- subtitle: chart.showDescription ? chart.subtitle : undefined,
2084
- source: chartWidgetSource(source, sourceQuery, queryText, snapshot),
2085
- result_table: {
2086
- columns: [
2087
- { key: xField, label: chartEncodingLabel(chart, "x", xField), type: chartWidgetColumnType(chartEncoding(chart, "x"), rows[0]?.[xField]) },
2088
- { key: "series", label: "Series", type: "text" },
2089
- { key: "value", label: chartEncodingLabel(chart, "y", "Value"), type: "number", unit }
2090
- ],
2091
- row_count: longRows.length,
2092
- rows: longRows,
2093
- truncated: false
2094
- },
2095
- visualization_spec: {
2096
- version: "1",
2097
- intent: chart.intent ?? "custom",
2098
- visualization_type: chart.type,
2099
- encodings: {
2100
- x: { field: xField, type: chartEncoding(chart, "x").type ?? "nominal" },
2101
- y: { aggregate: "sum", field: "value", type: "quantitative", unit },
2102
- color: { field: "series", type: "nominal" }
2103
- },
2104
- presentation: {
2105
- show_controls: true,
2106
- unit,
2107
- view_mode: "both"
2108
- },
2109
- settings
2110
- }
2111
- };
2112
- }
2113
- return null;
2114
- }
2115
- function chartWidgetSettings(chart) {
2116
- const source = chart?.settings && typeof chart.settings === "object" && !Array.isArray(chart.settings) ? chart.settings : {};
2117
- const type = chart?.type;
2118
- const orientation = source.orientation === "horizontal" || type === "horizontalBar" || type === "horizontalStackedBar" || type === "horizontalStackedBar100"
2119
- ? "horizontal"
2120
- : source.orientation === "vertical"
2121
- ? "vertical"
2122
- : undefined;
2123
- const groupMode = source.groupMode ?? source.group_mode ?? (type === "stackedBar100" || type === "horizontalStackedBar100"
2124
- ? "stacked100"
2125
- : type === "stackedBar" || type === "horizontalStackedBar"
2126
- ? "stacked"
2127
- : undefined);
2128
- return {
2129
- ...(orientation ? { orientation } : {}),
2130
- ...(["grouped", "stacked", "stacked100"].includes(groupMode) ? { group_mode: groupMode } : {})
2131
- };
2132
- }
2133
- function chartWidgetPayload(chart, rows, source, snapshot, sourceQueryText) {
2134
- const encodedPayload = chartHasEncodingSpec(chart) ? chartWidgetPayloadFromEncodings(chart, rows, source, snapshot, sourceQueryText) : null;
2135
- if (encodedPayload)
2136
- return encodedPayload;
2137
- return null;
2138
- }
2139
- function TableContent({ allowColumnResize = true, columnWidths, density, filters, onColumnWidthsChange, selectedFilters, snapshot, table, isFullscreen = false }) {
2140
- const rows = filterRowsForDataset(getRows(snapshot, table.dataset), table.dataset, filters, selectedFilters, table.columns.map((column) => column.field));
2141
- const [page, setPage] = useState(0);
2142
- const [sortState, setSortState] = useState(null);
2143
- const [isColumnResizing, setIsColumnResizing] = useState(false);
2144
- const headerCellRefs = useRef({});
2145
- const tableWrapRef = useRef(null);
2146
- const sortedRows = useMemo(() => {
2147
- if (!sortState)
2148
- return rows;
2149
- return [...rows].sort((a, b) => compareTableValues(a, b, sortState.field, sortState.direction));
2150
- }, [rows, sortState]);
2151
- const pageSize = isFullscreen ? Math.max(1, sortedRows.length) : TABLE_CARD_PAGE_SIZE;
2152
- const totalPages = Math.max(1, Math.ceil(sortedRows.length / pageSize));
2153
- const currentPage = Math.min(page, totalPages - 1);
2154
- const shouldShowPagination = rows.length > 0 && !isFullscreen && totalPages > 1;
2155
- const shouldShowCount = rows.length > 0 && shouldShowPagination;
2156
- const shouldShowFooter = shouldShowCount || shouldShowPagination;
2157
- const resultCountLabel = `${sortedRows.length.toLocaleString()} ${sortedRows.length === 1 ? "result" : "results"}`;
2158
- const visibleRows = isFullscreen
2159
- ? sortedRows
2160
- : sortedRows.slice(currentPage * pageSize, currentPage * pageSize + pageSize);
2161
- const estimatedColumnWidths = useMemo(() => estimateTableColumnWidths(table, rows, density), [density, rows, table]);
2162
- const activeColumnWidths = useMemo(() => {
2163
- return Object.fromEntries(table.columns.map((column) => [
2164
- column.field,
2165
- clamp(Math.round(columnWidths[column.field] ?? estimatedColumnWidths[column.field] ?? TABLE_COLUMN_DEFAULT_WIDTH), TABLE_COLUMN_MIN_WIDTH, TABLE_COLUMN_MAX_WIDTH)
2166
- ]));
2167
- }, [columnWidths, estimatedColumnWidths, table.columns]);
2168
- const tablePixelWidth = table.columns.reduce((total, column) => total + activeColumnWidths[column.field], 0);
2169
- const tableStyle = {
2170
- minWidth: `${tablePixelWidth}px`,
2171
- tableLayout: "fixed",
2172
- width: `${tablePixelWidth}px`
2173
- };
2174
- useEffect(() => {
2175
- setPage(0);
2176
- }, [isFullscreen, rows.length, sortState]);
2177
- useLayoutEffect(() => {
2178
- if (tableWrapRef.current) {
2179
- tableWrapRef.current.scrollLeft = 0;
2180
- }
2181
- }, [isFullscreen, rows.length, table.id]);
2182
- function toggleSort(field) {
2183
- setSortState((current) => {
2184
- if (current?.field !== field)
2185
- return { direction: "asc", field };
2186
- return { direction: current.direction === "asc" ? "desc" : "asc", field };
2187
- });
2188
- }
2189
- function measuredColumnWidths() {
2190
- return Object.fromEntries(table.columns.map((column) => {
2191
- const measuredWidth = headerCellRefs.current[column.field]?.getBoundingClientRect().width;
2192
- const width = measuredWidth ?? activeColumnWidths[column.field] ?? TABLE_COLUMN_DEFAULT_WIDTH;
2193
- return [
2194
- column.field,
2195
- clamp(Math.round(width), TABLE_COLUMN_MIN_WIDTH, TABLE_COLUMN_MAX_WIDTH)
2196
- ];
2197
- }));
2198
- }
2199
- function resizeColumnBy(field, delta, shouldPersist) {
2200
- const baseWidths = { ...activeColumnWidths, ...measuredColumnWidths(), ...columnWidths };
2201
- const currentWidth = baseWidths[field] ?? TABLE_COLUMN_DEFAULT_WIDTH;
2202
- onColumnWidthsChange(table.id, {
2203
- ...baseWidths,
2204
- [field]: clamp(Math.round(currentWidth + delta), TABLE_COLUMN_MIN_WIDTH, TABLE_COLUMN_MAX_WIDTH)
2205
- }, { persist: shouldPersist });
2206
- }
2207
- function startColumnResize(event, field) {
2208
- event.preventDefault();
2209
- event.stopPropagation();
2210
- const baseWidths = { ...activeColumnWidths, ...measuredColumnWidths(), ...columnWidths };
2211
- const startWidth = baseWidths[field] ?? TABLE_COLUMN_DEFAULT_WIDTH;
2212
- const startX = event.clientX;
2213
- let latestWidths = baseWidths;
2214
- onColumnWidthsChange(table.id, baseWidths, { persist: false });
2215
- setIsColumnResizing(true);
2216
- document.body.style.cursor = "col-resize";
2217
- document.body.style.userSelect = "none";
2218
- function handlePointerMove(moveEvent) {
2219
- const nextWidth = clamp(Math.round(startWidth + moveEvent.clientX - startX), TABLE_COLUMN_MIN_WIDTH, TABLE_COLUMN_MAX_WIDTH);
2220
- latestWidths = { ...baseWidths, [field]: nextWidth };
2221
- onColumnWidthsChange(table.id, latestWidths, { persist: false });
2222
- }
2223
- function finishResize() {
2224
- window.removeEventListener("pointermove", handlePointerMove);
2225
- window.removeEventListener("pointerup", finishResize);
2226
- window.removeEventListener("pointercancel", finishResize);
2227
- document.body.style.cursor = "";
2228
- document.body.style.userSelect = "";
2229
- setIsColumnResizing(false);
2230
- onColumnWidthsChange(table.id, latestWidths, { persist: true });
2231
- }
2232
- window.addEventListener("pointermove", handlePointerMove);
2233
- window.addEventListener("pointerup", finishResize);
2234
- window.addEventListener("pointercancel", finishResize);
2235
- }
2236
- function handleResizeHandleKeyDown(event, field) {
2237
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
2238
- return;
2239
- event.preventDefault();
2240
- event.stopPropagation();
2241
- resizeColumnBy(field, event.key === "ArrowRight" ? TABLE_COLUMN_KEYBOARD_STEP : -TABLE_COLUMN_KEYBOARD_STEP, true);
2242
- }
2243
- return (<>
2244
- {rows.length ? (<div className={`table-wrap table-density-${density} ${isFullscreen ? "fullscreen" : ""}`} ref={tableWrapRef}>
2245
- <div className="table-scroll-content">
2246
- <table className={`data-table data-table-${density} data-table-resizable ${isColumnResizing ? "is-column-resizing" : ""}`} style={tableStyle}>
2247
- <colgroup>
2248
- {table.columns.map((column) => (<col key={column.field} style={{ width: `${activeColumnWidths[column.field]}px` }}/>))}
2249
- </colgroup>
2250
- <thead>
2251
- <tr>
2252
- {table.columns.map((column) => {
2253
- const isSorted = sortState?.field === column.field;
2254
- const format = tableColumnFormat(column);
2255
- const isNumericColumn = isNumericTableColumn(column, rows);
2256
- const isCenteredColumn = column.align === "center";
2257
- const SortIcon = isSorted ? (sortState.direction === "asc" ? ArrowUp : ArrowDown) : null;
2258
- return (<th aria-sort={isSorted
2259
- ? sortState.direction === "asc"
2260
- ? "ascending"
2261
- : "descending"
2262
- : "none"} key={column.field} ref={(element) => {
2263
- headerCellRefs.current[column.field] = element;
2264
- }} className={isNumericColumn ? "table-header-number" : isCenteredColumn ? "center" : undefined} style={{ width: `${activeColumnWidths[column.field]}px` }}>
2265
- <button className="table-sort-button" onClick={() => toggleSort(column.field)} type="button">
2266
- <span>{column.label}</span>
2267
- {SortIcon ? <SortIcon aria-hidden="true" size={14} strokeWidth={2}/> : null}
2268
- </button>
2269
- {allowColumnResize ? (<button aria-label={`Resize ${column.label} column`} className="table-column-resize-handle" onKeyDown={(event) => handleResizeHandleKeyDown(event, column.field)} onPointerDown={(event) => startColumnResize(event, column.field)} title={`Resize ${column.label} column`} type="button"/>) : null}
2270
- </th>);
2271
- })}
2272
- </tr>
2273
- </thead>
2274
- <tbody>
2275
- {visibleRows.map((row, rowIndex) => (<tr key={`${currentPage}-${rowIndex}`}>
2276
- {table.columns.map((column) => {
2277
- const value = row[column.field];
2278
- const format = tableColumnFormat(column);
2279
- const isNumericColumn = isNumericTableColumn(column, rows);
2280
- const isCenteredColumn = column.align === "center";
2281
- const className = [
2282
- isNumericColumn ? "table-cell-number" : "",
2283
- isCenteredColumn ? "center" : "",
2284
- tableCellMovementClass(column, value)
2285
- ].filter(Boolean).join(" ") || undefined;
2286
- return (<td className={className} key={column.field}>
2287
- {formatTableCellValue(column, value)}
2288
- </td>);
2289
- })}
2290
- </tr>))}
2291
- </tbody>
2292
- </table>
2293
- </div>
2294
- </div>) : (<div className="empty-state">No rows match the selected filters.</div>)}
2295
- {shouldShowFooter ? (<div className="table-pagination">
2296
- {shouldShowCount ? <span className="table-result-count">{resultCountLabel}</span> : null}
2297
- {shouldShowPagination ? (<div className="table-page-control">
2298
- <span>
2299
- Page {currentPage + 1} of {totalPages}
2300
- </span>
2301
- <div className="table-page-buttons">
2302
- <button aria-label="Previous page" className="table-arrow-button" disabled={currentPage === 0} onClick={() => setPage((nextPage) => Math.max(0, nextPage - 1))} type="button">
2303
- <ChevronLeft aria-hidden="true" size={16} strokeWidth={2}/>
2304
- </button>
2305
- <button aria-label="Next page" className="table-arrow-button" disabled={currentPage >= totalPages - 1} onClick={() => setPage((nextPage) => Math.min(totalPages - 1, nextPage + 1))} type="button">
2306
- <ChevronRight aria-hidden="true" size={16} strokeWidth={2}/>
2307
- </button>
2308
- </div>
2309
- </div>) : null}
2310
- </div>) : null}
2311
- </>);
2312
- }
2313
- function DataTable({ columnWidths, filters, isEditMode, isMenuOpen, layout, manifest, onColumnWidthsChange, onDeleteBlock, onMenuOpenChange, onModalOpen, onRequestEditMode, onTextChange, selectedFilters, snapshot, table, textOverride }) {
2314
- const tableDensity = table.density ?? (manifest?.surface === "report" ? "spacious" : "dense");
2315
- const displayHeaderMarkdown = textOverride?.headerMarkdown ?? composeTableHeaderMarkdown(table);
2316
- const displayTitle = markdownFirstLine(displayHeaderMarkdown, table.title);
2317
- const { closeMenu, fixedMenuStyle, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isMenuOpen, onMenuOpenChange);
2318
- const menuItem = (label, icon, onClick, tone = "default") => (<button className={`viz-card-menu-item ${tone === "danger" ? "viz-card-menu-item-danger" : ""}`.trim()} key={label} onClick={() => {
2319
- onClick();
2320
- closeMenu();
2321
- }} role="menuitem" type="button">
2322
- <span aria-hidden="true" className="viz-card-menu-icon">
2323
- {icon}
2324
- </span>
2325
- <span>{label}</span>
2326
- </button>);
2327
- return (<section className={`panel table-panel table-card ${layout === "half" ? "" : "layout-full"}`}>
2328
- <PanelHeader action={<div className="viz-card-actions" data-image-export-exclude="true" ref={menuRef}>
2329
- <button aria-expanded={isMenuOpen} aria-label={`Open options for ${table.title}`} className="viz-card-menu-button viz-card__no-drag" onClick={(event) => {
2330
- event.stopPropagation();
2331
- toggleMenu();
2332
- }} onKeyDown={handleMenuButtonKeyDown} ref={menuButtonRef} type="button">
2333
- <Ellipsis aria-hidden="true" size={18} strokeWidth={2}/>
2334
- </button>
2335
- {shouldRenderMenu ? (<div className={`viz-card-menu menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu" style={fixedMenuStyle}>
2336
- {menuItem("View data source", <Database size={18} strokeWidth={2}/>, () => onModalOpen({ kind: "source", table }))}
2337
- {menuItem("View fullscreen", <Expand size={18} strokeWidth={2}/>, () => onModalOpen({ kind: "fullscreen", table }))}
2338
- {onDeleteBlock ? menuItem("Delete", <Trash2 size={18} strokeWidth={2}/>, onDeleteBlock, "danger") : null}
2339
- </div>) : null}
2340
- </div>} subtitle={table.subtitle} title={displayTitle} titleRowClassName="table-card__drag-handle">
2341
- <RichMarkdown ariaLabel={`Edit markdown header for ${table.title}`} className="editable-cell-header" isEditMode={isEditMode} markdown={displayHeaderMarkdown} onMarkdownChange={(nextMarkdown) => onTextChange(table.id, { headerMarkdown: nextMarkdown })} onRequestEditMode={onRequestEditMode} placeholder={composeTableHeaderMarkdown(table)} variant="cellHeader"/>
2342
- </PanelHeader>
2343
- <TableContent columnWidths={columnWidths} density={tableDensity} filters={filters} onColumnWidthsChange={onColumnWidthsChange} selectedFilters={selectedFilters} snapshot={snapshot} table={table}/>
2344
- </section>);
2345
- }
2346
- function TableModalDialog({ activeFilters, columnWidths, filters, kind, manifest, onColumnWidthsChange, onClose, selectedFilters, snapshot, table }) {
2347
- const dialogRef = useRef(null);
2348
- const source = sourceForTable(table, manifest?.sources ?? []);
2349
- const title = kind === "fullscreen" ? table.title : "Data source";
2350
- const sourceQuery = sourceQueryFromSourceSpec(source);
2351
- const previewRows = filterRowsForDataset(getRows(snapshot, table.dataset), table.dataset, filters, selectedFilters, table.columns.map((column) => column.field));
2352
- const buildDetails = sourceBuildDetails({
2353
- activeFilters,
2354
- columns: table.columns,
2355
- dataset: table.dataset,
2356
- source,
2357
- sourceQuery,
2358
- snapshot
2359
- });
2360
- useModalScrollLock(kind === "source");
2361
- useEffect(() => {
2362
- const dialog = dialogRef.current;
2363
- if (dialog && !dialog.open) {
2364
- dialog.showModal();
2365
- }
2366
- }, []);
2367
- return (<dialog aria-labelledby="table-modal-title" className={`native-modal ${kind === "source" ? "source-modal" : ""}`.trim()} onCancel={onClose} onClick={(event) => {
2368
- if (event.target === event.currentTarget) {
2369
- event.currentTarget.close();
2370
- }
2371
- }} onClose={onClose} ref={dialogRef}>
2372
- <section className={`modal-panel ${kind === "source" ? "source-modal-panel" : ""}`.trim()}>
2373
- <div className="modal-header">
2374
- <div>
2375
- <h2 id="table-modal-title">{title}</h2>
2376
- {kind === "fullscreen" && source?.label ? <p>{source.label}</p> : null}
2377
- </div>
2378
- <button aria-label={`Close ${kind === "source" ? "data source" : "fullscreen table"}`} className="modal-close-button" onClick={() => dialogRef.current?.close()} type="button">
2379
- <X aria-hidden="true" size={20} strokeWidth={2}/>
2380
- </button>
2381
- </div>
2382
- {kind === "fullscreen" ? (<TableContent columnWidths={columnWidths} density={table.density ?? (manifest?.surface === "report" ? "spacious" : "dense")} filters={filters} isFullscreen onColumnWidthsChange={onColumnWidthsChange} selectedFilters={selectedFilters} snapshot={snapshot} table={table}/>) : (<DataSourceDetails details={buildDetails} source={source} sourceQuery={sourceQuery}>
2383
- <SourceDataTable columns={table.columns} dataset={table.dataset} density={table.density ?? (manifest?.surface === "report" ? "spacious" : "dense")} rows={previewRows}/>
2384
- </DataSourceDetails>)}
2385
- </section>
2386
- </dialog>);
2387
- }
2388
- function ReportTextBlock({ block, isEditMode, isMenuOpen, onCopyResult, onDeleteBlock, onMenuOpenChange, onRequestEditMode, onTextChange, textOverride }) {
2389
- const cardRef = useRef(null);
2390
- const toneClass = `report-block-${block.type}`;
2391
- const blockMarkdown = composeBlockMarkdown(block, textOverride);
2392
- const { closeMenu, fixedMenuStyle, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isMenuOpen, onMenuOpenChange);
2393
- const { getPreparedImageBlob, preparedImageExportStatus, prepareImageExport, resetPreparedImageExport } = usePreparedImageExport(cardRef);
2394
- async function handleCopyMarkdown() {
2395
- try {
2396
- await copyTextToClipboard(blockMarkdown.trim());
2397
- onCopyResult("Copied markdown.");
2398
- }
2399
- catch (error) {
2400
- onCopyResult(error instanceof Error ? error.message : "Failed to copy markdown.", true);
2401
- }
2402
- }
2403
- function prepareImageExportQuietly(force = false) {
2404
- if (!shouldOfferImageClipboardCopy())
2405
- return;
2406
- const prepared = prepareImageExport({ force });
2407
- if (prepared)
2408
- void prepared.promise.catch(() => undefined);
2409
- }
2410
- async function handleCopyAsImage() {
2411
- if (!cardRef.current)
2412
- return;
2413
- try {
2414
- const copyResult = await copyElementAsImage(cardRef.current, getPreparedImageBlob());
2415
- resetPreparedImageExport();
2416
- onCopyResult(imageCopySuccessMessage("Copied text block as image.", copyResult));
2417
- }
2418
- catch (error) {
2419
- onCopyResult(error instanceof Error ? error.message : "Failed to copy image.", true);
2420
- }
2421
- }
2422
- const menuItem = (label, icon, onClick, tone = "default", onPrepare, disabled = false) => (<button className={`viz-card-menu-item ${tone === "danger" ? "viz-card-menu-item-danger" : ""}`.trim()} disabled={disabled} key={label} onFocus={onPrepare} onClick={() => {
2423
- void onClick();
2424
- closeMenu();
2425
- }} onPointerEnter={onPrepare} role="menuitem" type="button">
2426
- <span aria-hidden="true" className="viz-card-menu-icon">
2427
- {icon}
2428
- </span>
2429
- <span>{label}</span>
2430
- </button>);
2431
- return (<section className={`panel report-block report-markdown-block ${toneClass}`} id={block.id} ref={cardRef}>
2432
- <div className="viz-card-actions" data-image-export-exclude="true" ref={menuRef}>
2433
- <button aria-expanded={isMenuOpen} aria-label={`Open options for ${markdownFirstLine(blockMarkdown, "Text block")}`} className="viz-card-menu-button viz-card__no-drag" onClick={(event) => {
2434
- event.stopPropagation();
2435
- const nextIsMenuOpen = toggleMenu();
2436
- if (nextIsMenuOpen) {
2437
- prepareImageExportQuietly(true);
2438
- }
2439
- else {
2440
- resetPreparedImageExport();
2441
- }
2442
- }} onFocus={() => prepareImageExportQuietly()} onKeyDown={handleMenuButtonKeyDown} onPointerEnter={() => prepareImageExportQuietly()} ref={menuButtonRef} type="button">
2443
- <Ellipsis aria-hidden="true" size={18} strokeWidth={2}/>
2444
- </button>
2445
- {shouldRenderMenu ? (<div className={`viz-card-menu viz-card__no-drag menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu" style={fixedMenuStyle}>
2446
- {menuItem("Copy markdown", <Copy size={18} strokeWidth={2}/>, handleCopyMarkdown)}
2447
- {shouldOfferImageClipboardCopy()
2448
- ? menuItem("Copy as image", <Camera size={18} strokeWidth={2}/>, handleCopyAsImage, "default", prepareImageExportQuietly, preparedImageExportStatus === "pending")
2449
- : null}
2450
- {menuItem("Delete", <Trash2 size={18} strokeWidth={2}/>, onDeleteBlock, "danger")}
2451
- </div>) : null}
2452
- </div>
2453
- <div className="report-block-body markdown-body">
2454
- <RichMarkdown ariaLabel={`Edit markdown for ${block.type}`} className="report-markdown-editor" isEditMode={isEditMode} markdown={blockMarkdown} minRows={Math.max(2, Math.min(6, blockMarkdown.split(/\r?\n/).length))} onMarkdownChange={(nextMarkdown) => onTextChange(block.id, { bodyMarkdown: nextMarkdown })} onRequestEditMode={onRequestEditMode} placeholder={composeBlockMarkdown(block)} variant="reportBlock"/>
2455
- </div>
2456
- </section>);
2457
- }
2458
- function ReportHtmlBlock({ block, htmlOverride, isEditMode, isMenuOpen, onHtmlChange, onCopyResult, onDeleteBlock, onMenuOpenChange }) {
2459
- const html = composeBlockHtml(block, htmlOverride);
2460
- const title = block.id || "HTML block";
2461
- const { closeMenu, fixedMenuStyle, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isMenuOpen, onMenuOpenChange);
2462
- const frameRef = useRef(null);
2463
- const resizeObserverRef = useRef(null);
2464
- const [frameHeight, setFrameHeight] = useState(0);
2465
- const updateFrameHeight = useCallback(() => {
2466
- const nextHeight = measureHtmlFrameHeight(frameRef.current);
2467
- if (nextHeight > 0)
2468
- setFrameHeight(nextHeight);
2469
- }, []);
2470
- const handleFrameLoad = useCallback(() => {
2471
- resizeObserverRef.current?.disconnect();
2472
- const frameDocument = frameRef.current?.contentDocument;
2473
- if (frameDocument && typeof ResizeObserver !== "undefined") {
2474
- const observer = new ResizeObserver(updateFrameHeight);
2475
- if (frameDocument.documentElement)
2476
- observer.observe(frameDocument.documentElement);
2477
- if (frameDocument.body)
2478
- observer.observe(frameDocument.body);
2479
- resizeObserverRef.current = observer;
2480
- }
2481
- updateFrameHeight();
2482
- }, [updateFrameHeight]);
2483
- useEffect(() => {
2484
- setFrameHeight(0);
2485
- }, [html]);
2486
- useEffect(() => {
2487
- return () => resizeObserverRef.current?.disconnect();
2488
- }, []);
2489
- async function handleCopyHtml() {
2490
- try {
2491
- await copyTextToClipboard(html);
2492
- onCopyResult("Copied HTML.");
2493
- }
2494
- catch (error) {
2495
- onCopyResult(error instanceof Error ? error.message : "Failed to copy HTML.", true);
2496
- }
2497
- }
2498
- const menuItem = (label, icon, onClick, tone = "default") => (<button className={`viz-card-menu-item ${tone === "danger" ? "viz-card-menu-item-danger" : ""}`.trim()} key={label} onClick={() => {
2499
- void onClick();
2500
- closeMenu();
2501
- }} role="menuitem" type="button">
2502
- <span aria-hidden="true" className="viz-card-menu-icon">
2503
- {icon}
2504
- </span>
2505
- <span>{label}</span>
2506
- </button>);
2507
- return (<section className="report-block report-html-block" id={block.id}>
2508
- <div className="viz-card-actions" data-image-export-exclude="true" ref={menuRef}>
2509
- <button aria-expanded={isMenuOpen} aria-label={`Open options for ${title}`} className="viz-card-menu-button viz-card__no-drag" onClick={(event) => {
2510
- event.stopPropagation();
2511
- toggleMenu();
2512
- }} onKeyDown={handleMenuButtonKeyDown} ref={menuButtonRef} type="button">
2513
- <Ellipsis aria-hidden="true" size={18} strokeWidth={2}/>
2514
- </button>
2515
- {shouldRenderMenu ? (<div className={`viz-card-menu viz-card__no-drag menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu" style={fixedMenuStyle}>
2516
- {menuItem("Copy HTML", <Copy size={18} strokeWidth={2}/>, handleCopyHtml)}
2517
- {menuItem("Delete", <Trash2 size={18} strokeWidth={2}/>, onDeleteBlock, "danger")}
2518
- </div>) : null}
2519
- </div>
2520
- {isEditMode ? (<textarea aria-label={`Edit HTML for ${title}`} className="report-html-editor" onChange={(event) => onHtmlChange(block.id, { html: event.target.value })} spellCheck={false} value={html}/>) : (<iframe className="report-html-frame" onLoad={handleFrameLoad} ref={frameRef} sandbox="allow-same-origin" srcDoc={sandboxedReportHtml(html)} style={frameHeight ? { height: `${frameHeight}px` } : undefined} title={title}/>)}
2521
- </section>);
2522
- }
2523
- function ReportMetricStripBlock({ cards, filters, id, isMenuOpen, onCopyResult, onDeleteBlock, onMenuOpenChange, selectedFilters, snapshot }) {
2524
- const cardRef = useRef(null);
2525
- const { closeMenu, fixedMenuStyle, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isMenuOpen, onMenuOpenChange);
2526
- const { getPreparedImageBlob, preparedImageExportStatus, prepareImageExport, resetPreparedImageExport } = usePreparedImageExport(cardRef);
2527
- function prepareImageExportQuietly(force = false) {
2528
- if (!shouldOfferImageClipboardCopy())
2529
- return;
2530
- const prepared = prepareImageExport({ force });
2531
- if (prepared)
2532
- void prepared.promise.catch(() => undefined);
2533
- }
2534
- async function handleCopyAsImage() {
2535
- if (!cardRef.current)
2536
- return;
2537
- try {
2538
- const copyResult = await copyElementAsImage(cardRef.current, getPreparedImageBlob());
2539
- resetPreparedImageExport();
2540
- onCopyResult(imageCopySuccessMessage("Copied key metrics as image.", copyResult));
2541
- }
2542
- catch (error) {
2543
- onCopyResult(error instanceof Error ? error.message : "Failed to copy image.", true);
2544
- }
2545
- }
2546
- const menuItem = (label, icon, onClick, tone = "default", onPrepare, disabled = false) => (<button className={`viz-card-menu-item ${tone === "danger" ? "viz-card-menu-item-danger" : ""}`.trim()} disabled={disabled} key={label} onFocus={onPrepare} onClick={() => {
2547
- void onClick();
2548
- closeMenu();
2549
- }} onPointerEnter={onPrepare} role="menuitem" type="button">
2550
- <span aria-hidden="true" className="viz-card-menu-icon">
2551
- {icon}
2552
- </span>
2553
- <span>{label}</span>
2554
- </button>);
2555
- return (<section className="report-metric-strip-block" id={id} ref={cardRef}>
2556
- <div className="viz-card-actions" data-image-export-exclude="true" ref={menuRef}>
2557
- <button aria-expanded={isMenuOpen} aria-label="Open options for key metrics" className="viz-card-menu-button viz-card__no-drag" onClick={(event) => {
2558
- event.stopPropagation();
2559
- const nextIsMenuOpen = toggleMenu();
2560
- if (nextIsMenuOpen) {
2561
- prepareImageExportQuietly(true);
2562
- }
2563
- else {
2564
- resetPreparedImageExport();
2565
- }
2566
- }} onFocus={() => prepareImageExportQuietly()} onKeyDown={handleMenuButtonKeyDown} onPointerEnter={() => prepareImageExportQuietly()} ref={menuButtonRef} type="button">
2567
- <Ellipsis aria-hidden="true" size={18} strokeWidth={2}/>
2568
- </button>
2569
- {shouldRenderMenu ? (<div className={`viz-card-menu viz-card__no-drag menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu" style={fixedMenuStyle}>
2570
- {shouldOfferImageClipboardCopy()
2571
- ? menuItem("Copy as image", <Camera size={18} strokeWidth={2}/>, handleCopyAsImage, "default", prepareImageExportQuietly, preparedImageExportStatus === "pending")
2572
- : null}
2573
- {menuItem("Delete", <Trash2 size={18} strokeWidth={2}/>, onDeleteBlock, "danger")}
2574
- </div>) : null}
2575
- </div>
2576
- <KpiStrip cards={cards} filters={filters} selectedFilters={selectedFilters} snapshot={snapshot}/>
2577
- </section>);
2578
- }
2579
- function ReportBlockCard({ accessIssues, block, blockTextOverride, chart, chartSpecOverride, chartTextOverride, chartTypeOverride, columnWidths, filters, isBlockMenuOpen, isChartMenuOpen, isEditMode, isTableMenuOpen, layout, manifest, onBlockMenuOpenChange, onBlockTextChange, onChartTypeChange, onChartMenuOpenChange, onColumnWidthsChange, onDeleteBlock, onChartModalOpen, onCopyResult, onRequestEditMode, onTableMenuOpenChange, onTableModalOpen, onTableTextChange, onTextChange, selectedFilters, snapshot, table, tableTextOverride }) {
2580
- if (block.type === "html") {
2581
- return (<ReportHtmlBlock block={block} htmlOverride={blockTextOverride} isEditMode={isEditMode} isMenuOpen={isBlockMenuOpen} onHtmlChange={onBlockTextChange} onCopyResult={onCopyResult} onDeleteBlock={onDeleteBlock} onMenuOpenChange={onBlockMenuOpenChange}/>);
2582
- }
2583
- if (block.type === "chart" && chart) {
2584
- const overriddenChart = applyChartSpecOverride(chart, chartSpecOverride);
2585
- const chartRows = filterRowsForDataset(getRows(snapshot, overriddenChart.dataset), overriddenChart.dataset, filters, selectedFilters, chartUsedFields(overriddenChart));
2586
- const chartTypeOptions = compatibleChartTypesForArtifactCard(overriddenChart, chartRows);
2587
- const requestedType = chartTypeOverride ?? overriddenChart.type;
2588
- const activeType = chartTypeOptions.some((option) => option.type === requestedType)
2589
- ? requestedType
2590
- : overriddenChart.type;
2591
- const displayChart = withChartType(overriddenChart, activeType);
2592
- const accessIssue = accessIssueForChart(displayChart, accessIssues);
2593
- return (<VizCard accessIssue={accessIssue} chart={displayChart} chartTypeOptions={chartTypeOptions} isEditMode={isEditMode} isMenuOpen={isChartMenuOpen} layout={layout} onChartTypeChange={onChartTypeChange} onCopyResult={onCopyResult} onDeleteBlock={onDeleteBlock} onMenuOpenChange={onChartMenuOpenChange} onModalOpen={onChartModalOpen} onRequestEditMode={onRequestEditMode} onTextChange={onTextChange} textOverride={chartTextOverride}>
2594
- <ChartBody accessIssue={accessIssue} chart={displayChart} filters={filters} layout={layout} selectedFilters={selectedFilters} snapshot={snapshot}/>
2595
- </VizCard>);
2596
- }
2597
- if (block.type === "table" && table) {
2598
- return (<DataTable columnWidths={columnWidths} filters={filters} isEditMode={isEditMode} isMenuOpen={isTableMenuOpen} layout={layout} manifest={manifest} onColumnWidthsChange={onColumnWidthsChange} onDeleteBlock={onDeleteBlock} onMenuOpenChange={onTableMenuOpenChange} onModalOpen={onTableModalOpen} onRequestEditMode={onRequestEditMode} onTextChange={onTableTextChange} selectedFilters={selectedFilters} snapshot={snapshot} table={table} textOverride={tableTextOverride}/>);
2599
- }
2600
- return (<ReportTextBlock block={block} isEditMode={isEditMode} isMenuOpen={isBlockMenuOpen} onCopyResult={onCopyResult} onDeleteBlock={onDeleteBlock} onMenuOpenChange={onBlockMenuOpenChange} onRequestEditMode={onRequestEditMode} onTextChange={onBlockTextChange} textOverride={blockTextOverride}/>);
2601
- }
2602
- const EXPORT_TARGET_LABELS = {
2603
- site: "Publish to Sites",
2604
- html: "Create HTML file",
2605
- pdf: "Create PDF",
2606
- document: "Create Google Doc",
2607
- slides: "Create Google Slides"
2608
- };
2609
- const EXPORT_TARGET_ICONS = {
2610
- site: <Globe aria-hidden="true" size={16} strokeWidth={2}/>,
2611
- html: <FileText aria-hidden="true" size={16} strokeWidth={2}/>,
2612
- pdf: <FileDown aria-hidden="true" size={16} strokeWidth={2}/>,
2613
- document: <FileText aria-hidden="true" size={16} strokeWidth={2}/>,
2614
- slides: <Presentation aria-hidden="true" size={16} strokeWidth={2}/>
2615
- };
2616
- function packageControls(packageInfo) {
2617
- return packageInfo?.controls && typeof packageInfo.controls === "object" ? packageInfo.controls : {};
2618
- }
2619
- function artifactCapabilities(packageInfo) {
2620
- const controls = packageControls(packageInfo);
2621
- const hostedReadOnly = packageInfo?.hostedReadOnly === true || packageInfo?.deliveryMode === "site_creator";
2622
- return {
2623
- canEdit: !hostedReadOnly && controls.edit !== false,
2624
- canPublishHostedLink: !hostedReadOnly && controls.exportHostedLink !== false && controls.hostedLink !== false,
2625
- canExportHtml: controls.html !== false,
2626
- canExportPdf: controls.pdf !== false,
2627
- canExportDocument: controls.document !== false,
2628
- canExportSlides: controls.slides !== false
2629
- };
2630
- }
2631
- function exportTargetsForCapabilities(capabilities) {
2632
- return Object.keys(EXPORT_TARGET_LABELS).filter((target) => {
2633
- if (target === "site")
2634
- return capabilities.canPublishHostedLink;
2635
- if (target === "html")
2636
- return capabilities.canExportHtml;
2637
- if (target === "pdf")
2638
- return capabilities.canExportPdf;
2639
- if (target === "document")
2640
- return capabilities.canExportDocument;
2641
- if (target === "slides")
2642
- return capabilities.canExportSlides;
2643
- return false;
2644
- });
2645
- }
2646
- function appSurfaceLabel(manifest) {
2647
- return manifest?.surface === "report" ? "report" : "dashboard";
2648
- }
2649
- function compactArtifactUrl() {
2650
- try {
2651
- const url = new URL(window.location.href);
2652
- for (const name of ["locale", "deviceType", "unsafeSkipTargetOriginCheck"]) {
2653
- url.searchParams.delete(name);
2654
- }
2655
- return url.toString();
2656
- }
2657
- catch {
2658
- return window.location.href;
2659
- }
2660
- }
2661
- function usefulContextPath(value) {
2662
- const trimmedValue = value?.trim();
2663
- return trimmedValue && trimmedValue !== "tool payload" && !trimmedValue.startsWith("mcp://")
2664
- ? trimmedValue
2665
- : null;
2666
- }
2667
- function promptContext(manifest, snapshot, packageInfo) {
2668
- const packagePath = usefulContextPath(packageInfo?.root);
2669
- const manifestPath = usefulContextPath(packageInfo?.manifestPath);
2670
- const snapshotPath = usefulContextPath(packageInfo?.snapshotPath);
2671
- return [
2672
- `Artifact URL: ${compactArtifactUrl()}`,
2673
- packagePath ? `Package path: ${packagePath}` : null,
2674
- manifestPath ? `Manifest file: ${manifestPath}` : null,
2675
- snapshotPath ? `Snapshot file: ${snapshotPath}` : null,
2676
- `Generated at: ${snapshot?.generatedAt ?? manifest?.generatedAt ?? "unknown"}`
2677
- ]
2678
- .filter((line) => Boolean(line))
2679
- .join("\n");
2680
- }
2681
- function refreshPrompt(manifest, snapshot, packageInfo) {
2682
- const surface = appSurfaceLabel(manifest);
2683
- return `Refresh this ${surface} app using the artifact manifest's declared sources and the latest available time frame.
2684
-
2685
- Inspect the artifact manifest for source details before rerunning. Update the snapshot data and generatedAt metadata, rebuild any static export, and verify the refreshed ${surface} in the in-app browser. Keep the current layout and narrative structure unless the latest data makes a statement incorrect.
2686
-
2687
- ${promptContext(manifest, snapshot, packageInfo)}`;
2688
- }
2689
- function exportPrompt(target, manifest, snapshot, packageInfo) {
2690
- const surface = appSurfaceLabel(manifest);
2691
- const context = promptContext(manifest, snapshot, packageInfo);
2692
- if (target === "site") {
2693
- return `Publish this ${surface} app as a hosted Site Creator link.
2694
-
2695
- Use the current Data Analytics artifact as the source of truth. Create or reuse a Site Creator project for this ${surface}, materialize a Cloudflare Worker-compatible app that serves the current manifest, bounded snapshot, package metadata, and inline-safe source text through /api/manifest, /api/snapshot, /api/package, and /api/source-file, then deploy it through Site Creator. Preserve the rendered layout, charts, tables, source details, and narrative. Use the access scope I explicitly selected. If no access scope has been selected, keep the package local and ask for that publishing decision before deployment. Report the production URL and actual access mode only after successful deployment.
2696
-
2697
- If the local package files are unavailable from the context below, stop and ask for the report package or validated artifact payload rather than publishing an empty or stale report.
2698
-
2699
- ${context}`;
2700
- }
2701
- if (target === "html") {
2702
- return `Export this ${surface} app as a static, portable HTML artifact. Include the content title, narrative, charts, tables, and source details. Omit the interactive top bar and app-only controls from the exported artifact. Verify the HTML before delivery.
2703
-
2704
- ${context}`;
2705
- }
2706
- if (target === "document") {
2707
- return `Use the existing data-analytics:report-to-google-doc workflow to export this ${surface} app as a polished native document artifact. Use the current app content and static HTML export when available, preserve headings, charts, tables, source details, and the executive narrative, then verify the created document.
2708
-
2709
- ${context}`;
2710
- }
2711
- if (target === "slides") {
2712
- return `Use the existing data-analytics:report-to-google-slides workflow to export this ${surface} app as an executive-ready native slides artifact. Preserve the core claims, charts, tables, caveats, and source details, and verify the created deck.
2713
-
2714
- ${context}`;
2715
- }
2716
- return `Use the existing data-analytics:report-to-pdf workflow to create a PDF artifact from this ${surface} app. Use the static HTML/export path when available, preserve the content title, narrative, charts, tables, caveats, and source details, omit interactive top bars, share menus, edit controls, and app-only controls, then verify the PDF before delivery.
2717
-
2718
- ${context}`;
2719
- }
2720
- function setOptionalCodexSearchParam(url, name, value) {
2721
- const trimmedValue = value?.trim();
2722
- if (!trimmedValue)
2723
- return;
2724
- url.searchParams.set(name, trimmedValue);
2725
- }
2726
- function codexPromptUrl(prompt, packageInfo) {
2727
- const url = new URL("codex://threads/new");
2728
- setOptionalCodexSearchParam(url, "prompt", prompt);
2729
- setOptionalCodexSearchParam(url, "originUrl", packageInfo?.originUrl);
2730
- setOptionalCodexSearchParam(url, "path", packageInfo?.root);
2731
- return url.toString();
2732
- }
2733
- function codexHostPromptPayload(prompt, packageInfo) {
2734
- return {
2735
- originUrl: packageInfo?.originUrl,
2736
- path: packageInfo?.root,
2737
- prompt
2738
- };
2739
- }
2740
- function sendCodexPromptToHost(prompt, packageInfo) {
2741
- const hostApi = window.openai;
2742
- const payload = codexHostPromptPayload(prompt, packageInfo);
2743
- const launchers = [hostApi?.sendFollowUpMessage, hostApi?.openCodexPrompt];
2744
- for (const launch of launchers) {
2745
- if (typeof launch !== "function")
2746
- continue;
2747
- try {
2748
- void Promise.resolve(launch.call(hostApi, payload)).catch(() => { });
2749
- return true;
2750
- }
2751
- catch {
2752
- }
2753
- }
2754
- return false;
2755
- }
2756
- function isLocalPreviewHost() {
2757
- return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
2758
- }
2759
- function launchCodexDeepLink(url) {
2760
- if (isLocalPreviewHost())
2761
- return false;
2762
- let linkClicked = false;
2763
- try {
2764
- const link = document.createElement("a");
2765
- link.href = url;
2766
- link.target = "_top";
2767
- link.rel = "noopener noreferrer";
2768
- link.style.display = "none";
2769
- document.body.appendChild(link);
2770
- link.click();
2771
- link.remove();
2772
- linkClicked = true;
2773
- }
2774
- catch {
2775
- }
2776
- try {
2777
- if (window.top === window) {
2778
- window.location.assign(url);
2779
- return true;
2780
- }
2781
- }
2782
- catch {
2783
- }
2784
- return linkClicked;
2785
- }
2786
- function openCodexPrompt(prompt, packageInfo, onCopyResult) {
2787
- const url = codexPromptUrl(prompt, packageInfo);
2788
- const launched = launchCodexDeepLink(url);
2789
- const sentToHost = sendCodexPromptToHost(prompt, packageInfo);
2790
- const openedCodex = sentToHost || launched;
2791
- void copyTextToClipboard(prompt)
2792
- .then(() => {
2793
- onCopyResult(sentToHost
2794
- ? "Started in this chat."
2795
- : openedCodex
2796
- ? "Codex prompt copied. Opening Codex if supported."
2797
- : "Prompt copied. Open Codex and paste it to continue.", false);
2798
- })
2799
- .catch(() => {
2800
- onCopyResult(openedCodex
2801
- ? "Opening Codex if supported, but prompt copy was blocked."
2802
- : "Could not copy the Codex prompt from this preview.", true);
2803
- });
2804
- }
2805
- function AnalyticsTopBarFreshness({ dateLabel, onRefresh, status, statusLabel }) {
2806
- const refreshTitle = dateLabel === "Unknown"
2807
- ? "Refresh using latest available data."
2808
- : `Refresh using latest available data. Last updated ${dateLabel}.`;
2809
- const refreshLabel = dateLabel === "Unknown" ? "Refresh" : dateLabel;
2810
- return (<div className="analytics-top-bar-freshness">
2811
- <button aria-label={refreshTitle} className="top-bar-button top-bar-button-ghost top-bar-refresh-button" onClick={onRefresh} title={refreshTitle} type="button">
2812
- <RefreshCw aria-hidden="true" size={14} strokeWidth={2}/>
2813
- <span>{refreshLabel}</span>
2814
- {statusLabel ? <span className={`snapshot-status ${status}`}>{statusLabel}</span> : null}
2815
- </button>
2816
- </div>);
2817
- }
2818
- function AnalyticsTopBar({ chrome = "full", isEditMode, onCancelEdit, manifest, onCopyResult, onEdit, onRequestFullscreen, onSaveEdit, onTitleChange, packageInfo, snapshot, title }) {
2819
- const [isExportMenuOpen, setIsExportMenuOpen] = useState(false);
2820
- const { closeMenu, fixedMenuStyle, handleMenuButtonKeyDown, handleMenuKeyDown, menuButtonRef, menuMotionClass, menuRef, toggleMenu, shouldRenderMenu } = useDashboardMenu(isExportMenuOpen, setIsExportMenuOpen);
2821
- const capabilities = artifactCapabilities(packageInfo);
2822
- const exportTargets = exportTargetsForCapabilities(capabilities);
2823
- const lastRefresh = formatDate(snapshot?.generatedAt ?? manifest?.generatedAt);
2824
- const statusLabel = snapshotStatusLabel(snapshot?.status);
2825
- const dateLabel = lastRefresh;
2826
- const showStatusLabel = manifest?.surface !== "report" && statusLabel;
2827
- const showActions = chrome === "full";
2828
- const showInlineExpand = chrome === "inline" && typeof onRequestFullscreen === "function";
2829
- function requestRefresh() {
2830
- void openCodexPrompt(refreshPrompt(manifest, snapshot, packageInfo), packageInfo, onCopyResult);
2831
- }
2832
- function requestExport(target) {
2833
- closeMenu();
2834
- void openCodexPrompt(exportPrompt(target, manifest, snapshot, packageInfo), packageInfo, onCopyResult);
2835
- }
2836
- return (<div className="analytics-top-bar" aria-label={`${appSurfaceLabel(manifest)} actions`}>
2837
- <div className="analytics-top-bar-title">
2838
- <EditablePageTitle ariaLabel={`Edit ${appSurfaceLabel(manifest)} title`} isEditMode={isEditMode} onChange={onTitleChange} onRequestEditMode={onEdit} placeholder={composePageTitle(manifest)} readOnly={!showActions || !capabilities.canEdit} title={title}/>
2839
- </div>
2840
- {showInlineExpand ? (<div className="analytics-top-bar-actions">
2841
- <button aria-label={`Expand ${appSurfaceLabel(manifest)} fullscreen`} className="top-bar-button" onClick={onRequestFullscreen} title={`Expand ${appSurfaceLabel(manifest)} fullscreen`} type="button">
2842
- <Expand aria-hidden="true" size={14} strokeWidth={2}/>
2843
- <span>Expand</span>
2844
- </button>
2845
- </div>) : showActions ? (<div className="analytics-top-bar-actions">
2846
- <AnalyticsTopBarFreshness dateLabel={dateLabel} onRefresh={requestRefresh} status={snapshot?.status} statusLabel={showStatusLabel ? statusLabel : null}/>
2847
- {isEditMode && capabilities.canEdit ? (<>
2848
- <button className="top-bar-button" onClick={onCancelEdit} type="button">
2849
- <span>Cancel</span>
2850
- </button>
2851
- <button className="top-bar-button top-bar-button-primary" onClick={onSaveEdit} type="button">
2852
- <span>Save changes</span>
2853
- </button>
2854
- </>) : (<>
2855
- {capabilities.canEdit ? (
2856
- <button className="top-bar-button top-bar-edit-button" onClick={onEdit} type="button">
2857
- <Pencil aria-hidden="true" size={14} strokeWidth={2}/>
2858
- <span>Edit</span>
2859
- </button>
2860
- ): null}
2861
- {exportTargets.length ? (
2862
- <div className="export-menu">
2863
- <button ref={menuButtonRef} aria-expanded={isExportMenuOpen} aria-haspopup="menu" className="top-bar-button" onClick={toggleMenu} onKeyDown={handleMenuButtonKeyDown} type="button">
2864
- <span>Export</span>
2865
- <ChevronDown aria-hidden="true" size={14} strokeWidth={2}/>
2866
- </button>
2867
- {shouldRenderMenu ? (<div ref={menuRef} className={`export-menu-list menu-surface ${menuMotionClass}`} onKeyDown={handleMenuKeyDown} role="menu" style={fixedMenuStyle}>
2868
- {exportTargets.map((target) => (<button className="export-menu-item" key={target} onClick={() => requestExport(target)} role="menuitem" type="button">
2869
- {EXPORT_TARGET_ICONS[target]}
2870
- {EXPORT_TARGET_LABELS[target]}
2871
- </button>))}
2872
- </div>) : null}
2873
- </div>
2874
- ): null}
2875
- </>)}
2876
- </div>) : null}
2877
- </div>);
2878
- }
2879
- export default function App({ displayMode = "fullscreen", onRequestFullscreen } = {}) {
2880
- const [manifest, setManifest] = useState(null);
2881
- const [snapshot, setSnapshot] = useState(null);
2882
- const [packageInfo, setPackageInfo] = useState(null);
2883
- const [error, setError] = useState(null);
2884
- const [selectedFilters, setSelectedFilters] = useState({});
2885
- const [tableColumnWidths, setTableColumnWidths] = useState({});
2886
- const [pageTitle, setPageTitle] = useState("");
2887
- const [chartTextOverrides, setChartTextOverrides] = useState({});
2888
- const [chartSpecOverrides, setChartSpecOverrides] = useState({});
2889
- const [chartTypeOverrides, setChartTypeOverrides] = useState({});
2890
- const [tableTextOverrides, setTableTextOverrides] = useState({});
2891
- const [blockTextOverrides, setBlockTextOverrides] = useState({});
2892
- const [deletedReportBlockIds, setDeletedReportBlockIds] = useState([]);
2893
- const [openMenuChartId, setOpenMenuChartId] = useState(null);
2894
- const [openMenuTableId, setOpenMenuTableId] = useState(null);
2895
- const [openMenuBlockId, setOpenMenuBlockId] = useState(null);
2896
- const [chartModal, setChartModal] = useState(null);
2897
- const [tableModal, setTableModal] = useState(null);
2898
- const [copyMessage, setCopyMessage] = useState(null);
2899
- const [isEditMode, setIsEditMode] = useState(false);
2900
- const [layoutResetKey, setLayoutResetKey] = useState(0);
2901
- const editSnapshotRef = useRef(null);
2902
- const layoutDraftsRef = useRef({ dashboard: null, report: null });
2903
- useEffect(() => {
2904
- async function load() {
2905
- try {
2906
- const [nextManifest, nextSnapshot, nextPackageInfo] = await Promise.all([
2907
- fetchJson("/api/manifest"),
2908
- fetchJson("/api/snapshot"),
2909
- fetchOptionalJson("/api/package")
2910
- ]);
2911
- setManifest(normalizeManifest(nextManifest));
2912
- setSnapshot(normalizeSnapshot(nextSnapshot));
2913
- setPackageInfo(nextPackageInfo);
2914
- }
2915
- catch (loadError) {
2916
- setError(loadError instanceof Error ? loadError.message : "Failed to load report");
2917
- }
2918
- }
2919
- void load();
2920
- }, []);
2921
- const filters = manifest?.filters ?? EMPTY_FILTERS;
2922
- const cards = manifest?.cards ?? EMPTY_CARDS;
2923
- const charts = manifest?.charts ?? EMPTY_CHARTS;
2924
- const tables = manifest?.tables ?? EMPTY_TABLES;
2925
- const reportBlocks = manifest?.blocks ?? EMPTY_REPORT_BLOCKS;
2926
- const globalFilters = useMemo(() => getGlobalFilters(filters, cards, charts, tables), [cards, charts, filters, tables]);
2927
- const accessIssues = snapshot?.accessIssues ?? EMPTY_ACCESS_ISSUES;
2928
- const storageKey = contentLayoutKey(manifest);
2929
- const chartTextStorageKey = chartTextKey(manifest);
2930
- const chartSpecStorageKey = chartSpecKey(manifest);
2931
- const chartTypeStorageKey = chartTypeKey(manifest);
2932
- const pageTitleTextStorageKey = pageTitleTextKey(manifest);
2933
- const tableTextStorageKey = tableTextKey(manifest);
2934
- const blockTextStorageKey = blockTextKey(manifest);
2935
- const deletedReportBlockStorageKey = deletedReportBlocksKey(manifest);
2936
- const tableColumnWidthStorageKey = tableColumnWidthKey(manifest);
2937
- const reportStorageKey = reportContentLayoutKey(manifest);
2938
- const isReport = manifest?.surface === "report";
2939
- const activeSurfaceFilters = isReport ? filters : globalFilters;
2940
- const capabilities = useMemo(() => artifactCapabilities(packageInfo), [packageInfo]);
2941
- const activeEditMode = capabilities.canEdit && isEditMode;
2942
- const requestEditMode = capabilities.canEdit ? beginEditMode : undefined;
2943
- useEffect(() => {
2944
- document.title = pageTitle.trim() || composePageTitle(manifest);
2945
- }, [manifest, pageTitle]);
2946
- useEffect(() => {
2947
- if (!capabilities.canEdit && isEditMode) {
2948
- setIsEditMode(false);
2949
- }
2950
- }, [capabilities.canEdit, isEditMode]);
2951
- const cardsById = useMemo(() => new Map(cards.map((card) => [card.id, card])), [cards]);
2952
- const chartsById = useMemo(() => new Map(charts.map((chart) => [chart.id, chart])), [charts]);
2953
- const tablesById = useMemo(() => new Map(tables.map((table) => [table.id, table])), [tables]);
2954
- const reportGridBlocks = reportBlocks;
2955
- const visibleReportGridBlocks = useMemo(() => {
2956
- if (!deletedReportBlockIds.length)
2957
- return reportGridBlocks;
2958
- const deletedIds = new Set(deletedReportBlockIds);
2959
- return reportGridBlocks.filter((block) => !deletedIds.has(block.id));
2960
- }, [deletedReportBlockIds, reportGridBlocks]);
2961
- useEffect(() => {
2962
- if (!reportGridBlocks.length || !deletedReportBlockStorageKey) {
2963
- setDeletedReportBlockIds([]);
2964
- return;
2965
- }
2966
- const stored = window.localStorage.getItem(deletedReportBlockStorageKey);
2967
- if (!stored) {
2968
- setDeletedReportBlockIds([]);
2969
- return;
2970
- }
2971
- try {
2972
- const parsed = JSON.parse(stored);
2973
- const knownIds = new Set(reportGridBlocks.map((block) => block.id));
2974
- const nextIds = Array.isArray(parsed)
2975
- ? parsed.filter((id) => typeof id === "string" && knownIds.has(id))
2976
- : [];
2977
- setDeletedReportBlockIds(nextIds);
2978
- }
2979
- catch {
2980
- setDeletedReportBlockIds([]);
2981
- }
2982
- }, [deletedReportBlockStorageKey, reportGridBlocks]);
2983
- useEffect(() => {
2984
- if (!tables.length || !tableColumnWidthStorageKey) {
2985
- setTableColumnWidths({});
2986
- return;
2987
- }
2988
- const stored = window.localStorage.getItem(tableColumnWidthStorageKey);
2989
- if (!stored) {
2990
- setTableColumnWidths({});
2991
- return;
2992
- }
2993
- try {
2994
- const parsed = JSON.parse(stored);
2995
- const tableFields = new Map(tables.map((table) => [table.id, new Set(table.columns.map((column) => column.field))]));
2996
- const nextState = {};
2997
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2998
- for (const [tableId, rawWidths] of Object.entries(parsed)) {
2999
- const fields = tableFields.get(tableId);
3000
- if (!fields || !rawWidths || typeof rawWidths !== "object" || Array.isArray(rawWidths)) {
3001
- continue;
3002
- }
3003
- const nextWidths = {};
3004
- for (const [field, rawWidth] of Object.entries(rawWidths)) {
3005
- const width = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
3006
- if (fields.has(field) && Number.isFinite(width)) {
3007
- nextWidths[field] = clamp(Math.round(width), TABLE_COLUMN_MIN_WIDTH, TABLE_COLUMN_MAX_WIDTH);
3008
- }
3009
- }
3010
- if (Object.keys(nextWidths).length) {
3011
- nextState[tableId] = nextWidths;
3012
- }
3013
- }
3014
- }
3015
- setTableColumnWidths(nextState);
3016
- }
3017
- catch {
3018
- setTableColumnWidths({});
3019
- }
3020
- }, [tableColumnWidthStorageKey, tables]);
3021
- useEffect(() => {
3022
- const fallbackTitle = composePageTitle(manifest);
3023
- if (!pageTitleTextStorageKey) {
3024
- setPageTitle(fallbackTitle);
3025
- return;
3026
- }
3027
- const storedTitle = window.localStorage.getItem(pageTitleTextStorageKey);
3028
- setPageTitle(storedTitle ?? fallbackTitle);
3029
- }, [manifest, pageTitleTextStorageKey]);
3030
- useEffect(() => {
3031
- if (!charts.length || !chartTextStorageKey)
3032
- return;
3033
- const stored = window.localStorage.getItem(chartTextStorageKey);
3034
- if (!stored) {
3035
- setChartTextOverrides({});
3036
- return;
3037
- }
3038
- try {
3039
- const parsed = JSON.parse(stored);
3040
- const knownIds = new Set(charts.map((chart) => chart.id));
3041
- const nextOverrides = {};
3042
- for (const [chartId, override] of Object.entries(parsed)) {
3043
- if (knownIds.has(chartId)
3044
- && override
3045
- && typeof override === "object"
3046
- && (typeof override.headerMarkdown === "string"
3047
- || typeof override.title === "string"
3048
- || typeof override.subtitle === "string")) {
3049
- nextOverrides[chartId] = {
3050
- ...(typeof override.headerMarkdown === "string" ? { headerMarkdown: override.headerMarkdown } : {}),
3051
- ...(typeof override.title === "string" ? { title: override.title } : {}),
3052
- ...(typeof override.subtitle === "string" ? { subtitle: override.subtitle } : {})
3053
- };
3054
- }
3055
- }
3056
- setChartTextOverrides(nextOverrides);
3057
- }
3058
- catch {
3059
- setChartTextOverrides({});
3060
- }
3061
- }, [chartTextStorageKey, charts]);
3062
- useEffect(() => {
3063
- if (!charts.length || !chartSpecStorageKey) {
3064
- setChartSpecOverrides({});
3065
- return;
3066
- }
3067
- const stored = window.localStorage.getItem(chartSpecStorageKey);
3068
- if (!stored) {
3069
- setChartSpecOverrides({});
3070
- return;
3071
- }
3072
- try {
3073
- const parsed = JSON.parse(stored);
3074
- const chartsByIdForStorage = new Map(charts.map((chart) => [chart.id, chart]));
3075
- const nextOverrides = {};
3076
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3077
- for (const [chartId, override] of Object.entries(parsed)) {
3078
- const chart = chartsByIdForStorage.get(chartId);
3079
- if (!chart || !override || typeof override !== "object" || Array.isArray(override)) {
3080
- continue;
3081
- }
3082
- const sanitized = {};
3083
- if (sharedIsChartType(override.type)) {
3084
- sanitized.type = override.type;
3085
- }
3086
- const chartFields = new Set(chartUsedFields(chart));
3087
- if (override.encodings && typeof override.encodings === "object" && !Array.isArray(override.encodings)) {
3088
- const sizeEncoding = override.encodings.size;
3089
- if (sizeEncoding && typeof sizeEncoding === "object" && !Array.isArray(sizeEncoding) && typeof sizeEncoding.field === "string" && chartFields.has(sizeEncoding.field)) {
3090
- sanitized.encodings = { ...chart.encodings, size: { ...sizeEncoding, field: sizeEncoding.field } };
3091
- }
3092
- }
3093
- if (override.settings && typeof override.settings === "object" && !Array.isArray(override.settings)) {
3094
- sanitized.settings = {
3095
- ...(override.settings.orientation === "horizontal" || override.settings.orientation === "vertical" ? { orientation: override.settings.orientation } : {}),
3096
- ...(["grouped", "stacked", "stacked100"].includes(override.settings.groupMode) ? { groupMode: override.settings.groupMode } : {}),
3097
- };
3098
- if (!Object.keys(sanitized.settings).length) {
3099
- delete sanitized.settings;
3100
- }
3101
- }
3102
- if (Object.keys(sanitized).length) {
3103
- nextOverrides[chartId] = sanitized;
3104
- }
3105
- }
3106
- }
3107
- setChartSpecOverrides(nextOverrides);
3108
- }
3109
- catch {
3110
- setChartSpecOverrides({});
3111
- }
3112
- }, [chartSpecStorageKey, charts]);
3113
- useEffect(() => {
3114
- if (!charts.length || !chartTypeStorageKey) {
3115
- setChartTypeOverrides({});
3116
- return;
3117
- }
3118
- const stored = window.localStorage.getItem(chartTypeStorageKey);
3119
- if (!stored) {
3120
- setChartTypeOverrides({});
3121
- return;
3122
- }
3123
- try {
3124
- const parsed = JSON.parse(stored);
3125
- const knownIds = new Set(charts.map((chart) => chart.id));
3126
- const nextOverrides = {};
3127
- for (const [chartId, chartType] of Object.entries(parsed)) {
3128
- if (knownIds.has(chartId) && isChartType(chartType)) {
3129
- nextOverrides[chartId] = chartType;
3130
- }
3131
- }
3132
- setChartTypeOverrides(nextOverrides);
3133
- }
3134
- catch {
3135
- setChartTypeOverrides({});
3136
- }
3137
- }, [chartTypeStorageKey, charts]);
3138
- useEffect(() => {
3139
- if (!tables.length || !tableTextStorageKey)
3140
- return;
3141
- const stored = window.localStorage.getItem(tableTextStorageKey);
3142
- if (!stored) {
3143
- setTableTextOverrides({});
3144
- return;
3145
- }
3146
- try {
3147
- const parsed = JSON.parse(stored);
3148
- const knownIds = new Set(tables.map((table) => table.id));
3149
- const nextOverrides = {};
3150
- for (const [tableId, override] of Object.entries(parsed)) {
3151
- if (knownIds.has(tableId)
3152
- && override
3153
- && typeof override === "object"
3154
- && typeof override.headerMarkdown === "string") {
3155
- nextOverrides[tableId] = { headerMarkdown: override.headerMarkdown };
3156
- }
3157
- }
3158
- setTableTextOverrides(nextOverrides);
3159
- }
3160
- catch {
3161
- setTableTextOverrides({});
3162
- }
3163
- }, [tableTextStorageKey, tables]);
3164
- useEffect(() => {
3165
- if (!reportGridBlocks.length || !blockTextStorageKey)
3166
- return;
3167
- const stored = window.localStorage.getItem(blockTextStorageKey);
3168
- if (!stored) {
3169
- setBlockTextOverrides({});
3170
- return;
3171
- }
3172
- try {
3173
- const parsed = JSON.parse(stored);
3174
- const knownIds = new Set(reportGridBlocks.map((block) => block.id));
3175
- const nextOverrides = {};
3176
- for (const [blockId, override] of Object.entries(parsed)) {
3177
- if (knownIds.has(blockId) && override && typeof override === "object") {
3178
- const nextOverride = {
3179
- ...(visibleString(override.bodyMarkdown) ? { bodyMarkdown: override.bodyMarkdown } : {}),
3180
- ...(visibleString(override.html) ? { html: override.html } : {})
3181
- };
3182
- if (Object.keys(nextOverride).length)
3183
- nextOverrides[blockId] = nextOverride;
3184
- }
3185
- }
3186
- setBlockTextOverrides(nextOverrides);
3187
- }
3188
- catch {
3189
- setBlockTextOverrides({});
3190
- }
3191
- }, [blockTextStorageKey, reportGridBlocks]);
3192
- const filterDefaults = useMemo(() => {
3193
- return Object.fromEntries(activeSurfaceFilters.map((filter) => [filter.id, filter.defaultValue ?? "all"]));
3194
- }, [activeSurfaceFilters]);
3195
- useEffect(() => {
3196
- setSelectedFilters((current) => ({ ...filterDefaults, ...current }));
3197
- }, [filterDefaults]);
3198
- useEffect(() => {
3199
- if (!copyMessage)
3200
- return;
3201
- const timeout = window.setTimeout(() => setCopyMessage(null), 2600);
3202
- return () => window.clearTimeout(timeout);
3203
- }, [copyMessage]);
3204
- function persistEditableState() {
3205
- if (pageTitleTextStorageKey) {
3206
- window.localStorage.setItem(pageTitleTextStorageKey, pageTitle);
3207
- }
3208
- if (chartTextStorageKey) {
3209
- window.localStorage.setItem(chartTextStorageKey, JSON.stringify(chartTextOverrides));
3210
- }
3211
- if (chartSpecStorageKey) {
3212
- window.localStorage.setItem(chartSpecStorageKey, JSON.stringify(chartSpecOverrides));
3213
- }
3214
- if (chartTypeStorageKey) {
3215
- window.localStorage.setItem(chartTypeStorageKey, JSON.stringify(chartTypeOverrides));
3216
- }
3217
- if (tableTextStorageKey) {
3218
- window.localStorage.setItem(tableTextStorageKey, JSON.stringify(tableTextOverrides));
3219
- }
3220
- if (blockTextStorageKey) {
3221
- window.localStorage.setItem(blockTextStorageKey, JSON.stringify(blockTextOverrides));
3222
- }
3223
- if (deletedReportBlockStorageKey) {
3224
- window.localStorage.setItem(deletedReportBlockStorageKey, JSON.stringify(deletedReportBlockIds));
3225
- }
3226
- if (tableColumnWidthStorageKey) {
3227
- window.localStorage.setItem(tableColumnWidthStorageKey, JSON.stringify(tableColumnWidths));
3228
- }
3229
- if (storageKey && layoutDraftsRef.current.dashboard) {
3230
- window.localStorage.setItem(storageKey, JSON.stringify(layoutDraftsRef.current.dashboard));
3231
- }
3232
- if (reportStorageKey && layoutDraftsRef.current.report) {
3233
- window.localStorage.setItem(reportStorageKey, JSON.stringify(layoutDraftsRef.current.report));
3234
- }
3235
- }
3236
- function closeInlineMenus() {
3237
- setOpenMenuBlockId(null);
3238
- setOpenMenuChartId(null);
3239
- setOpenMenuTableId(null);
3240
- }
3241
- function beginEditMode() {
3242
- if (isEditMode)
3243
- return;
3244
- editSnapshotRef.current = {
3245
- blockTextOverrides: cloneSerializable(blockTextOverrides),
3246
- chartSpecOverrides: cloneSerializable(chartSpecOverrides),
3247
- chartTextOverrides: cloneSerializable(chartTextOverrides),
3248
- chartTypeOverrides: cloneSerializable(chartTypeOverrides),
3249
- deletedReportBlockIds: [...deletedReportBlockIds],
3250
- pageTitle,
3251
- tableColumnWidths: cloneSerializable(tableColumnWidths),
3252
- tableTextOverrides: cloneSerializable(tableTextOverrides)
3253
- };
3254
- layoutDraftsRef.current = { dashboard: null, report: null };
3255
- closeInlineMenus();
3256
- setIsEditMode(true);
3257
- }
3258
- function cancelEditMode() {
3259
- const snapshotState = editSnapshotRef.current;
3260
- if (snapshotState) {
3261
- setBlockTextOverrides(cloneSerializable(snapshotState.blockTextOverrides));
3262
- setChartSpecOverrides(cloneSerializable(snapshotState.chartSpecOverrides ?? {}));
3263
- setChartTextOverrides(cloneSerializable(snapshotState.chartTextOverrides));
3264
- setChartTypeOverrides(cloneSerializable(snapshotState.chartTypeOverrides));
3265
- setDeletedReportBlockIds([...snapshotState.deletedReportBlockIds]);
3266
- setPageTitle(snapshotState.pageTitle);
3267
- setTableColumnWidths(cloneSerializable(snapshotState.tableColumnWidths));
3268
- setTableTextOverrides(cloneSerializable(snapshotState.tableTextOverrides));
3269
- }
3270
- editSnapshotRef.current = null;
3271
- layoutDraftsRef.current = { dashboard: null, report: null };
3272
- closeInlineMenus();
3273
- setIsEditMode(false);
3274
- setLayoutResetKey((current) => current + 1);
3275
- }
3276
- function saveEditMode() {
3277
- persistEditableState();
3278
- editSnapshotRef.current = null;
3279
- layoutDraftsRef.current = { dashboard: null, report: null };
3280
- closeInlineMenus();
3281
- setIsEditMode(false);
3282
- }
3283
- function recordDashboardLayoutDraft(nextItems) {
3284
- layoutDraftsRef.current.dashboard = nextItems;
3285
- }
3286
- function recordReportLayoutDraft(nextItems) {
3287
- layoutDraftsRef.current.report = nextItems;
3288
- }
3289
- function updatePageTitle(nextTitle) {
3290
- setPageTitle(nextTitle);
3291
- if (!isEditMode && pageTitleTextStorageKey) {
3292
- window.localStorage.setItem(pageTitleTextStorageKey, nextTitle);
3293
- }
3294
- }
3295
- function updateChartText(chartId, nextText) {
3296
- setChartTextOverrides((current) => {
3297
- const merged = {
3298
- ...current,
3299
- [chartId]: {
3300
- ...current[chartId],
3301
- ...nextText
3302
- }
3303
- };
3304
- if (!isEditMode && chartTextStorageKey) {
3305
- window.localStorage.setItem(chartTextStorageKey, JSON.stringify(merged));
3306
- }
3307
- return merged;
3308
- });
3309
- }
3310
- function updateChartType(chartId, nextType) {
3311
- setChartTypeOverrides((current) => {
3312
- const originalType = charts.find((chart) => chart.id === chartId)?.type;
3313
- const merged = { ...current };
3314
- if (!originalType || nextType === originalType) {
3315
- delete merged[chartId];
3316
- }
3317
- else {
3318
- merged[chartId] = nextType;
3319
- }
3320
- if (!isEditMode && chartTypeStorageKey) {
3321
- window.localStorage.setItem(chartTypeStorageKey, JSON.stringify(merged));
3322
- }
3323
- return merged;
3324
- });
3325
- }
3326
- function updateChartSpec(chartId, widgetSpec) {
3327
- const chart = charts.find((candidate) => candidate.id === chartId);
3328
- if (!chart)
3329
- return;
3330
- if (!widgetSpec) {
3331
- setChartSpecOverrides((current) => {
3332
- const merged = { ...current };
3333
- delete merged[chartId];
3334
- if (!isEditMode && chartSpecStorageKey) {
3335
- window.localStorage.setItem(chartSpecStorageKey, JSON.stringify(merged));
3336
- }
3337
- return merged;
3338
- });
3339
- setChartTypeOverrides((current) => {
3340
- const merged = { ...current };
3341
- delete merged[chartId];
3342
- if (!isEditMode && chartTypeStorageKey) {
3343
- window.localStorage.setItem(chartTypeStorageKey, JSON.stringify(merged));
3344
- }
3345
- return merged;
3346
- });
3347
- return;
3348
- }
3349
- const nextOverride = chartSpecOverrideFromWidgetSpec(chart, widgetSpec);
3350
- const nextType = sharedIsChartType(nextOverride.type) ? nextOverride.type : chart.type;
3351
- setChartSpecOverrides((current) => {
3352
- const merged = { ...current };
3353
- if (Object.keys(nextOverride).length) {
3354
- merged[chartId] = nextOverride;
3355
- }
3356
- else {
3357
- delete merged[chartId];
3358
- }
3359
- if (!isEditMode && chartSpecStorageKey) {
3360
- window.localStorage.setItem(chartSpecStorageKey, JSON.stringify(merged));
3361
- }
3362
- return merged;
3363
- });
3364
- setChartTypeOverrides((current) => {
3365
- const merged = { ...current };
3366
- if (nextType === chart.type) {
3367
- delete merged[chartId];
3368
- }
3369
- else {
3370
- merged[chartId] = nextType;
3371
- }
3372
- if (!isEditMode && chartTypeStorageKey) {
3373
- window.localStorage.setItem(chartTypeStorageKey, JSON.stringify(merged));
3374
- }
3375
- return merged;
3376
- });
3377
- }
3378
- function updateTableColumnWidths(tableId, nextWidths, options = {}) {
3379
- setTableColumnWidths((current) => {
3380
- const tableSpec = tablesById.get(tableId);
3381
- if (!tableSpec)
3382
- return current;
3383
- const validFields = new Set(tableSpec.columns.map((column) => column.field));
3384
- const sanitizedWidths = {};
3385
- for (const [field, rawWidth] of Object.entries(nextWidths)) {
3386
- const width = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
3387
- if (validFields.has(field) && Number.isFinite(width)) {
3388
- sanitizedWidths[field] = clamp(Math.round(width), TABLE_COLUMN_MIN_WIDTH, TABLE_COLUMN_MAX_WIDTH);
3389
- }
3390
- }
3391
- const merged = { ...current };
3392
- if (Object.keys(sanitizedWidths).length) {
3393
- merged[tableId] = sanitizedWidths;
3394
- }
3395
- else {
3396
- delete merged[tableId];
3397
- }
3398
- if (!isEditMode && options.persist !== false && tableColumnWidthStorageKey) {
3399
- window.localStorage.setItem(tableColumnWidthStorageKey, JSON.stringify(merged));
3400
- }
3401
- return merged;
3402
- });
3403
- }
3404
- function updateTableText(tableId, nextText) {
3405
- setTableTextOverrides((current) => {
3406
- const merged = {
3407
- ...current,
3408
- [tableId]: {
3409
- ...current[tableId],
3410
- ...nextText
3411
- }
3412
- };
3413
- if (!isEditMode && tableTextStorageKey) {
3414
- window.localStorage.setItem(tableTextStorageKey, JSON.stringify(merged));
3415
- }
3416
- return merged;
3417
- });
3418
- }
3419
- function updateBlockText(blockId, nextText) {
3420
- setBlockTextOverrides((current) => {
3421
- const nextBlockOverride = {
3422
- ...current[blockId],
3423
- ...nextText
3424
- };
3425
- if ("bodyMarkdown" in nextText && !visibleString(nextText.bodyMarkdown)) {
3426
- delete nextBlockOverride.bodyMarkdown;
3427
- }
3428
- if ("html" in nextText && !visibleString(nextText.html)) {
3429
- delete nextBlockOverride.html;
3430
- }
3431
- const merged = { ...current };
3432
- if (Object.keys(nextBlockOverride).length)
3433
- merged[blockId] = nextBlockOverride;
3434
- else
3435
- delete merged[blockId];
3436
- if (!isEditMode && blockTextStorageKey) {
3437
- window.localStorage.setItem(blockTextStorageKey, JSON.stringify(merged));
3438
- }
3439
- return merged;
3440
- });
3441
- }
3442
- function deleteReportBlocks(blockIds) {
3443
- const idsToDelete = [...new Set(blockIds)];
3444
- if (!idsToDelete.length)
3445
- return;
3446
- setDeletedReportBlockIds((current) => {
3447
- const existingIds = new Set(current);
3448
- const nextIds = [...current];
3449
- for (const blockId of idsToDelete) {
3450
- if (!existingIds.has(blockId)) {
3451
- nextIds.push(blockId);
3452
- }
3453
- }
3454
- if (nextIds.length === current.length)
3455
- return current;
3456
- if (!isEditMode && deletedReportBlockStorageKey) {
3457
- window.localStorage.setItem(deletedReportBlockStorageKey, JSON.stringify(nextIds));
3458
- }
3459
- return nextIds;
3460
- });
3461
- setOpenMenuBlockId(null);
3462
- setOpenMenuChartId(null);
3463
- setOpenMenuTableId(null);
3464
- }
3465
- function deleteReportBlock(blockId) {
3466
- deleteReportBlocks([blockId]);
3467
- }
3468
- const dashboardContentBlocks = useMemo(() => {
3469
- const chartBlocks = charts.map((chart) => ({
3470
- className: "analytics-layout-item-chart",
3471
- defaultLayout: dashboardCardLayout(chart.layout),
3472
- id: `chart:${chart.id}`,
3473
- render: (layout, { setLayout }) => {
3474
- const overriddenChart = applyChartSpecOverride(chart, chartSpecOverrides[chart.id]);
3475
- const chartRows = filterRowsForDataset(getRows(snapshot, overriddenChart.dataset), overriddenChart.dataset, activeSurfaceFilters, selectedFilters, chartUsedFields(overriddenChart));
3476
- const chartTypeOptions = compatibleChartTypesForArtifactCard(overriddenChart, chartRows);
3477
- const requestedType = chartTypeOverrides[chart.id] ?? overriddenChart.type;
3478
- const activeType = chartTypeOptions.some((option) => option.type === requestedType)
3479
- ? requestedType
3480
- : overriddenChart.type;
3481
- const displayChart = withChartType(overriddenChart, activeType);
3482
- const accessIssue = accessIssueForChart(displayChart, accessIssues);
3483
- return (<VizCard accessIssue={accessIssue} chart={displayChart} chartTypeOptions={chartTypeOptions} isEditMode={activeEditMode} isMenuOpen={openMenuChartId === chart.id} layout={layout} onChartTypeChange={updateChartType} onCopyResult={(message, isError = false) => setCopyMessage({ isError, message })} onRequestEditMode={requestEditMode} onTextChange={updateChartText} onMenuOpenChange={(nextOpen) => {
3484
- setOpenMenuTableId(null);
3485
- setOpenMenuChartId(nextOpen ? chart.id : null);
3486
- }} onModalOpen={setChartModal} textOverride={chartTextOverrides[chart.id]}>
3487
- <ChartBody accessIssue={accessIssue} chart={displayChart} filters={activeSurfaceFilters} layout={layout} selectedFilters={selectedFilters} snapshot={snapshot}/>
3488
- </VizCard>);
3489
- }
3490
- }));
3491
- const tableBlocks = tables.map((table) => ({
3492
- className: "analytics-layout-item-table",
3493
- defaultLayout: dashboardCardLayout(table.layout ?? "full"),
3494
- id: `table:${table.id}`,
3495
- render: (layout) => (<DataTable columnWidths={tableColumnWidths[table.id] ?? {}} filters={activeSurfaceFilters} isEditMode={activeEditMode} isMenuOpen={openMenuTableId === table.id} layout={layout} manifest={manifest} onColumnWidthsChange={updateTableColumnWidths} onMenuOpenChange={(nextOpen) => {
3496
- setOpenMenuChartId(null);
3497
- setOpenMenuTableId(nextOpen ? table.id : null);
3498
- }} onModalOpen={setTableModal} onRequestEditMode={requestEditMode} onTextChange={updateTableText} selectedFilters={selectedFilters} snapshot={snapshot} table={table} textOverride={tableTextOverrides[table.id]}/>)
3499
- }));
3500
- const htmlBlocks = visibleReportGridBlocks
3501
- .filter((block) => block.type === "html")
3502
- .map((block) => ({
3503
- className: "analytics-layout-item-html",
3504
- defaultLayout: dashboardCardLayout(block.layout ?? "full"),
3505
- id: `html:${block.id}`,
3506
- render: () => (<ReportHtmlBlock block={block} htmlOverride={blockTextOverrides[block.id]} isEditMode={activeEditMode} isMenuOpen={openMenuBlockId === block.id} onHtmlChange={updateBlockText} onCopyResult={(message, isError = false) => setCopyMessage({ isError, message })} onDeleteBlock={capabilities.canEdit ? () => deleteReportBlock(block.id) : undefined} onMenuOpenChange={(nextOpen) => {
3507
- setOpenMenuChartId(null);
3508
- setOpenMenuTableId(null);
3509
- setOpenMenuBlockId(nextOpen ? block.id : null);
3510
- }}/>)
3511
- }));
3512
- return [...chartBlocks, ...tableBlocks, ...htmlBlocks];
3513
- }, [
3514
- accessIssues,
3515
- activeSurfaceFilters,
3516
- activeEditMode,
3517
- capabilities.canEdit,
3518
- chartTextOverrides,
3519
- chartSpecOverrides,
3520
- chartTypeOverrides,
3521
- charts,
3522
- manifest,
3523
- openMenuBlockId,
3524
- openMenuChartId,
3525
- openMenuTableId,
3526
- requestEditMode,
3527
- selectedFilters,
3528
- snapshot,
3529
- tableColumnWidths,
3530
- tableTextOverrides,
3531
- tables,
3532
- visibleReportGridBlocks
3533
- ]);
3534
- const reportContentBlocks = useMemo(() => {
3535
- return visibleReportGridBlocks.map((block) => {
3536
- if (block.type === "metric-strip") {
3537
- const metricCards = (block.cardIds ?? [])
3538
- .map((cardId) => cardsById.get(cardId))
3539
- .filter((card) => Boolean(card));
3540
- return {
3541
- className: "report-stack-item report-stack-item-metric-strip",
3542
- defaultLayout: reportCardLayout(block.layout),
3543
- id: block.id,
3544
- render: () => (<ReportMetricStripBlock cards={metricCards} filters={activeSurfaceFilters} id={block.id} isMenuOpen={openMenuBlockId === block.id} onCopyResult={(message, isError = false) => setCopyMessage({ isError, message })} onDeleteBlock={capabilities.canEdit ? () => deleteReportBlock(block.id) : undefined} onMenuOpenChange={(nextOpen) => {
3545
- setOpenMenuChartId(null);
3546
- setOpenMenuTableId(null);
3547
- setOpenMenuBlockId(nextOpen ? block.id : null);
3548
- }} selectedFilters={selectedFilters} snapshot={snapshot}/>)
3549
- };
3550
- }
3551
- const chart = block.chartId ? chartsById.get(block.chartId) : undefined;
3552
- const table = block.tableId ? tablesById.get(block.tableId) : undefined;
3553
- return {
3554
- className: `report-stack-item report-stack-item-${block.type}`,
3555
- defaultLayout: reportCardLayout(block.layout),
3556
- id: block.id,
3557
- render: (layout) => (<ReportBlockCard accessIssues={accessIssues} block={block} blockTextOverride={blockTextOverrides[block.id]} chart={chart} chartSpecOverride={chart ? chartSpecOverrides[chart.id] : undefined} chartTextOverride={chart ? chartTextOverrides[chart.id] : undefined} chartTypeOverride={chart ? chartTypeOverrides[chart.id] : undefined} columnWidths={table ? tableColumnWidths[table.id] ?? {} : {}} filters={activeSurfaceFilters} isBlockMenuOpen={openMenuBlockId === block.id} isChartMenuOpen={Boolean(chart && openMenuChartId === chart.id)} isEditMode={activeEditMode} isTableMenuOpen={Boolean(table && openMenuTableId === table.id)} layout={layout} manifest={manifest} onBlockMenuOpenChange={(nextOpen) => {
3558
- setOpenMenuChartId(null);
3559
- setOpenMenuTableId(null);
3560
- setOpenMenuBlockId(nextOpen ? block.id : null);
3561
- }} onBlockTextChange={updateBlockText} onChartTypeChange={updateChartType} onChartMenuOpenChange={(nextOpen) => {
3562
- setOpenMenuTableId(null);
3563
- setOpenMenuBlockId(null);
3564
- setOpenMenuChartId(chart && nextOpen ? chart.id : null);
3565
- }} onChartModalOpen={setChartModal} onColumnWidthsChange={updateTableColumnWidths} onCopyResult={(message, isError = false) => setCopyMessage({ isError, message })} onDeleteBlock={capabilities.canEdit ? () => deleteReportBlock(block.id) : undefined} onRequestEditMode={requestEditMode} onTableMenuOpenChange={(nextOpen) => {
3566
- setOpenMenuChartId(null);
3567
- setOpenMenuBlockId(null);
3568
- setOpenMenuTableId(table && nextOpen ? table.id : null);
3569
- }} onTableModalOpen={setTableModal} onTableTextChange={updateTableText} onTextChange={updateChartText} selectedFilters={selectedFilters} snapshot={snapshot} table={table} tableTextOverride={table ? tableTextOverrides[table.id] : undefined}/>)
3570
- };
3571
- });
3572
- }, [
3573
- accessIssues,
3574
- activeSurfaceFilters,
3575
- activeEditMode,
3576
- blockTextOverrides,
3577
- cardsById,
3578
- capabilities.canEdit,
3579
- chartSpecOverrides,
3580
- chartTextOverrides,
3581
- chartTypeOverrides,
3582
- chartsById,
3583
- manifest,
3584
- openMenuBlockId,
3585
- openMenuChartId,
3586
- openMenuTableId,
3587
- requestEditMode,
3588
- selectedFilters,
3589
- snapshot,
3590
- tableColumnWidths,
3591
- tableTextOverrides,
3592
- tablesById,
3593
- visibleReportGridBlocks
3594
- ]);
3595
- if (error) {
3596
- return (<DashboardShell surface={manifest?.surface}>
3597
- <div className="empty-state error-state">{error}</div>
3598
- </DashboardShell>);
3599
- }
3600
- const activeFilters = activeFilterSummary(activeSurfaceFilters, selectedFilters);
3601
- const chartModalSourceChart = chartModal
3602
- ? chartsById.get(chartModal.chart.id) ?? chartModal.chart
3603
- : null;
3604
- const chartModalBaseChart = chartModalSourceChart
3605
- ? applyChartSpecOverride(chartModalSourceChart, chartSpecOverrides[chartModalSourceChart.id])
3606
- : null;
3607
- const chartModalRows = chartModalBaseChart
3608
- ? filterRowsForDataset(getRows(snapshot, chartModalBaseChart.dataset), chartModalBaseChart.dataset, activeSurfaceFilters, selectedFilters, chartUsedFields(chartModalBaseChart))
3609
- : [];
3610
- const chartModalTypeOptions = chartModalBaseChart
3611
- ? compatibleChartTypesFor(chartModalBaseChart, chartModalRows)
3612
- : [];
3613
- const chartModalRequestedType = chartModalBaseChart
3614
- ? chartTypeOverrides[chartModalBaseChart.id] ?? chartModalBaseChart.type
3615
- : null;
3616
- const chartModalActiveType = chartModalRequestedType && chartModalTypeOptions.some((option) => option.type === chartModalRequestedType)
3617
- ? chartModalRequestedType
3618
- : chartModalBaseChart?.type ?? null;
3619
- const activeChartModal = chartModal?.kind === "fullscreen" && chartModalBaseChart && chartModalActiveType
3620
- ? {
3621
- chart: withChartType({
3622
- ...chartModalBaseChart,
3623
- subtitle: chartModal.chart.subtitle,
3624
- title: chartModal.chart.title
3625
- }, chartModalActiveType)
3626
- }
3627
- : null;
3628
- if (isReport) {
3629
- return (<DashboardShell isEditMode={activeEditMode} surface="report">
3630
- <AnalyticsTopBar chrome={displayMode === "inline" ? "inline" : "full"} isEditMode={activeEditMode} manifest={manifest} onCancelEdit={cancelEditMode} onCopyResult={(message, isError = false) => setCopyMessage({ isError, message })} onEdit={requestEditMode} onRequestFullscreen={onRequestFullscreen} onSaveEdit={saveEditMode} onTitleChange={updatePageTitle} packageInfo={packageInfo} snapshot={snapshot} title={pageTitle}/>
3631
- <AccessIssueStrip issues={accessIssues}/>
3632
- {copyMessage ? (<div className={`copy-toast ${copyMessage.isError ? "error" : ""}`} role="status">
3633
- {copyMessage.message}
3634
- </div>) : null}
3635
-
3636
- <AnalyticsLayoutCanvas ariaLabel="Report blocks" blocks={reportContentBlocks} cancelSelector={CARD_DRAG_CANCEL_SELECTOR} className="report-content-grid report-block-stack" isEditMode={activeEditMode} layoutResetKey={layoutResetKey} onLayoutChange={recordReportLayoutDraft} storageKey={reportStorageKey}/>
3637
- {activeChartModal ? (<ChartDetailPage accessIssue={accessIssueForChart(activeChartModal.chart, accessIssues)} chart={activeChartModal.chart} filters={activeSurfaceFilters} manifest={manifest} onChartSpecChange={updateChartSpec} onClose={() => setChartModal(null)} packageInfo={packageInfo} rows={chartModalRows} selectedFilters={selectedFilters} snapshot={snapshot}/>) : null}
3638
- {chartModal?.kind === "source" && chartModalBaseChart ? (<ChartSourceModalDialog activeFilters={activeFilters} chart={{
3639
- ...chartModalBaseChart,
3640
- subtitle: chartModal.chart.subtitle,
3641
- title: chartModal.chart.title
3642
- }} manifest={manifest} onClose={() => setChartModal(null)} rows={chartModalRows} snapshot={snapshot}/>) : null}
3643
- {tableModal ? (<TableModalDialog activeFilters={activeFilters} columnWidths={tableColumnWidths[tableModal.table.id] ?? {}} filters={activeSurfaceFilters} kind={tableModal.kind} manifest={manifest} onColumnWidthsChange={updateTableColumnWidths} onClose={() => setTableModal(null)} selectedFilters={selectedFilters} snapshot={snapshot} table={tableModal.table}/>) : null}
3644
- </DashboardShell>);
3645
- }
3646
- return (<DashboardShell isEditMode={activeEditMode} surface={manifest?.surface}>
3647
- <AnalyticsTopBar chrome={displayMode === "inline" ? "inline" : "full"} isEditMode={activeEditMode} manifest={manifest} onCancelEdit={cancelEditMode} onCopyResult={(message, isError = false) => setCopyMessage({ isError, message })} onEdit={requestEditMode} onRequestFullscreen={onRequestFullscreen} onSaveEdit={saveEditMode} onTitleChange={updatePageTitle} packageInfo={packageInfo} snapshot={snapshot} title={pageTitle}/>
3648
- <AccessIssueStrip issues={accessIssues}/>
3649
- {copyMessage ? (<div className={`copy-toast ${copyMessage.isError ? "error" : ""}`} role="status">
3650
- {copyMessage.message}
3651
- </div>) : null}
3652
-
3653
- <FilterToolbar filters={activeSurfaceFilters} onChange={setSelectedFilters} selectedFilters={selectedFilters} snapshot={snapshot}/>
3654
-
3655
- <KpiStrip cards={cards} filters={activeSurfaceFilters} selectedFilters={selectedFilters} snapshot={snapshot}/>
3656
-
3657
- <AnalyticsLayoutCanvas ariaLabel="Dashboard content" blocks={dashboardContentBlocks} cancelSelector={CARD_DRAG_CANCEL_SELECTOR} className="dashboard-content-grid" isEditMode={activeEditMode} layoutResetKey={layoutResetKey} onLayoutChange={recordDashboardLayoutDraft} storageKey={storageKey}/>
3658
- {activeChartModal ? (<ChartDetailPage accessIssue={accessIssueForChart(activeChartModal.chart, accessIssues)} chart={activeChartModal.chart} filters={activeSurfaceFilters} manifest={manifest} onChartSpecChange={updateChartSpec} onClose={() => setChartModal(null)} packageInfo={packageInfo} rows={chartModalRows} selectedFilters={selectedFilters} snapshot={snapshot}/>) : null}
3659
- {chartModal?.kind === "source" && chartModalBaseChart ? (<ChartSourceModalDialog activeFilters={activeFilters} chart={{
3660
- ...chartModalBaseChart,
3661
- subtitle: chartModal.chart.subtitle,
3662
- title: chartModal.chart.title
3663
- }} manifest={manifest} onClose={() => setChartModal(null)} rows={chartModalRows} snapshot={snapshot}/>) : null}
3664
- {tableModal ? (<TableModalDialog activeFilters={activeFilters} columnWidths={tableColumnWidths[tableModal.table.id] ?? {}} filters={activeSurfaceFilters} kind={tableModal.kind} manifest={manifest} onColumnWidthsChange={updateTableColumnWidths} onClose={() => setTableModal(null)} selectedFilters={selectedFilters} snapshot={snapshot} table={tableModal.table}/>) : null}
3665
- </DashboardShell>);
3666
- }