diff --git a/.github/workflows/build-macos.yaml b/.github/workflows/build-macos.yaml index 1ef16da5..709bb89d 100644 --- a/.github/workflows/build-macos.yaml +++ b/.github/workflows/build-macos.yaml @@ -11,6 +11,9 @@ on: tags: - v*.*.* +env: + SQLX_OFFLINE: "1" + jobs: build-macos: runs-on: @@ -38,18 +41,23 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 25 + node-version: 26 - uses: pnpm/action-setup@v6 with: cache: true - version: 10 + version: 11 # Change to '--frozen-lockfile' once this gets fixed: # https://github.com/pnpm/action-setup/issues/40 - name: Install Node dependencies run: pnpm install --no-frozen-lockfile + - name: Install Node dependencies for New UI + run: | + cd new-ui + pnpm install --no-frozen-lockfile + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: @@ -63,6 +71,11 @@ jobs: - name: Unlock keychain run: security -v unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" login.keychain + - name: Build new UI + run: | + cd new-ui + pnpm build + - name: Build app uses: tauri-apps/tauri-action@v0 env: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b1b883b5..f7bb499b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -30,11 +30,12 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 25 + node-version: 26 - uses: pnpm/action-setup@v6 with: - version: 10 + cache: true + version: 11 run_install: false - name: Get pnpm store directory @@ -42,22 +43,28 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - name: Setup pnpm cache - uses: actions/cache@v5 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-lint-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-lint-store- - # Change to '--frozen-lockfile' once this gets fixed: # https://github.com/pnpm/action-setup/issues/40 - name: Install Node dependencies run: pnpm install --no-frozen-lockfile + - name: Install Node dependencies for new UI + run: | + cd new-ui + pnpm install --no-frozen-lockfile + - name: Run Biome and Prettier Lint run: pnpm lint - # TODO: Restore when it works again: https://github.com/pnpm/pnpm/issues/11265 - # - name: Audit - # run: pnpm audit --prod + - name: Audit + run: pnpm audit --prod + + - name: Run Biome and Prettier Lint for new UI + run: | + cd new-ui + pnpm lint + + - name: Audit new UI + run: | + cd new-ui + pnpm audit --prod diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f7519b1f..c1ee0173 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -62,11 +62,12 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v6 with: - version: 10 + cache: true + version: 11 - uses: actions/setup-node@v6 with: - node-version: 25 + node-version: 26 - name: Get pnpm store directory run: | @@ -78,19 +79,17 @@ jobs: echo Version: $VERSION echo "VERSION=$VERSION" >> ${GITHUB_ENV} echo "DEFGUARD_CLIENT_BUILD_VERSION=${GITHUB_REF_NAME#v}" >> ${GITHUB_ENV} - - uses: actions/cache@v5 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-build-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-build-store- # Change to '--frozen-lockfile' once this gets fixed: # https://github.com/pnpm/action-setup/issues/40 - name: Install Node dependencies run: pnpm install --no-frozen-lockfile + - name: Install Node dependencies for new UI + run: | + cd new-ui + pnpm install --no-frozen-lockfile + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable @@ -98,6 +97,11 @@ jobs: run: | apt-get install -y build-essential libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev libxdo-dev unzip protobuf-compiler libprotobuf-dev rpm + - name: Build new UI + run: | + cd new-ui + pnpm build + - name: Build packages uses: tauri-apps/tauri-action@v0.5.23 env: @@ -166,11 +170,12 @@ jobs: echo "DEFGUARD_CLIENT_BUILD_VERSION=${GITHUB_REF_NAME#v}" >> ${GITHUB_ENV} - uses: actions/setup-node@v6 with: - node-version: 25 + node-version: 26 - uses: pnpm/action-setup@v6 with: - version: 10 + cache: true + version: 11 run_install: false - name: Get pnpm store directory @@ -178,19 +183,16 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> ${GITHUB_ENV} - - name: Setup pnpm cache - uses: actions/cache@v5 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-build-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-build-store- - # Change to '--frozen-lockfile' once this gets fixed: # https://github.com/pnpm/action-setup/issues/40 - name: Install Node dependencies run: pnpm install --no-frozen-lockfile + - name: Install Node dependencies for new UI + run: | + cd new-ui + pnpm install --no-frozen-lockfile + - name: Install Rust stable uses: dtolnay/rust-toolchain@stable @@ -199,6 +201,11 @@ jobs: sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev libxdo-dev unzip protobuf-compiler libprotobuf-dev rpm + - name: Build new UI + run: | + cd new-ui + pnpm build + - name: Build packages uses: tauri-apps/tauri-action@v0.5.23 # .24 seems broken, TODO: update when fixed env: @@ -343,21 +350,14 @@ jobs: # echo "VERSION=$env:VERSION" >> $env:GITHUB_ENV # - uses: actions/setup-node@v6 # with: - # node-version: 25 + # node-version: 26 # - uses: pnpm/action-setup@v6 # with: - # version: 10 + # version: 11 # run_install: false # - name: Get pnpm store directory # shell: bash # run: echo "STORE_PATH=$(pnpm store path --silent)" >> ${GITHUB_ENV} - # - uses: actions/cache@v5 - # name: Setup pnpm cache - # with: - # path: ${{ env.STORE_PATH }} - # key: ${{ runner.os }}-pnpm-build-store-${{ hashFiles('**/pnpm-lock.yaml') }} - # restore-keys: | - # ${{ runner.os }}-pnpm-build-store- # - name: Install deps # run: pnpm install --frozen-lockfile # - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38dbb284..ef8abe27 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ on: env: CARGO_TERM_COLOR: always + SQLX_OFFLINE: "1" # sccache SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 67f4f502..3c5ed200 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -1,4 +1,4 @@ vulnerabilities: - id: GHSA-wrw7-89jp-8q8g - expired_at: 2026-05-16 + expired_at: 2026-06-16 statement: 'glib is a transitive dependency of Tauri which we cannot update ourselves. Waiting for tauri to finish migration to gtk4-rs: https://github.com/tauri-apps/tauri/issues/12563' diff --git a/.typesafe-i18n.json b/.typesafe-i18n.json index b3224e1e..e5c75769 100644 --- a/.typesafe-i18n.json +++ b/.typesafe-i18n.json @@ -1,4 +1,4 @@ { "adapter": "react", - "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json" + "$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json" } \ No newline at end of file diff --git a/biome.json b/biome.json index 4bc6b7c6..3a317734 100644 --- a/biome.json +++ b/biome.json @@ -9,6 +9,7 @@ "ignoreUnknown": false, "includes": [ "src/**", + "!new-ui", "!src/i18n/*.ts", "!src/i18n/*.tsx", "!src/i18n/i18n-util", diff --git a/justfile b/justfile new file mode 100644 index 00000000..9e53b3e3 --- /dev/null +++ b/justfile @@ -0,0 +1,13 @@ +set shell := ["powershell.exe", "-c"] + +dev: + npx concurrently \ + -n "NEW,OLD,TAURI" \ + "cd new-ui && pnpm dev" \ + "pnpm dev" \ + "cargo tauri dev" + +build: + cd new-ui; pnpm build + pnpm build + cargo tauri build --config .\src-tauri\tauri.local.conf.json diff --git a/new-ui/.gitignore b/new-ui/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/new-ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/new-ui/.nvmrc b/new-ui/.nvmrc new file mode 100644 index 00000000..a682cfb9 --- /dev/null +++ b/new-ui/.nvmrc @@ -0,0 +1 @@ +v25 diff --git a/new-ui/.prettierignore b/new-ui/.prettierignore new file mode 100644 index 00000000..402ea008 --- /dev/null +++ b/new-ui/.prettierignore @@ -0,0 +1,3 @@ +/src/**/*.tsx +/src/**/*.ts +/src/**/*.js diff --git a/new-ui/.prettierrc b/new-ui/.prettierrc new file mode 100644 index 00000000..71a0f329 --- /dev/null +++ b/new-ui/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "tabWidth": 2, + "singleQuote": true, + "useTabs": false, + "printWidth": 90, + "endOfLine": "lf" +} diff --git a/new-ui/.stylelintrc.json b/new-ui/.stylelintrc.json new file mode 100644 index 00000000..6a4e2133 --- /dev/null +++ b/new-ui/.stylelintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["stylelint-config-standard-scss"], + "plugins": ["stylelint-scss"], + "rules": { + "at-rule-no-unknown": null, + "scss/at-rule-no-unknown": true, + "custom-property-empty-line-before": null, + "value-keyword-case": null + } +} diff --git a/new-ui/README.md b/new-ui/README.md new file mode 100644 index 00000000..7dbf7ebf --- /dev/null +++ b/new-ui/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/new-ui/biome.json b/new-ui/biome.json new file mode 100644 index 00000000..06f797be --- /dev/null +++ b/new-ui/biome.json @@ -0,0 +1,77 @@ +{ + "root": true, + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "src/**", + "!src/messages", + "!src/paraglide/**/*.js", + "!src/routeTree.gen.ts", + "!src/**/*.scss" + ] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "attributePosition": "auto", + "bracketSameLine": false, + "bracketSpacing": true, + "expand": "auto", + "lineEnding": "lf", + "lineWidth": 90, + "indentStyle": "space", + "useEditorconfig": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": "off", + "correctness": { + "useUniqueElementIds": "off" + }, + "style": { + "useLiteralEnumMembers": "off", + "useBlockStatements": "off" + }, + "suspicious": { + "noArrayIndexKey": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "attributePosition": "auto", + "bracketSameLine": false, + "bracketSpacing": true + } + }, + "css": { + "linter": { + "enabled": false + }, + "formatter": { + "enabled": false + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/new-ui/index.html b/new-ui/index.html new file mode 100644 index 00000000..07f928d0 --- /dev/null +++ b/new-ui/index.html @@ -0,0 +1,16 @@ + + + + + + + + webnext + + + +
+ + + + diff --git a/new-ui/package.json b/new-ui/package.json new file mode 100644 index 00000000..53a75495 --- /dev/null +++ b/new-ui/package.json @@ -0,0 +1,54 @@ +{ + "name": "new-ui", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "biome": "biome", + "lint": "biome check ./src/ && prettier src/**/*.scss --check --log-level error && stylelint \"src/**/*.scss\" -c ./.stylelintrc.json --fix && tsc -b", + "fix": "biome check ./src/ --write --unsafe && prettier src/**/*.scss -w --log-level silent", + "tsc": "tsc", + "preview": "vite preview" + }, + "dependencies": { + "@biomejs/biome": "^2.4.15", + "@floating-ui/react": "^0.27.19", + "@tanstack/react-form": "^1.32.0", + "@tanstack/react-query": "^5.100.10", + "@tanstack/react-router": "^1.169.2", + "@tanstack/router-plugin": "^1.167.35", + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-http": "^2.5.9", + "@tauri-apps/plugin-log": "^2.8.0", + "@uidotdev/usehooks": "^2.4.1", + "chart.js": "^4.5.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.20", + "motion": "^12.38.0", + "p-timeout": "^7.0.1", + "prettier": "^3.8.3", + "radashi": "^12.9.1", + "react": "^19.2.6", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.6", + "sass": "^1.99.0", + "zod": "^4.4.3", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@tanstack/devtools-vite": "^0.7.0", + "@types/node": "^25.7.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "globals": "^17.6.0", + "stylelint": "^17.11.1", + "stylelint-config-standard-scss": "^17.0.0", + "stylelint-scss": "^7.1.1", + "typescript": "~6.0.3", + "vite": "^8.0.13" + } +} diff --git a/new-ui/pnpm-lock.yaml b/new-ui/pnpm-lock.yaml new file mode 100644 index 00000000..4bf98348 --- /dev/null +++ b/new-ui/pnpm-lock.yaml @@ -0,0 +1,3135 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@biomejs/biome': + specifier: ^2.4.15 + version: 2.4.15 + '@floating-ui/react': + specifier: ^0.27.19 + version: 0.27.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-form': + specifier: ^1.32.0 + version: 1.32.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-query': + specifier: ^5.100.10 + version: 5.100.10(react@19.2.6) + '@tanstack/react-router': + specifier: ^1.169.2 + version: 1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-plugin': + specifier: ^1.167.35 + version: 1.167.35(@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0)) + '@tauri-apps/api': + specifier: ^2.11.0 + version: 2.11.0 + '@tauri-apps/plugin-http': + specifier: ^2.5.9 + version: 2.5.9 + '@tauri-apps/plugin-log': + specifier: ^2.8.0 + version: 2.8.0 + '@uidotdev/usehooks': + specifier: ^2.4.1 + version: 2.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + chart.js: + specifier: ^4.5.1 + version: 4.5.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dayjs: + specifier: ^1.11.20 + version: 1.11.20 + motion: + specifier: ^12.38.0 + version: 12.38.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + p-timeout: + specifier: ^7.0.1 + version: 7.0.1 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + radashi: + specifier: ^12.9.1 + version: 12.9.1 + react: + specifier: ^19.2.6 + version: 19.2.6 + react-chartjs-2: + specifier: ^5.3.1 + version: 5.3.1(chart.js@4.5.1)(react@19.2.6) + react-dom: + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) + sass: + specifier: ^1.99.0 + version: 1.99.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 + zustand: + specifier: ^5.0.13 + version: 5.0.13(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) + devDependencies: + '@tanstack/devtools-vite': + specifier: ^0.7.0 + version: 0.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0)) + '@types/node': + specifier: ^25.7.0 + version: 25.7.0 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0)) + autoprefixer: + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.14) + globals: + specifier: ^17.6.0 + version: 17.6.0 + stylelint: + specifier: ^17.11.1 + version: 17.11.1(typescript@6.0.3) + stylelint-config-standard-scss: + specifier: ^17.0.0 + version: 17.0.0(postcss@8.5.14)(stylelint@17.11.1(typescript@6.0.3)) + stylelint-scss: + specifier: ^7.1.1 + version: 7.1.1(stylelint@17.11.1(typescript@6.0.3)) + typescript: + specifier: ~6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.13 + version: 8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.4.15': + resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.15': + resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.15': + resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.15': + resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.15': + resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.15': + resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.15': + resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.15': + resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.15': + resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cacheable/memory@2.0.8': + resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + + '@cacheable/utils@2.4.1': + resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@csstools/media-query-list-parser@5.0.0': + resolution: {integrity: sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/selector-resolve-nested@4.0.0': + resolution: {integrity: sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + + '@csstools/selector-specificity@6.0.0': + resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.27.19': + resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-parser/binding-android-arm-eabi@0.120.0': + resolution: {integrity: sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.120.0': + resolution: {integrity: sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.120.0': + resolution: {integrity: sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.120.0': + resolution: {integrity: sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.120.0': + resolution: {integrity: sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': + resolution: {integrity: sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': + resolution: {integrity: sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': + resolution: {integrity: sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.120.0': + resolution: {integrity: sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': + resolution: {integrity: sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': + resolution: {integrity: sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': + resolution: {integrity: sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': + resolution: {integrity: sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.120.0': + resolution: {integrity: sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.120.0': + resolution: {integrity: sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.120.0': + resolution: {integrity: sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.120.0': + resolution: {integrity: sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + resolution: {integrity: sha512-1T0HKGcsz/BKo77t7+89L8Qvu4f9DoleKWHp3C5sJEcbCjDOLx3m9m722bWZTY+hANlUEs+yjlK+lBFsA+vrVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + resolution: {integrity: sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.120.0': + resolution: {integrity: sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + + '@oxc-project/types@0.130.0': + resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm64@1.0.1': + resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.1': + resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.1': + resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.1': + resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.1': + resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.1': + resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.1': + resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.1': + resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.1': + resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.1': + resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@tanstack/devtools-client@0.0.6': + resolution: {integrity: sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-bus@0.4.1': + resolution: {integrity: sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-client@0.4.3': + resolution: {integrity: sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==} + engines: {node: '>=18'} + hasBin: true + + '@tanstack/devtools-vite@0.7.0': + resolution: {integrity: sha512-VXki7K+Xwnpo3IKdNSWGe7YOvtZv33YlulGqaQ+YCpeQhYg8JFuxP50BXibDoRLj5EOX4r21Hs7COdxbRHXkTw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@tanstack/form-core@1.32.0': + resolution: {integrity: sha512-Tn5VRDSjyqjmaet2tJMuEWDRFyrCaon03vxXPlSSaiSs6C/N7lCIwGCXJbZXEUq1kTj8jYN9qyXHbsz4LQHcow==} + + '@tanstack/history@1.161.6': + resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} + engines: {node: '>=20.19'} + + '@tanstack/pacer-lite@0.1.1': + resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} + engines: {node: '>=18'} + + '@tanstack/query-core@5.100.10': + resolution: {integrity: sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==} + + '@tanstack/react-form@1.32.0': + resolution: {integrity: sha512-6WP5SQTA6/H9crCpvpq3ZppYWqtrdE5NjOy6ebABi6uAQPqhfTzrdjS9t40mCZCFtGI5585OhJV6zBP/KN2zcw==} + peerDependencies: + '@tanstack/react-start': '*' + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + + '@tanstack/react-query@5.100.10': + resolution: {integrity: sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router@1.169.2': + resolution: {integrity: sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.169.2': + resolution: {integrity: sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.166.42': + resolution: {integrity: sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.167.35': + resolution: {integrity: sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.169.2 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.161.8': + resolution: {integrity: sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + + '@tanstack/virtual-file-routes@1.161.7': + resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} + engines: {node: '>=20.19'} + hasBin: true + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + + '@tauri-apps/plugin-http@2.5.9': + resolution: {integrity: sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==} + + '@tauri-apps/plugin-log@2.8.0': + resolution: {integrity: sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/node@25.7.0': + resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@uidotdev/usehooks@2.4.1': + resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.3.0: + resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + baseline-browser-mapping@2.10.29: + resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.3.5: + resolution: {integrity: sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-functions-list@3.3.3: + resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} + engines: {node: '>=12'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + electron-to-chromium@1.5.355: + resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.3: + resolution: {integrity: sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@6.1.22: + resolution: {integrity: sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: 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] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + globby@16.2.0: + resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} + engines: {node: '>=20'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + has-flag@5.0.1: + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} + engines: {node: '>=12'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + html-tags@5.1.0: + resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} + engines: {node: '>=20.10'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + isbot@5.1.40: + resolution: {integrity: sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + launch-editor@2.13.2: + resolution: {integrity: sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mathml-tag-names@4.0.0: + resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + meow@14.1.0: + resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} + engines: {node: '>=20'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.44: + resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + oxc-parser@0.120.0: + resolution: {integrity: sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w==} + engines: {node: ^20.19.0 || >=22.12.0} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radashi@12.9.1: + resolution: {integrity: sha512-HCvrL1Ag7qnyH11UiSWQaEIiizJ7kldHjBw63aELoum7C8nQrSLqotLDuKKvoRPtO0w8azCzUQcL3yrU3lBksw==} + engines: {node: '>=16.0.0'} + + react-chartjs-2@5.3.1: + resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.0.1: + resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + stylelint-config-recommended-scss@17.0.1: + resolution: {integrity: sha512-x5DVehzJudcwF0od3sGpgkln2PLLranFE7twwbp7dqDINCyZvwzFkMc6TLhNOvazRiVBJYATQLouJY0xPGB8WA==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^17.0.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended@18.0.0: + resolution: {integrity: sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^17.0.0 + + stylelint-config-standard-scss@17.0.0: + resolution: {integrity: sha512-uLJS6xgOCBw5EMsDW7Ukji8l28qRoMnkRch15s0qwZpskXvWt9oPzMmcYM307m9GN4MxuWLsQh4I6hU9yI53cQ==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^17.0.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@40.0.0: + resolution: {integrity: sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^17.0.0 + + stylelint-scss@7.1.1: + resolution: {integrity: sha512-pLPXJZ7RtAFNLXe8gqarf3B56ScVTd1vPiL9IFgcJkIZsYPzAgLJPh2h9NHrp3BFeKrtAkIgwQ08QSmhvlv1gA==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^16.8.2 || ^17.0.0 + + stylelint@17.11.1: + resolution: {integrity: sha512-+smN/HqVTggUx3iuAzOi9fPh8SrH+cJWlZrYVldXoJ06orWBhZ4Ue/QEp64oei6pVrAh4w3tG+Y12Vw7MbCFRQ==} + engines: {node: '>=20.19.0'} + hasBin: true + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-hyperlinks@4.4.0: + resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==} + engines: {node: '>=20'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.21.0: + resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} + + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.0.13: + resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + write-file-atomic@7.0.1: + resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} + engines: {node: ^20.17.0 || >=22.9.0} + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zustand@5.0.13: + resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.4.15': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.15 + '@biomejs/cli-darwin-x64': 2.4.15 + '@biomejs/cli-linux-arm64': 2.4.15 + '@biomejs/cli-linux-arm64-musl': 2.4.15 + '@biomejs/cli-linux-x64': 2.4.15 + '@biomejs/cli-linux-x64-musl': 2.4.15 + '@biomejs/cli-win32-arm64': 2.4.15 + '@biomejs/cli-win32-x64': 2.4.15 + + '@biomejs/cli-darwin-arm64@2.4.15': + optional: true + + '@biomejs/cli-darwin-x64@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64@2.4.15': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-x64@2.4.15': + optional: true + + '@biomejs/cli-win32-arm64@2.4.15': + optional: true + + '@biomejs/cli-win32-x64@2.4.15': + optional: true + + '@cacheable/memory@2.0.8': + dependencies: + '@cacheable/utils': 2.4.1 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.4.1': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@floating-ui/react@0.27.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@floating-ui/utils': 0.2.11 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@kurkle/color@0.3.4': {} + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-parser/binding-android-arm-eabi@0.120.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.120.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.120.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.120.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.120.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.120.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.120.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.120.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.120.0': + optional: true + + '@oxc-project/types@0.120.0': {} + + '@oxc-project/types@0.130.0': {} + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@rolldown/binding-android-arm64@1.0.1': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.1': + optional: true + + '@rolldown/binding-darwin-x64@1.0.1': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.1': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.1': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.1': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.1': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.1': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.1': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.1': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@tanstack/devtools-client@0.0.6': + dependencies: + '@tanstack/devtools-event-client': 0.4.3 + + '@tanstack/devtools-event-bus@0.4.1': + dependencies: + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@tanstack/devtools-event-client@0.4.3': {} + + '@tanstack/devtools-vite@0.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0))': + dependencies: + '@tanstack/devtools-client': 0.0.6 + '@tanstack/devtools-event-bus': 0.4.1 + chalk: 5.6.2 + launch-editor: 2.13.2 + magic-string: 0.30.21 + oxc-parser: 0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + picomatch: 4.0.4 + vite: 8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - bufferutil + - utf-8-validate + + '@tanstack/form-core@1.32.0': + dependencies: + '@tanstack/devtools-event-client': 0.4.3 + '@tanstack/pacer-lite': 0.1.1 + '@tanstack/store': 0.9.3 + + '@tanstack/history@1.161.6': {} + + '@tanstack/pacer-lite@0.1.1': {} + + '@tanstack/query-core@5.100.10': {} + + '@tanstack/react-form@1.32.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/form-core': 1.32.0 + '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: 19.2.6 + transitivePeerDependencies: + - react-dom + + '@tanstack/react-query@5.100.10(react@19.2.6)': + dependencies: + '@tanstack/query-core': 5.100.10 + react: 19.2.6 + + '@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/history': 1.161.6 + '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-core': 1.169.2 + isbot: 5.1.40 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@tanstack/react-store@0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.6) + + '@tanstack/router-core@1.169.2': + dependencies: + '@tanstack/history': 1.161.6 + cookie-es: 3.1.1 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + '@tanstack/router-generator@1.166.42': + dependencies: + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.169.2 + '@tanstack/router-utils': 1.161.8 + '@tanstack/virtual-file-routes': 1.161.7 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.8.3 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.167.35(@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.169.2 + '@tanstack/router-generator': 1.166.42 + '@tanstack/router-utils': 1.161.8 + '@tanstack/virtual-file-routes': 1.161.7 + chokidar: 3.6.0 + unplugin: 3.0.0 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + vite: 8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.161.8': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + ansis: 4.3.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.16 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.3': {} + + '@tanstack/virtual-file-routes@1.161.7': {} + + '@tauri-apps/api@2.11.0': {} + + '@tauri-apps/plugin-http@2.5.9': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-log@2.8.0': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/node@25.7.0': + dependencies: + undici-types: 7.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@uidotdev/usehooks@2.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@vitejs/plugin-react@6.0.1(vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0) + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@2.0.1: {} + + astral-regex@2.0.0: {} + + autoprefixer@10.5.0(postcss@8.5.14): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001792 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-value-parser: 4.2.0 + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + baseline-browser-mapping@2.10.29: {} + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.29 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.355 + node-releases: 2.0.44 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + cacheable@2.3.5: + dependencies: + '@cacheable/memory': 2.0.8 + '@cacheable/utils': 2.4.1 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001792: {} + + chalk@5.6.2: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + 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 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + convert-source-map@2.0.0: {} + + cookie-es@3.1.1: {} + + cosmiconfig@9.0.1(typescript@6.0.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 6.0.3 + + css-functions-list@3.3.3: {} + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + dayjs@1.11.20: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + electron-to-chromium@1.5.355: {} + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + escalade@3.2.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + 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.8 + + fast-uri@3.1.2: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@11.1.3: + dependencies: + flat-cache: 6.1.22 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@6.1.22: + dependencies: + cacheable: 2.3.5 + flatted: 3.4.2 + hookified: 1.15.1 + + flatted@3.4.2: {} + + fraction.js@5.3.4: {} + + framer-motion@12.38.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.6.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@17.6.0: {} + + globby@16.2.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + + globjoin@0.1.4: {} + + has-flag@5.0.1: {} + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + html-tags@5.1.0: {} + + ignore@7.0.5: {} + + immutable@5.1.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@4.0.0: {} + + is-plain-object@5.0.0: {} + + isbot@5.1.40: {} + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.37.0: {} + + launch-editor@2.13.2: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lines-and-columns@1.2.4: {} + + lodash.truncate@4.4.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mathml-tag-names@4.0.0: {} + + mdn-data@2.27.1: {} + + meow@14.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.38.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + framer-motion: 12.38.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.44: {} + + normalize-path@3.0.0: {} + + oxc-parser@0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + dependencies: + '@oxc-project/types': 0.120.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.120.0 + '@oxc-parser/binding-android-arm64': 0.120.0 + '@oxc-parser/binding-darwin-arm64': 0.120.0 + '@oxc-parser/binding-darwin-x64': 0.120.0 + '@oxc-parser/binding-freebsd-x64': 0.120.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.120.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.120.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.120.0 + '@oxc-parser/binding-linux-arm64-musl': 0.120.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.120.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.120.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.120.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.120.0 + '@oxc-parser/binding-linux-x64-gnu': 0.120.0 + '@oxc-parser/binding-linux-x64-musl': 0.120.0 + '@oxc-parser/binding-openharmony-arm64': 0.120.0 + '@oxc-parser/binding-wasm32-wasi': 0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxc-parser/binding-win32-arm64-msvc': 0.120.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.120.0 + '@oxc-parser/binding-win32-x64-msvc': 0.120.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + p-timeout@7.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-scss@4.0.9(postcss@8.5.14): + dependencies: + postcss: 8.5.14 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier@3.8.3: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + queue-microtask@1.2.3: {} + + radashi@12.9.1: {} + + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.6): + dependencies: + chart.js: 4.5.1 + react: 19.2.6 + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rolldown@1.0.1: + dependencies: + '@oxc-project/types': 0.130.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.1 + '@rolldown/binding-darwin-arm64': 1.0.1 + '@rolldown/binding-darwin-x64': 1.0.1 + '@rolldown/binding-freebsd-x64': 1.0.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 + '@rolldown/binding-linux-arm64-gnu': 1.0.1 + '@rolldown/binding-linux-arm64-musl': 1.0.1 + '@rolldown/binding-linux-ppc64-gnu': 1.0.1 + '@rolldown/binding-linux-s390x-gnu': 1.0.1 + '@rolldown/binding-linux-x64-gnu': 1.0.1 + '@rolldown/binding-linux-x64-musl': 1.0.1 + '@rolldown/binding-openharmony-arm64': 1.0.1 + '@rolldown/binding-wasm32-wasi': 1.0.1 + '@rolldown/binding-win32-arm64-msvc': 1.0.1 + '@rolldown/binding-win32-x64-msvc': 1.0.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + seroval-plugins@1.5.4(seroval@1.5.4): + dependencies: + seroval: 1.5.4 + + seroval@1.5.4: {} + + shell-quote@1.8.3: {} + + signal-exit@4.1.0: {} + + slash@5.1.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + stylelint-config-recommended-scss@17.0.1(postcss@8.5.14)(stylelint@17.11.1(typescript@6.0.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.14) + stylelint: 17.11.1(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.11.1(typescript@6.0.3)) + stylelint-scss: 7.1.1(stylelint@17.11.1(typescript@6.0.3)) + optionalDependencies: + postcss: 8.5.14 + + stylelint-config-recommended@18.0.0(stylelint@17.11.1(typescript@6.0.3)): + dependencies: + stylelint: 17.11.1(typescript@6.0.3) + + stylelint-config-standard-scss@17.0.0(postcss@8.5.14)(stylelint@17.11.1(typescript@6.0.3)): + dependencies: + stylelint: 17.11.1(typescript@6.0.3) + stylelint-config-recommended-scss: 17.0.1(postcss@8.5.14)(stylelint@17.11.1(typescript@6.0.3)) + stylelint-config-standard: 40.0.0(stylelint@17.11.1(typescript@6.0.3)) + optionalDependencies: + postcss: 8.5.14 + + stylelint-config-standard@40.0.0(stylelint@17.11.1(typescript@6.0.3)): + dependencies: + stylelint: 17.11.1(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.11.1(typescript@6.0.3)) + + stylelint-scss@7.1.1(stylelint@17.11.1(typescript@6.0.3)): + dependencies: + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + '@csstools/css-tokenizer': 4.0.0 + css-tree: 3.2.1 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 17.11.1(typescript@6.0.3) + + stylelint@17.11.1(typescript@6.0.3): + dependencies: + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + colord: 2.9.3 + cosmiconfig: 9.0.1(typescript@6.0.3) + css-functions-list: 3.3.3 + css-tree: 3.2.1 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.3 + global-modules: 2.0.0 + globby: 16.2.0 + globjoin: 0.1.4 + html-tags: 5.1.0 + ignore: 7.0.5 + import-meta-resolve: 4.2.0 + mathml-tag-names: 4.0.0 + meow: 14.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.14 + postcss-safe-parser: 7.0.1(postcss@8.5.14) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + string-width: 8.2.1 + supports-hyperlinks: 4.4.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 7.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@10.2.2: {} + + supports-hyperlinks@4.4.0: + dependencies: + has-flag: 5.0.1 + supports-color: 10.2.2 + + svg-tags@1.0.0: {} + + tabbable@6.4.0: {} + + table@6.9.0: + dependencies: + ajv: 8.20.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@2.8.1: {} + + typescript@6.0.3: {} + + undici-types@7.21.0: {} + + unicorn-magic@0.4.0: {} + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + + util-deprecate@1.0.2: {} + + vite@8.0.13(@types/node@25.7.0)(jiti@2.7.0)(sass@1.99.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.7.0 + fsevents: 2.3.3 + jiti: 2.7.0 + sass: 1.99.0 + + webpack-virtual-modules@0.6.2: {} + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + write-file-atomic@7.0.1: + dependencies: + signal-exit: 4.1.0 + + ws@8.20.1: {} + + yallist@3.1.1: {} + + zod@3.25.76: {} + + zod@4.4.3: {} + + zustand@5.0.13(@types/react@19.2.14)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) diff --git a/new-ui/pnpm-workspace.yaml b/new-ui/pnpm-workspace.yaml new file mode 100644 index 00000000..620a7bb7 --- /dev/null +++ b/new-ui/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + '@parcel/watcher': true diff --git a/new-ui/public/favicon.svg b/new-ui/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/new-ui/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/new-ui/public/fonts/geist/Geist-Bold.woff2 b/new-ui/public/fonts/geist/Geist-Bold.woff2 new file mode 100644 index 00000000..46f524f4 Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-Bold.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-BoldItalic.woff2 b/new-ui/public/fonts/geist/Geist-BoldItalic.woff2 new file mode 100644 index 00000000..240acb99 Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-BoldItalic.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-Medium.woff2 b/new-ui/public/fonts/geist/Geist-Medium.woff2 new file mode 100644 index 00000000..ef6dbb21 Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-Medium.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-MediumItalic.woff2 b/new-ui/public/fonts/geist/Geist-MediumItalic.woff2 new file mode 100644 index 00000000..344fedaf Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-MediumItalic.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-Regular.woff2 b/new-ui/public/fonts/geist/Geist-Regular.woff2 new file mode 100644 index 00000000..0db0f194 Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-Regular.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-RegularItalic.woff2 b/new-ui/public/fonts/geist/Geist-RegularItalic.woff2 new file mode 100644 index 00000000..33e9948b Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-RegularItalic.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-SemiBold.woff2 b/new-ui/public/fonts/geist/Geist-SemiBold.woff2 new file mode 100644 index 00000000..83845230 Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-SemiBold.woff2 differ diff --git a/new-ui/public/fonts/geist/Geist-SemiBoldItalic.woff2 b/new-ui/public/fonts/geist/Geist-SemiBoldItalic.woff2 new file mode 100644 index 00000000..2f53ced4 Binary files /dev/null and b/new-ui/public/fonts/geist/Geist-SemiBoldItalic.woff2 differ diff --git a/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2 b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2 new file mode 100644 index 00000000..d60c270e Binary files /dev/null and b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2 differ diff --git a/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2 b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2 new file mode 100644 index 00000000..669d04cd Binary files /dev/null and b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2 differ diff --git a/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2 b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2 new file mode 100644 index 00000000..80cfd15e Binary files /dev/null and b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2 differ diff --git a/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2 b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2 new file mode 100644 index 00000000..40da4276 Binary files /dev/null and b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2 differ diff --git a/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2 b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2 new file mode 100644 index 00000000..5ead7b0d Binary files /dev/null and b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2 differ diff --git a/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2 b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2 new file mode 100644 index 00000000..c5dd294b Binary files /dev/null and b/new-ui/public/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2 differ diff --git a/new-ui/public/fonts/source_code_pro/SourceCodePro-Regular.woff2 b/new-ui/public/fonts/source_code_pro/SourceCodePro-Regular.woff2 new file mode 100644 index 00000000..40826f1a Binary files /dev/null and b/new-ui/public/fonts/source_code_pro/SourceCodePro-Regular.woff2 differ diff --git a/new-ui/public/icons.svg b/new-ui/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/new-ui/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/new-ui/src/app/App.tsx b/new-ui/src/app/App.tsx new file mode 100644 index 00000000..b1ffd0fc --- /dev/null +++ b/new-ui/src/app/App.tsx @@ -0,0 +1,23 @@ +import { QueryClientProvider } from '@tanstack/react-query'; +import { RouterProvider } from '@tanstack/react-router'; +import { MainBackground } from '../shared/components/MainBackground/MainBackground'; +import { TauriEventProvider } from '../shared/providers/TauriEventProvider'; +import { queryClient } from './query'; +import { router } from './router'; + +function App() { + return ( +
+ +
+ + + + + +
+
+ ); +} + +export default App; diff --git a/new-ui/src/app/day.ts b/new-ui/src/app/day.ts new file mode 100644 index 00000000..ef815f1e --- /dev/null +++ b/new-ui/src/app/day.ts @@ -0,0 +1,10 @@ +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/en'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(relativeTime); +dayjs.extend(utc); +dayjs.extend(localizedFormat); +dayjs.locale('en'); diff --git a/new-ui/src/app/query.ts b/new-ui/src/app/query.ts new file mode 100644 index 00000000..98775f69 --- /dev/null +++ b/new-ui/src/app/query.ts @@ -0,0 +1,40 @@ +import { MutationCache, QueryClient, type QueryKey } from '@tanstack/react-query'; + +type InvalidateMeta = { invalidate?: QueryKey[] | QueryKey }; + +let queryClient: QueryClient; + +type RO = readonly unknown[]; + +const isArrayFlat = (arr: RO | readonly RO[]): boolean => + arr.every((item) => !Array.isArray(item)); + +const mutationCache = new MutationCache({ + onSuccess: async (_data, _variables, _context, mutation) => { + const keys = (mutation.meta as InvalidateMeta | undefined)?.invalidate; + if (!Array.isArray(keys) || keys.length === 0) return; + if (isArrayFlat(keys)) { + await queryClient.invalidateQueries({ queryKey: keys }); + } else { + await Promise.all( + keys.map((key) => queryClient.invalidateQueries({ queryKey: key as QueryKey })), + ); + } + }, +}); + +queryClient = new QueryClient({ + mutationCache, + defaultOptions: { + queries: { + staleTime: 30_000, + gcTime: 10 * 60_000, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchOnReconnect: true, + retry: false, + }, + }, +}); + +export { queryClient }; diff --git a/new-ui/src/app/router.ts b/new-ui/src/app/router.ts new file mode 100644 index 00000000..100bf812 --- /dev/null +++ b/new-ui/src/app/router.ts @@ -0,0 +1,20 @@ +import { createRouter } from '@tanstack/react-router'; +import { routeTree } from '../routeTree.gen'; +import { NotFoundRoute } from '../shared/components/NotFoundRoute/NotFoundRoute'; +import { queryClient } from './query'; + +export const router = createRouter({ + routeTree, + basepath: import.meta.env.BASE_URL, + defaultPreloadStaleTime: 0, + defaultNotFoundComponent: NotFoundRoute, + context: { + queryClient, + }, +}); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} diff --git a/new-ui/src/main.tsx b/new-ui/src/main.tsx new file mode 100644 index 00000000..8d5ec030 --- /dev/null +++ b/new-ui/src/main.tsx @@ -0,0 +1,12 @@ +import './app/day.ts'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './app/App.tsx'; +import './shared/scss/index.scss'; + +// biome-ignore lint/style/noNonNullAssertion: this element is static in index.html +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/new-ui/src/pages/compact/CompactEmptyPage/CompactEmptyPage.tsx b/new-ui/src/pages/compact/CompactEmptyPage/CompactEmptyPage.tsx new file mode 100644 index 00000000..3c414b68 --- /dev/null +++ b/new-ui/src/pages/compact/CompactEmptyPage/CompactEmptyPage.tsx @@ -0,0 +1,29 @@ +import './style.scss'; +import { Button } from '../../../shared/components/Button/Button'; +import { ButtonSize, ButtonVariant } from '../../../shared/components/Button/types'; +import { Icon, IconKind } from '../../../shared/components/Icon'; +import { WindowHeader } from '../../../shared/components/WindowHeader/WindowHeader'; +import { api } from '../../../shared/rust-api/api'; +import { CompactPage } from '../CompactPage/CompactPage'; + +export const CompactEmptyPage = () => { + return ( + + +
+
+ +

{`You don't have any instances or tunnels yet. Click the button below to open Defguard.`}

+
+
+
+ ); +}; diff --git a/new-ui/src/pages/compact/CompactEmptyPage/style.scss b/new-ui/src/pages/compact/CompactEmptyPage/style.scss new file mode 100644 index 00000000..7fed3b63 --- /dev/null +++ b/new-ui/src/pages/compact/CompactEmptyPage/style.scss @@ -0,0 +1,24 @@ +#compact-empty-page { + .empty-card { + border-radius: 12px; + box-sizing: border-box; + padding: var(--spacing-lg); + background-color: var(--bg-dark-blue-40); + width: 100%; + min-height: 277px; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + + > .icon { + margin-bottom: var(--spacing-lg); + } + + > p { + font: var(--t-body-xs-400); + color: var(--bg-white-100); + padding-bottom: var(--spacing-xl); + } + } +} diff --git a/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx b/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx new file mode 100644 index 00000000..8549451b --- /dev/null +++ b/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx @@ -0,0 +1,109 @@ +import './style.scss'; +import { useQuery } from '@tanstack/react-query'; +import { useLoaderData } from '@tanstack/react-router'; +import { useEffect, useMemo } from 'react'; +import { Button } from '../../../shared/components/Button/Button'; +import { ButtonVariant } from '../../../shared/components/Button/types'; +import { Controls } from '../../../shared/components/Controls/Controls'; +import { Divider } from '../../../shared/components/Divider/Divider'; +import { LocationCard } from '../../../shared/components/LocationCard/LocationCard'; +import { WindowHeader } from '../../../shared/components/WindowHeader/WindowHeader'; +import { api } from '../../../shared/rust-api/api'; +import { + getInstancesQueryOptions, + getLocationsQueryOptions, +} from '../../../shared/rust-api/query'; +import { ThemeSpacing } from '../../../shared/types'; +import { isPresent } from '../../../shared/utils/isPresent'; +import { CompactPage } from '../CompactPage/CompactPage'; +import { InstanceSwitcher } from './components/InstanceSwitcher'; +import { useCompactLocationStore } from './hooks/useCompactLocationsStore'; + +export const CompactLocationsPage = () => { + const selection = useCompactLocationStore((s) => s.compactViewSelection); + const openLocation = useCompactLocationStore((s) => s.expandedLocation); + + const routeData = useLoaderData({ from: '/' }); + + const queryInstanceId = useMemo(() => { + if (!isPresent(selection)) return routeData.instances[0].id; + if (selection.kind === 'instance') return selection.data.id; + return selection.data.instance_id; + }, [selection, routeData.instances]); + + const { data: locations } = useQuery(getLocationsQueryOptions(queryInstanceId)); + + const { data: instances } = useQuery(getInstancesQueryOptions); + + const instanceInfo = useMemo(() => { + const allInstances = instances ?? routeData.instances; + if (!isPresent(selection)) return allInstances[0]; + if (selection.kind === 'instance') + return allInstances.find((i) => i.id === selection.data.id); + return allInstances.find((i) => i.id === selection.data.instance_id); + }, [selection, instances, routeData.instances]); + + const displayedLocations = useMemo(() => { + if (!isPresent(selection) || selection.kind === 'instance') { + return locations ?? routeData.locations; + } + return [selection.data]; + }, [selection, locations, routeData.locations]); + + useEffect(() => { + if (selection === null || instanceInfo === undefined) { + useCompactLocationStore.setState({ + compactViewSelection: { kind: 'instance', data: routeData.instances[0] }, + }); + } + }, [routeData.instances, instanceInfo, selection]); + + return ( + + +
+ +
+ {isPresent(instanceInfo) && + displayedLocations.map((location) => { + const isOpen = + location.id === openLocation || displayedLocations.length === 1; + return ( + { + if (isOpen) { + useCompactLocationStore.setState({ expandedLocation: null }); + } else { + useCompactLocationStore.setState({ expandedLocation: location.id }); + } + }} + /> + ); + })} +
+
+
+ + +
+
+ ); +}; diff --git a/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx b/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx new file mode 100644 index 00000000..cc7d6ff3 --- /dev/null +++ b/new-ui/src/pages/compact/CompactLocationsPage/components/InstanceSwitcher.tsx @@ -0,0 +1,84 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { Select } from '../../../../shared/components/Select/Select'; +import type { + SelectOption, + SelectOptionGroup, +} from '../../../../shared/components/Select/types'; +import { + getInstancesQueryOptions, + getTunnelsQueryOptions, +} from '../../../../shared/rust-api/query'; +import { isPresent } from '../../../../shared/utils/isPresent'; +import { + type CompactViewSelection, + useCompactLocationStore, +} from '../hooks/useCompactLocationsStore'; + +export const InstanceSwitcher = () => { + const selectedInstance = useCompactLocationStore((s) => s.compactViewSelection); + + const { data: tunnels } = useQuery(getTunnelsQueryOptions); + const { data: instances } = useQuery(getInstancesQueryOptions); + + const groups = useMemo((): readonly SelectOptionGroup[] => { + if (!isPresent(instances) || !isPresent(tunnels)) return []; + + const instanceGroup: SelectOptionGroup = { + key: 'instances', + label: 'Instances', + options: instances.map((instance) => ({ + key: instance.id, + label: instance.name, + value: { kind: 'instance', data: instance }, + })), + }; + + const tunnelGroup: SelectOptionGroup = { + key: 'tunnels', + label: 'Tunnels', + options: tunnels.map((tunnel) => ({ + key: tunnel.id ?? tunnel.name, + label: tunnel.name, + value: { kind: 'tunnel', data: tunnel }, + })), + }; + + return [instanceGroup, tunnelGroup]; + }, [instances, tunnels]); + + const totalOptions = useMemo( + () => groups.reduce((acc, g) => acc + g.options.length, 0), + [groups], + ); + + const selectedOption = useMemo((): SelectOption | undefined => { + if (!isPresent(selectedInstance)) return undefined; + for (const group of groups) { + const found = group.options.find((o) => { + if (selectedInstance.kind === 'instance' && o.value.kind === 'instance') { + return o.value.data.id === selectedInstance.data.id; + } + if (selectedInstance.kind === 'tunnel' && o.value.kind === 'tunnel') { + return o.value.data.id === selectedInstance.data.id; + } + return false; + }); + if (found) return found; + } + return undefined; + }, [selectedInstance, groups]); + + if (!isPresent(instances) || !isPresent(tunnels)) return null; + if (totalOptions <= 1) return null; + + return ( + + + + ); +}; diff --git a/new-ui/src/routeTree.gen.ts b/new-ui/src/routeTree.gen.ts new file mode 100644 index 00000000..c52b8783 --- /dev/null +++ b/new-ui/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as EmptyRouteImport } from './routes/empty' +import { Route as IndexRouteImport } from './routes/index' +import { Route as PlaygroundIndexRouteImport } from './routes/playground/index' + +const EmptyRoute = EmptyRouteImport.update({ + id: '/empty', + path: '/empty', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const PlaygroundIndexRoute = PlaygroundIndexRouteImport.update({ + id: '/playground/', + path: '/playground/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/empty': typeof EmptyRoute + '/playground/': typeof PlaygroundIndexRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/empty': typeof EmptyRoute + '/playground': typeof PlaygroundIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/empty': typeof EmptyRoute + '/playground/': typeof PlaygroundIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/empty' | '/playground/' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/empty' | '/playground' + id: '__root__' | '/' | '/empty' | '/playground/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + EmptyRoute: typeof EmptyRoute + PlaygroundIndexRoute: typeof PlaygroundIndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/empty': { + id: '/empty' + path: '/empty' + fullPath: '/empty' + preLoaderRoute: typeof EmptyRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/playground/': { + id: '/playground/' + path: '/playground' + fullPath: '/playground/' + preLoaderRoute: typeof PlaygroundIndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + EmptyRoute: EmptyRoute, + PlaygroundIndexRoute: PlaygroundIndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/new-ui/src/routes/__root.tsx b/new-ui/src/routes/__root.tsx new file mode 100644 index 00000000..0bf72615 --- /dev/null +++ b/new-ui/src/routes/__root.tsx @@ -0,0 +1,16 @@ +import type { QueryClient } from '@tanstack/react-query'; +import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'; + +interface RouterContext { + queryClient: QueryClient; +} + +export const Route = createRootRouteWithContext()({ + component: RootComponent, + pendingMs: 500, + pendingMinMs: 250, +}); + +function RootComponent() { + return ; +} diff --git a/new-ui/src/routes/empty.tsx b/new-ui/src/routes/empty.tsx new file mode 100644 index 00000000..d0197839 --- /dev/null +++ b/new-ui/src/routes/empty.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/empty')({ + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/empty"!
; +} diff --git a/new-ui/src/routes/index.tsx b/new-ui/src/routes/index.tsx new file mode 100644 index 00000000..7b2a9767 --- /dev/null +++ b/new-ui/src/routes/index.tsx @@ -0,0 +1,58 @@ +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { CompactLocationsPage } from '../pages/compact/CompactLocationsPage/CompactLocationsPage'; +import { useCompactLocationStore } from '../pages/compact/CompactLocationsPage/hooks/useCompactLocationsStore'; +import { + getInstancesQueryOptions, + getLocationsQueryOptions, + getTunnelsQueryOptions, +} from '../shared/rust-api/query'; +import type { LocationInfo } from '../shared/rust-api/types'; + +export const Route = createFileRoute('/')({ + loader: async ({ context }) => { + const [instances, tunnels] = await Promise.all([ + context.queryClient.fetchQuery(getInstancesQueryOptions), + context.queryClient.fetchQuery(getTunnelsQueryOptions), + ]); + + if (instances.length === 0 && tunnels.length === 0) { + throw redirect({ to: '/empty' }); + } + + const stored = useCompactLocationStore.getState().compactViewSelection; + + let storedIsValid: boolean; + if (stored === null) { + storedIsValid = false; + } else if (stored.kind === 'instance') { + storedIsValid = instances.some((i) => i.id === stored.data.id); + } else { + storedIsValid = tunnels.some((t) => t.id === stored.data.id); + } + + let selected: NonNullable; + if (storedIsValid && stored !== null) { + selected = stored; + } else if (instances.length > 0) { + selected = { kind: 'instance', data: instances[0] }; + } else { + selected = { kind: 'tunnel', data: tunnels[0] }; + } + + if (!storedIsValid) { + useCompactLocationStore.setState({ compactViewSelection: selected }); + } + + let locations: LocationInfo[]; + if (selected.kind === 'instance') { + locations = await context.queryClient.fetchQuery( + getLocationsQueryOptions(selected.data.id), + ); + } else { + locations = []; + } + + return { instances, tunnels, locations }; + }, + component: CompactLocationsPage, +}); diff --git a/new-ui/src/routes/playground/index.tsx b/new-ui/src/routes/playground/index.tsx new file mode 100644 index 00000000..3bf3cf21 --- /dev/null +++ b/new-ui/src/routes/playground/index.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { PlaygroundIndex } from '../../pages/playground/PlaygroundIndex'; + +export const Route = createFileRoute('/playground/')({ + component: PlaygroundIndex, +}); diff --git a/new-ui/src/shared/components/BoxIcon/BoxIcon.tsx b/new-ui/src/shared/components/BoxIcon/BoxIcon.tsx new file mode 100644 index 00000000..4fc89961 --- /dev/null +++ b/new-ui/src/shared/components/BoxIcon/BoxIcon.tsx @@ -0,0 +1,6 @@ +import './style.scss'; +import type { PropsWithChildren } from 'react'; + +export const BoxIcon = ({ children }: PropsWithChildren) => { + return
{children}
; +}; diff --git a/new-ui/src/shared/components/BoxIcon/style.scss b/new-ui/src/shared/components/BoxIcon/style.scss new file mode 100644 index 00000000..20394a2e --- /dev/null +++ b/new-ui/src/shared/components/BoxIcon/style.scss @@ -0,0 +1,14 @@ +.box-icon { + background: var(--bg-white-10); + border-radius: 8px; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + height: 24px; + width: 24px; + + .icon { + --icon-size: 16px; + } +} diff --git a/new-ui/src/shared/components/Button/Button.tsx b/new-ui/src/shared/components/Button/Button.tsx new file mode 100644 index 00000000..b1258b72 --- /dev/null +++ b/new-ui/src/shared/components/Button/Button.tsx @@ -0,0 +1,103 @@ +import './style.scss'; +import { clsx } from 'clsx'; +import { motion } from 'motion/react'; +import { useEffect, useRef, useState } from 'react'; +import { motionTransitionStandard } from '../../consts'; +import { isPresent } from '../../utils/isPresent'; +import { Icon } from '../Icon'; +import { LoaderSpinner } from '../LoaderSpinner/LoaderSpinner'; +import { type ButtonProps, ButtonSize, ButtonVariant } from './types'; + +export const Button = ({ + text, + testId, + iconLeft, + iconRight, + iconRightRotation, + containerProps, + onClick, + size = ButtonSize.Primary, + variant = ButtonVariant.Primary, + disabled = false, + loading = false, +}: ButtonProps) => { + const isLoading = loading && !disabled; + const [swapDirection, setSwapDirection] = useState<'to-loading' | 'to-content' | null>( + null, + ); + const previousLoadingRef = useRef(isLoading); + + useEffect(() => { + if (previousLoadingRef.current !== isLoading) { + setSwapDirection(isLoading ? 'to-loading' : 'to-content'); + previousLoadingRef.current = isLoading; + } + }, [isLoading]); + + const contentTransition = { + ...motionTransitionStandard, + delay: + !isLoading && swapDirection === 'to-content' + ? motionTransitionStandard.duration + : 0, + }; + + const loaderTransition = { + ...motionTransitionStandard, + delay: + isLoading && swapDirection === 'to-loading' ? motionTransitionStandard.duration : 0, + }; + + return ( +
+ +
+ ); +}; diff --git a/new-ui/src/shared/components/Button/style.scss b/new-ui/src/shared/components/Button/style.scss new file mode 100644 index 00000000..46828297 --- /dev/null +++ b/new-ui/src/shared/components/Button/style.scss @@ -0,0 +1,215 @@ +.btn-wrap { + display: inline-block; + min-width: 0; + position: relative; +} + +.btn { + --btn-font: var(--t-button-label-primary); + --btn-size: var(--button-size-primary); + --border-color: transparent; + --bg-color: transparent; + --text-color: var(--fg-white-100); + --loader-bg: transparent; + + height: var(--btn-size); + border: 1px solid var(--border-color); + box-sizing: border-box; + border-radius: var(--button-border-radius-primary); + background: var(--bg-color); + color: var(--text-color); + min-height: var(--btn-size); + padding: var(--spacing-sm) var(--spacing-lg); + position: relative; + display: inline-block; + min-width: 0; + user-select: none; + + > .btn-content { + display: inline-grid; + grid-template-columns: auto; + grid-template-rows: 1fr; + align-items: center; + column-gap: var(--spacing-md); + text-decoration: none !important; + justify-content: center; + color: inherit; + } + + @include animate(background-color, border-color, opacity); + + &:not(.disabled, .loading) { + cursor: pointer; + } + + &.disabled { + cursor: not-allowed; + pointer-events: none; + } + + .icon svg { + --icon-color: var(--text-color); + + path { + fill: var(--icon-color); + + @include animate(fill); + } + } + + .text { + font: var(--btn-font); + color: inherit; + + @include animate(color); + } + + &.icon-left { + > .btn-content { + grid-template-columns: 20px auto; + } + } + + &.icon-right { + > .btn-content { + grid-template-columns: auto 20px; + } + } + + &.icon-both { + > .btn-content { + grid-template-columns: 20px auto 20px; + } + } + + &.size-primary { + --btn-font: var(--t-button-label-primary); + --btn-size: 40px; + + border-radius: 8px; + padding: var(--spacing-sm) var(--spacing-lg); + } + + &.size-big { + --btn-font: var(--t-button-label-big); + --btn-size: 44px; + + border-radius: 100px; + padding: var(--spacing-sm) var(--spacing-lg); + } + + &.variant-primary { + --bg-color: var(--bg-white-100); + --border-color: var(--bg-white-100); + --text-color: var(--fg-action); + --loader-bg: var(--bg-white-10); + + &:hover { + --bg-color: var(--fg-white-80); + --border-color: var(--bg-color); + } + + &.disabled { + --bg-color: var(--bg-white-20); + --border-color: var(--bg-color); + --text-color: var(--fg-white-40); + } + + &:not(.disabled).loading { + --bg-color: var(--c-white-10); + --border-color: var(--bg-color); + } + } + + &.variant-secondary { + --bg-color: var(--bg-white-10); + --border-color: var(--bg-color); + --text-color: var(--fg-white-100); + --loader-bg: var(--bg-white-10); + + &:hover { + --bg-color: var(--bg-white-20); + } + + &.disabled { + --bg-color: var(--bg-white-5); + --border-color: var(--bg-color); + --text-color: var(--fg-white-40); + } + + &:not(.disabled).loading { + --bg-color: var(--bg-white-10); + --border-color: var(--bg-color); + } + } + + &.variant-critical { + --bg-color: var(--bg-critical); + --border-color: var(--bg-color); + --text-color: var(--fg-white-100); + --loader-bg: var(--bg-critical-disabled); + + &:hover { + --bg-color: var(--bg-critical-faded); + } + + &.disabled { + --bg-color: var(--bg-critical-disabled); + --border-color: var(--bg-color); + --text-color: var(--fg-white-40); + } + + &:not(.disabled).loading { + --bg-color: var(--bg-critical-disabled); + --border-color: var(--bg-color); + } + } + + &.variant-outlined { + --bg-color: transparent; + --border-color: var(--border-default); + --loader-bg: var(--bg-white-10); + --text-color: var(--fg-white-100); + + &:hover { + --bg-color: var(--bg-white-5); + } + + &.disabled { + --bg-color: transparent; + --border-color: var(--border-disabled); + --text-color: var(--fg-white-40); + } + + &:not(.disabled).loading { + --bg-color: transparent; + --border-color: var(--border-disabled); + } + } + + // since box-shadow adds on top of element it's needed to be set with loader overlay + &:not(.disabled).loading { + --loader-bg: var(--loader-bg); + + cursor: default; + } + + .loader-overlay { + background-color: var(--loader-bg); + border: none; + border-radius: inherit; + position: absolute; + inset: 0; + pointer-events: none; + width: 100%; + height: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + } +} + +a:has(.btn) { + text-decoration: none; +} diff --git a/new-ui/src/shared/components/Button/types.ts b/new-ui/src/shared/components/Button/types.ts new file mode 100644 index 00000000..fd1c7b3c --- /dev/null +++ b/new-ui/src/shared/components/Button/types.ts @@ -0,0 +1,32 @@ +import type { HTMLAttributes, MouseEventHandler } from 'react'; +import type { DirectionValue } from '../../types'; +import type { IconKindValue } from '../Icon/icon-types'; + +export const ButtonVariant = { + Primary: 'primary', + Secondary: 'secondary', + Critical: 'critical', + Outlined: 'outlined', +} as const; + +export type ButtonVariantValue = (typeof ButtonVariant)[keyof typeof ButtonVariant]; + +export const ButtonSize = { + Primary: 'primary', + Big: 'big', +} as const; +export type ButtonSizeValue = (typeof ButtonSize)[keyof typeof ButtonSize]; + +export type ButtonProps = { + text: string; + variant?: ButtonVariantValue; + size?: ButtonSizeValue; + iconLeft?: IconKindValue; + iconRight?: IconKindValue; + iconRightRotation?: DirectionValue; + testId?: string; + disabled?: boolean; + loading?: boolean; + containerProps?: Omit, 'onClick'>; + onClick?: MouseEventHandler; +}; diff --git a/new-ui/src/shared/components/Checkbox/Checkbox.tsx b/new-ui/src/shared/components/Checkbox/Checkbox.tsx new file mode 100644 index 00000000..97467ea4 --- /dev/null +++ b/new-ui/src/shared/components/Checkbox/Checkbox.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx'; +import './style.scss'; +import { isPresent } from '../../utils/isPresent'; +import { CheckboxIndicator } from '../CheckboxIndicator/CheckboxIndicator'; +import { FieldError } from '../FieldError/FieldError'; +import type { CheckboxProps } from './types'; + +export const Checkbox = ({ + text, + error, + testId, + active = false, + disabled = false, + children, + onClick, +}: CheckboxProps) => { + const hasError = isPresent(error); + + return ( +
+
+ + {isPresent(text) && {text}} + {isPresent(children) &&
{children}
} +
+ {isPresent(error) && error.length > 0 && } +
+ ); +}; diff --git a/new-ui/src/shared/components/Checkbox/style.scss b/new-ui/src/shared/components/Checkbox/style.scss new file mode 100644 index 00000000..21354e13 --- /dev/null +++ b/new-ui/src/shared/components/Checkbox/style.scss @@ -0,0 +1,48 @@ +/* stylelint-disable no-descending-specificity */ +.checkbox { + display: inline-flex; + flex-flow: column; + user-select: none; + + &.disabled { + cursor: not-allowed; + } + + & > .track { + &:not(.text) { + display: inline-block; + } + + &.text { + display: grid; + grid-template-columns: 24px auto; + grid-template-rows: 1fr; + column-gap: var(--spacing-sm); + align-items: center; + } + + &:not(.disabled) { + cursor: pointer; + } + + &.disabled { + pointer-events: none; + cursor: not-allowed; + + span { + color: var(--fg-disabled); + } + } + + span { + font: var(--t-body-sm-400); + color: var(--fg-white-100); + } + + &:not(.active, .disabled, .error):hover { + .box { + --border: var(--border-emphasis); + } + } + } +} diff --git a/new-ui/src/shared/components/Checkbox/types.ts b/new-ui/src/shared/components/Checkbox/types.ts new file mode 100644 index 00000000..2e972f62 --- /dev/null +++ b/new-ui/src/shared/components/Checkbox/types.ts @@ -0,0 +1,10 @@ +import type { PropsWithChildren } from 'react'; + +export interface CheckboxProps extends PropsWithChildren { + testId?: string; + active?: boolean; + error?: string; + disabled?: boolean; + text?: string; + onClick?: () => void; +} diff --git a/new-ui/src/shared/components/CheckboxIndicator/CheckboxIndicator.tsx b/new-ui/src/shared/components/CheckboxIndicator/CheckboxIndicator.tsx new file mode 100644 index 00000000..21f6d6be --- /dev/null +++ b/new-ui/src/shared/components/CheckboxIndicator/CheckboxIndicator.tsx @@ -0,0 +1,50 @@ +import './style.scss'; +import clsx from 'clsx'; +import type { MouseEventHandler, Ref } from 'react'; +import { ThemeVariable } from '../../types'; + +type Props = { + active: boolean; + disabled?: boolean; + error?: boolean; + onClick?: MouseEventHandler; + ref?: Ref; +}; + +export const CheckboxIndicator = ({ error, active, disabled, ref, onClick }: Props) => { + return ( +
+
+
+ {active && ( + + + + )} +
+
+ ); +}; diff --git a/new-ui/src/shared/components/CheckboxIndicator/style.scss b/new-ui/src/shared/components/CheckboxIndicator/style.scss new file mode 100644 index 00000000..8384eaf9 --- /dev/null +++ b/new-ui/src/shared/components/CheckboxIndicator/style.scss @@ -0,0 +1,75 @@ +/* stylelint-disable no-descending-specificity */ +.checkbox-indicator { + & > .box-positioner { + display: grid; + grid-template-columns: 24px; + grid-template-rows: 24px; + place-items: center center; + justify-content: center; + overflow: hidden; + + svg { + z-index: 0; + + path { + @include animate(fill); + } + } + + .box, + svg { + grid-column: 1 / 2; + grid-row: 1; + } + } +} + +.checkbox-indicator .box { + --bg: transparent; + --border: var(--border-action); + --icon: var(--fg-action); + + content: ' '; + display: block; + width: 20px; + height: 20px; + box-sizing: border-box; + border-radius: var(--radius-sm); + background-color: var(--bg); + background-clip: padding-box; + position: relative; + + &::before { + content: ''; + position: absolute; + inset: 0; + border: var(--border-1) solid var(--border); + border-radius: inherit; + pointer-events: none; + background-color: transparent; + background-clip: padding-box; + } + + @include animate(border-color, background-color); + + &.error { + --border: var(--border-critical); + } + + &:not(.disabled).active { + --bg: var(--bg-white-100); + --border: var(--bg); + --icon: var(--fg-action); + } + + &:not(.active).disabled { + --border: var(--border-disabled); + --bg: var(--bg-white-5); + } + + &.active.disabled { + --border: var(--bg); + --bg: var(--bg-white-10); + --icon: var(--fg-white-60); + } +} diff --git a/new-ui/src/shared/components/CodeInput/CodeInput.tsx b/new-ui/src/shared/components/CodeInput/CodeInput.tsx new file mode 100644 index 00000000..9ad3e4f1 --- /dev/null +++ b/new-ui/src/shared/components/CodeInput/CodeInput.tsx @@ -0,0 +1,137 @@ +import './style.scss'; +import { + type ClipboardEvent, + type KeyboardEvent, + useEffect, + useRef, + useState, +} from 'react'; +import { isPresent } from '../../utils/isPresent'; +import { FieldBox } from '../FieldBox/FieldBox'; +import { FieldError } from '../FieldError/FieldError'; + +interface Props { + length?: number; + value: string | null; + error?: string | null; + onChange: (value: string) => void; +} + +const toDigits = (value: string | null, length: number): string[] => { + const arr = Array.from({ length }, () => ''); + if (!value) return arr; + for (let i = 0; i < Math.min(value.length, length); i++) { + arr[i] = value[i] ?? ''; + } + return arr; +}; + +export const CodeInput = ({ onChange, value, error, length = 6 }: Props) => { + const [digits, setDigits] = useState(() => toDigits(value, length)); + const [focusedIndex, setFocusedIndex] = useState(null); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const prevLengthRef = useRef(length); + + useEffect(() => { + const lengthChanged = prevLengthRef.current !== length; + prevLengthRef.current = length; + + if (lengthChanged) { + setDigits(Array.from({ length }, () => '')); + requestAnimationFrame(() => inputRefs.current[0]?.focus()); + } else { + setDigits((current) => { + if (current.join('') === (value ?? '')) return current; + return toDigits(value, length); + }); + } + }, [value, length]); + + const focus = (index: number) => { + const clamped = Math.max(0, Math.min(index, length - 1)); + inputRefs.current[clamped]?.focus(); + }; + + const updateDigit = (index: number, digit: string) => { + setDigits((prev) => { + const updated = [...prev]; + updated[index] = digit; + onChange(updated.join('')); + return updated; + }); + }; + + const handleKeyDown = (index: number, e: KeyboardEvent) => { + if (e.ctrlKey || e.metaKey) return; + + if (e.key === 'Backspace') { + e.preventDefault(); + updateDigit(index, ''); + focus(index - 1); + } else if (e.key === 'Delete') { + e.preventDefault(); + updateDigit(index, ''); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + focus(index - 1); + } else if (e.key === 'ArrowRight') { + e.preventDefault(); + focus(index + 1); + } else if (/^[0-9]$/.test(e.key)) { + e.preventDefault(); + updateDigit(index, e.key); + if (index < length - 1) { + focus(index + 1); + } + } else if (e.key.length === 1) { + e.preventDefault(); + } + }; + + const handlePaste = (e: ClipboardEvent) => { + e.preventDefault(); + const cleaned = e.clipboardData.getData('text').trim().replace(/\D/g, ''); + if (cleaned.length === length) { + const newDigits = cleaned.split(''); + setDigits(newDigits); + onChange(cleaned); + focus(length - 1); + } + }; + + return ( +
+
+ {digits.map((digit, i) => ( + 0} + size="default" + onClick={() => { + const input = inputRefs.current[i]; + if (input) { + input.focus(); + } + }} + > + { + inputRefs.current[i] = el; + }} + type="text" + inputMode="numeric" + value={digit} + onFocus={() => setFocusedIndex(i)} + onBlur={() => setFocusedIndex(null)} + onKeyDown={(e) => handleKeyDown(i, e)} + onPaste={handlePaste} + onChange={() => {}} + /> + + ))} +
+ +
+ ); +}; diff --git a/new-ui/src/shared/components/CodeInput/style.scss b/new-ui/src/shared/components/CodeInput/style.scss new file mode 100644 index 00000000..d1e7ba99 --- /dev/null +++ b/new-ui/src/shared/components/CodeInput/style.scss @@ -0,0 +1,30 @@ +.code-input > .inputs-grid { + display: flex; + flex-flow: row nowrap; + gap: var(--spacing-md); + justify-content: center; + align-items: center; + + .field-box { + height: 36px; + width: 36px; + min-height: unset; + padding: 0; + cursor: text; + + input { + width: auto; + min-width: 12px; + height: 20px; + text-align: center; + background: transparent; + border: none; + outline: none; + color: var(--fg-white-100); + font: var(--t-input-text-primary); + line-height: 20px; + caret-color: transparent; + cursor: text; + } + } +} diff --git a/new-ui/src/shared/components/Controls/Controls.tsx b/new-ui/src/shared/components/Controls/Controls.tsx new file mode 100644 index 00000000..bc0ba3e7 --- /dev/null +++ b/new-ui/src/shared/components/Controls/Controls.tsx @@ -0,0 +1,13 @@ +import type { HTMLProps, PropsWithChildren } from 'react'; +import './style.scss'; +import clsx from 'clsx'; + +type Props = PropsWithChildren & HTMLProps; + +export const Controls = ({ children, className, ...props }: Props) => { + return ( +
+ {children} +
+ ); +}; diff --git a/new-ui/src/shared/components/Controls/style.scss b/new-ui/src/shared/components/Controls/style.scss new file mode 100644 index 00000000..b688d42b --- /dev/null +++ b/new-ui/src/shared/components/Controls/style.scss @@ -0,0 +1,16 @@ +.controls { + display: flex; + flex-flow: row nowrap; + column-gap: var(--spacing-md); + align-items: center; + justify-content: flex-start; + + .right { + margin-left: auto; + display: flex; + flex-flow: row nowrap; + column-gap: var(--spacing-md); + align-items: center; + justify-content: flex-end; + } +} diff --git a/new-ui/src/shared/components/Divider/Divider.tsx b/new-ui/src/shared/components/Divider/Divider.tsx new file mode 100644 index 00000000..2cf8076c --- /dev/null +++ b/new-ui/src/shared/components/Divider/Divider.tsx @@ -0,0 +1,59 @@ +import './style.scss'; +import clsx from 'clsx'; +import { type CSSProperties, useMemo } from 'react'; +import type { OrientationValue, ThemeSpacingValue } from '../../types'; +import { isPresent } from '../../utils/isPresent'; + +type Props = { + text?: string; + orientation?: OrientationValue; + spacing?: ThemeSpacingValue; +}; + +export const Divider = ({ text, spacing, orientation = 'horizontal' }: Props) => { + const textPresent = isPresent(text) && text.length > 0; + + const style = useMemo((): CSSProperties => { + const res: CSSProperties = {}; + if (spacing) { + switch (orientation) { + case 'horizontal': + res.paddingTop = spacing; + res.paddingBottom = spacing; + break; + case 'vertical': + res.paddingLeft = spacing; + res.paddingRight = spacing; + break; + } + } + return res; + }, [orientation, spacing]); + + return ( +
+ {orientation === 'horizontal' && ( + <> + {textPresent && ( + <> + + {text} + + + )} + {!textPresent && } + + )} + {orientation === 'vertical' && } +
+ ); +}; + +const Line = () => { + return
; +}; diff --git a/new-ui/src/shared/components/Divider/style.scss b/new-ui/src/shared/components/Divider/style.scss new file mode 100644 index 00000000..a75bd2fb --- /dev/null +++ b/new-ui/src/shared/components/Divider/style.scss @@ -0,0 +1,53 @@ +.divider { + --divider-line-size: 1px; + --divider-color: var(--bg-white-20); + + user-select: none; + + .line { + content: ' '; + display: block; + background-color: var(--divider-color); + border-radius: 0; + margin: 0; + padding: 0; + } + + &.vertical { + display: inline-block; + height: 10px; + + .line { + height: inherit; + width: var(--divider-line-size); + } + } + + &.horizontal { + width: 100%; + + .line { + width: 100%; + height: var(--divider-line-size); + } + } + + &.horizontal.text { + --divider-color: var(--bg-action-faded); + + display: grid; + grid-template-columns: 1fr auto 1fr; + grid-template-rows: 1fr; + column-gap: var(--spacing-lg); + align-items: center; + + .line { + width: 100%; + } + } + + span { + font: var(--t-body-xs-500); + color: var(--fg-white-70); + } +} diff --git a/new-ui/src/shared/components/EmptyState/EmptyState.tsx b/new-ui/src/shared/components/EmptyState/EmptyState.tsx new file mode 100644 index 00000000..a42c3da8 --- /dev/null +++ b/new-ui/src/shared/components/EmptyState/EmptyState.tsx @@ -0,0 +1,65 @@ +import { useMemo } from 'react'; +import './style.scss'; +import clsx from 'clsx'; +import { ThemeSpacing } from '../../types'; +import { isPresent } from '../../utils/isPresent'; +import { Button } from '../Button/Button'; +import { SizedBox } from '../SizedBox/SizedBox'; +import type { EmptyStateProps } from './types'; + +const Empty = () => { + return null; +}; + +export const EmptyState = ({ + ref, + icon, + primaryAction, + secondaryAction, + secondaryActionText, + subtitle, + title, + className, + id, + testId, +}: EmptyStateProps) => { + const RenderIcon = useMemo(() => { + if (!icon) return Empty; + return Empty; + }, [icon]); + + return ( +
+ {isPresent(icon) && ( + <> + + + + )} + {isPresent(title) && ( + <> +

{title}

+ + + )} + {isPresent(subtitle) &&

{subtitle}

} + + {isPresent(primaryAction) && ( + <> + + )} +
+ ); +}; diff --git a/new-ui/src/shared/components/EmptyState/style.scss b/new-ui/src/shared/components/EmptyState/style.scss new file mode 100644 index 00000000..90a3b596 --- /dev/null +++ b/new-ui/src/shared/components/EmptyState/style.scss @@ -0,0 +1,46 @@ +.empty-state { + display: flex; + flex-flow: column; + flex: none; + align-items: center; + justify-content: flex-start; + height: auto; + user-select: none; + + & > p, + & > span { + text-align: center; + } + + .title { + color: var(--fg-muted); + font: var(--t-body-primary-500); + } + + .subtitle { + color: var(--fg-muted); + font: var(--t-body-sm-400); + } + + .secondary-action { + background-color: transparent; + text-align: center; + color: var(--fg-action); + border: none; + padding: 0; + margin: 0; + } + + .empty-icon { + width: 40px; + height: 40px; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + flex: none; + padding: 4px; + border: 1px dashed var(--border-faded); + border-radius: 100px; + } +} diff --git a/new-ui/src/shared/components/EmptyState/types.ts b/new-ui/src/shared/components/EmptyState/types.ts new file mode 100644 index 00000000..7288f512 --- /dev/null +++ b/new-ui/src/shared/components/EmptyState/types.ts @@ -0,0 +1,15 @@ +import type { Ref } from 'react'; +import type { ButtonProps } from '../Button/types'; + +export type EmptyStateProps = { + ref?: Ref; + title?: string; + subtitle?: string; + icon?: string; + className?: string; + testId?: string; + id?: string; + primaryAction?: ButtonProps; + secondaryAction?: () => void; + secondaryActionText?: string; +}; diff --git a/new-ui/src/shared/components/EmptyStateFlexible/EmptyStateFlexible.tsx b/new-ui/src/shared/components/EmptyStateFlexible/EmptyStateFlexible.tsx new file mode 100644 index 00000000..4e47d97d --- /dev/null +++ b/new-ui/src/shared/components/EmptyStateFlexible/EmptyStateFlexible.tsx @@ -0,0 +1,35 @@ +import './style.scss'; +import { useWindowSize } from '@uidotdev/usehooks'; +import { useMemo, useRef } from 'react'; +import { EmptyState } from '../EmptyState/EmptyState'; +import type { EmptyStateProps } from '../EmptyState/types'; + +type Props = EmptyStateProps; + +export const EmptyStateFlexible = (props: Props) => { + const containerRef = useRef(null); + const windowHeight = useWindowSize().height; + + const initHeight = useMemo(() => { + if (!containerRef.current) return 0; + return window.innerHeight - containerRef.current.getBoundingClientRect().top; + }, []); + + const minHeight = useMemo(() => { + const container = containerRef.current; + if (!container || !windowHeight) return null; + return windowHeight - container.getBoundingClientRect().top; + }, [windowHeight]); + + return ( +
+ +
+ ); +}; diff --git a/new-ui/src/shared/components/EmptyStateFlexible/style.scss b/new-ui/src/shared/components/EmptyStateFlexible/style.scss new file mode 100644 index 00000000..8023b98d --- /dev/null +++ b/new-ui/src/shared/components/EmptyStateFlexible/style.scss @@ -0,0 +1,8 @@ +.flexible-empty-state { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + box-sizing: border-box; + padding: var(--spacing-2xl) 0; +} diff --git a/new-ui/src/shared/components/FieldBox/FieldBox.tsx b/new-ui/src/shared/components/FieldBox/FieldBox.tsx new file mode 100644 index 00000000..9a880157 --- /dev/null +++ b/new-ui/src/shared/components/FieldBox/FieldBox.tsx @@ -0,0 +1,58 @@ +import './style.scss'; +import clsx from 'clsx'; +import { isPresent } from '../../utils/isPresent'; +import { InteractionBox } from '../InteractionBox/InteractionBox'; +import type { FieldBoxProps } from './types'; + +// generalized field box for components like Input, shouldn't be in layout on it's own +export const FieldBox = ({ + children, + disabled, + error, + className, + boxRef, + interactionRef, + iconLeft, + iconRight, + size, + forceFocusState, + onInteractionClick, + reserveInteraction = false, + ...rest +}: FieldBoxProps) => { + const hasIconLeft = isPresent(iconLeft); + const hasIconRight = isPresent(iconRight) || reserveInteraction; + return ( +
+ {hasIconLeft && iconLeft} + {children} + {hasIconRight && ( + <> + {isPresent(iconRight) && ( + + {iconRight} + + )} + {!isPresent(iconRight) &&
} + + )} +
+ ); +}; diff --git a/new-ui/src/shared/components/FieldBox/style.scss b/new-ui/src/shared/components/FieldBox/style.scss new file mode 100644 index 00000000..8f69c369 --- /dev/null +++ b/new-ui/src/shared/components/FieldBox/style.scss @@ -0,0 +1,88 @@ +.field-box { + --border-color: var(--border-default); + --background-color: transparent; + + position: relative; + box-sizing: border-box; + display: grid; + grid-template-rows: 1fr; + align-items: center; + column-gap: var(--spacing-sm); + overflow: hidden; + cursor: pointer; + border: 1px solid var(--border-color); + border-radius: 8px; + padding: var(--spacing-sm) var(--spacing-md); + outline: none; + background-color: var(--background-color); + + @include animate(border-color, background-color); + + p, + span { + max-width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + &.size-default { + min-height: 36px; + + p, + span { + font: var(--t-input-text-primary); + color: var(--fg-white-100); + } + } + + &.grid-default { + grid-template-columns: 1fr; + } + + &.grid-left { + grid-template-columns: 20px 1fr; + } + + &.grid-right { + grid-template-columns: 1fr 20px; + } + + &.grid-both { + grid-template-columns: 20px 1fr 20px; + } + + .placeholder { + color: var(--fg-white-50); + font: var(--t-input-text-primary); + } + + .interaction-box { + & > button { + height: 28px; + width: 28px; + } + } + + &:not(.disabled, .error) { + &:hover { + --border-color: var(--border-emphasis); + } + + &:focus-within, + &.focus { + --border-color: var(--border-action); + } + } + + &.error { + --border-color: var(--border-critical); + } + + &.disabled { + --border-color: var(--border-disabled); + --background-color: var(--bg-white-10); + + cursor: not-allowed; + } +} diff --git a/new-ui/src/shared/components/FieldBox/types.ts b/new-ui/src/shared/components/FieldBox/types.ts new file mode 100644 index 00000000..5c8f117f --- /dev/null +++ b/new-ui/src/shared/components/FieldBox/types.ts @@ -0,0 +1,22 @@ +import type { + HTMLAttributes, + MouseEventHandler, + PropsWithChildren, + ReactNode, + Ref, +} from 'react'; + +export type FieldSize = 'lg' | 'default'; + +export interface FieldBoxProps extends HTMLAttributes, PropsWithChildren { + boxRef?: Ref; + interactionRef?: Ref; + error?: boolean; + disabled?: boolean; + iconLeft?: ReactNode; + iconRight?: ReactNode; + size?: FieldSize; + forceFocusState?: boolean; + onInteractionClick?: MouseEventHandler; + reserveInteraction?: boolean; +} diff --git a/new-ui/src/shared/components/FieldError/FieldError.tsx b/new-ui/src/shared/components/FieldError/FieldError.tsx new file mode 100644 index 00000000..0bb3e48c --- /dev/null +++ b/new-ui/src/shared/components/FieldError/FieldError.tsx @@ -0,0 +1,35 @@ +import { motion } from 'motion/react'; +import './style.scss'; +import { motionTransitionStandard } from '../../consts'; +import { isPresent } from '../../utils/isPresent'; + +type Props = { + error?: string | null; +}; + +export const FieldError = ({ error }: Props) => { + return ( + <> + {isPresent(error) && error.length > 0 && ( + + {error} + + )} + + ); +}; diff --git a/new-ui/src/shared/components/FieldError/style.scss b/new-ui/src/shared/components/FieldError/style.scss new file mode 100644 index 00000000..d606952e --- /dev/null +++ b/new-ui/src/shared/components/FieldError/style.scss @@ -0,0 +1,6 @@ +.field-error { + padding-top: 8px; + font: var(--t-input-error-message); + color: var(--bg-critical-muted); + user-select: none; +} diff --git a/new-ui/src/shared/components/FieldLabel/FieldLabel.tsx b/new-ui/src/shared/components/FieldLabel/FieldLabel.tsx new file mode 100644 index 00000000..3aadd31b --- /dev/null +++ b/new-ui/src/shared/components/FieldLabel/FieldLabel.tsx @@ -0,0 +1,42 @@ +import './style.scss'; + +import clsx from 'clsx'; +import type { MouseEventHandler, Ref } from 'react'; + +type Props = { + text: string; + id?: string; + ref?: Ref; + required?: boolean; + onClick?: MouseEventHandler; +}; + +export const FieldLabel = ({ text, ref, required, id, onClick }: Props) => { + return ( +
+ {required && ( + + + + )} + {text} +
+ ); +}; diff --git a/new-ui/src/shared/components/FieldLabel/style.scss b/new-ui/src/shared/components/FieldLabel/style.scss new file mode 100644 index 00000000..7647a723 --- /dev/null +++ b/new-ui/src/shared/components/FieldLabel/style.scss @@ -0,0 +1,33 @@ +.field-label { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-sm); + position: relative; + padding-bottom: var(--spacing-xs); + + span { + font: var(--t-input-title); + color: var(--fg-white-80); + } + + .required-icon { + user-select: none; + position: absolute; + left: 0; + top: 2px; + + path { + fill: var(--c-red); + } + } + + &.required { + padding-left: 8px; + } + + svg path { + fill: var(--fg-white-60); + } +} diff --git a/new-ui/src/shared/components/FloatingMenu/FloatingMenu.tsx b/new-ui/src/shared/components/FloatingMenu/FloatingMenu.tsx new file mode 100644 index 00000000..980b9456 --- /dev/null +++ b/new-ui/src/shared/components/FloatingMenu/FloatingMenu.tsx @@ -0,0 +1,14 @@ +import './style.scss'; +import clsx from 'clsx'; +import type { HTMLProps, PropsWithChildren } from 'react'; + +interface Props extends PropsWithChildren { + containerProps: HTMLProps; +} +export const FloatingMenu = ({ containerProps, children }: Props) => { + return ( +
+ {children} +
+ ); +}; diff --git a/new-ui/src/shared/components/FloatingMenu/style.scss b/new-ui/src/shared/components/FloatingMenu/style.scss new file mode 100644 index 00000000..efa24cdf --- /dev/null +++ b/new-ui/src/shared/components/FloatingMenu/style.scss @@ -0,0 +1,8 @@ +.floating-menu { + border-radius: 12px; + box-sizing: border-box; + padding: 8px; + background-color: var(--c-saturated-dark-blue-60); + box-shadow: 0 4px 12px 0 rgb(0 0 0 / 7%); + backdrop-filter: blur(12.5px); +} diff --git a/new-ui/src/shared/components/Fold/Fold.tsx b/new-ui/src/shared/components/Fold/Fold.tsx new file mode 100644 index 00000000..36278637 --- /dev/null +++ b/new-ui/src/shared/components/Fold/Fold.tsx @@ -0,0 +1,29 @@ +import './style.scss'; +import clsx from 'clsx'; +import type { HTMLAttributes, PropsWithChildren, Ref } from 'react'; + +export const Fold = ({ + ref, + className, + children, + open, + contentClassName, + ...rest +}: { + open: boolean; + ref?: Ref; + contentClassName?: string; +} & PropsWithChildren & + HTMLAttributes) => { + return ( +
+
{children}
+
+ ); +}; diff --git a/new-ui/src/shared/components/Fold/style.scss b/new-ui/src/shared/components/Fold/style.scss new file mode 100644 index 00000000..f11e4fc8 --- /dev/null +++ b/new-ui/src/shared/components/Fold/style.scss @@ -0,0 +1,19 @@ +.fold { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + + @include animate(grid-template-rows); + + &.folded { + grid-template-rows: 0fr; + } + + .fold-content { + overflow: hidden; + + // fix for content with single button in place + padding-bottom: 1px; + padding-left: 1px; + } +} diff --git a/new-ui/src/shared/components/Icon/Icon.tsx b/new-ui/src/shared/components/Icon/Icon.tsx new file mode 100644 index 00000000..73337ee7 --- /dev/null +++ b/new-ui/src/shared/components/Icon/Icon.tsx @@ -0,0 +1,492 @@ +import { type CSSProperties, type Ref, useMemo } from 'react'; +import type { IconKindValue } from './icon-types'; +import './style.scss'; +import clsx from 'clsx'; +import type { DirectionValue, ThemeVariableValue } from '../../types'; +import { isPresent } from '../../utils/isPresent'; +import { IconAccessSettings } from './icons/IconAccessSettings'; +import { IconActivity } from './icons/IconActivity'; +import { IconActivityNotes } from './icons/IconActivityNotes'; +import { IconAddAlias } from './icons/IconAddAlias'; +import { IconAddDevice } from './icons/IconAddDevice'; +import { IconAddGroup } from './icons/IconAddGroup'; +import { IconAddLocation } from './icons/IconAddLocation'; +import { IconAddRule } from './icons/IconAddRule'; +import { IconAddToken } from './icons/IconAddToken'; +import { IconAddUser } from './icons/IconAddUser'; +import { IconAliases } from './icons/IconAliases'; +import { IconAndroid } from './icons/IconAndroid'; +import { IconApple } from './icons/IconApple'; +import { IconAppStore } from './icons/IconAppstore'; +import { IconArchLinux } from './icons/IconArchLinux'; +import { IconArrowBig } from './icons/IconArrowBig'; +import { IconArrowSmall } from './icons/IconArrowSmall'; +import { IconAttentionFilled } from './icons/IconAttentionFilled'; +import { IconAttentionOutlined } from './icons/IconAttentionOutlined'; +import { IconAuthorisedApp } from './icons/IconAuthorisedApp'; +import { IconBiometric } from './icons/IconBiometric'; +import { IconBug } from './icons/IconBug'; +import { IconCalendar } from './icons/IconCalendar'; +import { IconChat } from './icons/IconChat'; +import { IconCheck } from './icons/IconCheck'; +import { IconCheckCircle } from './icons/IconCheckCircle'; +import { IconCheckFilled } from './icons/IconCheckFilled'; +import { IconClear } from './icons/IconClear'; +import { IconClose } from './icons/IconClose'; +import { IconCode } from './icons/IconCode'; +import { IconConfig } from './icons/IconConfig'; +import { IconConnectedDevices } from './icons/IconConnectedDevices'; +import { IconCopy } from './icons/IconCopy'; +import { IconCreditCard } from './icons/IconCreditCard'; +import { IconCustomize } from './icons/IconCustomize'; +import { IconDarkTheme } from './icons/IconDarkTheme'; +import { IconDebian } from './icons/IconDebian'; +import { IconDelete } from './icons/IconDelete'; +import { IconDeploy } from './icons/IconDeploy'; +import { IconDesktop } from './icons/IconDesktop'; +import { IconDevices } from './icons/IconDevices'; +import { IconDevicesActive } from './icons/IconDevicesActive'; +import { IconDisabled } from './icons/IconDisabled'; +import { IconDisableMfa } from './icons/IconDisableMfa'; +import { IconDisconnectAll } from './icons/IconDisconnectAll'; +import { IconDownload } from './icons/IconDownload'; +import { IconEdit } from './icons/IconEdit'; +import { IconEmptyPoint } from './icons/IconEmptyPoint'; +import { IconEnrollment } from './icons/IconEnrollment'; +import { IconEnter } from './icons/IconEnter'; +import { IconExternalMfa } from './icons/IconExternalMFA'; +import { IconFile } from './icons/IconFile'; +import { IconFileAdd } from './icons/IconFileAdd'; +import { IconFiltration } from './icons/IconFiltration'; +import { IconGateway } from './icons/IconGateway'; +import { IconGithub } from './icons/IconGithub'; +import { IconGlobe } from './icons/IconGlobe'; +import { IconGroups } from './icons/IconGroups'; +import { IconHamburger } from './icons/IconHamburger'; +import { IconHelp } from './icons/IconHelp'; +import { IconHide } from './icons/IconHide'; +import { IconInfoFilled } from './icons/IconInfoFilled'; +import { IconInfoOutlined } from './icons/IconInfoOutlined'; +import { IconInternalMfa } from './icons/IconInternalMFA'; +import { IconIpSuggest } from './icons/IconIpSuggest'; +import { IconKey } from './icons/IconKey'; +import { IconLightBulb } from './icons/IconLightBulb'; +import { IconLightTheme } from './icons/IconLightTheme'; +import { IconLinux } from './icons/IconLinux'; +import { IconLoader } from './icons/IconLoader'; +import { IconLocation } from './icons/IconLocation'; +import { IconLocationTracking } from './icons/IconLocationTracking'; +import { IconLockOpen } from './icons/IconLock'; +import { IconLockClosed } from './icons/IconLockClosed'; +import { IconLogout } from './icons/IconLogout'; +import { IconMail } from './icons/IconMail'; +import { IconMenu } from './icons/IconMenu'; +import { IconMinusCircle } from './icons/IconMinusCircle'; +import { IconMobile } from './icons/IconMobile'; +import { IconMobileLock } from './icons/IconMobileLock'; +import { IconNetworkSettings } from './icons/IconNetworkSettings'; +import { IconNotification } from './icons/IconNotification'; +import { IconOneTimePassword } from './icons/IconOneTimePassword'; +import { IconOnline } from './icons/IconOnline'; +import { IconOpenId } from './icons/IconOpenId'; +import { IconOpenInNewWindow } from './icons/IconOpenInNewWindow'; +import { IconPending } from './icons/IconPending'; +import { IconPieChart } from './icons/IconPieChart'; +import { IconPlay } from './icons/IconPlay'; +import { IconPlayFilled } from './icons/IconPlayFilled'; +import { IconPlus } from './icons/IconPlus'; +import { IconPlusCircle } from './icons/IconPlusCircle'; +import { IconProfile } from './icons/IconProfile'; +import { IconProtection } from './icons/IconProtection'; +import { IconRefresh } from './icons/IconRefresh'; +import { IconRequest } from './icons/IconRequest'; +import { IconRules } from './icons/IconRules'; +import { IconSearch } from './icons/IconSearch'; +import { IconServers } from './icons/IconServers'; +import { IconSettings } from './icons/IconSettings'; +import { IconShow } from './icons/IconShow'; +import { IconSortable } from './icons/IconSortable'; +import { IconStatusAttention } from './icons/IconStatusAttention'; +import { IconStatusAvailable } from './icons/IconStatusAvailable'; +import { IconStatusImportant } from './icons/IconStatusImportant'; +import { IconStatusPremium } from './icons/IconStatusPremium'; +import { IconStatusSimple } from './icons/IconStatusSimple'; +import { IconSupport } from './icons/IconSupport'; +import { IconSync } from './icons/IconSync'; +import { IconToken } from './icons/IconToken'; +import { IconTransactions } from './icons/IconTransactions'; +import { IconTutorial } from './icons/IconTutorial'; +import { IconTutorialNotAvailable } from './icons/IconTutorialNotAvailable'; +import { IconUbuntu } from './icons/IconUbuntu'; +import { IconUpload } from './icons/IconUpload'; +import { IconUser } from './icons/IconUser'; +import { IconUserActive } from './icons/IconUserActive'; +import { IconUsers } from './icons/IconUsers'; +import { IconWarningFilled } from './icons/IconWarningFilled'; +import { IconWarningOutlined } from './icons/IconWarningOutlined'; +import { IconWebhooks } from './icons/IconWebhooks'; +import { IconWindows } from './icons/IconWindows'; + +type Props = { + icon: T; + staticColor?: ThemeVariableValue; + size?: number; + rotationDirection?: DirectionValue; + customRotation?: number; + ref?: Ref; + className?: string; +}; + +type RotationMap = Record; + +const mapRotation = (kind: IconKindValue, direction: DirectionValue): number => { + switch (kind) { + case 'arrow-small': + case 'arrow-big': { + const map: RotationMap = { + down: 90, + right: 0, + up: -90, + left: 180, + }; + return map[direction]; + } + } + console.error(`Unimplemented rotation mapping for icon kind of ${kind}`); + // safe return for unimplemented + return 0; +}; + +const EmptyIcon = () => { + return null; +}; + +// Color should be set by css bcs some icons have different structures like 'loader' +export const Icon = ({ + icon: iconKind, + rotationDirection, + customRotation, + ref, + className, + staticColor, + size, +}: Props) => { + const IconToRender = useMemo(() => { + switch (iconKind) { + case 'mobile-lock': + return IconMobileLock; + case 'sync': + return IconSync; + case 'attention-filled': + return IconAttentionFilled; + case 'ip-suggest': + return IconIpSuggest; + case 'filtration': + return IconFiltration; + case 'rules': + return IconRules; + case 'add-rule': + return IconAddRule; + case 'add-alias': + return IconAddAlias; + case 'aliases': + return IconAliases; + case 'upload': + return IconUpload; + case 'lock-closed': + return IconLockClosed; + case 'enrollment': + return IconEnrollment; + case 'customize': + return IconCustomize; + case 'light-theme': + return IconLightTheme; + case 'dark-theme': + return IconDarkTheme; + case 'refresh': + return IconRefresh; + case 'network-settings': + return IconNetworkSettings; + case 'connected-devices': + return IconConnectedDevices; + case 'external-mfa': + return IconExternalMfa; + case 'internal-mfa': + return IconInternalMfa; + case 'token': + return IconToken; + case 'add-location': + return IconAddLocation; + case 'add-group': + return IconAddGroup; + case 'add-token': + return IconAddToken; + case 'online': + return IconOnline; + case 'key': + return IconKey; + case 'add-device': + return IconAddDevice; + case 'warning-filled': + return IconWarningFilled; + case 'warning-outlined': + return IconWarningOutlined; + case 'ubuntu': + return IconUbuntu; + case 'debian': + return IconDebian; + case 'arch-linux': + return IconArchLinux; + case 'disabled': + return IconDisabled; + case 'disable-mfa': + return IconDisableMfa; + case 'show': + return IconShow; + case 'hide': + return IconHide; + case 'copy': + return IconCopy; + case 'config': + return IconConfig; + case 'open-in-new-window': + return IconOpenInNewWindow; + case 'arrow-big': + return IconArrowBig; + case 'arrow-small': + return IconArrowSmall; + case 'loader': + return IconLoader; + case 'plus': + return IconPlus; + case 'status-simple': + return IconStatusSimple; + case 'lock-open': + return IconLockOpen; + case 'check-circle': + return IconCheckCircle; + case 'check-filled': + return IconCheckFilled; + case 'empty-point': + return IconEmptyPoint; + case 'desktop': + return IconDesktop; + case 'mobile': + return IconMobile; + case 'windows': + return IconWindows; + case 'linux': + return IconLinux; + case 'app-store': + return IconAppStore; + case 'apple': + return IconApple; + case 'android': + return IconAndroid; + case 'close': + return IconClose; + case 'file': + return IconFile; + case 'file-add': + return IconFileAdd; + case 'globe': + return IconGlobe; + case 'help': + return IconHelp; + case 'access-settings': + return IconAccessSettings; + case 'activity': + return IconActivity; + case 'activity-notes': + return IconActivityNotes; + case 'add-user': + return IconAddUser; + case 'analytics': + return EmptyIcon; + case 'archive': + return EmptyIcon; + case 'attention-outlined': + return IconAttentionOutlined; + case 'check': + return IconCheck; + case 'clear': + return IconClear; + case 'code': + return IconCode; + case 'collapse': + return EmptyIcon; + case 'credit-card': + return IconCreditCard; + case 'date': + return EmptyIcon; + case 'delete': + return IconDelete; + case 'deploy': + return IconDeploy; + case 'devices': + return IconDevices; + case 'devices-active': + return IconDevicesActive; + case 'download': + return IconDownload; + case 'edit': + return IconEdit; + case 'enter': + return IconEnter; + case 'expand': + return EmptyIcon; + case 'filter': + return EmptyIcon; + case 'gateway': + return IconGateway; + case 'gift': + return EmptyIcon; + case 'github': + return IconGithub; + case 'groups': + return IconGroups; + case 'hamburger': + return IconHamburger; + case 'info-filled': + return IconInfoFilled; + case 'info-outlined': + return IconInfoOutlined; + case 'location': + return IconLocation; + case 'location-preview': + return EmptyIcon; + case 'location-tracking': + return IconLocationTracking; + case 'logout': + return IconLogout; + case 'mail': + return IconMail; + case 'manage-keys': + return EmptyIcon; + case 'menu': + return IconMenu; + case 'minus-circle': + return IconMinusCircle; + case 'navigation-collapse': + return EmptyIcon; + case 'navigation-uncollapse': + return EmptyIcon; + case 'notification': + return IconNotification; + case 'one-time-password': + return IconOneTimePassword; + case 'openid': + return IconOpenId; + case 'pdf': + return EmptyIcon; + case 'pie-chart': + return IconPieChart; + case 'plus-circle': + return IconPlusCircle; + case 'profile': + return IconProfile; + case 'protection': + return IconProtection; + case 'qr': + return EmptyIcon; + case 'search': + return IconSearch; + case 'servers': + return IconServers; + case 'settings': + return IconSettings; + case 'sort': + return EmptyIcon; + case 'sortable': + return IconSortable; + case 'status-premium': + return IconStatusPremium; + case 'status-attention': + return IconStatusAttention; + case 'status-available': + return IconStatusAvailable; + case 'status-important': + return IconStatusImportant; + case 'support': + return IconSupport; + case 'transactions': + return IconTransactions; + case 'user': + return IconUser; + case 'user-active': + return IconUserActive; + case 'users': + return IconUsers; + case 'webhooks': + return IconWebhooks; + case 'yubi-keys': + return EmptyIcon; + case 'biometric': + return IconBiometric; + case 'pending': + return IconPending; + case 'bug': + return IconBug; + case 'chat': + return IconChat; + case 'request': + return IconRequest; + case 'calendar': + return IconCalendar; + case 'light-bulb': + return IconLightBulb; + case 'tutorial': + return IconTutorial; + case 'tutorial-not-available': + return IconTutorialNotAvailable; + case 'authorised-app': + return IconAuthorisedApp; + case 'play': + return IconPlay; + case 'play-filled': + return IconPlayFilled; + case 'disconnect-all': + return IconDisconnectAll; + } + }, [iconKind]); + + const getStyle = useMemo((): CSSProperties => { + const styles: CSSProperties = {}; + if (isPresent(staticColor)) { + // @ts-expect-error + styles['--icon-color'] = staticColor; + } + const transform: string[] = []; + // kind specific configurations + switch (iconKind) { + case 'arrow-big': + case 'arrow-small': + if (rotationDirection) { + transform.push(`rotate(${mapRotation(iconKind, rotationDirection)}deg)`); + } + break; + } + if (customRotation && !rotationDirection) { + transform.push(`rotate(${customRotation}deg)`); + } + if (size) { + styles.width = size; + styles.height = size; + } + if (transform.length) { + styles.transform = transform.join(' '); + } + return styles; + }, [iconKind, size, rotationDirection, customRotation, staticColor]); + + return ( +
+ +
+ ); +}; diff --git a/new-ui/src/shared/components/Icon/icon-types.ts b/new-ui/src/shared/components/Icon/icon-types.ts new file mode 100644 index 00000000..e3cff9a3 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icon-types.ts @@ -0,0 +1,141 @@ +export const IconKind = { + DisconnectAll: 'disconnect-all', + MobileLock: 'mobile-lock', + IpSuggest: 'ip-suggest', + Filtration: 'filtration', + AddAlias: 'add-alias', + Aliases: 'aliases', + Customize: 'customize', + NetworkSettings: 'network-settings', + AddGroup: 'add-group', + AddToken: 'add-token', + Key: 'key', + Biometric: 'biometric', + Hide: 'hide', + ArrowBig: 'arrow-big', + ArrowSmall: 'arrow-small', + PlusCircle: 'plus-circle', + MinusCircle: 'minus-circle', + Edit: 'edit', + Show: 'show', + Analytics: 'analytics', + Search: 'search', + Delete: 'delete', + Transactions: 'transactions', + Enrollment: 'enrollment', + Copy: 'copy', + Settings: 'settings', + Close: 'close', + Plus: 'plus', + Support: 'support', + Menu: 'menu', + Sync: 'sync', + Pending: 'pending', + Check: 'check', + Date: 'date', + CreditCard: 'credit-card', + Archive: 'archive', + PieChart: 'pie-chart', + Notification: 'notification', + Globe: 'globe', + Groups: 'groups', + OpenInNewWindow: 'open-in-new-window', + Users: 'users', + Mail: 'mail', + Filter: 'filter', + User: 'user', + LockOpen: 'lock-open', + LockClosed: 'lock-closed', + Servers: 'servers', + Protection: 'protection', + NavigationCollapse: 'navigation-collapse', + NavigationUncollapse: 'navigation-uncollapse', + Devices: 'devices', + Logout: 'logout', + YubiKeys: 'yubi-keys', + OpenId: 'openid', + Webhooks: 'webhooks', + Help: 'help', + ActivityNotes: 'activity-notes', + Activity: 'activity', + AccessSettings: 'access-settings', + Profile: 'profile', + AttentionOutlined: 'attention-outlined', + AttentionFilled: 'attention-filled', + WarningOutlined: 'warning-outlined', + Download: 'download', + Code: 'code', + Deploy: 'deploy', + Expand: 'expand', + Collapse: 'collapse', + CheckCircle: 'check-circle', + Location: 'location', + InfoOutlined: 'info-outlined', + InfoFilled: 'info-filled', + LocationPreview: 'location-preview', + AddUser: 'add-user', + QR: 'qr', + File: 'file', + FileAdd: 'file-add', + LocationTracking: 'location-tracking', + Config: 'config', + Gift: 'gift', + Hamburger: 'hamburger', + Sort: 'sort', + Sortable: 'sortable', + Gateway: 'gateway', + EmptyPoint: 'empty-point', + DevicesActive: 'devices-active', + UserActive: 'user-active', + Windows: 'windows', + AppStore: 'app-store', + Apple: 'apple', + Desktop: 'desktop', + Mobile: 'mobile', + Android: 'android', + Pdf: 'pdf', + Linux: 'linux', + Clear: 'clear', + CheckFilled: 'check-filled', + Enter: 'enter', + Github: 'github', + OneTimePassword: 'one-time-password', + Loader: 'loader', + ManageKeys: 'manage-keys', + StatusSimple: 'status-simple', + StatusAttention: 'status-attention', + StatusAvailable: 'status-available', + StatusImportant: 'status-important', + StatusPremium: 'status-premium', + Disabled: 'disabled', + ArchLinux: 'arch-linux', + Debian: 'debian', + Ubuntu: 'ubuntu', + AddDevice: 'add-device', + Token: 'token', + AddLocation: 'add-location', + InternalMFA: 'internal-mfa', + ExternalMFa: 'external-mfa', + ConnectedDevices: 'connected-devices', + Refresh: 'refresh', + Online: 'online', + LightTheme: 'light-theme', + DarkTheme: 'dark-theme', + WarningFilled: 'warning-filled', + Upload: 'upload', + AddRule: 'add-rule', + Rules: 'rules', + DisableMfa: 'disable-mfa', + Bug: 'bug', + Chat: 'chat', + Request: 'request', + Calendar: 'calendar', + LightBulb: 'light-bulb', + Tutorial: 'tutorial', + TutorialNotAvailable: 'tutorial-not-available', + AuthorisedApp: 'authorised-app', + Play: 'play', + PlayFilled: 'play-filled', +} as const; + +export type IconKindValue = (typeof IconKind)[keyof typeof IconKind]; diff --git a/new-ui/src/shared/components/Icon/icons/IconAccessSettings.tsx b/new-ui/src/shared/components/Icon/icons/IconAccessSettings.tsx new file mode 100644 index 00000000..5cb508a0 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAccessSettings.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAccessSettings = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconActivity.tsx b/new-ui/src/shared/components/Icon/icons/IconActivity.tsx new file mode 100644 index 00000000..28091c68 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconActivity.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconActivity = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconActivityNotes.tsx b/new-ui/src/shared/components/Icon/icons/IconActivityNotes.tsx new file mode 100644 index 00000000..37935b98 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconActivityNotes.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconActivityNotes = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddAlias.tsx b/new-ui/src/shared/components/Icon/icons/IconAddAlias.tsx new file mode 100644 index 00000000..3e50787b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddAlias.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddAlias = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddDevice.tsx b/new-ui/src/shared/components/Icon/icons/IconAddDevice.tsx new file mode 100644 index 00000000..5a77a88c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddDevice.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddDevice = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddGroup.tsx b/new-ui/src/shared/components/Icon/icons/IconAddGroup.tsx new file mode 100644 index 00000000..61585b2f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddGroup.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddGroup = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddLocation.tsx b/new-ui/src/shared/components/Icon/icons/IconAddLocation.tsx new file mode 100644 index 00000000..cb009a8d --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddLocation.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddLocation = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddRule.tsx b/new-ui/src/shared/components/Icon/icons/IconAddRule.tsx new file mode 100644 index 00000000..4a227329 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddRule.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddRule = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddToken.tsx b/new-ui/src/shared/components/Icon/icons/IconAddToken.tsx new file mode 100644 index 00000000..e1017c5e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddToken.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddToken = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAddUser.tsx b/new-ui/src/shared/components/Icon/icons/IconAddUser.tsx new file mode 100644 index 00000000..d9072e44 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAddUser.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAddUser = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAliases.tsx b/new-ui/src/shared/components/Icon/icons/IconAliases.tsx new file mode 100644 index 00000000..1e68e1ac --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAliases.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconAliases = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAndroid.tsx b/new-ui/src/shared/components/Icon/icons/IconAndroid.tsx new file mode 100644 index 00000000..bf37eaa5 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAndroid.tsx @@ -0,0 +1,31 @@ +import type { SVGProps } from 'react'; + +export const IconAndroid = (props: SVGProps) => { + return ( + + + + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconApple.tsx b/new-ui/src/shared/components/Icon/icons/IconApple.tsx new file mode 100644 index 00000000..d0bb87ae --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconApple.tsx @@ -0,0 +1,23 @@ +import type { SVGProps } from 'react'; + +export const IconApple = (props: SVGProps) => { + return ( + + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAppstore.tsx b/new-ui/src/shared/components/Icon/icons/IconAppstore.tsx new file mode 100644 index 00000000..1f75861a --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAppstore.tsx @@ -0,0 +1,47 @@ +import type { SVGProps } from 'react'; + +export const IconAppStore = (props: SVGProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconArchLinux.tsx b/new-ui/src/shared/components/Icon/icons/IconArchLinux.tsx new file mode 100644 index 00000000..ab1de5be --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconArchLinux.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconArchLinux = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconArrowBig.tsx b/new-ui/src/shared/components/Icon/icons/IconArrowBig.tsx new file mode 100644 index 00000000..3d80a851 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconArrowBig.tsx @@ -0,0 +1,16 @@ +import type { SVGProps } from 'react'; + +export const IconArrowBig = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconArrowSmall.tsx b/new-ui/src/shared/components/Icon/icons/IconArrowSmall.tsx new file mode 100644 index 00000000..4092fa37 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconArrowSmall.tsx @@ -0,0 +1,20 @@ +import type { SVGProps } from 'react'; + +export const IconArrowSmall = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAttentionFilled.tsx b/new-ui/src/shared/components/Icon/icons/IconAttentionFilled.tsx new file mode 100644 index 00000000..875a0720 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAttentionFilled.tsx @@ -0,0 +1,16 @@ +export const IconAttentionFilled = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAttentionOutlined.tsx b/new-ui/src/shared/components/Icon/icons/IconAttentionOutlined.tsx new file mode 100644 index 00000000..fd3ed62a --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAttentionOutlined.tsx @@ -0,0 +1,16 @@ +export const IconAttentionOutlined = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconAuthorisedApp.tsx b/new-ui/src/shared/components/Icon/icons/IconAuthorisedApp.tsx new file mode 100644 index 00000000..d1994d0a --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconAuthorisedApp.tsx @@ -0,0 +1,16 @@ +export const IconAuthorisedApp = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconBiometric.tsx b/new-ui/src/shared/components/Icon/icons/IconBiometric.tsx new file mode 100644 index 00000000..f8421958 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconBiometric.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconBiometric = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconBug.tsx b/new-ui/src/shared/components/Icon/icons/IconBug.tsx new file mode 100644 index 00000000..dab4f86c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconBug.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconBug = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCalendar.tsx b/new-ui/src/shared/components/Icon/icons/IconCalendar.tsx new file mode 100644 index 00000000..d3078da9 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCalendar.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCalendar = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconChat.tsx b/new-ui/src/shared/components/Icon/icons/IconChat.tsx new file mode 100644 index 00000000..2b3a636b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconChat.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconChat = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCheck.tsx b/new-ui/src/shared/components/Icon/icons/IconCheck.tsx new file mode 100644 index 00000000..580b0728 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCheck.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCheck = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCheckCircle.tsx b/new-ui/src/shared/components/Icon/icons/IconCheckCircle.tsx new file mode 100644 index 00000000..9a94792c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCheckCircle.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCheckCircle = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCheckFilled.tsx b/new-ui/src/shared/components/Icon/icons/IconCheckFilled.tsx new file mode 100644 index 00000000..0fc9b090 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCheckFilled.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCheckFilled = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconClear.tsx b/new-ui/src/shared/components/Icon/icons/IconClear.tsx new file mode 100644 index 00000000..4cda92d4 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconClear.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconClear = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconClose.tsx b/new-ui/src/shared/components/Icon/icons/IconClose.tsx new file mode 100644 index 00000000..cb8e9d1a --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconClose.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconClose = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCode.tsx b/new-ui/src/shared/components/Icon/icons/IconCode.tsx new file mode 100644 index 00000000..b0b76370 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCode.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCode = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconConfig.tsx b/new-ui/src/shared/components/Icon/icons/IconConfig.tsx new file mode 100644 index 00000000..12886c2f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconConfig.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconConfig = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconConnectedDevices.tsx b/new-ui/src/shared/components/Icon/icons/IconConnectedDevices.tsx new file mode 100644 index 00000000..d87efe7f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconConnectedDevices.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconConnectedDevices = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCopy.tsx b/new-ui/src/shared/components/Icon/icons/IconCopy.tsx new file mode 100644 index 00000000..eb5706fa --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCopy.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCopy = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCreditCard.tsx b/new-ui/src/shared/components/Icon/icons/IconCreditCard.tsx new file mode 100644 index 00000000..e2004ac3 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCreditCard.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCreditCard = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconCustomize.tsx b/new-ui/src/shared/components/Icon/icons/IconCustomize.tsx new file mode 100644 index 00000000..6bbbbf2b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconCustomize.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconCustomize = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDarkTheme.tsx b/new-ui/src/shared/components/Icon/icons/IconDarkTheme.tsx new file mode 100644 index 00000000..9ec3eb40 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDarkTheme.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDarkTheme = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDebian.tsx b/new-ui/src/shared/components/Icon/icons/IconDebian.tsx new file mode 100644 index 00000000..74295c5b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDebian.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDebian = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDelete.tsx b/new-ui/src/shared/components/Icon/icons/IconDelete.tsx new file mode 100644 index 00000000..3d7b881a --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDelete.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDelete = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDeploy.tsx b/new-ui/src/shared/components/Icon/icons/IconDeploy.tsx new file mode 100644 index 00000000..d5e20fe4 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDeploy.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDeploy = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDesktop.tsx b/new-ui/src/shared/components/Icon/icons/IconDesktop.tsx new file mode 100644 index 00000000..335b047c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDesktop.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDesktop = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDevices.tsx b/new-ui/src/shared/components/Icon/icons/IconDevices.tsx new file mode 100644 index 00000000..ec5bd61c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDevices.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDevices = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDevicesActive.tsx b/new-ui/src/shared/components/Icon/icons/IconDevicesActive.tsx new file mode 100644 index 00000000..2bbe756f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDevicesActive.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDevicesActive = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDisableMfa.tsx b/new-ui/src/shared/components/Icon/icons/IconDisableMfa.tsx new file mode 100644 index 00000000..7e823b7b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDisableMfa.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDisableMfa = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDisabled.tsx b/new-ui/src/shared/components/Icon/icons/IconDisabled.tsx new file mode 100644 index 00000000..33bb4931 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDisabled.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDisabled = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDisconnectAll.tsx b/new-ui/src/shared/components/Icon/icons/IconDisconnectAll.tsx new file mode 100644 index 00000000..14b80ec6 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDisconnectAll.tsx @@ -0,0 +1,16 @@ +export const IconDisconnectAll = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconDownload.tsx b/new-ui/src/shared/components/Icon/icons/IconDownload.tsx new file mode 100644 index 00000000..e822c647 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconDownload.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconDownload = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconEdit.tsx b/new-ui/src/shared/components/Icon/icons/IconEdit.tsx new file mode 100644 index 00000000..021da95e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconEdit.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconEdit = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconEmptyPoint.tsx b/new-ui/src/shared/components/Icon/icons/IconEmptyPoint.tsx new file mode 100644 index 00000000..a5b5323b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconEmptyPoint.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconEmptyPoint = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconEnrollment.tsx b/new-ui/src/shared/components/Icon/icons/IconEnrollment.tsx new file mode 100644 index 00000000..b0454e35 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconEnrollment.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconEnrollment = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconEnter.tsx b/new-ui/src/shared/components/Icon/icons/IconEnter.tsx new file mode 100644 index 00000000..c8b375a8 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconEnter.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconEnter = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconExternalMFA.tsx b/new-ui/src/shared/components/Icon/icons/IconExternalMFA.tsx new file mode 100644 index 00000000..e30fdbc0 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconExternalMFA.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconExternalMfa = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconFile.tsx b/new-ui/src/shared/components/Icon/icons/IconFile.tsx new file mode 100644 index 00000000..0f4266cf --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconFile.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconFile = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconFileAdd.tsx b/new-ui/src/shared/components/Icon/icons/IconFileAdd.tsx new file mode 100644 index 00000000..7c92896f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconFileAdd.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconFileAdd = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconFiltration.tsx b/new-ui/src/shared/components/Icon/icons/IconFiltration.tsx new file mode 100644 index 00000000..e3b64c3c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconFiltration.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconFiltration = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconGateway.tsx b/new-ui/src/shared/components/Icon/icons/IconGateway.tsx new file mode 100644 index 00000000..46348328 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconGateway.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react'; + +export const IconGateway = (_props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconGithub.tsx b/new-ui/src/shared/components/Icon/icons/IconGithub.tsx new file mode 100644 index 00000000..fc901b72 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconGithub.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const IconGithub = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconGlobe.tsx b/new-ui/src/shared/components/Icon/icons/IconGlobe.tsx new file mode 100644 index 00000000..c7d51e12 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconGlobe.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconGlobe = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconGroups.tsx b/new-ui/src/shared/components/Icon/icons/IconGroups.tsx new file mode 100644 index 00000000..376b25f4 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconGroups.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconGroups = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconHamburger.tsx b/new-ui/src/shared/components/Icon/icons/IconHamburger.tsx new file mode 100644 index 00000000..5c54f742 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconHamburger.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconHamburger = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconHelp.tsx b/new-ui/src/shared/components/Icon/icons/IconHelp.tsx new file mode 100644 index 00000000..60247038 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconHelp.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconHelp = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconHide.tsx b/new-ui/src/shared/components/Icon/icons/IconHide.tsx new file mode 100644 index 00000000..3dbe44a0 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconHide.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconHide = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconInfoFilled.tsx b/new-ui/src/shared/components/Icon/icons/IconInfoFilled.tsx new file mode 100644 index 00000000..38343d48 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconInfoFilled.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconInfoFilled = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconInfoOutlined.tsx b/new-ui/src/shared/components/Icon/icons/IconInfoOutlined.tsx new file mode 100644 index 00000000..ea68ad98 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconInfoOutlined.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconInfoOutlined = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconInternalMFA.tsx b/new-ui/src/shared/components/Icon/icons/IconInternalMFA.tsx new file mode 100644 index 00000000..4bd77066 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconInternalMFA.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconInternalMfa = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconIpSuggest.tsx b/new-ui/src/shared/components/Icon/icons/IconIpSuggest.tsx new file mode 100644 index 00000000..0ca67dce --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconIpSuggest.tsx @@ -0,0 +1,16 @@ +export const IconIpSuggest = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconKey.tsx b/new-ui/src/shared/components/Icon/icons/IconKey.tsx new file mode 100644 index 00000000..774c8d06 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconKey.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconKey = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLightBulb.tsx b/new-ui/src/shared/components/Icon/icons/IconLightBulb.tsx new file mode 100644 index 00000000..097c646e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLightBulb.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconLightBulb = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLightTheme.tsx b/new-ui/src/shared/components/Icon/icons/IconLightTheme.tsx new file mode 100644 index 00000000..b1fb5827 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLightTheme.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconLightTheme = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLinux.tsx b/new-ui/src/shared/components/Icon/icons/IconLinux.tsx new file mode 100644 index 00000000..f0a86fb3 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLinux.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; + +export const IconLinux = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLoader.tsx b/new-ui/src/shared/components/Icon/icons/IconLoader.tsx new file mode 100644 index 00000000..117773e4 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLoader.tsx @@ -0,0 +1,17 @@ +export const IconLoader = () => { + return ( + + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLocation.tsx b/new-ui/src/shared/components/Icon/icons/IconLocation.tsx new file mode 100644 index 00000000..fa792b7d --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLocation.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconLocation = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLocationTracking.tsx b/new-ui/src/shared/components/Icon/icons/IconLocationTracking.tsx new file mode 100644 index 00000000..f35049cc --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLocationTracking.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconLocationTracking = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLock.tsx b/new-ui/src/shared/components/Icon/icons/IconLock.tsx new file mode 100644 index 00000000..9bd4ef7f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLock.tsx @@ -0,0 +1,16 @@ +export const IconLockOpen = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLockClosed.tsx b/new-ui/src/shared/components/Icon/icons/IconLockClosed.tsx new file mode 100644 index 00000000..3ad83281 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLockClosed.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconLockClosed = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconLogout.tsx b/new-ui/src/shared/components/Icon/icons/IconLogout.tsx new file mode 100644 index 00000000..675ddd67 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconLogout.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconLogout = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconMail.tsx b/new-ui/src/shared/components/Icon/icons/IconMail.tsx new file mode 100644 index 00000000..1c2b8cfb --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconMail.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconMail = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconMenu.tsx b/new-ui/src/shared/components/Icon/icons/IconMenu.tsx new file mode 100644 index 00000000..2ca7cd47 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconMenu.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconMenu = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconMinusCircle.tsx b/new-ui/src/shared/components/Icon/icons/IconMinusCircle.tsx new file mode 100644 index 00000000..14285d6c --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconMinusCircle.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconMinusCircle = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconMobile.tsx b/new-ui/src/shared/components/Icon/icons/IconMobile.tsx new file mode 100644 index 00000000..a9a3c591 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconMobile.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconMobile = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconMobileLock.tsx b/new-ui/src/shared/components/Icon/icons/IconMobileLock.tsx new file mode 100644 index 00000000..cb4c4589 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconMobileLock.tsx @@ -0,0 +1,16 @@ +export const IconMobileLock = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconNetworkSettings.tsx b/new-ui/src/shared/components/Icon/icons/IconNetworkSettings.tsx new file mode 100644 index 00000000..f7a4b65b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconNetworkSettings.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconNetworkSettings = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconNotification.tsx b/new-ui/src/shared/components/Icon/icons/IconNotification.tsx new file mode 100644 index 00000000..bd0c947e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconNotification.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconNotification = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconOneTimePassword.tsx b/new-ui/src/shared/components/Icon/icons/IconOneTimePassword.tsx new file mode 100644 index 00000000..a138f912 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconOneTimePassword.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconOneTimePassword = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconOnline.tsx b/new-ui/src/shared/components/Icon/icons/IconOnline.tsx new file mode 100644 index 00000000..0e63d6a8 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconOnline.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconOnline = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconOpenId.tsx b/new-ui/src/shared/components/Icon/icons/IconOpenId.tsx new file mode 100644 index 00000000..c23dccb5 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconOpenId.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconOpenId = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconOpenInNewWindow.tsx b/new-ui/src/shared/components/Icon/icons/IconOpenInNewWindow.tsx new file mode 100644 index 00000000..69514713 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconOpenInNewWindow.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconOpenInNewWindow = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconPending.tsx b/new-ui/src/shared/components/Icon/icons/IconPending.tsx new file mode 100644 index 00000000..eb0e6e28 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconPending.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconPending = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconPieChart.tsx b/new-ui/src/shared/components/Icon/icons/IconPieChart.tsx new file mode 100644 index 00000000..5b57078e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconPieChart.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconPieChart = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconPlay.tsx b/new-ui/src/shared/components/Icon/icons/IconPlay.tsx new file mode 100644 index 00000000..5f510946 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconPlay.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react'; + +export const IconPlay = (_props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconPlayFilled.tsx b/new-ui/src/shared/components/Icon/icons/IconPlayFilled.tsx new file mode 100644 index 00000000..eab19451 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconPlayFilled.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react'; + +export const IconPlayFilled = (_props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconPlus.tsx b/new-ui/src/shared/components/Icon/icons/IconPlus.tsx new file mode 100644 index 00000000..b375db67 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconPlus.tsx @@ -0,0 +1,16 @@ +import type { SVGProps } from 'react'; + +export const IconPlus = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconPlusCircle.tsx b/new-ui/src/shared/components/Icon/icons/IconPlusCircle.tsx new file mode 100644 index 00000000..0bd3e2af --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconPlusCircle.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconPlusCircle = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconProfile.tsx b/new-ui/src/shared/components/Icon/icons/IconProfile.tsx new file mode 100644 index 00000000..1aea6212 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconProfile.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconProfile = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconProtection.tsx b/new-ui/src/shared/components/Icon/icons/IconProtection.tsx new file mode 100644 index 00000000..11d0c0ce --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconProtection.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconProtection = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconRefresh.tsx b/new-ui/src/shared/components/Icon/icons/IconRefresh.tsx new file mode 100644 index 00000000..782f7958 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconRefresh.tsx @@ -0,0 +1,28 @@ +import { type SVGProps, useId } from 'react'; + +export const IconRefresh = (props: SVGProps) => { + const id = useId(); + + return ( + + + + + + + + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconRequest.tsx b/new-ui/src/shared/components/Icon/icons/IconRequest.tsx new file mode 100644 index 00000000..ea84919e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconRequest.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconRequest = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconRules.tsx b/new-ui/src/shared/components/Icon/icons/IconRules.tsx new file mode 100644 index 00000000..8137f5e7 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconRules.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconRules = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconSearch.tsx b/new-ui/src/shared/components/Icon/icons/IconSearch.tsx new file mode 100644 index 00000000..8d51c753 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconSearch.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconSearch = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconServers.tsx b/new-ui/src/shared/components/Icon/icons/IconServers.tsx new file mode 100644 index 00000000..47f90f5f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconServers.tsx @@ -0,0 +1,16 @@ +export const IconServers = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconSettings.tsx b/new-ui/src/shared/components/Icon/icons/IconSettings.tsx new file mode 100644 index 00000000..e0408ebd --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconSettings.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconSettings = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconShow.tsx b/new-ui/src/shared/components/Icon/icons/IconShow.tsx new file mode 100644 index 00000000..54ee682f --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconShow.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconShow = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconSortable.tsx b/new-ui/src/shared/components/Icon/icons/IconSortable.tsx new file mode 100644 index 00000000..47efe023 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconSortable.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconSortable = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconStatusAttention.tsx b/new-ui/src/shared/components/Icon/icons/IconStatusAttention.tsx new file mode 100644 index 00000000..48ad14fb --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconStatusAttention.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconStatusAttention = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconStatusAvailable.tsx b/new-ui/src/shared/components/Icon/icons/IconStatusAvailable.tsx new file mode 100644 index 00000000..c2c6ebb3 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconStatusAvailable.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconStatusAvailable = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconStatusImportant.tsx b/new-ui/src/shared/components/Icon/icons/IconStatusImportant.tsx new file mode 100644 index 00000000..8c1cd791 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconStatusImportant.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconStatusImportant = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconStatusPremium.tsx b/new-ui/src/shared/components/Icon/icons/IconStatusPremium.tsx new file mode 100644 index 00000000..cceac144 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconStatusPremium.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconStatusPremium = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconStatusSimple.tsx b/new-ui/src/shared/components/Icon/icons/IconStatusSimple.tsx new file mode 100644 index 00000000..8ca913e8 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconStatusSimple.tsx @@ -0,0 +1,16 @@ +import type { SVGProps } from 'react'; + +export const IconStatusSimple = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconSupport.tsx b/new-ui/src/shared/components/Icon/icons/IconSupport.tsx new file mode 100644 index 00000000..edfea7fd --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconSupport.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconSupport = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconSync.tsx b/new-ui/src/shared/components/Icon/icons/IconSync.tsx new file mode 100644 index 00000000..1383a5fb --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconSync.tsx @@ -0,0 +1,16 @@ +export const IconSync = () => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconToken.tsx b/new-ui/src/shared/components/Icon/icons/IconToken.tsx new file mode 100644 index 00000000..33e2572e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconToken.tsx @@ -0,0 +1,27 @@ +import { type SVGProps, useId } from 'react'; + +export const IconToken = (props: SVGProps) => { + const id = useId(); + return ( + + + + + + + + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconTransactions.tsx b/new-ui/src/shared/components/Icon/icons/IconTransactions.tsx new file mode 100644 index 00000000..0b3039c4 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconTransactions.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconTransactions = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconTutorial.tsx b/new-ui/src/shared/components/Icon/icons/IconTutorial.tsx new file mode 100644 index 00000000..387953fd --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconTutorial.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react'; + +export const IconTutorial = (_props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconTutorialNotAvailable.tsx b/new-ui/src/shared/components/Icon/icons/IconTutorialNotAvailable.tsx new file mode 100644 index 00000000..dccae3c6 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconTutorialNotAvailable.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react'; + +export const IconTutorialNotAvailable = (_props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconUbuntu.tsx b/new-ui/src/shared/components/Icon/icons/IconUbuntu.tsx new file mode 100644 index 00000000..4279ff44 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconUbuntu.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconUbuntu = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconUpload.tsx b/new-ui/src/shared/components/Icon/icons/IconUpload.tsx new file mode 100644 index 00000000..b7760ba6 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconUpload.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconUpload = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconUser.tsx b/new-ui/src/shared/components/Icon/icons/IconUser.tsx new file mode 100644 index 00000000..8658cba7 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconUser.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconUser = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconUserActive.tsx b/new-ui/src/shared/components/Icon/icons/IconUserActive.tsx new file mode 100644 index 00000000..b2753171 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconUserActive.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconUserActive = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconUsers.tsx b/new-ui/src/shared/components/Icon/icons/IconUsers.tsx new file mode 100644 index 00000000..a8dbcd0d --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconUsers.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconUsers = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconWarningFilled.tsx b/new-ui/src/shared/components/Icon/icons/IconWarningFilled.tsx new file mode 100644 index 00000000..ba4a994b --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconWarningFilled.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconWarningFilled = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconWarningOutlined.tsx b/new-ui/src/shared/components/Icon/icons/IconWarningOutlined.tsx new file mode 100644 index 00000000..12dd4dca --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconWarningOutlined.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconWarningOutlined = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconWebhooks.tsx b/new-ui/src/shared/components/Icon/icons/IconWebhooks.tsx new file mode 100644 index 00000000..b47e332e --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconWebhooks.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconWebhooks = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/icons/IconWindows.tsx b/new-ui/src/shared/components/Icon/icons/IconWindows.tsx new file mode 100644 index 00000000..773876cc --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconWindows.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from 'react'; + +export const IconWindows = (props: SVGProps) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/Icon/index.ts b/new-ui/src/shared/components/Icon/index.ts new file mode 100644 index 00000000..a9ebdc3e --- /dev/null +++ b/new-ui/src/shared/components/Icon/index.ts @@ -0,0 +1,3 @@ +export { Icon } from './Icon'; +export type { IconKindValue } from './icon-types'; +export { IconKind } from './icon-types'; diff --git a/new-ui/src/shared/components/Icon/style.scss b/new-ui/src/shared/components/Icon/style.scss new file mode 100644 index 00000000..6a81db60 --- /dev/null +++ b/new-ui/src/shared/components/Icon/style.scss @@ -0,0 +1,41 @@ +.icon { + display: inline-block; + overflow: hidden; + user-select: none; + transition-property: transform; + width: var(--icon-size); + height: var(--icon-size); + + @include animate; + + svg { + width: inherit; + height: inherit; + + path { + @include animate(fill); + } + + circle { + @include animate(stroke); + } + } +} + +.icon svg { + path { + fill: var(--c-white-100); + } + + circle { + stroke: var(--c-white-100); + } +} + +.icon[style*='--icon-color'] svg path { + fill: var(--icon-color); +} + +.icon[style*='--icon-color'] svg circle { + stroke: var(--icon-color); +} diff --git a/new-ui/src/shared/components/IconButton/IconButton.tsx b/new-ui/src/shared/components/IconButton/IconButton.tsx new file mode 100644 index 00000000..3ae3800a --- /dev/null +++ b/new-ui/src/shared/components/IconButton/IconButton.tsx @@ -0,0 +1,26 @@ +import './style.scss'; +import clsx from 'clsx'; +import { Icon } from '../Icon/Icon'; +import { type IconButtonProps, IconButtonVariant } from './types'; + +export const IconButton = ({ + icon, + ref, + iconRotation, + className, + variant = IconButtonVariant.Big, + onClick, +}: IconButtonProps) => { + return ( +
{ + onClick?.(e); + }} + role="button" + > + +
+ ); +}; diff --git a/new-ui/src/shared/components/IconButton/style.scss b/new-ui/src/shared/components/IconButton/style.scss new file mode 100644 index 00000000..1182e367 --- /dev/null +++ b/new-ui/src/shared/components/IconButton/style.scss @@ -0,0 +1,76 @@ +.icon-button { + --size: 36px; + --bg: transparent; + --icon: var(--c-white-80); + --icon-size: 20px; + + border-radius: 8px; + height: var(--size); + width: var(--size); + display: inline-flex; + flex-flow: column; + align-items: center; + justify-content: center; + background: var(--bg); + cursor: pointer; + + @include animate(background); + + svg { + --icon-color: var(--icon); + } + + &.variant { + &-big, + &-big-selected { + --size: 36px; + --icon-size: 20px; + } + + &-small, + &-small-selected { + --size: 24px; + --icon-size: 16px; + } + + &-big { + --bg: transparent; + --icon: var(--c-white-80); + + &:hover { + --bg: var(--c-white-5); + --icon: var(--c-white-100); + } + } + + &-big-selected { + --bg: var(--c-white-10); + --icon: var(--c-white-100); + + &:hover { + --bg: var(--c-white-20); + --icon: var(--c-white-100); + } + } + + &-small { + --bg: var(--c-white-5); + --icon: var(--c-white-80); + + &:hover { + --bg: var(--c-white-10); + --icon: var(--c-white-100); + } + } + + &-small-selected { + --bg: var(--c-white-10); + --icon: var(--c-white-100); + + &:hover { + --bg: var(--c-white-20); + --icon: var(--c-white-100); + } + } + } +} diff --git a/new-ui/src/shared/components/IconButton/types.ts b/new-ui/src/shared/components/IconButton/types.ts new file mode 100644 index 00000000..6b1efaf3 --- /dev/null +++ b/new-ui/src/shared/components/IconButton/types.ts @@ -0,0 +1,22 @@ +import type { MouseEventHandler, Ref } from 'react'; +import type { DirectionValue } from '../../types'; +import type { IconKindValue } from '../Icon/icon-types'; + +export const IconButtonVariant = { + Big: 'big', + BigSelected: 'big-selected', + Small: 'small', + SmallSelected: 'small-selected', +} as const; + +export type IconButtonVariantValue = + (typeof IconButtonVariant)[keyof typeof IconButtonVariant]; + +export type IconButtonProps = { + variant: IconButtonVariantValue; + icon: IconKindValue; + iconRotation?: DirectionValue; + ref?: Ref; + className?: string; + onClick?: MouseEventHandler; +}; diff --git a/new-ui/src/shared/components/InteractionBox/InteractionBox.tsx b/new-ui/src/shared/components/InteractionBox/InteractionBox.tsx new file mode 100644 index 00000000..b29b64b7 --- /dev/null +++ b/new-ui/src/shared/components/InteractionBox/InteractionBox.tsx @@ -0,0 +1,47 @@ +import { type MouseEventHandler, type PropsWithChildren, type Ref, useMemo } from 'react'; +import './style.scss'; +import clsx from 'clsx'; + +type Props = { + interactionSize?: number; + onClick?: MouseEventHandler; + id?: string; + className?: string; + ref?: Ref; + disabled?: boolean; + tabIndex?: number; +}; + +export const InteractionBox = ({ + onClick, + className, + id, + ref, + tabIndex, + interactionSize, + disabled = false, + children, +}: Props & PropsWithChildren) => { + const style = useMemo(() => { + const res: Record = {}; + if (interactionSize) { + res['--interaction-size'] = `${interactionSize}px`; + } + return res; + }, [interactionSize]); + + return ( +
+ {children} + +
+ ); +}; diff --git a/new-ui/src/shared/components/InteractionBox/style.scss b/new-ui/src/shared/components/InteractionBox/style.scss new file mode 100644 index 00000000..18f2694d --- /dev/null +++ b/new-ui/src/shared/components/InteractionBox/style.scss @@ -0,0 +1,31 @@ +.interaction-box { + --interaction-size: 36px; + + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + flex: none; + position: relative; + user-select: none; + + & > button { + display: block; + position: absolute; + content: ' '; + width: var(--interaction-size); + height: var(--interaction-size); + background-color: transparent; + border: none; + cursor: pointer; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: 0; + margin: 0; + + &:disabled { + cursor: not-allowed; + } + } +} diff --git a/new-ui/src/shared/components/LoaderSpinner/LoaderSpinner.tsx b/new-ui/src/shared/components/LoaderSpinner/LoaderSpinner.tsx new file mode 100644 index 00000000..20e2bbab --- /dev/null +++ b/new-ui/src/shared/components/LoaderSpinner/LoaderSpinner.tsx @@ -0,0 +1,24 @@ +import clsx from 'clsx'; +import { Icon } from '../Icon'; +import './style.scss'; +import { useMemo } from 'react'; + +type Props = { + size?: number; + variant?: 'empty' | 'primary'; +}; + +export const LoaderSpinner = ({ size = 20, variant }: Props) => { + const variantClass = useMemo(() => (variant ? `variant-${variant}` : null), [variant]); + return ( +
+ +
+ ); +}; diff --git a/new-ui/src/shared/components/LoaderSpinner/style.scss b/new-ui/src/shared/components/LoaderSpinner/style.scss new file mode 100644 index 00000000..c538dc82 --- /dev/null +++ b/new-ui/src/shared/components/LoaderSpinner/style.scss @@ -0,0 +1,38 @@ +.loader-spinner { + display: inline-block; + user-select: none; + + &.variant-primary { + --spinner-track: var(--c-white-30); + --spinner-indicator: var(--c-white-100); + } + + & > .icon { + animation: spin 1s ease-in-out infinite; + } + + svg { + & > path { + stroke: var(--spinner-indicator); + fill: unset !important; + + @include animate(stroke); + } + + & > circle { + stroke: var(--spinner-track); + + @include animate(stroke); + } + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/new-ui/src/shared/components/LocationCard/LocationCard.tsx b/new-ui/src/shared/components/LocationCard/LocationCard.tsx new file mode 100644 index 00000000..a4d076fb --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/LocationCard.tsx @@ -0,0 +1,105 @@ +import './style.scss'; +import clsx from 'clsx'; +import type { ReactNode } from 'react'; +import { + ConnectionType, + type InstanceInfo, + type LocationInfo, +} from '../../rust-api/types'; +import { Direction } from '../../types'; +import { Fold } from '../Fold/Fold'; +import { IconKind } from '../Icon'; +import { IconButton } from '../IconButton/IconButton'; +import { IconButtonVariant } from '../IconButton/types'; +import { LocationCardIcon } from './components/LocationCardIcon'; +import { LocationCardProvider, useLocationCardContext } from './context/context'; +import { LocationCardViews, type LocationCardViewsValue } from './context/types'; +import { ConnectedView } from './views/ConnectedView/ConnectedView'; +import { DefaultView } from './views/DefaultView/DefaultView'; +import { LocationCardMfaEmailView } from './views/LocationCardMfaEmailView/LocationCardMfaEmailView'; +import { LocationCardMfaSettings } from './views/LocationCardMfaSettings/LocationCardMfaSettings'; +import { LocationCardMfaTotpView } from './views/LocationCardMfaTotpView/LocationCardMfaTotpView'; + +interface Props { + location: LocationInfo; + isOpen: boolean; + onOpen: () => void; + disableOpen?: boolean; + instance: InstanceInfo; +} + +const views: Record = { + [LocationCardViews.Default]: , + [LocationCardViews.MfaTotp]: , + [LocationCardViews.MfaEmail]: , + [LocationCardViews.MfaOidc]: null, + [LocationCardViews.MfaMobile]: null, + [LocationCardViews.MfaSettings]: , + [LocationCardViews.Connecting]: null, + [LocationCardViews.Connected]: , + [LocationCardViews.PostureCheckFail]: null, +}; + +interface InnerProps { + isOpen: boolean; + onOpen: () => void; + disableOpen: boolean; +} + +const LocationCardInner = ({ isOpen, onOpen, disableOpen }: InnerProps) => { + const { location, currentView } = useLocationCardContext(); + + return ( +
+
+
+ +
+

+ {location.connection_type === ConnectionType.Location + ? 'Location' + : 'Tunnel'} +

+
+

{location.name}

+ {location.active && ( +
+

Online

+
+ )} +
+
+
+
+ {!disableOpen && ( + + )} +
+
+ {views[currentView]} +
+ ); +}; + +export const LocationCard = ({ + location, + isOpen, + onOpen, + instance, + disableOpen = false, +}: Props) => { + return ( + + + + ); +}; diff --git a/new-ui/src/shared/components/LocationCard/assets/location_avatar.png b/new-ui/src/shared/components/LocationCard/assets/location_avatar.png new file mode 100644 index 00000000..ce0ce633 Binary files /dev/null and b/new-ui/src/shared/components/LocationCard/assets/location_avatar.png differ diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx new file mode 100644 index 00000000..19a583b8 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/ConnectButton.tsx @@ -0,0 +1,59 @@ +import './style.scss'; +import { useMutation } from '@tanstack/react-query'; +import clsx from 'clsx'; +import { api } from '../../../../rust-api/api'; +import { LocationMfaMode } from '../../../../rust-api/types'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; + +export const ConnectButton = () => { + const { location, setView, startMfa } = useLocationCardContext(); + + const { mutate: connect } = useMutation({ + mutationFn: api.connect, + onSuccess: () => { + setView(LocationCardViews.Connected); + }, + meta: { + invalidate: ['locations'], + }, + }); + + const { mutate: disconnect } = useMutation({ + mutationFn: api.disconnect, + onSuccess: () => { + setView(LocationCardViews.Default); + }, + meta: { + invalidate: ['locations'], + }, + }); + + return ( + + ); +}; diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss b/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss new file mode 100644 index 00000000..9b63060e --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/ConnectButton/style.scss @@ -0,0 +1,39 @@ +.connect-button { + --bg: linear-gradient(180deg, #fff 0%, #d3ddfb 100%); + --shadow: 0 4px 5px 0 rgb(53 84 179 / 7%); + --color: var(--fg-action); + --border: var(--bg); + + background: var(--bg); + border: 1px solid var(--border); + color: var(--color); + box-shadow: var(--shadow); + transition-duration: 200ms; + transition-property: background, border-color, box-shadow, color; + transition-timing-function: ease-in-out; + cursor: pointer; + display: inline-flex; + width: 100%; + align-items: center; + justify-content: center; + border-radius: 100px; + min-height: 38px; + box-sizing: border-box; + padding: 0 var(--spacing-lg); + + &.connected { + --bg: linear-gradient(rgb(255 255 255 / 0%), rgb(255 255 255 / 0%)); + --color: var(--fg-white-100); + --border: var(--border-default); + + &:hover { + --border: var(--bg-white-5); + --bg: linear-gradient(rgb(255 255 255 / 5%), rgb(255 255 255 / 5%)); + } + } + + p { + font: var(--t-body-sm-600); + color: inherit; + } +} diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectionChart/ConnectionChart.tsx b/new-ui/src/shared/components/LocationCard/components/ConnectionChart/ConnectionChart.tsx new file mode 100644 index 00000000..9332293c --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/ConnectionChart/ConnectionChart.tsx @@ -0,0 +1,108 @@ +import './style.scss'; +import { useQuery } from '@tanstack/react-query'; +import { BarElement, CategoryScale, Chart as ChartJS, LinearScale } from 'chart.js'; +import { sum } from 'radashi'; +import { useMemo } from 'react'; +import { Bar } from 'react-chartjs-2'; +import { getLocationStatsQueryOptions } from '../../../../rust-api/query'; +import type { ConnectionType } from '../../../../rust-api/types'; +import { BoxIcon } from '../../../BoxIcon/BoxIcon'; +import { Icon, IconKind } from '../../../Icon'; +import { TransferText } from '../../../TransferText/TransferText'; + +ChartJS.register(BarElement, CategoryScale, LinearScale); + +const UPLOAD_COLOR = 'rgba(255, 255, 255, 0.20)'; +const DOWNLOAD_COLOR = 'rgba(255, 255, 255, 1.0)'; + +interface Props { + locationId: number; + connectionType: ConnectionType; +} + +export const ConnectionChart = ({ locationId, connectionType }: Props) => { + const { data: stats } = useQuery( + getLocationStatsQueryOptions({ locationId, connectionType }), + ); + + const statsSum = useMemo( + () => ({ + download: sum(stats ?? [], (s) => s.download), + upload: sum(stats ?? [], (s) => s.upload), + }), + [stats], + ); + + const chartData = { + labels: stats?.map((s) => s.collected_at) ?? [], + datasets: [ + { + label: 'upload', + data: stats?.map((s) => s.upload) ?? [], + backgroundColor: UPLOAD_COLOR, + borderWidth: 0, + borderRadius: 0, + categoryPercentage: 0.95, + barPercentage: 1.0, + maxBarThickness: 2.2, + }, + { + label: 'download', + data: stats?.map((s) => s.download) ?? [], + backgroundColor: DOWNLOAD_COLOR, + borderWidth: 0, + borderRadius: 0, + categoryPercentage: 0.95, + barPercentage: 1.0, + maxBarThickness: 2.2, + }, + ], + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + animation: false as const, + layout: { padding: 0 }, + plugins: { + legend: { display: false }, + tooltip: { enabled: false }, + }, + scales: { + x: { + display: false, + grid: { display: false }, + border: { display: false }, + }, + y: { + display: false, + grid: { display: false }, + border: { display: false }, + }, + }, + }; + + if (!stats?.length) return null; + + return ( +
+
+ +
+
+
+ + + + +
+
+ + + + +
+
+
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/components/ConnectionChart/style.scss b/new-ui/src/shared/components/LocationCard/components/ConnectionChart/style.scss new file mode 100644 index 00000000..20aeb93c --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/ConnectionChart/style.scss @@ -0,0 +1,21 @@ +.connection-chart { + > .chart-container { + padding-bottom: var(--spacing-lg); + } + + > .stats-summary { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-3xl); + + > .summary { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-sm); + } + } +} diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardControls/LocationCardControls.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardControls/LocationCardControls.tsx new file mode 100644 index 00000000..a86ee157 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/LocationCardControls/LocationCardControls.tsx @@ -0,0 +1,5 @@ +import { Controls } from '../../../Controls/Controls'; + +export const LocationCardControls = () => { + return ; +}; diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardIcon.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardIcon.tsx new file mode 100644 index 00000000..3912e224 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/LocationCardIcon.tsx @@ -0,0 +1,20 @@ +import cardImage from '../assets/location_avatar.png'; + +export const LocationCardIcon = () => { + return ( +
+ +
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/components/LocationViewHeader/LocationViewHeader.tsx b/new-ui/src/shared/components/LocationCard/components/LocationViewHeader/LocationViewHeader.tsx new file mode 100644 index 00000000..a45fc9d5 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/LocationViewHeader/LocationViewHeader.tsx @@ -0,0 +1,15 @@ +import './style.scss'; +import type { PropsWithChildren } from 'react'; + +interface Props extends PropsWithChildren { + title: string; +} + +export const LocationViewHeader = ({ title, children }: Props) => { + return ( +
+

{title}

+ {children} +
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/components/LocationViewHeader/style.scss b/new-ui/src/shared/components/LocationCard/components/LocationViewHeader/style.scss new file mode 100644 index 00000000..a6563063 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/LocationViewHeader/style.scss @@ -0,0 +1,15 @@ +.location-card-view-header { + display: flex; + flex-flow: column; + row-gap: var(--spacing-xs); + + p { + font: var(--t-body-xs-400); + color: var(--fg-white-70); + } + + > .title { + font: var(--t-body-sm-500); + color: var(--fg-white-100); + } +} diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx new file mode 100644 index 00000000..7f1f65ce --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx @@ -0,0 +1,59 @@ +import './style.scss'; +import clsx from 'clsx'; +import { type HTMLProps, type MouseEventHandler, useMemo } from 'react'; +import type { MfaMethodValue } from '../../../../rust-api/types'; +import { mfaToText } from '../../../../utils/mfa'; +import { Icon, IconKind, type IconKindValue } from '../../../Icon'; + +interface Props { + factor: MfaMethodValue; + selected?: boolean; + isDefault?: boolean; + onClick?: MouseEventHandler; + containerProps?: Omit, 'onClick'>; +} + +export const MfaSelector = ({ + factor, + onClick, + containerProps, + selected = false, + isDefault = false, +}: Props) => { + const iconKind = useMemo((): IconKindValue => { + switch (factor) { + case 'email': + return 'mail'; + case 'mobileapprove': + return 'mobile-lock'; + case 'oidc': + return 'token'; + case 'totp': + return 'mobile-lock'; + case 'biometric': + return 'biometric'; + } + }, [factor]); + + return ( +
+ +
+

{mfaToText(factor)}

+ {isDefault && ( +
+

Default

+
+ )} +
+ {selected && } +
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss b/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss new file mode 100644 index 00000000..3d33c107 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss @@ -0,0 +1,76 @@ +.mfa-selector { + --bg: transparent; + --border: var(--border-default); + --icon: var(--fg-white-80); + --color: var(--fg-white-80); + + display: grid; + grid-template-columns: 20px minmax(0, 1fr) 16px; + column-gap: var(--spacing-sm); + background: var(--bg); + border: 1px solid var(--border); + color: var(--color); + user-select: none; + align-items: center; + box-sizing: border-box; + padding: 0 var(--spacing-md); + min-height: 40px; + border-radius: 8px; + cursor: pointer; + transition-duration: 250ms; + transition-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1); + transition-property: border-color, background, color; + + &:hover { + --bg: var(--bg-white-5); + --color: var(--fg-white-100); + --border: var(--border-default); + --icon: var(--fg-white-100); + } + + &.selected { + --bg: var(--bg-white-5); + --color: var(--fg-white-100); + --border: transparent; + --icon: var(--fg-white-100); + } + + > .middle { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-xs); + } + + .default-badge { + display: inline-block; + box-sizing: border-box; + padding: 2px 5px; + border-radius: 5px; + background: var(--bg-white-10); + + p { + font-family: var(--font-family-body); + font-size: 11px; + font-weight: 400; + line-height: normal; + letter-spacing: -0.11px; + } + } + + .factor-icon { + --icon-color: var(--icon); + + path { + transition-duration: 250ms; + transition-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1); + transition-property: fill; + } + } + + .name { + color: inherit; + font: var(--t-body-xs); + } +} diff --git a/new-ui/src/shared/components/LocationCard/context/context.tsx b/new-ui/src/shared/components/LocationCard/context/context.tsx new file mode 100644 index 00000000..b2e7e8b4 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/context/context.tsx @@ -0,0 +1,80 @@ +import { createContext, type ReactNode, useCallback, useContext, useState } from 'react'; +import type { InstanceInfo, LocationInfo } from '../../../rust-api/types'; +import { MfaMethod } from '../../../rust-api/types'; +import { LocationCardViews, type LocationCardViewsValue } from './types'; + +interface LocationCardContextValue { + location: LocationInfo; + instance: InstanceInfo; + currentView: LocationCardViewsValue; + previousView: LocationCardViewsValue | null; + setView: (view: LocationCardViewsValue) => void; + startMfa: () => void; +} + +const LocationCardContext = createContext(null); + +export const useLocationCardContext = (): LocationCardContextValue => { + const ctx = useContext(LocationCardContext); + if (!ctx) { + throw new Error('useLocationCardContext must be used within a LocationCardProvider'); + } + return ctx; +}; + +interface LocationCardProviderProps { + instance: InstanceInfo; + location: LocationInfo; + children: ReactNode; +} + +export const LocationCardProvider = ({ + location, + instance, + children, +}: LocationCardProviderProps) => { + const [previousView, setPreviousView] = useState(null); + const [currentView, setCurrentView] = useState( + location.active ? LocationCardViews.Connected : LocationCardViews.Default, + ); + + const setView = useCallback( + (view: LocationCardViewsValue) => { + setPreviousView(currentView); + setCurrentView(view); + }, + [currentView], + ); + + const startMfa = useCallback(() => { + switch (location.mfa_method) { + case MfaMethod.Totp: + setView(LocationCardViews.MfaTotp); + break; + case MfaMethod.Email: + setView(LocationCardViews.MfaEmail); + break; + case MfaMethod.Oidc: + setView(LocationCardViews.MfaOidc); + break; + case MfaMethod.MobileApprove: + setView(LocationCardViews.MfaMobile); + break; + } + }, [location.mfa_method, setView]); + + return ( + + {children} + + ); +}; diff --git a/new-ui/src/shared/components/LocationCard/context/types.ts b/new-ui/src/shared/components/LocationCard/context/types.ts new file mode 100644 index 00000000..a17e666e --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/context/types.ts @@ -0,0 +1,14 @@ +export const LocationCardViews = { + Default: 'default', + MfaTotp: 'mfa-totp', + MfaEmail: 'mfa-email', + MfaOidc: 'mfa-oidc', + MfaMobile: 'mfa-mobile', + MfaSettings: 'mfa-settings', + Connecting: 'connecting', + Connected: 'connected', + PostureCheckFail: 'posture-check-fail', +} as const; + +export type LocationCardViewsValue = + (typeof LocationCardViews)[keyof typeof LocationCardViews]; diff --git a/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts new file mode 100644 index 00000000..60a2e730 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts @@ -0,0 +1,155 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { fetch } from '@tauri-apps/plugin-http'; +import { error } from '@tauri-apps/plugin-log'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { api } from '../../../rust-api/api'; +import { + getInstancesQueryOptions, + getPlatformHeaderQueryOptions, +} from '../../../rust-api/query'; +import type { EdgeRequestHeaders } from '../../../rust-api/types'; +import { useLocationCardContext } from '../context/context'; +import { LocationCardViews } from '../context/types'; + +const MFA_ENDPOINT = 'api/v1/client-mfa'; + +type MfaStartResponse = { + token: string; + challenge?: string; +}; + +type MfaFinishResponse = { + preshared_key: string; +}; + +type MfaErrorResponse = { + error: string; +}; + +export const useMfaConnect = (method: 0 | 1) => { + const { location, setView } = useLocationCardContext(); + + const [token, setToken] = useState(null); + const [isStarting, setIsStarting] = useState(false); + const [startError, setStartError] = useState(null); + const [isVerifying, setIsVerifying] = useState(false); + const [verifyError, setVerifyError] = useState(null); + const [requestHeaders, setRequestHeaders] = useState(null); + + const { data: instances } = useQuery(getInstancesQueryOptions); + const { data: platformHeader } = useQuery(getPlatformHeaderQueryOptions); + + const instance = instances?.find((i) => i.id === location.instance_id); + + const { mutate: connectMutate } = useMutation({ + mutationFn: api.connect, + meta: { invalidate: ['locations'] }, + onSuccess: () => { + setView(LocationCardViews.Connected); + }, + onError: (err) => { + error(`Connect command failed after successful code verification\n${err}`); + }, + }); + + // Fire the /start request exactly once when instance + platformHeader are ready. + const startCalled = useRef(false); + + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional one-shot trigger via startCalled ref + useEffect(() => { + if (!instance || !platformHeader || startCalled.current) return; + startCalled.current = true; + + setIsStarting(true); + + (async () => { + let headers: EdgeRequestHeaders; + try { + headers = await api.getEdgeRequestHeaders(); + setRequestHeaders(headers); + } catch { + setStartError('Failed to load request headers'); + setIsStarting(false); + return; + } + + try { + const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + body: JSON.stringify({ + method, + pubkey: instance.pubkey, + location_id: location.network_id, + }), + }); + + if (res.ok) { + const data = (await res.json()) as MfaStartResponse; + setToken(data.token); + } else { + const data = (await res.json()) as MfaErrorResponse; + setStartError(data.error ?? 'Failed to start MFA'); + } + } catch { + setStartError('Failed to reach server'); + } finally { + setIsStarting(false); + } + })(); + }, [instance, platformHeader]); + + const verifyCode = useCallback( + async (code: string) => { + if (!token || !instance || !platformHeader || !requestHeaders) return; + + setIsVerifying(true); + setVerifyError(null); + + const body = JSON.stringify({ token, code }); + + try { + const res = await fetch(`${instance.proxy_url}${MFA_ENDPOINT}/finish`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...requestHeaders, + }, + body, + }); + + if (res.ok) { + const data = (await res.json()) as MfaFinishResponse; + connectMutate({ + locationId: location.id, + connectionType: location.connection_type, + presharedKey: data.preshared_key, + }); + } else { + const data = (await res.json()) as MfaErrorResponse; + const { error: errorMessage } = data; + if (errorMessage === 'Unauthorized') { + setVerifyError('Invalid code'); + } else if ( + errorMessage === 'invalid token' || + errorMessage === 'login session not found' + ) { + setView(LocationCardViews.Default); + } else { + setVerifyError('Verification failed'); + } + } + } catch { + setVerifyError('Failed to reach server'); + } finally { + setIsVerifying(false); + } + }, + [token, instance, platformHeader, requestHeaders, location, connectMutate, setView], + ); + + return { token, isStarting, startError, verifyCode, isVerifying, verifyError }; +}; diff --git a/new-ui/src/shared/components/LocationCard/style.scss b/new-ui/src/shared/components/LocationCard/style.scss new file mode 100644 index 00000000..9221d88a --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/style.scss @@ -0,0 +1,69 @@ +.location-card { + border-radius: 12px; + box-sizing: border-box; + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--bg-dark-blue-40); + + > .top-track { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + user-select: none; + + > .left { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-md); + + > .info { + display: flex; + flex-flow: column; + + .label { + font: var(--t-body-xs-400); + color: var(--fg-white-70); + } + + .location-name { + font: var(--t-body-primary-600); + color: var(--fg-white-100); + } + + > .bottom { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-sm); + } + + .online-badge { + display: inline-block; + box-sizing: border-box; + padding: 1px 4px; + border-radius: 4px; + background-color: #74ffb8; + + p { + font-family: var(--font-family-body); + font-size: 10px; + letter-spacing: 0.1px; + font-weight: 600; + color: #2f50c2; + } + } + } + } + + > .right { + margin-left: auto; + } + } + + .controls { + padding-top: var(--spacing-3xl); + } +} diff --git a/new-ui/src/shared/components/LocationCard/views/ConnectedView/ConnectedView.tsx b/new-ui/src/shared/components/LocationCard/views/ConnectedView/ConnectedView.tsx new file mode 100644 index 00000000..84ab3a92 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/ConnectedView/ConnectedView.tsx @@ -0,0 +1,94 @@ +import './style.scss'; +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { useEffect, useMemo } from 'react'; +import { api } from '../../../../rust-api/api'; +import { LocationMfaMode } from '../../../../rust-api/types'; +import { ThemeSpacing } from '../../../../types'; +import { mfaToText } from '../../../../utils/mfa'; +import { BoxIcon } from '../../../BoxIcon/BoxIcon'; +import { Divider } from '../../../Divider/Divider'; +import { Icon, IconKind } from '../../../Icon'; +import { SizedBox } from '../../../SizedBox/SizedBox'; +import { ConnectButton } from '../../components/ConnectButton/ConnectButton'; +import { ConnectionChart } from '../../components/ConnectionChart/ConnectionChart'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; + +export const ConnectedView = () => { + const { location, setView } = useLocationCardContext(); + + // const { data: currentConnection } = useQuery({ + // queryKey: ['locations', location.id, 'connection'], + // queryFn: () => + // api.getActiveConnection({ + // connectionType: location.connection_type, + // locationId: location.id, + // }), + // }); + + const { data: lastConnection } = useQuery({ + queryKey: ['locations', location.id, 'last-connect'], + queryFn: () => + api.getLastConnection({ + connectionType: location.connection_type, + locationId: location.id, + }), + }); + + const lastConnectedText = useMemo(() => { + if (!lastConnection) return 'Never'; + return dayjs.utc(lastConnection.end).local().format('DD MMM YYYY'); + }, [lastConnection]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: side-effect + useEffect(() => { + if (!location.active) { + setView(LocationCardViews.Default); + } + }, [location.active]); + + return ( +
+ +
+
+ + + +

Allowed traffic

+

+ {location.route_all_traffic ? 'All traffic' : 'Predefined traffic'} +

+
+ {location.location_mfa_mode !== LocationMfaMode.Disabled && ( +
+ + + +

Active MFA

+

{mfaToText(location.mfa_method ?? 'totp')}

+
+ )} +
+ +
+
+
Last connected
+
{lastConnectedText}
+
+
+
Assigned IP
+
{location.address}
+
+
+ + + + +
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/views/ConnectedView/style.scss b/new-ui/src/shared/components/LocationCard/views/ConnectedView/style.scss new file mode 100644 index 00000000..6b076ace --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/ConnectedView/style.scss @@ -0,0 +1,47 @@ +.location-view-connected { + > .tiles { + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: var(--spacing-md); + + > .tile { + background: var(--bg-white-5); + border-radius: 8px; + box-sizing: border-box; + padding: var(--spacing-sm) var(--spacing-md); + + > .box-icon { + margin-bottom: 13px; + } + + > .label { + font: var(--t-body-xxs-400); + color: var(--fg-white-50); + padding-bottom: var(--spacing-xs); + } + + > .label-value { + font: var(--t-body-xs-500); + } + } + } + + > .connection-info { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; + justify-content: space-between; + + > .info { + > .label { + font: var(--t-body-xxs-400); + color: var(--fg-white-50); + padding-bottom: var(--spacing-xs); + } + + > .label-value { + font: var(--t-body-xs-500); + } + } + } +} diff --git a/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx b/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx new file mode 100644 index 00000000..acfb49a9 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx @@ -0,0 +1,74 @@ +import './style.scss'; +import { useMutation } from '@tanstack/react-query'; +import { Fragment } from 'react/jsx-runtime'; +import { api } from '../../../../rust-api/api'; +import { + ClientTrafficPolicy, + LocationMfaMode, + MfaMethod, +} from '../../../../rust-api/types'; +import { ThemeSpacing } from '../../../../types'; +import { mfaToText } from '../../../../utils/mfa'; +import { Divider } from '../../../Divider/Divider'; +import { IconButton } from '../../../IconButton/IconButton'; +import { IconButtonVariant } from '../../../IconButton/types'; +import { SizedBox } from '../../../SizedBox/SizedBox'; +import { Toggle } from '../../../Toggle/Toggle'; +import { ConnectButton } from '../../components/ConnectButton/ConnectButton'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; + +export const DefaultView = () => { + const { location, instance, setView } = useLocationCardContext(); + + const mfaMethod = location.mfa_method ?? MfaMethod.Totp; + + const { mutate: updateRouting } = useMutation({ + mutationFn: api.updateLocationRouting, + meta: { + invalidate: ['locations'], + }, + }); + + return ( +
+ {instance.client_traffic_policy === ClientTrafficPolicy.None && ( + + + { + updateRouting({ + connectionType: location.connection_type, + locationId: location.id, + routeAllTraffic: !location.route_all_traffic, + }); + }} + /> + + )} + {location.location_mfa_mode !== LocationMfaMode.Disabled && mfaMethod && ( + + +
+
+

MFA

+
+

{mfaToText(mfaMethod)}

+ { + setView(LocationCardViews.MfaSettings); + }} + /> +
+
+ )} + + +
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss b/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss new file mode 100644 index 00000000..b32113ff --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss @@ -0,0 +1,35 @@ +.location-view-default { + .location-mfa-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) 24px; + grid-template-rows: 1fr; + align-items: center; + column-gap: var(--spacing-md); + + > .name { + font: var(--t-body-xs-500); + color: var(--fg-white-100); + } + + .mfa-badge { + border-radius: 4px; + box-sizing: border-box; + display: inline-flex; + flex-flow: row nowrap; + align-items: center; + padding: 0 4px; + height: 18px; + width: 32px; + background-color: var(--bg-white-100); + + p { + font: var(--font-family-body); + font-size: 11px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.11px; + color: var(--fg-action); + } + } + } +} diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx new file mode 100644 index 00000000..9b908c77 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useState } from 'react'; +import { ThemeSpacing } from '../../../../types'; +import { isPresent } from '../../../../utils/isPresent'; +import { Button } from '../../../Button/Button'; +import { ButtonVariant } from '../../../Button/types'; +import { CodeInput } from '../../../CodeInput/CodeInput'; +import { Controls } from '../../../Controls/Controls'; +import { Divider } from '../../../Divider/Divider'; +import { IconKind } from '../../../Icon'; +import { IconButton } from '../../../IconButton/IconButton'; +import { IconButtonVariant } from '../../../IconButton/types'; +import { SizedBox } from '../../../SizedBox/SizedBox'; +import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; +import { useMfaConnect } from '../../hooks/useMfaConnect'; + +export const LocationCardMfaEmailView = () => { + const { setView } = useLocationCardContext(); + const { verifyCode, isVerifying, verifyError, isStarting, startError } = + useMfaConnect(1); + + const [emailCode, setEmailCode] = useState(null); + const [error, setError] = useState(null); + + const handleVerify = useCallback(() => { + if (!isPresent(emailCode)) { + setError('Enter code'); + return; + } + if (emailCode.length !== 6) { + setError('6 digits are required'); + return; + } + verifyCode(emailCode); + }, [emailCode, verifyCode]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: side effect of code input + useEffect(() => { + setError(null); + }, [emailCode, setError]); + + // Reflect server-side verify errors into the local error state + useEffect(() => { + if (verifyError) setError(verifyError); + }, [verifyError]); + + return ( +
{ + if (e.key === 'Enter') handleVerify(); + }} + > + + +

Enter the 6-digit code sent to your email address.

+
+ + + + { + setView(LocationCardViews.Default); + }} + /> +
+
+
+
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx new file mode 100644 index 00000000..c8314ae6 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx @@ -0,0 +1,109 @@ +import './style.scss'; +import { useMutation } from '@tanstack/react-query'; +import { useMemo, useState } from 'react'; +import { api } from '../../../../rust-api/api'; +import { + LocationMfaMode, + MfaMethod, + type MfaMethodValue, +} from '../../../../rust-api/types'; +import { ThemeSpacing } from '../../../../types'; +import { Button } from '../../../Button/Button'; +import { ButtonVariant } from '../../../Button/types'; +import { Checkbox } from '../../../Checkbox/Checkbox'; +import { Controls } from '../../../Controls/Controls'; +import { Divider } from '../../../Divider/Divider'; +import { IconKind } from '../../../Icon'; +import { IconButton } from '../../../IconButton/IconButton'; +import { IconButtonVariant } from '../../../IconButton/types'; +import { SizedBox } from '../../../SizedBox/SizedBox'; +import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; +import { MfaSelector } from '../../components/MfaSelector/MfaSelector'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; + +export const LocationCardMfaSettings = () => { + const { mutate: setMfaMethod } = useMutation({ + mutationFn: api.setLocationMfaMethod, + meta: { + invalidate: [['locations']], + }, + }); + + const { previousView, setView, location } = useLocationCardContext(); + + const mfaMethod = location.mfa_method ?? MfaMethod.Totp; + + const [selectedPref, setSelectedPref] = useState( + mfaMethod ?? MfaMethod.Totp, + ); + + const MfaFactorsList = useMemo((): MfaMethodValue[] => { + if (location.location_mfa_mode === LocationMfaMode.Internal) { + return [MfaMethod.Totp, MfaMethod.Email, MfaMethod.MobileApprove]; + } + return [MfaMethod.Oidc]; + }, [location.location_mfa_mode]); + + const handleSubmit = () => { + if (selectedPref !== mfaMethod) { + setMfaMethod({ + locationId: location.id, + mfaMethod: selectedPref, + }); + setView(previousView ?? LocationCardViews.Default); + } + }; + + return ( +
+ + +

+ If you're having issues with your current verification method, you can choose + another one or set a new default. +

+
+ +
+ {MfaFactorsList.map((factor) => ( + setSelectedPref(factor)} + /> + ))} +
+ + + { + setView(previousView ?? LocationCardViews.Default); + }} + /> +
+
+
+
+ ); +}; diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/style.scss b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/style.scss new file mode 100644 index 00000000..f1133fe7 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/style.scss @@ -0,0 +1,21 @@ +.location-card-mfa-settings { + > .header { + padding-bottom: var(--spacing-xl); + + :nth-child(1) { + font: var(--t-body-sm-500); + } + + :nth-child(2) { + font: var(--t-body-xs-400); + color: var(--fg-white-70); + } + } + + > .methods { + display: flex; + flex-flow: column; + row-gap: var(--spacing-md); + padding-bottom: var(--spacing-md); + } +} diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx new file mode 100644 index 00000000..fc64eec1 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaTotpView/LocationCardMfaTotpView.tsx @@ -0,0 +1,87 @@ +import { useCallback, useEffect, useState } from 'react'; +import { ThemeSpacing } from '../../../../types'; +import { isPresent } from '../../../../utils/isPresent'; +import { Button } from '../../../Button/Button'; +import { ButtonVariant } from '../../../Button/types'; +import { CodeInput } from '../../../CodeInput/CodeInput'; +import { Controls } from '../../../Controls/Controls'; +import { Divider } from '../../../Divider/Divider'; +import { IconKind } from '../../../Icon'; +import { IconButton } from '../../../IconButton/IconButton'; +import { IconButtonVariant } from '../../../IconButton/types'; +import { SizedBox } from '../../../SizedBox/SizedBox'; +import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader'; +import { useLocationCardContext } from '../../context/context'; +import { LocationCardViews } from '../../context/types'; +import { useMfaConnect } from '../../hooks/useMfaConnect'; + +export const LocationCardMfaTotpView = () => { + const { setView } = useLocationCardContext(); + const { verifyCode, isVerifying, verifyError, isStarting, startError } = + useMfaConnect(0); + + const [totpCode, setTotpCode] = useState(null); + const [error, setError] = useState(null); + + const handleVerify = useCallback(() => { + if (!isPresent(totpCode)) { + setError('Enter code'); + return; + } + if (totpCode.replaceAll(' ', '').length !== 6) { + setError('6 digits are required'); + return; + } + verifyCode(totpCode); + }, [totpCode, verifyCode]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: side effect of code input + useEffect(() => { + setError(null); + }, [totpCode, setError]); + + // Reflect server-side verify errors into the local error state + useEffect(() => { + if (verifyError) setError(verifyError); + }, [verifyError]); + + return ( +
{ + if (e.key === 'Enter') handleVerify(); + }} + > + + +

Paste the code from your Authenticator Application.

+
+ + + + { + setView(LocationCardViews.Default); + }} + /> +
+
+
+
+ ); +}; diff --git a/new-ui/src/shared/components/MainBackground/MainBackground.tsx b/new-ui/src/shared/components/MainBackground/MainBackground.tsx new file mode 100644 index 00000000..5807ec1e --- /dev/null +++ b/new-ui/src/shared/components/MainBackground/MainBackground.tsx @@ -0,0 +1,57 @@ +import { useEffect, useRef } from 'react'; + +export const MainBackground = () => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const handleResize = () => { + // Get parent dimensions + // biome-ignore lint/style/noNonNullAssertion: Always have parent + const { clientWidth: w, clientHeight: h } = canvas.parentElement!; + + // Update internal resolution + canvas.width = w; + canvas.height = h; + + // Draw Gradient (134deg) + const angle = (134 * Math.PI) / 180; + const length = Math.sqrt(w ** 2 + h ** 2); + + const x1 = w / 2 - (Math.sin(angle) * length) / 2; + const y1 = h / 2 + (Math.cos(angle) * length) / 2; + const x2 = w / 2 + (Math.sin(angle) * length) / 2; + const y2 = h / 2 - (Math.cos(angle) * length) / 2; + + const gradient = ctx.createLinearGradient(x1, y1, x2, y2); + gradient.addColorStop(0, '#5B83FF'); + gradient.addColorStop(1, '#0036DB'); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, w, h); + }; + + // Initial draw + handleResize(); + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + return ( + + ); +}; diff --git a/new-ui/src/shared/components/Menu/Menu.tsx b/new-ui/src/shared/components/Menu/Menu.tsx new file mode 100644 index 00000000..6be5111f --- /dev/null +++ b/new-ui/src/shared/components/Menu/Menu.tsx @@ -0,0 +1,33 @@ +import { Fragment } from 'react'; +import { MenuItem } from './components/MenuItem'; +import './style.scss'; +import clsx from 'clsx'; +import { isPresent } from '../../utils/isPresent'; +import { MenuHeader } from './components/MenuHeader'; +import { MenuSpacer } from './components/MenuSpacer'; +import type { MenuProps } from './types'; + +export const Menu = ({ + itemGroups, + ref, + className, + onClose, + testId, + ...props +}: MenuProps) => { + return ( +
+ {itemGroups.map((group, groupIndex) => ( + + {isPresent(group.header) && } + {group.items.map((item) => ( + + ))} + {groupIndex !== itemGroups.length - 1 && itemGroups.length !== 1 && ( + + )} + + ))} +
+ ); +}; diff --git a/new-ui/src/shared/components/Menu/components/MenuHeader.tsx b/new-ui/src/shared/components/Menu/components/MenuHeader.tsx new file mode 100644 index 00000000..0e51cd77 --- /dev/null +++ b/new-ui/src/shared/components/Menu/components/MenuHeader.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx'; +import { isPresent } from '../../../utils/isPresent'; +import { Icon } from '../../Icon'; +import { InteractionBox } from '../../InteractionBox/InteractionBox'; +import type { MenuHeaderProps } from '../types'; + +export const MenuHeader = ({ text, testId, onHelp, onClose }: MenuHeaderProps) => { + return ( +
+

{text}

+ {isPresent(onHelp) && ( + { + onClose?.(); + onHelp(); + }} + > + + + )} +
+ ); +}; diff --git a/new-ui/src/shared/components/Menu/components/MenuItem.tsx b/new-ui/src/shared/components/Menu/components/MenuItem.tsx new file mode 100644 index 00000000..b6bafc86 --- /dev/null +++ b/new-ui/src/shared/components/Menu/components/MenuItem.tsx @@ -0,0 +1,48 @@ +import clsx from 'clsx'; +import { isPresent } from '../../../utils/isPresent'; +import { Icon } from '../../Icon'; +import type { MenuItemProps } from '../types'; + +export const MenuItem = ({ + disabled, + text, + icon, + items, + testId, + variant, + onClick, + onClose, +}: MenuItemProps) => { + const hasItems = isPresent(items) && items.length > 0; + const hasIcon = isPresent(icon); + + return ( +
{ + if (!disabled) { + onClick?.(); + if (!hasItems) { + onClose?.(); + } + } + }} + > + {isPresent(icon) && } +

{text}

+ {hasItems && ( +
+ +
+ )} +
+ ); +}; diff --git a/new-ui/src/shared/components/Menu/components/MenuSpacer.tsx b/new-ui/src/shared/components/Menu/components/MenuSpacer.tsx new file mode 100644 index 00000000..4510f86e --- /dev/null +++ b/new-ui/src/shared/components/Menu/components/MenuSpacer.tsx @@ -0,0 +1,7 @@ +export const MenuSpacer = () => { + return ( +
+
+
+ ); +}; diff --git a/new-ui/src/shared/components/Menu/style.scss b/new-ui/src/shared/components/Menu/style.scss new file mode 100644 index 00000000..78f595a4 --- /dev/null +++ b/new-ui/src/shared/components/Menu/style.scss @@ -0,0 +1,119 @@ +/* stylelint-disable no-descending-specificity */ +.menu { + display: flex; + flex-flow: column; + box-sizing: border-box; + padding: var(--spacing-sm); + border-radius: var(--radius-lg); + border: 1px solid var(--border-disabled); + background-color: var(--bg-default); + box-shadow: 0 4px 12px 0 rgb(0 0 0 / 7%); + overflow: hidden auto; + z-index: 5; + + .menu-spacer { + user-select: none; + padding: var(--spacing-sm) 0; + + & > .line { + display: block; + content: ' '; + background-color: var(--bg-white-20); + height: 1px; + width: 100%; + } + } + + .menu-header { + display: flex; + flex-flow: row nowrap; + column-gap: var(--spacing-md); + justify-content: space-between; + flex: none; + + p { + font: var(--t-menu-title); + color: var(--fg-muted); + padding-left: var(--spacing-sm); + } + + .interaction-box { + button { + height: 26px; + width: 26px; + } + + &:hover { + svg { + path { + fill: var(--fg-action); + } + } + } + } + } + + .menu-item { + --bg-color: var(--bg-default); + --color: var(--fg-default); + --icon-fill: var(--fg-muted); + + display: flex; + flex-flow: row nowrap; + flex: none; + align-items: center; + border-radius: var(--radius-md); + padding: 0 var(--spacing-sm); + column-gap: var(--spacing-md); + background-color: var(--bg-color); + cursor: pointer; + height: 36px; + color: var(--color); + position: relative; + min-width: 115px; + + @include animate(background-color); + + &.disabled { + cursor: not-allowed; + } + + &.nested { + // account for positioned icon on the right side + padding: 0 calc(var(--spacing-sm) + var(--spacing-md) + 20px) 0 var(--spacing-sm); + } + + &.variant-danger { + --color: var(--fg-critical); + --icon-fill: var(--fg-critical); + } + + &:not(.disabled) { + &:hover { + --bg-color: var(--bg-muted); + } + } + + p { + font: var(--t-menu-text); + color: inherit; + } + + & > .icon svg path { + fill: var(--icon-fill); + } + + & > .suffix { + position: absolute; + height: 20px; + width: 20px; + top: 50%; + right: var(--spacing-sm); + transform: translateY(-50%); + + svg path { + fill: var(--fg-muted); + } + } + } +} diff --git a/new-ui/src/shared/components/Menu/types.ts b/new-ui/src/shared/components/Menu/types.ts new file mode 100644 index 00000000..8537a932 --- /dev/null +++ b/new-ui/src/shared/components/Menu/types.ts @@ -0,0 +1,33 @@ +import type { HTMLAttributes, Ref } from 'react'; +import type { IconKindValue } from '../Icon/icon-types'; + +export interface MenuProps extends HTMLAttributes { + itemGroups: MenuItemsGroup[]; + ref?: Ref; + testId?: string; + onClose?: () => void; +} + +export interface MenuItemsGroup { + header?: MenuHeaderProps; + items: MenuItemProps[]; +} + +export interface MenuItemProps { + text: string; + variant?: 'default' | 'danger'; + disabled?: boolean; + icon?: IconKindValue; + items?: MenuItemProps[]; + testId?: string; + onClick?: () => void; + onClose?: () => void; +} + +export interface MenuHeaderProps { + text: string; + tooltip?: string; + testId?: string; + onClose?: () => void; + onHelp?: () => void; +} diff --git a/new-ui/src/shared/components/NotFoundRoute/NotFoundRoute.tsx b/new-ui/src/shared/components/NotFoundRoute/NotFoundRoute.tsx new file mode 100644 index 00000000..9705caeb --- /dev/null +++ b/new-ui/src/shared/components/NotFoundRoute/NotFoundRoute.tsx @@ -0,0 +1,19 @@ +import { useRouter } from '@tanstack/react-router'; + +export const NotFoundRoute = () => { + const router = useRouter(); + const availableRoutes = Object.keys(router.routesById); + + return ( +
+

Route not found

+

Detected: {window.location.href}

+

Available routes:

+
    + {availableRoutes.map((route) => ( +
  • {route}
  • + ))} +
+
+ ); +}; diff --git a/new-ui/src/shared/components/Select/Select.tsx b/new-ui/src/shared/components/Select/Select.tsx new file mode 100644 index 00000000..040debff --- /dev/null +++ b/new-ui/src/shared/components/Select/Select.tsx @@ -0,0 +1,222 @@ +import './style.scss'; +import { + autoUpdate, + FloatingPortal, + flip, + size as floatingSize, + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react'; +import clsx from 'clsx'; +import { Fragment, type JSX, useCallback, useId, useMemo, useState } from 'react'; +import { Direction, ThemeSpacing, ThemeVariable } from '../../types'; +import { isPresent } from '../../utils/isPresent'; +import { Divider } from '../Divider/Divider'; +import { FieldBox } from '../FieldBox/FieldBox'; +import { FieldError } from '../FieldError/FieldError'; +import { FieldLabel } from '../FieldLabel/FieldLabel'; +import { FloatingMenu } from '../FloatingMenu/FloatingMenu'; +import { Icon, IconKind } from '../Icon'; +import type { SelectOption, SelectOptionGroup, SelectProps } from './types'; + +export function Select(props: SelectProps): JSX.Element { + const labelId = useId(); + + const { + label, + options, + groups, + className, + placeholder, + testId, + error, + size = 'default', + disabled = false, + required = false, + } = props; + + const [floatingOpen, setFloatingOpen] = useState(false); + + const { refs, context, floatingStyles } = useFloating({ + placement: 'bottom-start', + open: floatingOpen, + onOpenChange: setFloatingOpen, + middleware: [ + offset(4), + flip(), + shift(), + floatingSize({ + apply({ rects, elements, availableHeight }) { + const refWidth = `${rects.reference.width}px`; + elements.floating.style.minWidth = refWidth; + elements.floating.style.maxHeight = `${availableHeight - 10}px`; + }, + }), + ], + whileElementsMounted: autoUpdate, + }); + + const selectedLabel = useMemo(() => props.value?.label ?? null, [props.value]); + + const renderedGroups: readonly SelectOptionGroup[] = groups ?? []; + const renderedOptions: readonly SelectOption[] = options ?? []; + + // biome-ignore lint/correctness/useExhaustiveDependencies: onChange + const handleChange = useCallback( + (option: SelectOption, isSelected: boolean) => { + if (isSelected) return; + props.onChange(option); + setFloatingOpen(false); + }, + [props.onChange, setFloatingOpen], + ); + + const click = useClick(context, { + toggle: true, + enabled: !disabled, + }); + + const dismiss = useDismiss(context, { + ancestorScroll: true, + escapeKey: true, + outsidePress: true, + }); + + const { getFloatingProps, getReferenceProps } = useInteractions([click, dismiss]); + + return ( + <> +
+
+ {isPresent(label) && ( + + )} + + } + forceFocusState={floatingOpen} + aria-labelledby={labelId} + {...getReferenceProps()} + > +
+ {isPresent(placeholder) && !isPresent(selectedLabel) && ( + {placeholder} + )} + {isPresent(selectedLabel) && {selectedLabel}} +
+
+ +
+
+ {floatingOpen && ( + + + {renderedOptions.map((option, optionIndex) => { + const isSelected = props.value?.key === option.key; + const isLast = renderedOptions.length - 1 === optionIndex; + return ( + + ); + })} + {renderedGroups + .filter((group) => group.options.length > 0) + .map((group, groupIndex, activeGroups) => { + const isLast = activeGroups.length - 1 === groupIndex; + const groupKey = group.key ?? `${group.label}-${groupIndex}`; + + return ( + +
+
+

{group.label}

+
+
+ {group.options.map((option, optionIndex) => { + const isSelected = props.value?.key === option.key; + const isLast = group.options.length - 1 === optionIndex; + return ( + + ); + })} + {!isLast && } +
+ ); + })} +
+
+ )} + + ); +} + +type SelectOptionItemProps = { + isLast: boolean; + isSelected: boolean; + onSelect: (option: SelectOption, isSelected: boolean) => void; + option: SelectOption; +}; + +function SelectOptionItem({ + isLast, + isSelected, + onSelect, + option, +}: SelectOptionItemProps): JSX.Element { + return ( +
{ + onSelect(option, isSelected); + }} + role="listitem" + > + {option.label} + {isSelected && ( + + )} +
+ ); +} diff --git a/new-ui/src/shared/components/Select/style.scss b/new-ui/src/shared/components/Select/style.scss new file mode 100644 index 00000000..b7291ba6 --- /dev/null +++ b/new-ui/src/shared/components/Select/style.scss @@ -0,0 +1,109 @@ +.select { + & > .inner { + box-sizing: border-box; + + .field-label { + cursor: pointer; + user-select: none; + } + + .field-box { + .box-track { + min-width: 0; + + .placeholder, + .value { + display: block; + user-select: none; + } + } + + &.size-default { + .box-track { + .value, + .placeholder { + font: var(--t-input-text-primary); + } + } + } + + &.size-lg { + .box-track { + .value, + .placeholder { + font: var(--t-input-text-big); + } + } + } + } + + &.disabled { + user-select: none; + + & > .field-label { + cursor: not-allowed; + } + } + } +} + +.select-floating { + z-index: 5; + position: absolute; + + .section-title { + box-sizing: border-box; + padding-left: var(--spacing-sm); + min-height: 24px; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + margin-bottom: 4px; + + p { + font: var(--t-menu-title); + color: var(--fg-white-60); + } + } + + .select-option { + --bg-color: transparent; + + display: grid; + grid-template-columns: minmax(0, 1fr) 20px; + grid-template-rows: 1fr; + box-sizing: border-box; + padding: 0 var(--spacing-sm); + user-select: none; + align-items: center; + justify-content: start; + background-color: var(--bg-color); + width: 100%; + min-height: 36px; + border-radius: 8px; + cursor: pointer; + column-gap: var(--spacing-md); + + @include animate(background-color); + + &:not(.last) { + margin-bottom: var(--spacing-xs); + } + + &:hover { + --bg-color: var(--bg-white-10); + } + + &.selected { + --bg-color: var(--bg-white-5); + } + + span { + font: var(--t-menu-text); + color: var(--fg-white-100); + + @include animate(color); + } + } +} diff --git a/new-ui/src/shared/components/Select/types.ts b/new-ui/src/shared/components/Select/types.ts new file mode 100644 index 00000000..31fa1dba --- /dev/null +++ b/new-ui/src/shared/components/Select/types.ts @@ -0,0 +1,48 @@ +import type { FieldBoxProps } from '../FieldBox/types'; + +export type SelectOption = { + key: string | number; + label: string; + value: T; + meta?: unknown; +}; + +export type SelectOptionGroup = { + key?: string | number; + label: string; + options: readonly SelectOption[]; +}; + +export type SelectSingleValue = SelectOption; + +type SelectOptionsSourceProps = + | { + options: readonly SelectOption[]; + groups?: never; + } + | { + options?: never; + groups: readonly SelectOptionGroup[]; + } + | { + options: readonly SelectOption[]; + groups: readonly SelectOptionGroup[]; + }; + +type BaseProps = { + testId?: string; + placeholder?: string; + disabled?: boolean; + className?: string; + label?: string; + required?: boolean; + error?: string; +} & Pick & + SelectOptionsSourceProps; + +export type SelectSingleProps = BaseProps & { + value: SelectSingleValue; + onChange: (v: SelectSingleValue) => void; +}; + +export type SelectProps = SelectSingleProps; diff --git a/new-ui/src/shared/components/SizedBox/SizedBox.tsx b/new-ui/src/shared/components/SizedBox/SizedBox.tsx new file mode 100644 index 00000000..544312fd --- /dev/null +++ b/new-ui/src/shared/components/SizedBox/SizedBox.tsx @@ -0,0 +1,19 @@ +import './style.scss'; + +type Props = { + height: string | number; + width?: string | number; +}; + +/**Spawns a block with a strict size, meant to fill spaces that are not regular like layouts that can't utilize "gap" css property due to irregular gaps in across same axis*/ +export const SizedBox = ({ width, height }: Props) => { + return ( +
+ ); +}; diff --git a/new-ui/src/shared/components/SizedBox/style.scss b/new-ui/src/shared/components/SizedBox/style.scss new file mode 100644 index 00000000..7897d8cc --- /dev/null +++ b/new-ui/src/shared/components/SizedBox/style.scss @@ -0,0 +1,7 @@ +.sized-box { + display: block; + user-select: none; + pointer-events: none; + content: ''; + flex: 0 0 auto; +} diff --git a/new-ui/src/shared/components/Toggle/Toggle.tsx b/new-ui/src/shared/components/Toggle/Toggle.tsx new file mode 100644 index 00000000..a0713353 --- /dev/null +++ b/new-ui/src/shared/components/Toggle/Toggle.tsx @@ -0,0 +1,35 @@ +import './style.scss'; +import clsx from 'clsx'; +import { isPresent } from '../../utils/isPresent'; +import type { ToggleProps } from './types'; + +export const Toggle = ({ + active, + testId, + label, + disabled = false, + onClick, +}: ToggleProps) => { + return ( +
{ + if (!disabled) { + onClick?.(e); + } + }} + > +
+
+
+ {isPresent(label) &&

{label}

} +
+ ); +}; diff --git a/new-ui/src/shared/components/Toggle/style.scss b/new-ui/src/shared/components/Toggle/style.scss new file mode 100644 index 00000000..f8eb8a32 --- /dev/null +++ b/new-ui/src/shared/components/Toggle/style.scss @@ -0,0 +1,72 @@ +.toggle { + --circle-x: 3px; + --circle-shadow: 0 1px 1px 0 rgb(0 0 0 0); + --border: var(--bg); + --bg: var(--bg-white-30); + --circle: var(--fg-white-100); + + cursor: pointer; + display: inline-flex; + flex-flow: row nowrap; + align-items: flex-start; + justify-content: flex-start; + column-gap: var(--spacing-md); + + .inner { + user-select: none; + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + width: 36px; + height: 20px; + box-sizing: border-box; + border-radius: var(--radius-full); + background-color: var(--bg); + border: var(--border-1) solid var(--border); + min-width: 36px; + flex-shrink: 0; + + @include animate(border-color, background-color); + + .circle { + display: inline-block; + width: 14px; + height: 14px; + background-color: var(--circle); + margin-left: var(--circle-x); + border-radius: var(--radius-full); + box-shadow: var(--circle-shadow); + + @include animate(background-color, margin-left, box-shadow); + } + } + + & > p { + user-select: none; + font: var(--t-body-sm-400); + color: var(--fg-white-100); + + @include animate(color); + } + + &.disabled { + --circle: var(--border-disabled); + --circle-x: 3px; + --border: var(--circle); + --bg: var(--bg-white-5); + + cursor: not-allowed; + + p { + color: var(--fg-white-60); + } + } + + &:not(.disabled).active { + --border: var(--bg); + --bg: var(--bg-white-90); + --circle: var(--c-saturated-additional-blue-neutral); + --circle-x: 16px; + } +} diff --git a/new-ui/src/shared/components/Toggle/types.ts b/new-ui/src/shared/components/Toggle/types.ts new file mode 100644 index 00000000..5cf0c9fe --- /dev/null +++ b/new-ui/src/shared/components/Toggle/types.ts @@ -0,0 +1,9 @@ +import type { MouseEventHandler } from 'react'; + +export interface ToggleProps { + active: boolean; + disabled?: boolean; + label?: string; + onClick?: MouseEventHandler; + testId?: string; +} diff --git a/new-ui/src/shared/components/TransferText/TransferText.tsx b/new-ui/src/shared/components/TransferText/TransferText.tsx new file mode 100644 index 00000000..3cf88fd9 --- /dev/null +++ b/new-ui/src/shared/components/TransferText/TransferText.tsx @@ -0,0 +1,17 @@ +import byteSize from 'byte-size'; +import clsx from 'clsx'; +import './style.scss'; + +type Props = { + data: number; +}; + +export const TransferText = ({ data }: Props) => { + const size = byteSize(data, { precision: 1 }); + + return ( +
+ {`${size.value} ${size.unit}`} +
+ ); +}; diff --git a/new-ui/src/shared/components/TransferText/style.scss b/new-ui/src/shared/components/TransferText/style.scss new file mode 100644 index 00000000..46552fa8 --- /dev/null +++ b/new-ui/src/shared/components/TransferText/style.scss @@ -0,0 +1,13 @@ +.transfer-text { + --color: var(--fg-white-100); + + display: flex; + flex-flow: row nowrap; + align-items: center; + column-gap: var(--spacing-xs); + + span { + font: var(--t-body-xxs-500); + color: var(--color); + } +} diff --git a/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx b/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx new file mode 100644 index 00000000..ecac32c8 --- /dev/null +++ b/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx @@ -0,0 +1,50 @@ +import clsx from 'clsx'; +import './style.scss'; +import { ConnectionWatcher } from './components/ConnectionWatcher/ConnectionsWatcher'; + +interface Props { + variant: 'compact' | 'desktop'; +} + +export const WindowHeader = ({ variant }: Props) => { + return ( +
+ +
+

Defguard VPN Client

+ +
+
+ ); +}; + +const LogoIcon = () => { + return ( + + + + + + + + + + + ); +}; diff --git a/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx new file mode 100644 index 00000000..5f3308bc --- /dev/null +++ b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx @@ -0,0 +1,138 @@ +import './style.scss'; +import { + autoUpdate, + FloatingPortal, + size as floatingSize, + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import clsx from 'clsx'; +import { useEffect, useState } from 'react'; +import { api } from '../../../../rust-api/api'; +import { ThemeSpacing, ThemeVariable } from '../../../../types'; +import { isPresent } from '../../../../utils/isPresent'; +import { Divider } from '../../../Divider/Divider'; +import { FloatingMenu } from '../../../FloatingMenu/FloatingMenu'; +import { Icon } from '../../../Icon'; + +export const ConnectionWatcher = () => { + const { mutate: disconnect } = useMutation({ + mutationFn: api.disconnectLocations, + }); + + const { data: connections } = useQuery({ + queryKey: ['alive-connection'], + queryFn: api.getAllActiveConnections, + refetchInterval: 5_000, + }); + + const connected = (connections?.length ?? 0) > 0; + + const [floatingOpen, setFloatingOpen] = useState(false); + + useEffect(() => { + if (!connected) { + setFloatingOpen(false); + } + }, [connected]); + + const { refs, context, floatingStyles } = useFloating({ + placement: 'bottom-start', + open: floatingOpen, + onOpenChange: setFloatingOpen, + middleware: [ + offset(4), + shift(), + floatingSize({ + apply({ rects, elements }) { + elements.floating.style.minWidth = `${rects.reference.width}px`; + }, + }), + ], + whileElementsMounted: autoUpdate, + }); + + const click = useClick(context, { + toggle: true, + enabled: connected, + }); + + const dismiss = useDismiss(context, { + ancestorScroll: true, + outsidePress: true, + }); + + const { getFloatingProps, getReferenceProps } = useInteractions([click, dismiss]); + + return ( + <> +
+ {!connected &&

Not connected

} + {connected && isPresent(connections) && ( +
+ +

{`Connected (${connections.length})`}

+ +
+ )} +
+ {floatingOpen && ( + + +

Connected locations

+ {connections?.map((con) => ( +
+ + + +

{con.name}

+
+ ))} + + +
+
+ )} + + ); +}; diff --git a/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss new file mode 100644 index 00000000..e40bfe4c --- /dev/null +++ b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss @@ -0,0 +1,93 @@ +.connection-watcher { + display: block; + box-sizing: border-box; + border-radius: 8px; + user-select: none; + min-height: 20px; + + &:not(.connected) { + padding: 0 var(--spacing-sm); + display: inline-flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; + border: 1px solid var(--border-default); + background: transparent; + flex-grow: 0; + min-width: 0; + + .no-connection-label { + font: var(--t-body-xxs-500); + color: var(--fg-white-60); + } + } + + &.connected { + display: flex; + flex-flow: row nowrap; + align-items: center; + cursor: pointer; + padding: 0 var(--spacing-xs); + background-color: var(--bg-success); + + > .connected-row { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-xs); + + p { + font: var(--t-body-xxs-500); + color: var(--fg-action); + } + } + } +} + +.connection-watcher-floating { + display: flex; + flex-flow: column; + + .label { + box-sizing: border-box; + padding-left: var(--spacing-sm); + font: var(--t-menu-title); + color: var(--fg-white-60); + min-height: 24px; + } + + .connection { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + box-sizing: border-box; + padding: 0 var(--spacing-sm); + column-gap: var(--spacing-md); + min-height: 36px; + + svg circle { + fill: var(--bg-success); + } + } + + .disconnect { + box-sizing: border-box; + background: transparent; + border: 0; + padding: 0 var(--spacing-sm); + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-sm); + min-height: 36px; + cursor: pointer; + + p { + font: var(--t-menu-text); + color: var(--fg-white-100); + } + } +} diff --git a/new-ui/src/shared/components/WindowHeader/style.scss b/new-ui/src/shared/components/WindowHeader/style.scss new file mode 100644 index 00000000..62603930 --- /dev/null +++ b/new-ui/src/shared/components/WindowHeader/style.scss @@ -0,0 +1,25 @@ +#window-header { + &.variant { + &-compact { + display: flex; + flex-flow: row; + align-items: center; + justify-content: flex-start; + column-gap: var(--spacing-lg); + padding-bottom: var(--spacing-lg); + } + } + + > .info { + display: flex; + flex-flow: column; + align-items: flex-start; + row-gap: var(--spacing-xs); + + > .label { + font: var(--t-body-sm-500); + color: var(--fg-white-100); + text-align: left; + } + } +} diff --git a/new-ui/src/shared/consts.ts b/new-ui/src/shared/consts.ts new file mode 100644 index 00000000..0be8e07a --- /dev/null +++ b/new-ui/src/shared/consts.ts @@ -0,0 +1,5 @@ +export const motionTransitionStandard = { + type: 'tween', + ease: 'easeOut', + duration: 0.16, +} as const; diff --git a/new-ui/src/shared/form-context.tsx b/new-ui/src/shared/form-context.tsx new file mode 100644 index 00000000..a6705eab --- /dev/null +++ b/new-ui/src/shared/form-context.tsx @@ -0,0 +1,4 @@ +import { createFormHookContexts } from '@tanstack/react-form'; + +export const { fieldContext, formContext, useFieldContext, useFormContext } = + createFormHookContexts(); diff --git a/new-ui/src/shared/form.tsx b/new-ui/src/shared/form.tsx new file mode 100644 index 00000000..0507726f --- /dev/null +++ b/new-ui/src/shared/form.tsx @@ -0,0 +1,11 @@ +import { createFormHook } from '@tanstack/react-form'; +import { fieldContext, formContext } from './form-context'; + +export { useFieldContext, useFormContext } from './form-context'; + +export const { useAppForm, withFieldGroup, withForm } = createFormHook({ + fieldContext, + formContext, + fieldComponents: {}, + formComponents: {}, +}); diff --git a/new-ui/src/shared/providers/TauriEventProvider.tsx b/new-ui/src/shared/providers/TauriEventProvider.tsx new file mode 100644 index 00000000..94aabbc2 --- /dev/null +++ b/new-ui/src/shared/providers/TauriEventProvider.tsx @@ -0,0 +1,82 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { listen } from '@tauri-apps/api/event'; +import { Fragment, type PropsWithChildren, useEffect } from 'react'; + +import { + type AddInstanceEventPayload, + type DeadConnectionDroppedPayload, + type DeadConnectionReconnectedPayload, + TauriEvent, +} from '../rust-api/types'; + +export const TauriEventProvider = ({ children }: PropsWithChildren) => { + const queryClient = useQueryClient(); + + useEffect(() => { + const unlisteners = Promise.all([ + listen(TauriEvent.ConnectionChanged, () => { + void queryClient.invalidateQueries({ queryKey: ['alive-connection'] }); + void queryClient.invalidateQueries({ queryKey: ['active-connection'] }); + void queryClient.invalidateQueries({ queryKey: ['locations'] }); + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + void queryClient.invalidateQueries({ queryKey: ['location-details'] }); + void queryClient.invalidateQueries({ queryKey: ['last-connection'] }); + }), + + listen(TauriEvent.InstanceUpdate, () => { + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + void queryClient.invalidateQueries({ queryKey: ['locations'] }); + }), + + listen(TauriEvent.LocationUpdate, () => { + void queryClient.invalidateQueries({ queryKey: ['locations'] }); + void queryClient.invalidateQueries({ queryKey: ['location-details'] }); + }), + + listen(TauriEvent.AppVersionFetch, () => { + void queryClient.invalidateQueries({ queryKey: ['latest-app-version'] }); + }), + + listen(TauriEvent.ConfigChanged, () => { + void queryClient.invalidateQueries({ queryKey: ['settings'] }); + void queryClient.invalidateQueries({ queryKey: ['provisioning-config'] }); + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + }), + + listen(TauriEvent.DeadConnectionDropped, () => { + void queryClient.invalidateQueries({ queryKey: ['alive-connection'] }); + void queryClient.invalidateQueries({ queryKey: ['active-connection'] }); + void queryClient.invalidateQueries({ queryKey: ['locations'] }); + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + }), + + listen( + TauriEvent.DeadConnectionReconnected, + () => { + void queryClient.invalidateQueries({ queryKey: ['alive-connection'] }); + void queryClient.invalidateQueries({ queryKey: ['active-connection'] }); + void queryClient.invalidateQueries({ queryKey: ['locations'] }); + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + }, + ), + + listen(TauriEvent.ApplicationConfigChanged, () => { + void queryClient.invalidateQueries({ queryKey: ['settings'] }); + }), + + listen(TauriEvent.AddInstance, () => { + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + }), + + listen(TauriEvent.UuidMismatch, () => { + void queryClient.invalidateQueries({ queryKey: ['instances'] }); + }), + ]); + + return () => { + void unlisteners.then((fns) => fns.forEach((fn) => void fn())); + }; + }, [queryClient]); + + return {children}; +}; diff --git a/new-ui/src/shared/rust-api/api.ts b/new-ui/src/shared/rust-api/api.ts new file mode 100644 index 00000000..4628ab90 --- /dev/null +++ b/new-ui/src/shared/rust-api/api.ts @@ -0,0 +1,171 @@ +import { getVersion } from '@tauri-apps/api/app'; + +import { invoke } from '@tauri-apps/api/core'; + +import type { + ActiveConnectionSummary, + AppConfig, + AppConfigPatch, + Connection, + ConnectionArgs, + EdgeRequestHeaders, + InstanceInfo, + LocationDetails, + LocationDetailsArgs, + LocationInfo, + LocationStats, + NewAppVersionInfo, + ProvisioningConfig, + RoutingArgs, + SaveConfigArgs, + SaveDeviceConfigResponse, + SetLocationMfaMethodArgs, + StatsArgs, + TunnelInfo, + TunnelRequest, + UpdateInstanceArgs, +} from './types'; +import { TauriCommand } from './types'; + +const getInstances = (): Promise => invoke(TauriCommand.AllInstances); + +const deleteInstance = (instanceId: number): Promise => + invoke(TauriCommand.DeleteInstance, { instanceId }); + +const updateInstance = (args: UpdateInstanceArgs): Promise => + invoke(TauriCommand.UpdateInstance, args); + +const saveDeviceConfig = (args: SaveConfigArgs): Promise => + invoke(TauriCommand.SaveDeviceConfig, args); + +const getLocations = (instanceId: number): Promise => + invoke(TauriCommand.AllLocations, { instanceId }); + +const getLocationDetails = (args: LocationDetailsArgs): Promise => + invoke(TauriCommand.LocationInterfaceDetails, args); + +const updateLocationRouting = (args: RoutingArgs): Promise => + invoke(TauriCommand.UpdateLocationRouting, args); + +const setLocationMfaMethod = (args: SetLocationMfaMethodArgs): Promise => + invoke(TauriCommand.SetLocationMfaMethod, args); + +const connect = (args: ConnectionArgs): Promise => + invoke(TauriCommand.Connect, args); + +const disconnect = (args: ConnectionArgs): Promise => + invoke(TauriCommand.Disconnect, args); + +const getLastConnection = (args: ConnectionArgs): Promise => + invoke(TauriCommand.LastConnection, args); + +const getConnectionHistory = (args: ConnectionArgs): Promise => + invoke(TauriCommand.AllConnections, args); + +const getActiveConnection = (args: ConnectionArgs): Promise => + invoke(TauriCommand.ActiveConnection, args); + +const getLocationStats = (args: StatsArgs): Promise => + invoke(TauriCommand.LocationStats, args); + +const getTunnels = (): Promise => invoke(TauriCommand.AllTunnels); + +const getTunnelDetails = (tunnelId: number): Promise => + invoke(TauriCommand.TunnelDetails, { tunnelId }); + +const parseTunnelConfig = ( + filename: string, + config: string, +): Promise> => + invoke(TauriCommand.ParseTunnelConfig, { filename, config }); + +const saveTunnel = (tunnel: TunnelRequest): Promise => + invoke(TauriCommand.SaveTunnel, { tunnel }); + +const updateTunnel = (tunnel: TunnelRequest): Promise => + invoke(TauriCommand.UpdateTunnel, { tunnel }); + +const deleteTunnel = (tunnelId: number): Promise => + invoke(TauriCommand.DeleteTunnel, { tunnelId }); + +const getAppConfig = (): Promise => invoke(TauriCommand.GetAppConfig); + +const setAppConfig = ( + configPatch: AppConfigPatch, + emitEvent: boolean, +): Promise => invoke(TauriCommand.SetAppConfig, { configPatch, emitEvent }); + +const getProvisioningConfig = (): Promise => + invoke(TauriCommand.GetProvisioningConfig); + +const getPlatformHeader = (): Promise => invoke(TauriCommand.GetPlatformHeader); + +const getLatestAppVersion = (): Promise => + invoke(TauriCommand.GetLatestAppVersion); + +const openLink = (link: string): Promise => invoke(TauriCommand.OpenLink, { link }); + +const startGlobalLogWatcher = (): Promise => + invoke(TauriCommand.StartGlobalLogWatcher); + +const stopGlobalLogWatcher = (): Promise => + invoke(TauriCommand.StopGlobalLogWatcher); + +const getAllActiveConnections = (): Promise => + invoke(TauriCommand.AllActiveConnections); + +const disconnectLocations = (locationIds: number[]): Promise => + invoke(TauriCommand.DisconnectLocations, { locationIds }); + +const getEdgeRequestHeaders = async (): Promise => { + const platform = await getPlatformHeader(); + const version = await getVersion().catch(() => 'unknown'); + return { + 'defguard-client-platform': platform, + 'defguard-client-version': version, + }; +}; + +const swapToOldUi = async () => invoke(TauriCommand.SwapToOldUi); + +export const api = { + getEdgeRequestHeaders, + // Instances + getInstances, + deleteInstance, + updateInstance, + saveDeviceConfig, + // Locations + getLocations, + getLocationDetails, + updateLocationRouting, + setLocationMfaMethod, + // Connections + connect, + disconnect, + getLastConnection, + getConnectionHistory, + getActiveConnection, + getLocationStats, + // Tunnels + getTunnels, + getTunnelDetails, + parseTunnelConfig, + saveTunnel, + updateTunnel, + deleteTunnel, + // App config + getAppConfig, + setAppConfig, + // Misc + getProvisioningConfig, + getPlatformHeader, + getLatestAppVersion, + openLink, + startGlobalLogWatcher, + stopGlobalLogWatcher, + getAllActiveConnections, + disconnectLocations, + // Window + swapToOldUi, +}; diff --git a/new-ui/src/shared/rust-api/query.ts b/new-ui/src/shared/rust-api/query.ts new file mode 100644 index 00000000..d42ef1a6 --- /dev/null +++ b/new-ui/src/shared/rust-api/query.ts @@ -0,0 +1,88 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { api } from './api'; +import type { ConnectionArgs, LocationDetailsArgs, StatsArgs } from './types'; + +export const getAllActiveConnectionQueryOptions = queryOptions({ + queryKey: ['alive-connections'] as const, + queryFn: api.getAllActiveConnections, + refetchInterval: 5_000, +}); + +export const getInstancesQueryOptions = queryOptions({ + queryKey: ['instances'] as const, + queryFn: () => api.getInstances(), + refetchInterval: 30_000, +}); + +export const getLocationsQueryOptions = (instanceId: number) => + queryOptions({ + queryKey: ['locations', instanceId] as const, + queryFn: () => api.getLocations(instanceId), + }); + +export const getLocationDetailsQueryOptions = (args: LocationDetailsArgs) => + queryOptions({ + queryKey: ['location-details', args.locationId, args.connectionType] as const, + queryFn: () => api.getLocationDetails(args), + }); + +export const getLastConnectionQueryOptions = (args: ConnectionArgs) => + queryOptions({ + queryKey: ['last-connection', args.locationId, args.connectionType] as const, + queryFn: () => api.getLastConnection(args), + }); + +export const getConnectionHistoryQueryOptions = (args: ConnectionArgs) => + queryOptions({ + queryKey: ['connection-history', args.locationId, args.connectionType] as const, + queryFn: () => api.getConnectionHistory(args), + }); + +export const getActiveConnectionQueryOptions = (args: ConnectionArgs) => + queryOptions({ + queryKey: ['active-connection', args.locationId, args.connectionType] as const, + queryFn: () => api.getActiveConnection(args), + }); + +export const getLocationStatsQueryOptions = (args: StatsArgs) => + queryOptions({ + queryKey: [ + 'location-stats', + args.locationId, + args.connectionType, + args.from, + ] as const, + queryFn: () => api.getLocationStats(args), + }); + +export const getTunnelsQueryOptions = queryOptions({ + queryKey: ['tunnels'] as const, + queryFn: () => api.getTunnels(), +}); + +export const getTunnelDetailsQueryOptions = (tunnelId: number) => + queryOptions({ + queryKey: ['tunnel-details', tunnelId] as const, + queryFn: () => api.getTunnelDetails(tunnelId), + }); + +export const getAppConfigQueryOptions = queryOptions({ + queryKey: ['settings'] as const, + queryFn: () => api.getAppConfig(), +}); + +export const getLatestAppVersionQueryOptions = queryOptions({ + queryKey: ['latest-app-version'] as const, + queryFn: () => api.getLatestAppVersion(), +}); + +export const getProvisioningConfigQueryOptions = queryOptions({ + queryKey: ['provisioning-config'] as const, + queryFn: () => api.getProvisioningConfig(), +}); + +export const getPlatformHeaderQueryOptions = queryOptions({ + queryKey: ['platform-header'] as const, + queryFn: () => api.getPlatformHeader(), +}); diff --git a/new-ui/src/shared/rust-api/types.ts b/new-ui/src/shared/rust-api/types.ts new file mode 100644 index 00000000..4b852311 --- /dev/null +++ b/new-ui/src/shared/rust-api/types.ts @@ -0,0 +1,343 @@ +export type EdgeRequestHeaders = { + 'defguard-client-version': string; + 'defguard-client-platform': string; +}; + +export const AppTheme = { + Light: 'light', + Dark: 'dark', +} as const; + +export type AppThemeValue = (typeof AppTheme)[keyof typeof AppTheme]; + +export const AppTrayTheme = { + Color: 'color', + White: 'white', + Black: 'black', + Gray: 'gray', +} as const; + +export type AppTrayTheme = (typeof AppTrayTheme)[keyof typeof AppTrayTheme]; + +export const LogLevel = { + Off: 'OFF', + Error: 'ERROR', + Warn: 'WARN', + Info: 'INFO', + Debug: 'DEBUG', + Trace: 'TRACE', +} as const; + +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; + +export const ClientTrafficPolicy = { + None: 'none', + DisableAllTraffic: 'disable_all_traffic', + ForceAllTraffic: 'force_all_traffic', +} as const; + +export type ClientTrafficPolicy = + (typeof ClientTrafficPolicy)[keyof typeof ClientTrafficPolicy]; + +export const LocationMfaMode = { + Disabled: 'disabled', + Internal: 'internal', + External: 'external', +} as const; + +export type LocationMfaMode = (typeof LocationMfaMode)[keyof typeof LocationMfaMode]; + +export const MfaMethod = { + Totp: 'totp', + Email: 'email', + Oidc: 'oidc', + Biometric: 'biometric', + MobileApprove: 'mobileapprove', +} as const; + +export type MfaMethodValue = (typeof MfaMethod)[keyof typeof MfaMethod]; + +export const ConnectionType = { + Location: 'Location', + Tunnel: 'Tunnel', +} as const; + +export type ConnectionType = (typeof ConnectionType)[keyof typeof ConnectionType]; + +/** Typed enum for every Tauri command available on the backend. */ +export const TauriCommand = { + // Instances + AllInstances: 'all_instances', + DeleteInstance: 'delete_instance', + UpdateInstance: 'update_instance', + SaveDeviceConfig: 'save_device_config', + // Locations + AllLocations: 'all_locations', + LocationInterfaceDetails: 'location_interface_details', + UpdateLocationRouting: 'update_location_routing', + SetLocationMfaMethod: 'set_location_mfa_method', + // Connections + Connect: 'connect', + Disconnect: 'disconnect', + LastConnection: 'last_connection', + AllConnections: 'all_connections', + ActiveConnection: 'active_connection', + LocationStats: 'location_stats', + // Tunnels + AllTunnels: 'all_tunnels', + TunnelDetails: 'tunnel_details', + ParseTunnelConfig: 'parse_tunnel_config', + SaveTunnel: 'save_tunnel', + UpdateTunnel: 'update_tunnel', + DeleteTunnel: 'delete_tunnel', + // App config + GetAppConfig: 'command_get_app_config', + SetAppConfig: 'command_set_app_config', + // Misc + GetProvisioningConfig: 'get_provisioning_config', + GetPlatformHeader: 'get_platform_header', + GetLatestAppVersion: 'get_latest_app_version', + OpenLink: 'open_link', + StartGlobalLogWatcher: 'start_global_logwatcher', + StopGlobalLogWatcher: 'stop_global_logwatcher', + AllActiveConnections: 'all_active_connections', + DisconnectLocations: 'disconnect_locations', + //Window + SwapToOldUi: 'swap_to_old_ui', +} as const; + +export type TauriCommand = (typeof TauriCommand)[keyof typeof TauriCommand]; + +/** Typed enum for every Tauri event emitted by the backend. */ +export const TauriEvent = { + ConnectionChanged: 'connection-changed', + InstanceUpdate: 'instance-update', + LocationUpdate: 'location-update', + AppVersionFetch: 'app-version-fetch', + ConfigChanged: 'config-changed', + DeadConnectionDropped: 'dead-connection-dropped', + DeadConnectionReconnected: 'dead-connection-reconnected', + ApplicationConfigChanged: 'application-config-changed', + AddInstance: 'add-instance', + MfaTrigger: 'mfa-trigger', + VersionMismatch: 'version-mismatch', + UuidMismatch: 'uuid-mismatch', +} as const; + +export type TauriEventValue = (typeof TauriEvent)[keyof typeof TauriEvent]; + +/** Payload for the `dead-connection-dropped` event. Mirrors `DeadConnDroppedOut` in events.rs. */ +export type DeadConnectionDroppedPayload = { + name: string; + con_type: ConnectionType; + peer_alive_period: number; +}; + +/** Payload for the `dead-connection-reconnected` event. Mirrors `DeadConnReconnected` in events.rs. */ +export type DeadConnectionReconnectedPayload = { + name: string; + con_type: ConnectionType; + peer_alive_period: number; +}; + +/** Payload for the `add-instance` event. Mirrors `AddInstancePayload` in events.rs. */ +export type AddInstanceEventPayload = { + token: string; + url: string; +}; + +export type ActiveConnectionSummary = { + id: number; + name: string; + connection_type: ConnectionType; +}; + +export type AppConfig = { + theme: AppThemeValue; + tray_theme: AppTrayTheme; + check_for_updates: boolean; + log_level: LogLevel; + /** Idle seconds before the connection is automatically dropped. */ + peer_alive_period: number; + /** Maximum transmission unit; 0 means system default. */ + mtu: number; +}; + +export type AppConfigPatch = Partial; + +export type InstanceInfo = { + id: number; + name: string; + /** Server-side UUID (not the SQLite row id). */ + uuid: string; + url: string; + proxy_url: string; + /** True when at least one location of this instance has an active connection. */ + active: boolean; + pubkey: string; + client_traffic_policy: ClientTrafficPolicy; + enterprise_enabled: boolean; + openid_display_name: string | null; +}; + +export type LocationInfo = { + id: number; + instance_id: number; + name: string; + address: string; + endpoint: string; + active: boolean; + route_all_traffic: boolean; + connection_type: ConnectionType; + pubkey: string; + network_id: number; + location_mfa_mode: LocationMfaMode; + mfa_method?: MfaMethodValue; +}; + +export type LocationStats = { + collected_at: number; + download: number; + upload: number; +}; + +export type Connection = { + id: number; + location_id: number; + connected_from: string; + start: string; + end: string; + upload?: number; + download?: number; +}; + +export type TunnelInfo = { + id?: number; + name: string; + address: string; + endpoint: string; + route_all_traffic: boolean; + active: boolean; + connection_type: ConnectionType; + instance_id: number; + network_id: number; + pubkey: string; + prvkey: string; + server_pubkey: string; + preshared_key?: string; + allowed_ips?: string; + dns?: string; + persistent_keep_alive: number; + pre_up?: string; + post_up?: string; + pre_down?: string; + post_down?: string; +}; + +export type LocationDetails = { + location_id: number; + name: string; + pubkey: string; + address: string; + dns?: string; + listen_port: number; + peer_pubkey: string; + peer_endpoint: string; + allowed_ips: string; + persistent_keepalive_interval?: number; + last_handshake?: number; +}; + +export type NewAppVersionInfo = { + version: string; + release_date: string; + release_notes_url: string; + update_url: string; +}; + +export type ProvisioningConfig = { + enrollment_token: string; + enrollment_url: string; +}; + +export type Device = { + id: number; + name: string; + pubkey: string; + privateKey?: string; + user_id: number; + created_at: number; +}; + +export type DeviceConfig = { + network_id: number; + network_name: string; + config: string; +}; + +export type CreateDeviceResponse = { + device: Device; + configs: DeviceConfig[]; + instance: InstanceInfo; +}; + +export type SaveDeviceConfigResponse = { + instance: InstanceInfo; + locations: LocationInfo[]; +}; + +// ── Request argument types ─────────────────────────────────────────────────── + +export type ConnectionArgs = { + locationId: number; + connectionType: ConnectionType; + presharedKey?: string; +}; + +export type RoutingArgs = { + locationId: number; + connectionType: ConnectionType; + routeAllTraffic?: boolean; +}; + +export type StatsArgs = { + locationId: number; + connectionType: ConnectionType; + from?: string; +}; + +export type LocationDetailsArgs = { + locationId: number; + connectionType: ConnectionType; +}; + +export type TunnelRequest = { + name: string; + pubkey: string; + prvkey: string; + address: string; + server_pubkey: string; + allowed_ips?: string; + endpoint: string; + dns?: string; + persistent_keep_alive: number; + pre_up?: string; + post_up?: string; + pre_down?: string; + post_down?: string; +}; + +export type SaveConfigArgs = { + privateKey: string; + response: CreateDeviceResponse; +}; + +export type UpdateInstanceArgs = { + instanceId: number; + response: CreateDeviceResponse; +}; + +export type SetLocationMfaMethodArgs = { + locationId: number; + mfaMethod: MfaMethodValue; +}; diff --git a/new-ui/src/shared/scss/_base.scss b/new-ui/src/shared/scss/_base.scss new file mode 100644 index 00000000..1efbcd5b --- /dev/null +++ b/new-ui/src/shared/scss/_base.scss @@ -0,0 +1,73 @@ +*:-moz-focus-inner { + border: 0; +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + transition: background-color 8553600s; + -webkit-text-fill-color: #fff !important; +} + +html, +body { + padding: 0; + margin: 0; +} + +// for apple +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: auto; +} + +#root, +#app { + min-height: 100dvh; + overflow: hidden; +} + +input[type='number']::-webkit-outer-spin-button, +input[type='number']::-webkit-inner-spin-button { + appearance: none; + margin: 0; +} + +input[type='number'] { + appearance: textfield; +} + +p, +span, +div, +section, +a, +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + color: var(--fg-white-100); + font-family: + geist, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Open Sans', + 'Helvetica Neue', + sans-serif; +} + +ul, +ol { + margin: 0; + padding: 0; +} diff --git a/new-ui/src/shared/scss/_fonts.scss b/new-ui/src/shared/scss/_fonts.scss new file mode 100644 index 00000000..88635eb6 --- /dev/null +++ b/new-ui/src/shared/scss/_fonts.scss @@ -0,0 +1,125 @@ +/* stylelint-disable font-family-name-quotes */ +// fonts.scss +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-RegularItalic.woff2') format('woff2'); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-Medium.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-MediumItalic.woff2') format('woff2'); + font-weight: 500; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-SemiBoldItalic.woff2') format('woff2'); + font-weight: 600; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Geist'; + src: url('/fonts/geist/Geist-BoldItalic.woff2') format('woff2'); + font-weight: 700; + font-style: italic; + font-display: swap; +} + +// source-code-pro + +@font-face { + font-family: 'Source Code Pro'; + src: url('/fonts/source_code_pro/SourceCodePro-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +// JetBrains Mono + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2') format('woff2'); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2') format('woff2'); + font-weight: 500; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2') format('woff2'); + font-weight: 600; + font-style: italic; + font-display: swap; +} diff --git a/new-ui/src/shared/scss/_shared_tokens.scss b/new-ui/src/shared/scss/_shared_tokens.scss new file mode 100644 index 00000000..5ba87614 --- /dev/null +++ b/new-ui/src/shared/scss/_shared_tokens.scss @@ -0,0 +1,260 @@ +$font-fallback: + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Open Sans', + 'Helvetica Neue', + sans-serif; +$geist: + geist, + #{$font-fallback}; +$source-code-pro: + 'Source Code Pro', + #{$font-fallback}; +$jetbrains: + 'JetBrains Mono', + #{$font-fallback}; +/* stylelint-disable value-keyword-case */ +:root { + // font settings + --font-family-title: #{$geist}; + --font-family-body: #{$geist}; + --font-family-component: #{$geist}; + --font-family-code: #{$source-code-pro}; + --font-family-jetbrains: #{$jetbrains}; + + --t-body-xxs-600: normal 600 11px/14px #{$geist}; + --t-body-xxs-500: normal 500 11px/14px #{$geist}; + --t-body-xxs-400: normal 400 11px/14px #{$geist}; + + --t-body-xs-600: normal 600 12px/16px #{$geist}; + --t-body-xs-500: normal 500 12px/16px #{$geist}; + --t-body-xs-400: normal 400 12px/16px #{$geist}; + + --t-body-sm-600: normal 600 14px/20px #{$geist}; + --t-body-sm-500: normal 500 14px/20px #{$geist}; + --t-body-sm-400: normal 400 14px/20px #{$geist}; + + --t-body-primary-400: normal 400 16px/24px #{$geist}; + --t-body-primary-600: normal 600 16px/24px #{$geist}; + --t-body-primary-500: normal 500 16px/24px #{$geist}; + + --t-h1: normal 600 32px/44px #{$geist}; + --t-h2: normal 600 28px/40px #{$geist}; + --t-h3: normal 600 24px/32px #{$geist}; + --t-h4: normal 600 20px/28px #{$geist}; + --t-h5: normal 600 18px/28px #{$geist}; + + --t-primary-400: normal 400 16px/24px #{$geist}; + --t-primary-500: normal 500 16px/24px #{$geist}; + --t-primary-600: normal 600 16px/24px #{$geist}; + + --t-small-400: normal 400 14px/20px #{$geist}; + --t-small-500: normal 500 14px/20px #{$geist}; + --t-small-600: normal 600 14px/20px #{$geist}; + + --t-tiny-400: normal 400 12px/16px #{$geist}; + --t-tiny-500: normal 500 12px/16px #{$geist}; + --t-tiny-600: normal 600 12px/16px #{$geist}; + + --t-smallest-400: normal 400 11px/16px #{$geist}; + --t-smallest-500: normal 500 11px/16px #{$geist}; + --t-smallest-600: normal 600 11px/16px #{$geist}; + + // border width + --border-1: 1px; + --border-2: 2px; + + // border-radius + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-xxl: 24px; + --radius-xxxl: 32px; + --radius-full: 100px; + + // scale + --size-xs: 16px; + --size-sm: 20px; + --size-md: 24px; + --size-xl: 32px; + --size-2xl: 36px; + --size-3xl: 40px; + --size-4xl: 44px; + --size-5xl: 60px; + + // spacing + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + --spacing-3xl: 32px; + --spacing-4xl: 40px; + --spacing-5xl: 48px; + --spacing-6xl: 64px; + --spacing-7xl: 80px; + --spacing-8xl: 96px; + --spacing-9xl: 120px; + + // color basis + --c-white-100: rgb(255 255 255); + --c-white-90: rgb(255 255 255 / 90%); + --c-white-80: rgb(255 255 255 / 80%); + --c-white-70: rgb(255 255 255 / 70%); + --c-white-60: rgb(255 255 255 / 60%); + --c-white-50: rgb(255 255 255 / 50%); + --c-white-40: rgb(255 255 255 / 40%); + --c-white-30: rgb(255 255 255 / 30%); + --c-white-20: rgb(255 255 255 / 20%); + --c-white-10: rgb(255 255 255 / 10%); + --c-white-5: rgb(255 255 255 / 5%); + --c-dark-neutral-1400: rgb(20 21 23); + --c-dark-neutral-1300: rgb(25 26 28); + --c-dark-neutral-1200: rgb(36 38 41); + --c-dark-neutral-1100: rgb(46 49 54); + --c-dark-neutral-1000: rgb(50 54 60); + --c-dark-neutral-900: rgb(61 67 75); + --c-dark-neutral-800: rgb(74 80 89); + --c-dark-neutral-700: rgb(94 102 114); + --c-dark-neutral-600: rgb(126 135 148); + --c-dark-neutral-500: rgb(147 156 169); + --c-dark-neutral-400: rgb(162 172 186); + --c-dark-neutral-300: rgb(184 192 205); + --c-dark-neutral-200: rgb(223 227 233); + --c-dark-neutral-100: rgb(240 242 245); + --c-dark-neutral-50: rgb(247 248 250); + --c-saturated-additional-error: rgb(204 60 60); + --c-saturated-additional-warning: rgb(255 149 0); + --c-saturated-additional-success: rgb(116 255 184); + --c-saturated-additional-blue-neutral: rgb(80 115 225); + --c-saturated-red-800: rgb(35 24 26); + --c-saturated-red-700: rgb(124 37 37); + --c-saturated-red-600: rgb(143 42 42); + --c-saturated-red-500: rgb(204 60 60); + --c-saturated-red-400: rgb(213 93 93); + --c-saturated-red-300: rgb(225 142 142); + --c-saturated-red-200: rgb(234 175 175); + --c-saturated-red-100: rgb(250 236 236); + --c-saturated-red-500-transparent: rgb(204 60 60 / 20%); + --c-saturated-dark-blue-100: rgb(0 25 137); + --c-saturated-dark-blue-90: rgb(0 25 137 / 90%); + --c-saturated-dark-blue-80: rgb(0 25 137 / 80%); + --c-saturated-dark-blue-70: rgb(0 25 137 / 70%); + --c-saturated-dark-blue-60: rgb(0 25 137 / 60%); + --c-saturated-dark-blue-50: rgb(0 25 137 / 50%); + --c-saturated-dark-blue-40: rgb(0 25 137 / 40%); + --c-saturated-dark-blue-30: rgb(0 25 137 / 30%); + --c-saturated-dark-blue-20: rgb(0 25 137 / 20%); + --c-saturated-dark-blue-10: rgb(0 25 137 / 10%); + --c-saturated-dark-blue-5: rgb(0 25 137 / 5%); + --c-saturated-green-700: rgb(2 78 23); + --c-saturated-green-600: rgb(2 90 27); + --c-saturated-green-500: rgb(3 128 38); + --c-saturated-green-400: rgb(46 150 75); + --c-saturated-green-300: rgb(109 181 129); + --c-saturated-green-200: rgb(152 203 166); + --c-saturated-green-100: rgb(230 242 233); + --c-saturated-green-500-transparent: rgb(3 128 38 / 8%); + --c-saturated-orange-700: rgb(156 91 0); + --c-saturated-orange-600: rgb(179 104 0); + --c-saturated-orange-500: rgb(255 149 0); + --c-saturated-orange-400: rgb(255 167 43); + --c-saturated-orange-300: rgb(255 194 107); + --c-saturated-orange-200: rgb(255 212 150); + --c-saturated-orange-100: rgb(255 244 230); + --c-saturated-orange-500-transparent: rgb(255 149 0 / 8%); + --c-saturated-violet-700: rgb(53 36 82); + --c-saturated-violet-600: rgb(83 55 128); + --c-saturated-violet-500: rgb(102 55 178); + --c-saturated-violet-400: rgb(136 106 186); + --c-saturated-violet-300: rgb(172 151 207); + --c-saturated-violet-200: rgb(196 181 221); + --c-saturated-violet-100: rgb(241 237 247); + --c-saturated-violet-transparent: rgb(102 55 178 / 8%); + --c-saturated-blue-800: rgb(21 24 31); + --c-saturated-blue-700: rgb(38 57 115); + --c-saturated-blue-600: rgb(51 77 156); + --c-saturated-blue-500: rgb(57 97 219); + --c-saturated-blue-400: rgb(87 119 217); + --c-saturated-blue-300: rgb(140 161 222); + --c-saturated-blue-200: rgb(180 196 242); + --c-saturated-blue-100: rgb(237 241 252); + --c-saturated-blue-50: rgb(249 250 254); + --c-saturated-blue-500-transparent: rgb(50 92 219 / 8%); + + // Components + + // Menu + --t-menu-title: 600 12px/24px #{$geist}; + --t-menu-text: 400 14px/24px #{$geist}; + --menu-spacing-icon: var(--spacing-md); + --menu-padding-sides: var(--spacing-sm); + --menu-border-radius-group: var(--radius-lg); + --menu-border-radius-item: var(--radius-md); + --menu-height: var(--size-2xl); + + // Icons + + --icon-size-lg: 32px; + --icon-size: 20px; + --icon-size-xs: 16px; + + // Modals + + --modal-title-size: 16px; + --modal-size-md: 640px; + --modal-size-sm: 480px; + --modal-spacing: var(--spacing-md); + --modal-spacing-sides: var(--spacing-lg); + --modal-border-radius: var(--radius-lg); + + // Buttons + + --t-button-label-big: normal 500 14px / normal #{$geist}; + --t-button-label-primary: normal 500 14px / normal #{$geist}; + + // Badge + --badge-border-radius: var(--radius-md); + --badge-spacing: var(--spacing-sm); + --badge-gap: var(--spacing-xs); + --t-badge: var(--t-body-xs-500); + --badge-height: 24px; + + // Inputs + --t-input-title: normal 500 12px / 16px #{$geist}; + --t-input-text-primary: normal 400 14px / 20px #{$geist}; + --t-input-text-big: normal 400 16px / 20px #{$geist}; + --t-input-error-message: normal 400 12px / 16px #{$geist}; + --input-border-radius: var(--radius-md); + --input-size-primary: 36px; + --input-size-lg: 44px; + --input-spacing-xs: var(--spacing-xs); + --input-spacing-sm: var(--spacing-sm); + --input-spacing-lg: var(--spacing-md); + + // Tooltip + --t-tooltip: normal 400 12px / 16px #{$geist}; + --tooltip-letter-spacing: 0.3; + --tooltip-spacing: var(--spacing-sm) var(--spacing-md); + + // custom + + // how much space does error message in all takes + --form-field-error-space: 24px; + + // how much space error should gap from main container + --form-field-error-gap: 8px; + + // navigation + --nav-width: 270px; + + --menu-shadow: 0 4px 12px 0 rgb(0 0 0 / 7%); +} diff --git a/new-ui/src/shared/scss/_skeleton.scss b/new-ui/src/shared/scss/_skeleton.scss new file mode 100644 index 00000000..6bb25ae6 --- /dev/null +++ b/new-ui/src/shared/scss/_skeleton.scss @@ -0,0 +1,4 @@ +.react-loading-skeleton { + --base-color: var(--bg-disabled); + --highlight-color: var(--bg-active); +} diff --git a/new-ui/src/shared/scss/_themes.scss b/new-ui/src/shared/scss/_themes.scss new file mode 100644 index 00000000..1746ccb8 --- /dev/null +++ b/new-ui/src/shared/scss/_themes.scss @@ -0,0 +1,56 @@ +:root[data-theme='light'] { + --bg-white-100: var(--c-white-100); + --bg-white-90: var(--c-white-90); + --bg-white-80: var(--c-white-80); + --bg-white-70: var(--c-white-70); + --bg-white-60: var(--c-white-60); + --bg-white-50: var(--c-white-50); + --bg-white-40: var(--c-white-40); + --bg-white-30: var(--c-white-30); + --bg-white-20: var(--c-white-20); + --bg-white-10: var(--c-white-10); + --bg-white-5: var(--c-white-5); + --fg-white-100: var(--c-white-100); + --fg-white-90: var(--c-white-90); + --fg-white-80: var(--c-white-80); + --fg-white-70: var(--c-white-70); + --fg-white-60: var(--c-white-60); + --fg-white-50: var(--c-white-50); + --fg-white-40: var(--c-white-40); + --fg-white-30: var(--c-white-30); + --fg-white-20: var(--c-white-20); + --fg-white-10: var(--c-white-10); + --fg-white-5: var(--c-white-5); + --bg-critical: var(--c-saturated-red-500); + --bg-neutral: var(--c-saturated-additional-blue-neutral); + --bg-dark-blue-60: var(--c-saturated-dark-blue-60); + --bg-dark-blue-40: var(--c-saturated-dark-blue-40); + --bg-dark-blue-30: var(--c-saturated-dark-blue-30); + --bg-dark-blue-20: var(--c-saturated-dark-blue-20); + --bg-success: var(--c-saturated-additional-success); + --bg-warning: var(--c-saturated-orange-500); + --bg-critical-faded: var(--c-saturated-red-400); + --bg-critical-muted: var(--c-saturated-red-200); + --bg-critical-disabled: var(--c-saturated-red-500-transparent); + --border-bg: var(--c-white-100); + --border-action: var(--fg-white-100); + --border-action-disabled: var(--fg-white-20); + --border-default: var(--c-white-40); + --border-disabled: var(--c-white-20); + --border-emphasis: var(--c-white-60); + --border-muted: var(--c-white-20); + --border-faded: var(--c-white-10); + --border-critical: var(--c-saturated-red-200); + --border-success: var(--c-saturated-additional-success); + --border-warning: var(--c-saturated-orange-300); + --fg-action: var(--c-saturated-blue-500); + --fg-attention: var(--c-saturated-orange-300); + --fg-critical: var(--c-saturated-red-200); + --fg-critical-muted: var(--c-saturated-red-100); + --fg-black: var(--c-dark-neutral-1400); + --fg-faded: var(--c-dark-neutral-900); + --fg-neutral: var(--c-dark-neutral-800); + --fg-muted: var(--c-dark-neutral-600); + --fg-disabled: var(--c-dark-neutral-500); + --fg-success: var(--c-saturated-additional-success); +} diff --git a/new-ui/src/shared/scss/global/_animate.scss b/new-ui/src/shared/scss/global/_animate.scss new file mode 100644 index 00000000..024cc924 --- /dev/null +++ b/new-ui/src/shared/scss/global/_animate.scss @@ -0,0 +1,11 @@ +@use 'sass:list'; + +// for now no prop required +@mixin animate($properties...) { + transition-timing-function: ease-out; + transition-duration: 160ms; + + @if list.length($properties) > 0 { + transition-property: $properties; + } +} diff --git a/new-ui/src/shared/scss/global/_breakpoints.scss b/new-ui/src/shared/scss/global/_breakpoints.scss new file mode 100644 index 00000000..cda8eaa8 --- /dev/null +++ b/new-ui/src/shared/scss/global/_breakpoints.scss @@ -0,0 +1,112 @@ +@use 'sass:map'; +@use 'sass:list'; + +$grid-breakpoints: ( + xs: 0, + sm: 320px, + md: 768px, + lg: 992px, + xl: 1200px, + xxl: 1600px, +); + +@function break-next( + $name, + $breakpoints: $grid-breakpoints, + $breakpoint-names: map.keys($breakpoints) +) { + $n: list.index($breakpoint-names, $name); + + @if not $n { + @error "breakpoint `#{$name}` not found in `#{$breakpoints}`"; + } + + @if $n < list.length($breakpoint-names) { + @return list.nth($breakpoint-names, $n + 1); + } + + @return null; +} + +@function break-min($name, $breakpoints: $grid-breakpoints) { + $min: map.get($breakpoints, $name); + + @if $min != 0 { + @return $min; + } + + @return null; +} + +@function break-max($name, $breakpoints: $grid-breakpoints) { + $max: map.get($breakpoints, $name); + + @if $max and $max > 0 { + @return $max - 0.02; + } + + @return null; +} + +@mixin break-up($name, $breakpoints: $grid-breakpoints) { + $min: break-min($name, $breakpoints); + + @if $min { + @media (min-width: $min) { + @content; + } + } @else { + @content; + } +} + +@mixin break-down($name, $breakpoints: $grid-breakpoints) { + $max: break-max($name, $breakpoints); + + @if $max { + @media (max-width: $max) { + @content; + } + } @else { + @content; + } +} + +@mixin break-between($lower, $upper, $breakpoints: $grid-breakpoints) { + $min: break-min($lower, $breakpoints); + $max: break-max($upper, $breakpoints); + + @if $min != null and $max != null { + @media (min-width: $min) and (max-width: $max) { + @content; + } + } @else if $max == null { + @include break-up($lower, $breakpoints) { + @content; + } + } @else if $min == null { + @include break-down($upper, $breakpoints) { + @content; + } + } +} + +@mixin break-only($name, $breakpoints: $grid-breakpoints) { + $min: break-min($name, $breakpoints); + $next: break-next($name, $breakpoints); + $max: break-max($next, $breakpoints); + + @if $min != null and $max != null { + @media (min-width: $min) and (max-width: $max) { + @content; + } + } @else if $max == null { + @include break-up($name, $breakpoints) { + @content; + } + } @else if $min == null { + @include break-down($next, $breakpoints) { + @content; + } + } +} diff --git a/new-ui/src/shared/scss/global/index.scss b/new-ui/src/shared/scss/global/index.scss new file mode 100644 index 00000000..87546734 --- /dev/null +++ b/new-ui/src/shared/scss/global/index.scss @@ -0,0 +1,2 @@ +@forward './animate'; +@forward './breakpoints'; diff --git a/new-ui/src/shared/scss/index.scss b/new-ui/src/shared/scss/index.scss new file mode 100644 index 00000000..f8d430ca --- /dev/null +++ b/new-ui/src/shared/scss/index.scss @@ -0,0 +1,5 @@ +@use './base'; +@use './shared_tokens'; +@use './skeleton'; +@use './themes'; +@use './fonts'; diff --git a/new-ui/src/shared/types.ts b/new-ui/src/shared/types.ts new file mode 100644 index 00000000..70b7b15f --- /dev/null +++ b/new-ui/src/shared/types.ts @@ -0,0 +1,92 @@ +export const Direction = { + UP: 'up', + DOWN: 'down', + LEFT: 'left', + RIGHT: 'right', +} as const; + +export type DirectionValue = (typeof Direction)[keyof typeof Direction]; + +export const Orientation = { + Horizontal: 'horizontal', + Vertical: 'vertical', +} as const; + +export type OrientationValue = (typeof Orientation)[keyof typeof Orientation]; + +export const ThemeSpacing = { + Xs: 'var(--spacing-xs)', + Sm: 'var(--spacing-sm)', + Md: 'var(--spacing-md)', + Lg: 'var(--spacing-lg)', + Xl: 'var(--spacing-xl)', + Xl2: 'var(--spacing-2xl)', + Xl3: 'var(--spacing-3xl)', + Xl4: 'var(--spacing-4xl)', + Xl5: 'var(--spacing-5xl)', + Xl6: 'var(--spacing-6xl)', + Xl7: 'var(--spacing-7xl)', + Xl8: 'var(--spacing-8xl)', + Xl9: 'var(--spacing-9xl)', +} as const; + +export type ThemeSpacingValue = (typeof ThemeSpacing)[keyof typeof ThemeSpacing]; + +export const ThemeVariable = { + BgWhite100: 'var(--bg-white-100)', + BgWhite90: 'var(--bg-white-90)', + BgWhite80: 'var(--bg-white-80)', + BgWhite70: 'var(--bg-white-70)', + BgWhite60: 'var(--bg-white-60)', + BgWhite50: 'var(--bg-white-50)', + BgWhite40: 'var(--bg-white-40)', + BgWhite30: 'var(--bg-white-30)', + BgWhite20: 'var(--bg-white-20)', + BgWhite10: 'var(--bg-white-10)', + BgWhite5: 'var(--bg-white-5)', + FgWhite100: 'var(--fg-white-100)', + FgWhite90: 'var(--fg-white-90)', + FgWhite80: 'var(--fg-white-80)', + FgWhite70: 'var(--fg-white-70)', + FgWhite60: 'var(--fg-white-60)', + FgWhite50: 'var(--fg-white-50)', + FgWhite40: 'var(--fg-white-40)', + FgWhite30: 'var(--fg-white-30)', + FgWhite20: 'var(--fg-white-20)', + FgWhite10: 'var(--fg-white-10)', + FgWhite5: 'var(--fg-white-5)', + BgCritical: 'var(--bg-critical)', + BgNeutral: 'var(--bg-neutral)', + BgDarkBlue60: 'var(--bg-dark-blue-60)', + BgDarkBlue40: 'var(--bg-dark-blue-40)', + BgDarkBlue30: 'var(--bg-dark-blue-30)', + BgDarkBlue20: 'var(--bg-dark-blue-20)', + BgSuccess: 'var(--bg-success)', + BgWarning: 'var(--bg-warning)', + BgCriticalFaded: 'var(--bg-critical-faded)', + BgCriticalMuted: 'var(--bg-critical-muted)', + BgCriticalDisabled: 'var(--bg-critical-disabled)', + BorderBg: 'var(--border-bg)', + BorderAction: 'var(--border-action)', + BorderActionDisabled: 'var(--border-action-disabled)', + BorderDefault: 'var(--border-default)', + BorderDisabled: 'var(--border-disabled)', + BorderEmphasis: 'var(--border-emphasis)', + BorderMuted: 'var(--border-muted)', + BorderFaded: 'var(--border-faded)', + BorderCritical: 'var(--border-critical)', + BorderSuccess: 'var(--border-success)', + BorderWarning: 'var(--border-warning)', + FgAction: 'var(--fg-action)', + FgAttention: 'var(--fg-attention)', + FgCritical: 'var(--fg-critical)', + FgCriticalMuted: 'var(--fg-critical-muted)', + FgBlack: 'var(--fg-black)', + FgFaded: 'var(--fg-faded)', + FgNeutral: 'var(--fg-neutral)', + FgMuted: 'var(--fg-muted)', + FgDisabled: 'var(--fg-disabled)', + FgSuccess: 'var(--fg-success)', +} as const; + +export type ThemeVariableValue = (typeof ThemeVariable)[keyof typeof ThemeVariable]; diff --git a/new-ui/src/shared/utils/detectClickOutside.ts b/new-ui/src/shared/utils/detectClickOutside.ts new file mode 100644 index 00000000..933609a8 --- /dev/null +++ b/new-ui/src/shared/utils/detectClickOutside.ts @@ -0,0 +1,22 @@ +/** + * Checks if mouse event clicked within any of provided rects + */ +export const detectClickInside = (event: MouseEvent, rects: DOMRect[]) => { + for (const domRect of rects) { + if (domRect) { + const start_x = domRect?.x; + const start_y = domRect?.y; + const end_x = start_x + domRect?.width; + const end_y = start_y + domRect.height; + if ( + event.clientX >= start_x && + event.clientX <= end_x && + event.clientY >= start_y && + event.clientY <= end_y + ) { + return true; + } + } + } + return false; +}; diff --git a/new-ui/src/shared/utils/download.ts b/new-ui/src/shared/utils/download.ts new file mode 100644 index 00000000..d8d09571 --- /dev/null +++ b/new-ui/src/shared/utils/download.ts @@ -0,0 +1,25 @@ +export const downloadText = ( + content: string, + filename: string, + extension: 'txt' | 'pub' | 'conf' = 'txt', +) => { + const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); + downloadFile(blob, filename, extension); +}; + +export const downloadFile = (blob: Blob, filename: string, extension: string) => { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.href = url; + link.style = 'visibility: hidden;'; + link.download = `${filename}.${extension}`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + setTimeout(() => { + URL.revokeObjectURL(url); + }, 5_000); +}; diff --git a/new-ui/src/shared/utils/isComparable.ts b/new-ui/src/shared/utils/isComparable.ts new file mode 100644 index 00000000..120180f1 --- /dev/null +++ b/new-ui/src/shared/utils/isComparable.ts @@ -0,0 +1,10 @@ +export const isComparableWithStrictEquality = (val: unknown) => { + const t = typeof val; + return ( + t === 'number' || + t === 'string' || + t === 'boolean' || + t === 'undefined' || + val === null + ); +}; diff --git a/new-ui/src/shared/utils/isPresent.ts b/new-ui/src/shared/utils/isPresent.ts new file mode 100644 index 00000000..510ce8d4 --- /dev/null +++ b/new-ui/src/shared/utils/isPresent.ts @@ -0,0 +1,3 @@ +export const isPresent = (value: T): value is NonNullable => { + return value !== null && value !== undefined; +}; diff --git a/new-ui/src/shared/utils/mergeRefs.ts b/new-ui/src/shared/utils/mergeRefs.ts new file mode 100644 index 00000000..5d13c288 --- /dev/null +++ b/new-ui/src/shared/utils/mergeRefs.ts @@ -0,0 +1,29 @@ +// extracted from https://github.com/gregberge/react-merge-refs +import type { Ref, RefCallback } from 'react'; + +function assignRef( + ref: Ref | undefined | null, + value: T | null, +): ReturnType> { + if (typeof ref === 'function') { + return ref(value); + } else if (ref) { + ref.current = value; + } +} + +export function mergeRefs(refs: (Ref | undefined)[]): Ref { + return (value: T | null) => { + const cleanups: (() => void)[] = []; + + for (const ref of refs) { + const cleanup = assignRef(ref, value); + const isCleanup = typeof cleanup === 'function'; + cleanups.push(isCleanup ? cleanup : () => assignRef(ref, null)); + } + + return () => { + for (const cleanup of cleanups) cleanup(); + }; + }; +} diff --git a/new-ui/src/shared/utils/mfa.ts b/new-ui/src/shared/utils/mfa.ts new file mode 100644 index 00000000..e7e2228a --- /dev/null +++ b/new-ui/src/shared/utils/mfa.ts @@ -0,0 +1,16 @@ +import type { MfaMethodValue } from '../rust-api/types'; + +export const mfaToText = (factor: MfaMethodValue): string => { + switch (factor) { + case 'email': + return 'Email'; + case 'mobileapprove': + return 'Mobile Client'; + case 'oidc': + return 'OpenID'; + case 'totp': + return 'Authenticator app'; + case 'biometric': + return 'Biometric'; + } +}; diff --git a/new-ui/src/shared/utils/sortByLabel.ts b/new-ui/src/shared/utils/sortByLabel.ts new file mode 100644 index 00000000..ede65fc9 --- /dev/null +++ b/new-ui/src/shared/utils/sortByLabel.ts @@ -0,0 +1,7 @@ +const collator = new Intl.Collator(undefined, { + numeric: true, + sensitivity: 'base', +}); + +export const sortByLabel = (items: readonly T[], selector: (item: T) => string): T[] => + [...items].sort((a, b) => collator.compare(selector(a), selector(b))); diff --git a/new-ui/src/shared/utils/zod.ts b/new-ui/src/shared/utils/zod.ts new file mode 100644 index 00000000..fcad5086 --- /dev/null +++ b/new-ui/src/shared/utils/zod.ts @@ -0,0 +1,10 @@ +import type z from 'zod'; + +export const createZodIssue = ( + message: string, + path: PropertyKey[], +): z.core.$ZodIssueCustom => ({ + code: 'custom', + message, + path, +}); diff --git a/new-ui/tsconfig.app.json b/new-ui/tsconfig.app.json new file mode 100644 index 00000000..7f42e5f7 --- /dev/null +++ b/new-ui/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/new-ui/tsconfig.json b/new-ui/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/new-ui/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/new-ui/tsconfig.node.json b/new-ui/tsconfig.node.json new file mode 100644 index 00000000..d3c52ea6 --- /dev/null +++ b/new-ui/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/new-ui/vite.config.ts b/new-ui/vite.config.ts new file mode 100644 index 00000000..10f4a4e6 --- /dev/null +++ b/new-ui/vite.config.ts @@ -0,0 +1,51 @@ +import react from '@vitejs/plugin-react'; +import autoprefixer from 'autoprefixer'; +import * as path from 'path'; +import { defineConfig } from 'vite'; +import { tanstackRouter } from '@tanstack/router-plugin/vite'; +import { devtools } from '@tanstack/devtools-vite'; + +const host = process.env.TAURI_DEV_HOST; + +// https://vitejs.dev/config/ +export default defineConfig(async ({ command }) => ({ + plugins: [devtools(), tanstackRouter(), react()], + clearScreen: false, + server: { + strictPort: true, + port: 5072, + host: host || false, + hmr: host + ? { + protocol: 'ws', + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ['**/src-tauri/**'], + }, + }, + resolve: { + alias: { + '@scssutils': path.resolve('./src/shared/scss/global'), + }, + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@use "@scssutils" as *;\n`, + }, + }, + postcss: { + plugins: [autoprefixer], + }, + }, + envPrefix: ['VITE_', 'TAURI_'], + base: command === 'build' ? '/new-ui/' : './', + build: { + outDir: '../dist/new-ui', + emptyOutDir: true, + }, +})); diff --git a/package.json b/package.json index d4db1374..ea89a2ac 100644 --- a/package.json +++ b/package.json @@ -82,21 +82,21 @@ "fast-deep-equal": "^3.1.3", "file-saver": "^2.0.5", "get-text-width": "^1.0.3", - "html-react-parser": "^5.2.17", + "html-react-parser": "^6.1.1", "itertools": "^2.6.0", "js-base64": "^3.7.8", "lodash-es": "^4.18.1", "merge-refs": "^2.0.0", "millify": "^6.1.0", "motion": "^12.38.0", - "p-timeout": "^6.1.4", + "p-timeout": "^7.0.1", "prop-types": "^15.8.1", "radash": "^12.1.1", "react": "^19.2.6", "react-auth-code-input": "^3.2.1", "react-click-away-listener": "^2.4.1", "react-dom": "^19.2.6", - "react-hook-form": "^7.75.0", + "react-hook-form": "^7.76.0", "react-hotkeys-hook": "^5.3.2", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", @@ -117,23 +117,23 @@ "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.100.10", "@tanstack/react-query-devtools": "^5.100.10", - "@tauri-apps/cli": "^2.11.1", + "@tauri-apps/cli": "^2.11.2", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.12.4", + "@types/node": "^25.8.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.2.0", - "@vitejs/plugin-react-swc": "^4.3.0", + "@vitejs/plugin-react": "^6.0.2", + "@vitejs/plugin-react-swc": "^4.3.1", "autoprefixer": "^10.5.0", "npm-run-all": "^4.1.5", "postcss": "^8.5.14", "prettier": "^3.8.3", - "sass": "~1.92.1", + "sass": "~1.99.0", "typedoc": "^0.28.19", "typesafe-i18n": "^5.27.1", "typescript": "^5.9.3", - "vite": "^7.3.3" + "vite": "^8.0.13" }, "volta": { "node": "20.5.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6795db2..bf1eda96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 0.27.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@hookform/resolvers': specifier: ^3.10.0 - version: 3.10.0(react-hook-form@7.75.0(react@19.2.6)) + version: 3.10.0(react-hook-form@7.76.0(react@19.2.6)) '@react-hook/resize-observer': specifier: ^2.0.2 version: 2.0.2(react@19.2.6) @@ -102,8 +102,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 html-react-parser: - specifier: ^5.2.17 - version: 5.2.17(@types/react@19.2.14)(react@19.2.6) + specifier: ^6.1.1 + version: 6.1.1(@types/react@19.2.14)(react@19.2.6) itertools: specifier: ^2.6.0 version: 2.6.0 @@ -123,8 +123,8 @@ importers: specifier: ^12.38.0 version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) p-timeout: - specifier: ^6.1.4 - version: 6.1.4 + specifier: ^7.0.1 + version: 7.0.1 prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -144,8 +144,8 @@ importers: specifier: ^19.2.6 version: 19.2.6(react@19.2.6) react-hook-form: - specifier: ^7.75.0 - version: 7.75.0(react@19.2.6) + specifier: ^7.76.0 + version: 7.76.0(react@19.2.6) react-hotkeys-hook: specifier: ^5.3.2 version: 5.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -202,8 +202,8 @@ importers: specifier: ^5.100.10 version: 5.100.10(@tanstack/react-query@5.100.10(react@19.2.6))(react@19.2.6) '@tauri-apps/cli': - specifier: ^2.11.1 - version: 2.11.1 + specifier: ^2.11.2 + version: 2.11.2 '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -211,8 +211,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.12.4 - version: 24.12.4 + specifier: ^25.8.0 + version: 25.8.0 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -220,11 +220,11 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': - specifier: ^5.2.0 - version: 5.2.0(vite@7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0)) + specifier: ^6.0.2 + version: 6.0.2(vite@8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0)) '@vitejs/plugin-react-swc': - specifier: ^4.3.0 - version: 4.3.0(vite@7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0)) + specifier: ^4.3.1 + version: 4.3.1(vite@8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0)) autoprefixer: specifier: ^10.5.0 version: 10.5.0(postcss@8.5.14) @@ -238,8 +238,8 @@ importers: specifier: ^3.8.3 version: 3.8.3 sass: - specifier: ~1.92.1 - version: 1.92.1 + specifier: ~1.99.0 + version: 1.99.0 typedoc: specifier: ^0.28.19 version: 0.28.19(typescript@5.9.3) @@ -250,8 +250,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 vite: - specifier: ^7.3.3 - version: 7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0) + specifier: ^8.0.13 + version: 8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0) packages: @@ -289,10 +289,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -314,18 +310,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -399,6 +383,15 @@ packages: cpu: [x64] os: [win32] + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -453,162 +446,6 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -660,6 +497,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.130.0': + resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -763,8 +609,8 @@ packages: peerDependencies: react: '>=18' - '@reduxjs/toolkit@2.11.2': - resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 || ^19 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -778,149 +624,103 @@ packages: resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - - '@rollup/rollup-android-arm-eabi@4.60.3': - resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.3': - resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + '@rolldown/binding-android-arm64@1.0.1': + resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.3': - resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + '@rolldown/binding-darwin-arm64@1.0.1': + resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.3': - resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + '@rolldown/binding-darwin-x64@1.0.1': + resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.3': - resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.3': - resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + '@rolldown/binding-freebsd-x64@1.0.1': + resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.3': - resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.3': - resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.3': - resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + '@rolldown/binding-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.3': - resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + '@rolldown/binding-linux-arm64-musl@1.0.1': + resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.3': - resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.3': - resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.3': - resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.3': - resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.3': - resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} - cpu: [riscv64] - os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.3': - resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.3': - resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + '@rolldown/binding-linux-s390x-gnu@1.0.1': + resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.3': - resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + '@rolldown/binding-linux-x64-gnu@1.0.1': + resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.3': - resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + '@rolldown/binding-linux-x64-musl@1.0.1': + resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.3': - resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.3': - resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + '@rolldown/binding-openharmony-arm64@1.0.1': + resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.3': - resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} - cpu: [arm64] - os: [win32] + '@rolldown/binding-wasm32-wasi@1.0.1': + resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] - '@rollup/rollup-win32-ia32-msvc@4.60.3': - resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} - cpu: [ia32] + '@rolldown/binding-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.3': - resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + '@rolldown/binding-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.3': - resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@shikijs/engine-oniguruma@3.23.0': resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} @@ -1174,79 +974,79 @@ packages: '@tauri-apps/api@2.11.0': resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} - '@tauri-apps/cli-darwin-arm64@2.11.1': - resolution: {integrity: sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==} + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.11.1': - resolution: {integrity: sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==} + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.1': - resolution: {integrity: sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.11.1': - resolution: {integrity: sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==} + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.11.1': - resolution: {integrity: sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==} + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.11.1': - resolution: {integrity: sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==} + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.11.1': - resolution: {integrity: sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==} + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.11.1': - resolution: {integrity: sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==} + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.11.1': - resolution: {integrity: sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==} + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.11.1': - resolution: {integrity: sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==} + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.11.1': - resolution: {integrity: sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==} + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.11.1': - resolution: {integrity: sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==} + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} engines: {node: '>= 10'} hasBin: true @@ -1283,17 +1083,8 @@ packages: '@tauri-apps/plugin-window-state@2.4.1': resolution: {integrity: sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==} - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} '@types/byte-size@8.1.2': resolution: {integrity: sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==} @@ -1331,9 +1122,6 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -1355,8 +1143,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.12.4': - resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + '@types/node@25.8.0': + resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1389,17 +1177,24 @@ packages: peerDependencies: react: '>= 16.8.0' - '@vitejs/plugin-react-swc@4.3.0': - resolution: {integrity: sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w==} + '@vitejs/plugin-react-swc@4.3.1': + resolution: {integrity: sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 || ^8 - '@vitejs/plugin-react@5.2.0': - resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -1453,8 +1248,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + baseline-browser-mapping@2.10.30: + resolution: {integrity: sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==} engines: {node: '>=6.0.0'} hasBin: true @@ -1732,16 +1527,32 @@ packages: dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-serializer@3.1.1: + resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} + engines: {node: '>=20.19.0'} + domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@3.0.0: + resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} + engines: {node: '>=20.19.0'} + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + domhandler@6.0.1: + resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} + engines: {node: '>=20.19.0'} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + domutils@4.0.2: + resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} + engines: {node: '>=20.19.0'} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -1749,8 +1560,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.353: - resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + electron-to-chromium@1.5.357: + resolution: {integrity: sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1759,9 +1570,9 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -1793,11 +1604,6 @@ packages: es-toolkit@1.46.1: resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1965,11 +1771,11 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - html-dom-parser@5.1.8: - resolution: {integrity: sha512-MCIUng//mF2qTtGHXJWr6OLfHWmg3Pm8ezpfiltF83tizPWY17JxT4dRLE8lykJ5bChJELoY3onQKPbufJHxYA==} + html-dom-parser@7.1.0: + resolution: {integrity: sha512-83BgaFSW/Sj6QTotGenvPvKfGxFzpFfrJNYes77mzqnq+YjVm12d4qeG0+108w4ejnam/+nCnnLuyyJlXkuPtA==} - html-react-parser@5.2.17: - resolution: {integrity: sha512-m+K/7Moq1jodAB4VL0RXSOmtwLUYoAsikZhwd+hGQe5Vtw2dbWfpFd60poxojMU0Tsh9w59mN1QLEcoHz0Dx9w==} + html-react-parser@6.1.1: + resolution: {integrity: sha512-TitVBqKNyZYGVutHdALEtaNZV29QmuBgJcqeWnF4voLunG6YBlMpnm5XCVHQhxhUPW14nw1LW27NReGbCfPU0A==} peerDependencies: '@types/react': 0.14 || 15 || 16 || 17 || 18 || 19 react: 0.14 || 15 || 16 || 17 || 18 || 19 @@ -1980,8 +1786,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - htmlparser2@10.1.0: - resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + htmlparser2@12.0.0: + resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} + engines: {node: '>=20.19.0'} immer@10.2.0: resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} @@ -2168,6 +1975,80 @@ packages: engines: {node: '>=6'} hasBin: true + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2410,9 +2291,9 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - p-timeout@6.1.4: - resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} - engines: {node: '>=14.16'} + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -2516,8 +2397,8 @@ packages: peerDependencies: react: ^19.2.6 - react-hook-form@7.75.0: - resolution: {integrity: sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==} + react-hook-form@7.76.0: + resolution: {integrity: sha512-eKtLGgFeSgkHqQD8J59AMZ9a4uD1D83iSIzt4YlTGD7liDen5rrjcUO1rVIGd9yC1gofryjtHbv+4ny4hkLWlw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -2550,8 +2431,8 @@ packages: peerDependencies: react: '*' - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} peerDependencies: '@types/react': ^18.2.25 || ^19 react: ^18.0 || ^19 @@ -2562,10 +2443,6 @@ packages: redux: optional: true - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - react-router-dom@6.30.3: resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} engines: {node: '>=14.0.0'} @@ -2654,9 +2531,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.60.3: - resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.1: + resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true rxjs@7.8.2: @@ -2674,8 +2551,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass@1.92.1: - resolution: {integrity: sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==} + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} engines: {node: '>=14.0.0'} hasBin: true @@ -2884,8 +2761,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -2946,15 +2823,16 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite@7.3.3: - resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + vite@8.0.13: + resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -2965,12 +2843,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -3125,8 +3005,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -3142,16 +3020,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/runtime@7.29.2': {} '@babel/template@7.28.6': @@ -3212,6 +3080,22 @@ snapshots: '@biomejs/cli-win32-x64@2.4.15': optional: true + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.28.6 @@ -3295,84 +3179,6 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -3422,9 +3228,9 @@ snapshots: - '@types/react' - supports-color - '@hookform/resolvers@3.10.0(react-hook-form@7.75.0(react@19.2.6))': + '@hookform/resolvers@3.10.0(react-hook-form@7.76.0(react@19.2.6))': dependencies: - react-hook-form: 7.75.0(react@19.2.6) + react-hook-form: 7.76.0(react@19.2.6) '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -3445,6 +3251,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.130.0': {} + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -3520,7 +3335,7 @@ snapshots: '@react-hook/passive-layout-effect': 1.2.1(react@19.2.6) react: 19.2.6 - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 @@ -3530,88 +3345,60 @@ snapshots: reselect: 5.1.1 optionalDependencies: react: 19.2.6 - react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) + react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) '@remix-run/router@1.23.2': {} - '@rolldown/pluginutils@1.0.0-rc.3': {} - - '@rolldown/pluginutils@1.0.0-rc.7': {} - - '@rollup/rollup-android-arm-eabi@4.60.3': - optional: true - - '@rollup/rollup-android-arm64@4.60.3': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.3': - optional: true - - '@rollup/rollup-darwin-x64@4.60.3': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.3': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.3': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.3': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.3': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.3': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.3': + '@rolldown/binding-android-arm64@1.0.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.3': + '@rolldown/binding-darwin-arm64@1.0.1': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.3': + '@rolldown/binding-darwin-x64@1.0.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.3': + '@rolldown/binding-freebsd-x64@1.0.1': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.3': + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.3': + '@rolldown/binding-linux-arm64-gnu@1.0.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.3': + '@rolldown/binding-linux-arm64-musl@1.0.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.3': + '@rolldown/binding-linux-ppc64-gnu@1.0.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.3': + '@rolldown/binding-linux-s390x-gnu@1.0.1': optional: true - '@rollup/rollup-linux-x64-musl@4.60.3': + '@rolldown/binding-linux-x64-gnu@1.0.1': optional: true - '@rollup/rollup-openbsd-x64@4.60.3': + '@rolldown/binding-linux-x64-musl@1.0.1': optional: true - '@rollup/rollup-openharmony-arm64@4.60.3': + '@rolldown/binding-openharmony-arm64@1.0.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.3': + '@rolldown/binding-wasm32-wasi@1.0.1': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.3': + '@rolldown/binding-win32-arm64-msvc@1.0.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.3': + '@rolldown/binding-win32-x64-msvc@1.0.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.3': - optional: true + '@rolldown/pluginutils@1.0.1': {} '@shikijs/engine-oniguruma@3.23.0': dependencies: @@ -3850,52 +3637,52 @@ snapshots: '@tauri-apps/api@2.11.0': {} - '@tauri-apps/cli-darwin-arm64@2.11.1': + '@tauri-apps/cli-darwin-arm64@2.11.2': optional: true - '@tauri-apps/cli-darwin-x64@2.11.1': + '@tauri-apps/cli-darwin-x64@2.11.2': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.1': + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.11.1': + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.11.1': + '@tauri-apps/cli-linux-arm64-musl@2.11.2': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.11.1': + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.11.1': + '@tauri-apps/cli-linux-x64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-musl@2.11.1': + '@tauri-apps/cli-linux-x64-musl@2.11.2': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.11.1': + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.11.1': + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.11.1': + '@tauri-apps/cli-win32-x64-msvc@2.11.2': optional: true - '@tauri-apps/cli@2.11.1': + '@tauri-apps/cli@2.11.2': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.1 - '@tauri-apps/cli-darwin-x64': 2.11.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.1 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.1 - '@tauri-apps/cli-linux-arm64-musl': 2.11.1 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.1 - '@tauri-apps/cli-linux-x64-gnu': 2.11.1 - '@tauri-apps/cli-linux-x64-musl': 2.11.1 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.1 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.1 - '@tauri-apps/cli-win32-x64-msvc': 2.11.1 + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: @@ -3941,26 +3728,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.0 - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.0 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@types/babel__traverse@7.28.0': + '@tybys/wasm-util@0.10.2': dependencies: - '@babel/types': 7.29.0 + tslib: 2.8.1 + optional: true '@types/byte-size@8.1.2': {} @@ -3996,8 +3767,6 @@ snapshots: dependencies: '@types/estree': 1.0.9 - '@types/estree@1.0.8': {} - '@types/estree@1.0.9': {} '@types/file-saver@2.0.7': {} @@ -4018,9 +3787,9 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.12.4': + '@types/node@25.8.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.24.6 '@types/parse-json@4.0.2': {} @@ -4047,25 +3816,18 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.6 - '@vitejs/plugin-react-swc@4.3.0(vite@7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0))': + '@vitejs/plugin-react-swc@4.3.1(vite@8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 + '@rolldown/pluginutils': 1.0.1 '@swc/core': 1.15.33 - vite: 7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0) + vite: 8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.2.0(vite@7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0) - transitivePeerDependencies: - - supports-color + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0) ansi-regex@5.0.1: {} @@ -4121,7 +3883,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.29: {} + baseline-browser-mapping@2.10.30: {} boolbase@1.0.0: {} @@ -4140,9 +3902,9 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.29 + baseline-browser-mapping: 2.10.30 caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.353 + electron-to-chromium: 1.5.357 node-releases: 2.0.44 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -4373,8 +4135,7 @@ snapshots: detect-browser@5.3.0: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} devlop@1.1.0: dependencies: @@ -4386,18 +4147,36 @@ snapshots: domhandler: 5.0.3 entities: 4.5.0 + dom-serializer@3.1.1: + dependencies: + domelementtype: 3.0.0 + domhandler: 6.0.1 + entities: 8.0.0 + domelementtype@2.3.0: {} + domelementtype@3.0.0: {} + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 + domhandler@6.0.1: + dependencies: + domelementtype: 3.0.0 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 + domutils@4.0.2: + dependencies: + dom-serializer: 3.1.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -4409,13 +4188,13 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.353: {} + electron-to-chromium@1.5.357: {} emoji-regex@8.0.0: {} entities@4.5.0: {} - entities@7.0.1: {} + entities@8.0.0: {} error-ex@1.3.4: dependencies: @@ -4501,35 +4280,6 @@ snapshots: es-toolkit@1.46.1: {} - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -4695,15 +4445,15 @@ snapshots: hosted-git-info@2.8.9: {} - html-dom-parser@5.1.8: + html-dom-parser@7.1.0: dependencies: - domhandler: 5.0.3 - htmlparser2: 10.1.0 + domhandler: 6.0.1 + htmlparser2: 12.0.0 - html-react-parser@5.2.17(@types/react@19.2.14)(react@19.2.6): + html-react-parser@6.1.1(@types/react@19.2.14)(react@19.2.6): dependencies: - domhandler: 5.0.3 - html-dom-parser: 5.1.8 + domhandler: 6.0.1 + html-dom-parser: 7.1.0 react: 19.2.6 react-property: 2.0.2 style-to-js: 1.1.21 @@ -4712,12 +4462,12 @@ snapshots: html-url-attributes@3.0.1: {} - htmlparser2@10.1.0: + htmlparser2@12.0.0: dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 7.0.1 + domelementtype: 3.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + entities: 8.0.0 immer@10.2.0: {} @@ -4894,6 +4644,55 @@ snapshots: json5@2.2.3: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@1.2.4: {} linkify-it@5.0.0: @@ -5271,7 +5070,7 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - p-timeout@6.1.4: {} + p-timeout@7.0.1: {} parent-module@1.0.1: dependencies: @@ -5359,7 +5158,7 @@ snapshots: react: 19.2.6 scheduler: 0.27.0 - react-hook-form@7.75.0(react@19.2.6): + react-hook-form@7.76.0(react@19.2.6): dependencies: react: 19.2.6 @@ -5400,7 +5199,7 @@ snapshots: qr.js: 0.0.0 react: 19.2.6 - react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1): + react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.2.6 @@ -5409,8 +5208,6 @@ snapshots: '@types/react': 19.2.14 redux: 5.0.1 - react-refresh@0.18.0: {} - react-router-dom@6.30.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@remix-run/router': 1.23.2 @@ -5446,7 +5243,7 @@ snapshots: recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@16.13.1)(react@19.2.6)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.46.1 @@ -5455,7 +5252,7 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) react-is: 16.13.1 - react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) + react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 use-sync-external-store: 1.6.0(react@19.2.6) @@ -5525,36 +5322,26 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.60.3: + rolldown@1.0.1: dependencies: - '@types/estree': 1.0.8 + '@oxc-project/types': 0.130.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.3 - '@rollup/rollup-android-arm64': 4.60.3 - '@rollup/rollup-darwin-arm64': 4.60.3 - '@rollup/rollup-darwin-x64': 4.60.3 - '@rollup/rollup-freebsd-arm64': 4.60.3 - '@rollup/rollup-freebsd-x64': 4.60.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 - '@rollup/rollup-linux-arm-musleabihf': 4.60.3 - '@rollup/rollup-linux-arm64-gnu': 4.60.3 - '@rollup/rollup-linux-arm64-musl': 4.60.3 - '@rollup/rollup-linux-loong64-gnu': 4.60.3 - '@rollup/rollup-linux-loong64-musl': 4.60.3 - '@rollup/rollup-linux-ppc64-gnu': 4.60.3 - '@rollup/rollup-linux-ppc64-musl': 4.60.3 - '@rollup/rollup-linux-riscv64-gnu': 4.60.3 - '@rollup/rollup-linux-riscv64-musl': 4.60.3 - '@rollup/rollup-linux-s390x-gnu': 4.60.3 - '@rollup/rollup-linux-x64-gnu': 4.60.3 - '@rollup/rollup-linux-x64-musl': 4.60.3 - '@rollup/rollup-openbsd-x64': 4.60.3 - '@rollup/rollup-openharmony-arm64': 4.60.3 - '@rollup/rollup-win32-arm64-msvc': 4.60.3 - '@rollup/rollup-win32-ia32-msvc': 4.60.3 - '@rollup/rollup-win32-x64-gnu': 4.60.3 - '@rollup/rollup-win32-x64-msvc': 4.60.3 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.1 + '@rolldown/binding-darwin-arm64': 1.0.1 + '@rolldown/binding-darwin-x64': 1.0.1 + '@rolldown/binding-freebsd-x64': 1.0.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 + '@rolldown/binding-linux-arm64-gnu': 1.0.1 + '@rolldown/binding-linux-arm64-musl': 1.0.1 + '@rolldown/binding-linux-ppc64-gnu': 1.0.1 + '@rolldown/binding-linux-s390x-gnu': 1.0.1 + '@rolldown/binding-linux-x64-gnu': 1.0.1 + '@rolldown/binding-linux-x64-musl': 1.0.1 + '@rolldown/binding-openharmony-arm64': 1.0.1 + '@rolldown/binding-wasm32-wasi': 1.0.1 + '@rolldown/binding-win32-arm64-msvc': 1.0.1 + '@rolldown/binding-win32-x64-msvc': 1.0.1 rxjs@7.8.2: dependencies: @@ -5579,7 +5366,7 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - sass@1.92.1: + sass@1.99.0: dependencies: chokidar: 4.0.3 immutable: 5.1.5 @@ -5834,7 +5621,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.16.0: {} + undici-types@7.24.6: {} unified@11.0.5: dependencies: @@ -5924,18 +5711,17 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.3.3(@types/node@24.12.4)(sass@1.92.1)(yaml@2.9.0): + vite@8.0.13(@types/node@25.8.0)(sass@1.99.0)(yaml@2.9.0): dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.14 - rollup: 4.60.3 + rolldown: 1.0.1 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 24.12.4 + '@types/node': 25.8.0 fsevents: 2.3.3 - sass: 1.92.1 + sass: 1.99.0 yaml: 2.9.0 which-boxed-primitive@1.1.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..65c830c8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + '@parcel/watcher': true + '@swc/core': true + esbuild: true diff --git a/src-tauri/.env b/src-tauri/.env new file mode 100644 index 00000000..4303819c --- /dev/null +++ b/src-tauri/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:dev.db diff --git a/src-tauri/.sqlx/query-50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1.json b/src-tauri/.sqlx/query-1c07ca7013959226ca9af064037bb64da07f7023f58ff8678d828a0ba50e2470.json similarity index 84% rename from src-tauri/.sqlx/query-50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1.json rename to src-tauri/.sqlx/query-1c07ca7013959226ca9af064037bb64da07f7023f58ff8678d828a0ba50e2470.json index 7692367f..d5164870 100644 --- a/src-tauri/.sqlx/query-50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1.json +++ b/src-tauri/.sqlx/query-1c07ca7013959226ca9af064037bb64da07f7023f58ff8678d828a0ba50e2470.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n posture_check_required FROM location WHERE instance_id = $1 AND service_location_mode <= $2 ORDER BY name ASC", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n mfa_method \"mfa_method: _\", posture_check_required FROM location WHERE instance_id = $1 AND service_location_mode <= $2 ORDER BY name ASC", "describe": { "columns": [ { @@ -69,8 +69,13 @@ "type_info": "Integer" }, { - "name": "posture_check_required", + "name": "mfa_method: _", "ordinal": 13, + "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 14, "type_info": "Bool" } ], @@ -91,8 +96,9 @@ false, false, false, + true, false ] }, - "hash": "50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1" + "hash": "1c07ca7013959226ca9af064037bb64da07f7023f58ff8678d828a0ba50e2470" } diff --git a/src-tauri/.sqlx/query-858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3.json b/src-tauri/.sqlx/query-49039d91cfdefbb32284d15af739a2090ba7baf9dee8cf00e8800b9a5f891fab.json similarity index 86% rename from src-tauri/.sqlx/query-858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3.json rename to src-tauri/.sqlx/query-49039d91cfdefbb32284d15af739a2090ba7baf9dee8cf00e8800b9a5f891fab.json index c6135a03..c6b90053 100644 --- a/src-tauri/.sqlx/query-858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3.json +++ b/src-tauri/.sqlx/query-49039d91cfdefbb32284d15af739a2090ba7baf9dee8cf00e8800b9a5f891fab.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\", posture_check_required FROM location WHERE service_location_mode <= $1 ORDER BY name ASC;", + "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\", mfa_method \"mfa_method: _\", posture_check_required FROM location WHERE service_location_mode <= $1 ORDER BY name ASC;", "describe": { "columns": [ { @@ -69,8 +69,13 @@ "type_info": "Integer" }, { - "name": "posture_check_required", + "name": "mfa_method: _", "ordinal": 13, + "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 14, "type_info": "Bool" } ], @@ -91,8 +96,9 @@ false, false, false, + true, false ] }, - "hash": "858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3" + "hash": "49039d91cfdefbb32284d15af739a2090ba7baf9dee8cf00e8800b9a5f891fab" } diff --git a/src-tauri/.sqlx/query-865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b.json b/src-tauri/.sqlx/query-a25979219918af2df8abca48a48d7fba459b79b74d462565088bf27d8e9fcd5d.json similarity index 86% rename from src-tauri/.sqlx/query-865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b.json rename to src-tauri/.sqlx/query-a25979219918af2df8abca48a48d7fba459b79b74d462565088bf27d8e9fcd5d.json index ddf2ceb9..48cdb545 100644 --- a/src-tauri/.sqlx/query-865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b.json +++ b/src-tauri/.sqlx/query-a25979219918af2df8abca48a48d7fba459b79b74d462565088bf27d8e9fcd5d.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n posture_check_required FROM location WHERE id = $1", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n mfa_method \"mfa_method: _\", posture_check_required FROM location WHERE id = $1", "describe": { "columns": [ { @@ -69,8 +69,13 @@ "type_info": "Integer" }, { - "name": "posture_check_required", + "name": "mfa_method: _", "ordinal": 13, + "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 14, "type_info": "Bool" } ], @@ -91,8 +96,9 @@ false, false, false, + true, false ] }, - "hash": "865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b" + "hash": "a25979219918af2df8abca48a48d7fba459b79b74d462565088bf27d8e9fcd5d" } diff --git a/src-tauri/.sqlx/query-95133767a4ebbd331c17667752e9603c2d351b677f6fac46e944d34b4913ab71.json b/src-tauri/.sqlx/query-e16f46ba4c2365de31db15551084eddaabf35d813a54eced9d38c951965ce83e.json similarity index 58% rename from src-tauri/.sqlx/query-95133767a4ebbd331c17667752e9603c2d351b677f6fac46e944d34b4913ab71.json rename to src-tauri/.sqlx/query-e16f46ba4c2365de31db15551084eddaabf35d813a54eced9d38c951965ce83e.json index 372f9e15..01577f67 100644 --- a/src-tauri/.sqlx/query-95133767a4ebbd331c17667752e9603c2d351b677f6fac46e944d34b4913ab71.json +++ b/src-tauri/.sqlx/query-e16f46ba4c2365de31db15551084eddaabf35d813a54eced9d38c951965ce83e.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12, posture_check_required = $13 WHERE id = $14", + "query": "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12, mfa_method = $13, posture_check_required = $14 WHERE id = $15", "describe": { "columns": [], "parameters": { - "Right": 14 + "Right": 15 }, "nullable": [] }, - "hash": "95133767a4ebbd331c17667752e9603c2d351b677f6fac46e944d34b4913ab71" + "hash": "e16f46ba4c2365de31db15551084eddaabf35d813a54eced9d38c951965ce83e" } diff --git a/src-tauri/.sqlx/query-7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c.json b/src-tauri/.sqlx/query-e27705d75d504385fbaea05c43eadecccdb12fcb43060dd383e7c9fd1516179e.json similarity index 55% rename from src-tauri/.sqlx/query-7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c.json rename to src-tauri/.sqlx/query-e27705d75d504385fbaea05c43eadecccdb12fcb43060dd383e7c9fd1516179e.json index 7eb7d1e7..538701cf 100644 --- a/src-tauri/.sqlx/query-7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c.json +++ b/src-tauri/.sqlx/query-e27705d75d504385fbaea05c43eadecccdb12fcb43060dd383e7c9fd1516179e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode, posture_check_required) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id \"id!\"", + "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode, mfa_method, posture_check_required) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id \"id!\"", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 13 + "Right": 14 }, "nullable": [ true ] }, - "hash": "7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c" + "hash": "e27705d75d504385fbaea05c43eadecccdb12fcb43060dd383e7c9fd1516179e" } diff --git a/src-tauri/.sqlx/query-ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0.json b/src-tauri/.sqlx/query-f88f92313f52f0b2c584f48b40e6edb4bc14d96f3000bd58a3ca68eecb8e4a88.json similarity index 86% rename from src-tauri/.sqlx/query-ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0.json rename to src-tauri/.sqlx/query-f88f92313f52f0b2c584f48b40e6edb4bc14d96f3000bd58a3ca68eecb8e4a88.json index dd6d4011..ee360eaf 100644 --- a/src-tauri/.sqlx/query-ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0.json +++ b/src-tauri/.sqlx/query-f88f92313f52f0b2c584f48b40e6edb4bc14d96f3000bd58a3ca68eecb8e4a88.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n posture_check_required FROM location WHERE pubkey = $1;", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n mfa_method \"mfa_method: _\", posture_check_required FROM location WHERE pubkey = $1;", "describe": { "columns": [ { @@ -69,8 +69,13 @@ "type_info": "Integer" }, { - "name": "posture_check_required", + "name": "mfa_method: _", "ordinal": 13, + "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 14, "type_info": "Bool" } ], @@ -91,8 +96,9 @@ false, false, false, + true, false ] }, - "hash": "ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0" + "hash": "f88f92313f52f0b2c584f48b40e6edb4bc14d96f3000bd58a3ca68eecb8e4a88" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8ebb6850..bcce63ed 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -501,9 +501,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -4170,9 +4170,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -4210,9 +4210,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -4426,18 +4426,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -6248,9 +6248,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4deba334e1190ba7cb498327affa11e5ece10d26a30ab2f27fcf09504b8d8b6" +checksum = "14311e7e9a03114cd4b65eedd54e8fed2945e17f08586ae97ef53bc0669f9581" dependencies = [ "libc", "memchr", @@ -6359,9 +6359,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" dependencies = [ "anyhow", "bytes", @@ -6411,9 +6411,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" dependencies = [ "anyhow", "cargo_toml", @@ -6432,9 +6432,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" dependencies = [ "base64 0.22.1", "brotli", @@ -6459,9 +6459,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6473,9 +6473,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.6.1" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" dependencies = [ "anyhow", "glob", @@ -6713,9 +6713,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" dependencies = [ "cookie", "dpi", @@ -6738,9 +6738,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", @@ -6764,9 +6764,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" dependencies = [ "anyhow", "brotli", @@ -7096,7 +7096,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -7159,7 +7159,7 @@ dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -7168,7 +7168,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -8852,9 +8852,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -9182,7 +9182,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.2", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -9210,7 +9210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 1.0.2", + "winnow 1.0.3", "zvariant", ] @@ -9339,7 +9339,7 @@ dependencies = [ "enumflags2", "serde", "url", - "winnow 1.0.2", + "winnow 1.0.3", "zvariant_derive", "zvariant_utils", ] @@ -9367,5 +9367,5 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow 1.0.2", + "winnow 1.0.3", ] diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 3be5441f..0bbd4977 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -3,6 +3,12 @@ use vergen_git2::{Emitter, Git2Builder}; fn main() -> Result<(), Box> { println!("cargo:rerun-if-env-changed=DEFGUARD_CLIENT_BUILD_VERSION"); + println!("cargo:rerun-if-env-changed=DEFGUARD_CLIENT_DEV"); + println!("cargo::rustc-check-cfg=cfg(defguard_client_dev)"); + if std::env::var("DEFGUARD_CLIENT_DEV").is_ok() { + println!("cargo::rustc-cfg=defguard_client_dev"); + } + // set VERGEN_GIT_SHA env variable based on git commit hash let git2 = Git2Builder::default().branch(true).sha(true).build()?; Emitter::default().add_instructions(&git2)?.emit()?; diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index d8b4de1b..935bd26c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -3,8 +3,13 @@ "identifier": "main-capability", "description": "Capability for the main window", "local": true, - "windows": ["main"], + "remote": { + "urls": ["http://localhost:5071/*", "http://localhost:5072/*"] + }, + "windows": ["new-ui", "old-ui"], "permissions": [ + "core:webview:allow-create-webview", + "core:webview:allow-create-webview-window", "core:default", "core:window:allow-create", "core:window:allow-center", @@ -49,6 +54,7 @@ "dialog:default", "clipboard-manager:allow-write-text", "process:allow-exit", + "allow-app-commands", { "identifier": "http:default", "allow": [ diff --git a/src-tauri/migrations/20260513120000_add_location_mfa_method.sql b/src-tauri/migrations/20260513120000_add_location_mfa_method.sql new file mode 100644 index 00000000..137ef86c --- /dev/null +++ b/src-tauri/migrations/20260513120000_add_location_mfa_method.sql @@ -0,0 +1,6 @@ +-- 0 - TOTP +-- 2 - OIDC +-- NULL - unset (used for Disabled MFA mode locations) +ALTER TABLE location ADD COLUMN mfa_method INTEGER; +UPDATE location SET mfa_method = 0 WHERE location_mfa_mode = 2; +UPDATE location SET mfa_method = 2 WHERE location_mfa_mode = 3; diff --git a/src-tauri/permissions/default.toml b/src-tauri/permissions/default.toml new file mode 100644 index 00000000..72526750 --- /dev/null +++ b/src-tauri/permissions/default.toml @@ -0,0 +1,39 @@ +[[permission]] +identifier = "allow-app-commands" +description = "Allow all application commands for both UI windows (old-ui and new-ui)." +commands.allow = [ + "all_locations", + "save_device_config", + "all_instances", + "connect", + "disconnect", + "update_instance", + "location_stats", + "location_interface_details", + "all_connections", + "last_connection", + "active_connection", + "update_location_routing", + "delete_instance", + "parse_tunnel_config", + "save_tunnel", + "all_tunnels", + "open_link", + "tunnel_details", + "update_tunnel", + "delete_tunnel", + "get_latest_app_version", + "start_global_logwatcher", + "stop_global_logwatcher", + "command_get_app_config", + "command_set_app_config", + "get_provisioning_config", + "get_platform_header", + "set_location_mfa_method", + "open_new_ui_window", + "open_old_ui_window", + "swap_to_new_ui", + "swap_to_old_ui", + "all_active_connections", + "disconnect_locations", +] diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index 179b9a6b..02e6ae4d 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -1,6 +1,9 @@ use std::{collections::HashMap, sync::Mutex}; -use tauri::async_runtime::{spawn, JoinHandle}; +use tauri::{ + async_runtime::{spawn, JoinHandle}, + PhysicalPosition, +}; use tokio_util::sync::CancellationToken; use crate::{ @@ -15,6 +18,7 @@ use crate::{ pub struct AppState { pub log_watchers: Mutex>, pub app_config: Mutex, + pub tray_click_position: Mutex>>, stat_threads: Mutex>>, // location ID is the key pub provisioning_config: Mutex>, } @@ -25,6 +29,7 @@ impl AppState { Self { log_watchers: Mutex::new(HashMap::new()), app_config: Mutex::new(config), + tray_click_position: Mutex::new(None), stat_threads: Mutex::new(HashMap::new()), provisioning_config: Mutex::new(provisioning_config), } diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index a01e6f99..0d0bf13e 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -29,10 +29,11 @@ use defguard_client::{ service, tray::{configure_tray_icon, setup_tray, show_main_window}, utils::load_log_targets, + window::*, LOG_FILENAME, VERSION, }; use log::{Level, LevelFilter}; -use tauri::{AppHandle, Builder, Manager, RunEvent, WindowEvent}; +use tauri::{AppHandle, Builder, Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent}; use tauri_plugin_log::{Target, TargetKind}; #[macro_use] @@ -180,7 +181,14 @@ fn main() { command_get_app_config, command_set_app_config, get_provisioning_config, - get_platform_header + get_platform_header, + set_location_mfa_method, + open_new_ui_window, + open_old_ui_window, + swap_to_new_ui, + swap_to_old_ui, + all_active_connections, + disconnect_locations, ]) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { @@ -339,6 +347,29 @@ fn main() { let state = AppState::new(config, provisioning_config); app.manage(state); + // Open new UI window. + let new_url = if cfg!(defguard_client_dev) { + WebviewUrl::External("http://localhost:5072".parse().unwrap()) + } else { + WebviewUrl::App("new-ui/".into()) + }; + WebviewWindowBuilder::new(app, NEW_UI_WINDOW_ID, new_url) + .title("New UI") + .inner_size(NEW_UI_WIDTH, NEW_UI_HEIGHT) + .visible(false) + .build()?; + + // Open old UI window. + let old_url = if cfg!(defguard_client_dev) { + WebviewUrl::External("http://localhost:5071".parse().unwrap()) + } else { + WebviewUrl::App("old-ui/index.html/".into()) + }; + WebviewWindowBuilder::new(app, OLD_UI_WINDOW_ID, old_url) + .title("Old UI") + .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) + .build()?; + info!("App setup completed, log level: {log_level}"); Ok(()) }) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index eaf5cdb0..bb2b342a 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -14,14 +14,14 @@ use tauri::{AppHandle, Emitter, Manager, State}; const UPDATE_URL: &str = "https://pkgs.defguard.net/api/update/check"; use crate::{ - active_connections::{find_connection, get_connection_id_by_type}, + active_connections::{find_connection, get_connection_id_by_type, ACTIVE_CONNECTIONS}, app_config::{AppConfig, AppConfigPatch}, appstate::AppState, database::{ models::{ connection::{ActiveConnection, Connection, ConnectionInfo}, instance::{ClientTrafficPolicy, Instance, InstanceInfo}, - location::{Location, LocationMfaMode}, + location::{infer_mfa_method, Location, LocationMfaMethod, LocationMfaMode}, location_stats::LocationStats, tunnel::{Tunnel, TunnelConnection, TunnelConnectionInfo, TunnelStats}, wireguard_keys::WireguardKeys, @@ -185,6 +185,78 @@ pub async fn disconnect( } } +#[tauri::command(async)] +pub async fn disconnect_locations(location_ids: Vec, handle: AppHandle) -> Result<(), Error> { + debug!( + "Received a command to disconnect {} location(s): {location_ids:?}", + location_ids.len() + ); + let state = handle.state::(); + let mut any_disconnected = false; + + for location_id in location_ids { + match Location::find_by_id(&*DB_POOL, location_id).await? { + Some(location) if location.is_service_location() => { + debug!( + "Skipping service location {location}(ID: {location_id}) in \ + disconnect_locations" + ); + continue; + } + None => { + debug!("Location with ID {location_id} not found in the database, skipping."); + continue; + } + _ => {} + } + + let name = get_tunnel_or_location_name(location_id, ConnectionType::Location).await; + debug!("Disconnecting from location {name}(ID: {location_id})"); + + if let Some(connection) = state + .remove_connection(location_id, ConnectionType::Location) + .await + { + disconnect_interface(&connection).await?; + stop_log_watcher_task(&handle, &connection.interface_name)?; + if let Err(err) = maybe_update_instance_config(location_id, &handle).await { + match err { + Error::CoreNotEnterprise => { + debug!( + "Tried to fetch instance config from core after disconnecting from \ + {name}(ID: {location_id}), but the core is not enterprise." + ); + } + Error::NoToken => { + debug!( + "Tried to fetch instance config from core after disconnecting from \ + {name}(ID: {location_id}), but the instance has no polling token." + ); + } + _ => { + warn!( + "Error while trying to fetch instance config after disconnecting \ + from {name}(ID: {location_id}): {err}" + ); + } + } + } + info!("Disconnected from location {name}(ID: {location_id})"); + any_disconnected = true; + } else { + debug!("No active connection found for location {name}(ID: {location_id}), skipping."); + } + } + + if any_disconnected { + handle.emit(EventKey::ConnectionChanged.into(), ())?; + reload_tray_menu(&handle).await; + configure_tray_icon(&handle).await?; + } + + Ok(()) +} + /// Triggers poll on location's instance config. Config will be updated if there are no more active /// connections for this instance. async fn maybe_update_instance_config(location_id: Id, handle: &AppHandle) -> Result<(), Error> { @@ -418,6 +490,7 @@ pub struct LocationInfo { pub pubkey: String, pub network_id: Id, pub location_mfa_mode: LocationMfaMode, + pub mfa_method: Option, } impl LocationInfo { @@ -470,6 +543,7 @@ pub async fn all_locations(instance_id: Id) -> Result, Error> pubkey: location.pubkey, network_id: location.network_id, location_mfa_mode: location.location_mfa_mode, + mfa_method: location.mfa_method, }; location_info.push(info); } @@ -546,10 +620,13 @@ pub(crate) async fn locations_changed( let mut new_location = Location::::from(location); // Ignore `route_all_traffic` flag as Defguard core does not have it. new_location.route_all_traffic = false; + // Canonicalize mfa_method so a user-set value doesn't falsely trigger a + // config-change detection when the mode hasn't actually changed. + new_location.mfa_method = infer_mfa_method(new_location.location_mfa_mode, None); new_location }) .collect::>(); - let core_locations = device_config + let core_locations: HashSet = device_config .configs .iter() .map(|config| config.clone().into_location(instance.id)) @@ -637,6 +714,11 @@ pub(crate) async fn do_update_instance( current_location.dns = new_location.dns; current_location.location_mfa_mode = new_location.location_mfa_mode; current_location.service_location_mode = new_location.service_location_mode; + // Correct mfa_method to remain consistent with the (possibly updated) mfa_mode. + current_location.mfa_method = infer_mfa_method( + current_location.location_mfa_mode, + current_location.mfa_method, + ); current_location.posture_check_required = new_location.posture_check_required; current_location.save(transaction.as_mut()).await?; info!("Location {current_location} configuration updated for instance {instance}"); @@ -978,6 +1060,33 @@ pub async fn update_location_routing( } } +#[tauri::command(async)] +pub async fn set_location_mfa_method( + location_id: Id, + mfa_method: LocationMfaMethod, + handle: AppHandle, +) -> Result<(), Error> { + debug!("Received command to set MFA method for location {location_id}"); + if let Some(mut location) = Location::find_by_id(&*DB_POOL, location_id).await? { + let inferred = infer_mfa_method(location.location_mfa_mode, Some(mfa_method)); + debug!( + "Setting MFA method for location {}(ID: {location_id}) to {inferred:?}", + location.name, + ); + location.mfa_method = inferred; + location.save(&*DB_POOL).await?; + debug!( + "MFA method updated for location {}(ID: {location_id})", + location.name, + ); + handle.emit(EventKey::LocationUpdate.into(), ())?; + Ok(()) + } else { + error!("Location with ID {location_id} not found, cannot set MFA method"); + Err(Error::NotFound) + } +} + #[cfg(target_os = "macos")] #[tauri::command(async)] pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), Error> { @@ -1297,8 +1406,7 @@ fn select_reported_app_version( ) -> String { build_version_override .filter(|version| !version.trim().is_empty()) - .map(str::to_owned) - .unwrap_or_else(|| package_version.to_owned()) + .map_or_else(|| package_version.to_owned(), str::to_owned) } fn reported_app_version(handle: &AppHandle) -> String { @@ -1413,6 +1521,37 @@ pub fn get_platform_header() -> String { construct_platform_header() } +#[derive(Debug, Serialize)] +pub struct ActiveConnectionSummary { + pub id: Id, + pub name: String, + pub connection_type: ConnectionType, +} + +#[tauri::command(async)] +pub async fn all_active_connections() -> Result, Error> { + debug!("Getting information about all active connections."); + let connections = ACTIVE_CONNECTIONS.lock().await; + let mut result = Vec::with_capacity(connections.len()); + for conn in connections.iter() { + if conn.connection_type == ConnectionType::Location { + match Location::find_by_id(&*DB_POOL, conn.location_id).await? { + Some(location) if location.is_service_location() => continue, + None => continue, + _ => {} + } + } + let name = get_tunnel_or_location_name(conn.location_id, conn.connection_type).await; + result.push(ActiveConnectionSummary { + id: conn.location_id, + name, + connection_type: conn.connection_type, + }); + } + debug!("Returning {} active connections.", result.len()); + Ok(result) +} + #[cfg(test)] mod tests { use super::select_reported_app_version; diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index 6891b8f6..85c470c3 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -22,7 +22,7 @@ use crate::{ }, }; -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] #[repr(u32)] #[serde(rename_all = "lowercase")] pub enum LocationMfaMode { @@ -64,6 +64,32 @@ impl From for ServiceLocationMode { } } +/// Discriminants match the proto `MfaMethod` enum. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] +#[repr(u32)] +#[serde(rename_all = "lowercase")] +pub enum LocationMfaMethod { + Totp = 0, + Email = 1, + Oidc = 2, + Biometric = 3, + MobileApprove = 4, +} + +pub(crate) fn infer_mfa_method( + mode: LocationMfaMode, + method: Option, +) -> Option { + match mode { + LocationMfaMode::Disabled => method, + LocationMfaMode::Internal => match method { + Some(LocationMfaMethod::Oidc) | None => Some(LocationMfaMethod::Totp), + Some(m) => Some(m), + }, + LocationMfaMode::External => Some(LocationMfaMethod::Oidc), + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct Location { pub id: I, @@ -80,6 +106,7 @@ pub struct Location { pub keepalive_interval: i64, pub location_mfa_mode: LocationMfaMode, pub service_location_mode: ServiceLocationMode, + pub mfa_method: Option, #[serde(default)] pub posture_check_required: bool, } @@ -114,7 +141,7 @@ impl Location { network_id, route_all_traffic, keepalive_interval, \ location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ service_location_mode \"service_location_mode: ServiceLocationMode\", \ - posture_check_required \ + mfa_method \"mfa_method: _\", posture_check_required \ FROM location WHERE service_location_mode <= $1 \ ORDER BY name ASC;", max_service_location_mode @@ -132,8 +159,8 @@ impl Location { "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, \ endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, \ keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12, \ - posture_check_required = $13 \ - WHERE id = $14", + mfa_method = $13, posture_check_required = $14 \ + WHERE id = $15", self.instance_id, self.name, self.address, @@ -146,6 +173,7 @@ impl Location { self.keepalive_interval, self.location_mfa_mode, self.service_location_mode, + self.mfa_method, self.posture_check_required, self.id, ) @@ -168,7 +196,7 @@ impl Location { network_id, route_all_traffic, keepalive_interval, \ location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ service_location_mode \"service_location_mode: ServiceLocationMode\", - posture_check_required \ + mfa_method \"mfa_method: _\", posture_check_required \ FROM location WHERE id = $1", location_id ) @@ -192,7 +220,7 @@ impl Location { network_id, route_all_traffic, keepalive_interval, \ location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ service_location_mode \"service_location_mode: ServiceLocationMode\", - posture_check_required \ + mfa_method \"mfa_method: _\", posture_check_required \ FROM location WHERE instance_id = $1 AND service_location_mode <= $2 \ ORDER BY name ASC", instance_id, @@ -215,7 +243,7 @@ impl Location { network_id, route_all_traffic, keepalive_interval, \ location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ service_location_mode \"service_location_mode: ServiceLocationMode\", - posture_check_required \ + mfa_method \"mfa_method: _\", posture_check_required \ FROM location WHERE pubkey = $1;", pubkey ) @@ -381,8 +409,8 @@ impl Location { let id = query_scalar!( "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, \ dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, \ - service_location_mode, posture_check_required) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) \ + service_location_mode, mfa_method, posture_check_required) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) \ RETURNING id \"id!\"", self.instance_id, self.name, @@ -396,6 +424,7 @@ impl Location { self.keepalive_interval, self.location_mfa_mode, self.service_location_mode, + self.mfa_method, self.posture_check_required, ) .fetch_one(executor) @@ -415,6 +444,7 @@ impl Location { keepalive_interval: self.keepalive_interval, location_mfa_mode: self.location_mfa_mode, service_location_mode: self.service_location_mode, + mfa_method: self.mfa_method, posture_check_required: self.posture_check_required, }) } @@ -443,6 +473,7 @@ impl From> for Location { keepalive_interval: location.keepalive_interval, location_mfa_mode: location.location_mfa_mode, service_location_mode: location.service_location_mode, + mfa_method: location.mfa_method, posture_check_required: location.posture_check_required, } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ae09fd4e..2d3137f0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,6 +32,7 @@ pub mod service; pub mod tray; pub mod utils; pub mod wg_config; +pub mod window; pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA")); pub const MIN_CORE_VERSION: Version = Version::new(1, 6, 0); diff --git a/src-tauri/src/proto.rs b/src-tauri/src/proto.rs index 32d99cd9..5fbeef75 100644 --- a/src-tauri/src/proto.rs +++ b/src-tauri/src/proto.rs @@ -1,5 +1,8 @@ use crate::database::models::{ - location::{Location, LocationMfaMode as MfaMode, ServiceLocationMode as SLocationMode}, + location::{ + infer_mfa_method, Location, LocationMfaMode as MfaMode, + ServiceLocationMode as SLocationMode, + }, Id, NoId, }; @@ -60,6 +63,7 @@ impl defguard::client_types::DeviceConfig { keepalive_interval: self.keepalive_interval.into(), location_mfa_mode, service_location_mode, + mfa_method: infer_mfa_method(location_mfa_mode, None), posture_check_required: self.posture_check_required.unwrap_or_default(), } } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index db6fcca1..fa194b1e 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -6,6 +6,8 @@ use tauri::{ AppHandle, Emitter, Manager, Runtime, }; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent}; + use crate::{ active_connections::{get_connection_id_by_type, ACTIVE_CONNECTIONS}, appstate::AppState, @@ -13,6 +15,7 @@ use crate::{ database::{models::location::Location, DB_POOL}, error::Error, events::EventKey, + window::{show_new_ui_window_near_tray, NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID}, ConnectionType, }; @@ -20,8 +23,6 @@ const SUBSCRIBE_UPDATES_LINK: &str = "https://defguard.net/newsletter"; const JOIN_COMMUNITY_LINK: &str = "https://github.com/DefGuard/defguard/discussions/new/choose"; const FOLLOW_US_LINK: &str = "https://floss.social/@defguard"; -const MAIN_WINDOW_ID: &str = "main"; - const TRAY_ICON_ID: &str = "tray"; const TRAY_EVENT_QUIT: &str = "quit"; @@ -31,6 +32,22 @@ const TRAY_EVENT_UPDATES: &str = "updates"; const TRAY_EVENT_COMMUNITY: &str = "community"; const TRAY_EVENT_FOLLOW: &str = "follow"; +fn store_tray_click_position(app: &AppHandle, event: &TrayIconEvent) { + let position = match event { + TrayIconEvent::Click { + button_state: MouseButtonState::Down, + position, + .. + } + | TrayIconEvent::DoubleClick { position, .. } => Some(*position), + _ => None, + }; + + if let Some(position) = position { + *app.state::().tray_click_position.lock().unwrap() = Some(position); + } +} + /// Generate contents of system tray menu. async fn generate_tray_menu(app: &AppHandle) -> Result, Error> { debug!("Generating tray menu."); @@ -129,6 +146,16 @@ pub async fn setup_tray(app: &AppHandle) -> Result<(), Error> { TrayIconBuilder::with_id(TRAY_ICON_ID) .menu(&tray_menu) .show_menu_on_left_click(true) + .on_tray_icon_event(|icon, event| { + store_tray_click_position(icon.app_handle(), &event); + if let TrayIconEvent::DoubleClick { + button: MouseButton::Left, + .. + } = event + { + show_new_ui_window_near_tray(icon.app_handle()); + } + }) .on_menu_event(handle_tray_menu_event) .build(app)?; // On other systems (especially Windows), system tray menu is on right-click, @@ -138,8 +165,13 @@ pub async fn setup_tray(app: &AppHandle) -> Result<(), Error> { .menu(&tray_menu) .show_menu_on_left_click(false) .on_tray_icon_event(|icon, event| { - if let tauri::tray::TrayIconEvent::DoubleClick { .. } = event { - show_main_window(icon.app_handle()); + store_tray_click_position(icon.app_handle(), &event); + if let TrayIconEvent::DoubleClick { + button: MouseButton::Left, + .. + } = event + { + show_new_ui_window_near_tray(icon.app_handle()); } }) .on_menu_event(handle_tray_menu_event) @@ -169,16 +201,21 @@ fn hide_main_window(app: &AppHandle) { warn!("Failed to hide application: {err}"); } #[cfg(not(target_os = "macos"))] - if let Some(main_window) = app.get_webview_window(MAIN_WINDOW_ID) { - if let Err(err) = main_window.hide() { - warn!("Failed to hide main window: {err}"); + for window_id in [NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID] { + if let Some(window) = app.get_webview_window(window_id) { + if let Err(err) = window.hide() { + warn!("Failed to hide window {window_id}: {err}"); + } } } } pub fn show_main_window(app: &AppHandle) { - if let Some(main_window) = app.get_webview_window(MAIN_WINDOW_ID) { - if let Err(err) = main_window.unminimize() { + if let Some(window) = app + .get_webview_window(NEW_UI_WINDOW_ID) + .or_else(|| app.get_webview_window(OLD_UI_WINDOW_ID)) + { + if let Err(err) = window.unminimize() { warn!("Failed to unminimize main window: {err}"); } #[cfg(target_os = "macos")] @@ -187,11 +224,11 @@ pub fn show_main_window(app: &AppHandle) { } #[cfg(not(target_os = "macos"))] { - if let Err(err) = main_window.show() { + if let Err(err) = window.show() { warn!("Failed to show main window: {err}"); } } - let _ = main_window.set_focus(); + let _ = window.set_focus(); } } @@ -203,7 +240,7 @@ pub fn handle_tray_menu_event(app: &AppHandle, event: MenuEvent) { info!("Received QUIT request. Initiating shutdown..."); handle.exit(0); } - TRAY_EVENT_SHOW => show_main_window(app), + TRAY_EVENT_SHOW => show_new_ui_window_near_tray(app), TRAY_EVENT_HIDE => hide_main_window(app), TRAY_EVENT_UPDATES => { let _ = webbrowser::open(SUBSCRIBE_UPDATES_LINK); diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs new file mode 100644 index 00000000..e4fce3fb --- /dev/null +++ b/src-tauri/src/window.rs @@ -0,0 +1,154 @@ +use tauri::{ + webview::WebviewWindowBuilder, AppHandle, LogicalPosition, Manager, Monitor, Position, + WebviewUrl, WebviewWindow, +}; + +use crate::appstate::AppState; + +pub const NEW_UI_WINDOW_ID: &str = "new-ui"; +pub const OLD_UI_WINDOW_ID: &str = "old-ui"; +pub const NEW_UI_WIDTH: f64 = 360.0; +pub const NEW_UI_HEIGHT: f64 = 675.0; +pub const OLD_UI_WIDTH: f64 = 920.0; +pub const OLD_UI_HEIGHT: f64 = 720.0; + +fn new_ui_url() -> WebviewUrl { + if cfg!(defguard_client_dev) { + WebviewUrl::External("http://localhost:5072".parse().unwrap()) + } else { + WebviewUrl::App("new-ui/".into()) + } +} + +fn old_ui_url() -> WebviewUrl { + if cfg!(defguard_client_dev) { + WebviewUrl::External("http://localhost:5071".parse().unwrap()) + } else { + WebviewUrl::App("old-ui/index.html".into()) + } +} + +/// Try to get monitor at the given position, with a fall back to primary monitor, and then to the +/// first one on the list of available monitors. +fn get_monitor_for_position(app: &AppHandle, x: f64, y: f64) -> Option { + if let Ok(Some(monitor)) = app.monitor_from_point(x, y) { + return Some(monitor); + } + + if let Ok(Some(monitor)) = app.primary_monitor() { + return Some(monitor); + } + + // On macOS, it seems this is the only working method (as of Tauri 2.11), but fortunately it + // returns the current monitor as the first one. + if let Ok(mut monitors) = app.available_monitors() { + monitors.pop() + } else { + None + } +} + +fn get_tray_window_position( + app: &AppHandle, + width: f64, + height: f64, +) -> Option> { + let app_state = app.state::(); + let tray_position = app_state.tray_click_position.lock().unwrap().to_owned()?; + + let monitor = get_monitor_for_position(app, tray_position.x, tray_position.y)?; + + let scale_factor = monitor.scale_factor(); + let monitor_position = monitor.position().to_logical::(scale_factor); + let monitor_size = monitor.size().to_logical::(scale_factor); + let tray_position = tray_position.to_logical::(scale_factor); + + let mut x = tray_position.x - (width / 2.0); + let center_y = monitor_position.y + (monitor_size.height / 2.0); + let mut y = if tray_position.y < center_y { + tray_position.y + } else { + tray_position.y - height + }; + + x = x.clamp( + monitor_position.x, + monitor_position.x + monitor_size.width - width, + ); + y = y.clamp( + monitor_position.y, + monitor_position.y + monitor_size.height - height, + ); + + Some(LogicalPosition::new(x, y)) +} + +fn position_window_near_tray(app: &AppHandle, window: &WebviewWindow, width: f64, height: f64) { + if let Some(position) = get_tray_window_position(app, width, height) { + if let Err(err) = window.set_position(Position::Logical(position)) { + warn!("Failed to position window near tray icon: {err}"); + } + } +} + +fn show_new_ui_window_internal(app: &AppHandle, near_tray: bool) { + let window = if let Some(window) = app.get_webview_window(NEW_UI_WINDOW_ID) { + let _ = window.unminimize(); + window + } else { + WebviewWindowBuilder::new(app, NEW_UI_WINDOW_ID, new_ui_url()) + .title("New UI") + .inner_size(NEW_UI_WIDTH, NEW_UI_HEIGHT) + .build() + .unwrap() + }; + if near_tray { + position_window_near_tray(app, &window, NEW_UI_WIDTH, NEW_UI_HEIGHT); + } + #[cfg(target_os = "macos")] + let _ = app.show(); + let _ = window.show(); + let _ = window.set_focus(); +} + +pub(crate) fn show_new_ui_window(app: &AppHandle) { + show_new_ui_window_internal(app, false); +} + +pub(crate) fn show_new_ui_window_near_tray(app: &AppHandle) { + show_new_ui_window_internal(app, true); +} + +#[tauri::command] +pub fn open_new_ui_window(app: AppHandle) { + show_new_ui_window(&app); +} + +#[tauri::command] +pub fn open_old_ui_window(app: AppHandle) { + let _window = WebviewWindowBuilder::new(&app, OLD_UI_WINDOW_ID, old_ui_url()) + .title("Old UI") + .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) + .build() + .unwrap(); +} + +#[tauri::command] +pub fn swap_to_old_ui(app: AppHandle) { + WebviewWindowBuilder::new(&app, OLD_UI_WINDOW_ID, old_ui_url()) + .title("Old UI") + .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) + .build() + .unwrap(); + if let Some(w) = app.get_webview_window(NEW_UI_WINDOW_ID) { + w.close().unwrap(); + } +} + +#[tauri::command] +pub fn swap_to_new_ui(app: AppHandle) { + show_new_ui_window(&app); + if let Some(w) = app.get_webview_window(OLD_UI_WINDOW_ID) { + w.close().unwrap(); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8359fac6..db27ec6a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,19 +1,14 @@ { "$schema": "https://schema.tauri.app/config/2", "build": { - "beforeBuildCommand": "pnpm build", - "beforeDevCommand": "pnpm dev", "frontendDist": "../dist", - "devUrl": "http://localhost:3001" + "devUrl": "http://localhost:5072" }, "bundle": { "active": true, "category": "Utility", "copyright": "Defguard", - "targets": [ - "deb", - "app" - ], + "targets": ["deb", "app"], "externalBin": [], "icon": [ "icons/32x32.png", @@ -34,10 +29,7 @@ "./resources-windows/fragments/service.wxs", "./resources-windows/fragments/provisioning.wxs" ], - "componentRefs": [ - "DefguardServiceFragment", - "ProvisioningScriptFragment" - ], + "componentRefs": ["DefguardServiceFragment", "ProvisioningScriptFragment"], "template": "./resources-windows/msi/main.wxs" } }, @@ -50,9 +42,7 @@ }, "minimumSystemVersion": "13.5" }, - "resources": [ - "resources/icons/*" - ], + "resources": ["resources/icons/*"], "shortDescription": "Defguard desktop client", "longDescription": "Defguard desktop client", "linux": { @@ -65,18 +55,14 @@ "../control/prerm": "../resources-linux/prerm", "../control/postrm": "../resources-linux/postrm" }, - "depends": [ - "desktop-file-utils" - ] + "depends": ["desktop-file-utils"] }, "rpm": { "files": { "/usr/sbin/defguard-service": "target/release/defguard-service", "/lib/systemd/system/defguard-service.service": "../resources-linux/defguard-service.service" }, - "depends": [ - "desktop-file-utils" - ], + "depends": ["desktop-file-utils"], "postInstallScript": "../resources-linux/postinst", "preRemoveScript": "../resources-linux/prerm", "postRemoveScript": "../resources-linux/postrm" @@ -89,35 +75,15 @@ "version": "1.6.9", "app": { "security": { - "capabilities": [ - "main-capability" - ], + "capabilities": ["main-capability"], "csp": null }, - "windows": [ - { - "fullscreen": false, - "center": true, - "maximized": true, - "height": 720, - "resizable": true, - "maximizable": true, - "minimizable": true, - "closable": true, - "title": "Defguard", - "width": 992, - "minWidth": 650, - "minHeight": 450, - "useHttpsScheme": true - } - ] + "windows": [] }, "plugins": { "deep-link": { "desktop": { - "schemes": [ - "defguard" - ] + "schemes": ["defguard"] } } } diff --git a/src-tauri/tauri.local.conf.json b/src-tauri/tauri.local.conf.json new file mode 100644 index 00000000..0176062c --- /dev/null +++ b/src-tauri/tauri.local.conf.json @@ -0,0 +1,9 @@ +{ + "bundle": { + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": null, + "timestampUrl": null + } + } +} diff --git a/src/shared/defguard-ui b/src/shared/defguard-ui index 8c697a69..1110ba80 160000 --- a/src/shared/defguard-ui +++ b/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 8c697a69a978cf6fd3591a4d83bc6bfc6edb26cb +Subproject commit 1110ba807491689efc40a2e28383ea1c6186fcad diff --git a/vite.config.ts b/vite.config.ts index 49bf0a10..9a6a76db 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,14 +3,27 @@ import autoprefixer from 'autoprefixer'; import * as path from 'path'; import { defineConfig } from 'vite'; +const host = process.env.TAURI_DEV_HOST; + // https://vitejs.dev/config/ -export default defineConfig({ - base: './', +export default defineConfig(async ({ command }) => ({ plugins: [react()], clearScreen: false, server: { strictPort: true, - port: 3001, + port: 5071, + host: host || false, + hmr: host + ? { + protocol: 'ws', + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ['**/src-tauri/**'], + }, }, resolve: { alias: { @@ -28,7 +41,10 @@ export default defineConfig({ }, }, envPrefix: ['VITE_', 'TAURI_'], + base: command === 'build' ? '/old-ui/' : './', build: { chunkSizeWarningLimit: 10000000, + outDir: './dist/old-ui', + emptyOutDir: true, }, -}); +}));