Compare commits

...

7 Commits

Author SHA1 Message Date
Julien Calixte
629fa87ec1 style(layout): give the macroplan grid more width 2026-06-17 01:01:35 +02:00
Julien Calixte
6e800008d9 docs(sample): drop "slip" wording from the sample plan 2026-06-17 01:01:35 +02:00
Julien Calixte
d8ce9a052c feat(grid): stop bars at the last symbol and shade today's column
- bar line begins/ends at a glyph centre — no stub past the first/last symbol
- glyph halos so the bar passes behind, not through, the symbols
- today's week is a shaded column instead of a vertical rule
- widen week columns so date labels stop colliding
- drop "slip" wording from the legend
2026-06-17 01:01:35 +02:00
Julien Calixte
7cd6a1f98e feat(editor): syntax-highlight the TOML source with Shiki 2026-06-17 01:01:35 +02:00
Julien Calixte
e51f3b96e7 test(model): cover bar extent relative to the now week 2026-06-17 01:01:35 +02:00
Julien Calixte
7470d41803 feat(model): keep an overdue feature's bar running to the now week 2026-06-17 01:01:35 +02:00
Julien Calixte
8567c13036 chore(deps): add shiki for editor syntax highlighting 2026-06-17 01:01:35 +02:00
8 changed files with 520 additions and 37 deletions

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"daisyui": "^5.5.23",
"shiki": "^4.2.0",
"smol-toml": "^1.6.1",
"vue": "^3.5.34"
},

337
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
daisyui:
specifier: ^5.5.23
version: 5.5.23
shiki:
specifier: ^4.2.0
version: 4.2.0
smol-toml:
specifier: ^1.6.1
version: 1.6.1
@@ -214,6 +217,37 @@ packages:
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@shikijs/core@4.2.0':
resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==}
engines: {node: '>=20'}
'@shikijs/engine-javascript@4.2.0':
resolution: {integrity: sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==}
engines: {node: '>=20'}
'@shikijs/engine-oniguruma@4.2.0':
resolution: {integrity: sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==}
engines: {node: '>=20'}
'@shikijs/langs@4.2.0':
resolution: {integrity: sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==}
engines: {node: '>=20'}
'@shikijs/primitive@4.2.0':
resolution: {integrity: sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==}
engines: {node: '>=20'}
'@shikijs/themes@4.2.0':
resolution: {integrity: sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==}
engines: {node: '>=20'}
'@shikijs/types@4.2.0':
resolution: {integrity: sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==}
engines: {node: '>=20'}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -323,15 +357,27 @@ packages:
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
'@types/node@24.13.2':
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@ungap/structured-clone@1.3.1':
resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
'@vitejs/plugin-vue@6.0.7':
resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -467,10 +513,19 @@ packages:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
character-entities-legacy@3.0.0:
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -478,6 +533,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
@@ -498,10 +556,17 @@ packages:
daisyui@5.5.23:
resolution: {integrity: sha512-xuheNUSL4T6ZVtWXoioqcNkjoyGX85QTDz4HTw2aBPfqk4fuMjax5HDo8qCmpV6M1YN8bGvfx5BpYCoDeRlt+A==}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -567,6 +632,15 @@ packages:
resolution: {integrity: sha512-bKrsQnFNpcjZG0UPsH7UMN7Oyp3AB42LXk2GuiQmu7l4QFxH7lsw5T1eWEtE2+vbIFrTC45sbNSB2pkB8MTfKA==}
engines: {node: '>=20.0.0'}
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
@@ -672,6 +746,24 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
micromark-util-character@2.1.1:
resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
micromark-util-encode@2.0.1:
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
micromark-util-sanitize-uri@2.0.1:
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
micromark-util-symbol@2.0.1:
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
micromark-util-types@2.0.2:
resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -697,6 +789,12 @@ packages:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'}
oniguruma-parser@0.12.2:
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
oniguruma-to-es@4.3.6:
resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -725,9 +823,21 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
property-information@7.2.0:
resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
regex-utilities@2.3.0:
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
regex@6.1.0:
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
rolldown@1.0.3:
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -746,6 +856,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
shiki@4.2.0:
resolution: {integrity: sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==}
engines: {node: '>=20'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -761,6 +875,9 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -775,6 +892,9 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -805,6 +925,9 @@ packages:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -816,6 +939,27 @@ packages:
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
unist-util-is@6.0.1:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
unist-util-visit-parents@6.0.2:
resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
unist-util-visit@5.1.0:
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite@8.0.16:
resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -954,6 +1098,9 @@ packages:
utf-8-validate:
optional: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
snapshots:
'@babel/helper-string-parser@7.29.7': {}
@@ -1078,6 +1225,46 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {}
'@shikijs/core@4.2.0':
dependencies:
'@shikijs/primitive': 4.2.0
'@shikijs/types': 4.2.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@4.2.0':
dependencies:
'@shikijs/types': 4.2.0
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.6
'@shikijs/engine-oniguruma@4.2.0':
dependencies:
'@shikijs/types': 4.2.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@4.2.0':
dependencies:
'@shikijs/types': 4.2.0
'@shikijs/primitive@4.2.0':
dependencies:
'@shikijs/types': 4.2.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/themes@4.2.0':
dependencies:
'@shikijs/types': 4.2.0
'@shikijs/types@4.2.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@10.0.2': {}
'@standard-schema/spec@1.1.0': {}
'@tailwindcss/node@4.3.1':
@@ -1162,16 +1349,28 @@ snapshots:
'@types/estree@1.0.9': {}
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/node@24.13.2':
dependencies:
undici-types: 7.18.2
'@types/unist@3.0.3': {}
'@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.13.2
'@ungap/structured-clone@1.3.1': {}
'@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0))(vue@3.5.38(typescript@6.0.3))':
dependencies:
'@rolldown/pluginutils': 1.0.1
@@ -1335,14 +1534,22 @@ snapshots:
dependencies:
'@types/node': 24.13.2
ccount@2.0.1: {}
chai@6.2.2: {}
character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
comma-separated-tokens@2.0.3: {}
commander@10.0.1: {}
config-chain@1.1.13:
@@ -1362,8 +1569,14 @@ snapshots:
daisyui@5.5.23: {}
dequal@2.0.3: {}
detect-libc@2.1.2: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
eastasianwidth@0.2.0: {}
editorconfig@1.0.7:
@@ -1430,6 +1643,26 @@ snapshots:
- bufferutil
- utf-8-validate
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
comma-separated-tokens: 2.0.3
hast-util-whitespace: 3.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.1
property-information: 7.2.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
html-void-elements@3.0.0: {}
ini@1.3.8: {}
is-fullwidth-code-point@3.0.0: {}
@@ -1509,6 +1742,35 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mdast-util-to-hast@13.2.1:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@ungap/structured-clone': 1.3.1
devlop: 1.1.0
micromark-util-sanitize-uri: 2.0.1
trim-lines: 3.0.1
unist-util-position: 5.0.0
unist-util-visit: 5.1.0
vfile: 6.0.3
micromark-util-character@2.1.1:
dependencies:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-util-encode@2.0.1: {}
micromark-util-sanitize-uri@2.0.1:
dependencies:
micromark-util-character: 2.1.1
micromark-util-encode: 2.0.1
micromark-util-symbol: 2.0.1
micromark-util-symbol@2.0.1: {}
micromark-util-types@2.0.2: {}
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.1
@@ -1525,6 +1787,14 @@ snapshots:
obug@2.1.3: {}
oniguruma-parser@0.12.2: {}
oniguruma-to-es@4.3.6:
dependencies:
oniguruma-parser: 0.12.2
regex: 6.1.0
regex-recursion: 6.0.2
package-json-from-dist@1.0.1: {}
path-browserify@1.0.1: {}
@@ -1548,8 +1818,20 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
property-information@7.2.0: {}
proto-list@1.2.4: {}
regex-recursion@6.0.2:
dependencies:
regex-utilities: 2.3.0
regex-utilities@2.3.0: {}
regex@6.1.0:
dependencies:
regex-utilities: 2.3.0
rolldown@1.0.3:
dependencies:
'@oxc-project/types': 0.133.0
@@ -1579,6 +1861,17 @@ snapshots:
shebang-regex@3.0.0: {}
shiki@4.2.0:
dependencies:
'@shikijs/core': 4.2.0
'@shikijs/engine-javascript': 4.2.0
'@shikijs/engine-oniguruma': 4.2.0
'@shikijs/langs': 4.2.0
'@shikijs/themes': 4.2.0
'@shikijs/types': 4.2.0
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
@@ -1587,6 +1880,8 @@ snapshots:
source-map-js@1.2.1: {}
space-separated-tokens@2.0.2: {}
stackback@0.0.2: {}
std-env@4.1.0: {}
@@ -1603,6 +1898,11 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.2.0
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -1626,6 +1926,8 @@ snapshots:
tinyrainbow@3.1.0: {}
trim-lines@3.0.1: {}
tslib@2.8.1:
optional: true
@@ -1633,6 +1935,39 @@ snapshots:
undici-types@7.18.2: {}
unist-util-is@6.0.1:
dependencies:
'@types/unist': 3.0.3
unist-util-position@5.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-stringify-position@4.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-visit-parents@6.0.2:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.1
unist-util-visit@5.1.0:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
vfile-message@4.0.3:
dependencies:
'@types/unist': 3.0.3
unist-util-stringify-position: 4.0.0
vfile@6.0.3:
dependencies:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0):
dependencies:
lightningcss: 1.32.0
@@ -1717,3 +2052,5 @@ snapshots:
strip-ansi: 7.2.0
ws@8.21.0: {}
zwitch@2.0.4: {}

View File

@@ -21,7 +21,7 @@ const { source, plan, error, resetToSample } = useMacroplan()
<main class="flex min-h-0 flex-1 flex-col md:flex-row">
<section
class="flex min-h-0 flex-col border-base-300 max-md:h-2/5 max-md:border-b md:w-2/5 md:max-w-xl md:border-r"
class="flex min-h-0 flex-col border-base-300 max-md:h-2/5 max-md:border-b md:w-1/3 md:max-w-md md:border-r"
>
<div class="px-4 py-2 text-xs font-semibold uppercase tracking-wide text-base-content/50">
Source (TOML)

View File

@@ -78,7 +78,9 @@ function markerAt(row: FeatureRow, w: WeekId): MarkerKind | null {
}
interface Cell {
inBar: boolean
// how far the bar line runs within this cell: none, half-from-center-right,
// half-from-left-to-center, or full width
line: 'none' | 'right' | 'left' | 'full'
isStart: boolean
glyph: string
glyphCls: string
@@ -90,8 +92,16 @@ const matrix = computed<Cell[][]>(() =>
weeks.value.map((w) => {
const m = markerAt(row, w)
const inBar = w >= row.startWeek && w <= row.barEndWeek
let line: Cell['line'] = 'none'
if (inBar) {
const isStart = w === row.startWeek
const isEnd = w === row.barEndWeek
// line runs center→edge so it begins and ends at a glyph centre,
// never stubbing past the first/last symbol
line = isStart && isEnd ? 'none' : isStart ? 'right' : isEnd ? 'left' : 'full'
}
return {
inBar,
line,
isStart: inBar && w === row.startWeek,
glyph: m ? GLYPH[m] : '',
glyphCls: m ? MARKER_CLASS[m] : '',
@@ -157,7 +167,7 @@ function milestoneTitle(ms: Plan['milestones']): string {
class="cell"
:class="colClass(ci)"
>
<i v-if="cell.inBar" class="bar" :class="TONE_TEXT[tone(row)]"></i>
<i v-if="cell.line !== 'none'" class="bar" :class="[cell.line, TONE_TEXT[tone(row)]]"></i>
<i v-if="cell.isStart" class="riser" :class="TONE_TEXT[tone(row)]"></i>
<span v-if="cell.glyph" class="glyph" :class="cell.glyphCls">{{ cell.glyph }}</span>
</div>
@@ -178,18 +188,18 @@ function milestoneTitle(ms: Plan['milestones']): string {
<!-- legend -->
<div class="legend">
<span><b class="text-base-content/50"></b> original estimate</span>
<span><b class="text-warning"></b> re-estimate (slip)</span>
<span><b class="text-warning"></b> re-estimate</span>
<span><b class="text-success"></b> on time</span>
<span><b class="text-error"></b> late</span>
<span><b></b> feature bar</span>
<span><b class="text-error"></b> milestone</span>
<span>vertical rule = now</span>
<span><b class="now-swatch"></b> today</span>
</div>
</template>
<style scoped>
.plan-grid {
--wk: 2.5rem;
--wk: 3.5rem;
display: grid;
width: max-content;
min-width: 100%;
@@ -219,6 +229,7 @@ function milestoneTitle(ms: Plan['milestones']): string {
flex-direction: column;
align-items: center;
line-height: 1.15;
padding-inline: 0.15rem;
}
.wklabel {
font-variant-numeric: tabular-nums;
@@ -271,20 +282,31 @@ function milestoneTitle(ms: Plan['milestones']): string {
}
.bar {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
transform: translateY(-50%);
background: currentColor;
opacity: 0.55;
}
.bar.full {
left: 0;
right: 0;
}
.bar.left {
left: 0;
right: 50%;
} /* enters from the left, stops at the glyph */
.bar.right {
left: 50%;
right: 0;
} /* starts at the first glyph, runs right */
.riser {
position: absolute;
left: 0;
left: 50%;
top: 28%;
bottom: 28%;
width: 2px;
transform: translateX(-50%);
background: currentColor;
opacity: 0.55;
}
@@ -293,6 +315,10 @@ function milestoneTitle(ms: Plan['milestones']): string {
z-index: 1;
font-size: 0.95rem;
line-height: 1;
padding: 0 0.18rem;
/* halo so the bar reads as passing behind, not through, the symbol */
background: var(--color-base-100);
border-radius: 0.3rem;
}
.learncell {
@@ -320,8 +346,12 @@ function milestoneTitle(ms: Plan['milestones']): string {
flex: none;
}
.col-now {
border-left: 2px solid var(--color-primary);
.cell.col-now {
background: color-mix(in oklch, var(--color-primary) 9%, var(--color-base-100));
}
.cell.col-now .glyph {
/* match the column tint so the halo doesn't punch a hole in it */
background: color-mix(in oklch, var(--color-primary) 9%, var(--color-base-100));
}
.col-ms {
border-left: 2px dashed color-mix(in oklch, var(--color-base-content) 35%, transparent);
@@ -338,4 +368,13 @@ function milestoneTitle(ms: Plan['milestones']): string {
.legend b {
font-weight: 700;
}
.now-swatch {
display: inline-block;
width: 0.8rem;
height: 0.8rem;
vertical-align: -0.1rem;
border-radius: 0.15rem;
background: color-mix(in oklch, var(--color-primary) 9%, var(--color-base-100));
border: 1px solid color-mix(in oklch, var(--color-primary) 30%, var(--color-base-100));
}
</style>

View File

@@ -1,23 +1,70 @@
<script setup lang="ts">
defineProps<{ modelValue: string; error: string | null }>()
import { ref, computed, onMounted } from 'vue'
import type { HighlighterCore } from 'shiki/core'
const props = defineProps<{ modelValue: string; error: string | null }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const textarea = ref<HTMLTextAreaElement>()
const backdrop = ref<HTMLElement>()
const highlighter = ref<HighlighterCore>()
onMounted(async () => {
// Lazy-load Shiki as its own chunk so it stays out of the initial bundle.
// Fine-grained: only the TOML grammar + one light theme, JS engine (no WASM).
const [core, engine, toml, theme] = await Promise.all([
import('shiki/core'),
import('shiki/engine/javascript'),
import('shiki/langs/toml.mjs'),
import('shiki/themes/github-light.mjs'),
])
highlighter.value = await core.createHighlighterCore({
themes: [theme.default],
langs: [toml.default],
engine: engine.createJavaScriptRegexEngine(),
})
})
const highlighted = computed(() => {
// a trailing newline needs a trailing char so the last backdrop line keeps height
const code = props.modelValue.endsWith('\n') ? props.modelValue + ' ' : props.modelValue
const hl = highlighter.value
if (!hl) return `<pre class="shiki"><code>${escapeHtml(code)}</code></pre>`
return hl.codeToHtml(code, { lang: 'toml', theme: 'github-light' })
})
function escapeHtml(s: string): string {
return s.replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]!)
}
function onInput(e: Event) {
emit('update:modelValue', (e.target as HTMLTextAreaElement).value)
}
function syncScroll() {
if (!textarea.value || !backdrop.value) return
backdrop.value.scrollTop = textarea.value.scrollTop
backdrop.value.scrollLeft = textarea.value.scrollLeft
}
</script>
<template>
<div class="editor">
<textarea
class="editor-area"
:value="modelValue"
spellcheck="false"
autocapitalize="off"
autocorrect="off"
aria-label="Macroplan TOML source"
@input="onInput"
></textarea>
<div class="code">
<div ref="backdrop" class="backdrop" aria-hidden="true" v-html="highlighted" />
<textarea
ref="textarea"
class="input"
:value="modelValue"
spellcheck="false"
autocapitalize="off"
autocorrect="off"
autocomplete="off"
aria-label="Macroplan TOML source"
@input="onInput"
@scroll="syncScroll"
></textarea>
</div>
<div v-if="error" class="editor-error" role="alert">
<span class="font-semibold">Cant parse:</span> {{ error }}
</div>
@@ -32,15 +79,20 @@ function onInput(e: Event) {
height: 100%;
min-height: 0;
}
.editor-area {
.code {
position: relative;
flex: 1;
width: 100%;
resize: none;
border: none;
outline: none;
min-height: 0;
}
/* Backdrop (highlighted) and textarea (input) share identical box metrics so the
transparent input text sits exactly over its colored Shiki copy. */
.backdrop,
.input {
position: absolute;
inset: 0;
margin: 0;
border: 0;
padding: 0.9rem 1rem;
background: var(--color-base-100);
color: var(--color-base-content);
font-family: inherit; /* Fira Code, from the global theme */
font-size: 0.8rem;
line-height: 1.6;
@@ -48,9 +100,36 @@ function onInput(e: Event) {
white-space: pre;
overflow: auto;
}
.backdrop {
z-index: 0;
pointer-events: none;
overflow: hidden; /* scroll is driven by the textarea via syncScroll */
background: var(--color-base-100);
color: var(--color-base-content);
}
/* Neutralise Shiki's own <pre> chrome — the .backdrop provides padding/metrics. */
.backdrop :deep(pre.shiki) {
margin: 0;
padding: 0;
background: transparent !important;
white-space: pre;
}
.backdrop :deep(pre.shiki),
.backdrop :deep(pre.shiki code) {
font: inherit;
}
.input {
z-index: 1;
resize: none;
outline: none;
background: transparent;
color: transparent;
caret-color: var(--color-base-content);
}
.input::selection {
background: color-mix(in oklch, var(--color-primary) 28%, transparent);
}
.editor-error {
position: sticky;
bottom: 0;
padding: 0.6rem 1rem;
font-size: 0.74rem;
background: var(--color-error);

View File

@@ -4,7 +4,7 @@
export const SAMPLE_PLAN = `title = "Q3 — Checkout revamp"
# A Feature: start week, Original Estimate (the immovable baseline), then any
# Re-estimates (slips), an optional Delivery, and an optional Learning / Status.
# Re-estimates, an optional Delivery, and an optional Learning / Status.
# Dates are TOML date literals; any day is snapped to that week's Monday.
[[feature]]
@@ -18,7 +18,7 @@ learning = "Spiking the OAuth flow first paid off — do discovery spikes earlie
name = "Payments"
start = 2026-06-01
original = 2026-06-15 # ◯ baseline
reestimates = [2026-06-29, 2026-07-13] # two slips → △ △
reestimates = [2026-06-29, 2026-07-13] # two re-estimates → △ △
delivered = 2026-07-20 # after the baseline → ▲ late
learning = "Vendor lead time was the real constraint — derisk vendors up front."
@@ -33,7 +33,7 @@ note = "No recovery plan yet — needs an owner."
name = "Search"
start = 2026-06-08
original = 2026-06-22
reestimates = [2026-07-06] # slipped once, still in flight → △
reestimates = [2026-07-06] # re-estimated once, still in flight → △
status = "orange"
note = "Third-party search API is flaky; spike a fallback."

View File

@@ -85,6 +85,29 @@ describe('F2 — on-time / late classification (ADR-0001)', () => {
})
})
describe('bar extent relative to now', () => {
const feat = (extra: string) =>
`[[feature]]\nname = "X"\nstart = 2026-06-01\noriginal = 2026-06-08\n${extra}\n`
it('extends an overdue undelivered bar to the now week, keeping ◯ at the estimate', () => {
// original 2026-06-08 is before now (week of 2026-06-15) and undelivered → overdue
const r = rowOf(feat('status = "red"'), 'X')
expect(r.delivered).toBe(false)
expect(r.barEndWeek).toBe('2026-06-15') // runs up to now, past the 06-08 estimate
expect(r.markers.find((m) => m.kind === 'original')?.week).toBe('2026-06-08')
})
it('does not extend a delivered bar past its delivery, even when now is later', () => {
const r = rowOf(feat('delivered = 2026-06-08'), 'X')
expect(r.barEndWeek).toBe('2026-06-08') // ends at delivery, not at the now week
})
it('ends an on-track undelivered bar at the future estimate (no extension back to now)', () => {
const r = rowOf('[[feature]]\nname="X"\nstart=2026-06-22\noriginal=2026-07-06\n', 'X')
expect(r.barEndWeek).toBe('2026-07-06')
})
})
describe('plan derivation', () => {
it('derives a contiguous week range and places the now line', () => {
const plan = buildPlan(parseMacroplan(SAMPLE_PLAN), TODAY)

View File

@@ -8,8 +8,8 @@ import type { RawPlan, RawFeature, Plan, FeatureRow, Marker, MilestoneLine } fro
* Re-estimate (ADR-0001). The full slip history (`◯` + every `△`) is preserved.
*/
export function buildPlan(raw: RawPlan, today: Date | string = new Date()): Plan {
const rows = raw.features.map(buildRow)
const nowWeek = mondayOf(today)
const rows = raw.features.map((f) => buildRow(f, nowWeek))
const milestones = raw.milestones.map((m): MilestoneLine => {
const week = mondayOf(m.week)
@@ -43,7 +43,7 @@ export function buildPlan(raw: RawPlan, today: Date | string = new Date()): Plan
return { title: raw.title, weeks, rows, milestones, nowWeek, nowInRange }
}
function buildRow(f: RawFeature): FeatureRow {
function buildRow(f: RawFeature, nowWeek: WeekId): FeatureRow {
const startWeek = mondayOf(f.start)
const originalWeek = mondayOf(f.original)
const deliveredWeek = f.delivered ? mondayOf(f.delivered) : undefined
@@ -62,7 +62,11 @@ function buildRow(f: RawFeature): FeatureRow {
markers.push({ week: originalWeek, kind: 'original' })
}
const barEndWeek = [startWeek, ...markers.map((m) => m.week)].reduce((a, b) => (a > b ? a : b))
const intrinsicEnd = [startWeek, ...markers.map((m) => m.week)].reduce((a, b) => (a > b ? a : b))
// A delivered Feature's bar ends at its delivery. An undelivered Feature that
// is already past its furthest estimate keeps "running" up to now (overdue),
// so its bar extends to the now week; otherwise it ends at the last marker.
const barEndWeek = delivered || intrinsicEnd > nowWeek ? intrinsicEnd : nowWeek
return {
name: f.name,