Compare commits
2 Commits
d6b3d3c88b
...
bd44626532
| Author | SHA1 | Date | |
|---|---|---|---|
| bd44626532 | |||
| b0cdf64bbf |
70
web/package-lock.json
generated
70
web/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "oikos-web",
|
"name": "oikos-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@surdeddd/wmkit": "^0.3.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"dompurify": "^3.4.11",
|
"dompurify": "^3.4.11",
|
||||||
@@ -876,7 +877,7 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -887,7 +888,7 @@
|
|||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
@@ -898,7 +899,7 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -908,14 +909,14 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -1282,11 +1283,44 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@surdeddd/wmkit": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@surdeddd/wmkit/-/wmkit-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-r5reUXN0Mcnx1nFjfB2grPCqpkH67S7UdaIY1bhyls5Tim4RyAAVscQSsimo1fOSqndZs9venjh3bueLJQVEMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=16",
|
||||||
|
"react": ">=18",
|
||||||
|
"solid-js": ">=1.8",
|
||||||
|
"svelte": ">=4",
|
||||||
|
"vue": ">=3.3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@angular/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"solid-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sveltejs/acorn-typescript": {
|
"node_modules/@sveltejs/acorn-typescript": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
|
||||||
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
|
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"acorn": "^8.9.0"
|
"acorn": "^8.9.0"
|
||||||
@@ -1643,7 +1677,7 @@
|
|||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
@@ -2032,7 +2066,7 @@
|
|||||||
"version": "8.17.0",
|
"version": "8.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -2105,7 +2139,7 @@
|
|||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2132,7 +2166,7 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2514,7 +2548,7 @@
|
|||||||
"version": "5.8.1",
|
"version": "5.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
|
||||||
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
|
"integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
@@ -2824,7 +2858,7 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
@@ -2862,7 +2896,7 @@
|
|||||||
"version": "2.2.13",
|
"version": "2.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.13.tgz",
|
||||||
"integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==",
|
"integrity": "sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
@@ -3330,7 +3364,7 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.6"
|
"@types/estree": "^1.0.6"
|
||||||
@@ -3754,7 +3788,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
@@ -3808,7 +3842,7 @@
|
|||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
@@ -4470,7 +4504,7 @@
|
|||||||
"version": "5.56.4",
|
"version": "5.56.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz",
|
||||||
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
|
"integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
@@ -6281,7 +6315,7 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@surdeddd/wmkit": "^0.3.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"dompurify": "^3.4.11",
|
"dompurify": "^3.4.11",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Knowledge from './pages/Knowledge.svelte'
|
import Knowledge from './pages/Knowledge.svelte'
|
||||||
import Learning from './pages/Learning.svelte'
|
import Learning from './pages/Learning.svelte'
|
||||||
import Config from './pages/Config.svelte'
|
import Config from './pages/Config.svelte'
|
||||||
|
import EntityDesktop from '$lib/components/EntityDesktop.svelte'
|
||||||
import { newChat } from '$lib/stores/chat'
|
import { newChat } from '$lib/stores/chat'
|
||||||
import { summary, subscribeContext, openSignalCount } from '$lib/stores/context'
|
import { summary, subscribeContext, openSignalCount } from '$lib/stores/context'
|
||||||
import { currentTask } from '$lib/stores/workspace'
|
import { currentTask } from '$lib/stores/workspace'
|
||||||
@@ -249,4 +250,6 @@
|
|||||||
</Sheet.Content>
|
</Sheet.Content>
|
||||||
</Sheet.Root>
|
</Sheet.Root>
|
||||||
|
|
||||||
|
<EntityDesktop />
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -242,6 +242,45 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* wmkit floating windows (EntityDesktop.svelte) — mapped onto the app's own
|
||||||
|
card/border/ring tokens instead of an imported wmkit theme, so windows
|
||||||
|
follow the terracotta/dark theme toggle for free. */
|
||||||
|
[data-wm-desktop] {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-wm-window] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--card-foreground);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: 0 8px 24px oklch(0 0 0 / 0.18);
|
||||||
|
overflow: hidden;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-wm-window][data-wm-focused] {
|
||||||
|
border-color: var(--ring);
|
||||||
|
box-shadow: 0 12px 32px oklch(0 0 0 / 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-wm-window][data-wm-dragging],
|
||||||
|
[data-wm-window][data-wm-resizing] {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-wm-window][data-wm-stage='minimized'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-wm-resize] {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent-blue);
|
color: var(--accent-blue);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collapsible.Root bind:open class="rounded-md border bg-card">
|
<Collapsible.Root bind:open class="rounded-md border bg-card">
|
||||||
<Collapsible.Trigger class="flex w-full cursor-pointer select-none items-center justify-between gap-2 px-2.5 py-1.5 text-left hover:bg-muted/50">
|
<Collapsible.Trigger class="flex w-full cursor-pointer select-none items-center justify-between gap-2 px-2 py-1 text-left hover:bg-muted/50">
|
||||||
<span class="text-xs font-medium">{title}{count !== undefined ? ` (${count})` : ''}</span>
|
<span class="text-xs font-medium">{title}{count !== undefined ? ` (${count})` : ''}</span>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open ? 'rotate-180' : ''}"
|
class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 {open ? 'rotate-180' : ''}"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
/>
|
/>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Content class="overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:animate-in data-[state=open]:fade-in">
|
<Collapsible.Content class="overflow-hidden data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:animate-in data-[state=open]:fade-in">
|
||||||
<div class="border-t px-2.5 py-2">
|
<div class="border-t px-2 py-1.5">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
|
|||||||
46
web/src/lib/components/EntityDesktop.svelte
Normal file
46
web/src/lib/components/EntityDesktop.svelte
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// Global floating-window layer — mounted once in App.svelte, above every
|
||||||
|
// page, so an entity opened from Knowledge Base, chat, or anywhere else
|
||||||
|
// lands in the same window stack instead of each page owning its own
|
||||||
|
// single-entity sidebar/sheet. See $lib/stores/windows.ts.
|
||||||
|
import { dk, wmState, openEntityWindow } from '$lib/stores/windows'
|
||||||
|
import EntityDetailContent from './EntityDetailContent.svelte'
|
||||||
|
import XIcon from '@lucide/svelte/icons/x'
|
||||||
|
import MinusIcon from '@lucide/svelte/icons/minus'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div use:dk.desktop class="pointer-events-none fixed inset-0 z-40">
|
||||||
|
{#each $wmState.order as id (id)}
|
||||||
|
{@const win = $wmState.windows[id]}
|
||||||
|
{#if win}
|
||||||
|
<section use:dk.window={{ id }} class="min-w-0" aria-label={win.title}>
|
||||||
|
<header data-wm-drag class="flex shrink-0 cursor-move items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5">
|
||||||
|
<span data-wm-title class="min-w-0 flex-1 truncate font-mono text-xs font-medium">{win.title}</span>
|
||||||
|
<div class="flex shrink-0 items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-wm-minimize
|
||||||
|
class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
aria-label="Minimize {win.title}"
|
||||||
|
>
|
||||||
|
<MinusIcon class="size-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-wm-close
|
||||||
|
class="rounded p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
aria-label="Close {win.title}"
|
||||||
|
>
|
||||||
|
<XIcon class="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div data-wm-content class="min-h-0 flex-1 overflow-hidden">
|
||||||
|
{#key id}
|
||||||
|
<EntityDetailContent slug={id} onSelectEntity={openEntityWindow} />
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col gap-2 overflow-y-auto p-3 md:p-4">
|
<div class="flex h-full flex-col gap-1.5 overflow-y-auto p-2">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<Skeleton class="h-6 w-48" />
|
<Skeleton class="h-6 w-48" />
|
||||||
<Skeleton class="h-8 w-full" />
|
<Skeleton class="h-8 w-full" />
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import EntityDetailContent from '$lib/components/EntityDetailContent.svelte'
|
|
||||||
import * as Sheet from '$lib/components/ui/sheet'
|
|
||||||
|
|
||||||
let { slug, open = $bindable(false) }: { slug: string | null; open?: boolean } = $props()
|
|
||||||
|
|
||||||
// Lets a relation click inside the sheet drill into that entity in place,
|
|
||||||
// without closing/reopening. Resets to the externally-requested slug
|
|
||||||
// whenever the caller opens the sheet on a different entity.
|
|
||||||
let currentSlug = $state<string | null>(null)
|
|
||||||
$effect(() => {
|
|
||||||
currentSlug = slug
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Sheet.Root bind:open>
|
|
||||||
<Sheet.Content side="right" class="w-full p-0 sm:max-w-2xl">
|
|
||||||
<Sheet.Header class="sr-only">
|
|
||||||
<Sheet.Title>{currentSlug ?? 'Entity detail'}</Sheet.Title>
|
|
||||||
<Sheet.Description>Entity detail panel</Sheet.Description>
|
|
||||||
</Sheet.Header>
|
|
||||||
{#if currentSlug}
|
|
||||||
{#key currentSlug}
|
|
||||||
<EntityDetailContent slug={currentSlug} onSelectEntity={(s) => (currentSlug = s)} />
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
</Sheet.Content>
|
|
||||||
</Sheet.Root>
|
|
||||||
@@ -13,12 +13,7 @@
|
|||||||
import { fetchGraph, type Entity } from '$lib/api'
|
import { fetchGraph, type Entity } from '$lib/api'
|
||||||
import { messages } from '$lib/stores/chat'
|
import { messages } from '$lib/stores/chat'
|
||||||
import { touched, healthDiffs } from '$lib/stores/workspace'
|
import { touched, healthDiffs } from '$lib/stores/workspace'
|
||||||
import { relativeTime } from '$lib/utils'
|
import { openEntityWindow, wmState } from '$lib/stores/windows'
|
||||||
import { Badge } from '$lib/components/ui/badge'
|
|
||||||
import { Button } from '$lib/components/ui/button'
|
|
||||||
import EntitySheet from '$lib/components/EntitySheet.svelte'
|
|
||||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
|
|
||||||
import XIcon from '@lucide/svelte/icons/x'
|
|
||||||
|
|
||||||
interface Node extends Entity {
|
interface Node extends Entity {
|
||||||
x?: number
|
x?: number
|
||||||
@@ -46,8 +41,6 @@
|
|||||||
let nodes = $state<Node[]>([])
|
let nodes = $state<Node[]>([])
|
||||||
let links = $state<Edge[]>([])
|
let links = $state<Edge[]>([])
|
||||||
let selected = $state<Node | null>(null)
|
let selected = $state<Node | null>(null)
|
||||||
let sheetSlug = $state<string | null>(null)
|
|
||||||
let sheetOpen = $state(false)
|
|
||||||
|
|
||||||
let sim: Simulation<Node, Edge> | null = null
|
let sim: Simulation<Node, Edge> | null = null
|
||||||
|
|
||||||
@@ -66,14 +59,6 @@
|
|||||||
let cw = $state(300)
|
let cw = $state(300)
|
||||||
let ch = $state(300)
|
let ch = $state(300)
|
||||||
|
|
||||||
// This component sits INSIDE one of TaskContextPanel's own resizable slots
|
|
||||||
// (Scope), so — unlike a top-level section — its total budget can change at
|
|
||||||
// any time from outside (dragging the outer Scope/Plan handle), including
|
|
||||||
// while the detail panel below is open. asideHeight tracks that live budget
|
|
||||||
// so detailHeight can self-clamp to it instead of trusting a one-time seed.
|
|
||||||
let asideEl = $state<HTMLElement | null>(null)
|
|
||||||
let asideHeight = $state(300)
|
|
||||||
|
|
||||||
function collectSlugs(value: unknown, out: Set<string>) {
|
function collectSlugs(value: unknown, out: Set<string>) {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const m = value.match(SLUG_RE)
|
const m = value.match(SLUG_RE)
|
||||||
@@ -217,13 +202,12 @@
|
|||||||
return () => ro.disconnect()
|
return () => ro.disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The highlight ring/dim styling below is tied to the node whose window
|
||||||
|
// was last opened — once that window is closed (from EntityDesktop, not
|
||||||
|
// necessarily from here), the ring should go with it rather than pointing
|
||||||
|
// at a window that no longer exists.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!asideEl) return
|
if (selected && !$wmState.windows[selected.slug]) selected = null
|
||||||
const ro = new ResizeObserver((entries) => {
|
|
||||||
asideHeight = Math.max(entries[0].contentRect.height, 1)
|
|
||||||
})
|
|
||||||
ro.observe(asideEl)
|
|
||||||
return () => ro.disconnect()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => sim?.stop())
|
onDestroy(() => sim?.stop())
|
||||||
@@ -267,52 +251,11 @@
|
|||||||
return typeof end === 'object' ? end.slug : end
|
return typeof end === 'object' ? end.slug : end
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── graph / detail resize ───────────────────────────────────────────
|
|
||||||
// Same drag handle, same feel as TaskContextPanel's Scope/Plan/Activity
|
|
||||||
// split — but the graph side stays flex-1 (always auto-fills whatever's
|
|
||||||
// left) rather than tracking its own pixel number. Only detailHeight is
|
|
||||||
// explicit, and it's continuously clamped against asideHeight (this
|
|
||||||
// component's actual live budget) rather than a value seeded once — so
|
|
||||||
// resizing the OUTER Scope section while the detail panel is open can't
|
|
||||||
// push this panel past its container the way a one-time seed could.
|
|
||||||
const MIN_GRAPH = 80
|
|
||||||
const MIN_DETAIL = 80
|
|
||||||
const HANDLE = 6
|
|
||||||
|
|
||||||
let detailHeight = $state(200)
|
|
||||||
let resizing = $state(false)
|
|
||||||
let resizeStartY = $state(0)
|
|
||||||
let resizeStartH = $state(0)
|
|
||||||
|
|
||||||
function maxDetailHeight(): number {
|
|
||||||
return Math.max(MIN_DETAIL, asideHeight - MIN_GRAPH - HANDLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const max = maxDetailHeight()
|
|
||||||
if (detailHeight > max) detailHeight = max
|
|
||||||
})
|
|
||||||
|
|
||||||
function onPointerDown(e: PointerEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
resizing = true
|
|
||||||
resizeStartY = e.clientY
|
|
||||||
resizeStartH = detailHeight
|
|
||||||
window.addEventListener('pointermove', onPointerMove)
|
|
||||||
window.addEventListener('pointerup', onPointerUp)
|
|
||||||
}
|
|
||||||
function onPointerMove(e: PointerEvent) {
|
|
||||||
if (!resizing) return
|
|
||||||
const dy = e.clientY - resizeStartY
|
|
||||||
detailHeight = Math.min(maxDetailHeight(), Math.max(MIN_DETAIL, resizeStartH - dy))
|
|
||||||
}
|
|
||||||
function onPointerUp() {
|
|
||||||
resizing = false
|
|
||||||
window.removeEventListener('pointermove', onPointerMove)
|
|
||||||
window.removeEventListener('pointerup', onPointerUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── drag / select ───────────────────────────────────────────────────
|
// ─── drag / select ───────────────────────────────────────────────────
|
||||||
|
// A click (pointerdown+up with no movement in between) opens the entity
|
||||||
|
// straight in its own floating window (EntityDesktop) instead of a
|
||||||
|
// click-through mini-panel — `selected` now only drives the highlight/dim
|
||||||
|
// styling below, so you can see at a glance which node you last opened.
|
||||||
let dragState: { node: Node; moved: boolean } | null = null
|
let dragState: { node: Node; moved: boolean } | null = null
|
||||||
|
|
||||||
function toLocal(clientX: number, clientY: number) {
|
function toLocal(clientX: number, clientY: number) {
|
||||||
@@ -334,6 +277,10 @@
|
|||||||
dragState.moved = true
|
dragState.moved = true
|
||||||
nodes = [...nodes]
|
nodes = [...nodes]
|
||||||
}
|
}
|
||||||
|
function selectAndOpen(node: Node) {
|
||||||
|
selected = node
|
||||||
|
openEntityWindow(node.slug)
|
||||||
|
}
|
||||||
function onUp() {
|
function onUp() {
|
||||||
if (!dragState) return
|
if (!dragState) return
|
||||||
const { node, moved } = dragState
|
const { node, moved } = dragState
|
||||||
@@ -341,15 +288,7 @@
|
|||||||
node.fy = null
|
node.fy = null
|
||||||
sim?.alphaTarget(0)
|
sim?.alphaTarget(0)
|
||||||
dragState = null
|
dragState = null
|
||||||
if (!moved) {
|
if (!moved) selectAndOpen(node)
|
||||||
const wasNull = selected === null
|
|
||||||
const next = selected?.slug === node.slug ? null : node
|
|
||||||
// A reasonable starting size on first open — the clamp effect above
|
|
||||||
// keeps it honest against the live container size from here on, so
|
|
||||||
// this doesn't need to be exact.
|
|
||||||
if (next && wasNull) detailHeight = Math.min(maxDetailHeight(), Math.round(ch * 0.45))
|
|
||||||
selected = next
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedRelations = $derived(
|
const selectedRelations = $derived(
|
||||||
@@ -362,15 +301,9 @@
|
|||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
|
|
||||||
function openFull() {
|
|
||||||
if (!selected) return
|
|
||||||
sheetSlug = selected.slug
|
|
||||||
sheetOpen = true
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside bind:this={asideEl} class="flex h-full min-h-0 flex-col bg-card/40">
|
<aside class="flex h-full min-h-0 flex-col bg-card/40">
|
||||||
{#if nowTouching}
|
{#if nowTouching}
|
||||||
<div class="flex shrink-0 items-center gap-1.5 border-b bg-primary/5 px-3 py-1.5 text-[11px] text-primary">
|
<div class="flex shrink-0 items-center gap-1.5 border-b bg-primary/5 px-3 py-1.5 text-[11px] text-primary">
|
||||||
<span class="size-1.5 animate-pulse rounded-full bg-primary"></span>
|
<span class="size-1.5 animate-pulse rounded-full bg-primary"></span>
|
||||||
@@ -455,7 +388,7 @@
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onpointerdown={(e) => onNodeDown(e, node)}
|
onpointerdown={(e) => onNodeDown(e, node)}
|
||||||
onkeydown={(e) => e.key === 'Enter' && (selected = node)}
|
onkeydown={(e) => e.key === 'Enter' && selectAndOpen(node)}
|
||||||
>
|
>
|
||||||
{#if isSel}
|
{#if isSel}
|
||||||
<circle r={r + 5} fill={nodeColor(node)} opacity="0.25" />
|
<circle r={r + 5} fill={nodeColor(node)} opacity="0.25" />
|
||||||
@@ -500,72 +433,4 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selected}
|
|
||||||
<!-- resize handle -->
|
|
||||||
<div
|
|
||||||
class="h-1.5 shrink-0 cursor-row-resize border-b hover:bg-primary/30 touch-none"
|
|
||||||
onpointerdown={onPointerDown}
|
|
||||||
role="separator"
|
|
||||||
aria-orientation="horizontal"
|
|
||||||
></div>
|
|
||||||
<div class="shrink-0 space-y-3 overflow-y-auto p-3 text-xs" style="height: {detailHeight}px">
|
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
|
||||||
<span class="min-w-0 flex-1 truncate font-mono text-sm font-semibold">{selected.slug}</span>
|
|
||||||
<Badge variant="outline">{selected.type}</Badge>
|
|
||||||
{#if selected.state}<Badge variant="secondary">{selected.state}</Badge>{/if}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
||||||
onclick={() => (selected = null)}
|
|
||||||
aria-label="Close entity detail"
|
|
||||||
>
|
|
||||||
<XIcon class="size-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#if selected.health}
|
|
||||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
|
||||||
<span class="size-2 rounded-full" style="background: {nodeColor(selected)}"></span>
|
|
||||||
{selected.health} · checked {relativeTime(selected.last_check_at)}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selected.attributes && Object.keys(selected.attributes).length}
|
|
||||||
<div>
|
|
||||||
<p class="mb-1 font-medium text-muted-foreground">Attributes</p>
|
|
||||||
<dl class="flex flex-col gap-1">
|
|
||||||
{#each Object.entries(selected.attributes).slice(0, 6) as [key, value]}
|
|
||||||
<div class="flex items-start justify-between gap-3 border-b pb-1 last:border-0">
|
|
||||||
<dt class="shrink-0 font-mono text-muted-foreground">{key}</dt>
|
|
||||||
<dd class="min-w-0 flex-1 truncate text-right">{typeof value === 'object' ? JSON.stringify(value) : String(value)}</dd>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if selectedRelations.length}
|
|
||||||
<div>
|
|
||||||
<p class="mb-1 font-medium text-muted-foreground">Relations ({selectedRelations.length})</p>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
{#each selectedRelations as rel}
|
|
||||||
<div class="flex items-center gap-1 font-mono">
|
|
||||||
<span class="text-muted-foreground">{rel.dir} {rel.type} →</span>
|
|
||||||
<button type="button" class="truncate hover:underline" onclick={() => { const n = nodes.find((x) => x.slug === rel.other); if (n) selected = n }}>
|
|
||||||
{rel.other}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" class="w-full" onclick={openFull}>
|
|
||||||
<ExternalLinkIcon class="mr-1 size-3.5" />
|
|
||||||
Full detail
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<EntitySheet slug={sheetSlug} bind:open={sheetOpen} />
|
|
||||||
|
|||||||
24
web/src/lib/stores/windows.ts
Normal file
24
web/src/lib/stores/windows.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// One global wmkit window manager for the whole app (mounted once by
|
||||||
|
// EntityDesktop.svelte in App.svelte) — this is what lets an entity opened
|
||||||
|
// from Knowledge Base, chat, or anywhere else land in the same floating
|
||||||
|
// window layer, with several windows open side by side, rather than each
|
||||||
|
// page owning its own single-entity sidebar/sheet.
|
||||||
|
import { createManager, createDesktop, wmStore } from '@surdeddd/wmkit/svelte'
|
||||||
|
|
||||||
|
export const wm = createManager({ defaultSize: { width: 480, height: 560 } })
|
||||||
|
export const dk = createDesktop(wm)
|
||||||
|
export const wmState = wmStore(wm)
|
||||||
|
|
||||||
|
// Opens a window for the entity, or focuses (and restores, if minimized) the
|
||||||
|
// existing one — wm.open() throws if a window with this id already exists,
|
||||||
|
// and slugs make natural, stable window ids (also dedupes "same entity
|
||||||
|
// opened twice" into one window instead of stacking duplicates).
|
||||||
|
export function openEntityWindow(slug: string | null): void {
|
||||||
|
if (!slug) return
|
||||||
|
if (wm.get(slug)) {
|
||||||
|
wm.restore(slug)
|
||||||
|
wm.focus(slug)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wm.open({ id: slug, title: slug })
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import EntityDetailContent from '$lib/components/EntityDetailContent.svelte'
|
import EntityDetailContent from '$lib/components/EntityDetailContent.svelte'
|
||||||
|
import { openEntityWindow } from '$lib/stores/windows'
|
||||||
|
|
||||||
let { slug }: { slug: string } = $props()
|
let { slug }: { slug: string } = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<EntityDetailContent {slug} />
|
<EntityDetailContent {slug} onSelectEntity={openEntityWindow} />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { Input } from '$lib/components/ui/input'
|
import { Input } from '$lib/components/ui/input'
|
||||||
import { Button } from '$lib/components/ui/button'
|
import { Button } from '$lib/components/ui/button'
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
||||||
import EntitySheet from '$lib/components/EntitySheet.svelte'
|
import { openEntityWindow } from '$lib/stores/windows'
|
||||||
import SearchIcon from '@lucide/svelte/icons/search'
|
import SearchIcon from '@lucide/svelte/icons/search'
|
||||||
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
||||||
import BotIcon from '@lucide/svelte/icons/bot'
|
import BotIcon from '@lucide/svelte/icons/bot'
|
||||||
@@ -56,13 +56,6 @@
|
|||||||
return `${Math.floor(s / 86400)}d ago`
|
return `${Math.floor(s / 86400)}d ago`
|
||||||
}
|
}
|
||||||
|
|
||||||
let sheetOpen = $state(false)
|
|
||||||
let selectedSlug = $state<string | null>(null)
|
|
||||||
|
|
||||||
function openEntity(slug: string) {
|
|
||||||
selectedSlug = slug
|
|
||||||
sheetOpen = true
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||||
@@ -131,7 +124,7 @@
|
|||||||
{#if hit.linked_entities?.length}
|
{#if hit.linked_entities?.length}
|
||||||
<div class="mt-1 flex flex-wrap gap-1">
|
<div class="mt-1 flex flex-wrap gap-1">
|
||||||
{#each hit.linked_entities as slug}
|
{#each hit.linked_entities as slug}
|
||||||
<button type="button" class="font-mono text-xs text-muted-foreground underline" onclick={() => openEntity(slug)}>{slug}</button>
|
<button type="button" class="font-mono text-xs text-muted-foreground underline" onclick={() => openEntityWindow(slug)}>{slug}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -182,5 +175,3 @@
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntitySheet slug={selectedSlug} bind:open={sheetOpen} />
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
import { liveEvents, subscribeEvents } from '$lib/stores/events'
|
import { liveEvents, subscribeEvents } from '$lib/stores/events'
|
||||||
import EntityTable from '$lib/components/EntityTable.svelte'
|
import EntityTable from '$lib/components/EntityTable.svelte'
|
||||||
import EntityGraph, { type GraphInfo } from '$lib/components/EntityGraph.svelte'
|
import EntityGraph, { type GraphInfo } from '$lib/components/EntityGraph.svelte'
|
||||||
import EntityDetailContent from '$lib/components/EntityDetailContent.svelte'
|
|
||||||
import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte'
|
import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte'
|
||||||
import { categories, filtersForCategory, type Category } from '$lib/categories'
|
import { categories, filtersForCategory, type Category } from '$lib/categories'
|
||||||
|
import { openEntityWindow, wmState } from '$lib/stores/windows'
|
||||||
import { Button } from '$lib/components/ui/button'
|
import { Button } from '$lib/components/ui/button'
|
||||||
import { Input } from '$lib/components/ui/input'
|
import { Input } from '$lib/components/ui/input'
|
||||||
import * as Select from '$lib/components/ui/select'
|
import * as Select from '$lib/components/ui/select'
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
import NetworkIcon from '@lucide/svelte/icons/share-2'
|
import NetworkIcon from '@lucide/svelte/icons/share-2'
|
||||||
import TableIcon from '@lucide/svelte/icons/table-2'
|
import TableIcon from '@lucide/svelte/icons/table-2'
|
||||||
import LocateFixedIcon from '@lucide/svelte/icons/locate-fixed'
|
import LocateFixedIcon from '@lucide/svelte/icons/locate-fixed'
|
||||||
import XIcon from '@lucide/svelte/icons/x'
|
|
||||||
|
|
||||||
type View = 'graph' | 'table'
|
type View = 'graph' | 'table'
|
||||||
|
|
||||||
@@ -26,7 +25,10 @@
|
|||||||
|
|
||||||
let category = $state<Category>('fleet')
|
let category = $state<Category>('fleet')
|
||||||
let view = $state<View>(loadView())
|
let view = $state<View>(loadView())
|
||||||
let selectedSlug = $state<string | null>(null)
|
// Tracks only the most recently opened entity, for row/node highlight —
|
||||||
|
// actual detail viewing now happens in floating windows (EntityDesktop),
|
||||||
|
// which can have several entities open at once.
|
||||||
|
let lastOpened = $state<string | null>(null)
|
||||||
|
|
||||||
function setView(v: View) {
|
function setView(v: View) {
|
||||||
view = v
|
view = v
|
||||||
@@ -34,9 +36,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function select(slug: string | null) {
|
function select(slug: string | null) {
|
||||||
selectedSlug = slug
|
lastOpened = slug
|
||||||
|
openEntityWindow(slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop the highlight once its window is closed (from EntityDesktop, not
|
||||||
|
// necessarily from here) rather than leaving a row/node marked "open" when
|
||||||
|
// it isn't anymore.
|
||||||
|
$effect(() => {
|
||||||
|
if (lastOpened && !$wmState.windows[lastOpened]) lastOpened = null
|
||||||
|
})
|
||||||
|
|
||||||
// ─── table data: fetched here (not inside EntityTable) so the search/type
|
// ─── table data: fetched here (not inside EntityTable) so the search/type
|
||||||
// toolbar lives in the shared page toolbar instead of the resizable browse
|
// toolbar lives in the shared page toolbar instead of the resizable browse
|
||||||
// pane, where its width is at the mercy of the divider and it would
|
// pane, where its width is at the mercy of the divider and it would
|
||||||
@@ -206,36 +216,6 @@
|
|||||||
function relColorFor(type: string): string {
|
function relColorFor(type: string): string {
|
||||||
return graphInfo.relColors.get(type) ?? '#30363d'
|
return graphInfo.relColors.get(type) ?? '#30363d'
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── resizable browse/detail split (pattern from Chat.svelte) ─────────
|
|
||||||
const DETAIL_MIN = 320
|
|
||||||
const DETAIL_MAX = 900
|
|
||||||
function loadDetailWidth(): number {
|
|
||||||
if (typeof localStorage === 'undefined') return 420
|
|
||||||
const v = Number(localStorage.getItem('oikos-kb-detail-width'))
|
|
||||||
return v >= DETAIL_MIN && v <= DETAIL_MAX ? v : 420
|
|
||||||
}
|
|
||||||
let detailWidth = $state(loadDetailWidth())
|
|
||||||
let resizing = $state(false)
|
|
||||||
|
|
||||||
function startResize(e: PointerEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
resizing = true
|
|
||||||
;(e.currentTarget as Element).setPointerCapture(e.pointerId)
|
|
||||||
const startX = e.clientX
|
|
||||||
const startW = detailWidth
|
|
||||||
function move(ev: PointerEvent) {
|
|
||||||
detailWidth = Math.min(DETAIL_MAX, Math.max(DETAIL_MIN, startW + (startX - ev.clientX)))
|
|
||||||
}
|
|
||||||
function up() {
|
|
||||||
resizing = false
|
|
||||||
if (typeof localStorage !== 'undefined') localStorage.setItem('oikos-kb-detail-width', String(detailWidth))
|
|
||||||
window.removeEventListener('pointermove', move)
|
|
||||||
window.removeEventListener('pointerup', up)
|
|
||||||
}
|
|
||||||
window.addEventListener('pointermove', move)
|
|
||||||
window.addEventListener('pointerup', up)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col gap-3 p-4">
|
<div class="flex h-full flex-col gap-3 p-4">
|
||||||
@@ -311,13 +291,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- resizable browse | detail split -->
|
<!-- browse pane — selecting an entity opens it in a floating window
|
||||||
<div class="flex min-h-0 flex-1">
|
(EntityDesktop, mounted globally in App.svelte) instead of a sidebar. -->
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
{#if view === 'graph'}
|
{#if view === 'graph'}
|
||||||
<EntityGraph
|
<EntityGraph
|
||||||
{category}
|
{category}
|
||||||
{selectedSlug}
|
selectedSlug={lastOpened}
|
||||||
onSelect={select}
|
onSelect={select}
|
||||||
bind:root={graphRoot}
|
bind:root={graphRoot}
|
||||||
depth={graphDepth}
|
depth={graphDepth}
|
||||||
@@ -329,40 +309,7 @@
|
|||||||
bind:info={graphInfo}
|
bind:info={graphInfo}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<EntityTable entities={filteredEntities} loading={tableLoading} {selectedSlug} onSelect={select} {childToParent} />
|
<EntityTable entities={filteredEntities} loading={tableLoading} selectedSlug={lastOpened} onSelect={select} {childToParent} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="group/rz relative w-1.5 shrink-0 cursor-col-resize touch-none"
|
|
||||||
onpointerdown={startResize}
|
|
||||||
aria-label="Resize detail panel"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 transition-colors {resizing
|
|
||||||
? 'bg-primary/60'
|
|
||||||
: 'bg-border group-hover/rz:bg-primary/50'}"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex shrink-0 flex-col overflow-hidden rounded-lg border" style="width: {detailWidth}px">
|
|
||||||
{#if selectedSlug}
|
|
||||||
<div class="flex shrink-0 items-center justify-end border-b p-1">
|
|
||||||
<Button variant="ghost" size="icon" class="size-7" onclick={() => select(null)} aria-label="Close detail panel">
|
|
||||||
<XIcon class="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="min-h-0 flex-1">
|
|
||||||
{#key selectedSlug}
|
|
||||||
<EntityDetailContent slug={selectedSlug} onSelectEntity={select} />
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex h-full items-center justify-center p-6 text-center text-sm text-muted-foreground">
|
|
||||||
Select an entity to see its detail.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
web/src/vite-env.d.ts
vendored
2
web/src/vite-env.d.ts
vendored
@@ -1,2 +1,4 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare const __OIKOS_VERSION__: string
|
declare const __OIKOS_VERSION__: string
|
||||||
declare const __OIKOS_DEV_TOKEN__: string
|
declare const __OIKOS_DEV_TOKEN__: string
|
||||||
|
|||||||
Reference in New Issue
Block a user