From 9fe06cb1924ccb576b152c2546854c21bb7fbdbe Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Mon, 24 Nov 2025 11:14:46 -0500 Subject: [PATCH 01/54] wip. camera source button and progress indicator working --- js/bun.lock | 255 ++++++++++++++++-- js/bun.lockb | Bin 0 -> 176544 bytes js/hang-demo/package.json | 3 +- js/hang-demo/src/publish.html | 1 + js/hang-demo/src/publish.ts | 1 + js/hang-ui/.prettierrc | 12 + js/hang-ui/Status.tsx | 0 js/hang-ui/package.json | 30 +++ js/hang-ui/rollup.config.mjs | 19 ++ .../publish/CameraPublishSourceButton.tsx | 32 +++ .../publish/MediaSourceSelector.tsx | 49 ++++ .../Components/publish/PublishControls.tsx | 34 +++ .../publish/PublishStatusIndicator.tsx | 32 +++ js/hang-ui/src/Components/publish/element.tsx | 55 ++++ .../src/Components/publish/publish-types.ts | 13 + js/hang-ui/src/Components/publish/styles.css | 32 +++ js/hang-ui/src/css.d.ts | 4 + js/hang-ui/src/types.ts | 0 js/hang-ui/tsconfig.json | 19 ++ js/hang/package.json | 1 + js/hang/src/publish/element.ts | 84 +++--- js/package.json | 3 +- 22 files changed, 618 insertions(+), 61 deletions(-) create mode 100755 js/bun.lockb create mode 100644 js/hang-ui/.prettierrc create mode 100644 js/hang-ui/Status.tsx create mode 100644 js/hang-ui/package.json create mode 100644 js/hang-ui/rollup.config.mjs create mode 100644 js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx create mode 100644 js/hang-ui/src/Components/publish/MediaSourceSelector.tsx create mode 100644 js/hang-ui/src/Components/publish/PublishControls.tsx create mode 100644 js/hang-ui/src/Components/publish/PublishStatusIndicator.tsx create mode 100644 js/hang-ui/src/Components/publish/element.tsx create mode 100644 js/hang-ui/src/Components/publish/publish-types.ts create mode 100644 js/hang-ui/src/Components/publish/styles.css create mode 100644 js/hang-ui/src/css.d.ts create mode 100644 js/hang-ui/src/types.ts create mode 100644 js/hang-ui/tsconfig.json diff --git a/js/bun.lock b/js/bun.lock index 1b9ff5286..9af1dd888 100644 --- a/js/bun.lock +++ b/js/bun.lock @@ -9,7 +9,7 @@ }, "hang": { "name": "@kixelated/hang", - "version": "0.6.2", + "version": "0.7.0", "dependencies": { "@kixelated/libavjs-webcodecs-polyfill": "^0.5.5", "@kixelated/moq": "workspace:^", @@ -20,6 +20,7 @@ "zod": "^4.1.5", }, "devDependencies": { + "@kixelated/hang-ui": "workspace:^", "@types/audioworklet": "^0.0.77", "@typescript/lib-dom": "npm:@types/web@^0.0.241", "fast-glob": "^3.3.3", @@ -33,6 +34,7 @@ "version": "0.1.0", "dependencies": { "@kixelated/hang": "workspace:^", + "@kixelated/hang-ui": "workspace:^", }, "devDependencies": { "@biomejs/biome": "^2.2.2", @@ -46,6 +48,19 @@ "vite-plugin-html": "^3.2.2", }, }, + "hang-ui": { + "name": "@kixelated/hang-ui", + "version": "0.1.0", + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-typescript": "^12.3.0", + "rollup": "^4.53.3", + "rollup-plugin-string": "^3.0.0", + "solid-element": "^1.9.1", + "solid-js": "^1.9.10", + "unplugin-solid": "^1.0.0", + }, + }, "moq": { "name": "@kixelated/moq", "version": "0.9.4", @@ -125,13 +140,41 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], "@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="], @@ -231,6 +274,8 @@ "@kixelated/hang-demo": ["@kixelated/hang-demo@workspace:hang-demo"], + "@kixelated/hang-ui": ["@kixelated/hang-ui@workspace:hang-ui"], + "@kixelated/libavjs-webcodecs-polyfill": ["@kixelated/libavjs-webcodecs-polyfill@0.5.5", "", { "dependencies": { "@libav.js/types": "^6.7.7", "@ungap/global-this": "^0.4.4" } }, "sha512-Q1zgnTMMQ2F7IE9ylx3C1XzVbg5vYN18jiDINO5U3kNPBOHdYuUlJsMhtBoqr1M6ocLtoiqdHmLs7tHFgrw5KA=="], "@kixelated/moq": ["@kixelated/moq@workspace:moq"], @@ -253,49 +298,57 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], + + "@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.3.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big=="], + "@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.1", "", { "os": "android", "cpu": "arm64" }, "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.1", "", { "os": "none", "cpu": "arm64" }, "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.1", "", { "os": "win32", "cpu": "x64" }, "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="], @@ -353,6 +406,8 @@ "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.43.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.43.0", "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.43.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA=="], @@ -409,10 +464,16 @@ "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -421,6 +482,8 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -429,6 +492,8 @@ "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -459,6 +524,8 @@ "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + "component-register": ["component-register@0.8.8", "", {}, "sha512-djhwcxjY+X9dacaYUEOkOm7tda8uOEDiMDigWysu3xv54M8o6XDlsjR1qt5Y8QLGiKg51fqXFIR2HUTmt9ys0Q=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], @@ -467,6 +534,8 @@ "consola": ["consola@2.15.3", "", {}, "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], @@ -483,6 +552,8 @@ "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], "dependency-tree": ["dependency-tree@11.2.0", "", { "dependencies": { "commander": "^12.1.0", "filing-cabinet": "^5.0.3", "precinct": "^12.2.0", "typescript": "^5.8.3" }, "bin": { "dependency-tree": "bin/cli.js" } }, "sha512-+C1H3mXhcvMCeu5i2Jpg9dc0N29TWTuT6vJD7mHLAfVmAbo9zW8NlkvQ1tYd3PDMab0IRQM0ccoyX68EZtx9xw=="], @@ -527,11 +598,13 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], - "entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], @@ -575,6 +648,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-amd-module-type": ["get-amd-module-type@6.0.1", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" } }, "sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -599,6 +674,8 @@ "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + "html-minifier-terser": ["html-minifier-terser@6.1.0", "", { "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", "commander": "^8.3.0", "he": "^1.2.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" } }, "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -619,6 +696,8 @@ "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], @@ -631,6 +710,8 @@ "is-url-superb": ["is-url-superb@4.0.0", "", {}, "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA=="], + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], @@ -643,6 +724,8 @@ "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], @@ -687,6 +770,8 @@ "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + "merge-anything": ["merge-anything@6.0.6", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-F3K1W45PvTjRZzbcYIhXntNr8cux00gUxR8IzNPPG+80gNlAHZGVBwFyN4x5yjw/7QkLPKDbRQBK4KrJKo69mw=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], @@ -715,6 +800,8 @@ "node-html-parser": ["node-html-parser@5.4.2", "", { "dependencies": { "css-select": "^4.2.1", "he": "1.2.0" } }, "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-source-walk": ["node-source-walk@7.0.1", "", { "dependencies": { "@babel/parser": "^7.26.7" } }, "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -731,6 +818,8 @@ "parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -789,7 +878,11 @@ "rimraf": ["rimraf@6.0.1", "", { "dependencies": { "glob": "^11.0.0", "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A=="], - "rollup": ["rollup@4.50.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.1", "@rollup/rollup-android-arm64": "4.50.1", "@rollup/rollup-darwin-arm64": "4.50.1", "@rollup/rollup-darwin-x64": "4.50.1", "@rollup/rollup-freebsd-arm64": "4.50.1", "@rollup/rollup-freebsd-x64": "4.50.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1", "@rollup/rollup-linux-arm64-musl": "4.50.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1", "@rollup/rollup-linux-x64-gnu": "4.50.1", "@rollup/rollup-linux-x64-musl": "4.50.1", "@rollup/rollup-openharmony-arm64": "4.50.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1", "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA=="], + "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + + "rollup-plugin-string": ["rollup-plugin-string@3.0.0", "", { "dependencies": { "rollup-pluginutils": "^2.4.1" } }, "sha512-vqyzgn9QefAgeKi+Y4A7jETeIAU1zQmS6VotH6bzm/zmUQEnYkpIGRaOBPY41oiWYV4JyBoGAaBjYMYuv+6wVw=="], + + "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -799,7 +892,7 @@ "sass-lookup": ["sass-lookup@6.1.0", "", { "dependencies": { "commander": "^12.1.0", "enhanced-resolve": "^5.18.0" }, "bin": { "sass-lookup": "bin/cli.js" } }, "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], @@ -815,7 +908,11 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="], + "solid-element": ["solid-element@1.9.1", "", { "dependencies": { "component-register": "^0.8.7" }, "peerDependencies": { "solid-js": "^1.9.3" } }, "sha512-baJy6Qz27oAUgkPlqOf3Y+7RsBiuVQrS51Nrh1ddDbrqrNPvJbIvehpUsTzLNFb2ZHIoHuNnDg330go/ZKcRdg=="], + + "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], + + "solid-refresh": ["solid-refresh@0.7.5", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-ZYMbjWsy7IwSF3+oZCNnReiTYSyCAFRvC7oLUKxxh1wPa6/6YIWqsxa+Ma2kM4F/ypWT69B1c0fmKeZRdLueGw=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -893,6 +990,12 @@ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "unplugin-solid": ["unplugin-solid@1.0.0", "", { "dependencies": { "@babel/core": "^7.28.3", "@rollup/pluginutils": "^5.2.0", "babel-preset-solid": "^1.9.9", "merge-anything": "^6.0.6", "solid-refresh": "^0.7.5", "unplugin": "^2.3.10", "vitefu": "^1.1.1" }, "peerDependencies": { "solid-js": "^1.9.9" } }, "sha512-pv1CS3XMtf3WwX8Dq9Bvo4qH6mfjN2xOgbaPcnqW1dLhyP/JQCvueGEsN0dYIZ4JvxaD/G/Ot1JnBzNQGHkfeA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="], @@ -901,12 +1004,16 @@ "vite-plugin-html": ["vite-plugin-html@3.2.2", "", { "dependencies": { "@rollup/pluginutils": "^4.2.0", "colorette": "^2.0.16", "connect-history-api-fallback": "^1.6.0", "consola": "^2.15.3", "dotenv": "^16.0.0", "dotenv-expand": "^8.0.2", "ejs": "^3.1.6", "fast-glob": "^3.2.11", "fs-extra": "^10.0.1", "html-minifier-terser": "^6.1.0", "node-html-parser": "^5.3.3", "pathe": "^0.2.0" }, "peerDependencies": { "vite": ">=2.0.0" } }, "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q=="], + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], "walkdir": ["walkdir@0.4.1", "", {}, "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -927,12 +1034,22 @@ "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@rollup/plugin-node-resolve/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/plugin-typescript/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -951,16 +1068,26 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@vue/compiler-core/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + "@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "dependency-tree/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "filing-cabinet/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], @@ -973,26 +1100,98 @@ "module-lookup-amd/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "node-source-walk/@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + "precinct/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + "sass-lookup/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "solid-refresh/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + "stylus-lookup/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "unplugin-solid/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "vite/rollup": ["rollup@4.50.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.1", "@rollup/rollup-android-arm64": "4.50.1", "@rollup/rollup-darwin-arm64": "4.50.1", "@rollup/rollup-darwin-x64": "4.50.1", "@rollup/rollup-freebsd-arm64": "4.50.1", "@rollup/rollup-freebsd-x64": "4.50.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1", "@rollup/rollup-linux-arm64-musl": "4.50.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1", "@rollup/rollup-linux-x64-gnu": "4.50.1", "@rollup/rollup-linux-x64-musl": "4.50.1", "@rollup/rollup-openharmony-arm64": "4.50.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1", "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA=="], + "vite-plugin-html/pathe": ["pathe@0.2.0", "", {}, "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/plugin-typescript/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "@vue/compiler-sfc/@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + "module-lookup-amd/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "node-source-walk/@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "solid-refresh/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "unplugin-solid/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.1", "", { "os": "android", "cpu": "arm" }, "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag=="], + + "vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.1", "", { "os": "android", "cpu": "arm64" }, "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw=="], + + "vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw=="], + + "vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw=="], + + "vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA=="], + + "vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ=="], + + "vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg=="], + + "vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.1", "", { "os": "linux", "cpu": "arm" }, "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw=="], + + "vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw=="], + + "vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w=="], + + "vite/rollup/@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.1", "", { "os": "linux", "cpu": "none" }, "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg=="], + + "vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg=="], + + "vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA=="], + + "vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.1", "", { "os": "linux", "cpu": "x64" }, "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg=="], + + "vite/rollup/@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.1", "", { "os": "none", "cpu": "arm64" }, "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA=="], + + "vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ=="], + + "vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A=="], + + "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.1", "", { "os": "win32", "cpu": "x64" }, "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA=="], + + "@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@vue/compiler-sfc/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + "module-lookup-amd/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "node-source-walk/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], } } diff --git a/js/bun.lockb b/js/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..7d27e5ce545595b6a9b56793a9df12289c9daa5a GIT binary patch literal 176544 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πsxE69gQOh`VeXgBqO zzm?T?^J>l8dk4(Y^n3tq0n!l$0e%u!DRTleWK77VQ)NrzABhHM3{UFwz+zbErPe4Bt2(JRKcXVE*$?A4PEnRJ;B?SI()=2$7@T2N7(C1h+F{b9%4DU3 zB8yF8-?(OAin;H=P>Qk#DU{E@+VTt}j*Z08RD~fa6Sz`@O-@@oz!QDyOcBTs)atiP zJmigm?ITbYP?%OG|T`pYWW7U|3G256a} zd+FAU$KvESlstqO-&TkncYR7fPNXY`w=VpnjnvQj*WyoFTp%+xwIFcdM;D^5w8$r_HJ) z55_R~g9}W3LhzXl#rG=xd8j&<7q?rpCml-d6{t&S^pFK|w0ZQ+H6V ziFrU0!%0?eSHruXR~sy4g7K*(DHJRewVol7$8?xjS~|f3I_P0iKac2+KN)`A$&Vsg ztn`Vu$z1R;=3qw!dwSMHlQEAB?}UEjqm`ju>3X+$#ipH;XY7r2cGA-k#8T`P=4HK; z6~ksLQ^{odhJcB<;g_>&FEw*Q!I;IY4=WerxV^~gci6((h)+2`yRC_t z;{cF6{y`-t9TuN{;|UdE1+BDHK^i!VxXy#jI`uhctj1s(ONKU1+dlZXfidSNnDVI3 z_Re=*)zwi{Q`dN^8Nq()6O~7i*IJI^8$)1Zl!w?P!E4@dLtwe`w0R~=Miy6=Y6@ux!qf%Bu z6Tn4W1>cs|^;B(ze&0Mr4rDV?ISYDDJ)fvhdm2bcH;b_xRaV|)tdmz1G}0{zxZGW8 z?IK(3EGC|(F!jkp8DD&1ucq^>dVhX(e^w#ib{f7b1$_}`Wc%GcnaVL~UhhR`#}KJr zX*tde;^Ra*{nzcDMTNexoiy0Sl2w7HqD%3$)h&)Q5AhqbW$tOodS)Fqvx`=sDdM`H zY)vL*quUER_SQ`t5lPJ;$6`LZX?QVVJEP?XCFObzDEEPor_S`j9+SS51~$2LmxoSn zr05Qhotg~*sQHN1`V49!C}qd6t}HEKG?orz3?nYI_bsmV@mc`xA&O-*X0<2H(O5H=699R!=OV_dP&th1YhJ(u`WHFED1b= zvRNkDV|Y^az|lfpJ>xAcy(Cc&$p`QMhS#=tG0PM&P<`U6H-uFh1) zN3<7W$0>9P$c~q_t~akj2A$19#IlCiiYLB1Y^RhaxlN9pLx}b3`L=(t*}r=6IAeg? z#6S*oaUlyj6S>4G+6s;sL}4w;l<<{D#f@WXHyKi0hQI2#H#(z4y*~3$JLe9NMQL!d zh$n_tMn}dR;Ir5yWy1&~z%r7mf{@N)OanJB<)y6(Q85q5Vwj%UzKhLeCC(H?f$gD5 z)d=hi?JMBk)RUMfXF+nzpubvTQ|2rB7ogVBQfX$rAUiiM2Ul4oRuP_0=vFHUkAtqQ zsEjHUpkr8%TAS;%S3*Y<&?Y0qVO*hlxxw-a2>sPcPc6%aC!iKc46Bq@NFaa~aWJ%v zd143A^Cza4N#)9DA}=L+NR?Dg)sS=5rX?ZQrm`t~&E`S?WlChv=O>?W=IOSx?(-!! zCTK_Hn7`ZYs%rE>P7n`Lw8u9aFid<_JXq`!@8@ziu061&E~!fHpGV!bHR;cCDF$4; z84Pr{_rrn8oM2g>S^|X31?Z6g^Ux$NWy=LSIq~a2$T9P%e8OR zmfh>>R%Vb>=qh2M&2Mc~1|csi1Ib^c1a9lJMuChNUFQLcVOS_8Aa%)2a&I>q;d}@} zV7s9uGn^1ALy#p$MqRzUet(Nj;sG!Z!DAe%w8-?~n2W%m$fLU8ebiEOWm2_Joi2ze5PO1o!LB-P><{@9KxY_TPW?ueb00 z?Qj0Z@BZ`CcfR?5|MlJ1zx0Fe{`Qal=c`})@sHpAmmhucH~;%9Klr1+`_-TP-nahZ z-+umwfBcod|Mh?Wv;X){$NgLXXl1?qNx!>!N(=9)-BZ6k`z`Tkv{`@p +

Other demos:

diff --git a/js/hang-demo/src/publish.ts b/js/hang-demo/src/publish.ts index a5985e0f0..69ce463fa 100644 --- a/js/hang-demo/src/publish.ts +++ b/js/hang-demo/src/publish.ts @@ -1,4 +1,5 @@ import "./highlight"; +import "@kixelated/hang-ui/publish/element"; // We need to import Web Components with fully-qualified paths because of tree-shaking. import HangPublish from "@kixelated/hang/publish/element"; diff --git a/js/hang-ui/.prettierrc b/js/hang-ui/.prettierrc new file mode 100644 index 000000000..8e2d018e8 --- /dev/null +++ b/js/hang-ui/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 80, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/js/hang-ui/Status.tsx b/js/hang-ui/Status.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/js/hang-ui/package.json b/js/hang-ui/package.json new file mode 100644 index 000000000..db87e3db7 --- /dev/null +++ b/js/hang-ui/package.json @@ -0,0 +1,30 @@ +{ + "name": "@kixelated/hang-ui", + "type": "module", + "version": "0.1.0", + "description": "Media over QUIC library UI components", + "license": "(MIT OR Apache-2.0)", + "repository": "github:kixelated/moq", + "main": "dist/publish-controls.esm.js", + "module": "dist/publish-controls.esm.js", + "exports": { + "./publish/element": { + "import": "./dist/publish-controls.esm.js", + "types": "./dist/Components/publish/publish-types.d.ts" + } + }, + "scripts": { + "build": "rollup -c && npm run build:types", + "build:types": "tsc -p tsconfig.json", + "clean": "rm -rf dist" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-typescript": "^12.3.0", + "rollup": "^4.53.3", + "rollup-plugin-string": "^3.0.0", + "solid-element": "^1.9.1", + "solid-js": "^1.9.10", + "unplugin-solid": "^1.0.0" + } +} diff --git a/js/hang-ui/rollup.config.mjs b/js/hang-ui/rollup.config.mjs new file mode 100644 index 000000000..bdf89067e --- /dev/null +++ b/js/hang-ui/rollup.config.mjs @@ -0,0 +1,19 @@ +import solid from 'unplugin-solid/rollup'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; +import { string } from 'rollup-plugin-string'; + +export default { + input: 'src/Components/publish/element.tsx', + output: { + file: 'dist/publish-controls.esm.js', + format: 'es', + sourcemap: true, + }, + plugins: [ + string({ include: '**/*.css' }), + solid({ dev: false, hydratable: false }), + nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), + typescript(), + ], +}; diff --git a/js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx b/js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx new file mode 100644 index 000000000..eab52d009 --- /dev/null +++ b/js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx @@ -0,0 +1,32 @@ +import MediaSourceSourceSelector from './MediaSourceSelector'; +import type { PublishButtonProps } from './publish-types'; + +interface CameraPublishSourceButtonProps extends PublishButtonProps { + videoSources?: MediaDeviceInfo[]; + onVideoSourceSelected?: (sourceId: string) => void; + requestedVideoSource?: MediaDeviceInfo['deviceId']; +} + +export default function CameraPublishSourceButton({ + isActive, + onClick, + onVideoSourceSelected, + videoSources, +}: CameraPublishSourceButtonProps) { + return ( +
+ + +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx new file mode 100644 index 000000000..c4d532048 --- /dev/null +++ b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx @@ -0,0 +1,49 @@ +import { createSignal } from 'solid-js'; + +type MediaSourceSelectorProps = { + isActive: boolean; + sources?: MediaDeviceInfo[]; + onSelected?: (sourceId: string) => void; +}; + +export default function MediaSourceSourceSelector({ + isActive, + sources, + onSelected, +}: MediaSourceSelectorProps) { + const [sourcesVisible, setSourcesVisible] = createSignal(false); + + const toggleSourcesVisible = () => { + if (sourcesVisible()) { + setSourcesVisible(false); + } else { + setSourcesVisible(true); + } + }; + + return ( + isActive && + sources?.length && ( + <> + + {sourcesVisible() ? 'β–Ό' : 'β–²'} + + {sourcesVisible() && ( + + )} + + ) + ); +} diff --git a/js/hang-ui/src/Components/publish/PublishControls.tsx b/js/hang-ui/src/Components/publish/PublishControls.tsx new file mode 100644 index 000000000..2ddf4b3db --- /dev/null +++ b/js/hang-ui/src/Components/publish/PublishControls.tsx @@ -0,0 +1,34 @@ +import type { PublishSourceType, PublishStatus } from './publish-types'; +import CameraPublishSourceButton from './CameraPublishSourceButton'; +import PublishStatusIndicator from './PublishStatusIndicator'; + +type PublishControlsProps = { + activeSources: PublishSourceType[]; + currentStatus: PublishStatus; + videoSources: MediaDeviceInfo[]; + audioSources: MediaDeviceInfo[]; + onAudioSourceSelected: ( + selectedSource: MediaDeviceInfo['deviceId'] + ) => void; + onVideoSourceSelected: ( + selectedSource: MediaDeviceInfo['deviceId'] + ) => void; + onSourceSelected: (selectedSource: PublishSourceType) => void; +}; + +export default function PublishControls(props: PublishControlsProps) { + return ( +
+
+ Source: + props.onSourceSelected('camera')} + videoSources={props.videoSources} + onVideoSourceSelected={props.onVideoSourceSelected} + /> +
+ +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/PublishStatusIndicator.tsx b/js/hang-ui/src/Components/publish/PublishStatusIndicator.tsx new file mode 100644 index 000000000..822105db0 --- /dev/null +++ b/js/hang-ui/src/Components/publish/PublishStatusIndicator.tsx @@ -0,0 +1,32 @@ +import type { PublishStatus } from './publish-types'; +import { Switch, Match } from 'solid-js'; + +type PublishStatusProps = { + currentStatus: PublishStatus; +}; + +export default function PublishStatusIndicator(props: PublishStatusProps) { + return ( +
+ + πŸ”΄ No URL + + πŸ”΄ Disconnected + + + 🟑 Connecting... + + + 🟑 Select Source + + + 🟒 Video Only + + + 🟒 Audio Only + + 🟒 Live + +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/element.tsx b/js/hang-ui/src/Components/publish/element.tsx new file mode 100644 index 000000000..a52f07f12 --- /dev/null +++ b/js/hang-ui/src/Components/publish/element.tsx @@ -0,0 +1,55 @@ +import { customElement } from 'solid-element'; +import PublishControls from './PublishControls'; +import styles from './styles.css'; +import type { PublishSourceType, PublishStatus } from './publish-types'; + +customElement( + 'hang-publish-controls', + { + activeSources: [], + currentStatus: 'no-url' as PublishStatus, + videoSources: [], + audioSources: [], + }, + function PublishControlsWebComponent(attributes, { element }) { + const onSourceSelected = (newSource: PublishSourceType) => { + element.dispatchEvent( + new CustomEvent('sourceselectionchange', { + detail: newSource, + }) + ); + }; + + const onAudioSourceSelected = ( + selectedSource: MediaDeviceInfo['deviceId'] + ) => { + element.dispatchEvent( + new CustomEvent('audiosourceselected', { + detail: selectedSource, + }) + ); + }; + + const onVideoSourceSelected = ( + selectedSource: MediaDeviceInfo['deviceId'] + ) => { + element.dispatchEvent( + new CustomEvent('videosourceselected', { + detail: selectedSource, + }) + ); + }; + + return ( + <> + + + + ); + } +); diff --git a/js/hang-ui/src/Components/publish/publish-types.ts b/js/hang-ui/src/Components/publish/publish-types.ts new file mode 100644 index 000000000..ce9cef71a --- /dev/null +++ b/js/hang-ui/src/Components/publish/publish-types.ts @@ -0,0 +1,13 @@ +export type PublishSourceType = 'microphone' | 'camera' | 'screen' | 'nothing'; +export type PublishStatus = 'no-url' | 'disconnected' | 'connecting' | 'live' | 'audio-only' | 'video-only' | 'select-source'; +export interface HangPublishControlsElement extends HTMLElement { + activeSources: PublishSourceType[]; + currentStatus: PublishStatus; + audioSources?: MediaDeviceInfo[]; + videoSources?: MediaDeviceInfo[]; + screenSources?: MediaDeviceInfo[]; +} +export interface PublishButtonProps { + isActive: boolean; + onClick: () => void; +} diff --git a/js/hang-ui/src/Components/publish/styles.css b/js/hang-ui/src/Components/publish/styles.css new file mode 100644 index 000000000..e7587cc47 --- /dev/null +++ b/js/hang-ui/src/Components/publish/styles.css @@ -0,0 +1,32 @@ +.publishControlsContainer { + display: flex; + justify-content: space-around; + gap: 16px; + margin: 8px 0; + align-content: center; +} + +.publishSourceSelectorContainer { + display: flex; + gap: 16px; +} + +.publishSourceButtonContainer { + display: flex; + position: relative; + align-items: center; +} + +.publishSourceButton { + cursor: pointer; + opacity: 0.5; +} + +.publishSourceButton.active { + opacity: 1; +} + +.mediaSourceVisibilityToggle { + font-size: 0.75em; + cursor: pointer; +} diff --git a/js/hang-ui/src/css.d.ts b/js/hang-ui/src/css.d.ts new file mode 100644 index 000000000..64653ede5 --- /dev/null +++ b/js/hang-ui/src/css.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: string; + export default content; +} \ No newline at end of file diff --git a/js/hang-ui/src/types.ts b/js/hang-ui/src/types.ts new file mode 100644 index 000000000..e69de29bb diff --git a/js/hang-ui/tsconfig.json b/js/hang-ui/tsconfig.json new file mode 100644 index 000000000..fe4e662e0 --- /dev/null +++ b/js/hang-ui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "skipLibCheck": true, + "outDir": "dist", + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + + }, + "include": ["src"] +} diff --git a/js/hang/package.json b/js/hang/package.json index 690373348..636b9ff0a 100644 --- a/js/hang/package.json +++ b/js/hang/package.json @@ -41,6 +41,7 @@ "devDependencies": { "@types/audioworklet": "^0.0.77", "@typescript/lib-dom": "npm:@types/web@^0.0.241", + "@kixelated/hang-ui": "workspace:^", "fast-glob": "^3.3.3", "rimraf": "^6.0.1", "typescript": "^5.9.2", diff --git a/js/hang/src/publish/element.ts b/js/hang/src/publish/element.ts index add8e3b8f..87e9d7702 100644 --- a/js/hang/src/publish/element.ts +++ b/js/hang/src/publish/element.ts @@ -3,6 +3,7 @@ import { Effect, Signal } from "@kixelated/signals"; import * as DOM from "@kixelated/signals/dom"; import { Broadcast } from "./broadcast"; import * as Source from "./source"; +import type { HangPublishControlsElement, PublishSourceType } from "@kixelated/hang-ui/publish/element"; // TODO: remove device; it's a backwards compatible alias for source. // TODO remove name; it's a backwards compatible alias for path. @@ -144,6 +145,7 @@ export class HangPublishInstance { broadcast: Broadcast; #preview: Signal; + #controlsElement: HangPublishControlsElement | undefined; #video = new Signal(undefined); #audio = new Signal(undefined); @@ -154,6 +156,7 @@ export class HangPublishInstance { // Watch to see if the preview element is added or removed. this.#preview = new Signal(this.parent.querySelector("video") as HTMLVideoElement | undefined); + this.#controlsElement = this.parent.querySelector("hang-publish-controls") as HangPublishControlsElement | undefined; const observer = new MutationObserver(() => { this.#preview.set(this.parent.querySelector("video") as HTMLVideoElement | undefined); }); @@ -199,7 +202,7 @@ export class HangPublishInstance { }); this.#signals.effect(this.#runSource.bind(this)); - this.#signals.effect(this.#renderControls.bind(this)); + this.#signals.effect(this.#connectControls.bind(this)); // Keep device signal in sync with source signal for backwards compatibility this.#signals.effect((effect) => { @@ -263,26 +266,45 @@ export class HangPublishInstance { } } - #renderControls(effect: Effect) { - const controls = DOM.create("div", { - style: { - display: "flex", - justifyContent: "space-around", - gap: "16px", - margin: "8px 0", - alignContent: "center", - }, - }); + #connectControls(effect: Effect) { + if (!this.#controlsElement) { + console.warn("No controls element found"); + return; + } - DOM.render(effect, this.parent, controls); + const shouldShowControls = effect.get(this.parent.signals.controls); + hideShowControls(this.#controlsElement, shouldShowControls); effect.effect((effect) => { - const show = effect.get(this.parent.signals.controls); - if (!show) return; + const showControls = effect.get(this.parent.signals.controls); + hideShowControls(this.#controlsElement, showControls); + }); - this.#renderSelect(controls, effect); - this.#renderStatus(controls, effect); + this.#setupStatusControls(effect); + + effect.event(this.#controlsElement, "sourceselectionchange", (source: Event) => { + const detail = (source as CustomEvent).detail as PublishSourceType; + if (detail === 'camera') { + if (this.parent.source === "camera") { + // Camera already selected, toggle video. + this.parent.video = !this.parent.video; + } else { + this.parent.source = "camera"; + this.parent.video = true; + } + } }); + // this.#setupCameraControls(effect); + + function hideShowControls(element: HangPublishControlsElement | undefined, show: boolean) { + if (!element) return; + + if (show) { + element.style.display = "block"; + } else { + element.style.display = "none"; + } + } } #renderSelect(parent: HTMLDivElement, effect: Effect) { @@ -298,7 +320,7 @@ export class HangPublishInstance { ); this.#renderMicrophone(container, effect); - this.#renderCamera(container, effect); + // this.#renderCamera(container, effect); this.#renderScreen(container, effect); this.#renderNothing(container, effect); @@ -392,7 +414,10 @@ export class HangPublishInstance { DOM.render(effect, parent, container); } - #renderCamera(parent: HTMLDivElement, effect: Effect) { + #setupCameraControls(parent: HTMLDivElement, effect: Effect) { + if (!this.#controlsElement) return; + + const container = DOM.create("div", { style: { display: "flex", @@ -525,34 +550,31 @@ export class HangPublishInstance { DOM.render(effect, parent, nothing); } - #renderStatus(parent: HTMLDivElement, effect: Effect) { - const container = DOM.create("div"); - + #setupStatusControls(effect: Effect) { effect.effect((effect) => { + if (!this.#controlsElement) return; + const url = effect.get(this.connection.url); const status = effect.get(this.connection.status); const audio = effect.get(this.broadcast.audio.source); const video = effect.get(this.broadcast.video.source); if (!url) { - container.textContent = "πŸ”΄\u00A0No URL"; + this.#controlsElement.currentStatus = "no-url"; } else if (status === "disconnected") { - container.textContent = "πŸ”΄\u00A0Disconnected"; + this.#controlsElement.currentStatus = "disconnected"; } else if (status === "connecting") { - container.textContent = "🟑\u00A0Connecting..."; + this.#controlsElement.currentStatus = "connecting"; } else if (!audio && !video) { - container.textContent = "🟑\u00A0Select Source"; + this.#controlsElement.currentStatus = "select-source"; } else if (!audio && video) { - container.textContent = "🟒\u00A0Video Only"; + this.#controlsElement.currentStatus = "video-only"; } else if (audio && !video) { - container.textContent = "🟒\u00A0Audio Only"; + this.#controlsElement.currentStatus = "audio-only"; } else if (audio && video) { - container.textContent = "🟒\u00A0Live"; + this.#controlsElement.currentStatus = "live"; } }); - - parent.appendChild(container); - effect.cleanup(() => parent.removeChild(container)); } close() { diff --git a/js/package.json b/js/package.json index 9cae177c3..0a204b14f 100644 --- a/js/package.json +++ b/js/package.json @@ -5,10 +5,11 @@ "type": "module", "workspaces": [ "moq", - "moq-clock", + "moq-clock", "moq-token", "hang", "hang-demo", + "hang-ui", "signals" ], "scripts": { From c3e9187c99c221ef3e92e4db9bc06f4a071b5741 Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Mon, 24 Nov 2025 13:39:18 -0500 Subject: [PATCH 02/54] all control buttons working. todo - cleanup. add tests --- .../publish/CameraPublishSourceButton.tsx | 32 -------- .../Components/publish/CameraSourceButton.tsx | 32 ++++++++ .../publish/MediaSourceSelector.tsx | 51 +++++------- .../publish/MicrophoneSourceButton.tsx | 32 ++++++++ .../publish/NothingSourceButton.tsx | 17 ++++ .../Components/publish/PublishControls.tsx | 39 +++++++-- .../Components/publish/ScreenSourceButton.tsx | 19 +++++ js/hang-ui/src/Components/publish/element.tsx | 22 ++--- .../src/Components/publish/publish-types.ts | 6 +- js/hang-ui/src/Components/publish/styles.css | 12 +++ js/hang/src/publish/element.ts | 82 ++++++++++++++++++- 11 files changed, 260 insertions(+), 84 deletions(-) delete mode 100644 js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx create mode 100644 js/hang-ui/src/Components/publish/CameraSourceButton.tsx create mode 100644 js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx create mode 100644 js/hang-ui/src/Components/publish/NothingSourceButton.tsx create mode 100644 js/hang-ui/src/Components/publish/ScreenSourceButton.tsx diff --git a/js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx b/js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx deleted file mode 100644 index eab52d009..000000000 --- a/js/hang-ui/src/Components/publish/CameraPublishSourceButton.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import MediaSourceSourceSelector from './MediaSourceSelector'; -import type { PublishButtonProps } from './publish-types'; - -interface CameraPublishSourceButtonProps extends PublishButtonProps { - videoSources?: MediaDeviceInfo[]; - onVideoSourceSelected?: (sourceId: string) => void; - requestedVideoSource?: MediaDeviceInfo['deviceId']; -} - -export default function CameraPublishSourceButton({ - isActive, - onClick, - onVideoSourceSelected, - videoSources, -}: CameraPublishSourceButtonProps) { - return ( -
- - -
- ); -} diff --git a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx new file mode 100644 index 000000000..5c7406439 --- /dev/null +++ b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx @@ -0,0 +1,32 @@ +import MediaSourceSourceSelector from './MediaSourceSelector'; +import type { PublishButtonProps } from './publish-types'; +import { Show } from 'solid-js'; + +interface CameraPublishSourceButtonProps extends PublishButtonProps { + cameraSources?: MediaDeviceInfo[]; + selectedCameraSource?: MediaDeviceInfo['deviceId']; + onCameraSourceSelected?: (sourceId: string) => void; +} + +export default function CameraSourceButton( + props: CameraPublishSourceButtonProps +) { + return ( +
+ + + + +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx index c4d532048..585844229 100644 --- a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx +++ b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx @@ -1,16 +1,12 @@ -import { createSignal } from 'solid-js'; +import { createSignal, Show } from 'solid-js'; type MediaSourceSelectorProps = { - isActive: boolean; sources?: MediaDeviceInfo[]; + selectedSource?: string; onSelected?: (sourceId: string) => void; }; -export default function MediaSourceSourceSelector({ - isActive, - sources, - onSelected, -}: MediaSourceSelectorProps) { +export default function MediaSourceSelector(props: MediaSourceSelectorProps) { const [sourcesVisible, setSourcesVisible] = createSignal(false); const toggleSourcesVisible = () => { @@ -22,28 +18,25 @@ export default function MediaSourceSourceSelector({ }; return ( - isActive && - sources?.length && ( - <> - + + {sourcesVisible() ? 'β–Ό' : 'β–²'} + + + onSelected?.(e.currentTarget.value)} - > - {sources.map((source) => ( - - ))} - - )} - - ) + {props.sources?.map((source) => ( + + ))} + + + ); } diff --git a/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx b/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx new file mode 100644 index 000000000..ebe572a07 --- /dev/null +++ b/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx @@ -0,0 +1,32 @@ +import { PublishButtonProps } from './publish-types'; +import MediaSourceSourceSelector from './MediaSourceSelector'; +import { Show } from 'solid-js'; + +interface MicrophonePublishSourceButtonProps extends PublishButtonProps { + microphoneSources?: MediaDeviceInfo[]; + selectedMicrophoneSource?: MediaDeviceInfo['deviceId']; + onMicrophoneSourceSelected?: (sourceId: string) => void; +} + +export default function MicrophoneSourceButton( + props: MicrophonePublishSourceButtonProps +) { + return ( +
+ + + + +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/NothingSourceButton.tsx b/js/hang-ui/src/Components/publish/NothingSourceButton.tsx new file mode 100644 index 000000000..92b4085ea --- /dev/null +++ b/js/hang-ui/src/Components/publish/NothingSourceButton.tsx @@ -0,0 +1,17 @@ +import { PublishButtonProps } from './publish-types'; + +interface NothingSourceButtonProps extends PublishButtonProps {} + +export default function NothingSourceButton(props: NothingSourceButtonProps) { + return ( +
+ +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/PublishControls.tsx b/js/hang-ui/src/Components/publish/PublishControls.tsx index 2ddf4b3db..d2a36b7da 100644 --- a/js/hang-ui/src/Components/publish/PublishControls.tsx +++ b/js/hang-ui/src/Components/publish/PublishControls.tsx @@ -1,16 +1,21 @@ import type { PublishSourceType, PublishStatus } from './publish-types'; -import CameraPublishSourceButton from './CameraPublishSourceButton'; +import CameraSourceButton from './CameraSourceButton'; +import ScreenSourceButton from './ScreenSourceButton'; +import MicrophoneSourceButton from './MicrophoneSourceButton'; import PublishStatusIndicator from './PublishStatusIndicator'; +import NothingSourceButton from './NothingSourceButton'; type PublishControlsProps = { activeSources: PublishSourceType[]; currentStatus: PublishStatus; - videoSources: MediaDeviceInfo[]; - audioSources: MediaDeviceInfo[]; - onAudioSourceSelected: ( + selectedCameraSource?: MediaDeviceInfo['deviceId']; + selectedMicrophoneSource?: MediaDeviceInfo['deviceId']; + cameraSources: MediaDeviceInfo[]; + microphoneSources: MediaDeviceInfo[]; + onMicrophoneSourceSelected: ( selectedSource: MediaDeviceInfo['deviceId'] ) => void; - onVideoSourceSelected: ( + onCameraSourceSelected: ( selectedSource: MediaDeviceInfo['deviceId'] ) => void; onSourceSelected: (selectedSource: PublishSourceType) => void; @@ -21,11 +26,29 @@ export default function PublishControls(props: PublishControlsProps) {
Source: - props.onSourceSelected('microphone')} + microphoneSources={props.microphoneSources} + selectedMicrophoneSource={props.selectedMicrophoneSource} + onMicrophoneSourceSelected={ + props.onMicrophoneSourceSelected + } + /> + props.onSourceSelected('camera')} - videoSources={props.videoSources} - onVideoSourceSelected={props.onVideoSourceSelected} + cameraSources={props.cameraSources} + selectedCameraSource={props.selectedCameraSource} + onCameraSourceSelected={props.onCameraSourceSelected} + /> + props.onSourceSelected('screen')} + /> + props.onSourceSelected('nothing')} />
diff --git a/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx b/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx new file mode 100644 index 000000000..1410b7236 --- /dev/null +++ b/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx @@ -0,0 +1,19 @@ +import { PublishButtonProps } from './publish-types'; + +interface ScreenPublishSourceButtonProps extends PublishButtonProps {} + +export default function ScreenSourceButton( + props: ScreenPublishSourceButtonProps +) { + return ( +
+ +
+ ); +} diff --git a/js/hang-ui/src/Components/publish/element.tsx b/js/hang-ui/src/Components/publish/element.tsx index a52f07f12..ba9fc0df8 100644 --- a/js/hang-ui/src/Components/publish/element.tsx +++ b/js/hang-ui/src/Components/publish/element.tsx @@ -6,10 +6,12 @@ import type { PublishSourceType, PublishStatus } from './publish-types'; customElement( 'hang-publish-controls', { - activeSources: [], + activeSources: [] as PublishSourceType[], + selectedCameraSource: undefined, + selectedMicrophoneSource: undefined, currentStatus: 'no-url' as PublishStatus, - videoSources: [], - audioSources: [], + cameraSources: [], + microphoneSources: [], }, function PublishControlsWebComponent(attributes, { element }) { const onSourceSelected = (newSource: PublishSourceType) => { @@ -20,21 +22,21 @@ customElement( ); }; - const onAudioSourceSelected = ( + const onMicrophoneSourceSelected = ( selectedSource: MediaDeviceInfo['deviceId'] ) => { element.dispatchEvent( - new CustomEvent('audiosourceselected', { + new CustomEvent('microphonesourceselected', { detail: selectedSource, }) ); }; - const onVideoSourceSelected = ( + const onCameraSourceSelected = ( selectedSource: MediaDeviceInfo['deviceId'] ) => { element.dispatchEvent( - new CustomEvent('videosourceselected', { + new CustomEvent('camerasourceselected', { detail: selectedSource, }) ); @@ -44,10 +46,10 @@ customElement( <> ); diff --git a/js/hang-ui/src/Components/publish/publish-types.ts b/js/hang-ui/src/Components/publish/publish-types.ts index ce9cef71a..82ab4dcb2 100644 --- a/js/hang-ui/src/Components/publish/publish-types.ts +++ b/js/hang-ui/src/Components/publish/publish-types.ts @@ -3,8 +3,10 @@ export type PublishStatus = 'no-url' | 'disconnected' | 'connecting' | 'live' | export interface HangPublishControlsElement extends HTMLElement { activeSources: PublishSourceType[]; currentStatus: PublishStatus; - audioSources?: MediaDeviceInfo[]; - videoSources?: MediaDeviceInfo[]; + selectedCameraSource?: MediaDeviceInfo['deviceId']; + selectedMicrophoneSource?: MediaDeviceInfo['deviceId']; + microphoneSources?: MediaDeviceInfo[]; + cameraSources?: MediaDeviceInfo[]; screenSources?: MediaDeviceInfo[]; } export interface PublishButtonProps { diff --git a/js/hang-ui/src/Components/publish/styles.css b/js/hang-ui/src/Components/publish/styles.css index e7587cc47..7098c8496 100644 --- a/js/hang-ui/src/Components/publish/styles.css +++ b/js/hang-ui/src/Components/publish/styles.css @@ -18,8 +18,12 @@ } .publishSourceButton { + all: unset; cursor: pointer; opacity: 0.5; + display: inline-block; + font: inherit; + color: inherit; } .publishSourceButton.active { @@ -29,4 +33,12 @@ .mediaSourceVisibilityToggle { font-size: 0.75em; cursor: pointer; + padding-left: 4px; +} + +.mediaSourceSelector { + position: absolute; + top: 100%; + transform: translateX(-50%); + display: block; } diff --git a/js/hang/src/publish/element.ts b/js/hang/src/publish/element.ts index 87e9d7702..6235da533 100644 --- a/js/hang/src/publish/element.ts +++ b/js/hang/src/publish/element.ts @@ -4,6 +4,7 @@ import * as DOM from "@kixelated/signals/dom"; import { Broadcast } from "./broadcast"; import * as Source from "./source"; import type { HangPublishControlsElement, PublishSourceType } from "@kixelated/hang-ui/publish/element"; +import { th } from "zod/locales"; // TODO: remove device; it's a backwards compatible alias for source. // TODO remove name; it's a backwards compatible alias for path. @@ -283,8 +284,9 @@ export class HangPublishInstance { this.#setupStatusControls(effect); effect.event(this.#controlsElement, "sourceselectionchange", (source: Event) => { - const detail = (source as CustomEvent).detail as PublishSourceType; - if (detail === 'camera') { + const selection = (source as CustomEvent).detail as PublishSourceType; + + if (selection === 'camera') { if (this.parent.source === "camera") { // Camera already selected, toggle video. this.parent.video = !this.parent.video; @@ -293,8 +295,75 @@ export class HangPublishInstance { this.parent.video = true; } } + + if (selection === 'microphone') { + if (this.parent.source === "camera") { + // Camera already selected, toggle audio. + this.parent.audio = !this.parent.audio; + } else { + this.parent.source = "camera"; + this.parent.audio = true; + } + } + + if (selection === 'screen') { + this.parent.source = "screen"; + } + + if (selection === 'nothing') { + this.parent.source = undefined; + } + }); + + effect.event(this.#controlsElement, "camerasourceselected", (event: Event) => { + const detail = (event as CustomEvent).detail as MediaDeviceInfo['deviceId']; + const video = effect.get(this.#video); + if (video instanceof Source.Camera) { + video.device.preferred.set(detail); + } + }); + + effect.event(this.#controlsElement, "microphonesourceselected", (event: Event) => { + const detail = (event as CustomEvent).detail as MediaDeviceInfo['deviceId']; + const audio = effect.get(this.#audio); + if (audio instanceof Source.Microphone) { + audio.device.preferred.set(detail); + } + }); + + // Provide available video devices to PublishControls. + effect.effect((effect) => { + if (!this.#controlsElement) return; + + const video = effect.get(this.#video); + if (!(video instanceof Source.Camera)) return; + + const enabled = effect.get(this.broadcast.video.hd.enabled); + if (!enabled) return; + + const devices = effect.get(video.device.available); + if (!devices || devices.length < 2) return; + + this.#controlsElement.cameraSources = devices; + this.#controlsElement.selectedCameraSource = effect.get(video.device.requested); + }); + + // Provide available audio devices to PublishControls. + effect.effect((effect) => { + if (!this.#controlsElement) return; + + const audio = effect.get(this.#audio); + if (!(audio instanceof Source.Microphone)) return; + + const enabled = effect.get(this.broadcast.audio.enabled); + if (!enabled) return; + + const devices = effect.get(audio.device.available); + if (!devices || devices.length < 2) return; + + this.#controlsElement.microphoneSources = devices; + this.#controlsElement.selectedMicrophoneSource = effect.get(audio.device.requested); }); - // this.#setupCameraControls(effect); function hideShowControls(element: HangPublishControlsElement | undefined, show: boolean) { if (!element) return; @@ -554,6 +623,7 @@ export class HangPublishInstance { effect.effect((effect) => { if (!this.#controlsElement) return; + const activeSources: PublishSourceType[] = []; const url = effect.get(this.connection.url); const status = effect.get(this.connection.status); const audio = effect.get(this.broadcast.audio.source); @@ -574,6 +644,12 @@ export class HangPublishInstance { } else if (audio && video) { this.#controlsElement.currentStatus = "live"; } + + if (audio) activeSources.push("microphone"); + if (this.parent.source === "camera") activeSources.push("camera"); + if (this.parent.source === "screen") activeSources.push("screen"); + + this.#controlsElement.activeSources = activeSources; }); } From 33dd7448293dde7ebe7ba9cb7c3a05031db6234d Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Mon, 24 Nov 2025 13:49:35 -0500 Subject: [PATCH 03/54] cleanup hang-publish to remove old dom code --- js/hang/src/publish/element.ts | 268 ++------------------------------- 1 file changed, 16 insertions(+), 252 deletions(-) diff --git a/js/hang/src/publish/element.ts b/js/hang/src/publish/element.ts index 6235da533..16b8d68e8 100644 --- a/js/hang/src/publish/element.ts +++ b/js/hang/src/publish/element.ts @@ -1,10 +1,8 @@ import * as Moq from "@kixelated/moq"; import { Effect, Signal } from "@kixelated/signals"; -import * as DOM from "@kixelated/signals/dom"; import { Broadcast } from "./broadcast"; import * as Source from "./source"; import type { HangPublishControlsElement, PublishSourceType } from "@kixelated/hang-ui/publish/element"; -import { th } from "zod/locales"; // TODO: remove device; it's a backwards compatible alias for source. // TODO remove name; it's a backwards compatible alias for path. @@ -365,265 +363,37 @@ export class HangPublishInstance { this.#controlsElement.selectedMicrophoneSource = effect.get(audio.device.requested); }); - function hideShowControls(element: HangPublishControlsElement | undefined, show: boolean) { - if (!element) return; - - if (show) { - element.style.display = "block"; - } else { - element.style.display = "none"; - } - } - } - - #renderSelect(parent: HTMLDivElement, effect: Effect) { - const container = DOM.create( - "div", - { - style: { - display: "flex", - gap: "16px", - }, - }, - "Source:", - ); - - this.#renderMicrophone(container, effect); - // this.#renderCamera(container, effect); - this.#renderScreen(container, effect); - this.#renderNothing(container, effect); - - DOM.render(effect, parent, container); - } - - #renderMicrophone(parent: HTMLDivElement, effect: Effect) { - const container = DOM.create("div", { - style: { - display: "flex", - position: "relative", - alignItems: "center", - }, - }); - - const microphone = DOM.create( - "button", - { - type: "button", - title: "Microphone", - style: { cursor: "pointer" }, - }, - "🎀", - ); - - DOM.render(effect, container, microphone); - - effect.event(microphone, "click", () => { - if (this.parent.source === "camera") { - // Camera already selected, toggle audio. - this.parent.audio = !this.parent.audio; - } else { - this.parent.source = "camera"; - this.parent.audio = true; - } - }); - + // Update active sources. effect.effect((effect) => { - const selected = effect.get(this.parent.signals.source); - const audio = effect.get(this.broadcast.audio.enabled); - microphone.style.opacity = selected === "camera" && audio ? "1" : "0.5"; - }); + if (!this.#controlsElement) return; - // List of the available audio devices and show a drop down if there are multiple. - effect.effect((effect) => { + const activeSources: PublishSourceType[] = []; const audio = effect.get(this.#audio); - if (!(audio instanceof Source.Microphone)) return; - - const enabled = effect.get(this.broadcast.audio.enabled); - if (!enabled) return; - - const devices = effect.get(audio.device.available); - if (!devices || devices.length < 2) return; - - const visible = new Signal(false); - - const select = DOM.create("select", { - style: { - position: "absolute", - top: "100%", - transform: "translateX(-50%)", - }, - }); - effect.event(select, "change", () => { - audio.device.preferred.set(select.value); - }); - - for (const device of devices) { - const option = DOM.create("option", { value: device.deviceId }, device.label); - DOM.render(effect, select, option); - } - - effect.effect((effect) => { - const active = effect.get(audio.device.requested); - select.value = active ?? ""; - }); - - const caret = DOM.create("span", { style: { fontSize: "0.75em", cursor: "pointer" } }, "β–Ό"); - effect.event(caret, "click", () => visible.update((v) => !v)); - - effect.effect((effect) => { - const v = effect.get(visible); - caret.innerText = v ? "β–Ό" : "β–²"; - select.style.display = v ? "block" : "none"; - }); - - DOM.render(effect, container, caret); - DOM.render(effect, container, select); - }); - - DOM.render(effect, parent, container); - } - - #setupCameraControls(parent: HTMLDivElement, effect: Effect) { - if (!this.#controlsElement) return; + const videoSource = effect.get(this.parent.signals.source); + // update active sources while we have values + if (audio) activeSources.push("microphone"); + if (videoSource === "camera") activeSources.push("camera"); + if (videoSource === "screen") activeSources.push("screen"); - const container = DOM.create("div", { - style: { - display: "flex", - position: "relative", - alignItems: "center", - }, + this.#controlsElement.activeSources = activeSources; }); - const camera = DOM.create( - "button", - { - type: "button", - title: "Camera", - style: { cursor: "pointer" }, - }, - "πŸ“·", - ); - - DOM.render(effect, container, camera); - - effect.event(camera, "click", () => { - if (this.parent.source === "camera") { - // Camera already selected, toggle video. - this.parent.video = !this.parent.video; + function hideShowControls(element: HangPublishControlsElement | undefined, show: boolean) { + if (!element) return; + + if (show) { + element.style.display = "block"; } else { - this.parent.source = "camera"; - this.parent.video = true; - } - }); - - effect.effect((effect) => { - const selected = effect.get(this.parent.signals.source); - const video = effect.get(this.broadcast.video.hd.enabled); - camera.style.opacity = selected === "camera" && video ? "1" : "0.5"; - }); - - // List of the available audio devices and show a drop down if there are multiple. - effect.effect((effect) => { - const video = effect.get(this.#video); - if (!(video instanceof Source.Camera)) return; - - const enabled = effect.get(this.broadcast.video.hd.enabled); - if (!enabled) return; - - const devices = effect.get(video.device.available); - if (!devices || devices.length < 2) return; - - const visible = new Signal(false); - - const select = DOM.create("select", { - style: { - position: "absolute", - top: "100%", - transform: "translateX(-50%)", - }, - }); - effect.event(select, "change", () => { - video.device.preferred.set(select.value); - }); - - for (const device of devices) { - const option = DOM.create("option", { value: device.deviceId }, device.label); - DOM.render(effect, select, option); + element.style.display = "none"; } - - effect.effect((effect) => { - const requested = effect.get(video.device.requested); - select.value = requested ?? ""; - }); - - const caret = DOM.create("span", { style: { fontSize: "0.75em", cursor: "pointer" } }, "β–Ό"); - effect.event(caret, "click", () => visible.update((v) => !v)); - - effect.effect((effect) => { - const v = effect.get(visible); - caret.innerText = v ? "β–Ό" : "β–²"; - select.style.display = v ? "block" : "none"; - }); - - DOM.render(effect, container, caret); - DOM.render(effect, container, select); - }); - - DOM.render(effect, parent, container); - } - - #renderScreen(parent: HTMLDivElement, effect: Effect) { - const screen = DOM.create( - "button", - { - type: "button", - title: "Screen", - style: { cursor: "pointer" }, - }, - "πŸ–₯️", - ); - - effect.event(screen, "click", () => { - this.parent.source = "screen"; - }); - - effect.effect((effect) => { - const selected = effect.get(this.parent.signals.source); - screen.style.opacity = selected === "screen" ? "1" : "0.5"; - }); - - DOM.render(effect, parent, screen); - } - - #renderNothing(parent: HTMLDivElement, effect: Effect) { - const nothing = DOM.create( - "button", - { - type: "button", - title: "Nothing", - style: { cursor: "pointer" }, - }, - "🚫", - ); - - effect.event(nothing, "click", () => { - this.parent.source = undefined; - }); - - effect.effect((effect) => { - const selected = effect.get(this.parent.signals.source); - nothing.style.opacity = selected === undefined ? "1" : "0.5"; - }); - - DOM.render(effect, parent, nothing); + } } #setupStatusControls(effect: Effect) { effect.effect((effect) => { if (!this.#controlsElement) return; - const activeSources: PublishSourceType[] = []; const url = effect.get(this.connection.url); const status = effect.get(this.connection.status); const audio = effect.get(this.broadcast.audio.source); @@ -644,12 +414,6 @@ export class HangPublishInstance { } else if (audio && video) { this.#controlsElement.currentStatus = "live"; } - - if (audio) activeSources.push("microphone"); - if (this.parent.source === "camera") activeSources.push("camera"); - if (this.parent.source === "screen") activeSources.push("screen"); - - this.#controlsElement.activeSources = activeSources; }); } From c0a1b1dc93ef8256195e57cbcd3424d9e4cb4585 Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Wed, 26 Nov 2025 20:37:01 -0500 Subject: [PATCH 04/54] wip. invert hang-publish to hang-publish-ui relationship. expose hang-publish exports from compiled sources --- js/bun.lock | 3 +- js/bun.lockb | Bin 176544 -> 176608 bytes js/hang-demo/src/publish.html | 6 +- js/hang-ui/{src/types.ts => README.md} | 0 js/hang-ui/package.json | 10 +- js/hang-ui/rollup.config.mjs | 28 +- .../Components/publish/CameraSourceButton.tsx | 51 ++- .../publish/MediaSourceSelector.tsx | 10 +- .../publish/MicrophoneSourceButton.tsx | 52 ++- .../publish/NothingSourceButton.tsx | 21 +- .../Components/publish/PublishControls.tsx | 49 +-- .../PublishControlsContextProvider.tsx | 186 +++++++++++ .../publish/PublishStatusIndicator.tsx | 28 +- .../Components/publish/ScreenSourceButton.tsx | 22 +- js/hang-ui/src/Components/publish/element.tsx | 64 ++-- .../src/Components/publish/publish-types.ts | 13 +- js/hang-ui/src/global.d.ts | 9 + js/hang-ui/tsconfig.json | 3 +- js/hang/package.json | 6 +- js/hang/src/publish/element.ts | 316 +++++++++--------- js/moq/package.json | 4 +- js/signals/package.json | 5 +- 22 files changed, 545 insertions(+), 341 deletions(-) rename js/hang-ui/{src/types.ts => README.md} (100%) create mode 100644 js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx create mode 100644 js/hang-ui/src/global.d.ts diff --git a/js/bun.lock b/js/bun.lock index 9af1dd888..bf6f28878 100644 --- a/js/bun.lock +++ b/js/bun.lock @@ -20,7 +20,6 @@ "zod": "^4.1.5", }, "devDependencies": { - "@kixelated/hang-ui": "workspace:^", "@types/audioworklet": "^0.0.77", "@typescript/lib-dom": "npm:@types/web@^0.0.241", "fast-glob": "^3.3.3", @@ -52,6 +51,8 @@ "name": "@kixelated/hang-ui", "version": "0.1.0", "devDependencies": { + "@kixelated/hang": "workspace:^0.7.0", + "@kixelated/signals": "workspace:^0.8.1", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.53.3", diff --git a/js/bun.lockb b/js/bun.lockb index 7d27e5ce545595b6a9b56793a9df12289c9daa5a..af3da0616eb1154263002bb1daf9e362b733569c 100755 GIT binary patch delta 5166 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 F{|hY5eS81_ diff --git a/js/hang-demo/src/publish.html b/js/hang-demo/src/publish.html index f87b08b1c..978fa79fb 100644 --- a/js/hang-demo/src/publish.html +++ b/js/hang-demo/src/publish.html @@ -22,11 +22,7 @@ Feel free to hard-code it if you have public access configured, like `url="https://relay.moq.dev/anon"` NOTE: `http` performs an insecure certificate check. You must use `https` in production. --> - - - - - +

Other demos:

    diff --git a/js/hang-ui/src/types.ts b/js/hang-ui/README.md similarity index 100% rename from js/hang-ui/src/types.ts rename to js/hang-ui/README.md diff --git a/js/hang-ui/package.json b/js/hang-ui/package.json index db87e3db7..a0ae93105 100644 --- a/js/hang-ui/package.json +++ b/js/hang-ui/package.json @@ -11,10 +11,14 @@ "./publish/element": { "import": "./dist/publish-controls.esm.js", "types": "./dist/Components/publish/publish-types.d.ts" + }, + "./watch/element": { + "import": "./dist/watch-controls.esm.js", + "types": "./dist/Components/watch/watch-types.d.ts" } }, "scripts": { - "build": "rollup -c && npm run build:types", + "build": "npm run clean && rollup -c && npm run build:types", "build:types": "tsc -p tsconfig.json", "clean": "rm -rf dist" }, @@ -25,6 +29,8 @@ "rollup-plugin-string": "^3.0.0", "solid-element": "^1.9.1", "solid-js": "^1.9.10", - "unplugin-solid": "^1.0.0" + "unplugin-solid": "^1.0.0", + "@kixelated/hang": "workspace:^0.7.0", + "@kixelated/signals": "workspace:^0.8.1" } } diff --git a/js/hang-ui/rollup.config.mjs b/js/hang-ui/rollup.config.mjs index bdf89067e..f5af6ae76 100644 --- a/js/hang-ui/rollup.config.mjs +++ b/js/hang-ui/rollup.config.mjs @@ -3,17 +3,19 @@ import nodeResolve from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; import { string } from 'rollup-plugin-string'; -export default { - input: 'src/Components/publish/element.tsx', - output: { - file: 'dist/publish-controls.esm.js', - format: 'es', - sourcemap: true, +export default [ + { + input: 'src/Components/publish/element.tsx', + output: { + file: 'dist/publish-controls.esm.js', + format: 'es', + sourcemap: true, + }, + plugins: [ + string({ include: '**/*.css' }), + solid({ dev: false, hydratable: false }), + nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), + typescript(), + ], }, - plugins: [ - string({ include: '**/*.css' }), - solid({ dev: false, hydratable: false }), - nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), - typescript(), - ], -}; +]; diff --git a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx index 5c7406439..dd387335d 100644 --- a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx @@ -1,30 +1,49 @@ import MediaSourceSourceSelector from './MediaSourceSelector'; -import type { PublishButtonProps } from './publish-types'; -import { Show } from 'solid-js'; +import { Show, useContext } from 'solid-js'; +import { PublishControlsContext } from './PublishControlsContextProvider'; -interface CameraPublishSourceButtonProps extends PublishButtonProps { - cameraSources?: MediaDeviceInfo[]; - selectedCameraSource?: MediaDeviceInfo['deviceId']; - onCameraSourceSelected?: (sourceId: string) => void; -} +export default function CameraSourceButton() { + const context = useContext(PublishControlsContext); + const onClick = () => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; + + if (hangPublishEl.source === 'camera') { + // Camera already selected, toggle video. + hangPublishEl.video = !hangPublishEl.video; + } else { + hangPublishEl.source = 'camera'; + hangPublishEl.video = true; + } + }; + + const onSourceSelected = (sourceId: MediaDeviceInfo['deviceId']) => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; + + hangPublishEl.videoDevice = sourceId; + }; -export default function CameraSourceButton( - props: CameraPublishSourceButtonProps -) { return (
    - +
    diff --git a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx index 585844229..53445a9c0 100644 --- a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx +++ b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx @@ -2,8 +2,8 @@ import { createSignal, Show } from 'solid-js'; type MediaSourceSelectorProps = { sources?: MediaDeviceInfo[]; - selectedSource?: string; - onSelected?: (sourceId: string) => void; + selectedSource?: MediaDeviceInfo['deviceId']; + onSelected?: (sourceId: MediaDeviceInfo['deviceId']) => void; }; export default function MediaSourceSelector(props: MediaSourceSelectorProps) { @@ -30,7 +30,11 @@ export default function MediaSourceSelector(props: MediaSourceSelectorProps) { + {volumeLabel()} +
+ ); +} diff --git a/js/hang-ui/src/Components/watch/WatchControls.tsx b/js/hang-ui/src/Components/watch/WatchControls.tsx index d43dee5d6..863add19a 100644 --- a/js/hang-ui/src/Components/watch/WatchControls.tsx +++ b/js/hang-ui/src/Components/watch/WatchControls.tsx @@ -1,9 +1,13 @@ import WatchStatusIndicator from "./WatchStatusIndicator"; import FullscreenButton from "./FullscreenButton"; +import PlayPauseButton from "./PlayPauseButton"; +import VolumeSlider from "./VolumeSlider"; export default function WatchControls() { return (
+ +
diff --git a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx index 1ddaf8225..5245b6318 100644 --- a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx +++ b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx @@ -12,16 +12,57 @@ type WatchStatus = "no-url" | "disconnected" | "connecting" | "offline" | "loadi type WatchControlsContextValue = { hangWatch: () => HangWatch | undefined; watchStatus: () => WatchStatus; + isPlaying: () => boolean; + isMuted: () => boolean; + setVolume: (vol: number) => void; + currentVolume: () => number; + togglePlayback: () => void; + toggleMuted: () => void; }; export const WatchControlsContext = createContext(); export default function WatchControlsContextProvider(props: WatchControlsContextProviderProps) { const [watchStatus, setWatchStatus] = createSignal("no-url"); + const [isPlaying, setIsPlaying] = createSignal(false); + const [isMuted, setIsMuted] = createSignal(false); + const [currentVolume, setCurrentVolume] = createSignal(0); + + const togglePlayback = () => { + const hangWatchEl = props.hangWatch(); + + if (hangWatchEl) { + hangWatchEl.paused = !hangWatchEl.paused; + } + }; + + const setVolume = (volume: number) => { + const hangWatchEl = props.hangWatch(); + + if (hangWatchEl) { + hangWatchEl.volume = volume / 100; + } + }; + + const toggleMuted = () => { + setIsMuted(!isMuted()); + + const hangWatchEl = props.hangWatch(); + + if (hangWatchEl) { + hangWatchEl.muted = isMuted(); + } + }; const value: WatchControlsContextValue = { hangWatch: props.hangWatch, watchStatus, + togglePlayback, + isPlaying, + setVolume, + isMuted, + currentVolume, + toggleMuted, }; createEffect(() => { @@ -32,14 +73,14 @@ export default function WatchControlsContextProvider(props: WatchControlsContext // @ts-ignore ignore custom event - todo add event map hangWatchEl.addEventListener("watch-instance-available", (event: CustomEvent) => { const watchInstance = event.detail.instance.peek?.() as HangWatchInstance; - onWatchInstanceAvailable(hangWatchEl, watchInstance); + onWatchInstanceAvailable(watchInstance); }); } }); return {props.children}; - function onWatchInstanceAvailable(el: HangWatch, watchInstance: HangWatchInstance) { + function onWatchInstanceAvailable(watchInstance: HangWatchInstance) { watchInstance?.signals.effect(function trackWatchStatus(effect) { const url = effect.get(watchInstance?.connection.url); const connection = effect.get(watchInstance?.connection.status); @@ -61,5 +102,15 @@ export default function WatchControlsContextProvider(props: WatchControlsContext setWatchStatus("connected"); } }); + + watchInstance?.signals.effect(function trackPlaying(effect) { + const paused = effect.get(watchInstance?.video.paused); + setIsPlaying(!paused); + }); + + watchInstance?.signals.effect(function trackVolume(effect) { + const volume = effect.get(watchInstance?.audio.volume); + setCurrentVolume(volume * 100); + }); } } diff --git a/js/hang-ui/src/Components/watch/styles.css b/js/hang-ui/src/Components/watch/styles.css index 65af26b7c..81d2458ba 100644 --- a/js/hang-ui/src/Components/watch/styles.css +++ b/js/hang-ui/src/Components/watch/styles.css @@ -3,4 +3,24 @@ gap: 8px; justify-content: space-around; align-content: center; -} \ No newline at end of file +} + +.watchControlButton { + all: unset; + cursor: pointer; + display: inline-block; + font: inherit; + color: inherit; +} + +.volumeSliderContainer { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.volumeLabel { + display: inline-block; + width: 2em; + text-align: right; +} diff --git a/js/hang/src/watch/element.ts b/js/hang/src/watch/element.ts index 7ee8ea4ea..86143045e 100644 --- a/js/hang/src/watch/element.ts +++ b/js/hang/src/watch/element.ts @@ -1,6 +1,5 @@ import * as Moq from "@kixelated/moq"; import { Effect, Signal } from "@kixelated/signals"; -import * as DOM from "@kixelated/signals/dom"; import type * as Time from "../time"; import * as Audio from "./audio"; import { Broadcast } from "./broadcast"; @@ -287,8 +286,6 @@ export class HangWatchInstance { const latency = Math.floor(effect.get(this.parent.signals.latency)); this.parent.setAttribute("latency", latency.toString()); }); - - this.signals.effect(this.#renderControls.bind(this)); } close() { @@ -298,99 +295,6 @@ export class HangWatchInstance { this.audio.close(); this.signals.close(); } - - #renderControls(effect: Effect) { - const controls = DOM.create("div", { - style: { - display: "flex", - justifyContent: "space-around", - gap: "8px", - alignContent: "center", - }, - }); - - DOM.render(effect, this.parent, controls); - - effect.effect((effect) => { - const show = effect.get(this.parent.signals.controls); - if (!show) return; - - this.#renderPause(controls, effect); - this.#renderVolume(controls, effect); - }); - } - - #renderPause(parent: HTMLDivElement, effect: Effect) { - const button = DOM.create("button", { - type: "button", - title: "Pause", - }); - - effect.event(button, "click", (e) => { - e.preventDefault(); - this.video.paused.update((prev) => !prev); - }); - - effect.effect((effect) => { - const paused = effect.get(this.video.paused); - button.textContent = paused ? "▢️" : "⏸️"; - }); - - DOM.render(effect, parent, button); - } - - #renderVolume(parent: HTMLDivElement, effect: Effect) { - const container = DOM.create("div", { - style: { - display: "flex", - alignItems: "center", - gap: "0.25rem", - }, - }); - - const muteButton = DOM.create("button", { - type: "button", - title: "Mute", - }); - - effect.event(muteButton, "click", () => { - this.audio.muted.update((p) => !p); - }); - - const volumeSlider = DOM.create("input", { - type: "range", - min: "0", - max: "100", - }); - - effect.event(volumeSlider, "input", (e) => { - const target = e.currentTarget as HTMLInputElement; - const volume = parseFloat(target.value) / 100; - this.audio.volume.set(volume); - }); - - const volumeLabel = DOM.create("span", { - style: { - display: "inline-block", - width: "2em", - textAlign: "right", - }, - }); - - effect.effect((effect) => { - const volume = effect.get(this.audio.volume); - const rounded = Math.round(volume * 100); - - muteButton.textContent = volume === 0 ? "πŸ”‡" : "πŸ”Š"; - volumeSlider.value = (volume * 100).toString(); - volumeLabel.textContent = `${rounded}%`; - }); - - DOM.render(effect, container, muteButton); - DOM.render(effect, container, volumeSlider); - DOM.render(effect, container, volumeLabel); - DOM.render(effect, parent, container); - } } customElements.define("hang-watch", HangWatch); From f0016715988322a8f08efebecdf96ebec4bd38c8 Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Thu, 27 Nov 2025 12:22:11 -0500 Subject: [PATCH 08/54] cleanup --- js/hang-ui/README.md | 0 js/hang-ui/Status.tsx | 0 js/hang-ui/package.json | 3 +-- .../publish/PublishControlsContextProvider.tsx | 2 +- js/hang-ui/src/global.d.ts | 9 --------- js/hang/src/publish/element.ts | 2 +- js/signals/package.json | 5 +---- 7 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 js/hang-ui/README.md delete mode 100644 js/hang-ui/Status.tsx delete mode 100644 js/hang-ui/src/global.d.ts diff --git a/js/hang-ui/README.md b/js/hang-ui/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/js/hang-ui/Status.tsx b/js/hang-ui/Status.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/js/hang-ui/package.json b/js/hang-ui/package.json index 7e4e9a4fb..558425c75 100644 --- a/js/hang-ui/package.json +++ b/js/hang-ui/package.json @@ -24,7 +24,6 @@ "solid-element": "^1.9.1", "solid-js": "^1.9.10", "unplugin-solid": "^1.0.0", - "@kixelated/hang": "workspace:^0.7.0", - "@kixelated/signals": "workspace:^0.8.1" + "@kixelated/hang": "workspace:^0.7.0" } } diff --git a/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx b/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx index 50ec0fc4e..31dfa0c6b 100644 --- a/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx +++ b/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx @@ -1,6 +1,6 @@ import { createContext, createEffect, createSignal } from "solid-js"; import type HangPublish from "@kixelated/hang/publish/element"; -import { HangPublishInstance } from "@kixelated/hang/publish/element"; +import type { HangPublishInstance } from "@kixelated/hang/publish/element"; type PublishStatus = "no-url" | "disconnected" | "connecting" | "live" | "audio-only" | "video-only" | "select-source"; diff --git a/js/hang-ui/src/global.d.ts b/js/hang-ui/src/global.d.ts deleted file mode 100644 index dcc4c35d0..000000000 --- a/js/hang-ui/src/global.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'solid-js'; - -declare module 'solid-js' { - namespace JSX { - interface IntrinsicElements { - 'hang-publish': any; // or a better type - } - } -} diff --git a/js/hang/src/publish/element.ts b/js/hang/src/publish/element.ts index 60413750a..102f482a6 100644 --- a/js/hang/src/publish/element.ts +++ b/js/hang/src/publish/element.ts @@ -8,7 +8,7 @@ import * as Source from "./source"; const OBSERVED = ["url", "name", "path", "device", "audio", "video", "controls", "source"] as const; type Observed = (typeof OBSERVED)[number]; -export type SourceType = "camera" | "screen"; +type SourceType = "camera" | "screen"; export interface HangPublishSignals { url: Signal; diff --git a/js/signals/package.json b/js/signals/package.json index 7783f07e0..e81035a49 100644 --- a/js/signals/package.json +++ b/js/signals/package.json @@ -6,10 +6,7 @@ "license": "(MIT OR Apache-2.0)", "repository": "github:kixelated/moq", "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, + ".": "./src/index.ts", "./solid": "./src/solid.ts", "./react": "./src/react.ts", "./dom": "./src/dom.ts" From a62ec0ef15296a4377d0c4b6a1f75dde4c6917da Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Thu, 27 Nov 2025 12:25:58 -0500 Subject: [PATCH 09/54] use rimraf for clean --- js/bun.lockb | Bin 176608 -> 176608 bytes js/hang-ui/package.json | 7 ++++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/js/bun.lockb b/js/bun.lockb index af3da0616eb1154263002bb1daf9e362b733569c..418c0d47bcf3e8942d027f39c420208f19b62b8a 100755 GIT binary patch delta 1371 zcmeH{&r4KM6vyv*lR7N{2y7EX=%OK9v zTm=zGyQIanz{x^u6HGgIA&!|+Q3^ti6n(#R1S$Ukq2Yb+=bi64ci!c_bMH*apDFoI z`)%Gg#+1aEF!&k7Wxin827YIt^uT4rA#eq(0Lz*udAp`D$+}-)Ef{d}=FFQaBzB^J z6ZR;hI$oQx4E`ehBk=jZNzMPuJg5CB;NnwSNLC`wfb>tQzmc>~ zsZX-*G{^;>1DQI{LCJgvVzN{HPOgwE&4PU3B~5I`;MuO>Yc>DB7+%v>`C>!y5f>kk z>D@AGe~!!Tv;59%kTG-t8zF$$4D5mD>P2wB-Z(GR<2IVJqXlVBa98m+uBkc(+BOCC z>S3)pc_|OYKAw;rGV4YsBq>1`3cI0;P>=okN_rD~6#F%aIVmU&@kICt%|oA{1!&RT zdM!nqzLjmFl^a1y0kf*rm z$P4g!!ZblCcl50ktLj=&cL)9;Gyoki&M!(q+`IRZYqyuDC2HG4UexA7-if`}Z`|z5 h8S~`FVE@X4p5Cxr&9tT3(#8#kylk~AN4)t*zX5Q7jSv6; delta 1378 zcmeH{&r4KM6vy9rUL$1yTd084|c`6|<005H}KOi>4H6(KfiW zF>oWaN+NCrP9~28Q~6=>$^#13|-l@8_QHId|Tj`|iD^a->v_ z+#E9bu;Vyy#BoC4dk|me1DYCeg@MuqR}uTbFQ6a%u6dH(ipC`Cs^A7N%E9Y1?*x&E zqkseIlnEWMmbA8_;ly@;jCZ2pL>d|VLHc{(^LO`Z{%__v>{kINpVmS$fVdr`-=Y3m z(mJ3%$-09eC)f!xb(oEk`6GzQjQW{obU4#4kPqzD#Cr5@ZN8R^{(oawrhny)O{Au6 zYFb{@$+D@=%Gpl7XKs)?*ah4OF^HRid*H5m9{RsG&OP;bjLK%FAiJA*s`#1J)F$+Q zs8>)g4zFV6zFe1$uiz~~ub~pOY-b)wT4LRJ>~W|EI$=IOl+y`5m0LHB;jPdu wfsw)7aCT_K^!wZnd)wz8t4%g*QP Date: Sat, 29 Nov 2025 22:01:08 -0500 Subject: [PATCH 10/54] add buffering control to watch view and file input source to publish view --- js/bun.lock | 3 +- .../Components/publish/CameraSourceButton.tsx | 83 +++++++++--------- .../Components/publish/FileSourceButton.tsx | 38 +++++++++ .../publish/MicrophoneSourceButton.tsx | 84 +++++++++---------- .../publish/NothingSourceButton.tsx | 45 +++++----- .../Components/publish/PublishControls.tsx | 36 ++++---- .../PublishControlsContextProvider.tsx | 21 +++++ .../Components/publish/ScreenSourceButton.tsx | 45 +++++----- js/hang-ui/src/Components/publish/styles.css | 4 + .../Components/watch/BufferingIndicator.tsx | 14 ++++ .../src/Components/watch/FullscreenButton.tsx | 2 +- .../src/Components/watch/PlayPauseButton.tsx | 7 +- .../src/Components/watch/VolumeSlider.tsx | 2 +- .../src/Components/watch/WatchControls.tsx | 2 +- .../watch/WatchControlsContextProvider.tsx | 11 +++ js/hang-ui/src/Components/watch/element.tsx | 22 ++--- js/hang-ui/src/Components/watch/styles.css | 35 +++++++- 17 files changed, 286 insertions(+), 168 deletions(-) create mode 100644 js/hang-ui/src/Components/publish/FileSourceButton.tsx create mode 100644 js/hang-ui/src/Components/watch/BufferingIndicator.tsx diff --git a/js/bun.lock b/js/bun.lock index bf6f28878..94db3edb7 100644 --- a/js/bun.lock +++ b/js/bun.lock @@ -51,10 +51,11 @@ "name": "@kixelated/hang-ui", "version": "0.1.0", "devDependencies": { + "@biomejs/biome": "^2.2.2", "@kixelated/hang": "workspace:^0.7.0", - "@kixelated/signals": "workspace:^0.8.1", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", + "rimraf": "^6.0.1", "rollup": "^4.53.3", "rollup-plugin-string": "^3.0.0", "solid-element": "^1.9.1", diff --git a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx index dd387335d..ef3ee026a 100644 --- a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx @@ -1,51 +1,46 @@ -import MediaSourceSourceSelector from './MediaSourceSelector'; -import { Show, useContext } from 'solid-js'; -import { PublishControlsContext } from './PublishControlsContextProvider'; +import MediaSourceSourceSelector from "./MediaSourceSelector"; +import { Show, useContext } from "solid-js"; +import { PublishControlsContext } from "./PublishControlsContextProvider"; export default function CameraSourceButton() { - const context = useContext(PublishControlsContext); - const onClick = () => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const context = useContext(PublishControlsContext); + const onClick = () => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - if (hangPublishEl.source === 'camera') { - // Camera already selected, toggle video. - hangPublishEl.video = !hangPublishEl.video; - } else { - hangPublishEl.source = 'camera'; - hangPublishEl.video = true; - } - }; + if (hangPublishEl.source === "camera") { + // Camera already selected, toggle video. + hangPublishEl.video = !hangPublishEl.video; + } else { + hangPublishEl.source = "camera"; + hangPublishEl.video = true; + } + }; - const onSourceSelected = (sourceId: MediaDeviceInfo['deviceId']) => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const onSourceSelected = (sourceId: MediaDeviceInfo["deviceId"]) => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - hangPublishEl.videoDevice = sourceId; - }; + hangPublishEl.videoDevice = sourceId; + }; - return ( -
- - - - -
- ); + return ( +
+ + + + +
+ ); } diff --git a/js/hang-ui/src/Components/publish/FileSourceButton.tsx b/js/hang-ui/src/Components/publish/FileSourceButton.tsx new file mode 100644 index 000000000..f833e529b --- /dev/null +++ b/js/hang-ui/src/Components/publish/FileSourceButton.tsx @@ -0,0 +1,38 @@ +import { createSignal, useContext } from "solid-js"; +import { PublishControlsContext } from "./PublishControlsContextProvider"; + +export default function FileSourceButton() { + const [fileInputRef, setFileInputRef] = createSignal(); + const context = useContext(PublishControlsContext); + const onClick = () => fileInputRef()?.click(); + const onChange = (event: Event) => { + const castedInputEl = event.target as HTMLInputElement; + const file = castedInputEl.files?.[0]; + + if (file) { + context?.setFile(file); + castedInputEl.value = ""; + } + }; + + return ( + <> + + + + ); +} diff --git a/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx b/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx index 28d1109a0..5cf31e644 100644 --- a/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx @@ -1,52 +1,46 @@ -import MediaSourceSourceSelector from './MediaSourceSelector'; -import { Show, useContext } from 'solid-js'; -import { PublishControlsContext } from './PublishControlsContextProvider'; +import MediaSourceSourceSelector from "./MediaSourceSelector"; +import { Show, useContext } from "solid-js"; +import { PublishControlsContext } from "./PublishControlsContextProvider"; export default function MicrophoneSourceButton() { - const context = useContext(PublishControlsContext); - const onClick = () => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const context = useContext(PublishControlsContext); + const onClick = () => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - if (hangPublishEl.source === 'camera') { - // Camera already selected, toggle audio. - hangPublishEl.audio = !hangPublishEl.audio; - } else { - hangPublishEl.source = 'camera'; - hangPublishEl.audio = true; - } - }; + if (hangPublishEl.source === "camera") { + // Camera already selected, toggle audio. + hangPublishEl.audio = !hangPublishEl.audio; + } else { + hangPublishEl.source = "camera"; + hangPublishEl.audio = true; + } + }; - const onSourceSelected = (sourceId: MediaDeviceInfo['deviceId']) => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const onSourceSelected = (sourceId: MediaDeviceInfo["deviceId"]) => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - hangPublishEl.audioDevice = sourceId; - }; + hangPublishEl.audioDevice = sourceId; + }; - return ( -
- - - - -
- ); + return ( +
+ + + + +
+ ); } diff --git a/js/hang-ui/src/Components/publish/NothingSourceButton.tsx b/js/hang-ui/src/Components/publish/NothingSourceButton.tsx index eac66f861..83cd906ea 100644 --- a/js/hang-ui/src/Components/publish/NothingSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/NothingSourceButton.tsx @@ -1,28 +1,27 @@ -import { useContext } from 'solid-js'; -import { PublishControlsContext } from './PublishControlsContextProvider'; +import { useContext } from "solid-js"; +import { PublishControlsContext } from "./PublishControlsContextProvider"; export default function NothingSourceButton() { - const context = useContext(PublishControlsContext); - const onClick = () => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const context = useContext(PublishControlsContext); + const onClick = () => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - hangPublishEl.source = undefined; - hangPublishEl.video = false; - hangPublishEl.audio = false; - }; + hangPublishEl.source = undefined; + hangPublishEl.video = false; + hangPublishEl.audio = false; + }; - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/js/hang-ui/src/Components/publish/PublishControls.tsx b/js/hang-ui/src/Components/publish/PublishControls.tsx index 576feb612..e0fe30fa5 100644 --- a/js/hang-ui/src/Components/publish/PublishControls.tsx +++ b/js/hang-ui/src/Components/publish/PublishControls.tsx @@ -1,20 +1,22 @@ -import CameraSourceButton from './CameraSourceButton'; -import ScreenSourceButton from './ScreenSourceButton'; -import MicrophoneSourceButton from './MicrophoneSourceButton'; -import PublishStatusIndicator from './PublishStatusIndicator'; -import NothingSourceButton from './NothingSourceButton'; +import CameraSourceButton from "./CameraSourceButton"; +import ScreenSourceButton from "./ScreenSourceButton"; +import MicrophoneSourceButton from "./MicrophoneSourceButton"; +import PublishStatusIndicator from "./PublishStatusIndicator"; +import FileSourceButton from "./FileSourceButton"; +import NothingSourceButton from "./NothingSourceButton"; export default function PublishControls() { - return ( -
-
- Source: - - - - -
- -
- ); + return ( +
+
+ Source: + + + + + +
+ +
+ ); } diff --git a/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx b/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx index 31dfa0c6b..95cc3bd33 100644 --- a/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx +++ b/js/hang-ui/src/Components/publish/PublishControlsContextProvider.tsx @@ -12,9 +12,11 @@ type PublishControlsContextValue = { microphoneActive: () => boolean; cameraActive?: () => boolean; screenActive?: () => boolean; + fileActive?: () => boolean; nothingActive?: () => boolean; selectedCameraSource?: () => MediaDeviceInfo["deviceId"] | undefined; selectedMicrophoneSource?: () => MediaDeviceInfo["deviceId"] | undefined; + setFile: (file: File) => void; }; type PublishControlsContextProviderProps = { @@ -34,9 +36,21 @@ export default function PublishControlsContextProvider(props: PublishControlsCon const [cameraActive, setCameraActive] = createSignal(false); const [screenActive, setScreenActive] = createSignal(false); const [microphoneActive, setMicrophoneActive] = createSignal(false); + const [fileActive, setFileActive] = createSignal(false); const [nothingActive, setNothingActive] = createSignal(false); const [publishStatus, setPublishStatus] = createSignal("no-url"); + const setFile = (file: File) => { + const hangPublishEl = props.hangPublish(); + if (!hangPublishEl) return; + + hangPublishEl.file = file; + hangPublishEl.source = "file"; + hangPublishEl.video = true; + hangPublishEl.audio = true; + setFileActive(true); + }; + const value: PublishControlsContextValue = { hangPublish: props.hangPublish, cameraDevices, @@ -45,6 +59,8 @@ export default function PublishControlsContextProvider(props: PublishControlsCon cameraActive, screenActive, microphoneActive, + fileActive, + setFile, nothingActive, selectedCameraSource, selectedMicrophoneSource, @@ -161,5 +177,10 @@ export default function PublishControlsContextProvider(props: PublishControlsCon setPublishStatus("live"); } }); + + publishInstance?.signals.effect(function trackFileActive(effect) { + const selectedSource = effect.get(el.signals.source); + setFileActive(selectedSource === "file"); + }); } } diff --git a/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx b/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx index def6b9079..0d1d069f7 100644 --- a/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/ScreenSourceButton.tsx @@ -1,28 +1,27 @@ -import { useContext } from 'solid-js'; -import { PublishControlsContext } from './PublishControlsContextProvider'; +import { useContext } from "solid-js"; +import { PublishControlsContext } from "./PublishControlsContextProvider"; export default function ScreenSourceButton() { - const context = useContext(PublishControlsContext); - const onClick = () => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const context = useContext(PublishControlsContext); + const onClick = () => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - hangPublishEl.source = 'screen'; - hangPublishEl.audio = false; - hangPublishEl.video = true; - }; + hangPublishEl.source = "screen"; + hangPublishEl.audio = false; + hangPublishEl.video = true; + }; - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/js/hang-ui/src/Components/publish/styles.css b/js/hang-ui/src/Components/publish/styles.css index 7098c8496..479a91636 100644 --- a/js/hang-ui/src/Components/publish/styles.css +++ b/js/hang-ui/src/Components/publish/styles.css @@ -42,3 +42,7 @@ transform: translateX(-50%); display: block; } + +.hidden { + display: none; +} diff --git a/js/hang-ui/src/Components/watch/BufferingIndicator.tsx b/js/hang-ui/src/Components/watch/BufferingIndicator.tsx new file mode 100644 index 000000000..4d72c4311 --- /dev/null +++ b/js/hang-ui/src/Components/watch/BufferingIndicator.tsx @@ -0,0 +1,14 @@ +import { Show, useContext } from "solid-js"; +import { WatchControlsContext } from "./WatchControlsContextProvider"; + +export default function BufferingIndicator() { + const context = useContext(WatchControlsContext); + + return ( + +
+
+
+ + ); +} diff --git a/js/hang-ui/src/Components/watch/FullscreenButton.tsx b/js/hang-ui/src/Components/watch/FullscreenButton.tsx index bbc87590d..a0cb4dd3b 100644 --- a/js/hang-ui/src/Components/watch/FullscreenButton.tsx +++ b/js/hang-ui/src/Components/watch/FullscreenButton.tsx @@ -12,7 +12,7 @@ export default function FullscreenButton() { }; return ( - ); diff --git a/js/hang-ui/src/Components/watch/PlayPauseButton.tsx b/js/hang-ui/src/Components/watch/PlayPauseButton.tsx index f0cbc5eac..4db75e55a 100644 --- a/js/hang-ui/src/Components/watch/PlayPauseButton.tsx +++ b/js/hang-ui/src/Components/watch/PlayPauseButton.tsx @@ -8,7 +8,12 @@ export default function PlayPauseButton() { }; return ( - ); diff --git a/js/hang-ui/src/Components/watch/VolumeSlider.tsx b/js/hang-ui/src/Components/watch/VolumeSlider.tsx index d8dfa08d5..85862c36d 100644 --- a/js/hang-ui/src/Components/watch/VolumeSlider.tsx +++ b/js/hang-ui/src/Components/watch/VolumeSlider.tsx @@ -18,7 +18,7 @@ export default function VolumeSlider() { return (
- diff --git a/js/hang-ui/src/Components/watch/WatchControls.tsx b/js/hang-ui/src/Components/watch/WatchControls.tsx index 863add19a..6b3a6dfa7 100644 --- a/js/hang-ui/src/Components/watch/WatchControls.tsx +++ b/js/hang-ui/src/Components/watch/WatchControls.tsx @@ -5,7 +5,7 @@ import VolumeSlider from "./VolumeSlider"; export default function WatchControls() { return ( -
+
diff --git a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx index 5245b6318..a8e289dd1 100644 --- a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx +++ b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx @@ -18,6 +18,7 @@ type WatchControlsContextValue = { currentVolume: () => number; togglePlayback: () => void; toggleMuted: () => void; + buffering: () => boolean; }; export const WatchControlsContext = createContext(); @@ -27,6 +28,7 @@ export default function WatchControlsContextProvider(props: WatchControlsContext const [isPlaying, setIsPlaying] = createSignal(false); const [isMuted, setIsMuted] = createSignal(false); const [currentVolume, setCurrentVolume] = createSignal(0); + const [buffering, setBuffering] = createSignal(false); const togglePlayback = () => { const hangWatchEl = props.hangWatch(); @@ -63,6 +65,7 @@ export default function WatchControlsContextProvider(props: WatchControlsContext isMuted, currentVolume, toggleMuted, + buffering, }; createEffect(() => { @@ -112,5 +115,13 @@ export default function WatchControlsContextProvider(props: WatchControlsContext const volume = effect.get(watchInstance?.audio.volume); setCurrentVolume(volume * 100); }); + + watchInstance?.signals.effect(function trackBuffering(effect) { + const syncStatus = effect.get(watchInstance?.video.source.syncStatus); + const bufferStatus = effect.get(watchInstance?.video.source.bufferStatus); + const shouldShow = syncStatus.state === "wait" || bufferStatus.state === "empty"; + + setBuffering(shouldShow); + }); } } diff --git a/js/hang-ui/src/Components/watch/element.tsx b/js/hang-ui/src/Components/watch/element.tsx index d35e361ac..9cccd8625 100644 --- a/js/hang-ui/src/Components/watch/element.tsx +++ b/js/hang-ui/src/Components/watch/element.tsx @@ -1,11 +1,12 @@ -import { customElement } from "solid-element"; -import { createSignal, onMount } from "solid-js"; -import WatchControls from "./WatchControls"; import styles from "./styles.css"; +import WatchControls from "./WatchControls"; +import BufferingIndicator from "./BufferingIndicator"; import type HangWatch from "@kixelated/hang/watch/element"; import WatchControlsContextProvider from "./WatchControlsContextProvider"; +import { customElement } from "solid-element"; +import { createSignal, onMount } from "solid-js"; -customElement("hang-publish-ui", {}, function PublishControlsWebComponent(attributes, { element }) { +customElement("hang-publish-ui", {}, function PublishControlsWebComponent(_, { element }) { const [hangWatchhEl, setHangWatchEl] = createSignal(); onMount(() => { @@ -17,12 +18,13 @@ customElement("hang-publish-ui", {}, function PublishControlsWebComponent(attrib }); return ( - <> + - - - - - +
+ + +
+ +
); }); diff --git a/js/hang-ui/src/Components/watch/styles.css b/js/hang-ui/src/Components/watch/styles.css index 81d2458ba..5b39e2633 100644 --- a/js/hang-ui/src/Components/watch/styles.css +++ b/js/hang-ui/src/Components/watch/styles.css @@ -1,4 +1,9 @@ -.watchContainer { +.watchVideoContainer { + display: block; + position: relative; +} + +.watchControlsContainer { display: flex; gap: 8px; justify-content: space-around; @@ -24,3 +29,31 @@ width: 2em; text-align: right; } + +@keyframes buffer-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.bufferingContainer { + position: absolute; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1; + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(2px); + pointer-events: auto; +} + +.bufferingSpinner { + width: 40px; + height: 40px; + border: 4px solid rgba(255, 255, 255, 0.2); + border-top: 4px solid #fff; + border-radius: 50%; + animation: buffer-spin 1s linear infinite; +} \ No newline at end of file From 15014014f3af668f938ff8da373f39d2fc955af0 Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Sat, 29 Nov 2025 22:18:12 -0500 Subject: [PATCH 11/54] if hang instance is available when element is provided, run signal setup immediately --- .../src/Components/watch/WatchControlsContextProvider.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx index a8e289dd1..106b9e8cf 100644 --- a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx +++ b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx @@ -78,6 +78,9 @@ export default function WatchControlsContextProvider(props: WatchControlsContext const watchInstance = event.detail.instance.peek?.() as HangWatchInstance; onWatchInstanceAvailable(watchInstance); }); + } else { + const hangWatchInstance = hangWatchEl.active.peek?.() as HangWatchInstance; + onWatchInstanceAvailable(hangWatchInstance); } }); From 8b23c5bc1a5738550e50c5afea3bb9f5d76894b1 Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Sun, 30 Nov 2025 00:00:28 -0500 Subject: [PATCH 12/54] clean up formatting to satisfy biome. remove dist dependency changes by using esbuild instead of typescript rollup plugin. --- js/bun.lock | 5 + js/bun.lockb | Bin 176608 -> 177712 bytes js/deno.lock | 15 + js/hang-ui/biome.json | 36 +- js/hang-ui/package.json | 2 +- js/hang-ui/rollup.config.mjs | 72 ++-- .../Components/publish/CameraSourceButton.tsx | 82 ++-- .../Components/publish/FileSourceButton.tsx | 68 +-- .../publish/MediaSourceSelector.tsx | 7 +- .../publish/MicrophoneSourceButton.tsx | 83 ++-- .../publish/NothingSourceButton.tsx | 44 +- .../Components/publish/PublishControls.tsx | 38 +- .../PublishControlsContextProvider.tsx | 400 ++++++++++-------- .../publish/PublishStatusIndicator.tsx | 46 +- .../Components/publish/ScreenSourceButton.tsx | 44 +- js/hang-ui/src/Components/publish/element.tsx | 52 +-- js/hang-ui/src/Components/publish/styles.css | 53 +-- .../Components/watch/BufferingIndicator.tsx | 20 +- .../src/Components/watch/FullscreenButton.tsx | 35 +- .../src/Components/watch/PlayPauseButton.tsx | 32 +- .../src/Components/watch/VolumeSlider.tsx | 55 ++- .../src/Components/watch/WatchControls.tsx | 24 +- .../watch/WatchControlsContextProvider.tsx | 262 ++++++------ .../Components/watch/WatchStatusIndicator.tsx | 44 +- js/hang-ui/src/Components/watch/element.tsx | 56 +-- js/hang-ui/src/Components/watch/styles.css | 54 +-- js/hang-ui/src/css.d.ts | 6 +- js/hang-ui/tsconfig.json | 33 +- js/hang/package.json | 10 +- js/moq/package.json | 4 +- js/package.json | 3 +- 31 files changed, 922 insertions(+), 763 deletions(-) diff --git a/js/bun.lock b/js/bun.lock index 94db3edb7..21a38a4eb 100644 --- a/js/bun.lock +++ b/js/bun.lock @@ -5,6 +5,7 @@ "name": "moq", "devDependencies": { "concurrently": "^9.2.1", + "rollup-plugin-esbuild": "^6.2.1", }, }, "hang": { @@ -882,6 +883,8 @@ "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + "rollup-plugin-esbuild": ["rollup-plugin-esbuild@6.2.1", "", { "dependencies": { "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "get-tsconfig": "^4.10.0", "unplugin-utils": "^0.2.4" }, "peerDependencies": { "esbuild": ">=0.18.0", "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-jTNOMGoMRhs0JuueJrJqbW8tOwxumaWYq+V5i+PD+8ecSCVkuX27tGW7BXqDgoULQ55rO7IdNxPcnsWtshz3AA=="], + "rollup-plugin-string": ["rollup-plugin-string@3.0.0", "", { "dependencies": { "rollup-pluginutils": "^2.4.1" } }, "sha512-vqyzgn9QefAgeKi+Y4A7jETeIAU1zQmS6VotH6bzm/zmUQEnYkpIGRaOBPY41oiWYV4JyBoGAaBjYMYuv+6wVw=="], "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], @@ -996,6 +999,8 @@ "unplugin-solid": ["unplugin-solid@1.0.0", "", { "dependencies": { "@babel/core": "^7.28.3", "@rollup/pluginutils": "^5.2.0", "babel-preset-solid": "^1.9.9", "merge-anything": "^6.0.6", "solid-refresh": "^0.7.5", "unplugin": "^2.3.10", "vitefu": "^1.1.1" }, "peerDependencies": { "solid-js": "^1.9.9" } }, "sha512-pv1CS3XMtf3WwX8Dq9Bvo4qH6mfjN2xOgbaPcnqW1dLhyP/JQCvueGEsN0dYIZ4JvxaD/G/Ot1JnBzNQGHkfeA=="], + "unplugin-utils": ["unplugin-utils@0.2.5", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg=="], + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], diff --git a/js/bun.lockb b/js/bun.lockb index 418c0d47bcf3e8942d027f39c420208f19b62b8a..98c3e44b4297bc648a6be772d70b50ecf41f07e8 100755 GIT binary patch delta 34232 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 Fe*u}EeGdQt diff --git a/js/deno.lock b/js/deno.lock index c9bf11d1d..5a8a87d3c 100644 --- a/js/deno.lock +++ b/js/deno.lock @@ -47,6 +47,21 @@ ] } }, + "hang-ui": { + "packageJson": { + "dependencies": [ + "npm:@biomejs/biome@^2.2.2", + "npm:@rollup/plugin-node-resolve@^16.0.3", + "npm:@rollup/plugin-typescript@^12.3.0", + "npm:rimraf@^6.0.1", + "npm:rollup-plugin-string@3", + "npm:rollup@^4.53.3", + "npm:solid-element@^1.9.1", + "npm:solid-js@^1.9.10", + "npm:unplugin-solid@1" + ] + } + }, "moq": { "packageJson": { "dependencies": [ diff --git a/js/hang-ui/biome.json b/js/hang-ui/biome.json index efd464db1..0f8631995 100644 --- a/js/hang-ui/biome.json +++ b/js/hang-ui/biome.json @@ -1,20 +1,20 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json", - "formatter": { - "enabled": true - }, - "javascript": { - "formatter": { - "lineWidth": 80, - "indentStyle": "space", - "indentWidth": 4, - "lineEnding": "lf", - "quoteStyle": "single", - "quoteProperties": "asNeeded", - "trailingCommas": "es5", - "bracketSpacing": true, - "arrowParentheses": "always", - "semicolons": "always" - } - } + "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json", + "formatter": { + "enabled": true + }, + "javascript": { + "formatter": { + "lineWidth": 80, + "indentStyle": "space", + "indentWidth": 4, + "lineEnding": "lf", + "quoteStyle": "single", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "bracketSpacing": true, + "arrowParentheses": "always", + "semicolons": "always" + } + } } diff --git a/js/hang-ui/package.json b/js/hang-ui/package.json index c79e15bd3..983c99dc4 100644 --- a/js/hang-ui/package.json +++ b/js/hang-ui/package.json @@ -13,11 +13,11 @@ }, "scripts": { "build": "npm run clean && rollup -c", + "check": "biome check --write src", "clean": "rimraf dist" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.53.3", "rollup-plugin-string": "^3.0.0", "solid-element": "^1.9.1", diff --git a/js/hang-ui/rollup.config.mjs b/js/hang-ui/rollup.config.mjs index 01269bd68..2535ef44c 100644 --- a/js/hang-ui/rollup.config.mjs +++ b/js/hang-ui/rollup.config.mjs @@ -1,35 +1,43 @@ -import solid from "unplugin-solid/rollup"; -import nodeResolve from "@rollup/plugin-node-resolve"; -import typescript from "@rollup/plugin-typescript"; -import { string } from "rollup-plugin-string"; +import nodeResolve from '@rollup/plugin-node-resolve'; +import esbuild from 'rollup-plugin-esbuild'; +import { string } from 'rollup-plugin-string'; +import solid from 'unplugin-solid/rollup'; export default [ - { - input: "src/Components/publish/element.tsx", - output: { - file: "dist/publish-controls.esm.js", - format: "es", - sourcemap: true, - }, - plugins: [ - string({ include: "**/*.css" }), - solid({ dev: false, hydratable: false }), - nodeResolve({ extensions: [".js", ".ts", ".tsx"] }), - typescript(), - ], - }, - { - input: "src/Components/watch/element.tsx", - output: { - file: "dist/watch-controls.esm.js", - format: "es", - sourcemap: true, - }, - plugins: [ - string({ include: "**/*.css" }), - solid({ dev: false, hydratable: false }), - nodeResolve({ extensions: [".js", ".ts", ".tsx"] }), - typescript(), - ], - }, + { + input: 'src/Components/publish/element.tsx', + output: { + file: 'dist/publish-controls.esm.js', + format: 'es', + sourcemap: true, + }, + plugins: [ + string({ include: '**/*.css' }), + solid({ dev: false, hydratable: false }), + esbuild({ + include: /\.[jt]sx?$/, // .js, .ts, .jsx, .tsx + jsx: 'preserve', // let unplugin-solid handle JSX + tsconfig: 'tsconfig.json', // optional; mainly for paths/aliases + }), + nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), + ], + }, + { + input: 'src/Components/watch/element.tsx', + output: { + file: 'dist/watch-controls.esm.js', + format: 'es', + sourcemap: true, + }, + plugins: [ + string({ include: '**/*.css' }), + solid({ dev: false, hydratable: false }), + esbuild({ + include: /\.[jt]sx?$/, // .js, .ts, .jsx, .tsx + jsx: 'preserve', // let unplugin-solid handle JSX + tsconfig: 'tsconfig.json', // optional; mainly for paths/aliases + }), + nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), + ], + }, ]; diff --git a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx index ef3ee026a..c9813b46c 100644 --- a/js/hang-ui/src/Components/publish/CameraSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/CameraSourceButton.tsx @@ -1,46 +1,50 @@ -import MediaSourceSourceSelector from "./MediaSourceSelector"; -import { Show, useContext } from "solid-js"; -import { PublishControlsContext } from "./PublishControlsContextProvider"; +import { Show, useContext } from 'solid-js'; +import MediaSourceSourceSelector from './MediaSourceSelector'; +import { PublishControlsContext } from './PublishControlsContextProvider'; export default function CameraSourceButton() { - const context = useContext(PublishControlsContext); - const onClick = () => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const context = useContext(PublishControlsContext); + const onClick = () => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - if (hangPublishEl.source === "camera") { - // Camera already selected, toggle video. - hangPublishEl.video = !hangPublishEl.video; - } else { - hangPublishEl.source = "camera"; - hangPublishEl.video = true; - } - }; + if (hangPublishEl.source === 'camera') { + // Camera already selected, toggle video. + hangPublishEl.video = !hangPublishEl.video; + } else { + hangPublishEl.source = 'camera'; + hangPublishEl.video = true; + } + }; - const onSourceSelected = (sourceId: MediaDeviceInfo["deviceId"]) => { - const hangPublishEl = context?.hangPublish(); - if (!hangPublishEl) return; + const onSourceSelected = (sourceId: MediaDeviceInfo['deviceId']) => { + const hangPublishEl = context?.hangPublish(); + if (!hangPublishEl) return; - hangPublishEl.videoDevice = sourceId; - }; + hangPublishEl.videoDevice = sourceId; + }; - return ( -
- - - - -
- ); + return ( +
+ + + + +
+ ); } diff --git a/js/hang-ui/src/Components/publish/FileSourceButton.tsx b/js/hang-ui/src/Components/publish/FileSourceButton.tsx index f833e529b..6fc9193b8 100644 --- a/js/hang-ui/src/Components/publish/FileSourceButton.tsx +++ b/js/hang-ui/src/Components/publish/FileSourceButton.tsx @@ -1,38 +1,40 @@ -import { createSignal, useContext } from "solid-js"; -import { PublishControlsContext } from "./PublishControlsContextProvider"; +import { createSignal, useContext } from 'solid-js'; +import { PublishControlsContext } from './PublishControlsContextProvider'; export default function FileSourceButton() { - const [fileInputRef, setFileInputRef] = createSignal(); - const context = useContext(PublishControlsContext); - const onClick = () => fileInputRef()?.click(); - const onChange = (event: Event) => { - const castedInputEl = event.target as HTMLInputElement; - const file = castedInputEl.files?.[0]; + const [fileInputRef, setFileInputRef] = createSignal< + HTMLInputElement | undefined + >(); + const context = useContext(PublishControlsContext); + const onClick = () => fileInputRef()?.click(); + const onChange = (event: Event) => { + const castedInputEl = event.target as HTMLInputElement; + const file = castedInputEl.files?.[0]; - if (file) { - context?.setFile(file); - castedInputEl.value = ""; - } - }; + if (file) { + context?.setFile(file); + castedInputEl.value = ''; + } + }; - return ( - <> - - - - ); + return ( + <> + + + + ); } diff --git a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx index 53445a9c0..36f234513 100644 --- a/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx +++ b/js/hang-ui/src/Components/publish/MediaSourceSelector.tsx @@ -19,13 +19,14 @@ export default function MediaSourceSelector(props: MediaSourceSelectorProps) { return ( <> - {sourcesVisible() ? 'β–Ό' : 'β–²'} - + - {volumeLabel()} -
- ); + return ( +
+ + + {volumeLabel()} +
+ ); } diff --git a/js/hang-ui/src/Components/watch/WatchControls.tsx b/js/hang-ui/src/Components/watch/WatchControls.tsx index 6b3a6dfa7..332324951 100644 --- a/js/hang-ui/src/Components/watch/WatchControls.tsx +++ b/js/hang-ui/src/Components/watch/WatchControls.tsx @@ -1,15 +1,15 @@ -import WatchStatusIndicator from "./WatchStatusIndicator"; -import FullscreenButton from "./FullscreenButton"; -import PlayPauseButton from "./PlayPauseButton"; -import VolumeSlider from "./VolumeSlider"; +import FullscreenButton from './FullscreenButton'; +import PlayPauseButton from './PlayPauseButton'; +import VolumeSlider from './VolumeSlider'; +import WatchStatusIndicator from './WatchStatusIndicator'; export default function WatchControls() { - return ( -
- - - - -
- ); + return ( +
+ + + + +
+ ); } diff --git a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx index 106b9e8cf..2678b62c7 100644 --- a/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx +++ b/js/hang-ui/src/Components/watch/WatchControlsContextProvider.tsx @@ -1,130 +1,154 @@ -import { createContext, createEffect, createSignal } from "solid-js"; -import type HangWatch from "@kixelated/hang/watch/element"; -import type { HangWatchInstance } from "@kixelated/hang/watch/element"; +import type HangWatch from '@kixelated/hang/watch/element'; +import type { HangWatchInstance } from '@kixelated/hang/watch/element'; +import type { JSX } from 'solid-js'; +import { createContext, createEffect, createSignal } from 'solid-js'; type WatchControlsContextProviderProps = { - hangWatch: () => HangWatch | undefined; - children: any; + hangWatch: () => HangWatch | undefined; + children: JSX.Element; }; -type WatchStatus = "no-url" | "disconnected" | "connecting" | "offline" | "loading" | "live" | "connected"; +type WatchStatus = + | 'no-url' + | 'disconnected' + | 'connecting' + | 'offline' + | 'loading' + | 'live' + | 'connected'; type WatchControlsContextValue = { - hangWatch: () => HangWatch | undefined; - watchStatus: () => WatchStatus; - isPlaying: () => boolean; - isMuted: () => boolean; - setVolume: (vol: number) => void; - currentVolume: () => number; - togglePlayback: () => void; - toggleMuted: () => void; - buffering: () => boolean; + hangWatch: () => HangWatch | undefined; + watchStatus: () => WatchStatus; + isPlaying: () => boolean; + isMuted: () => boolean; + setVolume: (vol: number) => void; + currentVolume: () => number; + togglePlayback: () => void; + toggleMuted: () => void; + buffering: () => boolean; }; export const WatchControlsContext = createContext(); -export default function WatchControlsContextProvider(props: WatchControlsContextProviderProps) { - const [watchStatus, setWatchStatus] = createSignal("no-url"); - const [isPlaying, setIsPlaying] = createSignal(false); - const [isMuted, setIsMuted] = createSignal(false); - const [currentVolume, setCurrentVolume] = createSignal(0); - const [buffering, setBuffering] = createSignal(false); - - const togglePlayback = () => { - const hangWatchEl = props.hangWatch(); - - if (hangWatchEl) { - hangWatchEl.paused = !hangWatchEl.paused; - } - }; - - const setVolume = (volume: number) => { - const hangWatchEl = props.hangWatch(); - - if (hangWatchEl) { - hangWatchEl.volume = volume / 100; - } - }; - - const toggleMuted = () => { - setIsMuted(!isMuted()); - - const hangWatchEl = props.hangWatch(); - - if (hangWatchEl) { - hangWatchEl.muted = isMuted(); - } - }; - - const value: WatchControlsContextValue = { - hangWatch: props.hangWatch, - watchStatus, - togglePlayback, - isPlaying, - setVolume, - isMuted, - currentVolume, - toggleMuted, - buffering, - }; - - createEffect(() => { - const hangWatchEl = props.hangWatch(); - if (!hangWatchEl) return; - - if (!hangWatchEl.active) { - // @ts-ignore ignore custom event - todo add event map - hangWatchEl.addEventListener("watch-instance-available", (event: CustomEvent) => { - const watchInstance = event.detail.instance.peek?.() as HangWatchInstance; - onWatchInstanceAvailable(watchInstance); - }); - } else { - const hangWatchInstance = hangWatchEl.active.peek?.() as HangWatchInstance; - onWatchInstanceAvailable(hangWatchInstance); - } - }); - - return {props.children}; - - function onWatchInstanceAvailable(watchInstance: HangWatchInstance) { - watchInstance?.signals.effect(function trackWatchStatus(effect) { - const url = effect.get(watchInstance?.connection.url); - const connection = effect.get(watchInstance?.connection.status); - const broadcast = effect.get(watchInstance?.broadcast.status); - - if (!url) { - setWatchStatus("no-url"); - } else if (connection === "disconnected") { - setWatchStatus("disconnected"); - } else if (connection === "connecting") { - setWatchStatus("connecting"); - } else if (broadcast === "offline") { - setWatchStatus("offline"); - } else if (broadcast === "loading") { - setWatchStatus("loading"); - } else if (broadcast === "live") { - setWatchStatus("live"); - } else if (connection === "connected") { - setWatchStatus("connected"); - } - }); - - watchInstance?.signals.effect(function trackPlaying(effect) { - const paused = effect.get(watchInstance?.video.paused); - setIsPlaying(!paused); - }); - - watchInstance?.signals.effect(function trackVolume(effect) { - const volume = effect.get(watchInstance?.audio.volume); - setCurrentVolume(volume * 100); - }); - - watchInstance?.signals.effect(function trackBuffering(effect) { - const syncStatus = effect.get(watchInstance?.video.source.syncStatus); - const bufferStatus = effect.get(watchInstance?.video.source.bufferStatus); - const shouldShow = syncStatus.state === "wait" || bufferStatus.state === "empty"; - - setBuffering(shouldShow); - }); - } +export default function WatchControlsContextProvider( + props: WatchControlsContextProviderProps +) { + const [watchStatus, setWatchStatus] = createSignal('no-url'); + const [isPlaying, setIsPlaying] = createSignal(false); + const [isMuted, setIsMuted] = createSignal(false); + const [currentVolume, setCurrentVolume] = createSignal(0); + const [buffering, setBuffering] = createSignal(false); + + const togglePlayback = () => { + const hangWatchEl = props.hangWatch(); + + if (hangWatchEl) { + hangWatchEl.paused = !hangWatchEl.paused; + } + }; + + const setVolume = (volume: number) => { + const hangWatchEl = props.hangWatch(); + + if (hangWatchEl) { + hangWatchEl.volume = volume / 100; + } + }; + + const toggleMuted = () => { + setIsMuted(!isMuted()); + + const hangWatchEl = props.hangWatch(); + + if (hangWatchEl) { + hangWatchEl.muted = isMuted(); + } + }; + + const value: WatchControlsContextValue = { + hangWatch: props.hangWatch, + watchStatus, + togglePlayback, + isPlaying, + setVolume, + isMuted, + currentVolume, + toggleMuted, + buffering, + }; + + createEffect(() => { + const hangWatchEl = props.hangWatch(); + if (!hangWatchEl) return; + + if (!hangWatchEl.active) { + // @ts-expect-error ignore custom event - todo add event map + hangWatchEl.addEventListener( + 'watch-instance-available', + (event: CustomEvent) => { + const watchInstance = + event.detail.instance.peek?.() as HangWatchInstance; + onWatchInstanceAvailable(watchInstance); + } + ); + } else { + const hangWatchInstance = + hangWatchEl.active.peek?.() as HangWatchInstance; + onWatchInstanceAvailable(hangWatchInstance); + } + }); + + return ( + + {props.children} + + ); + + function onWatchInstanceAvailable(watchInstance: HangWatchInstance) { + watchInstance?.signals.effect(function trackWatchStatus(effect) { + const url = effect.get(watchInstance?.connection.url); + const connection = effect.get(watchInstance?.connection.status); + const broadcast = effect.get(watchInstance?.broadcast.status); + + if (!url) { + setWatchStatus('no-url'); + } else if (connection === 'disconnected') { + setWatchStatus('disconnected'); + } else if (connection === 'connecting') { + setWatchStatus('connecting'); + } else if (broadcast === 'offline') { + setWatchStatus('offline'); + } else if (broadcast === 'loading') { + setWatchStatus('loading'); + } else if (broadcast === 'live') { + setWatchStatus('live'); + } else if (connection === 'connected') { + setWatchStatus('connected'); + } + }); + + watchInstance?.signals.effect(function trackPlaying(effect) { + const paused = effect.get(watchInstance?.video.paused); + setIsPlaying(!paused); + }); + + watchInstance?.signals.effect(function trackVolume(effect) { + const volume = effect.get(watchInstance?.audio.volume); + setCurrentVolume(volume * 100); + }); + + watchInstance?.signals.effect(function trackBuffering(effect) { + const syncStatus = effect.get( + watchInstance?.video.source.syncStatus + ); + const bufferStatus = effect.get( + watchInstance?.video.source.bufferStatus + ); + const shouldShow = + syncStatus.state === 'wait' || bufferStatus.state === 'empty'; + + setBuffering(shouldShow); + }); + } } diff --git a/js/hang-ui/src/Components/watch/WatchStatusIndicator.tsx b/js/hang-ui/src/Components/watch/WatchStatusIndicator.tsx index 08bbb15b3..fc80c4b15 100644 --- a/js/hang-ui/src/Components/watch/WatchStatusIndicator.tsx +++ b/js/hang-ui/src/Components/watch/WatchStatusIndicator.tsx @@ -1,20 +1,32 @@ -import { Switch, Match, useContext } from "solid-js"; -import { WatchControlsContext } from "./WatchControlsContextProvider"; +import { Match, Switch, useContext } from 'solid-js'; +import { WatchControlsContext } from './WatchControlsContextProvider'; export default function WatchStatusIndicator() { - const context = useContext(WatchControlsContext); + const context = useContext(WatchControlsContext); - return ( -
- - πŸ”΄ No URL - πŸ”΄ Disconnected - 🟑 Connecting... - πŸ”΄ Offline - 🟑 Loading... - 🟒 Live - 🟒 Connected - -
- ); + return ( + + + + πŸ”΄ No URL + + + πŸ”΄ Disconnected + + + 🟑 Connecting... + + + πŸ”΄ Offline + + + 🟑 Loading... + + 🟒 Live + + 🟒 Connected + + + + ); } diff --git a/js/hang-ui/src/Components/watch/element.tsx b/js/hang-ui/src/Components/watch/element.tsx index 9cccd8625..341857721 100644 --- a/js/hang-ui/src/Components/watch/element.tsx +++ b/js/hang-ui/src/Components/watch/element.tsx @@ -1,30 +1,34 @@ -import styles from "./styles.css"; -import WatchControls from "./WatchControls"; -import BufferingIndicator from "./BufferingIndicator"; -import type HangWatch from "@kixelated/hang/watch/element"; -import WatchControlsContextProvider from "./WatchControlsContextProvider"; -import { customElement } from "solid-element"; -import { createSignal, onMount } from "solid-js"; +import type HangWatch from '@kixelated/hang/watch/element'; +import { customElement } from 'solid-element'; +import { createSignal, onMount } from 'solid-js'; +import BufferingIndicator from './BufferingIndicator'; +import styles from './styles.css'; +import WatchControls from './WatchControls'; +import WatchControlsContextProvider from './WatchControlsContextProvider'; -customElement("hang-publish-ui", {}, function PublishControlsWebComponent(_, { element }) { - const [hangWatchhEl, setHangWatchEl] = createSignal(); +customElement( + 'hang-publish-ui', + {}, + function PublishControlsWebComponent(_, { element }) { + const [hangWatchhEl, setHangWatchEl] = createSignal(); - onMount(() => { - const watchEl = element.querySelector("hang-watch"); + onMount(() => { + const watchEl = element.querySelector('hang-watch'); - if (watchEl) { - setHangWatchEl(watchEl); - } - }); + if (watchEl) { + setHangWatchEl(watchEl); + } + }); - return ( - - -
- - -
- -
- ); -}); + return ( + + +
+ + +
+ +
+ ); + } +); diff --git a/js/hang-ui/src/Components/watch/styles.css b/js/hang-ui/src/Components/watch/styles.css index 5b39e2633..99e85d2d4 100644 --- a/js/hang-ui/src/Components/watch/styles.css +++ b/js/hang-ui/src/Components/watch/styles.css @@ -1,59 +1,63 @@ .watchVideoContainer { - display: block; - position: relative; + display: block; + position: relative; } .watchControlsContainer { - display: flex; + display: flex; gap: 8px; - justify-content: space-around; + justify-content: space-around; align-content: center; } .watchControlButton { - all: unset; - cursor: pointer; - display: inline-block; - font: inherit; - color: inherit; + all: unset; + cursor: pointer; + display: inline-block; + font: inherit; + color: inherit; } .volumeSliderContainer { - display: flex; - align-items: center; - gap: 0.25rem; + display: flex; + align-items: center; + gap: 0.25rem; } .volumeLabel { - display: inline-block; - width: 2em; - text-align: right; + display: inline-block; + width: 2em; + text-align: right; } @keyframes buffer-spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .bufferingContainer { - position: absolute; + position: absolute; justify-content: center; align-items: center; width: 100%; height: 100%; top: 0; - left: 0; - z-index: 1; - background-color: rgba(0, 0, 0, 0.4); + left: 0; + z-index: 1; + background-color: rgba(0, 0, 0, 0.4); backdrop-filter: blur(2px); pointer-events: auto; } .bufferingSpinner { - width: 40px; - height: 40px; + width: 40px; + height: 40px; border: 4px solid rgba(255, 255, 255, 0.2); - border-top: 4px solid #fff; + border-top: 4px solid #fff; border-radius: 50%; animation: buffer-spin 1s linear infinite; -} \ No newline at end of file +} diff --git a/js/hang-ui/src/css.d.ts b/js/hang-ui/src/css.d.ts index 64653ede5..d257ebed7 100644 --- a/js/hang-ui/src/css.d.ts +++ b/js/hang-ui/src/css.d.ts @@ -1,4 +1,4 @@ declare module '*.css' { - const content: string; - export default content; -} \ No newline at end of file + const content: string; + export default content; +} diff --git a/js/hang-ui/tsconfig.json b/js/hang-ui/tsconfig.json index 3c6809659..cb6ec2aec 100644 --- a/js/hang-ui/tsconfig.json +++ b/js/hang-ui/tsconfig.json @@ -1,18 +1,19 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "skipLibCheck": true, - "outDir": "dist", - "noEmit": false, - "declaration": true, - "emitDeclarationOnly": true, - }, - "include": ["src"] + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "skipLibCheck": true, + "outDir": "dist", + "noEmit": true, + "declaration": true, + "emitDeclarationOnly": true + }, + "include": ["src"] } diff --git a/js/hang/package.json b/js/hang/package.json index 798b75999..690373348 100644 --- a/js/hang/package.json +++ b/js/hang/package.json @@ -8,15 +8,9 @@ "exports": { ".": "./src/index.ts", "./publish": "./src/publish/index.ts", - "./publish/element": { - "import": "./dist/publish/element.js", - "types": "./dist/publish/element.d.ts" - }, + "./publish/element": "./src/publish/element.ts", "./watch": "./src/watch/index.ts", - "./watch/element": { - "import": "./dist/watch/element.js", - "types": "./dist/watch/element.d.ts" - }, + "./watch/element": "./src/watch/element.ts", "./meet": "./src/meet/index.ts", "./meet/element": "./src/meet/element.ts", "./catalog": "./src/catalog/index.ts", diff --git a/js/moq/package.json b/js/moq/package.json index bf04c6ec7..ef3b4234b 100644 --- a/js/moq/package.json +++ b/js/moq/package.json @@ -6,8 +6,8 @@ "license": "(MIT OR Apache-2.0)", "repository": "github:kixelated/moq", "exports": { - ".": "./dist/index.js", - "./zod": "./dist/zod.js" + ".": "./src/index.ts", + "./zod": "./src/zod.ts" }, "scripts": { "build": "rimraf dist && tsc -b && bun ../scripts/package.ts", diff --git a/js/package.json b/js/package.json index 0a204b14f..9cae177c3 100644 --- a/js/package.json +++ b/js/package.json @@ -5,11 +5,10 @@ "type": "module", "workspaces": [ "moq", - "moq-clock", + "moq-clock", "moq-token", "hang", "hang-demo", - "hang-ui", "signals" ], "scripts": { From c1879236a6e63e0638b5098248741ec5667b99a4 Mon Sep 17 00:00:00 2001 From: James Reetzke Date: Sun, 30 Nov 2025 00:13:53 -0500 Subject: [PATCH 13/54] fix webcomponent name --- js/hang-demo/src/index.html | 6 +-- js/hang-ui/package.json | 2 +- js/hang-ui/src/Components/watch/element.tsx | 56 ++++++++++----------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/js/hang-demo/src/index.html b/js/hang-demo/src/index.html index 2c76955bb..27212f127 100644 --- a/js/hang-demo/src/index.html +++ b/js/hang-demo/src/index.html @@ -27,11 +27,11 @@ TODO: There's a bug with Big Buck Bunny causing audio to stutter, so we need to increase the latency to 100ms. --> - - + + - +

Other demos: