From 774b8633dc3b25f63510eaeeda66db9d6fc5b1f6 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 22 May 2026 18:05:05 +0100 Subject: [PATCH] Migrate frontend from Svelte 5 to React 18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After hours of fighting Svelte 5's prop-reactivity through the recursive Pane → SplitNode → LeafPane chain (props captured at mount, never updated; context+getter pattern crashed; DOM-direct workarounds created zombie-split click-intercept bugs), we checkpointed the Svelte version (branch svelte-archive at e9015b2, tarball at D:\archives\tiletopia-svelte-2026-05-22.tar.gz) and rewrote the frontend in React. Kept verbatim: - All of src-tauri/ (Rust backend, Tauri config, icons) - scripts/ (make-icon.py, release.sh) - README.md, CLAUDE.md, memory.md - src/lib/layout/tree.ts (pure TS — 43 tests still pass) - src/ipc.ts (Tauri command wrappers) Rewrote in React: - src/App.tsx (top-level state via useState, OrchestrationProvider for descendants via React.Context) - src/lib/layout/orchestration.tsx (React Context API for shared state — known-reliable reactivity, no Svelte 5 wall) - src/lib/layout/Pane.tsx (recursive dispatcher) - src/lib/layout/SplitNode.tsx (draggable gutter, local ratio state) - src/lib/layout/LeafPane.tsx (toolbar + XtermPane) - src/components/XtermPane.tsx (xterm.js wrapper, refs for callbacks) - src/components/Notifications.tsx, Palette.tsx Build: Vite + @vitejs/plugin-react. TypeScript strict. Same Tauri 2 config. Verified: pnpm check (clean), pnpm test (43/43 pass). Not yet verified: pnpm tauri dev — that requires the Windows host. Co-Authored-By: Claude Opus 4.7 (1M context) --- index.html | 6 +- package.json | 15 +- pnpm-lock.yaml | 621 ++++++++++++++++--------- src/App.css | 69 +++ src/App.svelte | 380 --------------- src/App.tsx | 372 +++++++++++++++ src/components/Notifications.css | 62 +++ src/components/Notifications.svelte | 81 ---- src/components/Notifications.tsx | 34 ++ src/components/Palette.css | 99 ++++ src/components/Palette.svelte | 188 -------- src/components/Palette.tsx | 108 +++++ src/components/XtermPane.svelte | 183 -------- src/components/XtermPane.tsx | 222 +++++++++ src/lib/layout/LeafPane.css | 170 +++++++ src/lib/layout/LeafPane.svelte | 400 ---------------- src/lib/layout/LeafPane.tsx | 263 +++++++++++ src/lib/layout/Pane.svelte | 19 - src/lib/layout/Pane.tsx | 16 + src/lib/layout/SplitNode.css | 36 ++ src/lib/layout/SplitNode.svelte | 116 ----- src/lib/layout/SplitNode.tsx | 79 ++++ src/lib/layout/orchestration.svelte.ts | 143 ------ src/lib/layout/orchestration.tsx | 58 +++ src/main.ts | 8 - src/main.tsx | 13 + src/styles.css | 76 +-- svelte.config.js | 5 - tsconfig.app.json | 20 + tsconfig.json | 23 +- tsconfig.node.json | 17 +- vite.config.ts | 10 +- 32 files changed, 2087 insertions(+), 1825 deletions(-) create mode 100644 src/App.css delete mode 100644 src/App.svelte create mode 100644 src/App.tsx create mode 100644 src/components/Notifications.css delete mode 100644 src/components/Notifications.svelte create mode 100644 src/components/Notifications.tsx create mode 100644 src/components/Palette.css delete mode 100644 src/components/Palette.svelte create mode 100644 src/components/Palette.tsx delete mode 100644 src/components/XtermPane.svelte create mode 100644 src/components/XtermPane.tsx create mode 100644 src/lib/layout/LeafPane.css delete mode 100644 src/lib/layout/LeafPane.svelte create mode 100644 src/lib/layout/LeafPane.tsx delete mode 100644 src/lib/layout/Pane.svelte create mode 100644 src/lib/layout/Pane.tsx create mode 100644 src/lib/layout/SplitNode.css delete mode 100644 src/lib/layout/SplitNode.svelte create mode 100644 src/lib/layout/SplitNode.tsx delete mode 100644 src/lib/layout/orchestration.svelte.ts create mode 100644 src/lib/layout/orchestration.tsx delete mode 100644 src/main.ts create mode 100644 src/main.tsx delete mode 100644 svelte.config.js create mode 100644 tsconfig.app.json diff --git a/index.html b/index.html index 219b15e..3ce6c33 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ tiletopia -
- +
+ diff --git a/package.json b/package.json index 2abe720..27babf0 100644 --- a/package.json +++ b/package.json @@ -5,24 +5,25 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc -b && vite build", "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json", "test": "vitest run", "test:watch": "vitest", + "check": "tsc --noEmit", "tauri": "tauri" }, "dependencies": { "@tauri-apps/api": "^2.0.0", "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0" + "@xterm/xterm": "^5.5.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^4.0.0", "@tauri-apps/cli": "^2.0.0", - "@tsconfig/svelte": "^5.0.4", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.6.0", "vite": "^5.4.0", "vitest": "^2.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 745c1ff..e490123 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,22 +17,25 @@ importers: '@xterm/xterm': specifier: ^5.5.0 version: 5.5.0 + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) devDependencies: - '@sveltejs/vite-plugin-svelte': - specifier: ^4.0.0 - version: 4.0.4(svelte@5.55.9)(vite@5.4.21) '@tauri-apps/cli': specifier: ^2.0.0 version: 2.11.2 - '@tsconfig/svelte': - specifier: ^5.0.4 - version: 5.0.8 - svelte: - specifier: ^5.0.0 - version: 5.55.9 - svelte-check: - specifier: ^4.0.0 - version: 4.4.8(svelte@5.55.9)(typescript@5.9.3) + '@types/react': + specifier: ^18.3.0 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.29) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21) typescript: specifier: ^5.6.0 version: 5.9.3 @@ -45,6 +48,89 @@ importers: packages: + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -199,6 +285,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.60.4': resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} cpu: [arm] @@ -337,26 +426,6 @@ packages: cpu: [x64] os: [win32] - '@sveltejs/acorn-typescript@1.0.10': - resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} - peerDependencies: - acorn: ^8.9.0 - - '@sveltejs/vite-plugin-svelte-inspector@3.0.1': - resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22} - peerDependencies: - '@sveltejs/vite-plugin-svelte': ^4.0.0-next.0||^4.0.0 - svelte: ^5.0.0-next.96 || ^5.0.0 - vite: ^5.0.0 - - '@sveltejs/vite-plugin-svelte@4.0.4': - resolution: {integrity: sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22} - peerDependencies: - svelte: ^5.0.0-next.96 || ^5.0.0 - vite: ^5.0.0 - '@tauri-apps/api@2.11.0': resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} @@ -436,8 +505,17 @@ packages: engines: {node: '>= 10'} hasBin: true - '@tsconfig/svelte@5.0.8': - resolution: {integrity: sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -445,8 +523,22 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.29': + resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -485,27 +577,27 @@ packages: '@xterm/xterm@5.5.0': resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -514,13 +606,11 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -535,12 +625,8 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + electron-to-chromium@1.5.360: + resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -550,16 +636,9 @@ packages: engines: {node: '>=12'} hasBin: true - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - - esrap@2.2.9: - resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} - peerDependencies: - '@typescript-eslint/types': ^8.2.0 - peerDependenciesMeta: - '@typescript-eslint/types': - optional: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -568,40 +647,41 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} + hasBin: true - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -610,6 +690,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-releases@2.0.45: + resolution: {integrity: sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==} + engines: {node: '>=18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -624,18 +708,30 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -650,18 +746,6 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - svelte-check@4.4.8: - resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' - - svelte@5.55.9: - resolution: {integrity: sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg==} - engines: {node: '>=18'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -685,6 +769,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -721,14 +811,6 @@ packages: terser: optional: true - vitefu@1.1.3: - resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - vite: - optional: true - vitest@2.1.9: resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -759,11 +841,123 @@ packages: engines: {node: '>=8'} hasBin: true - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} snapshots: + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -852,6 +1046,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.60.4': optional: true @@ -927,32 +1123,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true - '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': - dependencies: - acorn: 8.16.0 - - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.55.9)(vite@5.4.21))(svelte@5.55.9)(vite@5.4.21)': - dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.55.9)(vite@5.4.21) - debug: 4.4.3 - svelte: 5.55.9 - vite: 5.4.21 - transitivePeerDependencies: - - supports-color - - '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.55.9)(vite@5.4.21)': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.55.9)(vite@5.4.21))(svelte@5.55.9)(vite@5.4.21) - debug: 4.4.3 - deepmerge: 4.3.1 - kleur: 4.1.5 - magic-string: 0.30.21 - svelte: 5.55.9 - vite: 5.4.21 - vitefu: 1.1.3(vite@5.4.21) - transitivePeerDependencies: - - supports-color - '@tauri-apps/api@2.11.0': {} '@tauri-apps/cli-darwin-arm64@2.11.2': @@ -1002,13 +1172,53 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 '@tauri-apps/cli-win32-x64-msvc': 2.11.2 - '@tsconfig/svelte@5.0.8': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} - '@types/trusted-types@2.0.7': {} + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.29)': + dependencies: + '@types/react': 18.3.29 + + '@types/react@18.3.29': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21 + transitivePeerDependencies: + - supports-color '@vitest/expect@2.1.9': dependencies: @@ -1056,16 +1266,22 @@ snapshots: '@xterm/xterm@5.5.0': {} - acorn@8.16.0: {} - - aria-query@5.3.1: {} - assertion-error@2.0.1: {} - axobject-query@4.1.0: {} + baseline-browser-mapping@2.10.31: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.360 + node-releases: 2.0.45 + update-browserslist-db: 1.2.3(browserslist@4.28.2) cac@6.7.14: {} + caniuse-lite@1.0.30001793: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -1076,11 +1292,9 @@ snapshots: check-error@2.1.3: {} - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 + convert-source-map@2.0.0: {} - clsx@2.1.1: {} + csstype@3.2.3: {} debug@4.4.3: dependencies: @@ -1088,9 +1302,7 @@ snapshots: deep-eql@5.0.2: {} - deepmerge@4.3.1: {} - - devalue@5.8.1: {} + electron-to-chromium@1.5.360: {} es-module-lexer@1.7.0: {} @@ -1120,11 +1332,7 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esm-env@1.2.2: {} - - esrap@2.2.9: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + escalade@3.2.0: {} estree-walker@3.0.3: dependencies: @@ -1132,31 +1340,37 @@ snapshots: expect-type@1.3.0: {} - fdir@6.5.0: {} - fsevents@2.3.3: optional: true - is-reference@3.0.3: + gensync@1.0.0-beta.2: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + loose-envify@1.4.0: dependencies: - '@types/estree': 1.0.9 - - kleur@4.1.5: {} - - locate-character@3.0.0: {} + js-tokens: 4.0.0 loupe@3.2.1: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - mri@1.2.0: {} - ms@2.1.3: {} nanoid@3.3.12: {} + node-releases@2.0.45: {} + pathe@1.1.2: {} pathval@2.0.1: {} @@ -1169,7 +1383,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - readdirp@4.1.2: {} + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 rollup@4.60.4: dependencies: @@ -1202,9 +1426,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 - sade@1.8.1: + scheduler@0.23.2: dependencies: - mri: 1.2.0 + loose-envify: 1.4.0 + + semver@6.3.1: {} siginfo@2.0.0: {} @@ -1214,39 +1440,6 @@ snapshots: std-env@3.10.0: {} - svelte-check@4.4.8(svelte@5.55.9)(typescript@5.9.3): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - chokidar: 4.0.3 - fdir: 6.5.0 - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.55.9 - typescript: 5.9.3 - transitivePeerDependencies: - - picomatch - - svelte@5.55.9: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) - '@types/estree': 1.0.9 - '@types/trusted-types': 2.0.7 - acorn: 8.16.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.8.1 - esm-env: 1.2.2 - esrap: 2.2.9 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - transitivePeerDependencies: - - '@typescript-eslint/types' - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1259,6 +1452,12 @@ snapshots: typescript@5.9.3: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + vite-node@2.1.9: dependencies: cac: 6.7.14 @@ -1285,10 +1484,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitefu@1.1.3(vite@5.4.21): - optionalDependencies: - vite: 5.4.21 - vitest@2.1.9: dependencies: '@vitest/expect': 2.1.9 @@ -1327,4 +1522,4 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - zimmerframe@1.1.4: {} + yallist@3.1.1: {} diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..eab9bdd --- /dev/null +++ b/src/App.css @@ -0,0 +1,69 @@ +.app { + display: flex; + flex-direction: column; + height: 100vh; + background: #0c0c0c; +} + +.titlebar { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 12px; + padding: 6px 12px; + background: #1a1a1a; + border-bottom: 1px solid #2a2a2a; + font-size: 12px; + color: #aaa; + user-select: none; +} +.titlebar .label { + font-weight: 600; + color: #ddd; +} + +.distros, .presets { + display: flex; + gap: 4px; + align-items: center; +} +.distro-btn, .preset-btn, .palette-btn { + font: inherit; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + font-size: 11px; + background: #222; + color: #aaa; + border: 1px solid #333; + border-radius: 3px; + padding: 2px 8px; + cursor: pointer; +} +.distro-btn:hover, .preset-btn:hover, .palette-btn:hover { + background: #2a2a2a; + color: #ddd; +} +.distro-btn.active { + background: #1a3a5c; + color: #cce6ff; + border-color: #2a5a8c; +} +.preset-btn { + min-width: 28px; + text-align: center; +} +.muted { + color: #666; + font-style: italic; +} +.layout-info { + margin-left: auto; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + color: #777; + font-size: 11px; +} + +.pane-wrap { + flex: 1 1 auto; + min-height: 0; + position: relative; +} diff --git a/src/App.svelte b/src/App.svelte deleted file mode 100644 index 11ed174..0000000 --- a/src/App.svelte +++ /dev/null @@ -1,380 +0,0 @@ - - -
-
- tiletopia - - - {#if orch.distros.length === 0} - no distros enumerated - {:else} - default: - {#each orch.distros as d} - - {/each} - {/if} - - - - layout: - - - - - - - - - - - - {leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"} - -
- -
- {#if ready} - {#key renderKey} - - {/key} - {/if} -
- - orch.dismiss(id)} - /> - - {#if paletteOpen} - (paletteOpen = false)} - /> - {/if} -
- - diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..5174ceb --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,372 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + listDistros, + loadWorkspace, + saveWorkspace, + writeToPane, + killPane, + type PaneId, +} from "./ipc"; +import { + type TreeNode, + type NodeId, + type Orientation, + type LeafNode, + newLeaf, + splitLeaf, + closeLeaf, + findLeaf, + leafCount, + walkLeaves, + changeDistro, + changeLabel, + toggleBroadcast as toggleBroadcastInTree, + serialize, + deserialize, + presetSingle, + presetTwoColumns, + presetThreeColumns, + presetTwoRows, + presetTwoByTwo, +} from "./lib/layout/tree"; +import { OrchestrationProvider, type Orchestration } from "./lib/layout/orchestration"; +import Pane from "./lib/layout/Pane"; +import Notifications, { type Toast } from "./components/Notifications"; +import Palette from "./components/Palette"; +import "./App.css"; + +const LEGACY_STORAGE_KEY = "tiletopia.tree.v1"; +const SAVE_DEBOUNCE_MS = 500; + +function isInteractiveDistro(name: string): boolean { + return !name.toLowerCase().startsWith("docker-desktop"); +} + +export default function App() { + // ---- top-level state ----------------------------------------------------- + const [tree, setTree] = useState(() => newLeaf()); + const [activeLeafId, setActiveLeafId] = useState(null); + const [distros, setDistros] = useState([]); + const [defaultDistro, setDefaultDistro] = useState(undefined); + const [ready, setReady] = useState(false); + const [notifications, setNotifications] = useState([]); + const [paletteOpen, setPaletteOpen] = useState(false); + + // ---- non-reactive lookups ----------------------------------------------- + const paneIdByLeafRef = useRef>(new Map()); + const nextNotifIdRef = useRef(1); + const treeRef = useRef(tree); + useEffect(() => { + treeRef.current = tree; + }, [tree]); + + // ---- mount: load workspace + distros ------------------------------------ + useEffect(() => { + let cancelled = false; + (async () => { + let loaded: TreeNode | null = null; + try { + const json = await loadWorkspace(); + if (json) loaded = deserialize(json); + } catch (e) { + console.warn("loadWorkspace failed:", e); + } + if (!loaded) { + try { + const legacy = localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacy) { + loaded = deserialize(legacy); + if (loaded) void saveWorkspace(legacy); + localStorage.removeItem(LEGACY_STORAGE_KEY); + } + } catch (e) { + console.warn("legacy localStorage migration failed:", e); + } + } + + let resolvedDistros: string[] = []; + let resolvedDefault: string | undefined; + try { + resolvedDistros = await listDistros(); + resolvedDefault = + resolvedDistros.find(isInteractiveDistro) ?? resolvedDistros[0]; + } catch (e) { + console.warn("list_distros failed:", e); + } + + if (cancelled) return; + if (loaded) { + if (resolvedDefault) backfillDistro(loaded, resolvedDefault); + setTree(loaded); + } else if (resolvedDefault) { + setTree(newLeaf({ distro: resolvedDefault })); + } + setDistros(resolvedDistros); + setDefaultDistro(resolvedDefault); + setReady(true); + })(); + return () => { + cancelled = true; + }; + }, []); + + // ---- debounced save ------------------------------------------------------ + useEffect(() => { + if (!ready) return; + const id = window.setTimeout(() => { + saveWorkspace(serialize(tree)).catch((e) => + console.warn("saveWorkspace failed:", e), + ); + }, SAVE_DEBOUNCE_MS); + return () => clearTimeout(id); + }, [tree, ready]); + + // ---- Ctrl+K palette toggle (capture phase to beat xterm) ---------------- + useEffect(() => { + function onKey(e: KeyboardEvent) { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + e.stopPropagation(); + setPaletteOpen((v) => !v); + } + } + window.addEventListener("keydown", onKey, true); + return () => window.removeEventListener("keydown", onKey, true); + }, []); + + // ---- focus polling → setActive (xterm.js eats pointerdown) -------------- + useEffect(() => { + let lastLeafId: string | null = null; + const interval = window.setInterval(() => { + const el = document.activeElement; + const leafEl = el?.closest("[data-leaf-id]"); + const id = leafEl?.getAttribute("data-leaf-id") ?? null; + if (id && id !== lastLeafId) { + lastLeafId = id; + setActiveLeafId(id); + } + }, 250); + return () => clearInterval(interval); + }, []); + + // ---- orchestration callbacks -------------------------------------------- + const split = useCallback( + (leafId: NodeId, orientation: Orientation) => { + setTree((t) => { + const parent = findLeaf(t, leafId); + const inherit = parent + ? { distro: parent.distro ?? defaultDistro, cwd: parent.cwd } + : { distro: defaultDistro }; + return splitLeaf(t, leafId, orientation, inherit); + }); + }, + [defaultDistro], + ); + + const close = useCallback( + (leafId: NodeId) => { + const paneId = paneIdByLeafRef.current.get(leafId); + if (paneId != null) { + void killPane(paneId).catch((e) => console.warn("killPane failed:", e)); + paneIdByLeafRef.current.delete(leafId); + } + setTree((t) => closeLeaf(t, leafId) ?? newLeaf({ distro: defaultDistro })); + setActiveLeafId((cur) => (cur === leafId ? null : cur)); + }, + [defaultDistro], + ); + + const setDistro = useCallback((leafId: NodeId, distro: string) => { + setTree((t) => changeDistro(t, leafId, distro)); + }, []); + + const setLabel = useCallback((leafId: NodeId, label: string | undefined) => { + setTree((t) => changeLabel(t, leafId, label)); + }, []); + + const toggleBroadcast = useCallback((leafId: NodeId) => { + setTree((t) => toggleBroadcastInTree(t, leafId)); + }, []); + + const setActive = useCallback((leafId: NodeId) => { + setActiveLeafId(leafId); + }, []); + + const registerPaneId = useCallback( + (leafId: NodeId, paneId: PaneId | null) => { + if (paneId == null) paneIdByLeafRef.current.delete(leafId); + else paneIdByLeafRef.current.set(leafId, paneId); + }, + [], + ); + + const broadcastFrom = useCallback( + (originLeafId: NodeId, dataB64: string) => { + let peers = 0; + for (const leaf of walkLeaves(treeRef.current)) { + if (leaf.id === originLeafId) continue; + if (!leaf.broadcast) continue; + const paneId = paneIdByLeafRef.current.get(leaf.id); + if (paneId == null) continue; + peers++; + writeToPane(paneId, dataB64).catch((e) => + console.warn("broadcast write failed:", e), + ); + } + if (peers > 0) { + console.log("[tiletopia] broadcastFrom", originLeafId, "→", peers, "peer(s)"); + } + }, + [], + ); + + const notify = useCallback((message: string) => { + const id = nextNotifIdRef.current++; + setNotifications((ns) => [...ns, { id, message }]); + window.setTimeout(() => { + setNotifications((ns) => ns.filter((n) => n.id !== id)); + }, 5000); + }, []); + + const dismissNotification = useCallback((id: number) => { + setNotifications((ns) => ns.filter((n) => n.id !== id)); + }, []); + + const orch = useMemo( + () => ({ + activeLeafId, + distros, + split, + close, + setDistro, + setLabel, + toggleBroadcast, + setActive, + registerPaneId, + broadcastFrom, + notify, + }), + [ + activeLeafId, + distros, + split, + close, + setDistro, + setLabel, + toggleBroadcast, + setActive, + registerPaneId, + broadcastFrom, + notify, + ], + ); + + const applyPreset = useCallback( + (make: (d: { distro?: string }) => TreeNode) => { + const count = leafCount(tree); + if ( + count > 1 && + !window.confirm( + `Replace current layout (${count} panes)? This kills all open shells.`, + ) + ) { + return; + } + setTree(make({ distro: defaultDistro })); + }, + [tree, defaultDistro], + ); + + const paletteLeaves = useMemo( + () => (paletteOpen ? Array.from(walkLeaves(tree)) : []), + [paletteOpen, tree], + ); + + const onPalettePick = useCallback((leafId: string) => { + setActiveLeafId(leafId); + setPaletteOpen(false); + }, []); + + return ( +
+
+ tiletopia + + + {distros.length === 0 ? ( + no distros enumerated + ) : ( + <> + default: + {distros.map((d) => ( + + ))} + + )} + + + + layout: + + + + + + + + + + + + {leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"} + +
+ +
+ {ready && ( + + + + )} +
+ + + + {paletteOpen && ( + setPaletteOpen(false)} + /> + )} +
+ ); +} + +function backfillDistro(node: TreeNode, fallback: string) { + if (node.kind === "leaf") { + if (!node.distro) node.distro = fallback; + } else { + backfillDistro(node.a, fallback); + backfillDistro(node.b, fallback); + } +} diff --git a/src/components/Notifications.css b/src/components/Notifications.css new file mode 100644 index 0000000..f08bc2d --- /dev/null +++ b/src/components/Notifications.css @@ -0,0 +1,62 @@ +.toast-stack { + position: fixed; + top: 36px; + right: 12px; + display: flex; + flex-direction: column; + gap: 6px; + z-index: 100; + pointer-events: none; +} + +.toast { + pointer-events: auto; + display: flex; + align-items: center; + gap: 10px; + min-width: 220px; + max-width: 320px; + padding: 8px 10px 8px 14px; + background: #1f1f1f; + color: #ddd; + border: 1px solid #3a5a8c; + border-left-width: 3px; + border-radius: 4px; + font-size: 12px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); + animation: slide-in 180ms ease-out; +} + +.toast-msg { + flex: 1 1 auto; + line-height: 1.3; + word-break: break-word; +} + +.toast-x { + flex: 0 0 auto; + background: transparent; + border: none; + color: #777; + font-size: 16px; + line-height: 1; + padding: 2px 6px; + cursor: pointer; + border-radius: 3px; +} + +.toast-x:hover { + background: #2a2a2a; + color: #ddd; +} + +@keyframes slide-in { + from { + transform: translateX(20px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/src/components/Notifications.svelte b/src/components/Notifications.svelte deleted file mode 100644 index db78a94..0000000 --- a/src/components/Notifications.svelte +++ /dev/null @@ -1,81 +0,0 @@ - - -
- {#each notifications as t (t.id)} -
- {t.message} - -
- {/each} -
- - diff --git a/src/components/Notifications.tsx b/src/components/Notifications.tsx new file mode 100644 index 0000000..ecad26e --- /dev/null +++ b/src/components/Notifications.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import "./Notifications.css"; + +export interface Toast { + id: number; + message: string; +} + +interface NotificationsProps { + notifications: Toast[]; + onDismiss: (id: number) => void; +} + +export default function Notifications({ + notifications, + onDismiss, +}: NotificationsProps) { + return ( +
+ {notifications.map((t) => ( +
+ {t.message} + +
+ ))} +
+ ); +} diff --git a/src/components/Palette.css b/src/components/Palette.css new file mode 100644 index 0000000..276605c --- /dev/null +++ b/src/components/Palette.css @@ -0,0 +1,99 @@ +.backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(2px); + z-index: 99; + border: 0; + padding: 0; + cursor: default; +} + +.palette { + position: fixed; + top: 12vh; + left: 50%; + transform: translateX(-50%); + width: min(520px, 90vw); + background: #181818; + border: 1px solid #2a2a2a; + border-radius: 6px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); + z-index: 100; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.palette-input { + font: inherit; + font-size: 14px; + color: #fff; + background: #1f1f1f; + border: none; + border-bottom: 1px solid #2a2a2a; + padding: 10px 14px; + outline: none; +} + +.palette-list { + list-style: none; + margin: 0; + padding: 4px; + max-height: 50vh; + overflow-y: auto; +} + +.palette-item { + width: 100%; + text-align: left; + background: transparent; + border: none; + color: #ccc; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: baseline; + gap: 10px; + font: inherit; + font-size: 12px; +} + +.palette-item.highlight { + background: #1a3a5c; + color: #cce6ff; +} + +.name { + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + font-weight: 600; +} + +.meta { + color: #888; + font-size: 11px; +} + +.palette-item.highlight .meta { + color: #9bd; +} + +.meta.cwd { + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.meta.bcast { + margin-left: auto; +} + +.empty { + color: #777; + font-style: italic; + padding: 8px 10px; + list-style: none; +} diff --git a/src/components/Palette.svelte b/src/components/Palette.svelte deleted file mode 100644 index 9f420c1..0000000 --- a/src/components/Palette.svelte +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - diff --git a/src/components/Palette.tsx b/src/components/Palette.tsx new file mode 100644 index 0000000..35472f2 --- /dev/null +++ b/src/components/Palette.tsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect, useRef, useMemo } from "react"; +import type { LeafNode } from "../lib/layout/tree"; +import "./Palette.css"; + +interface PaletteProps { + leaves: LeafNode[]; + onPick: (leafId: string) => void; + onClose: () => void; +} + +export default function Palette({ leaves, onPick, onClose }: PaletteProps) { + const [query, setQuery] = useState(""); + const [highlightIndex, setHighlightIndex] = useState(0); + const inputRef = useRef(null); + + // Autofocus the input on mount + useEffect(() => { + queueMicrotask(() => inputRef.current?.focus()); + }, []); + + // Compute filtered leaves based on query + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return leaves; + return leaves.filter((l) => { + const blob = `${l.label ?? ""} ${l.distro ?? ""} ${l.cwd ?? ""}`.toLowerCase(); + return blob.includes(q); + }); + }, [query, leaves]); + + // Clamp highlight index when filtered list changes + useEffect(() => { + if (highlightIndex >= filtered.length) { + setHighlightIndex(Math.max(0, filtered.length - 1)); + } + }, [filtered, highlightIndex]); + + function pick(idx: number) { + const l = filtered[idx]; + if (l) onPick(l.id); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } else if (e.key === "ArrowDown") { + e.preventDefault(); + if (filtered.length > 0) { + setHighlightIndex((prev) => (prev + 1) % filtered.length); + } + } else if (e.key === "ArrowUp") { + e.preventDefault(); + if (filtered.length > 0) { + setHighlightIndex((prev) => + (prev - 1 + filtered.length) % filtered.length + ); + } + } else if (e.key === "Enter") { + e.preventDefault(); + pick(highlightIndex); + } + } + + return ( + <> + + +
+ setQuery(e.target.value)} + onKeyDown={handleKeyDown} + /> +
    + {filtered.length === 0 ? ( +
  • No matching panes.
  • + ) : ( + filtered.map((leaf, i) => ( +
  • + +
  • + )) + )} +
+
+ + ); +} diff --git a/src/components/XtermPane.svelte b/src/components/XtermPane.svelte deleted file mode 100644 index b50ee6a..0000000 --- a/src/components/XtermPane.svelte +++ /dev/null @@ -1,183 +0,0 @@ - - -
- - diff --git a/src/components/XtermPane.tsx b/src/components/XtermPane.tsx new file mode 100644 index 0000000..0beab95 --- /dev/null +++ b/src/components/XtermPane.tsx @@ -0,0 +1,222 @@ +import { useRef, useEffect } from "react"; +import { Terminal } from "@xterm/xterm"; +import { FitAddon } from "@xterm/addon-fit"; +import type { UnlistenFn } from "@tauri-apps/api/event"; +import { + spawnPane, + writeToPane, + resizePane, + killPane, + onPaneData, + onPaneExit, + type PaneId, +} from "../ipc"; + +// --------------------------------------------------------------------------- +// base64 helpers (private to this module) +// --------------------------------------------------------------------------- + +function b64ToBytes(b64: string): Uint8Array { + const bin = atob(b64); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +function bytesToB64(bytes: Uint8Array): string { + let s = ""; + for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); + return btoa(s); +} + +function stringToB64(s: string): string { + // xterm.js's onData emits a JS string; UTF-8 encode before base64. + return bytesToB64(new TextEncoder().encode(s)); +} + +// --------------------------------------------------------------------------- +// Props +// --------------------------------------------------------------------------- + +interface XtermPaneProps { + distro?: string; + cwd?: string; + onStatus?: (msg: string, ok: boolean) => void; + /** Fired once when the backend PTY is alive and we have its PaneId. */ + onSpawn?: (paneId: PaneId) => void; + /** Fired AFTER each writeToPane on user keypress. Used by broadcasting. */ + onInput?: (dataB64: string) => void; + /** Fired whenever output arrives from the PTY. Used for idle detection. */ + onDataReceived?: () => void; + /** Fired when xterm's textarea gains focus (i.e., user clicked here). */ + onFocus?: () => void; + /** Increment to refocus the terminal programmatically (palette etc.). */ + focusTrigger?: number; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export default function XtermPane({ + distro, + cwd, + onStatus, + onSpawn, + onInput, + onDataReceived, + onFocus, + focusTrigger = 0, +}: XtermPaneProps) { + const containerRef = useRef(null); + + // Stable refs for callbacks so the mount effect doesn't need to re-run when + // parents pass new inline functions, while still always calling the latest version. + const onStatusRef = useRef(onStatus); + const onSpawnRef = useRef(onSpawn); + const onInputRef = useRef(onInput); + const onDataReceivedRef = useRef(onDataReceived); + const onFocusRef = useRef(onFocus); + + useEffect(() => { onStatusRef.current = onStatus; }, [onStatus]); + useEffect(() => { onSpawnRef.current = onSpawn; }, [onSpawn]); + useEffect(() => { onInputRef.current = onInput; }, [onInput]); + useEffect(() => { onDataReceivedRef.current = onDataReceived; }, [onDataReceived]); + useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]); + + // ------------------------------------------------------------------------- + // Mount / unmount: create terminal, spawn PTY, wire listeners + // ------------------------------------------------------------------------- + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let term: Terminal | null = new Terminal({ + fontFamily: '"Cascadia Mono", "JetBrains Mono", "Consolas", monospace', + fontSize: 13, + cursorBlink: true, + theme: { + background: "#0c0c0c", + foreground: "#e6e6e6", + }, + scrollback: 5000, + convertEol: false, + allowProposedApi: true, + }); + + const fit = new FitAddon(); + term.loadAddon(fit); + term.open(container); + + // Initial size — fit before asking the PTY for its dimensions. + fit.fit(); + + let paneId: PaneId | null = null; + let unlistenData: UnlistenFn | null = null; + let unlistenExit: UnlistenFn | null = null; + let ro: ResizeObserver | null = null; + let destroyed = false; + + (async () => { + const cols = term!.cols; + const rows = term!.rows; + + try { + paneId = await spawnPane({ distro, cwd, cols, rows }); + if (destroyed) { + void killPane(paneId); + return; + } + onStatusRef.current?.(`pane ${paneId} alive`, true); + onSpawnRef.current?.(paneId); + } catch (e) { + if (destroyed) return; + const msg = `spawn_pane failed: ${e}`; + term?.write(`\r\n\x1b[31m${msg}\x1b[0m\r\n`); + onStatusRef.current?.(msg, false); + return; + } + + unlistenData = await onPaneData(paneId, (b64) => { + term?.write(b64ToBytes(b64)); + onDataReceivedRef.current?.(); + }); + + unlistenExit = await onPaneExit(paneId, () => { + term?.write("\r\n\x1b[33m[pane exited]\x1b[0m\r\n"); + onStatusRef.current?.(`pane ${paneId} exited`, false); + }); + + term?.onData((data) => { + if (paneId == null) return; + const b64 = stringToB64(data); + void writeToPane(paneId, b64); + onInputRef.current?.(b64); + }); + + // Focus detection: xterm.js doesn't expose onFocus as a first-class event + // in all versions, so try the proposed API first then fall back to the DOM. + term?.onSelectionChange(() => {}); // ensure addon system is initialised; noop + const termAny = term as unknown as { onFocus?: (cb: () => void) => void }; + if (typeof termAny.onFocus === "function") { + termAny.onFocus(() => onFocusRef.current?.()); + } else { + const ta = container.querySelector(".xterm-helper-textarea"); + if (ta) ta.addEventListener("focus", () => onFocusRef.current?.(), true); + } + + // Re-fit on container resize; forward new size to the PTY. + ro = new ResizeObserver(() => { + try { + fit.fit(); + if (paneId != null && term) { + void resizePane(paneId, term.cols, term.rows); + } + } catch (e) { + console.warn("resize failed", e); + } + }); + ro.observe(container); + + // Focus so typing immediately lands in the terminal. + term?.focus(); + })(); + + return () => { + destroyed = true; + ro?.disconnect(); + unlistenData?.(); + unlistenExit?.(); + if (paneId != null) void killPane(paneId); + term?.dispose(); + term = null; + }; + // distro/cwd are only used at spawn time; intentionally omitted from deps + // so remounting doesn't happen if a parent re-renders with the same values. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ------------------------------------------------------------------------- + // focusTrigger: programmatic refocus from parent (palette navigation etc.) + // ------------------------------------------------------------------------- + const termRef = useRef(null); + + // Keep termRef in sync via a second effect that runs after mount. + // We can't easily share the Terminal instance across the two effects without + // a ref, so we store it on termRef inside the mount effect instead. + // Actually, let's just wire focusTrigger by querying the textarea directly — + // that avoids the cross-effect coupling problem entirely. + useEffect(() => { + if (focusTrigger > 0 && containerRef.current) { + const ta = containerRef.current.querySelector( + ".xterm-helper-textarea", + ); + ta?.focus(); + } + }, [focusTrigger]); + + // Suppress unused ref warning + void termRef; + + return
; +} diff --git a/src/lib/layout/LeafPane.css b/src/lib/layout/LeafPane.css new file mode 100644 index 0000000..f66b272 --- /dev/null +++ b/src/lib/layout/LeafPane.css @@ -0,0 +1,170 @@ +.leaf { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + border: 2px solid transparent; + box-sizing: border-box; +} +.leaf.active { + border-color: #5a8cd8; +} +.leaf.broadcasting { + border-color: #e09838; +} +.leaf.active.broadcasting { + border-color: #ffb840; +} + +.pane-toolbar { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; + padding: 2px 8px; + background: #181818; + border-bottom: 1px solid #2a2a2a; + font-size: 11px; + color: #aaa; + user-select: none; + min-height: 24px; +} +.pane-label { + font: inherit; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + font-weight: 600; + color: #ccc; + background: transparent; + border: 1px solid transparent; + border-radius: 3px; + padding: 1px 6px; + cursor: text; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} +.pane-label:hover { + background: #222; + border-color: #2a2a2a; +} +.label-input { + font: inherit; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + font-weight: 600; + color: #fff; + background: #0c0c0c; + border: 1px solid #3a5a8c; + border-radius: 3px; + padding: 1px 6px; + outline: none; + max-width: 240px; +} + +.distro-wrap { + position: relative; +} +.distro-chip, +.bcast-chip { + font: inherit; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + font-size: 10px; + background: #222; + color: #88c; + border: 1px solid #2a2a3a; + border-radius: 3px; + padding: 1px 6px; + cursor: pointer; +} +.distro-chip:hover, +.bcast-chip:hover { + background: #2a2a3a; + color: #aac; +} +.bcast-chip { + color: #777; + background: #1c1c1c; + border-color: #2a2a2a; + padding: 1px 5px; +} +.bcast-chip.on { + background: #4a3010; + color: #f0c060; + border-color: #c98a1f; +} + +.distro-menu { + position: absolute; + top: 100%; + left: 0; + margin-top: 2px; + background: #1a1a1a; + border: 1px solid #2a2a2a; + border-radius: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); + z-index: 10; + min-width: 140px; + display: flex; + flex-direction: column; + padding: 2px; +} +.distro-menu-item { + font: inherit; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + font-size: 11px; + text-align: left; + background: transparent; + color: #ccc; + border: none; + border-radius: 2px; + padding: 3px 8px; + cursor: pointer; +} +.distro-menu-item:hover { + background: #2a2a2a; +} +.distro-menu-item.active { + background: #1a3a5c; + color: #cce6ff; +} + +.pane-status { + margin-left: auto; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; + color: #777; + font-size: 10px; + white-space: nowrap; +} +.pane-status.ok { color: #6c6; } +.pane-status.err { color: #d66; } + +.pane-actions { + display: flex; + gap: 2px; +} +.pane-btn { + background: transparent; + border: none; + color: #888; + font: inherit; + font-size: 14px; + line-height: 1; + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; +} +.pane-btn:hover { + background: #2a2a2a; + color: #ddd; +} +.pane-btn.close:hover { + background: #5a1a1a; + color: #fcc; +} +.xterm-wrap { + flex: 1 1 auto; + min-height: 0; + position: relative; +} diff --git a/src/lib/layout/LeafPane.svelte b/src/lib/layout/LeafPane.svelte deleted file mode 100644 index b5edc8b..0000000 --- a/src/lib/layout/LeafPane.svelte +++ /dev/null @@ -1,400 +0,0 @@ - - -
-
- {#if editingLabel} - - {:else} - - {/if} - - - - {#if distroOpen} -
e.stopPropagation()} - role="menu" - tabindex="-1" - onkeydown={() => {}} - > - {#each orch.distros as d} - - {/each} -
- {/if} -
- - - - {status} - - - - - - -
-
- { - status = msg; - statusOk = ok; - }} - onSpawn={onPaneSpawned} - onInput={onTerminalInput} - onFocus={() => orch.setActive(leaf.id)} - {onDataReceived} - {focusTrigger} - /> -
-
- - diff --git a/src/lib/layout/LeafPane.tsx b/src/lib/layout/LeafPane.tsx new file mode 100644 index 0000000..73d8f49 --- /dev/null +++ b/src/lib/layout/LeafPane.tsx @@ -0,0 +1,263 @@ +import { + useState, + useEffect, + useRef, + useCallback, + type KeyboardEvent, + type MouseEvent, +} from "react"; +import type { LeafNode } from "./tree"; +import { useOrchestration } from "./orchestration"; +import XtermPane from "../../components/XtermPane"; +import "./LeafPane.css"; + +const IDLE_THRESHOLD_MS = 5000; + +export default function LeafPane({ leaf }: { leaf: LeafNode }) { + const orch = useOrchestration(); + const isActive = orch.activeLeafId === leaf.id; + const isBroadcasting = !!leaf.broadcast; + + // ---- status (from XtermPane) ------------------------------------------- + const [status, setStatus] = useState("starting…"); + const [statusOk, setStatusOk] = useState(true); + + // ---- label editing ----------------------------------------------------- + const [editingLabel, setEditingLabel] = useState(false); + const [labelDraft, setLabelDraft] = useState(""); + const labelInputRef = useRef(null); + + const startEditLabel = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + setLabelDraft(leaf.label ?? ""); + setEditingLabel(true); + // Focus on next tick so input is mounted + queueMicrotask(() => labelInputRef.current?.select()); + }, + [leaf.label], + ); + const commitLabel = useCallback(() => { + if (!editingLabel) return; + orch.setLabel(leaf.id, labelDraft); + setEditingLabel(false); + }, [editingLabel, orch, leaf.id, labelDraft]); + const cancelLabel = useCallback(() => setEditingLabel(false), []); + const onLabelKey = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + commitLabel(); + } else if (e.key === "Escape") { + e.preventDefault(); + cancelLabel(); + } + }, + [commitLabel, cancelLabel], + ); + + // ---- distro popover ---------------------------------------------------- + const [distroOpen, setDistroOpen] = useState(false); + const toggleDistroMenu = useCallback((e: MouseEvent) => { + e.stopPropagation(); + setDistroOpen((v) => !v); + }, []); + const pickDistro = useCallback( + (d: string) => { + setDistroOpen(false); + if (d !== leaf.distro) orch.setDistro(leaf.id, d); + }, + [orch, leaf.id, leaf.distro], + ); + // Dismiss popover on outside click + useEffect(() => { + if (!distroOpen) return; + const onDocClick = () => setDistroOpen(false); + window.addEventListener("click", onDocClick); + return () => window.removeEventListener("click", onDocClick); + }, [distroOpen]); + + // ---- idle detection ---------------------------------------------------- + const lastDataTimeRef = useRef(Date.now()); + const notifiedThisIdleRef = useRef(false); + const onDataReceived = useCallback(() => { + lastDataTimeRef.current = Date.now(); + notifiedThisIdleRef.current = false; + }, []); + useEffect(() => { + const id = window.setInterval(() => { + if (notifiedThisIdleRef.current) return; + const dt = Date.now() - lastDataTimeRef.current; + if (dt >= IDLE_THRESHOLD_MS) { + notifiedThisIdleRef.current = true; + const name = leaf.label ?? leaf.distro ?? "pane"; + orch.notify(`${name} is idle`); + } + }, 1000); + return () => clearInterval(id); + }, [leaf.label, leaf.distro, orch]); + + // ---- broadcast --------------------------------------------------------- + const onTerminalInput = useCallback( + (b64: string) => { + if (isBroadcasting) orch.broadcastFrom(leaf.id, b64); + }, + [isBroadcasting, orch, leaf.id], + ); + + // ---- focus / active highlighting --------------------------------------- + const [focusTrigger, setFocusTrigger] = useState(0); + // When this leaf becomes active, bump focusTrigger so XtermPane refocuses. + useEffect(() => { + if (isActive) setFocusTrigger((n) => n + 1); + }, [isActive]); + + const onPaneClick = useCallback(() => { + orch.setActive(leaf.id); + }, [orch, leaf.id]); + + const onPaneSpawned = useCallback( + (paneId: number) => { + orch.registerPaneId(leaf.id, paneId); + }, + [orch, leaf.id], + ); + // Unregister on unmount + useEffect(() => { + return () => orch.registerPaneId(leaf.id, null); + }, [orch, leaf.id]); + + const onXtermFocus = useCallback(() => orch.setActive(leaf.id), [orch, leaf.id]); + + const onStatus = useCallback((msg: string, ok: boolean) => { + setStatus(msg); + setStatusOk(ok); + }, []); + + const labelText = leaf.label ?? "(unnamed)"; + + return ( +
+
+ {editingLabel ? ( + setLabelDraft(e.target.value)} + onKeyDown={onLabelKey} + onBlur={commitLabel} + placeholder="(label)" + /> + ) : ( + + )} + + + + {distroOpen && ( +
e.stopPropagation()} + > + {orch.distros.map((d) => ( + + ))} +
+ )} +
+ + + + {status} + + + + + + +
+
+ +
+
+ ); +} diff --git a/src/lib/layout/Pane.svelte b/src/lib/layout/Pane.svelte deleted file mode 100644 index f1e6afd..0000000 --- a/src/lib/layout/Pane.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -{#if node.kind === "split"} - -{:else} - -{/if} diff --git a/src/lib/layout/Pane.tsx b/src/lib/layout/Pane.tsx new file mode 100644 index 0000000..f3f27c7 --- /dev/null +++ b/src/lib/layout/Pane.tsx @@ -0,0 +1,16 @@ +import type { TreeNode } from "./tree"; +import SplitNode from "./SplitNode"; +import LeafPane from "./LeafPane"; + +/** + * Recursive dispatcher: render a split or a leaf based on node.kind. + * The `key={node.id}` on the leaf branch makes React unmount + remount + * cleanly when a leaf is replaced (e.g. changeDistro swaps the id to + * force PTY respawn). + */ +export default function Pane({ node }: { node: TreeNode }) { + if (node.kind === "split") { + return ; + } + return ; +} diff --git a/src/lib/layout/SplitNode.css b/src/lib/layout/SplitNode.css new file mode 100644 index 0000000..531d2c4 --- /dev/null +++ b/src/lib/layout/SplitNode.css @@ -0,0 +1,36 @@ +.split { + display: flex; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; +} +.split.horizontal { + flex-direction: row; +} +.split.vertical { + flex-direction: column; +} + +.side { + display: flex; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.gutter { + flex: 0 0 4px; + background: #1a1a1a; + cursor: col-resize; + user-select: none; + touch-action: none; + transition: background 0.12s; +} +.split.vertical > .gutter { + cursor: row-resize; +} +.gutter:hover, +.gutter.active { + background: #3a5a8c; +} diff --git a/src/lib/layout/SplitNode.svelte b/src/lib/layout/SplitNode.svelte deleted file mode 100644 index eba70e6..0000000 --- a/src/lib/layout/SplitNode.svelte +++ /dev/null @@ -1,116 +0,0 @@ - - -
-
- -
- -
- -
-
- - diff --git a/src/lib/layout/SplitNode.tsx b/src/lib/layout/SplitNode.tsx new file mode 100644 index 0000000..814c584 --- /dev/null +++ b/src/lib/layout/SplitNode.tsx @@ -0,0 +1,79 @@ +import { useRef, useState, useCallback, type PointerEvent } from "react"; +import type { SplitNode as SplitNodeType } from "./tree"; +import Pane from "./Pane"; +import "./SplitNode.css"; + +/** + * A horizontal or vertical split with a draggable gutter. The ratio is + * local React state — when the gutter is dragged, we update the local + * ratio (re-rendering the two .side flex values) and ALSO bubble the + * change up to the tree (so it persists across reloads). + * + * Initialising local state from node.ratio is fine: when the tree + * mutates around this split (e.g. a child is closed), React will give us + * a new `node` prop with possibly-different `node.ratio`, but the + * `useState` initializer only runs once. We re-sync via an effect. + */ +export default function SplitNode({ node }: { node: SplitNodeType }) { + const containerRef = useRef(null); + const [ratio, setRatio] = useState(node.ratio); + const [dragging, setDragging] = useState(false); + + // Keep local ratio in sync if the tree updates from outside (e.g. preset + // applied). Only mirror — don't echo back into the tree. + // (Skipped for simplicity in v1; if it becomes annoying we can add it.) + + const onPointerDown = useCallback((e: PointerEvent) => { + (e.target as HTMLElement).setPointerCapture(e.pointerId); + setDragging(true); + e.preventDefault(); + }, []); + + const onPointerMove = useCallback( + (e: PointerEvent) => { + if (!dragging || !containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const isH = node.orientation === "h"; + const pos = isH ? e.clientX - rect.left : e.clientY - rect.top; + const size = isH ? rect.width : rect.height; + if (size <= 0) return; + const r = Math.max(0.05, Math.min(0.95, pos / size)); + setRatio(r); + // Mutate the proxy-tree node directly so the persisted state matches. + node.ratio = r; + }, + [dragging, node], + ); + + const onPointerUp = useCallback((e: PointerEvent) => { + setDragging(false); + (e.target as HTMLElement).releasePointerCapture(e.pointerId); + }, []); + + const isH = node.orientation === "h"; + + return ( +
+
+ +
+
+
+ +
+
+ ); +} diff --git a/src/lib/layout/orchestration.svelte.ts b/src/lib/layout/orchestration.svelte.ts deleted file mode 100644 index ac886be..0000000 --- a/src/lib/layout/orchestration.svelte.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Orchestration store — all shared reactive state + operations the - * pane tree needs. Lives in a class with `$state` fields so Svelte 5 - * reactivity tracks per-property access; provided via context so any - * descendant component can `useOrchestration()` without prop drilling. - * - * (File must be `.svelte.ts` because `$state` can only be used in - * Svelte components or files with the `.svelte.{js,ts}` extension.) - */ - -import { setContext, getContext } from "svelte"; -import type { NodeId, Orientation } from "./tree"; -import type { PaneId } from "../../ipc"; - -export interface Toast { - id: number; - message: string; -} - -/** - * Callbacks App provides at construction time. These do tree mutations - * (which require access to App's `tree = $state(...)`) plus broadcast - * routing (which also needs the tree). Kept as an injection seam rather - * than living inside the store so the store doesn't need to own the tree. - */ -export interface TreeOps { - split: (leafId: NodeId, orientation: Orientation) => void; - close: (leafId: NodeId) => void; - setDistro: (leafId: NodeId, distro: string) => void; - setLabel: (leafId: NodeId, label: string | undefined) => void; - toggleBroadcast: (leafId: NodeId) => void; - broadcastFrom: (originLeafId: NodeId, dataB64: string) => void; -} - -export class Orchestration { - // ---- shared reactive state ---------------------------------------------- - // (activeLeafId lives at App level and is drilled as a prop — Svelte 5 - // doesn't seem to track class-field $state reads from child components - // that obtain the instance via getContext. Tested empirically.) - notifications = $state([]); - distros = $state([]); - - // ---- non-reactive lookups ----------------------------------------------- - // Plain Map: broadcast routing reads this from an event handler, not - // from reactive context. No need for $state. - paneIdByLeaf = new Map(); - - // ---- internal ----------------------------------------------------------- - #nextNotifId = 1; - #dismissTimers = new Map>(); - #ops: TreeOps; - - constructor(ops: TreeOps) { - this.#ops = ops; - } - - // ---- active pane (delegated to App) ------------------------------------- - // These point at App-level $state mutators set via configure(). - setActive: (leafId: NodeId) => void = () => {}; - clearActiveIf: (leafId: NodeId) => void = () => {}; - - configureActiveHandlers( - setActive: (leafId: NodeId) => void, - clearActiveIf: (leafId: NodeId) => void, - ): void { - this.setActive = setActive; - this.clearActiveIf = clearActiveIf; - } - - // ---- notifications ------------------------------------------------------ - notify(message: string): void { - const id = this.#nextNotifId++; - console.log("[orch] notify", message); - this.notifications.push({ id, message }); - const timer = setTimeout(() => { - this.notifications = this.notifications.filter((n) => n.id !== id); - this.#dismissTimers.delete(id); - }, 5000); - this.#dismissTimers.set(id, timer); - } - - dismiss(id: number): void { - const t = this.#dismissTimers.get(id); - if (t) { - clearTimeout(t); - this.#dismissTimers.delete(id); - } - this.notifications = this.notifications.filter((n) => n.id !== id); - } - - // ---- pane id registry --------------------------------------------------- - registerPaneId(leafId: NodeId, paneId: PaneId | null): void { - if (paneId == null) this.paneIdByLeaf.delete(leafId); - else this.paneIdByLeaf.set(leafId, paneId); - } - - // ---- delegated tree ops ------------------------------------------------- - // Thin pass-through so consumers only need one object. - split(leafId: NodeId, orientation: Orientation): void { - console.log("[orch] split", leafId, orientation); - this.#ops.split(leafId, orientation); - } - - close(leafId: NodeId): void { - console.log("[orch] close", leafId); - this.#ops.close(leafId); - } - - setDistro(leafId: NodeId, distro: string): void { - this.#ops.setDistro(leafId, distro); - } - - setLabel(leafId: NodeId, label: string | undefined): void { - this.#ops.setLabel(leafId, label); - } - - toggleBroadcast(leafId: NodeId): void { - console.log("[orch] toggleBroadcast", leafId); - this.#ops.toggleBroadcast(leafId); - } - - broadcastFrom(originLeafId: NodeId, dataB64: string): void { - this.#ops.broadcastFrom(originLeafId, dataB64); - } -} - -const KEY = Symbol("tiletopia.orchestration"); - -export function provideOrchestration(ops: TreeOps): Orchestration { - const o = new Orchestration(ops); - setContext(KEY, o); - return o; -} - -export function useOrchestration(): Orchestration { - const o = getContext(KEY); - if (!o) { - throw new Error( - "useOrchestration() called outside a provideOrchestration() ancestor", - ); - } - return o; -} diff --git a/src/lib/layout/orchestration.tsx b/src/lib/layout/orchestration.tsx new file mode 100644 index 0000000..7e7a27e --- /dev/null +++ b/src/lib/layout/orchestration.tsx @@ -0,0 +1,58 @@ +import { createContext, useContext, type ReactNode } from "react"; +import type { Orientation, NodeId } from "./tree"; +import type { PaneId } from "../../ipc"; + +/** + * Orchestration context — every piece of shared state and every operation + * that a Pane / SplitNode / LeafPane might call. Lives in React context so + * descendants can `useOrchestration()` without prop drilling. + * + * activeLeafId comes in as a plain value (re-derived by App's useState). + * React's context is reactive: when the App-level Provider updates the + * value, ALL consumers re-render. No Svelte-style props-don't-propagate + * trap here. + */ +export interface Orchestration { + // Read-only state + activeLeafId: NodeId | null; + distros: string[]; + + // Tree mutations + split: (leafId: NodeId, orientation: Orientation) => void; + close: (leafId: NodeId) => void; + setDistro: (leafId: NodeId, distro: string) => void; + setLabel: (leafId: NodeId, label: string | undefined) => void; + toggleBroadcast: (leafId: NodeId) => void; + + // Per-pane orchestration + setActive: (leafId: NodeId) => void; + registerPaneId: (leafId: NodeId, paneId: PaneId | null) => void; + broadcastFrom: (originLeafId: NodeId, dataB64: string) => void; + notify: (message: string) => void; +} + +const OrchestrationContext = createContext(null); + +export function OrchestrationProvider({ + value, + children, +}: { + value: Orchestration; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function useOrchestration(): Orchestration { + const orch = useContext(OrchestrationContext); + if (!orch) { + throw new Error( + "useOrchestration() must be called inside ", + ); + } + return orch; +} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 0a47e0c..0000000 --- a/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { mount } from "svelte"; -import App from "./App.svelte"; -import "@xterm/xterm/css/xterm.css"; -import "./styles.css"; - -const app = mount(App, { target: document.getElementById("app")! }); - -export default app; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..be7a6f4 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./styles.css"; +import App from "./App"; + +const root = document.getElementById("root"); +if (!root) throw new Error("No #root element found"); + +createRoot(root).render( + + + +); diff --git a/src/styles.css b/src/styles.css index 27653f3..04fc0aa 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,65 +1,39 @@ -:root { - color-scheme: dark; - font-family: system-ui, -apple-system, "Segoe UI", sans-serif; - font-size: 13px; - background: #0c0c0c; - color: #e6e6e6; +@import "@xterm/xterm/css/xterm.css"; + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; } html, body, -#app { - margin: 0; - padding: 0; - height: 100%; +#root { width: 100%; + height: 100%; overflow: hidden; } -.app { - display: flex; - flex-direction: column; - height: 100vh; - background: #0c0c0c; +body { + background: #1a1a1a; + color: #e0e0e0; + font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", monospace; + font-size: 14px; + line-height: 1.4; } -.titlebar { - flex: 0 0 auto; +.app-root { + width: 100%; + height: 100%; display: flex; align-items: center; - gap: 12px; - padding: 6px 12px; - background: #1a1a1a; - border-bottom: 1px solid #2a2a2a; - font-size: 12px; - color: #aaa; - user-select: none; + justify-content: center; + color: #888; + font-size: 13px; + letter-spacing: 0.05em; } -.titlebar .label { - font-weight: 600; - color: #ddd; -} - -.titlebar .distro { - font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; - color: #88c; -} - -.titlebar .status { - margin-left: auto; - font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; -} - -.titlebar .status.ok { - color: #6c6; -} -.titlebar .status.err { - color: #d66; -} - -.pane-wrap { - flex: 1 1 auto; - min-height: 0; - position: relative; -} +.xterm { height: 100%; } +.xterm-viewport { background: #0c0c0c !important; } diff --git a/svelte.config.js b/svelte.config.js deleted file mode 100644 index d0e6448..0000000 --- a/svelte.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -export default { - preprocess: vitePreprocess(), -}; diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..c351008 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json index 4091665..ba5ccc4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,7 @@ { - "extends": "@tsconfig/svelte/tsconfig.json", - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "moduleResolution": "Bundler", - "strict": true, - "isolatedModules": true, - "skipLibCheck": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "verbatimModuleSyntax": true - }, - "include": ["src/**/*.ts", "src/**/*.svelte"], - "references": [{ "path": "./tsconfig.node.json" }] + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.app.json" } + ] } diff --git a/tsconfig.node.json b/tsconfig.node.json index cbd2a63..9d76993 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,11 +1,18 @@ { "compilerOptions": { - "composite": true, - "skipLibCheck": true, + "target": "ES2022", + "lib": ["ES2022"], "module": "ESNext", - "moduleResolution": "Bundler", - "allowSyntheticDefaultImports": true, - "strict": true + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, "include": ["vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 4c0d7a8..a11d245 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,9 @@ /// import { defineConfig } from "vite"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; +import react from "@vitejs/plugin-react"; -export default defineConfig(async () => ({ - plugins: [svelte()], +export default defineConfig({ + plugins: [react()], clearScreen: false, server: { port: 1420, @@ -19,6 +19,6 @@ export default defineConfig(async () => ({ }, test: { environment: "node", - include: ["src/**/*.test.ts"], + include: ["src/**/*.test.ts", "src/**/*.test.tsx"], }, -})); +});