@litfamily/litclaude 1.0.10 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (718) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +436 -45
  3. package/README_ko-KR.md +403 -17
  4. package/bin/install-ownership.mjs +155 -18
  5. package/bin/litclaude-ai.js +91 -9
  6. package/docs/ab/S11-baseline-desktop.webp +0 -0
  7. package/docs/ab/S11-baseline-phone.webp +0 -0
  8. package/docs/ab/S11-litclaude-desktop.webp +0 -0
  9. package/docs/ab/S11-litclaude-phone.webp +0 -0
  10. package/docs/ab/S3-baseline-desktop.webp +0 -0
  11. package/docs/ab/S3-baseline-phone.webp +0 -0
  12. package/docs/ab/S3-litclaude-desktop.webp +0 -0
  13. package/docs/ab/S3-litclaude-phone.webp +0 -0
  14. package/docs/ab/S4-baseline-desktop.webp +0 -0
  15. package/docs/ab/S4-baseline-phone.webp +0 -0
  16. package/docs/ab/S4-litclaude-desktop.webp +0 -0
  17. package/docs/ab/S4-litclaude-phone.webp +0 -0
  18. package/docs/ab/S5-baseline-pages.webp +0 -0
  19. package/docs/ab/S5-baseline-slides.webp +0 -0
  20. package/docs/ab/S5-litclaude-pages.webp +0 -0
  21. package/docs/ab/S5-litclaude-slides.webp +0 -0
  22. package/docs/ab/S7-litclaude-diagram.webp +0 -0
  23. package/docs/ab/S8-baseline-slides.webp +0 -0
  24. package/docs/ab/S8-litclaude-slides.webp +0 -0
  25. package/docs/ab/S9-litclaude-pages.webp +0 -0
  26. package/docs/agents.md +10 -7
  27. package/docs/assets/cover-motion-still.webp +0 -0
  28. package/docs/assets/cover-motion.webp +0 -0
  29. package/docs/assets/readme/badge-version.svg +1 -1
  30. package/docs/assets/skills/autoconference.webp +0 -0
  31. package/docs/assets/skills/automatic-checks.webp +0 -0
  32. package/docs/assets/skills/autoresearch.webp +0 -0
  33. package/docs/assets/skills/browser-drive.webp +0 -0
  34. package/docs/assets/skills/debugging.webp +0 -0
  35. package/docs/assets/skills/deep-interview.webp +0 -0
  36. package/docs/assets/skills/frontend-ui-ux.webp +0 -0
  37. package/docs/assets/skills/lit-burnoff-file.webp +0 -0
  38. package/docs/assets/skills/lit-burnoff.webp +0 -0
  39. package/docs/assets/skills/lit-code.webp +0 -0
  40. package/docs/assets/skills/lit-commit.webp +0 -0
  41. package/docs/assets/skills/lit-comprehend.webp +0 -0
  42. package/docs/assets/skills/lit-crucible.webp +0 -0
  43. package/docs/assets/skills/lit-diagram-drawer.webp +0 -0
  44. package/docs/assets/skills/lit-docx.webp +0 -0
  45. package/docs/assets/skills/lit-handoff.webp +0 -0
  46. package/docs/assets/skills/lit-humanizer.webp +0 -0
  47. package/docs/assets/skills/lit-init.webp +0 -0
  48. package/docs/assets/skills/lit-loop.webp +0 -0
  49. package/docs/assets/skills/lit-plan.webp +0 -0
  50. package/docs/assets/skills/lit-pptx.webp +0 -0
  51. package/docs/assets/skills/lit-recap.webp +0 -0
  52. package/docs/assets/skills/lit-scientific-visualization.webp +0 -0
  53. package/docs/assets/skills/lit-team.webp +0 -0
  54. package/docs/assets/skills/lit-typographic-motion.webp +0 -0
  55. package/docs/assets/skills/litgoal.webp +0 -0
  56. package/docs/assets/skills/litresearch.webp +0 -0
  57. package/docs/assets/skills/litwork.webp +0 -0
  58. package/docs/assets/skills/lsp-setup.webp +0 -0
  59. package/docs/assets/skills/readme-studio.webp +0 -0
  60. package/docs/assets/skills/refactor.webp +0 -0
  61. package/docs/assets/skills/review-work.webp +0 -0
  62. package/docs/assets/skills/start-work.webp +0 -0
  63. package/docs/assets/skills/structural-search.webp +0 -0
  64. package/docs/assets/skills/visual-qa.webp +0 -0
  65. package/docs/assets/skills/wikify.webp +0 -0
  66. package/docs/hooks.md +185 -41
  67. package/docs/migration.md +6 -3
  68. package/docs/privacy.md +6 -1
  69. package/package.json +4 -2
  70. package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
  71. package/plugins/litclaude/agents/korean-prose-editor.md +1 -0
  72. package/plugins/litclaude/agents/korean-style-analyzer.md +1 -0
  73. package/plugins/litclaude/agents/meaning-preservation-auditor.md +1 -0
  74. package/plugins/litclaude/agents/native-flow-reviewer.md +1 -0
  75. package/plugins/litclaude/agents/polish-orchestrator.md +1 -0
  76. package/plugins/litclaude/bin/litclaude-hook.js +347 -35
  77. package/plugins/litclaude/bin/litclaude-hud.js +29 -1
  78. package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
  79. package/plugins/litclaude/bundled-rules/lit-humanizer.md +14 -0
  80. package/plugins/litclaude/commands/korean-ai-slop-remover.md +2 -2
  81. package/plugins/litclaude/commands/lit-comprehend.md +11 -11
  82. package/plugins/litclaude/commands/lit-humanizer.md +75 -0
  83. package/plugins/litclaude/commands/lit-korean.md +4 -130
  84. package/plugins/litclaude/commands/litresearch.md +12 -10
  85. package/plugins/litclaude/hooks/hooks.json +1 -1
  86. package/plugins/litclaude/lib/canonical-runtime-commitments.mjs +2 -2
  87. package/plugins/litclaude/lib/canonical-skill-catalog.mjs +5 -1
  88. package/plugins/litclaude/lib/canonical-skill-resources.mjs +618 -13
  89. package/plugins/litclaude/lib/deliverable-hedge-guard.mjs +256 -350
  90. package/plugins/litclaude/lib/hud-accent-palette.mjs +33 -0
  91. package/plugins/litclaude/lib/interface-mode.mjs +33 -0
  92. package/plugins/litclaude/lib/interface-probe-gate.mjs +125 -0
  93. package/plugins/litclaude/lib/jev-skill-hint.mjs +445 -0
  94. package/plugins/litclaude/lib/motion-render-gate.mjs +164 -0
  95. package/plugins/litclaude/lib/mutated-file-paths.mjs +32 -1
  96. package/plugins/litclaude/lib/office-runtime-lock/package-lock.json +745 -0
  97. package/plugins/litclaude/lib/office-runtime-lock/package.json +12 -0
  98. package/plugins/litclaude/lib/office-runtime-lock/requirements.lock +15 -0
  99. package/plugins/litclaude/lib/office-runtime.mjs +255 -0
  100. package/plugins/litclaude/lib/office_data.py +493 -0
  101. package/plugins/litclaude/lib/office_runtime_bootstrap.py +61 -0
  102. package/plugins/litclaude/lib/ooxml_integrity.py +199 -0
  103. package/plugins/litclaude/lib/rename-aliases.mjs +3 -1
  104. package/plugins/litclaude/lib/render_pages.py +105 -0
  105. package/plugins/litclaude/skills/autoconference/SKILL.md +2 -0
  106. package/plugins/litclaude/skills/browser-drive/SKILL.md +19 -6
  107. package/plugins/litclaude/skills/browser-drive/scripts/capability-probe.mjs +44 -8
  108. package/plugins/litclaude/skills/frontend-ui-ux/ATTRIBUTION.md +26 -0
  109. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +50 -16
  110. package/plugins/litclaude/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
  111. package/plugins/litclaude/skills/frontend-ui-ux/references/complete-contract.md +32 -6
  112. package/plugins/litclaude/skills/frontend-ui-ux/references/craft-floor.md +257 -0
  113. package/plugins/litclaude/skills/frontend-ui-ux/references/default-editorial-pixel.json +6 -0
  114. package/plugins/litclaude/skills/frontend-ui-ux/references/interaction-motion.md +34 -88
  115. package/plugins/litclaude/skills/frontend-ui-ux/references/motion-guide.md +130 -0
  116. package/plugins/litclaude/skills/frontend-ui-ux/references/slop-register.md +150 -0
  117. package/plugins/litclaude/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -85
  118. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1534 -0
  119. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +141 -0
  120. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
  121. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +129 -0
  122. package/plugins/litclaude/skills/frontend-ui-ux/scripts/interface-probe.mjs +316 -0
  123. package/plugins/litclaude/skills/lit-comprehend/SKILL.md +34 -43
  124. package/plugins/litclaude/skills/lit-comprehend/references/artifact-template.md +5 -44
  125. package/plugins/litclaude/skills/lit-comprehend/scripts/verify-explainer.mjs +6 -16
  126. package/plugins/litclaude/skills/lit-diagram-drawer/NOTICE +33 -0
  127. package/plugins/litclaude/skills/lit-diagram-drawer/SKILL.md +123 -0
  128. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-architecture-dark.html +1 -0
  129. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-architecture-full.html +1 -0
  130. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-architecture-light.html +1 -0
  131. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bar-dark.html +1 -0
  132. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bar-full.html +1 -0
  133. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bar-light.html +1 -0
  134. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-beeswarm-dark.html +1 -0
  135. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-beeswarm-full.html +1 -0
  136. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-beeswarm-light.html +1 -0
  137. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bubble-dark.html +1 -0
  138. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bubble-full.html +1 -0
  139. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bubble-light.html +1 -0
  140. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bump-dark.html +1 -0
  141. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bump-full.html +1 -0
  142. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-bump-light.html +1 -0
  143. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-data-flow-dark.html +1 -0
  144. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-data-flow-full.html +1 -0
  145. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-data-flow-light.html +1 -0
  146. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-datalake-dark.html +1 -0
  147. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-datalake-full.html +1 -0
  148. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-datalake-light.html +1 -0
  149. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-db-schema-dark.html +1 -0
  150. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-db-schema-full.html +1 -0
  151. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-db-schema-light.html +1 -0
  152. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dependency-dark.html +1 -0
  153. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dependency-full.html +1 -0
  154. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dependency-light.html +1 -0
  155. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-deployment-dark.html +1 -0
  156. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-deployment-full.html +1 -0
  157. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-deployment-light.html +1 -0
  158. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-integration-dark.html +1 -0
  159. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-integration-full.html +1 -0
  160. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-integration-light.html +1 -0
  161. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-dark.html +1 -0
  162. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-full.html +1 -0
  163. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-dp-security-matrix-light.html +1 -0
  164. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-er-dark.html +1 -0
  165. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-er-full.html +1 -0
  166. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-er-light.html +1 -0
  167. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-fishbone-dark.html +1 -0
  168. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-fishbone-full.html +1 -0
  169. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-fishbone-light.html +1 -0
  170. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-flowchart-dark.html +1 -0
  171. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-flowchart-full.html +1 -0
  172. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-flowchart-light.html +1 -0
  173. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-gantt-dark.html +1 -0
  174. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-gantt-full.html +1 -0
  175. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-gantt-light.html +1 -0
  176. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-heatmap-dark.html +1 -0
  177. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-heatmap-full.html +1 -0
  178. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-heatmap-light.html +1 -0
  179. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-dark.html +1 -0
  180. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-full.html +1 -0
  181. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-light.html +1 -0
  182. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-dark.html +1 -0
  183. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-full.html +1 -0
  184. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-high-level-vertical-light.html +1 -0
  185. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-drawio-dark.html +1 -0
  186. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-drawio-full.html +1 -0
  187. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-drawio-light.html +1 -0
  188. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-dark.html +1 -0
  189. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-full.html +1 -0
  190. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-excalidraw-light.html +1 -0
  191. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-dark.html +1 -0
  192. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-full.html +1 -0
  193. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-import-mermaid-light.html +1 -0
  194. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-it-state-dark.html +1 -0
  195. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-it-state-full.html +1 -0
  196. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-it-state-light.html +1 -0
  197. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-journey-dark.html +1 -0
  198. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-journey-full.html +1 -0
  199. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-journey-light.html +1 -0
  200. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-kanban-dark.html +1 -0
  201. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-kanban-full.html +1 -0
  202. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-kanban-light.html +1 -0
  203. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-layers-dark.html +1 -0
  204. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-layers-full.html +1 -0
  205. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-layers-light.html +1 -0
  206. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-line-dark.html +1 -0
  207. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-line-full.html +1 -0
  208. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-line-light.html +1 -0
  209. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-dark.html +1 -0
  210. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-full.html +1 -0
  211. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-light.html +1 -0
  212. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-dark.html +1 -0
  213. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-full.html +1 -0
  214. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-loop-terminal-light.html +1 -0
  215. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-marimekko-dark.html +1 -0
  216. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-marimekko-full.html +1 -0
  217. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-marimekko-light.html +1 -0
  218. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-medallion-dark.html +1 -0
  219. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-medallion-full.html +1 -0
  220. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-medallion-light.html +1 -0
  221. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-nested-dark.html +1 -0
  222. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-nested-full.html +1 -0
  223. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-nested-light.html +1 -0
  224. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-org-chart-dark.html +1 -0
  225. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-org-chart-full.html +1 -0
  226. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-org-chart-light.html +1 -0
  227. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-dark.html +1 -0
  228. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-full.html +1 -0
  229. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-paved-road-animated-light.html +1 -0
  230. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-polar-dark.html +1 -0
  231. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-polar-full.html +1 -0
  232. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-polar-light.html +1 -0
  233. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-dark.html +1 -0
  234. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-full.html +1 -0
  235. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-policy-trace-animated-light.html +1 -0
  236. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-process-dark.html +1 -0
  237. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-process-full.html +1 -0
  238. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-process-light.html +1 -0
  239. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-pyramid-dark.html +1 -0
  240. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-pyramid-full.html +1 -0
  241. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-pyramid-light.html +1 -0
  242. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-dark.html +1 -0
  243. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-full.html +1 -0
  244. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-consultant-light.html +1 -0
  245. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-dark.html +1 -0
  246. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-full.html +1 -0
  247. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-quadrant-light.html +1 -0
  248. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-queue-animated-dark.html +1 -0
  249. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-queue-animated-full.html +1 -0
  250. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-queue-animated-light.html +1 -0
  251. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-radar-dark.html +1 -0
  252. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-radar-full.html +1 -0
  253. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-radar-light.html +1 -0
  254. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-ridgeline-dark.html +1 -0
  255. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-ridgeline-full.html +1 -0
  256. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-ridgeline-light.html +1 -0
  257. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sankey-dark.html +1 -0
  258. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sankey-full.html +1 -0
  259. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sankey-light.html +1 -0
  260. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-scatter-dark.html +1 -0
  261. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-scatter-full.html +1 -0
  262. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-scatter-light.html +1 -0
  263. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-dark.html +1 -0
  264. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-full.html +1 -0
  265. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-light.html +1 -0
  266. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-dark.html +1 -0
  267. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-full.html +1 -0
  268. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-sequence-oauth-light.html +1 -0
  269. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-slopegraph-dark.html +1 -0
  270. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-slopegraph-full.html +1 -0
  271. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-slopegraph-light.html +1 -0
  272. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-dark.html +1 -0
  273. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-full.html +1 -0
  274. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-dark.html +1 -0
  275. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-full.html +1 -0
  276. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-lifecycle-light.html +1 -0
  277. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-state-light.html +1 -0
  278. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-story-map-dark.html +1 -0
  279. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-story-map-full.html +1 -0
  280. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-story-map-light.html +1 -0
  281. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-streamgraph-dark.html +1 -0
  282. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-streamgraph-full.html +1 -0
  283. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-streamgraph-light.html +1 -0
  284. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-swimlane-dark.html +1 -0
  285. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-swimlane-full.html +1 -0
  286. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-swimlane-light.html +1 -0
  287. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-timeline-dark.html +1 -0
  288. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-timeline-full.html +1 -0
  289. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-timeline-light.html +1 -0
  290. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-dark.html +1 -0
  291. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-full.html +1 -0
  292. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-block-decomposition-light.html +1 -0
  293. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-dark.html +1 -0
  294. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-full.html +1 -0
  295. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-tree-light.html +1 -0
  296. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-treemap-dark.html +1 -0
  297. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-treemap-full.html +1 -0
  298. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-treemap-light.html +1 -0
  299. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-uml-class-dark.html +1 -0
  300. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-uml-class-full.html +1 -0
  301. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-uml-class-light.html +1 -0
  302. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-venn-dark.html +1 -0
  303. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-venn-full.html +1 -0
  304. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-venn-light.html +1 -0
  305. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-wardley-dark.html +1 -0
  306. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-wardley-full.html +1 -0
  307. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-wardley-light.html +1 -0
  308. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-waterfall-dark.html +1 -0
  309. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-waterfall-full.html +1 -0
  310. package/plugins/litclaude/skills/lit-diagram-drawer/assets/examples/type-waterfall-light.html +1 -0
  311. package/plugins/litclaude/skills/lit-diagram-drawer/assets/fonts/OFL.txt +94 -0
  312. package/plugins/litclaude/skills/lit-diagram-drawer/assets/fonts/PretendardVariable.woff2 +0 -0
  313. package/plugins/litclaude/skills/lit-diagram-drawer/assets/fonts/provenance.json +22 -0
  314. package/plugins/litclaude/skills/lit-diagram-drawer/assets/icons.html +231 -0
  315. package/plugins/litclaude/skills/lit-diagram-drawer/assets/index.html +1 -0
  316. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/devicon-MIT.txt +20 -0
  317. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/diagram-design-MIT.txt +21 -0
  318. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/log-z-MIT.txt +21 -0
  319. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/simple-icons-CC0.md +30 -0
  320. package/plugins/litclaude/skills/lit-diagram-drawer/assets/licenses/tabler-MIT.txt +21 -0
  321. package/plugins/litclaude/skills/lit-diagram-drawer/examples/01-service-architecture/after.html +1 -0
  322. package/plugins/litclaude/skills/lit-diagram-drawer/examples/01-service-architecture/brief.md +33 -0
  323. package/plugins/litclaude/skills/lit-diagram-drawer/examples/02-release-decision/after.html +1 -0
  324. package/plugins/litclaude/skills/lit-diagram-drawer/examples/02-release-decision/brief.md +18 -0
  325. package/plugins/litclaude/skills/lit-diagram-drawer/examples/03-data-retention/after.html +1 -0
  326. package/plugins/litclaude/skills/lit-diagram-drawer/examples/03-data-retention/brief.md +19 -0
  327. package/plugins/litclaude/skills/lit-diagram-drawer/examples/04-oauth-sequence/after.html +1 -0
  328. package/plugins/litclaude/skills/lit-diagram-drawer/examples/04-oauth-sequence/brief.md +17 -0
  329. package/plugins/litclaude/skills/lit-diagram-drawer/examples/05-review-lanes/after.html +1 -0
  330. package/plugins/litclaude/skills/lit-diagram-drawer/examples/05-review-lanes/brief.md +17 -0
  331. package/plugins/litclaude/skills/lit-diagram-drawer/examples/06-rollout-gates/after.html +1 -0
  332. package/plugins/litclaude/skills/lit-diagram-drawer/examples/06-rollout-gates/brief.md +17 -0
  333. package/plugins/litclaude/skills/lit-diagram-drawer/examples/07-deployment-boundary/after.html +1 -0
  334. package/plugins/litclaude/skills/lit-diagram-drawer/examples/07-deployment-boundary/brief.md +19 -0
  335. package/plugins/litclaude/skills/lit-diagram-drawer/examples/08-policy-traces/after.html +1 -0
  336. package/plugins/litclaude/skills/lit-diagram-drawer/examples/08-policy-traces/brief.md +20 -0
  337. package/plugins/litclaude/skills/lit-diagram-drawer/references/accessibility.md +37 -0
  338. package/plugins/litclaude/skills/lit-diagram-drawer/references/export.md +29 -0
  339. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-drawio.md +21 -0
  340. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-excalidraw.md +14 -0
  341. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-mermaid.md +18 -0
  342. package/plugins/litclaude/skills/lit-diagram-drawer/references/import-schema.md +28 -0
  343. package/plugins/litclaude/skills/lit-diagram-drawer/references/korean-typography.md +34 -0
  344. package/plugins/litclaude/skills/lit-diagram-drawer/references/motion.md +29 -0
  345. package/plugins/litclaude/skills/lit-diagram-drawer/references/office-pptx-docx.md +42 -0
  346. package/plugins/litclaude/skills/lit-diagram-drawer/references/output-spec.md +48 -0
  347. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-annotation.md +12 -0
  348. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-icons.md +21 -0
  349. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-sketchy.md +11 -0
  350. package/plugins/litclaude/skills/lit-diagram-drawer/references/primitive-terminal.md +11 -0
  351. package/plugins/litclaude/skills/lit-diagram-drawer/references/profiles.md +24 -0
  352. package/plugins/litclaude/skills/lit-diagram-drawer/references/semantic-patterns.md +27 -0
  353. package/plugins/litclaude/skills/lit-diagram-drawer/references/style-guide.md +67 -0
  354. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-architecture.md +51 -0
  355. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-bar.md +51 -0
  356. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-beeswarm.md +49 -0
  357. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-bubble.md +49 -0
  358. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-bump.md +49 -0
  359. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-catalog.json +1433 -0
  360. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-data-flow.md +51 -0
  361. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-datalake.md +49 -0
  362. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-db-schema.md +49 -0
  363. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-dependency.md +49 -0
  364. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-deployment.md +49 -0
  365. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-dp-integration.md +49 -0
  366. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-dp-security-matrix.md +49 -0
  367. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-er.md +49 -0
  368. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-fishbone.md +49 -0
  369. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-flowchart.md +49 -0
  370. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-gantt.md +49 -0
  371. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-heatmap.md +53 -0
  372. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-high-level-vertical.md +53 -0
  373. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-high-level.md +53 -0
  374. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-import-drawio.md +53 -0
  375. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-import-excalidraw.md +53 -0
  376. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-import-mermaid.md +53 -0
  377. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-it-state.md +54 -0
  378. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-journey.md +54 -0
  379. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-kanban.md +54 -0
  380. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-layers.md +54 -0
  381. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-line.md +54 -0
  382. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-loop-terminal.md +53 -0
  383. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-loop.md +54 -0
  384. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-marimekko.md +54 -0
  385. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-medallion.md +54 -0
  386. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-nested.md +59 -0
  387. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-org-chart.md +58 -0
  388. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-paved-road-animated.md +72 -0
  389. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-polar.md +67 -0
  390. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-policy-trace-animated.md +69 -0
  391. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-process.md +58 -0
  392. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-pyramid.md +59 -0
  393. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-quadrant-consultant.md +60 -0
  394. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-quadrant.md +57 -0
  395. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-queue-animated.md +63 -0
  396. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-radar.md +57 -0
  397. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-ridgeline.md +63 -0
  398. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-sankey.md +68 -0
  399. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-scatter.md +60 -0
  400. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-sequence-oauth.md +66 -0
  401. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-sequence.md +63 -0
  402. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-slopegraph.md +65 -0
  403. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-state-lifecycle.md +53 -0
  404. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-state.md +59 -0
  405. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-story-map.md +52 -0
  406. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-streamgraph.md +53 -0
  407. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-swimlane.md +52 -0
  408. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-timeline.md +52 -0
  409. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-tree-block-decomposition.md +57 -0
  410. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-tree.md +54 -0
  411. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-treemap.md +54 -0
  412. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-uml-class.md +52 -0
  413. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-venn.md +51 -0
  414. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-wardley.md +52 -0
  415. package/plugins/litclaude/skills/lit-diagram-drawer/references/type-waterfall.md +54 -0
  416. package/plugins/litclaude/skills/lit-diagram-drawer/references/verifier-guide.md +66 -0
  417. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/block-registry.mjs +43 -0
  418. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/brief-contract.mjs +94 -0
  419. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/check-visible-text.mjs +15 -0
  420. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/diagram-metrics.mjs +223 -0
  421. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/doctor.mjs +35 -0
  422. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/drawio-extract.mjs +635 -0
  423. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/excalidraw-extract.mjs +319 -0
  424. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/export.mjs +116 -0
  425. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/mermaid-extract.mjs +438 -0
  426. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/office-safety.mjs +54 -0
  427. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/renderer.mjs +33 -0
  428. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-all.mjs +58 -0
  429. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-brief.mjs +22 -0
  430. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-coverage.mjs +56 -0
  431. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-diagram.mjs +27 -0
  432. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-motion.mjs +12 -0
  433. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/verify-type.mjs +48 -0
  434. package/plugins/litclaude/skills/lit-diagram-drawer/scripts/visual-quality.mjs +376 -0
  435. package/plugins/litclaude/skills/lit-docx/NOTICE +33 -0
  436. package/plugins/litclaude/skills/lit-docx/SKILL.md +170 -0
  437. package/plugins/litclaude/skills/lit-docx/references/frontmatter_schema.md +42 -0
  438. package/plugins/litclaude/skills/lit-docx/references/journal_style_spec.md +17 -0
  439. package/plugins/litclaude/skills/lit-docx/references/markdown_quality_checklist.md +37 -0
  440. package/plugins/litclaude/skills/lit-docx/references/slop_phrase_list.yaml +58 -0
  441. package/plugins/litclaude/skills/lit-docx/references/slop_rules.md +35 -0
  442. package/plugins/litclaude/skills/lit-docx/scripts/clean_markdown.py +95 -0
  443. package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_docx.py +1194 -0
  444. package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_pdf.py +290 -0
  445. package/plugins/litclaude/skills/lit-docx/scripts/convert_pdf.py +139 -0
  446. package/plugins/litclaude/skills/lit-docx/scripts/edit_docx.py +306 -0
  447. package/plugins/litclaude/skills/lit-docx/scripts/embed_images.py +78 -0
  448. package/plugins/litclaude/skills/lit-docx/scripts/generate_docx_templates.py +500 -0
  449. package/plugins/litclaude/skills/lit-docx/scripts/qa_docx.py +177 -0
  450. package/plugins/litclaude/skills/lit-docx/scripts/slop_lint.py +561 -0
  451. package/plugins/litclaude/skills/lit-docx/scripts/visual_audit.py +48 -0
  452. package/plugins/litclaude/skills/lit-docx/templates/docx/acs.docx +0 -0
  453. package/plugins/litclaude/skills/lit-docx/templates/docx/elsevier.docx +0 -0
  454. package/plugins/litclaude/skills/lit-docx/templates/docx/ieee.docx +0 -0
  455. package/plugins/litclaude/skills/lit-docx/templates/docx/korean-generic.docx +0 -0
  456. package/plugins/litclaude/skills/lit-docx/templates/docx/nature.docx +0 -0
  457. package/plugins/litclaude/skills/lit-docx/templates/latex/acs/README.md +5 -0
  458. package/plugins/litclaude/skills/lit-docx/templates/latex/acs/template.tex +37 -0
  459. package/plugins/litclaude/skills/lit-docx/templates/latex/elsevier/README.md +5 -0
  460. package/plugins/litclaude/skills/lit-docx/templates/latex/elsevier/template.tex +38 -0
  461. package/plugins/litclaude/skills/lit-docx/templates/latex/ieee/README.md +5 -0
  462. package/plugins/litclaude/skills/lit-docx/templates/latex/ieee/template.tex +37 -0
  463. package/plugins/litclaude/skills/lit-docx/templates/latex/korean-generic/README.md +5 -0
  464. package/plugins/litclaude/skills/lit-docx/templates/latex/korean-generic/template.tex +37 -0
  465. package/plugins/litclaude/skills/lit-docx/templates/latex/nature/README.md +5 -0
  466. package/plugins/litclaude/skills/lit-docx/templates/latex/nature/template.tex +37 -0
  467. package/plugins/litclaude/skills/lit-docx/templates/registry.yaml +1366 -0
  468. package/plugins/litclaude/skills/lit-humanizer/NOTICE +160 -0
  469. package/plugins/litclaude/skills/lit-humanizer/SKILL.md +164 -0
  470. package/plugins/litclaude/skills/lit-humanizer/assets/always-on-rule.md +9 -0
  471. package/plugins/litclaude/skills/lit-humanizer/assets/report-skeleton.md +25 -0
  472. package/plugins/litclaude/skills/lit-humanizer/assets/slide-text-skeleton.md +28 -0
  473. package/plugins/litclaude/skills/lit-humanizer/bench/prompts.md +73 -0
  474. package/plugins/litclaude/skills/lit-humanizer/examples/accessibility-note/after.md +7 -0
  475. package/plugins/litclaude/skills/lit-humanizer/examples/accessibility-note/before.md +7 -0
  476. package/plugins/litclaude/skills/lit-humanizer/examples/api-reference/after.md +9 -0
  477. package/plugins/litclaude/skills/lit-humanizer/examples/api-reference/before.md +15 -0
  478. package/plugins/litclaude/skills/lit-humanizer/examples/changelog/after.md +5 -0
  479. package/plugins/litclaude/skills/lit-humanizer/examples/changelog/before.md +12 -0
  480. package/plugins/litclaude/skills/lit-humanizer/examples/code-comments/after.md +10 -0
  481. package/plugins/litclaude/skills/lit-humanizer/examples/code-comments/before.md +19 -0
  482. package/plugins/litclaude/skills/lit-humanizer/examples/commit/after.md +5 -0
  483. package/plugins/litclaude/skills/lit-humanizer/examples/commit/before.md +7 -0
  484. package/plugins/litclaude/skills/lit-humanizer/examples/data-release/after.md +7 -0
  485. package/plugins/litclaude/skills/lit-humanizer/examples/data-release/before.md +9 -0
  486. package/plugins/litclaude/skills/lit-humanizer/examples/english-essay/after.md +5 -0
  487. package/plugins/litclaude/skills/lit-humanizer/examples/english-essay/before.md +5 -0
  488. package/plugins/litclaude/skills/lit-humanizer/examples/incident-update/after.md +7 -0
  489. package/plugins/litclaude/skills/lit-humanizer/examples/incident-update/before.md +7 -0
  490. package/plugins/litclaude/skills/lit-humanizer/examples/ko-essay/after.md +7 -0
  491. package/plugins/litclaude/skills/lit-humanizer/examples/ko-essay/before.md +9 -0
  492. package/plugins/litclaude/skills/lit-humanizer/examples/ko-report/after.md +9 -0
  493. package/plugins/litclaude/skills/lit-humanizer/examples/ko-report/before.md +13 -0
  494. package/plugins/litclaude/skills/lit-humanizer/examples/ko-slide/after.md +19 -0
  495. package/plugins/litclaude/skills/lit-humanizer/examples/ko-slide/before.md +21 -0
  496. package/plugins/litclaude/skills/lit-humanizer/examples/meeting-brief/after.md +9 -0
  497. package/plugins/litclaude/skills/lit-humanizer/examples/meeting-brief/before.md +11 -0
  498. package/plugins/litclaude/skills/lit-humanizer/examples/policy-faq/after.md +9 -0
  499. package/plugins/litclaude/skills/lit-humanizer/examples/policy-faq/before.md +9 -0
  500. package/plugins/litclaude/skills/lit-humanizer/examples/pr/after.md +15 -0
  501. package/plugins/litclaude/skills/lit-humanizer/examples/pr/before.md +17 -0
  502. package/plugins/litclaude/skills/lit-humanizer/examples/project-update/after.md +7 -0
  503. package/plugins/litclaude/skills/lit-humanizer/examples/project-update/before.md +9 -0
  504. package/plugins/litclaude/skills/lit-humanizer/examples/readme/after.md +12 -0
  505. package/plugins/litclaude/skills/lit-humanizer/examples/readme/before.md +14 -0
  506. package/plugins/litclaude/skills/lit-humanizer/examples/report/after.md +13 -0
  507. package/plugins/litclaude/skills/lit-humanizer/examples/report/before.md +11 -0
  508. package/plugins/litclaude/skills/lit-humanizer/examples/research-abstract/after.md +5 -0
  509. package/plugins/litclaude/skills/lit-humanizer/examples/research-abstract/before.md +5 -0
  510. package/plugins/litclaude/skills/lit-humanizer/examples/slides/after.md +19 -0
  511. package/plugins/litclaude/skills/lit-humanizer/examples/slides/before.md +19 -0
  512. package/plugins/litclaude/skills/lit-humanizer/examples/troubleshooting/after.md +11 -0
  513. package/plugins/litclaude/skills/lit-humanizer/examples/troubleshooting/before.md +11 -0
  514. package/plugins/litclaude/skills/lit-humanizer/motion-guide.md +123 -0
  515. package/plugins/litclaude/skills/lit-humanizer/references/code-patterns.md +468 -0
  516. package/plugins/litclaude/skills/lit-humanizer/references/deliverable-channels.md +111 -0
  517. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns-checklist.md +43 -0
  518. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns-content.md +243 -0
  519. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns-structure.md +375 -0
  520. package/plugins/litclaude/skills/lit-humanizer/references/en-patterns.md +57 -0
  521. package/plugins/litclaude/skills/lit-humanizer/references/ko-metrics.md +33 -0
  522. package/plugins/litclaude/skills/lit-humanizer/references/ko-patterns-a-d.md +659 -0
  523. package/plugins/litclaude/skills/lit-humanizer/references/ko-patterns-e-j.md +387 -0
  524. package/plugins/litclaude/skills/lit-humanizer/references/ko-patterns.md +31 -0
  525. package/plugins/litclaude/skills/lit-humanizer/references/rewrite-playbook.md +54 -0
  526. package/plugins/litclaude/skills/lit-humanizer/references/taxonomy.md +153 -0
  527. package/plugins/litclaude/skills/lit-humanizer/rules.json +884 -0
  528. package/plugins/litclaude/skills/lit-humanizer/scripts/artifact-text.mjs +38 -0
  529. package/plugins/litclaude/skills/lit-humanizer/scripts/core.mjs +207 -0
  530. package/plugins/litclaude/skills/lit-humanizer/scripts/detect.mjs +64 -0
  531. package/plugins/litclaude/skills/lit-humanizer/scripts/extract_office_text.py +112 -0
  532. package/plugins/litclaude/skills/lit-humanizer/scripts/ko-metrics.mjs +124 -0
  533. package/plugins/litclaude/skills/lit-humanizer/scripts/scan-input.mjs +29 -0
  534. package/plugins/litclaude/skills/lit-loop/SKILL.md +30 -1
  535. package/plugins/litclaude/skills/lit-plan/SKILL.md +4 -1
  536. package/plugins/litclaude/skills/lit-pptx/NOTICE +38 -0
  537. package/plugins/litclaude/skills/lit-pptx/SKILL.md +207 -0
  538. package/plugins/litclaude/skills/lit-pptx/assets/azure/circle-lg.png +0 -0
  539. package/plugins/litclaude/skills/lit-pptx/assets/azure/circle-md.png +0 -0
  540. package/plugins/litclaude/skills/lit-pptx/assets/azure/circle-sm.png +0 -0
  541. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Black.otf +0 -0
  542. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Bold.otf +0 -0
  543. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Light.otf +0 -0
  544. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Medium.otf +0 -0
  545. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/A2Z-Regular.otf +0 -0
  546. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/OFL.txt +94 -0
  547. package/plugins/litclaude/skills/lit-pptx/fonts/a2z/SOURCE.md +29 -0
  548. package/plugins/litclaude/skills/lit-pptx/fonts/pretendard/OFL.txt +94 -0
  549. package/plugins/litclaude/skills/lit-pptx/fonts/pretendard/Pretendard-Bold.otf +0 -0
  550. package/plugins/litclaude/skills/lit-pptx/fonts/pretendard/Pretendard-Regular.otf +0 -0
  551. package/plugins/litclaude/skills/lit-pptx/fonts/provenance.json +78 -0
  552. package/plugins/litclaude/skills/lit-pptx/references/agents/analyst.md +49 -0
  553. package/plugins/litclaude/skills/lit-pptx/references/agents/antithesis.md +59 -0
  554. package/plugins/litclaude/skills/lit-pptx/references/agents/architect.md +44 -0
  555. package/plugins/litclaude/skills/lit-pptx/references/agents/critic.md +42 -0
  556. package/plugins/litclaude/skills/lit-pptx/references/agents/synthesizer.md +47 -0
  557. package/plugins/litclaude/skills/lit-pptx/references/agents/thesis.md +33 -0
  558. package/plugins/litclaude/skills/lit-pptx/references/anti-slop-checklist.md +81 -0
  559. package/plugins/litclaude/skills/lit-pptx/references/authoring-guide.md +173 -0
  560. package/plugins/litclaude/skills/lit-pptx/references/consensus-protocol.md +125 -0
  561. package/plugins/litclaude/skills/lit-pptx/references/data-display-cookbook.md +45 -0
  562. package/plugins/litclaude/skills/lit-pptx/references/design-system-azure.md +58 -0
  563. package/plugins/litclaude/skills/lit-pptx/references/interview-protocol.md +113 -0
  564. package/plugins/litclaude/skills/lit-pptx/references/stages/autopilot.md +48 -0
  565. package/plugins/litclaude/skills/lit-pptx/references/stages/interview.md +52 -0
  566. package/plugins/litclaude/skills/lit-pptx/references/stages/plan.md +42 -0
  567. package/plugins/litclaude/skills/lit-pptx/scripts/compile-deck.js +414 -0
  568. package/plugins/litclaude/skills/lit-pptx/scripts/craft_extras.py +373 -0
  569. package/plugins/litclaude/skills/lit-pptx/scripts/deck_craft.py +321 -0
  570. package/plugins/litclaude/skills/lit-pptx/scripts/embed_fonts.py +215 -0
  571. package/plugins/litclaude/skills/lit-pptx/scripts/forbidden-terms.json +55 -0
  572. package/plugins/litclaude/skills/lit-pptx/scripts/layout_inventory.py +418 -0
  573. package/plugins/litclaude/skills/lit-pptx/scripts/learn_template.py +446 -0
  574. package/plugins/litclaude/skills/lit-pptx/scripts/lib/derive-palette.js +176 -0
  575. package/plugins/litclaude/skills/lit-pptx/scripts/lib/directive-parser.js +244 -0
  576. package/plugins/litclaude/skills/lit-pptx/scripts/lib/font-map.js +79 -0
  577. package/plugins/litclaude/skills/lit-pptx/scripts/lib/gen-bg.js +86 -0
  578. package/plugins/litclaude/skills/lit-pptx/scripts/lib/gen-gradient-assets.js +37 -0
  579. package/plugins/litclaude/skills/lit-pptx/scripts/lib/layout-resolver.js +768 -0
  580. package/plugins/litclaude/skills/lit-pptx/scripts/lib/markdown-loader.js +136 -0
  581. package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-html.js +356 -0
  582. package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +2094 -0
  583. package/plugins/litclaude/skills/lit-pptx/scripts/lib/runtime-require.js +28 -0
  584. package/plugins/litclaude/skills/lit-pptx/scripts/lib/slide-ast.js +561 -0
  585. package/plugins/litclaude/skills/lit-pptx/scripts/lib/spec-validator.js +94 -0
  586. package/plugins/litclaude/skills/lit-pptx/scripts/lib/template-registry.js +394 -0
  587. package/plugins/litclaude/skills/lit-pptx/scripts/package.json +4 -0
  588. package/plugins/litclaude/skills/lit-pptx/scripts/qa_deck.py +766 -0
  589. package/plugins/litclaude/skills/lit-pptx/scripts/validate_pptx.py +215 -0
  590. package/plugins/litclaude/skills/lit-pptx/specs/markdown-slide-spec-v1.md +251 -0
  591. package/plugins/litclaude/skills/lit-pptx/specs/markdown-slide-spec-v2.md +214 -0
  592. package/plugins/litclaude/skills/lit-pptx/specs/slide-ast-v1.schema.json +229 -0
  593. package/plugins/litclaude/skills/lit-pptx/specs/slide-ast-v2.schema.json +144 -0
  594. package/plugins/litclaude/skills/lit-pptx/specs/template-enrollment-contract.md +382 -0
  595. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/capabilities.yaml +146 -0
  596. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +454 -0
  597. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +37 -0
  598. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/capabilities.yaml +154 -0
  599. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +475 -0
  600. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +37 -0
  601. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/capabilities.yaml +179 -0
  602. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/layout-mapping.yaml +299 -0
  603. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-A2Z/template.yaml +45 -0
  604. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/capabilities.yaml +181 -0
  605. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/layout-mapping.yaml +299 -0
  606. package/plugins/litclaude/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/template.yaml +44 -0
  607. package/plugins/litclaude/skills/lit-scientific-visualization/SKILL.md +1 -0
  608. package/plugins/litclaude/skills/lit-typographic-motion/SKILL.md +131 -0
  609. package/plugins/litclaude/skills/lit-typographic-motion/engine/NOTICE +23 -0
  610. package/plugins/litclaude/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +56 -0
  611. package/plugins/litclaude/skills/lit-typographic-motion/engine/audio/beat_grid.py +56 -0
  612. package/plugins/litclaude/skills/lit-typographic-motion/engine/audio/requirements.lock +1084 -0
  613. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/constants.mjs +211 -0
  614. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/contrast.mjs +90 -0
  615. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/events.mjs +46 -0
  616. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/flash.mjs +211 -0
  617. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/gate-rules.mjs +550 -0
  618. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/presets.mjs +123 -0
  619. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/text.mjs +100 -0
  620. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/timeline.mjs +142 -0
  621. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/util.mjs +167 -0
  622. package/plugins/litclaude/skills/lit-typographic-motion/engine/core/voices.mjs +19 -0
  623. package/plugins/litclaude/skills/lit-typographic-motion/engine/models/word-timing.json +198 -0
  624. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/analysis.mjs +108 -0
  625. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/brief.mjs +158 -0
  626. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/chrome.mjs +330 -0
  627. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/coverage.mjs +83 -0
  628. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/done.mjs +104 -0
  629. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/encode.mjs +104 -0
  630. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/frame-worker.mjs +36 -0
  631. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/look.mjs +128 -0
  632. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/pipeline.mjs +592 -0
  633. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/png.mjs +112 -0
  634. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/report.mjs +66 -0
  635. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/runtime.mjs +330 -0
  636. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/session.mjs +145 -0
  637. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/sound-cli.mjs +60 -0
  638. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/sound.mjs +641 -0
  639. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-capture.mjs +178 -0
  640. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-gate.mjs +122 -0
  641. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-qa.mjs +295 -0
  642. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage-serve.mjs +278 -0
  643. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stage.mjs +503 -0
  644. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/stills.mjs +100 -0
  645. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/treatment.mjs +343 -0
  646. package/plugins/litclaude/skills/lit-typographic-motion/engine/node/ws-server.mjs +132 -0
  647. package/plugins/litclaude/skills/lit-typographic-motion/engine/package-lock.json +25 -0
  648. package/plugins/litclaude/skills/lit-typographic-motion/engine/package.json +10 -0
  649. package/plugins/litclaude/skills/lit-typographic-motion/engine/stage/init.js +288 -0
  650. package/plugins/litclaude/skills/lit-typographic-motion/engine/stage/qa.js +157 -0
  651. package/plugins/litclaude/skills/lit-typographic-motion/engine/stage/stage-kit.js +701 -0
  652. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/boot.mjs +123 -0
  653. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/engine.mjs +281 -0
  654. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/gl.mjs +319 -0
  655. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/glsl-common.mjs +78 -0
  656. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/index.html +6 -0
  657. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/lines.mjs +112 -0
  658. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/crt.mjs +98 -0
  659. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/dither.mjs +49 -0
  660. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/glitch.mjs +77 -0
  661. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/swiss-grid.mjs +61 -0
  662. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/terminal-ui.mjs +88 -0
  663. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/passes/tidal-gradient.mjs +86 -0
  664. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/post.mjs +165 -0
  665. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scale.mjs +15 -0
  666. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scene.mjs +82 -0
  667. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/end-card.mjs +30 -0
  668. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/index.mjs +16 -0
  669. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/karaoke-line.mjs +47 -0
  670. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/kinetic-list.mjs +45 -0
  671. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/number-counter.mjs +29 -0
  672. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/signature.mjs +73 -0
  673. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/scenes/title-slam.mjs +73 -0
  674. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/stroke.mjs +236 -0
  675. package/plugins/litclaude/skills/lit-typographic-motion/engine/web/type.mjs +180 -0
  676. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w100-400.ttf +0 -0
  677. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w100-700.ttf +0 -0
  678. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w100-900.ttf +0 -0
  679. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w125-400.ttf +0 -0
  680. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w125-700.ttf +0 -0
  681. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w125-900.ttf +0 -0
  682. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w75-400.ttf +0 -0
  683. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w75-700.ttf +0 -0
  684. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/Archivo-w75-900.ttf +0 -0
  685. package/plugins/litclaude/skills/lit-typographic-motion/fonts/archivo/OFL.txt +93 -0
  686. package/plugins/litclaude/skills/lit-typographic-motion/fonts/manifest.json +437 -0
  687. package/plugins/litclaude/skills/lit-typographic-motion/fonts/silkscreen/OFL.txt +93 -0
  688. package/plugins/litclaude/skills/lit-typographic-motion/fonts/silkscreen/Silkscreen-Bold.ttf +0 -0
  689. package/plugins/litclaude/skills/lit-typographic-motion/fonts/silkscreen/Silkscreen-Regular.ttf +0 -0
  690. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/CREDITS +42 -0
  691. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSAllure.svg +216 -0
  692. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSFelix.svg +216 -0
  693. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSOsmotron.svg +216 -0
  694. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSReadability.svg +216 -0
  695. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/EMSTech.svg +217 -0
  696. package/plugins/litclaude/skills/lit-typographic-motion/fonts/stroke/OFL.txt +91 -0
  697. package/plugins/litclaude/skills/lit-typographic-motion/fonts/vt323/OFL.txt +93 -0
  698. package/plugins/litclaude/skills/lit-typographic-motion/fonts/vt323/VT323-Regular.ttf +0 -0
  699. package/plugins/litclaude/skills/lit-typographic-motion/references/brief-schema.md +91 -0
  700. package/plugins/litclaude/skills/lit-typographic-motion/references/craft-loop.md +69 -0
  701. package/plugins/litclaude/skills/lit-typographic-motion/references/engine-contract.md +104 -0
  702. package/plugins/litclaude/skills/lit-typographic-motion/references/look.md +106 -0
  703. package/plugins/litclaude/skills/lit-typographic-motion/references/runtime.md +65 -0
  704. package/plugins/litclaude/skills/lit-typographic-motion/references/sound.md +83 -0
  705. package/plugins/litclaude/skills/lit-typographic-motion/references/stage.md +192 -0
  706. package/plugins/litclaude/skills/lit-typographic-motion/references/style-bibles.md +117 -0
  707. package/plugins/litclaude/skills/lit-typographic-motion/references/treatment.md +114 -0
  708. package/plugins/litclaude/skills/lit-typographic-motion/references/type-craft.md +79 -0
  709. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion-doctor.mjs +78 -0
  710. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion-runtime.mjs +67 -0
  711. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion.mjs +132 -0
  712. package/plugins/litclaude/skills/litresearch/SKILL.md +54 -18
  713. package/scripts/doctor.mjs +5 -0
  714. package/scripts/postinstall.mjs +1 -0
  715. package/plugins/litclaude/skills/lit-korean/SKILL.md +0 -327
  716. /package/plugins/litclaude/skills/{lit-korean/references/fidelity-checklist.md → lit-humanizer/references/litclaude-fidelity-checklist.md} +0 -0
  717. /package/plugins/litclaude/skills/{lit-korean/references/rewrite-rules.md → lit-humanizer/references/litclaude-rewrite-rules.md} +0 -0
  718. /package/plugins/litclaude/skills/{lit-korean/references/style-patterns.md → lit-humanizer/references/litclaude-style-patterns.md} +0 -0
@@ -0,0 +1,2094 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * render-adapter-pptx.js — Render a resolved slide spec to a PPTX file.
5
+ *
6
+ * Architecture: Each text line is its own separate shape (individual addText()
7
+ * call). Bullet items use manual "• " prefix in green (#00AE41) Bold 16pt.
8
+ * Section headers are separate shapes in Bold 18pt black.
9
+ *
10
+ * Input: resolved spec from layout-resolver, templateObj from registry, output path.
11
+ * Output: writes a .pptx file via pptxgenjs.
12
+ */
13
+
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const path = require("path");
17
+ const { runtimeRequire } = require("./runtime-require");
18
+ const { extractContent, looksLikeKpi } = require("./layout-resolver");
19
+
20
+ // Narrowest a KPI badge can be and still read as one glance.
21
+ const KPI_MIN_CARD_WIDTH_IN = 1.9;
22
+
23
+ const TOOLKIT_ROOT = path.resolve(__dirname, "../..");
24
+ const ASSETS_MEDIA = path.join(TOOLKIT_ROOT, "assets", "media");
25
+ const ASSETS_AZURE = path.join(TOOLKIT_ROOT, "assets", "azure");
26
+
27
+ /** Resolve a decoration asset path: explicit absolute override (custom-accent
28
+ * regenerated gradients), then d.dir "azure" → assets/azure, else assets/media. */
29
+ function decorAssetPath(d) {
30
+ if (d.assetPath) return d.assetPath;
31
+ if (d.dir === "azure") return path.join(ASSETS_AZURE, d.asset);
32
+ return path.join(ASSETS_MEDIA, d.asset);
33
+ }
34
+ const hex = (c) => (c || "#000000").replace("#", "");
35
+
36
+ // A template may carry its own disclaimer line (`text:` on the decoration); this
37
+ // generic one names no organisation.
38
+ const DEFAULT_DISCLAIMER =
39
+ "\u203B \uBCF8 \uBB38\uC11C\uB294 \uB300\uC678\uBE44\uB85C, \uAD8C\uD55C\uC774 \uC5C6\uB294 \uC790\uC5D0\uAC8C \uC5F4\uB78C, \uBCF5\uC0AC, \uBC30\uD3EC, \uACF5\uAC1C, \uB610\uB294 \uC0AC\uC6A9\uC774 \uAE08\uC9C0\uB429\uB2C8\uB2E4.";
40
+
41
+ function linkedTableCell(text, options) {
42
+ const original = cleanMd(text);
43
+ let display = original;
44
+ let url = null;
45
+ const markdown = original.match(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/i);
46
+ if (markdown) {
47
+ display = original.replace(markdown[0], markdown[1]);
48
+ url = markdown[2];
49
+ } else {
50
+ const rawUrl = original.match(/https?:\/\/[^\s<>\])}]+/i);
51
+ const doi = original.match(/\b10\.\d{4,9}\/[^\s<>\])}]+/i);
52
+ if (rawUrl) url = rawUrl[0].replace(/[.,;:]+$/, "");
53
+ else if (doi) url = `https://doi.org/${doi[0].replace(/[.,;:]+$/, "")}`;
54
+ }
55
+ return {
56
+ text: display,
57
+ options: {
58
+ ...options,
59
+ ...(url ? { hyperlink: { url } } : {}),
60
+ },
61
+ };
62
+ }
63
+
64
+ // ── Text style constants (defaults; every template reassigns them in render()).
65
+ // These are reassigned from the template at the start of render() so the same
66
+ // adapter serves any enrolled template (e.g. BOILERPLATE-PRETENDARD).
67
+ let FONT_BOLD = "Pretendard";
68
+ let FONT_MEDIUM = "Pretendard";
69
+ let FONT_LIGHT = "Pretendard";
70
+ let CHAR_SPACING = -0.7;
71
+ // Chart colours and faces for templates without the rich palette; set per render().
72
+ let PLAIN_CHART_STYLE = { series: ["3C3836", "689D6A", "D79921", "B16286"], ink: "3C3836", ink_muted: "6E6256", line: "CBC2B5", titleFont: "Pretendard", bodyFont: "Pretendard" };
73
+
74
+ function expandHome(p) {
75
+ if (p === "~") return os.homedir();
76
+ if (p && p.startsWith(`~${path.sep}`)) return path.join(os.homedir(), p.slice(2));
77
+ return p;
78
+ }
79
+
80
+ function resolveImagePath(src, sourceDir) {
81
+ if (!src) throw new Error("Image source is empty");
82
+ const expanded = expandHome(src);
83
+ const candidates = [];
84
+ if (path.isAbsolute(expanded)) candidates.push(expanded);
85
+ else {
86
+ if (sourceDir) candidates.push(path.resolve(sourceDir, expanded));
87
+ candidates.push(path.resolve(process.cwd(), expanded));
88
+ candidates.push(path.join(ASSETS_MEDIA, expanded));
89
+ candidates.push(path.resolve(TOOLKIT_ROOT, expanded));
90
+ }
91
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
92
+ if (!found) {
93
+ throw new Error(`Image not found: ${src}. Checked: ${candidates.join(", ")}`);
94
+ }
95
+ return found;
96
+ }
97
+
98
+ function readImageSize(filePath) {
99
+ const buf = fs.readFileSync(filePath);
100
+ if (buf.length >= 24 && buf.toString("ascii", 1, 4) === "PNG") {
101
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
102
+ }
103
+ if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8) {
104
+ let offset = 2;
105
+ while (offset + 9 < buf.length) {
106
+ if (buf[offset] !== 0xff) { offset++; continue; }
107
+ const marker = buf[offset + 1];
108
+ const length = buf.readUInt16BE(offset + 2);
109
+ if (length < 2) break;
110
+ if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) ||
111
+ (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
112
+ return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5) };
113
+ }
114
+ offset += 2 + length;
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function containBox(x, y, w, h, imageSize) {
121
+ if (!imageSize || !imageSize.width || !imageSize.height) return { x, y, w, h };
122
+ const boxRatio = w / h;
123
+ const imageRatio = imageSize.width / imageSize.height;
124
+ if (imageRatio > boxRatio) {
125
+ const fittedH = w / imageRatio;
126
+ return { x, y: y + (h - fittedH) / 2, w, h: fittedH };
127
+ }
128
+ const fittedW = h * imageRatio;
129
+ return { x: x + (w - fittedW) / 2, y, w: fittedW, h };
130
+ }
131
+
132
+ // Bullet items
133
+ let BULLET_FONT = FONT_BOLD;
134
+ const BULLET_SIZE = 16;
135
+ let BULLET_COLOR = "1D4ED8"; // bullet color (reassigned per template)
136
+ let SECTION_ACCENT = "1D4ED8"; // section-card / accent color (reassigned per template)
137
+ const BULLET_PREFIX = "\u2022 "; // "• " (bullet + two spaces)
138
+ const BULLET_X = 0.59;
139
+ const BULLET_W = 8.8;
140
+ const BULLET_H = 0.38;
141
+ const BULLET_Y_SPACING = 0.42;
142
+ const BULLET_LINE_SPACING = 1.25;
143
+
144
+ // Section headers
145
+ let SEC_HEADER_FONT = FONT_BOLD;
146
+ const SEC_HEADER_SIZE = 18;
147
+ const SEC_HEADER_COLOR = "000000";
148
+ const SEC_HEADER_X = 0.39;
149
+ const SEC_HEADER_W = 9.0;
150
+ const SEC_HEADER_H = 0.5;
151
+
152
+ // Summary (narrow width)
153
+ const SUMMARY_HEADER_W = 4.3;
154
+ const SUMMARY_BULLET_W = 4.8;
155
+ const SUMMARY_BULLET_SIZE = 14;
156
+ const SUMMARY_BULLET_H = 0.34;
157
+ const SUMMARY_BULLET_Y_SPACING = 0.38;
158
+ const SUMMARY_BULLET_LINE_SPACING = 1.05;
159
+
160
+ // TOC items (separate shapes)
161
+ let TOC_FONT = FONT_BOLD;
162
+ const TOC_SIZE = 18;
163
+ const TOC_COLOR = "000000";
164
+ const TOC_X = 0.390;
165
+ const TOC_W = 7.500;
166
+ const TOC_H = 0.550;
167
+ const TOC_Y_START = 0.957;
168
+ const TOC_Y_SPACING = 0.650;
169
+
170
+ function weightedTextLength(text) {
171
+ let total = 0;
172
+ for (const ch of String(text || "")) {
173
+ if (/\s/.test(ch)) total += 0.35;
174
+ else if (/^[\x00-\x7F]$/.test(ch)) total += 0.58;
175
+ else total += 1.0;
176
+ }
177
+ return total;
178
+ }
179
+
180
+ function estimateWrappedLines(text, widthIn, fontSize) {
181
+ const usablePt = Math.max(12, (widthIn || 4) * 72 - 10);
182
+ // Conservative on purpose: PowerPoint wraps dense mixed
183
+ // Korean-English strings earlier than a naive ASCII width estimate.
184
+ const unitsPerLine = Math.max(4, usablePt / Math.max(4, fontSize * 0.95));
185
+ return Math.max(1, Math.ceil(weightedTextLength(text) / unitsPerLine));
186
+ }
187
+
188
+ function estimateTextHeight(text, widthIn, fontSize, lineSpacingMultiple) {
189
+ const lines = estimateWrappedLines(text, widthIn, fontSize);
190
+ return Math.max(0.14, ((lines * fontSize * lineSpacingMultiple) / 72) * 1.4 + 0.08);
191
+ }
192
+
193
+ function bodyElements(items) {
194
+ const elements = [];
195
+ let numberedCounter = 0;
196
+ for (const item of items) {
197
+ if (item.type === "section") {
198
+ elements.push({ kind: "section", text: item.heading || "" });
199
+ numberedCounter = 0;
200
+ for (const child of item.children || []) {
201
+ numberedCounter++;
202
+ elements.push({ kind: "numbered", text: `(${numberedCounter}) ${child.text || ""}` });
203
+ }
204
+ } else if (item.type === "bullet") {
205
+ elements.push({ kind: "bullet", text: item.text || "" });
206
+ numberedCounter = 0;
207
+ for (const child of item.children || []) {
208
+ numberedCounter++;
209
+ elements.push({ kind: "numbered", text: `(${numberedCounter}) ${child.text || ""}` });
210
+ }
211
+ } else if (item.type === "numbered") {
212
+ numberedCounter++;
213
+ elements.push({ kind: "numbered", text: `(${numberedCounter}) ${item.text || ""}` });
214
+ } else if (item.type === "text") {
215
+ elements.push({ kind: "text", text: item.text || "" });
216
+ }
217
+ }
218
+ return elements;
219
+ }
220
+
221
+ function chooseBodyStyle(items, widthIn, startY, bottomY) {
222
+ const available = Math.max(0.5, (bottomY || 7.0) - startY);
223
+ const sizes = [16, 15, 14, 13];
224
+ for (const bodySize of sizes) {
225
+ const sectionSize = Math.min(18, bodySize + 2);
226
+ const lineSpacing = bodySize <= 10 ? 1.05 : 1.18;
227
+ const gap = bodySize <= 10 ? 0.035 : 0.05;
228
+ const total = bodyElements(items).reduce((sum, el) => {
229
+ const size = el.kind === "section" ? sectionSize : bodySize;
230
+ const w = el.kind === "section" ? Math.min(widthIn, SEC_HEADER_W) : widthIn;
231
+ return sum + estimateTextHeight(el.text, w, size, lineSpacing) + gap;
232
+ }, 0);
233
+ if (total <= available) return { bodySize, sectionSize, lineSpacing, gap };
234
+ }
235
+ return { bodySize: 13, sectionSize: 15, lineSpacing: 1.08, gap: 0.04 };
236
+ }
237
+
238
+ function estimateSectionHeight(item, secW, bulletW, style) {
239
+ let total = estimateTextHeight(item.heading || "", secW, style.sectionSize, style.lineSpacing) + style.gap;
240
+ let numberedCounter = 0;
241
+ for (const child of item.children || []) {
242
+ numberedCounter++;
243
+ total += estimateTextHeight(`(${numberedCounter}) ${child.text || ""}`, bulletW, style.bodySize, style.lineSpacing) + style.gap;
244
+ }
245
+ return total + 0.12;
246
+ }
247
+
248
+ function addSectionCard(slide, x, y, w, h) {
249
+ slide.addShape("rect", {
250
+ x,
251
+ y: y - 0.035,
252
+ w,
253
+ h: Math.max(0.28, h),
254
+ fill: { color: "F7F9F8", transparency: 12 },
255
+ line: { color: "D8E2DD", width: 0.5, transparency: 20 },
256
+ });
257
+ }
258
+
259
+ function bodyItemsToDiagnosticLines(items) {
260
+ const lines = [];
261
+ let numberedCounter = 0;
262
+ for (const item of items || []) {
263
+ if (item.type === "section") {
264
+ lines.push(`■ ${item.heading || ""}`);
265
+ numberedCounter = 0;
266
+ for (const child of item.children || []) {
267
+ numberedCounter++;
268
+ lines.push(`(${numberedCounter}) ${child.text || ""}`);
269
+ }
270
+ } else if (item.type === "bullet") {
271
+ lines.push(`• ${item.text || ""}`);
272
+ numberedCounter = 0;
273
+ for (const child of item.children || []) {
274
+ numberedCounter++;
275
+ lines.push(`(${numberedCounter}) ${child.text || ""}`);
276
+ }
277
+ } else if (item.type === "numbered") {
278
+ numberedCounter++;
279
+ lines.push(`(${numberedCounter}) ${item.text || ""}`);
280
+ } else if (item.type === "text") {
281
+ lines.push(item.text || "");
282
+ }
283
+ }
284
+ return lines.filter(Boolean);
285
+ }
286
+
287
+ function bodyItemsToDiagnosticCards(items) {
288
+ const cards = [];
289
+ let numberedCounter = 0;
290
+ for (const item of items || []) {
291
+ if (item.type === "section" || item.type === "bullet") {
292
+ const title = item.heading || item.text || "";
293
+ const body = [];
294
+ numberedCounter = 0;
295
+ for (const child of item.children || []) {
296
+ numberedCounter++;
297
+ body.push(`(${numberedCounter}) ${child.text || ""}`);
298
+ }
299
+ if (title || body.length) cards.push({ title, body });
300
+ } else if (item.type === "numbered") {
301
+ numberedCounter++;
302
+ cards.push({ title: `(${numberedCounter})`, body: [item.text || ""] });
303
+ } else if (item.type === "text" && item.text) {
304
+ cards.push({ title: "", body: [item.text] });
305
+ }
306
+ }
307
+ return cards;
308
+ }
309
+
310
+ function splitLinesByWeight(lines, columns) {
311
+ const groups = Array.from({ length: columns }, () => []);
312
+ const weights = Array(columns).fill(0);
313
+ for (const line of lines) {
314
+ let target = 0;
315
+ for (let i = 1; i < columns; i++) {
316
+ if (weights[i] < weights[target]) target = i;
317
+ }
318
+ groups[target].push(line);
319
+ weights[target] += Math.max(20, weightedTextLength(line));
320
+ }
321
+ return groups;
322
+ }
323
+
324
+ function splitCardsByWeight(cards, columns) {
325
+ const groups = Array.from({ length: columns }, () => []);
326
+ const weights = Array(columns).fill(0);
327
+ for (const card of cards) {
328
+ let target = 0;
329
+ for (let i = 1; i < columns; i++) {
330
+ if (weights[i] < weights[target]) target = i;
331
+ }
332
+ groups[target].push(card);
333
+ const cardWeight = weightedTextLength(card.title || "") * 1.4
334
+ + (card.body || []).reduce((sum, line) => sum + weightedTextLength(line), 0);
335
+ weights[target] += Math.max(80, cardWeight);
336
+ }
337
+ return groups;
338
+ }
339
+
340
+ function renderDiagnosticCard(slide, card, x, y, w, h, idx) {
341
+ slide.addShape("rect", {
342
+ x,
343
+ y,
344
+ w,
345
+ h,
346
+ fill: { color: idx % 2 ? "F8FAF9" : "F3F6F4", transparency: 2 },
347
+ line: { color: "D6E2DB", width: 0.35, transparency: 12 },
348
+ });
349
+ slide.addShape("rect", {
350
+ x,
351
+ y,
352
+ w: 0.028,
353
+ h,
354
+ fill: { color: SECTION_ACCENT, transparency: 0 },
355
+ line: { color: SECTION_ACCENT, transparency: 100 },
356
+ });
357
+
358
+ const padX = 0.055;
359
+ const padY = 0.05;
360
+ const titleH = card.title ? Math.min(0.28, Math.max(0.18, h * 0.16)) : 0;
361
+ if (card.title) {
362
+ slide.addText(card.title, {
363
+ x: x + padX,
364
+ y: y + padY,
365
+ w: w - padX * 1.45,
366
+ h: titleH,
367
+ fontFace: FONT_BOLD,
368
+ fontSize: 7.4,
369
+ color: "002554",
370
+ charSpacing: -0.25,
371
+ fit: "shrink",
372
+ margin: 0.01,
373
+ breakLine: false,
374
+ valign: "top",
375
+ });
376
+ }
377
+
378
+ const bodyText = (card.body || []).join("\n");
379
+ if (bodyText) {
380
+ slide.addText(bodyText, {
381
+ x: x + padX,
382
+ y: y + padY + titleH + 0.015,
383
+ w: w - padX * 1.45,
384
+ h: Math.max(0.16, h - titleH - padY * 1.6),
385
+ fontFace: FONT_MEDIUM,
386
+ fontSize: 5.85,
387
+ color: "1F1F1F",
388
+ charSpacing: -0.18,
389
+ fit: "shrink",
390
+ margin: 0.008,
391
+ paraSpaceAfterPt: 0,
392
+ lineSpacingMultiple: 0.96,
393
+ breakLine: false,
394
+ valign: "top",
395
+ });
396
+ }
397
+ }
398
+
399
+ function estimateDiagnosticCardHeight(card, w) {
400
+ const padY = 0.05;
401
+ const titleH = card.title ? 0.24 : 0;
402
+ const bodyText = (card.body || []).join("\n");
403
+ const bodyH = bodyText ? estimateTextHeight(bodyText, Math.max(0.8, w - 0.12), 5.85, 1.02) : 0;
404
+ return Math.max(0.56, Math.min(2.35, padY * 2 + titleH + bodyH));
405
+ }
406
+
407
+ function renderCramStressBody(slide, items, pageW, pageH) {
408
+ const cards = bodyItemsToDiagnosticCards(items);
409
+ const columns = 3;
410
+ const groups = cards.length ? splitCardsByWeight(cards, columns) : splitLinesByWeight(bodyItemsToDiagnosticLines(items), columns).map((group) => [{ title: "", body: group }]);
411
+ const x = 0.28;
412
+ const y = 0.98;
413
+ const w = 7.78;
414
+ const h = pageH - y - 0.34;
415
+ const gap = 0.105;
416
+ const colW = (w - gap * (columns - 1)) / columns;
417
+ groups.forEach((group, idx) => {
418
+ const colX = x + idx * (colW + gap);
419
+ const cardGap = 0.075;
420
+ const availableH = h - cardGap * Math.max(0, group.length - 1);
421
+ const desiredHeights = group.map((card) => estimateDiagnosticCardHeight(card, colW));
422
+ const desiredTotal = desiredHeights.reduce((sum, value) => sum + value, 0) || 1;
423
+ const scale = desiredTotal > availableH ? availableH / desiredTotal : 1;
424
+ let cursorY = y;
425
+ group.forEach((card, cardIdx) => {
426
+ const remainingCards = group.length - cardIdx - 1;
427
+ const rawH = desiredHeights[cardIdx] * scale;
428
+ const cardH = Math.max(0.62, Math.min(rawH, h - (cursorY - y) - remainingCards * (0.62 + cardGap)));
429
+ renderDiagnosticCard(slide, card, colX, cursorY, colW, cardH, idx + cardIdx);
430
+ cursorY += cardH + cardGap;
431
+ });
432
+ });
433
+ }
434
+
435
+ function renderCramStressImageSheet(slide, content, sourceDir, pageW, pageH) {
436
+ if (!content) return;
437
+ const images = content.type === "image-grid"
438
+ ? (Array.isArray(content.images) ? content.images : [])
439
+ : content.type === "image" ? [content] : [];
440
+ if (images.length === 0) return;
441
+ const x = 8.24;
442
+ const y = 0.98;
443
+ const w = pageW - x - 0.25;
444
+ const h = pageH - y - 0.34;
445
+ const cols = images.length >= 8 ? 2 : 1;
446
+ const rows = Math.ceil(images.length / cols);
447
+ const gap = 0.04;
448
+ const cellW = (w - gap * (cols - 1)) / cols;
449
+ const headerH = 0.28;
450
+ const sheetH = Math.min(h - 0.9, rows * 0.58 + gap * (rows - 1));
451
+ const cellH = (sheetH - gap * (rows - 1)) / rows;
452
+ const noteY = y + headerH + sheetH + 0.16;
453
+ const noteH = Math.min(1.0, h - (noteY - y) - 0.08);
454
+ const panelH = noteY - y + noteH;
455
+ slide.addShape("rect", {
456
+ x: x - 0.04,
457
+ y: y - 0.04,
458
+ w: w + 0.08,
459
+ h: panelH + 0.08,
460
+ fill: { color: "F5F8F6", transparency: 0 },
461
+ line: { color: "CAD4CE", width: 0.5 },
462
+ });
463
+ slide.addText("RAW EVIDENCE CONTACT SHEET", {
464
+ x,
465
+ y: y + 0.02,
466
+ w,
467
+ h: 0.16,
468
+ fontFace: FONT_BOLD,
469
+ fontSize: 5.8,
470
+ color: "002554",
471
+ charSpacing: -0.15,
472
+ fit: "shrink",
473
+ margin: 0,
474
+ });
475
+ slide.addShape("rect", {
476
+ x,
477
+ y: y + 0.22,
478
+ w,
479
+ h: 0,
480
+ line: { color: SECTION_ACCENT, width: 0.45 },
481
+ });
482
+ images.forEach((image, idx) => {
483
+ const imgPath = resolveImagePath(image.src, sourceDir);
484
+ const col = idx % cols;
485
+ const row = Math.floor(idx / cols);
486
+ const fit = containBox(
487
+ x + col * (cellW + gap),
488
+ y + headerH + row * (cellH + gap),
489
+ cellW,
490
+ cellH,
491
+ readImageSize(imgPath),
492
+ );
493
+ slide.addImage({
494
+ path: imgPath,
495
+ ...fit,
496
+ altText: image.caption || path.basename(image.src || imgPath),
497
+ });
498
+ });
499
+ slide.addShape("rect", {
500
+ x,
501
+ y: noteY,
502
+ w,
503
+ h: noteH,
504
+ fill: { color: "FFFFFF", transparency: 0 },
505
+ line: { color: "D6E2DB", width: 0.35, transparency: 20 },
506
+ });
507
+ slide.addText("diagnostic cram-stress\nmicrotext is intentional\nproduction decks must synthesize\noverflow / marker / missing media = fail", {
508
+ x: x + 0.06,
509
+ y: noteY + 0.07,
510
+ w: w - 0.12,
511
+ h: Math.max(0.24, noteH - 0.14),
512
+ fontFace: FONT_MEDIUM,
513
+ fontSize: 5.3,
514
+ color: "333333",
515
+ charSpacing: -0.15,
516
+ fit: "shrink",
517
+ margin: 0.01,
518
+ breakLine: false,
519
+ lineSpacingMultiple: 1.02,
520
+ valign: "mid",
521
+ });
522
+ }
523
+
524
+ function isCramStressDeck(resolved) {
525
+ const meta = (resolved && resolved.deck && resolved.deck.metadata) || {};
526
+ return String(meta.render_mode || meta.renderMode || "").toLowerCase() === "cram-stress";
527
+ }
528
+
529
+ function renderCramStressSlide(slide, slideSpec, pageW, pageH, sourceDir) {
530
+ const regions = slideSpec.regions || {};
531
+ const title = regions.title && regions.title.content;
532
+ if (title) {
533
+ slide.addText(String(title), {
534
+ x: 0.24,
535
+ y: 0.24,
536
+ w: pageW - 1.72,
537
+ h: 0.46,
538
+ fontFace: FONT_BOLD,
539
+ fontSize: 13.5,
540
+ color: "000000",
541
+ charSpacing: CHAR_SPACING,
542
+ fit: "shrink",
543
+ });
544
+ slide.addShape("rect", {
545
+ x: 0.24,
546
+ y: 0.76,
547
+ w: pageW - 0.48,
548
+ h: 0,
549
+ line: { color: SECTION_ACCENT, width: 1.2 },
550
+ });
551
+ }
552
+ if (regions.body && regions.body.content && regions.body.content.type === "body") {
553
+ renderCramStressBody(slide, regions.body.content.items || [], pageW, pageH);
554
+ }
555
+ if (regions.image && regions.image.content) {
556
+ renderCramStressImageSheet(slide, regions.image.content, sourceDir, pageW, pageH);
557
+ }
558
+ }
559
+
560
+ function shouldUseDenseBodyFlow(items, widthIn, startY, bottomY) {
561
+ const elements = bodyElements(items);
562
+ const charCount = elements.reduce((sum, el) => sum + String(el.text || "").length, 0);
563
+ const available = Math.max(0.5, (bottomY || 7.0) - startY);
564
+ const estimatedAtMin = elements.reduce((sum, el) => {
565
+ const size = el.kind === "section" ? 12.8 : 11.5;
566
+ return sum + estimateTextHeight(el.text, Math.max(1.8, widthIn / 2), size, 1.02) + 0.025;
567
+ }, 0);
568
+ return widthIn >= 4.4 && (elements.length >= 18 || charCount >= 2400 || estimatedAtMin > available * 1.35);
569
+ }
570
+
571
+ function chooseDenseBodyStyle() {
572
+ return { bodySize: 11.5, sectionSize: 12.8, lineSpacing: 1.02, gap: 0.025 };
573
+ }
574
+
575
+ // ── Font role helpers ───────────────────────────────────────────────────────
576
+
577
+ /**
578
+ * Build font role map from template capabilities.
579
+ * Maps YAML field names to pptxgenjs option names.
580
+ */
581
+ function buildFontRoles(templateObj) {
582
+ if (!templateObj || !templateObj.capabilities || !templateObj.capabilities.font_roles) {
583
+ return {};
584
+ }
585
+
586
+ const roles = {};
587
+ for (const [name, def] of Object.entries(templateObj.capabilities.font_roles)) {
588
+ roles[name] = {
589
+ font: def.font,
590
+ size: def.size,
591
+ color: (def.color || "#000000").replace("#", ""),
592
+ align: def.align || "left",
593
+ charSpacing: def.char_spacing,
594
+ lineSpacing: def.line_spacing,
595
+ bold: def.bold,
596
+ spaceAfter: def.space_after,
597
+ bulletType: def.bullet_type,
598
+ bulletChar: def.bullet_char,
599
+ bulletFont: def.bullet_font,
600
+ numberType: def.number_type,
601
+ marL: def.marL,
602
+ indent: def.indent,
603
+ fill: def.fill ? def.fill.replace("#", "") : undefined,
604
+ borderColor: def.border_color ? def.border_color.replace("#", "") : undefined,
605
+ borderWidth: def.border_width,
606
+ };
607
+ }
608
+ return roles;
609
+ }
610
+
611
+ // ── Decoration rendering ────────────────────────────────────────────────────
612
+
613
+ /**
614
+ * Add decoration elements to a pptxgenjs slide.
615
+ * Note: slide_number is intentionally omitted — the reference template has none.
616
+ */
617
+ function addDecorations(slide, decorations, slideIdx, fontRoles) {
618
+ if (!decorations || !Array.isArray(decorations)) return;
619
+
620
+ for (const d of decorations) {
621
+ if (d.type === "image") {
622
+ slide.addImage({
623
+ path: decorAssetPath(d),
624
+ x: d.x || 0,
625
+ y: d.y || 0,
626
+ w: d.w || 1,
627
+ h: d.h || 1,
628
+ rotate: d.rotate || undefined,
629
+ altText: d.asset || "template-image",
630
+ });
631
+
632
+ } else if (d.type === "rect" || d.type === "roundRect" || d.type === "pill") {
633
+ // Solid color block / rounded card / full-radius pill. Optional text label.
634
+ const shape = d.type === "rect" ? "rect" : "roundRect";
635
+ const opts = {
636
+ x: d.x || 0, y: d.y || 0, w: d.w || 1, h: d.h || 0.5,
637
+ };
638
+ if (d.fill) opts.fill = { color: hex(d.fill), transparency: d.alpha || 0 };
639
+ else opts.fill = { type: "none" };
640
+ if (d.line) opts.line = { color: hex(d.line), width: d.line_width || 1 };
641
+ else opts.line = { type: "none" };
642
+ if (shape === "roundRect") opts.rectRadius = d.type === "pill" ? (d.h || 0.5) / 2 : (d.radius != null ? d.radius : 0.12);
643
+ slide.addShape(shape, opts);
644
+ if (d.text) {
645
+ slide.addText(d.text, {
646
+ x: d.x || 0, y: d.y || 0, w: d.w || 1, h: d.h || 0.5,
647
+ fontFace: d.font || FONT_BOLD, fontSize: d.size || 12,
648
+ color: hex(d.text_color || "#FFFFFF"), bold: !!d.bold,
649
+ align: d.align || "center", valign: "middle",
650
+ charSpacing: d.char_spacing != null ? d.char_spacing : CHAR_SPACING,
651
+ });
652
+ }
653
+
654
+ } else if (d.type === "ellipse" || d.type === "ring") {
655
+ const opts = { x: d.x || 0, y: d.y || 0, w: d.w || 1, h: d.h || (d.w || 1) };
656
+ if (d.type === "ring" || !d.fill) {
657
+ opts.fill = { type: "none" };
658
+ opts.line = { color: hex(d.line || d.color || "#FFFFFF"), width: d.line_width || 2 };
659
+ } else {
660
+ opts.fill = { color: hex(d.fill), transparency: d.alpha || 0 };
661
+ opts.line = { type: "none" };
662
+ }
663
+ slide.addShape("ellipse", opts);
664
+
665
+ } else if (d.type === "text") {
666
+ slide.addText(d.text || "", {
667
+ x: d.x || 0, y: d.y || 0, w: d.w || 4, h: d.h || 1,
668
+ fontFace: d.font || FONT_BOLD, fontSize: d.size || 14,
669
+ color: hex(d.color || "#000000"), bold: !!d.bold,
670
+ align: d.align || "left", valign: d.valign || "top",
671
+ charSpacing: d.char_spacing != null ? d.char_spacing : CHAR_SPACING,
672
+ transparency: d.transparency != null ? d.transparency : undefined,
673
+ });
674
+
675
+ } else if (d.type === "line") {
676
+ slide.addShape("rect", {
677
+ x: d.x || 0,
678
+ y: d.y || 0,
679
+ w: d.w || 1,
680
+ h: 0,
681
+ line: { color: (d.color || "#000000").replace("#", ""), width: d.width || 1 },
682
+ });
683
+
684
+ } else if (d.type === "confidential_mark") {
685
+ const role = fontRoles.confidential_mark || {};
686
+ const mX = d.x || 0, mY = d.y || 0, mW = d.w || 1, mH = d.h || 0.3;
687
+ const bColor = role.borderColor || "C00000";
688
+ const bPt = role.borderWidth || 1;
689
+ slide.addShape("rect", {
690
+ x: mX, y: mY, w: mW, h: mH,
691
+ line: { color: bColor, width: bPt },
692
+ });
693
+ slide.addText("\u5C0D \u5916 \u79D8", {
694
+ x: mX, y: mY, w: mW, h: mH,
695
+ fontFace: role.font || FONT_MEDIUM,
696
+ fontSize: role.size || 14,
697
+ color: role.color || "C00000",
698
+ align: role.align || "center",
699
+ valign: "middle",
700
+ });
701
+
702
+ } else if (d.type === "disclaimer") {
703
+ const role = fontRoles.disclaimer || {};
704
+ slide.addText(
705
+ d.text || DEFAULT_DISCLAIMER,
706
+ {
707
+ x: d.x || 0,
708
+ y: d.y || 0,
709
+ w: d.w || 1,
710
+ h: d.h || 0.2,
711
+ fontFace: role.font || FONT_LIGHT,
712
+ fontSize: role.size || 6,
713
+ color: role.color || "666666",
714
+ align: role.align || "left",
715
+ }
716
+ );
717
+ }
718
+ // slide_number intentionally removed — reference template has none
719
+ }
720
+ }
721
+
722
+ // ── Body shape rendering (individual shapes per line) ────────────────────
723
+
724
+ /**
725
+ * Add body items as individual shapes to a slide.
726
+ * Each section header and bullet item is a separate positioned shape.
727
+ *
728
+ * @param {object} slide - pptxgenjs slide
729
+ * @param {Array} items - AST items (section, bullet, numbered, text)
730
+ * @param {number} startY - Starting y position
731
+ * @param {number} maxX - Max width for positioning
732
+ * @returns {number} The y position after the last item
733
+ */
734
+ function addBodyShapes(slide, items, startY, maxX, bottomY, originX) {
735
+ const denseFlow = shouldUseDenseBodyFlow(items, maxX, startY, bottomY || 7.0);
736
+ const columns = denseFlow ? 2 : 1;
737
+ const colGap = denseFlow ? 0.16 : 0;
738
+ const colW = (maxX - colGap * (columns - 1)) / columns;
739
+ let col = 0;
740
+ let y = startY;
741
+ // Template regions have always drawn body text at the template's own left
742
+ // margin. A placed box carries its own, and passes it here.
743
+ const baseX = originX != null ? originX : SEC_HEADER_X;
744
+ const bodyIndent = denseFlow ? 0.13 : (BULLET_X - SEC_HEADER_X);
745
+ const BODY_FULL_W = 10.188;
746
+ const scale = colW / BODY_FULL_W;
747
+ const secW = denseFlow ? Math.max(1.4, colW - 0.04) : SEC_HEADER_W * scale;
748
+ const bulletW = denseFlow ? Math.max(1.2, colW - bodyIndent - 0.02) : BULLET_W * scale;
749
+ let numberedCounter = 0;
750
+ const style = denseFlow ? chooseDenseBodyStyle() : chooseBodyStyle(items, bulletW, startY, bottomY || 7.0);
751
+
752
+ function colX() { return baseX + col * (colW + colGap); }
753
+ function secX() { return colX(); }
754
+ function bulletX() { return colX() + bodyIndent; }
755
+ function maybeAdvanceColumn(nextHeight) {
756
+ if (!denseFlow || col >= columns - 1) return;
757
+ if (y + nextHeight > (bottomY || 7.0)) {
758
+ col++;
759
+ y = startY;
760
+ }
761
+ }
762
+
763
+ for (const item of items) {
764
+ if (item.type === "section") {
765
+ const sectionH = estimateTextHeight(item.heading, secW, style.sectionSize, style.lineSpacing);
766
+ const cardH = estimateSectionHeight(item, secW, bulletW, style);
767
+ maybeAdvanceColumn(Math.min(cardH, sectionH + 0.2));
768
+ addSectionCard(slide, secX() - 0.035, y - 0.03, Math.max(secW, bulletW + bodyIndent) + 0.07, cardH);
769
+ // Section header — separate shape, black, 1.5 line spacing
770
+ slide.addText(mdRuns(item.heading, {
771
+ fontFace: SEC_HEADER_FONT,
772
+ fontSize: style.sectionSize,
773
+ color: SEC_HEADER_COLOR,
774
+ charSpacing: CHAR_SPACING,
775
+ }), {
776
+ x: secX(),
777
+ y: y,
778
+ w: secW,
779
+ h: sectionH,
780
+ lineSpacingMultiple: style.lineSpacing,
781
+ });
782
+ y += sectionH + style.gap;
783
+
784
+ // Numbered children with (1)(2)(3) format
785
+ if (item.children) {
786
+ numberedCounter = 0;
787
+ for (const child of item.children) {
788
+ numberedCounter++;
789
+ const numPrefix = `(${numberedCounter}) `;
790
+ const text = numPrefix + cleanMd(child.text);
791
+ const h = estimateTextHeight(text, bulletW, style.bodySize, style.lineSpacing);
792
+ maybeAdvanceColumn(h);
793
+ slide.addText([
794
+ { text: numPrefix, options: { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING } },
795
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
796
+ ], {
797
+ x: bulletX(),
798
+ y: y,
799
+ w: bulletW,
800
+ h,
801
+ lineSpacingMultiple: style.lineSpacing,
802
+ });
803
+ y += h + style.gap;
804
+ }
805
+ }
806
+ y += 0.08;
807
+
808
+ } else if (item.type === "bullet") {
809
+ const h = estimateTextHeight(item.text, bulletW, style.bodySize, style.lineSpacing);
810
+ maybeAdvanceColumn(h);
811
+ // Bullet item — green prefix + black text
812
+ slide.addText([
813
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: style.bodySize, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
814
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
815
+ ], {
816
+ x: bulletX(),
817
+ y: y,
818
+ w: bulletW,
819
+ h,
820
+ lineSpacingMultiple: style.lineSpacing,
821
+ });
822
+ y += h + style.gap;
823
+
824
+ // Children (numbered sub-items)
825
+ if (item.children) {
826
+ numberedCounter = 0;
827
+ for (const child of item.children) {
828
+ numberedCounter++;
829
+ const numPrefix = `(${numberedCounter}) `;
830
+ const text = numPrefix + cleanMd(child.text);
831
+ const childH = estimateTextHeight(text, bulletW, style.bodySize, style.lineSpacing);
832
+ maybeAdvanceColumn(childH);
833
+ slide.addText([
834
+ { text: numPrefix, options: { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING } },
835
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
836
+ ], {
837
+ x: bulletX(),
838
+ y: y,
839
+ w: bulletW,
840
+ h: childH,
841
+ lineSpacingMultiple: style.lineSpacing,
842
+ });
843
+ y += childH + style.gap;
844
+ }
845
+ }
846
+
847
+ } else if (item.type === "numbered") {
848
+ numberedCounter++;
849
+ const numPrefix = `(${numberedCounter}) `;
850
+ const text = numPrefix + cleanMd(item.text);
851
+ const h = estimateTextHeight(text, bulletW, style.bodySize, style.lineSpacing);
852
+ maybeAdvanceColumn(h);
853
+ slide.addText([
854
+ { text: numPrefix, options: { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING } },
855
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: style.bodySize, color: "000000", charSpacing: CHAR_SPACING }),
856
+ ], {
857
+ x: bulletX(),
858
+ y: y,
859
+ w: bulletW,
860
+ h,
861
+ lineSpacingMultiple: style.lineSpacing,
862
+ });
863
+ y += h + style.gap;
864
+
865
+ } else if (item.type === "text") {
866
+ const h = estimateTextHeight(item.text, bulletW, style.bodySize, style.lineSpacing);
867
+ maybeAdvanceColumn(h);
868
+ slide.addText(mdRuns(item.text, {
869
+ fontFace: FONT_MEDIUM,
870
+ fontSize: style.bodySize,
871
+ color: "000000",
872
+ charSpacing: CHAR_SPACING,
873
+ }), {
874
+ x: bulletX(),
875
+ y: y,
876
+ w: bulletW,
877
+ h,
878
+ lineSpacingMultiple: style.lineSpacing,
879
+ });
880
+ y += h + style.gap;
881
+ }
882
+ }
883
+
884
+ return y;
885
+ }
886
+
887
+ /**
888
+ * Add summary group content as individual shapes.
889
+ * Uses narrow width (~4.3" for headers, ~4.1" for bullets).
890
+ */
891
+ function addSummaryShapes(slide, content, region) {
892
+ let y = region.y || 1.04;
893
+
894
+ // Group heading
895
+ if (content.heading) {
896
+ slide.addText(mdRuns(content.heading, {
897
+ fontFace: SEC_HEADER_FONT,
898
+ fontSize: SEC_HEADER_SIZE,
899
+ color: SEC_HEADER_COLOR,
900
+ charSpacing: CHAR_SPACING,
901
+ }), {
902
+ x: region.x || 0.39,
903
+ y: y,
904
+ w: SUMMARY_HEADER_W,
905
+ h: SEC_HEADER_H,
906
+ });
907
+ y += SEC_HEADER_H + 0.08;
908
+ }
909
+
910
+ // Items
911
+ if (content.items) {
912
+ for (const item of content.items) {
913
+ if (item.type === "section") {
914
+ // Section header
915
+ slide.addText(mdRuns(item.heading, {
916
+ fontFace: SEC_HEADER_FONT,
917
+ fontSize: SEC_HEADER_SIZE,
918
+ color: SEC_HEADER_COLOR,
919
+ charSpacing: CHAR_SPACING,
920
+ }), {
921
+ x: region.x || 0.39,
922
+ y: y,
923
+ w: SUMMARY_HEADER_W,
924
+ h: SEC_HEADER_H,
925
+ });
926
+ y += SEC_HEADER_H + 0.05;
927
+
928
+ // Children as bullet items
929
+ if (item.children) {
930
+ for (const child of item.children) {
931
+ slide.addText([
932
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
933
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
934
+ ], {
935
+ x: 0.59,
936
+ y: y,
937
+ w: SUMMARY_BULLET_W,
938
+ h: SUMMARY_BULLET_H,
939
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
940
+ });
941
+ y += SUMMARY_BULLET_Y_SPACING;
942
+ }
943
+ }
944
+
945
+ } else if (item.type === "bullet") {
946
+ slide.addText([
947
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
948
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
949
+ ], {
950
+ x: 0.59,
951
+ y: y,
952
+ w: SUMMARY_BULLET_W,
953
+ h: SUMMARY_BULLET_H,
954
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
955
+ });
956
+ y += SUMMARY_BULLET_Y_SPACING;
957
+
958
+ if (item.children) {
959
+ for (const child of item.children) {
960
+ slide.addText([
961
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
962
+ ...mdRuns(child.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
963
+ ], {
964
+ x: 0.59,
965
+ y: y,
966
+ w: SUMMARY_BULLET_W,
967
+ h: SUMMARY_BULLET_H,
968
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
969
+ });
970
+ y += SUMMARY_BULLET_Y_SPACING;
971
+ }
972
+ }
973
+
974
+ } else if (item.type === "numbered") {
975
+ slide.addText([
976
+ { text: BULLET_PREFIX, options: { fontFace: FONT_BOLD, fontSize: SUMMARY_BULLET_SIZE, color: BULLET_COLOR, charSpacing: CHAR_SPACING } },
977
+ ...mdRuns(item.text, { fontFace: FONT_MEDIUM, fontSize: SUMMARY_BULLET_SIZE, color: "000000", charSpacing: CHAR_SPACING }),
978
+ ], {
979
+ x: 0.59,
980
+ y: y,
981
+ w: SUMMARY_BULLET_W,
982
+ h: SUMMARY_BULLET_H,
983
+ lineSpacingMultiple: SUMMARY_BULLET_LINE_SPACING,
984
+ });
985
+ y += SUMMARY_BULLET_Y_SPACING;
986
+ }
987
+ }
988
+ }
989
+ }
990
+
991
+ // ── TOC rendering (separate shapes per item) ─────────────────────────────
992
+
993
+ /**
994
+ * Add TOC items as separate text shapes, matching TEMPLATE-PPTX.pptx exactly.
995
+ * Each item is: "N. text" in the bold face at 18pt, w=7.5, h=0.55, spaced 0.65" apart.
996
+ */
997
+ function addTocShapes(slide, items) {
998
+ let y = TOC_Y_START;
999
+ let num = 1;
1000
+
1001
+ for (const item of items) {
1002
+ const text = `${num}. ${cleanMd(item.text || item.heading || "")}`;
1003
+ slide.addText(text, {
1004
+ x: TOC_X,
1005
+ y: y,
1006
+ w: TOC_W,
1007
+ h: TOC_H,
1008
+ fontFace: TOC_FONT,
1009
+ fontSize: TOC_SIZE,
1010
+ color: TOC_COLOR,
1011
+ charSpacing: CHAR_SPACING,
1012
+ });
1013
+ y += TOC_Y_SPACING;
1014
+ num++;
1015
+ }
1016
+ }
1017
+
1018
+ /**
1019
+ * Check if a slide is a TOC slide by inspecting the title region content.
1020
+ */
1021
+ function isTocSlide(regions) {
1022
+ const title = regions.title;
1023
+ if (!title || typeof title.content !== "string") return false;
1024
+ return title.content.includes("목차");
1025
+ }
1026
+
1027
+ function adjustedRegionsForImageColumn(regions) {
1028
+ const body = regions.body;
1029
+ const image = regions.image;
1030
+ if (!body || !image || !body.content || !image.content) return regions;
1031
+ if (body.content.type !== "body" || !["image", "image-grid"].includes(image.content.type)) return regions;
1032
+
1033
+ const bodyX = body.x || 0;
1034
+ const bodyW = body.w || 10.188;
1035
+ const imageX = image.x || 0;
1036
+ const gap = 0.25;
1037
+ if (imageX <= bodyX || imageX >= bodyX + bodyW) return regions;
1038
+
1039
+ const denseImageGrid = image.content.type === "image-grid" && Array.isArray(image.content.images) && image.content.images.length >= 6;
1040
+ return {
1041
+ ...regions,
1042
+ body: {
1043
+ ...body,
1044
+ w: Math.max(0.5, imageX - bodyX - gap),
1045
+ },
1046
+ image: denseImageGrid ? {
1047
+ ...image,
1048
+ y: Math.min(image.y || 1.5, 1.08),
1049
+ h: Math.max(image.h || 3.0, 5.85),
1050
+ } : image,
1051
+ figure_caption: denseImageGrid && regions.figure_caption ? {
1052
+ ...regions.figure_caption,
1053
+ y: Math.max(regions.figure_caption.y || 4.55, 7.02),
1054
+ } : regions.figure_caption,
1055
+ };
1056
+ }
1057
+
1058
+ // ── Region rendering ────────────────────────────────────────────────────────
1059
+
1060
+ /**
1061
+ * Add a region as a positioned element on the slide.
1062
+ *
1063
+ * Content types:
1064
+ * - string: title, main-box, notes
1065
+ * - { type: "body" } bullet/section/numbered items → individual shapes
1066
+ * - { type: "kpi-table" } table with headers and rows
1067
+ * - { type: "image" } image element
1068
+ * - { type: "figure-caption" } figure caption (auto-numbered "도 N.")
1069
+ * - { type: "table-caption" } table caption (auto-numbered "표 N.")
1070
+ * - { type: "summary-group" } heading + items → individual shapes
1071
+ */
1072
+ function addRegion(slide, name, region, fontRoles, pageH, regionBottom, isToc, sourceDir) {
1073
+ if (!region) return;
1074
+
1075
+ const content = region.content;
1076
+ if (content === undefined || content === null) return;
1077
+ if (typeof content === "object" && !content.type) return;
1078
+
1079
+ const roleName = region.font_role || region.heading_role || region.sub_item_role;
1080
+ const role = fontRoles[roleName] || {};
1081
+
1082
+ // ── String content (title, main-box, notes) ────────────────────────
1083
+ if (typeof content === "string") {
1084
+ const opts = {
1085
+ x: region.x || 0,
1086
+ y: region.y || 0,
1087
+ w: region.w || 4,
1088
+ h: region.h || ((regionBottom || (pageH - 0.5)) - (region.y || 0)),
1089
+ fontFace: role.font || FONT_BOLD,
1090
+ fontSize: role.size || 16,
1091
+ color: (role.color || "000000").replace("#", ""),
1092
+ align: region.align || role.align || "left",
1093
+ valign: region.valign || "top",
1094
+ };
1095
+
1096
+ if (role.charSpacing != null) opts.charSpacing = role.charSpacing;
1097
+ if (role.lineSpacing) opts.lineSpacingMultiple = role.lineSpacing;
1098
+
1099
+ // Border (main_box)
1100
+ if (region.border) {
1101
+ opts.line = {
1102
+ color: (region.border.color || "#000000").replace("#", ""),
1103
+ width: region.border.width || 1,
1104
+ };
1105
+ }
1106
+
1107
+ // Autofit
1108
+ if (region.autofit === "resize") opts.autoFit = true;
1109
+ else if (region.autofit === "shrink") opts.shrinkText = true;
1110
+
1111
+ slide.addText(mdRuns(content, {
1112
+ fontFace: opts.fontFace,
1113
+ fontSize: opts.fontSize,
1114
+ color: opts.color,
1115
+ ...(opts.charSpacing != null ? { charSpacing: opts.charSpacing } : {}),
1116
+ }), opts);
1117
+ return;
1118
+ }
1119
+
1120
+ // ── Body content → TOC shapes or individual shapes ──────────────────
1121
+ if (content.type === "body") {
1122
+ if (isToc) {
1123
+ addTocShapes(slide, content.items || []);
1124
+ } else {
1125
+ const startY = region.y || 1.044;
1126
+ addBodyShapes(slide, content.items || [], startY, region.w || 10.188, regionBottom || pageH - 0.5, region.originX);
1127
+ }
1128
+ return;
1129
+ }
1130
+
1131
+ // ── KPI Table ──────────────────────────────────────────────────────
1132
+ if (content.type === "kpi-table" && content.chart) {
1133
+ renderChart(slide, content, region, PLAIN_CHART_STYLE, regionBottom);
1134
+ return;
1135
+ }
1136
+ if (content.type === "kpi-table") {
1137
+ const numeric = numericColumns(content.headers || [], content.rows || []);
1138
+ const metrics = tableMetrics((content.rows || []).length + 1, region.y || 0, regionBottom,
1139
+ { rowH: 0.35, maxRowH: 0.55, headerSize: 14, dataSize: 14 });
1140
+ const headerRole = fontRoles[region.font_role_header] || fontRoles.table_header;
1141
+ const dataRole = fontRoles[region.font_role_data] || fontRoles.table_data;
1142
+
1143
+ const tableData = [];
1144
+
1145
+ if (content.headers && content.headers.length > 0) {
1146
+ tableData.push(
1147
+ content.headers.map((h) => ({
1148
+ text: cleanMd(h),
1149
+ options: {
1150
+ bold: false,
1151
+ fontSize: headerRole ? headerRole.size : 14,
1152
+ fontFace: headerRole ? headerRole.font : FONT_BOLD,
1153
+ color: headerRole ? headerRole.color : "000000",
1154
+ align: numeric[content.headers.indexOf(h)] ? "right" : (headerRole ? headerRole.align : "left"),
1155
+ valign: "middle",
1156
+ ...(headerRole && headerRole.charSpacing != null
1157
+ ? { charSpacing: headerRole.charSpacing }
1158
+ : {}),
1159
+ ...(headerRole && headerRole.fill
1160
+ ? { fill: { color: headerRole.fill.replace("#", "") } }
1161
+ : {}),
1162
+ },
1163
+ }))
1164
+ );
1165
+ }
1166
+
1167
+ if (content.rows) {
1168
+ for (const row of content.rows) {
1169
+ tableData.push(
1170
+ row.map((cell, ci) => linkedTableCell(cell, {
1171
+ fontSize: dataRole ? dataRole.size : 14,
1172
+ fontFace: dataRole ? dataRole.font : FONT_MEDIUM,
1173
+ color: dataRole ? dataRole.color : "000000",
1174
+ align: numeric[ci] ? "right" : (dataRole ? dataRole.align : "left"),
1175
+ valign: "middle",
1176
+ ...(dataRole && dataRole.charSpacing != null
1177
+ ? { charSpacing: dataRole.charSpacing }
1178
+ : {}),
1179
+ }))
1180
+ );
1181
+ }
1182
+ }
1183
+
1184
+ const numCols = (content.headers && content.headers.length) || 1;
1185
+ const tblX = region.x || 0;
1186
+ const tblY = region.y || 0;
1187
+ const tblW = region.w || 4;
1188
+ slide.addTable(tableData, {
1189
+ x: tblX,
1190
+ y: tblY,
1191
+ w: tblW,
1192
+ border: { pt: 1.5, color: "000000" },
1193
+ colW: numCols > 0 && content.headers ? proportionalWidths(content.headers, content.rows || [], tblW) : Array(numCols).fill(tblW / numCols),
1194
+ margin: [3.6, 7.2, 3.6, 7.2],
1195
+ rowH: metrics.rowH,
1196
+ });
1197
+ return;
1198
+ }
1199
+
1200
+ // ── Image ──────────────────────────────────────────────────────────
1201
+ if (content.type === "image") {
1202
+ const imgPath = resolveImagePath(content.src, sourceDir);
1203
+ const fit = containBox(
1204
+ region.x || 0,
1205
+ region.y || 0,
1206
+ region.w || 2,
1207
+ region.h || 1,
1208
+ readImageSize(imgPath),
1209
+ );
1210
+ slide.addImage({
1211
+ path: imgPath,
1212
+ ...fit,
1213
+ altText: content.caption || path.basename(content.src || imgPath),
1214
+ });
1215
+ return;
1216
+ }
1217
+
1218
+ if (content.type === "image-grid") {
1219
+ const images = Array.isArray(content.images) ? content.images : [];
1220
+ const count = images.length;
1221
+ if (count === 0) return;
1222
+ const cols = count <= 2 ? count : 2;
1223
+ const rows = Math.ceil(count / cols);
1224
+ const gap = 0.06;
1225
+ const baseX = region.x || 0;
1226
+ const baseY = region.y || 0;
1227
+ const baseW = region.w || 4;
1228
+ const baseH = region.h || 3;
1229
+ const cellW = (baseW - gap * (cols - 1)) / cols;
1230
+ const cellH = (baseH - gap * (rows - 1)) / rows;
1231
+ images.forEach((image, idx) => {
1232
+ const imgPath = resolveImagePath(image.src, sourceDir);
1233
+ const col = idx % cols;
1234
+ const row = Math.floor(idx / cols);
1235
+ const fit = containBox(
1236
+ baseX + col * (cellW + gap),
1237
+ baseY + row * (cellH + gap),
1238
+ cellW,
1239
+ cellH,
1240
+ readImageSize(imgPath),
1241
+ );
1242
+ slide.addImage({
1243
+ path: imgPath,
1244
+ ...fit,
1245
+ altText: image.caption || path.basename(image.src || imgPath),
1246
+ });
1247
+ });
1248
+ return;
1249
+ }
1250
+
1251
+ // ── Figure Caption ────────────────────────────────────────────────
1252
+ if (content.type === "figure-caption") {
1253
+ const num = content.number || 1;
1254
+ const prefix = content.prefix || "도";
1255
+ const captionText = content.caption ? `${prefix} ${num}. ${cleanMd(content.caption)}` : `${prefix} ${num}.`;
1256
+ const captionRole = fontRoles.figure_caption || {};
1257
+ const captionW = region.w || 4;
1258
+ const captionSize = captionRole.size || 16;
1259
+ const captionSpacing = captionRole.lineSpacing || 1.3;
1260
+ slide.addText(captionText, {
1261
+ x: region.x || 0,
1262
+ y: region.y || 0,
1263
+ w: captionW,
1264
+ // A caption that wraps needs a box tall enough to hold every line, or it
1265
+ // overflows its frame. The declared region height is the floor, not the cap.
1266
+ h: Math.max(
1267
+ region.h || 0.456,
1268
+ estimateTextHeight(captionText, captionW, captionSize, captionSpacing)
1269
+ ),
1270
+ fontFace: captionRole.font || FONT_MEDIUM,
1271
+ fontSize: captionSize,
1272
+ color: (captionRole.color || "000000").replace("#", ""),
1273
+ align: region.align || captionRole.align || "center",
1274
+ charSpacing: captionRole.charSpacing != null ? captionRole.charSpacing : CHAR_SPACING,
1275
+ lineSpacingMultiple: captionRole.lineSpacing || 1.3,
1276
+ });
1277
+ return;
1278
+ }
1279
+
1280
+ // ── Table Caption ─────────────────────────────────────────────────
1281
+ if (content.type === "table-caption") {
1282
+ const num = content.number || 1;
1283
+ const prefix = content.prefix || "표";
1284
+ const captionText = content.caption ? `${prefix} ${num}. ${cleanMd(content.caption)}` : `${prefix} ${num}.`;
1285
+ const captionRole = fontRoles.table_caption || {};
1286
+ const captionW = region.w || 4;
1287
+ const captionSize = captionRole.size || 16;
1288
+ const captionSpacing = captionRole.lineSpacing || 1.3;
1289
+ slide.addText(captionText, {
1290
+ x: region.x || 0,
1291
+ y: region.y || 0,
1292
+ w: captionW,
1293
+ // Same rule as the figure caption: the declared region height is the
1294
+ // floor, not the cap, or a wrapping caption overflows its frame.
1295
+ h: Math.max(
1296
+ region.h || 0.456,
1297
+ estimateTextHeight(captionText, captionW, captionSize, captionSpacing)
1298
+ ),
1299
+ fontFace: captionRole.font || FONT_MEDIUM,
1300
+ fontSize: captionSize,
1301
+ color: (captionRole.color || "000000").replace("#", ""),
1302
+ align: region.align || captionRole.align || "center",
1303
+ charSpacing: captionRole.charSpacing != null ? captionRole.charSpacing : CHAR_SPACING,
1304
+ lineSpacingMultiple: captionRole.lineSpacing || 1.3,
1305
+ });
1306
+ return;
1307
+ }
1308
+
1309
+ // ── Summary Group → individual shapes ──────────────────────────────
1310
+ if (content.type === "summary-group") {
1311
+ addSummaryShapes(slide, content, region);
1312
+ return;
1313
+ }
1314
+ }
1315
+
1316
+ // ── AZURE rich rendering (isolated; only used when template.render_style==="azure") ──
1317
+
1318
+ /** Split a title string into runs, emphasizing **word** in the primary color. */
1319
+ function azureTitleRuns(text, P) {
1320
+ return text.split(/(\*\*[^*]+\*\*)/g).filter(Boolean).map((p) =>
1321
+ p.startsWith("**") && p.endsWith("**")
1322
+ ? { text: p.slice(2, -2), options: { color: hex(P.primary) } }
1323
+ : { text: p, options: {} }
1324
+ );
1325
+ }
1326
+
1327
+ /** Strip leftover markdown markers (### heading, list dashes, **bold**, `code`)
1328
+ * from inline text, returning a clean plain string. */
1329
+ function cleanMd(s) {
1330
+ return String(s || "")
1331
+ .replace(/^#{1,6}\s+/, "")
1332
+ .replace(/^\s*[*-]\s+/, "")
1333
+ .replace(/\*\*(.+?)\*\*/g, "$1")
1334
+ .replace(/`/g, "")
1335
+ .trim();
1336
+ }
1337
+
1338
+ /** Convert inline markdown in `text` to pptxgenjs styled runs: **x** → a bold
1339
+ * run, plain segments → normal runs. Leading heading/list markers and backticks
1340
+ * are stripped. `runOpts` styles every run; bold runs additionally set bold.
1341
+ * Guarantees no raw "**", "###", or backtick reaches the rendered <a:t>. */
1342
+ function mdRuns(text, runOpts = {}) {
1343
+ const clean = String(text || "")
1344
+ .replace(/^#{1,6}\s+/, "")
1345
+ .replace(/^\s*[*-]\s+/, "")
1346
+ .replace(/`/g, "");
1347
+ const parts = clean.split(/(\*\*[^*]+\*\*)/g).filter((p) => p !== "");
1348
+ if (parts.length === 0) return [{ text: "", options: { ...runOpts } }];
1349
+ return parts.map((p) =>
1350
+ p.startsWith("**") && p.endsWith("**")
1351
+ ? { text: p.slice(2, -2), options: { ...runOpts, bold: true } }
1352
+ : { text: p, options: { ...runOpts } }
1353
+ );
1354
+ }
1355
+
1356
+ /** Convert body items into up to 4 cards: {heading, lines[]}.
1357
+ * Handles both authoring styles:
1358
+ * - `### Title` (parsed as `text`) followed by `- **point**` (a `section`) → one card
1359
+ * titled by the heading, the section folded into the body.
1360
+ * - bare `- **Title**` sections → one card each (heading = bold title). */
1361
+ function bodyToCards(items) {
1362
+ const cards = [];
1363
+ let cur = null;
1364
+ for (const item of items || []) {
1365
+ if (item.type === "text" && item.text) {
1366
+ cur = { heading: cleanMd(item.text), lines: [] };
1367
+ cards.push(cur);
1368
+ } else if (item.type === "section" || item.type === "bullet") {
1369
+ const head = cleanMd(item.heading || item.text || "");
1370
+ const kids = (item.children || []).map((c) => cleanMd(c.text)).filter(Boolean);
1371
+ if (cur && cur.lines.length === 0 && cur.heading) {
1372
+ // fold this section under the preceding heading card
1373
+ if (head) cur.lines.push(head);
1374
+ kids.forEach((k) => cur.lines.push(" " + k));
1375
+ } else {
1376
+ cur = { heading: head, lines: kids };
1377
+ cards.push(cur);
1378
+ }
1379
+ } else if (item.type === "numbered" && item.text) {
1380
+ if (cur) cur.lines.push(cleanMd(item.text));
1381
+ else { cur = { heading: "", lines: [cleanMd(item.text)] }; cards.push(cur); }
1382
+ }
1383
+ }
1384
+ return cards.slice(0, 4);
1385
+ }
1386
+
1387
+ // Card geometry shared by drawing and sizing, so a card is exactly as tall as its text.
1388
+ const CARD_PAD = 0.30;
1389
+ const CARD_CHIP = 0.52;
1390
+ const CARD_MIN_H = 1.6;
1391
+
1392
+ // Primary reading text stays at or above the 12 pt floor of the design laws.
1393
+ function cardBodySize(card) {
1394
+ return card.lines && card.lines.length > 4 ? 12 : 13;
1395
+ }
1396
+
1397
+ // Card text height: the conservative wrap count, but line boxes at their real
1398
+ // leading (estimateTextHeight pads every paragraph, which left short cards hollow).
1399
+ function cardTextHeight(text, innerW, fontSize, lineSpacing) {
1400
+ return (estimateWrappedLines(text, innerW, fontSize) * fontSize * lineSpacing * 1.2) / 72;
1401
+ }
1402
+
1403
+ function cardHeadingHeight(card, innerW) {
1404
+ return card.heading ? cardTextHeight(card.heading, innerW, 15, 1.05) + 0.06 : 0;
1405
+ }
1406
+
1407
+ function cardBodyHeight(card, innerW) {
1408
+ if (card.bodyRuns && card.bodyRuns.length) return card.bodyHeight || 0;
1409
+ const fs = cardBodySize(card);
1410
+ return (card.lines || []).reduce((sum, line) => sum + cardTextHeight(line, innerW, fs, 1.3), 0.1);
1411
+ }
1412
+
1413
+ /** Height a card needs for its chip, heading and body at the width it is drawn. */
1414
+ function azureCardHeight(card, w) {
1415
+ const innerW = w - 2 * CARD_PAD;
1416
+ const head = card.heading ? 0.18 + Math.max(0.4, cardHeadingHeight(card, innerW)) + 0.04 : 0.18;
1417
+ return CARD_PAD + CARD_CHIP + head + cardBodyHeight(card, innerW) + CARD_PAD;
1418
+ }
1419
+
1420
+ /**
1421
+ * The height a row of cards is drawn at: the tallest card's content, never taller
1422
+ * than the region the template reserves and never shorter than CARD_MIN_H. A fixed
1423
+ * region height left short cards mostly empty; the space below a short row is free
1424
+ * for the takeaway, and the craft gate reports a slide that stays thin.
1425
+ */
1426
+ function fittedCardHeight(cards, w, regionH) {
1427
+ const need = Math.max(...cards.map((c) => azureCardHeight(c, w)));
1428
+ return Math.min(regionH, Math.max(CARD_MIN_H, need));
1429
+ }
1430
+
1431
+ /** Draw one rounded card with a numbered chip, heading, and body lines. */
1432
+ function drawAzureCard(slide, x, y, w, h, card, idx, P, emphasized) {
1433
+ const bg = emphasized ? hex(P.primary_deep) : hex(P.tint);
1434
+ const headColor = emphasized ? "FFFFFF" : hex(P.ink);
1435
+ const bodyColor = emphasized ? hex(P.emphasisTextColor) : hex(P.ink_muted);
1436
+ slide.addShape("roundRect", { x, y, w, h, rectRadius: 0.16, fill: { color: bg }, line: { type: "none" } });
1437
+ const pad = CARD_PAD;
1438
+ const innerW = w - 2 * pad;
1439
+ // numbered chip
1440
+ const chip = CARD_CHIP;
1441
+ slide.addShape("roundRect", {
1442
+ x: x + pad, y: y + pad, w: chip, h: chip, rectRadius: 0.12,
1443
+ fill: { color: emphasized ? "FFFFFF" : hex(P.primary) }, line: { type: "none" },
1444
+ });
1445
+ // Number sits in a text box whose rect is identical to the chip rect, with
1446
+ // margin:0 and tight line spacing so the numeral reads dead-centre.
1447
+ slide.addText(String(idx + 1).padStart(2, "0"), {
1448
+ x: x + pad, y: y + pad, w: chip, h: chip, align: "center", valign: "middle",
1449
+ fontFace: P.titleFont, fontSize: 15, bold: true,
1450
+ color: emphasized ? hex(P.primary_deep) : "FFFFFF",
1451
+ margin: 0, lineSpacingMultiple: 1,
1452
+ });
1453
+ const headH = cardHeadingHeight(card, innerW);
1454
+ if (card.heading) {
1455
+ slide.addText(card.heading, {
1456
+ x: x + pad, y: y + pad + chip + 0.18, w: innerW, h: Math.max(0.4, headH),
1457
+ fontFace: P.titleFont, fontSize: 15, bold: true, color: headColor, align: "left", valign: "top",
1458
+ charSpacing: -0.3,
1459
+ });
1460
+ }
1461
+ const bodyY = y + pad + chip + 0.18 + (card.heading ? Math.max(0.4, headH) + 0.04 : 0);
1462
+ const bodyH = Math.max(0.3, h - (bodyY - y) - 0.18);
1463
+ if (card.bodyRuns && card.bodyRuns.length) {
1464
+ slide.addText(card.bodyRuns, {
1465
+ x: x + pad, y: bodyY, w: innerW, h: bodyH,
1466
+ color: bodyColor, align: "left", valign: "top", lineSpacingMultiple: card.lineSpacing || 1.28, charSpacing: -0.2,
1467
+ });
1468
+ } else if (card.lines && card.lines.length) {
1469
+ // Shrink body font when there are many lines so it never exceeds the frame.
1470
+ const fs = cardBodySize(card);
1471
+ slide.addText(card.lines.join("\n"), {
1472
+ x: x + pad, y: bodyY, w: innerW, h: bodyH,
1473
+ fontFace: P.bodyFont, fontSize: fs, color: bodyColor, align: "left", valign: "top",
1474
+ lineSpacingMultiple: 1.3, charSpacing: -0.2,
1475
+ });
1476
+ }
1477
+ }
1478
+
1479
+ /** Render content/main body as a row of cards, as tall as the longest card needs. */
1480
+ function renderAzureCards(slide, items, region, P) {
1481
+ const cards = bodyToCards(items);
1482
+ if (!cards.length) return;
1483
+ const n = cards.length;
1484
+ const x0 = region.x || 0.6, y0 = region.y || 1.6, w = region.w || 12.1;
1485
+ const gap = 0.28;
1486
+ const cardW = (w - (n - 1) * gap) / n;
1487
+ const h = fittedCardHeight(cards, cardW, region.h || 3.0);
1488
+ cards.forEach((c, i) => {
1489
+ drawAzureCard(slide, x0 + i * (cardW + gap), y0, cardW, h, c, i, P, n >= 3 && i === n - 1);
1490
+ });
1491
+ return y0 + h;
1492
+ }
1493
+
1494
+ /** Flatten body/summary items (sections + children + bullets) into styled runs.
1495
+ * `scale` (≤1) shrinks fonts to fit a fixed card height. */
1496
+ function itemsToRuns(items, P, scale = 1) {
1497
+ const s = (v) => Math.max(8, Math.round(v * scale * 10) / 10);
1498
+ const runs = [];
1499
+ for (const it of items || []) {
1500
+ if (it.type === "section") {
1501
+ runs.push({ text: cleanMd(it.heading || ""), options: { bold: true, fontFace: P.titleFont, fontSize: s(14), color: hex(P.ink), breakLine: true } });
1502
+ for (const c of it.children || []) {
1503
+ runs.push({ text: " " + cleanMd(c.text || ""), options: { fontFace: P.bodyFont, fontSize: s(12.5), color: hex(P.ink_muted), breakLine: true } });
1504
+ }
1505
+ } else if (it.text) {
1506
+ runs.push({ text: (it.type === "numbered" ? " " : "• ") + cleanMd(it.text), options: { fontFace: P.bodyFont, fontSize: s(13), color: hex(P.ink_muted), breakLine: true } });
1507
+ }
1508
+ }
1509
+ return runs;
1510
+ }
1511
+
1512
+ /** Estimate the rendered height of a group's items at a given font scale,
1513
+ * using the same conservative metric inventory respects (estimateTextHeight). */
1514
+ function estimateGroupHeight(items, innerW, scale, lineSpacing) {
1515
+ let hsum = 0;
1516
+ for (const it of items || []) {
1517
+ if (it.type === "section") {
1518
+ hsum += estimateTextHeight(it.heading || "", innerW, 14 * scale, lineSpacing);
1519
+ for (const c of it.children || []) hsum += estimateTextHeight(" " + (c.text || ""), innerW - 0.1, 12.5 * scale, lineSpacing);
1520
+ } else if (it.text) {
1521
+ hsum += estimateTextHeight(it.text, innerW, 13 * scale, lineSpacing);
1522
+ }
1523
+ }
1524
+ return hsum;
1525
+ }
1526
+
1527
+ /** The height a group's runs take at their real leading, for sizing (not fitting) the card. */
1528
+ function groupTextHeight(items, innerW, scale, lineSpacing) {
1529
+ let hsum = 0.1;
1530
+ for (const it of items || []) {
1531
+ if (it.type === "section") {
1532
+ hsum += cardTextHeight(it.heading || "", innerW, 14 * scale, lineSpacing);
1533
+ for (const c of it.children || []) hsum += cardTextHeight(" " + (c.text || ""), innerW - 0.1, 12.5 * scale, lineSpacing);
1534
+ } else if (it.text) {
1535
+ hsum += cardTextHeight("• " + it.text, innerW, 13 * scale, lineSpacing);
1536
+ }
1537
+ }
1538
+ return hsum;
1539
+ }
1540
+
1541
+ /** A summary group as a card: the largest font scale (1 down to 0.55) whose text fits
1542
+ * `maxH`, and the height that text needs at that scale. */
1543
+ function groupCard(content, w, maxH, P) {
1544
+ const innerW = w - 2 * CARD_PAD;
1545
+ const heading = cleanMd(content.heading || "");
1546
+ const headH = heading ? 0.18 + Math.max(0.4, cardHeadingHeight({ heading }, innerW)) + 0.04 : 0.18;
1547
+ const fixed = CARD_PAD + CARD_CHIP + headH + 0.18;
1548
+ let scale = 0.55, ls = 1.12;
1549
+ for (const s of [1, 0.92, 0.85, 0.78, 0.72, 0.66, 0.6, 0.55]) {
1550
+ if (estimateGroupHeight(content.items, innerW, s, s < 1 ? 1.12 : 1.28) <= maxH - fixed) { scale = s; ls = s < 1 ? 1.12 : 1.28; break; }
1551
+ }
1552
+ const bodyHeight = groupTextHeight(content.items, innerW, scale, ls);
1553
+ return {
1554
+ card: { heading, bodyRuns: itemsToRuns(content.items, P, scale), lineSpacing: ls, bodyHeight },
1555
+ need: Math.min(maxH, fixed + bodyHeight + CARD_PAD - 0.18),
1556
+ };
1557
+ }
1558
+
1559
+ /** Render a single summary-group as one card, at `opts.height` (shared by the
1560
+ * groups of one slide) or at the height its own text needs. */
1561
+ function renderAzureGroupCard(slide, content, region, P, idx, height) {
1562
+ const w = region.w || 5.6, maxH = region.h || 2.4;
1563
+ const { card, need } = groupCard(content, w, maxH, P);
1564
+ const h = Math.min(maxH, Math.max(CARD_MIN_H, height || need));
1565
+ drawAzureCard(slide, region.x || 0.6, region.y || 1.6, w, h, card, idx, P, false);
1566
+ }
1567
+
1568
+ /** Render a general data table (palette-styled): primary header, zebra tint rows. */
1569
+ // ── Tables and charts that fill their region ────────────────────────────────
1570
+
1571
+ // A cell reads as a number when, after units and signs, only digits remain.
1572
+ const NUMERIC_CELL = /^[+\-−±▲▼△▽]?\s*[\d.,]+\s*(?:%p?|배|x|pt|bp|[가-힣]{1,3}|[A-Za-z]{1,3})?$/u;
1573
+ const isNumericCell = (text) => NUMERIC_CELL.test(cleanMd(String(text)).trim());
1574
+
1575
+ /** Columns whose data cells are all numbers are right-aligned. */
1576
+ function numericColumns(headers, rows) {
1577
+ return headers.map((_, ci) => {
1578
+ const cells = rows.map((r) => r[ci]).filter((c) => c != null && String(c).trim() !== "");
1579
+ return cells.length > 0 && cells.every(isNumericCell);
1580
+ });
1581
+ }
1582
+
1583
+ /** Column widths proportional to the longest cell, Hangul counted wider, with a floor. */
1584
+ function proportionalWidths(headers, rows, totalW) {
1585
+ const len = (t) => [...cleanMd(String(t || ""))].reduce((n, ch) => n + (/[\u3131-\uD7A3]/u.test(ch) ? 1.7 : 1), 0);
1586
+ const want = headers.map((h, ci) => Math.max(len(h), ...rows.map((r) => len(r[ci])), 4));
1587
+ const floor = Math.min(0.9, totalW / headers.length);
1588
+ const sum = want.reduce((a, b) => a + b, 0);
1589
+ let widths = want.map((w) => Math.max(floor, (w / sum) * totalW));
1590
+ const scale = totalW / widths.reduce((a, b) => a + b, 0);
1591
+ widths = widths.map((w) => w * scale);
1592
+ return widths;
1593
+ }
1594
+
1595
+ /**
1596
+ * Row height and font size for a table that should use the room it has: rows
1597
+ * grow toward `bottom` (up to a comfortable maximum) instead of leaving the lower
1598
+ * half of the slide empty, and a roomy table gets a larger font.
1599
+ */
1600
+ function tableMetrics(rowCount, y, bottom, base) {
1601
+ const room = Math.max(0, (bottom || y + rowCount * base.rowH) - y);
1602
+ const rowH = Math.min(base.maxRowH, Math.max(base.rowH, (room * 0.92) / rowCount));
1603
+ const scale = rowH >= 0.55 ? 1.2 : rowH >= 0.47 ? 1.1 : 1;
1604
+ return { rowH, headerSize: +(base.headerSize * scale).toFixed(1), dataSize: +(base.dataSize * scale).toFixed(1) };
1605
+ }
1606
+
1607
+ function renderAzureTable(slide, content, region, P, bottom) {
1608
+ const headers = content.headers || [];
1609
+ const rows = content.rows || [];
1610
+ if (!headers.length) return;
1611
+ const x = region.x || 0.6, y = region.y || 1.6, w = region.w || 6.0;
1612
+ const numeric = numericColumns(headers, rows);
1613
+ const m = tableMetrics(rows.length + 1, y, bottom, { rowH: 0.4, maxRowH: 0.72, headerSize: 12, dataSize: 11.5 });
1614
+ const tableRows = [];
1615
+ tableRows.push(headers.map((h, ci) => ({
1616
+ text: cleanMd(h),
1617
+ options: { bold: true, color: "FFFFFF", fill: { color: hex(P.primary) }, fontFace: P.titleFont, fontSize: m.headerSize, align: numeric[ci] ? "right" : "left", valign: "middle" },
1618
+ })));
1619
+ rows.forEach((r, ri) => {
1620
+ tableRows.push(r.map((c, ci) => linkedTableCell(c, {
1621
+ color: hex(P.ink), fill: { color: ri % 2 ? "FFFFFF" : hex(P.tint) }, fontFace: P.bodyFont, fontSize: m.dataSize,
1622
+ align: numeric[ci] ? "right" : "left", valign: "middle", bold: ci === 0 && !numeric[0],
1623
+ })));
1624
+ });
1625
+ slide.addTable(tableRows, {
1626
+ x, y, w, colW: proportionalWidths(headers, rows, w),
1627
+ border: { type: "solid", color: hex(P.line), pt: 0.5 }, rowH: m.rowH, margin: [0.04, 0.12, 0.04, 0.12], autoPage: false,
1628
+ });
1629
+ }
1630
+
1631
+ /** Parse "1,234", "▲12.5%", "−3" into a number; anything else is null. */
1632
+ function chartNumber(text) {
1633
+ const t = cleanMd(String(text || "")).trim().replace(/[,\s]/g, "").replace(/^[▲△+]/u, "").replace(/^[▼▽−]/u, "-");
1634
+ const m = t.match(/^-?\d+(?:\.\d+)?/);
1635
+ return m ? parseFloat(m[0]) : null;
1636
+ }
1637
+
1638
+ const CHART_TYPE = { bar: "bar", column: "bar", stacked: "bar", line: "line", area: "area", pie: "pie", doughnut: "doughnut" };
1639
+
1640
+ /**
1641
+ * A native PowerPoint chart (editable in PowerPoint: right-click > Edit Data)
1642
+ * from a `::: chart` block. It takes the whole region down to `bottom`.
1643
+ */
1644
+ function renderChart(slide, content, region, style, bottom) {
1645
+ const headers = content.headers || [];
1646
+ const rows = content.rows || [];
1647
+ const spec = content.chart || {};
1648
+ const kind = CHART_TYPE[spec.type] || "bar";
1649
+ const labels = rows.map((r) => cleanMd(String(r[0] || "")));
1650
+ const series = headers.slice(1).map((name, si) => ({
1651
+ name: cleanMd(String(name)),
1652
+ labels,
1653
+ values: rows.map((r) => chartNumber(r[si + 1]) ?? 0),
1654
+ }));
1655
+ const x = region.x || 0.6, y = region.y || 1.6, w = region.w || 8;
1656
+ const h = Math.max(1.6, ((bottom || y + (region.h || 3.2)) - y));
1657
+ const colors = style.series.map((c) => hex(c));
1658
+ const round = kind === "pie" || kind === "doughnut";
1659
+ const data = round ? series.slice(0, 1) : series;
1660
+ slide.addChart(kind, data, {
1661
+ x, y, w, h,
1662
+ barDir: spec.type === "bar" ? "bar" : "col",
1663
+ barGrouping: spec.type === "stacked" ? "stacked" : "clustered",
1664
+ barGapWidthPct: 60,
1665
+ chartColors: round ? labels.map((_, i) => colors[i % colors.length]) : colors,
1666
+ showLegend: round || data.length > 1,
1667
+ legendPos: round ? "r" : "t",
1668
+ legendFontFace: style.bodyFont, legendFontSize: 11, legendColor: hex(style.ink),
1669
+ showValue: !round && spec.labels !== false,
1670
+ showPercent: round && spec.labels !== false, showLabel: false,
1671
+ dataLabelFontFace: style.bodyFont, dataLabelFontSize: round ? 12 : 10,
1672
+ dataLabelColor: round ? "FFFFFF" : hex(style.ink),
1673
+ dataLabelPosition: round ? "ctr" : kind === "line" ? "t" : "outEnd",
1674
+ dataLabelFormatCode: round ? "0%" : "#,##0.##",
1675
+ catAxisLabelFontFace: style.bodyFont, catAxisLabelFontSize: 11, catAxisLabelColor: hex(style.ink),
1676
+ valAxisLabelFontFace: style.bodyFont, valAxisLabelFontSize: 10, valAxisLabelColor: hex(style.ink_muted),
1677
+ valAxisLabelFormatCode: "#,##0",
1678
+ valAxisHidden: false,
1679
+ catAxisLineShow: true, catAxisLineColor: hex(style.line),
1680
+ valGridLine: { color: hex(style.line), size: 0.5 },
1681
+ valAxisLineShow: false,
1682
+ lineSize: 2.5, lineDataSymbol: "circle", lineDataSymbolSize: 7,
1683
+ holeSize: 58,
1684
+ ...(spec.unit ? { showValAxisTitle: true, valAxisTitle: spec.unit, valAxisTitleFontSize: 10, valAxisTitleColor: hex(style.ink_muted), valAxisTitleFontFace: style.bodyFont } : {}),
1685
+ ...(spec.title ? { showTitle: true, title: spec.title, titleFontFace: style.titleFont, titleFontSize: 13, titleColor: hex(style.ink) } : {}),
1686
+ });
1687
+ }
1688
+
1689
+ /** A main-box as a tinted callout sized to its text, not an outlined frame. */
1690
+ function renderAzureCallout(slide, text, region, P) {
1691
+ const x = region.x || 0.84, y = region.y || 5.8, w = region.w || 11.6;
1692
+ const lines = Math.max(1, Math.ceil(cleanMd(text).length / Math.max(20, w * 9)));
1693
+ const h = Math.min(region.h || 1.0, 0.28 + lines * 0.3);
1694
+ slide.addShape("roundRect", { x, y, w, h, rectRadius: 0.1, fill: { color: hex(P.tint) }, line: { type: "none" } });
1695
+ slide.addText(cleanMd(text), {
1696
+ x: x + 0.24, y, w: w - 0.48, h, fontFace: P.bodyFont, fontSize: 13, bold: true,
1697
+ color: hex(P.primary_deep), align: "left", valign: "middle", margin: 0,
1698
+ });
1699
+ }
1700
+
1701
+ /** Render a kpi-table as a row of KPI cards (value + label); first card filled. */
1702
+ function renderAzureKpi(slide, content, region, P) {
1703
+ const cards = kpiCards(content);
1704
+ const values = cards.map((c) => c.value);
1705
+ const labels = cards.map((c) => c.label);
1706
+ const n = Math.min(values.length, 4);
1707
+ if (!n) return;
1708
+ const x0 = region.x || 0.6, y0 = region.y || 1.6, w = region.w || 12.1, h = region.h || 1.7;
1709
+ const gap = 0.26;
1710
+ const cardW = (w - (n - 1) * gap) / n;
1711
+ for (let i = 0; i < n; i++) {
1712
+ const fill = i === 0;
1713
+ const x = x0 + i * (cardW + gap);
1714
+ slide.addShape("roundRect", {
1715
+ x, y: y0, w: cardW, h, rectRadius: 0.16,
1716
+ fill: { color: fill ? hex(P.primary) : "FFFFFF" },
1717
+ line: fill ? { type: "none" } : { color: hex(P.line), width: 1 },
1718
+ });
1719
+ slide.addText(cleanMd(values[i]), {
1720
+ x: x + 0.28, y: y0 + 0.20, w: cardW - 0.56, h: 0.8,
1721
+ fontFace: P.titleFont, fontSize: 30, bold: true,
1722
+ color: fill ? "FFFFFF" : hex(P.primary), align: "left", valign: "middle", charSpacing: -1, margin: 0,
1723
+ });
1724
+ slide.addText(cleanMd(labels[i]).toUpperCase(), {
1725
+ x: x + 0.30, y: y0 + h - 0.5, w: cardW - 0.56, h: 0.32,
1726
+ fontFace: P.bodyFont, fontSize: 9.5, bold: false,
1727
+ color: fill ? hex(P.emphasisTextColor) : hex(P.ink_muted), align: "left", valign: "middle", charSpacing: 0.4, margin: 0,
1728
+ });
1729
+ }
1730
+ }
1731
+
1732
+ /** A pipe table reads as KPI in two shapes:
1733
+ * - TALL: 2 cols of value|label pairs, ≤4 pairs, value cell short/metric-like.
1734
+ * - WIDE: 3–4 cols, a single data row of short metric values (labels = header).
1735
+ * Anything else (multi-row, long cells) is a real data table. */
1736
+
1737
+
1738
+ /** Normalize a kpi-table into [{value,label}] cards for both KPI shapes. */
1739
+ /**
1740
+ * Is there room to draw this as KPI badges?
1741
+ *
1742
+ * A badge stacks a value over a label and reads as one glance, which stops
1743
+ * working the moment the card is narrower than its own words: in a column half
1744
+ * the slide wide, four badges leave 1.4in each and a label like "영업이익" wraps
1745
+ * to one character per line. Below the floor the same data is drawn as a table,
1746
+ * which narrows gracefully.
1747
+ */
1748
+ function kpiFits(content, widthIn) {
1749
+ const cards = kpiCards(content).length;
1750
+ return cards > 0 && (widthIn || 0) / cards >= KPI_MIN_CARD_WIDTH_IN;
1751
+ }
1752
+
1753
+ function kpiCards(content) {
1754
+ const headers = content.headers || [];
1755
+ const rows = content.rows || [];
1756
+ if (headers.length === 2) {
1757
+ return [headers, ...rows].slice(0, 4).map((p) => ({ value: p[0], label: p[1] }));
1758
+ }
1759
+ const row = rows[0] || [];
1760
+ return headers.slice(0, 4).map((lab, i) => ({ value: row[i] != null ? row[i] : "", label: lab }));
1761
+ }
1762
+
1763
+ /** Azure region dispatch. Returns true if it fully handled the region.
1764
+ * opts.hasImage = slide also has a populated image region (→ never card the body). */
1765
+ function renderAzureRegion(slide, layout, name, region, fontRoles, P, opts = {}) {
1766
+ const content = region.content;
1767
+ if (content == null) return false;
1768
+ if (name === "main_box" && typeof content === "string" && content.trim()) {
1769
+ // Below a card row that came out shorter than its region, the takeaway follows
1770
+ // the cards instead of leaving a band of empty slide between them.
1771
+ const follow = opts.flow && opts.flow.cardsBottom != null ? opts.flow.cardsBottom + 0.32 : null;
1772
+ renderAzureCallout(slide, content, follow != null && follow < (region.y || 5.8) ? { ...region, y: follow } : region, P);
1773
+ return true;
1774
+ }
1775
+ if (layout === "cover" && name === "title" && typeof content === "string" && content.includes("**")) {
1776
+ const role = fontRoles.cover_title || {};
1777
+ slide.addText(azureTitleRuns(content, P), {
1778
+ x: region.x || 0, y: region.y || 0, w: region.w || 8, h: region.h || 1,
1779
+ fontFace: role.font || P.titleFont, fontSize: role.size || 48, bold: role.bold !== false,
1780
+ align: role.align || "left", valign: "middle",
1781
+ charSpacing: role.charSpacing != null ? role.charSpacing : -1, lineSpacingMultiple: role.lineSpacing || 1.08,
1782
+ });
1783
+ return true;
1784
+ }
1785
+ if (typeof content === "object") {
1786
+ if (content.type === "body" && (layout === "content" || layout === "main")) {
1787
+ // Cards ONLY for section-structured bodies (≥2 ### sections) with no image.
1788
+ // Prose bullets, or any body sharing the slide with an image, render as a
1789
+ // normal text column (fall through to addRegion) so nothing is cramped.
1790
+ const sections = (content.items || []).filter((i) => i.type === "section");
1791
+ if (sections.length >= 2 && !opts.hasImage) {
1792
+ const bottom = renderAzureCards(slide, content.items, region, P);
1793
+ if (opts.flow) opts.flow.cardsBottom = bottom;
1794
+ return true;
1795
+ }
1796
+ return false;
1797
+ }
1798
+ if (content.type === "kpi-table") {
1799
+ if (content.chart) {
1800
+ // Pie and doughnut slices carry white labels, so they use the darker tones only.
1801
+ const round = ["pie", "doughnut"].includes(content.chart.type);
1802
+ const series = round ? [P.primary, P.primary_deep, P.azure, P.ink_muted] : [P.primary, P.azure_soft, P.primary_deep, P.ink_muted];
1803
+ renderChart(slide, content, region, { ...P, series }, opts.bottom);
1804
+ }
1805
+ else if (looksLikeKpi(content) && kpiFits(content, region.w)) {
1806
+ renderAzureKpi(slide, content, region, P);
1807
+ }
1808
+ else renderAzureTable(slide, content, region, P, opts.bottom);
1809
+ return true;
1810
+ }
1811
+ if (content.type === "summary-group") {
1812
+ const idx = name === "group_bottom" ? 1 : 0;
1813
+ renderAzureGroupCard(slide, content, region, P, idx, opts.groupHeight);
1814
+ return true;
1815
+ }
1816
+ }
1817
+ return false;
1818
+ }
1819
+
1820
+ // ── Placements (author-positioned boxes and shapes) ─────────────────────────
1821
+
1822
+ /** Region name a placed block should be rendered as. */
1823
+ function partRegionName(blockType) {
1824
+ switch (blockType) {
1825
+ case "title": return "title";
1826
+ case "kpi-table": return "table";
1827
+ case "image": return "image";
1828
+ case "figure-caption": return "figure_caption";
1829
+ case "table-caption": return "table_caption";
1830
+ default: return "body";
1831
+ }
1832
+ }
1833
+
1834
+ /** font_role a placed block falls back to when its box names none. */
1835
+ function defaultPartRole(blockType) {
1836
+ switch (blockType) {
1837
+ case "title": return "section_title";
1838
+ case "figure-caption": return "figure_caption";
1839
+ case "table-caption": return "table_caption";
1840
+ case "notes": return "disclaimer";
1841
+ default: return "body_text";
1842
+ }
1843
+ }
1844
+
1845
+ function estimatePartHeight(content, widthIn, role) {
1846
+ const size = role.size || 13;
1847
+ const spacing = role.lineSpacing || 1.3;
1848
+ if (typeof content === "string") {
1849
+ return estimateTextHeight(content, widthIn, size, spacing) + 0.08;
1850
+ }
1851
+ if (content && content.type === "body") {
1852
+ const text = (content.items || []).map((i) => i.heading || i.text || "").join("\n");
1853
+ return estimateTextHeight(text, widthIn, size, spacing) + 0.12;
1854
+ }
1855
+ return 0.5;
1856
+ }
1857
+
1858
+ /**
1859
+ * Turn a shape placement into the decoration element addDecorations reads.
1860
+ *
1861
+ * A rule is drawn as a zero-height rect whose stroke is the visible mark, so it
1862
+ * reads its colour and thickness from `color`/`width` while every other
1863
+ * primitive reads `line`/`line_width`. Authors write one spelling — `line=` and
1864
+ * `lw=` — and the difference is absorbed here.
1865
+ */
1866
+ function shapeToDecoration(placement, sourceDir) {
1867
+ const isRule = placement.shape === "line";
1868
+ const element = {
1869
+ type: placement.shape,
1870
+ x: placement.x, y: placement.y, w: placement.w, h: placement.h,
1871
+ };
1872
+ for (const [key, value] of Object.entries(placement)) {
1873
+ if (["kind", "z", "shape", "x", "y", "w", "h"].includes(key)) continue;
1874
+ if (key === "lw") element[isRule ? "width" : "line_width"] = value;
1875
+ else if (key === "line" && isRule) element.color = value;
1876
+ else if (key === "src") element.assetPath = resolveImagePath(value, sourceDir);
1877
+ else element[key] = value;
1878
+ }
1879
+ return element;
1880
+ }
1881
+
1882
+ /**
1883
+ * Render one placed box by flowing its blocks down the box.
1884
+ *
1885
+ * Every block but the last takes the height its content needs; the last takes
1886
+ * whatever is left, so a heading followed by body fills the box rather than
1887
+ * floating in its top edge. Nothing here consults the auto-nudge rules: a box
1888
+ * lands exactly where the author put it, and the QA gate reports the consequence.
1889
+ */
1890
+ function renderPlacementBox(slide, placement, ctx) {
1891
+ const blocks = placement.blocks || [];
1892
+ if (blocks.length === 0) return;
1893
+
1894
+ // Columns replace the body region, so they render in the template's own idiom.
1895
+ // An explicit box is literal: the author asked for this content, here.
1896
+ const useRich = ctx.rich && placement.z === "content";
1897
+ const bottom = placement.y + (placement.h != null ? placement.h : 1.0);
1898
+ let y = placement.y;
1899
+
1900
+ blocks.forEach((block, i) => {
1901
+ const remaining = Math.max(0.2, bottom - y);
1902
+ const roleName = placement.role || defaultPartRole(block.type);
1903
+ const role = ctx.fontRoles[roleName] || {};
1904
+ const content = extractContent(block);
1905
+ const isLast = i === blocks.length - 1;
1906
+ const h = isLast
1907
+ ? remaining
1908
+ : Math.min(remaining, Math.max(0.2, estimatePartHeight(content, placement.w, role)));
1909
+
1910
+ const region = {
1911
+ x: placement.x, y, w: placement.w, h,
1912
+ content,
1913
+ font_role: roleName,
1914
+ originX: placement.x,
1915
+ ...(placement.align ? { align: placement.align } : {}),
1916
+ ...(placement.valign ? { valign: placement.valign } : {}),
1917
+ };
1918
+
1919
+ const name = partRegionName(block.type);
1920
+ const rendered = useRich &&
1921
+ renderAzureRegion(slide, ctx.layout, name, region, ctx.fontRoles, ctx.P, {});
1922
+ if (!rendered) {
1923
+ addRegion(slide, name, region, ctx.fontRoles, ctx.pageH, y + h, false, ctx.sourceDir);
1924
+ }
1925
+ y += h;
1926
+ });
1927
+ }
1928
+
1929
+ function renderPlacements(slide, placements, band, ctx) {
1930
+ for (const placement of placements) {
1931
+ if (placement.z !== band) continue;
1932
+ if (placement.kind === "shape") {
1933
+ addDecorations(slide, [shapeToDecoration(placement, ctx.sourceDir)], ctx.slideIndex, ctx.fontRoles);
1934
+ } else {
1935
+ renderPlacementBox(slide, placement, ctx);
1936
+ }
1937
+ }
1938
+ }
1939
+
1940
+ // ── Main render function ────────────────────────────────────────────────────
1941
+
1942
+ /**
1943
+ * Render the full resolved spec to a PPTX file.
1944
+ *
1945
+ * @param {object} resolved - Resolved spec from layout-resolver
1946
+ * @param {object} templateObj - Template object from template-registry
1947
+ * @param {string} outputPath - Where to write the .pptx file
1948
+ * @returns {Promise<string>} Resolves with the output path
1949
+ */
1950
+ /**
1951
+ * The deck-wide `notice:` (for example "예시 데이터 — 실제 수치로 바꿔 주세요") as a
1952
+ * coloured tag at the bottom left of every slide, cover included, so a sample or
1953
+ * draft deck can never be mistaken for a sourced one. A 9 pt grey footnote was easy
1954
+ * to miss; the tag keeps its own fill and text colour (template palette
1955
+ * `notice_fill` / `notice_ink`, default a light red with dark red text, 6:1).
1956
+ */
1957
+ const NOTICE_FILL = "FDECEA";
1958
+ const NOTICE_INK = "A21B12";
1959
+
1960
+ function addNotice(slide, text, pageW, pageH, pal) {
1961
+ const label = cleanMd(text);
1962
+ const fontSize = 10.5;
1963
+ const textW = (weightedTextLength(label) * fontSize * 0.98) / 72;
1964
+ const w = Math.min(pageW * 0.55, textW + 0.46);
1965
+ const h = 0.3;
1966
+ const x = 0.45, y = pageH - h - 0.14;
1967
+ const fill = ((pal && pal.notice_fill) || NOTICE_FILL).replace("#", "");
1968
+ const ink = ((pal && pal.notice_ink) || NOTICE_INK).replace("#", "");
1969
+ slide.addShape("roundRect", { x, y, w, h, rectRadius: 0.15, fill: { color: fill }, line: { type: "none" }, objectName: "lit-notice tag" });
1970
+ slide.addText(label, {
1971
+ objectName: "lit-notice text",
1972
+ x: x + 0.16, y, w: w - 0.32, h,
1973
+ fontFace: FONT_BOLD, fontSize, bold: true, color: ink,
1974
+ align: "left", valign: "middle", margin: 0, fit: "shrink",
1975
+ });
1976
+ }
1977
+
1978
+ async function render(resolved, templateObj, outputPath) {
1979
+ const fontRoles = buildFontRoles(templateObj);
1980
+
1981
+ // Brand-neutralize: derive font/spacing/accent from the template, falling back
1982
+ // to Pretendard when a template names no family.
1983
+ const tpl = (templateObj && templateObj.template) || {};
1984
+ const tplFonts = tpl.fonts || {};
1985
+ const tplType = tpl.global_typography || {};
1986
+ FONT_BOLD = tplFonts.title || "Pretendard";
1987
+ FONT_MEDIUM = tplFonts.body || "Pretendard";
1988
+ FONT_LIGHT = tplFonts.light || "Pretendard";
1989
+ CHAR_SPACING = tplType.char_spacing != null ? tplType.char_spacing : -0.7;
1990
+ BULLET_FONT = FONT_BOLD;
1991
+ SEC_HEADER_FONT = FONT_BOLD;
1992
+ TOC_FONT = FONT_BOLD;
1993
+ BULLET_COLOR = (tplType.bullet_color || "1D4ED8").replace("#", "");
1994
+ SECTION_ACCENT = (tplType.section_accent || "1D4ED8").replace("#", "");
1995
+
1996
+ // Rich card/KPI/divider rendering — palette-driven, shared by render_style
1997
+ // "azure". Tokens come from the template palette.
1998
+ const rich = tpl.render_style === "azure";
1999
+ const pal = tpl.palette || {};
2000
+ PLAIN_CHART_STYLE = {
2001
+ series: [pal.ink || "#3C3836", pal.accent_green_2 || pal.primary || "#689D6A", pal.accent_amber || "#D79921", pal.accent_purple || "#B16286"],
2002
+ ink: pal.ink || "#3C3836", ink_muted: pal.ink_muted || "#6E6256", line: pal.line || "#CBC2B5",
2003
+ titleFont: FONT_BOLD, bodyFont: FONT_MEDIUM,
2004
+ };
2005
+ const P = {
2006
+ titleFont: FONT_BOLD, bodyFont: FONT_MEDIUM,
2007
+ primary: pal.primary || "#1D4ED8", primary_deep: pal.primary_deep || "#0B2E6F",
2008
+ ink: pal.ink || "#0E1B2C", ink_muted: pal.ink_muted || "#51607A",
2009
+ tint: pal.tint || "#EEF4FF", line: pal.line || "#D5DEEC", azure_soft: pal.azure_soft || "#93C5FD", azure: pal.azure || "#3B82F6",
2010
+ emphasisTextColor: pal.emphasis_text || "#C9D8F5",
2011
+ };
2012
+
2013
+ const dims =
2014
+ (templateObj && templateObj.template && templateObj.template.dimensions) || {};
2015
+ const pageW = dims.width || 10.833;
2016
+ const pageH = dims.height || 7.5;
2017
+
2018
+ // Loaded here, not at module load, so AST-only and --list-* runs never install.
2019
+ const PptxGenJS = runtimeRequire("pptxgenjs");
2020
+ const pptx = new PptxGenJS();
2021
+ pptx.defineLayout({ name: "A4_LANDSCAPE", width: pageW, height: pageH });
2022
+ pptx.layout = "A4_LANDSCAPE";
2023
+
2024
+ const title = (resolved.deck && resolved.deck.title) || "Presentation";
2025
+ pptx.title = title;
2026
+ const sourceDir = resolved.sourceDir;
2027
+ const cramStress = isCramStressDeck(resolved);
2028
+ const deckMeta = (resolved.deck && resolved.deck.metadata) || {};
2029
+ const notice = typeof deckMeta.notice === "string" && deckMeta.notice.trim() ? deckMeta.notice.trim() : null;
2030
+
2031
+ for (const slideSpec of resolved.slides || []) {
2032
+ const slide = pptx.addSlide();
2033
+
2034
+ // Draw order is a band at a time: template decorations, then anything the
2035
+ // author put under the content, then the template's regions, then the
2036
+ // columns that replaced the body, then anything sitting on top.
2037
+ addDecorations(slide, slideSpec.decorations, slideSpec.index, fontRoles);
2038
+
2039
+ const placements = slideSpec.placements || [];
2040
+ const placementCtx = {
2041
+ rich, P, fontRoles, pageH, sourceDir,
2042
+ layout: slideSpec.layout,
2043
+ slideIndex: slideSpec.index,
2044
+ };
2045
+ renderPlacements(slide, placements, "under", placementCtx);
2046
+
2047
+ const regions = adjustedRegionsForImageColumn(slideSpec.regions || {});
2048
+ if (cramStress && ["content", "main"].includes(slideSpec.layout) && regions.body) {
2049
+ renderCramStressSlide(slide, { ...slideSpec, regions }, pageW, pageH, sourceDir);
2050
+ continue;
2051
+ }
2052
+
2053
+ // Calculate bottom boundary for regions without explicit h
2054
+ const bottomBoundary = {};
2055
+ for (const [name, region] of Object.entries(regions)) {
2056
+ let bottom = pageH - 0.5;
2057
+ for (const [otherName, otherRegion] of Object.entries(regions)) {
2058
+ if (otherName === name || otherName === "title") continue;
2059
+ if (name === "body" && ["image", "figure_caption"].includes(otherName)) continue;
2060
+ if (otherRegion.y && otherRegion.y > (region.y || 0) && otherRegion.h) {
2061
+ const candidateBottom = otherRegion.y - 0.15;
2062
+ if (candidateBottom < bottom) bottom = candidateBottom;
2063
+ }
2064
+ }
2065
+ bottomBoundary[name] = bottom;
2066
+ }
2067
+
2068
+ const tocSlide = isTocSlide(regions);
2069
+ const hasImage = Object.entries(regions).some(([n, r]) =>
2070
+ (n === "image" || n === "figure") && r.content &&
2071
+ (r.content.type === "image" || r.content.type === "image-grid"));
2072
+
2073
+ // Summary groups sit side by side, so they share the height the fuller one needs.
2074
+ const groups = Object.values(regions).filter((r) => r.content && r.content.type === "summary-group");
2075
+ const groupHeight = rich && groups.length
2076
+ ? Math.max(...groups.map((r) => groupCard(r.content, r.w || 5.6, r.h || 2.4, P).need))
2077
+ : null;
2078
+
2079
+ const flow = {};
2080
+ for (const [name, region] of Object.entries(regions)) {
2081
+ if (rich && renderAzureRegion(slide, slideSpec.layout, name, region, fontRoles, P, { hasImage, bottom: bottomBoundary[name], groupHeight, flow })) continue;
2082
+ addRegion(slide, name, region, fontRoles, pageH, bottomBoundary[name], tocSlide, sourceDir);
2083
+ }
2084
+
2085
+ renderPlacements(slide, placements, "content", placementCtx);
2086
+ renderPlacements(slide, placements, "over", placementCtx);
2087
+ if (notice) addNotice(slide, notice, pageW, pageH, pal);
2088
+ }
2089
+
2090
+ await pptx.writeFile({ fileName: outputPath });
2091
+ return outputPath;
2092
+ }
2093
+
2094
+ module.exports = { render };