From 76315ab75f74318e98e2f6898db375e68f2e4f26 Mon Sep 17 00:00:00 2001 From: Gallay Lajos Date: Tue, 10 Feb 2026 09:40:23 +0100 Subject: [PATCH 01/11] chore: Shades 12 Upgrade --- common/package.json | 8 +- frontend/package.json | 22 +- package.json | 12 +- service/package.json | 22 +- yarn.lock | 751 +++++++++++++++++++----------------------- 5 files changed, 373 insertions(+), 442 deletions(-) diff --git a/common/package.json b/common/package.json index c3979577..04501af1 100644 --- a/common/package.json +++ b/common/package.json @@ -25,13 +25,13 @@ "create-schemas": "node ./dist/bin/create-schemas.js" }, "devDependencies": { - "@types/node": "^25.2.0", - "ts-json-schema-generator": "^2.4.0", + "@types/node": "^25.2.2", + "ts-json-schema-generator": "^2.5.0", "vitest": "^4.0.18" }, "dependencies": { - "@furystack/core": "^15.0.34", - "@furystack/rest": "^8.0.34", + "@furystack/core": "^15.0.35", + "@furystack/rest": "^8.0.35", "ollama": "^0.6.3" } } diff --git a/frontend/package.json b/frontend/package.json index 98d34bec..4b532797 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,22 +12,22 @@ "license": "ISC", "devDependencies": { "@codecov/vite-plugin": "^1.9.1", - "@furystack/rest": "^8.0.34", + "@furystack/rest": "^8.0.35", "@types/marked": "^6.0.0", "typescript": "^5.9.3", "vite": "^7.3.1" }, "dependencies": { - "@furystack/cache": "^5.0.28", - "@furystack/core": "^15.0.34", - "@furystack/inject": "^12.0.28", - "@furystack/logging": "^8.0.28", - "@furystack/rest-client-fetch": "^8.0.34", - "@furystack/shades": "^11.1.0", - "@furystack/shades-common-components": "^11.0.0", - "@furystack/shades-lottie": "^7.0.36", + "@furystack/cache": "^5.0.29", + "@furystack/core": "^15.0.35", + "@furystack/inject": "^12.0.29", + "@furystack/logging": "^8.0.29", + "@furystack/rest-client-fetch": "^8.0.35", + "@furystack/shades": "^12.0.0", + "@furystack/shades-common-components": "^12.0.0", + "@furystack/shades-lottie": "^8.0.0", "@furystack/utils": "^8.1.9", - "@types/node": "^25.2.0", + "@types/node": "^25.2.2", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-search": "^0.16.0", "@xterm/addon-web-links": "^0.12.0", @@ -39,6 +39,6 @@ "ollama": "^0.6.3", "path-to-regexp": "^8.3.0", "semaphore-async-await": "^1.5.1", - "video.js": "8.23.6" + "video.js": "8.23.7" } } diff --git a/package.json b/package.json index e78f9f9c..ee77947a 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,16 @@ ] }, "devDependencies": { - "@eslint/js": "^9.39.2", + "@eslint/js": "^10.0.1", "@furystack/yarn-plugin-changelog": "^1.0.2", - "@playwright/test": "^1.58.1", + "@playwright/test": "^1.58.2", "@types/jsdom": "^27.0.0", - "@types/node": "^25.2.0", + "@types/node": "^25.2.2", "@vitest/coverage-v8": "^4.0.18", - "eslint": "^9.39.2", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "2.32.0", - "eslint-plugin-jsdoc": "^62.5.0", + "eslint-plugin-jsdoc": "^62.5.4", "eslint-plugin-playwright": "^2.5.1", "eslint-plugin-prettier": "^5.5.5", "husky": "^9.1.7", @@ -34,7 +34,7 @@ "prettier": "^3.8.1", "rimraf": "^6.1.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.54.0", + "typescript-eslint": "^8.55.0", "vite": "^7.3.1", "vitest": "^4.0.18" }, diff --git a/service/package.json b/service/package.json index 0aef1054..99a6a943 100644 --- a/service/package.json +++ b/service/package.json @@ -14,22 +14,22 @@ "devDependencies": { "@types/ffprobe": "^1.1.8", "@types/formidable": "^3.4.6", - "@types/node": "^25.2.0", + "@types/node": "^25.2.2", "@types/ping": "^0.4.4", "typescript": "^5.9.3" }, "dependencies": { - "@furystack/cache": "^5.0.28", - "@furystack/core": "^15.0.34", - "@furystack/inject": "^12.0.28", - "@furystack/logging": "^8.0.28", - "@furystack/repository": "^10.0.34", - "@furystack/rest": "^8.0.34", - "@furystack/rest-service": "^11.0.2", - "@furystack/security": "^6.0.34", - "@furystack/sequelize-store": "^6.0.35", + "@furystack/cache": "^5.0.29", + "@furystack/core": "^15.0.35", + "@furystack/inject": "^12.0.29", + "@furystack/logging": "^8.0.29", + "@furystack/repository": "^10.0.35", + "@furystack/rest": "^8.0.35", + "@furystack/rest-service": "^11.0.3", + "@furystack/security": "^6.0.35", + "@furystack/sequelize-store": "^6.0.36", "@furystack/utils": "^8.1.9", - "@furystack/websocket-api": "^13.1.6", + "@furystack/websocket-api": "^13.1.7", "chokidar": "^5.0.0", "common": "workspace:^", "formidable": "^3.5.4", diff --git a/yarn.lock b/yarn.lock index edacc633..d6c70f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -281,7 +281,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.5.5": +"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5": version: 7.28.6 resolution: "@babel/runtime@npm:7.28.6" checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d @@ -384,16 +384,16 @@ __metadata: languageName: node linkType: hard -"@es-joy/jsdoccomment@npm:~0.83.0": - version: 0.83.0 - resolution: "@es-joy/jsdoccomment@npm:0.83.0" +"@es-joy/jsdoccomment@npm:~0.84.0": + version: 0.84.0 + resolution: "@es-joy/jsdoccomment@npm:0.84.0" dependencies: "@types/estree": "npm:^1.0.8" - "@typescript-eslint/types": "npm:^8.53.1" + "@typescript-eslint/types": "npm:^8.54.0" comment-parser: "npm:1.4.5" esquery: "npm:^1.7.0" - jsdoc-type-pratt-parser: "npm:~7.1.0" - checksum: 10c0/55fae1cbceac0abe19d83ea2a6b4b3f864655878b990a1ee3c0efa398926ed473042dd9d7e723aaa926eef0b12d4f5b46b61a6f30b3e50542d4da3b2adb182ce + jsdoc-type-pratt-parser: "npm:~7.1.1" + checksum: 10c0/b5562c176dde36cd2956bb115b79229d2253b27d6d7e52820eb55c509f75a72048ae8ea8d57193b33be42728c1aa7a5ee20937b4967175291cb4ae60fdda318d languageName: node linkType: hard @@ -597,80 +597,68 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": +"@eslint-community/regexpp@npm:^4.12.2": version: 4.12.2 resolution: "@eslint-community/regexpp@npm:4.12.2" checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d languageName: node linkType: hard -"@eslint/config-array@npm:^0.21.1": - version: 0.21.1 - resolution: "@eslint/config-array@npm:0.21.1" +"@eslint/config-array@npm:^0.23.0": + version: 0.23.1 + resolution: "@eslint/config-array@npm:0.23.1" dependencies: - "@eslint/object-schema": "npm:^2.1.7" + "@eslint/object-schema": "npm:^3.0.1" debug: "npm:^4.3.1" - minimatch: "npm:^3.1.2" - checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c + minimatch: "npm:^10.1.1" + checksum: 10c0/9a676f3820b3c4dcea8053d07b22c8d8c2501c68d146d35a046e74f825de98deee3679b0cd980e0493a727c26efcb65cd508a96679402936c4ae86ab04a6c918 languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.4.2": - version: 0.4.2 - resolution: "@eslint/config-helpers@npm:0.4.2" +"@eslint/config-helpers@npm:^0.5.2": + version: 0.5.2 + resolution: "@eslint/config-helpers@npm:0.5.2" dependencies: - "@eslint/core": "npm:^0.17.0" - checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4 + "@eslint/core": "npm:^1.1.0" + checksum: 10c0/0dc65bc5dd80441afbf5007cae702a5d9dd08893e95fed702a463366cf9ce2f4fd90adb09f9012cb4fcc9783d897ccb739067b1b8a5942f4c8288a6efb396d58 languageName: node linkType: hard -"@eslint/core@npm:^0.17.0": - version: 0.17.0 - resolution: "@eslint/core@npm:0.17.0" +"@eslint/core@npm:^1.1.0": + version: 1.1.0 + resolution: "@eslint/core@npm:1.1.0" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e - languageName: node - linkType: hard - -"@eslint/eslintrc@npm:^3.3.1": - version: 3.3.3 - resolution: "@eslint/eslintrc@npm:3.3.3" - dependencies: - ajv: "npm:^6.12.4" - debug: "npm:^4.3.2" - espree: "npm:^10.0.1" - globals: "npm:^14.0.0" - ignore: "npm:^5.2.0" - import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.1" - minimatch: "npm:^3.1.2" - strip-json-comments: "npm:^3.1.1" - checksum: 10c0/532c7acc7ddd042724c28b1f020bd7bf148fcd4653bb44c8314168b5f772508c842ce4ee070299cac51c5c5757d2124bdcfcef5551c8c58ff9986e3e17f2260d + checksum: 10c0/0f875d6f24fbf67cc796e01c2ca82884f755488052ed84183e56377c5b90fe10b491a26e600642db4daea1d5d8ab7906ec12f2bd5cbdb5004b0ef73c802bdb57 languageName: node linkType: hard -"@eslint/js@npm:9.39.2, @eslint/js@npm:^9.39.2": - version: 9.39.2 - resolution: "@eslint/js@npm:9.39.2" - checksum: 10c0/00f51c52b04ac79faebfaa65a9652b2093b9c924e945479f1f3945473f78aee83cbc76c8d70bbffbf06f7024626575b16d97b66eab16182e1d0d39daff2f26f5 +"@eslint/js@npm:^10.0.1": + version: 10.0.1 + resolution: "@eslint/js@npm:10.0.1" + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + checksum: 10c0/9f3fcaf71ba7fdf65d82e8faad6ecfe97e11801cc3c362b306a88ea1ed1344ae0d35330dddb0e8ad18f010f6687a70b75491b9e01c8af57acd7987cee6b3ec6c languageName: node linkType: hard -"@eslint/object-schema@npm:^2.1.7": - version: 2.1.7 - resolution: "@eslint/object-schema@npm:2.1.7" - checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87 +"@eslint/object-schema@npm:^3.0.1": + version: 3.0.1 + resolution: "@eslint/object-schema@npm:3.0.1" + checksum: 10c0/96ddab8a2f5f1ae4203c8881b9c25a9177e27ca19cd609ea0c275e09d9a59ef0bbcb46e8ef59b887a9054933d96b23c70a98e652a77532273be9cce82f4e38e9 languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.4.1": - version: 0.4.1 - resolution: "@eslint/plugin-kit@npm:0.4.1" +"@eslint/plugin-kit@npm:^0.6.0": + version: 0.6.0 + resolution: "@eslint/plugin-kit@npm:0.6.0" dependencies: - "@eslint/core": "npm:^0.17.0" + "@eslint/core": "npm:^1.1.0" levn: "npm:^0.4.1" - checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b + checksum: 10c0/1d726338a9f4537fe2848796c44d801093ea3a99166dbc45bc6f7742fa2ad74ce0c2f114092ce4460710a9dfe5ea6e3500446f81842388bf81328c97c3a43d9d languageName: node linkType: hard @@ -693,151 +681,151 @@ __metadata: languageName: node linkType: hard -"@furystack/cache@npm:^5.0.28": - version: 5.0.28 - resolution: "@furystack/cache@npm:5.0.28" +"@furystack/cache@npm:^5.0.29": + version: 5.0.29 + resolution: "@furystack/cache@npm:5.0.29" dependencies: - "@furystack/inject": "npm:^12.0.28" + "@furystack/inject": "npm:^12.0.29" "@furystack/utils": "npm:^8.1.9" semaphore-async-await: "npm:^1.5.1" - checksum: 10c0/fe20ac07e62804e2bbca24d261fb14b5afdedd8c87b1a3ccb5628ceb9a06516c7a984051fd8e74b0037b22a652df08d6fa34f602de3fcb78989e68abecc2330a + checksum: 10c0/f633368ddae65d05eeb18ba31d87cd1f8ee761b9842c176f643868cec93a114accb0d71b4b3eaf7e27d65ff418fb2d7e0099e5d774c68bd94b1788f1897764a3 languageName: node linkType: hard -"@furystack/core@npm:^15.0.34": - version: 15.0.34 - resolution: "@furystack/core@npm:15.0.34" +"@furystack/core@npm:^15.0.35": + version: 15.0.35 + resolution: "@furystack/core@npm:15.0.35" dependencies: - "@furystack/inject": "npm:^12.0.28" + "@furystack/inject": "npm:^12.0.29" "@furystack/utils": "npm:^8.1.9" - checksum: 10c0/ac84e6f45f69e1467166d41d588f162240f4fbca932ea87f56187ef86cff3638ea733c117eeb5f5eed266cc7a0a31302d9ee58cfc91a5d02645c3e0c5df9382b + checksum: 10c0/1fcfbcebfe614bf1ec69a57a7257759df4a2e53cd4f7616af12a80c2e49c3648be070cdc161c977babd9b3e907b71400da70cc38a95bf83ef82ee64385810584 languageName: node linkType: hard -"@furystack/inject@npm:^12.0.28": - version: 12.0.28 - resolution: "@furystack/inject@npm:12.0.28" +"@furystack/inject@npm:^12.0.29": + version: 12.0.29 + resolution: "@furystack/inject@npm:12.0.29" dependencies: "@furystack/utils": "npm:^8.1.9" - checksum: 10c0/291c4d3350486c243f487e13dfdf1e1888bf1d22485553bba2a18b728865a651ed59011abe66e53d120d1a4e29f2abc42bee4e761955b51da2e50b0024957f60 + checksum: 10c0/ca1b37830c5b8d13f45bd539842ba61a196381113bc51d3532497ba3b095a441705fdd5b0a33b6371a4c5242611713cec89de682fe55c278888da2f60ff139a8 languageName: node linkType: hard -"@furystack/logging@npm:^8.0.28": - version: 8.0.28 - resolution: "@furystack/logging@npm:8.0.28" +"@furystack/logging@npm:^8.0.29": + version: 8.0.29 + resolution: "@furystack/logging@npm:8.0.29" dependencies: - "@furystack/inject": "npm:^12.0.28" - checksum: 10c0/51e7d8f287d609b9cc2580f676a2c6a60867d1e6ec6b16a90cf53085cddfb2aee576f8f726815299449bb12c62b86fb1cc9c4be9994a73960ee3663803463cf2 + "@furystack/inject": "npm:^12.0.29" + checksum: 10c0/eae7ddaaa27a99eb36c80b253397fb798581824a075cd1c5042d67c4ad7f4828387c563433e783da648de2023cc29382a74b08a1ffd07dc4a7f8534af2811100 languageName: node linkType: hard -"@furystack/repository@npm:^10.0.34": - version: 10.0.34 - resolution: "@furystack/repository@npm:10.0.34" +"@furystack/repository@npm:^10.0.35": + version: 10.0.35 + resolution: "@furystack/repository@npm:10.0.35" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" "@furystack/utils": "npm:^8.1.9" - checksum: 10c0/4cca1fd3a42c018c144213a0be062c518534caad73b3e56ff9e40ea7968420d0b457878780ecf81b5963a35cf9add53c5b582e629ccd20db40d917266ff69b36 + checksum: 10c0/ea26bfc69f52d67c7b9b06b9ee865f8fbb584687f3a694c541fd4b05d30348f87720bad5b02d4853f2fb33ebbbc78497ec231e7d6862151d28edc9e7ebd0d231 languageName: node linkType: hard -"@furystack/rest-client-fetch@npm:^8.0.34": - version: 8.0.34 - resolution: "@furystack/rest-client-fetch@npm:8.0.34" +"@furystack/rest-client-fetch@npm:^8.0.35": + version: 8.0.35 + resolution: "@furystack/rest-client-fetch@npm:8.0.35" dependencies: - "@furystack/rest": "npm:^8.0.34" + "@furystack/rest": "npm:^8.0.35" path-to-regexp: "npm:^8.3.0" - checksum: 10c0/a7e969d0284797371f604a2187718ba498df4d0ad1b6642f386a67219a2eeb32aef93c8db8a4f3cadb7ba4d7f09971e1b63bcdcb9fa04eee68e694ff6be83338 + checksum: 10c0/5abfd1a12a6004d063ac4d6f5b658b341ba69263bc6644138571f0450d292361144f37e68944f2b877c905595bd90791c0e9353765c6365ab23b77140e6cf7e2 languageName: node linkType: hard -"@furystack/rest-service@npm:^11.0.2": - version: 11.0.2 - resolution: "@furystack/rest-service@npm:11.0.2" +"@furystack/rest-service@npm:^11.0.3": + version: 11.0.3 + resolution: "@furystack/rest-service@npm:11.0.3" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - "@furystack/repository": "npm:^10.0.34" - "@furystack/rest": "npm:^8.0.34" - "@furystack/security": "npm:^6.0.34" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + "@furystack/repository": "npm:^10.0.35" + "@furystack/rest": "npm:^8.0.35" + "@furystack/security": "npm:^6.0.35" "@furystack/utils": "npm:^8.1.9" ajv: "npm:^8.17.1" ajv-formats: "npm:^3.0.1" path-to-regexp: "npm:^8.3.0" semaphore-async-await: "npm:^1.5.1" - checksum: 10c0/0be1475db9c609932e47a7e3436cd7a4921b7e5cb0e15714125be90527dc5366e1621f78af881b2d79619ae72d1bbfec14e8ecd10def862d2f42bb64f70a1626 + checksum: 10c0/511a198bf43bbac9eed975fb5c4b147c2112c52d1462bde8ea803837375e4a2453b54022c5151d130eb286d4dbd96f537b365b91dcfcb66911006cde15f0f66e languageName: node linkType: hard -"@furystack/rest@npm:^8.0.34": - version: 8.0.34 - resolution: "@furystack/rest@npm:8.0.34" +"@furystack/rest@npm:^8.0.35": + version: 8.0.35 + resolution: "@furystack/rest@npm:8.0.35" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - checksum: 10c0/e8bcbb290f089f307cec422ba2d2f5d38c776c95932e81272a086a8276927f46407deefa8e79f2287f4d696349d7691b3011aa3f1673df34771a9d768e96d4e1 + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + checksum: 10c0/60a439695821f39e3e28ac906502a5fe4eacaab7c4ee4523a6ca4434fbb78f578453a938b8eb5f5ae8f1dc7257705e4c915ed5b43c7d4287c72c47bc6cbd927a languageName: node linkType: hard -"@furystack/security@npm:^6.0.34": - version: 6.0.34 - resolution: "@furystack/security@npm:6.0.34" +"@furystack/security@npm:^6.0.35": + version: 6.0.35 + resolution: "@furystack/security@npm:6.0.35" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - checksum: 10c0/0dbe67f7582d5fdfbcf9956c79e122754c69a037f9ea807bd293847a9f5d02e138b2b8b657dee8fd005913334107147e154f499724bc849e4932a5b316d1616c + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + checksum: 10c0/6b93a42e90092d22935bd2f54e2c06f82c4c4e490dbb9212d6a140f2f86c6a906684ab308cd8dfb82a11dd91115f64f4e8b5ec7e7cc547b9b185d3b4629d3717 languageName: node linkType: hard -"@furystack/sequelize-store@npm:^6.0.35": - version: 6.0.35 - resolution: "@furystack/sequelize-store@npm:6.0.35" +"@furystack/sequelize-store@npm:^6.0.36": + version: 6.0.36 + resolution: "@furystack/sequelize-store@npm:6.0.36" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" "@furystack/utils": "npm:^8.1.9" semaphore-async-await: "npm:^1.5.1" sequelize: "npm:^6.37.7" - checksum: 10c0/ef05580f2bf6f53e07d2fc354d0cabecab8fddf4e3558042728810d7bb3d85483404c84d83a82d9540f8b1efca6401656364ad49836c1069991408e4634553d2 + checksum: 10c0/2827ee29453e3b63d626df052aebb7a6f6e11eb0d3adc1d9d3282d53418a513a5d92d64f054d98e39904229c1ce437cedb86c647871d1eb08994aaa894abe3dd languageName: node linkType: hard -"@furystack/shades-common-components@npm:^11.0.0": - version: 11.0.0 - resolution: "@furystack/shades-common-components@npm:11.0.0" +"@furystack/shades-common-components@npm:^12.0.0": + version: 12.0.0 + resolution: "@furystack/shades-common-components@npm:12.0.0" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - "@furystack/shades": "npm:^11.1.0" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + "@furystack/shades": "npm:^12.0.0" "@furystack/utils": "npm:^8.1.9" path-to-regexp: "npm:^8.3.0" semaphore-async-await: "npm:^1.5.1" - checksum: 10c0/b6c5338694e25c8db60314e1567609e458ebfcc94c758c5d9a93d38469bf25f60a999a7c73e66b385360943d4af41d987ce8e2ec920543f8b892ff3e50f0a95d + checksum: 10c0/30e8e8dd57c8b673f17e0b86f70ab3e74da11c45bec106c3446dd51e0e07ecce4316dd47f3bfd8036c810e80805f3557658c9f13bab407a56d5d39214673444c languageName: node linkType: hard -"@furystack/shades-lottie@npm:^7.0.36": - version: 7.0.36 - resolution: "@furystack/shades-lottie@npm:7.0.36" +"@furystack/shades-lottie@npm:^8.0.0": + version: 8.0.0 + resolution: "@furystack/shades-lottie@npm:8.0.0" dependencies: - "@furystack/shades": "npm:^11.1.0" + "@furystack/shades": "npm:^12.0.0" "@lottiefiles/lottie-player": "npm:^2.0.12" - checksum: 10c0/1d84f61034bacbf50b5dae219f0268c3514f5360eee11dba8db845b2961ae369e4ca5533853bb7da57d5e22c545a42abdc93c5983cf881ed988f4ce95751f21b + checksum: 10c0/8ad0206457085926691c83ee9513790169796341e68b5b4ff87be02a345370816a3964b3fd7dc70334234e626ed05cd5618ebf2f96f3a13197f263cc38fffa6e languageName: node linkType: hard -"@furystack/shades@npm:^11.1.0": - version: 11.1.0 - resolution: "@furystack/shades@npm:11.1.0" +"@furystack/shades@npm:^12.0.0": + version: 12.0.0 + resolution: "@furystack/shades@npm:12.0.0" dependencies: - "@furystack/inject": "npm:^12.0.28" - "@furystack/rest": "npm:^8.0.34" + "@furystack/inject": "npm:^12.0.29" + "@furystack/rest": "npm:^8.0.35" "@furystack/utils": "npm:^8.1.9" path-to-regexp: "npm:^8.3.0" semaphore-async-await: "npm:^1.5.1" - checksum: 10c0/300a288254c2c5e319a1bbe922fd0eb4606d13d45d428200fd88833fc4cbcdae757a6d3bf19dd41a72f5ab36b44a31897dc18dd4133fd0a06dc4becbd6660635 + checksum: 10c0/43c0643e36231852715be60da39cb6127cc6736728ee187985a8fb970c7ea27b1588edb96a93ae9106b3aac8655f9a2637f911744d27c46205e44ce9d8a7f6c4 languageName: node linkType: hard @@ -848,16 +836,16 @@ __metadata: languageName: node linkType: hard -"@furystack/websocket-api@npm:^13.1.6": - version: 13.1.6 - resolution: "@furystack/websocket-api@npm:13.1.6" +"@furystack/websocket-api@npm:^13.1.7": + version: 13.1.7 + resolution: "@furystack/websocket-api@npm:13.1.7" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - "@furystack/rest-service": "npm:^11.0.2" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + "@furystack/rest-service": "npm:^11.0.3" "@furystack/utils": "npm:^8.1.9" ws: "npm:^8.19.0" - checksum: 10c0/8f9c5e95b3c4641aac5038aed1ee3ebb73be6e34b8f3152aea7d6a686670c8756e789bf435fed2ec329da79d52eb782f4a38f1a92361c32a03364ec05594f98f + checksum: 10c0/39bf949fb27595206af32669af28921cdbdac68e8b29f5f7a77702be12d5cfa58b0889d3d589010efce9afda624eca9d0e6aeec1c902f6e8fb9f0632d0f8881d languageName: node linkType: hard @@ -1244,14 +1232,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.58.1": - version: 1.58.1 - resolution: "@playwright/test@npm:1.58.1" +"@playwright/test@npm:^1.58.2": + version: 1.58.2 + resolution: "@playwright/test@npm:1.58.2" dependencies: - playwright: "npm:1.58.1" + playwright: "npm:1.58.2" bin: playwright: cli.js - checksum: 10c0/ca32be812c6f86b2247109eaecd2fed452414debee05b4b0d690a3397f6bd08a56e0b2484f74d20fa0e7494508ee1cbdcbc27864acd5093e34c3f94d0e278188 + checksum: 10c0/2164c03ad97c3653ff02e8818a71f3b2bbc344ac07924c9d8e31cd57505d6d37596015a41f51396b3ed8de6840f59143eaa9c21bf65515963da20740119811da languageName: node linkType: hard @@ -1594,6 +1582,13 @@ __metadata: languageName: node linkType: hard +"@types/esrecurse@npm:^4.3.1": + version: 4.3.1 + resolution: "@types/esrecurse@npm:4.3.1" + checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03 + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -1674,7 +1669,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:^25.2.0": +"@types/node@npm:*": version: 25.2.0 resolution: "@types/node@npm:25.2.0" dependencies: @@ -1692,6 +1687,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^25.2.2": + version: 25.2.2 + resolution: "@types/node@npm:25.2.2" + dependencies: + undici-types: "npm:~7.16.0" + checksum: 10c0/45aa45b00df0aac4712c2d6e934a6ed21ac54e0284dd726df1c7620b8c7d36a4fb601b9f8fe1d2951298d1ee7618cf8275688e329c295eb36e8b8fa827a8e334 + languageName: node + linkType: hard + "@types/pako@npm:^1.0.1": version: 1.0.7 resolution: "@types/pako@npm:1.0.7" @@ -1757,105 +1761,112 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" +"@typescript-eslint/eslint-plugin@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.55.0" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.54.0" - "@typescript-eslint/type-utils": "npm:8.54.0" - "@typescript-eslint/utils": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" + "@typescript-eslint/scope-manager": "npm:8.55.0" + "@typescript-eslint/type-utils": "npm:8.55.0" + "@typescript-eslint/utils": "npm:8.55.0" + "@typescript-eslint/visitor-keys": "npm:8.55.0" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.4.0" peerDependencies: - "@typescript-eslint/parser": ^8.54.0 + "@typescript-eslint/parser": ^8.55.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/e533c8285880b883e02a833f378597c2776e6b0c20a5935440e2a02c1c42f40069a8badcf6d581bb4ec35a6856a806c4b66674c1c15c33cd64cc6b9c0cdd1dad + checksum: 10c0/e15973dfc822f6a455142433fa393ea2dd9fd4ba443e0d2fb68c6be7cd9a36e13412f061ccfe436a2c90fa070c4538bdd50985d374e85606c98800d372c17eb9 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/parser@npm:8.54.0" +"@typescript-eslint/parser@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/parser@npm:8.55.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.54.0" - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" + "@typescript-eslint/scope-manager": "npm:8.55.0" + "@typescript-eslint/types": "npm:8.55.0" + "@typescript-eslint/typescript-estree": "npm:8.55.0" + "@typescript-eslint/visitor-keys": "npm:8.55.0" debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/60a1cfe94bc23086f03701640f4d83d7e37b8f4d729011e0f029e5accf2b3d099c50938c0a798a399e86046279432ff663f33102ba4338c4c82f7acead2bcbac + checksum: 10c0/8b8f8caf64a43b98bff8e7bb99cd62d7c72daeee44e80e0a5f693dd376d9c898997e0b9fd5521604d1445bcb24552f54aed5cae022072f8c354a2baf2a452284 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/project-service@npm:8.54.0" +"@typescript-eslint/project-service@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/project-service@npm:8.55.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.54.0" - "@typescript-eslint/types": "npm:^8.54.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.55.0" + "@typescript-eslint/types": "npm:^8.55.0" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/3392ae259199021a80616a44d9484d1c363f61bc5c631dff2d08c6a906c98716a20caa7b832b8970120a1eb1eb2de3ee890cd527d6edb04f532f4e48a690a792 + checksum: 10c0/f35273a63635d2de84409f68dfcea901ed2cd3f08206abb825d742b929c8fce66e0a6a32524d87ce895a7c4c2549e4388baa08644c0a5244c9708151b0f62f52 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/scope-manager@npm:8.54.0" +"@typescript-eslint/scope-manager@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/scope-manager@npm:8.55.0" dependencies: - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" - checksum: 10c0/794740a5c0c1afc38d71e6bc59cc62870286e40d99f15e9760e76fb3d4197e961ee151c286c428535c404f5137721242a14da21350b749d0feb1f589f167814f + "@typescript-eslint/types": "npm:8.55.0" + "@typescript-eslint/visitor-keys": "npm:8.55.0" + checksum: 10c0/c42bd6b8e4936cac8bee3adbc2f707e3aee5f16af3dd18c1d095f4a1b881471b58de73abc0ad176db98654683a808946902e51d86efff39dc7610d29152c3078 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" +"@typescript-eslint/tsconfig-utils@npm:8.55.0, @typescript-eslint/tsconfig-utils@npm:^8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.55.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/e8598b0f051650c085d749002138d12249a3efd03e7de02e9e7913939dddd649d159b91f29ca3d28f5ee798b3f528a7195688e23c5e0b315d534e7af20a0c99a + checksum: 10c0/77b9a0d0b1d6ab0ce26c81394bb1aa969649016d2857e5f915a15b88012ac3dccec9fc5ff65535e1cc373434e1462513f7964e416a8d7a695f7277dcd39ec2af languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/type-utils@npm:8.54.0" +"@typescript-eslint/type-utils@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/type-utils@npm:8.55.0" dependencies: - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" - "@typescript-eslint/utils": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.55.0" + "@typescript-eslint/typescript-estree": "npm:8.55.0" + "@typescript-eslint/utils": "npm:8.55.0" debug: "npm:^4.4.3" ts-api-utils: "npm:^2.4.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/ad807800d8b2662f823505249a84a6f5b1246b192a7ff08c49f298e220e4d9bb3d76f1f0852510421e030161604a4b939bff87f11b9074f118a3bd1d26139c6f + checksum: 10c0/4987440d6e1ee2ae8024259796381612ab2fc81821ff93c45400f803726ea4894a25d07afa5f80cdf3081a189d99dc83a3a8dcd94ff9a4cab81461fe28ab9aef languageName: node linkType: hard -"@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.53.1, @typescript-eslint/types@npm:^8.54.0": +"@typescript-eslint/types@npm:8.55.0, @typescript-eslint/types@npm:^8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/types@npm:8.55.0" + checksum: 10c0/dc572f55966e2f0fee149e5d5e42a91cedcdeac451bff29704eb701f9336f123bbc7d7abcfbda717f9e1ef6b402fa24679908bc6032e67513287403037ef345f + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:^8.54.0": version: 8.54.0 resolution: "@typescript-eslint/types@npm:8.54.0" checksum: 10c0/2219594fe5e8931ff91fd1b7a2606d33cd4f093d43f9ca71bcaa37f106ef79ad51f830dea51392f7e3d8bca77f7077ef98733f87bc008fad2f0bbd9ea5fb8a40 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" +"@typescript-eslint/typescript-estree@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.55.0" dependencies: - "@typescript-eslint/project-service": "npm:8.54.0" - "@typescript-eslint/tsconfig-utils": "npm:8.54.0" - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/visitor-keys": "npm:8.54.0" + "@typescript-eslint/project-service": "npm:8.55.0" + "@typescript-eslint/tsconfig-utils": "npm:8.55.0" + "@typescript-eslint/types": "npm:8.55.0" + "@typescript-eslint/visitor-keys": "npm:8.55.0" debug: "npm:^4.4.3" minimatch: "npm:^9.0.5" semver: "npm:^7.7.3" @@ -1863,32 +1874,32 @@ __metadata: ts-api-utils: "npm:^2.4.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/1a1a7c0a318e71f3547ab5573198d36165ea152c50447ef92e6326303f9a5c397606201ba80c7b86a725dcdd2913e924be94466a0c33b1b0c3ee852059e646b6 + checksum: 10c0/2db3ff9489945ad04508b14009eb0f6b2b7c6c2469805327fa09ffa460af354cd181ff2e8153f9008bd60254efb54a004a59ccacbdbc9c963956e2c2c1189dbc languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/utils@npm:8.54.0" +"@typescript-eslint/utils@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/utils@npm:8.55.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.54.0" - "@typescript-eslint/types": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/scope-manager": "npm:8.55.0" + "@typescript-eslint/types": "npm:8.55.0" + "@typescript-eslint/typescript-estree": "npm:8.55.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/949a97dca8024d39666e04ecdf2d4e12722f5064c387901e72bdcc7adafb96cf650a070dc79f9dd46fa1aae6ac2b5eac5ae3fe5a6979385208c28809a1bd143f + checksum: 10c0/b57b86ac531e433c8057279805e6c903250460bc937cea46ec3b9284181a38f23b7c1ef092e8a1e37179432b39bd587c33db7f031b4243b1207ef37f23e4f24f languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.54.0": - version: 8.54.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" +"@typescript-eslint/visitor-keys@npm:8.55.0": + version: 8.55.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.55.0" dependencies: - "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.55.0" eslint-visitor-keys: "npm:^4.2.1" - checksum: 10c0/f83a9aa92f7f4d1fdb12cbca28c6f5704c36371264606b456388b2c869fc61e73c86d3736556e1bb6e253f3a607128b5b1bf6c68395800ca06f18705576faadd + checksum: 10c0/995c5ca91f7c7c1f3c4fdb4f98654abdff55efa570076b9b012da4cc203ebe7e2aee57ba83208ae51c2aef496c45cb8f6909560349131b779f31ce6f8758da23 languageName: node linkType: hard @@ -1910,6 +1921,24 @@ __metadata: languageName: node linkType: hard +"@videojs/http-streaming@npm:^3.17.3": + version: 3.17.4 + resolution: "@videojs/http-streaming@npm:3.17.4" + dependencies: + "@babel/runtime": "npm:^7.12.5" + "@videojs/vhs-utils": "npm:^4.1.1" + aes-decrypter: "npm:^4.0.2" + global: "npm:^4.4.0" + m3u8-parser: "npm:^7.2.0" + mpd-parser: "npm:^1.3.1" + mux.js: "npm:7.1.0" + video.js: "npm:^7 || ^8" + peerDependencies: + video.js: ^8.19.0 + checksum: 10c0/b975ff15ddcdf619f08e0676edcb7a06902029af9c3891902d1d8b813c56b6dd2a5da08a50cc6dea62c596c4d5a5d3e37b94d45f7afc729e7210310c5efe44a2 + languageName: node + linkType: hard + "@videojs/vhs-utils@npm:^4.0.0, @videojs/vhs-utils@npm:^4.1.1": version: 4.1.1 resolution: "@videojs/vhs-utils@npm:4.1.1" @@ -2877,13 +2906,6 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^2.0.1": - version: 2.0.1 - resolution: "argparse@npm:2.0.1" - checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e - languageName: node - linkType: hard - "array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" @@ -3233,13 +3255,6 @@ __metadata: languageName: node linkType: hard -"callsites@npm:^3.0.0": - version: 3.1.0 - resolution: "callsites@npm:3.1.0" - checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 - languageName: node - linkType: hard - "camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" @@ -3263,7 +3278,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": +"chalk@npm:4.1.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -3444,13 +3459,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^13.1.0": - version: 13.1.0 - resolution: "commander@npm:13.1.0" - checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164 - languageName: node - linkType: hard - "commander@npm:^14.0.2": version: 14.0.3 resolution: "commander@npm:14.0.3" @@ -3469,11 +3477,11 @@ __metadata: version: 0.0.0-use.local resolution: "common@workspace:common" dependencies: - "@furystack/core": "npm:^15.0.34" - "@furystack/rest": "npm:^8.0.34" - "@types/node": "npm:^25.2.0" + "@furystack/core": "npm:^15.0.35" + "@furystack/rest": "npm:^8.0.35" + "@types/node": "npm:^25.2.2" ollama: "npm:^0.6.3" - ts-json-schema-generator: "npm:^2.4.0" + ts-json-schema-generator: "npm:^2.5.0" vitest: "npm:^4.0.18" languageName: unknown linkType: soft @@ -4132,11 +4140,11 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:^62.5.0": - version: 62.5.0 - resolution: "eslint-plugin-jsdoc@npm:62.5.0" +"eslint-plugin-jsdoc@npm:^62.5.4": + version: 62.5.4 + resolution: "eslint-plugin-jsdoc@npm:62.5.4" dependencies: - "@es-joy/jsdoccomment": "npm:~0.83.0" + "@es-joy/jsdoccomment": "npm:~0.84.0" "@es-joy/resolve.exports": "npm:1.2.0" are-docs-informative: "npm:^0.0.2" comment-parser: "npm:1.4.5" @@ -4152,7 +4160,7 @@ __metadata: to-valid-identifier: "npm:^1.0.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10c0/840e62319bcd1cd24bbf25a709239881dc9a064c548a34e6933261429ff182b720898b45760c0e8e645b00e9597c73a68c7d89d2c7a714b72ac80714907f968d + checksum: 10c0/576a3dd2279c09ec579cbb944afed78029d5525a18dbef37097d510a0241c3792a597c440e80a299240772d49a6d0624bc90ffec32a72fca1e806e0554ac2119 languageName: node linkType: hard @@ -4187,13 +4195,15 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.4.0": - version: 8.4.0 - resolution: "eslint-scope@npm:8.4.0" +"eslint-scope@npm:^9.1.0": + version: 9.1.0 + resolution: "eslint-scope@npm:9.1.0" dependencies: + "@types/esrecurse": "npm:^4.3.1" + "@types/estree": "npm:^1.0.8" esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0 + checksum: 10c0/b503f739bb1d8da2e94b56b7655aaaa3af35e3180b93310523b11d326b90c4caf00ec0138a601c56f672a4da17958cf28d0c76806e448e5d35429754d2691040 languageName: node linkType: hard @@ -4218,31 +4228,28 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^9.39.2": - version: 9.39.2 - resolution: "eslint@npm:9.39.2" +"eslint@npm:^10.0.0": + version: 10.0.0 + resolution: "eslint@npm:10.0.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" - "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.21.1" - "@eslint/config-helpers": "npm:^0.4.2" - "@eslint/core": "npm:^0.17.0" - "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.39.2" - "@eslint/plugin-kit": "npm:^0.4.1" + "@eslint-community/regexpp": "npm:^4.12.2" + "@eslint/config-array": "npm:^0.23.0" + "@eslint/config-helpers": "npm:^0.5.2" + "@eslint/core": "npm:^1.1.0" + "@eslint/plugin-kit": "npm:^0.6.0" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" "@types/estree": "npm:^1.0.6" ajv: "npm:^6.12.4" - chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.4.0" - eslint-visitor-keys: "npm:^4.2.1" - espree: "npm:^10.4.0" - esquery: "npm:^1.5.0" + eslint-scope: "npm:^9.1.0" + eslint-visitor-keys: "npm:^5.0.0" + espree: "npm:^11.1.0" + esquery: "npm:^1.7.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" file-entry-cache: "npm:^8.0.0" @@ -4252,8 +4259,7 @@ __metadata: imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" - lodash.merge: "npm:^4.6.2" - minimatch: "npm:^3.1.2" + minimatch: "npm:^10.1.1" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" peerDependencies: @@ -4263,18 +4269,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/bb88ca8fd16bb7e1ac3e13804c54d41c583214460c0faa7b3e7c574e69c5600c7122295500fb4b0c06067831111db740931e98da1340329527658e1cf80073d3 - languageName: node - linkType: hard - -"espree@npm:^10.0.1, espree@npm:^10.4.0": - version: 10.4.0 - resolution: "espree@npm:10.4.0" - dependencies: - acorn: "npm:^8.15.0" - acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.2.1" - checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b + checksum: 10c0/87f3aa069693969841d773423c214ec83226873ead8565a65bdb40a7a0d3d5c95b8262c8232403eea235c5e1477457f893a3b6a72a0f4abc6bf2fee8f8410ef8 languageName: node linkType: hard @@ -4299,7 +4294,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.5.0, esquery@npm:^1.7.0": +"esquery@npm:^1.7.0": version: 1.7.0 resolution: "esquery@npm:1.7.0" dependencies: @@ -4498,7 +4493,7 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": +"foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" dependencies: @@ -4524,18 +4519,18 @@ __metadata: resolution: "frontend@workspace:frontend" dependencies: "@codecov/vite-plugin": "npm:^1.9.1" - "@furystack/cache": "npm:^5.0.28" - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - "@furystack/logging": "npm:^8.0.28" - "@furystack/rest": "npm:^8.0.34" - "@furystack/rest-client-fetch": "npm:^8.0.34" - "@furystack/shades": "npm:^11.1.0" - "@furystack/shades-common-components": "npm:^11.0.0" - "@furystack/shades-lottie": "npm:^7.0.36" + "@furystack/cache": "npm:^5.0.29" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + "@furystack/logging": "npm:^8.0.29" + "@furystack/rest": "npm:^8.0.35" + "@furystack/rest-client-fetch": "npm:^8.0.35" + "@furystack/shades": "npm:^12.0.0" + "@furystack/shades-common-components": "npm:^12.0.0" + "@furystack/shades-lottie": "npm:^8.0.0" "@furystack/utils": "npm:^8.1.9" "@types/marked": "npm:^6.0.0" - "@types/node": "npm:^25.2.0" + "@types/node": "npm:^25.2.2" "@xterm/addon-fit": "npm:^0.11.0" "@xterm/addon-search": "npm:^0.16.0" "@xterm/addon-web-links": "npm:^0.12.0" @@ -4548,7 +4543,7 @@ __metadata: path-to-regexp: "npm:^8.3.0" semaphore-async-await: "npm:^1.5.1" typescript: "npm:^5.9.3" - video.js: "npm:8.23.6" + video.js: "npm:8.23.7" vite: "npm:^7.3.1" languageName: unknown linkType: soft @@ -4792,22 +4787,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.0.1": - version: 11.1.0 - resolution: "glob@npm:11.1.0" - dependencies: - foreground-child: "npm:^3.3.1" - jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.1.1" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^2.0.0" - bin: - glob: dist/esm/bin.mjs - checksum: 10c0/1ceae07f23e316a6fa74581d9a74be6e8c2e590d2f7205034dd5c0435c53f5f7b712c2be00c3b65bf0a49294a1c6f4b98cd84c7637e29453b5aa13b79f1763a2 - languageName: node - linkType: hard - "glob@npm:^13.0.0": version: 13.0.0 resolution: "glob@npm:13.0.0" @@ -4843,13 +4822,6 @@ __metadata: languageName: node linkType: hard -"globals@npm:^14.0.0": - version: 14.0.0 - resolution: "globals@npm:14.0.0" - checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d - languageName: node - linkType: hard - "globals@npm:^16.4.0": version: 16.5.0 resolution: "globals@npm:16.5.0" @@ -5121,16 +5093,6 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.2.1": - version: 3.3.1 - resolution: "import-fresh@npm:3.3.1" - dependencies: - parent-module: "npm:^1.0.0" - resolve-from: "npm:^4.0.0" - checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -5610,15 +5572,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^4.1.1": - version: 4.1.1 - resolution: "jackspeak@npm:4.1.1" - dependencies: - "@isaacs/cliui": "npm:^8.0.2" - checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042 - languageName: node - linkType: hard - "js-tokens@npm:^10.0.0": version: 10.0.0 resolution: "js-tokens@npm:10.0.0" @@ -5645,21 +5598,10 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.1": - version: 4.1.1 - resolution: "js-yaml@npm:4.1.1" - dependencies: - argparse: "npm:^2.0.1" - bin: - js-yaml: bin/js-yaml.js - checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7 - languageName: node - linkType: hard - -"jsdoc-type-pratt-parser@npm:~7.1.0": - version: 7.1.0 - resolution: "jsdoc-type-pratt-parser@npm:7.1.0" - checksum: 10c0/440c40b465c0bc2611aa1187cc47778ec3caf47512184ba1d3491efa16fffdc180bb41ec43136b7faac9fe41c1fdd2ab17aa2422df7c656c006897ebfd9d448f +"jsdoc-type-pratt-parser@npm:~7.1.1": + version: 7.1.1 + resolution: "jsdoc-type-pratt-parser@npm:7.1.1" + checksum: 10c0/5a5216a75962b3a8a3a1e7e09a19b31b5a373c06c726a00b081480daee00196250d4acc8dfbecc0a7846d439a5bcf4a326df6348b879cf95f60c62ce5818dadb languageName: node linkType: hard @@ -5834,13 +5776,6 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": - version: 4.6.2 - resolution: "lodash.merge@npm:4.6.2" - checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506 - languageName: node - linkType: hard - "lodash@npm:^4.17.20, lodash@npm:^4.17.21": version: 4.17.23 resolution: "lodash@npm:4.17.23" @@ -6729,15 +6664,6 @@ __metadata: languageName: node linkType: hard -"parent-module@npm:^1.0.0": - version: 1.0.1 - resolution: "parent-module@npm:1.0.1" - dependencies: - callsites: "npm:^3.0.0" - checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 - languageName: node - linkType: hard - "parse-imports-exports@npm:^0.2.4": version: 0.2.4 resolution: "parse-imports-exports@npm:0.2.4" @@ -6870,16 +6796,16 @@ __metadata: version: 0.0.0-use.local resolution: "pi-rat@workspace:." dependencies: - "@eslint/js": "npm:^9.39.2" + "@eslint/js": "npm:^10.0.1" "@furystack/yarn-plugin-changelog": "npm:^1.0.2" - "@playwright/test": "npm:^1.58.1" + "@playwright/test": "npm:^1.58.2" "@types/jsdom": "npm:^27.0.0" - "@types/node": "npm:^25.2.0" + "@types/node": "npm:^25.2.2" "@vitest/coverage-v8": "npm:^4.0.18" - eslint: "npm:^9.39.2" + eslint: "npm:^10.0.0" eslint-config-prettier: "npm:^10.1.8" eslint-plugin-import: "npm:2.32.0" - eslint-plugin-jsdoc: "npm:^62.5.0" + eslint-plugin-jsdoc: "npm:^62.5.4" eslint-plugin-playwright: "npm:^2.5.1" eslint-plugin-prettier: "npm:^5.5.5" husky: "npm:^9.1.7" @@ -6888,7 +6814,7 @@ __metadata: prettier: "npm:^3.8.1" rimraf: "npm:^6.1.2" typescript: "npm:^5.9.3" - typescript-eslint: "npm:^8.54.0" + typescript-eslint: "npm:^8.55.0" vite: "npm:^7.3.1" vitest: "npm:^4.0.18" languageName: unknown @@ -6942,27 +6868,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.58.1": - version: 1.58.1 - resolution: "playwright-core@npm:1.58.1" +"playwright-core@npm:1.58.2": + version: 1.58.2 + resolution: "playwright-core@npm:1.58.2" bin: playwright-core: cli.js - checksum: 10c0/2c12755579148cbd13811cc1a01e9693432f0e4595c76ebb02d2e1b4ee7286719c6769fdb26cda61f218bc49b7ddd4de5d856abbd034acde4ff3dbeee93e4773 + checksum: 10c0/5aa15b2b764e6ffe738293a09081a6f7023847a0dbf4cd05fe10eed2e25450d321baf7482f938f2d2eb330291e197fa23e57b29a5b552b89927ceb791266225b languageName: node linkType: hard -"playwright@npm:1.58.1": - version: 1.58.1 - resolution: "playwright@npm:1.58.1" +"playwright@npm:1.58.2": + version: 1.58.2 + resolution: "playwright@npm:1.58.2" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.58.1" + playwright-core: "npm:1.58.2" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/29cb2b34ad80f9dc1b27d26d8cf56e0964d7787e0beb18b25fd9d087a09ce56a359779104d2a1717d08789c2f2713928ef59140b2905e6ef00b2cb6df58bb107 + checksum: 10c0/d060d9b7cc124bd8b5dffebaab5e84f6b34654a553758fe7b19cc598dfbee93f6ecfbdc1832b40a6380ae04eade86ef3285ba03aa0b136799e83402246dc0727 languageName: node linkType: hard @@ -7250,13 +7176,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^4.0.0": - version: 4.0.0 - resolution: "resolve-from@npm:4.0.0" - checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 - languageName: node - linkType: hard - "resolve@npm:^1.22.4": version: 1.22.11 resolution: "resolve@npm:1.22.11" @@ -7615,20 +7534,20 @@ __metadata: version: 0.0.0-use.local resolution: "service@workspace:service" dependencies: - "@furystack/cache": "npm:^5.0.28" - "@furystack/core": "npm:^15.0.34" - "@furystack/inject": "npm:^12.0.28" - "@furystack/logging": "npm:^8.0.28" - "@furystack/repository": "npm:^10.0.34" - "@furystack/rest": "npm:^8.0.34" - "@furystack/rest-service": "npm:^11.0.2" - "@furystack/security": "npm:^6.0.34" - "@furystack/sequelize-store": "npm:^6.0.35" + "@furystack/cache": "npm:^5.0.29" + "@furystack/core": "npm:^15.0.35" + "@furystack/inject": "npm:^12.0.29" + "@furystack/logging": "npm:^8.0.29" + "@furystack/repository": "npm:^10.0.35" + "@furystack/rest": "npm:^8.0.35" + "@furystack/rest-service": "npm:^11.0.3" + "@furystack/security": "npm:^6.0.35" + "@furystack/sequelize-store": "npm:^6.0.36" "@furystack/utils": "npm:^8.1.9" - "@furystack/websocket-api": "npm:^13.1.6" + "@furystack/websocket-api": "npm:^13.1.7" "@types/ffprobe": "npm:^1.1.8" "@types/formidable": "npm:^3.4.6" - "@types/node": "npm:^25.2.0" + "@types/node": "npm:^25.2.2" "@types/ping": "npm:^0.4.4" chokidar: "npm:^5.0.0" common: "workspace:^" @@ -8112,13 +8031,6 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:^3.1.1": - version: 3.1.1 - resolution: "strip-json-comments@npm:3.1.1" - checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd - languageName: node - linkType: hard - "strip-json-comments@npm:~2.0.1": version: 2.0.1 resolution: "strip-json-comments@npm:2.0.1" @@ -8346,21 +8258,20 @@ __metadata: languageName: node linkType: hard -"ts-json-schema-generator@npm:^2.4.0": - version: 2.4.0 - resolution: "ts-json-schema-generator@npm:2.4.0" +"ts-json-schema-generator@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-json-schema-generator@npm:2.5.0" dependencies: "@types/json-schema": "npm:^7.0.15" - commander: "npm:^13.1.0" - glob: "npm:^11.0.1" + commander: "npm:^14.0.2" json5: "npm:^2.2.3" normalize-path: "npm:^3.0.0" safe-stable-stringify: "npm:^2.5.0" tslib: "npm:^2.8.1" - typescript: "npm:^5.8.2" + typescript: "npm:^5.9.3" bin: ts-json-schema-generator: bin/ts-json-schema-generator.js - checksum: 10c0/b8dad83ab0a13bb938ed0b99fd0afc72dca1e35257d6609ce4c05dd08009e710b5ef4a062db0b1a82bcd43af1870df7f49229a419f414dff4e0e2541cadecc75 + checksum: 10c0/5b64b79980e59f3d524e4e93687c592eab10aba6488fb3a979f1d47f157bbec23b3fb83b65171aa76c7c91f6d03e729e16b7fcf61caec13d674db5422eb543bd languageName: node linkType: hard @@ -8500,22 +8411,22 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^8.54.0": - version: 8.54.0 - resolution: "typescript-eslint@npm:8.54.0" +"typescript-eslint@npm:^8.55.0": + version: 8.55.0 + resolution: "typescript-eslint@npm:8.55.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.54.0" - "@typescript-eslint/parser": "npm:8.54.0" - "@typescript-eslint/typescript-estree": "npm:8.54.0" - "@typescript-eslint/utils": "npm:8.54.0" + "@typescript-eslint/eslint-plugin": "npm:8.55.0" + "@typescript-eslint/parser": "npm:8.55.0" + "@typescript-eslint/typescript-estree": "npm:8.55.0" + "@typescript-eslint/utils": "npm:8.55.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/0ba92aa22c0aa10c88b0f4732950ed64245947f1c4ac17328dff94b43eaeddd3068595788725781fba07a87cc964304a075b3e37f9a86312173498fcc6ab4338 + checksum: 10c0/92e3e058a57bb29be7498093fd72f875e010170e1ca19214ae1bd1a1c9454354f71613ac9a6981f1e7e1d9e8b52df8888a1f42d0f2809dd5aeaf27f502787fda languageName: node linkType: hard -"typescript@npm:^5.8.2, typescript@npm:^5.9.3": +"typescript@npm:^5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" bin: @@ -8525,7 +8436,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.8.2#optional!builtin, typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: @@ -8680,7 +8591,27 @@ __metadata: languageName: node linkType: hard -"video.js@npm:8.23.6, video.js@npm:^7 || ^8": +"video.js@npm:8.23.7": + version: 8.23.7 + resolution: "video.js@npm:8.23.7" + dependencies: + "@babel/runtime": "npm:^7.28.4" + "@videojs/http-streaming": "npm:^3.17.3" + "@videojs/vhs-utils": "npm:^4.1.1" + "@videojs/xhr": "npm:2.7.0" + aes-decrypter: "npm:^4.0.2" + global: "npm:4.4.0" + m3u8-parser: "npm:^7.2.0" + mpd-parser: "npm:^1.3.1" + mux.js: "npm:^7.0.1" + videojs-contrib-quality-levels: "npm:4.1.0" + videojs-font: "npm:4.2.0" + videojs-vtt.js: "npm:0.15.5" + checksum: 10c0/28ccb630ff9562f547bd3cd3e3abfef1d4346e1040b5101d876770ff7e3ad8ce670ffcbf725444b9422b45944ce4749dda5e23a7b88fb6d1d735c6ab920c4e32 + languageName: node + linkType: hard + +"video.js@npm:^7 || ^8": version: 8.23.6 resolution: "video.js@npm:8.23.6" dependencies: From 759909f5cd816fcacff20baf4655241052d0f900 Mon Sep 17 00:00:00 2001 From: Gallay Lajos Date: Tue, 10 Feb 2026 10:43:56 +0100 Subject: [PATCH 02/11] migrations --- frontend/src/components/Separator.spec.tsx | 45 ----------- frontend/src/components/Separator.tsx | 12 --- frontend/src/components/body.tsx | 16 ++-- frontend/src/components/bubble-background.tsx | 60 ++++++++++----- frontend/src/components/context-menu.tsx | 62 +++++++-------- .../dashboard/device-availability.tsx | 21 ++---- .../components/dashboard/icon-url-widget.tsx | 12 +-- .../src/components/dashboard/movie-widget.tsx | 26 ++++--- .../components/dashboard/series-widget.tsx | 26 ++++--- .../src/components/dashboard/widget.spec.tsx | 10 ++- frontend/src/components/error-404.spec.tsx | 6 +- frontend/src/components/github-logo/index.tsx | 8 +- frontend/src/components/layout.tsx | 14 ++-- frontend/src/components/monaco-editor.tsx | 58 ++++++++------ .../related-movies-modal.tsx | 16 ++-- .../src/components/role-tag/index.spec.tsx | 16 +++- .../src/components/routes/admin-routes.tsx | 8 +- frontend/src/components/routes/ai-routes.tsx | 4 +- .../src/components/routes/auth-routes.tsx | 11 ++- .../src/components/routes/chat-routes.tsx | 4 +- .../components/routes/dashboard-routes.tsx | 5 +- .../src/components/routes/entity-routes.tsx | 46 ++++++------ .../components/routes/file-browser-routes.tsx | 5 +- frontend/src/components/routes/iot-routes.tsx | 5 +- .../src/components/routes/logging-routes.tsx | 13 ++-- .../src/components/routes/movie-routes.tsx | 14 ++-- .../src/components/routes/user-routes.tsx | 8 +- .../settings-menu-item.spec.tsx | 6 +- .../settings-sidebar/settings-menu-item.tsx | 6 +- .../components/theme-switch/index.spec.tsx | 9 ++- frontend/src/components/wizard-step.spec.tsx | 11 ++- frontend/src/components/wizard-step.tsx | 45 ++++++----- frontend/src/navigate-to-route.ts | 13 ++-- frontend/src/pages/admin/ai-settings.spec.tsx | 29 +++++-- frontend/src/pages/admin/app-settings.tsx | 29 +++---- .../src/pages/admin/iot-settings.spec.tsx | 33 ++++++-- frontend/src/pages/admin/omdb-settings.tsx | 16 ++-- .../src/pages/admin/user-details.spec.tsx | 75 ++++++++++++++----- frontend/src/pages/admin/user-list.spec.tsx | 48 ++++++++---- frontend/src/pages/ai/ai-chat-input.tsx | 11 ++- .../src/pages/ai/ai-chat-message-list.tsx | 16 ++-- .../src/pages/ai/create-ai-chat-button.tsx | 11 ++- frontend/src/pages/chat/add-chat-button.tsx | 13 ++-- frontend/src/pages/chat/invite-button.tsx | 13 ++-- frontend/src/pages/chat/message-input.tsx | 10 ++- frontend/src/pages/chat/message-list.tsx | 16 ++-- frontend/src/pages/entities/dashboards.tsx | 4 +- .../file-browser/create-drive-wizard.tsx | 11 ++- .../pages/file-browser/file-context-menu.tsx | 13 ++-- .../pages/file-browser/file-info-modal.tsx | 10 +-- .../pages/logging/log-entries-terminal.tsx | 36 ++++++--- frontend/src/pages/login.tsx | 36 +++++---- frontend/src/pages/movies/movie-overview.tsx | 6 +- .../movie-player-v2-component.tsx | 35 +++++---- frontend/src/pages/movies/series-overview.tsx | 6 +- frontend/src/pages/register.tsx | 52 ++++++++----- 56 files changed, 661 insertions(+), 489 deletions(-) delete mode 100644 frontend/src/components/Separator.spec.tsx delete mode 100644 frontend/src/components/Separator.tsx diff --git a/frontend/src/components/Separator.spec.tsx b/frontend/src/components/Separator.spec.tsx deleted file mode 100644 index 3aff0131..00000000 --- a/frontend/src/components/Separator.spec.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot } from '@furystack/shades' -import { usingAsync } from '@furystack/utils' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { Separator } from './Separator.js' - -describe('Separator', () => { - beforeEach(() => { - document.body.innerHTML = '
' - }) - - afterEach(() => { - document.body.innerHTML = '' - }) - - it('should render with correct shadow DOM name', async () => { - await usingAsync(new Injector(), async (injector) => { - const rootElement = document.getElementById('root') as HTMLDivElement - - initializeShadeRoot({ - injector, - rootElement, - jsxElement: , - }) - - const separator = rootElement.querySelector('shade-app-separator') - expect(separator).toBeTruthy() - }) - }) - - it('should render as empty component', async () => { - await usingAsync(new Injector(), async (injector) => { - const rootElement = document.getElementById('root') as HTMLDivElement - - initializeShadeRoot({ - injector, - rootElement, - jsxElement: , - }) - - const separator = rootElement.querySelector('shade-app-separator') - expect(separator?.textContent?.trim()).toBe('') - }) - }) -}) diff --git a/frontend/src/components/Separator.tsx b/frontend/src/components/Separator.tsx deleted file mode 100644 index ebc60c41..00000000 --- a/frontend/src/components/Separator.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Shade } from '@furystack/shades' - -export const Separator = Shade({ - shadowDomName: 'shade-app-separator', - css: { - display: 'block', - width: '100%', - borderBottom: '1px solid rgba(128,128,128,0.5)', - margin: '1em 0', - }, - render: () => null, -}) diff --git a/frontend/src/components/body.tsx b/frontend/src/components/body.tsx index 16145431..bfe68e00 100644 --- a/frontend/src/components/body.tsx +++ b/frontend/src/components/body.tsx @@ -1,4 +1,4 @@ -import { createComponent, Router, Shade } from '@furystack/shades' +import { createComponent, NestedRouter, Shade } from '@furystack/shades' import { cssVariableTheme } from '@furystack/shades-common-components' import { Init, Offline } from '../pages/index.js' import { SessionService } from '../services/session.js' @@ -14,7 +14,7 @@ import { loggingRoutes } from './routes/logging-routes.js' import { movieRoutes } from './routes/movie-routes.js' import { userRoutes } from './routes/user-routes.js' -export const Body = Shade<{ style?: Partial }>({ +export const Body = Shade({ shadowDomName: 'shade-app-body', css: { color: cssVariableTheme.text.secondary, @@ -29,23 +29,23 @@ export const Body = Shade<{ style?: Partial }>({ switch (sessionState) { case 'authenticated': return ( - ) case 'offline': return case 'unauthenticated': - return + return default: return } diff --git a/frontend/src/components/bubble-background.tsx b/frontend/src/components/bubble-background.tsx index 1e12c213..ea56c02d 100644 --- a/frontend/src/components/bubble-background.tsx +++ b/frontend/src/components/bubble-background.tsx @@ -20,8 +20,18 @@ const randomizeBlobVars = (el?: HTMLElement) => export const Blob = Shade({ shadowDomName: 'shade-bubbles-blob', - render: ({ element }) => { - randomizeBlobVars(element) + render: ({ useHostProps }) => { + const vars = { + '--x': `${randomInRange(-300, 300)}`, + '--y': `${randomInRange(-300, 300)}`, + '--scale': `${Math.random() + 0.5}`, + '--hue': `${randomInRange(64, 192)}`, + '--opacity': `${Math.random() * 0.1 + 0.05}`, + '--blur': `${randomInRange(1, 30)}px`, + '--blob-duration': `${randomInRange(0.2, 0.6)}s`, + '--blob-easing': `${Math.random() > 0.5 ? 'ease-out' : 'linear'}`, + } + useHostProps({ style: vars }) return (
const BlobGroup = Shade({ shadowDomName: 'shade-bubbles-blob-group', - render: ({ children, element }) => { - randomizeBlobGroupVars(element) + render: ({ children, useHostProps }) => { + const vars = { + '--x': `${randomInRange(0, 100)}`, + '--y': `${randomInRange(0, 100)}`, + '--scale': `${randomInRange(0.5, 1)}`, + '--duration': `${randomInRange(0.1, 0.3)}s`, + '--origin-x': `${randomInRange(-100, 100)}`, + '--origin-y': `${randomInRange(-100, 100)}`, + '--direction': `${Math.random() > 0.5 ? 'normal' : 'reverse'}`, + '--timing': `${ + Math.random() < 0.01 ? 'cubic-bezier(0.230, 1.000, 0.320, 1.000)' : Math.random() > 0.5 ? 'ease' : 'linear' + }`, + } + useHostProps({ style: vars }) return (
export const BubbleBackground = Shade({ shadowDomName: 'bubble-background', - constructed: ({ element }) => { - const randomizeHandler = () => { - element.querySelectorAll('shade-bubbles-blob-group').forEach((el) => randomizeBlobGroupVars(el as HTMLElement)) - // randomizeBlobVars(element) - element.querySelectorAll('shade-bubbles-blob').forEach((el) => randomizeBlobVars(el as HTMLElement)) - // randomizeBlobGroupVars(element) - } - document.addEventListener('mouseup', randomizeHandler) - return () => document.removeEventListener('mouseup', randomizeHandler) - }, - render: ({ children }) => { + render: ({ children, useDisposable, useRef }) => { + const containerRef = useRef('container') + + useDisposable('mouseupListener', () => { + const randomizeHandler = () => { + const container = containerRef.current + if (!container) return + container.querySelectorAll('shade-bubbles-blob-group').forEach((el) => randomizeBlobGroupVars(el as HTMLElement)) + container.querySelectorAll('shade-bubbles-blob').forEach((el) => randomizeBlobVars(el as HTMLElement)) + } + document.addEventListener('mouseup', randomizeHandler) + return { + [Symbol.dispose]: () => document.removeEventListener('mouseup', randomizeHandler), + } + }) return ( - <> +
{new Array(8).fill(0).map(() => ( {new Array(3).fill(0).map(() => ( @@ -106,7 +132,7 @@ export const BubbleBackground = Shade({ ))} {children} - +
) }, }) diff --git a/frontend/src/components/context-menu.tsx b/frontend/src/components/context-menu.tsx index 92c14a80..e9a3153e 100644 --- a/frontend/src/components/context-menu.tsx +++ b/frontend/src/components/context-menu.tsx @@ -1,6 +1,5 @@ import { createComponent, Shade } from '@furystack/shades' import { collapse, expand, Paper } from '@furystack/shades-common-components' -import { ObservableValue } from '@furystack/utils' import type { Icon as IconModel } from 'common' import { Icon } from './Icon.js' @@ -64,44 +63,28 @@ export const ContextMenu = Shade({ backdropFilter: 'blur(20px)', }, }, - constructed: ({ useDisposable }) => { - const isOpen = useDisposable('isOpen', () => new ObservableValue(false)) - - const listener = (_ev: MouseEvent) => { - isOpen.setValue(false) - } - - window.addEventListener('click', listener) - window.addEventListener('contextmenu', listener) - - return () => { - window.removeEventListener('click', listener) - window.removeEventListener('contextmenu', listener) - } - }, - render: ({ props, useDisposable, children, element }) => { + render: ({ props, useDisposable, children, useRef }) => { const { items } = props + const menuRef = useRef('menuItems') - const isOpen = useDisposable('isOpen', () => new ObservableValue(false)) - - isOpen.subscribe((value) => { - const menu = element.querySelector('.menuItems') as HTMLUListElement - try { - if (value) { - menu.getAnimations().forEach((a) => a.cancel()) - menu.style.display = 'block' - void expand(menu).then(() => { - menu.style.opacity = '1' - }) - } else { + useDisposable('windowListeners', () => { + const listener = () => { + const menu = menuRef.current + if (menu) { menu.getAnimations().forEach((a) => a.cancel()) void collapse(menu).then(() => { menu.style.display = 'none' menu.style.opacity = '0' }) } - } catch (error) { - /** in-progress animations will throw */ + } + window.addEventListener('click', listener) + window.addEventListener('contextmenu', listener) + return { + [Symbol.dispose]: () => { + window.removeEventListener('click', listener) + window.removeEventListener('contextmenu', listener) + }, } }) @@ -110,15 +93,20 @@ export const ContextMenu = Shade({ oncontextmenu={(ev) => { ev.preventDefault() setTimeout(() => { - const menu = element.querySelector('.menuItems') as HTMLUListElement - menu.style.display = 'block' - menu.style.top = `${ev.clientY}px` - menu.style.left = `${ev.clientX}px` - isOpen.setValue(true) + const menu = menuRef.current + if (menu) { + menu.getAnimations().forEach((a) => a.cancel()) + menu.style.display = 'block' + menu.style.top = `${ev.clientY}px` + menu.style.left = `${ev.clientX}px` + void expand(menu).then(() => { + menu.style.opacity = '1' + }) + } }) }} > - + {items.map((itemProps) => ( ))} diff --git a/frontend/src/components/dashboard/device-availability.tsx b/frontend/src/components/dashboard/device-availability.tsx index f323094d..c8f8e2a7 100644 --- a/frontend/src/components/dashboard/device-availability.tsx +++ b/frontend/src/components/dashboard/device-availability.tsx @@ -1,6 +1,6 @@ import { isFailedCacheResult, isLoadedCacheResult, isPendingCacheResult } from '@furystack/cache' import { serializeToQueryString } from '@furystack/rest' -import { LinkToRoute, Shade, createComponent } from '@furystack/shades' +import { NestedRouteLink, Shade, compileRoute, createComponent } from '@furystack/shades' import { Skeleton, promisifyAnimation } from '@furystack/shades-common-components' import type { DeviceAvailability as DeviceAvailabilityProps } from 'common' import { navigateToRoute } from '../../navigate-to-route.js' @@ -45,16 +45,8 @@ export const DeviceAvailability = Shade { - element.style.transform = 'scale(0)' - void promisifyAnimation(element, [{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { - fill: 'forwards', - delay: (props.index || 0) * 160 + Math.random() * 100, - duration: 700, - easing: 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', - }) - }, - render: ({ props, injector, useObservable }) => { + render: ({ props, injector, useObservable, useHostProps }) => { + useHostProps({ style: { transform: 'scale(0)' } }) const { size = 256 } = props const iotDevices = injector.getInstance(IotDevicesService) @@ -64,11 +56,10 @@ export const DeviceAvailability = Shade
- + ) } else if (isPendingCacheResult(device)) { return diff --git a/frontend/src/components/dashboard/icon-url-widget.tsx b/frontend/src/components/dashboard/icon-url-widget.tsx index bcf965e6..dd0585e5 100644 --- a/frontend/src/components/dashboard/icon-url-widget.tsx +++ b/frontend/src/components/dashboard/icon-url-widget.tsx @@ -1,4 +1,4 @@ -import { RouteLink, Shade, createComponent } from '@furystack/shades' +import { NestedRouteLink, Shade, createComponent } from '@furystack/shades' import { promisifyAnimation } from '@furystack/shades-common-components' const focus = (el: HTMLElement) => { @@ -88,9 +88,10 @@ export const IconUrlWidget = Shade({ textOverflow: 'ellipsis', }, }, - render: ({ props, element }) => { + render: ({ props, useRef }) => { + const cardRef = useRef('card') setTimeout(() => { - const el = element.querySelector('a div') + const el = cardRef.current if (el) { void promisifyAnimation(el, [{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { fill: 'forwards', @@ -102,8 +103,9 @@ export const IconUrlWidget = Shade({ }) return ( - +
focus(ev.target as HTMLElement)} onfocus={(ev) => focus(ev.target as HTMLElement)} @@ -120,7 +122,7 @@ export const IconUrlWidget = Shade({
{props.icon}
{props.name}
-
+ ) }, }) diff --git a/frontend/src/components/dashboard/movie-widget.tsx b/frontend/src/components/dashboard/movie-widget.tsx index c1a00327..5c453f4c 100644 --- a/frontend/src/components/dashboard/movie-widget.tsx +++ b/frontend/src/components/dashboard/movie-widget.tsx @@ -1,6 +1,6 @@ import { isFailedCacheResult, isLoadedCacheResult, isPendingCacheResult } from '@furystack/cache' import { serializeToQueryString } from '@furystack/rest' -import { LazyLoad, RouteLink, Shade, createComponent } from '@furystack/shades' +import { LazyLoad, NestedRouteLink, Shade, createComponent } from '@furystack/shades' import { Skeleton, promisifyAnimation } from '@furystack/shades-common-components' import { navigateToRoute } from '../../navigate-to-route.js' import { MovieFilesService } from '../../services/movie-files-service.js' @@ -46,17 +46,19 @@ export const MovieWidget = Shade<{ size?: number }>({ shadowDomName: 'pi-rat-movie-widget', - constructed: ({ props, element }) => { + render: ({ props, injector, useObservable, useRef }) => { + const cardRef = useRef('card') setTimeout(() => { - void promisifyAnimation(element.querySelector('a div'), [{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { - fill: 'forwards', - delay: (props.index || 0) * 160 + Math.random() * 100, - duration: 700, - easing: 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', - }) + const el = cardRef.current + if (el) { + void promisifyAnimation(el, [{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { + fill: 'forwards', + delay: (props.index || 0) * 160 + Math.random() * 100, + duration: 700, + easing: 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', + }) + } }, 1000) - }, - render: ({ props, injector, useObservable }) => { const { imdbId, size = 256 } = props const movieService = injector.getInstance(MoviesService) @@ -74,7 +76,7 @@ export const MovieWidget = Shade<{ if (isLoadedCacheResult(movie)) { return ( - +
focus(ev.target as HTMLElement)} onblur={(ev) => blur(ev.target as HTMLElement)} @@ -216,7 +218,7 @@ export const MovieWidget = Shade<{ />
-
+ ) } else if (isPendingCacheResult(movie)) { return diff --git a/frontend/src/components/dashboard/series-widget.tsx b/frontend/src/components/dashboard/series-widget.tsx index 30e8adda..4c6992c1 100644 --- a/frontend/src/components/dashboard/series-widget.tsx +++ b/frontend/src/components/dashboard/series-widget.tsx @@ -1,5 +1,5 @@ import { isFailedCacheResult, isLoadedCacheResult, isPendingCacheResult } from '@furystack/cache' -import { RouteLink, Shade, createComponent } from '@furystack/shades' +import { NestedRouteLink, Shade, createComponent } from '@furystack/shades' import { Skeleton, promisifyAnimation } from '@furystack/shades-common-components' import { SeriesService } from '../../services/series-service.js' @@ -39,17 +39,19 @@ export const SeriesWidget = Shade<{ size?: number }>({ shadowDomName: 'pi-rat-series-widget', - constructed: ({ props, element }) => { + render: ({ props, injector, useObservable, useRef }) => { + const cardRef = useRef('card') setTimeout(() => { - void promisifyAnimation(element.querySelector('a div'), [{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { - fill: 'forwards', - delay: (props.index || 0) * 160 + Math.random() * 100, - duration: 700, - easing: 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', - }) + const el = cardRef.current + if (el) { + void promisifyAnimation(el, [{ transform: 'scale(0)' }, { transform: 'scale(1)' }], { + fill: 'forwards', + delay: (props.index || 0) * 160 + Math.random() * 100, + duration: 700, + easing: 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', + }) + } }, 1000) - }, - render: ({ props, injector, useObservable }) => { const { imdbId, size = 256 } = props const seriesService = injector.getInstance(SeriesService) @@ -59,7 +61,7 @@ export const SeriesWidget = Shade<{ if (isLoadedCacheResult(series)) { return ( - +
focus(ev.target as HTMLElement)} onblur={(ev) => blur(ev.target as HTMLElement)} @@ -117,7 +119,7 @@ export const SeriesWidget = Shade<{ {series.value.title}
-
+ ) } else if (isPendingCacheResult(series)) { return diff --git a/frontend/src/components/dashboard/widget.spec.tsx b/frontend/src/components/dashboard/widget.spec.tsx index ae2d6974..e92be805 100644 --- a/frontend/src/components/dashboard/widget.spec.tsx +++ b/frontend/src/components/dashboard/widget.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot } from '@furystack/shades' +import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { usingAsync } from '@furystack/utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -30,6 +30,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -48,6 +49,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -66,6 +68,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -85,6 +88,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -103,6 +107,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -121,6 +126,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -139,6 +145,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() @@ -157,6 +164,7 @@ describe('Widget', () => { rootElement, jsxElement: , }) + await flushUpdates() const widget = document.querySelector('pi-rat-widget') expect(widget).toBeTruthy() diff --git a/frontend/src/components/error-404.spec.tsx b/frontend/src/components/error-404.spec.tsx index 64b03acb..bb7df7cf 100644 --- a/frontend/src/components/error-404.spec.tsx +++ b/frontend/src/components/error-404.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot, ScreenService } from '@furystack/shades' +import { ScreenService, createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { ThemeProviderService } from '@furystack/shades-common-components' import { ObservableValue, usingAsync } from '@furystack/utils' import { afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -46,6 +46,7 @@ describe('Error404', () => { rootElement, jsxElement: , }) + await flushUpdates() const error404 = rootElement.querySelector('shade-404-not-found') expect(error404).toBeTruthy() @@ -63,6 +64,7 @@ describe('Error404', () => { rootElement, jsxElement: , }) + await flushUpdates() const error404 = rootElement.querySelector('shade-404-not-found') expect(error404?.textContent).toContain('The page you are looking for is not exists') @@ -80,6 +82,7 @@ describe('Error404', () => { rootElement, jsxElement: , }) + await flushUpdates() const error404 = rootElement.querySelector('shade-404-not-found') expect(error404?.textContent).toContain('The URL above is correct') @@ -99,6 +102,7 @@ describe('Error404', () => { rootElement, jsxElement: , }) + await flushUpdates() const genericErrorPage = rootElement.querySelector('multiverse-generic-error-page') expect(genericErrorPage).toBeTruthy() diff --git a/frontend/src/components/github-logo/index.tsx b/frontend/src/components/github-logo/index.tsx index 4ef1a48a..d290cf9a 100644 --- a/frontend/src/components/github-logo/index.tsx +++ b/frontend/src/components/github-logo/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { attachStyles, Shade } from '@furystack/shades' +import { Shade } from '@furystack/shades' import { ThemeProviderService } from '@furystack/shades-common-components' // @ts-ignore import ghLight from './gh-light.png' @@ -14,7 +14,7 @@ export const GithubLogo = Shade({ shadowDomName: 'github-logo', elementBaseName: 'img', elementBase: HTMLImageElement, - render: ({ props, useDisposable, useState, injector, element }) => { + render: ({ props, useDisposable, useState, injector, useHostProps }) => { const themeProvider = injector.getInstance(ThemeProviderService) const [theme, setTheme] = useState( 'themeName', @@ -27,10 +27,10 @@ export const GithubLogo = Shade({ }), ) - attachStyles(element, props) - Object.assign(element, { + useHostProps({ src: theme === 'dark' ? ghLight : ghDark, alt: 'gh-logo', + style: props.style as Record | undefined, }) return null diff --git a/frontend/src/components/layout.tsx b/frontend/src/components/layout.tsx index 096629ac..79ebc6b9 100644 --- a/frontend/src/components/layout.tsx +++ b/frontend/src/components/layout.tsx @@ -1,5 +1,5 @@ import { createComponent, Shade } from '@furystack/shades' -import { cssVariableTheme, NotyList } from '@furystack/shades-common-components' +import { cssVariableTheme, NotyList, PageLayout } from '@furystack/shades-common-components' import { InstallService } from '../services/install-service.js' import { Body } from './body.js' import { Header } from './header.js' @@ -26,10 +26,14 @@ export const Layout = Shade({ const result = await injector.getInstance(InstallService).getServiceStatus() if (result.state === 'installed') { return ( - <> -
- - + , + }} + > + + ) } else if (result.state === 'needsInstall') { const { InstallerPage } = await import('../installer/index.js') diff --git a/frontend/src/components/monaco-editor.tsx b/frontend/src/components/monaco-editor.tsx index 02296a5b..6e5d2eb5 100644 --- a/frontend/src/components/monaco-editor.tsx +++ b/frontend/src/components/monaco-editor.tsx @@ -1,4 +1,4 @@ -import { Shade } from '@furystack/shades' +import { Shade, createComponent } from '@furystack/shades' import type { Uri } from 'monaco-editor' import { editor } from 'monaco-editor/esm/vs/editor/editor.api.js' import 'monaco-editor/esm/vs/editor/editor.main' @@ -7,15 +7,23 @@ import { ThemeProviderService, getCssVariable } from '@furystack/shades-common-c import { darkTheme } from '../themes/dark.js' import './worker-config' -export interface MonacoEditorProps { +export type MonacoEditorProps = { options: editor.IStandaloneEditorConstructionOptions value?: string onValueChange?: (value: string) => void modelUri?: Uri } + export const MonacoEditor = Shade({ shadowDomName: 'monaco-editor', - constructed: ({ element, props, injector, useState, useDisposable }) => { + css: { + display: 'block', + height: 'calc(100% - 96px)', + width: '100%', + position: 'relative', + }, + render: ({ props, injector, useState, useDisposable, useRef }) => { + const containerRef = useRef('container') const themeProvider = injector.getInstance(ThemeProviderService) const [theme] = useState<'vs-light' | 'vs-dark'>( @@ -23,33 +31,39 @@ export const MonacoEditor = Shade({ getCssVariable(themeProvider.theme.background.default) === darkTheme.background.default ? 'vs-dark' : 'vs-light', ) - const editorInstance = editor.create(element as HTMLElement, { ...props.options, theme }) + useDisposable('monacoEditor', () => { + const container = containerRef.current + if (!container) { + return { [Symbol.dispose]: () => {} } + } + + const editorInstance = editor.create(container, { ...props.options, theme }) + editorInstance.setValue(props.value || '') - editorInstance.setValue(props.value || '') - if (props.onValueChange) { - editorInstance.onKeyUp(() => { - props.onValueChange?.(editorInstance.getValue()) - }) - } + if (props.onValueChange) { + editorInstance.onKeyUp(() => { + props.onValueChange?.(editorInstance.getValue()) + }) + } - if (props.modelUri) { - useDisposable('monacoModelUri', () => { + if (props.modelUri) { const model = editor.createModel(editorInstance.getValue(), 'json', props.modelUri) editorInstance.setModel(model) return { [Symbol.dispose]: () => { model.dispose() + editorInstance.dispose() }, } - }) - } - return () => editorInstance.dispose() - }, - render: ({ element }) => { - element.style.display = 'block' - element.style.height = 'calc(100% - 96px)' - element.style.width = '100%' - element.style.position = 'relative' - return null + } + + return { + [Symbol.dispose]: () => { + editorInstance.dispose() + }, + } + }) + + return
}, }) diff --git a/frontend/src/components/movie-file-management/related-movies-modal.tsx b/frontend/src/components/movie-file-management/related-movies-modal.tsx index a9c18961..b10f08f4 100644 --- a/frontend/src/components/movie-file-management/related-movies-modal.tsx +++ b/frontend/src/components/movie-file-management/related-movies-modal.tsx @@ -1,14 +1,14 @@ import { Shade, createComponent } from '@furystack/shades' import { Button, Modal, Paper, fadeIn, fadeOut } from '@furystack/shades-common-components' -import type { ObservableValue } from '@furystack/utils' import type { DirectoryEntry } from 'common' import { getFallbackMetadata } from 'common' import { FileIcon } from '../../pages/file-browser/file-icon.js' -import { Separator } from '../Separator.js' +import { Divider } from '@furystack/shades-common-components' import { RelatedMoviesModalContent } from './related-movies-modal-content.js' -interface ManageMovieModalProps { - isOpened: ObservableValue +type ManageMovieModalProps = { + isOpened: boolean + onClose: () => void file: DirectoryEntry drive: string path: string @@ -44,7 +44,7 @@ export const RelatedMoviesModal = Shade({ }, }, render: ({ props }) => { - const { isOpened, drive, file, path } = props + const { isOpened, onClose, drive, file, path } = props const fallbackMeta = getFallbackMetadata(`${path}/${file.name}`) @@ -56,7 +56,7 @@ export const RelatedMoviesModal = Shade({ backdropFilter: 'blur(5px)', zIndex: '2', }} - onClose={() => isOpened.setValue(false)} + onClose={onClose} showAnimation={fadeIn} hideAnimation={fadeOut} > @@ -70,10 +70,10 @@ export const RelatedMoviesModal = Shade({
{`${path === '/' ? '' : path}/${file.name}`}
- +
- +
diff --git a/frontend/src/components/role-tag/index.spec.tsx b/frontend/src/components/role-tag/index.spec.tsx index dcd30c07..1a4a2fd9 100644 --- a/frontend/src/components/role-tag/index.spec.tsx +++ b/frontend/src/components/role-tag/index.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot } from '@furystack/shades' +import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { usingAsync } from '@furystack/utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -24,6 +24,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') expect(roleTag).toBeTruthy() @@ -40,6 +41,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') expect(roleTag?.textContent).toContain('Media Manager') @@ -55,6 +57,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') expect(roleTag?.textContent).toContain('Viewer') @@ -70,6 +73,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') expect(roleTag?.textContent).toContain('IoT Manager') @@ -85,6 +89,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const span = roleTag?.querySelector('span') @@ -103,6 +108,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const buttons = roleTag?.querySelectorAll('button') @@ -120,6 +126,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const removeButton = roleTag?.querySelector('button') @@ -139,6 +146,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const removeButton = roleTag?.querySelector('button') @@ -157,6 +165,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const restoreButton = roleTag?.querySelector('button') @@ -177,6 +186,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const buttons = roleTag?.querySelectorAll('button') @@ -198,6 +208,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const removeButton = roleTag?.querySelector('button') as HTMLButtonElement @@ -217,6 +228,7 @@ describe('RoleTag', () => { rootElement, jsxElement: , }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const restoreButton = roleTag?.querySelector('button') as HTMLButtonElement @@ -241,6 +253,7 @@ describe('RoleTag', () => { ), }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const removeButton = roleTag?.querySelector('button') as HTMLButtonElement @@ -266,6 +279,7 @@ describe('RoleTag', () => { ), }) + await flushUpdates() const roleTag = document.querySelector('role-tag') const restoreButton = roleTag?.querySelector('button') as HTMLButtonElement diff --git a/frontend/src/components/routes/admin-routes.tsx b/frontend/src/components/routes/admin-routes.tsx index 9a5d3f20..b6ddad64 100644 --- a/frontend/src/components/routes/admin-routes.tsx +++ b/frontend/src/components/routes/admin-routes.tsx @@ -1,4 +1,4 @@ -import { createComponent, type Route } from '@furystack/shades' +import { createComponent } from '@furystack/shades' import { PiRatLazyLoad } from '../pirat-lazy-load.js' import { onLeave, onVisit } from './route-animations.js' @@ -19,6 +19,8 @@ export const appSettingsRoute = { /> ) }, -} satisfies Route +} -export const adminRoutes = [appSettingsRoute] as const +export const adminRoutes = { + [appSettingsRoute.url]: appSettingsRoute, +} diff --git a/frontend/src/components/routes/ai-routes.tsx b/frontend/src/components/routes/ai-routes.tsx index 91923234..aa98829e 100644 --- a/frontend/src/components/routes/ai-routes.tsx +++ b/frontend/src/components/routes/ai-routes.tsx @@ -18,4 +18,6 @@ export const aiPageRoute = { }, } -export const aiRoutes = [aiPageRoute] as const +export const aiRoutes = { + [aiPageRoute.url]: aiPageRoute, +} diff --git a/frontend/src/components/routes/auth-routes.tsx b/frontend/src/components/routes/auth-routes.tsx index d728c8ce..e176bf94 100644 --- a/frontend/src/components/routes/auth-routes.tsx +++ b/frontend/src/components/routes/auth-routes.tsx @@ -1,4 +1,4 @@ -import { createComponent, type Route } from '@furystack/shades' +import { createComponent } from '@furystack/shades' import { Login } from '../../pages/login.js' import { Register } from '../../pages/register.js' import { onLeave, onVisit } from './route-animations.js' @@ -8,13 +8,16 @@ export const registerRoute = { onVisit, onLeave, component: () => , -} satisfies Route +} export const defaultAuthRoute = { url: '', onVisit, onLeave, component: () => , -} satisfies Route +} -export const authRoutes = [registerRoute, defaultAuthRoute] as const +export const authRoutes = { + [registerRoute.url]: registerRoute, + [defaultAuthRoute.url]: defaultAuthRoute, +} diff --git a/frontend/src/components/routes/chat-routes.tsx b/frontend/src/components/routes/chat-routes.tsx index 4136a757..effb962b 100644 --- a/frontend/src/components/routes/chat-routes.tsx +++ b/frontend/src/components/routes/chat-routes.tsx @@ -18,4 +18,6 @@ export const chatPageRoute = { }, } -export const chatRoutes = [chatPageRoute] as const +export const chatRoutes = { + [chatPageRoute.url]: chatPageRoute, +} diff --git a/frontend/src/components/routes/dashboard-routes.tsx b/frontend/src/components/routes/dashboard-routes.tsx index f7489abe..af86f5b0 100644 --- a/frontend/src/components/routes/dashboard-routes.tsx +++ b/frontend/src/components/routes/dashboard-routes.tsx @@ -21,4 +21,7 @@ export const defaultDashboardRoute = { component: () => , } -export const dashboardRoutes = [loadableDashboardRoute, defaultDashboardRoute] as const +export const dashboardRoutes = { + [loadableDashboardRoute.url]: loadableDashboardRoute, + [defaultDashboardRoute.url]: defaultDashboardRoute, +} diff --git a/frontend/src/components/routes/entity-routes.tsx b/frontend/src/components/routes/entity-routes.tsx index c06159ab..9be090d7 100644 --- a/frontend/src/components/routes/entity-routes.tsx +++ b/frontend/src/components/routes/entity-routes.tsx @@ -1,4 +1,4 @@ -import { createComponent, type Route } from '@furystack/shades' +import { createComponent } from '@furystack/shades' import { PiRatLazyLoad } from '../pirat-lazy-load.js' import { onLeave, onVisit } from './route-animations.js' @@ -14,7 +14,7 @@ export const entityDrivesRoute = { }} /> ), -} satisfies Route +} export const entityUsersRoute = { url: '/entities/users', @@ -28,7 +28,7 @@ export const entityUsersRoute = { }} /> ), -} satisfies Route +} export const entityDashboardsRoute = { url: '/entities/dashboards', @@ -42,7 +42,7 @@ export const entityDashboardsRoute = { }} /> ), -} satisfies Route +} export const entityMoviesRoute = { url: '/entities/movies', @@ -56,7 +56,7 @@ export const entityMoviesRoute = { }} /> ), -} satisfies Route +} export const entityMovieFilesRoute = { url: '/entities/movie-files', @@ -70,7 +70,7 @@ export const entityMovieFilesRoute = { }} /> ), -} satisfies Route +} export const entityOmdbMovieMetadataRoute = { url: '/entities/omdb-movie-metadata', @@ -84,7 +84,7 @@ export const entityOmdbMovieMetadataRoute = { }} /> ), -} satisfies Route +} export const entityOmdbSeriesMetadataRoute = { url: '/entities/omdb-series-metadata', @@ -98,7 +98,7 @@ export const entityOmdbSeriesMetadataRoute = { }} /> ), -} satisfies Route +} export const entityConfigRoute = { url: '/entities/config', @@ -112,7 +112,7 @@ export const entityConfigRoute = { }} /> ), -} satisfies Route +} export const entityDeviceRoute = { url: '/entities/iot-devices', @@ -126,7 +126,7 @@ export const entityDeviceRoute = { }} /> ), -} satisfies Route +} export const entityLoggingRoute = { url: '/entities/logging', @@ -140,17 +140,17 @@ export const entityLoggingRoute = { }} /> ), -} satisfies Route +} -export const entityRoutes = [ - entityDrivesRoute, - entityUsersRoute, - entityDashboardsRoute, - entityMoviesRoute, - entityMovieFilesRoute, - entityOmdbMovieMetadataRoute, - entityOmdbSeriesMetadataRoute, - entityConfigRoute, - entityDeviceRoute, - entityLoggingRoute, -] +export const entityRoutes = { + [entityDrivesRoute.url]: entityDrivesRoute, + [entityUsersRoute.url]: entityUsersRoute, + [entityDashboardsRoute.url]: entityDashboardsRoute, + [entityMoviesRoute.url]: entityMoviesRoute, + [entityMovieFilesRoute.url]: entityMovieFilesRoute, + [entityOmdbMovieMetadataRoute.url]: entityOmdbMovieMetadataRoute, + [entityOmdbSeriesMetadataRoute.url]: entityOmdbSeriesMetadataRoute, + [entityConfigRoute.url]: entityConfigRoute, + [entityDeviceRoute.url]: entityDeviceRoute, + [entityLoggingRoute.url]: entityLoggingRoute, +} diff --git a/frontend/src/components/routes/file-browser-routes.tsx b/frontend/src/components/routes/file-browser-routes.tsx index 846d8a81..cca75f0e 100644 --- a/frontend/src/components/routes/file-browser-routes.tsx +++ b/frontend/src/components/routes/file-browser-routes.tsx @@ -32,4 +32,7 @@ export const fileBrowserOpenFileRoute = { ), } -export const fileBrowserRoutes = [fileBrowserRoute, fileBrowserOpenFileRoute] as const +export const fileBrowserRoutes = { + [fileBrowserRoute.url]: fileBrowserRoute, + [fileBrowserOpenFileRoute.url]: fileBrowserOpenFileRoute, +} diff --git a/frontend/src/components/routes/iot-routes.tsx b/frontend/src/components/routes/iot-routes.tsx index e0d1e5b5..2250f222 100644 --- a/frontend/src/components/routes/iot-routes.tsx +++ b/frontend/src/components/routes/iot-routes.tsx @@ -21,4 +21,7 @@ export const iotDeviceRoute = { }, } -export const iotRoutes = [iotDeviceListRoute, iotDeviceRoute] +export const iotRoutes = { + [iotDeviceListRoute.url]: iotDeviceListRoute, + [iotDeviceRoute.url]: iotDeviceRoute, +} diff --git a/frontend/src/components/routes/logging-routes.tsx b/frontend/src/components/routes/logging-routes.tsx index 73af45b0..412d338e 100644 --- a/frontend/src/components/routes/logging-routes.tsx +++ b/frontend/src/components/routes/logging-routes.tsx @@ -1,4 +1,4 @@ -import { createComponent, type Route } from '@furystack/shades' +import { createComponent } from '@furystack/shades' import { PiRatLazyLoad } from '../pirat-lazy-load.js' import { onLeave, onVisit } from './route-animations.js' @@ -16,13 +16,13 @@ export const LogEntriesTerminalRoute = { /> ) }, -} satisfies Route +} export const logEntryRoute = { url: '/logging/log-entry/:id', onVisit, onLeave, - component: ({ match }) => { + component: ({ match }: { match: { params: { id: string } } }) => { return ( { @@ -32,6 +32,9 @@ export const logEntryRoute = { /> ) }, -} satisfies Route<{ id: string }> +} -export const loggingRoutes = [LogEntriesTerminalRoute, logEntryRoute] as const +export const loggingRoutes = { + [LogEntriesTerminalRoute.url]: LogEntriesTerminalRoute, + [logEntryRoute.url]: logEntryRoute, +} diff --git a/frontend/src/components/routes/movie-routes.tsx b/frontend/src/components/routes/movie-routes.tsx index 2a882f25..273f9e03 100644 --- a/frontend/src/components/routes/movie-routes.tsx +++ b/frontend/src/components/routes/movie-routes.tsx @@ -50,10 +50,10 @@ export const seriesOverviewRoute = { }, } -export const movieRoutes = [ - movieListRoute, - watchMovieRoute, - movieOverviewRoute, - seriesListRoute, - seriesOverviewRoute, -] as const +export const movieRoutes = { + [movieListRoute.url]: movieListRoute, + [watchMovieRoute.url]: watchMovieRoute, + [movieOverviewRoute.url]: movieOverviewRoute, + [seriesListRoute.url]: seriesListRoute, + [seriesOverviewRoute.url]: seriesOverviewRoute, +} diff --git a/frontend/src/components/routes/user-routes.tsx b/frontend/src/components/routes/user-routes.tsx index 926e7b2a..4534c594 100644 --- a/frontend/src/components/routes/user-routes.tsx +++ b/frontend/src/components/routes/user-routes.tsx @@ -1,4 +1,4 @@ -import { createComponent, type Route } from '@furystack/shades' +import { createComponent } from '@furystack/shades' import { PiRatLazyLoad } from '../pirat-lazy-load.js' import { onLeave, onVisit } from './route-animations.js' @@ -16,6 +16,8 @@ export const userSettingsRoute = { /> ) }, -} satisfies Route +} -export const userRoutes = [userSettingsRoute] as const +export const userRoutes = { + [userSettingsRoute.url]: userSettingsRoute, +} diff --git a/frontend/src/components/settings-sidebar/settings-menu-item.spec.tsx b/frontend/src/components/settings-sidebar/settings-menu-item.spec.tsx index 465aae7d..d092b7e4 100644 --- a/frontend/src/components/settings-sidebar/settings-menu-item.spec.tsx +++ b/frontend/src/components/settings-sidebar/settings-menu-item.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot, LocationService } from '@furystack/shades' +import { LocationService, createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { usingAsync } from '@furystack/utils' import { afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -26,6 +26,7 @@ describe('SettingsMenuItem', () => { rootElement, jsxElement: , }) + await flushUpdates() const menuItem = document.querySelector('settings-menu-item') expect(menuItem).toBeTruthy() @@ -50,6 +51,7 @@ describe('SettingsMenuItem', () => { rootElement, jsxElement: , }) + await flushUpdates() const menuItem = document.querySelector('settings-menu-item') expect(menuItem).toBeTruthy() @@ -73,6 +75,7 @@ describe('SettingsMenuItem', () => { rootElement, jsxElement: , }) + await flushUpdates() const menuItem = document.querySelector('settings-menu-item') expect(menuItem).toBeTruthy() @@ -95,6 +98,7 @@ describe('SettingsMenuItem', () => { rootElement, jsxElement: , }) + await flushUpdates() const menuItem = document.querySelector('settings-menu-item') expect(menuItem).toBeTruthy() diff --git a/frontend/src/components/settings-sidebar/settings-menu-item.tsx b/frontend/src/components/settings-sidebar/settings-menu-item.tsx index 182d5fab..6d039835 100644 --- a/frontend/src/components/settings-sidebar/settings-menu-item.tsx +++ b/frontend/src/components/settings-sidebar/settings-menu-item.tsx @@ -1,4 +1,4 @@ -import { createComponent, LocationService, RouteLink, Shade } from '@furystack/shades' +import { createComponent, LocationService, NestedRouteLink, Shade } from '@furystack/shades' import { cssVariableTheme } from '@furystack/shades-common-components' import { match, type MatchOptions } from 'path-to-regexp' @@ -40,7 +40,7 @@ export const SettingsMenuItem = Shade({ const isActive = !!match(href, routingOptions)(currentPath) return ( - ({ > {icon} {label} - + ) }, }) diff --git a/frontend/src/components/theme-switch/index.spec.tsx b/frontend/src/components/theme-switch/index.spec.tsx index 4ba9a5c9..796efcc7 100644 --- a/frontend/src/components/theme-switch/index.spec.tsx +++ b/frontend/src/components/theme-switch/index.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot } from '@furystack/shades' +import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { ThemeProviderService } from '@furystack/shades-common-components' import { usingAsync } from '@furystack/utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -35,6 +35,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() const themeSwitch = rootElement.querySelector('theme-switch') expect(themeSwitch).toBeTruthy() @@ -51,6 +52,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() const themeSwitch = rootElement.querySelector('theme-switch') expect(themeSwitch).toBeTruthy() @@ -67,6 +69,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() const themeSwitch = rootElement.querySelector('theme-switch') expect(themeSwitch).toBeTruthy() @@ -84,6 +87,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() const themeSwitch = rootElement.querySelector('theme-switch') expect(themeSwitch).toBeTruthy() @@ -101,6 +105,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() const themeSwitch = rootElement.querySelector('theme-switch') expect(themeSwitch).toBeTruthy() @@ -118,6 +123,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() expect(mockThemeProvider.subscribe).toHaveBeenCalledWith('themeChanged', expect.any(Function)) }) @@ -133,6 +139,7 @@ describe('ThemeSwitch', () => { rootElement, jsxElement: , }) + await flushUpdates() const themeSwitch = rootElement.querySelector('theme-switch') expect(themeSwitch).toBeTruthy() diff --git a/frontend/src/components/wizard-step.spec.tsx b/frontend/src/components/wizard-step.spec.tsx index 66828e10..191ba63c 100644 --- a/frontend/src/components/wizard-step.spec.tsx +++ b/frontend/src/components/wizard-step.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot, ScreenService } from '@furystack/shades' +import { ScreenService, createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { ObservableValue, usingAsync } from '@furystack/utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -67,6 +67,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() const wizardStep = document.querySelector('wizard-step') expect(wizardStep).toBeTruthy() @@ -90,6 +91,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() const wizardStep = document.querySelector('wizard-step') expect(wizardStep).toBeTruthy() @@ -114,6 +116,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() vi.runAllTimers() @@ -138,6 +141,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() vi.runAllTimers() @@ -162,6 +166,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() vi.runAllTimers() @@ -186,6 +191,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() vi.runAllTimers() @@ -211,6 +217,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() vi.runAllTimers() @@ -237,6 +244,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() const wizardStep = document.querySelector('wizard-step') const form = wizardStep?.querySelector('form') @@ -262,6 +270,7 @@ describe('WizardStep', () => { ), }) + await flushUpdates() const wizardStep = document.querySelector('wizard-step') const form = wizardStep?.querySelector('form') diff --git a/frontend/src/components/wizard-step.tsx b/frontend/src/components/wizard-step.tsx index a8a7f803..266732e9 100644 --- a/frontend/src/components/wizard-step.tsx +++ b/frontend/src/components/wizard-step.tsx @@ -32,30 +32,39 @@ export const WizardStep = Shade< opacity: '0', }, }, - render: ({ props, element, children, useObservable, injector }) => { + render: ({ props, children, useObservable, injector, useRef }) => { + const h1Ref = useRef('h1') + const contentRef = useRef('content') + const actionsRef = useRef('actions') + const formRef = useRef('form') + setTimeout(() => { - void showParallax(element.querySelector('h1')) - void showParallax(element.querySelector('div.content'), { delay: 200, duration: 600 }) - void showParallax(element.querySelector('div.actions'), { delay: 400, duration: 2000 }) + void showParallax(h1Ref.current) + void showParallax(contentRef.current, { delay: 200, duration: 600 }) + void showParallax(actionsRef.current, { delay: 400, duration: 2000 }) }, 1) - const updateScreenSize = (isLargeScreen: boolean) => { - const form = element?.querySelector('form') - if (form) { - form.style.padding = '16px' - form.style.width = isLargeScreen ? '800px' : `${window.innerWidth - 16}px` - form.style.height = isLargeScreen ? '500px' : `${window.innerHeight - 192}px` - } - } - const [isLargeScreen] = useObservable('screenSize', injector.getInstance(ScreenService).screenSize.atLeast.md, { - onChange: updateScreenSize, + onChange: (isLarge) => { + const form = formRef.current + if (form) { + form.style.padding = '16px' + form.style.width = isLarge ? '800px' : `${window.innerWidth - 16}px` + form.style.height = isLarge ? '500px' : `${window.innerHeight - 192}px` + } + }, }) - updateScreenSize(isLargeScreen) + const form = formRef.current + if (form) { + form.style.padding = '16px' + form.style.width = isLargeScreen ? '800px' : `${window.innerWidth - 16}px` + form.style.height = isLargeScreen ? '500px' : `${window.innerHeight - 192}px` + } return (
{ ev.preventDefault() if (props.onSubmit) { @@ -65,9 +74,9 @@ export const WizardStep = Shade< } }} > -

{props.title}

-
{children}
-
+

{props.title}

+
{children}
+
diff --git a/frontend/src/navigate-to-route.ts b/frontend/src/navigate-to-route.ts index 19a725aa..7fc761f1 100644 --- a/frontend/src/navigate-to-route.ts +++ b/frontend/src/navigate-to-route.ts @@ -1,10 +1,13 @@ import type { Injector } from '@furystack/inject' -import type { Route } from '@furystack/shades' -import { LocationService } from '@furystack/shades' -import { compile } from 'path-to-regexp' +import { LocationService, compileRoute } from '@furystack/shades' -export const navigateToRoute = (injector: Injector, route: Route, params: T, queryString = '') => { - const destinationPath = compile(route.url)(params) +export const navigateToRoute = >( + injector: Injector, + route: { url: string }, + params: T, + queryString = '', +) => { + const destinationPath = compileRoute(route.url, params) const fullPath = destinationPath + (queryString ? `?${queryString}` : '') || '/' window.history.pushState({}, '', fullPath) injector.getInstance(LocationService).updateState() diff --git a/frontend/src/pages/admin/ai-settings.spec.tsx b/frontend/src/pages/admin/ai-settings.spec.tsx index d2a9f806..ae3637de 100644 --- a/frontend/src/pages/admin/ai-settings.spec.tsx +++ b/frontend/src/pages/admin/ai-settings.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot } from '@furystack/shades' +import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { NotyService } from '@furystack/shades-common-components' import { ObservableValue } from '@furystack/utils' import type { Config, OllamaConfig } from 'common' @@ -61,7 +61,7 @@ describe('AiSettingsPage', () => { document.body.innerHTML = '' }) - it('should render the AI settings page with header', () => { + it('should render the AI settings page with header', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -69,13 +69,14 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('ai-settings-page') expect(page).toBeTruthy() expect(page?.textContent).toContain('Ollama Integration') }) - it('should display loading state', () => { + it('should display loading state', async () => { configObservable.setValue({ status: 'loading' }) const rootElement = document.getElementById('root') as HTMLDivElement @@ -85,12 +86,13 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('ai-settings-page') expect(page?.textContent).toContain('Loading settings...') }) - it('should render the form with host input when loaded', () => { + it('should render the form with host input when loaded', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -98,6 +100,8 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('ai-settings-page') const hostInput = page?.querySelector('input[name="host"]') as HTMLInputElement @@ -106,7 +110,7 @@ describe('AiSettingsPage', () => { expect(hostInput?.type).toBe('url') }) - it('should render save button', () => { + it('should render save button', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -114,13 +118,15 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('ai-settings-page') const saveButton = page?.querySelector('button[type="submit"]') expect(saveButton).toBeTruthy() }) - it('should call ConfigService.getConfigAsObservable on render', () => { + it('should call ConfigService.getConfigAsObservable on render', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -128,11 +134,12 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() expect(mockConfigService.getConfigAsObservable).toHaveBeenCalledWith('OLLAMA_CONFIG') }) - it('should render with empty host when config value is empty', () => { + it('should render with empty host when config value is empty', async () => { configObservable.setValue({ status: 'loaded', value: createMockOllamaConfig(''), @@ -146,6 +153,8 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('ai-settings-page') const hostInput = page?.querySelector('input[name="host"]') as HTMLInputElement @@ -160,6 +169,8 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('ai-settings-page') const form = page?.querySelector('form') as HTMLFormElement @@ -167,6 +178,7 @@ describe('AiSettingsPage', () => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) await new Promise((resolve) => setTimeout(resolve, 50)) + await flushUpdates() expect(mockNotyService.emit).toHaveBeenCalledWith('onNotyAdded', { title: 'Success', @@ -185,6 +197,8 @@ describe('AiSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('ai-settings-page') const form = page?.querySelector('form') as HTMLFormElement @@ -192,6 +206,7 @@ describe('AiSettingsPage', () => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) await new Promise((resolve) => setTimeout(resolve, 50)) + await flushUpdates() expect(mockNotyService.emit).toHaveBeenCalledWith('onNotyAdded', { title: 'Error', diff --git a/frontend/src/pages/admin/app-settings.tsx b/frontend/src/pages/admin/app-settings.tsx index 13ea42ef..122e23d7 100644 --- a/frontend/src/pages/admin/app-settings.tsx +++ b/frontend/src/pages/admin/app-settings.tsx @@ -1,11 +1,10 @@ -import { createComponent, LocationService, Router, Shade } from '@furystack/shades' +import { createComponent, LocationService, NestedRouter, Shade } from '@furystack/shades' import type { MatchResult } from 'path-to-regexp' import { PiRatLazyLoad } from '../../components/pirat-lazy-load.js' import { SettingsMenuItem, SettingsMenuSection, SettingsSidebar } from '../../components/settings-sidebar/index.js' -const settingsRoutes = [ - { - url: '/app-settings/omdb', +const settingsRoutes = { + '/app-settings/omdb': { component: () => ( { @@ -15,8 +14,7 @@ const settingsRoutes = [ /> ), }, - { - url: '/app-settings/streaming', + '/app-settings/streaming': { component: () => ( { @@ -26,8 +24,7 @@ const settingsRoutes = [ /> ), }, - { - url: '/app-settings/iot', + '/app-settings/iot': { component: () => ( { @@ -37,8 +34,7 @@ const settingsRoutes = [ /> ), }, - { - url: '/app-settings/ai', + '/app-settings/ai': { component: () => ( { @@ -48,8 +44,7 @@ const settingsRoutes = [ /> ), }, - { - url: '/app-settings/users/:username', + '/app-settings/users/:username': { component: ({ match }: { match: MatchResult<{ username: string }> }) => ( { @@ -59,8 +54,7 @@ const settingsRoutes = [ /> ), }, - { - url: '/app-settings/users', + '/app-settings/users': { component: () => ( { @@ -70,8 +64,7 @@ const settingsRoutes = [ /> ), }, - { - url: '/app-settings', + '/app-settings': { component: () => ( { @@ -81,7 +74,7 @@ const settingsRoutes = [ /> ), }, -] +} export const AppSettingsPage = Shade({ shadowDomName: 'app-settings-page', @@ -136,7 +129,7 @@ export const AppSettingsPage = Shade({
- +
) diff --git a/frontend/src/pages/admin/iot-settings.spec.tsx b/frontend/src/pages/admin/iot-settings.spec.tsx index 1959d558..ea00aef7 100644 --- a/frontend/src/pages/admin/iot-settings.spec.tsx +++ b/frontend/src/pages/admin/iot-settings.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot } from '@furystack/shades' +import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { NotyService } from '@furystack/shades-common-components' import { ObservableValue } from '@furystack/utils' import type { Config, IotConfig } from 'common' @@ -62,7 +62,7 @@ describe('IotSettingsPage', () => { document.body.innerHTML = '' }) - it('should render the IOT settings page with header', () => { + it('should render the IOT settings page with header', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -70,13 +70,14 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('iot-settings-page') expect(page).toBeTruthy() expect(page?.textContent).toContain('IOT Device Availability') }) - it('should display loading state', () => { + it('should display loading state', async () => { configObservable.setValue({ status: 'loading' }) const rootElement = document.getElementById('root') as HTMLDivElement @@ -86,12 +87,13 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('iot-settings-page') expect(page?.textContent).toContain('Loading settings...') }) - it('should render the form with ping interval and timeout inputs when loaded', () => { + it('should render the form with ping interval and timeout inputs when loaded', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -99,6 +101,8 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('iot-settings-page') @@ -119,7 +123,7 @@ describe('IotSettingsPage', () => { expect(pingTimeoutInput?.required).toBe(true) }) - it('should render save button', () => { + it('should render save button', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -127,13 +131,15 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('iot-settings-page') const saveButton = page?.querySelector('button[type="submit"]') expect(saveButton).toBeTruthy() }) - it('should call ConfigService.getConfigAsObservable on render', () => { + it('should call ConfigService.getConfigAsObservable on render', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -141,11 +147,12 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() expect(mockConfigService.getConfigAsObservable).toHaveBeenCalledWith('IOT_CONFIG') }) - it('should render with custom values from config', () => { + it('should render with custom values from config', async () => { configObservable.setValue({ status: 'loaded', value: createMockIotConfig(60000, 5000), @@ -159,6 +166,8 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('iot-settings-page') const pingIntervalInput = page?.querySelector('input[name="pingIntervalMs"]') as HTMLInputElement @@ -168,7 +177,7 @@ describe('IotSettingsPage', () => { expect(pingTimeoutInput?.value).toBe('5000') }) - it('should display validation constraints in help text', () => { + it('should display validation constraints in help text', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -176,6 +185,8 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('iot-settings-page') const helpText = page?.textContent @@ -194,6 +205,8 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('iot-settings-page') const form = page?.querySelector('form') as HTMLFormElement @@ -201,6 +214,7 @@ describe('IotSettingsPage', () => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) await new Promise((resolve) => setTimeout(resolve, 50)) + await flushUpdates() expect(mockNotyService.emit).toHaveBeenCalledWith('onNotyAdded', { title: 'Success', @@ -219,6 +233,8 @@ describe('IotSettingsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('iot-settings-page') const form = page?.querySelector('form') as HTMLFormElement @@ -226,6 +242,7 @@ describe('IotSettingsPage', () => { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) await new Promise((resolve) => setTimeout(resolve, 50)) + await flushUpdates() expect(mockNotyService.emit).toHaveBeenCalledWith('onNotyAdded', { title: 'Error', diff --git a/frontend/src/pages/admin/omdb-settings.tsx b/frontend/src/pages/admin/omdb-settings.tsx index 60e4d449..f2aa4de4 100644 --- a/frontend/src/pages/admin/omdb-settings.tsx +++ b/frontend/src/pages/admin/omdb-settings.tsx @@ -65,7 +65,7 @@ export const OmdbSettingsPage = Shade({ paddingTop: '16px', }, }, - render: ({ injector, useObservable, useDisposable, element }) => { + render: ({ injector, useObservable, useDisposable, useState }) => { const configService = injector.getInstance(ConfigService) const notyService = injector.getInstance(NotyService) @@ -73,15 +73,10 @@ export const OmdbSettingsPage = Shade({ const isLoadingObservable = useDisposable('isLoading', () => new ObservableValue(false)) const [isLoading] = useObservable('isLoadingValue', isLoadingObservable) + const [isApiKeyVisible, setApiKeyVisible] = useState('apiKeyVisible', false) const toggleApiKeyVisibility = () => { - const input = element.querySelector('input[name="apiKey"]') - const button = element.querySelector('[data-toggle-visibility]') - if (input && button) { - const isPassword = input.type === 'password' - input.type = isPassword ? 'text' : 'password' - button.textContent = isPassword ? '๐Ÿ™ˆ Hide' : '๐Ÿ‘๏ธ Show' - } + setApiKeyVisible(!isApiKeyVisible) } const handleSubmit = async (formData: Record) => { @@ -144,7 +139,7 @@ export const OmdbSettingsPage = Shade({ - ๐Ÿ‘๏ธ Show + {isApiKeyVisible ? '๐Ÿ™ˆ Hide' : '๐Ÿ‘๏ธ Show'}
diff --git a/frontend/src/pages/admin/user-details.spec.tsx b/frontend/src/pages/admin/user-details.spec.tsx index f72e41b8..75ca290b 100644 --- a/frontend/src/pages/admin/user-details.spec.tsx +++ b/frontend/src/pages/admin/user-details.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot, LocationService } from '@furystack/shades' +import { LocationService, createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { NotyService } from '@furystack/shades-common-components' import { ObservableValue } from '@furystack/utils' import type { User } from 'common' @@ -57,7 +57,7 @@ describe('UserDetailsPage', () => { }) describe('rendering', () => { - it('should render the user details page with header', () => { + it('should render the user details page with header', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -65,13 +65,14 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page).toBeTruthy() expect(page?.textContent).toContain('User Details') }) - it('should display loading state', () => { + it('should display loading state', async () => { userObservable.setValue({ status: 'loading' }) const rootElement = document.getElementById('root') as HTMLDivElement @@ -81,12 +82,13 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Loading user...') }) - it('should display error state with go back button', () => { + it('should display error state with go back button', async () => { userObservable.setValue({ status: 'failed', error: new Error('User not found'), @@ -100,6 +102,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Error: User not found') @@ -109,7 +112,7 @@ describe('UserDetailsPage', () => { expect(buttons?.length).toBeGreaterThanOrEqual(1) }) - it('should display fallback error message for non-Error objects', () => { + it('should display fallback error message for non-Error objects', async () => { userObservable.setValue({ status: 'failed', error: 'string error', @@ -123,6 +126,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Failed to load user') @@ -130,7 +134,7 @@ describe('UserDetailsPage', () => { }) describe('user information display', () => { - it('should display username', () => { + it('should display username', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -138,13 +142,14 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Username:') expect(page?.textContent).toContain('testuser@example.com') }) - it('should display created date', () => { + it('should display created date', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -152,12 +157,13 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Created:') }) - it('should display last updated date', () => { + it('should display last updated date', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -165,6 +171,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Last Updated:') @@ -172,7 +179,7 @@ describe('UserDetailsPage', () => { }) describe('roles section', () => { - it('should display roles section header', () => { + it('should display roles section header', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -180,12 +187,13 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Roles') }) - it('should render role tags for user roles', () => { + it('should render role tags for user roles', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -193,6 +201,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const roleTags = page?.querySelectorAll('role-tag') @@ -200,7 +209,7 @@ describe('UserDetailsPage', () => { expect(roleTags?.length).toBe(1) }) - it('should render multiple role tags for user with multiple roles', () => { + it('should render multiple role tags for user with multiple roles', async () => { userObservable.setValue({ status: 'loaded', value: createMockUser('testuser@example.com', ['admin', 'viewer', 'media-manager']), @@ -214,6 +223,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const roleTags = page?.querySelectorAll('role-tag') @@ -221,7 +231,7 @@ describe('UserDetailsPage', () => { expect(roleTags?.length).toBe(3) }) - it('should display "No roles assigned" for user without roles', () => { + it('should display "No roles assigned" for user without roles', async () => { userObservable.setValue({ status: 'loaded', value: createMockUser('testuser@example.com', []), @@ -235,6 +245,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('No roles assigned') @@ -242,7 +253,7 @@ describe('UserDetailsPage', () => { }) describe('add role dropdown', () => { - it('should display Add Role dropdown', () => { + it('should display Add Role dropdown', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -250,6 +261,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') expect(page?.textContent).toContain('Add Role:') @@ -258,7 +270,7 @@ describe('UserDetailsPage', () => { expect(select).toBeTruthy() }) - it('should show available roles that user does not have', () => { + it('should show available roles that user does not have', async () => { userObservable.setValue({ status: 'loaded', value: createMockUser('testuser@example.com', ['admin']), @@ -272,6 +284,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') @@ -286,7 +299,7 @@ describe('UserDetailsPage', () => { expect(select?.textContent).not.toContain('Application Admin') }) - it('should not show dropdown when user has all roles', () => { + it('should not show dropdown when user has all roles', async () => { userObservable.setValue({ status: 'loaded', value: createMockUser('testuser@example.com', ['admin', 'viewer', 'media-manager', 'iot-manager']), @@ -300,6 +313,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') @@ -308,7 +322,7 @@ describe('UserDetailsPage', () => { }) describe('action buttons', () => { - it('should render action buttons', () => { + it('should render action buttons', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -316,6 +330,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') // Back button, Save Changes button, and Cancel button @@ -323,7 +338,7 @@ describe('UserDetailsPage', () => { expect(buttons?.length).toBeGreaterThanOrEqual(3) }) - it('should have Save Changes and Cancel buttons disabled when no changes', () => { + it('should have Save Changes and Cancel buttons disabled when no changes', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -331,6 +346,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') // Button component sets disabled attribute on the button element @@ -341,7 +357,7 @@ describe('UserDetailsPage', () => { }) describe('navigation', () => { - it('should navigate back to user list when Back button is clicked', () => { + it('should navigate back to user list when Back button is clicked', async () => { const rootElement = document.getElementById('root') as HTMLDivElement const locationService = injector.getInstance(LocationService) const updateStateSpy = vi.spyOn(locationService, 'updateState') @@ -351,6 +367,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') // Back button is the first button element in the page @@ -364,7 +381,7 @@ describe('UserDetailsPage', () => { }) describe('role editing', () => { - it('should add role when selected from dropdown', () => { + it('should add role when selected from dropdown', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -372,6 +389,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') as HTMLSelectElement @@ -379,6 +397,7 @@ describe('UserDetailsPage', () => { // Simulate selecting a role select.value = 'viewer' select.dispatchEvent(new Event('change', { bubbles: true })) + await flushUpdates() // Should now have 2 role tags (admin + viewer) const roleTags = page?.querySelectorAll('role-tag') @@ -403,6 +422,8 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('user-details-page') @@ -413,6 +434,8 @@ describe('UserDetailsPage', () => { // Wait for re-render await new Promise((resolve) => setTimeout(resolve, 10)) + await flushUpdates() + await flushUpdates() // No disabled buttons (Save and Cancel are enabled) const disabledButtons = page?.querySelectorAll('button[disabled]') @@ -427,6 +450,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') as HTMLSelectElement @@ -467,6 +491,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') as HTMLSelectElement @@ -500,6 +525,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') as HTMLSelectElement @@ -534,6 +560,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') as HTMLSelectElement @@ -576,6 +603,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-details-page') const select = page?.querySelector('select') as HTMLSelectElement @@ -619,6 +647,8 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() + await flushUpdates() const page = document.querySelector('user-details-page') @@ -628,6 +658,8 @@ describe('UserDetailsPage', () => { removeButton?.click() await new Promise((resolve) => setTimeout(resolve, 10)) + await flushUpdates() + await flushUpdates() // Click Save (index 1 of action buttons, excluding role-tag buttons) const actionButtons = getActionButtons(page) @@ -635,6 +667,8 @@ describe('UserDetailsPage', () => { saveButton.click() await new Promise((resolve) => setTimeout(resolve, 10)) + await flushUpdates() + await flushUpdates() expect(page?.textContent).toContain('User must have at least one role') expect(mockUsersService.updateUser).not.toHaveBeenCalled() @@ -642,7 +676,7 @@ describe('UserDetailsPage', () => { }) describe('service integration', () => { - it('should call getUserAsObservable with username on render', () => { + it('should call getUserAsObservable with username on render', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -650,6 +684,7 @@ describe('UserDetailsPage', () => { rootElement, jsxElement: , }) + await flushUpdates() expect(mockUsersService.getUserAsObservable).toHaveBeenCalledWith('testuser@example.com') }) diff --git a/frontend/src/pages/admin/user-list.spec.tsx b/frontend/src/pages/admin/user-list.spec.tsx index 4386c328..5bae4640 100644 --- a/frontend/src/pages/admin/user-list.spec.tsx +++ b/frontend/src/pages/admin/user-list.spec.tsx @@ -1,5 +1,5 @@ import { Injector } from '@furystack/inject' -import { createComponent, initializeShadeRoot, LocationService } from '@furystack/shades' +import { LocationService, createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades' import { ObservableValue } from '@furystack/utils' import type { User } from 'common' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -47,7 +47,7 @@ describe('UserListPage', () => { }) describe('rendering', () => { - it('should render the user list page with header', () => { + it('should render the user list page with header', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -55,6 +55,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page).toBeTruthy() @@ -62,7 +63,7 @@ describe('UserListPage', () => { expect(page?.textContent).toContain('Manage user accounts and their roles.') }) - it('should display loading state', () => { + it('should display loading state', async () => { usersObservable.setValue({ status: 'loading' }) const rootElement = document.getElementById('root') as HTMLDivElement @@ -72,12 +73,13 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page?.textContent).toContain('Loading users...') }) - it('should display uninitialized state as loading', () => { + it('should display uninitialized state as loading', async () => { usersObservable.setValue({ status: 'uninitialized' }) const rootElement = document.getElementById('root') as HTMLDivElement @@ -87,12 +89,13 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page?.textContent).toContain('Loading users...') }) - it('should display error state with retry button', () => { + it('should display error state with retry button', async () => { usersObservable.setValue({ status: 'failed', error: new Error('Network error'), @@ -106,6 +109,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page?.textContent).toContain('Error: Network error') @@ -115,7 +119,7 @@ describe('UserListPage', () => { expect(retryButton).toBeTruthy() }) - it('should display error message for non-Error objects', () => { + it('should display error message for non-Error objects', async () => { usersObservable.setValue({ status: 'failed', error: 'string error', @@ -129,6 +133,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page?.textContent).toContain('Failed to load users') @@ -136,7 +141,7 @@ describe('UserListPage', () => { }) describe('table display', () => { - it('should render table with headers', () => { + it('should render table with headers', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -144,6 +149,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const headers = page?.querySelectorAll('th') @@ -155,7 +161,7 @@ describe('UserListPage', () => { expect(headers?.[3]?.textContent).toContain('Actions') }) - it('should render users in table rows', () => { + it('should render users in table rows', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -163,6 +169,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const rows = page?.querySelectorAll('tbody tr') @@ -172,7 +179,7 @@ describe('UserListPage', () => { expect(rows?.[1]?.textContent).toContain('user2@example.com') }) - it('should render role tags for each user', () => { + it('should render role tags for each user', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -180,6 +187,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const roleTags = page?.querySelectorAll('role-tag') @@ -187,7 +195,7 @@ describe('UserListPage', () => { expect(roleTags?.length).toBe(2) // One for admin, one for viewer }) - it('should display "No roles" message for user without roles', () => { + it('should display "No roles" message for user without roles', async () => { usersObservable.setValue({ status: 'loaded', value: { @@ -204,12 +212,13 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page?.textContent).toContain('No roles') }) - it('should display empty state when no users exist', () => { + it('should display empty state when no users exist', async () => { usersObservable.setValue({ status: 'loaded', value: { @@ -226,12 +235,13 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') expect(page?.textContent).toContain('No users found.') }) - it('should render Edit button for each user', () => { + it('should render Edit button for each user', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -239,6 +249,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') // Button component renders button elements within table rows @@ -252,7 +263,7 @@ describe('UserListPage', () => { }) describe('navigation', () => { - it('should navigate to user details when Edit button is clicked', () => { + it('should navigate to user details when Edit button is clicked', async () => { const rootElement = document.getElementById('root') as HTMLDivElement const locationService = injector.getInstance(LocationService) const updateStateSpy = vi.spyOn(locationService, 'updateState') @@ -262,6 +273,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const editButton = page?.querySelector('tbody button') as HTMLButtonElement @@ -271,7 +283,7 @@ describe('UserListPage', () => { expect(updateStateSpy).toHaveBeenCalled() }) - it('should navigate to user details when table row is clicked', () => { + it('should navigate to user details when table row is clicked', async () => { const rootElement = document.getElementById('root') as HTMLDivElement const locationService = injector.getInstance(LocationService) const updateStateSpy = vi.spyOn(locationService, 'updateState') @@ -281,6 +293,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const row = page?.querySelector('tbody tr') as HTMLTableRowElement @@ -290,7 +303,7 @@ describe('UserListPage', () => { expect(updateStateSpy).toHaveBeenCalled() }) - it('should encode username in URL to handle special characters', () => { + it('should encode username in URL to handle special characters', async () => { usersObservable.setValue({ status: 'loaded', value: { @@ -307,6 +320,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const row = page?.querySelector('tbody tr') as HTMLTableRowElement @@ -331,6 +345,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() const page = document.querySelector('user-list-page') const retryButton = page?.querySelector('button') as HTMLButtonElement @@ -342,7 +357,7 @@ describe('UserListPage', () => { }) describe('service integration', () => { - it('should call findUsersAsObservable on render', () => { + it('should call findUsersAsObservable on render', async () => { const rootElement = document.getElementById('root') as HTMLDivElement initializeShadeRoot({ @@ -350,6 +365,7 @@ describe('UserListPage', () => { rootElement, jsxElement: , }) + await flushUpdates() expect(mockUsersService.findUsersAsObservable).toHaveBeenCalledWith({}) }) diff --git a/frontend/src/pages/ai/ai-chat-input.tsx b/frontend/src/pages/ai/ai-chat-input.tsx index d2f36e54..9a2eb738 100644 --- a/frontend/src/pages/ai/ai-chat-input.tsx +++ b/frontend/src/pages/ai/ai-chat-input.tsx @@ -11,10 +11,11 @@ export const AiChatInput = Shade<{ selectedChatId: string }>({ flexDirection: 'row', width: '100%', }, - render: ({ props, injector, useObservable, element }) => { + render: ({ props, injector, useObservable, useRef }) => { const aiChatMessageService = injector.getInstance(AiChatMessageService) const aiChatService = injector.getInstance(AiChatService) const sessionService = injector.getInstance(SessionService) + const formRef = useRef('form') const [selectedChat] = useObservable( 'selectedChat', @@ -27,6 +28,7 @@ export const AiChatInput = Shade<{ selectedChatId: string }>({ return ( + ref={formRef} onSubmit={({ message }) => { void aiChatMessageService .createChatMessage({ @@ -38,7 +40,12 @@ export const AiChatInput = Shade<{ selectedChatId: string }>({ owner: sessionService.currentUser.getValue()!.username, visibility: selectedChat?.value?.entries[0]?.visibility ?? 'private', }) - .then(() => element.querySelector('form')?.reset()) + .then(() => { + const form = formRef.current?.querySelector('form') ?? formRef.current + if (form && 'reset' in form) { + ;(form as HTMLFormElement).reset() + } + }) }} validate={(formData): formData is { message: string } => { return ( diff --git a/frontend/src/pages/ai/ai-chat-message-list.tsx b/frontend/src/pages/ai/ai-chat-message-list.tsx index dc64e805..02dc24e6 100644 --- a/frontend/src/pages/ai/ai-chat-message-list.tsx +++ b/frontend/src/pages/ai/ai-chat-message-list.tsx @@ -9,16 +9,17 @@ export const AiChatMessageList = Shade<{ selectedChatId: string }>({ shadowDomName: 'pi-rat-ai-chat-message-list', - style: { + css: { display: 'flex', flexDirection: 'column', width: '100%', height: 'calc(100% - 124px)', overflowY: 'auto', }, - render: ({ useObservable, injector, props, element, useDisposable }) => { + render: ({ useObservable, injector, props, useDisposable, useRef }) => { const { selectedChatId } = props const aiChatService = injector.getInstance(AiChatMessageService) + const containerRef = useRef('container') const [messages] = useObservable( 'messages', @@ -46,10 +47,13 @@ export const AiChatMessageList = Shade<{ const scrollToBottom = (behavior: ScrollBehavior = 'instant') => { setTimeout(() => { requestAnimationFrame(() => { - element.scrollTo({ - top: element.scrollHeight, - behavior, - }) + const el = containerRef.current + if (el) { + el.scrollTo({ + top: el.scrollHeight, + behavior, + }) + } }) }, 1) } diff --git a/frontend/src/pages/ai/create-ai-chat-button.tsx b/frontend/src/pages/ai/create-ai-chat-button.tsx index 69e86879..d87a4e7e 100644 --- a/frontend/src/pages/ai/create-ai-chat-button.tsx +++ b/frontend/src/pages/ai/create-ai-chat-button.tsx @@ -1,6 +1,5 @@ import { createComponent, Shade } from '@furystack/shades' import { Button, Form, Input, Modal, NotyService, Paper } from '@furystack/shades-common-components' -import { ObservableValue } from '@furystack/utils' import type { AiChat } from 'common' import { ErrorDisplay } from '../../components/error-display.js' import { SessionService } from '../../services/session.js' @@ -9,9 +8,9 @@ import { AiModelSelector } from './ai-model-selector.js' export const CreateAiChatButton = Shade({ shadowDomName: 'pi-rat-create-ai-chat-button', - render: ({ injector, useDisposable }) => { + render: ({ injector, useState }) => { const aiChatService = injector.getInstance(AiChatService) - const isModalOpen = useDisposable('isModalOpen', () => new ObservableValue(false)) + const [isModalOpen, setIsModalOpen] = useState('isModalOpen', false) const session = injector.getInstance(SessionService) const noty = injector.getInstance(NotyService) @@ -21,14 +20,14 @@ export const CreateAiChatButton = Shade({ isModalOpen.setValue(false)} + onClose={() => setIsModalOpen(false)} backdropStyle={{ position: 'fixed', top: '0', @@ -60,7 +59,7 @@ export const CreateAiChatButton = Shade({ name: chat.name.trim(), }) .then(() => { - isModalOpen.setValue(false) + setIsModalOpen(false) noty.emit('onNotyAdded', { type: 'success', body: `AI chat "${chat.name}" created successfully!`, diff --git a/frontend/src/pages/chat/add-chat-button.tsx b/frontend/src/pages/chat/add-chat-button.tsx index 76c28215..eb2ab7e8 100644 --- a/frontend/src/pages/chat/add-chat-button.tsx +++ b/frontend/src/pages/chat/add-chat-button.tsx @@ -1,14 +1,13 @@ import { createComponent, Shade } from '@furystack/shades' import { Button, Form, Input, Modal, Paper } from '@furystack/shades-common-components' -import { ObservableValue } from '@furystack/utils' import type { Chat } from 'common' import { SessionService } from '../../services/session.js' import { ChatService } from './chat-service.js' export const AddChatButton = Shade({ shadowDomName: 'shade-app-chat-add-chat-button', - render: ({ useDisposable, injector }) => { - const isModalOpen = useDisposable('isModalOpen', () => new ObservableValue(false)) + render: ({ useState, injector }) => { + const [isModalOpen, setIsModalOpen] = useState('isModalOpen', false) const session = injector.getInstance(SessionService) @@ -16,10 +15,10 @@ export const AddChatButton = Shade({ return ( <> - + isModalOpen.setValue(false)} + onClose={() => setIsModalOpen(false)} backdropStyle={{ position: 'fixed', top: '0', @@ -47,7 +46,7 @@ export const AddChatButton = Shade({ owner: session.currentUser.getValue()?.username || '', }) .then(() => { - isModalOpen.setValue(false) + setIsModalOpen(false) }) .catch((error) => { console.error('Error adding chat:', error) @@ -60,7 +59,7 @@ export const AddChatButton = Shade({ - + diff --git a/frontend/src/pages/chat/invite-button.tsx b/frontend/src/pages/chat/invite-button.tsx index cc7f08e8..1a173fb1 100644 --- a/frontend/src/pages/chat/invite-button.tsx +++ b/frontend/src/pages/chat/invite-button.tsx @@ -1,6 +1,5 @@ import { createComponent, Shade } from '@furystack/shades' import { Button, Form, Input, Modal, NotyService, Paper } from '@furystack/shades-common-components' -import { ObservableValue } from '@furystack/utils' import type { Chat } from 'common' import { ErrorDisplay } from '../../components/error-display.js' import { SessionService } from '../../services/session.js' @@ -8,8 +7,8 @@ import { ChatInvitationService } from './chat-intivation-service.js' export const InviteButton = Shade<{ chat: Chat }>({ shadowDomName: 'shade-app-invite-button', - render: ({ useDisposable, props, injector }) => { - const isModalOpen = useDisposable('isModalOpen', () => new ObservableValue(false)) + render: ({ useState, props, injector }) => { + const [isModalOpen, setIsModalOpen] = useState('isModalOpen', false) const currentUser = injector.getInstance(SessionService).currentUser.getValue() @@ -18,7 +17,7 @@ export const InviteButton = Shade<{ chat: Chat }>({ return ( <> - + ({ background: 'rgba(128,128,128, 0.3)', backdropFilter: 'blur(5px)', }} - onClose={() => isModalOpen.setValue(false)} + onClose={() => setIsModalOpen(false)} isVisible={isModalOpen} > ev.stopPropagation()}> @@ -56,7 +55,7 @@ export const InviteButton = Shade<{ chat: Chat }>({ message: formData.message.trim() || '', }) .then(() => { - isModalOpen.setValue(false) + setIsModalOpen(false) noty.emit('onNotyAdded', { type: 'success', title: 'โœ… Success', @@ -93,7 +92,7 @@ export const InviteButton = Shade<{ chat: Chat }>({
- +
diff --git a/frontend/src/pages/chat/message-input.tsx b/frontend/src/pages/chat/message-input.tsx index 1a358dea..77d79606 100644 --- a/frontend/src/pages/chat/message-input.tsx +++ b/frontend/src/pages/chat/message-input.tsx @@ -6,13 +6,15 @@ import { ChatMessageService } from './chat-messages-service.js' export const MessageInput = Shade<{ chat: Chat }>({ shadowDomName: 'shade-app-message-input', - render: ({ injector, props, element }) => { + render: ({ injector, props, useRef }) => { const chatService = injector.getInstance(ChatMessageService) const session = injector.getInstance(SessionService) const theme = injector.getInstance(ThemeProviderService) + const formRef = useRef('form') return ( + ref={formRef} onSubmit={(formData) => { void chatService.addChatMessage({ id: crypto.randomUUID(), @@ -22,9 +24,9 @@ export const MessageInput = Shade<{ chat: Chat }>({ owner: session.currentUser.getValue()?.username || '', attachments: [], }) - const form = element.firstElementChild as HTMLFormElement - if (form) { - form.reset() + const form = formRef.current?.querySelector('form') ?? formRef.current + if (form && 'reset' in form) { + ;(form as HTMLFormElement).reset() } }} validate={(formData: unknown): formData is { content: string } => { diff --git a/frontend/src/pages/chat/message-list.tsx b/frontend/src/pages/chat/message-list.tsx index 7b5818ae..3d771271 100644 --- a/frontend/src/pages/chat/message-list.tsx +++ b/frontend/src/pages/chat/message-list.tsx @@ -42,7 +42,7 @@ const ChatLineAvatar = styledElement('div', { export const MessageList = Shade<{ chat: Chat }>({ shadowDomName: 'shade-app-message-list', - style: { + css: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start', @@ -50,13 +50,17 @@ export const MessageList = Shade<{ chat: Chat }>({ overflowY: 'auto', width: '100%', }, - render: ({ injector, props, useObservable, element }) => { + render: ({ injector, props, useObservable, useRef }) => { + const listRef = useRef('list') setTimeout(() => { requestAnimationFrame(() => { - element.scrollTo({ - behavior: 'instant', - top: Math.max(element.scrollHeight, element.offsetHeight), - }) + const el = listRef.current + if (el) { + el.scrollTo({ + behavior: 'instant', + top: Math.max(el.scrollHeight, el.offsetHeight), + }) + } }) }, 1) const chatMessageService = injector.getInstance(ChatMessageService) diff --git a/frontend/src/pages/entities/dashboards.tsx b/frontend/src/pages/entities/dashboards.tsx index b481d0af..a7eb12fb 100644 --- a/frontend/src/pages/entities/dashboards.tsx +++ b/frontend/src/pages/entities/dashboards.tsx @@ -1,4 +1,4 @@ -import { createComponent, RouteLink, Shade } from '@furystack/shades' +import { createComponent, NestedRouteLink, Shade } from '@furystack/shades' import { Dashboard } from 'common' import dashboardSchemas from 'common/schemas/dashboard-entities.json' with { type: 'json' } import { GenericEditorService } from '../../components/generic-editor/generic-editor-service.js' @@ -55,7 +55,7 @@ export const DashboardsPage = Shade({ styles={{}} rowComponents={{ id: ({ id }) => { - return Preview + return Preview }, }} modelUri={modelUri} diff --git a/frontend/src/pages/file-browser/create-drive-wizard.tsx b/frontend/src/pages/file-browser/create-drive-wizard.tsx index 438c133e..7ce03a7c 100644 --- a/frontend/src/pages/file-browser/create-drive-wizard.tsx +++ b/frontend/src/pages/file-browser/create-drive-wizard.tsx @@ -1,7 +1,6 @@ import { createComponent, Shade } from '@furystack/shades' import type { WizardStepProps } from '@furystack/shades-common-components' import { Button, fadeIn, fadeOut, Input, Modal, NotyService, Wizard } from '@furystack/shades-common-components' -import { ObservableValue } from '@furystack/utils' import { WizardStep } from '../../components/wizard-step.js' import { DrivesService } from '../../services/drives-service.js' import { getErrorMessage } from '../../services/get-error-message.js' @@ -67,21 +66,21 @@ export const AddDriveStep = Shade({ export const CreateDriveWizard = Shade<{ onDriveAdded?: () => void }>({ shadowDomName: 'create-drive-wizard', - render: ({ useDisposable, props }) => { - const isOpened = useDisposable('isOpened', () => new ObservableValue(false)) + render: ({ useState, props }) => { + const [isOpened, setIsOpened] = useState('isOpened', false) return ( <> isOpened.setValue(false)} + onClose={() => setIsOpened(false)} showAnimation={fadeIn} hideAnimation={fadeOut} > { - isOpened.setValue(false) + setIsOpened(false) props.onDriveAdded?.() }} > @@ -90,7 +89,7 @@ export const CreateDriveWizard = Shade<{ onDriveAdded?: () => void }>({ style={{ position: 'fixed', bottom: '1em', right: '1em', zIndex: '1' }} variant="outlined" color="success" - onclick={() => isOpened.setValue(true)} + onclick={() => setIsOpened(true)} title="Add Drive" > + diff --git a/frontend/src/pages/file-browser/file-context-menu.tsx b/frontend/src/pages/file-browser/file-context-menu.tsx index 009555c5..a2c50dd7 100644 --- a/frontend/src/pages/file-browser/file-context-menu.tsx +++ b/frontend/src/pages/file-browser/file-context-menu.tsx @@ -1,6 +1,5 @@ import { Shade, createComponent } from '@furystack/shades' import { NotyService } from '@furystack/shades-common-components' -import { ObservableValue } from '@furystack/utils' import type { DirectoryEntry } from 'common' import { getFallbackMetadata, getFullPath, isMovieFile, isSampleFile } from 'common' import { ContextMenu } from '../../components/context-menu.js' @@ -15,10 +14,10 @@ export const FileContextMenu = Shade<{ open: () => void }>({ shadowDomName: 'file-context-menu', - render: ({ children, props, useDisposable, injector }) => { + render: ({ children, props, useState, injector }) => { const { entry, currentDriveLetter, currentPath, open } = props - const isInfoVisible = useDisposable('isInfoVisible', () => new ObservableValue(false)) - const isRelatedMoviesVisible = useDisposable('isRelatedMoviesVisible', () => new ObservableValue(false)) + const [isInfoVisible, setInfoVisible] = useState('isInfoVisible', false) + const [isRelatedMoviesVisible, setRelatedMoviesVisible] = useState('isRelatedMoviesVisible', false) const path = `${currentDriveLetter}:${currentPath}/${entry.name}` const movieMetadata = props.entry.isFile && !isSampleFile(path) && isMovieFile(path) && getFallbackMetadata(path) @@ -45,7 +44,7 @@ export const FileContextMenu = Shade<{ movieMetadata.type === 'episode' ? `S${movieMetadata.season}E${movieMetadata.episode}` : '' }`, onClick: () => { - isRelatedMoviesVisible.setValue(true) + setRelatedMoviesVisible(true) }, }, { @@ -123,7 +122,7 @@ export const FileContextMenu = Shade<{ icon: { type: 'font', value: 'โ„น๏ธ' } as const, label: 'Show file info', onClick: () => { - isInfoVisible.setValue(true) + setInfoVisible(true) }, }, ]} @@ -133,6 +132,7 @@ export const FileContextMenu = Shade<{ setInfoVisible(false)} currentDriveLetter={currentDriveLetter} currentPath={currentPath} /> @@ -142,6 +142,7 @@ export const FileContextMenu = Shade<{ path={currentPath} file={entry} isOpened={isRelatedMoviesVisible} + onClose={() => setRelatedMoviesVisible(false)} /> )} diff --git a/frontend/src/pages/file-browser/file-info-modal.tsx b/frontend/src/pages/file-browser/file-info-modal.tsx index 8859388c..e35771e4 100644 --- a/frontend/src/pages/file-browser/file-info-modal.tsx +++ b/frontend/src/pages/file-browser/file-info-modal.tsx @@ -1,12 +1,12 @@ import { Shade, createComponent } from '@furystack/shades' import { Button, Modal, Paper, fadeIn, fadeOut } from '@furystack/shades-common-components' -import type { ObservableValue } from '@furystack/utils' import type { DirectoryEntry } from 'common' import { FileIcon } from './file-icon.js' export const FileInfoModal = Shade<{ entry: DirectoryEntry - isInfoVisible: ObservableValue + isInfoVisible: boolean + onClose: () => void currentDriveLetter: string currentPath: string }>({ @@ -33,7 +33,7 @@ export const FileInfoModal = Shade<{ }, }, render: ({ props }) => { - const { entry, isInfoVisible, currentDriveLetter, currentPath } = props + const { entry, isInfoVisible, onClose, currentDriveLetter, currentPath } = props return ( isInfoVisible.setValue(false)} + onClose={onClose} showAnimation={fadeIn} hideAnimation={fadeOut} > @@ -91,7 +91,7 @@ export const FileInfoModal = Shade<{
- +
diff --git a/frontend/src/pages/logging/log-entries-terminal.tsx b/frontend/src/pages/logging/log-entries-terminal.tsx index 0e1144c7..7962e22d 100644 --- a/frontend/src/pages/logging/log-entries-terminal.tsx +++ b/frontend/src/pages/logging/log-entries-terminal.tsx @@ -1,6 +1,6 @@ import type { CacheResult } from '@furystack/cache' import type { GetCollectionResult } from '@furystack/rest' -import { Shade, type RenderOptions } from '@furystack/shades' +import { Shade, createComponent, type RenderOptions } from '@furystack/shades' import { FitAddon } from '@xterm/addon-fit' import { SearchAddon } from '@xterm/addon-search' import { WebLinksAddon } from '@xterm/addon-web-links' @@ -12,8 +12,21 @@ import { logEntryRoute } from '../../components/routes/logging-routes.js' import { navigateToRoute } from '../../navigate-to-route.js' import { LoggingService } from '../../services/logging-service.js' -const useDisposableTerminal = ({ useDisposable, element, injector }: RenderOptions) => { +const useDisposableTerminal = ( + { useDisposable, injector }: Pick, 'useDisposable' | 'injector'>, + containerEl: HTMLElement | null, +) => { return useDisposable('terminal', () => { + if (!containerEl) { + const dummyTerminal = new Terminal() + return { + terminal: dummyTerminal, + fitAddon: new FitAddon(), + searchAddon: new SearchAddon(), + webLinksAddon: new WebLinksAddon(), + [Symbol.dispose]: () => dummyTerminal.dispose(), + } + } const terminal = new Terminal({ linkHandler: { activate: (ev: MouseEvent, url: string) => { @@ -34,7 +47,7 @@ const useDisposableTerminal = ({ useDisposable, element, injector }: RenderOptio terminal.loadAddon(fitAddon) terminal.loadAddon(searchAddon) terminal.loadAddon(webLinksAddon) - terminal.open(element) + terminal.open(containerEl) fitAddon.fit() return { @@ -68,8 +81,6 @@ const fillTerminalWithLogEntries = (terminal: Terminal, logEntries: CacheResult< ? '\x1B[36mD\x1B[0m' : '\x1B[32mI\x1B[0m' - // Try the OSC8 link syntax - const url = compile(logEntryRoute.url)({ id: logEntry.id }) const showMoreLink = `\x1B]8;;${window.location.origin}${url}\x1B\\[show more]\x1B]8;;\x1B\\` @@ -79,7 +90,10 @@ const fillTerminalWithLogEntries = (terminal: Terminal, logEntries: CacheResult< }) } -const useLogEntries = ({ injector, useObservable }: RenderOptions, terminal: Terminal) => { +const useLogEntries = ( + { injector, useObservable }: Pick, 'injector' | 'useObservable'>, + terminal: Terminal, +) => { return useObservable( 'logEntries', injector.getInstance(LoggingService).findLogEntryAsObservable({ @@ -102,13 +116,13 @@ export const LogEntriesTerminal = Shade({ width: '100%', height: '100%', }, - constructed: (renderOptions) => { - const { terminal } = useDisposableTerminal(renderOptions) + render: (renderOptions) => { + const containerRef = renderOptions.useRef('container') + const { terminal } = useDisposableTerminal(renderOptions, containerRef.current) const [entries] = useLogEntries(renderOptions, terminal) fillTerminalWithLogEntries(terminal, entries) - }, - render: () => { - return null + + return
}, }) diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index ef276e43..b93fac2c 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -25,22 +25,9 @@ export const Login = Shade({ padding: '1em 0', }, }, - constructed: ({ element }) => { - element.querySelector('input[autofocus]')?.focus() - }, - render: ({ injector, useObservable, element }) => { + render: ({ injector, useObservable }) => { const sessionService = injector.getInstance(SessionService) - useObservable('isOperationInProgress', sessionService.isOperationInProgress, { - onChange: (isOperationInProgress) => { - const els = [...element.querySelectorAll('input').values(), ...element.querySelectorAll('button').values()] - els.forEach((el) => { - el.disabled = isOperationInProgress - }) - if (!isOperationInProgress) { - element.querySelector('input[autofocus]')?.focus() - } - }, - }) + const [isOperationInProgress] = useObservable('isOperationInProgress', sessionService.isOperationInProgress) return ( @@ -51,13 +38,24 @@ export const Login = Shade({ onSubmit={({ userName, password }) => void sessionService.login(userName, password)} >

Login

- - + +
- -
diff --git a/frontend/src/pages/movies/movie-overview.tsx b/frontend/src/pages/movies/movie-overview.tsx index 4a09a228..9da8e5ff 100644 --- a/frontend/src/pages/movies/movie-overview.tsx +++ b/frontend/src/pages/movies/movie-overview.tsx @@ -93,7 +93,8 @@ export const PlayButtons = Shade<{ imdbId: string }>({ export const MovieOverview = Shade<{ imdbId: string }>({ shadowDomName: 'shade-movie-overview', - render: ({ props, useObservable, injector, element }) => { + render: ({ props, useObservable, injector, useRef }) => { + const imgRef = useRef('posterImg') const [currentUser] = useObservable('currentUser', injector.getInstance(SessionService).currentUser) const [isDesktop] = useObservable('isDesktop', injector.getInstance(ScreenService).screenSize.atLeast.md) const movieService = injector.getInstance(MoviesService) @@ -103,7 +104,7 @@ export const MovieOverview = Shade<{ imdbId: string }>({ if (isLoadedCacheResult(movieResult)) { setTimeout(() => { void promisifyAnimation( - element.querySelector('img'), + imgRef.current, [ { opacity: 0, transform: 'scale(0.85)' }, { opacity: 1, transform: 'scale(1)' }, @@ -131,6 +132,7 @@ export const MovieOverview = Shade<{ imdbId: string }>({ >
{`thumbnail({ shadowDomName: 'pirat-movie-player-v2', - constructed: ({ useDisposable, element, injector, props }) => { - const getVideo = () => element.querySelector('video') as HTMLVideoElement + render: ({ props, useDisposable, useRef, injector }) => { + const videoRef = useRef('video') + const containerRef = useRef('container') const { driveLetter, path } = props.file const watchProgressService = injector.getInstance(WatchProgressService) - const watchProgressUpdater = useDisposable('watchProgressUpdater', () => { - const video = getVideo() + useDisposable('watchProgressUpdater', () => { + const video = videoRef.current + if (!video) { + return { [Symbol.asyncDispose]: async () => {} } + } return new WatchProgressUpdater({ intervalMs: 10 * 1000, onSave: async (progress) => { @@ -44,12 +48,6 @@ export const MoviePlayerV2 = Shade({ videoElement: video, }) }) - - return () => { - void watchProgressUpdater[Symbol.asyncDispose]() - } - }, - render: ({ props, element, useDisposable, injector }) => { const { watchProgress, file } = props const api = injector.getInstance(MediaApiClient) @@ -66,11 +64,16 @@ export const MoviePlayerV2 = Shade({ }) useDisposable('mouseMoveListener', () => { + const container = containerRef.current + if (!container) { + return { [Symbol.dispose]: () => {} } + } + const elementHideDelay = 3000 const createTimedOutHide = () => setTimeout(() => { - element.querySelectorAll('.hideOnPlay').forEach((el) => { + container.querySelectorAll('.hideOnPlay').forEach((el) => { void promisifyAnimation( el, [ @@ -94,7 +97,7 @@ export const MoviePlayerV2 = Shade({ const onMouseMove = () => { clearTimeout(timeoutId) - element.querySelectorAll('.hideOnPlay').forEach((el) => { + container.querySelectorAll('.hideOnPlay').forEach((el) => { void promisifyAnimation( el, [ @@ -114,11 +117,11 @@ export const MoviePlayerV2 = Shade({ }) timeoutId = createTimedOutHide() } - element.addEventListener('mousemove', onMouseMove) + container.addEventListener('mousemove', onMouseMove) return { [Symbol.dispose]: () => { - element.removeEventListener('mousemove', onMouseMove) + container.removeEventListener('mousemove', onMouseMove) }, } }) @@ -126,6 +129,7 @@ export const MoviePlayerV2 = Shade({ if (ENABLE_MEDIA_CHROME) { return (
({