Refinement-Runde Panel/Wiki: Bedienung, Themes, Archiv-Wahl, Frontend-Tests
- Slug transliteriert Umlaute/ß (fuer statt f-r); aktiver Tab mit Akzent + Unterstrich; kein aktiver Tab bei zugeklapptem Panel (mode.clear) - Suche: Live-Update auch beim Löschen (<3 Zeichen: Hinweis), #tag-Filter in der Query, Treffer öffnen im Dokument-Tab (panel_load, ohne Frontmatter); Wiki-Sprung-Button im Dokument; Wiki-Fehler sichtbar als Toast - Archiv-Formular am Archiv-Button (Ordner/Beschreibung/Schlagwörter); panel_archive_cmd nimmt Meta entgegen - Wiki-Übersicht: Zuletzt-Sektion, Backlink-Zähler, Wurzel-Eyebrow - Themes: Panel-Farben über CSS-Variablen, applyTheme leitet sie aus dem jeweiligen Theme ab (auch helle); gemeinsames themes.ts - Fenster: Doppelklick maximiert; gelöstes Fenster merkt Größe/Position (Window-ACL: set-position/outer-position/inner-size ergänzt); open_panel_window async (GTK-Deadlock-Vorgabe); Panel-Fenster spiegelt JS-Fehler ins Dev-Log - Archiv pro Projekt wähl-/abwählbar (Einstellungsdialog, setzt/entfernt Permissions); require_archive_home legt den Ordner an; ohne Archiv nur Befehle+Dokument im Panel - Frontend-Tests: vitest + happy-dom, 27 Tests über Modus-, Such-, Wiki-, Editor- und Formular-Logik (npm test)
This commit is contained in:
Generated
+458
@@ -28,8 +28,10 @@
|
|||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"happy-dom": "^20.11.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.1",
|
"vite": "^8.1.1",
|
||||||
|
"vitest": "^4.1.10",
|
||||||
"vue-tsc": "^3.3.5"
|
"vue-tsc": "^3.3.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -482,6 +484,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/api": {
|
"node_modules/@tauri-apps/api": {
|
||||||
"version": "2.11.1",
|
"version": "2.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz",
|
||||||
@@ -756,6 +765,31 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.13.2",
|
"version": "24.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||||
@@ -766,6 +800,23 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/whatwg-mimetype": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.7",
|
"version": "6.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
||||||
@@ -783,6 +834,129 @@
|
|||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@volar/language-core": {
|
"node_modules/@volar/language-core": {
|
||||||
"version": "2.4.28",
|
"version": "2.4.28",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||||||
@@ -994,6 +1168,46 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer-image-size": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1022,12 +1236,29 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -1097,6 +1328,25 @@
|
|||||||
"node": ">8.0.0"
|
"node": ">8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/happy-dom": {
|
||||||
|
"version": "20.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.0.tgz",
|
||||||
|
"integrity": "sha512-XogN4asPd1a56di9prVG6bZxteNcXsZxxKmAvcEfc5Px5Ca2hMyMgk8wvqK2K1V8zXg40j9VANXsDaJYh9DeNA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": ">=20.0.0",
|
||||||
|
"@types/whatwg-mimetype": "^3.0.2",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"buffer-image-size": "^0.6.4",
|
||||||
|
"entities": "^7.0.1",
|
||||||
|
"whatwg-mimetype": "^3.0.0",
|
||||||
|
"ws": "^8.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
@@ -1416,6 +1666,20 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/opentype.js": {
|
"node_modules/opentype.js": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz",
|
||||||
@@ -1435,6 +1699,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1525,6 +1796,13 @@
|
|||||||
"@rolldown/binding-win32-x64-msvc": "1.1.4"
|
"@rolldown/binding-win32-x64-msvc": "1.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1534,12 +1812,43 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tiny-inflate": {
|
"node_modules/tiny-inflate": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -1557,6 +1866,16 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@@ -1664,6 +1983,96 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.1.10",
|
||||||
|
"@vitest/mocker": "4.1.10",
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"@vitest/runner": "4.1.10",
|
||||||
|
"@vitest/snapshot": "4.1.10",
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"es-module-lexer": "^2.0.0",
|
||||||
|
"expect-type": "^1.3.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^4.0.0-rc.1",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.1.0",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.1.10",
|
||||||
|
"@vitest/browser-preview": "4.1.10",
|
||||||
|
"@vitest/browser-webdriverio": "4.1.10",
|
||||||
|
"@vitest/coverage-istanbul": "4.1.10",
|
||||||
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
|
"@vitest/ui": "4.1.10",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vscode-uri": {
|
"node_modules/vscode-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||||
@@ -1730,6 +2139,55 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||||
|
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"test": "vitest run",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
@@ -30,8 +31,10 @@
|
|||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"happy-dom": "^20.11.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.1",
|
"vite": "^8.1.1",
|
||||||
|
"vitest": "^4.1.10",
|
||||||
"vue-tsc": "^3.3.5"
|
"vue-tsc": "^3.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-29
@@ -4,12 +4,27 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Dokument</title>
|
<title>Dokument</title>
|
||||||
<style>
|
<style>
|
||||||
|
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
||||||
|
:root {
|
||||||
|
--bg: #1e1e2e;
|
||||||
|
--surface: #181825;
|
||||||
|
--tile: #11111b;
|
||||||
|
--line: #313244;
|
||||||
|
--line-strong: #45475a;
|
||||||
|
--text: #cdd6f4;
|
||||||
|
--muted: #a6adc8;
|
||||||
|
--faint: #6c7086;
|
||||||
|
--accent: #89b4fa;
|
||||||
|
--ok: #a6e3a1;
|
||||||
|
--warn: #f9e2af;
|
||||||
|
--err: #f38ba8;
|
||||||
|
}
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #1e1e2e;
|
background: var(--bg);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@@ -19,7 +34,7 @@
|
|||||||
/* Feiner Fensterrahmen fürs dekorationslose Fenster (nur Linux). */
|
/* Feiner Fensterrahmen fürs dekorationslose Fenster (nur Linux). */
|
||||||
:root[data-platform="other"] body {
|
:root[data-platform="other"] body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #313244;
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.panel-head {
|
.panel-head {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -27,8 +42,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
padding: 7px 8px 9px 14px;
|
padding: 7px 8px 9px 14px;
|
||||||
background: #181825;
|
background: var(--surface);
|
||||||
border-bottom: 1px solid #313244;
|
border-bottom: 1px solid var(--line);
|
||||||
font:
|
font:
|
||||||
500 12px/1 -apple-system,
|
500 12px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -50,8 +65,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
background: #181825;
|
background: var(--surface);
|
||||||
border-bottom: 1px solid #313244;
|
border-bottom: 1px solid var(--line);
|
||||||
font:
|
font:
|
||||||
500 13px/1 -apple-system,
|
500 13px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -85,14 +100,14 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
.winbtn:hover {
|
.winbtn:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
}
|
}
|
||||||
.winbtn:hover svg {
|
.winbtn:hover svg {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.winbtn.close:hover {
|
.winbtn.close:hover {
|
||||||
background: #f38ba8;
|
background: var(--err);
|
||||||
color: #11111b;
|
color: var(--tile);
|
||||||
}
|
}
|
||||||
/* macOS hat native Fensterknöpfe — eigene Min/Max dort ausblenden. */
|
/* macOS hat native Fensterknöpfe — eigene Min/Max dort ausblenden. */
|
||||||
:root[data-platform="mac"] .winbtn.osctl {
|
:root[data-platform="mac"] .winbtn.osctl {
|
||||||
@@ -172,7 +187,7 @@
|
|||||||
.tab-sep {
|
.tab-sep {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background: #45475a;
|
background: var(--line-strong);
|
||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
@@ -188,7 +203,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.panel-title[contenteditable="true"] {
|
.panel-title[contenteditable="true"] {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -200,7 +215,7 @@
|
|||||||
.panel-btn {
|
.panel-btn {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 9px;
|
padding: 6px 9px;
|
||||||
@@ -209,14 +224,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.panel-btn:hover {
|
.panel-btn:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.panel-btn.copied {
|
.panel-btn.copied {
|
||||||
color: #a6e3a1;
|
color: var(--ok);
|
||||||
}
|
}
|
||||||
.panel-btn.error {
|
.panel-btn.error {
|
||||||
color: #f38ba8;
|
color: var(--err);
|
||||||
}
|
}
|
||||||
.panel-btn svg {
|
.panel-btn svg {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -227,11 +242,18 @@
|
|||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
.panel-btn.active {
|
.panel-btn.active {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
|
}
|
||||||
|
/* Aktiver Tab: Akzentfarbe + Unterstrich statt nur Flächenwechsel. */
|
||||||
|
.panel-tabs .panel-btn.active {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: inset 0 -2px 0 currentColor;
|
||||||
}
|
}
|
||||||
.panel-btn.changed {
|
.panel-btn.changed {
|
||||||
color: #f9e2af;
|
color: var(--warn);
|
||||||
}
|
}
|
||||||
.panel-editor {
|
.panel-editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -258,7 +280,7 @@
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 9px;
|
padding: 6px 9px;
|
||||||
@@ -266,8 +288,8 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.panel-lang:hover {
|
.panel-lang:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
#panel-content {
|
#panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -284,7 +306,7 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
#panel-content::-webkit-scrollbar-thumb {
|
#panel-content::-webkit-scrollbar-thumb {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
#panel-content.raw {
|
#panel-content.raw {
|
||||||
@@ -321,12 +343,12 @@
|
|||||||
#panel-content.md code {
|
#panel-content.md code {
|
||||||
font-family: "JetBrains Mono", Menlo, monospace;
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
#panel-content.md pre {
|
#panel-content.md pre {
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -336,12 +358,12 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#panel-content.md blockquote {
|
#panel-content.md blockquote {
|
||||||
border-left: 3px solid #313244;
|
border-left: 3px solid var(--line);
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
#panel-content.md a {
|
#panel-content.md a {
|
||||||
color: #89b4fa;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.panel-btn[hidden],
|
.panel-btn[hidden],
|
||||||
.panel-lang[hidden],
|
.panel-lang[hidden],
|
||||||
@@ -388,6 +410,9 @@
|
|||||||
<option value="it">IT</option>
|
<option value="it">IT</option>
|
||||||
<option value="nl">NL</option>
|
<option value="nl">NL</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button class="panel-btn draft-only" id="panel-wiki-jump" title="Im Wiki öffnen">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M8 3.5C6.6 2.4 4.6 2 2.5 2v11c2.1 0 4.1.4 5.5 1.5 1.4-1.1 3.4-1.5 5.5-1.5V2c-2.1 0-4.1.4-5.5 1.5z" /><path d="M8 3.5v11" /></svg>
|
||||||
|
</button>
|
||||||
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
||||||
<button class="panel-btn draft-only" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
<button class="panel-btn draft-only" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-size",
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-set-position",
|
||||||
|
"core:window:allow-outer-position",
|
||||||
|
"core:window:allow-inner-size",
|
||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-toggle-maximize",
|
"core:window:allow-toggle-maximize",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
|
|||||||
@@ -308,11 +308,14 @@ pub(crate) fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
|||||||
terminal::panel_set,
|
terminal::panel_set,
|
||||||
terminal::search_read,
|
terminal::search_read,
|
||||||
terminal::search_run,
|
terminal::search_run,
|
||||||
|
terminal::panel_load,
|
||||||
terminal::wiki_read,
|
terminal::wiki_read,
|
||||||
terminal::wiki_open,
|
terminal::wiki_open,
|
||||||
terminal::open_panel_window,
|
terminal::open_panel_window,
|
||||||
commands::panel_archive_dir_cmd,
|
commands::panel_archive_dir_cmd,
|
||||||
commands::panel_archive_cmd,
|
commands::panel_archive_cmd,
|
||||||
|
commands::set_archive_home_cmd,
|
||||||
|
commands::clear_archive_home_cmd,
|
||||||
commands::archive_docs_cmd,
|
commands::archive_docs_cmd,
|
||||||
commands::reveal_path_cmd,
|
commands::reveal_path_cmd,
|
||||||
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
||||||
|
|||||||
@@ -342,12 +342,37 @@ pub(crate) fn panel_archive_dir_cmd(project: String) -> Option<String> {
|
|||||||
project_archive_home(&project).map(|p| p.display().to_string())
|
project_archive_home(&project).map(|p| p.display().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Archiviert den aktuellen Panel-Entwurf. `dir` (aus dem Ordner-Dialog) setzt
|
/// Setzt das Archiv-Home eines Projekts (Einstellungsdialog) — inklusive
|
||||||
/// das Archiv-Home zugleich; ohne `dir` muss es konfiguriert sein.
|
/// Permissions-Eintrag in der Projekt-settings.json.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, String> {
|
pub(crate) fn set_archive_home_cmd(project: String, dir: String) -> Result<(), String> {
|
||||||
archive_panel_content(&project, dir.as_deref(), &ArchiveMeta::default())
|
crate::domain::archive::set_project_archive_home(&project, &dir)
|
||||||
.map(|p| p.display().to_string())
|
}
|
||||||
|
|
||||||
|
/// Wählt das Archiv ab (Einstellungsdialog): Config-Eintrag und Permissions
|
||||||
|
/// weg, der Ordner bleibt liegen.
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) fn clear_archive_home_cmd(project: String) -> Result<(), String> {
|
||||||
|
crate::domain::archive::clear_project_archive_home(&project)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Archiviert den aktuellen Panel-Entwurf. `dir` (aus dem Ordner-Dialog) setzt
|
||||||
|
/// das Archiv-Home zugleich; ohne `dir` muss es konfiguriert sein. Ordner,
|
||||||
|
/// Beschreibung und Schlagwörter kommen aus dem Archiv-Formular im Panel.
|
||||||
|
#[tauri::command]
|
||||||
|
pub(crate) fn panel_archive_cmd(
|
||||||
|
project: String,
|
||||||
|
dir: Option<String>,
|
||||||
|
folder: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
tags: Option<Vec<String>>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let meta = ArchiveMeta {
|
||||||
|
folder: folder.filter(|f| !f.trim().is_empty()),
|
||||||
|
description: description.filter(|d| !d.trim().is_empty()),
|
||||||
|
tags: tags.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
archive_panel_content(&project, dir.as_deref(), &meta).map(|p| p.display().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dokumentliste des Archiv-Index (frisch gescannt) fürs Panel.
|
/// Dokumentliste des Archiv-Index (frisch gescannt) fürs Panel.
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ pub(crate) fn project_archive_home(project: &str) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Archiv-Home als Result — die gemeinsame Vorbedingung aller Archiv-Tools.
|
/// Archiv-Home als Result — die gemeinsame Vorbedingung aller Archiv-Tools.
|
||||||
|
/// Legt den konfigurierten Ordner an, falls er (noch) nicht existiert.
|
||||||
pub(crate) fn require_archive_home(project: &str) -> Result<PathBuf, String> {
|
pub(crate) fn require_archive_home(project: &str) -> Result<PathBuf, String> {
|
||||||
project_archive_home(project).ok_or_else(|| "kein Archiv-Ordner gesetzt".into())
|
let home = project_archive_home(project).ok_or("kein Archiv-Ordner gesetzt")?;
|
||||||
|
fs::create_dir_all(&home).map_err(|e| format!("{}: {e}", home.display()))?;
|
||||||
|
Ok(home)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setzt das Archiv-Home: ai-control.json (~-relativ) und ein Eintrag in
|
/// Setzt das Archiv-Home: ai-control.json (~-relativ) und ein Eintrag in
|
||||||
@@ -87,6 +90,42 @@ fn add_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(),
|
|||||||
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wählt das Archiv ab: archiveHome aus der ai-control.json entfernen und die
|
||||||
|
/// beim Setzen eingetragenen Rechte (additionalDirectories + Edit-Allow) aus
|
||||||
|
/// der Projekt-settings.json zurücknehmen. Der Ordner selbst bleibt liegen.
|
||||||
|
pub(crate) fn clear_project_archive_home(project: &str) -> Result<(), String> {
|
||||||
|
let paths = Paths::real();
|
||||||
|
let mut cfg = read_project_config_in(&paths, project)?;
|
||||||
|
let Some(dir) = cfg.archive_home.take() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
write_project_config_in(&paths, project, &cfg)?;
|
||||||
|
remove_archive_permission(&paths, project, &dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gegenstück zu add_archive_permission.
|
||||||
|
fn remove_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(), String> {
|
||||||
|
let sp = settings_path(&project_dir(paths, project)?);
|
||||||
|
if !sp.is_file() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut v: serde_json::Value =
|
||||||
|
serde_json::from_str(&fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?)
|
||||||
|
.map_err(|e| format!("{}: {e}", sp.display()))?;
|
||||||
|
let Some(perms) = v.get_mut("permissions").and_then(|p| p.as_object_mut()) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if let Some(dirs) = perms.get_mut("additionalDirectories").and_then(|d| d.as_array_mut()) {
|
||||||
|
dirs.retain(|d| d.as_str() != Some(dir));
|
||||||
|
}
|
||||||
|
let edit = format!("Edit({dir}/**)");
|
||||||
|
if let Some(allow) = perms.get_mut("allow").and_then(|a| a.as_array_mut()) {
|
||||||
|
allow.retain(|p| p.as_str() != Some(&edit));
|
||||||
|
}
|
||||||
|
let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadaten beim Archivieren: Unterordner im Archiv-Home plus Frontmatter-Felder.
|
/// Metadaten beim Archivieren: Unterordner im Archiv-Home plus Frontmatter-Felder.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct ArchiveMeta {
|
pub(crate) struct ArchiveMeta {
|
||||||
@@ -252,10 +291,15 @@ fn first_line(text: &str) -> String {
|
|||||||
pub(crate) fn slugify(s: &str) -> String {
|
pub(crate) fn slugify(s: &str) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
if c.is_ascii_alphanumeric() {
|
// Umlaute/ß transliterieren statt verschlucken („für's" → fuer-s).
|
||||||
out.push(c.to_ascii_lowercase());
|
match c {
|
||||||
} else if !out.ends_with('-') {
|
'ä' | 'Ä' => out.push_str("ae"),
|
||||||
out.push('-');
|
'ö' | 'Ö' => out.push_str("oe"),
|
||||||
|
'ü' | 'Ü' => out.push_str("ue"),
|
||||||
|
'ß' => out.push_str("ss"),
|
||||||
|
_ if c.is_ascii_alphanumeric() => out.push(c.to_ascii_lowercase()),
|
||||||
|
_ if !out.ends_with('-') => out.push('-'),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let out: String = out.trim_matches('-').chars().take(60).collect();
|
let out: String = out.trim_matches('-').chars().take(60).collect();
|
||||||
@@ -314,6 +358,8 @@ mod tests {
|
|||||||
assert_eq!(slugify("ADR: Logging vereinheitlichen"), "adr-logging-vereinheitlichen");
|
assert_eq!(slugify("ADR: Logging vereinheitlichen"), "adr-logging-vereinheitlichen");
|
||||||
assert_eq!(slugify(" "), "entwurf");
|
assert_eq!(slugify(" "), "entwurf");
|
||||||
assert_eq!(slugify("###"), "entwurf");
|
assert_eq!(slugify("###"), "entwurf");
|
||||||
|
assert_eq!(slugify("Ein Test für's Archiv"), "ein-test-fuer-s-archiv");
|
||||||
|
assert_eq!(slugify("Größe ÄÖÜ"), "groesse-aeoeue");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ pub(crate) struct WikiPage {
|
|||||||
pub(crate) tag: Option<String>,
|
pub(crate) tag: Option<String>,
|
||||||
pub(crate) total: usize,
|
pub(crate) total: usize,
|
||||||
pub(crate) tags: Vec<TagCount>,
|
pub(crate) tags: Vec<TagCount>,
|
||||||
|
/// Die neuesten Dokumente über alle Ordner; leer, wenn es nur einen
|
||||||
|
/// Ordner gibt (dann wäre die Sektion eine Dublette der Liste).
|
||||||
|
pub(crate) recent: Vec<WikiDocEntry>,
|
||||||
pub(crate) folders: Vec<WikiFolder>,
|
pub(crate) folders: Vec<WikiFolder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +161,8 @@ pub(crate) struct WikiDocEntry {
|
|||||||
pub(crate) tags: Vec<String>,
|
pub(crate) tags: Vec<String>,
|
||||||
/// Archivierungsdatum aus dem Zeitstempel-Stem (`YYYY-MM-DD`).
|
/// Archivierungsdatum aus dem Zeitstempel-Stem (`YYYY-MM-DD`).
|
||||||
pub(crate) date: Option<String>,
|
pub(crate) date: Option<String>,
|
||||||
|
/// Zahl der Dokumente, die per Wikilink hierher zeigen.
|
||||||
|
pub(crate) backlinks: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn archive_page(home: &Path, tag: Option<&str>) -> Result<WikiPage, String> {
|
pub(crate) fn archive_page(home: &Path, tag: Option<&str>) -> Result<WikiPage, String> {
|
||||||
@@ -181,6 +186,17 @@ pub(crate) fn archive_page(home: &Path, tag: Option<&str>) -> Result<WikiPage, S
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
folders.entry(folder).or_default().push(doc);
|
folders.entry(folder).or_default().push(doc);
|
||||||
}
|
}
|
||||||
|
let recent = if folders.len() > 1 {
|
||||||
|
// Neueste zuerst über alle Ordner — der Zeitstempel-Stem sortiert
|
||||||
|
// chronologisch, verglichen wird der Dateiname.
|
||||||
|
let mut all = selected.clone();
|
||||||
|
all.sort_by(|a, b| {
|
||||||
|
Path::new(&b.relpath).file_name().cmp(&Path::new(&a.relpath).file_name())
|
||||||
|
});
|
||||||
|
all.into_iter().take(5).map(doc_entry).collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
Ok(WikiPage {
|
Ok(WikiPage {
|
||||||
kind: "page",
|
kind: "page",
|
||||||
home: home.display().to_string(),
|
home: home.display().to_string(),
|
||||||
@@ -190,6 +206,7 @@ pub(crate) fn archive_page(home: &Path, tag: Option<&str>) -> Result<WikiPage, S
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, count)| TagCount { name: name.to_string(), count })
|
.map(|(name, count)| TagCount { name: name.to_string(), count })
|
||||||
.collect(),
|
.collect(),
|
||||||
|
recent,
|
||||||
folders: folders
|
folders: folders
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, mut list)| {
|
.map(|(name, mut list)| {
|
||||||
@@ -214,6 +231,7 @@ fn doc_entry(doc: &Doc) -> WikiDocEntry {
|
|||||||
description: doc.description.clone(),
|
description: doc.description.clone(),
|
||||||
tags: doc.tags.clone(),
|
tags: doc.tags.clone(),
|
||||||
date,
|
date,
|
||||||
|
backlinks: doc.backlinks.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +360,12 @@ mod tests {
|
|||||||
assert_eq!(adr.date.as_deref(), Some("2026-07-19"));
|
assert_eq!(adr.date.as_deref(), Some("2026-07-19"));
|
||||||
assert_eq!(page.folders[1].name, "konzepte");
|
assert_eq!(page.folders[1].name, "konzepte");
|
||||||
assert_eq!(page.folders[1].docs[0].name, "notiz-deploy");
|
assert_eq!(page.folders[1].docs[0].name, "notiz-deploy");
|
||||||
|
// Zwei Ordner → Zuletzt-Sektion, neuestes Dokument zuerst, mit
|
||||||
|
// Backlink-Zähler aus dem Scan.
|
||||||
|
let recent: Vec<&str> = page.recent.iter().map(|d| d.name.as_str()).collect();
|
||||||
|
assert_eq!(recent, vec!["notiz-deploy", "adr-logging"]);
|
||||||
|
assert_eq!(page.recent[0].backlinks, 1);
|
||||||
|
assert_eq!(page.recent[1].backlinks, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -286,16 +286,26 @@ pub fn search_read(project: String) -> String {
|
|||||||
std::fs::read_to_string(search_file(&project)).unwrap_or_default()
|
std::fs::read_to_string(search_file(&project)).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lädt ein Archiv-Dokument in den Dokument-Puffer (Treffer-Klick in der
|
||||||
|
/// Suche) — ohne Frontmatter-Block, wie ein frischer Entwurf. Der Watcher
|
||||||
|
/// meldet den neuen Inhalt als `panel-update`.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn panel_load(project: String, path: String) -> Result<(), String> {
|
||||||
|
let text = std::fs::read_to_string(&path).map_err(|e| format!("{path}: {e}"))?;
|
||||||
|
let body = crate::domain::archive::strip_frontmatter(&text);
|
||||||
|
std::fs::write(panel_file(&project), body).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Suche aus dem Panel-Suchfeld: läuft wie das MCP-Tool search_archive und
|
/// Suche aus dem Panel-Suchfeld: läuft wie das MCP-Tool search_archive und
|
||||||
/// schreibt die Treffer-Datei; der Watcher zieht sie in die Ansicht (beide
|
/// schreibt die Treffer-Datei; der Watcher zieht sie in die Ansicht (beide
|
||||||
/// Fenster).
|
/// Fenster).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn search_run(project: String, query: String) -> Result<(), String> {
|
pub fn search_run(project: String, query: String, tag: Option<String>) -> Result<(), String> {
|
||||||
let home = crate::domain::archive::require_archive_home(&project)?;
|
let home = crate::domain::archive::require_archive_home(&project)?;
|
||||||
let hits = crate::domain::archive_search::search(&home, &query, None, 20)?;
|
let hits = crate::domain::archive_search::search(&home, &query, tag.as_deref(), 20)?;
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
"query": query,
|
"query": query,
|
||||||
"tag": null,
|
"tag": tag,
|
||||||
"home": home.display().to_string(),
|
"home": home.display().to_string(),
|
||||||
"hits": hits,
|
"hits": hits,
|
||||||
});
|
});
|
||||||
@@ -329,8 +339,10 @@ pub fn wiki_open(project: String, name: String) -> Result<(), String> {
|
|||||||
|
|
||||||
/// Löst das Panel in ein eigenes Fenster ab. Existiert es schon, kommt es nach
|
/// Löst das Panel in ein eigenes Fenster ab. Existiert es schon, kommt es nach
|
||||||
/// vorn. `panel-detached` blendet das angedockte Panel im Terminal-Fenster aus.
|
/// vorn. `panel-detached` blendet das angedockte Panel im Terminal-Fenster aus.
|
||||||
|
/// Async, weil Fenster-Erzeugung aus einem synchronen Command auf dem
|
||||||
|
/// GTK-Mainloop klemmen kann (Tauri-Vorgabe für window create in Commands).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_panel_window(app: AppHandle, project: String) -> Result<(), String> {
|
pub async fn open_panel_window(app: AppHandle, project: String) -> Result<(), String> {
|
||||||
let label = format!("panel-{project}");
|
let label = format!("panel-{project}");
|
||||||
if let Some(w) = app.get_webview_window(&label) {
|
if let Some(w) = app.get_webview_window(&label) {
|
||||||
let _ = w.set_focus();
|
let _ = w.set_focus();
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Archiv-Formular: Aufklappen, Meta-Aufbereitung, Tastatur.
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { initArchiveForm } from "./archive-form";
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
document.body.innerHTML = `<button id="btn">Archiv</button>`;
|
||||||
|
const onSubmit = vi.fn();
|
||||||
|
const form = initArchiveForm(document.getElementById("btn")!, onSubmit);
|
||||||
|
const root = document.querySelector<HTMLElement>(".archive-form")!;
|
||||||
|
const inputs = [...root.querySelectorAll("input")];
|
||||||
|
return { form, onSubmit, root, inputs };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("initArchiveForm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("klappt auf und zu", () => {
|
||||||
|
const { form, root } = setup();
|
||||||
|
expect(root.hidden).toBe(true);
|
||||||
|
form.toggle();
|
||||||
|
expect(root.hidden).toBe(false);
|
||||||
|
form.toggle();
|
||||||
|
expect(root.hidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("liefert getrimmte Meta; Leeres wird undefined, Tags gesplittet", () => {
|
||||||
|
const { form, onSubmit, root, inputs } = setup();
|
||||||
|
form.toggle();
|
||||||
|
inputs[0].value = " konzepte/panel ";
|
||||||
|
inputs[1].value = "";
|
||||||
|
inputs[2].value = " adr, infra ,, ";
|
||||||
|
root.querySelector<HTMLElement>(".archive-form-submit")!.click();
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
|
folder: "konzepte/panel",
|
||||||
|
description: undefined,
|
||||||
|
tags: ["adr", "infra"],
|
||||||
|
});
|
||||||
|
expect(root.hidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Enter schickt ab, Escape schließt ohne Abschicken", () => {
|
||||||
|
const { form, onSubmit, root } = setup();
|
||||||
|
form.toggle();
|
||||||
|
root.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||||
|
expect(root.hidden).toBe(true);
|
||||||
|
expect(onSubmit).not.toHaveBeenCalled();
|
||||||
|
form.toggle();
|
||||||
|
root.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||||
|
expect(onSubmit).toHaveBeenCalledOnce();
|
||||||
|
expect(root.hidden).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/// Archiv-Formular: klappt unter dem Archiv-Button auf und fragt Ordner,
|
||||||
|
/// Beschreibung und Schlagwörter ab (alles optional); Enter oder der Button
|
||||||
|
/// archivieren, Escape schließt. Gemeinsam für das angedockte Panel und das
|
||||||
|
/// abgelöste Fenster.
|
||||||
|
|
||||||
|
export interface ArchiveFormMeta {
|
||||||
|
folder?: string;
|
||||||
|
description?: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initArchiveForm(
|
||||||
|
anchor: HTMLElement,
|
||||||
|
onSubmit: (meta: ArchiveFormMeta) => void,
|
||||||
|
): { toggle(): void } {
|
||||||
|
function field(placeholder: string): HTMLInputElement {
|
||||||
|
const i = document.createElement("input");
|
||||||
|
i.type = "text";
|
||||||
|
i.placeholder = placeholder;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = document.createElement("div");
|
||||||
|
form.className = "archive-form";
|
||||||
|
form.hidden = true;
|
||||||
|
const folder = field("Ordner — z. B. konzepte/panel");
|
||||||
|
const desc = field("Beschreibung");
|
||||||
|
const tags = field("Schlagwörter, kommagetrennt");
|
||||||
|
const submit = document.createElement("button");
|
||||||
|
submit.className = "archive-form-submit";
|
||||||
|
submit.textContent = "Archivieren";
|
||||||
|
form.append(folder, desc, tags, submit);
|
||||||
|
document.body.append(form);
|
||||||
|
|
||||||
|
const meta = (): ArchiveFormMeta => ({
|
||||||
|
folder: folder.value.trim() || undefined,
|
||||||
|
description: desc.value.trim() || undefined,
|
||||||
|
tags: tags.value
|
||||||
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
});
|
||||||
|
const close = () => {
|
||||||
|
form.hidden = true;
|
||||||
|
};
|
||||||
|
const open = () => {
|
||||||
|
const r = anchor.getBoundingClientRect();
|
||||||
|
form.style.top = `${r.bottom + 6}px`;
|
||||||
|
form.style.right = `${Math.max(8, window.innerWidth - r.right - 4)}px`;
|
||||||
|
form.hidden = false;
|
||||||
|
folder.focus();
|
||||||
|
};
|
||||||
|
const fire = () => {
|
||||||
|
onSubmit(meta());
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
submit.addEventListener("click", fire);
|
||||||
|
form.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
fire();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { toggle: () => (form.hidden ? open() : close()) };
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Modus-Logik (Tabs) und Kachel-Ansicht der Befehls-History.
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { initCommandsView, initPanelMode } from "./commands-view";
|
||||||
|
|
||||||
|
function modeSetup() {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="tabs">
|
||||||
|
<button data-mode="commands">Befehle</button>
|
||||||
|
<button data-mode="draft">Dokument</button>
|
||||||
|
<button data-mode="wiki">Wiki</button>
|
||||||
|
<button data-mode="search">Suche</button>
|
||||||
|
</div>
|
||||||
|
<div id="title">Mein Titel</div>
|
||||||
|
<div id="draft-el"></div>
|
||||||
|
<div id="cc" hidden></div>
|
||||||
|
<div id="sc" hidden></div>
|
||||||
|
<div id="wc" hidden></div>`;
|
||||||
|
const el = (id: string) => document.getElementById(id)!;
|
||||||
|
const flush = vi.fn();
|
||||||
|
const mode = initPanelMode({
|
||||||
|
tabsEl: el("tabs"),
|
||||||
|
draftEls: [el("draft-el")],
|
||||||
|
commandsContent: el("cc"),
|
||||||
|
searchContent: el("sc"),
|
||||||
|
wikiContent: el("wc"),
|
||||||
|
titleEl: el("title"),
|
||||||
|
flush,
|
||||||
|
});
|
||||||
|
const tab = (m: string) =>
|
||||||
|
document.querySelector<HTMLElement>(`[data-mode="${m}"]`)!;
|
||||||
|
return { mode, flush, el, tab };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("initPanelMode", () => {
|
||||||
|
it("startet im Dokument-Modus mit aktivem Tab", () => {
|
||||||
|
const { el, tab } = modeSetup();
|
||||||
|
expect(el("draft-el").hidden).toBe(false);
|
||||||
|
expect(el("cc").hidden).toBe(true);
|
||||||
|
expect(tab("draft").classList.contains("active")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wechselt Sichtbarkeit, Titel und aktive Markierung", () => {
|
||||||
|
const { mode, flush, el, tab } = modeSetup();
|
||||||
|
mode.to("commands");
|
||||||
|
expect(flush).toHaveBeenCalledOnce();
|
||||||
|
expect(el("cc").hidden).toBe(false);
|
||||||
|
expect(el("draft-el").hidden).toBe(true);
|
||||||
|
expect(el("title").textContent).toBe("Befehle");
|
||||||
|
expect(tab("commands").classList.contains("active")).toBe(true);
|
||||||
|
expect(tab("draft").classList.contains("active")).toBe(false);
|
||||||
|
mode.to("draft");
|
||||||
|
expect(el("title").textContent).toBe("Mein Titel");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Tab-Klick wechselt den Modus", () => {
|
||||||
|
const { el, tab } = modeSetup();
|
||||||
|
tab("wiki").click();
|
||||||
|
expect(el("wc").hidden).toBe(false);
|
||||||
|
expect(el("draft-el").hidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clear hebt die Auswahl auf; danach wählt to() wieder aus", () => {
|
||||||
|
const { mode, el, tab } = modeSetup();
|
||||||
|
mode.clear();
|
||||||
|
expect(document.querySelectorAll(".active").length).toBe(0);
|
||||||
|
expect(mode.current()).toBe(null);
|
||||||
|
mode.to("draft");
|
||||||
|
expect(tab("draft").classList.contains("active")).toBe(true);
|
||||||
|
expect(el("title").textContent).toBe("Mein Titel");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initCommandsView", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = `<div id="c"></div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const jsonl = [
|
||||||
|
JSON.stringify({ ts: 1700000000, session: true }),
|
||||||
|
JSON.stringify({
|
||||||
|
ts: 1700000100,
|
||||||
|
commands: [{ cmd: "ls -la", note: "Liste" }, { cmd: "pwd" }],
|
||||||
|
}),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
it("rendert Kacheln, Session-Trenner und meldet empty korrekt", () => {
|
||||||
|
const view = initCommandsView(document.getElementById("c")!, () => {});
|
||||||
|
view.set(jsonl);
|
||||||
|
expect(view.empty()).toBe(false);
|
||||||
|
expect(document.querySelectorAll(".cmd-tile").length).toBe(2);
|
||||||
|
expect(document.querySelectorAll(".cmd-sep").length).toBe(1);
|
||||||
|
expect(document.querySelector(".cmd-text")!.textContent).toBe("ls -la");
|
||||||
|
view.set("");
|
||||||
|
expect(view.empty()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("meldet Löschen mit Record- und Eintrags-Index", () => {
|
||||||
|
const onDelete = vi.fn();
|
||||||
|
const view = initCommandsView(document.getElementById("c")!, onDelete);
|
||||||
|
view.set(jsonl);
|
||||||
|
const dels = document.querySelectorAll<HTMLElement>(".cmd-del");
|
||||||
|
dels[1].click(); // zweite Kachel im Record 1
|
||||||
|
expect(onDelete).toHaveBeenCalledWith(1, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
+12
-3
@@ -171,7 +171,8 @@ export function initPanelMode(opts: {
|
|||||||
titleEl: HTMLElement;
|
titleEl: HTMLElement;
|
||||||
flush: () => void;
|
flush: () => void;
|
||||||
}) {
|
}) {
|
||||||
let mode: PanelMode = "draft";
|
// `null` = kein Tab aktiv (Panel zugeklappt).
|
||||||
|
let mode: PanelMode | null = "draft";
|
||||||
let draftTitle = "";
|
let draftTitle = "";
|
||||||
const tabs = [...opts.tabsEl.querySelectorAll<HTMLElement>("[data-mode]")];
|
const tabs = [...opts.tabsEl.querySelectorAll<HTMLElement>("[data-mode]")];
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ export function initPanelMode(opts: {
|
|||||||
opts.wikiContent.hidden = mode !== "wiki";
|
opts.wikiContent.hidden = mode !== "wiki";
|
||||||
for (const el of opts.draftEls) el.hidden = mode !== "draft";
|
for (const el of opts.draftEls) el.hidden = mode !== "draft";
|
||||||
for (const t of tabs) t.classList.toggle("active", t.dataset.mode === mode);
|
for (const t of tabs) t.classList.toggle("active", t.dataset.mode === mode);
|
||||||
if (mode !== "draft") opts.titleEl.textContent = LABEL[mode];
|
if (mode && mode !== "draft") opts.titleEl.textContent = LABEL[mode];
|
||||||
}
|
}
|
||||||
|
|
||||||
function to(m: PanelMode) {
|
function to(m: PanelMode) {
|
||||||
@@ -193,7 +194,15 @@ export function initPanelMode(opts: {
|
|||||||
if (m === "draft") opts.titleEl.textContent = draftTitle;
|
if (m === "draft") opts.titleEl.textContent = draftTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Auswahl aufheben (Panel zugeklappt) — der nächste to()-Aufruf oder
|
||||||
|
/// Tab-Klick wählt wieder aus.
|
||||||
|
function clear() {
|
||||||
|
if (mode === "draft") draftTitle = opts.titleEl.textContent || "Dokument";
|
||||||
|
mode = null;
|
||||||
|
for (const t of tabs) t.classList.remove("active");
|
||||||
|
}
|
||||||
|
|
||||||
for (const t of tabs) t.addEventListener("click", () => to(t.dataset.mode as PanelMode));
|
for (const t of tabs) t.addEventListener("click", () => to(t.dataset.mode as PanelMode));
|
||||||
apply();
|
apply();
|
||||||
return { to, current: () => mode };
|
return { to, clear, current: () => mode };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ interface TerminalSettings {
|
|||||||
title: string;
|
title: string;
|
||||||
todo: boolean;
|
todo: boolean;
|
||||||
workDirs: string[];
|
workDirs: string[];
|
||||||
|
archiveHome: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = ref<TerminalSettings | null>(null);
|
const settings = ref<TerminalSettings | null>(null);
|
||||||
@@ -168,6 +169,9 @@ async function openSettings(p: Project) {
|
|||||||
const workDirs = await invoke<string[]>("project_work_dirs", {
|
const workDirs = await invoke<string[]>("project_work_dirs", {
|
||||||
project: p.name,
|
project: p.name,
|
||||||
});
|
});
|
||||||
|
const archiveHome = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||||
|
project: p.name,
|
||||||
|
});
|
||||||
settings.value = {
|
settings.value = {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
path: p.path,
|
path: p.path,
|
||||||
@@ -176,6 +180,7 @@ async function openSettings(p: Project) {
|
|||||||
title: p.terminal.title ?? "",
|
title: p.terminal.title ?? "",
|
||||||
todo,
|
todo,
|
||||||
workDirs,
|
workDirs,
|
||||||
|
archiveHome,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = String(e);
|
error.value = String(e);
|
||||||
@@ -222,6 +227,33 @@ async function removeWorkDir(dir: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archiv-Home schreibt wie die Arbeitsordner direkt (Config + Permissions);
|
||||||
|
// ohne Archiv zeigt das Panel nur Befehle und Dokument. Greift ab dem
|
||||||
|
// nächsten Session-Start.
|
||||||
|
async function chooseArchive() {
|
||||||
|
const s = settings.value!;
|
||||||
|
const dir = await open({ directory: true, multiple: false });
|
||||||
|
if (typeof dir !== "string") return;
|
||||||
|
try {
|
||||||
|
await invoke("set_archive_home_cmd", { project: s.name, dir });
|
||||||
|
s.archiveHome = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||||
|
project: s.name,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error.value = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearArchive() {
|
||||||
|
const s = settings.value!;
|
||||||
|
try {
|
||||||
|
await invoke("clear_archive_home_cmd", { project: s.name });
|
||||||
|
s.archiveHome = null;
|
||||||
|
} catch (e) {
|
||||||
|
error.value = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pickIcon() {
|
async function pickIcon() {
|
||||||
const file = await open({
|
const file = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
@@ -649,6 +681,16 @@ onUnmounted(() => window.clearInterval(timer));
|
|||||||
<button @click="addWorkDir">{{ $t("projects.addWorkDir") }}</button>
|
<button @click="addWorkDir">{{ $t("projects.addWorkDir") }}</button>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
{{ $t("projects.archive") }}
|
||||||
|
<span class="icon-row">
|
||||||
|
<span class="icon-path">{{ settings.archiveHome ?? $t("projects.archiveNone") }}</span>
|
||||||
|
<button @click="chooseArchive">{{ $t("projects.changeDir") }}</button>
|
||||||
|
<button v-if="settings.archiveHome" @click="clearArchive">
|
||||||
|
{{ $t("projects.remove") }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
{{ $t("projects.todo") }}
|
{{ $t("projects.todo") }}
|
||||||
<span class="checkline">
|
<span class="checkline">
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ const de = {
|
|||||||
addFolder: "Projekt importieren …",
|
addFolder: "Projekt importieren …",
|
||||||
addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
|
addFolderTitle: "Bestehenden Ordner als Projekt aufnehmen",
|
||||||
addWorkDir: "Ordner hinzufügen …",
|
addWorkDir: "Ordner hinzufügen …",
|
||||||
|
archive: "Archiv",
|
||||||
|
archiveNone: "kein Archiv — Panel ohne Wiki und Suche",
|
||||||
changeDir: "Ändern …",
|
changeDir: "Ändern …",
|
||||||
wizardTitle: "Neues Projekt",
|
wizardTitle: "Neues Projekt",
|
||||||
name: "Name",
|
name: "Name",
|
||||||
@@ -178,6 +180,8 @@ const en: typeof de = {
|
|||||||
addFolder: "Import project …",
|
addFolder: "Import project …",
|
||||||
addFolderTitle: "Add an existing folder as a project",
|
addFolderTitle: "Add an existing folder as a project",
|
||||||
addWorkDir: "Add folder …",
|
addWorkDir: "Add folder …",
|
||||||
|
archive: "Archive",
|
||||||
|
archiveNone: "no archive — panel without wiki and search",
|
||||||
changeDir: "Change …",
|
changeDir: "Change …",
|
||||||
wizardTitle: "New project",
|
wizardTitle: "New project",
|
||||||
name: "Name",
|
name: "Name",
|
||||||
|
|||||||
+120
-58
@@ -12,14 +12,14 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
#commands-content::-webkit-scrollbar-thumb {
|
#commands-content::-webkit-scrollbar-thumb {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.cmd-sep {
|
.cmd-sep {
|
||||||
margin: 16px 0 8px;
|
margin: 16px 0 8px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid #313244;
|
border-top: 1px solid var(--line);
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font:
|
font:
|
||||||
500 11px/1 -apple-system,
|
500 11px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font:
|
font:
|
||||||
500 11px/1 -apple-system,
|
500 11px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -45,27 +45,27 @@
|
|||||||
.cmd-all {
|
.cmd-all {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
.cmd-all:hover {
|
.cmd-all:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.cmd-all.copied {
|
.cmd-all.copied {
|
||||||
color: #a6e3a1;
|
color: var(--ok);
|
||||||
}
|
}
|
||||||
.cmd-del:hover {
|
.cmd-del:hover {
|
||||||
color: #f38ba8;
|
color: var(--err);
|
||||||
}
|
}
|
||||||
.cmd-tile {
|
.cmd-tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border: 1px solid #313244;
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 6px 8px 10px;
|
padding: 8px 6px 8px 10px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
}
|
}
|
||||||
.cmd-note {
|
.cmd-note {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
font:
|
font:
|
||||||
400 11px/1.4 -apple-system,
|
400 11px/1.4 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -104,11 +104,11 @@
|
|||||||
.hit-search input {
|
.hit-search input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border: 1px solid #313244;
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
font:
|
font:
|
||||||
400 12px/1.4 -apple-system,
|
400 12px/1.4 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -116,20 +116,20 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
.hit-search input:focus {
|
.hit-search input:focus {
|
||||||
border-color: #45475a;
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
.hit-search input::placeholder {
|
.hit-search input::placeholder {
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
}
|
}
|
||||||
#search-content::-webkit-scrollbar {
|
#search-content::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
#search-content::-webkit-scrollbar-thumb {
|
#search-content::-webkit-scrollbar-thumb {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.hit-head {
|
.hit-head {
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font:
|
font:
|
||||||
500 11px/1 -apple-system,
|
500 11px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -138,15 +138,15 @@
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
.hit-tile {
|
.hit-tile {
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border: 1px solid #313244;
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.hit-tile:hover {
|
.hit-tile:hover {
|
||||||
border-color: #45475a;
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
.hit-title {
|
.hit-title {
|
||||||
font:
|
font:
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
}
|
}
|
||||||
.hit-snippet {
|
.hit-snippet {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
font:
|
font:
|
||||||
400 11px/1.5 -apple-system,
|
400 11px/1.5 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -164,12 +164,12 @@
|
|||||||
}
|
}
|
||||||
.hit-snippet mark {
|
.hit-snippet mark {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #f9e2af;
|
color: var(--warn);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.hit-path {
|
.hit-path {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font-family: "JetBrains Mono", Menlo, monospace;
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
#wiki-content::-webkit-scrollbar-thumb {
|
#wiki-content::-webkit-scrollbar-thumb {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.wiki-head {
|
.wiki-head {
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.wiki-head-title {
|
.wiki-head-title {
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
font:
|
font:
|
||||||
600 13px/1.3 -apple-system,
|
600 13px/1.3 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
}
|
}
|
||||||
.wiki-head-sub {
|
.wiki-head-sub {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font-family: "JetBrains Mono", Menlo, monospace;
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.wiki-head-right {
|
.wiki-head-right {
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font:
|
font:
|
||||||
500 11px/1 -apple-system,
|
500 11px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -228,9 +228,9 @@
|
|||||||
margin: 10px 0 4px;
|
margin: 10px 0 4px;
|
||||||
}
|
}
|
||||||
.wiki-chip {
|
.wiki-chip {
|
||||||
border: 1px solid #313244;
|
border: 1px solid var(--line);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font:
|
font:
|
||||||
@@ -239,19 +239,19 @@
|
|||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
.wiki-chip:hover {
|
.wiki-chip:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.wiki-chip.active {
|
.wiki-chip.active {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-color: #45475a;
|
border-color: var(--line-strong);
|
||||||
color: #89b4fa;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.wiki-folder {
|
.wiki-folder {
|
||||||
margin: 14px 0 2px;
|
margin: 14px 0 2px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border-top: 1px solid #313244;
|
border-top: 1px solid var(--line);
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font:
|
font:
|
||||||
500 10px/1 -apple-system,
|
500 10px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.wiki-doc:hover {
|
.wiki-doc:hover {
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
}
|
}
|
||||||
.wiki-doc-line {
|
.wiki-doc-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -278,20 +278,20 @@
|
|||||||
.wiki-doc-title {
|
.wiki-doc-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: #89b4fa;
|
color: var(--accent);
|
||||||
font:
|
font:
|
||||||
600 12px/1.4 -apple-system,
|
600 12px/1.4 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
.wiki-doc-date {
|
.wiki-doc-date {
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font-family: "JetBrains Mono", Menlo, monospace;
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
.wiki-doc-desc {
|
.wiki-doc-desc {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
font:
|
font:
|
||||||
400 11px/1.5 -apple-system,
|
400 11px/1.5 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
}
|
}
|
||||||
.wiki-empty {
|
.wiki-empty {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
font:
|
font:
|
||||||
400 12px/1.6 -apple-system,
|
400 12px/1.6 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
.wiki-empty strong {
|
.wiki-empty strong {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.wiki-doc-head {
|
.wiki-doc-head {
|
||||||
@@ -327,7 +327,7 @@
|
|||||||
.wiki-back {
|
.wiki-back {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
font:
|
font:
|
||||||
500 11px/1 -apple-system,
|
500 11px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -336,8 +336,8 @@
|
|||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
.wiki-back:hover {
|
.wiki-back:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.wiki-body {
|
.wiki-body {
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
@@ -374,12 +374,12 @@
|
|||||||
.wiki-body code {
|
.wiki-body code {
|
||||||
font-family: "JetBrains Mono", Menlo, monospace;
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
.wiki-body pre {
|
.wiki-body pre {
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -389,28 +389,90 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.wiki-body blockquote {
|
.wiki-body blockquote {
|
||||||
border-left: 3px solid #313244;
|
border-left: 3px solid var(--line);
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.wiki-body a {
|
.wiki-body a {
|
||||||
color: #89b4fa;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.wiki-backlinks {
|
.wiki-backlinks {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid #313244;
|
border-top: 1px solid var(--line);
|
||||||
color: #6c7086;
|
color: var(--faint);
|
||||||
font:
|
font:
|
||||||
500 11px/1.6 -apple-system,
|
500 11px/1.6 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
.wiki-backlinks a {
|
.wiki-backlinks a {
|
||||||
color: #89b4fa;
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Backlink-Zähler in der Dokumentzeile. */
|
||||||
|
.wiki-doc-back {
|
||||||
|
color: var(--faint);
|
||||||
|
font:
|
||||||
|
500 10px/1 -apple-system,
|
||||||
|
system-ui,
|
||||||
|
sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Archiv-Formular: klappt unter dem Archiv-Button auf. */
|
||||||
|
.archive-form {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 260px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.archive-form[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.archive-form input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--tile);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
color: var(--text);
|
||||||
|
font:
|
||||||
|
400 12px/1.4 -apple-system,
|
||||||
|
system-ui,
|
||||||
|
sans-serif;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.archive-form input:focus {
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
}
|
||||||
|
.archive-form input::placeholder {
|
||||||
|
color: var(--faint);
|
||||||
|
}
|
||||||
|
.archive-form-submit {
|
||||||
|
border: none;
|
||||||
|
background: var(--line);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font:
|
||||||
|
500 12px/1.4 -apple-system,
|
||||||
|
system-ui,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
.archive-form-submit:hover {
|
||||||
|
background: var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
/* Sichtbare Fehlermeldung (panelToast), oben rechts über dem Panel-Inhalt. */
|
/* Sichtbare Fehlermeldung (panelToast), oben rechts über dem Panel-Inhalt. */
|
||||||
.panel-toast {
|
.panel-toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -418,9 +480,9 @@
|
|||||||
right: 12px;
|
right: 12px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border: 1px solid #f38ba8;
|
border: 1px solid var(--err);
|
||||||
color: #f38ba8;
|
color: var(--err);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font:
|
font:
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Dokument-Ansicht: Wikilink-Verlinkung im DOM und Editor-Flush.
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { initPanelView, linkWikiRefs } from "./panel-view";
|
||||||
|
|
||||||
|
describe("linkWikiRefs", () => {
|
||||||
|
it("verlinkt [[ziel]] und [[ziel|Label]], Code bleibt unangetastet", () => {
|
||||||
|
const root = document.createElement("div");
|
||||||
|
root.innerHTML =
|
||||||
|
"<p>Siehe [[adr-logging]] und [[notiz|die Notiz]].</p>" +
|
||||||
|
"<pre><code>if [[ -f x ]]; then</code></pre>";
|
||||||
|
const onClick = vi.fn();
|
||||||
|
linkWikiRefs(root, onClick);
|
||||||
|
const links = [...root.querySelectorAll<HTMLElement>("a.wiki")];
|
||||||
|
expect(links.map((a) => a.textContent)).toEqual(["adr-logging", "die Notiz"]);
|
||||||
|
links[1].click();
|
||||||
|
expect(onClick).toHaveBeenCalledWith("notiz");
|
||||||
|
expect(root.querySelector("code")!.textContent).toBe("if [[ -f x ]]; then");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function viewSetup(onCommit: (text: string) => void | Promise<void>) {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="content"></div>
|
||||||
|
<button id="copy"></button>
|
||||||
|
<button id="mode"></button>
|
||||||
|
<button id="edit"></button>`;
|
||||||
|
const view = initPanelView({
|
||||||
|
content: document.getElementById("content")!,
|
||||||
|
copyBtn: document.getElementById("copy")!,
|
||||||
|
modeBtn: document.getElementById("mode")!,
|
||||||
|
editContentBtn: document.getElementById("edit")!,
|
||||||
|
onCommit,
|
||||||
|
});
|
||||||
|
const editor = document.querySelector<HTMLTextAreaElement>(".panel-editor")!;
|
||||||
|
return { view, editor, editBtn: document.getElementById("edit")! };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("initPanelView — Editor", () => {
|
||||||
|
it("flush speichert eine offene Bearbeitung und beendet sie", async () => {
|
||||||
|
const onCommit = vi.fn();
|
||||||
|
const { view, editor, editBtn } = viewSetup(onCommit);
|
||||||
|
view.set("Alt");
|
||||||
|
editBtn.click();
|
||||||
|
expect(editor.hidden).toBe(false);
|
||||||
|
editor.value = "Neu";
|
||||||
|
await view.flush();
|
||||||
|
expect(onCommit).toHaveBeenCalledWith("Neu");
|
||||||
|
expect(editor.hidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flush ohne offene Bearbeitung tut nichts", async () => {
|
||||||
|
const onCommit = vi.fn();
|
||||||
|
const { view } = viewSetup(onCommit);
|
||||||
|
view.set("Alt");
|
||||||
|
await view.flush();
|
||||||
|
expect(onCommit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puffert eingehende Updates während der Bearbeitung", () => {
|
||||||
|
const { view, editor, editBtn } = viewSetup(() => {});
|
||||||
|
view.set("Alt");
|
||||||
|
editBtn.click();
|
||||||
|
view.set("Update von außen");
|
||||||
|
expect(editBtn.classList.contains("changed")).toBe(true);
|
||||||
|
editor.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
||||||
|
expect(view.raw()).toBe("Update von außen");
|
||||||
|
});
|
||||||
|
});
|
||||||
+42
-9
@@ -21,7 +21,7 @@ import "./panel-tiles.css";
|
|||||||
export interface PanelWiring {
|
export interface PanelWiring {
|
||||||
view: PanelView;
|
view: PanelView;
|
||||||
cmdView: CommandsView;
|
cmdView: CommandsView;
|
||||||
mode: { to(m: PanelMode): void; current(): PanelMode };
|
mode: { to(m: PanelMode): void; clear(): void; current(): PanelMode | null };
|
||||||
/// Entwurfstext beim Start (für die Anfangs-Modus-Entscheidung).
|
/// Entwurfstext beim Start (für die Anfangs-Modus-Entscheidung).
|
||||||
draft: string;
|
draft: string;
|
||||||
}
|
}
|
||||||
@@ -31,17 +31,37 @@ export async function wirePanel(
|
|||||||
onIncoming?: () => void,
|
onIncoming?: () => void,
|
||||||
): Promise<PanelWiring> {
|
): Promise<PanelWiring> {
|
||||||
const titleEl = document.querySelector(".panel-title") as HTMLElement;
|
const titleEl = document.querySelector(".panel-title") as HTMLElement;
|
||||||
const [defaultLang, draft, cmds, search, wiki] = await Promise.all([
|
const [defaultLang, draft, cmds, search, wiki, archiveHome] = await Promise.all([
|
||||||
invoke<string>("spellcheck_lang"),
|
invoke<string>("spellcheck_lang"),
|
||||||
invoke<string>("panel_read", { project }),
|
invoke<string>("panel_read", { project }),
|
||||||
invoke<string>("commands_read", { project }),
|
invoke<string>("commands_read", { project }),
|
||||||
invoke<string>("search_read", { project }),
|
invoke<string>("search_read", { project }),
|
||||||
invoke<string>("wiki_read", { project }),
|
invoke<string>("wiki_read", { project }),
|
||||||
|
invoke<string | null>("panel_archive_dir_cmd", { project }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Jeder Wiki-Sprung (Wikilink, Chip, Suchtreffer) geht als ein Invoke an den
|
// Ohne Archiv (in den Projekt-Einstellungen abgewählt) gibt es nur Befehle
|
||||||
// Kern; das Ergebnis kommt über den Wiki-Puffer und `wiki-update` zurück.
|
// und Dokument — Wiki-/Suche-Tabs und die Archiv-Werkzeuge verschwinden.
|
||||||
const openWiki = (name: string) => invoke("wiki_open", { project, name });
|
if (!archiveHome) {
|
||||||
|
for (const sel of [
|
||||||
|
'#panel-tabs [data-mode="wiki"]',
|
||||||
|
'#panel-tabs [data-mode="search"]',
|
||||||
|
"#panel-archive",
|
||||||
|
"#panel-wiki-jump",
|
||||||
|
]) {
|
||||||
|
const el = document.querySelector<HTMLElement>(sel)!;
|
||||||
|
el.hidden = true;
|
||||||
|
// Raus aus der draft-only-Menge, sonst blendet der Modus-Umschalter
|
||||||
|
// die Archiv-Werkzeuge im Dokument-Modus wieder ein.
|
||||||
|
el.classList.remove("draft-only");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jeder Wiki-Sprung (Wikilink, Chip, Dokument-Sprung) geht als ein Invoke an
|
||||||
|
// den Kern; das Ergebnis kommt über den Wiki-Puffer und `wiki-update` zurück.
|
||||||
|
// Fehler (z. B. Ziel nicht im Archiv) erscheinen als Toast.
|
||||||
|
const openWiki = (name: string) =>
|
||||||
|
invoke("wiki_open", { project, name }).catch((e) => panelToast(String(e)));
|
||||||
|
|
||||||
const view = initPanelView({
|
const view = initPanelView({
|
||||||
content: document.getElementById("panel-content")!,
|
content: document.getElementById("panel-content")!,
|
||||||
@@ -58,13 +78,21 @@ export async function wirePanel(
|
|||||||
const cmdView = initCommandsView(document.getElementById("commands-content")!, (line, entry) =>
|
const cmdView = initCommandsView(document.getElementById("commands-content")!, (line, entry) =>
|
||||||
invoke("commands_delete", { project, line, entry }),
|
invoke("commands_delete", { project, line, entry }),
|
||||||
);
|
);
|
||||||
|
// Treffer-Klick lädt das Dokument in den Dokument-Tab (dort editier- und
|
||||||
|
// archivierbar); der Sprung ins Wiki geht von dort aus.
|
||||||
const searchView = initSearchView(
|
const searchView = initSearchView(
|
||||||
document.getElementById("search-content")!,
|
document.getElementById("search-content")!,
|
||||||
(relpath) => openWiki(relpath.split("/").pop()!.replace(/\.md$/, "")),
|
(path) =>
|
||||||
(query) =>
|
void invoke("panel_load", { project, path }).catch((e) => panelToast(String(e))),
|
||||||
void invoke("search_run", { project, query }).catch((e) =>
|
(raw) => {
|
||||||
|
// `#tag`-Tokens filtern aufs Schlagwort, der Rest ist die Volltext-Query.
|
||||||
|
const words = raw.split(/\s+/).filter(Boolean);
|
||||||
|
const tag = words.find((w) => w.startsWith("#"))?.slice(1) ?? null;
|
||||||
|
const query = words.filter((w) => !w.startsWith("#")).join(" ");
|
||||||
|
void invoke("search_run", { project, query, tag }).catch((e) =>
|
||||||
panelToast(`Suche fehlgeschlagen: ${e}`),
|
panelToast(`Suche fehlgeschlagen: ${e}`),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const wikiView = initWikiView(document.getElementById("wiki-content")!, openWiki);
|
const wikiView = initWikiView(document.getElementById("wiki-content")!, openWiki);
|
||||||
const mode = initPanelMode({
|
const mode = initPanelMode({
|
||||||
@@ -92,6 +120,11 @@ export async function wirePanel(
|
|||||||
if (wikiView.empty()) void openWiki("tag:");
|
if (wikiView.empty()) void openWiki("tag:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sprung Dokument → Wiki: löst den angezeigten Titel gegen das Archiv auf.
|
||||||
|
document
|
||||||
|
.getElementById("panel-wiki-jump")!
|
||||||
|
.addEventListener("click", () => void openWiki(titleEl.textContent || ""));
|
||||||
|
|
||||||
await listen<string>("panel-update", (e) => {
|
await listen<string>("panel-update", (e) => {
|
||||||
view.set(e.payload);
|
view.set(e.payload);
|
||||||
mode.to("draft");
|
mode.to("draft");
|
||||||
|
|||||||
+65
-12
@@ -2,10 +2,12 @@ import "@fontsource/jetbrains-mono/400.css";
|
|||||||
import "@fontsource/jetbrains-mono/500.css";
|
import "@fontsource/jetbrains-mono/500.css";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
|
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
|
||||||
import { emit } from "@tauri-apps/api/event";
|
import { emit } from "@tauri-apps/api/event";
|
||||||
import { wirePanel } from "./panel-wiring";
|
import { wirePanel } from "./panel-wiring";
|
||||||
import { flash, panelToast } from "./commands-view";
|
import { flash, panelToast } from "./commands-view";
|
||||||
import { THEMES } from "./themes";
|
import { initArchiveForm } from "./archive-form";
|
||||||
|
import { applyTheme, THEMES } from "./themes";
|
||||||
|
|
||||||
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
||||||
// danach denselben Update-Events wie das angedockte Panel; startet in
|
// danach denselben Update-Events wie das angedockte Panel; startet in
|
||||||
@@ -13,6 +15,23 @@ import { THEMES } from "./themes";
|
|||||||
const project = new URLSearchParams(location.search).get("project")!;
|
const project = new URLSearchParams(location.search).get("project")!;
|
||||||
const win = getCurrentWebviewWindow();
|
const win = getCurrentWebviewWindow();
|
||||||
|
|
||||||
|
// Debug-Instrumentierung wie im Terminal-Fenster: Fehler auf der Seite
|
||||||
|
// anzeigen und ins Dev-Log spiegeln — ein toter Start ist sonst unsichtbar.
|
||||||
|
function showError(msg: string) {
|
||||||
|
const el = document.createElement("pre");
|
||||||
|
el.style.cssText =
|
||||||
|
"color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
|
||||||
|
el.textContent = msg;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
invoke("term_log", { msg: `panel: ${msg}` });
|
||||||
|
}
|
||||||
|
window.addEventListener("error", (e) =>
|
||||||
|
showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
|
||||||
|
);
|
||||||
|
window.addEventListener("unhandledrejection", (e) =>
|
||||||
|
showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
|
||||||
|
);
|
||||||
|
|
||||||
// Dekorationsloses Fenster (Linux): Plattform-Flag + Resize-Zonen wie im
|
// Dekorationsloses Fenster (Linux): Plattform-Flag + Resize-Zonen wie im
|
||||||
// Terminal-Fenster; macOS behält die native Deko.
|
// Terminal-Fenster; macOS behält die native Deko.
|
||||||
const isMac = /Mac|Macintosh/.test(navigator.userAgent);
|
const isMac = /Mac|Macintosh/.test(navigator.userAgent);
|
||||||
@@ -32,6 +51,37 @@ if (!isMac) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Doppelklick auf die Kopfleiste maximiert (Fenster-Konvention).
|
||||||
|
document.querySelector(".panel-topbar")!.addEventListener("dblclick", (e) => {
|
||||||
|
if ((e.target as HTMLElement).hasAttribute("data-tauri-drag-region")) {
|
||||||
|
win.toggleMaximize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fenstergeometrie merken und beim nächsten Öffnen wiederherstellen; unter
|
||||||
|
// Wayland vergibt der Compositor die Position, dann greift nur die Größe.
|
||||||
|
const GEO_KEY = `panel-geometry:${project}`;
|
||||||
|
const savedGeo = localStorage.getItem(GEO_KEY);
|
||||||
|
if (savedGeo) {
|
||||||
|
const g = JSON.parse(savedGeo);
|
||||||
|
await win.setSize(new PhysicalSize(g.w, g.h));
|
||||||
|
await win.setPosition(new PhysicalPosition(g.x, g.y));
|
||||||
|
}
|
||||||
|
let geoTimer: number | undefined;
|
||||||
|
const saveGeo = () => {
|
||||||
|
clearTimeout(geoTimer);
|
||||||
|
geoTimer = window.setTimeout(async () => {
|
||||||
|
const pos = await win.outerPosition();
|
||||||
|
const size = await win.innerSize();
|
||||||
|
localStorage.setItem(
|
||||||
|
GEO_KEY,
|
||||||
|
JSON.stringify({ x: pos.x, y: pos.y, w: size.width, h: size.height }),
|
||||||
|
);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
await win.onMoved(saveGeo);
|
||||||
|
await win.onResized(saveGeo);
|
||||||
|
|
||||||
// Farben ans Theme koppeln — derselbe Look wie das angedockte Panel im
|
// Farben ans Theme koppeln — derselbe Look wie das angedockte Panel im
|
||||||
// Terminal-Fenster (die CSS-Defaults sind Mocha).
|
// Terminal-Fenster (die CSS-Defaults sind Mocha).
|
||||||
interface Project {
|
interface Project {
|
||||||
@@ -40,23 +90,19 @@ interface Project {
|
|||||||
}
|
}
|
||||||
const projects = await invoke<Project[]>("list_projects");
|
const projects = await invoke<Project[]>("list_projects");
|
||||||
const picked = THEMES[projects.find((p) => p.name === project)?.terminal.theme ?? "mocha"];
|
const picked = THEMES[projects.find((p) => p.name === project)?.terminal.theme ?? "mocha"];
|
||||||
|
applyTheme(picked);
|
||||||
|
// Fensterhintergrund des Panel-Fensters ist die Kopf-Fläche, nicht das
|
||||||
|
// Terminal-Dunkel.
|
||||||
document.documentElement.style.background = picked.header;
|
document.documentElement.style.background = picked.header;
|
||||||
document.body.style.background = picked.header;
|
document.body.style.background = picked.header;
|
||||||
document.body.style.color = picked.xterm.foreground;
|
|
||||||
document.body.style.borderColor = picked.border;
|
|
||||||
const topbar = document.querySelector(".panel-topbar") as HTMLElement;
|
|
||||||
topbar.style.background = picked.header;
|
|
||||||
topbar.style.borderBottomColor = picked.border;
|
|
||||||
const head = document.querySelector(".panel-head") as HTMLElement;
|
|
||||||
head.style.background = picked.header;
|
|
||||||
head.style.borderBottomColor = picked.border;
|
|
||||||
|
|
||||||
const { view, cmdView, mode, draft } = await wirePanel(project);
|
const { view, cmdView, mode, draft } = await wirePanel(project);
|
||||||
if (!draft.trim() && !cmdView.empty()) mode.to("commands");
|
if (!draft.trim() && !cmdView.empty()) mode.to("commands");
|
||||||
|
|
||||||
// Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
|
// Archivieren: wie im angedockten Panel — Formular aufklappen, Abschicken
|
||||||
|
// wählt notfalls erst das Archiv-Home per Dialog.
|
||||||
const archiveBtn = document.getElementById("panel-archive")!;
|
const archiveBtn = document.getElementById("panel-archive")!;
|
||||||
archiveBtn.addEventListener("click", async () => {
|
const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
|
||||||
try {
|
try {
|
||||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||||
project,
|
project,
|
||||||
@@ -69,7 +115,13 @@ archiveBtn.addEventListener("click", async () => {
|
|||||||
dir = chosen as string;
|
dir = chosen as string;
|
||||||
}
|
}
|
||||||
await view.flush(); // offene Bearbeitung erst speichern — archiviert, was zu sehen ist
|
await view.flush(); // offene Bearbeitung erst speichern — archiviert, was zu sehen ist
|
||||||
const path = await invoke<string>("panel_archive_cmd", { project, dir });
|
const path = await invoke<string>("panel_archive_cmd", {
|
||||||
|
project,
|
||||||
|
dir,
|
||||||
|
folder: meta.folder ?? null,
|
||||||
|
description: meta.description ?? null,
|
||||||
|
tags: meta.tags,
|
||||||
|
});
|
||||||
flash(archiveBtn, "copied", 1400);
|
flash(archiveBtn, "copied", 1400);
|
||||||
invoke("reveal_path_cmd", { path });
|
invoke("reveal_path_cmd", { path });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -77,6 +129,7 @@ archiveBtn.addEventListener("click", async () => {
|
|||||||
panelToast(`Archivieren fehlgeschlagen: ${e}`);
|
panelToast(`Archivieren fehlgeschlagen: ${e}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
archiveBtn.addEventListener("click", () => archiveForm.toggle());
|
||||||
|
|
||||||
// „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.
|
// „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.
|
||||||
document.getElementById("panel-dock")!.addEventListener("click", async () => {
|
document.getElementById("panel-dock")!.addEventListener("click", async () => {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Suchfeld-Logik (Live-Suche, Debounce, Schwelle) und Treffer-Kacheln.
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { initSearchView } from "./search-view";
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
document.body.innerHTML = `<div id="s"></div>`;
|
||||||
|
const onOpen = vi.fn();
|
||||||
|
const onSearch = vi.fn();
|
||||||
|
const view = initSearchView(document.getElementById("s")!, onOpen, onSearch);
|
||||||
|
const input = document.querySelector<HTMLInputElement>(".hit-search input")!;
|
||||||
|
const type = (v: string) => {
|
||||||
|
input.value = v;
|
||||||
|
input.dispatchEvent(new Event("input"));
|
||||||
|
};
|
||||||
|
const enter = () =>
|
||||||
|
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||||
|
return { view, onOpen, onSearch, input, type, enter };
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = JSON.stringify({
|
||||||
|
query: "arch",
|
||||||
|
tag: null,
|
||||||
|
home: "/tmp/archiv",
|
||||||
|
hits: [{ relpath: "a/2026-01-01_0000-x.md", title: "X", snippet: "ein **arch**iv" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initSearchView", () => {
|
||||||
|
beforeEach(() => vi.useFakeTimers());
|
||||||
|
afterEach(() => vi.useRealTimers());
|
||||||
|
|
||||||
|
it("sucht ab 3 Zeichen nach 300 ms mit Präfix-Stern", () => {
|
||||||
|
const { onSearch, type } = setup();
|
||||||
|
type("arch");
|
||||||
|
expect(onSearch).not.toHaveBeenCalled();
|
||||||
|
vi.advanceTimersByTime(300);
|
||||||
|
expect(onSearch).toHaveBeenCalledWith("arch*");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hängt an #tag-Tokens keinen Stern an", () => {
|
||||||
|
const { onSearch, type } = setup();
|
||||||
|
type("panel #adr");
|
||||||
|
vi.advanceTimersByTime(300);
|
||||||
|
expect(onSearch).toHaveBeenCalledWith("panel #adr");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("entprellt: nur die letzte Eingabe zählt", () => {
|
||||||
|
const { onSearch, type } = setup();
|
||||||
|
type("arc");
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
type("arch");
|
||||||
|
vi.advanceTimersByTime(300);
|
||||||
|
expect(onSearch).toHaveBeenCalledOnce();
|
||||||
|
expect(onSearch).toHaveBeenCalledWith("arch*");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("räumt beim Löschen unter 3 Zeichen auf und zeigt den Hinweis", () => {
|
||||||
|
const { view, onSearch, type } = setup();
|
||||||
|
view.set(run);
|
||||||
|
expect(document.querySelectorAll(".hit-tile").length).toBe(1);
|
||||||
|
type("ar");
|
||||||
|
expect(onSearch).not.toHaveBeenCalled();
|
||||||
|
expect(document.querySelectorAll(".hit-tile").length).toBe(0);
|
||||||
|
expect(document.querySelector(".hit-head")!.textContent).toContain(
|
||||||
|
"Mindestens 3 Zeichen",
|
||||||
|
);
|
||||||
|
type("");
|
||||||
|
expect(document.querySelector(".hit-head")).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Enter sucht sofort und wörtlich", () => {
|
||||||
|
const { onSearch, type, enter } = setup();
|
||||||
|
type("panel arch");
|
||||||
|
enter();
|
||||||
|
expect(onSearch).toHaveBeenCalledWith("panel arch");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rendert Treffer; Klick liefert den absoluten Pfad", () => {
|
||||||
|
const { view, onOpen } = setup();
|
||||||
|
view.set(run);
|
||||||
|
const tile = document.querySelector<HTMLElement>(".hit-tile")!;
|
||||||
|
expect(tile.querySelector(".hit-title")!.textContent).toBe("X");
|
||||||
|
expect(tile.querySelectorAll("mark").length).toBe(1);
|
||||||
|
tile.click();
|
||||||
|
expect(onOpen).toHaveBeenCalledWith("/tmp/archiv/a/2026-01-01_0000-x.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zeigt „Keine Treffer“ bei leerem Ergebnis", () => {
|
||||||
|
const { view } = setup();
|
||||||
|
view.set(JSON.stringify({ query: "nix", tag: "adr", home: "/a", hits: [] }));
|
||||||
|
expect(document.querySelector(".hit-head")!.textContent).toBe(
|
||||||
|
"Keine Treffer für „nix“ · #adr",
|
||||||
|
);
|
||||||
|
expect(view.empty()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("übernimmt die Query eines fremden Suchlaufs ins Feld", () => {
|
||||||
|
const { view, input } = setup();
|
||||||
|
view.set(run);
|
||||||
|
expect(input.value).toBe("arch");
|
||||||
|
});
|
||||||
|
});
|
||||||
+26
-9
@@ -1,6 +1,6 @@
|
|||||||
/// Kachel-Ansicht der Archiv-Suchtreffer: rendert die Suchtreffer-Datei
|
/// Kachel-Ansicht der Archiv-Suchtreffer: rendert die Suchtreffer-Datei
|
||||||
/// (search_archive im MCP-Server) als klickbare Kacheln — Klick reicht den
|
/// (search_archive im MCP-Server) als klickbare Kacheln — Klick reicht den
|
||||||
/// relpath des Treffers an onOpen. Titel/Snippet/Pfad sind Fremdtext und
|
/// absoluten Pfad des Treffers an onOpen. Titel/Snippet/Pfad sind Fremdtext und
|
||||||
/// gehen nie durch innerHTML; die `**…**`-Marker im Snippet werden per
|
/// gehen nie durch innerHTML; die `**…**`-Marker im Snippet werden per
|
||||||
/// Split in <mark>-Elemente übersetzt.
|
/// Split in <mark>-Elemente übersetzt.
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export interface SearchView {
|
|||||||
|
|
||||||
export function initSearchView(
|
export function initSearchView(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
onOpen: (relpath: string) => void,
|
onOpen: (path: string) => void,
|
||||||
onSearch: (query: string) => void,
|
onSearch: (query: string) => void,
|
||||||
): SearchView {
|
): SearchView {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -34,16 +34,30 @@ export function initSearchView(
|
|||||||
bar.className = "hit-search";
|
bar.className = "hit-search";
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
input.type = "search";
|
input.type = "search";
|
||||||
input.placeholder = "Archiv durchsuchen";
|
input.placeholder = "Archiv durchsuchen — #tag filtert";
|
||||||
// Live-Suche ab 3 Zeichen, entprellt (300 ms Debounce); endet die Eingabe
|
// Live-Suche ab 3 Zeichen, entprellt (300 ms Debounce); endet die Eingabe
|
||||||
// mitten im Wort, wird das letzte Wort als Präfix gesucht (arch → arch*).
|
// mitten im Wort, wird das letzte Wort als Präfix gesucht (arch → arch*) —
|
||||||
// Enter sucht sofort und wörtlich — für exakte FTS-Syntax (Phrasen, OR/NOT).
|
// außer es ist ein #tag-Token. Enter sucht sofort und wörtlich — für exakte
|
||||||
|
// FTS-Syntax (Phrasen, OR/NOT).
|
||||||
let pending: number | undefined;
|
let pending: number | undefined;
|
||||||
input.addEventListener("input", () => {
|
input.addEventListener("input", () => {
|
||||||
clearTimeout(pending);
|
clearTimeout(pending);
|
||||||
const q = input.value.trim();
|
const q = input.value.trim();
|
||||||
if (q.length < 3) return;
|
if (q.length < 3) {
|
||||||
const live = /[\p{L}\p{N}]$/u.test(q) ? `${q}*` : q;
|
// Beim Löschen unter die Schwelle: alte Treffer weg, kurzer Hinweis.
|
||||||
|
count = 0;
|
||||||
|
results.textContent = "";
|
||||||
|
if (q) {
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "hit-head";
|
||||||
|
head.textContent = "Mindestens 3 Zeichen — oder Enter.";
|
||||||
|
results.append(head);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const last = q.split(/\s+/).pop()!;
|
||||||
|
const live =
|
||||||
|
!last.startsWith("#") && /[\p{L}\p{N}]$/u.test(q) ? `${q}*` : q;
|
||||||
pending = window.setTimeout(() => onSearch(live), 300);
|
pending = window.setTimeout(() => onSearch(live), 300);
|
||||||
});
|
});
|
||||||
input.addEventListener("keydown", (e) => {
|
input.addEventListener("keydown", (e) => {
|
||||||
@@ -77,7 +91,10 @@ export function initSearchView(
|
|||||||
head.className = "hit-head";
|
head.className = "hit-head";
|
||||||
const what = run.query.trim() ? `„${run.query}“` : "";
|
const what = run.query.trim() ? `„${run.query}“` : "";
|
||||||
const tag = run.tag ? `#${run.tag}` : "";
|
const tag = run.tag ? `#${run.tag}` : "";
|
||||||
head.textContent = `${run.hits.length} Treffer ${[what, tag].filter(Boolean).join(" · ")}`;
|
const scope = [what, tag].filter(Boolean).join(" · ");
|
||||||
|
head.textContent = run.hits.length
|
||||||
|
? `${run.hits.length} Treffer ${scope}`
|
||||||
|
: `Keine Treffer für ${scope}`;
|
||||||
results.append(head);
|
results.append(head);
|
||||||
for (const hit of run.hits) {
|
for (const hit of run.hits) {
|
||||||
const tile = document.createElement("div");
|
const tile = document.createElement("div");
|
||||||
@@ -89,7 +106,7 @@ export function initSearchView(
|
|||||||
path.className = "hit-path";
|
path.className = "hit-path";
|
||||||
path.textContent = hit.relpath;
|
path.textContent = hit.relpath;
|
||||||
tile.append(title, snippetEl(hit.snippet), path);
|
tile.append(title, snippetEl(hit.snippet), path);
|
||||||
tile.addEventListener("click", () => onOpen(hit.relpath));
|
tile.addEventListener("click", () => onOpen(`${run.home}/${hit.relpath}`));
|
||||||
results.append(tile);
|
results.append(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-22
@@ -11,8 +11,9 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { wirePanel } from "./panel-wiring";
|
import { wirePanel } from "./panel-wiring";
|
||||||
import { THEMES } from "./themes";
|
import { applyTheme, THEMES } from "./themes";
|
||||||
import { flash, panelToast } from "./commands-view";
|
import { flash, panelToast } from "./commands-view";
|
||||||
|
import { initArchiveForm } from "./archive-form";
|
||||||
|
|
||||||
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
||||||
function showError(msg: string) {
|
function showError(msg: string) {
|
||||||
@@ -56,6 +57,13 @@ if (!isMac) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Doppelklick auf die Kopfleiste maximiert (Fenster-Konvention).
|
||||||
|
document.querySelector("header")!.addEventListener("dblclick", (e) => {
|
||||||
|
if ((e.target as HTMLElement).hasAttribute("data-tauri-drag-region")) {
|
||||||
|
win.toggleMaximize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Fensterhintergrund beim Öffnen kommt aus terminal.rs (theme_background);
|
// Fensterhintergrund beim Öffnen kommt aus terminal.rs (theme_background);
|
||||||
// dort nicht gepflegte Themes fallen auf Mocha zurück (nur kurzer Moment
|
// dort nicht gepflegte Themes fallen auf Mocha zurück (nur kurzer Moment
|
||||||
// bis die Webview lädt).
|
// bis die Webview lädt).
|
||||||
@@ -90,14 +98,8 @@ if (cfg?.terminal.icon) {
|
|||||||
const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
|
const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
|
||||||
const theme = picked.xterm;
|
const theme = picked.xterm;
|
||||||
|
|
||||||
// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
|
// Seitenfarben ans Theme anpassen — alles Weitere hängt an den CSS-Variablen.
|
||||||
document.documentElement.style.background = theme.background;
|
applyTheme(picked);
|
||||||
document.body.style.background = theme.background;
|
|
||||||
document.body.style.borderColor = picked.border;
|
|
||||||
const header = document.querySelector("header") as HTMLElement;
|
|
||||||
header.style.background = picked.header;
|
|
||||||
header.style.borderBottomColor = picked.border;
|
|
||||||
header.style.color = theme.foreground;
|
|
||||||
|
|
||||||
// Globale Schriftgröße (settings.json: terminalFontSize), ein Wert für alle.
|
// Globale Schriftgröße (settings.json: terminalFontSize), ein Wert für alle.
|
||||||
const DEFAULT_FONT_SIZE = 13;
|
const DEFAULT_FONT_SIZE = 13;
|
||||||
@@ -186,13 +188,6 @@ term.focus();
|
|||||||
const panel = document.getElementById("panel")!;
|
const panel = document.getElementById("panel")!;
|
||||||
const splitter = document.getElementById("splitter")!;
|
const splitter = document.getElementById("splitter")!;
|
||||||
|
|
||||||
// Panelfarben ans Theme koppeln.
|
|
||||||
panel.style.background = picked.header;
|
|
||||||
panel.style.color = theme.foreground;
|
|
||||||
splitter.style.background = picked.border;
|
|
||||||
(panel.querySelector(".panel-head") as HTMLElement).style.borderBottomColor =
|
|
||||||
picked.border;
|
|
||||||
|
|
||||||
let detached = false;
|
let detached = false;
|
||||||
let hasContent = false;
|
let hasContent = false;
|
||||||
|
|
||||||
@@ -209,39 +204,52 @@ function hidePanel() {
|
|||||||
|
|
||||||
// Gemeinsame Verdrahtung (Views, Modi, Update-Events); jedes eingehende
|
// Gemeinsame Verdrahtung (Views, Modi, Update-Events); jedes eingehende
|
||||||
// Update blendet das angedockte Panel ein, solange es nicht abgelöst ist.
|
// Update blendet das angedockte Panel ein, solange es nicht abgelöst ist.
|
||||||
const { view } = await wirePanel(project, () => {
|
const { view, mode } = await wirePanel(project, () => {
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
if (!detached) showPanel();
|
if (!detached) showPanel();
|
||||||
});
|
});
|
||||||
|
// Panel startet zugeklappt — kein Tab aktiv, bis eine Ansicht geöffnet wird.
|
||||||
|
mode.clear();
|
||||||
|
|
||||||
// Tab-Klick im Header öffnet das (angedockte) Panel, falls es zu ist.
|
// Tab-Klick im Header öffnet das (angedockte) Panel, falls es zu ist.
|
||||||
document.getElementById("panel-tabs")!.addEventListener("click", () => {
|
const headerTabs = document.getElementById("panel-tabs")!;
|
||||||
|
headerTabs.addEventListener("click", () => {
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
if (!detached) showPanel();
|
if (!detached) showPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Solange das Panel abgelöst ist, hat das eigene Fenster die Tabs — die im
|
||||||
|
// Header verschwinden.
|
||||||
await listen("panel-detached", () => {
|
await listen("panel-detached", () => {
|
||||||
detached = true;
|
detached = true;
|
||||||
|
headerTabs.hidden = true;
|
||||||
hidePanel();
|
hidePanel();
|
||||||
|
mode.clear();
|
||||||
});
|
});
|
||||||
await listen("panel-attached", () => {
|
await listen("panel-attached", () => {
|
||||||
detached = false;
|
detached = false;
|
||||||
|
headerTabs.hidden = false;
|
||||||
if (hasContent) showPanel();
|
if (hasContent) showPanel();
|
||||||
});
|
});
|
||||||
// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt
|
// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt
|
||||||
// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.
|
// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.
|
||||||
await listen("panel-window-closed", () => {
|
await listen("panel-window-closed", () => {
|
||||||
detached = false;
|
detached = false;
|
||||||
|
headerTabs.hidden = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("panel-detach")!
|
.getElementById("panel-detach")!
|
||||||
.addEventListener("click", () => invoke("open_panel_window", { project }));
|
.addEventListener("click", () => invoke("open_panel_window", { project }));
|
||||||
document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
|
document.getElementById("panel-hide")!.addEventListener("click", () => {
|
||||||
|
hidePanel();
|
||||||
|
mode.clear();
|
||||||
|
});
|
||||||
|
|
||||||
// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
|
// Archivieren: Button klappt das Formular (Ordner/Beschreibung/Schlagwörter)
|
||||||
|
// auf; Abschicken wählt notfalls erst das Archiv-Home per Dialog.
|
||||||
const archiveBtn = document.getElementById("panel-archive")!;
|
const archiveBtn = document.getElementById("panel-archive")!;
|
||||||
archiveBtn.addEventListener("click", async () => {
|
const archiveForm = initArchiveForm(archiveBtn, async (meta) => {
|
||||||
try {
|
try {
|
||||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||||
project,
|
project,
|
||||||
@@ -254,7 +262,13 @@ archiveBtn.addEventListener("click", async () => {
|
|||||||
dir = chosen as string;
|
dir = chosen as string;
|
||||||
}
|
}
|
||||||
await view.flush(); // offene Bearbeitung erst speichern — archiviert, was zu sehen ist
|
await view.flush(); // offene Bearbeitung erst speichern — archiviert, was zu sehen ist
|
||||||
const path = await invoke<string>("panel_archive_cmd", { project, dir });
|
const path = await invoke<string>("panel_archive_cmd", {
|
||||||
|
project,
|
||||||
|
dir,
|
||||||
|
folder: meta.folder ?? null,
|
||||||
|
description: meta.description ?? null,
|
||||||
|
tags: meta.tags,
|
||||||
|
});
|
||||||
flash(archiveBtn, "copied", 1400);
|
flash(archiveBtn, "copied", 1400);
|
||||||
invoke("reveal_path_cmd", { path });
|
invoke("reveal_path_cmd", { path });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -263,6 +277,7 @@ archiveBtn.addEventListener("click", async () => {
|
|||||||
invoke("term_log", { msg: `archive: ${e}` });
|
invoke("term_log", { msg: `archive: ${e}` });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
archiveBtn.addEventListener("click", () => archiveForm.toggle());
|
||||||
|
|
||||||
// Splitter: Panelbreite ziehen.
|
// Splitter: Panelbreite ziehen.
|
||||||
splitter.addEventListener("mousedown", (e) => {
|
splitter.addEventListener("mousedown", (e) => {
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
/// Farbschemata der Fenster: Header-/Rahmenfarbe plus xterm-Theme —
|
/// Farbschemata der Fenster: Header-/Rahmenfarbe plus xterm-Theme —
|
||||||
/// genutzt vom Terminal-Fenster (Header, angedocktes Panel) und vom
|
/// genutzt vom Terminal-Fenster (Header, angedocktes Panel) und vom
|
||||||
/// abgelösten Panel-Fenster (panel.ts), damit beide gleich aussehen.
|
/// abgelösten Panel-Fenster (panel.ts), damit beide gleich aussehen.
|
||||||
|
|
||||||
|
/// Überträgt ein Theme auf die CSS-Variablen der Seite; die Defaults in den
|
||||||
|
/// HTML-Styleblöcken sind Mocha. Fehlt einem Theme ein Farbwert, bleibt der
|
||||||
|
/// Mocha-Default stehen.
|
||||||
|
export function applyTheme(picked: (typeof THEMES)[string]) {
|
||||||
|
const x = picked.xterm;
|
||||||
|
const vars: Record<string, string | undefined> = {
|
||||||
|
"--bg": x.background,
|
||||||
|
"--surface": picked.header,
|
||||||
|
"--tile": x.background,
|
||||||
|
"--line": picked.border,
|
||||||
|
"--line-strong": x.black,
|
||||||
|
"--text": x.foreground,
|
||||||
|
"--muted": x.brightWhite,
|
||||||
|
"--faint": x.brightBlack,
|
||||||
|
"--accent": x.blue,
|
||||||
|
"--ok": x.green,
|
||||||
|
"--warn": x.yellow,
|
||||||
|
"--err": x.red,
|
||||||
|
};
|
||||||
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
|
if (v) document.documentElement.style.setProperty(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const THEMES: Record<
|
export const THEMES: Record<
|
||||||
string,
|
string,
|
||||||
{ header: string; border: string; xterm: Record<string, string> }
|
{ header: string; border: string; xterm: Record<string, string> }
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// Wiki-Ansicht: Übersichts-/Schlagwort-Seiten, Dokumentseite, Leerzustände.
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { initWikiView } from "./wiki-view";
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
document.body.innerHTML = `<div id="w"></div>`;
|
||||||
|
const onLink = vi.fn();
|
||||||
|
const view = initWikiView(document.getElementById("w")!, onLink);
|
||||||
|
return { view, onLink };
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = (name: string, extra: object = {}) => ({
|
||||||
|
name,
|
||||||
|
title: name.toUpperCase(),
|
||||||
|
description: null,
|
||||||
|
tags: [],
|
||||||
|
date: "2026-07-19",
|
||||||
|
backlinks: 0,
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = JSON.stringify({
|
||||||
|
kind: "page",
|
||||||
|
home: "/tmp/archiv",
|
||||||
|
tag: null,
|
||||||
|
total: 3,
|
||||||
|
tags: [
|
||||||
|
{ name: "adr", count: 2 },
|
||||||
|
{ name: "infra", count: 1 },
|
||||||
|
],
|
||||||
|
recent: [doc("neu")],
|
||||||
|
folders: [
|
||||||
|
{ name: "", docs: [doc("wurzel-doc", { backlinks: 2, tags: ["adr"] })] },
|
||||||
|
{ name: "konzepte", docs: [doc("neu"), doc("alt")] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initWikiView — Seiten", () => {
|
||||||
|
it("rendert Kopf, Chips, Zuletzt- und Ordner-Sektionen", () => {
|
||||||
|
const { view } = setup();
|
||||||
|
view.set(page);
|
||||||
|
expect(document.querySelector(".wiki-head-title")!.textContent).toBe("Archiv");
|
||||||
|
expect(document.querySelector(".wiki-head-right")!.textContent).toBe("3 Dokumente");
|
||||||
|
const chips = [...document.querySelectorAll(".wiki-chips .wiki-chip")].map(
|
||||||
|
(c) => c.textContent,
|
||||||
|
);
|
||||||
|
expect(chips).toEqual(["Alle", "#adr 2", "#infra 1"]);
|
||||||
|
const eyebrows = [...document.querySelectorAll(".wiki-folder")].map(
|
||||||
|
(e) => e.textContent,
|
||||||
|
);
|
||||||
|
expect(eyebrows).toEqual(["Zuletzt", "Wurzel", "konzepte/"]);
|
||||||
|
expect(document.querySelector(".wiki-doc-back")!.textContent).toBe("↩ 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Dokumentzeile öffnet das Dokument, Tag-Chip die Tag-Seite", () => {
|
||||||
|
const { view, onLink } = setup();
|
||||||
|
view.set(page);
|
||||||
|
const rows = document.querySelectorAll<HTMLElement>(".wiki-doc");
|
||||||
|
rows[1].click(); // Wurzel-Dokument (nach der Zuletzt-Zeile)
|
||||||
|
expect(onLink).toHaveBeenCalledWith("wurzel-doc");
|
||||||
|
rows[1].querySelector<HTMLElement>(".wiki-doc-tags .wiki-chip")!.click();
|
||||||
|
expect(onLink).toHaveBeenCalledWith("tag:adr");
|
||||||
|
document.querySelector<HTMLElement>(".wiki-chips .wiki-chip")!.click();
|
||||||
|
expect(onLink).toHaveBeenCalledWith("tag:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Tag-Seite: Titel, aktiver Chip, Leerfall", () => {
|
||||||
|
const { view } = setup();
|
||||||
|
view.set(
|
||||||
|
JSON.stringify({
|
||||||
|
kind: "page",
|
||||||
|
home: "/a",
|
||||||
|
tag: "adr",
|
||||||
|
total: 0,
|
||||||
|
tags: [{ name: "adr", count: 1 }],
|
||||||
|
recent: [],
|
||||||
|
folders: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(document.querySelector(".wiki-head-title")!.textContent).toBe("#adr");
|
||||||
|
const active = document.querySelector(".wiki-chip.active")!;
|
||||||
|
expect(active.textContent).toBe("#adr 1");
|
||||||
|
expect(document.querySelector(".wiki-empty strong")!.textContent).toBe(
|
||||||
|
"Keine Dokumente mit #adr.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leeres Archiv erklärt das Archivieren", () => {
|
||||||
|
const { view } = setup();
|
||||||
|
view.set(
|
||||||
|
JSON.stringify({
|
||||||
|
kind: "page",
|
||||||
|
home: "/a",
|
||||||
|
tag: null,
|
||||||
|
total: 0,
|
||||||
|
tags: [],
|
||||||
|
recent: [],
|
||||||
|
folders: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(document.querySelector(".wiki-empty")!.textContent).toContain(
|
||||||
|
"Das Archiv ist leer.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initWikiView — Dokument und Leerzustand", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rendert Markdown, Rücksprung, Tags und Backlinks", () => {
|
||||||
|
const { view, onLink } = setup();
|
||||||
|
view.set(
|
||||||
|
JSON.stringify({
|
||||||
|
kind: "doc",
|
||||||
|
home: "/a",
|
||||||
|
relpath: "konzepte/2026-07-19_1000-x.md",
|
||||||
|
name: "x",
|
||||||
|
title: "X",
|
||||||
|
tags: ["adr"],
|
||||||
|
backlinks: ["anderes-doc"],
|
||||||
|
markdown: "# Überschrift\n\nText mit [[ziel|Label]].\n",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(document.querySelector(".wiki-body h1")!.textContent).toBe("Überschrift");
|
||||||
|
const wiki = document.querySelector<HTMLElement>(".wiki-body a.wiki")!;
|
||||||
|
expect(wiki.textContent).toBe("Label");
|
||||||
|
wiki.click();
|
||||||
|
expect(onLink).toHaveBeenCalledWith("ziel");
|
||||||
|
document.querySelector<HTMLElement>(".wiki-back")!.click();
|
||||||
|
expect(onLink).toHaveBeenCalledWith("tag:");
|
||||||
|
const back = document.querySelector<HTMLElement>(".wiki-backlinks a")!;
|
||||||
|
back.click();
|
||||||
|
expect(onLink).toHaveBeenCalledWith("anderes-doc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leerer Puffer bietet den Übersichts-Einstieg", () => {
|
||||||
|
const { view, onLink } = setup();
|
||||||
|
view.set("");
|
||||||
|
expect(view.empty()).toBe(true);
|
||||||
|
document.querySelector<HTMLElement>(".wiki-empty .wiki-chip")!.click();
|
||||||
|
expect(onLink).toHaveBeenCalledWith("tag:");
|
||||||
|
view.set(page);
|
||||||
|
expect(view.empty()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ interface DocEntry {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
date?: string | null;
|
date?: string | null;
|
||||||
|
backlinks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Folder {
|
interface Folder {
|
||||||
@@ -27,6 +28,7 @@ interface Page {
|
|||||||
tag?: string | null;
|
tag?: string | null;
|
||||||
total: number;
|
total: number;
|
||||||
tags: { name: string; count: number }[];
|
tags: { name: string; count: number }[];
|
||||||
|
recent: DocEntry[];
|
||||||
folders: Folder[];
|
folders: Folder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +105,13 @@ export function initWikiView(
|
|||||||
title.className = "wiki-doc-title";
|
title.className = "wiki-doc-title";
|
||||||
title.textContent = doc.title;
|
title.textContent = doc.title;
|
||||||
line.append(title);
|
line.append(title);
|
||||||
|
if (doc.backlinks) {
|
||||||
|
const back = document.createElement("div");
|
||||||
|
back.className = "wiki-doc-back";
|
||||||
|
back.title = "Verweise hierher";
|
||||||
|
back.textContent = `↩ ${doc.backlinks}`;
|
||||||
|
line.append(back);
|
||||||
|
}
|
||||||
if (doc.date) {
|
if (doc.date) {
|
||||||
const date = document.createElement("div");
|
const date = document.createElement("div");
|
||||||
date.className = "wiki-doc-date";
|
date.className = "wiki-doc-date";
|
||||||
@@ -143,12 +152,24 @@ export function initWikiView(
|
|||||||
container.append(empty);
|
container.append(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (p.recent.length) {
|
||||||
|
const eyebrow = document.createElement("div");
|
||||||
|
eyebrow.className = "wiki-folder";
|
||||||
|
eyebrow.textContent = "Zuletzt";
|
||||||
|
container.append(eyebrow);
|
||||||
|
for (const doc of p.recent) container.append(docRow(doc));
|
||||||
|
}
|
||||||
for (const folder of p.folders) {
|
for (const folder of p.folders) {
|
||||||
if (folder.name) {
|
if (folder.name) {
|
||||||
const eyebrow = document.createElement("div");
|
const eyebrow = document.createElement("div");
|
||||||
eyebrow.className = "wiki-folder";
|
eyebrow.className = "wiki-folder";
|
||||||
eyebrow.textContent = `${folder.name}/`;
|
eyebrow.textContent = `${folder.name}/`;
|
||||||
container.append(eyebrow);
|
container.append(eyebrow);
|
||||||
|
} else if (p.recent.length) {
|
||||||
|
const eyebrow = document.createElement("div");
|
||||||
|
eyebrow.className = "wiki-folder";
|
||||||
|
eyebrow.textContent = "Wurzel";
|
||||||
|
container.append(eyebrow);
|
||||||
}
|
}
|
||||||
for (const doc of folder.docs) container.append(docRow(doc));
|
for (const doc of folder.docs) container.append(docRow(doc));
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-34
@@ -4,11 +4,26 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Terminal</title>
|
<title>Terminal</title>
|
||||||
<style>
|
<style>
|
||||||
|
/* Theme-Variablen — Defaults sind Mocha; applyTheme (themes.ts) überschreibt. */
|
||||||
|
:root {
|
||||||
|
--bg: #1e1e2e;
|
||||||
|
--surface: #181825;
|
||||||
|
--tile: #11111b;
|
||||||
|
--line: #313244;
|
||||||
|
--line-strong: #45475a;
|
||||||
|
--text: #cdd6f4;
|
||||||
|
--muted: #a6adc8;
|
||||||
|
--faint: #6c7086;
|
||||||
|
--accent: #89b4fa;
|
||||||
|
--ok: #a6e3a1;
|
||||||
|
--warn: #f9e2af;
|
||||||
|
--err: #f38ba8;
|
||||||
|
}
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #1e1e2e;
|
background: var(--bg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@@ -18,7 +33,7 @@
|
|||||||
/* Feiner Fensterrahmen fürs dekorationslose Fenster (nur Linux). */
|
/* Feiner Fensterrahmen fürs dekorationslose Fenster (nur Linux). */
|
||||||
:root[data-platform="other"] body {
|
:root[data-platform="other"] body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #313244;
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
header {
|
header {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -28,9 +43,9 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
/* Linux: schmaler Rand links; macOS: Ampel-Platz (Override unten) */
|
/* Linux: schmaler Rand links; macOS: Ampel-Platz (Override unten) */
|
||||||
padding: 0 0 0 16px;
|
padding: 0 0 0 16px;
|
||||||
background: #181825;
|
background: var(--surface);
|
||||||
border-bottom: 1px solid #313244;
|
border-bottom: 1px solid var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
font:
|
font:
|
||||||
500 13px/1 -apple-system,
|
500 13px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -59,16 +74,23 @@
|
|||||||
header .panel-tabs {
|
header .panel-tabs {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
header .panel-tabs[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/* Ohne Tabs (Panel abgelöst) rückt das Pool-Badge selbst nach rechts. */
|
||||||
|
header .panel-tabs[hidden] + #pool {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
.tab-sep {
|
.tab-sep {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background: #45475a;
|
background: var(--line-strong);
|
||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
#pool {
|
#pool {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -103,14 +125,14 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
.winbtn:hover {
|
.winbtn:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
}
|
}
|
||||||
.winbtn:hover svg {
|
.winbtn:hover svg {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.winbtn.close:hover {
|
.winbtn.close:hover {
|
||||||
background: #f38ba8;
|
background: var(--err);
|
||||||
color: #11111b;
|
color: var(--tile);
|
||||||
}
|
}
|
||||||
/* Resize-Zonen für das dekorationslose Fenster (nur Linux). */
|
/* Resize-Zonen für das dekorationslose Fenster (nur Linux). */
|
||||||
#resize-grips {
|
#resize-grips {
|
||||||
@@ -199,7 +221,7 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
#term .xterm-viewport::-webkit-scrollbar-thumb {
|
#term .xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
#term .xterm-viewport::-webkit-scrollbar-track {
|
#term .xterm-viewport::-webkit-scrollbar-track {
|
||||||
@@ -210,7 +232,7 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
}
|
}
|
||||||
#panel {
|
#panel {
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -218,8 +240,8 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #181825;
|
background: var(--surface);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
#panel[hidden],
|
#panel[hidden],
|
||||||
#splitter[hidden] {
|
#splitter[hidden] {
|
||||||
@@ -231,7 +253,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
padding: 6px 8px 8px 12px;
|
padding: 6px 8px 8px 12px;
|
||||||
border-bottom: 1px solid #313244;
|
border-bottom: 1px solid var(--line);
|
||||||
font:
|
font:
|
||||||
500 12px/1 -apple-system,
|
500 12px/1 -apple-system,
|
||||||
system-ui,
|
system-ui,
|
||||||
@@ -261,7 +283,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.panel-title[contenteditable="true"] {
|
.panel-title[contenteditable="true"] {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -273,7 +295,7 @@
|
|||||||
.panel-btn {
|
.panel-btn {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
@@ -282,14 +304,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.panel-btn:hover {
|
.panel-btn:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.panel-btn.copied {
|
.panel-btn.copied {
|
||||||
color: #a6e3a1;
|
color: var(--ok);
|
||||||
}
|
}
|
||||||
.panel-btn.error {
|
.panel-btn.error {
|
||||||
color: #f38ba8;
|
color: var(--err);
|
||||||
}
|
}
|
||||||
.panel-btn svg {
|
.panel-btn svg {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -300,11 +322,18 @@
|
|||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
}
|
}
|
||||||
.panel-btn.active {
|
.panel-btn.active {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
|
}
|
||||||
|
/* Aktiver Tab: Akzentfarbe + Unterstrich statt nur Flächenwechsel. */
|
||||||
|
.panel-tabs .panel-btn.active {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: inset 0 -2px 0 currentColor;
|
||||||
}
|
}
|
||||||
.panel-btn.changed {
|
.panel-btn.changed {
|
||||||
color: #f9e2af;
|
color: var(--warn);
|
||||||
}
|
}
|
||||||
.panel-editor {
|
.panel-editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -331,7 +360,7 @@
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 6px 9px;
|
padding: 6px 9px;
|
||||||
@@ -339,8 +368,8 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.panel-lang:hover {
|
.panel-lang:hover {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
color: #cdd6f4;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
#panel-content {
|
#panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -357,7 +386,7 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
#panel-content::-webkit-scrollbar-thumb {
|
#panel-content::-webkit-scrollbar-thumb {
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
#panel-content.raw {
|
#panel-content.raw {
|
||||||
@@ -394,12 +423,12 @@
|
|||||||
#panel-content.md code {
|
#panel-content.md code {
|
||||||
font-family: "JetBrains Mono", Menlo, monospace;
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
background: #313244;
|
background: var(--line);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
#panel-content.md pre {
|
#panel-content.md pre {
|
||||||
background: #11111b;
|
background: var(--tile);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -409,12 +438,12 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#panel-content.md blockquote {
|
#panel-content.md blockquote {
|
||||||
border-left: 3px solid #313244;
|
border-left: 3px solid var(--line);
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
color: #a6adc8;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
#panel-content.md a {
|
#panel-content.md a {
|
||||||
color: #89b4fa;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
.panel-btn[hidden],
|
.panel-btn[hidden],
|
||||||
.panel-lang[hidden],
|
.panel-lang[hidden],
|
||||||
@@ -473,6 +502,9 @@
|
|||||||
<option value="it">IT</option>
|
<option value="it">IT</option>
|
||||||
<option value="nl">NL</option>
|
<option value="nl">NL</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button class="panel-btn draft-only" id="panel-wiki-jump" title="Im Wiki öffnen">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M8 3.5C6.6 2.4 4.6 2 2.5 2v11c2.1 0 4.1.4 5.5 1.5 1.4-1.1 3.4-1.5 5.5-1.5V2c-2.1 0-4.1.4-5.5 1.5z" /><path d="M8 3.5v11" /></svg>
|
||||||
|
</button>
|
||||||
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
<button class="panel-btn draft-only" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
||||||
<button class="panel-btn draft-only" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
<button class="panel-btn draft-only" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "happy-dom",
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user