dreamcontext 0.17.1 → 0.18.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/README.md +321 -565
- package/agents/sleep-product.md +21 -2
- package/agents/sleep-state.md +24 -0
- package/agents/sleep-tasks.md +16 -0
- package/dist/agents/sleep-product.md +21 -2
- package/dist/agents/sleep-state.md +24 -0
- package/dist/agents/sleep-tasks.md +16 -0
- package/dist/dashboard/announcements/dashboard-highlights-0-17-0-18.excalidraw.md +1531 -0
- package/dist/dashboard/announcements/goal-skill-v2.excalidraw.md +1490 -0
- package/dist/dashboard/announcements/task-manager.excalidraw.md +1217 -0
- package/dist/dashboard/announcements/visual-announcements.excalidraw.md +1373 -0
- package/dist/dashboard/announcements.json +38 -0
- package/dist/dashboard/assets/{BrainCanvas3D-DyF_3yYD.js → BrainCanvas3D-D9G3dOLf.js} +1 -1
- package/dist/dashboard/assets/{_baseUniq-Do8VMBA0.js → _baseUniq-Cyshlz4Y.js} +1 -1
- package/dist/dashboard/assets/{ar-SA-G6X2FPQ2-DmbXDb2R.js → ar-SA-G6X2FPQ2-qj-2Cbgo.js} +1 -1
- package/dist/dashboard/assets/{arc-CwtUBQfU.js → arc-0OnX5WDs.js} +1 -1
- package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-O9bOog_f.js → architectureDiagram-Q4EWVU46-Cx3c0nUi.js} +1 -1
- package/dist/dashboard/assets/{az-AZ-76LH7QW2-c9Zk10vG.js → az-AZ-76LH7QW2-CBuoh6M3.js} +1 -1
- package/dist/dashboard/assets/{bg-BG-XCXSNQG7-uMTg84lX.js → bg-BG-XCXSNQG7-X5QYyZWx.js} +1 -1
- package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-BTENXCSO.js → blockDiagram-DXYQGD6D-BwESAg1j.js} +1 -1
- package/dist/dashboard/assets/{bn-BD-2XOGV67Q-DUFKHcua.js → bn-BD-2XOGV67Q-DlodQLNR.js} +1 -1
- package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-DMXPWRiQ.js → c4Diagram-AHTNJAMY-CopL0lbP.js} +1 -1
- package/dist/dashboard/assets/{ca-ES-6MX7JW3Y-BuYX1q89.js → ca-ES-6MX7JW3Y-DfqF6Yhy.js} +1 -1
- package/dist/dashboard/assets/channel-BLisRbYb.js +1 -0
- package/dist/dashboard/assets/{chunk-4BX2VUAB-DgefvnWS.js → chunk-4BX2VUAB-CRwifLK3.js} +1 -1
- package/dist/dashboard/assets/{chunk-4TB4RGXK-BnFO5G1t.js → chunk-4TB4RGXK-uxJYY_64.js} +1 -1
- package/dist/dashboard/assets/{chunk-55IACEB6-COm2TYEM.js → chunk-55IACEB6-DhYIp7Oc.js} +1 -1
- package/dist/dashboard/assets/{chunk-EDXVE4YY-CtsGCwZp.js → chunk-EDXVE4YY-CCxfcirt.js} +1 -1
- package/dist/dashboard/assets/{chunk-FMBD7UC4-cfSvx35j.js → chunk-FMBD7UC4-D7OC8QFC.js} +1 -1
- package/dist/dashboard/assets/{chunk-OYMX7WX6-DAhDKteL.js → chunk-OYMX7WX6-CwHgaUQo.js} +1 -1
- package/dist/dashboard/assets/{chunk-QZHKN3VN-B5h4qML3.js → chunk-QZHKN3VN-7Idpf6YJ.js} +1 -1
- package/dist/dashboard/assets/{chunk-YZCP3GAM-CGp_NiRg.js → chunk-YZCP3GAM-D4Lh0fgr.js} +1 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-CgqeQiHi.js +1 -0
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-CgqeQiHi.js +1 -0
- package/dist/dashboard/assets/clone-CQXNMCV5.js +1 -0
- package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-Dk7CMFA4.js → cose-bilkent-S5V4N54A-BYcMtJTF.js} +1 -1
- package/dist/dashboard/assets/{cs-CZ-2BRQDIVT-zfUTiS_d.js → cs-CZ-2BRQDIVT-rnrpKdSJ.js} +1 -1
- package/dist/dashboard/assets/{da-DK-5WZEPLOC-CU14f8NA.js → da-DK-5WZEPLOC-D5OGAvk7.js} +1 -1
- package/dist/dashboard/assets/{dagre-KV5264BT-BiMVxC4Q.js → dagre-KV5264BT-V0f1_b4R.js} +1 -1
- package/dist/dashboard/assets/{de-DE-XR44H4JA-CDOHAJGo.js → de-DE-XR44H4JA-CDyb8YfL.js} +1 -1
- package/dist/dashboard/assets/{diagram-5BDNPKRD-DfJEsVTA.js → diagram-5BDNPKRD-BmpizoaS.js} +1 -1
- package/dist/dashboard/assets/{diagram-G4DWMVQ6-CztLHIcY.js → diagram-G4DWMVQ6-ClQg-LTu.js} +1 -1
- package/dist/dashboard/assets/{diagram-MMDJMWI5-DqoQLlWK.js → diagram-MMDJMWI5-BkXcDMIV.js} +1 -1
- package/dist/dashboard/assets/{diagram-TYMM5635-DvyvQBCc.js → diagram-TYMM5635-BbFiiCM2.js} +1 -1
- package/dist/dashboard/assets/{el-GR-BZB4AONW-ChUphbcv.js → el-GR-BZB4AONW-Y1YnKZL0.js} +1 -1
- package/dist/dashboard/assets/{erDiagram-SMLLAGMA-D_uSs2_2.js → erDiagram-SMLLAGMA-Dzdv7_l5.js} +1 -1
- package/dist/dashboard/assets/{es-ES-U4NZUMDT-BNalBEhb.js → es-ES-U4NZUMDT-D0T536hs.js} +1 -1
- package/dist/dashboard/assets/{eu-ES-A7QVB2H4-BMiQyA2y.js → eu-ES-A7QVB2H4-Bfrya0et.js} +1 -1
- package/dist/dashboard/assets/{fa-IR-HGAKTJCU-Bd6ZnTSo.js → fa-IR-HGAKTJCU-pYE_hG-L.js} +1 -1
- package/dist/dashboard/assets/{fi-FI-Z5N7JZ37-BWAfJadC.js → fi-FI-Z5N7JZ37-D3C-fNyk.js} +1 -1
- package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-Bmsp6kyU.js → flowDiagram-DWJPFMVM-CFhyjwKY.js} +1 -1
- package/dist/dashboard/assets/{fr-FR-RHASNOE6-CspbYzOW.js → fr-FR-RHASNOE6-CgwnG5JW.js} +1 -1
- package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-wEdysXo_.js → ganttDiagram-T4ZO3ILL-BRKRiSzf.js} +1 -1
- package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-BEiGpPBj.js → gitGraphDiagram-UUTBAWPF-CTqCK49d.js} +1 -1
- package/dist/dashboard/assets/{gl-ES-HMX3MZ6V-CV76vsWr.js → gl-ES-HMX3MZ6V-DjJTqTm3.js} +1 -1
- package/dist/dashboard/assets/{graph-CZFIeNHZ.js → graph-D429kSbY.js} +1 -1
- package/dist/dashboard/assets/{he-IL-6SHJWFNN-Y1VoXJ_H.js → he-IL-6SHJWFNN-C84FlH6r.js} +1 -1
- package/dist/dashboard/assets/{hi-IN-IWLTKZ5I-Crh5S49o.js → hi-IN-IWLTKZ5I-Blhkfum2.js} +1 -1
- package/dist/dashboard/assets/{hu-HU-A5ZG7DT2-D4I3gCbp.js → hu-HU-A5ZG7DT2-DLC1JfN-.js} +1 -1
- package/dist/dashboard/assets/{id-ID-SAP4L64H-Chvzkuvi.js → id-ID-SAP4L64H-C3pB_IWB.js} +1 -1
- package/dist/dashboard/assets/image-FgR93jdt.js +1 -0
- package/dist/dashboard/assets/index-2m8ueOUn.js +1 -0
- package/dist/dashboard/assets/index-CWwjuihq.css +32 -0
- package/dist/dashboard/assets/{index-C7Bo5AQH.js → index-CbOXCDms.js} +1 -1
- package/dist/dashboard/assets/index-SJ64pf-4.js +551 -0
- package/dist/dashboard/assets/{infoDiagram-42DDH7IO-BzfuDHl4.js → infoDiagram-42DDH7IO-CIzDShDw.js} +1 -1
- package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-BVoNJ9VS.js → ishikawaDiagram-UXIWVN3A-CuasYzdX.js} +1 -1
- package/dist/dashboard/assets/{it-IT-JPQ66NNP-BH_gN5es.js → it-IT-JPQ66NNP-DGGEoNPu.js} +1 -1
- package/dist/dashboard/assets/{ja-JP-DBVTYXUO-DU_WmLVa.js → ja-JP-DBVTYXUO-CEqP9aps.js} +1 -1
- package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-DbwICa_a.js → journeyDiagram-VCZTEJTY-DHiq2OTQ.js} +1 -1
- package/dist/dashboard/assets/{kaa-6HZHGXH3-KYU9qowM.js → kaa-6HZHGXH3-GOFmTAXq.js} +1 -1
- package/dist/dashboard/assets/{kab-KAB-ZGHBKWFO-bjhyiUq5.js → kab-KAB-ZGHBKWFO-B865Wwzu.js} +1 -1
- package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-D9XucLs4.js → kanban-definition-6JOO6SKY-BhlTKFbL.js} +1 -1
- package/dist/dashboard/assets/{kk-KZ-P5N5QNE5-CYTx_dAZ.js → kk-KZ-P5N5QNE5-CNfuE6b6.js} +1 -1
- package/dist/dashboard/assets/{km-KH-HSX4SM5Z-DqsuY4Lf.js → km-KH-HSX4SM5Z-CsmgRWxu.js} +1 -1
- package/dist/dashboard/assets/{ko-KR-MTYHY66A-BZe7NywF.js → ko-KR-MTYHY66A-B_02RgEz.js} +1 -1
- package/dist/dashboard/assets/{ku-TR-6OUDTVRD-F5d6HFWt.js → ku-TR-6OUDTVRD-JfQA5u05.js} +1 -1
- package/dist/dashboard/assets/{layout-CotqU0S9.js → layout-DvrQSvOI.js} +1 -1
- package/dist/dashboard/assets/{linear-Dqv87Scf.js → linear-6r0qttLs.js} +1 -1
- package/dist/dashboard/assets/{lt-LT-XHIRWOB4-BCCuaPbS.js → lt-LT-XHIRWOB4-DWFUXEdf.js} +1 -1
- package/dist/dashboard/assets/{lv-LV-5QDEKY6T-NLGi-vJ6.js → lv-LV-5QDEKY6T-C5yAd0n_.js} +1 -1
- package/dist/dashboard/assets/{min-BbJdnc3h.js → min-D_EP9dBf.js} +1 -1
- package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-Dk4QySu7.js → mindmap-definition-QFDTVHPH-Col3ao9B.js} +1 -1
- package/dist/dashboard/assets/{mr-IN-CRQNXWMA-BnGDIgVN.js → mr-IN-CRQNXWMA-Dwjz0Xdm.js} +1 -1
- package/dist/dashboard/assets/{my-MM-5M5IBNSE-CspgYE9_.js → my-MM-5M5IBNSE-EXJDNyjA.js} +1 -1
- package/dist/dashboard/assets/{nb-NO-T6EIAALU-BeRPw3hu.js → nb-NO-T6EIAALU-BWm2bgLd.js} +1 -1
- package/dist/dashboard/assets/{nl-NL-IS3SIHDZ-C-G-bljM.js → nl-NL-IS3SIHDZ-f8nP-yp-.js} +1 -1
- package/dist/dashboard/assets/{nn-NO-6E72VCQL-Ct7VZY8D.js → nn-NO-6E72VCQL-B0c_gCNR.js} +1 -1
- package/dist/dashboard/assets/{oc-FR-POXYY2M6-Cv5-C4Qq.js → oc-FR-POXYY2M6-C40PSwAh.js} +1 -1
- package/dist/dashboard/assets/{pa-IN-N4M65BXN-DAgkIThL.js → pa-IN-N4M65BXN-DDcgXFkB.js} +1 -1
- package/dist/dashboard/assets/{percentages-BXMCSKIN-vidRYabK.js → percentages-BXMCSKIN-MHBQP8nI.js} +7 -7
- package/dist/dashboard/assets/{pica-Cy-Xl-mi.js → pica-C5CFA09W.js} +1 -1
- package/dist/dashboard/assets/{pieDiagram-DEJITSTG-BnP4-zwd.js → pieDiagram-DEJITSTG-DFHmB0fL.js} +1 -1
- package/dist/dashboard/assets/{pl-PL-T2D74RX3-BuJQ3HD-.js → pl-PL-T2D74RX3-yFTBt0Xu.js} +1 -1
- package/dist/dashboard/assets/{pt-BR-5N22H2LF-BbAqsvD6.js → pt-BR-5N22H2LF-ilAW3Fdv.js} +1 -1
- package/dist/dashboard/assets/{pt-PT-UZXXM6DQ-DzFJedU3.js → pt-PT-UZXXM6DQ-CXHFN95K.js} +1 -1
- package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-BXN88cHy.js → quadrantDiagram-34T5L4WZ-CR732_oP.js} +1 -1
- package/dist/dashboard/assets/{requirementDiagram-MS252O5E-D34hL42a.js → requirementDiagram-MS252O5E-mKoVG52Z.js} +1 -1
- package/dist/dashboard/assets/{ro-RO-JPDTUUEW-BV2-sOiv.js → ro-RO-JPDTUUEW-BKlWpyF4.js} +1 -1
- package/dist/dashboard/assets/{ru-RU-B4JR7IUQ-BCL4N9Ej.js → ru-RU-B4JR7IUQ-BGoxlhJm.js} +1 -1
- package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-0mFeok-3.js → sankeyDiagram-XADWPNL6-DXipts7M.js} +1 -1
- package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-C-5nW5j6.js → sequenceDiagram-FGHM5R23-DFQhU8q7.js} +1 -1
- package/dist/dashboard/assets/{si-LK-N5RQ5JYF-CNhaw4jx.js → si-LK-N5RQ5JYF-yDbJWFTH.js} +1 -1
- package/dist/dashboard/assets/{sk-SK-C5VTKIMK-mHCphOXu.js → sk-SK-C5VTKIMK-DglyhbNB.js} +1 -1
- package/dist/dashboard/assets/{sl-SI-NN7IZMDC--lzgHthQ.js → sl-SI-NN7IZMDC-DPfq0s4I.js} +1 -1
- package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-C50iDFqe.js → stateDiagram-FHFEXIEX-Dm50jiEh.js} +1 -1
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-CoSm11sD.js +1 -0
- package/dist/dashboard/assets/{subset-shared.chunk-DNsjL37s.js → subset-shared.chunk-OHz6M6ct.js} +1 -1
- package/dist/dashboard/assets/{subset-worker.chunk-BaV3r-bl.js → subset-worker.chunk-BzwrOBtO.js} +1 -1
- package/dist/dashboard/assets/{sv-SE-XGPEYMSR-ckw8jfGN.js → sv-SE-XGPEYMSR-BLHNwr4k.js} +1 -1
- package/dist/dashboard/assets/{ta-IN-2NMHFXQM-CSeNmVhS.js → ta-IN-2NMHFXQM-Dw-6hUpN.js} +1 -1
- package/dist/dashboard/assets/{th-TH-HPSO5L25-Cvn1sg_3.js → th-TH-HPSO5L25-DPkcM_k9.js} +1 -1
- package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-DRYfuqHT.js → timeline-definition-GMOUNBTQ-BKFNQ_7e.js} +1 -1
- package/dist/dashboard/assets/{tr-TR-DEFEU3FU-CyhY22TB.js → tr-TR-DEFEU3FU-rBVsY5RS.js} +1 -1
- package/dist/dashboard/assets/{uk-UA-QMV73CPH-BvvAEO6U.js → uk-UA-QMV73CPH-CV6_yTZs.js} +1 -1
- package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-Crril7Cj.js → vennDiagram-DHZGUBPP-BZb_r5tY.js} +1 -1
- package/dist/dashboard/assets/{vi-VN-M7AON7JQ-c7AwSUZ8.js → vi-VN-M7AON7JQ-CJMgSsLz.js} +1 -1
- package/dist/dashboard/assets/{wardley-RL74JXVD-Da5AhSWU.js → wardley-RL74JXVD-DgqjmWyN.js} +1 -1
- package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-C4GeB_Df.js → wardleyDiagram-NUSXRM2D-BDpniS9m.js} +1 -1
- package/dist/dashboard/assets/webviewWindow-Bwv2421L.js +1 -0
- package/dist/dashboard/assets/window-BObcHN8e.js +1 -0
- package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-BPCFKXqR.js → xychartDiagram-5P7HB3ND-CCyPcx2A.js} +1 -1
- package/dist/dashboard/assets/{zh-CN-LNUGB5OW-BvNiZwKU.js → zh-CN-LNUGB5OW-9LJLF_AC.js} +1 -1
- package/dist/dashboard/assets/{zh-HK-E62DVLB3-CCFtoCWb.js → zh-HK-E62DVLB3-IefAiePp.js} +1 -1
- package/dist/dashboard/assets/{zh-TW-RAJ6MFWO-bYBr-stu.js → zh-TW-RAJ6MFWO-CHeY9rNZ.js} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/index.js +4099 -1538
- package/dist/skill-packs/agents/goal-implementer.md +42 -1
- package/dist/skill-packs/agents/goal-plan-reviewer.md +7 -0
- package/dist/skill-packs/agents/goal-planner.md +34 -0
- package/dist/skill-packs/catalog.json +31 -21
- package/dist/skill-packs/excalidraw/SKILL.md +244 -13
- package/dist/skill-packs/excalidraw/examples/Chart Kit.excalidraw.md +15740 -0
- package/dist/skill-packs/excalidraw/examples/Wireframe Kit.excalidraw.md +16860 -0
- package/dist/skill-packs/excalidraw/examples/atomic_check.js +99 -0
- package/dist/skill-packs/excalidraw/examples/chart.spec.json +122 -0
- package/dist/skill-packs/excalidraw/examples/chart_board.js +173 -0
- package/dist/skill-packs/excalidraw/examples/hello.spec.json +79 -9
- package/dist/skill-packs/excalidraw/examples/visual_board.js +82 -0
- package/dist/skill-packs/excalidraw/examples/wireframe_board.js +161 -0
- package/dist/skill-packs/excalidraw/scripts/build_excalidraw.js +292 -27
- package/dist/skill-packs/excalidraw/scripts/lib/charts.js +771 -0
- package/dist/skill-packs/excalidraw/scripts/lib/style.js +369 -35
- package/dist/skill-packs/excalidraw/scripts/lib/wireframe.js +406 -0
- package/dist/skill-packs/goal-skill/SKILL.md +369 -100
- package/dist/skill-packs/goal-skill/assets/goal-skill-demo.cjs +51 -0
- package/dist/skill-packs/goal-skill/assets/goal-skill-viewer.cjs +181 -0
- package/package.json +2 -1
- package/skill/references/integrations.md +54 -0
- package/skill/references/knowledge-and-recall.md +3 -1
- package/skill/references/sleep.md +4 -4
- package/skill-packs/agents/goal-implementer.md +42 -1
- package/skill-packs/agents/goal-plan-reviewer.md +7 -0
- package/skill-packs/agents/goal-planner.md +34 -0
- package/skill-packs/catalog.json +31 -21
- package/skill-packs/excalidraw/SKILL.md +244 -13
- package/skill-packs/excalidraw/examples/Chart Kit.excalidraw.md +15740 -0
- package/skill-packs/excalidraw/examples/Wireframe Kit.excalidraw.md +16860 -0
- package/skill-packs/excalidraw/examples/atomic_check.js +99 -0
- package/skill-packs/excalidraw/examples/chart.spec.json +122 -0
- package/skill-packs/excalidraw/examples/chart_board.js +173 -0
- package/skill-packs/excalidraw/examples/hello.spec.json +79 -9
- package/skill-packs/excalidraw/examples/visual_board.js +82 -0
- package/skill-packs/excalidraw/examples/wireframe_board.js +161 -0
- package/skill-packs/excalidraw/scripts/build_excalidraw.js +292 -27
- package/skill-packs/excalidraw/scripts/lib/charts.js +771 -0
- package/skill-packs/excalidraw/scripts/lib/style.js +369 -35
- package/skill-packs/excalidraw/scripts/lib/wireframe.js +406 -0
- package/skill-packs/goal-skill/SKILL.md +369 -100
- package/skill-packs/goal-skill/assets/goal-skill-demo.cjs +51 -0
- package/skill-packs/goal-skill/assets/goal-skill-viewer.cjs +181 -0
- package/skill-task-manager/SKILL.md +93 -0
- package/dist/dashboard/assets/channel-2YfRjala.js +0 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-DB_UnN1h.js +0 -1
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-DB_UnN1h.js +0 -1
- package/dist/dashboard/assets/clone-DaDvB3N6.js +0 -1
- package/dist/dashboard/assets/index-BIfrbaFS.js +0 -516
- package/dist/dashboard/assets/index-BdZ3tppu.css +0 -32
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-eW9-s0Sm.js +0 -1
- package/dist/dashboard/assets/webviewWindow-BbG9Cr7f.js +0 -1
- package/dist/dashboard/assets/window-DmNpeJAL.js +0 -1
|
@@ -2,9 +2,26 @@
|
|
|
2
2
|
// Module, Research). Excalifont (fontFamily 5), solid fills, always-rounded rects, 2px #1e1e1e ink,
|
|
3
3
|
// Excalidraw's native pastel swatches used semantically. These helpers return ElementSpec[] for
|
|
4
4
|
// build_excalidraw.js's buildExcalidraw({ elements }).
|
|
5
|
+
//
|
|
6
|
+
// Three rules keep boards clean and readable — the reason this file exists:
|
|
7
|
+
// 1. READABLE MEASURE — body text wraps to a bounded reading width (READ_W), never the whole
|
|
8
|
+
// board. sectionTitle/bullets/annotate/prose all cap + wrap. Long text is
|
|
9
|
+
// unreadable when it runs edge-to-edge; keep the measure ~60 chars.
|
|
10
|
+
// 2. NO OVERLAP — lay boards out with stack()/row() (flow layout: each element is placed
|
|
11
|
+
// after the previous one's measured height/width, so boxes can't collide).
|
|
12
|
+
// build_excalidraw also AUDITS the finished scene and warns on overlaps.
|
|
13
|
+
// 3. VISUAL-FIRST — Excalidraw's strength is pictures, not walls of text. Prefer the visual
|
|
14
|
+
// kit (funnel, windowFrame, button, input, textRows, imagePlaceholder,
|
|
15
|
+
// navbar, avatar, chip) over paragraphs. Use textRows() as body-copy filler
|
|
16
|
+
// in wireframes instead of real sentences.
|
|
5
17
|
|
|
6
18
|
const INK = '#1e1e1e';
|
|
7
19
|
|
|
20
|
+
// Comfortable reading measures (px). READ_W ≈ 60 Excalifont chars at 18px — the width past which a
|
|
21
|
+
// line of text gets hard to scan. NOTE_W is for terse side-annotations. Never let prose exceed READ_W.
|
|
22
|
+
const READ_W = 620;
|
|
23
|
+
const NOTE_W = 260;
|
|
24
|
+
|
|
8
25
|
// semantic palette: { fill, stroke } — matches the vault's swatches and their meaning
|
|
9
26
|
const PALETTE = {
|
|
10
27
|
green: { fill: '#b2f2bb', stroke: '#2f9e44' }, // benefit / positive / go
|
|
@@ -20,9 +37,23 @@ const PALETTE = {
|
|
|
20
37
|
};
|
|
21
38
|
function pal(c) { return PALETTE[c] || { fill: c, stroke: INK }; }
|
|
22
39
|
|
|
40
|
+
// neutral tones for wireframes / prototypes — grayscale chrome so the accent colors carry meaning
|
|
41
|
+
const WIRE = {
|
|
42
|
+
paper: '#ffffff',
|
|
43
|
+
panel: '#f8f9fa',
|
|
44
|
+
chrome: '#f1f3f5',
|
|
45
|
+
fill: '#f1f3f5',
|
|
46
|
+
line: '#dee2e6',
|
|
47
|
+
edge: '#ced4da',
|
|
48
|
+
hint: '#adb5bd',
|
|
49
|
+
text: '#868e96',
|
|
50
|
+
strong: '#495057',
|
|
51
|
+
};
|
|
52
|
+
|
|
23
53
|
let _gc = 0;
|
|
24
54
|
const newGroup = (hint) => `g${++_gc}-${hint || ''}`;
|
|
25
55
|
const lines = (t) => String(t).split('\n').length;
|
|
56
|
+
const LH = 1.25; // line height used everywhere height is derived from line count
|
|
26
57
|
|
|
27
58
|
// --- text fitting --------------------------------------------------------
|
|
28
59
|
// Excalifont glyph advance, in multiples of fontSize. Latin is ~0.58; emoji, arrows, CJK and
|
|
@@ -50,30 +81,35 @@ function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w +=
|
|
|
50
81
|
// Greedy word-wrap to a pixel width, honoring existing \n and HARD-breaking any single word that is
|
|
51
82
|
// itself wider than the box (e.g. a long URL). Returns the text WITH the newlines baked in — the
|
|
52
83
|
// Obsidian plugin keeps them instead of re-flowing the label into one over-wide centered line.
|
|
84
|
+
// A logical line's LEADING INDENT is preserved and re-applied to every line it wraps into, so a
|
|
85
|
+
// hanging indent (bullets(), indented notes) survives. Mirrors wrapText() in build_excalidraw.js.
|
|
53
86
|
function wrapToWidth(text, fontSize, width) {
|
|
54
87
|
const out = [];
|
|
55
88
|
for (const logical of String(text).split('\n')) {
|
|
56
|
-
const
|
|
89
|
+
const indent = (logical.match(/^[ \t]*/) || [''])[0];
|
|
90
|
+
const avail = Math.max(1, width - measureText(indent, fontSize));
|
|
91
|
+
const push = (s) => out.push(indent + s);
|
|
92
|
+
const words = logical.slice(indent.length).split(/\s+/).filter(Boolean);
|
|
57
93
|
if (!words.length) { out.push(''); continue; }
|
|
58
94
|
let cur = '';
|
|
59
95
|
for (let word of words) {
|
|
60
|
-
while (measureText(word, fontSize) >
|
|
96
|
+
while (measureText(word, fontSize) > avail) {
|
|
61
97
|
let acc = '';
|
|
62
98
|
for (const ch of word) {
|
|
63
|
-
if (acc && measureText(acc + ch, fontSize) >
|
|
99
|
+
if (acc && measureText(acc + ch, fontSize) > avail) break;
|
|
64
100
|
acc += ch;
|
|
65
101
|
}
|
|
66
|
-
if (cur) {
|
|
67
|
-
|
|
102
|
+
if (cur) { push(cur); cur = ''; }
|
|
103
|
+
push(acc);
|
|
68
104
|
word = word.slice(acc.length);
|
|
69
105
|
if (!word) break;
|
|
70
106
|
}
|
|
71
107
|
if (!word) continue;
|
|
72
108
|
const candidate = cur ? cur + ' ' + word : word;
|
|
73
|
-
if (cur && measureText(candidate, fontSize) >
|
|
109
|
+
if (cur && measureText(candidate, fontSize) > avail) { push(cur); cur = word; }
|
|
74
110
|
else cur = candidate;
|
|
75
111
|
}
|
|
76
|
-
if (cur)
|
|
112
|
+
if (cur) push(cur);
|
|
77
113
|
}
|
|
78
114
|
return out.join('\n');
|
|
79
115
|
}
|
|
@@ -84,7 +120,7 @@ function warnFit(msg) { if (!_warned.has(msg)) { _warned.add(msg); try { console
|
|
|
84
120
|
// Wrap `text` to the card's inner width, then shrink the font (down to minFont) until the wrapped
|
|
85
121
|
// label also fits the card height. Returns { text (with baked-in newlines), fontSize, lineCount, fits }.
|
|
86
122
|
function fitText(o) {
|
|
87
|
-
const { text = '', w = 260, h = 92, fontSize = 20, minFont = 9, lineHeight =
|
|
123
|
+
const { text = '', w = 260, h = 92, fontSize = 20, minFont = 9, lineHeight = LH, padX = 10, padY = 8 } = o;
|
|
88
124
|
const innerW = Math.max(1, w - 2 * padX);
|
|
89
125
|
const innerH = Math.max(1, h - 2 * padY);
|
|
90
126
|
let fs = Math.max(minFont, Math.round(fontSize));
|
|
@@ -98,9 +134,11 @@ function fitText(o) {
|
|
|
98
134
|
return { text: wrapped, fontSize: fs, lineCount, fits: lineCount * fs * lineHeight <= innerH };
|
|
99
135
|
}
|
|
100
136
|
|
|
101
|
-
//
|
|
137
|
+
// Arc-length midpoint of a polyline — keeps a connector label on the line even when it elbows.
|
|
138
|
+
// Also reports whether the segment the midpoint lands on runs VERTICALLY, so the label can be placed
|
|
139
|
+
// beside a vertical run instead of being struck through by it.
|
|
102
140
|
function polyMid(pts) {
|
|
103
|
-
if (!pts || pts.length < 2) return (pts && pts[0]) ||
|
|
141
|
+
if (!pts || pts.length < 2) return { x: (pts && pts[0] && pts[0][0]) || 0, y: (pts && pts[0] && pts[0][1]) || 0, vertical: false };
|
|
104
142
|
const seg = [];
|
|
105
143
|
let total = 0;
|
|
106
144
|
for (let i = 1; i < pts.length; i++) { const l = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); seg.push(l); total += l; }
|
|
@@ -108,13 +146,142 @@ function polyMid(pts) {
|
|
|
108
146
|
for (let i = 1; i < pts.length; i++) {
|
|
109
147
|
if (half <= seg[i - 1] || i === pts.length - 1) {
|
|
110
148
|
const t = seg[i - 1] ? half / seg[i - 1] : 0;
|
|
111
|
-
|
|
149
|
+
const dx = pts[i][0] - pts[i - 1][0], dy = pts[i][1] - pts[i - 1][1];
|
|
150
|
+
return { x: pts[i - 1][0] + dx * t, y: pts[i - 1][1] + dy * t, vertical: Math.abs(dy) > Math.abs(dx) };
|
|
112
151
|
}
|
|
113
152
|
half -= seg[i - 1];
|
|
114
153
|
}
|
|
115
|
-
return pts[0];
|
|
154
|
+
return { x: pts[0][0], y: pts[0][1], vertical: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- measurement + auto-layout ------------------------------------------
|
|
158
|
+
// Every builder below returns an ElementSpec[] that ALSO carries `.x/.y/.w/.h/.nextX/.nextY` so you
|
|
159
|
+
// can flow-lay-out boards without doing coordinate math by hand (the #1 source of overlap). `box()`
|
|
160
|
+
// stamps those; `bbox()` recovers them for a plain array; `stack()`/`row()` place a list with gaps.
|
|
161
|
+
function box(els, x, y, w, h) {
|
|
162
|
+
els.x = x; els.y = y; els.w = w; els.h = h; els.nextX = x + w; els.nextY = y + h;
|
|
163
|
+
return els;
|
|
164
|
+
}
|
|
165
|
+
// Tag every element of a composite (that doesn't already belong to a nested group) with ONE group id,
|
|
166
|
+
// so the widget moves as a unit in Excalidraw and the build-time overlap auditor treats its own chrome
|
|
167
|
+
// as intentional. Returns the same array (carried metadata intact).
|
|
168
|
+
function G(els, hint) {
|
|
169
|
+
const g = newGroup(hint);
|
|
170
|
+
for (const e of els) { if (e.group == null) e.group = g; }
|
|
171
|
+
return els;
|
|
172
|
+
}
|
|
173
|
+
// Pixel bounding box of a spec array (pre-build form). Handles point-based (line/arrow) + boxed els.
|
|
174
|
+
function bbox(els) {
|
|
175
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
176
|
+
for (const e of els) {
|
|
177
|
+
let x0, y0, x1, y1;
|
|
178
|
+
if (e.points && e.points.length) {
|
|
179
|
+
const xs = e.points.map((p) => p[0]); const ys = e.points.map((p) => p[1]);
|
|
180
|
+
x0 = (e.x || 0) + Math.min(...xs); y0 = (e.y || 0) + Math.min(...ys);
|
|
181
|
+
x1 = (e.x || 0) + Math.max(...xs); y1 = (e.y || 0) + Math.max(...ys);
|
|
182
|
+
} else {
|
|
183
|
+
x0 = e.x || 0; y0 = e.y || 0;
|
|
184
|
+
const w = e.width != null ? e.width : (e.text ? measureText(String(e.text).split('\n')[0], e.fontSize || 20) : 0);
|
|
185
|
+
const h = e.height != null ? e.height : (e.text ? lines(e.text) * (e.fontSize || 20) * LH : 0);
|
|
186
|
+
x1 = x0 + w; y1 = y0 + h;
|
|
187
|
+
}
|
|
188
|
+
if (x0 < minX) minX = x0; if (y0 < minY) minY = y0;
|
|
189
|
+
if (x1 > maxX) maxX = x1; if (y1 > maxY) maxY = y1;
|
|
190
|
+
}
|
|
191
|
+
if (!isFinite(minX)) return { x: 0, y: 0, w: 0, h: 0 };
|
|
192
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
116
193
|
}
|
|
194
|
+
const measH = (els) => (els && els.h != null) ? els.h : bbox(els).h;
|
|
195
|
+
const measW = (els) => (els && els.w != null) ? els.w : bbox(els).w;
|
|
117
196
|
|
|
197
|
+
// Move a spec array by (dx,dy). x/y is the offset for BOTH boxed and point-based elements, so this
|
|
198
|
+
// works uniformly. Preserves carried metadata.
|
|
199
|
+
function translate(els, dx, dy) {
|
|
200
|
+
const moved = els.map((e) => Object.assign({}, e, { x: (e.x || 0) + dx, y: (e.y || 0) + dy }));
|
|
201
|
+
if (els.w != null) box(moved, (els.x || 0) + dx, (els.y || 0) + dy, els.w, els.h);
|
|
202
|
+
return moved;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Flow a list of blocks TOP-TO-BOTTOM with a fixed gap — the height of each block is measured, so the
|
|
206
|
+
// next one is placed right below it and nothing ever overlaps. Each item is either a factory
|
|
207
|
+
// `(x, y) => els` (preferred: it draws itself at the running cursor) or an already-built `els` array
|
|
208
|
+
// (it gets shifted down to the cursor). Returns the merged array carrying `.w/.h/.nextY`.
|
|
209
|
+
function stack(o) {
|
|
210
|
+
const { x = 0, y = 0, gap = 24, items = [], align = 'left', width = null } = o;
|
|
211
|
+
const out = []; let cy = y, maxW = 0;
|
|
212
|
+
for (const it of items) {
|
|
213
|
+
if (it == null) continue;
|
|
214
|
+
let els = (typeof it === 'function') ? it(x, cy) : it;
|
|
215
|
+
if (typeof it !== 'function') { const b = bbox(els); els = translate(els, x - b.x, cy - b.y); }
|
|
216
|
+
const w = measW(els), h = measH(els);
|
|
217
|
+
if (align === 'center' && width != null) els = translate(els, (width - w) / 2, 0);
|
|
218
|
+
else if (align === 'right' && width != null) els = translate(els, (width - w), 0);
|
|
219
|
+
out.push(...els);
|
|
220
|
+
maxW = Math.max(maxW, w);
|
|
221
|
+
cy += h + gap;
|
|
222
|
+
}
|
|
223
|
+
return box(out, x, y, width || maxW, Math.max(0, cy - gap - y));
|
|
224
|
+
}
|
|
225
|
+
// Flow a list of blocks LEFT-TO-RIGHT with a fixed gap (same contract as stack, horizontal).
|
|
226
|
+
function row(o) {
|
|
227
|
+
const { x = 0, y = 0, gap = 24, items = [], valign = 'top' } = o;
|
|
228
|
+
const out = []; let cx = x, maxH = 0;
|
|
229
|
+
const built = items.filter(Boolean).map((it) => (typeof it === 'function') ? it : it);
|
|
230
|
+
// first measure heights so valign can center within the tallest block
|
|
231
|
+
const measured = built.map((it) => (typeof it === 'function') ? it(0, 0) : it);
|
|
232
|
+
maxH = measured.reduce((m, els) => Math.max(m, measH(els)), 0);
|
|
233
|
+
built.forEach((it, i) => {
|
|
234
|
+
const probe = measured[i];
|
|
235
|
+
const w = measW(probe), h = measH(probe);
|
|
236
|
+
const dy = valign === 'middle' ? (maxH - h) / 2 : valign === 'bottom' ? (maxH - h) : 0;
|
|
237
|
+
let els = (typeof it === 'function') ? it(cx, y + dy) : translate(it, cx - bbox(it).x, y + dy - bbox(it).y);
|
|
238
|
+
out.push(...els);
|
|
239
|
+
cx += w + gap;
|
|
240
|
+
});
|
|
241
|
+
return box(out, x, y, Math.max(0, cx - gap - x), maxH);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// --- readable text primitives -------------------------------------------
|
|
245
|
+
// A bounded, wrapped paragraph. Text NEVER exceeds `width` (default the reading measure) — this is
|
|
246
|
+
// the antidote to edge-to-edge text. Height is derived from the wrapped line count. Visual-first
|
|
247
|
+
// boards use this sparingly; prefer pictures.
|
|
248
|
+
function prose(o) {
|
|
249
|
+
const { x = 0, y = 0, text = '', fontSize = 18, width = READ_W, color = INK, align = 'left' } = o;
|
|
250
|
+
const w = Math.min(width, Math.max(40, measureText(String(text), fontSize)));
|
|
251
|
+
const wrapped = wrapToWidth(text, fontSize, w);
|
|
252
|
+
const lc = wrapped.split('\n').length;
|
|
253
|
+
const out = [{ type: 'text', x, y, text: wrapped, fontSize, color, width: w, align, fontFamily: 5 }];
|
|
254
|
+
return box(out, x, y, w, lc * fontSize * LH);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// big plain section header (no box), like "Configuration" / "Flow". Long titles WRAP to `maxWidth`
|
|
258
|
+
// instead of running off the board.
|
|
259
|
+
function sectionTitle(o) {
|
|
260
|
+
const { x = 0, y = 0, text, fontSize = 44, color = INK, maxWidth = 1000, align = 'left' } = o;
|
|
261
|
+
const w = Math.min(maxWidth, Math.max(200, measureText(String(text), fontSize) + fontSize));
|
|
262
|
+
const wrapped = wrapToWidth(text, fontSize, w);
|
|
263
|
+
const lc = wrapped.split('\n').length;
|
|
264
|
+
const out = [{ type: 'text', x, y, text: wrapped, fontSize, color, width: w, align, fontFamily: 5 }];
|
|
265
|
+
return box(out, x, y, w, lc * fontSize * LH);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// bullet list as one left-aligned block. Each item wraps to `width` (default reading measure) with a
|
|
269
|
+
// hanging indent under the bullet, so long items stay readable instead of one giant line.
|
|
270
|
+
function bullets(o) {
|
|
271
|
+
const { x = 0, y = 0, items = [], fontSize = 20, color = INK, bullet = '• ', width = READ_W } = o;
|
|
272
|
+
const indent = ' '.repeat(bullet.length);
|
|
273
|
+
const bw = measureText(bullet, fontSize);
|
|
274
|
+
const outLines = [];
|
|
275
|
+
for (const it of items) {
|
|
276
|
+
const wrapped = wrapToWidth(it, fontSize, Math.max(40, width - bw)).split('\n');
|
|
277
|
+
wrapped.forEach((ln, i) => outLines.push((i === 0 ? bullet : indent) + ln));
|
|
278
|
+
}
|
|
279
|
+
const text = outLines.join('\n');
|
|
280
|
+
const out = [{ type: 'text', x, y, text, fontSize, color, width, fontFamily: 5 }];
|
|
281
|
+
return box(out, x, y, width, outLines.length * fontSize * LH);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- rounded card / flow node -------------------------------------------
|
|
118
285
|
// rounded card: filled rect + centered label, grouped so they move together. The label is
|
|
119
286
|
// hard-wrapped to the card width and the font auto-shrinks (to `minFont`) until it also fits the
|
|
120
287
|
// height, so text can never spill outside the box. Warns if even the floor font won't fit.
|
|
@@ -124,28 +291,22 @@ function card(o) {
|
|
|
124
291
|
const p = pal(color);
|
|
125
292
|
const fit = fitText({ text, w, h, fontSize, minFont, padX, padY });
|
|
126
293
|
if (!fit.fits) warnFit(`label ${JSON.stringify(String(text).slice(0, 32))} won't fit a ${w}×${h} card even at ${minFont}px — enlarge the card or shorten the text.`);
|
|
127
|
-
const textH = fit.lineCount * fit.fontSize *
|
|
294
|
+
const textH = fit.lineCount * fit.fontSize * LH;
|
|
128
295
|
const ty = y + Math.max(padY, (h - textH) / 2);
|
|
129
|
-
return [
|
|
296
|
+
return box([
|
|
130
297
|
{ type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth, roundness, group },
|
|
131
298
|
{ type: 'text', x, y: ty, text: fit.text, fontSize: fit.fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
|
|
132
|
-
];
|
|
299
|
+
], x, y, w, h);
|
|
133
300
|
}
|
|
134
301
|
// small node (flow step) — card with tighter defaults
|
|
135
302
|
function node(o) { return card(Object.assign({ w: 200, h: 64, fontSize: 18 }, o)); }
|
|
136
303
|
|
|
137
|
-
// big plain section header (no box), like "Configuration" / "Flow"
|
|
138
|
-
function sectionTitle(o) {
|
|
139
|
-
const { x, y, text, fontSize = 44, color = INK } = o;
|
|
140
|
-
return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: Math.max(200, String(text).length * fontSize * 0.6) }];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
304
|
// labeled connector arrow between two points (centers of cards, etc.).
|
|
144
305
|
// Routing: pass `via:[[x,y],…]` for explicit waypoints, or `elbow:'hv'|'vh'` for an auto right-angle
|
|
145
306
|
// bend ('hv' = horizontal then vertical, 'vh' = vertical then horizontal). Use these to route a link
|
|
146
307
|
// AROUND intervening boxes instead of cutting a diagonal straight through them.
|
|
147
308
|
function connector(o) {
|
|
148
|
-
const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16, via = null, elbow = null } = o;
|
|
309
|
+
const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16, via = null, elbow = null, labelSide = 'left' } = o;
|
|
149
310
|
let pts;
|
|
150
311
|
if (via && via.length) pts = [from, ...via, to];
|
|
151
312
|
else if (elbow === 'hv') pts = [from, [to[0], from[1]], to];
|
|
@@ -153,19 +314,26 @@ function connector(o) {
|
|
|
153
314
|
else pts = [from, to];
|
|
154
315
|
const out = [{ type: 'arrow', points: pts, strokeColor, strokeWidth, strokeStyle: dashed ? 'dashed' : 'solid', endArrow: 'arrow', startArrow: double ? 'arrow' : null }];
|
|
155
316
|
if (label) {
|
|
156
|
-
const
|
|
317
|
+
const m = polyMid(pts);
|
|
157
318
|
const w = Math.max(40, measureText(String(label), fontSize) + fontSize);
|
|
158
|
-
|
|
319
|
+
// On a vertical run the line would strike straight through a centered label, so sit the label
|
|
320
|
+
// beside it (`labelSide`, default left); on a horizontal run keep it centered just above.
|
|
321
|
+
const el = m.vertical
|
|
322
|
+
? { x: labelSide === 'right' ? m.x + 8 : m.x - w - 8, y: m.y - fontSize * 0.62, align: labelSide === 'right' ? 'left' : 'right' }
|
|
323
|
+
: { x: m.x - w / 2, y: m.y - fontSize - 4, align: 'center' };
|
|
324
|
+
out.push({ type: 'text', x: el.x, y: el.y, text: label, fontSize, color: labelColor, width: w, align: el.align, fontFamily: 5 });
|
|
159
325
|
}
|
|
160
326
|
return out;
|
|
161
327
|
}
|
|
162
328
|
|
|
163
|
-
// side-annotation: short arrow from a node edge to a spec note (the "90sec / deepseek..." pattern)
|
|
329
|
+
// side-annotation: short arrow from a node edge to a spec note (the "90sec / deepseek..." pattern).
|
|
330
|
+
// The note wraps to NOTE_W so it stays a tidy column, never a runaway line.
|
|
164
331
|
function annotate(o) {
|
|
165
|
-
const { from, to, text, fontSize = 16, color = '#495057' } = o;
|
|
332
|
+
const { from, to, text, fontSize = 16, color = '#495057', width = NOTE_W } = o;
|
|
333
|
+
const wrapped = wrapToWidth(text, fontSize, width);
|
|
166
334
|
return [
|
|
167
335
|
{ type: 'arrow', points: [from, to], strokeColor: color, strokeWidth: 1.5, endArrow: 'arrow' },
|
|
168
|
-
{ type: 'text', x: to[0] + 6, y: to[1] - fontSize * 0.6, text, fontSize, color, width
|
|
336
|
+
{ type: 'text', x: to[0] + 6, y: to[1] - fontSize * 0.6, text: wrapped, fontSize, color, width, align: 'left', fontFamily: 5 },
|
|
169
337
|
];
|
|
170
338
|
}
|
|
171
339
|
|
|
@@ -178,13 +346,12 @@ function column(o) {
|
|
|
178
346
|
const c = (typeof it === 'object' && it.color) || color;
|
|
179
347
|
out.push(...card({ x, y: y + i * (h + gap), w, h, text: t, color: c, fontSize }));
|
|
180
348
|
});
|
|
181
|
-
return out;
|
|
349
|
+
return box(out, x, y, w, items.length ? items.length * (h + gap) - gap : 0);
|
|
182
350
|
}
|
|
183
351
|
|
|
184
352
|
// hub-and-spoke: central node + satellites with arrows (the "Personalization Layer" + APIs pattern)
|
|
185
353
|
function hub(o) {
|
|
186
354
|
const { cx, cy, label, color = 'purple', rx = 64, ry = 44, radius = 170, spokes = [], spokeColor = 'blue', spokeRx = 30, spokeRy = 30, startAngle = -Math.PI / 2 } = o;
|
|
187
|
-
const group = newGroup('hub');
|
|
188
355
|
const out = [];
|
|
189
356
|
const p = pal(color);
|
|
190
357
|
out.push({ type: 'ellipse', x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2, strokeColor: p.stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth: 2 });
|
|
@@ -203,13 +370,169 @@ function hub(o) {
|
|
|
203
370
|
return out;
|
|
204
371
|
}
|
|
205
372
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
373
|
+
// --- funnel -------------------------------------------------------------
|
|
374
|
+
// A classic marketing/conversion funnel: filled trapezoid bands narrowing top→bottom, one per stage,
|
|
375
|
+
// each with a centered (auto-fit) label. Optional per-stage `note` (e.g. a metric / drop-off) is
|
|
376
|
+
// placed in a right margin with a short horizontal connector — no diagonal crossings. `stages` is a
|
|
377
|
+
// list of { label, note?, color? }. Returns ElementSpec[] carrying `.w/.h/.nextY`.
|
|
378
|
+
function funnel(o) {
|
|
379
|
+
const {
|
|
380
|
+
x = 0, y = 0, w = 520, stageH = 84, gap = 8, stages = [],
|
|
381
|
+
topW = null, botW = null, fontSize = 20, labelColor = INK,
|
|
382
|
+
noteColor = WIRE.strong, noteGap = 44, notes: showNotes = true,
|
|
383
|
+
} = o;
|
|
384
|
+
const n = Math.max(1, stages.length);
|
|
385
|
+
const tW = topW != null ? topW : w;
|
|
386
|
+
const bW = botW != null ? botW : Math.max(80, Math.round(w * 0.34));
|
|
387
|
+
const cx = x + w / 2;
|
|
388
|
+
const palette = ['blue', 'purple', 'yellow', 'mint', 'green', 'red'];
|
|
389
|
+
const out = [];
|
|
390
|
+
const hasNotes = showNotes && stages.some((s) => s.note);
|
|
391
|
+
let cy = y;
|
|
392
|
+
stages.forEach((s, i) => {
|
|
393
|
+
const wt = tW + (bW - tW) * (i / n);
|
|
394
|
+
const wb = tW + (bW - tW) * ((i + 1) / n);
|
|
395
|
+
const p = pal(s.color || palette[i % palette.length]);
|
|
396
|
+
// closed, filled trapezoid (sharp corners read as a funnel; roundness blobs it)
|
|
397
|
+
out.push({
|
|
398
|
+
type: 'line',
|
|
399
|
+
points: [[cx - wt / 2, cy], [cx + wt / 2, cy], [cx + wb / 2, cy + stageH], [cx - wb / 2, cy + stageH], [cx - wt / 2, cy]],
|
|
400
|
+
strokeColor: p.stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth: 2,
|
|
401
|
+
});
|
|
402
|
+
// label fit to the narrower (bottom) width so it never spills past the band edges
|
|
403
|
+
const lw = Math.max(60, Math.min(wt, wb) - 24);
|
|
404
|
+
const fit = fitText({ text: s.label, w: lw + 24, h: stageH, fontSize, minFont: 12 });
|
|
405
|
+
const th = fit.lineCount * fit.fontSize * LH;
|
|
406
|
+
out.push({ type: 'text', x: cx - lw / 2, y: cy + (stageH - th) / 2, text: fit.text, fontSize: fit.fontSize, color: labelColor, width: lw, align: 'center', fontFamily: 5 });
|
|
407
|
+
if (hasNotes && s.note) {
|
|
408
|
+
const edgeX = cx + ((wt + wb) / 2) / 2; // right edge at the band's vertical middle
|
|
409
|
+
const midY = cy + stageH / 2;
|
|
410
|
+
const nx = x + w + noteGap;
|
|
411
|
+
out.push({ type: 'arrow', points: [[edgeX, midY], [nx - 6, midY]], strokeColor: noteColor, strokeWidth: 1.5, endArrow: 'arrow' });
|
|
412
|
+
const wrapped = wrapToWidth(s.note, 15, NOTE_W);
|
|
413
|
+
out.push({ type: 'text', x: nx, y: midY - 15 * 0.6 * lines(wrapped), text: wrapped, fontSize: 15, color: noteColor, width: NOTE_W, align: 'left', fontFamily: 5 });
|
|
414
|
+
}
|
|
415
|
+
cy += stageH + gap;
|
|
416
|
+
});
|
|
417
|
+
const totalW = w + (hasNotes ? noteGap + NOTE_W : 0);
|
|
418
|
+
return box(out, x, y, totalW, Math.max(0, cy - gap - y));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- wireframe / prototype kit ------------------------------------------
|
|
422
|
+
// Neutral, grayscale components so you can sketch a UI PROTOTYPE (browser page, app screen, form)
|
|
423
|
+
// that reads as a mock, not as decoration. Real content stays sparse; textRows() stands in for body
|
|
424
|
+
// copy so a wireframe never becomes a wall of text.
|
|
425
|
+
|
|
426
|
+
// A window / browser chrome. `kind:'browser'` draws a URL bar; `kind:'app'` a centered title.
|
|
427
|
+
// `.inner = {x,y,w,h}` is the safe content region — place children there and they won't hit the chrome.
|
|
428
|
+
function windowFrame(o) {
|
|
429
|
+
const { x = 0, y = 0, w = 560, h = 380, title = '', url = null, kind = 'browser', bar = 38, fill = WIRE.paper, chrome = WIRE.chrome, stroke = WIRE.edge, pad = 16 } = o;
|
|
430
|
+
const out = [];
|
|
431
|
+
out.push({ type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: fill, fillStyle: 'solid', strokeWidth: 2, roundness: true });
|
|
432
|
+
out.push({ type: 'rectangle', x, y, width: w, height: bar, strokeColor: stroke, backgroundColor: chrome, fillStyle: 'solid', strokeWidth: 2, roundness: true });
|
|
433
|
+
['#ff5f57', '#febc2e', '#28c840'].forEach((c, i) => out.push({ type: 'ellipse', x: x + 14 + i * 20, y: y + bar / 2 - 5, width: 10, height: 10, strokeColor: c, backgroundColor: c, fillStyle: 'solid', strokeWidth: 1 }));
|
|
434
|
+
if (kind === 'browser') {
|
|
435
|
+
const ux = x + 80, uw = w - 96;
|
|
436
|
+
out.push({ type: 'rectangle', x: ux, y: y + 7, width: uw, height: bar - 14, strokeColor: WIRE.line, backgroundColor: WIRE.paper, fillStyle: 'solid', strokeWidth: 1, roundness: true });
|
|
437
|
+
if (url) out.push({ type: 'text', x: ux + 12, y: y + bar / 2 - 7, text: url, fontSize: 12, color: WIRE.text, width: uw - 24, fontFamily: 3 });
|
|
438
|
+
} else if (title) {
|
|
439
|
+
out.push({ type: 'text', x: x + 80, y: y + bar / 2 - 8, text: title, fontSize: 13, color: WIRE.strong, width: w - 160, align: 'center', fontFamily: 5 });
|
|
440
|
+
}
|
|
441
|
+
G(out, 'window');
|
|
442
|
+
box(out, x, y, w, h);
|
|
443
|
+
out.inner = { x: x + pad, y: y + bar + pad, w: w - 2 * pad, h: h - bar - 2 * pad };
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// A solid CTA button (default) or an outline button (`variant:'outline'`).
|
|
448
|
+
function button(o) {
|
|
449
|
+
const { x = 0, y = 0, w = 150, h = 42, text = 'Button', color = 'blue', fontSize = 15, variant = 'solid', textColor = '#ffffff' } = o;
|
|
450
|
+
const p = pal(color);
|
|
451
|
+
const solid = variant !== 'outline';
|
|
452
|
+
const fit = fitText({ text, w, h, fontSize, minFont: 11 });
|
|
453
|
+
const th = fit.lineCount * fit.fontSize * LH;
|
|
454
|
+
const g = newGroup('button');
|
|
455
|
+
return box([
|
|
456
|
+
{ type: 'rectangle', x, y, width: w, height: h, strokeColor: p.stroke, backgroundColor: solid ? p.stroke : WIRE.paper, fillStyle: 'solid', strokeWidth: 2, roundness: true, group: g },
|
|
457
|
+
{ type: 'text', x, y: y + (h - th) / 2, text: fit.text, fontSize: fit.fontSize, color: solid ? textColor : p.stroke, width: w, align: 'center', fontFamily: 5, group: g },
|
|
458
|
+
], x, y, w, h);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// A text input field with a muted placeholder, and an optional label above it.
|
|
462
|
+
function input(o) {
|
|
463
|
+
const { x = 0, y = 0, w = 260, h = 42, placeholder = '', label = null, fontSize = 14 } = o;
|
|
464
|
+
const out = []; let yy = y;
|
|
465
|
+
if (label) { out.push({ type: 'text', x, y: yy, text: label, fontSize: 12, color: WIRE.text, width: w, fontFamily: 5 }); yy += 12 * LH + 6; }
|
|
466
|
+
out.push({ type: 'rectangle', x, y: yy, width: w, height: h, strokeColor: WIRE.edge, backgroundColor: WIRE.paper, fillStyle: 'solid', strokeWidth: 1.5, roundness: true });
|
|
467
|
+
if (placeholder) out.push({ type: 'text', x: x + 12, y: yy + h / 2 - fontSize * 0.6, text: placeholder, fontSize, color: WIRE.hint, width: w - 24, fontFamily: 5 });
|
|
468
|
+
return box(G(out, 'input'), x, y, w, (yy - y) + h);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Grey bars that stand in for body copy in a wireframe — VISUAL filler, no real text. The last row is
|
|
472
|
+
// short (`last` fraction) like a real paragraph's final line.
|
|
473
|
+
function textRows(o) {
|
|
474
|
+
const { x = 0, y = 0, w = 320, rows = 3, lineH = 12, gap = 10, color = WIRE.line, last = 0.6 } = o;
|
|
475
|
+
const out = [];
|
|
476
|
+
for (let i = 0; i < rows; i++) {
|
|
477
|
+
const rw = (i === rows - 1 && rows > 1) ? Math.round(w * last) : w;
|
|
478
|
+
out.push({ type: 'rectangle', x, y: y + i * (lineH + gap), width: rw, height: lineH, strokeColor: 'transparent', backgroundColor: color, fillStyle: 'solid', strokeWidth: 0, roundness: true });
|
|
479
|
+
}
|
|
480
|
+
return box(out, x, y, w, rows * lineH + (rows - 1) * gap);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// The universal image-placeholder glyph: a box with an X. Use anywhere a real screenshot would go.
|
|
484
|
+
function imagePlaceholder(o) {
|
|
485
|
+
const { x = 0, y = 0, w = 220, h = 150, fill = WIRE.fill, stroke = WIRE.hint, label = null } = o;
|
|
486
|
+
const out = [
|
|
487
|
+
{ type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: fill, fillStyle: 'solid', strokeWidth: 1.5, roundness: false },
|
|
488
|
+
{ type: 'line', points: [[x, y], [x + w, y + h]], strokeColor: stroke, strokeWidth: 1.5 },
|
|
489
|
+
{ type: 'line', points: [[x + w, y], [x, y + h]], strokeColor: stroke, strokeWidth: 1.5 },
|
|
490
|
+
];
|
|
491
|
+
if (label) out.push({ type: 'text', x, y: y + h / 2 - 8, text: label, fontSize: 13, color: WIRE.text, width: w, align: 'center', fontFamily: 5 });
|
|
492
|
+
return box(G(out, 'imgph'), x, y, w, h);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// A round avatar/user placeholder.
|
|
496
|
+
function avatar(o) {
|
|
497
|
+
const { cx = 0, cy = 0, r = 24, fill = WIRE.line, stroke = WIRE.hint } = o;
|
|
498
|
+
return box([{ type: 'ellipse', x: cx - r, y: cy - r, width: 2 * r, height: 2 * r, strokeColor: stroke, backgroundColor: fill, fillStyle: 'solid', strokeWidth: 1.5 }], cx - r, cy - r, 2 * r, 2 * r);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// A top navigation bar: brand on the left, links on the right, optional CTA button.
|
|
502
|
+
function navbar(o) {
|
|
503
|
+
const { x = 0, y = 0, w = 800, h = 54, brand = '', items = [], cta = null, fill = WIRE.paper, stroke = WIRE.line, color = WIRE.strong } = o;
|
|
504
|
+
const out = [{ type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: fill, fillStyle: 'solid', strokeWidth: 1.5, roundness: false }];
|
|
505
|
+
if (brand) out.push({ type: 'text', x: x + 22, y: y + h / 2 - 11, text: brand, fontSize: 18, color: INK, fontFamily: 5 });
|
|
506
|
+
let rx = x + w - 22 - (cta ? 132 : 0);
|
|
507
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
508
|
+
const t = items[i]; const tw = measureText(t, 14) + 20; rx -= tw;
|
|
509
|
+
out.push({ type: 'text', x: rx, y: y + h / 2 - 9, text: t, fontSize: 14, color, width: tw, align: 'center', fontFamily: 5 });
|
|
510
|
+
}
|
|
511
|
+
if (cta) out.push(...button({ x: x + w - 22 - 120, y: y + (h - 34) / 2, w: 120, h: 34, text: cta, fontSize: 13 }));
|
|
512
|
+
return box(G(out, 'navbar'), x, y, w, h);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// A small pill/tag/badge sized to its text.
|
|
516
|
+
function chip(o) {
|
|
517
|
+
const { x = 0, y = 0, text = '', color = 'gray', fontSize = 13 } = o;
|
|
518
|
+
const p = pal(color);
|
|
519
|
+
const w = Math.round(measureText(text, fontSize) + 22), h = Math.round(fontSize + 14);
|
|
520
|
+
const g = newGroup('chip');
|
|
521
|
+
return box([
|
|
522
|
+
{ type: 'rectangle', x, y, width: w, height: h, strokeColor: p.stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth: 1.5, roundness: true, group: g },
|
|
523
|
+
{ type: 'text', x, y: y + (h - fontSize * LH) / 2, text, fontSize, color: p.stroke, width: w, align: 'center', fontFamily: 5, group: g },
|
|
524
|
+
], x, y, w, h);
|
|
211
525
|
}
|
|
212
526
|
|
|
527
|
+
// A thin horizontal rule.
|
|
528
|
+
function divider(o) {
|
|
529
|
+
const { x = 0, y = 0, w = 600, color = WIRE.line, strokeWidth = 1.5 } = o;
|
|
530
|
+
return box([{ type: 'line', points: [[x, y], [x + w, y]], strokeColor: color, strokeWidth }], x, y, w, 0);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// bullet list as a single left-aligned block (their "Configuration" spec list pattern) — kept for
|
|
534
|
+
// back-compat; prefer bullets() above which wraps to a reading measure.
|
|
535
|
+
|
|
213
536
|
// center helpers for wiring connectors to card edges
|
|
214
537
|
const center = (x, y, w, h) => [x + w / 2, y + h / 2];
|
|
215
538
|
const rightOf = (x, y, w, h) => [x + w, y + h / 2];
|
|
@@ -217,4 +540,15 @@ const leftOf = (x, y, w, h) => [x, y + h / 2];
|
|
|
217
540
|
const bottomOf = (x, y, w, h) => [x + w / 2, y + h];
|
|
218
541
|
const topOf = (x, y, w, h) => [x + w / 2, y];
|
|
219
542
|
|
|
220
|
-
module.exports = {
|
|
543
|
+
module.exports = {
|
|
544
|
+
INK, PALETTE, WIRE, READ_W, NOTE_W, pal,
|
|
545
|
+
// text + layout
|
|
546
|
+
measureText, fitText, wrapToWidth, prose, sectionTitle, bullets, annotate,
|
|
547
|
+
box, bbox, translate, stack, row,
|
|
548
|
+
// structure
|
|
549
|
+
card, node, connector, column, hub, funnel,
|
|
550
|
+
// wireframe / prototype kit
|
|
551
|
+
windowFrame, button, input, textRows, imagePlaceholder, avatar, navbar, chip, divider,
|
|
552
|
+
// edge helpers
|
|
553
|
+
center, rightOf, leftOf, bottomOf, topOf,
|
|
554
|
+
};
|