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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/artifact/route.js +9 -9
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js +3 -3
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js +8 -8
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js +8 -8
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/[id]/route.js +3 -3
- package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/route.js +3 -3
- package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js +4 -4
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js +11 -11
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/reach/route.js +11 -11
- package/.next/server/app/api/reach/route.js.nft.json +1 -1
- package/.next/server/app/api/routine/route.js +7 -7
- package/.next/server/app/api/routine/route.js.nft.json +1 -1
- package/.next/server/app/api/signins/route.js +5 -5
- package/.next/server/app/api/signins/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js +7 -7
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +7 -7
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js +5 -5
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/settings/route.js +5 -5
- package/.next/server/app/api/thursday/settings/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/text/route.js +13 -13
- package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +12 -12
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/files/route.js +8 -8
- package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/route.js +9 -9
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +3 -3
- package/.next/server/app/artifact/[...path]/page.js +4 -4
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next/server/app/page/server-reference-manifest.json +60 -60
- package/.next/server/app/page.js +5 -5
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__00qyw01._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__01ypm_z._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__021dkui._.js +3 -0
- package/.next/server/chunks/{[root-of-the-server]__18e65_-._.js → [root-of-the-server]__03xamry._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__05dp5tr._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__09gdscj._.js +7 -0
- package/.next/server/chunks/{[root-of-the-server]__10_7vqz._.js → [root-of-the-server]__0b36yfw._.js} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__1gamvcz._.js → [root-of-the-server]__0jzwdg-._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0nmust6._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__0svzjwa._.js +7 -0
- package/.next/server/chunks/{[root-of-the-server]__16yw3mb._.js → [root-of-the-server]__12m24xl._.js} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__1zrwxis._.js → [root-of-the-server]__12tahb4._.js} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__1ep-pao._.js → [root-of-the-server]__1_h4hf9._.js} +4 -4
- package/.next/server/chunks/{[root-of-the-server]__1-tdj60._.js → [root-of-the-server]__1de1o6m._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__1voqqyc._.js → [root-of-the-server]__1ignh6-._.js} +1 -1
- package/.next/server/chunks/[root-of-the-server]__1t02-1p._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
- package/.next/server/chunks/{[root-of-the-server]__11g20j-._.js → [root-of-the-server]__1yl5l25._.js} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__1mbd2w_._.js → [root-of-the-server]__208vmyf._.js} +1 -1
- package/.next/server/chunks/{_1mfu9a3._.js → _02lipqs._.js} +1 -1
- package/.next/server/chunks/_05sa7xl._.js +1 -1
- package/.next/server/chunks/{_1rg5bfd._.js → _074f2ts._.js} +1 -1
- package/.next/server/chunks/_07nxi75._.js +3 -3
- package/.next/server/chunks/_08btiir._.js +7 -0
- package/.next/server/chunks/_0caua_s._.js +1 -1
- package/.next/server/chunks/{_0qoush_._.js → _0gt6cbj._.js} +1 -1
- package/.next/server/chunks/_0xygcxs._.js +3 -3
- package/.next/server/chunks/{_09c4we8._.js → _17i1lt2._.js} +1 -1
- package/.next/server/chunks/_19kea1l._.js +9 -9
- package/.next/server/chunks/_1gn2r2q._.js +3 -3
- package/.next/server/chunks/{_03niwml._.js → _1ntwp1s._.js} +1 -1
- package/.next/server/chunks/_1q6e56i._.js +3 -3
- package/.next/server/chunks/{_02fn1o3._.js → _1tkzgtm._.js} +1 -1
- package/.next/server/chunks/_1wl2g8_._.js +2 -2
- package/.next/server/chunks/{_1w6uf9k._.js → _1y3mnz4._.js} +1 -1
- package/.next/server/chunks/features_workspace_workspace_ts_13h82gv._.js +4 -4
- package/.next/server/chunks/features_workspace_workspace_ts_1iyjroj._.js +4 -4
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0cux6zt._.js → node_modules_consola_dist_chunks_prompt_mjs_04oxl25._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0w32bqh._.js → node_modules_consola_dist_chunks_prompt_mjs_0g87zst._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_1clmnaj._.js → node_modules_consola_dist_chunks_prompt_mjs_1341cnf._.js} +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__02jdtqa._.js → [root-of-the-server]__07660xu._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__0rnjkw2._.js → [root-of-the-server]__149x25m._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__17h1ggo._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__19s8qyj._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1jyeu61._.js +1 -1
- package/.next/server/chunks/ssr/_088vzgn._.js +3 -0
- package/.next/server/chunks/ssr/{_0wupyvd._.js → _0_vnw9v._.js} +2 -2
- package/.next/server/chunks/ssr/{_1u5vsy-._.js → _0aeknva._.js} +2 -2
- package/.next/server/chunks/ssr/_0fo4552._.js +3 -3
- package/.next/server/chunks/ssr/{_0-ou1b0._.js → _0n2scfq._.js} +2 -2
- package/.next/server/chunks/ssr/_0o_o9ji._.js +3 -0
- package/.next/server/chunks/ssr/_0qne8xs._.js +3 -0
- package/.next/server/chunks/ssr/_0r0g9ki._.js +1 -1
- package/.next/server/chunks/ssr/_0v9r8g2._.js +3 -0
- package/.next/server/chunks/ssr/_0x8ophf._.js +1 -1
- package/.next/server/chunks/ssr/{_0ncuqct._.js → _0ztbn5q._.js} +2 -2
- package/.next/server/chunks/ssr/{_0w8r7o7._.js → _1-41lck._.js} +2 -2
- package/.next/server/chunks/ssr/{_10fwcz_._.js → _14dfh7a._.js} +2 -2
- package/.next/server/chunks/ssr/{_1f6s34p._.js → _15m5ctz._.js} +2 -2
- package/.next/server/chunks/ssr/{_11pnnvd._.js → _18-e7zi._.js} +2 -2
- package/.next/server/chunks/ssr/{_0y28gl3._.js → _1c5iqz1._.js} +3 -3
- package/.next/server/chunks/ssr/{_1dqaksc._.js → _1hbjh6n._.js} +2 -2
- package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
- package/.next/server/chunks/ssr/{_1k0efi3._.js → _1sttj8x._.js} +2 -2
- package/.next/server/chunks/ssr/_1uij_rj._.js +1 -1
- package/.next/server/chunks/ssr/_1umbr78._.js +1 -1
- package/.next/server/chunks/ssr/{_17ble8k._.js → _1vtgiou._.js} +3 -3
- package/.next/server/chunks/ssr/_1w9w1ok._.js +1 -1
- package/.next/server/chunks/ssr/{_1z586yw._.js → _1z6pirx._.js} +2 -2
- package/.next/server/chunks/ssr/features_0796o8r._.js +1 -1
- package/.next/server/chunks/ssr/features_bot_1knyan7._.js +3 -0
- package/.next/server/chunks/ssr/features_bot_1kr0_m-._.js +3 -0
- package/.next/server/chunks/ssr/{features_config_11dpa7r._.js → features_config_0n0zu89._.js} +3 -3
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
- package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
- package/.next/server/chunks/ssr/features_workspace_workspace_ts_1wc7_u_._.js +4 -4
- package/.next/server/chunks/ssr/{node_modules_1r5o-bo._.js → node_modules_00h1t1j._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1o9n7_n._.js → node_modules_02ficyi._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0u24mg0._.js → node_modules_03q71_j._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_138kcrz._.js → node_modules_03uejh6._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0x_2io2._.js → node_modules_06grs09._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0n1js5k._.js → node_modules_07mo9rn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0bas34k._.js → node_modules_09e8kz_._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1dzye0n._.js → node_modules_09ikzel._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_18yn_bs._.js → node_modules_0_d_7gq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ka8te1._.js → node_modules_0_p8w7p._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0mark4f._.js → node_modules_0f5bmkb._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0sdjzae._.js → node_modules_0kwie-o._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1ozyial._.js → node_modules_0r-9e4n._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_156m1qh._.js → node_modules_0u_8z64._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0-ew8f8._.js → node_modules_0zf3wuu._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1xua-g1._.js → node_modules_1-7fc7h._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ld7p-1._.js → node_modules_142z3o5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0-8r9yw._.js → node_modules_180l3i0._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1tj7_9z._.js → node_modules_197vh1s._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0y5uubt._.js → node_modules_1_n9o9s._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0etz1i0._.js → node_modules_1fw32pg._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0bbwe-u._.js → node_modules_1hefp6h._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_097cl9f._.js → node_modules_1irot3r._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0uibk9z._.js → node_modules_1om2itc._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1zkzksd._.js → node_modules_1ubln7w._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_02uwbwp._.js → node_modules_1wyhtw9._.js} +1 -1
- 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
- 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
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +61 -61
- package/.next/static/chunks/{1ub9obhgfm4jl.js → 024plctckvq-1.js} +1 -1
- package/.next/static/chunks/04toe_3nqfjqt.js +1 -0
- package/.next/static/chunks/{0609ejkwuy4sf.js → 0_b_bi2pobasx.js} +2 -2
- package/.next/static/chunks/{3ro66d9dc70bc.js → 0dfvwgnyrp8qt.js} +1 -1
- package/.next/static/chunks/{314szf8yer-3n.js → 0f47nnshdlucs.js} +1 -1
- package/.next/static/chunks/0sowxe4nnl5qu.js +1 -0
- package/.next/static/chunks/{07vcuxousbmy1.js → 0sv142q1yrva2.js} +1 -1
- package/.next/static/chunks/{0q4ws7qd21chx.js → 0tp868040z9nn.js} +2 -2
- package/.next/static/chunks/{0unonxbajyknw.js → 1-63yde5eaz7i.js} +1 -1
- package/.next/static/chunks/14qitm0kwsb6g.js +15 -0
- package/.next/static/chunks/{2xjr5b-yk_h3o.js → 1bgvxd3xny53w.js} +1 -1
- package/.next/static/chunks/{245_cp14iz637.js → 1cyrrz4q871aj.js} +1 -1
- package/.next/static/chunks/1st3b0c9vhxg3.js +1 -0
- package/.next/static/chunks/{39miwvg4k-b2d.js → 1wbqaidq7vc3_.js} +1 -1
- package/.next/static/chunks/{0cl_fgkphk1oz.js → 28sxb_k460e9h.js} +1 -1
- package/.next/static/chunks/{3j-l8un0d77v7.js → 2v6e_9p95o2e3.js} +1 -1
- package/.next/static/chunks/{23tx-2paw9qa6.js → 3-qp777vtfglj.js} +1 -1
- package/.next/static/chunks/{2jbjyazqfqyoh.js → 33ekkscpcq-z1.js} +1 -1
- package/.next/static/chunks/388l-zszwa_jf.js +1 -0
- package/.next/static/chunks/{3vwjs5kq11417.js → 38edd4rpcbvzp.js} +1 -1
- package/.next/static/chunks/3bwrvpjk95_x4.js +1 -0
- package/.next/static/chunks/{42m371fn687r5.js → 3itdg6t243u6f.js} +1 -1
- package/.next/static/chunks/{2oa4gpkzjtpni.js → 44vre0f7xqsh-.js} +1 -1
- package/README.md +12 -12
- package/guide/bots.md +48 -8
- package/guide/calls.md +13 -12
- package/guide/setup.md +4 -2
- package/package.json +1 -1
- package/public/sounds/NOTICE +3 -0
- package/public/sounds/opening-land.ogg +0 -0
- package/public/sounds/opening-rise.ogg +0 -0
- package/public/sounds/opening-step-1.ogg +0 -0
- package/public/sounds/opening-step-2.ogg +0 -0
- package/public/sounds/opening-step-3.ogg +0 -0
- package/skills/README.md +3 -0
- package/skills/artifact/SKILL.md +6 -5
- package/skills/artifact/references/book.md +22 -8
- package/skills/artifact/references/deck.md +4 -2
- package/skills/artifact/references/document.md +9 -0
- package/skills/artifact/references/sheet.md +108 -0
- package/skills/artifact/runtime/book/book.css +75 -0
- package/skills/artifact/runtime/book/book.html +6 -3
- package/skills/artifact/runtime/book/book.js +21 -0
- package/skills/artifact/runtime/canvas/canvas.css +1 -1
- package/skills/artifact/runtime/canvas/canvas.js +11 -1
- package/skills/artifact/runtime/deck/deck.css +164 -1
- package/skills/artifact/runtime/deck/deck.js +76 -1
- package/skills/artifact/runtime/document/docx.js +631 -0
- package/skills/artifact/runtime/document/markdown.mjs +58 -2
- package/skills/artifact/runtime/document/quick.css +66 -0
- package/skills/artifact/runtime/document/quick.html +1 -0
- package/skills/artifact/runtime/document/quick.js +154 -0
- package/skills/artifact/runtime/document/word.mjs +65 -0
- package/skills/artifact/runtime/sheet/format.mjs +385 -0
- package/skills/artifact/runtime/sheet/formula.mjs +755 -0
- package/skills/artifact/runtime/sheet/sheet.css +499 -0
- package/skills/artifact/runtime/sheet/sheet.html +102 -0
- package/skills/artifact/runtime/sheet/sheet.js +1361 -0
- package/skills/artifact/runtime/sheet/xlsx.mjs +320 -0
- package/skills/artifact/runtime/shell/head.html +1 -1
- package/skills/artifact/runtime/shell/shell.css +3 -0
- package/skills/artifact/runtime/shell/wear.mjs +47 -1
- package/skills/artifact/runtime/vendor/fflate.LICENSE.md +23 -0
- package/skills/artifact/runtime/vendor/fflate.mjs +2710 -0
- package/skills/artifact/scripts/book.mjs +141 -5
- package/skills/artifact/scripts/chart.mjs +229 -11
- package/skills/artifact/scripts/document.mjs +29 -2
- package/skills/artifact/scripts/spreadsheet.mjs +1068 -0
- package/skills/data-report/SKILL.md +8 -1
- package/.next/server/chunks/[root-of-the-server]__0aijj66._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0tvoct-._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__118qjk_._.js +0 -7
- package/.next/server/chunks/[root-of-the-server]__1b8tp42._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__1gzcbda._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1qcn1ti._.js +0 -7
- package/.next/server/chunks/_0xx3ryc._.js +0 -7
- package/.next/server/chunks/ssr/[root-of-the-server]__1y5_1cv._.js +0 -3
- package/.next/server/chunks/ssr/_0l1-xc6._.js +0 -3
- package/.next/server/chunks/ssr/_1g48r6t._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1k25jdg._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +0 -3
- package/.next/server/chunks/ssr/lib_utils_ts_0_f1xvm._.js +0 -3
- package/.next/static/chunks/00d54fcfm-lko.js +0 -1
- package/.next/static/chunks/0gjlsu6vrfzod.js +0 -1
- package/.next/static/chunks/17yqt1p_svv13.js +0 -1
- package/.next/static/chunks/203pbwecfs3tm.js +0 -1
- package/.next/static/chunks/3ghvoe_mlljzs.js +0 -15
- package/.next/static/chunks/3gn7dubb4-pwy.js +0 -1
- /package/.next/static/{RtLRT9EEaGn-OqwpMFdYg → VtbCMn-0G969p8QJsWdGv}/_buildManifest.js +0 -0
- /package/.next/static/{RtLRT9EEaGn-OqwpMFdYg → VtbCMn-0G969p8QJsWdGv}/_clientMiddlewareManifest.js +0 -0
- /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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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">
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
29
|
-
// outside the range drawn
|
|
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
|
-
|
|
623
|
-
|
|
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
|
|
836
|
+
if (kind === "line" || kind === "stacked")
|
|
659
837
|
throw new Stop(
|
|
660
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|