Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a168265063 | |||
|
daedbfd865
|
|||
| 7093e58fe4 | |||
|
cab759ec61
|
|||
| e45a1a1c98 | |||
| edc863e020 | |||
| b006f571bf | |||
| ea3cc8b26c | |||
| 2bb541fba6 |
@@ -1 +1,29 @@
|
|||||||
tem
|
# hikari-desktop
|
||||||
|
|
||||||
|
Desktop companion application featuring Hikari.
|
||||||
|
|
||||||
|
## Live Version
|
||||||
|
|
||||||
|
This page is currently deployed. [View the live website.](https://git.nhcarrigan.com/nhcarrigan/hikari-desktop/releases)
|
||||||
|
|
||||||
|
## Feedback and Bugs
|
||||||
|
|
||||||
|
If you have feedback or a bug report, please [log a ticket on our forum](https://support.nhcarrigan.com).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you would like to contribute to the project, you may create a Pull Request containing your proposed changes and we will review it as soon as we are able! Please review our [contributing guidelines](CONTRIBUTING.md) first.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
Before interacting with our community, please read our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This software is licensed under our [global software license](https://docs.nhcarrigan.com/#/license).
|
||||||
|
|
||||||
|
Copyright held by Naomi Carrigan.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hikari-desktop",
|
"name": "hikari-desktop",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -27,15 +27,42 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "6.8.1",
|
||||||
|
"@codemirror/lang-angular": "^0.1.4",
|
||||||
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-go": "^6.0.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/lang-java": "^6.0.2",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-less": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
|
"@codemirror/lang-sass": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
|
"@codemirror/lang-vue": "^0.1.3",
|
||||||
|
"@codemirror/lang-wast": "^6.0.2",
|
||||||
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.1",
|
||||||
|
"@codemirror/legacy-modes": "^6.5.2",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.39.11",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||||
"@tauri-apps/plugin-notification": "^2",
|
"@tauri-apps/plugin-notification": "2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-os": "^2",
|
"@tauri-apps/plugin-os": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.4",
|
"@tauri-apps/plugin-shell": "^2.3.4",
|
||||||
"@tauri-apps/plugin-store": "^2",
|
"@tauri-apps/plugin-store": "^2",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"marked": "^17.0.1"
|
"marked": "^17.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,84 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@codemirror/commands':
|
||||||
|
specifier: 6.8.1
|
||||||
|
version: 6.8.1
|
||||||
|
'@codemirror/lang-angular':
|
||||||
|
specifier: ^0.1.4
|
||||||
|
version: 0.1.4
|
||||||
|
'@codemirror/lang-cpp':
|
||||||
|
specifier: ^6.0.3
|
||||||
|
version: 6.0.3
|
||||||
|
'@codemirror/lang-css':
|
||||||
|
specifier: ^6.3.1
|
||||||
|
version: 6.3.1
|
||||||
|
'@codemirror/lang-go':
|
||||||
|
specifier: ^6.0.1
|
||||||
|
version: 6.0.1
|
||||||
|
'@codemirror/lang-html':
|
||||||
|
specifier: ^6.4.11
|
||||||
|
version: 6.4.11
|
||||||
|
'@codemirror/lang-java':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-javascript':
|
||||||
|
specifier: ^6.2.4
|
||||||
|
version: 6.2.4
|
||||||
|
'@codemirror/lang-json':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-less':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-markdown':
|
||||||
|
specifier: ^6.5.0
|
||||||
|
version: 6.5.0
|
||||||
|
'@codemirror/lang-php':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-python':
|
||||||
|
specifier: ^6.2.1
|
||||||
|
version: 6.2.1
|
||||||
|
'@codemirror/lang-rust':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-sass':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-sql':
|
||||||
|
specifier: ^6.10.0
|
||||||
|
version: 6.10.0
|
||||||
|
'@codemirror/lang-vue':
|
||||||
|
specifier: ^0.1.3
|
||||||
|
version: 0.1.3
|
||||||
|
'@codemirror/lang-wast':
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
|
'@codemirror/lang-xml':
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0
|
||||||
|
'@codemirror/lang-yaml':
|
||||||
|
specifier: ^6.1.2
|
||||||
|
version: 6.1.2
|
||||||
|
'@codemirror/language':
|
||||||
|
specifier: ^6.12.1
|
||||||
|
version: 6.12.1
|
||||||
|
'@codemirror/legacy-modes':
|
||||||
|
specifier: ^6.5.2
|
||||||
|
version: 6.5.2
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: ^6.5.4
|
||||||
|
version: 6.5.4
|
||||||
|
'@codemirror/theme-one-dark':
|
||||||
|
specifier: ^6.1.3
|
||||||
|
version: 6.1.3
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: ^6.39.11
|
||||||
|
version: 6.39.11
|
||||||
|
'@lezer/highlight':
|
||||||
|
specifier: ^1.2.3
|
||||||
|
version: 1.2.3
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.9.1
|
version: 2.9.1
|
||||||
@@ -21,7 +99,7 @@ importers:
|
|||||||
specifier: ^2.4.5
|
specifier: ^2.4.5
|
||||||
version: 2.4.5
|
version: 2.4.5
|
||||||
'@tauri-apps/plugin-notification':
|
'@tauri-apps/plugin-notification':
|
||||||
specifier: ^2
|
specifier: 2.3.3
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
'@tauri-apps/plugin-opener':
|
'@tauri-apps/plugin-opener':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
@@ -35,6 +113,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-store':
|
'@tauri-apps/plugin-store':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.4.2
|
version: 2.4.2
|
||||||
|
codemirror:
|
||||||
|
specifier: ^6.0.2
|
||||||
|
version: 6.0.2
|
||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.11.1
|
specifier: ^11.11.1
|
||||||
version: 11.11.1
|
version: 11.11.1
|
||||||
@@ -158,6 +239,90 @@ packages:
|
|||||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@codemirror/autocomplete@6.20.0':
|
||||||
|
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
|
||||||
|
|
||||||
|
'@codemirror/commands@6.8.1':
|
||||||
|
resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==}
|
||||||
|
|
||||||
|
'@codemirror/lang-angular@0.1.4':
|
||||||
|
resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==}
|
||||||
|
|
||||||
|
'@codemirror/lang-cpp@6.0.3':
|
||||||
|
resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==}
|
||||||
|
|
||||||
|
'@codemirror/lang-css@6.3.1':
|
||||||
|
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
|
||||||
|
|
||||||
|
'@codemirror/lang-go@6.0.1':
|
||||||
|
resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==}
|
||||||
|
|
||||||
|
'@codemirror/lang-html@6.4.11':
|
||||||
|
resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
|
||||||
|
|
||||||
|
'@codemirror/lang-java@6.0.2':
|
||||||
|
resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==}
|
||||||
|
|
||||||
|
'@codemirror/lang-javascript@6.2.4':
|
||||||
|
resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==}
|
||||||
|
|
||||||
|
'@codemirror/lang-json@6.0.2':
|
||||||
|
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
|
||||||
|
|
||||||
|
'@codemirror/lang-less@6.0.2':
|
||||||
|
resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==}
|
||||||
|
|
||||||
|
'@codemirror/lang-markdown@6.5.0':
|
||||||
|
resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==}
|
||||||
|
|
||||||
|
'@codemirror/lang-php@6.0.2':
|
||||||
|
resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==}
|
||||||
|
|
||||||
|
'@codemirror/lang-python@6.2.1':
|
||||||
|
resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
|
||||||
|
|
||||||
|
'@codemirror/lang-rust@6.0.2':
|
||||||
|
resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==}
|
||||||
|
|
||||||
|
'@codemirror/lang-sass@6.0.2':
|
||||||
|
resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==}
|
||||||
|
|
||||||
|
'@codemirror/lang-sql@6.10.0':
|
||||||
|
resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
|
||||||
|
|
||||||
|
'@codemirror/lang-vue@0.1.3':
|
||||||
|
resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==}
|
||||||
|
|
||||||
|
'@codemirror/lang-wast@6.0.2':
|
||||||
|
resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==}
|
||||||
|
|
||||||
|
'@codemirror/lang-xml@6.1.0':
|
||||||
|
resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==}
|
||||||
|
|
||||||
|
'@codemirror/lang-yaml@6.1.2':
|
||||||
|
resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==}
|
||||||
|
|
||||||
|
'@codemirror/language@6.12.1':
|
||||||
|
resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
|
||||||
|
|
||||||
|
'@codemirror/legacy-modes@6.5.2':
|
||||||
|
resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==}
|
||||||
|
|
||||||
|
'@codemirror/lint@6.9.3':
|
||||||
|
resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==}
|
||||||
|
|
||||||
|
'@codemirror/search@6.6.0':
|
||||||
|
resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
|
||||||
|
|
||||||
|
'@codemirror/state@6.5.4':
|
||||||
|
resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
|
||||||
|
|
||||||
|
'@codemirror/theme-one-dark@6.1.3':
|
||||||
|
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
|
||||||
|
|
||||||
|
'@codemirror/view@6.39.11':
|
||||||
|
resolution: {integrity: sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==}
|
||||||
|
|
||||||
'@csstools/color-helpers@5.1.0':
|
'@csstools/color-helpers@5.1.0':
|
||||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -425,6 +590,60 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@lezer/common@1.5.0':
|
||||||
|
resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
|
||||||
|
|
||||||
|
'@lezer/cpp@1.1.5':
|
||||||
|
resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==}
|
||||||
|
|
||||||
|
'@lezer/css@1.3.0':
|
||||||
|
resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
|
||||||
|
|
||||||
|
'@lezer/go@1.0.1':
|
||||||
|
resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==}
|
||||||
|
|
||||||
|
'@lezer/highlight@1.2.3':
|
||||||
|
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
|
||||||
|
|
||||||
|
'@lezer/html@1.3.13':
|
||||||
|
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
|
||||||
|
|
||||||
|
'@lezer/java@1.1.3':
|
||||||
|
resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==}
|
||||||
|
|
||||||
|
'@lezer/javascript@1.5.4':
|
||||||
|
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
|
||||||
|
|
||||||
|
'@lezer/json@1.0.3':
|
||||||
|
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
|
||||||
|
|
||||||
|
'@lezer/lr@1.4.8':
|
||||||
|
resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
|
||||||
|
|
||||||
|
'@lezer/markdown@1.6.3':
|
||||||
|
resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==}
|
||||||
|
|
||||||
|
'@lezer/php@1.0.5':
|
||||||
|
resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==}
|
||||||
|
|
||||||
|
'@lezer/python@1.1.18':
|
||||||
|
resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==}
|
||||||
|
|
||||||
|
'@lezer/rust@1.0.2':
|
||||||
|
resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==}
|
||||||
|
|
||||||
|
'@lezer/sass@1.1.0':
|
||||||
|
resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==}
|
||||||
|
|
||||||
|
'@lezer/xml@1.0.6':
|
||||||
|
resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==}
|
||||||
|
|
||||||
|
'@lezer/yaml@1.0.3':
|
||||||
|
resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
|
||||||
|
|
||||||
|
'@marijn/find-cluster-break@1.0.2':
|
||||||
|
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -1015,6 +1234,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
codemirror@6.0.2:
|
||||||
|
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -1029,6 +1251,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
crelt@1.0.6:
|
||||||
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1663,6 +1888,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
style-mod@4.1.3:
|
||||||
|
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1842,6 +2070,9 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
w3c-keyname@2.2.8:
|
||||||
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
w3c-xmlserializer@5.0.0:
|
w3c-xmlserializer@5.0.0:
|
||||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1949,6 +2180,216 @@ snapshots:
|
|||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
|
'@codemirror/autocomplete@6.20.0':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
|
||||||
|
'@codemirror/commands@6.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
|
||||||
|
'@codemirror/lang-angular@0.1.4':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/lang-html': 6.4.11
|
||||||
|
'@codemirror/lang-javascript': 6.2.4
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@codemirror/lang-cpp@6.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/cpp': 1.1.5
|
||||||
|
|
||||||
|
'@codemirror/lang-css@6.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/css': 1.3.0
|
||||||
|
|
||||||
|
'@codemirror/lang-go@6.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/go': 1.0.1
|
||||||
|
|
||||||
|
'@codemirror/lang-html@6.4.11':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/lang-css': 6.3.1
|
||||||
|
'@codemirror/lang-javascript': 6.2.4
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/css': 1.3.0
|
||||||
|
'@lezer/html': 1.3.13
|
||||||
|
|
||||||
|
'@codemirror/lang-java@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/java': 1.1.3
|
||||||
|
|
||||||
|
'@codemirror/lang-javascript@6.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/lint': 6.9.3
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/javascript': 1.5.4
|
||||||
|
|
||||||
|
'@codemirror/lang-json@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/json': 1.0.3
|
||||||
|
|
||||||
|
'@codemirror/lang-less@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/lang-css': 6.3.1
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@codemirror/lang-markdown@6.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/lang-html': 6.4.11
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/markdown': 1.6.3
|
||||||
|
|
||||||
|
'@codemirror/lang-php@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/lang-html': 6.4.11
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/php': 1.0.5
|
||||||
|
|
||||||
|
'@codemirror/lang-python@6.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/python': 1.1.18
|
||||||
|
|
||||||
|
'@codemirror/lang-rust@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/rust': 1.0.2
|
||||||
|
|
||||||
|
'@codemirror/lang-sass@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/lang-css': 6.3.1
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/sass': 1.1.0
|
||||||
|
|
||||||
|
'@codemirror/lang-sql@6.10.0':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@codemirror/lang-vue@0.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/lang-html': 6.4.11
|
||||||
|
'@codemirror/lang-javascript': 6.2.4
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@codemirror/lang-wast@6.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@codemirror/lang-xml@6.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/xml': 1.0.6
|
||||||
|
|
||||||
|
'@codemirror/lang-yaml@6.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
'@lezer/yaml': 1.0.3
|
||||||
|
|
||||||
|
'@codemirror/language@6.12.1':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
style-mod: 4.1.3
|
||||||
|
|
||||||
|
'@codemirror/legacy-modes@6.5.2':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
|
||||||
|
'@codemirror/lint@6.9.3':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
crelt: 1.0.6
|
||||||
|
|
||||||
|
'@codemirror/search@6.6.0':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
crelt: 1.0.6
|
||||||
|
|
||||||
|
'@codemirror/state@6.5.4':
|
||||||
|
dependencies:
|
||||||
|
'@marijn/find-cluster-break': 1.0.2
|
||||||
|
|
||||||
|
'@codemirror/theme-one-dark@6.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
|
||||||
|
'@codemirror/view@6.39.11':
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
crelt: 1.0.6
|
||||||
|
style-mod: 4.1.3
|
||||||
|
w3c-keyname: 2.2.8
|
||||||
|
|
||||||
'@csstools/color-helpers@5.1.0': {}
|
'@csstools/color-helpers@5.1.0': {}
|
||||||
|
|
||||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||||
@@ -2127,6 +2568,101 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@lezer/common@1.5.0': {}
|
||||||
|
|
||||||
|
'@lezer/cpp@1.1.5':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/css@1.3.0':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/go@1.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/highlight@1.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
|
||||||
|
'@lezer/html@1.3.13':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/java@1.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/javascript@1.5.4':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/json@1.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/lr@1.4.8':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
|
||||||
|
'@lezer/markdown@1.6.3':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
|
||||||
|
'@lezer/php@1.0.5':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/python@1.1.18':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/rust@1.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/sass@1.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/xml@1.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@lezer/yaml@1.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@lezer/common': 1.5.0
|
||||||
|
'@lezer/highlight': 1.2.3
|
||||||
|
'@lezer/lr': 1.4.8
|
||||||
|
|
||||||
|
'@marijn/find-cluster-break@1.0.2': {}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.55.1':
|
'@rollup/rollup-android-arm-eabi@4.55.1':
|
||||||
@@ -2678,6 +3214,16 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
codemirror@6.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@codemirror/autocomplete': 6.20.0
|
||||||
|
'@codemirror/commands': 6.8.1
|
||||||
|
'@codemirror/language': 6.12.1
|
||||||
|
'@codemirror/lint': 6.9.3
|
||||||
|
'@codemirror/search': 6.6.0
|
||||||
|
'@codemirror/state': 6.5.4
|
||||||
|
'@codemirror/view': 6.39.11
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -2688,6 +3234,8 @@ snapshots:
|
|||||||
|
|
||||||
cookie@0.6.0: {}
|
cookie@0.6.0: {}
|
||||||
|
|
||||||
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -3304,6 +3852,8 @@ snapshots:
|
|||||||
|
|
||||||
strip-json-comments@3.1.1: {}
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
|
style-mod@4.1.3: {}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
@@ -3463,6 +4013,8 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
w3c-xmlserializer@5.0.0:
|
w3c-xmlserializer@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
xml-name-validator: 5.0.0
|
xml-name-validator: 5.0.0
|
||||||
|
|||||||
@@ -1602,7 +1602,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "0.3.0"
|
version = "1.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "1.0.0"
|
version = "1.1.1"
|
||||||
description = "Hikari - Claude Code Visual Assistant"
|
description = "Hikari - Claude Code Visual Assistant"
|
||||||
authors = ["Naomi Carrigan"]
|
authors = ["Naomi Carrigan"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 466 KiB |
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 58 KiB |
@@ -394,6 +394,168 @@ pub async fn get_file_size(file_path: String) -> Result<u64, String> {
|
|||||||
Ok(metadata.len())
|
Ok(metadata.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Editor File Operations ====================
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct FileEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
#[serde(rename = "isDirectory")]
|
||||||
|
pub is_directory: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let dir_path = Path::new(&path);
|
||||||
|
|
||||||
|
if !dir_path.exists() {
|
||||||
|
return Err(format!("Directory does not exist: {}", path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dir_path.is_dir() {
|
||||||
|
return Err(format!("Path is not a directory: {}", path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = fs::read_dir(dir_path)
|
||||||
|
.map_err(|e| format!("Failed to read directory: {}", e))?;
|
||||||
|
|
||||||
|
let mut file_entries = Vec::new();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
|
||||||
|
let path = entry.path();
|
||||||
|
let name = entry
|
||||||
|
.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Skip hidden files by default (can be made configurable later)
|
||||||
|
if name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_entries.push(FileEntry {
|
||||||
|
name,
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
is_directory: path.is_dir(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file_entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn read_file_content(path: String) -> Result<String, String> {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fs::read_to_string(&path)
|
||||||
|
.map_err(|e| format!("Failed to read file: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn write_file_content(path: String, content: String) -> Result<(), String> {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fs::write(&path, content)
|
||||||
|
.map_err(|e| format!("Failed to write file: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_file(path: String) -> Result<(), String> {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let file_path = Path::new(&path);
|
||||||
|
|
||||||
|
if file_path.exists() {
|
||||||
|
return Err("File already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_directory(path: String) -> Result<(), String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let dir_path = Path::new(&path);
|
||||||
|
|
||||||
|
if dir_path.exists() {
|
||||||
|
return Err("Directory already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(dir_path).map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_file(path: String) -> Result<(), String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let file_path = Path::new(&path);
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Err("File does not exist".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_path.is_dir() {
|
||||||
|
return Err("Path is a directory, use delete_directory instead".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_file(file_path).map_err(|e| format!("Failed to delete file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_directory(path: String) -> Result<(), String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let dir_path = Path::new(&path);
|
||||||
|
|
||||||
|
if !dir_path.exists() {
|
||||||
|
return Err("Directory does not exist".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dir_path.is_dir() {
|
||||||
|
return Err("Path is not a directory".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_dir_all(dir_path).map_err(|e| format!("Failed to delete directory: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let old = Path::new(&old_path);
|
||||||
|
let new = Path::new(&new_path);
|
||||||
|
|
||||||
|
if !old.exists() {
|
||||||
|
return Err("Path does not exist".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.exists() {
|
||||||
|
return Err("Destination already exists".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::rename(old, new).map_err(|e| format!("Failed to rename: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -151,6 +151,14 @@ pub fn run() {
|
|||||||
search_clipboard_entries,
|
search_clipboard_entries,
|
||||||
get_clipboard_languages,
|
get_clipboard_languages,
|
||||||
update_clipboard_language,
|
update_clipboard_language,
|
||||||
|
list_directory,
|
||||||
|
read_file_content,
|
||||||
|
write_file_content,
|
||||||
|
create_file,
|
||||||
|
create_directory,
|
||||||
|
delete_file,
|
||||||
|
delete_directory,
|
||||||
|
rename_path,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ fn process_json_line(
|
|||||||
subtype,
|
subtype,
|
||||||
result,
|
result,
|
||||||
permission_denials,
|
permission_denials,
|
||||||
usage: _,
|
usage,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let state = if subtype == "success" {
|
let state = if subtype == "success" {
|
||||||
@@ -713,6 +713,21 @@ fn process_json_line(
|
|||||||
CharacterState::Error
|
CharacterState::Error
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track token usage from Result messages if available
|
||||||
|
// This captures tokens from tool outputs and other operations
|
||||||
|
if let Some(usage_info) = usage {
|
||||||
|
// We need the model info to calculate cost properly
|
||||||
|
// For now, use the last known model from stats
|
||||||
|
let model = {
|
||||||
|
let stats_guard = stats.read();
|
||||||
|
stats_guard.model.clone().unwrap_or_else(|| "claude-opus-4-20250514".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stats_guard = stats.write();
|
||||||
|
stats_guard.add_usage(usage_info.input_tokens, usage_info.output_tokens, &model);
|
||||||
|
println!("Result message tokens - input: {}, output: {}", usage_info.input_tokens, usage_info.output_tokens);
|
||||||
|
}
|
||||||
|
|
||||||
// Always emit updated stats on result message (less frequent)
|
// Always emit updated stats on result message (less frequent)
|
||||||
// This includes the latest session duration
|
// This includes the latest session duration
|
||||||
let newly_unlocked = {
|
let newly_unlocked = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "hikari-desktop",
|
"productName": "hikari-desktop",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"identifier": "com.naomi.hikari-desktop",
|
"identifier": "com.naomi.hikari-desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte";
|
import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte";
|
||||||
import QuickActionsPanel from "$lib/components/QuickActionsPanel.svelte";
|
import QuickActionsPanel from "$lib/components/QuickActionsPanel.svelte";
|
||||||
import ClipboardHistoryPanel from "$lib/components/ClipboardHistoryPanel.svelte";
|
import ClipboardHistoryPanel from "$lib/components/ClipboardHistoryPanel.svelte";
|
||||||
|
import TextInputContextMenu from "$lib/components/TextInputContextMenu.svelte";
|
||||||
import type { Attachment } from "$lib/types/messages";
|
import type { Attachment } from "$lib/types/messages";
|
||||||
|
|
||||||
const INPUT_HISTORY_KEY = "hikari-input-history";
|
const INPUT_HISTORY_KEY = "hikari-input-history";
|
||||||
@@ -49,6 +50,23 @@
|
|||||||
let showClipboardHistory = $state(false);
|
let showClipboardHistory = $state(false);
|
||||||
let streamerModeActive = $state(false);
|
let streamerModeActive = $state(false);
|
||||||
|
|
||||||
|
// Context menu state
|
||||||
|
let textareaElement: HTMLTextAreaElement | undefined = $state();
|
||||||
|
let contextMenuShow = $state(false);
|
||||||
|
let contextMenuX = $state(0);
|
||||||
|
let contextMenuY = $state(0);
|
||||||
|
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
contextMenuShow = true;
|
||||||
|
contextMenuX = event.clientX;
|
||||||
|
contextMenuY = event.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenuShow = false;
|
||||||
|
}
|
||||||
|
|
||||||
isStreamerMode.subscribe((value) => {
|
isStreamerMode.subscribe((value) => {
|
||||||
streamerModeActive = value;
|
streamerModeActive = value;
|
||||||
});
|
});
|
||||||
@@ -876,10 +894,12 @@ User: ${formattedMessage}`;
|
|||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="resize-handle" onmousedown={handleResizeStart} title="Drag to resize"></div>
|
<div class="resize-handle" onmousedown={handleResizeStart} title="Drag to resize"></div>
|
||||||
<textarea
|
<textarea
|
||||||
|
bind:this={textareaElement}
|
||||||
bind:value={inputValue}
|
bind:value={inputValue}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
oninput={handleInputChange}
|
oninput={handleInputChange}
|
||||||
onpaste={handlePaste}
|
onpaste={handlePaste}
|
||||||
|
oncontextmenu={handleContextMenu}
|
||||||
placeholder={isConnected
|
placeholder={isConnected
|
||||||
? "Ask Hikari anything... (type / for commands)"
|
? "Ask Hikari anything... (type / for commands)"
|
||||||
: "Connect to Claude first..."}
|
: "Connect to Claude first..."}
|
||||||
@@ -958,6 +978,15 @@ User: ${formattedMessage}`;
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if contextMenuShow && textareaElement}
|
||||||
|
<TextInputContextMenu
|
||||||
|
x={contextMenuX}
|
||||||
|
y={contextMenuY}
|
||||||
|
inputElement={textareaElement}
|
||||||
|
onClose={closeContextMenu}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.input-bar {
|
.input-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
{ keys: ["Escape"], description: "Close modals and panels" },
|
{ keys: ["Escape"], description: "Close modals and panels" },
|
||||||
{ keys: ["Ctrl", "L"], description: "Clear the terminal" },
|
{ keys: ["Ctrl", "L"], description: "Clear the terminal" },
|
||||||
{ keys: ["Ctrl", ","], description: "Open settings" },
|
{ keys: ["Ctrl", ","], description: "Open settings" },
|
||||||
|
{ keys: ["Ctrl", "E"], description: "Toggle file editor" },
|
||||||
{ keys: ["Ctrl", "Shift", "M"], description: "Toggle compact mode" },
|
{ keys: ["Ctrl", "Shift", "M"], description: "Toggle compact mode" },
|
||||||
{ keys: ["Ctrl", "Shift", "S"], description: "Toggle streamer mode" },
|
{ keys: ["Ctrl", "Shift", "S"], description: "Toggle streamer mode" },
|
||||||
],
|
],
|
||||||
@@ -26,6 +27,17 @@
|
|||||||
{ keys: ["↓"], description: "Next input from history" },
|
{ keys: ["↓"], description: "Next input from history" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
category: "File Editor",
|
||||||
|
items: [
|
||||||
|
{ keys: ["Ctrl", "E"], description: "Toggle editor view" },
|
||||||
|
{ keys: ["Ctrl", "B"], description: "Toggle file browser" },
|
||||||
|
{ keys: ["Ctrl", "S"], description: "Save current file" },
|
||||||
|
{ keys: ["Ctrl", "W"], description: "Close current tab" },
|
||||||
|
{ keys: ["Ctrl", "N"], description: "New file" },
|
||||||
|
{ keys: ["Right-click"], description: "Context menu (New/Delete)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
category: "Slash Commands",
|
category: "Slash Commands",
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { claudeStore } from "$lib/stores/claude";
|
import { claudeStore } from "$lib/stores/claude";
|
||||||
import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config";
|
import { configStore, type HikariConfig, isStreamerMode } from "$lib/stores/config";
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
import type { ConnectionStatus } from "$lib/types/messages";
|
import type { ConnectionStatus } from "$lib/types/messages";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import StatsDisplay from "./StatsDisplay.svelte";
|
import StatsDisplay from "./StatsDisplay.svelte";
|
||||||
@@ -80,6 +81,15 @@
|
|||||||
streamerModeActive = value;
|
streamerModeActive = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let editorVisible = $state(false);
|
||||||
|
editorStore.isEditorVisible.subscribe((value) => {
|
||||||
|
editorVisible = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleEditor() {
|
||||||
|
editorStore.toggleEditor();
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
appVersion = await getVersion();
|
appVersion = await getVersion();
|
||||||
});
|
});
|
||||||
@@ -307,6 +317,25 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={toggleEditor}
|
||||||
|
disabled={connectionStatus !== "connected"}
|
||||||
|
class="p-1 text-gray-500 icon-trans-hover {editorVisible
|
||||||
|
? 'text-[var(--trans-pink)]'
|
||||||
|
: ''} disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
title={connectionStatus === "connected"
|
||||||
|
? "File Editor (Ctrl+E)"
|
||||||
|
: "Connect to enable file editor"}
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showStats = !showStats)}
|
onclick={() => (showStats = !showStats)}
|
||||||
class="p-1 text-gray-500 icon-trans-hover {showStats ? 'text-[var(--trans-pink)]' : ''}"
|
class="p-1 text-gray-500 icon-trans-hover {showStats ? 'text-[var(--trans-pink)]' : ''}"
|
||||||
|
|||||||
@@ -155,6 +155,30 @@
|
|||||||
terminalElement.removeEventListener("copy", handleCopy);
|
terminalElement.removeEventListener("copy", handleCopy);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Copy message content to clipboard
|
||||||
|
async function copyMessage(content: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
// Optionally capture to clipboard history
|
||||||
|
await clipboardStore.captureClipboard(content, null, "Message from chat");
|
||||||
|
|
||||||
|
// Visual feedback could be added here if needed
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy message:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State for showing "Copied!" feedback
|
||||||
|
let copiedMessageId: string | null = null;
|
||||||
|
|
||||||
|
async function handleCopyMessage(messageId: string, content: string) {
|
||||||
|
await copyMessage(content);
|
||||||
|
copiedMessageId = messageId;
|
||||||
|
setTimeout(() => {
|
||||||
|
copiedMessageId = null;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -185,7 +209,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each lines as line (line.id)}
|
{#each lines as line (line.id)}
|
||||||
<div class="terminal-line mb-2 {getLineClass(line.type)}">
|
<div class="terminal-line mb-2 {getLineClass(line.type)} relative group">
|
||||||
<span class="terminal-timestamp text-xs mr-2">{formatTime(line.timestamp)}</span>
|
<span class="terminal-timestamp text-xs mr-2">{formatTime(line.timestamp)}</span>
|
||||||
{#if getLinePrefix(line.type)}
|
{#if getLinePrefix(line.type)}
|
||||||
<span class="terminal-prefix mr-2">{getLinePrefix(line.type)}</span>
|
<span class="terminal-prefix mr-2">{getLinePrefix(line.type)}</span>
|
||||||
@@ -193,11 +217,33 @@
|
|||||||
{#if line.toolName}
|
{#if line.toolName}
|
||||||
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if line.type === "assistant"}
|
{#if line.type === "assistant" || line.type === "user"}
|
||||||
|
<div class="message-content-wrapper">
|
||||||
<Markdown
|
<Markdown
|
||||||
content={maskPaths(line.content, hidePaths)}
|
content={maskPaths(line.content, hidePaths)}
|
||||||
searchQuery={currentSearchQuery}
|
searchQuery={currentSearchQuery}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
class="copy-message-btn opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onclick={() => handleCopyMessage(line.id, line.content)}
|
||||||
|
title="Copy message"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="copy-text">{copiedMessageId === line.id ? "Copied!" : "Copy"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<HighlightedText
|
<HighlightedText
|
||||||
content={maskPaths(line.content, hidePaths)}
|
content={maskPaths(line.content, hidePaths)}
|
||||||
@@ -267,4 +313,45 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message content wrapper for positioning */
|
||||||
|
.message-content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button styling */
|
||||||
|
.copy-message-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-message-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-hover, var(--border-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-message-btn svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure relative positioning for parent */
|
||||||
|
.terminal-line {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
inputElement: HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { x, y, inputElement, onClose }: Props = $props();
|
||||||
|
|
||||||
|
// Menu element reference for measuring
|
||||||
|
let menuElement: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
// Adjusted position to keep menu within viewport
|
||||||
|
let adjustedX = $derived.by(() => {
|
||||||
|
if (!menuElement) return x;
|
||||||
|
const menuWidth = menuElement.offsetWidth || 180;
|
||||||
|
const maxX = window.innerWidth - menuWidth - 8;
|
||||||
|
return Math.min(x, maxX);
|
||||||
|
});
|
||||||
|
|
||||||
|
let adjustedY = $derived.by(() => {
|
||||||
|
if (!menuElement) return y;
|
||||||
|
const menuHeight = menuElement.offsetHeight || 250;
|
||||||
|
const maxY = window.innerHeight - menuHeight - 8;
|
||||||
|
return Math.min(y, maxY);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function execCommand(command: "cut" | "copy" | "paste" | "selectAll" | "undo" | "redo") {
|
||||||
|
inputElement.focus();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "cut":
|
||||||
|
document.execCommand("cut");
|
||||||
|
break;
|
||||||
|
case "copy":
|
||||||
|
document.execCommand("copy");
|
||||||
|
break;
|
||||||
|
case "paste":
|
||||||
|
document.execCommand("paste");
|
||||||
|
break;
|
||||||
|
case "selectAll":
|
||||||
|
inputElement.select();
|
||||||
|
break;
|
||||||
|
case "undo":
|
||||||
|
document.execCommand("undo");
|
||||||
|
break;
|
||||||
|
case "redo":
|
||||||
|
document.execCommand("redo");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a selection
|
||||||
|
let hasSelection = $derived(inputElement.selectionStart !== inputElement.selectionEnd);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||||
|
<div
|
||||||
|
class="menu-overlay"
|
||||||
|
onclick={onClose}
|
||||||
|
oncontextmenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={menuElement}
|
||||||
|
class="menu-content"
|
||||||
|
style="left: {adjustedX}px; top: {adjustedY}px;"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button class="menu-item" onclick={() => execCommand("undo")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 10h10a5 5 0 0 1 5 5v2M3 10l4-4M3 10l4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Undo
|
||||||
|
<span class="shortcut">Ctrl+Z</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("redo")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 10H11a5 5 0 0 0-5 5v2M21 10l-4-4M21 10l-4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Redo
|
||||||
|
<span class="shortcut">Ctrl+Y</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("cut")} disabled={!hasSelection}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M14.121 14.121L19 19m-7-7l7-7m-7 7l-2.879 2.879M12 12L9.121 9.121m0 5.758a3 3 0 1 0-4.243-4.243 3 3 0 0 0 4.243 4.243zm0-5.758a3 3 0 1 0-4.243-4.243 3 3 0 0 0 4.243 4.243z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Cut
|
||||||
|
<span class="shortcut">Ctrl+X</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("copy")} disabled={!hasSelection}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 16H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2m-6 12h8a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
<span class="shortcut">Ctrl+C</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("paste")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Paste
|
||||||
|
<span class="shortcut">Ctrl+V</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("selectAll")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9h6v6H9z" />
|
||||||
|
</svg>
|
||||||
|
Select All
|
||||||
|
<span class="shortcut">Ctrl+A</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
min-width: 180px;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover:not(:disabled) {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { EditorView, basicSetup } from "codemirror";
|
||||||
|
import { EditorState, Compartment } from "@codemirror/state";
|
||||||
|
import { keymap } from "@codemirror/view";
|
||||||
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
|
import { python } from "@codemirror/lang-python";
|
||||||
|
import { rust } from "@codemirror/lang-rust";
|
||||||
|
import { html } from "@codemirror/lang-html";
|
||||||
|
import { css } from "@codemirror/lang-css";
|
||||||
|
import { json } from "@codemirror/lang-json";
|
||||||
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
|
import { xml } from "@codemirror/lang-xml";
|
||||||
|
import { sql } from "@codemirror/lang-sql";
|
||||||
|
import { java } from "@codemirror/lang-java";
|
||||||
|
import { cpp } from "@codemirror/lang-cpp";
|
||||||
|
import { php } from "@codemirror/lang-php";
|
||||||
|
import { go } from "@codemirror/lang-go";
|
||||||
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
|
import { sass } from "@codemirror/lang-sass";
|
||||||
|
import { less } from "@codemirror/lang-less";
|
||||||
|
import { vue } from "@codemirror/lang-vue";
|
||||||
|
import { angular } from "@codemirror/lang-angular";
|
||||||
|
import { wast } from "@codemirror/lang-wast";
|
||||||
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
|
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||||
|
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
|
||||||
|
import { swift } from "@codemirror/legacy-modes/mode/swift";
|
||||||
|
import { lua } from "@codemirror/legacy-modes/mode/lua";
|
||||||
|
import { r } from "@codemirror/legacy-modes/mode/r";
|
||||||
|
import { toml } from "@codemirror/legacy-modes/mode/toml";
|
||||||
|
import { dockerFile } from "@codemirror/legacy-modes/mode/dockerfile";
|
||||||
|
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||||
|
import { tags } from "@lezer/highlight";
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
import { configStore } from "$lib/stores/config";
|
||||||
|
import EditorContextMenu from "./EditorContextMenu.svelte";
|
||||||
|
import type { EditorTab } from "$lib/types/editor";
|
||||||
|
import type { Extension } from "@codemirror/state";
|
||||||
|
|
||||||
|
export let tab: EditorTab;
|
||||||
|
|
||||||
|
let editorContainer: HTMLDivElement;
|
||||||
|
let view: EditorView | null = null;
|
||||||
|
let themeCompartment = new Compartment();
|
||||||
|
|
||||||
|
// Context menu state
|
||||||
|
let contextMenuShow = false;
|
||||||
|
let contextMenuX = 0;
|
||||||
|
let contextMenuY = 0;
|
||||||
|
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
contextMenuShow = true;
|
||||||
|
contextMenuX = event.clientX;
|
||||||
|
contextMenuY = event.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenuShow = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to theme changes
|
||||||
|
const config = configStore.config;
|
||||||
|
|
||||||
|
// Light theme
|
||||||
|
const lightTheme = EditorView.theme(
|
||||||
|
{
|
||||||
|
"&": {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
color: "#24292e",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
caretColor: "#24292e",
|
||||||
|
},
|
||||||
|
".cm-cursor, .cm-dropCursor": {
|
||||||
|
borderLeftColor: "#24292e",
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||||
|
backgroundColor: "#c8d3f5",
|
||||||
|
},
|
||||||
|
".cm-panels": {
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
color: "#24292e",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-top": {
|
||||||
|
borderBottom: "1px solid #e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-bottom": {
|
||||||
|
borderTop: "1px solid #e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-searchMatch": {
|
||||||
|
backgroundColor: "#ffdf5d",
|
||||||
|
outline: "1px solid #c4a000",
|
||||||
|
},
|
||||||
|
".cm-searchMatch.cm-searchMatch-selected": {
|
||||||
|
backgroundColor: "#c4a000",
|
||||||
|
},
|
||||||
|
".cm-activeLine": {
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
},
|
||||||
|
".cm-selectionMatch": {
|
||||||
|
backgroundColor: "#c8d3f5",
|
||||||
|
},
|
||||||
|
".cm-matchingBracket, .cm-nonmatchingBracket": {
|
||||||
|
backgroundColor: "#c8d3f5",
|
||||||
|
outline: "1px solid #888",
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
color: "#6a737d",
|
||||||
|
border: "none",
|
||||||
|
borderRight: "1px solid #e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-activeLineGutter": {
|
||||||
|
backgroundColor: "#e1e4e8",
|
||||||
|
},
|
||||||
|
".cm-foldPlaceholder": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: "#6a737d",
|
||||||
|
},
|
||||||
|
".cm-tooltip": {
|
||||||
|
border: "1px solid #e1e4e8",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||||
|
borderTopColor: "transparent",
|
||||||
|
borderBottomColor: "transparent",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||||
|
borderTopColor: "#ffffff",
|
||||||
|
borderBottomColor: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete": {
|
||||||
|
"& > ul > li[aria-selected]": {
|
||||||
|
backgroundColor: "#e1e4e8",
|
||||||
|
color: "#24292e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const lightHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.keyword, color: "#d73a49" },
|
||||||
|
{
|
||||||
|
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||||
|
color: "#6f42c1",
|
||||||
|
},
|
||||||
|
{ tag: [tags.function(tags.variableName), tags.labelName], color: "#6f42c1" },
|
||||||
|
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: "#005cc5" },
|
||||||
|
{ tag: [tags.definition(tags.name), tags.separator], color: "#24292e" },
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.typeName,
|
||||||
|
tags.className,
|
||||||
|
tags.number,
|
||||||
|
tags.changed,
|
||||||
|
tags.annotation,
|
||||||
|
tags.modifier,
|
||||||
|
tags.self,
|
||||||
|
tags.namespace,
|
||||||
|
],
|
||||||
|
color: "#e36209",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.operator,
|
||||||
|
tags.operatorKeyword,
|
||||||
|
tags.url,
|
||||||
|
tags.escape,
|
||||||
|
tags.regexp,
|
||||||
|
tags.link,
|
||||||
|
tags.special(tags.string),
|
||||||
|
],
|
||||||
|
color: "#032f62",
|
||||||
|
},
|
||||||
|
{ tag: [tags.meta, tags.comment], color: "#6a737d" },
|
||||||
|
{ tag: tags.strong, fontWeight: "bold" },
|
||||||
|
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||||
|
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||||
|
{ tag: tags.link, color: "#032f62", textDecoration: "underline" },
|
||||||
|
{ tag: tags.heading, fontWeight: "bold", color: "#005cc5" },
|
||||||
|
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: "#005cc5" },
|
||||||
|
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: "#22863a" },
|
||||||
|
{ tag: tags.invalid, color: "#cb2431" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// High contrast theme
|
||||||
|
const highContrastTheme = EditorView.theme(
|
||||||
|
{
|
||||||
|
"&": {
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
caretColor: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-cursor, .cm-dropCursor": {
|
||||||
|
borderLeftColor: "#ffffff",
|
||||||
|
borderLeftWidth: "2px",
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||||
|
backgroundColor: "#264f78",
|
||||||
|
},
|
||||||
|
".cm-panels": {
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-top": {
|
||||||
|
borderBottom: "2px solid #ffffff",
|
||||||
|
},
|
||||||
|
".cm-panels.cm-panels-bottom": {
|
||||||
|
borderTop: "2px solid #ffffff",
|
||||||
|
},
|
||||||
|
".cm-searchMatch": {
|
||||||
|
backgroundColor: "#515c6a",
|
||||||
|
outline: "2px solid #ffff00",
|
||||||
|
},
|
||||||
|
".cm-searchMatch.cm-searchMatch-selected": {
|
||||||
|
backgroundColor: "#ffff00",
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
".cm-activeLine": {
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
},
|
||||||
|
".cm-selectionMatch": {
|
||||||
|
backgroundColor: "#264f78",
|
||||||
|
},
|
||||||
|
".cm-matchingBracket, .cm-nonmatchingBracket": {
|
||||||
|
backgroundColor: "#515c6a",
|
||||||
|
outline: "2px solid #ffff00",
|
||||||
|
},
|
||||||
|
".cm-gutters": {
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
color: "#858585",
|
||||||
|
border: "none",
|
||||||
|
borderRight: "2px solid #ffffff",
|
||||||
|
},
|
||||||
|
".cm-activeLineGutter": {
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".cm-foldPlaceholder": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: "none",
|
||||||
|
color: "#ffff00",
|
||||||
|
},
|
||||||
|
".cm-tooltip": {
|
||||||
|
border: "2px solid #ffffff",
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||||
|
borderTopColor: "transparent",
|
||||||
|
borderBottomColor: "transparent",
|
||||||
|
},
|
||||||
|
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||||
|
borderTopColor: "#000000",
|
||||||
|
borderBottomColor: "#000000",
|
||||||
|
},
|
||||||
|
".cm-tooltip-autocomplete": {
|
||||||
|
"& > ul > li[aria-selected]": {
|
||||||
|
backgroundColor: "#264f78",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const highContrastHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.keyword, color: "#569cd6", fontWeight: "bold" },
|
||||||
|
{
|
||||||
|
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||||
|
color: "#9cdcfe",
|
||||||
|
},
|
||||||
|
{ tag: [tags.function(tags.variableName), tags.labelName], color: "#dcdcaa" },
|
||||||
|
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: "#4fc1ff" },
|
||||||
|
{ tag: [tags.definition(tags.name), tags.separator], color: "#ffffff" },
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.typeName,
|
||||||
|
tags.className,
|
||||||
|
tags.number,
|
||||||
|
tags.changed,
|
||||||
|
tags.annotation,
|
||||||
|
tags.modifier,
|
||||||
|
tags.self,
|
||||||
|
tags.namespace,
|
||||||
|
],
|
||||||
|
color: "#4ec9b0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: [
|
||||||
|
tags.operator,
|
||||||
|
tags.operatorKeyword,
|
||||||
|
tags.url,
|
||||||
|
tags.escape,
|
||||||
|
tags.regexp,
|
||||||
|
tags.link,
|
||||||
|
tags.special(tags.string),
|
||||||
|
],
|
||||||
|
color: "#d4d4d4",
|
||||||
|
},
|
||||||
|
{ tag: [tags.meta, tags.comment], color: "#6a9955" },
|
||||||
|
{ tag: tags.strong, fontWeight: "bold" },
|
||||||
|
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||||
|
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||||
|
{ tag: tags.link, color: "#3794ff", textDecoration: "underline" },
|
||||||
|
{ tag: tags.heading, fontWeight: "bold", color: "#569cd6" },
|
||||||
|
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: "#569cd6" },
|
||||||
|
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: "#ce9178" },
|
||||||
|
{ tag: tags.invalid, color: "#f44747" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function getThemeExtension(theme: string): Extension {
|
||||||
|
switch (theme) {
|
||||||
|
case "light":
|
||||||
|
return [lightTheme, syntaxHighlighting(lightHighlightStyle)];
|
||||||
|
case "high-contrast":
|
||||||
|
return [highContrastTheme, syntaxHighlighting(highContrastHighlightStyle)];
|
||||||
|
case "dark":
|
||||||
|
case "custom":
|
||||||
|
default:
|
||||||
|
return oneDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLanguageExtension(language: string): Extension {
|
||||||
|
const languageMap: Record<string, () => Extension> = {
|
||||||
|
javascript: () => javascript({ jsx: true, typescript: false }),
|
||||||
|
typescript: () => javascript({ jsx: true, typescript: true }),
|
||||||
|
python: () => python(),
|
||||||
|
rust: () => rust(),
|
||||||
|
html: () => html(),
|
||||||
|
css: () => css(),
|
||||||
|
json: () => json(),
|
||||||
|
markdown: () => markdown(),
|
||||||
|
xml: () => xml(),
|
||||||
|
sql: () => sql(),
|
||||||
|
java: () => java(),
|
||||||
|
c: () => cpp(),
|
||||||
|
cpp: () => cpp(),
|
||||||
|
csharp: () => cpp(),
|
||||||
|
php: () => php(),
|
||||||
|
go: () => go(),
|
||||||
|
yaml: () => yaml(),
|
||||||
|
scss: () => sass(),
|
||||||
|
sass: () => sass(),
|
||||||
|
less: () => less(),
|
||||||
|
vue: () => vue(),
|
||||||
|
angular: () => angular(),
|
||||||
|
wasm: () => wast(),
|
||||||
|
shell: () => StreamLanguage.define(shell),
|
||||||
|
ruby: () => StreamLanguage.define(ruby),
|
||||||
|
swift: () => StreamLanguage.define(swift),
|
||||||
|
lua: () => StreamLanguage.define(lua),
|
||||||
|
r: () => StreamLanguage.define(r),
|
||||||
|
toml: () => StreamLanguage.define(toml),
|
||||||
|
dockerfile: () => StreamLanguage.define(dockerFile),
|
||||||
|
powershell: () => StreamLanguage.define(powerShell),
|
||||||
|
svelte: () => html(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExtension = languageMap[language];
|
||||||
|
return getExtension ? getExtension() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEditor() {
|
||||||
|
if (!editorContainer) return;
|
||||||
|
|
||||||
|
const currentTheme = $config.theme;
|
||||||
|
|
||||||
|
const saveKeymap = keymap.of([
|
||||||
|
{
|
||||||
|
key: "Mod-s",
|
||||||
|
run: () => {
|
||||||
|
editorStore.saveFile(tab.id);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const updateListener = EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
const content = update.state.doc.toString();
|
||||||
|
editorStore.updateTabContent(tab.id, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: tab.content,
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
themeCompartment.of(getThemeExtension(currentTheme)),
|
||||||
|
getLanguageExtension(tab.language),
|
||||||
|
saveKeymap,
|
||||||
|
updateListener,
|
||||||
|
EditorView.theme({
|
||||||
|
"&": {
|
||||||
|
height: "100%",
|
||||||
|
fontSize: "14px",
|
||||||
|
},
|
||||||
|
".cm-scroller": {
|
||||||
|
overflow: "auto",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
view = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: editorContainer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyEditor() {
|
||||||
|
if (view) {
|
||||||
|
view.destroy();
|
||||||
|
view = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for theme changes and update the editor
|
||||||
|
$: if (view && $config.theme) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: themeCompartment.reconfigure(getThemeExtension($config.theme)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
createEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
destroyEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (view && tab.content !== view.state.doc.toString()) {
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.state.doc.length,
|
||||||
|
insert: tab.content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="code-editor" bind:this={editorContainer} oncontextmenu={handleContextMenu}></div>
|
||||||
|
|
||||||
|
{#if contextMenuShow && view}
|
||||||
|
<EditorContextMenu
|
||||||
|
x={contextMenuX}
|
||||||
|
y={contextMenuY}
|
||||||
|
editorView={view}
|
||||||
|
onClose={closeContextMenu}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code-editor {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor :global(.cm-editor) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor :global(.cm-focused) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
destructive?: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmText = "Confirm",
|
||||||
|
cancelText = "Cancel",
|
||||||
|
destructive = false,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onCancel();
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="dialog-overlay" onclick={onCancel}>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="dialog-content" onclick={(e) => e.stopPropagation()}>
|
||||||
|
<h2 class="dialog-title">{title}</h2>
|
||||||
|
<p class="dialog-message">{message}</p>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="btn-cancel" onclick={onCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button class="btn-confirm" class:destructive onclick={onConfirm}>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
margin: 0 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-message {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:hover {
|
||||||
|
background-color: var(--accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm.destructive {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm.destructive:hover {
|
||||||
|
background-color: #b91c1c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
editorView: EditorView;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { x, y, editorView, onClose }: Props = $props();
|
||||||
|
|
||||||
|
// Menu element reference for measuring
|
||||||
|
let menuElement: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
// Adjusted position to keep menu within viewport
|
||||||
|
let adjustedX = $derived.by(() => {
|
||||||
|
if (!menuElement) return x;
|
||||||
|
const menuWidth = menuElement.offsetWidth || 180;
|
||||||
|
const maxX = window.innerWidth - menuWidth - 8;
|
||||||
|
return Math.min(x, maxX);
|
||||||
|
});
|
||||||
|
|
||||||
|
let adjustedY = $derived.by(() => {
|
||||||
|
if (!menuElement) return y;
|
||||||
|
const menuHeight = menuElement.offsetHeight || 250;
|
||||||
|
const maxY = window.innerHeight - menuHeight - 8;
|
||||||
|
return Math.min(y, maxY);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function execCommand(command: "cut" | "copy" | "paste" | "selectAll" | "undo" | "redo") {
|
||||||
|
editorView.focus();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "cut":
|
||||||
|
document.execCommand("cut");
|
||||||
|
break;
|
||||||
|
case "copy":
|
||||||
|
document.execCommand("copy");
|
||||||
|
break;
|
||||||
|
case "paste":
|
||||||
|
document.execCommand("paste");
|
||||||
|
break;
|
||||||
|
case "selectAll":
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: { anchor: 0, head: editorView.state.doc.length },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "undo":
|
||||||
|
import("@codemirror/commands").then(({ undo }) => {
|
||||||
|
undo(editorView);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "redo":
|
||||||
|
import("@codemirror/commands").then(({ redo }) => {
|
||||||
|
redo(editorView);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a selection
|
||||||
|
let hasSelection = $derived(
|
||||||
|
editorView.state.selection.main.from !== editorView.state.selection.main.to
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
||||||
|
<div
|
||||||
|
class="menu-overlay"
|
||||||
|
onclick={onClose}
|
||||||
|
oncontextmenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={menuElement}
|
||||||
|
class="menu-content"
|
||||||
|
style="left: {adjustedX}px; top: {adjustedY}px;"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button class="menu-item" onclick={() => execCommand("undo")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 10h10a5 5 0 0 1 5 5v2M3 10l4-4M3 10l4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Undo
|
||||||
|
<span class="shortcut">Ctrl+Z</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("redo")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 10H11a5 5 0 0 0-5 5v2M21 10l-4-4M21 10l-4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Redo
|
||||||
|
<span class="shortcut">Ctrl+Y</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("cut")} disabled={!hasSelection}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M14.121 14.121L19 19m-7-7l7-7m-7 7l-2.879 2.879M12 12L9.121 9.121m0 5.758a3 3 0 1 0-4.243-4.243 3 3 0 0 0 4.243 4.243zm0-5.758a3 3 0 1 0-4.243-4.243 3 3 0 0 0 4.243 4.243z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Cut
|
||||||
|
<span class="shortcut">Ctrl+X</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("copy")} disabled={!hasSelection}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 16H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2m-6 12h8a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
<span class="shortcut">Ctrl+C</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("paste")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Paste
|
||||||
|
<span class="shortcut">Ctrl+V</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={() => execCommand("selectAll")}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9h6v6H9z" />
|
||||||
|
</svg>
|
||||||
|
Select All
|
||||||
|
<span class="shortcut">Ctrl+A</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
min-width: 180px;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover:not(:disabled) {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
import FileBrowser from "./FileBrowser.svelte";
|
||||||
|
import EditorTabs from "./EditorTabs.svelte";
|
||||||
|
import CodeEditor from "./CodeEditor.svelte";
|
||||||
|
|
||||||
|
const isFileBrowserOpen = editorStore.isFileBrowserOpen;
|
||||||
|
const activeTab = editorStore.activeTab;
|
||||||
|
const saveError = editorStore.saveError;
|
||||||
|
|
||||||
|
function toggleFileBrowser() {
|
||||||
|
editorStore.toggleFileBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
try {
|
||||||
|
await editorStore.saveFile();
|
||||||
|
} catch {
|
||||||
|
// Error is already set in the store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editor-panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<button
|
||||||
|
class="toolbar-button"
|
||||||
|
class:active={$isFileBrowserOpen}
|
||||||
|
on:click={toggleFileBrowser}
|
||||||
|
title="Toggle file browser (Ctrl+B)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="toolbar-spacer"></div>
|
||||||
|
|
||||||
|
{#if $activeTab}
|
||||||
|
<button class="toolbar-button" on:click={handleSave} title="Save (Ctrl+S)">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
||||||
|
<polyline points="17 21 17 13 7 13 7 21" />
|
||||||
|
<polyline points="7 3 7 8 15 8" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $saveError}
|
||||||
|
<div class="error-banner">
|
||||||
|
<span>{$saveError}</span>
|
||||||
|
<button
|
||||||
|
class="dismiss-button"
|
||||||
|
on:click={() => {}}
|
||||||
|
title="Dismiss error"
|
||||||
|
aria-label="Dismiss error"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="editor-content">
|
||||||
|
{#if $isFileBrowserOpen}
|
||||||
|
<div class="file-browser-container">
|
||||||
|
<FileBrowser />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="editor-main">
|
||||||
|
<EditorTabs />
|
||||||
|
|
||||||
|
<div class="editor-area">
|
||||||
|
{#if $activeTab}
|
||||||
|
{#key $activeTab.id}
|
||||||
|
<CodeEditor tab={$activeTab} />
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
|
<div class="no-file">
|
||||||
|
<div class="no-file-content">
|
||||||
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
</svg>
|
||||||
|
<p>Select a file to edit</p>
|
||||||
|
<p class="hint">Use the file browser on the left or press Ctrl+B to toggle it</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button.active {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #ff000022;
|
||||||
|
border-bottom: 1px solid #ff0000;
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dismiss-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #ff6b6b;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dismiss-button:hover {
|
||||||
|
background-color: #ff000033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-container {
|
||||||
|
width: 250px;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 400px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-file {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--bg-terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-file-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-file-content svg {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-file-content p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-file-content .hint {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
|
||||||
|
const tabs = editorStore.tabs;
|
||||||
|
const activeTabId = editorStore.activeTabId;
|
||||||
|
|
||||||
|
function handleTabClick(tabId: string) {
|
||||||
|
editorStore.setActiveTab(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseTab(event: MouseEvent, tabId: string) {
|
||||||
|
event.stopPropagation();
|
||||||
|
editorStore.closeTab(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent, tabId: string) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
editorStore.setActiveTab(tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseKeydown(event: KeyboardEvent, tabId: string) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
editorStore.closeTab(tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editor-tabs">
|
||||||
|
{#if $tabs.length === 0}
|
||||||
|
<div class="no-tabs">No files open</div>
|
||||||
|
{:else}
|
||||||
|
<div class="tabs-container" role="tablist">
|
||||||
|
{#each $tabs as tab (tab.id)}
|
||||||
|
<div
|
||||||
|
class="tab"
|
||||||
|
class:active={tab.id === $activeTabId}
|
||||||
|
class:dirty={tab.isDirty}
|
||||||
|
role="tab"
|
||||||
|
tabindex="0"
|
||||||
|
aria-selected={tab.id === $activeTabId}
|
||||||
|
on:click={() => handleTabClick(tab.id)}
|
||||||
|
on:keydown={(e) => handleKeydown(e, tab.id)}
|
||||||
|
title={tab.filePath}
|
||||||
|
>
|
||||||
|
<span class="tab-name">
|
||||||
|
{tab.fileName}
|
||||||
|
{#if tab.isDirty}
|
||||||
|
<span class="dirty-indicator">*</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
on:click={(e) => handleCloseTab(e, tab.id)}
|
||||||
|
on:keydown={(e) => handleCloseKeydown(e, tab.id)}
|
||||||
|
title="Close tab"
|
||||||
|
aria-label="Close {tab.fileName}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tabs {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:focus {
|
||||||
|
outline: 1px solid var(--accent-primary);
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background-color: var(--bg-terminal);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 2px solid var(--accent-primary);
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dirty-indicator {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover .close-button,
|
||||||
|
.tab.active .close-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.dirty .close-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
import FileTreeItem from "./FileTreeItem.svelte";
|
||||||
|
import FileContextMenu from "./FileContextMenu.svelte";
|
||||||
|
import InputDialog from "./InputDialog.svelte";
|
||||||
|
import ConfirmDialog from "./ConfirmDialog.svelte";
|
||||||
|
import type { FileEntry } from "$lib/types/editor";
|
||||||
|
|
||||||
|
const fileTree = editorStore.fileTree;
|
||||||
|
const isLoadingTree = editorStore.isLoadingTree;
|
||||||
|
const currentDirectory = editorStore.currentDirectory;
|
||||||
|
|
||||||
|
// Listen for Ctrl+N keyboard shortcut from +page.svelte
|
||||||
|
function handleNewFileEvent() {
|
||||||
|
handleNewFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener("editor-new-file", handleNewFileEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener("editor-new-file", handleNewFileEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Context menu state
|
||||||
|
let contextMenu = $state<{
|
||||||
|
show: boolean;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
targetEntry: FileEntry | null;
|
||||||
|
}>({
|
||||||
|
show: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
targetEntry: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dialog state
|
||||||
|
let dialog = $state<{
|
||||||
|
type: "newFile" | "newFolder" | "delete" | "rename" | null;
|
||||||
|
parentPath: string;
|
||||||
|
targetEntry: FileEntry | null;
|
||||||
|
}>({
|
||||||
|
type: null,
|
||||||
|
parentPath: "",
|
||||||
|
targetEntry: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
const dir = $currentDirectory;
|
||||||
|
if (dir) {
|
||||||
|
editorStore.initializeFileTree(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(event: MouseEvent, entry: FileEntry | null = null) {
|
||||||
|
event.preventDefault();
|
||||||
|
contextMenu = {
|
||||||
|
show: true,
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
targetEntry: entry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenu = { show: false, x: 0, y: 0, targetEntry: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewFileDialog(parentPath: string) {
|
||||||
|
dialog = { type: "newFile", parentPath, targetEntry: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewFolderDialog(parentPath: string) {
|
||||||
|
dialog = { type: "newFolder", parentPath, targetEntry: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteDialog(entry: FileEntry) {
|
||||||
|
dialog = { type: "delete", parentPath: "", targetEntry: entry };
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRenameDialog(entry: FileEntry) {
|
||||||
|
dialog = { type: "rename", parentPath: "", targetEntry: entry };
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
dialog = { type: null, parentPath: "", targetEntry: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateFile(fileName: string) {
|
||||||
|
await editorStore.createFile(dialog.parentPath, fileName);
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateFolder(folderName: string) {
|
||||||
|
await editorStore.createDirectory(dialog.parentPath, folderName);
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!dialog.targetEntry) return;
|
||||||
|
|
||||||
|
if (dialog.targetEntry.isDirectory) {
|
||||||
|
await editorStore.deleteDirectory(dialog.targetEntry.path);
|
||||||
|
} else {
|
||||||
|
await editorStore.deleteFile(dialog.targetEntry.path);
|
||||||
|
}
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRename(newName: string) {
|
||||||
|
if (!dialog.targetEntry) return;
|
||||||
|
await editorStore.renamePath(dialog.targetEntry.path, newName);
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewFile() {
|
||||||
|
openNewFileDialog($currentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewFolder() {
|
||||||
|
openNewFolderDialog($currentDirectory);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="file-browser" oncontextmenu={(e) => handleContextMenu(e, null)}>
|
||||||
|
<div class="header">
|
||||||
|
<span class="title">Files</span>
|
||||||
|
<div class="header-buttons">
|
||||||
|
<button class="header-button" onclick={handleNewFile} title="New File (Ctrl+N)">
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<path d="M14 2v6h6" />
|
||||||
|
<path d="M12 18v-6" />
|
||||||
|
<path d="M9 15h6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="header-button" onclick={handleNewFolder} title="New Folder">
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||||
|
<path d="M12 11v6" />
|
||||||
|
<path d="M9 14h6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="header-button" onclick={handleRefresh} title="Refresh file tree">
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M23 4v6h-6" />
|
||||||
|
<path d="M1 20v-6h6" />
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree-container">
|
||||||
|
{#if $isLoadingTree}
|
||||||
|
<div class="loading">
|
||||||
|
<svg
|
||||||
|
class="spinner"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32">
|
||||||
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
dur="1s"
|
||||||
|
values="32;0"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
{:else if $fileTree.length === 0}
|
||||||
|
<div class="empty">
|
||||||
|
<span>No files found</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="tree">
|
||||||
|
{#each $fileTree as entry (entry.path)}
|
||||||
|
<FileTreeItem {entry} onContextMenu={handleContextMenu} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if contextMenu.show}
|
||||||
|
<FileContextMenu
|
||||||
|
x={contextMenu.x}
|
||||||
|
y={contextMenu.y}
|
||||||
|
targetEntry={contextMenu.targetEntry}
|
||||||
|
currentDirectory={$currentDirectory}
|
||||||
|
onNewFile={openNewFileDialog}
|
||||||
|
onNewFolder={openNewFolderDialog}
|
||||||
|
onRename={openRenameDialog}
|
||||||
|
onDelete={openDeleteDialog}
|
||||||
|
onClose={closeContextMenu}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if dialog.type === "newFile"}
|
||||||
|
<InputDialog
|
||||||
|
title="New File"
|
||||||
|
placeholder="Enter file name..."
|
||||||
|
confirmText="Create"
|
||||||
|
onConfirm={handleCreateFile}
|
||||||
|
onCancel={closeDialog}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if dialog.type === "newFolder"}
|
||||||
|
<InputDialog
|
||||||
|
title="New Folder"
|
||||||
|
placeholder="Enter folder name..."
|
||||||
|
confirmText="Create"
|
||||||
|
onConfirm={handleCreateFolder}
|
||||||
|
onCancel={closeDialog}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if dialog.type === "delete" && dialog.targetEntry}
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Delete {dialog.targetEntry.isDirectory ? 'Folder' : 'File'}"
|
||||||
|
message="Are you sure you want to delete '{dialog.targetEntry.name}'? {dialog.targetEntry
|
||||||
|
.isDirectory
|
||||||
|
? 'This will also delete all files and folders inside it.'
|
||||||
|
: 'This action cannot be undone.'}"
|
||||||
|
confirmText="Delete"
|
||||||
|
destructive={true}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
onCancel={closeDialog}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if dialog.type === "rename" && dialog.targetEntry}
|
||||||
|
<InputDialog
|
||||||
|
title="Rename {dialog.targetEntry.isDirectory ? 'Folder' : 'File'}"
|
||||||
|
placeholder="Enter new name..."
|
||||||
|
confirmText="Rename"
|
||||||
|
initialValue={dialog.targetEntry.name}
|
||||||
|
onConfirm={handleRename}
|
||||||
|
onCancel={closeDialog}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-browser {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { FileEntry } from "$lib/types/editor";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
targetEntry: FileEntry | null;
|
||||||
|
currentDirectory: string;
|
||||||
|
onNewFile: (parentPath: string) => void;
|
||||||
|
onNewFolder: (parentPath: string) => void;
|
||||||
|
onRename: (entry: FileEntry) => void;
|
||||||
|
onDelete: (entry: FileEntry) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
targetEntry,
|
||||||
|
currentDirectory,
|
||||||
|
onNewFile,
|
||||||
|
onNewFolder,
|
||||||
|
onRename,
|
||||||
|
onDelete,
|
||||||
|
onClose,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
function handleNewFile() {
|
||||||
|
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
||||||
|
onNewFile(parentPath);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewFolder() {
|
||||||
|
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
||||||
|
onNewFolder(parentPath);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRename() {
|
||||||
|
if (targetEntry) {
|
||||||
|
onRename(targetEntry);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (targetEntry) {
|
||||||
|
onDelete(targetEntry);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu element reference for measuring
|
||||||
|
let menuElement: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
// Adjusted position to keep menu within viewport
|
||||||
|
let adjustedX = $derived.by(() => {
|
||||||
|
if (!menuElement) return x;
|
||||||
|
const menuWidth = menuElement.offsetWidth || 160;
|
||||||
|
const maxX = window.innerWidth - menuWidth - 8;
|
||||||
|
return Math.min(x, maxX);
|
||||||
|
});
|
||||||
|
|
||||||
|
let adjustedY = $derived.by(() => {
|
||||||
|
if (!menuElement) return y;
|
||||||
|
const menuHeight = menuElement.offsetHeight || 200;
|
||||||
|
const maxY = window.innerHeight - menuHeight - 8;
|
||||||
|
return Math.min(y, maxY);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="menu-overlay"
|
||||||
|
onclick={onClose}
|
||||||
|
oncontextmenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={menuElement}
|
||||||
|
class="menu-content"
|
||||||
|
style="left: {adjustedX}px; top: {adjustedY}px;"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button class="menu-item" onclick={handleNewFile}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
New File
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={handleNewFolder}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
New Folder
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if targetEntry}
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
|
||||||
|
<button class="menu-item" onclick={handleRename}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="menu-item destructive" onclick={handleDelete}>
|
||||||
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Delete {targetEntry.isDirectory ? "Folder" : "File"}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
min-width: 160px;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.destructive {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { FileEntry } from "$lib/types/editor";
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
import Self from "./FileTreeItem.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
entry: FileEntry;
|
||||||
|
depth?: number;
|
||||||
|
onContextMenu?: (event: MouseEvent, entry: FileEntry) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { entry, depth = 0, onContextMenu }: Props = $props();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
editorStore.toggleDirectory(entry);
|
||||||
|
} else {
|
||||||
|
editorStore.openFile(entry.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
handleClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onContextMenu?.(event, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpanded = $derived(entry.isExpanded ?? false);
|
||||||
|
const isLoading = $derived(entry.isLoading ?? false);
|
||||||
|
const children = $derived(entry.children ?? []);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="file-tree-item">
|
||||||
|
<button
|
||||||
|
class="item-row"
|
||||||
|
class:directory={entry.isDirectory}
|
||||||
|
class:file={!entry.isDirectory}
|
||||||
|
style="padding-left: {depth * 16 + 8}px"
|
||||||
|
onclick={handleClick}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
oncontextmenu={handleContextMenu}
|
||||||
|
title={entry.path}
|
||||||
|
>
|
||||||
|
{#if entry.isDirectory}
|
||||||
|
<span class="icon">
|
||||||
|
{#if isLoading}
|
||||||
|
<svg
|
||||||
|
class="spinner"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32">
|
||||||
|
<animate
|
||||||
|
attributeName="stroke-dashoffset"
|
||||||
|
dur="1s"
|
||||||
|
values="32;0"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
{:else if isExpanded}
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M9 6l6 6-6 6" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="folder-icon">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="icon spacer"></span>
|
||||||
|
<span class="file-icon">
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="name">{entry.name}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if entry.isDirectory && isExpanded && children.length > 0}
|
||||||
|
<div class="children">
|
||||||
|
{#each children as child (child.path)}
|
||||||
|
<Self entry={child} depth={depth + 1} {onContextMenu} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-tree-item {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row:hover {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row:focus {
|
||||||
|
outline: 1px solid var(--accent-primary);
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.spacer {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.children {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
placeholder?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
onConfirm: (value: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
placeholder = "",
|
||||||
|
confirmText = "Create",
|
||||||
|
cancelText = "Cancel",
|
||||||
|
initialValue = "",
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let inputValue = $state(initialValue);
|
||||||
|
let inputElement: HTMLInputElement | undefined = $state();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (inputElement) {
|
||||||
|
inputElement.focus();
|
||||||
|
// Select all text for rename operations
|
||||||
|
if (initialValue) {
|
||||||
|
inputElement.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const trimmed = inputValue.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
onConfirm(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onCancel();
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="dialog-overlay" onclick={onCancel}>
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="dialog-content" onclick={(e) => e.stopPropagation()}>
|
||||||
|
<h2 class="dialog-title">{title}</h2>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={inputElement}
|
||||||
|
bind:value={inputValue}
|
||||||
|
type="text"
|
||||||
|
{placeholder}
|
||||||
|
class="dialog-input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="btn-cancel" onclick={onCancel}>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button class="btn-confirm" onclick={handleSubmit} disabled={!inputValue.trim()}>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
margin: 0 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-input {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-input::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-input:focus {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:hover {
|
||||||
|
background-color: var(--accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
import { writable, derived, get } from "svelte/store";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { EditorState, EditorTab, FileEntry } from "$lib/types/editor";
|
||||||
|
|
||||||
|
const defaultState: EditorState = {
|
||||||
|
tabs: [],
|
||||||
|
activeTabId: null,
|
||||||
|
isFileBrowserOpen: true,
|
||||||
|
currentDirectory: "",
|
||||||
|
fileTree: [],
|
||||||
|
isLoadingTree: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getLanguageFromPath(filePath: string): string {
|
||||||
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
js: "javascript",
|
||||||
|
jsx: "javascript",
|
||||||
|
ts: "typescript",
|
||||||
|
tsx: "typescript",
|
||||||
|
py: "python",
|
||||||
|
rs: "rust",
|
||||||
|
go: "go",
|
||||||
|
java: "java",
|
||||||
|
c: "c",
|
||||||
|
cpp: "cpp",
|
||||||
|
h: "c",
|
||||||
|
hpp: "cpp",
|
||||||
|
cs: "csharp",
|
||||||
|
rb: "ruby",
|
||||||
|
php: "php",
|
||||||
|
swift: "swift",
|
||||||
|
kt: "kotlin",
|
||||||
|
scala: "scala",
|
||||||
|
html: "html",
|
||||||
|
htm: "html",
|
||||||
|
css: "css",
|
||||||
|
scss: "scss",
|
||||||
|
sass: "sass",
|
||||||
|
less: "less",
|
||||||
|
json: "json",
|
||||||
|
xml: "xml",
|
||||||
|
yaml: "yaml",
|
||||||
|
yml: "yaml",
|
||||||
|
toml: "toml",
|
||||||
|
md: "markdown",
|
||||||
|
markdown: "markdown",
|
||||||
|
sql: "sql",
|
||||||
|
sh: "shell",
|
||||||
|
bash: "shell",
|
||||||
|
zsh: "shell",
|
||||||
|
ps1: "powershell",
|
||||||
|
dockerfile: "dockerfile",
|
||||||
|
svelte: "svelte",
|
||||||
|
vue: "vue",
|
||||||
|
graphql: "graphql",
|
||||||
|
gql: "graphql",
|
||||||
|
lua: "lua",
|
||||||
|
r: "r",
|
||||||
|
dart: "dart",
|
||||||
|
elm: "elm",
|
||||||
|
ex: "elixir",
|
||||||
|
exs: "elixir",
|
||||||
|
erl: "erlang",
|
||||||
|
hs: "haskell",
|
||||||
|
clj: "clojure",
|
||||||
|
lisp: "lisp",
|
||||||
|
ml: "ocaml",
|
||||||
|
fs: "fsharp",
|
||||||
|
zig: "zig",
|
||||||
|
nim: "nim",
|
||||||
|
v: "v",
|
||||||
|
wasm: "wasm",
|
||||||
|
wat: "wasm",
|
||||||
|
};
|
||||||
|
return languageMap[ext] || "plaintext";
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTabId(): string {
|
||||||
|
return `tab-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEditorStore() {
|
||||||
|
const state = writable<EditorState>(defaultState);
|
||||||
|
const isEditorVisible = writable<boolean>(false);
|
||||||
|
const saveError = writable<string | null>(null);
|
||||||
|
|
||||||
|
async function loadDirectory(dirPath: string): Promise<FileEntry[]> {
|
||||||
|
try {
|
||||||
|
const entries = await invoke<FileEntry[]>("list_directory", { path: dirPath });
|
||||||
|
return entries.sort((a, b) => {
|
||||||
|
if (a.isDirectory && !b.isDirectory) return -1;
|
||||||
|
if (!a.isDirectory && b.isDirectory) return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load directory:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeFileTree(rootPath: string) {
|
||||||
|
state.update((s) => ({ ...s, isLoadingTree: true, currentDirectory: rootPath }));
|
||||||
|
try {
|
||||||
|
const entries = await loadDirectory(rootPath);
|
||||||
|
state.update((s) => ({ ...s, fileTree: entries, isLoadingTree: false }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize file tree:", error);
|
||||||
|
state.update((s) => ({ ...s, isLoadingTree: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleDirectory(entry: FileEntry) {
|
||||||
|
if (!entry.isDirectory) return;
|
||||||
|
|
||||||
|
state.update((s) => {
|
||||||
|
const updateTree = (entries: FileEntry[]): FileEntry[] => {
|
||||||
|
return entries.map((e) => {
|
||||||
|
if (e.path === entry.path) {
|
||||||
|
return { ...e, isExpanded: !e.isExpanded, isLoading: !e.isExpanded && !e.children };
|
||||||
|
}
|
||||||
|
if (e.children) {
|
||||||
|
return { ...e, children: updateTree(e.children) };
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return { ...s, fileTree: updateTree(s.fileTree) };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!entry.isExpanded && !entry.children) {
|
||||||
|
const children = await loadDirectory(entry.path);
|
||||||
|
state.update((s) => {
|
||||||
|
const updateTree = (entries: FileEntry[]): FileEntry[] => {
|
||||||
|
return entries.map((e) => {
|
||||||
|
if (e.path === entry.path) {
|
||||||
|
return { ...e, children, isLoading: false };
|
||||||
|
}
|
||||||
|
if (e.children) {
|
||||||
|
return { ...e, children: updateTree(e.children) };
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return { ...s, fileTree: updateTree(s.fileTree) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openFile(filePath: string) {
|
||||||
|
const currentState = get(state);
|
||||||
|
|
||||||
|
const existingTab = currentState.tabs.find((t) => t.filePath === filePath);
|
||||||
|
if (existingTab) {
|
||||||
|
state.update((s) => ({ ...s, activeTabId: existingTab.id }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await invoke<string>("read_file_content", { path: filePath });
|
||||||
|
const fileName = filePath.split(/[/\\]/).pop() || "untitled";
|
||||||
|
const language = getLanguageFromPath(filePath);
|
||||||
|
const newTab: EditorTab = {
|
||||||
|
id: generateTabId(),
|
||||||
|
filePath,
|
||||||
|
fileName,
|
||||||
|
content,
|
||||||
|
originalContent: content,
|
||||||
|
isDirty: false,
|
||||||
|
language,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
tabs: [...s.tabs, newTab],
|
||||||
|
activeTabId: newTab.id,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open file:", error);
|
||||||
|
saveError.set(`Failed to open file: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile(tabId?: string) {
|
||||||
|
const currentState = get(state);
|
||||||
|
const tab = tabId
|
||||||
|
? currentState.tabs.find((t) => t.id === tabId)
|
||||||
|
: currentState.tabs.find((t) => t.id === currentState.activeTabId);
|
||||||
|
|
||||||
|
if (!tab) return;
|
||||||
|
|
||||||
|
saveError.set(null);
|
||||||
|
try {
|
||||||
|
await invoke("write_file_content", { path: tab.filePath, content: tab.content });
|
||||||
|
state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
tabs: s.tabs.map((t) =>
|
||||||
|
t.id === tab.id ? { ...t, originalContent: t.content, isDirty: false } : t
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save file:", error);
|
||||||
|
saveError.set(`Failed to save file: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTabContent(tabId: string, content: string) {
|
||||||
|
state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
tabs: s.tabs.map((t) =>
|
||||||
|
t.id === tabId ? { ...t, content, isDirty: content !== t.originalContent } : t
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTab(tabId: string) {
|
||||||
|
state.update((s) => {
|
||||||
|
const tabIndex = s.tabs.findIndex((t) => t.id === tabId);
|
||||||
|
const newTabs = s.tabs.filter((t) => t.id !== tabId);
|
||||||
|
|
||||||
|
let newActiveId = s.activeTabId;
|
||||||
|
if (s.activeTabId === tabId) {
|
||||||
|
if (newTabs.length === 0) {
|
||||||
|
newActiveId = null;
|
||||||
|
} else if (tabIndex >= newTabs.length) {
|
||||||
|
newActiveId = newTabs[newTabs.length - 1].id;
|
||||||
|
} else {
|
||||||
|
newActiveId = newTabs[tabIndex].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...s, tabs: newTabs, activeTabId: newActiveId };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveTab(tabId: string) {
|
||||||
|
state.update((s) => ({ ...s, activeTabId: tabId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFileBrowser() {
|
||||||
|
state.update((s) => ({ ...s, isFileBrowserOpen: !s.isFileBrowserOpen }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEditor() {
|
||||||
|
isEditorVisible.update((v) => !v);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFile(parentPath: string, fileName: string): Promise<boolean> {
|
||||||
|
const filePath = `${parentPath}/${fileName}`;
|
||||||
|
try {
|
||||||
|
await invoke("create_file", { path: filePath });
|
||||||
|
// Refresh the parent directory
|
||||||
|
await refreshDirectory(parentPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create file:", error);
|
||||||
|
saveError.set(`Failed to create file: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDirectory(parentPath: string, dirName: string): Promise<boolean> {
|
||||||
|
const dirPath = `${parentPath}/${dirName}`;
|
||||||
|
try {
|
||||||
|
await invoke("create_directory", { path: dirPath });
|
||||||
|
// Refresh the parent directory
|
||||||
|
await refreshDirectory(parentPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create directory:", error);
|
||||||
|
saveError.set(`Failed to create directory: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFile(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke("delete_file", { path: filePath });
|
||||||
|
// Close the tab if it's open
|
||||||
|
const currentState = get(state);
|
||||||
|
const openTab = currentState.tabs.find((t) => t.filePath === filePath);
|
||||||
|
if (openTab) {
|
||||||
|
closeTab(openTab.id);
|
||||||
|
}
|
||||||
|
// Refresh the parent directory
|
||||||
|
const parentPath = filePath.substring(0, filePath.lastIndexOf("/"));
|
||||||
|
await refreshDirectory(parentPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete file:", error);
|
||||||
|
saveError.set(`Failed to delete file: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDirectory(dirPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke("delete_directory", { path: dirPath });
|
||||||
|
// Close any tabs that are in this directory
|
||||||
|
const currentState = get(state);
|
||||||
|
const tabsToClose = currentState.tabs.filter((t) => t.filePath.startsWith(dirPath + "/"));
|
||||||
|
tabsToClose.forEach((tab) => closeTab(tab.id));
|
||||||
|
// Refresh the parent directory
|
||||||
|
const parentPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
|
||||||
|
await refreshDirectory(parentPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete directory:", error);
|
||||||
|
saveError.set(`Failed to delete directory: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDirectory(dirPath: string) {
|
||||||
|
const currentState = get(state);
|
||||||
|
|
||||||
|
// If refreshing the root directory, reload the entire tree
|
||||||
|
if (dirPath === currentState.currentDirectory) {
|
||||||
|
const entries = await loadDirectory(dirPath);
|
||||||
|
state.update((s) => ({ ...s, fileTree: entries }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, update the specific directory in the tree
|
||||||
|
const children = await loadDirectory(dirPath);
|
||||||
|
state.update((s) => {
|
||||||
|
const updateTree = (entries: FileEntry[]): FileEntry[] => {
|
||||||
|
return entries.map((e) => {
|
||||||
|
if (e.path === dirPath) {
|
||||||
|
return { ...e, children, isExpanded: true };
|
||||||
|
}
|
||||||
|
if (e.children) {
|
||||||
|
return { ...e, children: updateTree(e.children) };
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return { ...s, fileTree: updateTree(s.fileTree) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renamePath(oldPath: string, newName: string): Promise<boolean> {
|
||||||
|
const parentPath = oldPath.substring(0, oldPath.lastIndexOf("/"));
|
||||||
|
const newPath = `${parentPath}/${newName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("rename_path", { oldPath, newPath });
|
||||||
|
|
||||||
|
// Update any open tabs that reference this path
|
||||||
|
state.update((s) => ({
|
||||||
|
...s,
|
||||||
|
tabs: s.tabs.map((t) => {
|
||||||
|
if (t.filePath === oldPath) {
|
||||||
|
// Exact match - this file was renamed
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
filePath: newPath,
|
||||||
|
fileName: newName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (t.filePath.startsWith(oldPath + "/")) {
|
||||||
|
// File is inside a renamed directory
|
||||||
|
const relativePath = t.filePath.substring(oldPath.length);
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
filePath: newPath + relativePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh the parent directory
|
||||||
|
await refreshDirectory(parentPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to rename:", error);
|
||||||
|
saveError.set(`Failed to rename: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditor() {
|
||||||
|
isEditorVisible.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideEditor() {
|
||||||
|
isEditorVisible.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: { subscribe: state.subscribe },
|
||||||
|
isEditorVisible: { subscribe: isEditorVisible.subscribe },
|
||||||
|
saveError: { subscribe: saveError.subscribe },
|
||||||
|
|
||||||
|
initializeFileTree,
|
||||||
|
toggleDirectory,
|
||||||
|
openFile,
|
||||||
|
saveFile,
|
||||||
|
updateTabContent,
|
||||||
|
closeTab,
|
||||||
|
setActiveTab,
|
||||||
|
toggleFileBrowser,
|
||||||
|
toggleEditor,
|
||||||
|
showEditor,
|
||||||
|
hideEditor,
|
||||||
|
createFile,
|
||||||
|
createDirectory,
|
||||||
|
deleteFile,
|
||||||
|
deleteDirectory,
|
||||||
|
refreshDirectory,
|
||||||
|
renamePath,
|
||||||
|
|
||||||
|
tabs: derived(state, ($state) => $state.tabs),
|
||||||
|
activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)),
|
||||||
|
activeTabId: derived(state, ($state) => $state.activeTabId),
|
||||||
|
fileTree: derived(state, ($state) => $state.fileTree),
|
||||||
|
isFileBrowserOpen: derived(state, ($state) => $state.isFileBrowserOpen),
|
||||||
|
isLoadingTree: derived(state, ($state) => $state.isLoadingTree),
|
||||||
|
currentDirectory: derived(state, ($state) => $state.currentDirectory),
|
||||||
|
hasDirtyTabs: derived(state, ($state) => $state.tabs.some((t) => t.isDirty)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editorStore = createEditorStore();
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface FileEntry {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
isDirectory: boolean;
|
||||||
|
isExpanded?: boolean;
|
||||||
|
children?: FileEntry[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorTab {
|
||||||
|
id: string;
|
||||||
|
filePath: string;
|
||||||
|
fileName: string;
|
||||||
|
content: string;
|
||||||
|
originalContent: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorState {
|
||||||
|
tabs: EditorTab[];
|
||||||
|
activeTabId: string | null;
|
||||||
|
isFileBrowserOpen: boolean;
|
||||||
|
currentDirectory: string;
|
||||||
|
fileTree: FileEntry[];
|
||||||
|
isLoadingTree: boolean;
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
// Prevent the default context menu globally
|
||||||
|
// Individual components can show their own context menus by calling event.stopPropagation()
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window oncontextmenu={handleContextMenu} />
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
||||||
import { conversationsStore } from "$lib/stores/conversations";
|
import { conversationsStore } from "$lib/stores/conversations";
|
||||||
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
|
import { getCurrentWindow, LogicalSize } from "@tauri-apps/api/window";
|
||||||
import "$lib/notifications/testNotifications";
|
import "$lib/notifications/testNotifications";
|
||||||
import Terminal from "$lib/components/Terminal.svelte";
|
import Terminal from "$lib/components/Terminal.svelte";
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
import StatusBar from "$lib/components/StatusBar.svelte";
|
import StatusBar from "$lib/components/StatusBar.svelte";
|
||||||
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
|
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
|
||||||
import CompactMode from "$lib/components/CompactMode.svelte";
|
import CompactMode from "$lib/components/CompactMode.svelte";
|
||||||
|
import EditorPanel from "$lib/components/editor/EditorPanel.svelte";
|
||||||
import { characterState } from "$lib/stores/character";
|
import { characterState } from "$lib/stores/character";
|
||||||
import type { CharacterState } from "$lib/types/states";
|
import type { CharacterState } from "$lib/types/states";
|
||||||
import PermissionModal from "$lib/components/PermissionModal.svelte";
|
import PermissionModal from "$lib/components/PermissionModal.svelte";
|
||||||
@@ -29,6 +31,32 @@
|
|||||||
let currentCharacterState: CharacterState = $state("idle");
|
let currentCharacterState: CharacterState = $state("idle");
|
||||||
let compactModeActive = $state(false);
|
let compactModeActive = $state(false);
|
||||||
|
|
||||||
|
// Editor state
|
||||||
|
const isEditorVisible = editorStore.isEditorVisible;
|
||||||
|
let lastInitializedCwd = "";
|
||||||
|
|
||||||
|
// Track connection status and CWD for the editor
|
||||||
|
const connectionStatus = claudeStore.connectionStatus;
|
||||||
|
const currentWorkingDirectory = claudeStore.currentWorkingDirectory;
|
||||||
|
|
||||||
|
// Initialize/update editor file tree when CWD changes while editor is visible
|
||||||
|
$effect(() => {
|
||||||
|
const visible = $isEditorVisible;
|
||||||
|
const cwd = $currentWorkingDirectory;
|
||||||
|
const connected = $connectionStatus === "connected";
|
||||||
|
|
||||||
|
// Only initialize when editor is visible, connected, and CWD is set
|
||||||
|
if (visible && connected && cwd && cwd !== lastInitializedCwd) {
|
||||||
|
lastInitializedCwd = cwd;
|
||||||
|
editorStore.initializeFileTree(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide editor if disconnected
|
||||||
|
if (!connected && visible) {
|
||||||
|
editorStore.hideEditor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Window size constants
|
// Window size constants
|
||||||
const COMPACT_WIDTH = 280;
|
const COMPACT_WIDTH = 280;
|
||||||
const COMPACT_HEIGHT = 400;
|
const COMPACT_HEIGHT = 400;
|
||||||
@@ -176,6 +204,49 @@
|
|||||||
toggleCompactMode();
|
toggleCompactMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl+E - Toggle editor panel (only when connected)
|
||||||
|
if (event.ctrlKey && event.key === "e") {
|
||||||
|
event.preventDefault();
|
||||||
|
// Only allow opening the editor when connected
|
||||||
|
if (get(claudeStore.connectionStatus) === "connected") {
|
||||||
|
editorStore.toggleEditor();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+B - Toggle file browser (when editor is visible)
|
||||||
|
if (event.ctrlKey && event.key === "b" && get(editorStore.isEditorVisible)) {
|
||||||
|
event.preventDefault();
|
||||||
|
editorStore.toggleFileBrowser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+S - Save current file (when editor is visible)
|
||||||
|
if (event.ctrlKey && event.key === "s" && get(editorStore.isEditorVisible)) {
|
||||||
|
event.preventDefault();
|
||||||
|
editorStore.saveFile();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+W - Close current tab (when editor is visible)
|
||||||
|
if (event.ctrlKey && event.key === "w" && get(editorStore.isEditorVisible)) {
|
||||||
|
event.preventDefault();
|
||||||
|
const activeTabId = get(editorStore.activeTabId);
|
||||||
|
if (activeTabId) {
|
||||||
|
editorStore.closeTab(activeTabId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+N - New file (when editor is visible)
|
||||||
|
// Note: This just emits an event that FileBrowser listens to
|
||||||
|
if (event.ctrlKey && event.key === "n" && get(editorStore.isEditorVisible)) {
|
||||||
|
event.preventDefault();
|
||||||
|
// Dispatch a custom event that FileBrowser will listen to
|
||||||
|
window.dispatchEvent(new CustomEvent("editor-new-file"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInterrupt() {
|
async function handleInterrupt() {
|
||||||
@@ -330,10 +401,14 @@
|
|||||||
onmousedown={startResize}
|
onmousedown={startResize}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Right panel: Terminal and input -->
|
<!-- Right panel: Terminal/Editor and input -->
|
||||||
<div class="terminal-panel flex-1 flex flex-col min-w-0">
|
<div class="terminal-panel flex-1 flex flex-col min-w-0">
|
||||||
|
{#if $isEditorVisible}
|
||||||
|
<EditorPanel />
|
||||||
|
{:else}
|
||||||
<Terminal />
|
<Terminal />
|
||||||
<InputBar />
|
<InputBar />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||