thursday-agent 0.16.0 → 0.17.0

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 (277) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/prerender-manifest.json +3 -3
  4. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/api/artifact/route.js +9 -9
  17. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  18. package/.next/server/app/api/bot/memory/files/route.js +3 -3
  19. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  20. package/.next/server/app/api/bot/memory/route.js +8 -8
  21. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/route.js +8 -8
  23. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  24. package/.next/server/app/api/bot/thread/[id]/route.js +3 -3
  25. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
  26. package/.next/server/app/api/bot/thread/route.js +3 -3
  27. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
  28. package/.next/server/app/api/config/route.js.nft.json +1 -1
  29. package/.next/server/app/api/events/route.js.nft.json +1 -1
  30. package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
  31. package/.next/server/app/api/file/[...path]/route.js +4 -4
  32. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  34. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  35. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  36. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  37. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  38. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  39. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  40. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  41. package/.next/server/app/api/memory/edit/route.js +11 -11
  42. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  43. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  44. package/.next/server/app/api/reach/route.js +11 -11
  45. package/.next/server/app/api/reach/route.js.nft.json +1 -1
  46. package/.next/server/app/api/routine/route.js +7 -7
  47. package/.next/server/app/api/routine/route.js.nft.json +1 -1
  48. package/.next/server/app/api/signins/route.js +5 -5
  49. package/.next/server/app/api/signins/route.js.nft.json +1 -1
  50. package/.next/server/app/api/skills/[source]/[name]/route.js +7 -7
  51. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  52. package/.next/server/app/api/skills/route.js +7 -7
  53. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  54. package/.next/server/app/api/thursday/call/route.js +5 -5
  55. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  56. package/.next/server/app/api/thursday/settings/route.js +5 -5
  57. package/.next/server/app/api/thursday/settings/route.js.nft.json +1 -1
  58. package/.next/server/app/api/thursday/text/route.js +13 -13
  59. package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
  60. package/.next/server/app/api/thursday/tool-call/route.js +12 -12
  61. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  62. package/.next/server/app/api/workspace/files/route.js +8 -8
  63. package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
  64. package/.next/server/app/api/workspace/route.js +9 -9
  65. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  66. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +3 -3
  67. package/.next/server/app/artifact/[...path]/page.js +4 -4
  68. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  69. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  70. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  71. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  72. package/.next/server/app/page/server-reference-manifest.json +60 -60
  73. package/.next/server/app/page.js +5 -5
  74. package/.next/server/app/page.js.nft.json +1 -1
  75. package/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/.next/server/chunks/[root-of-the-server]__00qyw01._.js +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__01ypm_z._.js +3 -0
  78. package/.next/server/chunks/[root-of-the-server]__021dkui._.js +3 -0
  79. package/.next/server/chunks/{[root-of-the-server]__18e65_-._.js → [root-of-the-server]__03xamry._.js} +2 -2
  80. package/.next/server/chunks/[root-of-the-server]__05dp5tr._.js +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__09gdscj._.js +7 -0
  82. package/.next/server/chunks/{[root-of-the-server]__10_7vqz._.js → [root-of-the-server]__0b36yfw._.js} +1 -1
  83. package/.next/server/chunks/{[root-of-the-server]__1gamvcz._.js → [root-of-the-server]__0jzwdg-._.js} +2 -2
  84. package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
  85. package/.next/server/chunks/[root-of-the-server]__0nmust6._.js +5 -0
  86. package/.next/server/chunks/[root-of-the-server]__0svzjwa._.js +7 -0
  87. package/.next/server/chunks/{[root-of-the-server]__16yw3mb._.js → [root-of-the-server]__12m24xl._.js} +1 -1
  88. package/.next/server/chunks/{[root-of-the-server]__1zrwxis._.js → [root-of-the-server]__12tahb4._.js} +1 -1
  89. package/.next/server/chunks/{[root-of-the-server]__1ep-pao._.js → [root-of-the-server]__1_h4hf9._.js} +4 -4
  90. package/.next/server/chunks/{[root-of-the-server]__1-tdj60._.js → [root-of-the-server]__1de1o6m._.js} +2 -2
  91. package/.next/server/chunks/{[root-of-the-server]__1voqqyc._.js → [root-of-the-server]__1ignh6-._.js} +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__1t02-1p._.js +3 -0
  93. package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
  94. package/.next/server/chunks/{[root-of-the-server]__11g20j-._.js → [root-of-the-server]__1yl5l25._.js} +1 -1
  95. package/.next/server/chunks/{[root-of-the-server]__1mbd2w_._.js → [root-of-the-server]__208vmyf._.js} +1 -1
  96. package/.next/server/chunks/{_1mfu9a3._.js → _02lipqs._.js} +1 -1
  97. package/.next/server/chunks/_05sa7xl._.js +1 -1
  98. package/.next/server/chunks/{_1rg5bfd._.js → _074f2ts._.js} +1 -1
  99. package/.next/server/chunks/_07nxi75._.js +3 -3
  100. package/.next/server/chunks/_08btiir._.js +7 -0
  101. package/.next/server/chunks/_0caua_s._.js +1 -1
  102. package/.next/server/chunks/{_0qoush_._.js → _0gt6cbj._.js} +1 -1
  103. package/.next/server/chunks/_0xygcxs._.js +3 -3
  104. package/.next/server/chunks/{_09c4we8._.js → _17i1lt2._.js} +1 -1
  105. package/.next/server/chunks/_19kea1l._.js +9 -9
  106. package/.next/server/chunks/_1gn2r2q._.js +3 -3
  107. package/.next/server/chunks/{_03niwml._.js → _1ntwp1s._.js} +1 -1
  108. package/.next/server/chunks/_1q6e56i._.js +3 -3
  109. package/.next/server/chunks/{_02fn1o3._.js → _1tkzgtm._.js} +1 -1
  110. package/.next/server/chunks/_1wl2g8_._.js +2 -2
  111. package/.next/server/chunks/{_1w6uf9k._.js → _1y3mnz4._.js} +1 -1
  112. package/.next/server/chunks/features_workspace_workspace_ts_13h82gv._.js +4 -4
  113. package/.next/server/chunks/features_workspace_workspace_ts_1iyjroj._.js +4 -4
  114. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0cux6zt._.js → node_modules_consola_dist_chunks_prompt_mjs_04oxl25._.js} +1 -1
  115. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0w32bqh._.js → node_modules_consola_dist_chunks_prompt_mjs_0g87zst._.js} +1 -1
  116. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_1clmnaj._.js → node_modules_consola_dist_chunks_prompt_mjs_1341cnf._.js} +1 -1
  117. package/.next/server/chunks/ssr/{[root-of-the-server]__02jdtqa._.js → [root-of-the-server]__07660xu._.js} +2 -2
  118. package/.next/server/chunks/ssr/{[root-of-the-server]__0rnjkw2._.js → [root-of-the-server]__149x25m._.js} +2 -2
  119. package/.next/server/chunks/ssr/[root-of-the-server]__17h1ggo._.js +1 -1
  120. package/.next/server/chunks/ssr/[root-of-the-server]__19s8qyj._.js +3 -0
  121. package/.next/server/chunks/ssr/[root-of-the-server]__1jyeu61._.js +1 -1
  122. package/.next/server/chunks/ssr/_088vzgn._.js +3 -0
  123. package/.next/server/chunks/ssr/{_0wupyvd._.js → _0_vnw9v._.js} +2 -2
  124. package/.next/server/chunks/ssr/{_1u5vsy-._.js → _0aeknva._.js} +2 -2
  125. package/.next/server/chunks/ssr/_0fo4552._.js +3 -3
  126. package/.next/server/chunks/ssr/{_0-ou1b0._.js → _0n2scfq._.js} +2 -2
  127. package/.next/server/chunks/ssr/_0o_o9ji._.js +3 -0
  128. package/.next/server/chunks/ssr/_0qne8xs._.js +3 -0
  129. package/.next/server/chunks/ssr/_0r0g9ki._.js +1 -1
  130. package/.next/server/chunks/ssr/_0v9r8g2._.js +3 -0
  131. package/.next/server/chunks/ssr/_0x8ophf._.js +1 -1
  132. package/.next/server/chunks/ssr/{_0ncuqct._.js → _0ztbn5q._.js} +2 -2
  133. package/.next/server/chunks/ssr/{_0w8r7o7._.js → _1-41lck._.js} +2 -2
  134. package/.next/server/chunks/ssr/{_10fwcz_._.js → _14dfh7a._.js} +2 -2
  135. package/.next/server/chunks/ssr/{_1f6s34p._.js → _15m5ctz._.js} +2 -2
  136. package/.next/server/chunks/ssr/{_11pnnvd._.js → _18-e7zi._.js} +2 -2
  137. package/.next/server/chunks/ssr/{_0y28gl3._.js → _1c5iqz1._.js} +3 -3
  138. package/.next/server/chunks/ssr/{_1dqaksc._.js → _1hbjh6n._.js} +2 -2
  139. package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
  140. package/.next/server/chunks/ssr/{_1k0efi3._.js → _1sttj8x._.js} +2 -2
  141. package/.next/server/chunks/ssr/_1uij_rj._.js +1 -1
  142. package/.next/server/chunks/ssr/_1umbr78._.js +1 -1
  143. package/.next/server/chunks/ssr/{_17ble8k._.js → _1vtgiou._.js} +3 -3
  144. package/.next/server/chunks/ssr/_1w9w1ok._.js +1 -1
  145. package/.next/server/chunks/ssr/{_1z586yw._.js → _1z6pirx._.js} +2 -2
  146. package/.next/server/chunks/ssr/features_0796o8r._.js +1 -1
  147. package/.next/server/chunks/ssr/features_bot_1knyan7._.js +3 -0
  148. package/.next/server/chunks/ssr/features_bot_1kr0_m-._.js +3 -0
  149. package/.next/server/chunks/ssr/{features_config_11dpa7r._.js → features_config_0n0zu89._.js} +3 -3
  150. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  151. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
  152. package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
  153. package/.next/server/chunks/ssr/features_workspace_workspace_ts_1wc7_u_._.js +4 -4
  154. package/.next/server/chunks/ssr/{node_modules_1r5o-bo._.js → node_modules_00h1t1j._.js} +1 -1
  155. package/.next/server/chunks/ssr/{node_modules_1o9n7_n._.js → node_modules_02ficyi._.js} +1 -1
  156. package/.next/server/chunks/ssr/{node_modules_0u24mg0._.js → node_modules_03q71_j._.js} +1 -1
  157. package/.next/server/chunks/ssr/{node_modules_138kcrz._.js → node_modules_03uejh6._.js} +1 -1
  158. package/.next/server/chunks/ssr/{node_modules_0x_2io2._.js → node_modules_06grs09._.js} +1 -1
  159. package/.next/server/chunks/ssr/{node_modules_0n1js5k._.js → node_modules_07mo9rn._.js} +1 -1
  160. package/.next/server/chunks/ssr/{node_modules_0bas34k._.js → node_modules_09e8kz_._.js} +1 -1
  161. package/.next/server/chunks/ssr/{node_modules_1dzye0n._.js → node_modules_09ikzel._.js} +1 -1
  162. package/.next/server/chunks/ssr/{node_modules_18yn_bs._.js → node_modules_0_d_7gq._.js} +1 -1
  163. package/.next/server/chunks/ssr/{node_modules_0ka8te1._.js → node_modules_0_p8w7p._.js} +1 -1
  164. package/.next/server/chunks/ssr/{node_modules_0mark4f._.js → node_modules_0f5bmkb._.js} +1 -1
  165. package/.next/server/chunks/ssr/{node_modules_0sdjzae._.js → node_modules_0kwie-o._.js} +1 -1
  166. package/.next/server/chunks/ssr/{node_modules_1ozyial._.js → node_modules_0r-9e4n._.js} +1 -1
  167. package/.next/server/chunks/ssr/{node_modules_156m1qh._.js → node_modules_0u_8z64._.js} +1 -1
  168. package/.next/server/chunks/ssr/{node_modules_0-ew8f8._.js → node_modules_0zf3wuu._.js} +1 -1
  169. package/.next/server/chunks/ssr/{node_modules_1xua-g1._.js → node_modules_1-7fc7h._.js} +1 -1
  170. package/.next/server/chunks/ssr/{node_modules_0ld7p-1._.js → node_modules_142z3o5._.js} +1 -1
  171. package/.next/server/chunks/ssr/{node_modules_0-8r9yw._.js → node_modules_180l3i0._.js} +1 -1
  172. package/.next/server/chunks/ssr/{node_modules_1tj7_9z._.js → node_modules_197vh1s._.js} +1 -1
  173. package/.next/server/chunks/ssr/{node_modules_0y5uubt._.js → node_modules_1_n9o9s._.js} +1 -1
  174. package/.next/server/chunks/ssr/{node_modules_0etz1i0._.js → node_modules_1fw32pg._.js} +1 -1
  175. package/.next/server/chunks/ssr/{node_modules_0bbwe-u._.js → node_modules_1hefp6h._.js} +1 -1
  176. package/.next/server/chunks/ssr/{node_modules_097cl9f._.js → node_modules_1irot3r._.js} +1 -1
  177. package/.next/server/chunks/ssr/{node_modules_0uibk9z._.js → node_modules_1om2itc._.js} +1 -1
  178. package/.next/server/chunks/ssr/{node_modules_1zkzksd._.js → node_modules_1ubln7w._.js} +1 -1
  179. package/.next/server/chunks/ssr/{node_modules_02uwbwp._.js → node_modules_1wyhtw9._.js} +1 -1
  180. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_133hjui._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0aczkbn._.js} +1 -1
  181. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1b3vwd3._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0ah684s._.js} +1 -1
  182. package/.next/server/middleware-build-manifest.js +3 -3
  183. package/.next/server/pages/404.html +1 -1
  184. package/.next/server/pages/500.html +1 -1
  185. package/.next/server/server-reference-manifest.js +1 -1
  186. package/.next/server/server-reference-manifest.json +61 -61
  187. package/.next/static/chunks/{1ub9obhgfm4jl.js → 024plctckvq-1.js} +1 -1
  188. package/.next/static/chunks/04toe_3nqfjqt.js +1 -0
  189. package/.next/static/chunks/{0609ejkwuy4sf.js → 0_b_bi2pobasx.js} +2 -2
  190. package/.next/static/chunks/{3ro66d9dc70bc.js → 0dfvwgnyrp8qt.js} +1 -1
  191. package/.next/static/chunks/{314szf8yer-3n.js → 0f47nnshdlucs.js} +1 -1
  192. package/.next/static/chunks/0sowxe4nnl5qu.js +1 -0
  193. package/.next/static/chunks/{07vcuxousbmy1.js → 0sv142q1yrva2.js} +1 -1
  194. package/.next/static/chunks/{0q4ws7qd21chx.js → 0tp868040z9nn.js} +2 -2
  195. package/.next/static/chunks/{0unonxbajyknw.js → 1-63yde5eaz7i.js} +1 -1
  196. package/.next/static/chunks/14qitm0kwsb6g.js +15 -0
  197. package/.next/static/chunks/{2xjr5b-yk_h3o.js → 1bgvxd3xny53w.js} +1 -1
  198. package/.next/static/chunks/{245_cp14iz637.js → 1cyrrz4q871aj.js} +1 -1
  199. package/.next/static/chunks/1st3b0c9vhxg3.js +1 -0
  200. package/.next/static/chunks/{39miwvg4k-b2d.js → 1wbqaidq7vc3_.js} +1 -1
  201. package/.next/static/chunks/{0cl_fgkphk1oz.js → 28sxb_k460e9h.js} +1 -1
  202. package/.next/static/chunks/{3j-l8un0d77v7.js → 2v6e_9p95o2e3.js} +1 -1
  203. package/.next/static/chunks/{23tx-2paw9qa6.js → 3-qp777vtfglj.js} +1 -1
  204. package/.next/static/chunks/{2jbjyazqfqyoh.js → 33ekkscpcq-z1.js} +1 -1
  205. package/.next/static/chunks/388l-zszwa_jf.js +1 -0
  206. package/.next/static/chunks/{3vwjs5kq11417.js → 38edd4rpcbvzp.js} +1 -1
  207. package/.next/static/chunks/3bwrvpjk95_x4.js +1 -0
  208. package/.next/static/chunks/{42m371fn687r5.js → 3itdg6t243u6f.js} +1 -1
  209. package/.next/static/chunks/{2oa4gpkzjtpni.js → 44vre0f7xqsh-.js} +1 -1
  210. package/README.md +12 -12
  211. package/guide/bots.md +48 -8
  212. package/guide/calls.md +13 -12
  213. package/guide/setup.md +4 -2
  214. package/package.json +1 -1
  215. package/public/sounds/NOTICE +3 -0
  216. package/public/sounds/opening-land.ogg +0 -0
  217. package/public/sounds/opening-rise.ogg +0 -0
  218. package/public/sounds/opening-step-1.ogg +0 -0
  219. package/public/sounds/opening-step-2.ogg +0 -0
  220. package/public/sounds/opening-step-3.ogg +0 -0
  221. package/skills/README.md +3 -0
  222. package/skills/artifact/SKILL.md +6 -5
  223. package/skills/artifact/references/book.md +22 -8
  224. package/skills/artifact/references/deck.md +4 -2
  225. package/skills/artifact/references/document.md +9 -0
  226. package/skills/artifact/references/sheet.md +108 -0
  227. package/skills/artifact/runtime/book/book.css +75 -0
  228. package/skills/artifact/runtime/book/book.html +6 -3
  229. package/skills/artifact/runtime/book/book.js +21 -0
  230. package/skills/artifact/runtime/canvas/canvas.css +1 -1
  231. package/skills/artifact/runtime/canvas/canvas.js +11 -1
  232. package/skills/artifact/runtime/deck/deck.css +164 -1
  233. package/skills/artifact/runtime/deck/deck.js +76 -1
  234. package/skills/artifact/runtime/document/docx.js +631 -0
  235. package/skills/artifact/runtime/document/markdown.mjs +58 -2
  236. package/skills/artifact/runtime/document/quick.css +66 -0
  237. package/skills/artifact/runtime/document/quick.html +1 -0
  238. package/skills/artifact/runtime/document/quick.js +154 -0
  239. package/skills/artifact/runtime/document/word.mjs +65 -0
  240. package/skills/artifact/runtime/sheet/format.mjs +385 -0
  241. package/skills/artifact/runtime/sheet/formula.mjs +755 -0
  242. package/skills/artifact/runtime/sheet/sheet.css +499 -0
  243. package/skills/artifact/runtime/sheet/sheet.html +102 -0
  244. package/skills/artifact/runtime/sheet/sheet.js +1361 -0
  245. package/skills/artifact/runtime/sheet/xlsx.mjs +320 -0
  246. package/skills/artifact/runtime/shell/head.html +1 -1
  247. package/skills/artifact/runtime/shell/shell.css +3 -0
  248. package/skills/artifact/runtime/shell/wear.mjs +47 -1
  249. package/skills/artifact/runtime/vendor/fflate.LICENSE.md +23 -0
  250. package/skills/artifact/runtime/vendor/fflate.mjs +2710 -0
  251. package/skills/artifact/scripts/book.mjs +141 -5
  252. package/skills/artifact/scripts/chart.mjs +229 -11
  253. package/skills/artifact/scripts/document.mjs +29 -2
  254. package/skills/artifact/scripts/spreadsheet.mjs +1068 -0
  255. package/skills/data-report/SKILL.md +8 -1
  256. package/.next/server/chunks/[root-of-the-server]__0aijj66._.js +0 -3
  257. package/.next/server/chunks/[root-of-the-server]__0tvoct-._.js +0 -3
  258. package/.next/server/chunks/[root-of-the-server]__118qjk_._.js +0 -7
  259. package/.next/server/chunks/[root-of-the-server]__1b8tp42._.js +0 -5
  260. package/.next/server/chunks/[root-of-the-server]__1gzcbda._.js +0 -3
  261. package/.next/server/chunks/[root-of-the-server]__1qcn1ti._.js +0 -7
  262. package/.next/server/chunks/_0xx3ryc._.js +0 -7
  263. package/.next/server/chunks/ssr/[root-of-the-server]__1y5_1cv._.js +0 -3
  264. package/.next/server/chunks/ssr/_0l1-xc6._.js +0 -3
  265. package/.next/server/chunks/ssr/_1g48r6t._.js +0 -3
  266. package/.next/server/chunks/ssr/features_bot_1k25jdg._.js +0 -3
  267. package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +0 -3
  268. package/.next/server/chunks/ssr/lib_utils_ts_0_f1xvm._.js +0 -3
  269. package/.next/static/chunks/00d54fcfm-lko.js +0 -1
  270. package/.next/static/chunks/0gjlsu6vrfzod.js +0 -1
  271. package/.next/static/chunks/17yqt1p_svv13.js +0 -1
  272. package/.next/static/chunks/203pbwecfs3tm.js +0 -1
  273. package/.next/static/chunks/3ghvoe_mlljzs.js +0 -15
  274. package/.next/static/chunks/3gn7dubb4-pwy.js +0 -1
  275. /package/.next/static/{RtLRT9EEaGn-OqwpMFdYg → VtbCMn-0G969p8QJsWdGv}/_buildManifest.js +0 -0
  276. /package/.next/static/{RtLRT9EEaGn-OqwpMFdYg → VtbCMn-0G969p8QJsWdGv}/_clientMiddlewareManifest.js +0 -0
  277. /package/.next/static/{RtLRT9EEaGn-OqwpMFdYg → VtbCMn-0G969p8QJsWdGv}/_ssgManifest.js +0 -0
@@ -2,6 +2,8 @@
2
2
  // A picture book: one HTML file in a folder of its own, printed and read aloud.
3
3
  //
4
4
  // node book.mjs new <name> the book, styled, to write pages into
5
+ // node book.mjs put <name> <pages.html> [--lang ko] the pages written in <pages.html> put
6
+ // into the book, checked first
5
7
  // node book.mjs shots <name> every page as a picture, and all on one, in scratch/
6
8
  // node book.mjs pdf <name> the book as a PDF, one page a sheet
7
9
  // node book.mjs video <name> <audio>... [--size WxH] the book as an mp4, one audio file per page
@@ -15,6 +17,7 @@ import {
15
17
  } from "node:fs";
16
18
  import { dirname, join, resolve } from "node:path";
17
19
  import { fileURLToPath } from "node:url";
20
+ import { markSvg } from "../runtime/shell/wear.mjs";
18
21
  import {
19
22
  ARTIFACTS,
20
23
  NAME,
@@ -51,6 +54,17 @@ function openBook(name) {
51
54
  return book;
52
55
  }
53
56
 
57
+ /** Who is making the book, with their face (shell wear.mjs markSvg); nothing outside a bot's job. */
58
+ function maker() {
59
+ const bot = process.env.THURSDAY_BOT?.trim();
60
+ if (!bot) return "";
61
+ const name = bot.replace(
62
+ /[&<>"]/g,
63
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
64
+ );
65
+ return `<p class="maker" hidden>${markSvg(24)}${name}</p>`;
66
+ }
67
+
54
68
  /** One HTML file with its stylesheet and page turning inlined, which the web, print and the video all read. */
55
69
  function newBook(name) {
56
70
  const out = bookFile(name);
@@ -63,11 +77,132 @@ function newBook(name) {
63
77
  out,
64
78
  part("book.html")
65
79
  .replaceAll("{{title}}", name)
80
+ .replace("{{maker}}", () => maker())
66
81
  .replace("/* book.css */", () => part("book.css"))
67
82
  .replace("// book.js", () => part("book.js")),
68
83
  );
69
84
  console.log(
70
- `${shown(out)} is ready: one file that opens offline, styled already. Write one <section class="page"> per page inside <main> (the comment inside says how), and hand back this path. Its pictures, its PDF and its video belong in the folder around it, ${shown(dirname(out))}, which is what the user sees as the book.`,
85
+ `${shown(out)} is ready: one file that opens offline, styled already. Write its pages, one <section class="page"> each (references/book.md), in a file of your own, then: node ${SCRIPT} put ${name} <that file> — and hand back this path. Its pictures, its PDF and its video belong in the folder around it, ${shown(dirname(out))}, which is what the user sees as the book.`,
86
+ );
87
+ }
88
+
89
+ /** Where the book's own <main> starts and ends: the last one opened before it closes. */
90
+ function mainOf(html) {
91
+ const end = html.lastIndexOf("</main>");
92
+ const start = html.lastIndexOf("<main>", end);
93
+ if (start === -1 || end === -1) return null;
94
+ return { start: start + "<main>".length, end };
95
+ }
96
+
97
+ /** The book's pages as they are, or a Stop when none has been written into it yet. */
98
+ function writtenBook(name) {
99
+ const book = openBook(name);
100
+ const html = readFileSync(book, "utf8");
101
+ const at = mainOf(html);
102
+ const pages = at ? html.slice(at.start, at.end) : "";
103
+ // The pages `new` leaves: a cover named for the file and a page with nothing to read aloud
104
+ if (!/<section\b/.test(pages) || /<section\b[^>]*data-say=""/.test(pages))
105
+ throw new Stop(
106
+ `${shown(book)} has no pages written into it yet${/data-say=""/.test(pages) ? " (a page in it has nothing in data-say)" : ""}. Write them in a file and put them: node ${SCRIPT} put ${name} <pages.html>.`,
107
+ );
108
+ return book;
109
+ }
110
+
111
+ /**
112
+ * The pages in `from` checked and put into the book's <main>, in place of what it held; the
113
+ * rest of the file — its style, its page turning, who made it — as it was. Every page is a
114
+ * <section class="page"> with data-say; the first is the cover with an <h1>; a page is a
115
+ * picture (<figure> with an <img> or an <svg>) and at most two <p>; a quiz page is its <h2>
116
+ * question, two to four choices, one of them right, and an answer. A picture beside the book
117
+ * must be there. What breaks one of these stops, naming the page.
118
+ */
119
+ function putPages(name, from, args) {
120
+ const book = openBook(name);
121
+ if (!from || !existsSync(from))
122
+ throw new Stop(
123
+ `No file ${from ?? ""}. Write the pages in one and give it: node ${SCRIPT} put ${name} <pages.html>`,
124
+ );
125
+ const written = readFileSync(from, "utf8").replace(/<!--[\s\S]*?-->/g, "");
126
+ const sections = written.match(/<section\b[^>]*>[\s\S]*?<\/section>/g) ?? [];
127
+ const outside = written
128
+ .replace(/<section\b[^>]*>[\s\S]*?<\/section>/g, "")
129
+ .trim();
130
+ if (outside)
131
+ throw new Stop(
132
+ `Only pages go in the file, each a <section class="page">…</section>; this is outside them: ${outside.slice(0, 120)}`,
133
+ );
134
+ if (sections.length < 2)
135
+ throw new Stop(
136
+ 'A book is a cover and at least one page: two <section class="page"> at least.',
137
+ );
138
+ const folder = dirname(book);
139
+ sections.forEach((section, i) => {
140
+ const n = `Page ${i + 1}`;
141
+ const open = /^<section\b[^>]*>/.exec(section)[0];
142
+ const classes = /class="([^"]*)"/.exec(open)?.[1].split(/\s+/) ?? [];
143
+ const count = (re) => (section.match(re) ?? []).length;
144
+ if (!classes.includes("page"))
145
+ throw new Stop(`${n} is not a <section class="page">.`);
146
+ if (!/data-say="[^"]*\S[^"]*"/.test(open))
147
+ throw new Stop(
148
+ `${n} has no data-say: the sentence read aloud over it in the video.`,
149
+ );
150
+ if (count(/<svg\b/g) !== count(/<\/svg>/g))
151
+ throw new Stop(`${n}: an <svg> in it is not closed.`);
152
+ if (i === 0 && (!classes.includes("cover") || !/<h1\b/.test(section)))
153
+ throw new Stop(
154
+ `${n} is the cover: <section class="page cover" data-say="…"> with its picture and an <h1>.`,
155
+ );
156
+ if (classes.includes("quiz")) {
157
+ const choices = /<ol class="choices">([\s\S]*?)<\/ol>/.exec(section)?.[1];
158
+ const picks = (choices?.match(/<li\b/g) ?? []).length;
159
+ if (!/<h2\b/.test(section) || !choices)
160
+ throw new Stop(
161
+ `${n} is a quiz: an <h2> question, then <ol class="choices"> of two to four <li><button>.`,
162
+ );
163
+ const buttons = choices.match(/<button\b[^>]*>/g) ?? [];
164
+ if (buttons.length < 2 || buttons.length > 4 || picks !== buttons.length)
165
+ throw new Stop(
166
+ `${n}: a quiz has two to four choices, each an <li> holding one <button>; it has ${buttons.length}.`,
167
+ );
168
+ if (buttons.filter((b) => /\sdata-right\b/.test(b)).length !== 1)
169
+ throw new Stop(`${n}: exactly one choice's <button> has data-right.`);
170
+ if (!/<p class="answer">/.test(section))
171
+ throw new Stop(`${n}: a quiz ends with <p class="answer"> saying why.`);
172
+ } else if (i > 0) {
173
+ if (!/<figure\b[\s\S]*?(<img\b|<svg\b)[\s\S]*?<\/figure>/.test(section))
174
+ throw new Stop(
175
+ `${n} has no picture: a <figure> with an <img> or an <svg> in it.`,
176
+ );
177
+ const lines = count(/<p\b/g);
178
+ if (lines < 1 || lines > 2)
179
+ throw new Stop(
180
+ `${n} has ${lines} lines; a page has one or two <p> under its picture, and more is another page.`,
181
+ );
182
+ }
183
+ for (const [, src] of section.matchAll(/<img\b[^>]*\ssrc="([^"]+)"/g))
184
+ if (!/^(https?:|data:)/.test(src) && !existsSync(resolve(folder, src)))
185
+ throw new Stop(
186
+ `${n}: its picture ${src} is not in ${shown(folder)}; put the file there, or name it as it is.`,
187
+ );
188
+ });
189
+ let html = readFileSync(book, "utf8");
190
+ const at = mainOf(html);
191
+ if (!at) throw new Stop(`${shown(book)} has no <main> to put pages in.`);
192
+ html = `${html.slice(0, at.start)}\n${sections.map((s) => ` ${s.trim()}`).join("\n\n")}\n ${html.slice(at.end)}`;
193
+ const at2 = args.indexOf("--lang");
194
+ if (at2 !== -1) {
195
+ const lang = args[at2 + 1] ?? "";
196
+ if (!/^[a-zA-Z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(lang))
197
+ throw new Stop(`--lang takes a language code: ko, en, ja, pt-BR.`);
198
+ html = html.replace(/<html\b[^>]*>/, `<html lang="${lang}">`);
199
+ }
200
+ writeFileSync(book, html);
201
+ const quiz = sections.some((s) =>
202
+ /^<section\b[^>]*class="[^"]*\bquiz\b/.test(s),
203
+ );
204
+ console.log(
205
+ `Put ${sections.length} pages into ${shown(book)}${quiz ? ", a quiz among them" : ""}. Look at them once: node ${SCRIPT} shots ${name}`,
71
206
  );
72
207
  }
73
208
 
@@ -76,7 +211,7 @@ function newBook(name) {
76
211
  * scratch/: a look at the book, never part of it, so never in the book's own folder.
77
212
  */
78
213
  function shotBook(name) {
79
- const book = openBook(name);
214
+ const book = writtenBook(name);
80
215
  const out = join(WORKSPACE, "scratch", `${name}-shots`);
81
216
  const sheet = join(out, "book.png");
82
217
  rmSync(out, { recursive: true, force: true });
@@ -108,7 +243,7 @@ function shotBook(name) {
108
243
  }
109
244
 
110
245
  function videoBook(name, args) {
111
- const book = openBook(name);
246
+ const book = writtenBook(name);
112
247
  const at = args.indexOf("--size");
113
248
  const size = at === -1 ? "1920x1080" : (args[at + 1] ?? "");
114
249
  const voices = args.filter((_, i) => at === -1 || (i !== at && i !== at + 1));
@@ -129,11 +264,12 @@ try {
129
264
  if (command === "new") newBook(rest[0]);
130
265
  else if (command === "shots") shotBook(rest[0]);
131
266
  else if (command === "pdf")
132
- await bookPdf({ book: openBook(rest[0]), shown }, Stop);
267
+ await bookPdf({ book: writtenBook(rest[0]), shown }, Stop);
268
+ else if (command === "put") putPages(rest[0], rest[1], rest.slice(2));
133
269
  else if (command === "video") videoBook(rest[0], rest.slice(1));
134
270
  else
135
271
  throw new Stop(
136
- "Usage: book.mjs new <name> | shots <name> | pdf <name> | video <name> <audio>... [--size WxH]",
272
+ "Usage: book.mjs new <name> | put <name> <pages.html> [--lang ko] | shots <name> | pdf <name> | video <name> <audio>... [--size WxH]",
137
273
  );
138
274
  } catch (error) {
139
275
  if (!(error instanceof Stop)) throw error;
@@ -11,7 +11,11 @@
11
11
  // light card that reads on any ground: a deck's image slide (fit
12
12
  // whole), a board, a post
13
13
  //
14
- // --kind line|bar line when the first column is dates, bar otherwise
14
+ // --kind line|bar|column|donut|stacked
15
+ // line when the first column is dates, bar otherwise. column: upright
16
+ // bars in the CSV's order, for periods. donut: one column's shares,
17
+ // five rows at most. stacked: two or more columns as parts of one bar
18
+ // a row, in the CSV's order
15
19
  // --title "<text>" what the chart shows (default: the CSV's `# title:`)
16
20
  // --columns a,b which value columns to draw (default: all)
17
21
  // --from / --to a date range to draw, YYYY[-MM[-DD]] or YYYY-Qn. Each bound takes
@@ -19,14 +23,16 @@
19
23
  // --index rebase every line to 100 at its first shared date
20
24
  // --mark "2025-03=Rate cut" a dated event line, on a line chart; repeat for more
21
25
  // --unit "%" --prefix "$" around every number shown
22
- // --highlight "<label>" the one bar in the accent color; the rest go quiet (bars only)
23
- // --keep-order bars in the CSV's order instead of largest first
26
+ // --highlight "<label>" the one bar or slice in the accent color; the rest go quiet
27
+ // --keep-order bars and slices in the CSV's order instead of largest first
28
+ // --share a stacked bar as each part's share of its row, every row 100%
24
29
  // --source "<url or text>" when the CSV has no `# source:` line
25
30
  // --note "<text>" one line under the chart: an estimate, a gap, a break in the series
26
31
  // --locale de how numbers and dates are written: the reader's language tag
27
32
  //
28
- // An unknown --kind, a bound that is not a date, a --highlight naming no row and a --mark
29
- // outside the range drawn all stop, rather than draw something quietly wrong.
33
+ // An unknown --kind, a bound that is not a date, a --highlight naming no row, a --mark
34
+ // outside the range drawn, a donut of more than five parts or of a negative one, and a
35
+ // negative part in a stacked bar all stop, rather than draw something quietly wrong.
30
36
  //
31
37
  // The figure carries no words of its own beyond what it is given, so it reads the same
32
38
  // in any language: the site's name, a date, "CSV".
@@ -456,6 +462,174 @@ function barChart({ labels, series, fmt, highlight, size }) {
456
462
  };
457
463
  }
458
464
 
465
+ /** A legend for several columns, as the bars draw it: a swatch and a name each. */
466
+ function legendRow(series, size, x0 = 0) {
467
+ return series.map((s, n) => {
468
+ const x = x0 + n * (size.W < 500 ? 110 : 130);
469
+ return `<rect class="bar c${(n % 6) + 1}" x="${x}" y="4" width="12" height="12" rx="2"/><text class="tick" x="${x + 18}" y="14">${esc(cut(s.name, size.nameChars + 2))}</text>`;
470
+ });
471
+ }
472
+
473
+ /** Upright bars, one group a row in the CSV's order: periods read left to right. */
474
+ function columnChart({ labels, series, fmt, highlight, size }) {
475
+ const { W } = size;
476
+ const per = series.length;
477
+ const legend = per > 1 ? 26 : 0;
478
+ const H = (W < 500 ? 220 : 280) + legend;
479
+ const all = series.flatMap((s) => s.values).filter((v) => v !== null);
480
+ const lo = Math.min(0, ...all);
481
+ const hi = Math.max(0, ...all);
482
+ const top = legend + 20;
483
+ const bottom = 26 + (lo < 0 ? 16 : 0);
484
+ const slot = (W - 8) / labels.length;
485
+ const group = slot * 0.72;
486
+ const barW = group / per;
487
+ const py = (v) => top + (1 - (v - lo) / (hi - lo || 1)) * (H - top - bottom);
488
+ const chars = Math.max(3, Math.floor(slot / (FONT * 0.62)));
489
+ const parts = per > 1 ? legendRow(series, size) : [];
490
+ labels.forEach((label, g) => {
491
+ const x0 = 4 + g * slot + (slot - group) / 2;
492
+ const quiet = highlight && label !== highlight;
493
+ series.forEach((s, n) => {
494
+ const v = s.values[g];
495
+ if (v === null) return;
496
+ const [y1, y2] = [py(Math.max(0, v)), py(Math.min(0, v))];
497
+ const color = quiet
498
+ ? "quiet"
499
+ : highlight && per === 1
500
+ ? "c1"
501
+ : `c${(n % 6) + 1}`;
502
+ const x = x0 + n * barW;
503
+ parts.push(
504
+ `<rect class="bar ${color}" x="${x + 1}" y="${y1}" width="${Math.max(1, barW - 2)}" height="${Math.max(1, y2 - y1)}" rx="3"><title>${esc(label)}${per > 1 ? ` — ${esc(s.name)}` : ""}: ${esc(fmt.label(v))}</title></rect>`,
505
+ );
506
+ // A value over its bar where it fits the bar's width
507
+ if (textWidth(fmt.label(v), 11) <= barW + 6)
508
+ parts.push(
509
+ `<text class="value${quiet ? " quiet-text" : ""}" x="${x + barW / 2}" y="${v < 0 ? y2 + 14 : y1 - 6}" text-anchor="middle" font-size="11">${esc(fmt.label(v))}</text>`,
510
+ );
511
+ });
512
+ parts.push(
513
+ `<text class="label${quiet ? " quiet-text" : ""}" x="${4 + g * slot + slot / 2}" y="${H - 8}" text-anchor="middle">${esc(cut(label, chars))}<title>${esc(label)}</title></text>`,
514
+ );
515
+ });
516
+ parts.push(
517
+ `<line class="${lo < 0 ? "zero" : "grid"}" x1="4" x2="${W - 4}" y1="${py(0)}" y2="${py(0)}"/>`,
518
+ );
519
+ return {
520
+ svg: `<svg class="chart-svg ${W < 500 ? "narrow" : "wide"}" viewBox="0 0 ${W} ${H}" role="img">${parts.join("")}</svg>`,
521
+ axis: null,
522
+ };
523
+ }
524
+
525
+ /** One column's rows as shares of their whole, around a ring, the largest first. */
526
+ function donutChart({ labels, series, highlight, size, locale }) {
527
+ const { W } = size;
528
+ const values = series[0].values.map((v) => v ?? 0);
529
+ const total = values.reduce((sum, v) => sum + v, 0) || 1;
530
+ const pct = new Intl.NumberFormat(locale, {
531
+ style: "percent",
532
+ maximumFractionDigits: 0,
533
+ });
534
+ const narrow = W < 500;
535
+ const R = narrow ? 62 : 84;
536
+ const ring = narrow ? 24 : 30;
537
+ const c = R + ring / 2 + 4;
538
+ const rows = labels.length;
539
+ const H = Math.max(c * 2, rows * 26 + 8);
540
+ const cy = H / 2;
541
+ const lead = highlight ? labels.indexOf(highlight) : 0;
542
+ const parts = [];
543
+ let at = 0;
544
+ values.forEach((v, i) => {
545
+ const share = (v / total) * 100;
546
+ const quiet = highlight && i !== lead;
547
+ const color = quiet ? "quiet" : highlight ? "c1" : `c${(i % 6) + 1}`;
548
+ // pathLength 100: the dash is the share itself, a hair of gap between slices
549
+ const len = Math.max(0, share - (rows > 1 ? 0.6 : 0));
550
+ parts.push(
551
+ `<circle class="slice ${color}" cx="${c}" cy="${cy}" r="${R}" pathLength="100" stroke-width="${ring}" stroke-dasharray="${len.toFixed(2)} ${(100 - len).toFixed(2)}" stroke-dashoffset="${(-at).toFixed(2)}" transform="rotate(-90 ${c} ${cy})"><title>${esc(labels[i])}: ${pct.format(v / total)}</title></circle>`,
552
+ );
553
+ at += share;
554
+ });
555
+ parts.push(
556
+ `<text class="label" x="${c}" y="${cy + 6}" text-anchor="middle" font-size="${narrow ? 22 : 28}" font-weight="700">${pct.format(values[lead] / total)}</text>`,
557
+ `<text class="tick" x="${c}" y="${cy + (narrow ? 24 : 28)}" text-anchor="middle">${esc(cut(labels[lead], narrow ? 10 : 14))}</text>`,
558
+ );
559
+ const lx = c * 2 + (narrow ? 16 : 32);
560
+ labels.forEach((label, i) => {
561
+ const y = cy - (rows * 26) / 2 + i * 26 + 17;
562
+ const quiet = highlight && i !== lead;
563
+ const color = quiet ? "quiet" : highlight ? "c1" : `c${(i % 6) + 1}`;
564
+ parts.push(
565
+ `<rect class="bar ${color}" x="${lx}" y="${y - 10}" width="12" height="12" rx="2"/>`,
566
+ `<text class="label${quiet ? " quiet-text" : ""}" x="${lx + 20}" y="${y}">${esc(cut(label, size.labelChars))}<title>${esc(label)}</title></text>`,
567
+ `<text class="value" x="${W - 4}" y="${y}" text-anchor="end">${pct.format(values[i] / total)}</text>`,
568
+ );
569
+ });
570
+ return {
571
+ svg: `<svg class="chart-svg ${narrow ? "narrow" : "wide"}" viewBox="0 0 ${W} ${H}" role="img">${parts.join("")}</svg>`,
572
+ axis: null,
573
+ };
574
+ }
575
+
576
+ /** Each row one bar made of its columns' parts; `share` makes every row its parts of 100%. */
577
+ function stackedChart({ labels, series, fmt, share, size, locale }) {
578
+ const { W } = size;
579
+ const pct = new Intl.NumberFormat(locale, {
580
+ style: "percent",
581
+ maximumFractionDigits: 0,
582
+ });
583
+ const barH = 26;
584
+ const gap = 12;
585
+ const legend = 26;
586
+ const H = legend + labels.length * (barH + gap) + 4;
587
+ const totals = labels.map((_, g) =>
588
+ series.reduce((sum, s) => sum + (s.values[g] ?? 0), 0),
589
+ );
590
+ const shownLabel = (l) => cut(l, size.labelChars);
591
+ const left = Math.min(
592
+ W < 500 ? 110 : 200,
593
+ Math.max(...labels.map((l) => textWidth(shownLabel(l), FONT))) + 14,
594
+ );
595
+ const valueRoom = share
596
+ ? 8
597
+ : Math.max(...totals.map((v) => textWidth(fmt.label(v), FONT))) + 12;
598
+ const span = W - left - valueRoom;
599
+ const most = share ? 1 : Math.max(...totals) || 1;
600
+ const parts = legendRow(series, size, W < 500 ? 0 : left);
601
+ labels.forEach((label, g) => {
602
+ const y = legend + g * (barH + gap);
603
+ const whole = totals[g] || 1;
604
+ parts.push(
605
+ `<text class="label" x="${left - 10}" y="${y + barH / 2 + 4}" text-anchor="end">${esc(shownLabel(label))}<title>${esc(label)}</title></text>`,
606
+ );
607
+ let x = left;
608
+ series.forEach((s, n) => {
609
+ const v = s.values[g] ?? 0;
610
+ const w = ((share ? v / whole : v) / most) * span;
611
+ if (w <= 0) return;
612
+ const said = share ? pct.format(v / whole) : fmt.label(v);
613
+ parts.push(
614
+ `<rect class="bar c${(n % 6) + 1}" x="${x}" y="${y}" width="${w}" height="${barH}"><title>${esc(label)} — ${esc(s.name)}: ${esc(said)}</title></rect>`,
615
+ );
616
+ if (textWidth(said, 11) + 10 <= w)
617
+ parts.push(
618
+ `<text class="part" x="${x + 6}" y="${y + barH / 2 + 4}" font-size="11">${esc(said)}</text>`,
619
+ );
620
+ x += w;
621
+ });
622
+ if (!share)
623
+ parts.push(
624
+ `<text class="value" x="${x + 6}" y="${y + barH / 2 + 4}">${esc(fmt.label(totals[g]))}</text>`,
625
+ );
626
+ });
627
+ return {
628
+ svg: `<svg class="chart-svg ${W < 500 ? "narrow" : "wide"}" viewBox="0 0 ${W} ${H}" role="img">${parts.join("")}</svg>`,
629
+ axis: null,
630
+ };
631
+ }
632
+
459
633
  // ── The page ───────────────────────────────────────────────────────────────
460
634
 
461
635
  const STYLE = `<style id="chart-style">
@@ -475,6 +649,7 @@ const STYLE = `<style id="chart-style">
475
649
  .chart-svg .c1{--c:#2563eb}.chart-svg .c2{--c:#d97706}.chart-svg .c3{--c:#059669}.chart-svg .c4{--c:#db2777}.chart-svg .c5{--c:#7c3aed}.chart-svg .c6{--c:#64748b}
476
650
  .chart-svg .quiet{--c:color-mix(in srgb,var(--muted,#71717a) 45%,transparent)}
477
651
  .chart-svg .line{stroke:var(--c)}.chart-svg .dot,.chart-svg .bar{fill:var(--c)}.chart-svg .end{fill:var(--c)}
652
+ .chart-svg .slice{fill:none;stroke:var(--c)}.chart-svg .part{fill:#fff;font-weight:600}
478
653
  .chart-svg .hover line{stroke:var(--muted,#71717a)}
479
654
  .chart-svg .hover rect{fill:var(--bg,#fff);stroke:var(--line,#e4e4e7)}
480
655
  .chart-svg .hover text{fill:var(--fg,#18181b);font-size:12px}
@@ -619,8 +794,11 @@ function main() {
619
794
  });
620
795
  const dated = rows.every((r) => toTime(r[0]) !== null);
621
796
  const kind = flags.kind ?? (dated ? "line" : "bar");
622
- if (kind !== "line" && kind !== "bar")
623
- throw new Stop(`"${flags.kind}" is not a kind: --kind line or --kind bar.`);
797
+ const KINDS = ["line", "bar", "column", "donut", "stacked"];
798
+ if (!KINDS.includes(kind))
799
+ throw new Stop(
800
+ `"${flags.kind}" is not a kind: --kind ${KINDS.join(", ")}.`,
801
+ );
624
802
  if (kind === "line" && !dated)
625
803
  throw new Stop(
626
804
  "A line needs dates in the first column (YYYY, YYYY-MM, YYYY-MM-DD or YYYY-Qn); draw categories with --kind bar.",
@@ -655,9 +833,9 @@ function main() {
655
833
  if (highlight !== undefined) {
656
834
  if (typeof highlight !== "string")
657
835
  throw new Stop('--highlight needs a row: --highlight "<label>".');
658
- if (kind !== "bar")
836
+ if (kind === "line" || kind === "stacked")
659
837
  throw new Stop(
660
- "--highlight picks out one bar; a line chart has none. Drop it, or draw --kind bar.",
838
+ `--highlight picks out one bar or slice; a ${kind} chart has none. Drop it, or draw --kind bar.`,
661
839
  );
662
840
  if (!body.some((r) => r[0] === highlight))
663
841
  throw new Stop(
@@ -675,6 +853,37 @@ function main() {
675
853
  }));
676
854
  if (series.every((s) => s.values.every((v) => v === null)))
677
855
  throw new Stop(`No numbers in ${wanted.join(", ")}.`);
856
+ const negative = series.some((s) =>
857
+ s.values.some((v) => v !== null && v < 0),
858
+ );
859
+ if (kind === "donut") {
860
+ if (series.length !== 1)
861
+ throw new Stop(
862
+ `A donut shows one column's shares; this draws ${series.length}. Pick one with --columns, or draw --kind stacked.`,
863
+ );
864
+ if (body.length > 5)
865
+ throw new Stop(
866
+ `A donut reads at five parts or fewer; this has ${body.length}. Draw --kind bar, or put the smallest together in one row.`,
867
+ );
868
+ if (negative)
869
+ throw new Stop(
870
+ "A donut's parts are shares of a whole, and a negative one is no share. Draw --kind bar.",
871
+ );
872
+ }
873
+ if (flags.share && kind !== "stacked")
874
+ throw new Stop(
875
+ "--share draws each row of a stacked bar as 100%; drop it, or draw --kind stacked.",
876
+ );
877
+ if (kind === "stacked") {
878
+ if (series.length < 2)
879
+ throw new Stop(
880
+ "A stacked bar needs two or more value columns, the parts of each row. Draw --kind bar.",
881
+ );
882
+ if (negative)
883
+ throw new Stop(
884
+ "A stacked bar cannot hold a negative part: the parts would overlap. Draw --kind column.",
885
+ );
886
+ }
678
887
 
679
888
  let chart;
680
889
  let lineDraw;
@@ -725,14 +934,21 @@ function main() {
725
934
  chart = both(lineDraw);
726
935
  } else {
727
936
  let order = body.map((_, i) => i);
728
- if (!flags["keep-order"])
937
+ // Bars and slices largest first; columns and stacked bars keep the rows' own order
938
+ if ((kind === "bar" || kind === "donut") && !flags["keep-order"])
729
939
  order.sort(
730
940
  (a, b) =>
731
941
  (series[0].values[b] ?? -Infinity) -
732
942
  (series[0].values[a] ?? -Infinity),
733
943
  );
944
+ const drawer = {
945
+ bar: barChart,
946
+ column: columnChart,
947
+ donut: donutChart,
948
+ stacked: stackedChart,
949
+ }[kind];
734
950
  barDraw = (size) =>
735
- barChart({
951
+ drawer({
736
952
  labels: order.map((i) => body[i][0]),
737
953
  series: series.map((s) => ({
738
954
  ...s,
@@ -740,6 +956,8 @@ function main() {
740
956
  })),
741
957
  fmt,
742
958
  highlight: highlight ?? null,
959
+ share: Boolean(flags.share),
960
+ locale,
743
961
  size,
744
962
  });
745
963
  chart = both(barDraw);
@@ -14,6 +14,9 @@
14
14
  // the body as it is now, as HTML, into <file> to change
15
15
  // node document.mjs shots <name|path>
16
16
  // the page as it opens, down to three pictures in scratch/
17
+ // node document.mjs docx <name|path>
18
+ // the page as a Word file beside it (<name>.docx), edits
19
+ // and all, as Export › Word file makes it
17
20
  import { spawnSync } from "node:child_process";
18
21
  import {
19
22
  existsSync,
@@ -83,7 +86,7 @@ function makePage(name, ...args) {
83
86
  part("quick.html")
84
87
  .replace("{{body}}", () => part(join("pages", `${kind}.html`)))
85
88
  .replace("{{css}}", () => part("quick.css"))
86
- .replace("{{js}}", () => part("quick.js"))
89
+ .replace("{{js}}", () => `${part("quick.js")}\n${part("docx.js")}`)
87
90
  .replaceAll("{{title}}", name)
88
91
  .replaceAll("{{today}}", today),
89
92
  ),
@@ -218,15 +221,39 @@ function shotPage(name) {
218
221
  console.log(`Look at them with look_at, from ${shown(out)}.`);
219
222
  }
220
223
 
224
+ /** The page as a Word file beside it, made by the page's own converter in a headless browser. */
225
+ function wordFile(name) {
226
+ const file = pageAt(name);
227
+ const out = join(dirname(file), `${basename(file, ".html")}.docx`);
228
+ const skills = process.env.THURSDAY_SKILLS || resolve(SKILL, "..");
229
+ const done = spawnSync(
230
+ process.execPath,
231
+ [
232
+ join(skills, "artifact", "runtime", "document", "word.mjs"),
233
+ file,
234
+ "--out",
235
+ out,
236
+ ],
237
+ { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] },
238
+ );
239
+ if (done.status !== 0)
240
+ throw new Stop("Fix what it names above, then run this again.");
241
+ const { missed } = JSON.parse(done.stdout.trim().split("\n").at(-1));
242
+ console.log(
243
+ `Wrote ${shown(out)}: the document as a Word file. Hand back its path beside the page's.${missed.length ? ` Pictures that could not be put in, each written in as its words: ${missed.join("; ")}.` : ""}`,
244
+ );
245
+ }
246
+
221
247
  const [command, ...rest] = process.argv.slice(2);
222
248
  try {
223
249
  if (command === "new" || command === "quick") newPage(...rest);
224
250
  else if (command === "put") putBody(...rest);
225
251
  else if (command === "get") getBody(...rest);
226
252
  else if (command === "shots") shotPage(rest[0]);
253
+ else if (command === "docx") wordFile(rest[0]);
227
254
  else
228
255
  throw new Stop(
229
- "Usage: document.mjs new <name> [--from <kind>] | put <name|path> <file.md|file.html> | get <name|path> <file> | shots <name|path>",
256
+ "Usage: document.mjs new <name> [--from <kind>] | put <name|path> <file.md|file.html> | get <name|path> <file> | shots <name|path> | docx <name|path>",
230
257
  );
231
258
  } catch (error) {
232
259
  if (!(error instanceof Stop)) throw error;