From 086f7a85c6f5a71aca779ec0beea032e3f818d8a Mon Sep 17 00:00:00 2001 From: Jamil Date: Tue, 23 Jan 2024 09:07:25 -0800 Subject: [PATCH] refactor(windows): Windows UI polish (#3338) - Add Tailwind and Flowbite for consistent UI --------- Signed-off-by: Reactor Scram Co-authored-by: Reactor Scram --- .github/dependabot.yml | 4 + .github/workflows/_rust.yml | 2 +- .github/workflows/cd.yml | 2 +- rust/windows-client/.gitignore | 6 + rust/windows-client/README.md | 28 +- rust/windows-client/build-debug.bat | 14 + rust/windows-client/build.bat | 14 + rust/windows-client/build.sh | 18 + rust/windows-client/dev.bat | 14 + rust/windows-client/dev.sh | 24 + rust/windows-client/package.json | 25 + rust/windows-client/pnpm-lock.yaml | 1168 +++++++++++++++++ rust/windows-client/src-tauri/Cargo.toml | 2 +- .../src-tauri/src/client/gui.rs | 19 +- .../src-tauri/src/client/logging.rs | 56 +- .../src-tauri/src/client/settings.rs | 17 +- rust/windows-client/src-tauri/tauri.conf.json | 6 +- rust/windows-client/src/about.html | 4 +- rust/windows-client/src/input.css | 9 + rust/windows-client/src/main.js | 1 - rust/windows-client/src/settings.html | 222 +++- rust/windows-client/src/settings.js | 173 --- rust/windows-client/src/settings.ts | 225 ++++ rust/windows-client/src/styles.css | 114 -- rust/windows-client/tailwind.config.js | 61 + rust/windows-client/tsconfig.json | 12 + 26 files changed, 1822 insertions(+), 418 deletions(-) create mode 100755 rust/windows-client/build-debug.bat create mode 100755 rust/windows-client/build.bat create mode 100755 rust/windows-client/build.sh create mode 100755 rust/windows-client/dev.bat create mode 100755 rust/windows-client/dev.sh create mode 100644 rust/windows-client/package.json create mode 100644 rust/windows-client/pnpm-lock.yaml create mode 100644 rust/windows-client/src/input.css delete mode 100644 rust/windows-client/src/main.js delete mode 100644 rust/windows-client/src/settings.js create mode 100644 rust/windows-client/src/settings.ts delete mode 100644 rust/windows-client/src/styles.css create mode 100644 rust/windows-client/tailwind.config.js create mode 100644 rust/windows-client/tsconfig.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 38736571d..9886068a1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -68,6 +68,10 @@ updates: directory: website/ schedule: interval: monthly + - package-ecosystem: npm + directory: rust/windows-client/ + schedule: + interval: monthly - package-ecosystem: npm directory: elixir/apps/web/assets/ schedule: diff --git a/.github/workflows/_rust.yml b/.github/workflows/_rust.yml index 825158285..4a0039881 100644 --- a/.github/workflows/_rust.yml +++ b/.github/workflows/_rust.yml @@ -103,7 +103,7 @@ jobs: # Used for release artifact # In release mode the name comes from tauri.conf.json - cp "target/release/Firezone Windows Client.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe" + cp "target/release/Firezone.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe" cp "target/release/bundle/msi/*.msi" "${{ env.BINARY_DEST_PATH }}-x64.msi" Get-FileHash ${{ env.BINARY_DEST_PATH }}-x64.exe -Algorithm SHA256 | Select-Object Hash > ${{ env.BINARY_DEST_PATH }}-x64.exe.sha256sum.txt diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9232a9210..b5446fdac 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -269,7 +269,7 @@ jobs: # Used for release artifact # This should match 'build-tauri' in _rust.yml - cp "target/release/Firezone Windows Client.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe" + cp "target/release/Firezone.exe" "${{ env.BINARY_DEST_PATH }}-x64.exe" cp "target/release/bundle/msi/*.msi" "${{ env.BINARY_DEST_PATH }}-x64.msi" Get-FileHash ${{ env.BINARY_DEST_PATH }}-x64.exe -Algorithm SHA256 | Select-Object Hash > ${{ env.BINARY_DEST_PATH }}-x64.exe.sha256sum.txt diff --git a/rust/windows-client/.gitignore b/rust/windows-client/.gitignore index 1e4cda132..ff86aab18 100644 --- a/rust/windows-client/.gitignore +++ b/rust/windows-client/.gitignore @@ -20,3 +20,9 @@ dist-ssr *.njsproj *.sln *.sw? + +# CSS output from the Tailwind compiler +src/output.css + +# JS output from the TypeScript compiler +src/**/*.js diff --git a/rust/windows-client/README.md b/rust/windows-client/README.md index 957b1bda0..c36c3a29b 100644 --- a/rust/windows-client/README.md +++ b/rust/windows-client/README.md @@ -7,7 +7,7 @@ This crate houses a Windows GUI client. This is the minimal toolchain needed to compile natively for x86_64 Windows: 1. [Install rustup](https://win.rustup.rs/x86_64) for Windows. -1. Install Tauri tooling: `cargo install tauri-cli` +1. Install [pnpm](https://pnpm.io/installation) for your platform. ### Recommended IDE Setup @@ -19,15 +19,20 @@ This is the minimal toolchain needed to compile natively for x86_64 Windows: ## Building +Builds are best started from the frontend tool `pnpm`. This ensures typescript +and css is compiled properly before bundling the application. + +See the [`package.json`](./package.json) script for more details as to what's +going on under the hood. + ```powershell # Builds a release exe -cargo tauri build +pnpm build -# The release exe, MSI, and NSIS installer should be up in the workspace. +# The release exe and MSI installer should be up in the workspace. # The exe can run without being installed -stat ../target/release/firezone-windows-client.exe -stat ../target/release/bundle/msi/firezone-windows-client_0.0.0_x64_en-US.msi -stat ../target/release/bundle/nsis/firezone-windows-client_0.0.0_x64-setup.exe +stat ../target/release/Firezone.exe +stat ../target/release/bundle/msi/Firezone_0.0.0_x64_en-US.msi ``` ## Running @@ -35,16 +40,15 @@ stat ../target/release/bundle/nsis/firezone-windows-client_0.0.0_x64-setup.exe From this dir: ```powershell -# Tauri has some hot-reloading features. If the Rust code changes it will even recompile -# and restart the program for you. -RUST_LOG=info,firezone_windows_client=debug cargo tauri dev +# This will start the frontend tools in watch mode and then run `tauri dev` +pnpm dev # You can call debug subcommands on the exe from this directory too # e.g. this is equivalent to `cargo run -- debug hostname` cargo tauri dev -- -- debug hostname # The exe is up in the workspace -stat ../target/debug/firezone-windows-client.exe +stat ../target/debug/Firezone.exe ``` The app's config and logs will be stored at @@ -56,6 +60,10 @@ Tauri says it should work on Windows 10, Version 1803 and up. Older versions may work if you [manually install WebView2](https://tauri.app/v1/guides/getting-started/prerequisites#2-webview2) +`x86_64` architecture is supported at this time. See +[this issue](https://github.com/firezone/firezone/issues/2992) for `aarch64` +support. + ## Threat model We can split this to its own doc or generalize it to the whole project if diff --git a/rust/windows-client/build-debug.bat b/rust/windows-client/build-debug.bat new file mode 100755 index 000000000..499eace95 --- /dev/null +++ b/rust/windows-client/build-debug.bat @@ -0,0 +1,14 @@ +@echo off +setlocal enabledelayedexpansion + +REM Copy frontend dependencies +copy "node_modules\flowbite\dist\flowbite.min.js" "src\" + +REM Compile TypeScript +call pnpm tsc + +REM Compile CSS +call pnpm tailwindcss -i src\input.css -o src\output.css + +REM Compile Rust and bundle +call tauri build --debug --bundles none diff --git a/rust/windows-client/build.bat b/rust/windows-client/build.bat new file mode 100755 index 000000000..94186c811 --- /dev/null +++ b/rust/windows-client/build.bat @@ -0,0 +1,14 @@ +@echo off +setlocal enabledelayedexpansion + +REM Copy frontend dependencies +copy "node_modules\flowbite\dist\flowbite.min.js" "src\" + +REM Compile TypeScript +call pnpm tsc + +REM Compile CSS +call pnpm tailwindcss -i src\input.css -o src\output.css + +REM Compile Rust and bundle +call tauri build diff --git a/rust/windows-client/build.sh b/rust/windows-client/build.sh new file mode 100755 index 000000000..2c81a1482 --- /dev/null +++ b/rust/windows-client/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# The Windows client obviously doesn't build for *nix, but this +# script is helpful for doing UI work on those platforms for the +# Windows client. +set -e + +# Copy frontend dependencies +cp node_modules/flowbite/dist/flowbite.min.js src/ + +# Compile TypeScript +tsc + +# Compile CSS +tailwindcss -i src/input.css -o src/output.css + +# Compile Rust and bundle +tauri build diff --git a/rust/windows-client/dev.bat b/rust/windows-client/dev.bat new file mode 100755 index 000000000..e7564a0ba --- /dev/null +++ b/rust/windows-client/dev.bat @@ -0,0 +1,14 @@ +@echo off +setlocal enabledelayedexpansion + +REM Copy frontend dependencies +copy "node_modules\flowbite\dist\flowbite.min.js" "src\" + +REM Compile TypeScript in watch mode +start tsc --watch + +REM Compile CSS in watch mode +start call npx tailwindcss -i src\input.css -o src\output.css --watch + +REM Start Tauri hot-reloading +tauri dev diff --git a/rust/windows-client/dev.sh b/rust/windows-client/dev.sh new file mode 100755 index 000000000..a8bb953ee --- /dev/null +++ b/rust/windows-client/dev.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# The Windows client obviously doesn't build for *nix, but this +# script is helpful for doing UI work on those platforms for the +# Windows client. +set -e + +# Fixes exiting with Ctrl-C +stop() { + kill $(jobs -p) +} +trap stop SIGINT SIGTERM + +# Copy frontend dependencies +cp node_modules/flowbite/dist/flowbite.min.js src/ + +# Compile TypeScript +tsc --watch & + +# Compile CSS +tailwindcss -i src/input.css -o src/output.css --watch & + +# Start Tauri hot-reloading: Not applicable for Windows +# tauri dev diff --git a/rust/windows-client/package.json b/rust/windows-client/package.json new file mode 100644 index 000000000..5bdf7e61e --- /dev/null +++ b/rust/windows-client/package.json @@ -0,0 +1,25 @@ +{ + "scripts": { + "build": "run-script-os", + "build:win32": "call build.bat", + "build:darwin:linux": "./build.sh", + "build-debug": "run-script-os", + "build-debug:win32": "call build-debug.bat", + "dev": "run-script-os", + "dev:win32": "call dev.bat", + "dev:darwin:linux": "./dev.sh", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^1.0", + "@tauri-apps/cli": "^1.0", + "flowbite": "^2.2.1" + }, + "devDependencies": { + "@types/node": "18", + "http-server": "^14.1.1", + "run-script-os": "^1.1.6", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" + } +} diff --git a/rust/windows-client/pnpm-lock.yaml b/rust/windows-client/pnpm-lock.yaml new file mode 100644 index 000000000..b877a3de1 --- /dev/null +++ b/rust/windows-client/pnpm-lock.yaml @@ -0,0 +1,1168 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@tauri-apps/api': + specifier: ^1.0 + version: 1.5.3 + '@tauri-apps/cli': + specifier: ^1.0 + version: 1.5.9 + flowbite: + specifier: ^2.2.1 + version: 2.2.1 + +devDependencies: + '@types/node': + specifier: '18' + version: 18.19.8 + http-server: + specifier: ^14.1.1 + version: 14.1.1 + run-script-os: + specifier: ^1.1.6 + version: 1.1.6 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.1 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.22 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.16.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + + /@tauri-apps/api@1.5.3: + resolution: {integrity: sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==} + engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} + dev: false + + /@tauri-apps/cli-darwin-arm64@1.5.9: + resolution: {integrity: sha512-7C2Jf8f0gzv778mLYb7Eszqqv1bm9Wzews81MRTqKrUIcC+eZEtDXLex+JaEkEzFEUrgIafdOvMBVEavF030IA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-darwin-x64@1.5.9: + resolution: {integrity: sha512-LHKytpkofPYgH8RShWvwDa3hD1ws131x7g7zNasJPfOiCWLqYVQFUuQVmjEUt8+dpHe/P/err5h4z+YZru2d0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-linux-arm-gnueabihf@1.5.9: + resolution: {integrity: sha512-teGK20IYKx+dVn8wFq/Lg57Q9ce7foq1KHSfyHi464LVt1T0V1rsmULSgZpQPPj/NYPF5BG78PcWYv64yH86jw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-linux-arm64-gnu@1.5.9: + resolution: {integrity: sha512-onJ/DW5Crw38qVx+wquY4uBbfCxVhzhdJmlCYqnYyXsZZmSiPUfSyhV58y+5TYB0q1hG8eYdB5x8VAwzByhGzw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-linux-arm64-musl@1.5.9: + resolution: {integrity: sha512-23AYoLD3acakLp9NtheKQDJl8F66eTOflxoPzdJNRy13hUSxb+W9qpz4rRA+CIzkjICFvO2i3UWjeV9QwDVpsQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-linux-x64-gnu@1.5.9: + resolution: {integrity: sha512-9PQA1rE7gh41W2ylyKd5qOGOds55ymaYPml9KOpM0g+cxmCXa+8Wf9K5NKvACnJldJJ6cekWzIyB4eN6o5T+yQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-linux-x64-musl@1.5.9: + resolution: {integrity: sha512-5hdbNFeDsrJ/pXZ4cSQV4bJwUXPPxXxN3/pAtNUqIph7q+vLcBXOXIMoS64iuyaluJC59lhEwlWZFz+EPv0Hqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-win32-arm64-msvc@1.5.9: + resolution: {integrity: sha512-O18JufjSB3hSJYu5WWByONouGeX7DraLAtXLErsG1r/VS3zHd/zyuzycrVUaObNXk5bfGlIP0Ypt+RvZJILN2w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-win32-ia32-msvc@1.5.9: + resolution: {integrity: sha512-FQxtxTZu0JVBihfd/lmpxo7jyMOesjWQehfyVUqtgMfm5+Pvvw0Y+ZioeDi1TZkFVrT3QDYy8R4LqDLSZVMQRA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli-win32-x64-msvc@1.5.9: + resolution: {integrity: sha512-EeI1+L518cIBLKw0qUFwnLIySBeSmPQjPLIlNwSukHSro4tAQPHycEVGgKrdToiCWgaZJBA0e5aRSds0Du2TWg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tauri-apps/cli@1.5.9: + resolution: {integrity: sha512-knSt/9AvCTeyfC6wkyeouF9hBW/0Mzuw+5vBKEvzaGPQsfFJo1ZCp5FkdiZpGBBfnm09BhugasGRTGofzatfqQ==} + engines: {node: '>= 10'} + hasBin: true + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 1.5.9 + '@tauri-apps/cli-darwin-x64': 1.5.9 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.5.9 + '@tauri-apps/cli-linux-arm64-gnu': 1.5.9 + '@tauri-apps/cli-linux-arm64-musl': 1.5.9 + '@tauri-apps/cli-linux-x64-gnu': 1.5.9 + '@tauri-apps/cli-linux-x64-musl': 1.5.9 + '@tauri-apps/cli-win32-arm64-msvc': 1.5.9 + '@tauri-apps/cli-win32-ia32-msvc': 1.5.9 + '@tauri-apps/cli-win32-x64-msvc': 1.5.9 + dev: false + + /@types/node@18.19.8: + resolution: {integrity: sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==} + dependencies: + undici-types: 5.26.5 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + dependencies: + lodash: 4.17.21 + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.2.0 + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /corser@2.0.1: + resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} + engines: {node: '>= 0.4.0'} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.16.0: + resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /flowbite@2.2.1: + resolution: {integrity: sha512-iiZyBTtriEDRHrqXZgpKHaxl4B2J8HZUP8Yn1RXozUDKszWHDVj4GxQqMMB9AJHRWOgXV/4E/LJZ/zqQgBUhWA==} + dependencies: + '@popperjs/core': 2.11.8 + mini-svg-data-uri: 1.4.4 + dev: false + + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.5 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + dev: true + + /http-server@14.1.1: + resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} + engines: {node: '>=12'} + hasBin: true + dependencies: + basic-auth: 2.0.1 + chalk: 4.1.2 + corser: 2.0.1 + he: 1.2.0 + html-encoding-sniffer: 3.0.0 + http-proxy: 1.18.1 + mime: 1.6.0 + minimist: 1.2.8 + opener: 1.5.2 + portfinder: 1.0.32 + secure-compare: 3.0.1 + union: 0.5.0 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + - supports-color + dev: true + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + engines: {node: 14 || >=16.14} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + dev: false + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.1.0 + minipass: 7.0.4 + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /portfinder@1.0.32: + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} + dependencies: + async: 2.6.4 + debug: 3.2.7 + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + + /postcss-import@15.1.0(postcss@8.4.33): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.33): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.33 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.33): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.0.0 + postcss: 8.4.33 + yaml: 2.3.4 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.33): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.33 + postcss-selector-parser: 6.0.15 + dev: true + + /postcss-selector-parser@6.0.15: + resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.33: + resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /run-script-os@1.1.6: + resolution: {integrity: sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==} + hasBin: true + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /secure-compare@3.0.1: + resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} + dev: true + + /set-function-length@1.2.0: + resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 10.3.10 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tailwindcss@3.4.1: + resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.33 + postcss-import: 15.1.0(postcss@8.4.33) + postcss-js: 4.0.1(postcss@8.4.33) + postcss-load-config: 4.0.2(postcss@8.4.33) + postcss-nested: 6.0.1(postcss@8.4.33) + postcss-selector-parser: 6.0.15 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /union@0.5.0: + resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} + engines: {node: '>= 0.8.0'} + dependencies: + qs: 6.11.2 + dev: true + + /url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true diff --git a/rust/windows-client/src-tauri/Cargo.toml b/rust/windows-client/src-tauri/Cargo.toml index b7a68149a..bd12c43f7 100644 --- a/rust/windows-client/src-tauri/Cargo.toml +++ b/rust/windows-client/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ name = "firezone-windows-client" # mark:automatic-version version = "1.0.0" -description = "Firezone Windows Client" +description = "Firezone" edition = "2021" [build-dependencies] diff --git a/rust/windows-client/src-tauri/src/client/gui.rs b/rust/windows-client/src-tauri/src/client/gui.rs index b44be5979..2109b90b9 100644 --- a/rust/windows-client/src-tauri/src/client/gui.rs +++ b/rust/windows-client/src-tauri/src/client/gui.rs @@ -138,9 +138,10 @@ pub(crate) fn run(params: client::GuiParams) -> Result<()> { }) .invoke_handler(tauri::generate_handler![ logging::clear_logs, + logging::count_logs, logging::export_logs, - logging::start_stop_log_counting, settings::apply_advanced_settings, + settings::reset_advanced_settings, settings::get_advanced_settings, ]) .system_tray(tray) @@ -273,7 +274,6 @@ pub(crate) enum ControllerRequest { Quit, SchemeRequest(url::Url), SignIn, - StartStopLogCounting(bool), SignOut, TunnelReady, } @@ -350,7 +350,6 @@ struct Controller { /// The UUIDv4 device ID persisted to disk /// Sent verbatim to Session::connect device_id: String, - log_counting_task: Option>>, logging_handles: client::logging::Handles, /// Tells us when to wake up and look for a new resource list. Tokio docs say that memory reads and writes are synchronized when notifying, so we don't need an extra mutex on the resources. notify_controller: Arc, @@ -380,7 +379,6 @@ impl Controller { ctlr_tx, session: None, device_id, - log_counting_task: None, logging_handles, notify_controller, tunnel_ready: false, @@ -584,19 +582,6 @@ async fn run_controller( )?; } } - Req::StartStopLogCounting(enable) => { - if enable { - if controller.log_counting_task.is_none() { - let app = app.clone(); - controller.log_counting_task = Some(tokio::spawn(logging::count_logs(app))); - tracing::debug!("started log counting"); - } - } else if let Some(t) = controller.log_counting_task { - t.abort(); - controller.log_counting_task = None; - tracing::debug!("cancelled log counting"); - } - } Req::TunnelReady => { controller.tunnel_ready = true; controller.refresh_system_tray_menu()?; diff --git a/rust/windows-client/src-tauri/src/client/logging.rs b/rust/windows-client/src-tauri/src/client/logging.rs index 5a7033b9c..c627a12c1 100644 --- a/rust/windows-client/src-tauri/src/client/logging.rs +++ b/rust/windows-client/src-tauri/src/client/logging.rs @@ -10,7 +10,6 @@ use std::{ result::Result as StdResult, str::FromStr, }; -use tauri::Manager; use tokio::task::spawn_blocking; use tracing::subscriber::set_global_default; use tracing_log::LogTracer; @@ -42,27 +41,8 @@ pub(crate) fn setup(log_filter: &str) -> Result { } #[tauri::command] -pub(crate) async fn start_stop_log_counting( - managed: tauri::State<'_, Managed>, - enable: bool, -) -> StdResult<(), String> { - // Delegate this to Controller so that it can decide whether to obey. - // e.g. if there's already a count task running, it may refuse to start another. - managed - .ctlr_tx - .send(ControllerRequest::StartStopLogCounting(enable)) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub(crate) async fn clear_logs( - app: tauri::AppHandle, - managed: tauri::State<'_, Managed>, -) -> StdResult<(), String> { - clear_logs_inner(app, &managed) - .await - .map_err(|e| e.to_string()) +pub(crate) async fn clear_logs(managed: tauri::State<'_, Managed>) -> StdResult<(), String> { + clear_logs_inner(&managed).await.map_err(|e| e.to_string()) } #[tauri::command] @@ -72,18 +52,27 @@ pub(crate) async fn export_logs(managed: tauri::State<'_, Managed>) -> StdResult .map_err(|e| e.to_string()) } +#[derive(Clone, Default, Serialize)] +pub(crate) struct FileCount { + bytes: u64, + files: u64, +} + +#[tauri::command] +pub(crate) async fn count_logs() -> StdResult { + count_logs_inner().await.map_err(|e| e.to_string()) +} + /// Delete all files in the logs directory. /// /// This includes the current log file, so we won't write any more logs to disk /// until the file rolls over or the app restarts. -pub(crate) async fn clear_logs_inner(app: tauri::AppHandle, managed: &Managed) -> Result<()> { +pub(crate) async fn clear_logs_inner(managed: &Managed) -> Result<()> { let mut dir = tokio::fs::read_dir("logs").await?; while let Some(entry) = dir.next_entry().await? { tokio::fs::remove_file(entry.path()).await?; } - count_logs(app).await?; - managed.fault_msleep(5000).await; Ok(()) } @@ -146,23 +135,16 @@ pub(crate) async fn export_logs_to(path: PathBuf, stem: PathBuf) -> Result<()> { Ok(()) } -#[derive(Clone, Default, Serialize)] -struct FileCount { - bytes: u64, - files: u64, -} - -pub(crate) async fn count_logs(app: tauri::AppHandle) -> Result<()> { +/// Count log files and their sizes +pub(crate) async fn count_logs_inner() -> Result { let mut dir = tokio::fs::read_dir("logs").await?; let mut file_count = FileCount::default(); - // Zero out the GUI - app.emit_all("file_count_progress", None::)?; + while let Some(entry) = dir.next_entry().await? { let md = entry.metadata().await?; file_count.files += 1; file_count.bytes += md.len(); } - // Show the result on the GUI - app.emit_all("file_count_progress", Some(&file_count))?; - Ok(()) + + Ok(file_count) } diff --git a/rust/windows-client/src-tauri/src/client/settings.rs b/rust/windows-client/src-tauri/src/client/settings.rs index 1d241c2c9..a9ba4c9d4 100644 --- a/rust/windows-client/src-tauri/src/client/settings.rs +++ b/rust/windows-client/src-tauri/src/client/settings.rs @@ -53,11 +53,24 @@ pub(crate) async fn apply_advanced_settings( managed: tauri::State<'_, Managed>, settings: AdvancedSettings, ) -> StdResult<(), String> { - apply_advanced_settings_inner(managed.inner(), settings) + apply_advanced_settings_inner(managed.inner(), &settings) .await .map_err(|e| e.to_string()) } +#[tauri::command] +pub(crate) async fn reset_advanced_settings( + managed: tauri::State<'_, Managed>, +) -> StdResult { + let settings = AdvancedSettings::default(); + + apply_advanced_settings_inner(managed.inner(), &settings) + .await + .map_err(|e| e.to_string())?; + + Ok(settings) +} + #[tauri::command] pub(crate) async fn get_advanced_settings( managed: tauri::State<'_, Managed>, @@ -75,7 +88,7 @@ pub(crate) async fn get_advanced_settings( pub(crate) async fn apply_advanced_settings_inner( managed: &Managed, - settings: AdvancedSettings, + settings: &AdvancedSettings, ) -> Result<()> { let DirAndPath { dir, path } = advanced_settings_path()?; tokio::fs::create_dir_all(&dir).await?; diff --git a/rust/windows-client/src-tauri/tauri.conf.json b/rust/windows-client/src-tauri/tauri.conf.json index 4985a54ab..db7c68321 100644 --- a/rust/windows-client/src-tauri/tauri.conf.json +++ b/rust/windows-client/src-tauri/tauri.conf.json @@ -1,5 +1,5 @@ { - "build": { + "build": { "beforeDevCommand": "", "beforeBuildCommand": "", "devPath": "../src", @@ -7,7 +7,7 @@ "withGlobalTauri": true }, "package": { - "productName": "Firezone Windows Client" + "productName": "Firezone" }, "tauri": { "allowlist": { @@ -25,7 +25,7 @@ "icons/firezone.ico" ], "publisher": "Firezone", - "shortDescription": "Firezone Windows client" + "shortDescription": "Firezone" }, "security": { "csp": null diff --git a/rust/windows-client/src/about.html b/rust/windows-client/src/about.html index 4c1920016..1610e7ac6 100644 --- a/rust/windows-client/src/about.html +++ b/rust/windows-client/src/about.html @@ -2,10 +2,10 @@ - + About Firezone - + + - -
-
- - + +
+
+
    + + +
- -

- -

-
- - +
+ -
-

+
- -
-
-
- - -
- -
- - -
- -
- - -
- -

WARNING: These settings are intended for internal debug purposes only. Changing these is not supported and will disrupt access to your resources.

- -
- - -
-
-
+ diff --git a/rust/windows-client/src/settings.js b/rust/windows-client/src/settings.js deleted file mode 100644 index 2e87b5afb..000000000 --- a/rust/windows-client/src/settings.js +++ /dev/null @@ -1,173 +0,0 @@ -let auth_base_url_input; -let api_url_input; -let log_count_output; -let log_filter_input; -let reset_advanced_settings_btn; -let apply_advanced_settings_btn; -let export_logs_btn; -let clear_logs_btn; - -const querySel = function(id) { - return document.querySelector(id); -}; - -const { invoke } = window.__TAURI__.tauri; -const { listen } = window.__TAURI__.event; - -// Lock the UI when we're saving to disk, since disk writes are technically async. -// Parameters: -// - locked - Boolean, true to lock the UI, false to unlock it. -function lock_advanced_settings_form(locked) { - auth_base_url_input.disabled = locked; - api_url_input.disabled = locked; - log_filter_input.disabled = locked; - - if (locked) { - reset_advanced_settings_btn.textContent = "..."; - apply_advanced_settings_btn.textContent = "..."; - } - else { - reset_advanced_settings_btn.textContent = "Reset to Defaults"; - apply_advanced_settings_btn.textContent = "Apply"; - } - - reset_advanced_settings_btn.disabled = locked; - apply_advanced_settings_btn.disabled = locked; -} - -function lock_logs_form(locked) { - export_logs_btn.disabled = locked; - clear_logs_btn.disabled = locked; -} - -async function apply_advanced_settings() { - lock_advanced_settings_form(true); - - // Invoke Rust - // TODO: Why doesn't JS' await syntax work here? - invoke("apply_advanced_settings", { - "settings": { - "auth_base_url": auth_base_url_input.value, - "api_url": api_url_input.value, - "log_filter": log_filter_input.value - } - }) - .then(() => { - lock_advanced_settings_form(false); - }) - .catch((e) => { - console.error(e); - lock_advanced_settings_form(false); - }); -} - -async function get_advanced_settings() { - lock_advanced_settings_form(true); - - invoke("get_advanced_settings") - .then((settings) => { - auth_base_url_input.value = settings["auth_base_url"]; - api_url_input.value = settings["api_url"]; - log_filter_input.value = settings["log_filter"]; - lock_advanced_settings_form(false); - }) - .catch((e) => { - console.error(e); - lock_advanced_settings_form(false); - }); -} - -async function export_logs() { - lock_logs_form(true); - - invoke("export_logs") - .then(() => { - lock_logs_form(false); - }) - .catch((e) => { - console.error(e); - lock_logs_form(false); - }); -} - -async function clear_logs() { - lock_logs_form(true); - - invoke("clear_logs") - .then(() => { - lock_logs_form(false); - }) - .catch((e) => { - console.error(e); - lock_logs_form(false); - }); -} - -function openTab(evt, tabName) { - let tabcontent = document.getElementsByClassName("tabcontent"); - for (let i = 0; i < tabcontent.length; i++) { - tabcontent[i].style.display = "none"; - } - - let tablinks = document.getElementsByClassName("tablinks"); - for (let i = 0; i < tablinks.length; i++) { - // TODO: There's a better way to change classes on an element - tablinks[i].className = tablinks[i].className.replace(" active", ""); - } - - document.getElementById(tabName).style.display = "block"; - // TODO: There's a better way to do this - evt.currentTarget.className += " active"; - - invoke("start_stop_log_counting", {"enable": tabName == "tab_logs"}) - .then(() => { - // Good - }) - .catch((e) => { - console.error(e); - }); -} - -async function setup() { - // Advanced tab - auth_base_url_input = querySel("#auth-base-url-input"); - api_url_input = querySel("#api-url-input"); - log_count_output = querySel("#log-count-output"); - log_filter_input = querySel("#log-filter-input"); - reset_advanced_settings_btn = querySel("#reset-advanced-settings-btn"); - apply_advanced_settings_btn = querySel("#apply-advanced-settings-btn"); - - querySel("#advanced-settings-form").addEventListener("submit", (e) => { - e.preventDefault(); - apply_advanced_settings(); - }); - - // Logs tab - export_logs_btn = querySel("#export-logs-btn"); - clear_logs_btn = querySel("#clear-logs-btn"); - - export_logs_btn.addEventListener("click", (e) => { - export_logs(); - }); - clear_logs_btn.addEventListener("click", (e) => { - clear_logs(); - }); - - await listen("file_count_progress", (event) => { - const pl = event.payload; - - let s = "Calculating..."; - if (!! pl) { - const megabytes = Math.round(pl.bytes / 100000) / 10; - s = `${pl.files} files, ${megabytes} MB`; - } - log_count_output.innerText = s; - }); - - // TODO: Why doesn't this open the Advanced tab by default? - querySel("#tab_advanced").click(); - - get_advanced_settings().await; -} - -window.addEventListener("DOMContentLoaded",setup); diff --git a/rust/windows-client/src/settings.ts b/rust/windows-client/src/settings.ts new file mode 100644 index 000000000..8453d5db7 --- /dev/null +++ b/rust/windows-client/src/settings.ts @@ -0,0 +1,225 @@ +// Purpose: TypeScript file for the settings page. + +// Custom types +interface Settings { + auth_base_url: string; + api_url: string; + log_filter: string; +} + +interface TauriEvent { + type: string; + payload: any; +} + +interface FileCount { + files: number; + bytes: number; +} + +// Stub Tauri API for TypeScript. Helpful when developing without Tauri running. +export {}; +declare global { + interface Window { + __TAURI__: { + tauri: { + invoke: (cmd: string, args?: any) => Promise; + }; + event: { + listen: (cmd: string, callback: (event: TauriEvent) => void) => void; + }; + }; + } +} +window.__TAURI__ = window.__TAURI__ || { + tauri: { + invoke: (_cmd: string, _args?: any) => { + return Promise.reject("Tauri API not initialized"); + }, + }, + event: { + listen: (_cmd: string, _callback: (event: TauriEvent) => void) => { + console.error("Tauri API not initialized"); + }, + }, +}; + +const { invoke } = window.__TAURI__.tauri; +const { listen } = window.__TAURI__.event; + +// DOM elements +const form = document.getElementById("advanced-settings-form"); +const authBaseUrlInput = ( + document.getElementById("auth-base-url-input") +); +const apiUrlInput = document.getElementById("api-url-input"); +const logFilterInput = ( + document.getElementById("log-filter-input") +); +const logCountOutput = ( + document.getElementById("log-count-output") +); +const resetAdvancedSettingsBtn = ( + document.getElementById("reset-advanced-settings-btn") +); +const applyAdvancedSettingsBtn = ( + document.getElementById("apply-advanced-settings-btn") +); +const exportLogsBtn = ( + document.getElementById("export-logs-btn") +); +const clearLogsBtn = ( + document.getElementById("clear-logs-btn") +); +const logsTabBtn = document.getElementById("logs-tab"); + +// Rust bridge functions + +// Lock the UI when we're saving to disk, since disk writes are technically async. +function lockAdvancedSettingsForm() { + authBaseUrlInput.disabled = true; + apiUrlInput.disabled = true; + logFilterInput.disabled = true; + resetAdvancedSettingsBtn.disabled = true; + applyAdvancedSettingsBtn.disabled = true; + + resetAdvancedSettingsBtn.textContent = "Updating..."; + applyAdvancedSettingsBtn.textContent = "Updating..."; +} + +function unlockAdvancedSettingsForm() { + authBaseUrlInput.disabled = false; + apiUrlInput.disabled = false; + logFilterInput.disabled = false; + resetAdvancedSettingsBtn.disabled = false; + applyAdvancedSettingsBtn.disabled = false; + + resetAdvancedSettingsBtn.textContent = "Reset to Defaults"; + applyAdvancedSettingsBtn.textContent = "Apply"; +} + +function lockLogsForm() { + exportLogsBtn.disabled = true; + clearLogsBtn.disabled = true; +} + +function unlockLogsForm() { + exportLogsBtn.disabled = false; + clearLogsBtn.disabled = false; +} + +async function applyAdvancedSettings() { + console.log("Applying advanced settings"); + lockAdvancedSettingsForm(); + + invoke("apply_advanced_settings", { + settings: { + auth_base_url: authBaseUrlInput.value, + api_url: apiUrlInput.value, + log_filter: logFilterInput.value, + }, + }) + .catch((e: Error) => { + console.error(e); + }) + .finally(() => { + unlockAdvancedSettingsForm(); + }); +} + +async function resetAdvancedSettings() { + console.log("Resetting advanced settings"); + lockAdvancedSettingsForm(); + + invoke("reset_advanced_settings") + .then((settings: Settings) => { + authBaseUrlInput.value = settings.auth_base_url; + apiUrlInput.value = settings.api_url; + logFilterInput.value = settings.log_filter; + }) + .catch((e: Error) => { + console.error(e); + }) + .finally(() => { + unlockAdvancedSettingsForm(); + }); +} + +async function getAdvancedSettings() { + console.log("Getting advanced settings"); + lockAdvancedSettingsForm(); + + invoke("get_advanced_settings") + .then((settings: Settings) => { + authBaseUrlInput.value = settings.auth_base_url; + apiUrlInput.value = settings.api_url; + logFilterInput.value = settings.log_filter; + }) + .catch((e: Error) => { + console.error(e); + }) + .finally(() => { + unlockAdvancedSettingsForm(); + }); +} + +async function exportLogs() { + console.log("Exporting logs"); + lockLogsForm(); + + invoke("export_logs") + .catch((e: Error) => { + console.error(e); + }) + .finally(() => { + unlockLogsForm(); + }); +} + +async function clearLogs() { + console.log("Clearing logs"); + lockLogsForm(); + + invoke("clear_logs") + .catch((e: Error) => { + console.error(e); + }) + .finally(() => { + logCountOutput.innerText = "0 files, 0 MB"; + unlockLogsForm(); + }); +} + +async function countLogs() { + invoke("count_logs") + .then((fileCount) => { + console.log(fileCount); + const megabytes = Math.round(fileCount.bytes / 100000) / 10; + logCountOutput.innerText = `${fileCount.files} files, ${megabytes} MB`; + }) + .catch((e: Error) => { + console.error(e); + logCountOutput.innerText = `Error counting logs: ${e.message}`; + }); +} + +// Setup event listeners +form.addEventListener("submit", (e) => { + e.preventDefault(); + applyAdvancedSettings(); +}); +resetAdvancedSettingsBtn.addEventListener("click", (e) => { + resetAdvancedSettings(); +}); +exportLogsBtn.addEventListener("click", (e) => { + exportLogs(); +}); +clearLogsBtn.addEventListener("click", (e) => { + clearLogs(); +}); +logsTabBtn.addEventListener("click", (e) => { + countLogs(); +}); + +// Load settings +getAdvancedSettings(); diff --git a/rust/windows-client/src/styles.css b/rust/windows-client/src/styles.css deleted file mode 100644 index 9599b334a..000000000 --- a/rust/windows-client/src/styles.css +++ /dev/null @@ -1,114 +0,0 @@ -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - - color: #0f0f0f; - background-color: #f6f6f6; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -.container { - margin: 0; - padding-top: 10vh; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: 0.75s; -} - -.logo.tauri:hover { - filter: drop-shadow(0 0 2em #24c8db); -} - -.row { - display: flex; - justify-content: center; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -h1 { - text-align: center; -} - -input, -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - color: #0f0f0f; - background-color: #ffffff; - transition: border-color 0.25s; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -} - -button { - cursor: pointer; -} - -button:hover { - border-color: #396cd8; -} -button:active { - border-color: #396cd8; - background-color: #e8e8e8; -} - -input, -button { - outline: none; -} - -input:disabled, -button:disabled { - background: #888; -} - -#greet-input { - margin-right: 5px; -} - -@media (prefers-color-scheme: dark) { - :root { - color: #f6f6f6; - background-color: #2f2f2f; - } - - a:hover { - color: #24c8db; - } - - input, - button { - color: #ffffff; - background-color: #0f0f0f98; - } - button:active { - background-color: #0f0f0f69; - } -} diff --git a/rust/windows-client/tailwind.config.js b/rust/windows-client/tailwind.config.js new file mode 100644 index 000000000..01ee94466 --- /dev/null +++ b/rust/windows-client/tailwind.config.js @@ -0,0 +1,61 @@ +const firezoneColors = { + // See our brand palette in Figma. + // These have been reversed to match Tailwind's default order. + + // primary: orange + "heat-wave": { + 50: "#fff9f5", + 100: "#fff1e5", + 200: "#ffddc2", + 300: "#ffbc85", + 400: "#ff9a47", + 450: "#ff7300", + 500: "#ff7605", + 600: "#c25700", + 700: "#7f3900", + 800: "#5c2900", + 900: "#331700", + }, + // accent: violet + "electric-violet": { + 50: "#f8f5ff", + 100: "#ece5ff", + 200: "#d2c2ff", + 300: "#a585ff", + 400: "#7847ff", + 450: "#5e00d6", + 500: "#4805ff", + 600: "#3400c2", + 700: "#37007f", + 800: "#28005c", + 900: "#160033", + }, + // neutral: night-rider + "night-rider": { + 50: "#fcfcfc", + 100: "#f8f7f7", + 200: "#ebebea", + 300: "#dfdedd", + 400: "#c7c4c2", + 500: "#a7a3a0", + 600: "#90867f", + 700: "#766a60", + 800: "#4c3e33", + 900: "#1b140e", + }, +}; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{html,ts,js}"], + theme: { + extend: { + colors: { + primary: firezoneColors["heat-wave"], + accent: firezoneColors["electric-violet"], + neutral: firezoneColors["night-rider"], + }, + }, + }, + plugins: [require("flowbite/plugin")], +}; diff --git a/rust/windows-client/tsconfig.json b/rust/windows-client/tsconfig.json new file mode 100644 index 000000000..9bb1afefc --- /dev/null +++ b/rust/windows-client/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "moduleResolution": "node" + }, + "include": ["./src/**/*"], + "exclude": ["node_modules"] +}