Compare commits
6 Commits
fbeffccf93
...
7fcae34c60
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fcae34c60 | ||
|
|
1535c829ca | ||
|
|
c62bdde727 | ||
|
|
a7a4df52a9 | ||
|
|
c1d72184eb | ||
|
|
2c0e9c360f |
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|||||||
@@ -6,20 +6,25 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"daisyui": "^5.5.23",
|
"daisyui": "^5.5.23",
|
||||||
|
"smol-toml": "^1.6.1",
|
||||||
"vue": "^3.5.34"
|
"vue": "^3.5.34"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
|
"@vue/test-utils": "^2.4.11",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"happy-dom": "^20.10.4",
|
||||||
"tailwindcss": "^4.3.1",
|
"tailwindcss": "^4.3.1",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
|
"vitest": "^4.1.9",
|
||||||
"vue-tsc": "^3.2.8"
|
"vue-tsc": "^3.2.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
693
pnpm-lock.yaml
generated
693
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
daisyui:
|
daisyui:
|
||||||
specifier: ^5.5.23
|
specifier: ^5.5.23
|
||||||
version: 5.5.23
|
version: 5.5.23
|
||||||
|
smol-toml:
|
||||||
|
specifier: ^1.6.1
|
||||||
|
version: 1.6.1
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.34
|
specifier: ^3.5.34
|
||||||
version: 3.5.38(typescript@6.0.3)
|
version: 3.5.38(typescript@6.0.3)
|
||||||
@@ -24,9 +27,15 @@ importers:
|
|||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.6
|
specifier: ^6.0.6
|
||||||
version: 6.0.7(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0))(vue@3.5.38(typescript@6.0.3))
|
version: 6.0.7(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0))(vue@3.5.38(typescript@6.0.3))
|
||||||
|
'@vue/test-utils':
|
||||||
|
specifier: ^2.4.11
|
||||||
|
version: 2.4.11(@vue/compiler-dom@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))
|
||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: ^0.9.1
|
specifier: ^0.9.1
|
||||||
version: 0.9.1(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
version: 0.9.1(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||||
|
happy-dom:
|
||||||
|
specifier: ^20.10.4
|
||||||
|
version: 20.10.4
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.3.1
|
version: 4.3.1
|
||||||
@@ -36,6 +45,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.12
|
specifier: ^8.0.12
|
||||||
version: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)
|
version: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.1.9
|
||||||
|
version: 4.1.9(@types/node@24.13.2)(happy-dom@20.10.4)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0))
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.2.8
|
specifier: ^3.2.8
|
||||||
version: 3.3.5(typescript@6.0.3)
|
version: 3.3.5(typescript@6.0.3)
|
||||||
@@ -68,6 +80,10 @@ packages:
|
|||||||
'@emnapi/wasi-threads@1.2.1':
|
'@emnapi/wasi-threads@1.2.1':
|
||||||
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
|
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
@@ -90,9 +106,16 @@ packages:
|
|||||||
'@emnapi/core': ^1.7.1
|
'@emnapi/core': ^1.7.1
|
||||||
'@emnapi/runtime': ^1.7.1
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
|
'@one-ini/wasm@0.1.1':
|
||||||
|
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||||
|
|
||||||
'@oxc-project/types@0.133.0':
|
'@oxc-project/types@0.133.0':
|
||||||
resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
|
resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
|
||||||
|
|
||||||
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.3':
|
'@rolldown/binding-android-arm64@1.0.3':
|
||||||
resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==}
|
resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -191,6 +214,9 @@ packages:
|
|||||||
'@rolldown/pluginutils@1.0.1':
|
'@rolldown/pluginutils@1.0.1':
|
||||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.1':
|
'@tailwindcss/node@4.3.1':
|
||||||
resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==}
|
resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==}
|
||||||
|
|
||||||
@@ -288,9 +314,24 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2':
|
||||||
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
|
'@types/estree@1.0.9':
|
||||||
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
'@types/node@24.13.2':
|
'@types/node@24.13.2':
|
||||||
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
|
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
|
||||||
|
|
||||||
|
'@types/whatwg-mimetype@3.0.2':
|
||||||
|
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.7':
|
'@vitejs/plugin-vue@6.0.7':
|
||||||
resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==}
|
resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -298,6 +339,35 @@ packages:
|
|||||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
vue: ^3.2.25
|
vue: ^3.2.25
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.9':
|
||||||
|
resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.9':
|
||||||
|
resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.9':
|
||||||
|
resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.9':
|
||||||
|
resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.9':
|
||||||
|
resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.9':
|
||||||
|
resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.9':
|
||||||
|
resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
|
||||||
|
|
||||||
'@volar/language-core@2.4.28':
|
'@volar/language-core@2.4.28':
|
||||||
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
|
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
|
||||||
|
|
||||||
@@ -339,6 +409,16 @@ packages:
|
|||||||
'@vue/shared@3.5.38':
|
'@vue/shared@3.5.38':
|
||||||
resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==}
|
resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==}
|
||||||
|
|
||||||
|
'@vue/test-utils@2.4.11':
|
||||||
|
resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/compiler-dom': 3.x
|
||||||
|
'@vue/server-renderer': 3.x
|
||||||
|
vue: 3.x
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/server-renderer':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vue/tsconfig@0.9.1':
|
'@vue/tsconfig@0.9.1':
|
||||||
resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==}
|
resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -350,9 +430,68 @@ packages:
|
|||||||
vue:
|
vue:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
abbrev@2.0.0:
|
||||||
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
alien-signals@3.2.1:
|
alien-signals@3.2.1:
|
||||||
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-regex@6.2.2:
|
||||||
|
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@6.2.3:
|
||||||
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
balanced-match@1.0.2:
|
||||||
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
brace-expansion@2.1.1:
|
||||||
|
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
|
||||||
|
|
||||||
|
buffer-image-size@0.6.4:
|
||||||
|
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
chai@6.2.2:
|
||||||
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@10.0.1:
|
||||||
|
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
config-chain@1.1.13:
|
||||||
|
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
|
||||||
|
|
||||||
|
convert-source-map@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@@ -363,6 +502,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0:
|
||||||
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
editorconfig@1.0.7:
|
||||||
|
resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2:
|
||||||
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
enhanced-resolve@5.21.6:
|
enhanced-resolve@5.21.6:
|
||||||
resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
|
resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -371,9 +524,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
es-module-lexer@2.1.0:
|
||||||
|
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||||
|
|
||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
|
expect-type@1.3.0:
|
||||||
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -383,18 +546,52 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
glob@10.5.0:
|
||||||
|
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
happy-dom@20.10.4:
|
||||||
|
resolution: {integrity: sha512-bKrsQnFNpcjZG0UPsH7UMN7Oyp3AB42LXk2GuiQmu7l4QFxH7lsw5T1eWEtE2+vbIFrTC45sbNSB2pkB8MTfKA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
|
ini@1.3.8:
|
||||||
|
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
isexe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
jackspeak@3.4.3:
|
||||||
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jiti@2.7.0:
|
jiti@2.7.0:
|
||||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
js-beautify@1.15.4:
|
||||||
|
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
js-cookie@3.0.8:
|
||||||
|
resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.32.0:
|
lightningcss-android-arm64@1.32.0:
|
||||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -469,9 +666,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
lru-cache@10.4.3:
|
||||||
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
minimatch@9.0.9:
|
||||||
|
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
minipass@7.1.3:
|
||||||
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
muggle-string@0.4.1:
|
muggle-string@0.4.1:
|
||||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||||
|
|
||||||
@@ -480,9 +688,32 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nopt@7.2.1:
|
||||||
|
resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
obug@2.1.3:
|
||||||
|
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
path-key@3.1.1:
|
||||||
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-scurry@1.11.1:
|
||||||
|
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.18'}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -494,15 +725,64 @@ packages:
|
|||||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
proto-list@1.2.4:
|
||||||
|
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
||||||
|
|
||||||
rolldown@1.0.3:
|
rolldown@1.0.3:
|
||||||
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
|
resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.8.4:
|
||||||
|
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shebang-regex@3.0.0:
|
||||||
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
|
signal-exit@4.1.0:
|
||||||
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
smol-toml@1.6.1:
|
||||||
|
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
|
std-env@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@7.2.0:
|
||||||
|
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
tailwindcss@4.3.1:
|
tailwindcss@4.3.1:
|
||||||
resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==}
|
resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==}
|
||||||
|
|
||||||
@@ -510,10 +790,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@1.2.4:
|
||||||
|
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0:
|
||||||
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -568,9 +859,53 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@4.1.9:
|
||||||
|
resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==}
|
||||||
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
|
'@vitest/browser-playwright': 4.1.9
|
||||||
|
'@vitest/browser-preview': 4.1.9
|
||||||
|
'@vitest/browser-webdriverio': 4.1.9
|
||||||
|
'@vitest/coverage-istanbul': 4.1.9
|
||||||
|
'@vitest/coverage-v8': 4.1.9
|
||||||
|
'@vitest/ui': 4.1.9
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-playwright':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-preview':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-webdriverio':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-istanbul':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vscode-uri@3.1.0:
|
vscode-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||||
|
|
||||||
|
vue-component-type-helpers@3.3.5:
|
||||||
|
resolution: {integrity: sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==}
|
||||||
|
|
||||||
vue-tsc@3.3.5:
|
vue-tsc@3.3.5:
|
||||||
resolution: {integrity: sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==}
|
resolution: {integrity: sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -585,6 +920,40 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
whatwg-mimetype@3.0.0:
|
||||||
|
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ws@8.21.0:
|
||||||
|
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7': {}
|
'@babel/helper-string-parser@7.29.7': {}
|
||||||
@@ -616,6 +985,15 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
dependencies:
|
||||||
|
string-width: 5.1.2
|
||||||
|
string-width-cjs: string-width@4.2.3
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
strip-ansi-cjs: strip-ansi@6.0.1
|
||||||
|
wrap-ansi: 8.1.0
|
||||||
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -642,8 +1020,13 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.2
|
'@tybys/wasm-util': 0.10.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@one-ini/wasm@0.1.1': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.133.0': {}
|
'@oxc-project/types@0.133.0': {}
|
||||||
|
|
||||||
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.3':
|
'@rolldown/binding-android-arm64@1.0.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -695,6 +1078,8 @@ snapshots:
|
|||||||
|
|
||||||
'@rolldown/pluginutils@1.0.1': {}
|
'@rolldown/pluginutils@1.0.1': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@tailwindcss/node@4.3.1':
|
'@tailwindcss/node@4.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -768,16 +1153,72 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/deep-eql': 4.0.2
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
'@types/node@24.13.2':
|
'@types/node@24.13.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
|
'@types/whatwg-mimetype@3.0.2': {}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
|
||||||
'@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))':
|
'@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:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)
|
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)
|
||||||
vue: 3.5.38(typescript@6.0.3)
|
vue: 3.5.38(typescript@6.0.3)
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 4.1.9
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
chai: 6.2.2
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.9(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 4.1.9
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.9
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.9': {}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.9
|
||||||
|
convert-source-map: 2.0.0
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@volar/language-core@2.4.28':
|
'@volar/language-core@2.4.28':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/source-map': 2.4.28
|
'@volar/source-map': 2.4.28
|
||||||
@@ -854,19 +1295,88 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/shared@3.5.38': {}
|
'@vue/shared@3.5.38': {}
|
||||||
|
|
||||||
|
'@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.5.38
|
||||||
|
js-beautify: 1.15.4
|
||||||
|
vue: 3.5.38(typescript@6.0.3)
|
||||||
|
vue-component-type-helpers: 3.3.5
|
||||||
|
optionalDependencies:
|
||||||
|
'@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@6.0.3))
|
||||||
|
|
||||||
'@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
|
'@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
vue: 3.5.38(typescript@6.0.3)
|
vue: 3.5.38(typescript@6.0.3)
|
||||||
|
|
||||||
|
abbrev@2.0.0: {}
|
||||||
|
|
||||||
alien-signals@3.2.1: {}
|
alien-signals@3.2.1: {}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-regex@6.2.2: {}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
brace-expansion@2.1.1:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
buffer-image-size@0.6.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
|
||||||
|
chai@6.2.2: {}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@10.0.1: {}
|
||||||
|
|
||||||
|
config-chain@1.1.13:
|
||||||
|
dependencies:
|
||||||
|
ini: 1.3.8
|
||||||
|
proto-list: 1.2.4
|
||||||
|
|
||||||
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
path-key: 3.1.1
|
||||||
|
shebang-command: 2.0.0
|
||||||
|
which: 2.0.2
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
daisyui@5.5.23: {}
|
daisyui@5.5.23: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
editorconfig@1.0.7:
|
||||||
|
dependencies:
|
||||||
|
'@one-ini/wasm': 0.1.1
|
||||||
|
commander: 10.0.1
|
||||||
|
minimatch: 9.0.9
|
||||||
|
semver: 7.8.4
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
enhanced-resolve@5.21.6:
|
enhanced-resolve@5.21.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -874,19 +1384,76 @@ snapshots:
|
|||||||
|
|
||||||
entities@7.0.1: {}
|
entities@7.0.1: {}
|
||||||
|
|
||||||
|
es-module-lexer@2.1.0: {}
|
||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
glob@10.5.0:
|
||||||
|
dependencies:
|
||||||
|
foreground-child: 3.3.1
|
||||||
|
jackspeak: 3.4.3
|
||||||
|
minimatch: 9.0.9
|
||||||
|
minipass: 7.1.3
|
||||||
|
package-json-from-dist: 1.0.1
|
||||||
|
path-scurry: 1.11.1
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
happy-dom@20.10.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
'@types/whatwg-mimetype': 3.0.2
|
||||||
|
'@types/ws': 8.18.1
|
||||||
|
buffer-image-size: 0.6.4
|
||||||
|
entities: 7.0.1
|
||||||
|
whatwg-mimetype: 3.0.0
|
||||||
|
ws: 8.21.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
ini@1.3.8: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
jackspeak@3.4.3:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/cliui': 8.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jiti@2.7.0: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
|
js-beautify@1.15.4:
|
||||||
|
dependencies:
|
||||||
|
config-chain: 1.1.13
|
||||||
|
editorconfig: 1.0.7
|
||||||
|
glob: 10.5.0
|
||||||
|
js-cookie: 3.0.8
|
||||||
|
nopt: 7.2.1
|
||||||
|
|
||||||
|
js-cookie@3.0.8: {}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.32.0:
|
lightningcss-android-arm64@1.32.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -936,16 +1503,41 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.32.0
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
lightningcss-win32-x64-msvc: 1.32.0
|
lightningcss-win32-x64-msvc: 1.32.0
|
||||||
|
|
||||||
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
minimatch@9.0.9:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 2.1.1
|
||||||
|
|
||||||
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
nanoid@3.3.12: {}
|
nanoid@3.3.12: {}
|
||||||
|
|
||||||
|
nopt@7.2.1:
|
||||||
|
dependencies:
|
||||||
|
abbrev: 2.0.0
|
||||||
|
|
||||||
|
obug@2.1.3: {}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
path-scurry@1.11.1:
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 10.4.3
|
||||||
|
minipass: 7.1.3
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.4: {}
|
picomatch@4.0.4: {}
|
||||||
@@ -956,6 +1548,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
proto-list@1.2.4: {}
|
||||||
|
|
||||||
rolldown@1.0.3:
|
rolldown@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.133.0
|
'@oxc-project/types': 0.133.0
|
||||||
@@ -977,17 +1571,61 @@ snapshots:
|
|||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
'@rolldown/binding-win32-arm64-msvc': 1.0.3
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
'@rolldown/binding-win32-x64-msvc': 1.0.3
|
||||||
|
|
||||||
|
semver@7.8.4: {}
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
shebang-regex: 3.0.0
|
||||||
|
|
||||||
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
smol-toml@1.6.1: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
|
std-env@4.1.0: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
eastasianwidth: 0.2.0
|
||||||
|
emoji-regex: 9.2.2
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
strip-ansi@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 6.2.2
|
||||||
|
|
||||||
tailwindcss@4.3.1: {}
|
tailwindcss@4.3.1: {}
|
||||||
|
|
||||||
tapable@2.3.3: {}
|
tapable@2.3.3: {}
|
||||||
|
|
||||||
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@1.2.4: {}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1007,8 +1645,38 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
|
|
||||||
|
vitest@4.1.9(@types/node@24.13.2)(happy-dom@20.10.4)(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0)):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 4.1.9
|
||||||
|
'@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@24.13.2)(jiti@2.7.0))
|
||||||
|
'@vitest/pretty-format': 4.1.9
|
||||||
|
'@vitest/runner': 4.1.9
|
||||||
|
'@vitest/snapshot': 4.1.9
|
||||||
|
'@vitest/spy': 4.1.9
|
||||||
|
'@vitest/utils': 4.1.9
|
||||||
|
es-module-lexer: 2.1.0
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.3
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
std-env: 4.1.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.2.4
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
vite: 8.0.16(@types/node@24.13.2)(jiti@2.7.0)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.13.2
|
||||||
|
happy-dom: 20.10.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- msw
|
||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
|
vue-component-type-helpers@3.3.5: {}
|
||||||
|
|
||||||
vue-tsc@3.3.5(typescript@6.0.3):
|
vue-tsc@3.3.5(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/typescript': 2.4.28
|
'@volar/typescript': 2.4.28
|
||||||
@@ -1024,3 +1692,28 @@ snapshots:
|
|||||||
'@vue/shared': 3.5.38
|
'@vue/shared': 3.5.38
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
|
whatwg-mimetype@3.0.0: {}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 6.2.3
|
||||||
|
string-width: 5.1.2
|
||||||
|
strip-ansi: 7.2.0
|
||||||
|
|
||||||
|
ws@8.21.0: {}
|
||||||
|
|||||||
44
src/App.vue
44
src/App.vue
@@ -1,19 +1,39 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { useMacroplan } from './composables/useMacroplan'
|
||||||
|
import PlanEditor from './components/PlanEditor.vue'
|
||||||
|
import MacroplanGrid from './components/MacroplanGrid.vue'
|
||||||
|
|
||||||
|
const { source, plan, error, resetToSample } = useMacroplan()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="font-sans min-h-screen bg-base-200 flex items-center justify-center p-4">
|
<div class="flex h-screen flex-col bg-base-200 font-sans text-base-content">
|
||||||
<div class="card w-full max-w-md bg-base-100 shadow-xl">
|
<header class="navbar min-h-0 gap-2 border-b border-base-300 bg-base-100 px-4 py-2">
|
||||||
<div class="card-body items-center text-center gap-3">
|
<img src="/favicon.svg" alt="" class="size-6" />
|
||||||
<img src="/favicon.svg" alt="" class="size-12" />
|
<div class="flex-1">
|
||||||
<h1 class="card-title text-3xl">Macroplan</h1>
|
<h1 class="text-lg font-semibold leading-tight">Macroplan</h1>
|
||||||
<p class="text-base-content/70">
|
<p class="text-xs text-base-content/60 leading-tight">
|
||||||
A week-granular, learning-oriented view of the features a team has
|
A week-granular, learning-oriented record of what we committed to deliver.
|
||||||
committed to deliver.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="card-actions mt-2">
|
|
||||||
<button class="btn btn-primary">Get started</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" @click="resetToSample">Reset to sample</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-2 text-xs font-semibold uppercase tracking-wide text-base-content/50">
|
||||||
|
Source (TOML)
|
||||||
</div>
|
</div>
|
||||||
|
<PlanEditor v-model="source" :error="error" class="min-h-0 flex-1" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="min-h-0 flex-1 overflow-auto p-4">
|
||||||
|
<h2 v-if="plan" class="mb-3 text-sm font-semibold text-base-content/70">{{ plan.title }}</h2>
|
||||||
|
<MacroplanGrid v-if="plan" :plan="plan" />
|
||||||
|
<p v-else class="text-sm text-base-content/60">Nothing to render yet — fix the source on the left.</p>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
52
src/components/MacroplanGrid.test.ts
Normal file
52
src/components/MacroplanGrid.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import MacroplanGrid from './MacroplanGrid.vue'
|
||||||
|
import { parseMacroplan } from '../model/parse'
|
||||||
|
import { buildPlan } from '../model/plan'
|
||||||
|
import { SAMPLE_PLAN } from '../data/sample'
|
||||||
|
|
||||||
|
const plan = buildPlan(parseMacroplan(SAMPLE_PLAN), '2026-06-17')
|
||||||
|
|
||||||
|
function mountGrid() {
|
||||||
|
return mount(MacroplanGrid, { props: { plan } })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MacroplanGrid renders the sample plan', () => {
|
||||||
|
it('renders one row per feature, in order', () => {
|
||||||
|
const names = mountGrid()
|
||||||
|
.findAll('.namecell')
|
||||||
|
.map((n) => n.text())
|
||||||
|
expect(names).toEqual(['Auth', 'Payments', 'Dashboard', 'Search', 'Notifications'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the right markers per feature', () => {
|
||||||
|
const rows = mountGrid().findAll('.namecell')
|
||||||
|
// each feature row is namecell + week cells + learncell; grab the row text via the grid
|
||||||
|
const grid = mountGrid()
|
||||||
|
const text = grid.text()
|
||||||
|
expect(rows).toHaveLength(5)
|
||||||
|
// on-time, late, and slip glyphs all present somewhere in the grid
|
||||||
|
expect(text).toContain('◉') // Auth delivered on time
|
||||||
|
expect(text).toContain('▲') // Payments delivered late
|
||||||
|
expect(text).toContain('△') // re-estimates
|
||||||
|
expect(text).toContain('◯') // open original estimates
|
||||||
|
})
|
||||||
|
|
||||||
|
it('draws a now column and the milestone header', () => {
|
||||||
|
const w = mountGrid()
|
||||||
|
expect(w.find('.col-now').exists()).toBe(true)
|
||||||
|
expect(w.text()).toContain('now')
|
||||||
|
expect(w.text()).toContain('MVP go-live')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a learning for a delivered feature and a status note for an in-flight one', () => {
|
||||||
|
const text = mountGrid().text()
|
||||||
|
expect(text).toContain('Vendor lead time') // Payments learning
|
||||||
|
expect(text).toContain('No recovery plan yet') // Dashboard status note
|
||||||
|
})
|
||||||
|
|
||||||
|
it('labels week columns', () => {
|
||||||
|
expect(mountGrid().text()).toContain('Jun 15')
|
||||||
|
})
|
||||||
|
})
|
||||||
341
src/components/MacroplanGrid.vue
Normal file
341
src/components/MacroplanGrid.vue
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { Plan, FeatureRow, MarkerKind } from '../model/types'
|
||||||
|
import { weekLabel, type WeekId } from '../model/week'
|
||||||
|
|
||||||
|
const props = defineProps<{ plan: Plan }>()
|
||||||
|
|
||||||
|
type Tone = 'success' | 'warning' | 'error' | 'neutral'
|
||||||
|
|
||||||
|
const GLYPH: Record<MarkerKind, string> = {
|
||||||
|
original: '◯',
|
||||||
|
reestimate: '△',
|
||||||
|
'delivered-on-time': '◉',
|
||||||
|
'delivered-late': '▲',
|
||||||
|
}
|
||||||
|
const MARKER_CLASS: Record<MarkerKind, string> = {
|
||||||
|
original: 'text-base-content/50',
|
||||||
|
reestimate: 'text-warning',
|
||||||
|
'delivered-on-time': 'text-success',
|
||||||
|
'delivered-late': 'text-error',
|
||||||
|
}
|
||||||
|
// When two markers land on the same week, the higher rank owns the cell.
|
||||||
|
const RANK: Record<MarkerKind, number> = {
|
||||||
|
'delivered-late': 3,
|
||||||
|
'delivered-on-time': 3,
|
||||||
|
reestimate: 2,
|
||||||
|
original: 1,
|
||||||
|
}
|
||||||
|
const TONE_TEXT: Record<Tone, string> = {
|
||||||
|
success: 'text-success',
|
||||||
|
warning: 'text-warning',
|
||||||
|
error: 'text-error',
|
||||||
|
neutral: 'text-primary',
|
||||||
|
}
|
||||||
|
const TONE_BORDER: Record<Tone, string> = {
|
||||||
|
success: 'border-success',
|
||||||
|
warning: 'border-warning',
|
||||||
|
error: 'border-error',
|
||||||
|
neutral: 'border-primary',
|
||||||
|
}
|
||||||
|
const TONE_DOT: Record<Tone, string> = {
|
||||||
|
success: 'bg-success',
|
||||||
|
warning: 'bg-warning',
|
||||||
|
error: 'bg-error',
|
||||||
|
neutral: 'bg-base-300',
|
||||||
|
}
|
||||||
|
|
||||||
|
const weeks = computed(() => props.plan.weeks)
|
||||||
|
|
||||||
|
const gridStyle = computed(() => ({
|
||||||
|
gridTemplateColumns: `minmax(7rem, max-content) repeat(${weeks.value.length}, var(--wk)) minmax(9rem, 1fr)`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
function tone(row: FeatureRow): Tone {
|
||||||
|
if (row.delivered) return row.onTime ? 'success' : 'error'
|
||||||
|
if (row.status === 'green') return 'success'
|
||||||
|
if (row.status === 'orange') return 'warning'
|
||||||
|
if (row.status === 'red') return 'error'
|
||||||
|
return 'neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusWord(row: FeatureRow): string {
|
||||||
|
return row.status === 'green'
|
||||||
|
? 'on track'
|
||||||
|
: row.status === 'orange'
|
||||||
|
? 'at risk'
|
||||||
|
: row.status === 'red'
|
||||||
|
? 'blocked'
|
||||||
|
: 'in flight'
|
||||||
|
}
|
||||||
|
|
||||||
|
function markerAt(row: FeatureRow, w: WeekId): MarkerKind | null {
|
||||||
|
let best: MarkerKind | null = null
|
||||||
|
for (const m of row.markers) {
|
||||||
|
if (m.week === w && (best === null || RANK[m.kind] > RANK[best])) best = m.kind
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cell {
|
||||||
|
inBar: boolean
|
||||||
|
isStart: boolean
|
||||||
|
glyph: string
|
||||||
|
glyphCls: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// rows × weeks render matrix, computed once.
|
||||||
|
const matrix = computed<Cell[][]>(() =>
|
||||||
|
props.plan.rows.map((row) =>
|
||||||
|
weeks.value.map((w) => {
|
||||||
|
const m = markerAt(row, w)
|
||||||
|
const inBar = w >= row.startWeek && w <= row.barEndWeek
|
||||||
|
return {
|
||||||
|
inBar,
|
||||||
|
isStart: inBar && w === row.startWeek,
|
||||||
|
glyph: m ? GLYPH[m] : '',
|
||||||
|
glyphCls: m ? MARKER_CLASS[m] : '',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Per-week column metadata for headers + the now / milestone vertical rules.
|
||||||
|
const cols = computed(() =>
|
||||||
|
weeks.value.map((w) => ({
|
||||||
|
w,
|
||||||
|
label: weekLabel(w),
|
||||||
|
isNow: props.plan.nowInRange && w === props.plan.nowWeek,
|
||||||
|
milestones: props.plan.milestones.filter((m) => m.week === w),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
function colClass(i: number): string {
|
||||||
|
const c = cols.value[i]
|
||||||
|
if (c.isNow) return 'col-now'
|
||||||
|
if (c.milestones.length) return 'col-ms'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function milestoneTitle(ms: Plan['milestones']): string {
|
||||||
|
return ms
|
||||||
|
.map((m) => (m.unmet.length ? `${m.name} — unmet: ${m.unmet.join(', ')}` : `${m.name} — all met`))
|
||||||
|
.join(' · ')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="overflow-auto rounded-box border border-base-300 bg-base-100">
|
||||||
|
<div class="plan-grid" :style="gridStyle">
|
||||||
|
<!-- header row -->
|
||||||
|
<div class="hcell corner">{{ plan.title }}</div>
|
||||||
|
<div
|
||||||
|
v-for="(c, ci) in cols"
|
||||||
|
:key="'h-' + c.w"
|
||||||
|
class="hcell wkhead"
|
||||||
|
:class="[colClass(ci), { 'now-head': c.isNow }]"
|
||||||
|
>
|
||||||
|
<span class="wklabel">{{ c.label }}</span>
|
||||||
|
<span v-if="c.isNow" class="badge-now">now</span>
|
||||||
|
<span
|
||||||
|
v-else-if="c.milestones.length"
|
||||||
|
class="badge-ms"
|
||||||
|
:title="milestoneTitle(c.milestones)"
|
||||||
|
>◆ {{ c.milestones[0].name
|
||||||
|
}}<span v-if="c.milestones.some((m) => m.unmet.length)" class="text-error"> !</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="hcell learnhead">Learning / Status</div>
|
||||||
|
|
||||||
|
<!-- feature rows -->
|
||||||
|
<template v-for="(row, ri) in plan.rows" :key="row.name">
|
||||||
|
<div class="namecell border-l-4" :class="TONE_BORDER[tone(row)]">
|
||||||
|
<span class="truncate">{{ row.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(cell, ci) in matrix[ri]"
|
||||||
|
:key="row.name + '-' + ci"
|
||||||
|
class="cell"
|
||||||
|
:class="colClass(ci)"
|
||||||
|
>
|
||||||
|
<i v-if="cell.inBar" class="bar" :class="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>
|
||||||
|
<div class="learncell">
|
||||||
|
<template v-if="row.delivered">
|
||||||
|
<span v-if="row.learning" class="note">{{ row.learning }}</span>
|
||||||
|
<span v-else class="muted">{{ row.onTime ? 'delivered on time' : 'delivered late' }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="dot" :class="TONE_DOT[tone(row)]"></span>
|
||||||
|
<span class="note" :title="row.note || ''">{{ row.note || statusWord(row) }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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-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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.plan-grid {
|
||||||
|
--wk: 2.5rem;
|
||||||
|
display: grid;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hcell {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: var(--color-base-200);
|
||||||
|
border-bottom: 1px solid var(--color-base-300);
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.corner {
|
||||||
|
left: 0;
|
||||||
|
z-index: 30;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.wkhead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
.wklabel {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.now-head {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.badge-now {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.badge-ms {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
font-weight: 600;
|
||||||
|
max-width: 6rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.learnhead {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namecell {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--color-base-100);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid var(--color-base-200);
|
||||||
|
}
|
||||||
|
.namecell .truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 2rem;
|
||||||
|
border-bottom: 1px solid var(--color-base-200);
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
height: 2px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
.riser {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 28%;
|
||||||
|
bottom: 28%;
|
||||||
|
width: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
.glyph {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.learncell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
border-bottom: 1px solid var(--color-base-200);
|
||||||
|
}
|
||||||
|
.learncell .muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.learncell .note {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-now {
|
||||||
|
border-left: 2px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
.col-ms {
|
||||||
|
border-left: 2px dashed color-mix(in oklch, var(--color-base-content) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem 1.25rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.legend b {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
src/components/PlanEditor.vue
Normal file
60
src/components/PlanEditor.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{ modelValue: string; error: string | null }>()
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||||
|
|
||||||
|
function onInput(e: Event) {
|
||||||
|
emit('update:modelValue', (e.target as HTMLTextAreaElement).value)
|
||||||
|
}
|
||||||
|
</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 v-if="error" class="editor-error" role="alert">
|
||||||
|
<span class="font-semibold">Can’t parse:</span> {{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.editor-area {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
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;
|
||||||
|
tab-size: 2;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.editor-error {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
background: var(--color-error);
|
||||||
|
color: var(--color-error-content);
|
||||||
|
border-top: 1px solid color-mix(in oklch, black 12%, var(--color-error));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/composables/useMacroplan.ts
Normal file
50
src/composables/useMacroplan.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { parseMacroplan } from '../model/parse'
|
||||||
|
import { buildPlan } from '../model/plan'
|
||||||
|
import type { Plan } from '../model/types'
|
||||||
|
import { SAMPLE_PLAN } from '../data/sample'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'macroplan:source'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authoring state for a single Macroplan: the TOML source (autosaved to
|
||||||
|
* localStorage), the parsed Plan, and the current parse error. The last
|
||||||
|
* successfully-parsed Plan keeps rendering through transient typos (F3).
|
||||||
|
*/
|
||||||
|
export function useMacroplan() {
|
||||||
|
const source = ref(localStorage.getItem(STORAGE_KEY) ?? SAMPLE_PLAN)
|
||||||
|
const lastGood = ref<Plan | null>(null)
|
||||||
|
|
||||||
|
const parsed = computed<{ plan: Plan | null; error: string | null }>(() => {
|
||||||
|
try {
|
||||||
|
return { plan: buildPlan(parseMacroplan(source.value)), error: null }
|
||||||
|
} catch (e) {
|
||||||
|
return { plan: null, error: e instanceof Error ? e.message : String(e) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
parsed,
|
||||||
|
(p) => {
|
||||||
|
if (p.plan) lastGood.value = p.plan
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(source, (v) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, v)
|
||||||
|
} catch {
|
||||||
|
/* localStorage may be full or blocked — autosave is best-effort */
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
plan: computed(() => parsed.value.plan ?? lastGood.value),
|
||||||
|
error: computed(() => parsed.value.error),
|
||||||
|
resetToSample: () => {
|
||||||
|
source.value = SAMPLE_PLAN
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/data/sample.ts
Normal file
50
src/data/sample.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Default Macroplan shown on first load — exercises every state:
|
||||||
|
// on-time delivery, late delivery with slips, in-flight (green/orange/red),
|
||||||
|
// an overdue Feature, and a Milestone with unmet required Features.
|
||||||
|
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.
|
||||||
|
# Dates are TOML date literals; any day is snapped to that week's Monday.
|
||||||
|
|
||||||
|
[[feature]]
|
||||||
|
name = "Auth"
|
||||||
|
start = 2026-06-01
|
||||||
|
original = 2026-06-15
|
||||||
|
delivered = 2026-06-15 # on time → ◉
|
||||||
|
learning = "Spiking the OAuth flow first paid off — do discovery spikes earlier."
|
||||||
|
|
||||||
|
[[feature]]
|
||||||
|
name = "Payments"
|
||||||
|
start = 2026-06-01
|
||||||
|
original = 2026-06-15 # ◯ baseline
|
||||||
|
reestimates = [2026-06-29, 2026-07-13] # two slips → △ △
|
||||||
|
delivered = 2026-07-20 # after the baseline → ▲ late
|
||||||
|
learning = "Vendor lead time was the real constraint — derisk vendors up front."
|
||||||
|
|
||||||
|
[[feature]]
|
||||||
|
name = "Dashboard"
|
||||||
|
start = 2026-06-01
|
||||||
|
original = 2026-06-08 # already past 'now' and undelivered → overdue
|
||||||
|
status = "red"
|
||||||
|
note = "No recovery plan yet — needs an owner."
|
||||||
|
|
||||||
|
[[feature]]
|
||||||
|
name = "Search"
|
||||||
|
start = 2026-06-08
|
||||||
|
original = 2026-06-22
|
||||||
|
reestimates = [2026-07-06] # slipped once, still in flight → △
|
||||||
|
status = "orange"
|
||||||
|
note = "Third-party search API is flaky; spike a fallback."
|
||||||
|
|
||||||
|
[[feature]]
|
||||||
|
name = "Notifications"
|
||||||
|
start = 2026-06-22
|
||||||
|
original = 2026-07-06
|
||||||
|
status = "green"
|
||||||
|
|
||||||
|
[[milestone]]
|
||||||
|
name = "MVP go-live"
|
||||||
|
week = 2026-07-06
|
||||||
|
requires = ["Auth", "Payments", "Dashboard"]
|
||||||
|
`
|
||||||
85
src/model/parse.ts
Normal file
85
src/model/parse.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { parse as parseToml } from 'smol-toml'
|
||||||
|
import { toYmd } from './week'
|
||||||
|
import type { RawPlan, RawFeature, RawMilestone, StatusLevel } from './types'
|
||||||
|
|
||||||
|
/** Thrown for any malformed source — message is safe to show the author. */
|
||||||
|
export class PlanParseError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'PlanParseError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUSES: StatusLevel[] = ['green', 'orange', 'red']
|
||||||
|
|
||||||
|
/** Parse + validate a Macroplan TOML source into the raw model. */
|
||||||
|
export function parseMacroplan(source: string): RawPlan {
|
||||||
|
let data: Record<string, unknown>
|
||||||
|
try {
|
||||||
|
data = parseToml(source) as Record<string, unknown>
|
||||||
|
} catch (e) {
|
||||||
|
throw new PlanParseError(e instanceof Error ? e.message : String(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = asBlocks(data.feature, 'feature').map(parseFeature)
|
||||||
|
const milestones = asBlocks(data.milestone, 'milestone').map(parseMilestone)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: data.title != null ? String(data.title) : 'Untitled Macroplan',
|
||||||
|
features,
|
||||||
|
milestones,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function asBlocks(value: unknown, key: string): Record<string, unknown>[] {
|
||||||
|
if (value == null) return []
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
throw new PlanParseError(`\`${key}\` must be written as [[${key}]] blocks`)
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFeature(f: Record<string, unknown>, i: number): RawFeature {
|
||||||
|
const where = f.name ? `feature "${String(f.name)}"` : `feature #${i + 1}`
|
||||||
|
if (!f.name) throw new PlanParseError(`${where}: missing \`name\``)
|
||||||
|
if (f.start == null) throw new PlanParseError(`${where}: missing \`start\` date`)
|
||||||
|
if (f.original == null) throw new PlanParseError(`${where}: missing \`original\` estimate date`)
|
||||||
|
if (f.status != null && !STATUSES.includes(f.status as StatusLevel)) {
|
||||||
|
throw new PlanParseError(`${where}: \`status\` must be one of ${STATUSES.join(', ')}`)
|
||||||
|
}
|
||||||
|
if (f.reestimates != null && !Array.isArray(f.reestimates)) {
|
||||||
|
throw new PlanParseError(`${where}: \`reestimates\` must be a list of dates`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: String(f.name),
|
||||||
|
start: toYmdOr(where, 'start', f.start),
|
||||||
|
original: toYmdOr(where, 'original', f.original),
|
||||||
|
reestimates: ((f.reestimates as unknown[]) ?? []).map((d) => toYmdOr(where, 'reestimates', d)),
|
||||||
|
delivered: f.delivered != null ? toYmdOr(where, 'delivered', f.delivered) : undefined,
|
||||||
|
learning: f.learning != null ? String(f.learning) : undefined,
|
||||||
|
status: f.status as StatusLevel | undefined,
|
||||||
|
note: f.note != null ? String(f.note) : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMilestone(m: Record<string, unknown>, i: number): RawMilestone {
|
||||||
|
const where = m.name ? `milestone "${String(m.name)}"` : `milestone #${i + 1}`
|
||||||
|
if (!m.name) throw new PlanParseError(`${where}: missing \`name\``)
|
||||||
|
if (m.week == null) throw new PlanParseError(`${where}: missing \`week\` date`)
|
||||||
|
if (m.requires != null && !Array.isArray(m.requires)) {
|
||||||
|
throw new PlanParseError(`${where}: \`requires\` must be a list of feature names`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: String(m.name),
|
||||||
|
week: toYmdOr(where, 'week', m.week),
|
||||||
|
requires: ((m.requires as unknown[]) ?? []).map(String),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toYmdOr(where: string, field: string, value: unknown): string {
|
||||||
|
try {
|
||||||
|
return toYmd(value)
|
||||||
|
} catch {
|
||||||
|
throw new PlanParseError(`${where}: \`${field}\` must be a date (e.g. 2026-06-01)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/model/plan.test.ts
Normal file
126
src/model/plan.test.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { parseMacroplan, PlanParseError } from './parse'
|
||||||
|
import { buildPlan } from './plan'
|
||||||
|
import { mondayOf, weekRange } from './week'
|
||||||
|
import type { FeatureRow } from './types'
|
||||||
|
import { SAMPLE_PLAN } from '../data/sample'
|
||||||
|
|
||||||
|
const TODAY = '2026-06-17' // a Wednesday → week of Mon 2026-06-15
|
||||||
|
|
||||||
|
function rowOf(source: string, name: string): FeatureRow {
|
||||||
|
const plan = buildPlan(parseMacroplan(source), TODAY)
|
||||||
|
const row = plan.rows.find((r) => r.name === name)
|
||||||
|
if (!row) throw new Error(`no row ${name}`)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('week math', () => {
|
||||||
|
it('snaps any day to its Monday', () => {
|
||||||
|
expect(mondayOf('2026-06-17')).toBe('2026-06-15') // Wed → Mon
|
||||||
|
expect(mondayOf('2026-06-15')).toBe('2026-06-15') // Mon → Mon
|
||||||
|
expect(mondayOf('2026-06-21')).toBe('2026-06-15') // Sun → that week's Mon
|
||||||
|
expect(mondayOf('2026-06-01')).toBe('2026-06-01') // Mon
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds an inclusive contiguous Monday range', () => {
|
||||||
|
expect(weekRange('2026-06-01', '2026-06-22')).toEqual([
|
||||||
|
'2026-06-01',
|
||||||
|
'2026-06-08',
|
||||||
|
'2026-06-15',
|
||||||
|
'2026-06-22',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('F2 — on-time / late classification (ADR-0001)', () => {
|
||||||
|
const base = (extra: string) =>
|
||||||
|
`[[feature]]\nname = "X"\nstart = 2026-06-01\noriginal = 2026-06-15\n${extra}\n`
|
||||||
|
|
||||||
|
it('delivered on the Original Estimate week → ◉, no ◯, onTime true', () => {
|
||||||
|
const r = rowOf(base('delivered = 2026-06-15'), 'X')
|
||||||
|
expect(r.onTime).toBe(true)
|
||||||
|
expect(r.markers.map((m) => m.kind)).toEqual(['delivered-on-time'])
|
||||||
|
expect(r.markers[0].week).toBe('2026-06-15')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivered earlier than the Original Estimate → still on-time ◉, bar ends at delivery', () => {
|
||||||
|
const r = rowOf(base('delivered = 2026-06-08'), 'X')
|
||||||
|
expect(r.onTime).toBe(true)
|
||||||
|
expect(r.markers.map((m) => m.kind)).toEqual(['delivered-on-time'])
|
||||||
|
expect(r.markers[0].week).toBe('2026-06-08')
|
||||||
|
expect(r.barEndWeek).toBe('2026-06-08') // no ◯ dangling in the future
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delivered after the Original Estimate → ▲ late, ◯ baseline preserved', () => {
|
||||||
|
const r = rowOf(base('delivered = 2026-06-29'), 'X')
|
||||||
|
expect(r.onTime).toBe(false)
|
||||||
|
const kinds = r.markers.map((m) => `${m.kind}@${m.week}`).sort()
|
||||||
|
expect(kinds).toEqual(['delivered-late@2026-06-29', 'original@2026-06-15'])
|
||||||
|
expect(r.barEndWeek).toBe('2026-06-29')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('late delivery with multiple slips keeps ◯ + every △ + ▲ (judged vs original, not re-estimate)', () => {
|
||||||
|
const r = rowOf(
|
||||||
|
base('reestimates = [2026-06-29, 2026-07-13]\ndelivered = 2026-07-20'),
|
||||||
|
'X',
|
||||||
|
)
|
||||||
|
expect(r.onTime).toBe(false) // 07-20 > original 06-15, regardless of the 07-13 re-estimate
|
||||||
|
expect(r.slipCount).toBe(2)
|
||||||
|
const byKind = r.markers.reduce<Record<string, string[]>>((acc, m) => {
|
||||||
|
;(acc[m.kind] ??= []).push(m.week)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
expect(byKind.original).toEqual(['2026-06-15'])
|
||||||
|
expect(byKind.reestimate?.sort()).toEqual(['2026-06-29', '2026-07-13'])
|
||||||
|
expect(byKind['delivered-late']).toEqual(['2026-07-20'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('in-flight (undelivered) → ◯ only, onTime null, bar ends at the furthest estimate', () => {
|
||||||
|
const r = rowOf(base('reestimates = [2026-06-29]\nstatus = "red"'), 'X')
|
||||||
|
expect(r.onTime).toBeNull()
|
||||||
|
expect(r.delivered).toBe(false)
|
||||||
|
expect(r.status).toBe('red')
|
||||||
|
expect(r.barEndWeek).toBe('2026-06-29') // furthest open estimate
|
||||||
|
expect(r.markers.some((m) => m.kind === 'original')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('plan derivation', () => {
|
||||||
|
it('derives a contiguous week range and places the now line', () => {
|
||||||
|
const plan = buildPlan(parseMacroplan(SAMPLE_PLAN), TODAY)
|
||||||
|
expect(plan.weeks[0]).toBe('2026-06-01') // earliest start
|
||||||
|
expect(plan.weeks.at(-1)).toBe('2026-07-20') // latest marker (Payments delivery)
|
||||||
|
// contiguous, weekly
|
||||||
|
expect(plan.weeks).toContain('2026-06-29')
|
||||||
|
expect(plan.nowWeek).toBe('2026-06-15')
|
||||||
|
expect(plan.nowInRange).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flags a Milestone’s unmet required Features (undelivered or delivered after the milestone)', () => {
|
||||||
|
const plan = buildPlan(parseMacroplan(SAMPLE_PLAN), TODAY)
|
||||||
|
const mvp = plan.milestones.find((m) => m.name === 'MVP go-live')!
|
||||||
|
expect(mvp.week).toBe('2026-07-06')
|
||||||
|
// Auth delivered 06-15 (met); Payments delivered 07-20 > 07-06 (unmet); Dashboard undelivered (unmet)
|
||||||
|
expect(mvp.unmet.sort()).toEqual(['Dashboard', 'Payments'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parse validation', () => {
|
||||||
|
it('rejects a feature missing its Original Estimate', () => {
|
||||||
|
expect(() => parseMacroplan('[[feature]]\nname = "A"\nstart = 2026-06-01\n')).toThrow(
|
||||||
|
PlanParseError,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects an invalid status', () => {
|
||||||
|
expect(() =>
|
||||||
|
parseMacroplan('[[feature]]\nname="A"\nstart=2026-06-01\noriginal=2026-06-08\nstatus="blue"\n'),
|
||||||
|
).toThrow(/status/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses the bundled sample without error', () => {
|
||||||
|
const raw = parseMacroplan(SAMPLE_PLAN)
|
||||||
|
expect(raw.features).toHaveLength(5)
|
||||||
|
expect(raw.milestones).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
79
src/model/plan.ts
Normal file
79
src/model/plan.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { mondayOf, weekRange, type WeekId } from './week'
|
||||||
|
import type { RawPlan, RawFeature, Plan, FeatureRow, Marker, MilestoneLine } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the render-ready Plan from the raw model (component C2).
|
||||||
|
*
|
||||||
|
* On-time vs. late is judged ONLY against the Original Estimate, never a
|
||||||
|
* 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 milestones = raw.milestones.map((m): MilestoneLine => {
|
||||||
|
const week = mondayOf(m.week)
|
||||||
|
const unmet = m.requires.filter((name) => {
|
||||||
|
const row = rows.find((r) => r.name === name)
|
||||||
|
if (!row) return true // references an unknown Feature → can't be met
|
||||||
|
const delivery = row.markers.find(
|
||||||
|
(x) => x.kind === 'delivered-on-time' || x.kind === 'delivered-late',
|
||||||
|
)
|
||||||
|
return !delivery || delivery.week > week // undelivered, or delivered after the milestone
|
||||||
|
})
|
||||||
|
return { name: m.name, week, requires: m.requires, unmet }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Range: earliest start to last marker/milestone (CONTEXT.md). Empty weeks drawn.
|
||||||
|
const allWeeks: WeekId[] = []
|
||||||
|
for (const r of rows) {
|
||||||
|
allWeeks.push(r.startWeek, r.barEndWeek)
|
||||||
|
for (const mk of r.markers) allWeeks.push(mk.week)
|
||||||
|
}
|
||||||
|
for (const m of milestones) allWeeks.push(m.week)
|
||||||
|
|
||||||
|
let weeks: WeekId[] = []
|
||||||
|
if (allWeeks.length) {
|
||||||
|
const start = allWeeks.reduce((a, b) => (a < b ? a : b))
|
||||||
|
const end = allWeeks.reduce((a, b) => (a > b ? a : b))
|
||||||
|
weeks = weekRange(start, end)
|
||||||
|
}
|
||||||
|
const nowInRange = weeks.length > 0 && nowWeek >= weeks[0] && nowWeek <= weeks[weeks.length - 1]
|
||||||
|
|
||||||
|
return { title: raw.title, weeks, rows, milestones, nowWeek, nowInRange }
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRow(f: RawFeature): FeatureRow {
|
||||||
|
const startWeek = mondayOf(f.start)
|
||||||
|
const originalWeek = mondayOf(f.original)
|
||||||
|
const deliveredWeek = f.delivered ? mondayOf(f.delivered) : undefined
|
||||||
|
const delivered = deliveredWeek != null
|
||||||
|
// ADR-0001: compare the Delivery week to the Original Estimate week only.
|
||||||
|
const onTime = delivered ? deliveredWeek! <= originalWeek : null
|
||||||
|
|
||||||
|
const markers: Marker[] = []
|
||||||
|
for (const re of f.reestimates) markers.push({ week: mondayOf(re), kind: 'reestimate' })
|
||||||
|
if (delivered) {
|
||||||
|
markers.push({ week: deliveredWeek!, kind: onTime ? 'delivered-on-time' : 'delivered-late' })
|
||||||
|
}
|
||||||
|
// The Original Estimate `◯` stands unless an on-time/early delivery already
|
||||||
|
// occupies (or precedes) it — then the delivery marker speaks for it.
|
||||||
|
if (!(delivered && onTime)) {
|
||||||
|
markers.push({ week: originalWeek, kind: 'original' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const barEndWeek = [startWeek, ...markers.map((m) => m.week)].reduce((a, b) => (a > b ? a : b))
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
startWeek,
|
||||||
|
barEndWeek,
|
||||||
|
markers,
|
||||||
|
delivered,
|
||||||
|
onTime,
|
||||||
|
status: f.status,
|
||||||
|
note: f.note,
|
||||||
|
learning: f.learning,
|
||||||
|
slipCount: f.reestimates.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/model/types.ts
Normal file
70
src/model/types.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import type { WeekId } from './week'
|
||||||
|
|
||||||
|
export type StatusLevel = 'green' | 'orange' | 'red'
|
||||||
|
|
||||||
|
// ── Raw model: as authored, after TOML parse + validation, before derivation ──
|
||||||
|
|
||||||
|
export interface RawFeature {
|
||||||
|
name: string
|
||||||
|
start: string // yyyy-mm-dd
|
||||||
|
original: string // yyyy-mm-dd — the Original Estimate
|
||||||
|
reestimates: string[] // yyyy-mm-dd[]
|
||||||
|
delivered?: string // yyyy-mm-dd
|
||||||
|
learning?: string
|
||||||
|
status?: StatusLevel
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawMilestone {
|
||||||
|
name: string
|
||||||
|
week: string // yyyy-mm-dd
|
||||||
|
requires: string[] // Feature names
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawPlan {
|
||||||
|
title: string
|
||||||
|
features: RawFeature[]
|
||||||
|
milestones: RawMilestone[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Derived model: render-ready (C2 output) ──
|
||||||
|
|
||||||
|
export type MarkerKind =
|
||||||
|
| 'original' // ◯ Original Estimate, not yet delivered
|
||||||
|
| 'reestimate' // △ a slip to a later week
|
||||||
|
| 'delivered-on-time' // ◉ delivered on/before the Original Estimate
|
||||||
|
| 'delivered-late' // ▲ delivered after the Original Estimate
|
||||||
|
|
||||||
|
export interface Marker {
|
||||||
|
week: WeekId
|
||||||
|
kind: MarkerKind
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeatureRow {
|
||||||
|
name: string
|
||||||
|
startWeek: WeekId
|
||||||
|
barEndWeek: WeekId
|
||||||
|
markers: Marker[]
|
||||||
|
delivered: boolean
|
||||||
|
onTime: boolean | null // null while in-flight
|
||||||
|
status?: StatusLevel
|
||||||
|
note?: string
|
||||||
|
learning?: string
|
||||||
|
slipCount: number // number of Re-estimates
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MilestoneLine {
|
||||||
|
name: string
|
||||||
|
week: WeekId
|
||||||
|
requires: string[]
|
||||||
|
unmet: string[] // required Features not delivered on/before this week
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Plan {
|
||||||
|
title: string
|
||||||
|
weeks: WeekId[]
|
||||||
|
rows: FeatureRow[]
|
||||||
|
milestones: MilestoneLine[]
|
||||||
|
nowWeek: WeekId
|
||||||
|
nowInRange: boolean
|
||||||
|
}
|
||||||
59
src/model/week.ts
Normal file
59
src/model/week.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Week math. A "Week" is identified by the ISO date (yyyy-mm-dd) of its Monday.
|
||||||
|
// ISO yyyy-mm-dd strings sort lexicographically == chronologically, so plain
|
||||||
|
// string comparison (`<`, `<=`) is valid week ordering — we lean on that.
|
||||||
|
|
||||||
|
export type WeekId = string // 'yyyy-mm-dd', always a Monday
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a yyyy-mm-dd string from a TOML date (smol-toml's `TomlDate.toISOString()`
|
||||||
|
* returns the authored local date, e.g. "2026-06-01") or a plain string.
|
||||||
|
*/
|
||||||
|
export function toYmd(value: unknown): string {
|
||||||
|
if (value instanceof Date) return value.toISOString().slice(0, 10)
|
||||||
|
if (typeof value === 'string') return value.slice(0, 10)
|
||||||
|
throw new Error(`expected a date, got ${JSON.stringify(value)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchor at UTC noon so day-of-week / day arithmetic never crosses a DST or
|
||||||
|
// timezone boundary.
|
||||||
|
function utcNoon(ymd: string): Date {
|
||||||
|
const [y, m, d] = ymd.split('-').map(Number)
|
||||||
|
return new Date(Date.UTC(y, m - 1, d, 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(dt: Date): string {
|
||||||
|
return dt.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Monday (yyyy-mm-dd) of the ISO week containing the given date. */
|
||||||
|
export function mondayOf(value: unknown): WeekId {
|
||||||
|
const dt = utcNoon(toYmd(value))
|
||||||
|
const dow = dt.getUTCDay() // 0=Sun .. 6=Sat
|
||||||
|
const shift = dow === 0 ? -6 : 1 - dow
|
||||||
|
dt.setUTCDate(dt.getUTCDate() + shift)
|
||||||
|
return fmt(dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add n weeks to a Monday WeekId (n may be negative). */
|
||||||
|
export function addWeeks(week: WeekId, n: number): WeekId {
|
||||||
|
const dt = utcNoon(week)
|
||||||
|
dt.setUTCDate(dt.getUTCDate() + n * 7)
|
||||||
|
return fmt(dt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inclusive list of Monday WeekIds from start..end (both must already be Mondays). */
|
||||||
|
export function weekRange(start: WeekId, end: WeekId): WeekId[] {
|
||||||
|
if (start > end) return [start]
|
||||||
|
const weeks: WeekId[] = []
|
||||||
|
for (let w = start; w <= end; w = addWeeks(w, 1)) weeks.push(w)
|
||||||
|
return weeks
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Short column label for a week, e.g. "Jun 15". */
|
||||||
|
export function weekLabel(week: WeekId): string {
|
||||||
|
return utcNoon(week).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user