From 1fae208f1ef5c65c4c04ef716356c3e35415c2a6 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 19:00:00 +0100 Subject: [PATCH 01/34] Upgraded `@cloudflare/kumo` to version `1.16.0` --- packages/local-explorer-ui/package.json | 2 +- pnpm-lock.yaml | 558 +++++++++++++++++++++++- 2 files changed, 543 insertions(+), 17 deletions(-) diff --git a/packages/local-explorer-ui/package.json b/packages/local-explorer-ui/package.json index 2b274e75a4..0ae1810590 100644 --- a/packages/local-explorer-ui/package.json +++ b/packages/local-explorer-ui/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@base-ui/react": "^1.1.0", - "@cloudflare/kumo": "^1.5.0", + "@cloudflare/kumo": "^1.16.0", "@cloudflare/workers-editor-shared": "^0.1.1", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9808b15e44..b386da6057 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1915,8 +1915,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@cloudflare/kumo': - specifier: ^1.5.0 - version: 1.5.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + specifier: ^1.16.0 + version: 1.16.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) '@cloudflare/workers-editor-shared': specifier: ^0.1.1 version: 0.1.1(@cloudflare/style-const@6.1.3(react@19.2.4))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@6.1.3(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -4696,6 +4696,17 @@ packages: '@types/react': optional: true + '@base-ui/react@1.3.0': + resolution: {integrity: sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@base-ui/utils@0.2.4': resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} peerDependencies: @@ -4706,6 +4717,16 @@ packages: '@types/react': optional: true + '@base-ui/utils@0.2.6': + resolution: {integrity: sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@better-auth/core@1.5.4': resolution: {integrity: sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ==} peerDependencies: @@ -5022,11 +5043,12 @@ packages: peerDependencies: react: ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0 - '@cloudflare/kumo@1.5.0': - resolution: {integrity: sha512-Y2fE72C3KwniG94SYROVtMwD5wNx/IXQ3CPbZv+ayD37nEXx1He3D5qFwh5PgPGtBQCrHiAL4J2b9v2FkgEvkQ==} + '@cloudflare/kumo@1.16.0': + resolution: {integrity: sha512-uCrj7jGPvdXj8lrdQBfMGKzV3JTDi7hUBsLf4jpirD7QHvZMsGe6XuU+KKvQFqDTmj5ELXQVES4YVoducxZ7Tg==} hasBin: true peerDependencies: '@phosphor-icons/react': ^2.1.10 + echarts: ^6.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 zod: ^4.0.0 @@ -5247,6 +5269,9 @@ packages: resolution: {integrity: sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==} engines: {node: '>= 6'} + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -6093,15 +6118,27 @@ packages: '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/dom@1.7.5': resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/react-dom@2.1.7': resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/react@0.27.18': resolution: {integrity: sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==} peerDependencies: @@ -6111,6 +6148,9 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@hey-api/codegen-core@0.7.1': resolution: {integrity: sha512-X5qG+rr/BJvr+pEGcoW6l2azoZGrVuxsviEIhuf+3VwL9bk0atfubT65Xwo+4jDxXvjbhZvlwS0Ty3I7mLE2fg==} engines: {node: '>=20.19.0'} @@ -8050,6 +8090,37 @@ packages: resolution: {integrity: sha512-A4srR9mEBFdVXwSEKjQ94msUbVkMr8JeFiEj9ouOFORw/Y/ux/WV2bWVD/ZI9wq0TcTNK8L1wBgU8UMS5lIq3A==} engines: {node: '>=14.18'} + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -8376,6 +8447,10 @@ packages: peerDependencies: '@svgr/core': '*' + '@tabby_ai/hijri-converter@1.0.5': + resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} + engines: {node: '>=16.0.0'} + '@tailwindcss/node@4.2.2': resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} @@ -8632,6 +8707,9 @@ packages: '@types/glob-to-regexp@0.4.1': resolution: {integrity: sha512-S0mIukll6fbF0tvrKic/jj+jI8SHoSvGU+Cs95b/jzZEnBYCbj+7aJtQ9yeABuK3xP1okwA3jEH9qIRayijnvQ==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -8674,6 +8752,9 @@ packages: '@types/lodash@4.17.23': resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mime-types@3.0.1': resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==} @@ -8806,6 +8887,9 @@ packages: '@types/supports-color@8.1.1': resolution: {integrity: sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@9.0.4': resolution: {integrity: sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==} @@ -8836,6 +8920,9 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@verdaccio/auth@8.0.0-next-8.7': resolution: {integrity: sha512-CSLBAsCJT1oOpJ4OWnVGmN6o/ZilDNa7Aa5+AU1LI2lbRblqgr4BVRn07GFqimJ//6+tPzl8BHgyiCbBhh1ZiA==} engines: {node: '>=18'} @@ -9550,6 +9637,9 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -9562,6 +9652,12 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -9680,6 +9776,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + command-exists@1.2.9: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} @@ -9933,6 +10032,9 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -10069,6 +10171,10 @@ packages: deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -10091,6 +10197,9 @@ packages: devalue@5.6.3: resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + devtools-protocol@0.0.1182435: resolution: {integrity: sha512-EmlkWb62wSbQNE1gRZZsi4KZYRaF5Skpp183LhRU7+sadKR06O1dHCjZmFSEG6Kv7P6S/UYLxcY3NlYwqKM99w==} @@ -10309,6 +10418,9 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -10793,6 +10905,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -11013,6 +11139,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -11045,6 +11177,9 @@ packages: html-rewriter-wasm@0.4.1: resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -11874,6 +12009,9 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} @@ -11909,6 +12047,21 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -12066,6 +12219,26 @@ packages: socks: optional: true + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -12305,6 +12478,12 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + open@11.0.0: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} @@ -13012,6 +13191,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -13107,6 +13289,12 @@ packages: react-addons-shallow-compare@15.6.3: resolution: {integrity: sha512-EDJbgKTtGRLhr3wiGDXK/+AEJ59yqGS+tKE6mue0aNXT6ZMR7VJbbzIiT6akotmHg1BLj46ElJSb+NBMp80XBg==} + react-day-picker@9.14.0: + resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + react-display-name@0.2.5: resolution: {integrity: sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==} @@ -13249,6 +13437,15 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp-to-ast@0.5.0: resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} @@ -13565,6 +13762,10 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -13676,6 +13877,9 @@ packages: engines: {node: '>= 8'} deprecated: The work that was done in this beta branch won't be included in future versions + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} @@ -13804,6 +14008,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -14061,6 +14268,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -14133,6 +14343,9 @@ packages: unplugin-unused: optional: true + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -14368,6 +14581,21 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} @@ -14490,6 +14718,12 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@4.0.1: resolution: {integrity: sha512-JFbAKMjJdJbeXJVwQNoi8M26lP+5Ene4/ryv9w0Z7Ca5N0DdxYEak9V3C0tqwHO7WZ9JLbwMsuUZOqYIyBRwSQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -14863,6 +15097,12 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@actions/core@1.11.1': @@ -15715,6 +15955,19 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 + '@base-ui/react@1.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.2 + '@base-ui/utils': 0.2.6(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@base-ui/utils@0.2.4(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@babel/runtime': 7.28.6 @@ -15726,6 +15979,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 + '@base-ui/utils@0.2.6(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.2 + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260401.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 @@ -16212,17 +16476,24 @@ snapshots: dependencies: react: 19.2.4 - '@cloudflare/kumo@1.5.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)': + '@cloudflare/kumo@1.16.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)': dependencies: - '@base-ui/react': 1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@base-ui/react': 1.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@phosphor-icons/react': 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 clsx: 2.1.1 + echarts: 6.0.0 + motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 + react-day-picker: 9.14.0(react@19.2.4) react-dom: 19.2.4(react@19.2.4) + shiki: 4.0.2 tailwind-merge: 3.4.0 optionalDependencies: zod: 4.3.6 transitivePeerDependencies: + - '@emotion/is-prop-valid' - '@types/react' '@cloudflare/kv-asset-handler@0.1.3': @@ -16491,6 +16762,8 @@ snapshots: tunnel-agent: 0.6.0 uuid: 8.3.2 + '@date-fns/tz@1.4.1': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': dependencies: react: 19.2.4 @@ -16995,17 +17268,32 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.10 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + '@floating-ui/dom@1.7.5': dependencies: '@floating-ui/core': 1.7.4 '@floating-ui/utils': 0.2.10 + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.7.5 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@floating-ui/react@0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -17016,6 +17304,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.11': {} + '@hey-api/codegen-core@0.7.1(typescript@5.8.3)': dependencies: '@hey-api/types': 0.1.3(typescript@5.8.3) @@ -17948,36 +18238,36 @@ snapshots: '@radix-ui/number@1.0.1': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@radix-ui/primitive@1.0.1': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 '@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 '@radix-ui/react-direction@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 '@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 @@ -17988,7 +18278,7 @@ snapshots: '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -18016,7 +18306,7 @@ snapshots: '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 optionalDependencies: @@ -18024,14 +18314,14 @@ snapshots: '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 @@ -18599,6 +18889,46 @@ snapshots: dependencies: '@sentry/types': 8.9.2 + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.5 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.27.8': {} '@sindresorhus/is@7.0.2': {} @@ -19061,6 +19391,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@tabby_ai/hijri-converter@1.0.5': {} + '@tailwindcss/node@4.2.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -19329,6 +19661,10 @@ snapshots: '@types/glob-to-regexp@0.4.1': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.4': {} @@ -19371,6 +19707,10 @@ snapshots: '@types/lodash@4.17.23': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mime-types@3.0.1': {} '@types/mime@1.3.5': {} @@ -19501,6 +19841,8 @@ snapshots: '@types/supports-color@8.1.1': {} + '@types/unist@3.0.3': {} + '@types/uuid@9.0.4': {} '@types/webidl-conversions@7.0.3': {} @@ -19532,6 +19874,8 @@ snapshots: '@types/node': 22.15.17 optional: true + '@ungap/structured-clone@1.3.0': {} + '@verdaccio/auth@8.0.0-next-8.7': dependencies: '@verdaccio/config': 8.0.0-next-8.7 @@ -20389,6 +20733,8 @@ snapshots: caseless@0.12.0: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk@4.1.2: @@ -20398,6 +20744,10 @@ snapshots: chalk@5.3.0: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + chardet@2.1.1: {} chevrotain@10.5.0: @@ -20520,6 +20870,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + command-exists@1.2.9: {} commander@11.1.0: {} @@ -20786,6 +21138,8 @@ snapshots: dataloader@1.4.0: {} + date-fns-jalali@4.1.0-0: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.28.6 @@ -20876,6 +21230,8 @@ snapshots: deprecation@2.3.1: {} + dequal@2.0.3: {} + destr@2.0.5: {} destroy@1.2.0: {} @@ -20889,6 +21245,10 @@ snapshots: devalue@5.6.3: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + devtools-protocol@0.0.1182435: {} devtools-protocol@0.0.1299070: {} @@ -21013,6 +21373,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + ee-first@1.1.1: {} effect@3.18.4: @@ -21778,6 +22143,15 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + fresh@0.5.2: {} fresh@2.0.0: {} @@ -22021,6 +22395,24 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + he@1.2.0: {} headers-polyfill@4.0.3: {} @@ -22041,6 +22433,8 @@ snapshots: html-rewriter-wasm@0.4.1: {} + html-void-elements@3.0.0: {} + http-cache-semantics@4.1.1: {} http-errors@2.0.0: @@ -22784,6 +23178,18 @@ snapshots: md5-file@5.0.0: {} + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + mdn-data@2.0.28: {} mdn-data@2.0.30: {} @@ -22804,6 +23210,23 @@ snapshots: methods@1.1.2: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -22933,6 +23356,20 @@ snapshots: bson: 7.2.0 mongodb-connection-string-url: 7.0.1 + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + mri@1.2.0: {} mrmime@2.0.1: {} @@ -23175,6 +23612,14 @@ snapshots: dependencies: mimic-fn: 4.0.0 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.5: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + open@11.0.0: dependencies: default-browser: 5.4.0 @@ -23877,6 +24322,8 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 + property-information@7.1.0: {} + proto-list@1.2.4: {} protobufjs@7.4.0: @@ -24000,6 +24447,14 @@ snapshots: dependencies: object-assign: 4.1.1 + react-day-picker@9.14.0(react@19.2.4): + dependencies: + '@date-fns/tz': 1.4.1 + '@tabby_ai/hijri-converter': 1.0.5 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.4 + react-display-name@0.2.5: {} react-dom@18.3.1(react@18.3.1): @@ -24186,6 +24641,16 @@ snapshots: regenerator-runtime@0.14.1: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp-to-ast@0.5.0: {} regexp.prototype.flags@1.5.4: @@ -24657,6 +25122,17 @@ snapshots: shell-quote@1.8.3: {} + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -24778,6 +25254,8 @@ snapshots: dependencies: whatwg-url: 7.1.0 + space-separated-tokens@2.0.2: {} + sparse-bitfield@3.0.3: dependencies: memory-pager: 1.5.0 @@ -24926,6 +25404,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -25178,6 +25661,8 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} @@ -25255,6 +25740,8 @@ snapshots: - synckit - vue-tsc + tslib@2.3.0: {} + tslib@2.8.1: {} tsup@8.3.0(@microsoft/api-extractor@7.52.8(@types/node@22.15.17))(jiti@2.6.1)(postcss@8.5.8)(supports-color@9.2.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.1): @@ -25569,6 +26056,29 @@ snapshots: unicorn-magic@0.1.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universal-user-agent@6.0.1: {} universalify@0.1.2: {} @@ -25734,6 +26244,16 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-plugin-dts@4.0.1(@types/node@22.15.17)(rollup@4.57.1)(typescript@5.8.3)(vite@8.0.1(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.47.4(@types/node@22.15.17) @@ -26178,3 +26698,9 @@ snapshots: zod@3.25.76: {} zod@4.3.6: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 + + zwitch@2.0.4: {} From b47364f309a1f21d24426a9172f638db9344650a Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 14:49:42 +0100 Subject: [PATCH 02/34] Added initial re-worked sidebar --- .../src/components/Sidebar.tsx | 399 +++++++++--------- .../local-explorer-ui/src/routes/__root.tsx | 44 +- 2 files changed, 225 insertions(+), 218 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index aee6d7f1f0..697bc7bd47 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -1,13 +1,18 @@ -import { CloudflareLogo, cn } from "@cloudflare/kumo"; -import { Collapsible } from "@cloudflare/kumo/primitives/collapsible"; -import { CaretRightIcon } from "@phosphor-icons/react"; -import { Link } from "@tanstack/react-router"; +import { + Badge, + CloudflareLogo, + cn, + Sidebar, + useSidebar, +} from "@cloudflare/kumo"; +import { MoonIcon } from "@phosphor-icons/react"; +import { useRouter } from "@tanstack/react-router"; import D1Icon from "../assets/icons/d1.svg?react"; import DOIcon from "../assets/icons/durable-objects.svg?react"; import KVIcon from "../assets/icons/kv.svg?react"; import R2Icon from "../assets/icons/r2.svg?react"; import WorkflowsIcon from "../assets/icons/workflows.svg?react"; -import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector"; +import { type LocalExplorerWorker } from "./WorkerSelector"; import type { D1DatabaseResponse, R2Bucket, @@ -15,82 +20,8 @@ import type { WorkersNamespace, WorkflowsWorkflow, } from "../api"; -import type { FileRouteTypes } from "../routeTree.gen"; import type { FC } from "react"; -interface SidebarItemGroupProps { - emptyLabel: string; - error: string | null; - icon: FC<{ className?: string }>; - items: Array<{ - id: string; - isActive: boolean; - label: string; - link: { - params: object; - search?: object; - to: FileRouteTypes["to"]; - }; - }>; - title: string; -} - -function SidebarItemGroup({ - emptyLabel, - error, - icon: Icon, - items, - title, -}: SidebarItemGroupProps): JSX.Element { - return ( - - - - - {title} - - - -
    - {error ? ( -
  • {error}
  • - ) : null} - - {!error - ? items.map((item) => ( -
  • - - {item.label} - -
  • - )) - : null} - - {!error && items.length === 0 && ( -
  • - {emptyLabel} -
  • - )} -
-
-
- ); -} - interface SidebarProps { currentPath: string; d1Error: string | null; @@ -108,7 +39,7 @@ interface SidebarProps { workflowsError: string | null; } -export function Sidebar({ +export function AppSidebar({ currentPath, d1Error, databases, @@ -120,136 +51,210 @@ export function Sidebar({ r2Error, workers, selectedWorker, - onWorkerChange, + // onWorkerChange, workflows, workflowsError, }: SidebarProps) { - const showWorkerSelector = workers.length > 1; + const router = useRouter(); + const sidebar = useSidebar(); + + // const showWorkerSelector = workers.length > 1; // Only include the worker search param when there are multiple workers. // This keeps URLs clean in the common single-worker case. const workerSearch = workers.length > 1 ? { worker: selectedWorker } : {}; + const sidebarItemGroups = [ + { + emptyLabel: "No databases", + error: d1Error, + icon: D1Icon, + items: databases.map((db) => ({ + href: router.buildLocation({ + params: { databaseId: db.uuid as string }, + search: { table: undefined, ...workerSearch }, + to: "/d1/$databaseId", + }).href, + id: db.uuid as string, + isActive: currentPath === `/d1/${db.uuid}`, + label: db.name as string, + })), + title: "D1 Databases", + }, + { + emptyLabel: "No SQLite namespaces", + error: doError, + icon: DOIcon, + items: doNamespaces.map((ns) => { + const className = ns.class ?? ns.name ?? ns.id ?? "Unknown"; + return { + href: router.buildLocation({ + params: { className }, + search: workerSearch, + to: "/do/$className", + }).href, + id: ns.id as string, + isActive: + currentPath === `/do/${className}` || + currentPath.startsWith(`/do/${className}/`), + label: className, + }; + }), + title: "Durable Objects", + }, + { + emptyLabel: "No namespaces", + error: kvError, + icon: KVIcon, + items: kvNamespaces.map((ns) => ({ + href: router.buildLocation({ + params: { namespaceId: ns.id }, + search: workerSearch, + to: "/kv/$namespaceId", + }).href, + id: ns.id, + isActive: currentPath === `/kv/${ns.id}`, + label: ns.title, + })), + title: "KV Namespaces", + }, + { + emptyLabel: "No buckets", + error: r2Error, + icon: R2Icon, + items: r2Buckets.map((bucket) => { + const bucketName = bucket.name ?? "Unknown"; + return { + href: router.buildLocation({ + params: { bucketName }, + search: workerSearch, + to: "/r2/$bucketName", + }).href, + id: bucketName, + isActive: + currentPath === `/r2/${bucketName}` || + currentPath.startsWith(`/r2/${bucketName}/`), + label: bucketName, + }; + }), + title: "R2 Buckets", + }, + { + emptyLabel: "No workflows", + error: workflowsError, + icon: WorkflowsIcon, + items: workflows.map((wf) => ({ + href: router.buildLocation({ + params: { workflowName: wf.name }, + search: workerSearch, + to: "/workflows/$workflowName", + }).href, + id: wf.name as string, + isActive: + currentPath === `/workflows/${wf.name}` || + currentPath.startsWith(`/workflows/${wf.name}/`), + label: wf.name as string, + })), + title: "Workflows", + }, + ] satisfies Array<{ + emptyLabel: string; + error: string | null; + icon: FC<{ className?: string }>; + items: Array<{ + href: string; + id: string; + isActive: boolean; + label: string; + }>; + title: string; + }>; + return ( - + + {!group.error && group.items.length === 0 ? ( +
+ {group.emptyLabel} +
+ ) : ( + group.items.map((item) => ( + + + {item.label} + + + )) + )} +
+ + ))} + + + + + } + onClick={() => {}} + tooltip="Switch theme" + type="button" + /> + + + + ); } diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index ba77c16455..e89a6a4641 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import { Toasty } from "@cloudflare/kumo"; +import { Sidebar, Toasty } from "@cloudflare/kumo"; import { createRootRoute, Outlet, @@ -14,7 +14,7 @@ import { workersKvNamespaceListNamespaces, workflowsListWorkflows, } from "../api"; -import { Sidebar } from "../components/Sidebar"; +import { AppSidebar } from "../components/Sidebar"; import { filterVisibleWorkers } from "../components/WorkerSelector"; import type { D1DatabaseResponse, @@ -211,25 +211,27 @@ function RootLayout() { return (
- -
- -
+ + +
+ +
+
); From 931bcb289f2b3474bffbb7053ab844b7032d1bd6 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 14:49:51 +0100 Subject: [PATCH 03/34] Minor code formatting --- packages/local-explorer-ui/src/components/Breadcrumbs.tsx | 2 +- .../src/components/studio/WindowTab/ItemRenderer.tsx | 6 +++--- .../local-explorer-ui/src/components/workflows/StepRow.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Breadcrumbs.tsx b/packages/local-explorer-ui/src/components/Breadcrumbs.tsx index 9265797ed8..ebab5698a0 100644 --- a/packages/local-explorer-ui/src/components/Breadcrumbs.tsx +++ b/packages/local-explorer-ui/src/components/Breadcrumbs.tsx @@ -15,7 +15,7 @@ export function Breadcrumbs({ title, }: BreadcrumbsProps): JSX.Element { return ( -
+
}> {title} diff --git a/packages/local-explorer-ui/src/components/studio/WindowTab/ItemRenderer.tsx b/packages/local-explorer-ui/src/components/studio/WindowTab/ItemRenderer.tsx index d8d9a5487b..3888e58368 100644 --- a/packages/local-explorer-ui/src/components/studio/WindowTab/ItemRenderer.tsx +++ b/packages/local-explorer-ui/src/components/studio/WindowTab/ItemRenderer.tsx @@ -31,10 +31,10 @@ export function StudioWindowTabItemRenderer({ return (
+
); From ef83ff7450c0d888046793d0eddd861d05cef197 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 14:57:10 +0100 Subject: [PATCH 04/34] Minor UI gap fix --- .../src/routes/workflows/$workflowName/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx index 0507d9b780..e5e2154ee1 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx @@ -848,7 +848,7 @@ function WorkflowInstancesView() {
{totalCount > 0 && ( -
+
{perPage}} From e579611196b24341dd41ca857c5b3f4bd07bfa92 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 15:25:03 +0100 Subject: [PATCH 05/34] Minor sidebar item margin fix --- packages/local-explorer-ui/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 697bc7bd47..284b27f64f 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -228,7 +228,7 @@ export function AppSidebar({
) : ( group.items.map((item) => ( - + Date: Tue, 7 Apr 2026 15:26:11 +0100 Subject: [PATCH 06/34] Persist sidebar item collapse state to local storage --- .../src/__tests__/utils/sidebar-state.test.ts | 194 ++++++++++++++++++ .../src/components/Sidebar.tsx | 30 ++- .../src/utils/sidebar-state.ts | 70 +++++++ 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts create mode 100644 packages/local-explorer-ui/src/utils/sidebar-state.ts diff --git a/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts b/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts new file mode 100644 index 0000000000..83a7b7f089 --- /dev/null +++ b/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts @@ -0,0 +1,194 @@ +import { afterEach, beforeEach, describe, test, vi } from "vitest"; +import { + DEFAULT_GROUP_STATE, + loadGroupState, + saveGroupState, + SIDEBAR_GROUP_IDS, +} from "../../utils/sidebar-state"; +import type { SidebarGroupState } from "../../utils/sidebar-state"; + +const STORAGE_KEY = "local-explorer.sidebar.groups.v1"; + +/** + * Minimal localStorage stub scoped to each test. + */ +function createStorageStub(): Storage { + const store = new Map(); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + removeItem: (key: string) => { + store.delete(key); + }, + clear: () => { + store.clear(); + }, + get length() { + return store.size; + }, + key: (_index: number) => null, + }; +} + +describe("sidebar-state", () => { + let storageStub: Storage; + + beforeEach(() => { + storageStub = createStorageStub(); + vi.stubGlobal("localStorage", storageStub); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe("getDefaultGroupState", () => { + test("returns all groups expanded", ({ expect }) => { + for (const id of SIDEBAR_GROUP_IDS) { + expect(DEFAULT_GROUP_STATE[id]).toBe(true); + } + }); + + test("contains exactly the known group IDs", ({ expect }) => { + expect(Object.keys(DEFAULT_GROUP_STATE).sort()).toEqual( + [...SIDEBAR_GROUP_IDS].sort() + ); + }); + }); + + describe("loadGroupState", () => { + test("returns defaults when nothing is stored", ({ expect }) => { + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + + test("returns defaults when stored value is not valid JSON", ({ + expect, + }) => { + storageStub.setItem(STORAGE_KEY, "not-json{{{"); + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + + test("returns defaults when stored value is null JSON", ({ expect }) => { + storageStub.setItem(STORAGE_KEY, "null"); + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + + test("returns defaults when stored value is an array", ({ expect }) => { + storageStub.setItem(STORAGE_KEY, "[1,2,3]"); + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + + test("returns defaults when stored value is a string", ({ expect }) => { + storageStub.setItem(STORAGE_KEY, '"hello"'); + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + + test("returns defaults when stored value is a number", ({ expect }) => { + storageStub.setItem(STORAGE_KEY, "42"); + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + + test("merges partial stored state onto defaults", ({ expect }) => { + storageStub.setItem(STORAGE_KEY, JSON.stringify({ d1: false })); + const state = loadGroupState(); + expect(state.d1).toBe(false); + expect(state.do).toBe(true); + expect(state.kv).toBe(true); + expect(state.r2).toBe(true); + expect(state.workflows).toBe(true); + }); + + test("respects all stored boolean values", ({ expect }) => { + const stored: SidebarGroupState = { + d1: false, + do: false, + kv: true, + r2: false, + workflows: true, + }; + storageStub.setItem(STORAGE_KEY, JSON.stringify(stored)); + expect(loadGroupState()).toEqual(stored); + }); + + test("ignores non-boolean values in stored object", ({ expect }) => { + storageStub.setItem( + STORAGE_KEY, + JSON.stringify({ d1: "yes", do: 42, kv: null, r2: false }) + ); + const state = loadGroupState(); + // Non-boolean values should fall back to defaults (true) + expect(state.d1).toBe(true); + expect(state.do).toBe(true); + expect(state.kv).toBe(true); + // Valid boolean should be respected + expect(state.r2).toBe(false); + expect(state.workflows).toBe(true); + }); + + test("ignores unknown keys in stored object", ({ expect }) => { + storageStub.setItem( + STORAGE_KEY, + JSON.stringify({ d1: false, unknownGroup: false }) + ); + const state = loadGroupState(); + expect(state.d1).toBe(false); + expect("unknownGroup" in state).toBe(false); + }); + + test("returns defaults when localStorage.getItem throws", ({ expect }) => { + vi.spyOn(storageStub, "getItem").mockImplementation(() => { + throw new Error("SecurityError"); + }); + expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); + }); + }); + + describe("saveGroupState", () => { + test("persists state to localStorage", ({ expect }) => { + const state: SidebarGroupState = { + d1: false, + do: true, + kv: false, + r2: true, + workflows: false, + }; + saveGroupState(state); + const raw = storageStub.getItem(STORAGE_KEY); + expect(raw).not.toBeNull(); + expect(JSON.parse(raw as string)).toEqual(state); + }); + + test("does not throw when localStorage.setItem throws", ({ expect }) => { + vi.spyOn(storageStub, "setItem").mockImplementation(() => { + throw new Error("QuotaExceededError"); + }); + expect(() => saveGroupState(DEFAULT_GROUP_STATE)).not.toThrow(); + }); + }); + + describe("round-trip", () => { + test("loadGroupState returns what saveGroupState persisted", ({ + expect, + }) => { + const state: SidebarGroupState = { + d1: false, + do: false, + kv: true, + r2: false, + workflows: true, + }; + saveGroupState(state); + expect(loadGroupState()).toEqual(state); + }); + + test("multiple saves overwrite previous state", ({ expect }) => { + saveGroupState({ ...DEFAULT_GROUP_STATE, d1: false }); + saveGroupState({ ...DEFAULT_GROUP_STATE, kv: false }); + const state = loadGroupState(); + expect(state.d1).toBe(true); // overwritten by second save + expect(state.kv).toBe(false); + }); + }); +}); diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 284b27f64f..4f809159f7 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -7,11 +7,13 @@ import { } from "@cloudflare/kumo"; import { MoonIcon } from "@phosphor-icons/react"; import { useRouter } from "@tanstack/react-router"; +import { useCallback, useState } from "react"; import D1Icon from "../assets/icons/d1.svg?react"; import DOIcon from "../assets/icons/durable-objects.svg?react"; import KVIcon from "../assets/icons/kv.svg?react"; import R2Icon from "../assets/icons/r2.svg?react"; import WorkflowsIcon from "../assets/icons/workflows.svg?react"; +import { loadGroupState, saveGroupState } from "../utils/sidebar-state"; import { type LocalExplorerWorker } from "./WorkerSelector"; import type { D1DatabaseResponse, @@ -20,6 +22,7 @@ import type { WorkersNamespace, WorkflowsWorkflow, } from "../api"; +import type { SidebarGroupId } from "../utils/sidebar-state"; import type { FC } from "react"; interface SidebarProps { @@ -58,6 +61,19 @@ export function AppSidebar({ const router = useRouter(); const sidebar = useSidebar(); + const [groupOpen, setGroupOpen] = useState(loadGroupState); + + const handleGroupOpenChange = useCallback( + (groupId: SidebarGroupId, open: boolean) => { + setGroupOpen((prev) => { + const next = { ...prev, [groupId]: open }; + saveGroupState(next); + return next; + }); + }, + [] + ); + // const showWorkerSelector = workers.length > 1; // Only include the worker search param when there are multiple workers. @@ -68,6 +84,7 @@ export function AppSidebar({ { emptyLabel: "No databases", error: d1Error, + groupId: "d1" as const, icon: D1Icon, items: databases.map((db) => ({ href: router.buildLocation({ @@ -84,6 +101,7 @@ export function AppSidebar({ { emptyLabel: "No SQLite namespaces", error: doError, + groupId: "do" as const, icon: DOIcon, items: doNamespaces.map((ns) => { const className = ns.class ?? ns.name ?? ns.id ?? "Unknown"; @@ -105,6 +123,7 @@ export function AppSidebar({ { emptyLabel: "No namespaces", error: kvError, + groupId: "kv" as const, icon: KVIcon, items: kvNamespaces.map((ns) => ({ href: router.buildLocation({ @@ -121,6 +140,7 @@ export function AppSidebar({ { emptyLabel: "No buckets", error: r2Error, + groupId: "r2" as const, icon: R2Icon, items: r2Buckets.map((bucket) => { const bucketName = bucket.name ?? "Unknown"; @@ -142,6 +162,7 @@ export function AppSidebar({ { emptyLabel: "No workflows", error: workflowsError, + groupId: "workflows" as const, icon: WorkflowsIcon, items: workflows.map((wf) => ({ href: router.buildLocation({ @@ -160,6 +181,7 @@ export function AppSidebar({ ] satisfies Array<{ emptyLabel: string; error: string | null; + groupId: SidebarGroupId; icon: FC<{ className?: string }>; items: Array<{ href: string; @@ -210,7 +232,13 @@ export function AppSidebar({ {sidebarItemGroups.map((group) => ( - + { + handleGroupOpenChange(group.groupId, open); + }} + > ; + +export const DEFAULT_GROUP_STATE: SidebarGroupState = { + d1: true, + do: true, + kv: true, + r2: true, + workflows: true, +}; + +/** + * Read persisted group state from `localStorage`. + * + * Returns the default (all-expanded) state when: + * - `localStorage` is unavailable (e.g. SSR, security restrictions) + * - the stored value is missing, not valid JSON, or not an object + * + * Merges stored values onto defaults so that newly-added groups + * automatically default to expanded without requiring a migration. + */ +export function loadGroupState(): SidebarGroupState { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw === null) { + return DEFAULT_GROUP_STATE; + } + + const parsed: unknown = JSON.parse(raw); + if ( + typeof parsed !== "object" || + parsed === null || + Array.isArray(parsed) + ) { + return DEFAULT_GROUP_STATE; + } + + const record = parsed as Record; + const merged = { ...DEFAULT_GROUP_STATE }; + + for (const id of SIDEBAR_GROUP_IDS) { + if (typeof record[id] === "boolean") { + merged[id] = record[id]; + } + } + + return merged; + } catch { + return DEFAULT_GROUP_STATE; + } +} + +/** + * Persist group state to `localStorage`. + * + * Silently swallows errors (e.g. quota exceeded, security restrictions) + * so the UI never breaks due to storage failures. + */ +export function saveGroupState(state: SidebarGroupState): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch { + // Silently ignore storage errors + } +} From 0cbef86e248502034673b8786bc3020ef003cbe9 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 15:38:43 +0100 Subject: [PATCH 07/34] Re-added worker selector --- .../local-explorer-ui/src/components/Sidebar.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 4f809159f7..89cebc190e 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -14,7 +14,7 @@ import KVIcon from "../assets/icons/kv.svg?react"; import R2Icon from "../assets/icons/r2.svg?react"; import WorkflowsIcon from "../assets/icons/workflows.svg?react"; import { loadGroupState, saveGroupState } from "../utils/sidebar-state"; -import { type LocalExplorerWorker } from "./WorkerSelector"; +import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector"; import type { D1DatabaseResponse, R2Bucket, @@ -54,7 +54,7 @@ export function AppSidebar({ r2Error, workers, selectedWorker, - // onWorkerChange, + onWorkerChange, workflows, workflowsError, }: SidebarProps) { @@ -74,7 +74,7 @@ export function AppSidebar({ [] ); - // const showWorkerSelector = workers.length > 1; + const showWorkerSelector = workers.length > 1; // Only include the worker search param when there are multiple workers. // This keeps URLs clean in the common single-worker case. @@ -230,6 +230,14 @@ export function AppSidebar({ + {showWorkerSelector && ( + + )} + {sidebarItemGroups.map((group) => ( Date: Tue, 7 Apr 2026 15:40:10 +0100 Subject: [PATCH 08/34] Added collapsed sidebar support to worker selector --- .../src/components/WorkerSelector.tsx | 120 +++++++++++------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/packages/local-explorer-ui/src/components/WorkerSelector.tsx b/packages/local-explorer-ui/src/components/WorkerSelector.tsx index 0da1f968e8..0c207db375 100644 --- a/packages/local-explorer-ui/src/components/WorkerSelector.tsx +++ b/packages/local-explorer-ui/src/components/WorkerSelector.tsx @@ -1,3 +1,4 @@ +import { Tooltip, useSidebar } from "@cloudflare/kumo"; import { Select } from "@cloudflare/kumo/primitives/select"; import { CaretUpDownIcon, @@ -45,6 +46,8 @@ export function WorkerSelector({ selectedWorker, onWorkerChange, }: WorkerSelectorProps): JSX.Element { + const sidebar = useSidebar(); + const [open, setOpen] = useState(false); const handleValueChange = (value: string | null): void => { @@ -57,14 +60,14 @@ export function WorkerSelector({ // Find the current worker that is hosting this explorer (isSelf = true) const selfWorker = workers.find((w) => w.isSelf); - return ( -
- + const selectRoot = ( + + {sidebar.open ? ( @@ -74,50 +77,69 @@ export function WorkerSelector({ + ) : ( + + + + )} - - - - - {workers.map((worker) => { - const isSelected = selectedWorker === worker.name; - const Icon = isSelected ? CheckIcon : TerminalIcon; + + + + + {workers.map((worker) => { + const isSelected = selectedWorker === worker.name; + const Icon = isSelected ? CheckIcon : TerminalIcon; - return ( - - - + return ( + + + + + + + {worker.name} + {worker.isSelf && selfWorker && ( + + current + + )} - - - {worker.name} - {worker.isSelf && selfWorker && ( - - current - - )} - - - - ); - })} - - - - - + + + ); + })} + + + + + + ); + + if (sidebar.open) { + return
{selectRoot}
; + } + + return ( +
+ + {selectRoot} +
); } From 7dce51effc373810202eeae7386b13370b468db3 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 15:59:33 +0100 Subject: [PATCH 09/34] Added basic theme switching support --- .../src/__tests__/utils/theme-state.test.ts | 157 ++++++++++++++++++ .../src/components/Sidebar.tsx | 60 +++++-- packages/local-explorer-ui/src/main.tsx | 16 +- .../local-explorer-ui/src/routes/__root.tsx | 27 ++- .../src/utils/theme-state.ts | 101 +++++++++++ 5 files changed, 337 insertions(+), 24 deletions(-) create mode 100644 packages/local-explorer-ui/src/__tests__/utils/theme-state.test.ts create mode 100644 packages/local-explorer-ui/src/utils/theme-state.ts diff --git a/packages/local-explorer-ui/src/__tests__/utils/theme-state.test.ts b/packages/local-explorer-ui/src/__tests__/utils/theme-state.test.ts new file mode 100644 index 0000000000..2c7cbd78b8 --- /dev/null +++ b/packages/local-explorer-ui/src/__tests__/utils/theme-state.test.ts @@ -0,0 +1,157 @@ +import { afterEach, beforeEach, describe, test, vi } from "vitest"; +import { + getNextThemeMode, + loadThemeMode, + resolveThemeMode, + saveThemeMode, + THEME_MODES, +} from "../../utils/theme-state"; +import type { ThemeMode } from "../../utils/theme-state"; + +const STORAGE_KEY = "local-explorer.theme.v1"; + +/** + * Minimal localStorage stub scoped to each test. + */ +function createStorageStub(): Storage { + const store = new Map(); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + removeItem: (key: string) => { + store.delete(key); + }, + clear: () => { + store.clear(); + }, + get length() { + return store.size; + }, + key: (_index: number) => null, + }; +} + +describe("theme-state", () => { + let storageStub: Storage; + + beforeEach(() => { + storageStub = createStorageStub(); + vi.stubGlobal("localStorage", storageStub); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe("THEME_MODES", () => { + test("contains exactly light, dark, system in cycle order", ({ + expect, + }) => { + expect(THEME_MODES).toEqual(["light", "dark", "system"]); + }); + }); + + describe("getNextThemeMode", () => { + test("cycles light -> dark", ({ expect }) => { + expect(getNextThemeMode("light")).toBe("dark"); + }); + + test("cycles dark -> system", ({ expect }) => { + expect(getNextThemeMode("dark")).toBe("system"); + }); + + test("cycles system -> light", ({ expect }) => { + expect(getNextThemeMode("system")).toBe("light"); + }); + + test("full cycle returns to the starting mode", ({ expect }) => { + let mode: ThemeMode = "light"; + for (let i = 0; i < THEME_MODES.length; i++) { + mode = getNextThemeMode(mode); + } + expect(mode).toBe("light"); + }); + }); + + describe("resolveThemeMode", () => { + test('returns "light" for mode "light" regardless of OS', ({ expect }) => { + expect(resolveThemeMode("light", false)).toBe("light"); + expect(resolveThemeMode("light", true)).toBe("light"); + }); + + test('returns "dark" for mode "dark" regardless of OS', ({ expect }) => { + expect(resolveThemeMode("dark", false)).toBe("dark"); + expect(resolveThemeMode("dark", true)).toBe("dark"); + }); + + test('returns OS preference for mode "system"', ({ expect }) => { + expect(resolveThemeMode("system", false)).toBe("light"); + expect(resolveThemeMode("system", true)).toBe("dark"); + }); + }); + + describe("loadThemeMode", () => { + test('returns "system" when nothing is stored', ({ expect }) => { + expect(loadThemeMode()).toBe("system"); + }); + + test('returns "system" for unrecognised stored value', ({ expect }) => { + storageStub.setItem(STORAGE_KEY, "neon"); + expect(loadThemeMode()).toBe("system"); + }); + + test('returns "system" for empty string', ({ expect }) => { + storageStub.setItem(STORAGE_KEY, ""); + expect(loadThemeMode()).toBe("system"); + }); + + test("returns stored mode when valid", ({ expect }) => { + for (const mode of THEME_MODES) { + storageStub.setItem(STORAGE_KEY, mode); + expect(loadThemeMode()).toBe(mode); + } + }); + + test('returns "system" when `localStorage.getItem` throws', ({ + expect, + }) => { + vi.spyOn(storageStub, "getItem").mockImplementation(() => { + throw new Error("SecurityError"); + }); + expect(loadThemeMode()).toBe("system"); + }); + }); + + describe("saveThemeMode", () => { + test("persists mode to `localStorage`", ({ expect }) => { + saveThemeMode("dark"); + expect(storageStub.getItem(STORAGE_KEY)).toBe("dark"); + }); + + test("overwrites previous value", ({ expect }) => { + saveThemeMode("light"); + saveThemeMode("dark"); + expect(storageStub.getItem(STORAGE_KEY)).toBe("dark"); + }); + + test("does not throw when `localStorage.setItem` throws", ({ expect }) => { + vi.spyOn(storageStub, "setItem").mockImplementation(() => { + throw new Error("QuotaExceededError"); + }); + expect(() => saveThemeMode("dark")).not.toThrow(); + }); + }); + + describe("round-trip", () => { + test("`loadThemeMode` returns what saveThemeMode persisted", ({ + expect, + }) => { + for (const mode of THEME_MODES) { + saveThemeMode(mode); + expect(loadThemeMode()).toBe(mode); + } + }); + }); +}); diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 89cebc190e..5c15f06e56 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -5,7 +5,7 @@ import { Sidebar, useSidebar, } from "@cloudflare/kumo"; -import { MoonIcon } from "@phosphor-icons/react"; +import { MonitorIcon, MoonIcon, SunIcon } from "@phosphor-icons/react"; import { useRouter } from "@tanstack/react-router"; import { useCallback, useState } from "react"; import D1Icon from "../assets/icons/d1.svg?react"; @@ -14,6 +14,7 @@ import KVIcon from "../assets/icons/kv.svg?react"; import R2Icon from "../assets/icons/r2.svg?react"; import WorkflowsIcon from "../assets/icons/workflows.svg?react"; import { loadGroupState, saveGroupState } from "../utils/sidebar-state"; +import { getNextThemeMode } from "../utils/theme-state"; import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector"; import type { D1DatabaseResponse, @@ -23,8 +24,30 @@ import type { WorkflowsWorkflow, } from "../api"; import type { SidebarGroupId } from "../utils/sidebar-state"; +import type { ThemeMode } from "../utils/theme-state"; import type { FC } from "react"; +const THEME_MODE_CONFIG = { + light: { + icon: SunIcon, + label: "Light", + }, + dark: { + icon: MoonIcon, + label: "Dark", + }, + system: { + icon: MonitorIcon, + label: "System", + }, +} satisfies Record< + ThemeMode, + { + icon: typeof SunIcon; + label: string; + } +>; + interface SidebarProps { currentPath: string; d1Error: string | null; @@ -33,11 +56,13 @@ interface SidebarProps { doNamespaces: WorkersNamespace[]; kvError: string | null; kvNamespaces: WorkersKvNamespace[]; + onCycleTheme: () => void; + onWorkerChange: (workerName: string) => void; r2Buckets: R2Bucket[]; r2Error: string | null; - workers: LocalExplorerWorker[]; selectedWorker: string; - onWorkerChange: (workerName: string) => void; + themeMode: ThemeMode; + workers: LocalExplorerWorker[]; workflows: WorkflowsWorkflow[]; workflowsError: string | null; } @@ -50,11 +75,13 @@ export function AppSidebar({ doNamespaces, kvError, kvNamespaces, + onCycleTheme, + onWorkerChange, r2Buckets, r2Error, - workers, selectedWorker, - onWorkerChange, + themeMode, + workers, workflows, workflowsError, }: SidebarProps) { @@ -281,13 +308,22 @@ export function AppSidebar({ - } - onClick={() => {}} - tooltip="Switch theme" - type="button" - /> + {(() => { + const { icon: Icon, label } = THEME_MODE_CONFIG[themeMode]; + const nextLabel = + THEME_MODE_CONFIG[getNextThemeMode(themeMode)].label; + + return ( + } + onClick={onCycleTheme} + tooltip={`Theme: ${label}`} + type="button" + /> + ); + })()} diff --git a/packages/local-explorer-ui/src/main.tsx b/packages/local-explorer-ui/src/main.tsx index 9dfb05bf75..64bebf8205 100644 --- a/packages/local-explorer-ui/src/main.tsx +++ b/packages/local-explorer-ui/src/main.tsx @@ -3,17 +3,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./styles/tailwind.css"; import { routeTree } from "./routeTree.gen"; +import { applyThemeMode, loadThemeMode } from "./utils/theme-state"; +// Apply the persisted (or default "system") theme immediately so the first +// paint already has the correct `data-mode` on . const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); - -function syncColorModeWithOS(): void { - document.documentElement.dataset.mode = darkModeQuery.matches - ? "dark" - : "light"; -} - -syncColorModeWithOS(); -darkModeQuery.addEventListener("change", syncColorModeWithOS); +applyThemeMode(loadThemeMode(), darkModeQuery.matches); +darkModeQuery.addEventListener("change", () => { + applyThemeMode(loadThemeMode(), darkModeQuery.matches); +}); // eslint-disable-next-line turbo/no-undeclared-env-vars -- replaced at build time const router = createRouter({ routeTree, basepath: import.meta.env.BASE_URL }); diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index e89a6a4641..f44d486706 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -5,7 +5,7 @@ import { useRouter, useRouterState, } from "@tanstack/react-router"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { d1ListDatabases, durableObjectsNamespaceListNamespaces, @@ -16,6 +16,12 @@ import { } from "../api"; import { AppSidebar } from "../components/Sidebar"; import { filterVisibleWorkers } from "../components/WorkerSelector"; +import { + applyThemeMode, + getNextThemeMode, + loadThemeMode, + saveThemeMode, +} from "../utils/theme-state"; import type { D1DatabaseResponse, LocalExplorerWorker, @@ -24,6 +30,7 @@ import type { WorkersNamespace, WorkflowsWorkflow, } from "../api"; +import type { ThemeMode } from "../utils/theme-state"; // Extended types with workerName for filtering type KvNamespaceWithWorker = WorkersKvNamespace & { workerName?: string }; @@ -127,6 +134,18 @@ function RootLayout() { ); const router = useRouter(); + const [themeMode, setThemeMode] = useState(loadThemeMode); + + const handleCycleTheme = useCallback(() => { + const next = getNextThemeMode(themeMode); + saveThemeMode(next); + applyThemeMode( + next, + window.matchMedia("(prefers-color-scheme: dark)").matches + ); + setThemeMode(next); + }, [themeMode]); + // Filter out internal workers (like __asset-worker__, __router-worker__, etc.) const visibleWorkers = useMemo( () => filterVisibleWorkers(loaderData.workers), @@ -220,11 +239,13 @@ function RootLayout() { doNamespaces={filteredData.doNamespaces} kvError={loaderData.kvError} kvNamespaces={filteredData.kvNamespaces} + onCycleTheme={handleCycleTheme} + onWorkerChange={handleWorkerChange} r2Buckets={filteredData.r2Buckets} r2Error={loaderData.r2Error} - workers={visibleWorkers} selectedWorker={selectedWorker} - onWorkerChange={handleWorkerChange} + themeMode={themeMode} + workers={visibleWorkers} workflows={filteredData.workflows} workflowsError={loaderData.workflowsError} /> diff --git a/packages/local-explorer-ui/src/utils/theme-state.ts b/packages/local-explorer-ui/src/utils/theme-state.ts new file mode 100644 index 0000000000..65e56d79e2 --- /dev/null +++ b/packages/local-explorer-ui/src/utils/theme-state.ts @@ -0,0 +1,101 @@ +const STORAGE_KEY = "local-explorer.theme.v1"; + +/** + * The user-selectable theme modes. + * + * - `"light"` / `"dark"` force the corresponding appearance. + * - `"system"` follows the OS preference via `prefers-color-scheme`. + */ +export type ThemeMode = "dark" | "light" | "system"; + +/** + * Ordered cycle: light -> dark -> system -> light + */ +export const THEME_MODES: readonly ThemeMode[] = [ + "light", + "dark", + "system", +] as const; + +/** + * Return the next mode in the cycle. + * + * light -> dark -> system -> light ... + */ +export function getNextThemeMode(current: ThemeMode): ThemeMode { + const index = THEME_MODES.indexOf(current); + const next = THEME_MODES[(index + 1) % THEME_MODES.length]; + + // Safety: modular arithmetic on a non-empty array always yields a valid index. + // The fallback satisfies the type checker without a non-null assertion. + return next ?? "system"; +} + +/** + * Resolve a `ThemeMode` to the effective `"light" | "dark"` value + * that should be applied to `document.documentElement.dataset.mode`. + * + * @param mode - The user-selected mode. + * @param prefers - Dark Whether the OS currently prefers dark mode. + */ +export function resolveThemeMode( + mode: ThemeMode, + prefersDark: boolean +): "dark" | "light" { + if (mode === "system") { + return prefersDark ? "dark" : "light"; + } + + return mode; +} + +/** + * Read the persisted theme mode from `localStorage`. + * + * Returns `"system"` when: + * - `localStorage` is unavailable (e.g. SSR, security restrictions) + * - the stored value is missing or not a recognised mode string + */ +export function loadThemeMode(): ThemeMode { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw !== null && isThemeMode(raw)) { + return raw; + } + + return "system"; + } catch { + return "system"; + } +} + +/** + * Persist the selected theme mode to `localStorage`. + * + * Silently swallows errors (e.g. quota exceeded, security restrictions) + * so the UI never breaks due to storage failures. + */ +export function saveThemeMode(mode: ThemeMode): void { + try { + localStorage.setItem(STORAGE_KEY, mode); + } catch { + // Silently ignore storage errors + } +} + +/** + * Apply the effective theme to the document root element. + * + * Sets `data-mode` on `` which Kumo's CSS uses for dark/light theming. + */ +export function applyThemeMode(mode: ThemeMode, prefersDark: boolean): void { + document.documentElement.dataset.mode = resolveThemeMode(mode, prefersDark); +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +function isThemeMode(value: string): value is ThemeMode { + return (THEME_MODES as readonly string[]).includes(value); +} From e4b2ef3190cf19b5c9fd5220ccf055dcffdd8407 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 16:00:04 +0100 Subject: [PATCH 10/34] Updated worker selector badge label from "current" to "Host" --- packages/local-explorer-ui/src/components/WorkerSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/WorkerSelector.tsx b/packages/local-explorer-ui/src/components/WorkerSelector.tsx index 0c207db375..dad7004ca3 100644 --- a/packages/local-explorer-ui/src/components/WorkerSelector.tsx +++ b/packages/local-explorer-ui/src/components/WorkerSelector.tsx @@ -116,7 +116,7 @@ export function WorkerSelector({ {worker.name} {worker.isSelf && selfWorker && ( - current + Host )} From 1169f50a3cd781de1fc6b6d4477f0244952b4b1d Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 16:16:56 +0100 Subject: [PATCH 11/34] Persist sidebar collapsed state to local storage --- .../src/__tests__/utils/sidebar-state.test.ts | 140 ++++++++++++++++-- .../local-explorer-ui/src/routes/__root.tsx | 16 +- .../src/utils/sidebar-state.ts | 48 +++++- 3 files changed, 189 insertions(+), 15 deletions(-) diff --git a/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts b/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts index 83a7b7f089..349142998c 100644 --- a/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts +++ b/packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts @@ -1,13 +1,17 @@ import { afterEach, beforeEach, describe, test, vi } from "vitest"; import { DEFAULT_GROUP_STATE, + DEFAULT_SIDEBAR_OPEN, loadGroupState, + loadSidebarOpenState, saveGroupState, + saveSidebarOpenState, SIDEBAR_GROUP_IDS, } from "../../utils/sidebar-state"; import type { SidebarGroupState } from "../../utils/sidebar-state"; -const STORAGE_KEY = "local-explorer.sidebar.groups.v1"; +const GROUPS_STORAGE_KEY = "local-explorer.sidebar.groups.v1"; +const OPEN_STORAGE_KEY = "local-explorer.sidebar.open.v1"; /** * Minimal localStorage stub scoped to each test. @@ -66,32 +70,32 @@ describe("sidebar-state", () => { test("returns defaults when stored value is not valid JSON", ({ expect, }) => { - storageStub.setItem(STORAGE_KEY, "not-json{{{"); + storageStub.setItem(GROUPS_STORAGE_KEY, "not-json{{{"); expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); }); test("returns defaults when stored value is null JSON", ({ expect }) => { - storageStub.setItem(STORAGE_KEY, "null"); + storageStub.setItem(GROUPS_STORAGE_KEY, "null"); expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); }); test("returns defaults when stored value is an array", ({ expect }) => { - storageStub.setItem(STORAGE_KEY, "[1,2,3]"); + storageStub.setItem(GROUPS_STORAGE_KEY, "[1,2,3]"); expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); }); test("returns defaults when stored value is a string", ({ expect }) => { - storageStub.setItem(STORAGE_KEY, '"hello"'); + storageStub.setItem(GROUPS_STORAGE_KEY, '"hello"'); expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); }); test("returns defaults when stored value is a number", ({ expect }) => { - storageStub.setItem(STORAGE_KEY, "42"); + storageStub.setItem(GROUPS_STORAGE_KEY, "42"); expect(loadGroupState()).toEqual(DEFAULT_GROUP_STATE); }); test("merges partial stored state onto defaults", ({ expect }) => { - storageStub.setItem(STORAGE_KEY, JSON.stringify({ d1: false })); + storageStub.setItem(GROUPS_STORAGE_KEY, JSON.stringify({ d1: false })); const state = loadGroupState(); expect(state.d1).toBe(false); expect(state.do).toBe(true); @@ -108,13 +112,13 @@ describe("sidebar-state", () => { r2: false, workflows: true, }; - storageStub.setItem(STORAGE_KEY, JSON.stringify(stored)); + storageStub.setItem(GROUPS_STORAGE_KEY, JSON.stringify(stored)); expect(loadGroupState()).toEqual(stored); }); test("ignores non-boolean values in stored object", ({ expect }) => { storageStub.setItem( - STORAGE_KEY, + GROUPS_STORAGE_KEY, JSON.stringify({ d1: "yes", do: 42, kv: null, r2: false }) ); const state = loadGroupState(); @@ -129,7 +133,7 @@ describe("sidebar-state", () => { test("ignores unknown keys in stored object", ({ expect }) => { storageStub.setItem( - STORAGE_KEY, + GROUPS_STORAGE_KEY, JSON.stringify({ d1: false, unknownGroup: false }) ); const state = loadGroupState(); @@ -155,7 +159,7 @@ describe("sidebar-state", () => { workflows: false, }; saveGroupState(state); - const raw = storageStub.getItem(STORAGE_KEY); + const raw = storageStub.getItem(GROUPS_STORAGE_KEY); expect(raw).not.toBeNull(); expect(JSON.parse(raw as string)).toEqual(state); }); @@ -191,4 +195,118 @@ describe("sidebar-state", () => { expect(state.kv).toBe(false); }); }); + + describe("DEFAULT_SIDEBAR_OPEN", () => { + test("defaults to true (expanded)", ({ expect }) => { + expect(DEFAULT_SIDEBAR_OPEN).toBe(true); + }); + }); + + describe("loadSidebarOpenState", () => { + test("returns default when nothing is stored", ({ expect }) => { + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns default when stored value is not valid JSON", ({ + expect, + }) => { + storageStub.setItem(OPEN_STORAGE_KEY, "not-json{{{"); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns default when stored value is null JSON", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, "null"); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns default when stored value is a string", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, '"hello"'); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns default when stored value is a number", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, "42"); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns default when stored value is an object", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, '{"open":true}'); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns default when stored value is an array", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, "[true]"); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + + test("returns true when stored value is true", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, "true"); + expect(loadSidebarOpenState()).toBe(true); + }); + + test("returns false when stored value is false", ({ expect }) => { + storageStub.setItem(OPEN_STORAGE_KEY, "false"); + expect(loadSidebarOpenState()).toBe(false); + }); + + test("returns default when localStorage.getItem throws", ({ expect }) => { + vi.spyOn(storageStub, "getItem").mockImplementation(() => { + throw new Error("SecurityError"); + }); + expect(loadSidebarOpenState()).toBe(DEFAULT_SIDEBAR_OPEN); + }); + }); + + describe("saveSidebarOpenState", () => { + test("persists true to `localStorage`", ({ expect }) => { + saveSidebarOpenState(true); + const raw = storageStub.getItem(OPEN_STORAGE_KEY); + expect(raw).toBe("true"); + }); + + test("persists false to `localStorage`", ({ expect }) => { + saveSidebarOpenState(false); + const raw = storageStub.getItem(OPEN_STORAGE_KEY); + expect(raw).toBe("false"); + }); + + test("does not throw when localStorage.setItem throws", ({ expect }) => { + vi.spyOn(storageStub, "setItem").mockImplementation(() => { + throw new Error("QuotaExceededError"); + }); + expect(() => saveSidebarOpenState(true)).not.toThrow(); + }); + }); + + describe("sidebar open round-trip", () => { + test("`loadSidebarOpenState` returns what `saveSidebarOpenState` persisted", ({ + expect, + }) => { + saveSidebarOpenState(false); + expect(loadSidebarOpenState()).toBe(false); + + saveSidebarOpenState(true); + expect(loadSidebarOpenState()).toBe(true); + }); + + test("multiple saves overwrite previous state", ({ expect }) => { + saveSidebarOpenState(false); + saveSidebarOpenState(true); + expect(loadSidebarOpenState()).toBe(true); + }); + }); + + describe("storage key isolation", () => { + test("sidebar open state and group state use separate keys", ({ + expect, + }) => { + saveSidebarOpenState(false); + saveGroupState({ ...DEFAULT_GROUP_STATE, d1: false }); + + // Each should be independently readable + expect(loadSidebarOpenState()).toBe(false); + expect(loadGroupState().d1).toBe(false); + expect(loadGroupState().kv).toBe(true); + }); + }); }); diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index f44d486706..b1f9c8ea00 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -16,6 +16,10 @@ import { } from "../api"; import { AppSidebar } from "../components/Sidebar"; import { filterVisibleWorkers } from "../components/WorkerSelector"; +import { + loadSidebarOpenState, + saveSidebarOpenState, +} from "../utils/sidebar-state"; import { applyThemeMode, getNextThemeMode, @@ -134,8 +138,14 @@ function RootLayout() { ); const router = useRouter(); + const [sidebarOpen, setSidebarOpen] = useState(loadSidebarOpenState); const [themeMode, setThemeMode] = useState(loadThemeMode); + const handleSidebarOpenChange = useCallback((open: boolean) => { + setSidebarOpen(open); + saveSidebarOpenState(open); + }, []); + const handleCycleTheme = useCallback(() => { const next = getNextThemeMode(themeMode); saveThemeMode(next); @@ -230,7 +240,11 @@ function RootLayout() { return (
- + Date: Tue, 7 Apr 2026 16:48:36 +0100 Subject: [PATCH 12/34] Added collapsed sidebare item groups --- .../src/components/Sidebar.tsx | 94 +++++++++++-------- .../src/components/SidebarGroupPopup.tsx | 76 +++++++++++++++ 2 files changed, 131 insertions(+), 39 deletions(-) create mode 100644 packages/local-explorer-ui/src/components/SidebarGroupPopup.tsx diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 5c15f06e56..83997ba988 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -15,6 +15,7 @@ import R2Icon from "../assets/icons/r2.svg?react"; import WorkflowsIcon from "../assets/icons/workflows.svg?react"; import { loadGroupState, saveGroupState } from "../utils/sidebar-state"; import { getNextThemeMode } from "../utils/theme-state"; +import { SidebarGroupPopup } from "./SidebarGroupPopup"; import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector"; import type { D1DatabaseResponse, @@ -265,46 +266,61 @@ export function AppSidebar({ /> )} - - {sidebarItemGroups.map((group) => ( - { - handleGroupOpenChange(group.groupId, open); - }} - > - } - > - {group.title} - - } - /> + {sidebar.open ? ( + + {sidebarItemGroups.map((group) => ( + { + handleGroupOpenChange(group.groupId, open); + }} + > + } + > + {group.title} + + } + /> - - {!group.error && group.items.length === 0 ? ( -
- {group.emptyLabel} -
- ) : ( - group.items.map((item) => ( - - - {item.label} - - - )) - )} -
-
- ))} -
+ + {!group.error && group.items.length === 0 ? ( +
+ {group.emptyLabel} +
+ ) : ( + group.items.map((item) => ( + + + {item.label} + + + )) + )} +
+
+ ))} +
+ ) : ( + + {sidebarItemGroups.map((group) => ( + } + items={group.items} + key={group.groupId} + title={group.title} + /> + ))} + + )} diff --git a/packages/local-explorer-ui/src/components/SidebarGroupPopup.tsx b/packages/local-explorer-ui/src/components/SidebarGroupPopup.tsx new file mode 100644 index 0000000000..ec05ddf90a --- /dev/null +++ b/packages/local-explorer-ui/src/components/SidebarGroupPopup.tsx @@ -0,0 +1,76 @@ +import { Sidebar, type SidebarMenuButtonProps } from "@cloudflare/kumo"; +import { Popover } from "@cloudflare/kumo/primitives/popover"; + +interface SidebarGroupItem { + href: string; + id: string; + isActive: boolean; + label: string; +} + +interface SidebarGroupPopupProps { + emptyLabel: string; + error: string | null; + icon: SidebarMenuButtonProps["icon"]; + items: SidebarGroupItem[]; + title: string; +} + +export function SidebarGroupPopup({ + emptyLabel, + error, + icon, + items, + title, +}: SidebarGroupPopupProps): JSX.Element { + const hasActiveItem = items.some((item) => item.isActive); + + return ( + + } + /> + + + + +
+ + {title} + +
+ +
+ {error ? ( +
{error}
+ ) : items.length === 0 ? ( +
+ {emptyLabel} +
+ ) : ( + items.map((item) => ( + + {item.label} + + )) + )} +
+
+
+
+
+ ); +} From b26f4190e147d61ed9f96756dfdfef6983b83630 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 16:56:41 +0100 Subject: [PATCH 13/34] Added spacing to sidebar items --- packages/local-explorer-ui/src/components/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 83997ba988..5b9485fea4 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -267,7 +267,7 @@ export function AppSidebar({ )} {sidebar.open ? ( - + {sidebarItemGroups.map((group) => ( ) : ( - + {sidebarItemGroups.map((group) => ( Date: Tue, 7 Apr 2026 16:56:52 +0100 Subject: [PATCH 14/34] Offset empty sidebar group text --- packages/local-explorer-ui/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 5b9485fea4..d996ff2157 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -288,7 +288,7 @@ export function AppSidebar({ {!group.error && group.items.length === 0 ? ( -
+
{group.emptyLabel}
) : ( From 355a43f4cdb696158aca1058ce2e496d44ce33b9 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 16:56:59 +0100 Subject: [PATCH 15/34] Fixed worker selector padding --- packages/local-explorer-ui/src/components/WorkerSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/WorkerSelector.tsx b/packages/local-explorer-ui/src/components/WorkerSelector.tsx index dad7004ca3..841202da4f 100644 --- a/packages/local-explorer-ui/src/components/WorkerSelector.tsx +++ b/packages/local-explorer-ui/src/components/WorkerSelector.tsx @@ -132,7 +132,7 @@ export function WorkerSelector({ ); if (sidebar.open) { - return
{selectRoot}
; + return
{selectRoot}
; } return ( From feb8307560bbec97b5f78c8df594fbb6cbf7dd1e Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 17:19:35 +0100 Subject: [PATCH 16/34] Upgrade `@cloudflare/kumo` to version `1.17.0` --- packages/local-explorer-ui/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/local-explorer-ui/package.json b/packages/local-explorer-ui/package.json index 0ae1810590..2b1551a414 100644 --- a/packages/local-explorer-ui/package.json +++ b/packages/local-explorer-ui/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@base-ui/react": "^1.1.0", - "@cloudflare/kumo": "^1.16.0", + "@cloudflare/kumo": "^1.17.0", "@cloudflare/workers-editor-shared": "^0.1.1", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c0d741cb..edf076b2e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1915,8 +1915,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@cloudflare/kumo': - specifier: ^1.16.0 - version: 1.16.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) + specifier: ^1.17.0 + version: 1.17.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) '@cloudflare/workers-editor-shared': specifier: ^0.1.1 version: 0.1.1(@cloudflare/style-const@6.1.3(react@19.2.4))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@6.1.3(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -5043,8 +5043,8 @@ packages: peerDependencies: react: ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0 - '@cloudflare/kumo@1.16.0': - resolution: {integrity: sha512-uCrj7jGPvdXj8lrdQBfMGKzV3JTDi7hUBsLf4jpirD7QHvZMsGe6XuU+KKvQFqDTmj5ELXQVES4YVoducxZ7Tg==} + '@cloudflare/kumo@1.17.0': + resolution: {integrity: sha512-BWa/EbsFAu5fBk4pLKjd+qZQH6jh6K1/vfLW/bIiCcqc/9641FuAME5CAQgCf7jHvkHrbMBxzg4jB6ZdJCW9FA==} hasBin: true peerDependencies: '@phosphor-icons/react': ^2.1.10 @@ -16476,7 +16476,7 @@ snapshots: dependencies: react: 19.2.4 - '@cloudflare/kumo@1.16.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)': + '@cloudflare/kumo@1.17.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.13)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)': dependencies: '@base-ui/react': 1.3.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@phosphor-icons/react': 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) From a5de881d0f83927210dab07b3f811fcce6099d6b Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Tue, 7 Apr 2026 17:24:25 +0100 Subject: [PATCH 17/34] Added changeset --- .changeset/update-local-explorer-sidebar.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/update-local-explorer-sidebar.md diff --git a/.changeset/update-local-explorer-sidebar.md b/.changeset/update-local-explorer-sidebar.md new file mode 100644 index 0000000000..8300df284a --- /dev/null +++ b/.changeset/update-local-explorer-sidebar.md @@ -0,0 +1,11 @@ +--- +"@cloudflare/local-explorer-ui": minor +--- + +Update local explorer sidebar with collapsible groups, theme persistence, and Kumo v1.17 + +Adds localStorage persistence for sidebar group expansion states and theme mode (light/dark/system). The sidebar now uses Kumo v1.17 primitives with collapsible groups and a theme toggle in the footer. + +Users can now cycle between light, dark, and system theme modes, and their preference will be persisted across sessions. + +Sidebar groups (D1, Durable Objects, KV, R2, Workflows) also remember their collapsed/expanded state. From 2e8b971f952cc94c014c3a393eb8d27dc08970c9 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 8 Apr 2026 10:12:53 +0100 Subject: [PATCH 18/34] Minor sidebare error / empty label fixes --- packages/local-explorer-ui/src/components/Sidebar.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index d996ff2157..cef5a33fa1 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -287,7 +287,11 @@ export function AppSidebar({ /> - {!group.error && group.items.length === 0 ? ( + {group.error ? ( +
+ {group.error} +
+ ) : group.items.length === 0 ? (
{group.emptyLabel}
From 3b3064877f8f7b8c11b550cb2a26c8901b1330d6 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 8 Apr 2026 10:25:24 +0100 Subject: [PATCH 19/34] Add strictly typed `worker` search param to all routes --- packages/local-explorer-ui/src/routes/d1/$databaseId.tsx | 3 ++- packages/local-explorer-ui/src/routes/do/$className.tsx | 3 +++ packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx | 3 +++ packages/local-explorer-ui/src/routes/r2/$bucketName.tsx | 3 +++ packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx | 2 +- .../local-explorer-ui/src/routes/workflows/$workflowName.tsx | 3 +++ 6 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx index 2148891509..8a17305646 100644 --- a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx +++ b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx @@ -36,8 +36,9 @@ export const Route = createFileRoute("/d1/$databaseId")({ tables, }; }, - validateSearch: (search) => ({ + validateSearch: (search): { table?: string; worker?: string } => ({ table: typeof search.table === "string" ? search.table : undefined, + worker: typeof search.worker === "string" ? search.worker : undefined, }), }); diff --git a/packages/local-explorer-ui/src/routes/do/$className.tsx b/packages/local-explorer-ui/src/routes/do/$className.tsx index 2482492fb4..62e2e2f908 100644 --- a/packages/local-explorer-ui/src/routes/do/$className.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className.tsx @@ -23,4 +23,7 @@ export const Route = createFileRoute("/do/$className")({ namespaceId: namespace.id, }; }, + validateSearch: (search: Record): { worker?: string } => ({ + worker: typeof search.worker === "string" ? search.worker : undefined, + }), }); diff --git a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx index 683788ffeb..5c143b1893 100644 --- a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx +++ b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx @@ -54,6 +54,9 @@ export const Route = createFileRoute("/kv/$namespaceId")({ hasMore: !!cursor, }; }, + validateSearch: (search: Record): { worker?: string } => ({ + worker: typeof search.worker === "string" ? search.worker : undefined, + }), }); // Helper functions for optimistic entry state updates diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx index 72c633d063..16bbfbec3b 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx @@ -2,4 +2,7 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; export const Route = createFileRoute("/r2/$bucketName")({ component: () => , + validateSearch: (search: Record): { worker?: string } => ({ + worker: typeof search.worker === "string" ? search.worker : undefined, + }), }); diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx index 06c0578020..b327c70989 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx @@ -35,8 +35,8 @@ export interface R2BucketSearch { export const Route = createFileRoute("/r2/$bucketName/")({ component: BucketView, validateSearch: (search: Record): R2BucketSearch => ({ - prefix: typeof search.prefix === "string" ? search.prefix : undefined, delimiter: search.delimiter === false ? false : true, + prefix: typeof search.prefix === "string" ? search.prefix : undefined, }), loaderDeps: ({ search }) => ({ prefix: search.prefix, diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx index f22d66f298..cd814a3985 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx @@ -7,4 +7,7 @@ export const Route = createFileRoute("/workflows/$workflowName")({ workflowName: params.workflowName, }; }, + validateSearch: (search: Record): { worker?: string } => ({ + worker: typeof search.worker === "string" ? search.worker : undefined, + }), }); From 9a5ae4a3cd3eb49662b0c8ca6a1d11fa1abdd83a Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 12:09:41 +0100 Subject: [PATCH 20/34] Minor DO class name search input border color fix --- packages/local-explorer-ui/src/routes/do/$className/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/routes/do/$className/index.tsx b/packages/local-explorer-ui/src/routes/do/$className/index.tsx index 3f77fe7b1a..dc4335a74a 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/index.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/index.tsx @@ -160,7 +160,7 @@ function NamespaceView() { setOpenInstanceInput(e.target.value)} From f7ad8475e6bea9b978f2bbbbe5928c0d9ebb1bb0 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 12:09:57 +0100 Subject: [PATCH 21/34] Fixed sidebar height --- packages/local-explorer-ui/src/routes/__root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index 3ce7b1389a..b2557064c1 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -83,7 +83,7 @@ function RootLayout() { return ( -
+
Date: Thu, 9 Apr 2026 12:13:10 +0100 Subject: [PATCH 22/34] Fixed sidebar toggle button hover background color --- packages/local-explorer-ui/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index ab491deac2..0ffa11c4b5 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -313,7 +313,7 @@ export function AppSidebar({ ); })()} - + ); From 05b957a09d346c2485508ccb7151cdc9d3cff074 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 12:25:46 +0100 Subject: [PATCH 23/34] Minor sidebar item group active state / spacing fixes --- .../src/components/Sidebar.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 0ffa11c4b5..836b4b1971 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -260,22 +260,24 @@ export function AppSidebar({ /> - {group.items.length === 0 ? ( -
- {group.emptyLabel} -
- ) : ( - group.items.map((item) => ( - + + {group.items.length === 0 ? ( +
+ {group.emptyLabel} +
+ ) : ( + group.items.map((item) => ( {item.label} -
- )) - )} + )) + )} +
))} From f9e3060dd58169d1a9a16602a841761bff4bb431 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 15:36:15 +0100 Subject: [PATCH 24/34] Removed sidebar resource disabled errors --- packages/local-explorer-ui/src/components/Sidebar.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 836b4b1971..b85041bac6 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -90,7 +90,6 @@ export function AppSidebar({ const sidebarItemGroups = [ { emptyLabel: "No databases", - // error: d1Error, groupId: "d1" as const, icon: D1Icon, items: d1Databases.map((db) => ({ @@ -107,7 +106,6 @@ export function AppSidebar({ }, { emptyLabel: "No SQLite namespaces", - // error: doError, groupId: "do" as const, icon: DOIcon, items: doNamespaces.map((ns) => ({ @@ -126,7 +124,6 @@ export function AppSidebar({ }, { emptyLabel: "No namespaces", - // error: kvError, groupId: "kv" as const, icon: KVIcon, items: kvNamespaces.map((ns) => ({ @@ -143,7 +140,6 @@ export function AppSidebar({ }, { emptyLabel: "No buckets", - // error: r2Error, groupId: "r2" as const, icon: R2Icon, items: r2Buckets.map((bucket) => ({ @@ -162,7 +158,6 @@ export function AppSidebar({ }, { emptyLabel: "No workflows", - // error: workflowsError, groupId: "workflows" as const, icon: WorkflowsIcon, items: workflows.map((wf) => ({ @@ -181,7 +176,6 @@ export function AppSidebar({ }, ] satisfies Array<{ emptyLabel: string; - // error: string | null; groupId: SidebarGroupId; icon: FC<{ className?: string }>; items: Array<{ From c1bf1ff787d1569b4aa47d48c2715a1c56d2300f Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 15:36:35 +0100 Subject: [PATCH 25/34] Minor type improvements --- packages/local-explorer-ui/src/components/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index b85041bac6..7e178a961f 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -18,7 +18,7 @@ import { getNextThemeMode } from "../utils/theme-state"; import { SidebarGroupPopup } from "./SidebarGroupPopup"; import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector"; import type { LocalExplorerWorkerBindings } from "../api"; -import type { SidebarGroupId } from "../utils/sidebar-state"; +import type { SidebarGroupId, SidebarGroupState } from "../utils/sidebar-state"; import type { ThemeMode } from "../utils/theme-state"; import type { FC } from "react"; @@ -65,7 +65,7 @@ export function AppSidebar({ const router = useRouter(); const sidebar = useSidebar(); - const [groupOpen, setGroupOpen] = useState(loadGroupState); + const [groupOpen, setGroupOpen] = useState(loadGroupState); const handleGroupOpenChange = useCallback( (groupId: SidebarGroupId, open: boolean) => { From 6c28b627776837d81364b9791aafe71716f7fa97 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 23:17:36 +0100 Subject: [PATCH 26/34] Minor sidebar item group margin fix --- packages/local-explorer-ui/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index 7e178a961f..18c2cc83c9 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -254,7 +254,7 @@ export function AppSidebar({ /> - + {group.items.length === 0 ? (
{group.emptyLabel} From 2f0a5daff93d68f7f033f66e8fb640de348aacb7 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 23:31:23 +0100 Subject: [PATCH 27/34] Temp: Patch sidebar collapse transition easing --- packages/local-explorer-ui/src/styles/tailwind.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/local-explorer-ui/src/styles/tailwind.css b/packages/local-explorer-ui/src/styles/tailwind.css index c18a7c5a78..61115a1661 100644 --- a/packages/local-explorer-ui/src/styles/tailwind.css +++ b/packages/local-explorer-ui/src/styles/tailwind.css @@ -42,6 +42,10 @@ } } +[data-sidebar="sidebar"] [data-closed]:not([data-ending-style]) { + height: var(--collapsible-panel-height) !important; +} + /* Animated Cloudflare Logo keyframes */ @keyframes animated-logo-container { 0% { From bda89327a4d12d254ed131597f9a74b66b96cbbe Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 23:56:16 +0100 Subject: [PATCH 28/34] Fixed DO E2E tests' `navigateToDOObjectByName` helper --- .../local-explorer-ui/src/__e2e__/utils.ts | 75 +++++++++++++------ 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index fb33535978..1d4304898e 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -144,27 +144,28 @@ export async function navigateToDOObject( */ export async function navigateToDOObjectByName( className: string, - table?: string + table?: string, + objectName: string = "test-object" ): Promise { await navigateToDOClass(className); await waitForText(className); - const openStudioLink = page.locator('a:has-text("Open Studio")').first(); - await openStudioLink.waitFor({ state: "visible", timeout: 10_000 }); - - const href = await openStudioLink.getAttribute("href"); - if (!href) { - throw new Error("Could not find href on Open Studio link"); - } + await fillByPlaceholder("Enter instance name or hex ID...", objectName); + await page.getByRole("button", { name: "Open Studio" }).click(); + await waitForPageLoad(); - // Extract the object ID from the href (format: /cdn-cgi/explorer/do/{className}/{objectId}) - const match = href.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([a-f0-9]+)/); + // Extract the object ID from the current URL after navigation. + const objectPath = new URL(page.url()).pathname; + const match = objectPath.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([^/?#]+)/); if (!match || !match[1]) { - throw new Error(`Could not extract object ID from href: ${href}`); + throw new Error(`Could not extract object ID from URL path: ${objectPath}`); } + const objectId: string = match[1]; - await navigateToDOObject(className, objectId, table); + if (table) { + await navigateToDOObject(className, objectId, table); + } return objectId; } @@ -178,9 +179,36 @@ export async function waitForText( timeout?: number; } ): Promise { - await page.waitForSelector(`text=${text}`, { - timeout: options?.timeout ?? WAIT_OPTIONS.timeout, - }); + await page.waitForFunction( + (expectedText: string) => { + const elements = Array.from(document.querySelectorAll("body *")); + + return elements.some((element) => { + if (!(element instanceof HTMLElement)) { + return false; + } + + const textContent = element.textContent?.trim(); + if (!textContent || !textContent.includes(expectedText)) { + return false; + } + + const styles = window.getComputedStyle(element); + if ( + styles.display === "none" || + styles.visibility === "hidden" || + styles.opacity === "0" + ) { + return false; + } + + const rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); + }, + text, + { timeout: options?.timeout ?? WAIT_OPTIONS.timeout } + ); } /** @@ -306,21 +334,20 @@ export async function runQuery(): Promise { * Run all SQL statements in the editor using the dropdown menu. */ export async function runAllQueries(): Promise { - const runDropdown = page.locator( - 'button:has(svg[class*="CaretDownIcon"]), button:has-text("Run") + button' - ); - await runDropdown.click(); - - await page.getByText("Run all statements").click(); + const isMac = process.platform === "darwin"; + const runAllKey = isMac ? "Meta+Shift+Enter" : "Control+Shift+Enter"; + await page.keyboard.press(runAllKey); } /** * Open the table selector dropdown in the breadcrumb bar. */ export async function openTableSelector(): Promise { - // The `TableSelect` uses a `Select.Trigger` which contains either "Select table" or the table name - // Find the `Select` trigger by looking for the text + caret icon combo - const tableSelector = page.locator('text="Select table"').first(); + // The `TableSelect` trigger text is dynamic ("Select table" or current table name). + // Target the table selector trigger by its unique utility class on the breadcrumb row. + const tableSelector = page + .locator('button[class*="-mx-1.5"]:visible') + .first(); await tableSelector.click(); await page.waitForSelector('[role="listbox"]', { From c9540e6b6bd22dc89f32544967d702b96d94f378 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Fri, 10 Apr 2026 00:01:14 +0100 Subject: [PATCH 29/34] Added TODO --- packages/local-explorer-ui/src/styles/tailwind.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/local-explorer-ui/src/styles/tailwind.css b/packages/local-explorer-ui/src/styles/tailwind.css index 61115a1661..d17d450de1 100644 --- a/packages/local-explorer-ui/src/styles/tailwind.css +++ b/packages/local-explorer-ui/src/styles/tailwind.css @@ -42,6 +42,8 @@ } } +/* TODO: Remove this once this PR is merged and release */ +/* https://github.com/cloudflare/kumo/pull/388 */ [data-sidebar="sidebar"] [data-closed]:not([data-ending-style]) { height: var(--collapsible-panel-height) !important; } From 2419395bd64f65882bbbe7c4ca7cce05fe149fb0 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Fri, 10 Apr 2026 09:34:21 +0100 Subject: [PATCH 30/34] Streamlined R2 breadcrumb E2E checks --- .../src/__e2e__/r2/r2-bucket.spec.ts | 19 ++++-- .../local-explorer-ui/src/__e2e__/utils.ts | 67 ++++++++++--------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts b/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts index 199627238c..87a928c3d7 100644 --- a/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts +++ b/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts @@ -8,6 +8,7 @@ import { navigateToR2Object, page, seedR2, + waitForBreadcrumbText, waitForDialog, waitForTableRows, waitForText, @@ -88,8 +89,8 @@ describe("R2 Bucket", () => { await navigateToR2Bucket("my-bucket"); await waitForTableRows(1); - await waitForText("R2"); - await waitForText("my-bucket"); + await waitForBreadcrumbText("R2"); + await waitForBreadcrumbText("my-bucket"); }); test("navigates into a directory and updates breadcrumbs", async () => { @@ -273,10 +274,10 @@ describe("R2 Bucket", () => { await navigateToR2Object("my-bucket", "documents/report.txt"); // Breadcrumbs should show: R2 > my-bucket > documents > report.txt - await waitForText("R2"); - await waitForText("my-bucket"); - await waitForText("documents"); - await waitForText("report.txt"); + await waitForBreadcrumbText("R2"); + await waitForBreadcrumbText("my-bucket"); + await waitForBreadcrumbText("documents"); + await waitForBreadcrumbText("report.txt"); }); }); @@ -358,7 +359,11 @@ describe("R2 Bucket", () => { await waitForDialog(); await waitForText("Delete object?"); - await waitForText("readme.txt"); + + // "readme.txt" appears in multiple places (sidebar, breadcrumbs, heading), + // so scope the assertion to the dialog. + const dialog = page.getByRole("dialog"); + await dialog.getByText("readme.txt").waitFor({ state: "visible" }); }); test("deletes object from detail page and navigates back", async () => { diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index 1d4304898e..fab90340e4 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -53,7 +53,7 @@ export async function seedDO(objectId: string = "test-object"): Promise { */ const WAIT_OPTIONS = { interval: 100, - timeout: 10_000, + timeout: 1_000, }; /** @@ -179,36 +179,34 @@ export async function waitForText( timeout?: number; } ): Promise { - await page.waitForFunction( - (expectedText: string) => { - const elements = Array.from(document.querySelectorAll("body *")); - - return elements.some((element) => { - if (!(element instanceof HTMLElement)) { - return false; - } - - const textContent = element.textContent?.trim(); - if (!textContent || !textContent.includes(expectedText)) { - return false; - } - - const styles = window.getComputedStyle(element); - if ( - styles.display === "none" || - styles.visibility === "hidden" || - styles.opacity === "0" - ) { - return false; - } - - const rect = element.getBoundingClientRect(); - return rect.width > 0 && rect.height > 0; - }); - }, - text, - { timeout: options?.timeout ?? WAIT_OPTIONS.timeout } + await page.waitForSelector(`text=${text}`, { + timeout: options?.timeout ?? WAIT_OPTIONS.timeout, + }); +} + +/** + * Wait for text to appear inside the breadcrumb navigation bar. + * + * Use this instead of `waitForText` when the same text also appears in the + * sidebar (e.g. bucket names, object keys) to avoid Playwright resolving + * to the wrong element. + */ +export async function waitForBreadcrumbText( + text: string, + options?: { + timeout?: number; + } +): Promise { + // The Kumo breadcrumb `
) : ( items.map((item) => ( - {item.label} - + )) )}
From cd3904abf43ad580f1e1da0ef4b09b4ab8f91557 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Fri, 10 Apr 2026 11:25:41 +0100 Subject: [PATCH 33/34] Minor code formatting --- packages/local-explorer-ui/src/__e2e__/utils.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index fab90340e4..09a9fa02bb 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -203,10 +203,13 @@ export async function waitForBreadcrumbText( const desktopBreadcrumb = page.locator( 'nav[aria-label="breadcrumb"] > .hidden.sm\\:contents' ); - await desktopBreadcrumb.getByText(text).first().waitFor({ - state: "visible", - timeout: options?.timeout ?? WAIT_OPTIONS.timeout, - }); + await desktopBreadcrumb + .getByText(text) + .first() + .waitFor({ + state: "visible", + timeout: options?.timeout ?? WAIT_OPTIONS.timeout, + }); } /** From 01b4b8ded6df05263e2a8796f066f6d64f1b2700 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Fri, 10 Apr 2026 11:56:49 +0100 Subject: [PATCH 34/34] Restore e2e test timeout of 10 seconds --- packages/local-explorer-ui/src/__e2e__/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index 09a9fa02bb..78b6ba8e20 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -53,7 +53,7 @@ export async function seedDO(objectId: string = "test-object"): Promise { */ const WAIT_OPTIONS = { interval: 100, - timeout: 1_000, + timeout: 10_000, }; /**