From 0b661faa4e0b73b0b4fe058da5574c53c3e7737e Mon Sep 17 00:00:00 2001 From: snodel Date: Mon, 14 Sep 2026 09:24:14 -0700 Subject: [PATCH 01/13] fix(tracker): show indexing state while cache status loads - Initialize collection cache status as `not-started` - Add coverage for the pending cache status request --- .../app/browser/store/browser.store.spec.ts | 14 +- .../src/app/browser/store/browser.store.ts | 2 +- package.json | 2 +- pnpm-lock.yaml | 3493 +++++++++++------ pnpm-workspace.yaml | 12 +- 5 files changed, 2263 insertions(+), 1260 deletions(-) diff --git a/apps/tracker/src/app/browser/store/browser.store.spec.ts b/apps/tracker/src/app/browser/store/browser.store.spec.ts index d952152c..1a29b391 100644 --- a/apps/tracker/src/app/browser/store/browser.store.spec.ts +++ b/apps/tracker/src/app/browser/store/browser.store.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { of, throwError } from 'rxjs'; +import { NEVER, of, throwError } from 'rxjs'; import { BrowserStore } from './browser.store'; import { BrowserApiService } from '../services/browser-api.service'; import type { ResourceTreeDto, CacheStatusDto } from '@simoncodes-ca/data-transfer'; @@ -108,6 +108,18 @@ describe('BrowserStore', () => { }); describe('Collection Selection', () => { + it('should show indexing state while the initial cache status request is pending', () => { + vi.spyOn(apiService, 'getCacheStatus').mockReturnValue(NEVER); + + store.setSelectedCollection({ + collectionName: 'app-translations', + locales: ['en', 'es'], + }); + + expect(store.cacheStatus()).toBe('not-started'); + expect(store.isCacheIndexing()).toBe(true); + }); + it('should set selected collection and load root folders', async () => { vi.spyOn(apiService, 'getCacheStatus').mockReturnValue(of(mockCacheReady)); vi.spyOn(apiService, 'getResourceTree').mockReturnValue(of(mockTreeRoot)); diff --git a/apps/tracker/src/app/browser/store/browser.store.ts b/apps/tracker/src/app/browser/store/browser.store.ts index 37cdb5fc..27e925a2 100644 --- a/apps/tracker/src/app/browser/store/browser.store.ts +++ b/apps/tracker/src/app/browser/store/browser.store.ts @@ -146,7 +146,7 @@ export const BrowserStore = signalStore( isReadOnly: params.readOnly ?? false, selectedLocales: loaded?.selectedLocales || [], baseLocale, - cacheStatus: null, + cacheStatus: 'not-started', cacheError: null, collectionStats: null, currentFolderPath: '', diff --git a/package.json b/package.json index ee7c7d6d..09d1743d 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "provenance": true }, "license": "MIT", - "packageManager": "pnpm@10.27.0", + "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c", "engines": { "node": ">=20", "pnpm": ">=10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6789497f..696568b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,82 +1,167 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.4.1 + version: 12.4.1 + +packages: + + '@pnpm/exe.android-arm64@12.4.1': + resolution: {integrity: sha512-/HwsqXMSmlOfgtV9+O0ratzjV6Vd/8n1hh4rGHpCGmDURvt52MwZxdbhyxP4K03ZfvgSDvotFPr8O+RzE5Eu8A==} + cpu: [arm64] + os: [android] + + '@pnpm/exe.android-x64@12.4.1': + resolution: {integrity: sha512-+l74Qb4c2YjOzNKHXJLg+1wr8xHM1ckkUhnU5KRUK9TJqiiczt6yeTZqqzHIlJ8i6pAoj5V0OJevDRwnQKLgrQ==} + cpu: [x64] + os: [android] + + '@pnpm/exe.darwin-arm64@12.4.1': + resolution: {integrity: sha512-6rkZkT3iGfaxknUdGHraqSWFvTa6N0ajAHluv9Ax0GRWs0sIcGNiFhDopv6xSZCsJZmG483aNS/b6UEDy3blfw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.4.1': + resolution: {integrity: sha512-Vb1CHlR88HghC1qUxjxjs82zQSXnXacPD+btG2CmG8Q/hBU7Q0/b2YWJKSQqZXicunV5khFtfTlAwJddV9RkYA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.freebsd-x64@12.4.1': + resolution: {integrity: sha512-iT3iHz3Nl0Sxxj7UPOtZ/aQ81AFsQhjoeWMAlPkSRO04gsgDGAXv3UYxOFaesMWsfNxaGn0A+CITbuCHFF66FA==} + cpu: [x64] + os: [freebsd] + + '@pnpm/exe.linux-arm64-musl@12.4.1': + resolution: {integrity: sha512-aBooZfNXM5f+OGUgCAMFWpE/kAhsWfvmqIyMtHy6zl3aNxyInWrcY/Saln/UElzo7lZWMC0Yktroxd/2J26lNQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.4.1': + resolution: {integrity: sha512-TlOdacTTP09BgcMvwWBFRsu8VAjfwqwnslBj+XSq1JFM3ck4f3k+1O/747EuctxZxs3/o785b6Q3s7Pd92Ptsg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-ppc64@12.4.1': + resolution: {integrity: sha512-r/ab/MlIBo75oizUP5ITiziCCrnXz4SJwfErLQ+603AshB+Yq7xTMCoMtzTSCAMu6aaymIKkAFtZvd2J75Wq0w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-riscv64@12.4.1': + resolution: {integrity: sha512-C/D1QWdKMiB8+wv/spl1rGITFUuqC+aI/fb6h8NB5sqxsOaGXeYc/g891oCuzggHn6SODk9p+I09hI76x/cMNw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-s390x@12.4.1': + resolution: {integrity: sha512-nxz5zD4yXt94uzbStDk0QTPKW+aE92hH1b5tFXK9ctB1HE9Xcq1vjTiA2LsZMFC6p4gdu5OwQeFqYNAVGa6QlA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.4.1': + resolution: {integrity: sha512-5AwgFdGhVUg2kIweYGfxzSLEHiIG77PQhZAkXC3TwofQHsu1Wr+TrV5/rNX2PopFnHRzuE581zoB8F6Wle32yg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.4.1': + resolution: {integrity: sha512-FJOZuuuQMhp0oLzBtcKkLXknBI92hfkmSnlKc47vfin4HrmfID5khY2lGekL9tCzk1cpR+HShEw/meFl+nHtzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.4.1': + resolution: {integrity: sha512-OO7eKBL9S+xk5hRy+JUUZSNJknGuGe3GEUffsrlC2LbqKF02g+VX1anGIzSbJDvkkdnpJhSlxF5AUz2JzJu2Jw==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.4.1': + resolution: {integrity: sha512-x7gJHZgHo6hp354xCYA2NvoFzYJkHwovt2kVsUBK5EmXULLcP51JGMq09CX+5FRFJUZFKoXjLu3mZWmfP7o6PQ==} + cpu: [x64] + os: [win32] + + pnpm@12.4.1: + resolution: {integrity: sha512-LoHjmdc/6DkNqyXgaqeIq3pZCCSNL1o3D4K0gRR6ano2e/gEj5pv22Rg8hpm8FQt7bi5TKLIcjWWdBkgsWVtTA==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.android-arm64@12.4.1': + optional: true + + '@pnpm/exe.android-x64@12.4.1': + optional: true + + '@pnpm/exe.darwin-arm64@12.4.1': + optional: true + + '@pnpm/exe.darwin-x64@12.4.1': + optional: true + + '@pnpm/exe.freebsd-x64@12.4.1': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.4.1': + optional: true + + '@pnpm/exe.linux-arm64@12.4.1': + optional: true + + '@pnpm/exe.linux-ppc64@12.4.1': + optional: true + + '@pnpm/exe.linux-riscv64@12.4.1': + optional: true + + '@pnpm/exe.linux-s390x@12.4.1': + optional: true + + '@pnpm/exe.linux-x64-musl@12.4.1': + optional: true + + '@pnpm/exe.linux-x64@12.4.1': + optional: true + + '@pnpm/exe.win32-arm64@12.4.1': + optional: true + + '@pnpm/exe.win32-x64@12.4.1': + optional: true + + pnpm@12.4.1: + optionalDependencies: + '@pnpm/exe.android-arm64': 12.4.1 + '@pnpm/exe.android-x64': 12.4.1 + '@pnpm/exe.darwin-arm64': 12.4.1 + '@pnpm/exe.darwin-x64': 12.4.1 + '@pnpm/exe.freebsd-x64': 12.4.1 + '@pnpm/exe.linux-arm64': 12.4.1 + '@pnpm/exe.linux-arm64-musl': 12.4.1 + '@pnpm/exe.linux-ppc64': 12.4.1 + '@pnpm/exe.linux-riscv64': 12.4.1 + '@pnpm/exe.linux-s390x': 12.4.1 + '@pnpm/exe.linux-x64': 12.4.1 + '@pnpm/exe.linux-x64-musl': 12.4.1 + '@pnpm/exe.win32-arm64': 12.4.1 + '@pnpm/exe.win32-x64': 12.4.1 + +--- lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - axios: '>=1.18.0' - tar: '>=7.5.18' - multer: '>=2.2.0' - immutable: '>=5.1.8' - '@tootallnate/once': '>=3.0.1' - koa: '>=3.1.2' - minimatch@<3.1.4: 3.1.4 - minimatch@>=5.0.0 <5.1.8: 5.1.8 - minimatch@>=9.0.0 <9.0.7: 9.0.7 - minimatch@>=10.0.0 <10.2.3: 10.2.3 - ajv@<6.14.0: 6.14.0 - ajv@>=7.0.0-alpha.0 <8.18.0: 8.18.0 - rollup: '>=4.59.0' - webpack@>=5.49.0 <5.104.1: 5.104.1 - '@modelcontextprotocol/sdk': '>=1.26.0' - '@isaacs/brace-expansion': '>=5.0.1' - diff@>=4.0.0 <4.0.4: 4.0.4 - qs@>=6.7.0 <6.15.2: 6.15.2 - file-type@>=13.0.0 <21.3.2: 21.3.2 - lodash-es: '>=4.18.1' - lodash: '>=4.18.1' - path-to-regexp@<0.1.13: 0.1.13 - path-to-regexp@>=8.0.0 <8.4.0: 8.4.0 - handlebars@>=4.0.0 <=4.7.8: 4.7.9 - serialize-javascript@<7.0.5: 7.0.5 - node-forge@<1.4.0: 1.4.0 - picomatch@>=4.0.0 <4.0.4: 4.0.4 - flatted@<=3.4.1: 3.4.2 - yaml@>=2.0.0 <2.8.3: 2.8.3 - follow-redirects: '>=1.16.0' - hono: '>=4.12.34' - ip-address: '>=10.3.1' - '@babel/plugin-transform-modules-systemjs': '>=7.29.4' - picomatch@>=2.0.0 <3.0.0: 2.3.2 - glob@>=10.2.0 <10.5.0: 10.5.0 - yaml@>=1.0.0 <1.10.3: 1.10.3 - esbuild@<=0.24.2: 0.25.0 - esbuild@>=0.27.3 <0.28.1: 0.28.1 - http-proxy-middleware@>=0.16.0 <2.0.10: 2.0.10 - http-proxy-middleware@>=3.0.0 <3.0.7: 3.0.7 - '@babel/core@<7.29.6': 7.29.6 - form-data@>=4.0.0 <4.0.6: 4.0.6 - piscina@>=5.0.0-alpha.0 <5.2.0: 5.2.0 - launch-editor@<=2.14.0: 2.14.1 - shell-quote@<=1.8.3: 1.8.4 - uuid@<11.1.1: 11.1.1 - vite@>=7.0.0 <=7.3.4: 7.3.6 - svgo@>=3.0.0 <3.3.4: 3.3.4 - svgo@>=4.0.0 <4.0.2: 4.0.2 - fast-uri@<3.1.5: 3.1.5 - '@hono/node-server@<1.19.15': 1.19.15 - webpack-dev-server@>=5.0.0 <5.2.6: 5.2.6 - postcss@<8.5.23: 8.5.23 - ws@>=8.0.0 <8.21.0: 8.21.0 - tmp@<0.2.7: 0.2.7 - undici@<6.28.0: 6.28.0 - js-yaml@<3.15.1: 3.15.1 - js-yaml@>=4.0.0 <4.3.1: 4.3.1 - body-parser@<1.20.6: 1.20.6 - body-parser@>=2.0.0 <2.3.0: 2.3.0 - brace-expansion@>=1.0.0 <1.1.18: 1.1.18 - brace-expansion@>=2.0.0 <2.1.2: 2.1.2 - brace-expansion@>=3.0.0 <5.0.7: 5.0.7 - websocket-driver@<0.7.5: 0.7.5 - adm-zip@<0.6.0: 0.6.0 - nanoid@>=3.0.0 <3.3.18: 3.3.18 - postcss-selector-parser@>=6.0.0 <6.1.3: 6.1.3 - postcss-selector-parser@>=7.0.0 <7.1.3: 7.1.3 - sigstore@<=4.1.0: 4.1.1 - '@sigstore/core@<=3.2.0': 3.2.1 - '@sigstore/verify@3.1.0': 3.1.1 - importers: .: @@ -122,19 +207,19 @@ importers: version: 5.1.1 '@nestjs/common': specifier: ^11.0.0 - version: 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': specifier: ^11.1.18 - version: 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.0 - version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18) + version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(supports-color@8.1.1) '@nestjs/serve-static': specifier: ^5.0.3 - version: 5.0.3(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)(express@5.2.1) + version: 5.0.3(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(express@5.2.1(supports-color@8.1.1)) '@nestjs/swagger': specifier: ^11.2.0 - version: 11.2.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.1.14) + version: 11.2.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(reflect-metadata@0.1.14) '@ngrx/signals': specifier: ^20.0.1 version: 20.0.1(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -143,7 +228,7 @@ importers: version: 14.0.1 express: specifier: ^5.2.1 - version: 5.2.1 + version: 5.2.1(supports-color@8.1.1) prompts: specifier: ^2.4.2 version: 2.4.2 @@ -165,25 +250,25 @@ importers: devDependencies: '@analogjs/vite-plugin-angular': specifier: ~1.19.1 - version: 1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)) + version: 1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)) '@analogjs/vitest-angular': specifier: ~1.19.1 - version: 1.19.4(@analogjs/vite-plugin-angular@1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)))(@angular-devkit/architect@0.2003.24(chokidar@4.0.3))(vitest@3.2.6) + version: 1.19.4(@analogjs/vite-plugin-angular@1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)))(@angular-devkit/architect@0.2003.24(chokidar@4.0.3))(vitest@3.2.6) '@angular-devkit/core': specifier: ~20.3.0 - version: 20.3.3(chokidar@4.0.3) + version: 20.3.24(chokidar@4.0.3) '@angular-devkit/schematics': specifier: ~20.3.0 version: 20.3.24(chokidar@4.0.3) '@angular/build': specifier: ~20.3.0 - version: 20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3) + version: 20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3) '@angular/cli': specifier: ~20.3.0 - version: 20.3.24(@types/node@20.19.9)(chokidar@4.0.3) + version: 20.3.24(@types/node@20.19.9)(chokidar@4.0.3)(supports-color@8.1.1) '@angular/compiler-cli': specifier: ~20.3.0 - version: 20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2) + version: 20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2) '@angular/language-service': specifier: ~20.3.0 version: 20.3.19 @@ -207,70 +292,70 @@ importers: version: 11.0.7(chokidar@4.0.3)(typescript@5.9.2) '@nestjs/testing': specifier: ^11.0.0 - version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.6) + version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.6) '@ngneat/spectator': specifier: ^22.0.0 version: 22.0.0(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/router@20.3.30(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)) '@nx/angular': specifier: 22.7.9 - version: 22.7.9(facc33cf0716ce05a968cc4e029d9e3c) + version: 22.7.9(3f137da2344d682390527b18b5279b4a) '@nx/esbuild': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) '@nx/eslint': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + version: 22.7.9(970ddfd867418be042d7af6db9ddbf38) '@nx/eslint-plugin': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.4.2)))(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(typescript@5.9.2) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2) '@nx/jest': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) '@nx/js': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) '@nx/nest': specifier: 22.7.9 - version: 22.7.9(c46c164d45cf7da0c8732e0c30f9c061) + version: 22.7.9(c33e04cd93160f87e5faef925fe4fc14) '@nx/node': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) '@nx/vite': specifier: 22.7.9 - version: 22.7.9(6df4234e208ff1705d1304e6172497ea) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))(vitest@3.2.6) '@nx/vitest': specifier: 22.7.9 - version: 22.7.9(6df4234e208ff1705d1304e6172497ea) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))(vitest@3.2.6) '@nx/web': specifier: 22.7.9 - version: 22.7.9(c35aa15b9f1d41e28c1dcb3ee2fdad58) + version: 22.7.9(034a38cadaa6b7dbc4ae32d15f7f7ab4) '@nx/webpack': specifier: 22.7.9 - version: 22.7.9(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(typescript@5.9.2)(webpack-cli@5.1.4(webpack@5.104.1)) + version: 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(uglify-js@3.19.3)(webpack-cli@5.1.4) '@schematics/angular': specifier: ~20.3.0 version: 20.3.24(chokidar@4.0.3) '@semantic-release/changelog': specifier: ^6.0.3 - version: 6.0.3(semantic-release@24.2.3(typescript@5.9.2)) + version: 6.0.3(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2)) '@semantic-release/commit-analyzer': specifier: ^13.0.1 - version: 13.0.1(semantic-release@24.2.3(typescript@5.9.2)) + version: 13.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) '@semantic-release/exec': specifier: ^7.1.0 - version: 7.1.0(semantic-release@24.2.3(typescript@5.9.2)) + version: 7.1.0(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) '@semantic-release/git': specifier: ^10.0.1 - version: 10.0.1(semantic-release@24.2.3(typescript@5.9.2)) + version: 10.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) '@semantic-release/npm': specifier: ^13.1.5 - version: 13.1.5(semantic-release@24.2.3(typescript@5.9.2)) + version: 13.1.5(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2)) '@semantic-release/release-notes-generator': specifier: ^14.0.3 - version: 14.0.3(semantic-release@24.2.3(typescript@5.9.2)) + version: 14.0.3(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) '@swc-node/register': specifier: ~1.12.1 - version: 1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2) + version: 1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2) '@swc/core': specifier: ~1.16.2 version: 1.16.2(@swc/helpers@0.5.17) @@ -294,16 +379,16 @@ importers: version: 2.4.9 '@typescript-eslint/utils': specifier: ^8.40.0 - version: 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + version: 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@vitest/coverage-v8': specifier: ^3.0.5 - version: 3.2.4(vitest@3.2.6) + version: 3.2.4(supports-color@8.1.1)(vitest@3.2.6) '@vitest/ui': specifier: ^3.0.0 version: 3.2.4(vitest@3.2.6) angular-eslint: specifier: ^20.2.0 - version: 20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2))(typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(typescript@5.9.2) + version: 20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(typescript@5.9.2) commitizen: specifier: 4.3.1 version: 4.3.1(@types/node@20.19.9)(typescript@5.9.2) @@ -315,16 +400,16 @@ importers: version: 0.25.5 eslint: specifier: ^9.8.0 - version: 9.36.0(jiti@2.4.2) + version: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.0.0 - version: 10.1.8(eslint@9.36.0(jiti@2.4.2)) + version: 10.1.8(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)) husky: specifier: 9.1.7 version: 9.1.7 jest: specifier: ^30.0.2 - version: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + version: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) jest-environment-node: specifier: ^30.0.2 version: 30.1.2 @@ -336,25 +421,25 @@ importers: version: 2.4.2 jsdom: specifier: ~22.1.0 - version: 22.1.0 + version: 22.1.0(supports-color@8.1.1) jsonc-eslint-parser: specifier: ^2.1.0 version: 2.4.1 lodash: - specifier: '>=4.18.1' + specifier: ^4.18.1 version: 4.18.1 nx: specifier: 22.7.9 - version: 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) + version: 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) prettier: specifier: ^2.6.2 version: 2.8.8 semantic-release: specifier: ^24.2.3 - version: 24.2.3(typescript@5.9.2) + version: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) ts-jest: specifier: ^29.4.0 - version: 29.4.4(@babel/core@7.29.6)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.6))(esbuild@0.25.5)(jest-util@30.0.5)(jest@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)))(typescript@5.9.2) + version: 29.4.4(@babel/core@7.29.6(supports-color@8.1.1))(@jest/transform@30.1.2(supports-color@8.1.1))(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1))(esbuild@0.25.5)(jest-util@30.0.5)(jest@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)))(typescript@5.9.2) ts-node: specifier: 10.9.1 version: 10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2) @@ -363,19 +448,19 @@ importers: version: 5.9.2 typescript-eslint: specifier: ^8.40.0 - version: 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + version: 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) vite: specifier: ^7.3.6 version: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) vite-plugin-dts: specifier: ~4.5.0 - version: 4.5.4(@types/node@20.19.9)(rollup@4.59.0)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3)) + version: 4.5.4(@types/node@20.19.9)(rollup@4.59.0)(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3)) vitest: specifier: ^3.2.6 - version: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + version: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) webpack-cli: specifier: ^5.1.4 - version: 5.1.4(webpack@5.104.1) + version: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) libs/data-transfer: {} @@ -558,6 +643,7 @@ packages: '@angular/animations@20.2.4': resolution: {integrity: sha512-mXiTlXZgAF4uYonOt7l2w7uvLLTJEk6jqs3H291bYuoDRM8R166UjN7ygAeBmPiJ4TLMyKGkwMQy3b1Vvw4RQA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: '@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.' peerDependencies: '@angular/core': 20.2.4 @@ -576,7 +662,7 @@ packages: karma: ^6.4.0 less: ^4.2.0 ng-packagr: ^20.0.0 - postcss: 8.5.23 + postcss: ^8.4.0 tailwindcss: ^2.0.0 || ^3.0.0 || ^4.0.0 tslib: ^2.3.0 typescript: '>=5.8 <6.0' @@ -723,6 +809,10 @@ packages: resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} + engines: {node: '>=6.9.0'} + '@babel/core@7.29.6': resolution: {integrity: sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==} engines: {node: '>=6.9.0'} @@ -731,10 +821,6 @@ packages: resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} 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/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} @@ -751,18 +837,18 @@ packages: resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/helper-create-regexp-features-plugin@7.27.1': resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/helper-define-polyfill-provider@0.6.5': resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} @@ -780,7 +866,7 @@ packages: resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} @@ -798,13 +884,13 @@ packages: resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/helper-replace-supers@7.27.1': resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/helper-skip-transparent-expression-wrappers@7.27.1': resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} @@ -865,487 +951,487 @@ packages: resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.13.0 '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-proposal-decorators@7.28.0': resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-bigint@7.8.3': resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-properties@7.12.13': resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-class-static-block@7.14.5': resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-decorators@7.27.1': resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-assertions@7.27.1': resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-attributes@7.27.1': resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-json-strings@7.8.3': resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-jsx@7.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-numeric-separator@7.10.4': resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-object-rest-spread@7.8.3': resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-catch-binding@7.8.3': resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-optional-chaining@7.8.3': resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-private-property-in-object@7.14.5': resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-top-level-await@7.14.5': resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-typescript@7.27.1': resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-transform-arrow-functions@7.27.1': resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-async-generator-functions@7.28.0': resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-async-to-generator@7.27.1': resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-block-scoped-functions@7.27.1': resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-block-scoping@7.28.4': resolution: {integrity: sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-class-properties@7.27.1': resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-class-static-block@7.28.3': resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.12.0 '@babel/plugin-transform-classes@7.28.4': resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-computed-properties@7.27.1': resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-destructuring@7.28.0': resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-dotall-regex@7.27.1': resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-duplicate-keys@7.27.1': resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-transform-dynamic-import@7.27.1': resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-explicit-resource-management@7.28.0': resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-exponentiation-operator@7.27.1': resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-export-namespace-from@7.27.1': resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-for-of@7.27.1': resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-function-name@7.27.1': resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-json-strings@7.27.1': resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-literals@7.27.1': resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-logical-assignment-operators@7.27.1': resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-member-expression-literals@7.27.1': resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-amd@7.27.1': resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-commonjs@7.27.1': resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-systemjs@7.29.4': resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-umd@7.27.1': resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-transform-new-target@7.27.1': resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-numeric-separator@7.27.1': resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-object-rest-spread@7.28.4': resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-object-super@7.27.1': resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-optional-catch-binding@7.27.1': resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-optional-chaining@7.27.1': resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-parameters@7.27.7': resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-private-methods@7.27.1': resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-private-property-in-object@7.27.1': resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-property-literals@7.27.1': resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-regenerator@7.28.4': resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-regexp-modifiers@7.27.1': resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/plugin-transform-reserved-words@7.27.1': resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-runtime@7.28.3': resolution: {integrity: sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-shorthand-properties@7.27.1': resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-spread@7.27.1': resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-sticky-regex@7.27.1': resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-template-literals@7.27.1': resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-typeof-symbol@7.27.1': resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-typescript@7.28.0': resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-escapes@7.27.1': resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-property-regex@7.27.1': resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-regex@7.27.1': resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-unicode-sets-regex@7.27.1': resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 '@babel/preset-env@7.28.3': resolution: {integrity: sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/preset-modules@0.1.6-no-external-plugins': resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 '@babel/preset-typescript@7.27.1': resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} @@ -1408,24 +1494,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.3.13': resolution: {integrity: sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.13': resolution: {integrity: sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.3.13': resolution: {integrity: sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.3.13': resolution: {integrity: sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==} @@ -1562,6 +1652,18 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -1574,6 +1676,18 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} @@ -1586,6 +1700,18 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} @@ -1598,6 +1724,18 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} @@ -1610,6 +1748,18 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} @@ -1622,6 +1772,18 @@ packages: cpu: [x64] 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/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} @@ -1634,6 +1796,18 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} @@ -1646,6 +1820,18 @@ packages: cpu: [x64] 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/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} @@ -1658,6 +1844,18 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} @@ -1670,6 +1868,18 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} @@ -1682,6 +1892,18 @@ packages: cpu: [ia32] 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-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} @@ -1694,6 +1916,18 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} @@ -1706,6 +1940,18 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} @@ -1718,6 +1964,18 @@ packages: cpu: [ppc64] 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-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} @@ -1730,6 +1988,18 @@ packages: cpu: [riscv64] 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-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} @@ -1742,6 +2012,18 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} @@ -1754,6 +2036,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} @@ -1766,6 +2060,18 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} @@ -1778,6 +2084,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} @@ -1790,6 +2108,18 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} @@ -1802,12 +2132,36 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} 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/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} @@ -1820,6 +2174,18 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} @@ -1832,6 +2198,18 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} @@ -1844,6 +2222,18 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} @@ -1856,6 +2246,18 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -1908,7 +2310,7 @@ packages: resolution: {integrity: sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg==} engines: {node: '>=18.14.1'} peerDependencies: - hono: '>=4.12.34' + hono: ^4 '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -2069,6 +2471,14 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2323,8 +2733,8 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} - '@modelcontextprotocol/sdk@1.27.1': - resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -2356,7 +2766,7 @@ packages: peerDependencies: typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 vue-tsc: '>=1.0.24' - webpack: 5.104.1 + webpack: ^5.0.0 peerDependenciesMeta: typescript: optional: true @@ -2385,7 +2795,7 @@ packages: '@module-federation/node@2.7.50': resolution: {integrity: sha512-mbpQRdafyeWgsmYoJfdhOQf76zS6onOGpC2X1ELpWXB1Y4BcZGloL0CLjNMNon9m3ucfpc99tOGAQqFzQVkSBQ==} peerDependencies: - webpack: 5.104.1 + webpack: ^5.40.0 peerDependenciesMeta: webpack: optional: true @@ -2506,42 +2916,49 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -2749,24 +3166,28 @@ packages: engines: {node: '>= 12'} cpu: [arm64] os: [linux] + libc: [glibc] '@node-rs/xxhash-linux-arm64-musl@1.7.7': resolution: {integrity: sha512-m1Wz3ySODY7XZixTgLn9n59tUVSTmhO4iwcTLX5lRNnJaCOiWywsySPPbo3++oebSGTwAdJEmPJq5BLPSEZGGQ==} engines: {node: '>= 12'} cpu: [arm64] os: [linux] + libc: [musl] '@node-rs/xxhash-linux-x64-gnu@1.7.7': resolution: {integrity: sha512-rdLsrNjOUbOCdsdZZqfqeE24y/yeSR8c11w5Xkwj/Qkrcpaq3qB5f3aIW+6WuQJjdBsJwOxHCspSuUiaY6Xx4g==} engines: {node: '>= 12'} cpu: [x64] os: [linux] + libc: [glibc] '@node-rs/xxhash-linux-x64-musl@1.7.7': resolution: {integrity: sha512-1LmaKDAIoqZcSQJG22cZMH9aL4B6jg5kQygg6pY7NyICzw+taLPLHhaMMEE+E0n2BodRXzVjZvF15pxi4qXtug==} engines: {node: '>= 12'} cpu: [x64] os: [linux] + libc: [musl] '@node-rs/xxhash-win32-arm64-msvc@1.7.7': resolution: {integrity: sha512-F2QaHmplRZbg1T/AlQuW11MmRsxLvNVEsofeQd92/u704L5+BBEVc13MutJwioXNIAW1GmgOpfwVG6ZGeLAoyA==} @@ -2873,7 +3294,7 @@ packages: '@nx/esbuild@22.7.9': resolution: {integrity: sha512-gVPtY+DDD5XX2B9CpIaIEYGqLUaml+8hSWNKp9G2Qtuw94nxB3G3XcIRbDsoCk7gjZwEG4A6eq2nXi/sZHBwwg==} peerDependencies: - esbuild: 0.25.0 + esbuild: '>=0.19.2 <1.0.0' peerDependenciesMeta: esbuild: optional: true @@ -2943,21 +3364,25 @@ packages: resolution: {integrity: sha512-a52HCtTnXghGZ7ScVaKANnVv4cpxWQ5hnKsjfrt0HWbPmuOwHT8con2xshgFruHs66k9mn3u0jqlq88O0fvWGQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-musl@22.7.9': resolution: {integrity: sha512-WVaaINMAI8CtsgV3UjNCXc5ttPz75aLSvCxZ6547IrWankukcENgQiWTL6DxATwjstWf2tXUPY7veIJZ5siVXA==} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-gnu@22.7.9': resolution: {integrity: sha512-KEqnx2sX3DVhS+SY5tKPSuEwZHeed6wHNN/jNNYE2+W/2IiMBJp12tIhPtKNeME0QTSgzViaasgXD5GQCnxkzQ==} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-musl@22.7.9': resolution: {integrity: sha512-mBWQuG/jZ7jvDUekxgN13rtKGNQNvuY6pXiOKLNxTFIfTgSCw7/QtmyzHtJBPyg0FbirXoEUmtAXIDBMF3t8+A==} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-win32-arm64-msvc@22.7.9': resolution: {integrity: sha512-PTz7K04KybQiUXS63zrnVbtbYDxQq4e2n+gB4RVB6YWHTqkIHpAUwo8r1dGq36jt5gwfUfle9xVmGuHgchy70g==} @@ -2978,14 +3403,14 @@ packages: '@nx/vite@22.7.9': resolution: {integrity: sha512-BUCSSFQIWoEBw3BFiQ+YXjIMBWaXxinEdf8CpDwOiUQqErgqA6F7YCi50PeBPAFosMQRdZwCzGx+w/1pJDwfaQ==} peerDependencies: - vite: 7.3.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vitest: ^1.3.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 '@nx/vitest@22.7.9': resolution: {integrity: sha512-s0DRh6MPa/eGj+xU/lVPnnMq2pRAz0r60juIVMLX6+UZPbJkyKDazw5msfYd46xTU+7I0xeFr8WJ14BhDyiaVw==} peerDependencies: '@nx/eslint': 22.7.9 - vite: 7.3.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vitest: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 peerDependenciesMeta: '@nx/eslint': @@ -3117,41 +3542,49 @@ packages: resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.24.2': resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.24.2': resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.24.2': resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.24.2': resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} @@ -3202,36 +3635,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -3324,7 +3763,7 @@ packages: resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: '>=4.59.0' + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -3363,66 +3802,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -3468,21 +3920,25 @@ packages: resolution: {integrity: sha512-fvZX6xZPvBT8qipSpvkKMX5M7yd2BSpZNCZXcefw6gA3uC7LI3gu+er0LrDXY1PtPzVuHTyDx+abwWpagV3PiQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-musl@1.6.8': resolution: {integrity: sha512-++XMKcMNrt59HcFBLnRaJcn70k3X0GwkAegZBVpel8xYIAgvoXT5+L8P1ExId/yTFxqedaz8DbcxQnNmMozviw==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-gnu@1.6.8': resolution: {integrity: sha512-tv3BWkTE1TndfX+DsE1rSTg8fBevCxujNZ3MlfZ22Wfy9x1FMXTJlWG8VIOXmaaJ1wUHzv8S7cE2YUUJ2LuiCg==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-musl@1.6.8': resolution: {integrity: sha512-DCGgZ5/in1O3FjHWqXnDsncRy+48cMhfuUAAUyl0yDj1NpsZu9pP+xfGLvGcQTiYrVl7IH9Aojf1eShP/77WGA==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-wasm32-wasi@1.6.8': resolution: {integrity: sha512-VUwdhl/lI4m6o1OGCZ9JwtMjTV/yLY5VZTQdEPKb40JMTlmZ5MBlr5xk7ByaXXYHr6I+qnqEm73iMKQvg6iknw==} @@ -3717,36 +4173,42 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.16.2': resolution: {integrity: sha512-4gFarKaFnlJTSlJYKmMhV4u+3YE4uYfiydpBoYjmgQhCf9lAieOq+WilZaK9vVSHeqLuQpTEiGULZqAdsRX5Dw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-ppc64-gnu@1.16.2': resolution: {integrity: sha512-syqSLGd6KlZ1PciNzs6bIUlhOuFztZufebOHaERjc4N4SqNZxyqYd4I+jj/EfOYnpe0kNjccn9HJLN1p5dz3+w==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@swc/core-linux-s390x-gnu@1.16.2': resolution: {integrity: sha512-ZBBLK+ewGyXLzWeMS7wbKtWBdnif6etn7xvPY/iOfbdsjX/+bgkp1pQt2lWF2wlu2hXYZuhJ/tHZE/QR8/apzg==} engines: {node: '>=10'} cpu: [s390x] os: [linux] + libc: [glibc] '@swc/core-linux-x64-gnu@1.16.2': resolution: {integrity: sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.16.2': resolution: {integrity: sha512-PghXJlVM1cgtLfNUR1vxFo1z+PDRAe8cWAJlZZ7spmeiN7BospGXg/MHUg7oNSgwSX7Zo//YKv9P5yD9apsFJQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.16.2': resolution: {integrity: sha512-StTOSefYBxemvNYYUI3UmO1a8y+hSPjjfHogC2TEHL+Z1PlEBim/XtLas5rS04jAzT9RrNmbtX911SZ42H9jSQ==} @@ -3788,15 +4250,15 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@tokenizer/inflate@0.4.1': - resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + '@tokenizer/inflate@0.2.7': + resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tootallnate/once@3.0.1': - resolution: {integrity: sha512-VyMVKRrpHTT8PnotUeV8L/mDaMwD5DaAKCFLP73zAqAtvF0FCqky+Ki7BYbFCYQmqFyTe9316Ed5zS70QUR9eg==} + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} engines: {node: '>= 10'} '@ts-morph/common@0.22.0': @@ -3921,6 +4383,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} @@ -4074,41 +4539,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -4134,7 +4607,7 @@ packages: resolution: {integrity: sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} peerDependencies: - vite: 7.3.6 + vite: ^6.0.0 || ^7.0.0 '@vitest/coverage-v8@3.2.4': resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} @@ -4152,7 +4625,7 @@ packages: resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} peerDependencies: msw: ^2.4.9 - vite: 7.3.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true @@ -4193,6 +4666,11 @@ packages: '@volar/typescript@2.4.23': resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} @@ -4263,21 +4741,21 @@ packages: resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} engines: {node: '>=14.15.0'} peerDependencies: - webpack: 5.104.1 + webpack: 5.x.x webpack-cli: 5.x.x '@webpack-cli/info@2.0.2': resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} engines: {node: '>=14.15.0'} peerDependencies: - webpack: 5.104.1 + webpack: 5.x.x webpack-cli: 5.x.x '@webpack-cli/serve@2.0.5': resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} engines: {node: '>=14.15.0'} peerDependencies: - webpack: 5.104.1 + webpack: 5.x.x webpack-cli: 5.x.x webpack-dev-server: '*' peerDependenciesMeta: @@ -4364,7 +4842,7 @@ packages: ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: - ajv: 8.18.0 + ajv: ^8.5.0 peerDependenciesMeta: ajv: optional: true @@ -4372,7 +4850,7 @@ packages: ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: - ajv: 8.18.0 + ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true @@ -4380,7 +4858,7 @@ packages: ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: - ajv: 8.18.0 + ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true @@ -4388,16 +4866,25 @@ packages: ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: - ajv: 6.14.0 + ajv: ^6.9.1 ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: - ajv: 8.18.0 + ajv: ^8.8.2 ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -4520,7 +5007,7 @@ packages: engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: - postcss: 8.5.23 + postcss: ^8.1.0 axios@1.18.1: resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} @@ -4533,19 +5020,19 @@ packages: resolution: {integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.11.0 babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} peerDependencies: - '@babel/core': 7.29.6 - webpack: 5.104.1 + '@babel/core': ^7.12.0 + webpack: '>=5' babel-plugin-const-enum@1.2.0: resolution: {integrity: sha512-o1m/6iyyFnp9MRsK1dHF3bneqyf3AlM2q3A/YbgQr2pCat6B6XJVDv2TXqzfY2RYUi4mak6WAksSBPlyYGx9dg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0-0 babel-plugin-istanbul@7.0.1: resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} @@ -4562,22 +5049,22 @@ packages: babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-polyfill-corejs3@0.13.0: resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-polyfill-regenerator@0.6.5: resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 babel-plugin-transform-typescript-metadata@0.3.2: resolution: {integrity: sha512-mWEvCQTgXQf48yDqgN7CH50waTyYBeP2Lpqx4nNWab9sxEpdXVeKgfj1qYI2/TgUPQtNFZ85i3PemRtnXVYYJg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7 '@babel/traverse': ^7 peerDependenciesMeta: '@babel/traverse': @@ -4586,13 +5073,13 @@ packages: babel-preset-current-node-syntax@1.2.0: resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.0.0 || ^8.0.0-0 babel-preset-jest@30.0.1: resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': ^7.11.0 balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -5057,7 +5544,7 @@ packages: resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==} engines: {node: '>= 20.9.0'} peerDependencies: - webpack: 5.104.1 + webpack: ^5.1.0 core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} @@ -5118,14 +5605,14 @@ packages: resolution: {integrity: sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: 8.5.23 + postcss: ^8.0.9 css-loader@6.11.0: resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} engines: {node: '>= 12.13.0'} peerDependencies: '@rspack/core': 0.x || 1.x - webpack: 5.104.1 + webpack: ^5.0.0 peerDependenciesMeta: '@rspack/core': optional: true @@ -5142,7 +5629,7 @@ packages: csso: '*' esbuild: '*' lightningcss: '*' - webpack: 5.104.1 + webpack: ^5.0.0 peerDependenciesMeta: '@parcel/css': optional: true @@ -5188,19 +5675,19 @@ packages: resolution: {integrity: sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 cssnano-utils@5.0.3: resolution: {integrity: sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 cssnano@7.1.9: resolution: {integrity: sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -5537,6 +6024,16 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -5759,7 +6256,7 @@ packages: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} peerDependencies: - picomatch: 4.0.4 + picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true @@ -5783,8 +6280,8 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-type@21.3.2: - resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==} + file-type@21.0.0: + resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==} engines: {node: '>=20'} fill-range@7.1.1: @@ -5870,7 +6367,7 @@ packages: engines: {node: '>=14.21.3'} peerDependencies: typescript: '>3.6.0' - webpack: 5.104.1 + webpack: ^5.11.0 form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} @@ -6236,7 +6733,7 @@ packages: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: 8.5.23 + postcss: ^8.1.0 identity-obj-proxy@3.0.0: resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} @@ -6342,6 +6839,10 @@ packages: resolution: {integrity: sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==} engines: {node: '>=12'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ip-address@10.7.0: resolution: {integrity: sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==} engines: {node: '>= 12'} @@ -6520,7 +7021,7 @@ packages: isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: - ws: 8.21.0 + ws: '*' issue-parser@7.0.1: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} @@ -6716,6 +7217,10 @@ packages: resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} hasBin: true + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + js-yaml@4.3.1: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true @@ -6808,7 +7313,7 @@ packages: peerDependencies: '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 less: ^3.5.0 || ^4.0.0 - webpack: 5.104.1 + webpack: ^5.0.0 peerDependenciesMeta: '@rspack/core': optional: true @@ -6942,6 +7447,12 @@ packages: lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -7127,11 +7638,15 @@ packages: resolution: {integrity: sha512-euWmddf0sk9Nv1O0gfeeUAvAkoSlWncNLF77C0TP2+WoPvy8mAHKOzMajcCz2dzvyt3CNgxb1obIEVFIRxaipg==} engines: {node: '>= 12.13.0'} peerDependencies: - webpack: 5.104.1 + webpack: ^5.0.0 minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@10.2.3: resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} engines: {node: 18 || 20 || >=22} @@ -7189,6 +7704,10 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -7223,8 +7742,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - multer@2.2.0: - resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} engines: {node: '>= 10.16.0'} multicast-dns@7.2.5: @@ -7299,6 +7818,10 @@ packages: encoding: optional: true + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -7615,6 +8138,10 @@ packages: ordered-binary@1.6.0: resolution: {integrity: sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + oxc-resolver@11.24.2: resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} @@ -7791,6 +8318,10 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-to-regexp@8.4.0: resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==} @@ -7819,6 +8350,14 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -7839,8 +8378,8 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - piscina@5.2.0: - resolution: {integrity: sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==} + piscina@5.1.3: + resolution: {integrity: sha512-0u3N7H4+hbr40KjuVn2uNhOcthu/9usKhnw5vT3J7ply79v3D3M8naI00el9Klcy16x557VsEkkUQaHCWFXC/g==} engines: {node: '>=20.x'} pkce-challenge@5.0.0: @@ -7881,57 +8420,57 @@ packages: resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} engines: {node: ^18.12 || ^20.9 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.4.38 postcss-colormin@7.0.10: resolution: {integrity: sha512-yFr6JezOolHLta/buLE71VKPh2mXursp4saVe98/ol8ZnEWhL+racShqPKlvd/DKWLre/39B6HhcMXf7RZ3hxg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-convert-values@7.0.12: resolution: {integrity: sha512-xurKu5qqk4viR3Cp3p4xBR4KfnZm4w4ys6+UBwBmeuBSNkH7+DtLnYOYnOffgtE4yx8sH9S1VZ6RAAvROXzP2Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-discard-comments@7.0.8: resolution: {integrity: sha512-CvvS5S9WrXblFXCEJ9nVo+4z+eA7zSC7Z88V1HEJuwlQhlFnYTIjg1xJY+BCUiG2bvICap2tXii4mP22BD108Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-discard-duplicates@7.0.4: resolution: {integrity: sha512-VBNn1+EuMZkeGVVtz0gRfbNGtx9IFgAsAV+E2pHtXPrp4qfGBkhTIiAuE/wrb+Y6Pakg9NewAlfTpYIFAWODtw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-discard-empty@7.0.3: resolution: {integrity: sha512-M2pyjQCU+/7cMHVtL6bKTHjv0lZnPLMpicgr67Dlth7AbuV9gjVTtUqaRwn6Pp6BwSDspUzhz8SaUrRykJU5Dw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-discard-overridden@7.0.3: resolution: {integrity: sha512-aNovXo9UsZuRNLzHJtp13lHIvinDPfiXBPePpXkSjCbgp++iU2FqE+YxvjIsg6EdyPZsASFbfu+JcBFVsErXIQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-import@14.1.0: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} peerDependencies: - postcss: 8.5.23 + postcss: ^8.0.0 postcss-loader@8.2.0: resolution: {integrity: sha512-tHX+RkpsXVcc7st4dSdDGliI+r4aAQDuv+v3vFYHixb6YgjreG5AG4SEB0kDK8u2s6htqEEpKlkhSBUTvWKYnA==} engines: {node: '>= 18.12.0'} peerDependencies: '@rspack/core': 0.x || 1.x - postcss: 8.5.23 - webpack: 5.104.1 + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 peerDependenciesMeta: '@rspack/core': optional: true @@ -7943,8 +8482,8 @@ packages: engines: {node: '>= 18.12.0'} peerDependencies: '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 - postcss: 8.5.23 - webpack: 5.104.1 + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 peerDependenciesMeta: '@rspack/core': optional: true @@ -7958,133 +8497,133 @@ packages: resolution: {integrity: sha512-b3mfYUxR388u5Pt0HPcVIUtUDn/k15UfTY9M+ORW+meCR6JLNxoZffiYvXyOYQoRYQNZyX/UFkMCM/mNHxe1qA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-merge-rules@7.0.11: resolution: {integrity: sha512-SJUPM18g2BmPhf8BVlbwqWz4aK3pLu6u6xjfwEzra7xL6IBR10sUaiB++EzqcVfadPHrKBSMlNdP+XieykhI+Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-minify-font-values@7.0.3: resolution: {integrity: sha512-yilG/VOaNI74IylQvAQQxm3/wZVBkXyYUqNUAdxqwtbWUXPsbK1q8Ms0mL83v+f8YicgcyfYCRZtWACUdYajpA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-minify-gradients@7.0.5: resolution: {integrity: sha512-YraROyQRg3BI1+Hg8E05B/JPdnTm8EDSVu4P2BxdM+CRiOyfmou809+chGIqo6fQqwjPGQ947nbGncSjmTU1WQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-minify-params@7.0.9: resolution: {integrity: sha512-R8itbB8BhlpoYyBm1ou0dD+vJnQ3F6adQipR4UnkCHUwlo+S9WXJaDRg1RHjC8YVAtIdrQzSWvJl40HnGDTKjA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-minify-selectors@7.1.2: resolution: {integrity: sha512-aQtrEWKwqafNlExcKHQvPGsXR2+vlUqqJtf5XsCQcgsSb5PL4wlujWBYDJuWsP4UnQX1YHDHU8qRlD+1PzTQ+Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-modules-extract-imports@3.1.0: resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: 8.5.23 + postcss: ^8.1.0 postcss-modules-local-by-default@4.2.0: resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: 8.5.23 + postcss: ^8.1.0 postcss-modules-scope@3.2.1: resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: 8.5.23 + postcss: ^8.1.0 postcss-modules-values@4.0.0: resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: 8.5.23 + postcss: ^8.1.0 postcss-normalize-charset@7.0.3: resolution: {integrity: sha512-NoBfZu8PR4c2NlmjvrqQTzCzLY79hwcSRgNQ3ZiNK0ABzf9kYKloE/jNj+/8GQY1wsm8pRRgANk6ydLH8cwo0Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-display-values@7.0.3: resolution: {integrity: sha512-ldsCX0QIt05pKIOobZtVQ48wXJecr+czw4+e1/YjVhLMqslShgpVxgPtI2CefURR8oyVoYaU/l829MMwExDMLw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-positions@7.0.4: resolution: {integrity: sha512-VEvlpeGd3Ju1Hqa/oN4jaP3+ms4laYwkEL9N9u+B6k54PZjXbW1n6wI+aVprf1BQXlCYpS5+1pl/7/vHiKgARg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-repeat-style@7.0.4: resolution: {integrity: sha512-6mPKlY/8cSaDHxX502wERADarJsccwlky6yIrOapHH2ZgfoKAV94SbiTKfKEs4EEpdazuc3J72WsqeYk7hp9+Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-string@7.0.3: resolution: {integrity: sha512-HnEQPUchi1eznmDKEYrKUTqrprEq97SrpUYClgUkv7V2zRODD9DFoUsYU+m9ZOetmD5ku7fEMZB/lwy8IT6xVQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-timing-functions@7.0.3: resolution: {integrity: sha512-zmEzHdvpZBZu0OKlbJSfgASQvaayyAoVuWtvyr34IJ/LyS+DaOKvvR3EvFJ9RWWtNIx+CMvO125OVophaxNYew==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-unicode@7.0.9: resolution: {integrity: sha512-DRAdWfeh/TjmhLJsw91vdiWCnUod9iwvM7xyS02/nF/sLsCR3A8l3pztrSUrWG8DSBqfX7yEk9FM0USaVJ2mSg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-url@7.0.3: resolution: {integrity: sha512-CL93wmloq5qsffmFv+bw24MIRbmhHrp53qoh1LDAb/5TtjWEXI/np4xcP/Gw9oWCb2XyWnqHYLDUwiKRoJBA1Q==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-normalize-whitespace@7.0.3: resolution: {integrity: sha512-FdHjjn+Ht5Z2ZRjNOmeCbNq6lq09sUYKpmlF/Aq0XjVNSLTL6fmHlA/3swN2wP2caY9GV/tjSDcIIyS7aN7W0A==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-ordered-values@7.0.4: resolution: {integrity: sha512-nubSi49hDHQk4E8KIj+IbLY8Bg+8OcSUEhgyolgM+atnOvXjV7EjaR6bac4YGZoFyPa9mWoAF3EaYbWdFkKqVg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-reduce-initial@7.0.9: resolution: {integrity: sha512-ztTNPdIxXTxtBcG03E9u8v44M4ElXbMIRT7pf2onlquGula0Y83nKKxqM22FA/hMgkfCjN7ohevkVlaNwI8iOQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-reduce-transforms@7.0.3: resolution: {integrity: sha512-FXsnN9ZwcZTT8Yf8cAHA8qIGUXcX6WfLd9JoYhrdDfmvsVhhfqkkv7m4AC3rwFOfz+GzkUa87OCKF9dUcicd+g==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-selector-parser@7.1.3: resolution: {integrity: sha512-ajnd7iZnqjJDkyHNfznl/ZVO0lWqvBmQXfKKENx9/p/bEiF/L3eHwdydNUg9RXZx6xfZWOCmXmBa5oeB+YrAPQ==} @@ -8094,13 +8633,13 @@ packages: resolution: {integrity: sha512-2QfoFOYMcj8lwcVEf9WeTlkVIAm7u2QvOEhMzkQU3KUhhGX/l8hVV9EtjMv4iq3E9iI3OeeMN0YoMLbGusuigw==} engines: {node: ^18.12.0 || ^20.9.0 || >= 18} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-unique-selectors@7.0.7: resolution: {integrity: sha512-d+sCkaRnSefghOUdH8CMJZV9yUQhj2ojpe8Nw/lA+LV1UOfeleGkLTl6XdCFFSai9UJ+DJPb69FFuqthXYsY8w==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -8201,10 +8740,6 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} - raw-body@3.0.1: - resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} - engines: {node: '>= 0.10'} - raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -8434,48 +8969,56 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: glibc sass-embedded-linux-arm@1.93.2: resolution: {integrity: sha512-N3+D/ToHtzwLDO+lSH05Wo6/KRxFBPnbjVHASOlHzqJnK+g5cqex7IFAp6ozzlRStySk61Rp6d+YGrqZ6/P0PA==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] + libc: glibc sass-embedded-linux-musl-arm64@1.93.2: resolution: {integrity: sha512-+3EHuDPkMiAX5kytsjEC1bKZCawB9J6pm2eBIzzLMPWbf5xdx++vO1DpT7hD4bm4ZGn0eVHgSOKIfP6CVz6tVg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: musl sass-embedded-linux-musl-arm@1.93.2: resolution: {integrity: sha512-XBTvx66yRenvEsp3VaJCb3HQSyqCsUh7R+pbxcN5TuzueybZi0LXvn9zneksdXcmjACMlMpIVXi6LyHPQkYc8A==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] + libc: musl sass-embedded-linux-musl-riscv64@1.93.2: resolution: {integrity: sha512-0sB5kmVZDKTYzmCSlTUnjh6mzOhzmQiW/NNI5g8JS4JiHw2sDNTvt1dsFTuqFkUHyEOY3ESTsfHHBQV8Ip4bEA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: musl sass-embedded-linux-musl-x64@1.93.2: resolution: {integrity: sha512-t3ejQ+1LEVuHy7JHBI2tWHhoMfhedUNDjGJR2FKaLgrtJntGnyD1RyX0xb3nuqL/UXiEAtmTmZY+Uh3SLUe1Hg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: musl sass-embedded-linux-riscv64@1.93.2: resolution: {integrity: sha512-e7AndEwAbFtXaLy6on4BfNGTr3wtGZQmypUgYpSNVcYDO+CWxatKVY4cxbehMPhxG9g5ru+eaMfynvhZt7fLaA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: glibc sass-embedded-linux-x64@1.93.2: resolution: {integrity: sha512-U3EIUZQL11DU0xDDHXexd4PYPHQaSQa2hzc4EzmhHqrAj+TyfYO94htjWOd+DdTPtSwmLp+9cTWwPZBODzC96w==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: glibc sass-embedded-unknown-all@1.93.2: resolution: {integrity: sha512-7VnaOmyewcXohiuoFagJ3SK5ddP9yXpU0rzz+pZQmS1/+5O6vzyFCUoEt3HDRaLctH4GT3nUGoK1jg0ae62IfQ==} @@ -8506,7 +9049,7 @@ packages: node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 sass: ^1.3.0 sass-embedded: '*' - webpack: 5.104.1 + webpack: ^5.0.0 peerDependenciesMeta: '@rspack/core': optional: true @@ -8558,6 +9101,10 @@ packages: select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + selfsigned@5.5.0: resolution: {integrity: sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==} engines: {node: '>=18'} @@ -8740,7 +9287,7 @@ packages: resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} engines: {node: '>= 18.12.0'} peerDependencies: - webpack: 5.104.1 + webpack: ^5.72.1 source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -8911,13 +9458,13 @@ packages: resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} engines: {node: '>= 12.13.0'} peerDependencies: - webpack: 5.104.1 + webpack: ^5.0.0 stylehacks@7.0.11: resolution: {integrity: sha512-iODNfhXVLqc5LADs+Y6Oh5wJuK5ZcHbVng8aiK3y9pjMQdc5hLrBW0eFU6FtnpNrE6PoEg/MmFTU4waotj5WNg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: - postcss: 8.5.23 + postcss: ^8.5.13 super-regex@1.0.0: resolution: {integrity: sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==} @@ -9000,7 +9547,7 @@ packages: '@swc/core': '*' esbuild: '*' uglify-js: '*' - webpack: 5.104.1 + webpack: ^5.1.0 peerDependenciesMeta: '@swc/core': optional: true @@ -9078,6 +9625,10 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} @@ -9147,7 +9698,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: - '@babel/core': 7.29.6 + '@babel/core': '>=7.0.0-beta.0 <8' '@jest/transform': ^29.0.0 || ^30.0.0 '@jest/types': ^29.0.0 || ^30.0.0 babel-jest: ^29.0.0 || ^30.0.0 @@ -9174,7 +9725,7 @@ packages: engines: {node: '>=12.0.0'} peerDependencies: typescript: '*' - webpack: 5.104.1 + webpack: ^5.0.0 ts-morph@21.0.1: resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==} @@ -9406,8 +9957,9 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -9446,11 +9998,51 @@ packages: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: typescript: '*' - vite: 7.3.6 + vite: '*' peerDependenciesMeta: vite: optional: true + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.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' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@7.3.6: resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9466,7 +10058,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: 2.8.3 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true @@ -9555,7 +10147,7 @@ packages: hasBin: true peerDependencies: '@webpack-cli/generators': '*' - webpack: 5.104.1 + webpack: 5.x.x webpack-bundle-analyzer: '*' webpack-dev-server: '*' peerDependenciesMeta: @@ -9570,17 +10162,30 @@ packages: resolution: {integrity: sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==} engines: {node: '>= 18.12.0'} peerDependencies: - webpack: 5.104.1 + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + + webpack-dev-server@5.2.2: + resolution: {integrity: sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==} + engines: {node: '>= 18.12.0'} + hasBin: true + peerDependencies: + webpack: ^5.0.0 + webpack-cli: '*' peerDependenciesMeta: webpack: optional: true + webpack-cli: + optional: true webpack-dev-server@5.2.6: resolution: {integrity: sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==} engines: {node: '>= 18.12.0'} hasBin: true peerDependencies: - webpack: 5.104.1 + webpack: ^5.0.0 webpack-cli: '*' peerDependenciesMeta: webpack: @@ -9605,7 +10210,7 @@ packages: engines: {node: '>= 12'} peerDependencies: html-webpack-plugin: '>= 5.0.0-beta.1 < 6' - webpack: 5.104.1 + webpack: ^5.12.0 peerDependenciesMeta: html-webpack-plugin: optional: true @@ -9920,18 +10525,18 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@analogjs/vite-plugin-angular@1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3))': + '@analogjs/vite-plugin-angular@1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3))': dependencies: ts-morph: 21.0.1 vfile: 6.0.3 optionalDependencies: - '@angular/build': 20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3) + '@angular/build': 20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3) - '@analogjs/vitest-angular@1.19.4(@analogjs/vite-plugin-angular@1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)))(@angular-devkit/architect@0.2003.24(chokidar@4.0.3))(vitest@3.2.6)': + '@analogjs/vitest-angular@1.19.4(@analogjs/vite-plugin-angular@1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)))(@angular-devkit/architect@0.2003.24(chokidar@4.0.3))(vitest@3.2.6)': dependencies: - '@analogjs/vite-plugin-angular': 1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)) + '@analogjs/vite-plugin-angular': 1.19.4(@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)) '@angular-devkit/architect': 0.2003.24(chokidar@4.0.3) - vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) '@angular-devkit/architect@0.2003.24(chokidar@4.0.3)': dependencies: @@ -9949,10 +10554,10 @@ snapshots: '@angular-devkit/core@19.2.15(chokidar@4.0.3)': dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) jsonc-parser: 3.3.1 - picomatch: 4.0.4 + picomatch: 4.0.2 rxjs: 7.8.1 source-map: 0.7.4 optionalDependencies: @@ -9971,10 +10576,10 @@ snapshots: '@angular-devkit/core@20.3.3(chokidar@4.0.3)': dependencies: - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) jsonc-parser: 3.3.1 - picomatch: 4.0.4 + picomatch: 4.0.3 rxjs: 7.8.2 source-map: 0.7.6 optionalDependencies: @@ -10000,44 +10605,44 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-eslint/builder@20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@angular-eslint/builder@20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2)': dependencies: '@angular-devkit/architect': 0.2003.3(chokidar@4.0.3) - '@angular-devkit/core': 20.3.3(chokidar@4.0.3) - eslint: 9.36.0(jiti@2.4.2) + '@angular-devkit/core': 20.3.24(chokidar@4.0.3) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - chokidar '@angular-eslint/bundled-angular-compiler@20.3.0': {} - '@angular-eslint/eslint-plugin-template@20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@angular-eslint/eslint-plugin-template@20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.3.0 - '@angular-eslint/template-parser': 20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@angular-eslint/utils': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@angular-eslint/template-parser': 20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@angular-eslint/utils': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) aria-query: 5.3.2 axobject-query: 4.1.0 - eslint: 9.36.0(jiti@2.4.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 - '@angular-eslint/eslint-plugin@20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@angular-eslint/eslint-plugin@20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.3.0 - '@angular-eslint/utils': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - eslint: 9.36.0(jiti@2.4.2) + '@angular-eslint/utils': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 - '@angular-eslint/schematics@20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@angular-eslint/schematics@20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2)': dependencies: - '@angular-devkit/core': 20.3.3(chokidar@4.0.3) + '@angular-devkit/core': 20.3.24(chokidar@4.0.3) '@angular-devkit/schematics': 20.3.24(chokidar@4.0.3) - '@angular-eslint/eslint-plugin': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@angular-eslint/eslint-plugin-template': 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@angular-eslint/eslint-plugin': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@angular-eslint/eslint-plugin-template': 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) ignore: 7.0.5 semver: 7.7.2 strip-json-comments: 3.1.1 @@ -10049,18 +10654,18 @@ snapshots: - eslint - typescript - '@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.3.0 - eslint: 9.36.0(jiti@2.4.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) eslint-scope: 8.4.0 typescript: 5.9.2 - '@angular-eslint/utils@20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@angular-eslint/utils@20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 20.3.0 - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - eslint: 9.36.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 '@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))': @@ -10068,29 +10673,29 @@ snapshots: '@angular/core': 20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1) tslib: 2.8.1 - '@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)': + '@angular/build@20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.24(chokidar@4.0.3) '@angular/compiler': 20.3.30 - '@angular/compiler-cli': 20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2) - '@babel/core': 7.29.6 + '@angular/compiler-cli': 20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2) + '@babel/core': 7.28.3(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 '@inquirer/confirm': 5.1.14(@types/node@20.19.9) - '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3)) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3)) beasties: 0.3.5 browserslist: 4.28.1 - esbuild: 0.28.1 - https-proxy-agent: 7.0.6 - istanbul-lib-instrument: 6.0.3 + esbuild: 0.28.0 + https-proxy-agent: 7.0.6(supports-color@8.1.1) + istanbul-lib-instrument: 6.0.3(supports-color@8.1.1) jsonc-parser: 3.3.1 listr2: 9.0.1 magic-string: 0.30.17 mrmime: 2.0.1 parse5-html-rewriting-stream: 8.0.0 picomatch: 4.0.4 - piscina: 5.2.0 + piscina: 5.1.3 rollup: 4.59.0 sass: 1.90.0 semver: 7.7.2 @@ -10098,7 +10703,7 @@ snapshots: tinyglobby: 0.2.14 tslib: 2.8.1 typescript: 5.9.2 - vite: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3) + vite: 7.3.2(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3) watchpack: 2.4.4 optionalDependencies: '@angular/core': 20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1) @@ -10106,7 +10711,7 @@ snapshots: less: 4.4.1 lmdb: 3.4.2 postcss: 8.5.23 - vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - chokidar @@ -10128,14 +10733,14 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/cli@20.3.24(@types/node@20.19.9)(chokidar@4.0.3)': + '@angular/cli@20.3.24(@types/node@20.19.9)(chokidar@4.0.3)(supports-color@8.1.1)': dependencies: '@angular-devkit/architect': 0.2003.24(chokidar@4.0.3) '@angular-devkit/core': 20.3.24(chokidar@4.0.3) '@angular-devkit/schematics': 20.3.24(chokidar@4.0.3) '@inquirer/prompts': 7.8.2(@types/node@20.19.9) '@listr2/prompt-adapter-inquirer': 3.0.1(@inquirer/prompts@7.8.2(@types/node@20.19.9))(@types/node@20.19.9)(listr2@9.0.1) - '@modelcontextprotocol/sdk': 1.27.1(zod@4.1.13) + '@modelcontextprotocol/sdk': 1.26.0(supports-color@8.1.1)(zod@4.1.13) '@schematics/angular': 20.3.24(chokidar@4.0.3) '@yarnpkg/lockfile': 1.1.0 algoliasearch: 5.35.0 @@ -10143,7 +10748,7 @@ snapshots: jsonc-parser: 3.3.1 listr2: 9.0.1 npm-package-arg: 13.0.0 - pacote: 21.0.4 + pacote: 21.0.4(supports-color@8.1.1) resolve: 1.22.10 semver: 7.7.2 yargs: 18.0.0 @@ -10160,10 +10765,10 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2)': + '@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@angular/compiler': 20.3.30 - '@babel/core': 7.29.6 + '@babel/core': 7.28.3(supports-color@8.1.1) '@jridgewell/sourcemap-codec': 1.5.5 chokidar: 4.0.3 convert-source-map: 1.9.0 @@ -10252,20 +10857,40 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.6': + '@babel/core@7.28.3(supports-color@8.1.1)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.3(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.0(supports-color@8.1.1) + '@babel/types': 7.29.7 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.29.6(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.3 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10280,14 +10905,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@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/generator@7.29.7': dependencies: '@babel/parser': 7.29.7 @@ -10304,36 +10921,36 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.1 + browserslist: 4.28.9 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.29.6)': + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.27.1(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.6) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.29.6)': + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.29.6)': + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -10341,26 +10958,35 @@ snapshots: '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.27.1': + '@babel/helper-member-expression-to-functions@7.27.1(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.28.6(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.6)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.3(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.0(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': + dependencies: + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -10372,27 +10998,27 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.6)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.29.0 + '@babel/helper-wrap-function': 7.28.3(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.27.1(@babel/core@7.29.6)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-member-expression-to-functions': 7.27.1(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + '@babel/helper-skip-transparent-expression-wrappers@7.27.1(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -10413,10 +11039,10 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - '@babel/helper-wrap-function@7.28.3': + '@babel/helper-wrap-function@7.28.3(supports-color@8.1.1)': dependencies: '@babel/template': 7.29.7 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -10438,602 +11064,602 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.6) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.29.6)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.6)': + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.29.6) + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.6)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.6)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.6)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.6)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.6)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.6)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.6)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.6)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.6)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.6)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.6)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.6)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.6)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.6)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.6)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.6)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.29.6)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.6) - '@babel/traverse': 7.29.0 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-imports': 7.28.6 + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.6) + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-block-scoping@7.28.4(@babel/core@7.29.6)': + '@babel/plugin-transform-block-scoping@7.28.4(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.29.6)': + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.29.6)': + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.6) - '@babel/traverse': 7.29.0 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/template': 7.29.7 - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.29.6)': + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.29.6)': + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.6) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.6)': + '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.29.6)': + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.6) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.6) - '@babel/traverse': 7.29.0 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.6) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.6)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.29.6)': + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-runtime@7.28.3(@babel/core@7.29.6)': + '@babel/plugin-transform-runtime@7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-module-imports': 7.28.6 + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.29.6) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.6) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.29.6) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.29.6)': + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6) + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.6) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.29.6)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/preset-env@7.28.3(@babel/core@7.29.6)': + '@babel/preset-env@7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.29.6) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.6) - '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.6) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.29.6) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-block-scoping': 7.28.4(@babel/core@7.29.6) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.29.6) - '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.29.6) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.6) - '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.29.6) - '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.6) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.29.6) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.6) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.29.6) - '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.29.6) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.6) - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.29.6) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.6) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.29.6) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-block-scoping': 7.28.4(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.6(supports-color@8.1.1)) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.45.1 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.6)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.6(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/types': 7.29.7 esutils: 2.0.3 - '@babel/preset-typescript@7.27.1(@babel/core@7.29.6)': + '@babel/preset-typescript@7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.29.6) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11057,15 +11683,15 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.0(supports-color@8.1.1)': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 '@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(supports-color@7.2.0) + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11293,127 +11919,259 @@ snapshots: '@esbuild/aix-ppc64@0.25.5': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true '@esbuild/android-arm64@0.25.5': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + '@esbuild/android-arm64@0.28.1': optional: true '@esbuild/android-arm@0.25.5': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + '@esbuild/android-arm@0.28.1': optional: true '@esbuild/android-x64@0.25.5': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + '@esbuild/android-x64@0.28.1': optional: true '@esbuild/darwin-arm64@0.25.5': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + '@esbuild/darwin-arm64@0.28.1': optional: true '@esbuild/darwin-x64@0.25.5': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + '@esbuild/darwin-x64@0.28.1': optional: true '@esbuild/freebsd-arm64@0.25.5': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + '@esbuild/freebsd-arm64@0.28.1': optional: true '@esbuild/freebsd-x64@0.25.5': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + '@esbuild/freebsd-x64@0.28.1': optional: true '@esbuild/linux-arm64@0.25.5': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + '@esbuild/linux-arm64@0.28.1': optional: true '@esbuild/linux-arm@0.25.5': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + '@esbuild/linux-arm@0.28.1': optional: true '@esbuild/linux-ia32@0.25.5': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + '@esbuild/linux-ia32@0.28.1': optional: true '@esbuild/linux-loong64@0.25.5': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + '@esbuild/linux-loong64@0.28.1': optional: true '@esbuild/linux-mips64el@0.25.5': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + '@esbuild/linux-mips64el@0.28.1': optional: true '@esbuild/linux-ppc64@0.25.5': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + '@esbuild/linux-ppc64@0.28.1': optional: true '@esbuild/linux-riscv64@0.25.5': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + '@esbuild/linux-riscv64@0.28.1': optional: true '@esbuild/linux-s390x@0.25.5': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + '@esbuild/linux-s390x@0.28.1': optional: true '@esbuild/linux-x64@0.25.5': optional: true - '@esbuild/linux-x64@0.28.1': + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.25.5': + '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-arm64@0.28.1': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.25.5': + '@esbuild/openbsd-x64@0.25.5': optional: true - '@esbuild/netbsd-x64@0.28.1': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.25.5': + '@esbuild/openbsd-x64@0.28.0': optional: true - '@esbuild/openbsd-arm64@0.28.1': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.25.5': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.1': + '@esbuild/openharmony-arm64@0.28.0': optional: true '@esbuild/openharmony-arm64@0.28.1': @@ -11422,38 +12180,62 @@ snapshots: '@esbuild/sunos-x64@0.25.5': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + '@esbuild/sunos-x64@0.28.1': optional: true '@esbuild/win32-arm64@0.25.5': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + '@esbuild/win32-arm64@0.28.1': optional: true '@esbuild/win32-ia32@0.25.5': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + '@esbuild/win32-ia32@0.28.1': optional: true '@esbuild/win32-x64@0.25.5': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))': dependencies: - eslint: 9.36.0(jiti@2.4.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.21.0': + '@eslint/config-array@0.21.0(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.4 transitivePeerDependencies: - supports-color @@ -11464,10 +12246,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.1(supports-color@8.1.1)': dependencies: ajv: 6.14.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -11636,6 +12418,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.9 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.1': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -11668,13 +12456,13 @@ snapshots: jest-util: 30.0.5 slash: 3.0.0 - '@jest/core@30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))': + '@jest/core@30.1.3(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))': dependencies: '@jest/console': 30.1.2 '@jest/pattern': 30.0.1 - '@jest/reporters': 30.1.3 + '@jest/reporters': 30.1.3(supports-color@8.1.1) '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 + '@jest/transform': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 '@types/node': 20.19.9 ansi-escapes: 4.3.2 @@ -11683,15 +12471,15 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.0.5 - jest-config: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + jest-config: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) jest-haste-map: 30.1.0 jest-message-util: 30.1.0 jest-regex-util: 30.0.1 jest-resolve: 30.1.3 - jest-resolve-dependencies: 30.1.3 - jest-runner: 30.1.3 - jest-runtime: 30.1.3 - jest-snapshot: 30.1.2 + jest-resolve-dependencies: 30.1.3(supports-color@8.1.1) + jest-runner: 30.1.3(supports-color@8.1.1) + jest-runtime: 30.1.3(supports-color@8.1.1) + jest-snapshot: 30.1.2(supports-color@8.1.1) jest-util: 30.0.5 jest-validate: 30.1.0 jest-watcher: 30.1.3 @@ -11717,10 +12505,10 @@ snapshots: dependencies: '@jest/get-type': 30.1.0 - '@jest/expect@30.1.2': + '@jest/expect@30.1.2(supports-color@8.1.1)': dependencies: expect: 30.1.2 - jest-snapshot: 30.1.2 + jest-snapshot: 30.1.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11735,10 +12523,10 @@ snapshots: '@jest/get-type@30.1.0': {} - '@jest/globals@30.1.2': + '@jest/globals@30.1.2(supports-color@8.1.1)': dependencies: '@jest/environment': 30.1.2 - '@jest/expect': 30.1.2 + '@jest/expect': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 jest-mock: 30.0.5 transitivePeerDependencies: @@ -11749,12 +12537,12 @@ snapshots: '@types/node': 20.19.9 jest-regex-util: 30.0.1 - '@jest/reporters@30.1.3': + '@jest/reporters@30.1.3(supports-color@8.1.1)': dependencies: '@bcoe/v8-coverage': 0.2.3 '@jest/console': 30.1.2 '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 + '@jest/transform': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 '@jridgewell/trace-mapping': 0.3.31 '@types/node': 20.19.9 @@ -11764,9 +12552,9 @@ snapshots: glob: 10.5.0 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 + istanbul-lib-instrument: 6.0.3(supports-color@8.1.1) istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 + istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) istanbul-reports: 3.2.0 jest-message-util: 30.1.0 jest-util: 30.0.5 @@ -11808,12 +12596,12 @@ snapshots: jest-haste-map: 30.1.0 slash: 3.0.0 - '@jest/transform@30.1.2': + '@jest/transform@30.1.2(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@jest/types': 30.0.5 '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 7.0.1 + babel-plugin-istanbul: 7.0.1(supports-color@8.1.1) chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 @@ -12001,8 +12789,8 @@ snapshots: '@rushstack/rig-package': 0.6.0 '@rushstack/terminal': 0.19.3(@types/node@20.19.9) '@rushstack/ts-command-line': 5.1.3(@types/node@20.19.9) - lodash: 4.18.1 - minimatch: 10.2.3 + lodash: 4.17.23 + minimatch: 10.0.3 resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 @@ -12013,13 +12801,13 @@ snapshots: '@microsoft/tsdoc-config@0.17.1': dependencies: '@microsoft/tsdoc': 0.15.1 - ajv: 8.18.0 + ajv: 8.12.0 jju: 1.4.0 resolve: 1.22.10 '@microsoft/tsdoc@0.15.1': {} - '@modelcontextprotocol/sdk@1.27.1(zod@4.1.13)': + '@modelcontextprotocol/sdk@1.26.0(supports-color@8.1.1)(zod@4.1.13)': dependencies: '@hono/node-server': 1.19.15(hono@4.13.5) ajv: 8.18.0 @@ -12029,13 +12817,13 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 8.3.1(express@5.2.1) + express: 5.2.1(supports-color@8.1.1) + express-rate-limit: 8.3.1(express@5.2.1(supports-color@8.1.1)) hono: 4.13.5 jose: 6.2.1 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 - raw-body: 3.0.1 + raw-body: 3.0.2 zod: 4.1.13 zod-to-json-schema: 3.25.1(zod@4.1.13) transitivePeerDependencies: @@ -12089,7 +12877,7 @@ snapshots: tapable: 2.3.0 optionalDependencies: typescript: 5.9.2 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) transitivePeerDependencies: - '@rspack/core' - bufferutil @@ -12127,7 +12915,7 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) tapable: 2.3.0 optionalDependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) transitivePeerDependencies: - '@rspack/core' - bufferutil @@ -12317,9 +13105,9 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1)': dependencies: - file-type: 21.3.2 + file-type: 21.0.0(supports-color@8.1.1) iterare: 1.2.1 load-esm: 1.0.2 reflect-metadata: 0.1.14 @@ -12329,9 +13117,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/core@11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -12341,21 +13129,21 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(supports-color@8.1.1) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(reflect-metadata@0.1.14)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) reflect-metadata: 0.1.14 - '@nestjs/platform-express@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + '@nestjs/platform-express@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(supports-color@8.1.1)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) cors: 2.8.5 - express: 5.1.0 - multer: 2.2.0 - path-to-regexp: 8.4.0 + express: 5.1.0(supports-color@8.1.1) + multer: 2.0.2 + path-to-regexp: 8.2.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -12371,33 +13159,33 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/serve-static@5.0.3(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)(express@5.2.1)': + '@nestjs/serve-static@5.0.3(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(express@5.2.1(supports-color@8.1.1))': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) - path-to-regexp: 8.4.0 + '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) + path-to-regexp: 8.2.0 optionalDependencies: - express: 5.2.1 + express: 5.2.1(supports-color@8.1.1) - '@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.1.14)': + '@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(reflect-metadata@0.1.14)': dependencies: '@microsoft/tsdoc': 0.15.1 - '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14) - js-yaml: 4.3.1 - lodash: 4.18.1 - path-to-regexp: 8.4.0 + '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(reflect-metadata@0.1.14) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 8.2.0 reflect-metadata: 0.1.14 swagger-ui-dist: 5.21.0 - '@nestjs/testing@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.6)': + '@nestjs/testing@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.6)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.1.14)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.18)(supports-color@8.1.1) '@ngneat/spectator@22.0.0(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/router@20.3.30(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))': dependencies: @@ -12484,13 +13272,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@npmcli/agent@4.0.0': + '@npmcli/agent@4.0.0(supports-color@8.1.1)': dependencies: agent-base: 7.1.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) lru-cache: 11.2.2 - socks-proxy-agent: 8.0.5 + socks-proxy-agent: 8.0.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -12532,12 +13320,12 @@ snapshots: '@npmcli/redact@4.0.0': {} - '@npmcli/run-script@10.0.4': + '@npmcli/run-script@10.0.4(supports-color@8.1.1)': dependencies: '@npmcli/node-gyp': 5.0.0 '@npmcli/package-json': 7.0.5 '@npmcli/promise-spawn': 9.0.1 - node-gyp: 12.2.0 + node-gyp: 12.2.0(supports-color@8.1.1) proc-log: 6.1.0 transitivePeerDependencies: - supports-color @@ -12546,21 +13334,21 @@ snapshots: dependencies: consola: 3.4.2 - '@nx/angular@22.7.9(facc33cf0716ce05a968cc4e029d9e3c)': + '@nx/angular@22.7.9(3f137da2344d682390527b18b5279b4a)': dependencies: - '@angular-devkit/core': 20.3.3(chokidar@4.0.3) + '@angular-devkit/core': 20.3.24(chokidar@4.0.3) '@angular-devkit/schematics': 20.3.24(chokidar@4.0.3) - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/eslint': 22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/module-federation': 22.7.9(7c4ed127403b24d69ea201d25687d59e) - '@nx/rspack': 22.7.9(c368dd4c0f8a032c73679fe612264804) - '@nx/web': 22.7.9(c35aa15b9f1d41e28c1dcb3ee2fdad58) - '@nx/webpack': 22.7.9(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(typescript@5.9.2)(webpack-cli@5.1.4(webpack@5.104.1)) - '@nx/workspace': 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/eslint': 22.7.9(970ddfd867418be042d7af6db9ddbf38) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) + '@nx/module-federation': 22.7.9(226af6d766ed45442586785119fb6e50) + '@nx/rspack': 22.7.9(fcd7df152813cab5f46918f0293c4750) + '@nx/web': 22.7.9(034a38cadaa6b7dbc4ae32d15f7f7ab4) + '@nx/webpack': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(uglify-js@3.19.3)(webpack-cli@5.1.4) + '@nx/workspace': 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) '@schematics/angular': 20.3.24(chokidar@4.0.3) - '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) enquirer: 2.3.6 magic-string: 0.30.19 picocolors: 1.1.1 @@ -12570,7 +13358,7 @@ snapshots: tslib: 2.8.1 webpack-merge: 5.10.0 optionalDependencies: - '@angular/build': 20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3) + '@angular/build': 20.3.24(@angular/compiler-cli@20.3.19(@angular/compiler@20.3.30)(supports-color@8.1.1)(typescript@5.9.2))(@angular/compiler@20.3.30)(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.30(@angular/animations@20.2.4(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.30(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.30(@angular/compiler@20.3.30)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@20.19.9)(chokidar@4.0.3)(jiti@2.4.2)(less@4.4.1)(postcss@8.5.23)(sass-embedded@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(tslib@2.8.1)(typescript@5.9.2)(vitest@3.2.6)(yaml@2.8.3) transitivePeerDependencies: - '@babel/traverse' - '@module-federation/enhanced' @@ -12608,29 +13396,29 @@ snapshots: - webpack-cli - webpack-hot-middleware - '@nx/devkit@22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': + '@nx/devkit@22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) + nx: 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) semver: 7.7.2 tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/docker@22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': + '@nx/docker@22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) enquirer: 2.3.6 tslib: 2.8.1 transitivePeerDependencies: - nx - '@nx/esbuild@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': + '@nx/esbuild@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) picocolors: 1.1.1 tinyglobby: 0.2.15 tsconfig-paths: 4.2.0 @@ -12645,14 +13433,14 @@ snapshots: - supports-color - verdaccio - '@nx/eslint-plugin@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.4.2)))(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(typescript@5.9.2)': + '@nx/eslint-plugin@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) chalk: 4.1.2 confusing-browser-globals: 1.0.11 globals: 17.12.0 @@ -12660,7 +13448,7 @@ snapshots: semver: 7.7.2 tslib: 2.8.1 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@9.36.0(jiti@2.4.2)) + eslint-config-prettier: 10.1.8(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -12671,16 +13459,16 @@ snapshots: - typescript - verdaccio - '@nx/eslint@22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': + '@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - eslint: 9.36.0(jiti@2.4.2) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) semver: 7.7.2 tslib: 2.8.1 typescript: 5.9.2 optionalDependencies: - '@nx/jest': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) + '@nx/jest': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) '@zkochan/js-yaml': 0.0.7 transitivePeerDependencies: - '@babel/traverse' @@ -12690,15 +13478,15 @@ snapshots: - supports-color - verdaccio - '@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2)': + '@nx/jest@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2)': dependencies: - '@jest/reporters': 30.1.3 + '@jest/reporters': 30.1.3(supports-color@8.1.1) '@jest/test-result': 30.1.3 - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) identity-obj-proxy: 3.0.0 - jest-config: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + jest-config: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) jest-resolve: 30.1.3 jest-util: 30.0.5 minimatch: 10.2.5 @@ -12721,21 +13509,21 @@ snapshots: - typescript - verdaccio - '@nx/js@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))': + '@nx/js@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.6 - '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.6) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-transform-runtime': 7.28.3(@babel/core@7.29.6) - '@babel/preset-env': 7.28.3(@babel/core@7.29.6) - '@babel/preset-typescript': 7.27.1(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-runtime': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-env': 7.28.3(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) '@babel/runtime': 7.28.4 - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/workspace': 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/workspace': 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) '@zkochan/js-yaml': 0.0.7 - babel-plugin-const-enum: 1.2.0(@babel/core@7.29.6) + babel-plugin-const-enum: 1.2.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-macros: 3.1.0 - babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.29.6)(@babel/traverse@7.29.0) + babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.29.6(supports-color@8.1.1))(@babel/traverse@7.29.0(supports-color@8.1.1)) chalk: 4.1.2 columnify: 1.6.0 detect-port: 2.1.0 @@ -12756,20 +13544,20 @@ snapshots: - nx - supports-color - '@nx/module-federation@22.7.9(7c4ed127403b24d69ea201d25687d59e)': + '@nx/module-federation@22.7.9(226af6d766ed45442586785119fb6e50)': dependencies: '@module-federation/enhanced': 2.9.0(@rspack/core@1.6.8(@swc/helpers@0.5.17))(typescript@5.9.2)(webpack@5.104.1) '@module-federation/node': 2.7.50(@rspack/core@1.6.8(@swc/helpers@0.5.17))(typescript@5.9.2)(webpack@5.104.1) '@module-federation/sdk': 2.9.0 - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/web': 22.7.9(c35aa15b9f1d41e28c1dcb3ee2fdad58) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) + '@nx/web': 22.7.9(034a38cadaa6b7dbc4ae32d15f7f7ab4) '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - express: 4.22.2 - http-proxy-middleware: 3.0.7 + express: 4.22.2(supports-color@8.1.1) + http-proxy-middleware: 3.0.7(supports-color@8.1.1) picocolors: 1.1.1 tslib: 2.8.1 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) transitivePeerDependencies: - '@babel/traverse' - '@nx/cypress' @@ -12793,13 +13581,13 @@ snapshots: - vue-tsc - webpack-cli - '@nx/nest@22.7.9(c46c164d45cf7da0c8732e0c30f9c061)': + '@nx/nest@22.7.9(c33e04cd93160f87e5faef925fe4fc14)': dependencies: '@nestjs/schematics': 11.0.7(chokidar@4.0.3)(typescript@5.9.2) - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/eslint': 22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/node': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/eslint': 22.7.9(970ddfd867418be042d7af6db9ddbf38) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) + '@nx/node': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) tslib: 2.8.1 transitivePeerDependencies: - '@babel/traverse' @@ -12819,15 +13607,15 @@ snapshots: - typescript - verdaccio - '@nx/node@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2)': + '@nx/node@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/docker': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/eslint': 22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/jest': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/docker': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/eslint': 22.7.9(970ddfd867418be042d7af6db9ddbf38) + '@nx/jest': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) kill-port: 1.6.1 - tcp-port-used: 1.0.2 + tcp-port-used: 1.0.2(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - '@babel/traverse' @@ -12875,24 +13663,24 @@ snapshots: '@nx/nx-win32-x64-msvc@22.7.9': optional: true - '@nx/rspack@22.7.9(c368dd4c0f8a032c73679fe612264804)': + '@nx/rspack@22.7.9(fcd7df152813cab5f46918f0293c4750)': dependencies: '@module-federation/enhanced': 2.9.0(@rspack/core@1.6.8(@swc/helpers@0.5.17))(typescript@5.9.2)(webpack@5.104.1) '@module-federation/node': 2.7.50(@rspack/core@1.6.8(@swc/helpers@0.5.17))(typescript@5.9.2)(webpack@5.104.1) - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/module-federation': 22.7.9(7c4ed127403b24d69ea201d25687d59e) - '@nx/web': 22.7.9(c35aa15b9f1d41e28c1dcb3ee2fdad58) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) + '@nx/module-federation': 22.7.9(226af6d766ed45442586785119fb6e50) + '@nx/web': 22.7.9(034a38cadaa6b7dbc4ae32d15f7f7ab4) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - '@rspack/dev-server': 1.1.4(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@types/express@5.0.6)(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1) + '@rspack/dev-server': 1.1.4(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@types/express@5.0.6)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1) '@rspack/plugin-react-refresh': 1.5.1(react-refresh@0.17.0) autoprefixer: 10.4.21(postcss@8.5.23) browserslist: 4.28.1 css-loader: 6.11.0(@rspack/core@1.6.8(@swc/helpers@0.5.17))(webpack@5.104.1) enquirer: 2.3.6 - express: 4.22.2 - http-proxy-middleware: 3.0.7 + express: 4.22.2(supports-color@8.1.1) + http-proxy-middleware: 3.0.7(supports-color@8.1.1) less-loader: 12.3.3(@rspack/core@1.6.8(@swc/helpers@0.5.17))(less@4.4.1)(webpack@5.104.1) license-webpack-plugin: 4.0.2(webpack@5.104.1) loader-utils: 2.0.4 @@ -12908,7 +13696,7 @@ snapshots: style-loader: 3.3.4(webpack@5.104.1) ts-checker-rspack-plugin: 1.1.5(@rspack/core@1.6.8(@swc/helpers@0.5.17))(typescript@5.9.2) tslib: 2.8.1 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@babel/traverse' @@ -12938,11 +13726,11 @@ snapshots: - webpack-cli - webpack-hot-middleware - '@nx/vite@22.7.9(6df4234e208ff1705d1304e6172497ea)': + '@nx/vite@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))(vitest@3.2.6)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/vitest': 22.7.9(6df4234e208ff1705d1304e6172497ea) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) + '@nx/vitest': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))(vitest@3.2.6) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) ajv: 8.18.0 enquirer: 2.3.6 @@ -12951,7 +13739,7 @@ snapshots: tsconfig-paths: 4.2.0 tslib: 2.8.1 vite: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) - vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) transitivePeerDependencies: - '@babel/traverse' - '@nx/eslint' @@ -12962,17 +13750,17 @@ snapshots: - typescript - verdaccio - '@nx/vitest@22.7.9(6df4234e208ff1705d1304e6172497ea)': + '@nx/vitest@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))(vitest@3.2.6)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) semver: 7.7.4 tslib: 2.8.1 optionalDependencies: - '@nx/eslint': 22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/eslint': 22.7.9(970ddfd867418be042d7af6db9ddbf38) vite: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) - vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -12982,19 +13770,19 @@ snapshots: - typescript - verdaccio - '@nx/web@22.7.9(c35aa15b9f1d41e28c1dcb3ee2fdad58)': + '@nx/web@22.7.9(034a38cadaa6b7dbc4ae32d15f7f7ab4)': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) detect-port: 2.1.0 - http-server: 14.1.1 + http-server: 14.1.1(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) picocolors: 1.1.1 tslib: 2.8.1 optionalDependencies: - '@nx/eslint': 22.7.9(@babel/traverse@7.29.0)(@nx/jest@22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.36.0(jiti@2.4.2))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/jest': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) - '@nx/vite': 22.7.9(6df4234e208ff1705d1304e6172497ea) - '@nx/webpack': 22.7.9(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(typescript@5.9.2)(webpack-cli@5.1.4(webpack@5.104.1)) + '@nx/eslint': 22.7.9(970ddfd867418be042d7af6db9ddbf38) + '@nx/jest': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2))(typescript@5.9.2) + '@nx/vite': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@nx/eslint@22.7.9(970ddfd867418be042d7af6db9ddbf38))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))(vitest@3.2.6) + '@nx/webpack': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(uglify-js@3.19.3)(webpack-cli@5.1.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -13004,19 +13792,19 @@ snapshots: - supports-color - verdaccio - '@nx/webpack@22.7.9(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(typescript@5.9.2)(webpack-cli@5.1.4(webpack@5.104.1))': + '@nx/webpack@22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.25.5)(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1)(typescript@5.9.2)(uglify-js@3.19.3)(webpack-cli@5.1.4)': dependencies: - '@babel/core': 7.29.6 - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) - '@nx/js': 22.7.9(@babel/traverse@7.29.0)(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/js': 22.7.9(@babel/traverse@7.29.0(supports-color@8.1.1))(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)))(supports-color@8.1.1) '@phenomnomnominal/tsquery': 6.2.0(typescript@5.9.2) ajv: 8.18.0 autoprefixer: 10.4.21(postcss@8.5.23) - babel-loader: 9.2.1(@babel/core@7.29.6)(webpack@5.104.1) + babel-loader: 9.2.1(@babel/core@7.29.6(supports-color@8.1.1))(webpack@5.104.1) browserslist: 4.28.1 copy-webpack-plugin: 14.0.0(webpack@5.104.1) css-loader: 6.11.0(@rspack/core@1.6.8(@swc/helpers@0.5.17))(webpack@5.104.1) - css-minimizer-webpack-plugin: 8.0.0(esbuild@0.25.5)(webpack@5.104.1) + css-minimizer-webpack-plugin: 8.0.0(csso@5.0.5)(esbuild@0.25.5)(webpack@5.104.1) fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.2)(webpack@5.104.1) less: 4.4.1 less-loader: 12.3.3(@rspack/core@1.6.8(@swc/helpers@0.5.17))(less@4.4.1)(webpack@5.104.1) @@ -13034,12 +13822,12 @@ snapshots: sass-loader: 16.0.5(@rspack/core@1.6.8(@swc/helpers@0.5.17))(sass-embedded@1.93.2)(sass@1.93.2)(webpack@5.104.1) source-map-loader: 5.0.0(webpack@5.104.1) style-loader: 3.3.4(webpack@5.104.1) - terser-webpack-plugin: 5.4.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.104.1) + terser-webpack-plugin: 5.4.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack@5.104.1) ts-loader: 9.5.4(typescript@5.9.2)(webpack@5.104.1) tsconfig-paths-webpack-plugin: 4.2.0 tslib: 2.8.1 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) - webpack-dev-server: 5.2.6(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1) webpack-node-externals: 3.0.0 webpack-subresource-integrity: 5.1.0(webpack@5.104.1) transitivePeerDependencies: @@ -13065,13 +13853,13 @@ snapshots: - verdaccio - webpack-cli - '@nx/workspace@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))': + '@nx/workspace@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))': dependencies: - '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) + '@nx/devkit': 22.7.9(nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17))) '@zkochan/js-yaml': 0.0.7 chalk: 4.1.2 enquirer: 2.3.6 - nx: 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) + nx: 22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)) picomatch: 4.0.4 semver: 7.7.2 tslib: 2.8.1 @@ -13522,13 +14310,13 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.17 - '@rspack/dev-server@1.1.4(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@types/express@5.0.6)(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1)': + '@rspack/dev-server@1.1.4(@rspack/core@1.6.8(@swc/helpers@0.5.17))(@types/express@5.0.6)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1)': dependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) chokidar: 3.6.0 - http-proxy-middleware: 2.0.10(@types/express@5.0.6) + http-proxy-middleware: 2.0.10(@types/express@5.0.6)(debug@4.4.3(supports-color@8.1.1)) p-retry: 6.2.1 - webpack-dev-server: 5.2.6(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1) + webpack-dev-server: 5.2.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1) ws: 8.21.0 transitivePeerDependencies: - '@types/express' @@ -13551,9 +14339,9 @@ snapshots: '@rushstack/node-core-library@5.18.0(@types/node@20.19.9)': dependencies: - ajv: 8.18.0 - ajv-draft-04: 1.0.0(ajv@8.18.0) - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) fs-extra: 11.3.0 import-lazy: 4.0.0 jju: 1.4.0 @@ -13600,25 +14388,25 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/changelog@6.0.3(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/changelog@6.0.3(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))': dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 fs-extra: 11.3.0 lodash: 4.18.1 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) - '@semantic-release/commit-analyzer@13.0.1(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/commit-analyzer@13.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1)': dependencies: conventional-changelog-angular: 8.0.0 conventional-changelog-writer: 8.0.1 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.1.0 - debug: 4.4.0 - import-from-esm: 2.0.0 + debug: 4.4.0(supports-color@8.1.1) + import-from-esm: 2.0.0(supports-color@8.1.1) lodash-es: 4.18.1 micromatch: 4.0.8 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - supports-color @@ -13626,33 +14414,33 @@ snapshots: '@semantic-release/error@4.0.0': {} - '@semantic-release/exec@7.1.0(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/exec@7.1.0(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1)': dependencies: '@semantic-release/error': 4.0.0 aggregate-error: 3.1.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) execa: 9.5.2 lodash-es: 4.18.1 parse-json: 8.3.0 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - supports-color - '@semantic-release/git@10.0.1(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/git@10.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1)': dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) dir-glob: 3.0.1 execa: 5.1.1 lodash: 4.18.1 micromatch: 4.0.8 p-reduce: 2.1.0 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - supports-color - '@semantic-release/github@11.0.1(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/github@11.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1)': dependencies: '@octokit/core': 6.1.5 '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.5) @@ -13660,21 +14448,21 @@ snapshots: '@octokit/plugin-throttling': 9.6.1(@octokit/core@6.1.5) '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) dir-glob: 3.0.1 globby: 14.1.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) issue-parser: 7.0.1 lodash-es: 4.18.1 mime: 4.0.7 p-filter: 4.1.0 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) url-join: 5.0.0 transitivePeerDependencies: - supports-color - '@semantic-release/npm@12.0.1(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/npm@12.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))': dependencies: '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 @@ -13687,11 +14475,11 @@ snapshots: rc: 1.2.8 read-pkg: 9.0.1 registry-auth-token: 5.1.0 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) semver: 7.7.2 tempy: 3.1.0 - '@semantic-release/npm@13.1.5(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/npm@13.1.5(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))': dependencies: '@actions/core': 3.0.1 '@semantic-release/error': 4.0.0 @@ -13706,23 +14494,23 @@ snapshots: rc: 1.2.8 read-pkg: 10.1.0 registry-auth-token: 5.1.0 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) semver: 7.7.2 tempy: 3.1.0 - '@semantic-release/release-notes-generator@14.0.3(semantic-release@24.2.3(typescript@5.9.2))': + '@semantic-release/release-notes-generator@14.0.3(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1)': dependencies: conventional-changelog-angular: 8.0.0 conventional-changelog-writer: 8.0.1 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) get-stream: 7.0.1 - import-from-esm: 2.0.0 + import-from-esm: 2.0.0(supports-color@8.1.1) into-stream: 7.0.0 lodash-es: 4.18.1 read-package-up: 11.0.0 - semantic-release: 24.2.3(typescript@5.9.2) + semantic-release: 24.2.3(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - supports-color @@ -13734,21 +14522,21 @@ snapshots: '@sigstore/protobuf-specs@0.5.1': {} - '@sigstore/sign@4.1.1': + '@sigstore/sign@4.1.1(supports-color@8.1.1)': dependencies: '@gar/promise-retry': 1.0.3 '@sigstore/bundle': 4.0.0 '@sigstore/core': 3.2.1 '@sigstore/protobuf-specs': 0.5.1 - make-fetch-happen: 15.0.5 + make-fetch-happen: 15.0.5(supports-color@8.1.1) proc-log: 6.1.0 transitivePeerDependencies: - supports-color - '@sigstore/tuf@4.0.2': + '@sigstore/tuf@4.0.2(supports-color@8.1.1)': dependencies: '@sigstore/protobuf-specs': 0.5.1 - tuf-js: 4.1.0 + tuf-js: 4.1.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13779,14 +14567,14 @@ snapshots: '@swc/core': 1.16.2(@swc/helpers@0.5.17) '@swc/types': 0.1.28 - '@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2)': + '@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@node-rs/xxhash': 1.7.7 '@swc-node/core': 1.15.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.16.2(@swc/helpers@0.5.17) colorette: 2.0.20 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) fast-json-stable-stringify: 2.1.0 oxc-resolver: 11.24.2 pirates: 4.0.7 @@ -13877,16 +14665,17 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@tokenizer/inflate@0.4.1': + '@tokenizer/inflate@0.2.7(supports-color@8.1.1)': dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) + fflate: 0.8.2 token-types: 6.1.1 transitivePeerDependencies: - supports-color '@tokenizer/token@0.3.0': {} - '@tootallnate/once@3.0.1': {} + '@tootallnate/once@2.0.1': {} '@ts-morph/common@0.22.0': dependencies: @@ -14047,6 +14836,10 @@ snapshots: '@types/mime@1.3.5': {} + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 20.19.9 + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 @@ -14104,15 +14897,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/scope-manager': 8.44.1 - '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.44.1 - eslint: 9.36.0(jiti@2.4.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) graphemer: 1.4.0 ignore: 7.0.3 natural-compare: 1.4.0 @@ -14121,23 +14914,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 8.44.1 '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.44.1(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 8.44.1 - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.36.0(jiti@2.4.2) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.44.1(typescript@5.9.2)': + '@typescript-eslint/project-service@8.44.1(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2) '@typescript-eslint/types': 8.44.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -14151,13 +14944,13 @@ snapshots: dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.36.0(jiti@2.4.2) + '@typescript-eslint/typescript-estree': 8.44.1(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: @@ -14165,13 +14958,13 @@ snapshots: '@typescript-eslint/types@8.44.1': {} - '@typescript-eslint/typescript-estree@8.44.1(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.44.1(supports-color@8.1.1)(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.44.1(typescript@5.9.2) + '@typescript-eslint/project-service': 8.44.1(supports-color@8.1.1)(typescript@5.9.2) '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2) '@typescript-eslint/types': 8.44.1 '@typescript-eslint/visitor-keys': 8.44.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.7 @@ -14181,13 +14974,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)) '@typescript-eslint/scope-manager': 8.44.1 '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - eslint: 9.36.0(jiti@2.4.2) + '@typescript-eslint/typescript-estree': 8.44.1(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -14258,26 +15051,26 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3))': dependencies: - vite: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vite: 7.3.2(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.6)': + '@vitest/coverage-v8@3.2.4(supports-color@8.1.1)(vitest@3.2.6)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 ast-v8-to-istanbul: 0.3.5 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 + istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) istanbul-reports: 3.2.0 magic-string: 0.30.19 magicast: 0.3.5 std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -14330,7 +15123,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vitest: 3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) '@vitest/utils@3.2.4': dependencies: @@ -14350,11 +15143,13 @@ snapshots: '@volar/source-map@2.4.23': {} - '@volar/typescript@2.4.23': + '@volar/typescript@2.4.23(typescript@5.9.2)': dependencies: '@volar/language-core': 2.4.23 path-browserify: 1.0.1 vscode-uri: 3.1.0 + optionalDependencies: + typescript: 5.9.2 '@vue/compiler-core@3.5.22': dependencies: @@ -14465,20 +15260,22 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1)': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.104.1)': dependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) - webpack-cli: 5.1.4(webpack@5.104.1) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1)': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.104.1)': dependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) - webpack-cli: 5.1.4(webpack@5.104.1) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1)': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.6)(webpack@5.104.1)': dependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) - webpack-cli: 5.1.4(webpack@5.104.1) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) + optionalDependencies: + webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1) '@xtuc/ieee754@1.2.0': {} @@ -14533,6 +15330,12 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@6.0.2(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + agent-base@7.1.3: {} aggregate-error@3.1.0: @@ -14545,14 +15348,22 @@ snapshots: clean-stack: 5.2.0 indent-string: 5.0.0 - ajv-draft-04@1.0.0(ajv@8.18.0): + ajv-draft-04@1.0.0(ajv@8.13.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.13.0 ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -14573,6 +15384,27 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -14599,20 +15431,20 @@ snapshots: alien-signals@0.4.14: {} - angular-eslint@20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2))(typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(typescript@5.9.2): + angular-eslint@20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(typescript@5.9.2): dependencies: - '@angular-devkit/core': 20.3.3(chokidar@4.0.3) + '@angular-devkit/core': 20.3.24(chokidar@4.0.3) '@angular-devkit/schematics': 20.3.24(chokidar@4.0.3) - '@angular-eslint/builder': 20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@angular-eslint/eslint-plugin': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@angular-eslint/eslint-plugin-template': 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@angular-eslint/schematics': 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@angular-eslint/template-parser': 20.3.0(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + '@angular-eslint/builder': 20.3.0(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@angular-eslint/eslint-plugin': 20.3.0(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@angular-eslint/eslint-plugin-template': 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@angular-eslint/schematics': 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2))(@typescript-eslint/types@8.44.1)(@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) + '@angular-eslint/template-parser': 20.3.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(typescript@5.9.2) '@typescript-eslint/types': 8.44.1 - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - eslint: 9.36.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 - typescript-eslint: 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) + typescript-eslint: 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) transitivePeerDependencies: - chokidar - supports-color @@ -14706,9 +15538,9 @@ snapshots: postcss: 8.5.23 postcss-value-parser: 4.2.0 - axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + axios@1.18.1(debug@4.4.3(supports-color@8.1.1))(supports-color@7.2.0): dependencies: - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) form-data: 4.0.6 https-proxy-agent: 5.0.1(supports-color@7.2.0) proxy-from-env: 2.1.0 @@ -14718,41 +15550,41 @@ snapshots: axobject-query@4.1.0: {} - babel-jest@30.1.2(@babel/core@7.29.6): + babel-jest@30.1.2(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.6 - '@jest/transform': 30.1.2 + '@babel/core': 7.29.6(supports-color@8.1.1) + '@jest/transform': 30.1.2(supports-color@8.1.1) '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 7.0.1 - babel-preset-jest: 30.0.1(@babel/core@7.29.6) + babel-plugin-istanbul: 7.0.1(supports-color@8.1.1) + babel-preset-jest: 30.0.1(@babel/core@7.29.6(supports-color@8.1.1)) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-loader@9.2.1(@babel/core@7.29.6)(webpack@5.104.1): + babel-loader@9.2.1(@babel/core@7.29.6(supports-color@8.1.1))(webpack@5.104.1): dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) - babel-plugin-const-enum@1.2.0(@babel/core@7.29.6): + babel-plugin-const-enum@1.2.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.6) - '@babel/traverse': 7.29.0 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - babel-plugin-istanbul@7.0.1: + babel-plugin-istanbul@7.0.1(supports-color@8.1.1): dependencies: '@babel/helper-plugin-utils': 7.27.1 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 6.0.3 + istanbul-lib-instrument: 6.0.3(supports-color@8.1.1) test-exclude: 6.0.0 transitivePeerDependencies: - supports-color @@ -14769,61 +15601,61 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.10 - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.29.6): + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.6 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.6): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.6 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.45.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.29.6): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.6 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.29.6) + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - babel-plugin-transform-typescript-metadata@0.3.2(@babel/core@7.29.6)(@babel/traverse@7.29.0): + babel-plugin-transform-typescript-metadata@0.3.2(@babel/core@7.29.6(supports-color@8.1.1))(@babel/traverse@7.29.0(supports-color@8.1.1)): dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 optionalDependencies: - '@babel/traverse': 7.29.0 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.6): - dependencies: - '@babel/core': 7.29.6 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.6) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.6) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.6) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.6) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.6) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.6) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.6) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.6) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.6) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.6) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.6) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.6) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.6) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.6) - - babel-preset-jest@30.0.1(@babel/core@7.29.6): - dependencies: - '@babel/core': 7.29.6 + '@babel/traverse': 7.29.0(supports-color@8.1.1) + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.6(supports-color@8.1.1)): + dependencies: + '@babel/core': 7.29.6(supports-color@8.1.1) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.6(supports-color@8.1.1)) + + babel-preset-jest@30.0.1(@babel/core@7.29.6(supports-color@8.1.1)): + dependencies: + '@babel/core': 7.29.6(supports-color@8.1.1) babel-plugin-jest-hoist: 30.0.1 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.6) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.6(supports-color@8.1.1)) balanced-match@1.0.2: {} @@ -14866,11 +15698,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.6: + body-parser@1.20.6(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -14883,11 +15715,11 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.3.0: + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -15192,7 +16024,7 @@ snapshots: glob: 7.2.3 inquirer: 8.2.5 is-utf8: 0.2.1 - lodash: 4.18.1 + lodash: 4.17.21 minimist: 1.2.7 strip-bom: 4.0.0 strip-json-comments: 3.1.1 @@ -15213,11 +16045,11 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.8.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -15318,7 +16150,7 @@ snapshots: schema-utils: 4.3.3 serialize-javascript: 7.0.5 tinyglobby: 0.2.15 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) core-js-compat@3.45.1: dependencies: @@ -15394,9 +16226,9 @@ snapshots: semver: 7.7.2 optionalDependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) - css-minimizer-webpack-plugin@8.0.0(esbuild@0.25.5)(webpack@5.104.1): + css-minimizer-webpack-plugin@8.0.0(csso@5.0.5)(esbuild@0.25.5)(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 7.1.9(postcss@8.5.23) @@ -15404,8 +16236,9 @@ snapshots: postcss: 8.5.23 schema-utils: 4.3.3 serialize-javascript: 7.0.5 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) optionalDependencies: + csso: 5.0.5 esbuild: 0.25.5 css-select@5.2.2: @@ -15516,17 +16349,23 @@ snapshots: de-indent@1.0.2: {} - debug@2.6.9: + debug@2.6.9(supports-color@8.1.1): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 8.1.1 - debug@4.3.1: + debug@4.3.1(supports-color@8.1.1): dependencies: ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 - debug@4.4.0: + debug@4.4.0(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 debug@4.4.3(supports-color@7.2.0): dependencies: @@ -15534,6 +16373,12 @@ snapshots: optionalDependencies: supports-color: 7.2.0 + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + decimal.js@10.6.0: {} dedent@0.7.0: {} @@ -15772,6 +16617,64 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 + 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 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -15813,9 +16716,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.4.2)): + eslint-config-prettier@10.1.8(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)): dependencies: - eslint: 9.36.0(jiti@2.4.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) eslint-scope@5.1.1: dependencies: @@ -15831,14 +16734,14 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.36.0(jiti@2.4.2): + eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 + '@eslint/config-array': 0.21.0(supports-color@8.1.1) '@eslint/config-helpers': 0.3.1 '@eslint/core': 0.15.2 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.1(supports-color@8.1.1) '@eslint/js': 9.36.0 '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.7 @@ -15849,7 +16752,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -15983,26 +16886,26 @@ snapshots: exponential-backoff@3.1.2: {} - express-rate-limit@8.3.1(express@5.2.1): + express-rate-limit@8.3.1(express@5.2.1(supports-color@8.1.1)): dependencies: - express: 5.2.1 - ip-address: 10.7.0 + express: 5.2.1(supports-color@8.1.1) + ip-address: 10.1.0 - express@4.22.2: + express@4.22.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.6 + body-parser: 1.20.6(supports-color@8.1.1) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.1 cookie-signature: 1.0.6 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 + finalhandler: 1.3.1(supports-color@8.1.1) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -16014,8 +16917,8 @@ snapshots: qs: 6.15.2 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 + send: 0.19.0(supports-color@8.1.1) + serve-static: 1.16.2(supports-color@8.1.1) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -16024,19 +16927,19 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.1.0: + express@5.1.0(supports-color@8.1.1): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@8.1.1) content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.1 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.0(supports-color@8.1.1) fresh: 2.0.0 http-errors: 2.0.0 merge-descriptors: 2.0.0 @@ -16047,29 +16950,29 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.2 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 + router: 2.2.0(supports-color@8.1.1) + send: 1.2.0(supports-color@8.1.1) + serve-static: 2.2.0(supports-color@8.1.1) statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@8.1.1): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@8.1.1) content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.1 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.0(supports-color@8.1.1) fresh: 2.0.0 http-errors: 2.0.0 merge-descriptors: 2.0.0 @@ -16080,9 +16983,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.2 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 + router: 2.2.0(supports-color@8.1.1) + send: 1.2.0(supports-color@8.1.1) + serve-static: 2.2.0(supports-color@8.1.1) statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -16095,7 +16998,7 @@ snapshots: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 - tmp: 0.2.7 + tmp: 0.0.33 fast-content-type-parse@2.0.1: {} @@ -16153,9 +17056,9 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-type@21.3.2: + file-type@21.0.0(supports-color@8.1.1): dependencies: - '@tokenizer/inflate': 0.4.1 + '@tokenizer/inflate': 0.2.7(supports-color@8.1.1) strtok3: 10.3.4 token-types: 6.1.1 uint8array-extras: 1.5.0 @@ -16166,9 +17069,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: + finalhandler@1.3.1(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -16178,9 +17081,9 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.0: + finalhandler@2.1.0(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -16249,13 +17152,9 @@ snapshots: flatted@3.4.2: {} - follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): - optionalDependencies: - debug: 4.4.3(supports-color@7.2.0) - - follow-redirects@1.16.0(debug@4.4.3): + follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): optionalDependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) foreground-child@3.3.1: dependencies: @@ -16277,7 +17176,7 @@ snapshots: semver: 7.7.2 tapable: 2.3.0 typescript: 5.9.2 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) form-data@4.0.6: dependencies: @@ -16575,25 +17474,25 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@5.0.0: + http-proxy-agent@5.0.0(supports-color@8.1.1): dependencies: - '@tootallnate/once': 3.0.1 - agent-base: 6.0.2(supports-color@7.2.0) - debug: 4.4.3(supports-color@7.2.0) + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@8.1.1): dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - http-proxy-middleware@2.0.10(@types/express@4.17.25): + http-proxy-middleware@2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)): dependencies: '@types/http-proxy': 1.17.16 - http-proxy: 1.18.1(debug@4.4.3) + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -16602,10 +17501,10 @@ snapshots: transitivePeerDependencies: - debug - http-proxy-middleware@2.0.10(@types/express@5.0.6): + http-proxy-middleware@2.0.10(@types/express@5.0.6)(debug@4.4.3(supports-color@8.1.1)): dependencies: '@types/http-proxy': 1.17.16 - http-proxy: 1.18.1(debug@4.4.3) + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -16614,37 +17513,37 @@ snapshots: transitivePeerDependencies: - debug - http-proxy-middleware@3.0.7: + http-proxy-middleware@3.0.7(supports-color@8.1.1): dependencies: '@types/http-proxy': 1.17.16 - debug: 4.4.3(supports-color@7.2.0) - http-proxy: 1.18.1(debug@4.4.3) + debug: 4.4.3(supports-color@8.1.1) + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) is-glob: 4.0.3 is-plain-object: 5.0.0 micromatch: 4.0.8 transitivePeerDependencies: - supports-color - http-proxy@1.18.1(debug@4.4.3): + http-proxy@1.18.1(debug@4.4.3(supports-color@8.1.1)): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.16.0(debug@4.4.3) + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) requires-port: 1.0.0 transitivePeerDependencies: - debug - http-server@14.1.1: + http-server@14.1.1(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1): dependencies: basic-auth: 2.0.1 chalk: 4.1.2 corser: 2.0.1 he: 1.2.0 html-encoding-sniffer: 3.0.0 - http-proxy: 1.18.1(debug@4.4.3) + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) mime: 1.6.0 minimist: 1.2.8 opener: 1.5.2 - portfinder: 1.0.38 + portfinder: 1.0.38(supports-color@8.1.1) secure-compare: 3.0.1 union: 0.5.0 url-join: 4.0.1 @@ -16659,10 +17558,17 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@5.0.1(supports-color@8.1.1): + dependencies: + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6(supports-color@8.1.1): dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16722,9 +17628,9 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-from-esm@2.0.0: + import-from-esm@2.0.0(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) import-meta-resolve: 4.1.0 transitivePeerDependencies: - supports-color @@ -16788,6 +17694,8 @@ snapshots: from2: 2.3.0 p-is-promise: 3.0.0 + ip-address@10.1.0: {} + ip-address@10.7.0: {} ip-regex@4.3.0: {} @@ -16914,9 +17822,9 @@ snapshots: istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@6.0.3: + istanbul-lib-instrument@6.0.3(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/parser': 7.28.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -16930,10 +17838,10 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: + istanbul-lib-source-maps@5.0.6(supports-color@8.1.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -16959,10 +17867,10 @@ snapshots: jest-util: 30.0.5 p-limit: 3.1.0 - jest-circus@30.1.3(babel-plugin-macros@3.1.0): + jest-circus@30.1.3(babel-plugin-macros@3.1.0)(supports-color@8.1.1): dependencies: '@jest/environment': 30.1.2 - '@jest/expect': 30.1.2 + '@jest/expect': 30.1.2(supports-color@8.1.1) '@jest/test-result': 30.1.3 '@jest/types': 30.0.5 '@types/node': 20.19.9 @@ -16973,8 +17881,8 @@ snapshots: jest-each: 30.1.0 jest-matcher-utils: 30.1.2 jest-message-util: 30.1.0 - jest-runtime: 30.1.3 - jest-snapshot: 30.1.2 + jest-runtime: 30.1.3(supports-color@8.1.1) + jest-snapshot: 30.1.2(supports-color@8.1.1) jest-util: 30.0.5 p-limit: 3.1.0 pretty-format: 30.0.5 @@ -16985,15 +17893,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)): + jest-cli@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)): dependencies: - '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) '@jest/test-result': 30.1.3 '@jest/types': 30.0.5 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + jest-config: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) jest-util: 30.0.5 jest-validate: 30.1.0 yargs: 17.7.2 @@ -17004,25 +17912,25 @@ snapshots: - supports-color - ts-node - jest-config@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)): + jest-config@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)): dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@jest/get-type': 30.1.0 '@jest/pattern': 30.0.1 '@jest/test-sequencer': 30.1.3 '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.29.6) + babel-jest: 30.1.2(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) chalk: 4.1.2 ci-info: 4.3.0 deepmerge: 4.3.1 glob: 10.5.0 graceful-fs: 4.2.11 - jest-circus: 30.1.3(babel-plugin-macros@3.1.0) + jest-circus: 30.1.3(babel-plugin-macros@3.1.0)(supports-color@8.1.1) jest-docblock: 30.0.1 jest-environment-node: 30.1.2 jest-regex-util: 30.0.1 jest-resolve: 30.1.3 - jest-runner: 30.1.3 + jest-runner: 30.1.3(supports-color@8.1.1) jest-util: 30.0.5 jest-validate: 30.1.0 micromatch: 4.0.8 @@ -17117,10 +18025,10 @@ snapshots: jest-regex-util@30.0.1: {} - jest-resolve-dependencies@30.1.3: + jest-resolve-dependencies@30.1.3(supports-color@8.1.1): dependencies: jest-regex-util: 30.0.1 - jest-snapshot: 30.1.2 + jest-snapshot: 30.1.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -17135,12 +18043,12 @@ snapshots: slash: 3.0.0 unrs-resolver: 1.11.1 - jest-runner@30.1.3: + jest-runner@30.1.3(supports-color@8.1.1): dependencies: '@jest/console': 30.1.2 '@jest/environment': 30.1.2 '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 + '@jest/transform': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 '@types/node': 20.19.9 chalk: 4.1.2 @@ -17153,7 +18061,7 @@ snapshots: jest-leak-detector: 30.1.0 jest-message-util: 30.1.0 jest-resolve: 30.1.3 - jest-runtime: 30.1.3 + jest-runtime: 30.1.3(supports-color@8.1.1) jest-util: 30.0.5 jest-watcher: 30.1.3 jest-worker: 30.1.0 @@ -17162,14 +18070,14 @@ snapshots: transitivePeerDependencies: - supports-color - jest-runtime@30.1.3: + jest-runtime@30.1.3(supports-color@8.1.1): dependencies: '@jest/environment': 30.1.2 '@jest/fake-timers': 30.1.2 - '@jest/globals': 30.1.2 + '@jest/globals': 30.1.2(supports-color@8.1.1) '@jest/source-map': 30.0.1 '@jest/test-result': 30.1.3 - '@jest/transform': 30.1.2 + '@jest/transform': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 '@types/node': 20.19.9 chalk: 4.1.2 @@ -17182,26 +18090,26 @@ snapshots: jest-mock: 30.0.5 jest-regex-util: 30.0.1 jest-resolve: 30.1.3 - jest-snapshot: 30.1.2 + jest-snapshot: 30.1.2(supports-color@8.1.1) jest-util: 30.0.5 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.1.2: + jest-snapshot@30.1.2(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.6 + '@babel/core': 7.29.6(supports-color@8.1.1) '@babel/generator': 7.28.3 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.6) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.6) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.6(supports-color@8.1.1)) '@babel/types': 7.28.4 '@jest/expect-utils': 30.1.2 '@jest/get-type': 30.1.0 '@jest/snapshot-utils': 30.1.2 - '@jest/transform': 30.1.2 + '@jest/transform': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.6) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.6(supports-color@8.1.1)) chalk: 4.1.2 expect: 30.1.2 graceful-fs: 4.2.11 @@ -17258,12 +18166,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)): + jest@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)): dependencies: - '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) '@jest/types': 30.0.5 import-local: 3.2.0 - jest-cli: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + jest-cli: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -17290,11 +18198,15 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + js-yaml@4.3.1: dependencies: argparse: 2.0.1 - jsdom@22.1.0: + jsdom@22.1.0(supports-color@8.1.1): dependencies: abab: 2.0.6 cssstyle: 3.0.0 @@ -17303,8 +18215,8 @@ snapshots: domexception: 4.0.0 form-data: 4.0.6 html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1(supports-color@7.2.0) + http-proxy-agent: 5.0.0(supports-color@8.1.1) + https-proxy-agent: 5.0.1(supports-color@8.1.1) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.22 parse5: 7.3.0 @@ -17388,7 +18300,7 @@ snapshots: less: 4.4.1 optionalDependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) less@4.4.1: dependencies: @@ -17415,7 +18327,7 @@ snapshots: dependencies: webpack-sources: 3.3.3 optionalDependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) lilconfig@3.1.3: {} @@ -17523,6 +18435,10 @@ snapshots: lodash.upperfirst@4.3.1: {} + lodash@4.17.21: {} + + lodash@4.17.23: {} + lodash@4.18.1: {} log-symbols@4.1.0: @@ -17587,10 +18503,10 @@ snapshots: make-error@1.3.6: {} - make-fetch-happen@15.0.5: + make-fetch-happen@15.0.5(supports-color@8.1.1): dependencies: '@gar/promise-retry': 1.0.3 - '@npmcli/agent': 4.0.0 + '@npmcli/agent': 4.0.0(supports-color@8.1.1) '@npmcli/redact': 4.0.0 cacache: 20.0.4 http-cache-semantics: 4.2.0 @@ -17696,10 +18612,14 @@ snapshots: mini-css-extract-plugin@2.4.7(webpack@5.104.1): dependencies: schema-utils: 4.3.3 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) minimalistic-assert@1.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.1 + minimatch@10.2.3: dependencies: brace-expansion: 5.0.7 @@ -17756,6 +18676,10 @@ snapshots: dependencies: minipass: 7.1.2 + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mkdirp@3.0.1: {} mlly@1.8.0: @@ -17794,12 +18718,15 @@ snapshots: muggle-string@0.4.1: {} - multer@2.2.0: + multer@2.0.2: dependencies: append-field: 1.0.0 busboy: 1.6.0 concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 type-is: 1.6.18 + xtend: 4.0.2 multicast-dns@7.2.5: dependencies: @@ -17859,17 +18786,19 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-forge@1.4.0: {} + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.1 optional: true - node-gyp@12.2.0: + node-gyp@12.2.0(supports-color@8.1.1): dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.2 graceful-fs: 4.2.11 - make-fetch-happen: 15.0.5 + make-fetch-happen: 15.0.5(supports-color@8.1.1) nopt: 9.0.0 proc-log: 6.1.0 semver: 7.7.2 @@ -17938,11 +18867,11 @@ snapshots: npm-package-arg: 13.0.0 semver: 7.7.2 - npm-registry-fetch@19.1.1: + npm-registry-fetch@19.1.1(supports-color@8.1.1): dependencies: '@npmcli/redact': 4.0.0 jsonparse: 1.3.1 - make-fetch-happen: 15.0.5 + make-fetch-happen: 15.0.5(supports-color@8.1.1) minipass: 7.1.2 minipass-fetch: 5.0.2 minizlib: 3.1.0 @@ -17974,7 +18903,7 @@ snapshots: nwsapi@2.2.22: {} - nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)): + nx@22.7.9(@swc-node/register@1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2))(@swc/core@1.16.2(@swc/helpers@0.5.17)): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -17990,7 +18919,7 @@ snapshots: ansi-styles: 4.3.0 argparse: 2.0.1 asynckit: 0.4.0 - axios: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) + axios: 1.18.1(debug@4.4.3(supports-color@8.1.1))(supports-color@7.2.0) balanced-match: 4.0.3 base64-js: 1.5.1 bl: 4.1.0 @@ -18024,7 +18953,7 @@ snapshots: escape-string-regexp: 1.0.5 figures: 3.2.0 flat: 5.0.2 - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) form-data: 4.0.6 fs-constants: 1.0.0 function-bind: 1.1.2 @@ -18101,7 +19030,7 @@ snapshots: '@nx/nx-linux-x64-musl': 22.7.9 '@nx/nx-win32-arm64-msvc': 22.7.9 '@nx/nx-win32-x64-msvc': 22.7.9 - '@swc-node/register': 1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(typescript@5.9.2) + '@swc-node/register': 1.12.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@swc/types@0.1.28)(supports-color@8.1.1)(typescript@5.9.2) '@swc/core': 1.16.2(@swc/helpers@0.5.17) object-assign@4.1.1: {} @@ -18194,6 +19123,8 @@ snapshots: ordered-binary@1.6.0: optional: true + os-tmpdir@1.0.2: {} + oxc-resolver@11.24.2: optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.24.2 @@ -18274,23 +19205,23 @@ snapshots: package-json-from-dist@1.0.1: {} - pacote@21.0.4: + pacote@21.0.4(supports-color@8.1.1): dependencies: '@npmcli/git': 7.0.2 '@npmcli/installed-package-contents': 4.0.0 '@npmcli/package-json': 7.0.5 '@npmcli/promise-spawn': 9.0.1 - '@npmcli/run-script': 10.0.4 + '@npmcli/run-script': 10.0.4(supports-color@8.1.1) cacache: 20.0.4 fs-minipass: 3.0.3 minipass: 7.1.2 npm-package-arg: 13.0.0 npm-packlist: 10.0.2 npm-pick-manifest: 11.0.3 - npm-registry-fetch: 19.1.1 + npm-registry-fetch: 19.1.1(supports-color@8.1.1) proc-log: 6.1.0 promise-retry: 2.0.1 - sigstore: 4.1.1 + sigstore: 4.1.1(supports-color@8.1.1) ssri: 13.0.1 tar: 7.5.22 transitivePeerDependencies: @@ -18382,6 +19313,8 @@ snapshots: path-to-regexp@0.1.13: {} + path-to-regexp@8.2.0: {} + path-to-regexp@8.4.0: {} path-to-regexp@8.4.2: {} @@ -18398,6 +19331,10 @@ snapshots: picomatch@2.3.2: {} + picomatch@4.0.2: {} + + picomatch@4.0.3: {} + picomatch@4.0.4: {} pify@2.3.0: {} @@ -18409,7 +19346,7 @@ snapshots: pirates@4.0.7: {} - piscina@5.2.0: + piscina@5.1.3: optionalDependencies: '@napi-rs/nice': 1.1.1 @@ -18451,10 +19388,10 @@ snapshots: pluralize@8.0.0: {} - portfinder@1.0.38: + portfinder@1.0.38(supports-color@8.1.1): dependencies: async: 3.2.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18510,7 +19447,7 @@ snapshots: semver: 7.7.2 optionalDependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) transitivePeerDependencies: - typescript @@ -18522,7 +19459,7 @@ snapshots: semver: 7.7.2 optionalDependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) transitivePeerDependencies: - typescript @@ -18757,13 +19694,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@3.0.1: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.7.0 - unpipe: 1.0.0 - raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -18943,9 +19873,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 - router@2.2.0: + router@2.2.0(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -19074,7 +20004,7 @@ snapshots: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) sass: 1.93.2 sass-embedded: 1.93.2 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) sass@1.90.0: dependencies: @@ -19124,21 +20054,26 @@ snapshots: select-hose@2.0.0: {} + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.14 + node-forge: 1.4.0 + selfsigned@5.5.0: dependencies: '@peculiar/x509': 1.14.3 pkijs: 3.4.0 - semantic-release@24.2.3(typescript@5.9.2): + semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2): dependencies: - '@semantic-release/commit-analyzer': 13.0.1(semantic-release@24.2.3(typescript@5.9.2)) + '@semantic-release/commit-analyzer': 13.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 11.0.1(semantic-release@24.2.3(typescript@5.9.2)) - '@semantic-release/npm': 12.0.1(semantic-release@24.2.3(typescript@5.9.2)) - '@semantic-release/release-notes-generator': 14.0.3(semantic-release@24.2.3(typescript@5.9.2)) + '@semantic-release/github': 11.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) + '@semantic-release/npm': 12.0.1(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2)) + '@semantic-release/release-notes-generator': 14.0.3(semantic-release@24.2.3(supports-color@8.1.1)(typescript@5.9.2))(supports-color@8.1.1) aggregate-error: 5.0.0 cosmiconfig: 9.0.0(typescript@5.9.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) env-ci: 11.1.0 execa: 9.5.2 figures: 6.1.0 @@ -19147,7 +20082,7 @@ snapshots: git-log-parser: 1.2.1 hook-std: 3.0.0 hosted-git-info: 8.0.2 - import-from-esm: 2.0.0 + import-from-esm: 2.0.0(supports-color@8.1.1) lodash-es: 4.18.1 marked: 12.0.2 marked-terminal: 7.3.0(marked@12.0.2) @@ -19185,9 +20120,9 @@ snapshots: semver@7.7.4: {} - send@0.19.0: + send@0.19.0(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 encodeurl: 1.0.2 @@ -19203,9 +20138,9 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.0: + send@1.2.0(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -19221,11 +20156,11 @@ snapshots: serialize-javascript@7.0.5: {} - serve-index@1.9.1: + serve-index@1.9.1(supports-color@8.1.1): dependencies: accepts: 1.3.8 batch: 0.6.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) escape-html: 1.0.3 http-errors: 1.6.3 mime-types: 2.1.35 @@ -19233,21 +20168,21 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.2: + serve-static@1.16.2(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 0.19.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - serve-static@2.2.0: + serve-static@2.2.0(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19309,13 +20244,13 @@ snapshots: figures: 2.0.0 pkg-conf: 2.1.0 - sigstore@4.1.1: + sigstore@4.1.1(supports-color@8.1.1): dependencies: '@sigstore/bundle': 4.0.0 '@sigstore/core': 3.2.1 '@sigstore/protobuf-specs': 0.5.1 - '@sigstore/sign': 4.1.1 - '@sigstore/tuf': 4.0.2 + '@sigstore/sign': 4.1.1(supports-color@8.1.1) + '@sigstore/tuf': 4.0.2(supports-color@8.1.1) '@sigstore/verify': 3.1.1 transitivePeerDependencies: - supports-color @@ -19353,13 +20288,13 @@ snapshots: sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 - uuid: 11.1.1 + uuid: 8.3.2 websocket-driver: 0.7.5 - socks-proxy-agent@8.0.5: + socks-proxy-agent@8.0.5(supports-color@8.1.1): dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -19375,7 +20310,7 @@ snapshots: dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) source-map-support@0.5.13: dependencies: @@ -19419,9 +20354,9 @@ snapshots: spdx-license-ids@3.0.21: {} - spdy-transport@3.0.0: + spdy-transport@3.0.0(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -19430,13 +20365,13 @@ snapshots: transitivePeerDependencies: - supports-color - spdy@4.0.2: + spdy@4.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 - spdy-transport: 3.0.0 + spdy-transport: 3.0.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19542,7 +20477,7 @@ snapshots: style-loader@3.3.4(webpack@5.104.1): dependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) stylehacks@7.0.11(postcss@8.5.23): dependencies: @@ -19620,9 +20555,9 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tcp-port-used@1.0.2: + tcp-port-used@1.0.2(supports-color@8.1.1): dependencies: - debug: 4.3.1 + debug: 4.3.1(supports-color@8.1.1) is2: 2.0.9 transitivePeerDependencies: - supports-color @@ -19636,16 +20571,17 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 - terser-webpack-plugin@5.4.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.104.1): + terser-webpack-plugin@5.4.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.44.0 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.16.2(@swc/helpers@0.5.17) esbuild: 0.25.5 + uglify-js: 3.19.3 terser@5.44.0: dependencies: @@ -19713,6 +20649,10 @@ snapshots: tinyspy@4.0.4: {} + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + tmp@0.2.7: {} tmpl@1.0.5: {} @@ -19769,12 +20709,12 @@ snapshots: optionalDependencies: '@rspack/core': 1.6.8(@swc/helpers@0.5.17) - ts-jest@29.4.4(@babel/core@7.29.6)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.6))(esbuild@0.25.5)(jest-util@30.0.5)(jest@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)))(typescript@5.9.2): + ts-jest@29.4.4(@babel/core@7.29.6(supports-color@8.1.1))(@jest/transform@30.1.2(supports-color@8.1.1))(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1))(esbuild@0.25.5)(jest-util@30.0.5)(jest@30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)))(typescript@5.9.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.9 - jest: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) + jest: 30.1.3(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(ts-node@10.9.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(@types/node@20.19.9)(typescript@5.9.2)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19783,10 +20723,10 @@ snapshots: typescript: 5.9.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.29.6 - '@jest/transform': 30.1.2 + '@babel/core': 7.29.6(supports-color@8.1.1) + '@jest/transform': 30.1.2(supports-color@8.1.1) '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.29.6) + babel-jest: 30.1.2(@babel/core@7.29.6(supports-color@8.1.1))(supports-color@8.1.1) esbuild: 0.25.5 jest-util: 30.0.5 @@ -19798,7 +20738,7 @@ snapshots: semver: 7.7.2 source-map: 0.7.6 typescript: 5.9.2 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) ts-morph@21.0.1: dependencies: @@ -19846,11 +20786,11 @@ snapshots: dependencies: tslib: 1.14.1 - tuf-js@4.1.0: + tuf-js@4.1.0(supports-color@8.1.1): dependencies: '@tufjs/models': 4.1.0 - debug: 4.4.3(supports-color@7.2.0) - make-fetch-happen: 15.0.5 + debug: 4.4.3(supports-color@8.1.1) + make-fetch-happen: 15.0.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19897,13 +20837,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2): + typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2))(typescript@5.9.2) - eslint: 9.36.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2))(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.44.1(supports-color@8.1.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.4.2)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) + eslint: 9.36.0(jiti@2.4.2)(supports-color@8.1.1) typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -20021,7 +20961,7 @@ snapshots: utils-merge@1.0.1: {} - uuid@11.1.1: {} + uuid@8.3.2: {} v8-compile-cache-lib@3.0.1: {} @@ -20052,10 +20992,10 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3): + vite-node@3.2.4(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) @@ -20073,14 +21013,14 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.59.0)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3)): + vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.59.0)(supports-color@8.1.1)(typescript@5.9.2)(vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3)): dependencies: '@microsoft/api-extractor': 7.53.3(@types/node@20.19.9) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@volar/typescript': 2.4.23 + '@volar/typescript': 2.4.23(typescript@5.9.2) '@vue/language-core': 2.2.0(typescript@5.9.2) compare-versions: 6.1.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 1.1.2 magic-string: 0.30.19 @@ -20092,9 +21032,9 @@ snapshots: - rollup - supports-color - vite@7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3): + vite@7.3.2(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.90.0)(terser@5.44.0)(yaml@2.8.3): dependencies: - esbuild: 0.28.1 + esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.23 @@ -20128,7 +21068,7 @@ snapshots: terser: 5.44.0 yaml: 2.8.3 - vitest@3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3): + vitest@3.2.6(@types/node@20.19.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0(supports-color@8.1.1))(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.6 @@ -20139,7 +21079,7 @@ snapshots: '@vitest/spy': 3.2.6 '@vitest/utils': 3.2.6 chai: 5.3.3 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@8.1.1) expect-type: 1.2.2 magic-string: 0.30.19 pathe: 2.0.3 @@ -20151,12 +21091,12 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 7.3.6(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@20.19.9)(jiti@2.4.2)(less@4.4.1)(sass-embedded@1.93.2)(sass@1.93.2)(supports-color@8.1.1)(terser@5.44.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.9 '@vitest/ui': 3.2.4(vitest@3.2.6) - jsdom: 22.1.0 + jsdom: 22.1.0(supports-color@8.1.1) transitivePeerDependencies: - jiti - less @@ -20201,12 +21141,12 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-cli@5.1.4(webpack@5.104.1): + webpack-cli@5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.104.1) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.104.1) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack-dev-server@5.2.6)(webpack@5.104.1) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -20215,8 +21155,10 @@ snapshots: import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) webpack-merge: 5.10.0 + optionalDependencies: + webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1) webpack-dev-middleware@7.4.5(webpack@5.104.1): dependencies: @@ -20227,9 +21169,48 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + + webpack-dev-server@5.2.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/express-serve-static-core': 4.19.6 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.8 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1(supports-color@8.1.1) + connect-history-api-fallback: 2.0.0 + express: 4.22.2(supports-color@8.1.1) + graceful-fs: 4.2.11 + http-proxy-middleware: 2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)) + ipaddr.js: 2.2.0 + launch-editor: 2.14.1 + open: 10.2.0 + p-retry: 6.2.1 + schema-utils: 4.3.3 + selfsigned: 2.4.1 + serve-index: 1.9.1(supports-color@8.1.1) + sockjs: 0.3.24 + spdy: 4.0.2(supports-color@8.1.1) + webpack-dev-middleware: 7.4.5(webpack@5.104.1) + ws: 8.21.0 + optionalDependencies: + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate - webpack-dev-server@5.2.6(webpack-cli@5.1.4(webpack@5.104.1))(webpack@5.104.1): + webpack-dev-server@5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack-cli@5.1.4)(webpack@5.104.1): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -20243,25 +21224,25 @@ snapshots: bonjour-service: 1.3.0 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.8.1 + compression: 1.8.1(supports-color@8.1.1) connect-history-api-fallback: 2.0.0 - express: 4.22.2 + express: 4.22.2(supports-color@8.1.1) graceful-fs: 4.2.11 - http-proxy-middleware: 2.0.10(@types/express@4.17.25) + http-proxy-middleware: 2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)) ipaddr.js: 2.2.0 launch-editor: 2.14.1 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 selfsigned: 5.5.0 - serve-index: 1.9.1 + serve-index: 1.9.1(supports-color@8.1.1) sockjs: 0.3.24 - spdy: 4.0.2 + spdy: 4.0.2(supports-color@8.1.1) webpack-dev-middleware: 7.4.5(webpack@5.104.1) ws: 8.21.0 optionalDependencies: - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) - webpack-cli: 5.1.4(webpack@5.104.1) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) + webpack-cli: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) transitivePeerDependencies: - bufferutil - debug @@ -20281,9 +21262,9 @@ snapshots: webpack-subresource-integrity@5.1.0(webpack@5.104.1): dependencies: typed-assert: 1.0.9 - webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)) + webpack: 5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4) - webpack@5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack-cli@5.1.4(webpack@5.104.1)): + webpack@5.104.1(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -20307,11 +21288,11 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.4.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.104.1) + terser-webpack-plugin: 5.4.0(@swc/core@1.16.2(@swc/helpers@0.5.17))(esbuild@0.25.5)(uglify-js@3.19.3)(webpack@5.104.1) watchpack: 2.4.4 webpack-sources: 3.3.3 optionalDependencies: - webpack-cli: 5.1.4(webpack@5.104.1) + webpack-cli: 5.1.4(webpack-dev-server@5.2.6)(webpack@5.104.1) transitivePeerDependencies: - '@swc/core' - esbuild diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1fb05a55..8a2048fc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,13 @@ packages: - "libs/*" - - "apps/*" \ No newline at end of file + - "apps/*" +allowBuilds: + '@nestjs/core': true + '@parcel/watcher': true + '@scarf/scarf': true + '@swc/core': true + esbuild: true + lmdb: true + msgpackr-extract: true + nx: true + unrs-resolver: true From 32273576b4927ddb70934fbe72c784e22833b4e0 Mon Sep 17 00:00:00 2001 From: snodel Date: Mon, 14 Sep 2026 10:01:47 -0700 Subject: [PATCH 02/13] fix(tracker): make compact rows show one locale consistently - Default compact mode to the base locale - Persist compact locale selections and update filter labels - Simplify compact row annotations and styling --- .../app/browser/store/browser.store.spec.ts | 48 +++++- .../src/app/browser/store/browser.store.ts | 8 +- .../browser/store/density-mode.utils.spec.ts | 36 +++-- .../app/browser/store/density-mode.utils.ts | 42 +++--- .../store/features/with-filter.feature.ts | 39 ++++- .../features/with-view-preferences.feature.ts | 2 +- .../browser/store/view-preferences.types.ts | 2 +- .../locale-filter/locale-filter.spec.ts | 11 ++ .../header/locale-filter/locale-filter.ts | 8 + .../list/translation-item/item-locales.scss | 9 +- .../translation-item/translation-item.html | 100 ++++--------- .../translation-item/translation-item.scss | 129 +++------------- .../translation-item/translation-item.spec.ts | 140 ++++++++++++------ .../list/translation-item/translation-item.ts | 79 ++++------ .../translationItem/resource_entries.json | 20 --- .../browser/translationItem/tracker_meta.json | 60 -------- 16 files changed, 329 insertions(+), 404 deletions(-) diff --git a/apps/tracker/src/app/browser/store/browser.store.spec.ts b/apps/tracker/src/app/browser/store/browser.store.spec.ts index 1a29b391..d330bb5f 100644 --- a/apps/tracker/src/app/browser/store/browser.store.spec.ts +++ b/apps/tracker/src/app/browser/store/browser.store.spec.ts @@ -754,7 +754,7 @@ describe('BrowserStore', () => { densityMode: 'full' as const, selectedLocales: ['es', 'fr'], showNestedResources: true, - compactLocale: null, + compactLocale: undefined, compactLocaleManuallyChanged: false, sortField: 'key' as const, sortDirection: 'asc' as const, @@ -775,29 +775,52 @@ describe('BrowserStore', () => { expect(loaded).toEqual(prefs); }); - it('should automatically reduce selectedLocales to first when switching to compact', async () => { + it('should start compact on the base locale regardless of the full-density filter', async () => { vi.spyOn(apiService, 'getResourceTree').mockReturnValue(of(mockTreeRoot)); - // Setup available locales and initial selection store.setSelectedCollection({ collectionName: 'c1', locales: ['en', 'es', 'fr'], + baseLocale: 'en', }); await waitForSignals(); - // multi-select + store.setDensityMode('full'); store.setSelectedLocales(['es', 'fr']); expect(store.selectedLocales()).toEqual(['es', 'fr']); store.setDensityMode('compact'); - // Should reduce to the first selected locale - expect(store.selectedLocales().length).toBe(1); - expect(store.selectedLocales()[0]).toBe('es'); + // Compact shows one locale, and it is the base until the user picks another + expect(store.selectedLocales()).toEqual(['en']); + expect(store.compactDisplayLocale()).toBe('en'); expect(store.densityMode()).toBe('compact'); }); + it('should remember the compact locale as soon as it is picked', async () => { + vi.spyOn(apiService, 'getResourceTree').mockReturnValue(of(mockTreeRoot)); + + store.setSelectedCollection({ + collectionName: 'c1', + locales: ['en', 'es', 'fr'], + baseLocale: 'en', + }); + + await waitForSignals(); + + store.setDensityMode('compact'); + store.setSelectedLocales(['fr']); + + expect(store.compactDisplayLocale()).toBe('fr'); + expect(store.compactLocale()).toBe('fr'); + + // Round-trips through full density and back + store.setDensityMode('full'); + store.setDensityMode('compact'); + expect(store.compactDisplayLocale()).toBe('fr'); + }); + it('should load preferences when collection is selected', async () => { const coll = 'load-collection'; const prefs = { @@ -853,7 +876,7 @@ describe('BrowserStore', () => { densityMode: 'compact' as const, selectedLocales: [], showNestedResources: true, - compactLocale: null, + compactLocale: undefined, compactLocaleManuallyChanged: false, sortField: 'key' as const, sortDirection: 'asc' as const, @@ -1013,6 +1036,15 @@ describe('BrowserStore', () => { store.setSelectedLocales(['en', 'es']); expect(store.localeFilterText()).toBe('2 locales'); }); + + it('should name the displayed locale in compact mode, never "All locales"', () => { + store.setBaseLocale('en'); + store.setDensityMode('compact'); + expect(store.localeFilterText()).toBe('en'); + + store.setSelectedLocales(['fr']); + expect(store.localeFilterText()).toBe('fr'); + }); }); describe('Computed: filteredLocales', () => { diff --git a/apps/tracker/src/app/browser/store/browser.store.ts b/apps/tracker/src/app/browser/store/browser.store.ts index 27e925a2..c03e0c17 100644 --- a/apps/tracker/src/app/browser/store/browser.store.ts +++ b/apps/tracker/src/app/browser/store/browser.store.ts @@ -38,7 +38,7 @@ interface RootState { error: string | null; currentFolderPath: string; densityMode: DensityMode; - compactLocale: string | null; + compactLocale: string | undefined; compactLocaleManuallyChanged: boolean; nonCompactSelectedLocales: string[]; } @@ -52,7 +52,7 @@ const initialRootState: RootState = { error: null, currentFolderPath: '', densityMode: 'compact', - compactLocale: null, + compactLocale: undefined, compactLocaleManuallyChanged: false, nonCompactSelectedLocales: [], }; @@ -154,7 +154,7 @@ export const BrowserStore = signalStore( preFilterExpandedFolders: null, isRootExpanded: true, showNestedResources: loaded?.showNestedResources ?? true, - compactLocale: loaded?.compactLocale ?? null, + compactLocale: loaded?.compactLocale ?? undefined, compactLocaleManuallyChanged: loaded?.compactLocaleManuallyChanged ?? false, nonCompactSelectedLocales: [], sortField: loaded?.sortField ?? 'key', @@ -169,7 +169,7 @@ export const BrowserStore = signalStore( if (loaded?.densityMode) { const mode: DensityMode = (loaded.densityMode as string) === 'medium' ? 'compact' : loaded.densityMode; if (mode === 'compact') { - const savedCompactLocale = loaded?.compactLocale ?? null; + const savedCompactLocale = loaded?.compactLocale ?? undefined; const savedSelectedLocales = loaded?.selectedLocales ?? []; const newSelected = resolveCompactLocale({ savedCompactLocale, diff --git a/apps/tracker/src/app/browser/store/density-mode.utils.spec.ts b/apps/tracker/src/app/browser/store/density-mode.utils.spec.ts index 4a8a581f..05863d52 100644 --- a/apps/tracker/src/app/browser/store/density-mode.utils.spec.ts +++ b/apps/tracker/src/app/browser/store/density-mode.utils.spec.ts @@ -6,7 +6,7 @@ const baseCtx: DensityModeContext = { currentSelectedLocales: ['en', 'es', 'de'], availableLocales: ['en', 'es', 'de', 'fr'], baseLocale: 'en', - compactLocale: null, + compactLocale: undefined, compactLocaleManuallyChanged: false, nonCompactSelectedLocales: [], }; @@ -32,17 +32,19 @@ describe('computeDensityModeTransition', () => { it('ignores saved compact locale if not in availableLocales', () => { const ctx: DensityModeContext = { ...baseCtx, compactLocale: 'jp' }; const result = computeDensityModeTransition('compact', ctx); - expect(result.selectedLocales).toEqual(['en']); // first from currentSelectedLocales + expect(result.selectedLocales).toEqual(['en']); // base locale }); - it('falls back to first of current selection when no saved compact locale', () => { - const result = computeDensityModeTransition('compact', baseCtx); + it('starts on the base locale, not the full-density filter, when nothing is saved', () => { + // The full-density filter is a set to compare, not a choice of what to read alone. + const ctx: DensityModeContext = { ...baseCtx, currentSelectedLocales: ['de', 'es'] }; + const result = computeDensityModeTransition('compact', ctx); expect(result.selectedLocales).toEqual(['en']); expect(result.compactLocale).toBe('en'); }); it('falls back to base locale when no selection and no compact locale', () => { - const ctx: DensityModeContext = { ...baseCtx, currentSelectedLocales: [], compactLocale: null }; + const ctx: DensityModeContext = { ...baseCtx, currentSelectedLocales: [], compactLocale: undefined }; const result = computeDensityModeTransition('compact', ctx); expect(result.selectedLocales).toEqual(['en']); }); @@ -51,7 +53,7 @@ describe('computeDensityModeTransition', () => { const ctx: DensityModeContext = { ...baseCtx, currentSelectedLocales: [], - compactLocale: null, + compactLocale: undefined, baseLocale: '', availableLocales: ['fr', 'de'], }; @@ -127,7 +129,7 @@ describe('resolveCompactLocale', () => { it('falls back to base locale when no saved compact locale and no selection', () => { const result = resolveCompactLocale({ - savedCompactLocale: null, + savedCompactLocale: undefined, currentSelectedLocales: [], availableLocales, baseLocale: 'en', @@ -137,7 +139,7 @@ describe('resolveCompactLocale', () => { it('falls back to first available locale when no base locale and no selection', () => { const result = resolveCompactLocale({ - savedCompactLocale: null, + savedCompactLocale: undefined, currentSelectedLocales: [], availableLocales: ['de', 'fr'], baseLocale: '', @@ -145,19 +147,29 @@ describe('resolveCompactLocale', () => { expect(result).toEqual(['de']); }); - it('returns first of current selection when multi-selected', () => { + it('falls back to base locale when the saved selection is not a single locale', () => { const result = resolveCompactLocale({ - savedCompactLocale: null, + savedCompactLocale: undefined, currentSelectedLocales: ['de', 'en'], availableLocales, baseLocale: 'en', }); - expect(result).toEqual(['de']); + expect(result).toEqual(['en']); + }); + + it('falls back to base locale when the saved single selection is no longer available', () => { + const result = resolveCompactLocale({ + savedCompactLocale: undefined, + currentSelectedLocales: ['jp'], + availableLocales, + baseLocale: 'en', + }); + expect(result).toEqual(['en']); }); it('returns current single selection unchanged', () => { const result = resolveCompactLocale({ - savedCompactLocale: null, + savedCompactLocale: undefined, currentSelectedLocales: ['es'], availableLocales, baseLocale: 'en', diff --git a/apps/tracker/src/app/browser/store/density-mode.utils.ts b/apps/tracker/src/app/browser/store/density-mode.utils.ts index 4ce88013..99e0929d 100644 --- a/apps/tracker/src/app/browser/store/density-mode.utils.ts +++ b/apps/tracker/src/app/browser/store/density-mode.utils.ts @@ -2,7 +2,7 @@ import type { DensityMode } from '../types/density-mode'; export interface DensityModeTransition { readonly selectedLocales: string[]; - readonly compactLocale: string | null; + readonly compactLocale: string | undefined; readonly compactLocaleManuallyChanged: boolean; readonly nonCompactSelectedLocales: string[]; } @@ -12,7 +12,7 @@ export interface DensityModeContext { readonly currentSelectedLocales: string[]; readonly availableLocales: string[]; readonly baseLocale: string; - readonly compactLocale: string | null; + readonly compactLocale: string | undefined; readonly compactLocaleManuallyChanged: boolean; readonly nonCompactSelectedLocales: string[]; } @@ -41,16 +41,13 @@ export function computeDensityModeTransition( nonCompactSelectedLocales = context.currentSelectedLocales; compactLocaleManuallyChanged = false; + // Compact starts on the locale it was last showing, and otherwise on the + // base locale. The full-density filter is deliberately not consulted: it is a + // set of locales to compare, not a statement about which one to read alone. if (compactLocale && context.availableLocales.includes(compactLocale)) { selectedLocales = [compactLocale]; - } else if (context.currentSelectedLocales.length > 0) { - selectedLocales = [context.currentSelectedLocales[0]]; - compactLocale = context.currentSelectedLocales[0]; } else { - const fallback = - context.baseLocale && context.availableLocales.includes(context.baseLocale) - ? context.baseLocale - : (context.availableLocales[0] ?? null); + const fallback = defaultCompactLocale(context.availableLocales, context.baseLocale); if (fallback) { selectedLocales = [fallback]; @@ -72,12 +69,24 @@ export function computeDensityModeTransition( return { selectedLocales, compactLocale, compactLocaleManuallyChanged, nonCompactSelectedLocales }; } +/** + * The base locale when the collection has it, otherwise its first locale, or + * undefined when there is nothing to show at all. + */ +function defaultCompactLocale(availableLocales: string[], baseLocale: string): string | undefined { + if (baseLocale && availableLocales.includes(baseLocale)) return baseLocale; + return availableLocales.at(0); +} + /** * Resolves a single locale to display in compact mode from a saved preference and available options. * Used during collection initialization when restoring view preferences. + * + * Order: the remembered compact locale, then a saved single selection (older + * preferences persisted only that), then the base locale. */ export function resolveCompactLocale(options: { - readonly savedCompactLocale: string | null; + readonly savedCompactLocale: string | undefined; readonly currentSelectedLocales: string[]; readonly availableLocales: string[]; readonly baseLocale: string; @@ -88,15 +97,10 @@ export function resolveCompactLocale(options: { return [savedCompactLocale]; } - if (currentSelectedLocales.length === 0) { - if (baseLocale && availableLocales.includes(baseLocale)) return [baseLocale]; - if (availableLocales.length > 0) return [availableLocales[0]]; - return []; - } - - if (currentSelectedLocales.length > 1) { - return [currentSelectedLocales[0]]; + if (currentSelectedLocales.length === 1 && availableLocales.includes(currentSelectedLocales[0])) { + return currentSelectedLocales; } - return currentSelectedLocales; + const fallback = defaultCompactLocale(availableLocales, baseLocale); + return fallback ? [fallback] : []; } diff --git a/apps/tracker/src/app/browser/store/features/with-filter.feature.ts b/apps/tracker/src/app/browser/store/features/with-filter.feature.ts index 42fce044..814cf907 100644 --- a/apps/tracker/src/app/browser/store/features/with-filter.feature.ts +++ b/apps/tracker/src/app/browser/store/features/with-filter.feature.ts @@ -16,6 +16,18 @@ const initialFilterState: FilterState = { sortDirection: 'asc', }; +/** + * The one locale a compact row shows: the single selection when there is one, + * otherwise the base locale, otherwise the collection's first locale. Compact + * never shows a pair, so this is also what the locale filter must report. + */ +function resolveCompactDisplayLocale(selected: string[], available: string[], base: string): string { + const picked = selected.find((locale) => available.includes(locale)); + if (picked) return picked; + if (base && available.includes(base)) return base; + return available[0] ?? base; +} + export function withFilterFeature<_>() { return signalStoreFeature( { @@ -23,11 +35,12 @@ export function withFilterFeature<_>() { availableLocales: string[]; baseLocale: string; densityMode: string; + compactLocale: string | undefined; compactLocaleManuallyChanged: boolean; }>(), }, withState(initialFilterState), - withComputed(({ selectedLocales, selectedStatuses, availableLocales, baseLocale }) => ({ + withComputed(({ selectedLocales, selectedStatuses, availableLocales, baseLocale, densityMode }) => ({ isShowingAllLocales: computed(() => { const selected = selectedLocales(); const available = availableLocales(); @@ -38,6 +51,12 @@ export function withFilterFeature<_>() { const selected = selectedLocales(); const available = availableLocales(); + // Compact shows exactly one locale, so "All locales" would be a lie about + // what is on screen. Name the locale the rows are actually showing. + if (densityMode() === 'compact') { + return resolveCompactDisplayLocale(selected, available, baseLocale()); + } + if (selected.length === 0 || selected.length === available.length) return 'All locales'; if (selected.length === 1) return selected[0]; return `${selected.length} locales`; @@ -58,6 +77,19 @@ export function withFilterFeature<_>() { return result; }), + /** + * The single locale a compact row displays. + * + * Compact shows one value per row, never a pair. The selection is the + * single-select locale filter; when nothing is selected the row shows the + * base locale, because that is the string the developer wrote and arrived + * looking for. A collection with no base locale among its files falls back + * to its first locale so the row is never blank. + */ + compactDisplayLocale: computed(() => + resolveCompactDisplayLocale(selectedLocales(), availableLocales(), baseLocale()), + ), + filterableLocales: computed(() => { const available = availableLocales(); const base = baseLocale(); @@ -71,6 +103,10 @@ export function withFilterFeature<_>() { const isCompactMode = store.densityMode() === 'compact'; patchState(store, { selectedLocales: locales, + // In compact the selection *is* the compact locale, so it is remembered + // as soon as it changes — not only when the user leaves compact. That is + // what lets a reload land on the locale they were looking at. + compactLocale: isCompactMode ? locales.at(0) : store.compactLocale(), compactLocaleManuallyChanged: isCompactMode ? true : store.compactLocaleManuallyChanged(), }); }, @@ -82,6 +118,7 @@ export function withFilterFeature<_>() { patchState(store, { selectedLocales: newLocales, + compactLocale: isCompactMode ? newLocales.at(0) : store.compactLocale(), compactLocaleManuallyChanged: isCompactMode ? true : store.compactLocaleManuallyChanged(), }); }, diff --git a/apps/tracker/src/app/browser/store/features/with-view-preferences.feature.ts b/apps/tracker/src/app/browser/store/features/with-view-preferences.feature.ts index 741433ac..f55e5515 100644 --- a/apps/tracker/src/app/browser/store/features/with-view-preferences.feature.ts +++ b/apps/tracker/src/app/browser/store/features/with-view-preferences.feature.ts @@ -40,7 +40,7 @@ export function withViewPreferencesFeature<_>() { selectedStatuses: TranslationStatus[]; availableLocales: string[]; baseLocale: string; - compactLocale: string | null; + compactLocale: string | undefined; compactLocaleManuallyChanged: boolean; nonCompactSelectedLocales: string[]; }>(), diff --git a/apps/tracker/src/app/browser/store/view-preferences.types.ts b/apps/tracker/src/app/browser/store/view-preferences.types.ts index b89aac1e..2bfcaf1c 100644 --- a/apps/tracker/src/app/browser/store/view-preferences.types.ts +++ b/apps/tracker/src/app/browser/store/view-preferences.types.ts @@ -5,7 +5,7 @@ export interface ViewPreferences { densityMode: DensityMode; selectedLocales: string[]; showNestedResources: boolean; - compactLocale: string | null; + compactLocale: string | undefined; compactLocaleManuallyChanged: boolean; sortField: 'key' | 'status'; sortDirection: 'asc' | 'desc'; diff --git a/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.spec.ts b/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.spec.ts index f40ff8ce..d607d4ae 100644 --- a/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.spec.ts +++ b/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.spec.ts @@ -16,6 +16,7 @@ describe('LocaleFilter', () => { availableLocales: signal(['en', 'es', 'fr', 'de']), filterableLocales: signal(['es', 'fr', 'de']), // Excludes base locale 'en' selectedLocales: signal([]), + compactDisplayLocale: signal('en'), localeFilterText: signal('All locales'), isShowingAllLocales: signal(true), toggleLocale: vi.fn(), @@ -75,6 +76,16 @@ describe('LocaleFilter', () => { expect(component.isLocaleSelected('fr')).toBe(false); }); + it('should check the displayed locale in single-select mode even with no selection', () => { + fixture.componentRef.setInput('multiSelect', false); + mockStore.selectedLocales.set([]); + mockStore.compactDisplayLocale.set('en'); + fixture.detectChanges(); + + expect(component.isLocaleSelected('en')).toBe(true); + expect(component.isLocaleSelected('es')).toBe(false); + }); + it('should toggle locale', () => { component.toggleLocale('en'); expect(mockStore.toggleLocale).toHaveBeenCalledWith('en'); diff --git a/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.ts b/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.ts index 62733e22..fb175088 100644 --- a/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.ts +++ b/apps/tracker/src/app/browser/translations/header/locale-filter/locale-filter.ts @@ -71,8 +71,16 @@ export class LocaleFilter { /** * Checks if a locale is currently selected. + * + * In single-select mode the checked radio is the locale the compact rows are + * showing, which is the base locale until the user picks one. Reading the raw + * selection would leave every radio unchecked on first load while the list + * plainly shows a locale. */ isLocaleSelected(locale: string): boolean { + if (!this.multiSelect()) { + return this.store.compactDisplayLocale() === locale; + } return this.store.selectedLocales().includes(locale); } diff --git a/apps/tracker/src/app/browser/translations/list/translation-item/item-locales.scss b/apps/tracker/src/app/browser/translations/list/translation-item/item-locales.scss index 5c7b8316..68ff9fb0 100644 --- a/apps/tracker/src/app/browser/translations/list/translation-item/item-locales.scss +++ b/apps/tracker/src/app/browser/translations/list/translation-item/item-locales.scss @@ -63,7 +63,7 @@ flex-shrink: 0; font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); - padding: calc(var(--spacing-1) / 2) var(--spacing-2); + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-2) calc(var(--spacing-1) / 2); } /* @@ -98,7 +98,10 @@ font-weight: var(--font-weight-semibold); gap: calc(var(--spacing-1) / 2); letter-spacing: 0.01em; - padding: calc(var(--spacing-1) / 2) var(--spacing-2); + /* The extra pixel on top is an optical correction: at this size the label's + x-height sits below the geometric centre, so equal padding reads as text + hugging the bottom edge. The compact chip carries the same asymmetry. */ + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-2) calc(var(--spacing-1) / 2); /* Holds the column steady across locales; the longest label sets the floor, and min-width lets RU and DE grow past it rather than truncate. */ min-width: 96px; @@ -152,7 +155,7 @@ font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); gap: calc(var(--spacing-1) / 2); - padding: calc(var(--spacing-1) / 2) var(--spacing-2); + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-2) calc(var(--spacing-1) / 2); /* Matches the status chip it sits beside, so the two read as one row of annotations rather than two things at slightly different heights. */ } diff --git a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.html b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.html index 5f04b865..30a48b66 100644 --- a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.html +++ b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.html @@ -25,9 +25,10 @@ @if (currentDensityMode() === 'compact') { - - + - - @switch (compactLayout()) { - @case ('paired') { - - {{ compactDisplay().locale }} - - @if (compactDisplay().status) { - - } - - @if (compactDisplay().isSameAsBase) { - - - {{ TOKENS.BROWSER.TRANSLATIONITEM.SAMEASSOURCETAG | transloco }} - - } - - - + @if (hasCompactAnnotation()) { + + @if (compactDisplay().needsAttention) { + - - } - - @case ('source-only') { - - - {{ compactDisplay().locale }} - {{ TOKENS.BROWSER.TRANSLATIONITEM.BASELOCALETAG | transloco }} - - } + } - @case ('no-base') { - - - {{ compactDisplay().locale }} - - @if (compactDisplay().status) { - - } - - - - } + > + + {{ TOKENS.BROWSER.TRANSLATIONITEM.SAMEASSOURCETAG | transloco }} + + } + } } @else { diff --git a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.scss b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.scss index 30846dee..0867874c 100644 --- a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.scss +++ b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.scss @@ -87,20 +87,19 @@ /* * Compact zones. The row is one line composed by the header in `inline` layout: - * drag handle · key · identity · annotation · rail. Everything here is about what - * gives way first when the line runs out of room. + * drag handle · key · value · annotation · rail. One locale's value, never a + * pair; the annotation appears only when the value needs attention. */ -/* Identity — the source string. Takes the larger share and truncates last, - because it is the only thing on the row a developer already recognises. */ -.compact-identity { +/* The value — whichever locale compact is showing. Takes the larger share and + truncates last: it is the row's content, everything else is a marker. */ +.compact-value { color: var(--color-text-primary); - // The zone that absorbs the squeeze. Everything in the annotation is a chip or - // a code that clips rather than reflows, so the identity is the only thing that - // can give way gracefully — and an ellipsis on a sentence is readable, where a - // status chip cut down to "Ne" is not. + // The zone that absorbs the squeeze. Everything in the annotation is a chip + // that clips rather than reflows, so the value is the only thing that can give + // way gracefully — and an ellipsis on a sentence is readable, where a status + // chip cut down to "Ne" is not. flex: 1 1 0; - min-width: 4rem; font-size: var(--font-size-base); font-weight: var(--font-weight-medium); line-height: var(--line-height-snug); @@ -110,93 +109,32 @@ white-space: nowrap; } -/* No source text at all — a state to read, not content. */ -.compact-identity--empty { +/* No value for this locale — a state to read, not content. */ +.compact-value--empty { color: var(--color-text-secondary); font-style: italic; } -/* Annotation — the selected locale, measured against the identity beside it. */ +/* Annotation — only rendered when the row has work to report: a new or stale + status, or a translation that is the source text verbatim. */ .compact-annotation { align-items: center; color: var(--color-text-secondary); display: flex; - // Sizes to its own content and never shrinks below it: its parts are chips and - // codes with nothing to truncate. What it can no longer afford is dropped - // wholesale by the container queries at the end of this file, in priority - // order, rather than sliced in half. + // Sizes to its own content and never shrinks below it: its parts are chips + // with nothing to truncate. What it can no longer afford is dropped wholesale + // by the container queries at the end of this file, rather than sliced in half. flex: 0 0 auto; gap: var(--spacing-2); - max-width: 100%; - min-width: 0; -} - -/* The tagged variants carry no value, so they claim only the width they need - and leave the rest to the identity. */ -.compact-annotation--tagged { - flex: 0 0 auto; -} - -.compact-translation { - flex: 1 1 auto; - font-size: var(--font-size-sm); - line-height: var(--line-height-snug); min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.compact-translation--empty { - color: var(--color-text-tertiary); - font-style: italic; -} - -.compact-locale-code { - color: var(--color-text-secondary); - flex-shrink: 0; - font-size: var(--font-size-xs); - font-weight: var(--font-weight-semibold); - text-transform: uppercase; -} - -/* Marks a row that is showing base-locale source text, not a translation. */ -.compact-locale-tag { - background-color: var(--color-background-subtle); - border-radius: var(--border-radius-full); - color: var(--color-text-placeholder); - flex-shrink: 0; - font-size: var(--font-size-xs); - padding: 0 var(--spacing-1); -} - -/* - * The collection has no base locale, so the app chose which locale to show. That - * choice is arbitrary and must never be silent — this names it as a default and - * its tooltip points at the locale filter. A real button so the explanation is - * reachable from the keyboard, not only on hover. - */ -.default-locale-tag { - background-color: var(--color-background-subtle); - border: 1px dashed var(--color-border-strong); - border-radius: var(--border-radius-full); - color: var(--color-text-placeholder); - cursor: help; - flex-shrink: 0; - font-family: inherit; - font-size: var(--font-size-xs); - padding: 0 var(--spacing-2); - - &:focus-visible { - outline: 2px solid var(--focus-ring-color); - outline-offset: 2px; - } } /* * Compact status chip. Mirrors item-locales.scss so a status looks identical in * both densities — the toggle changes how much you see, never what a colour - * means. + * means. Only `new` and `stale` are ever rendered here: the quiet states are + * left to the rollup ring. The 2px of vertical padding matches the full-density + * chip; without it the text sat flush against the border and read as cramped. */ .compact-annotation .status-chip { align-items: center; @@ -211,7 +149,7 @@ gap: calc(var(--spacing-1) / 2); letter-spacing: 0.01em; line-height: var(--line-height-snug); - padding: 0 var(--spacing-2); + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-2) calc(var(--spacing-1) / 2); &.status-new { background-color: var(--color-status-new-surface); @@ -224,18 +162,6 @@ border-color: var(--color-status-stale-border); color: var(--color-status-stale-ink); } - - &.status-translated { - background-color: var(--color-status-translated-surface); - border-color: var(--color-status-translated-border); - color: var(--color-status-translated-ink); - } - - &.status-verified { - background-color: var(--color-status-verified-surface); - border-color: var(--color-status-verified-border); - color: var(--color-status-verified-ink); - } } .compact-annotation .status-chip__icon { @@ -258,7 +184,7 @@ font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); gap: calc(var(--spacing-1) / 2); - padding: 0 var(--spacing-2); + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-2) calc(var(--spacing-1) / 2); } .compact-annotation .same-as-source__icon { @@ -475,16 +401,7 @@ } .compact-annotation .same-as-source { - padding: 0 var(--spacing-1); - } -} - -@container (max-width: 40rem) { - /* Below this the translation is a word or two of noise. The status chip and - the marker still say what state the row is in, and the editor shows the - value in full. */ - .compact-annotation .compact-translation { - display: none; + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-1) calc(var(--spacing-1) / 2); } } @@ -495,6 +412,6 @@ } .compact-annotation .status-chip { - padding: 0 var(--spacing-1); + padding: calc(var(--spacing-1) / 2 + 1px) var(--spacing-1) calc(var(--spacing-1) / 2); } } diff --git a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.spec.ts b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.spec.ts index 2eb31230..ecdc3981 100644 --- a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.spec.ts +++ b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.spec.ts @@ -15,6 +15,10 @@ async function configureTranslationItemTestBed(): Promise<{ component: TranslationItem; store: InstanceType; }> { + // View preferences persist per collection, so a compact locale picked by one + // test would otherwise be restored by the next. + localStorage.clear(); + await TestBed.configureTestingModule({ imports: [TranslationItem, getTranslocoTestingModule()], providers: [ @@ -70,59 +74,91 @@ describe('TranslationItem', () => { status: { es: 'new' }, } as any; - // Compact mode with 'es' selected: filteredLocales() = ['es'], so primaryLocaleValue() = '' store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es'], baseLocale: 'en' }); + store.setDensityMode('compact'); store.setSelectedLocales(['es']); fixture.componentRef.setInput('translation', t); fixture.detectChanges(); - // primaryLocaleValue shows the non-base locale's value ('es' which is empty) - expect(component.primaryLocaleValue()).toBe(''); + expect(component.compactDisplay().value).toBe(''); const html = fixture.nativeElement.innerHTML as string; expect(html).toContain('No translation'); }); - it('should flag the base locale as a fallback when no non-base locale is available', () => { - store.setSelectedCollection({ collectionName: 'test', locales: ['en'], baseLocale: 'en' }); - store.setDensityMode('full'); - store.clearAllLocales(); - fixture.componentRef.setInput('translation', mockTranslation); - fixture.detectChanges(); + describe('compact row composition', () => { + it('shows the base value alone by default', () => { + store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es'], baseLocale: 'en' }); + store.setDensityMode('compact'); + fixture.componentRef.setInput('translation', mockTranslation); + fixture.detectChanges(); - expect(component.compactDisplay().locale).toBe(store.baseLocale()); - expect(component.compactDisplay().isBaseFallback).toBe(true); - const html = fixture.nativeElement.innerHTML as string; - expect(html).toContain('common.buttons.save'); - }); + const display = component.compactDisplay(); + expect(display.locale).toBe('en'); + expect(display.isBase).toBe(true); + expect(display.value).toBe('Save'); + // Source text carries no status and no marker + expect(display.status).toBeUndefined(); + expect(component.hasCompactAnnotation()).toBe(false); + + const html = fixture.nativeElement.innerHTML as string; + expect(html).toContain('Save'); + expect(html).not.toContain('Guardar'); + }); - describe('compact row composition', () => { - it('leads with the base value and annotates it with the selected locale', () => { + it('replaces the base value with the selected locale value, not beside it', () => { store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es'], baseLocale: 'en' }); store.setDensityMode('compact'); store.setSelectedLocales(['es']); fixture.componentRef.setInput('translation', mockTranslation); fixture.detectChanges(); - expect(component.compactLayout()).toBe('paired'); - // The source string the developer wrote identifies the row... - expect(component.identityValue()).toBe('Save'); - // ...and the selected locale sits beside it as the comparison. expect(component.compactDisplay().locale).toBe('es'); expect(component.compactDisplay().value).toBe('Guardar'); + + const html = fixture.nativeElement.innerHTML as string; + expect(html).toContain('Guardar'); + expect(html).not.toContain('>Save<'); }); - it('shows source text alone when the selection resolves to the base locale', () => { - store.setSelectedCollection({ collectionName: 'test', locales: ['en'], baseLocale: 'en' }); + it('hides the status chip for translated and verified rows', () => { + store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es', 'fr'], baseLocale: 'en' }); store.setDensityMode('compact'); - store.clearAllLocales(); + store.setSelectedLocales(['es']); fixture.componentRef.setInput('translation', mockTranslation); fixture.detectChanges(); - expect(component.compactLayout()).toBe('source-only'); - expect(component.identityValue()).toBe('Save'); + expect(component.compactDisplay().status).toBe('translated'); + expect(component.compactDisplay().needsAttention).toBe(false); + expect(fixture.nativeElement.querySelector('.compact-annotation .status-chip')).toBeNull(); + + store.setSelectedLocales(['fr']); + fixture.detectChanges(); + expect(component.compactDisplay().status).toBe('verified'); + expect(fixture.nativeElement.querySelector('.compact-annotation .status-chip')).toBeNull(); + }); + + it('shows the status chip only for new and stale rows', () => { + const needsWork: ResourceSummaryDto = { + key: 'common.buttons.save', + translations: { en: 'Save', es: 'Guardar viejo', fr: '' }, + status: { es: 'stale', fr: 'new' }, + } as ResourceSummaryDto; + + store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es', 'fr'], baseLocale: 'en' }); + store.setDensityMode('compact'); + store.setSelectedLocales(['es']); + fixture.componentRef.setInput('translation', needsWork); + fixture.detectChanges(); + + expect(component.compactDisplay().needsAttention).toBe(true); + expect(fixture.nativeElement.querySelector('.compact-annotation .status-chip.status-stale')).not.toBeNull(); + + store.setSelectedLocales(['fr']); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.compact-annotation .status-chip.status-new')).not.toBeNull(); }); - it('falls back to the shown locale when the collection carries no base locale', () => { + it('falls back to the first locale when the collection carries no base locale', () => { // A vendored collection can ship translations with no base locale at all. const noBase: ResourceSummaryDto = { key: 'agGrid.addToLabels', @@ -132,15 +168,11 @@ describe('TranslationItem', () => { store.setSelectedCollection({ collectionName: 'ds', locales: ['ar', 'de'], baseLocale: 'en' }); store.setDensityMode('compact'); - store.setSelectedLocales(['ar']); fixture.componentRef.setInput('translation', noBase); fixture.detectChanges(); - expect(component.compactLayout()).toBe('no-base'); - // There is no source text to lead with, so the shown locale becomes identity. - expect(component.identityValue()).toBe('إضافة'); - // And the arbitrary pick is named rather than silent. - expect(fixture.nativeElement.innerHTML as string).toContain('default'); + expect(component.compactDisplay().locale).toBe('ar'); + expect(component.compactDisplay().value).toBe('إضافة'); }); it('marks a translation that is the source text verbatim', () => { @@ -159,6 +191,27 @@ describe('TranslationItem', () => { // Status says `translated`; a checksum cannot see that nobody touched it. expect(component.compactDisplay().status).toBe('translated'); expect(component.compactDisplay().isSameAsBase).toBe(true); + expect(component.hasCompactAnnotation()).toBe(true); + expect(fixture.nativeElement.querySelector('.compact-annotation .same-as-source')).not.toBeNull(); + }); + + it('shows one marker at most: the status chip wins over same-as-source', () => { + const untouchedNew: ResourceSummaryDto = { + key: 'common.buttons.add', + translations: { en: 'Add', 'fr-ca': 'Add' }, + status: { 'fr-ca': 'new' }, + } as ResourceSummaryDto; + + store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'fr-ca'], baseLocale: 'en' }); + store.setDensityMode('compact'); + store.setSelectedLocales(['fr-ca']); + fixture.componentRef.setInput('translation', untouchedNew); + fixture.detectChanges(); + + expect(component.compactDisplay().needsAttention).toBe(true); + expect(component.compactDisplay().isSameAsBase).toBe(false); + expect(fixture.nativeElement.querySelector('.compact-annotation .status-chip.status-new')).not.toBeNull(); + expect(fixture.nativeElement.querySelector('.compact-annotation .same-as-source')).toBeNull(); }); }); @@ -206,36 +259,31 @@ describe('TranslationItem - Compact helpers', () => { ({ fixture, component, store } = await configureTranslationItemTestBed()); }); - it('should display the first non-base locale, not the base locale', () => { + it('should show the base locale in compact until a locale is picked', () => { store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es', 'fr'], baseLocale: 'en' }); - store.setDensityMode('full'); - store.clearAllLocales(); + store.setDensityMode('compact'); fixture.componentRef.setInput('translation', mockTranslation); fixture.detectChanges(); - expect(component.compactDisplay().locale).toBe('es'); - expect(component.compactDisplay().isBaseFallback).toBe(false); + expect(component.compactDisplay().locale).toBe('en'); + expect(component.compactDisplay().isBase).toBe(true); }); - it('should return selected non-base locale value, or base value when only base is selected', () => { + it('should follow the single compact selection', () => { store.setSelectedCollection({ collectionName: 'test', locales: ['en', 'es'], baseLocale: 'en' }); - store.setDensityMode('full'); - store.clearAllLocales(); + store.setDensityMode('compact'); fixture.componentRef.setInput('translation', mockTranslation); fixture.detectChanges(); - // When a non-base locale is in the list alongside base, show non-base value - expect(component.primaryLocaleValue()).toBe('Guardar'); + expect(component.compactDisplay().value).toBe('Save'); - // When only a non-base locale is selected, show its value store.setSelectedLocales(['es']); fixture.detectChanges(); - expect(component.primaryLocaleValue()).toBe('Guardar'); + expect(component.compactDisplay().value).toBe('Guardar'); - // When only base locale is selected (or no non-base locales), show base value store.setSelectedLocales(['en']); fixture.detectChanges(); - expect(component.primaryLocaleValue()).toBe('Save'); + expect(component.compactDisplay().value).toBe('Save'); }); it('rollupStatus should calculate worst status across all locales', () => { diff --git a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.ts b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.ts index 0e46a6e7..d3bb55cd 100644 --- a/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.ts +++ b/apps/tracker/src/app/browser/translations/list/translation-item/translation-item.ts @@ -1,6 +1,5 @@ import { Component, ChangeDetectionStrategy, input, output, computed, effect, inject, signal } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { CdkDrag, CdkDragPlaceholder } from '@angular/cdk/drag-drop'; import type { ResourceSummaryDto } from '@simoncodes-ca/data-transfer'; import { BrowserStore } from '../../../store/browser.store'; @@ -54,7 +53,6 @@ const STATUS_SORT_PRIORITY: Record = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ MatIconModule, - MatTooltipModule, TranslationItemHeader, TranslationItemLocales, HighlightPipe, @@ -166,29 +164,42 @@ export class TranslationItem { readonly currentDensityMode = computed(() => this.#store.densityMode()); /** - * The locale whose value the compact row annotates, together with its status. + * What the compact row shows: one locale's value, and only what is worth + * saying about it. * - * `isBaseFallback` marks the case where the compact locale selection resolved to - * the base locale itself — there is then only one string on the row, and it is - * source text rather than a translation. + * The locale is the store's single compact selection — the base locale until + * the user picks another, at which point that locale's value takes the base + * value's place rather than sitting beside it. Compact used to show source and + * translation as a pair, which made the translation an awkward annotation + * hanging off the right-hand end of the row. + * + * `needsAttention` gates the status chip. `translated` and `verified` are the + * quiet states — the rollup already reports them — so a chip on every row saying + * so was noise. Only `new` and `stale` name work to be done, and only they are + * shown. A row carries at most one marker: the chip when the status asks for + * work, otherwise the same-as-source flag when a "finished" translation is + * really the source text. */ readonly compactDisplay = computed(() => { + const locale = this.#store.compactDisplayLocale(); const base = this.#store.baseLocale(); - const nonBaseLocales = this.#store.filteredLocales().filter((locale) => locale !== base); - - const locale = nonBaseLocales.length > 0 ? nonBaseLocales[0] : base; const translation = this.translation(); const value = translation.translations[locale] || ''; - const isBaseFallback = nonBaseLocales.length === 0; + const isBase = locale === base; + const status = isBase ? undefined : translation.status?.[locale]; + const needsAttention = status === 'new' || status === 'stale'; return { locale, value, - status: translation.status?.[locale], - isBaseFallback, - // A base-locale fallback row is already labelled `source`; saying "same as - // source" next to it would be the same fact twice. - isSameAsBase: !isBaseFallback && isIdenticalToBase(value, translation.translations[base] || ''), + isBase, + status, + needsAttention, + // Source text is trivially the same as itself, so the marker only means + // something for a translation — and only for one the status chip already + // calls finished. A `new` row that still holds the English copy is flagged + // once, by the chip; a second marker saying the same thing is noise. + isSameAsBase: !isBase && !needsAttention && isIdenticalToBase(value, translation.translations[base] || ''), }; }); @@ -198,42 +209,12 @@ export class TranslationItem { /** Transloco token for the compact row's status label ('' when the status is unknown). */ readonly compactStatusToken = computed(() => statusLabelTokenFor(this.compactDisplay().status)); - /** - * How the compact row composes itself. Three genuinely different situations, - * named rather than inferred in the template: - * - * - `paired` — the ordinary case. The base value identifies the row and the - * selected locale's value sits beside it, so source and translation can be - * judged against each other in one horizontal glance. - * - `source-only` — the compact locale selection resolved to the base locale - * itself. One string, and it is source text; there is nothing to compare it - * against, so no annotation value is shown. - * - `no-base` — the collection carries no base locale at all (a vendored - * design-system collection may ship `ar`…`sv` and no `en`). There is no source - * text to lead with, so the shown locale becomes the identity and the row says - * the pick was automatic. - */ - readonly compactLayout = computed<'paired' | 'source-only' | 'no-base'>(() => { - if (this.compactDisplay().isBaseFallback) return 'source-only'; - if (!this.baseValue().trim()) return 'no-base'; - return 'paired'; + /** Whether the compact row has anything to say beyond the value itself. */ + readonly hasCompactAnnotation = computed(() => { + const display = this.compactDisplay(); + return display.needsAttention || display.isSameAsBase; }); - /** - * The string that identifies a compact row. - * - * The base value wherever one exists, because it is the only text on the row the - * developer already knows — they wrote it, and they arrived looking for it. - * Compact used to omit it entirely and lead with a translation, which handed a - * developer scanning an eleven-locale collection a script they could not read. - */ - readonly identityValue = computed(() => - this.compactLayout() === 'no-base' ? this.compactDisplay().value : this.baseValue(), - ); - - /** Value rendered in the compact row's annotation zone. */ - readonly primaryLocaleValue = computed(() => this.compactDisplay().value); - /** Signal controlling whether the full-mode content is expanded */ readonly isExpanded = signal(false); diff --git a/apps/tracker/src/i18n/browser/translationItem/resource_entries.json b/apps/tracker/src/i18n/browser/translationItem/resource_entries.json index 247c6ed7..d26de6a8 100644 --- a/apps/tracker/src/i18n/browser/translationItem/resource_entries.json +++ b/apps/tracker/src/i18n/browser/translationItem/resource_entries.json @@ -178,25 +178,5 @@ "ru": "{locale} идентичен исходному тексту", "ja": "{locale} はソーステキストと同一です", "de": "{locale} ist identisch mit dem Quelltext" - }, - "defaultLocaleTag": { - "source": "default", - "comment": "Short marker on the compact row when the shown locale was picked automatically rather than chosen by the user", - "tags": ["browser"], - "es": "por defecto", - "fr-ca": "défaut", - "ru": "по умолчанию", - "ja": "デフォルト", - "de": "Standard" - }, - "defaultLocaleTooltipX": { - "source": "This collection has no base locale, so {locale} is shown by default. Pick a different one with the locale filter.", - "comment": "Tooltip explaining why a locale was auto-selected for the compact row, and how to change it", - "tags": ["browser"], - "es": "Esta colección no tiene una configuración regional base, por lo que {locale} se muestra por defecto. Seleccione una diferente con el filtro de configuración regional.", - "fr-ca": "Cette collection n'a pas de langue de base ; donc, {locale} est affiché par défaut. Choisissez-en un autre à l'aide du filtre de langue.", - "ru": "В этой коллекции отсутствует базовая локаль, поэтому по умолчанию отображается {locale}. Выберите другую локаль с помощью фильтра локали.", - "ja": "このコレクションには基本ロケールが設定されていないため、デフォルトで{locale}が表示されます。ロケールフィルターを使用して別のロケールを選択してください。", - "de": "Diese Sammlung hat kein Basisgebietsschema, daher wird standardmäßig {locale} angezeigt. Wählen Sie mit dem Gebietsschemafilter ein anderes Gebietsschema aus." } } diff --git a/apps/tracker/src/i18n/browser/translationItem/tracker_meta.json b/apps/tracker/src/i18n/browser/translationItem/tracker_meta.json index 4eb171b0..f8bd4da6 100644 --- a/apps/tracker/src/i18n/browser/translationItem/tracker_meta.json +++ b/apps/tracker/src/i18n/browser/translationItem/tracker_meta.json @@ -538,65 +538,5 @@ "baseChecksum": "c0dc809859930b46f606dc17a108ecd2", "status": "translated" } - }, - "defaultLocaleTag": { - "en": { - "checksum": "c21f969b5f03d33d43e04f8f136e7682" - }, - "es": { - "checksum": "e2642a0564b9c5cc80485e23ce0952b3", - "baseChecksum": "c21f969b5f03d33d43e04f8f136e7682", - "status": "translated" - }, - "fr-ca": { - "checksum": "dec6fff71afd6dbe1bfad9a9d49fa88d", - "baseChecksum": "c21f969b5f03d33d43e04f8f136e7682", - "status": "translated" - }, - "ru": { - "checksum": "df5286777a9daed8bceab13c26a98fca", - "baseChecksum": "c21f969b5f03d33d43e04f8f136e7682", - "status": "translated" - }, - "ja": { - "checksum": "646d7148c8576be37d4c8219e0d0fa09", - "baseChecksum": "c21f969b5f03d33d43e04f8f136e7682", - "status": "translated" - }, - "de": { - "checksum": "eb6d8ae6f20283755b339c0dc273988b", - "baseChecksum": "c21f969b5f03d33d43e04f8f136e7682", - "status": "translated" - } - }, - "defaultLocaleTooltipX": { - "en": { - "checksum": "438526f9f6e730c0c85c090770f71ae6" - }, - "es": { - "checksum": "d32ce491d5540f894709c756eb24e7bd", - "baseChecksum": "438526f9f6e730c0c85c090770f71ae6", - "status": "translated" - }, - "fr-ca": { - "checksum": "b9f5200aa369f04397b069efafec132e", - "baseChecksum": "438526f9f6e730c0c85c090770f71ae6", - "status": "translated" - }, - "ru": { - "checksum": "65fee06fcc3216b3fa92a14fa36c7d4c", - "baseChecksum": "438526f9f6e730c0c85c090770f71ae6", - "status": "translated" - }, - "ja": { - "checksum": "3f6d6a3383ae4f4780b46db445218376", - "baseChecksum": "438526f9f6e730c0c85c090770f71ae6", - "status": "translated" - }, - "de": { - "checksum": "54ba17e119798edf39a39a772244e071", - "baseChecksum": "438526f9f6e730c0c85c090770f71ae6", - "status": "translated" - } } } From 09531f9622061c84dc3b575d4bda387b3efe6479 Mon Sep 17 00:00:00 2001 From: snodel Date: Mon, 14 Sep 2026 14:17:42 -0700 Subject: [PATCH 03/13] feat(tracker): redesign the create and edit collection dialogs Replace the flat stack of outlined fields with three labelled groups (Source, Locales, Options). Locales become pill chips with a BASE tag; clicking a chip sets the base while creating, and the base is locked while editing, including a base inherited from the global config. Read-only is a switch row with the node_modules auto-detection explained inline, and tags and protected terms sit behind a disclosure row with counts. Header, top rule and footer match the translation editor. Adds 17 collections.dialog.* resources via the CLI; other locales are left as new entries for translation. Co-Authored-By: Claude Fable 5.1 --- .../collection-form-dialog-data.ts | 7 + .../collection-form-dialog.html | 461 +++++++++---- .../collection-form-dialog.scss | 649 ++++++++++++++++-- .../collection-form-dialog.spec.ts | 111 +++ .../collection-form-dialog.ts | 130 +++- .../app/collections/collections-manager.ts | 13 +- .../collections/dialog/resource_entries.json | 170 +++++ .../i18n/collections/dialog/tracker_meta.json | 510 ++++++++++++++ apps/tracker/src/styles.scss | 30 + 9 files changed, 1851 insertions(+), 230 deletions(-) diff --git a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog-data.ts b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog-data.ts index 0a987252..fdda514e 100644 --- a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog-data.ts +++ b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog-data.ts @@ -18,4 +18,11 @@ export interface CollectionFormDialogData { * Collection configuration (only present in edit mode). */ config?: LingoTrackerCollectionDto; + + /** + * The base locale the collection actually compares against (edit mode). Falls back to the + * global config when the collection does not set its own, so the dialog can mark and lock it + * without writing an explicit `baseLocale` into the collection. + */ + effectiveBaseLocale?: string; } diff --git a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.html b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.html index 260ec65b..cff80b94 100644 --- a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.html +++ b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.html @@ -1,165 +1,346 @@ -
-

{{ dialogTitle | transloco }}

+ diff --git a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.scss b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.scss index 7c0c9c4b..b326291c 100644 --- a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.scss +++ b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.scss @@ -1,129 +1,646 @@ +// The outer box (width, top rule, zero container padding) is owned by +// `.collection-form-dialog-panel` in styles.scss, because the CDK appends the +// overlay pane outside this component. Everything here is the inside. :host { display: block; - min-width: 420px; - max-width: 600px; + max-height: inherit; + // The panel is one step lighter than the page so the field wells read as set + // into it. Light: page and panel share the parchment; dark: the panel lifts + // to the subtle tone and the wells stay on the page black. + --dialog-surface: var(--color-background); + --dialog-well: var(--color-background); +} + +:host-context([data-theme='dark']) { + --dialog-surface: var(--color-background-subtle); +} + +@media (prefers-color-scheme: dark) { + :host-context(:root:not([data-theme='light']):not([data-theme='dark'])) { + --dialog-surface: var(--color-background-subtle); + } +} + +.dialog-wrapper { + display: flex; + flex-direction: column; + max-height: inherit; + background: var(--dialog-surface); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Header +// ───────────────────────────────────────────────────────────────────────────── + +.dialog-header { + position: relative; + flex-shrink: 0; + display: flex; + align-items: flex-start; + gap: var(--spacing-3); + padding: var(--spacing-5) var(--spacing-6) var(--spacing-4); + border-bottom: 1px solid var(--color-border); +} + +.dialog-header-tile { + display: grid; + place-items: center; + flex: none; + width: 38px; + height: 38px; + border-radius: var(--border-radius-lg); + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent); + color: var(--color-primary); + + mat-icon { + font-size: 19px; + width: 19px; + height: 19px; + } +} + +.dialog-header-text { + flex: 1; + min-width: 0; + // Reserve space so long titles never slide under the close button. + padding-right: var(--spacing-8); +} + +.dialog-title { + margin: 0 0 var(--spacing-1); + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-tight); + letter-spacing: -0.01em; + color: var(--color-text-primary); +} + +.dialog-subtitle { + margin: 0; + font-size: var(--font-size-sm); + line-height: var(--line-height-snug); + color: var(--color-text-secondary); +} + +// File names inside prose keep the mono voice the paths use elsewhere, at the +// surrounding size so the line does not jump. +.dialog-subtitle code, +.field-hint code { + font-family: var(--font-family-mono); + font-size: 0.95em; + color: var(--color-text-primary); +} + +.dialog-close { + position: absolute; + top: var(--spacing-3); + right: var(--spacing-3); + color: var(--color-text-secondary); + transition: color var(--transition-fast); - .mdc-dialog__content { - padding-top: var(--spacing-4); + &:hover { + color: var(--color-text-primary); } } -mat-dialog-content { - padding: var(--spacing-6); +// ───────────────────────────────────────────────────────────────────────────── +// Body & groups +// ───────────────────────────────────────────────────────────────────────────── + +.dialog-body { + // Material caps its content at 65vh; here the body takes whatever the panel + // leaves after the header and footer, so the whole create form sits above + // the fold on a laptop and only long edits scroll. + flex: 1 1 auto; + min-height: 0; + max-height: none; + padding: var(--spacing-5) var(--spacing-6) var(--spacing-4); form { display: flex; flex-direction: column; - gap: var(--spacing-4); - } - - .full-width { - width: 100%; + gap: var(--spacing-5); } } -.locale-section { +.group { display: flex; flex-direction: column; - gap: var(--spacing-2); + gap: var(--spacing-3); } -.locale-section-label { - margin: 0; - font-size: var(--font-size-xs); - color: var(--mat-sys-on-surface-variant); - letter-spacing: 0.03em; +.group-head { + display: flex; + flex-direction: column; + gap: 2px; + + h3 { + margin: 0; + font-size: var(--font-size-base); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-snug); + color: var(--color-text-primary); + } + + p { + margin: 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-snug); + color: var(--color-text-secondary); + } } -.locale-list { +// ───────────────────────────────────────────────────────────────────────────── +// Fields +// ───────────────────────────────────────────────────────────────────────────── + +.field { display: flex; flex-direction: column; - border: 1px solid var(--mat-sys-outline-variant); - border-radius: var(--border-radius-sm); - overflow: hidden; + gap: var(--spacing-1); + min-width: 0; +} + +.field-label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-normal); + color: var(--color-text-primary); } -.locale-row { +.text-input { display: flex; align-items: center; - padding: var(--spacing-1) var(--spacing-2) var(--spacing-1) var(--spacing-3); - border-bottom: 1px solid var(--mat-sys-outline-variant); + gap: var(--spacing-2); + height: 40px; + padding: 0 var(--spacing-3); + border: 1px solid var(--color-border-strong); + border-radius: var(--border-radius-md); + background: var(--dialog-well); + color: var(--color-text-primary); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); - &:last-child { - border-bottom: none; + > mat-icon { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); } - .locale-radio { + input { flex: 1; + min-width: 0; + height: 100%; + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; + font-size: var(--font-size-base); + outline: none; + + &::placeholder { + color: var(--color-text-placeholder); + } } - .locale-code { + &:focus-within { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary) 22%, transparent); + } + + &--mono input { font-family: var(--font-family-mono); font-size: var(--font-size-sm); } - .locale-remove-btn { - flex-shrink: 0; - width: var(--spacing-8); - height: var(--spacing-8); - color: var(--mat-sys-on-surface-variant); + &--invalid, + &--invalid:focus-within { + border-color: var(--color-error); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 18%, transparent); + } + + &--locked { + border-style: dashed; + background: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-base); + } +} + +.field-hint, +.field-error { + margin: 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-normal); +} + +.field-hint { + color: var(--color-text-secondary); + + &--icon { + display: flex; + align-items: flex-start; + gap: var(--spacing-1); mat-icon { - /* Material icon size override */ - font-size: 18px; - width: 18px; - height: 18px; + flex: none; + font-size: 14px; + width: 14px; + height: 14px; + margin-top: 2px; + color: var(--color-text-tertiary); } + } + + &--path span { + font-family: var(--font-family-mono); + overflow-wrap: anywhere; + } +} + +.field-error { + color: var(--color-error); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Locale chip editor +// ───────────────────────────────────────────────────────────────────────────── + +.chip-editor { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2); + border: 1px solid var(--color-border-strong); + border-radius: var(--border-radius-md); + background: var(--dialog-well); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + + &:focus-within { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary) 22%, transparent); + } + + &--empty { + border-style: dashed; + } + + &--invalid, + &--invalid:focus-within { + border-color: var(--color-error); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 18%, transparent); + } +} + +.chip-list { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-2); + margin: 0; + padding: 0; + list-style: none; + flex: 0 1 auto; + min-width: 0; +} + +// Same pill as the Collections card, so a locale looks like a locale everywhere. +.locale-chip { + display: inline-flex; + align-items: center; + border-radius: var(--border-radius-full); + background: var(--color-background-muted); + border: 1px solid transparent; + color: var(--color-text-secondary); + transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); + + &--base { + background: color-mix(in srgb, var(--color-secondary) 16%, transparent); + border-color: color-mix(in srgb, var(--color-secondary) 35%, transparent); + color: color-mix(in srgb, var(--color-secondary) 45%, var(--color-text-primary)); + } +} + +.locale-chip-body { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + padding: 3px var(--spacing-2) 3px var(--spacing-3); + border: 0; + border-radius: inherit; + background: transparent; + color: inherit; + font: inherit; + line-height: 1.5; + + &--button { + cursor: pointer; &:hover { - color: var(--mat-sys-error); + color: var(--color-text-primary); + } + + .locale-chip:not(.locale-chip--base) &:hover { + background: color-mix(in srgb, var(--color-secondary) 10%, transparent); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 1px; } } } -.locale-empty-hint { - margin: 0; - padding: var(--spacing-3); +.locale-chip-code { + font-family: var(--font-family-mono); font-size: var(--font-size-sm); - color: var(--mat-sys-on-surface-variant); - font-style: italic; - border: 1px dashed var(--mat-sys-outline-variant); - border-radius: var(--border-radius-sm); - text-align: center; + font-weight: var(--font-weight-semibold); +} + +.locale-chip-tag { + font-size: 0.625rem; + font-weight: var(--font-weight-semibold); + letter-spacing: 0.05em; + text-transform: uppercase; + opacity: 0.9; +} + +.locale-chip-lock { + font-size: 14px; + width: 14px; + height: 14px; +} + +.locale-chip-remove { + display: grid; + place-items: center; + width: 24px; + height: 24px; + margin: 0 3px 0 -4px; + padding: 0; + border: 0; + border-radius: var(--border-radius-full); + background: transparent; + color: var(--color-text-tertiary); + cursor: pointer; + transition: color var(--transition-fast), background var(--transition-fast); + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &:hover { + color: var(--color-error); + background: color-mix(in srgb, var(--color-error) 12%, transparent); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 1px; + } } -.locale-add-row { +.chip-add { + display: flex; + align-items: center; + gap: var(--spacing-1); + flex: 1 1 180px; + min-width: 180px; + min-height: 30px; + padding: 0 var(--spacing-1); + + > mat-icon { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); + } +} + +.chip-add-input { + flex: 1; + min-width: 120px; + height: 30px; + border: 0; + padding: 0 var(--spacing-1); + background: transparent; + color: var(--color-text-primary); + font: inherit; + font-size: var(--font-size-sm); + outline: none; + + &::placeholder { + color: var(--color-text-placeholder); + } +} + +.chip-add-button { + flex: none; + height: 28px; + padding: 0 var(--spacing-3); + border: 1px solid var(--color-border-strong); + border-radius: var(--border-radius-full); + background: var(--color-background); + color: var(--color-text-primary); + font: inherit; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + cursor: pointer; + + &:hover { + border-color: var(--color-secondary); + color: var(--color-secondary-active); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Read-only switch row +// ───────────────────────────────────────────────────────────────────────────── + +.switch-row { display: flex; align-items: flex-start; - gap: var(--spacing-2); + gap: var(--spacing-3); + padding: var(--spacing-3) var(--spacing-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + background: var(--color-background-subtle); + transition: border-color var(--transition-normal), background var(--transition-normal); - .locale-add-field { - flex: 1; + &--on { + border-color: color-mix(in srgb, var(--color-warning) 35%, var(--color-border)); + background: color-mix(in srgb, var(--color-warning) 6%, var(--color-background-subtle)); } +} - .locale-add-btn { - margin-top: var(--spacing-1); +.switch-tile { + display: grid; + place-items: center; + flex: none; + width: 30px; + height: 30px; + border-radius: var(--border-radius-md); + background: color-mix(in srgb, var(--color-warning) 14%, transparent); + color: var(--color-warning-text); + + mat-icon { + font-size: 17px; + width: 17px; + height: 17px; } } -mat-dialog-actions { - padding: var(--spacing-4) var(--spacing-6); +.switch-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.switch-label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-snug); + color: var(--color-text-primary); +} + +.switch-description { + font-size: var(--font-size-xs); + line-height: var(--line-height-snug); + color: var(--color-text-secondary); +} + +.switch-control { + flex: none; + margin-top: 2px; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tags & protected terms disclosure +// ───────────────────────────────────────────────────────────────────────────── + +.disclosure { + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + overflow: hidden; +} + +.disclosure-toggle { + display: flex; + align-items: center; gap: var(--spacing-2); + width: 100%; + padding: var(--spacing-2) var(--spacing-3) var(--spacing-2) var(--spacing-4); + border: 0; + background: transparent; + color: var(--color-text-primary); + font: inherit; + text-align: left; + cursor: pointer; + transition: background var(--transition-fast); + + &:hover { + background: var(--color-background-subtle); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } } -.readonly-section { - margin-top: var(--spacing-4); +.disclosure-icon { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); +} - .readonly-hint { - margin: var(--spacing-1) 0 0 var(--spacing-6); - font-size: var(--font-size-xs, 0.75rem); - color: var(--color-text-secondary); +.disclosure-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + line-height: 1.5; +} + +.disclosure-summary { + margin-left: auto; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + white-space: nowrap; +} + +.disclosure-chevron { + flex: none; + color: var(--color-text-tertiary); + transition: transform var(--transition-normal); + + .disclosure--open & { + transform: rotate(180deg); } } -.protected-terms-file { - margin: -8px 0 12px; - font-size: 12px; - opacity: 0.7; - word-break: break-all; +.disclosure-panel { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + padding: var(--spacing-2) var(--spacing-4) var(--spacing-4); + border-top: 1px solid var(--color-border-subtle); + animation: disclosure-reveal var(--transition-normal) both; + + .field-label { + font-size: var(--font-size-sm); + } +} + +@keyframes disclosure-reveal { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: none; + } +} + +// Tags and protected terms are words, not codes: the same pill, set in Nunito +// on the subtle tone, so the disclosure speaks the dialog's one chip language. +.text-chip { + background: var(--color-background-subtle); + border-color: var(--color-border); + color: var(--color-text-primary); + + .locale-chip-body { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Footer +// ───────────────────────────────────────────────────────────────────────────── + +.dialog-footer { + flex-shrink: 0; + padding: var(--spacing-4) var(--spacing-6) var(--spacing-5); + border-top: 1px solid var(--color-border); + gap: var(--spacing-2); +} + +@media (prefers-reduced-motion: reduce) { + .disclosure-panel { + animation: none; + } + + .disclosure-chevron { + transition: none; + } } diff --git a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.spec.ts b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.spec.ts index efdf85cf..5767f165 100644 --- a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.spec.ts +++ b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.spec.ts @@ -130,6 +130,49 @@ describe('CollectionFormDialog — create mode', () => { expect(component.form.controls.baseLocale.value).toBe(''); }); + it('should let a clicked locale chip become the base', () => { + component.addLocaleInput.setValue('en'); + component.addLocale(); + component.addLocaleInput.setValue('fr-ca'); + component.addLocale(); + + component.setBaseLocale('fr-ca'); + + expect(component.form.controls.baseLocale.value).toBe('fr-ca'); + expect(component.isBaseLocale('fr-ca')).toBe(true); + expect(component.isBaseLocale('en')).toBe(false); + }); + + it('should ignore a base locale that is not in the list', () => { + component.addLocaleInput.setValue('en'); + component.addLocale(); + + component.setBaseLocale('de'); + + expect(component.form.controls.baseLocale.value).toBe('en'); + }); + + it('should add a pending locale when the input loses focus', () => { + component.addLocaleInput.setValue('es'); + component.addLocaleIfPending(); + + expect(component.form.controls.locales.at(0).value).toBe('es'); + }); + + it('should keep the tags and protected terms disclosure closed when there is nothing in it', () => { + expect(component.advancedOpen()).toBe(false); + component.toggleAdvanced(); + expect(component.advancedOpen()).toBe(true); + }); + + it('should mark required fields touched instead of closing when submitted empty', async () => { + await component.onSubmit(); + + expect(component.form.controls.name.touched).toBe(true); + expect(component.showNameError).toBe(true); + expect(component.showFolderError).toBe(true); + }); + it('should not close dialog when form is invalid', async () => { await component.onSubmit(); expect(mockDialogRef.close).not.toHaveBeenCalled(); @@ -174,6 +217,29 @@ describe('CollectionFormDialog — create mode', () => { expect(component.protectedTermsList()).toEqual(['iPhone', 'Node.js']); }); + it('should commit a typed tag on Enter or comma and clear the input', () => { + const input = { value: ' Design System ' } as HTMLInputElement; + const enter = { key: 'Enter', preventDefault: vi.fn() } as unknown as KeyboardEvent; + + component.onChipInputKeydown(enter, input, 'tag'); + + expect(enter.preventDefault).toHaveBeenCalled(); + expect(component.tagsList()).toEqual(['design-system']); + expect(input.value).toBe(''); + + const other = { key: 'a', preventDefault: vi.fn() } as unknown as KeyboardEvent; + component.onChipInputKeydown(other, { value: 'x' } as HTMLInputElement, 'tag'); + expect(component.tagsList()).toEqual(['design-system']); + }); + + it('should commit a pending protected term when its input blurs', () => { + const input = { value: 'iPhone' } as HTMLInputElement; + component.commitChipInput(input, 'term'); + + expect(component.protectedTermsList()).toEqual(['iPhone']); + expect(input.value).toBe(''); + }); + it('should remove a protected term', () => { component.addProtectedTerm({ value: 'iPhone', chipInput: { clear: () => undefined } } as never); component.addProtectedTerm({ value: 'C++', chipInput: { clear: () => undefined } } as never); @@ -312,4 +378,49 @@ describe('CollectionFormDialog — edit mode', () => { expect(component.form.controls.locales.length).toBe(3); expect(component.form.controls.locales.at(0).value).toBe('en'); }); + + it('should not offer a remove control for the base locale in edit mode', () => { + expect(component.canRemoveLocale(0)).toBe(false); + expect(component.canRemoveLocale(1)).toBe(true); + }); + + it('should not let the base locale change in edit mode', () => { + component.setBaseLocale('es'); + expect(component.form.controls.baseLocale.value).toBe('en'); + }); + + it('should open the disclosure when protected terms already exist', () => { + expect(component.advancedOpen()).toBe(true); + }); +}); + +describe('CollectionFormDialog — edit mode with inherited base locale', () => { + let component: CollectionFormDialog; + let mockDialogRef: { close: ReturnType }; + + beforeEach(async () => { + TestBed.resetTestingModule(); + const built = await buildTestBed({ + mode: 'edit', + name: 'inherits-base', + config: { translationsFolder: './i18n', locales: ['en', 'de'] }, + effectiveBaseLocale: 'en', + }); + component = built.fixture.componentInstance; + mockDialogRef = built.mockDialogRef; + }); + + it('should mark and lock the inherited base locale', () => { + expect(component.displayedBaseLocale).toBe('en'); + expect(component.isBaseLocale('en')).toBe(true); + expect(component.canRemoveLocale(0)).toBe(false); + expect(component.canRemoveLocale(1)).toBe(true); + }); + + it('should not write the inherited base locale into the collection on save', async () => { + await component.onSubmit(); + + const closeArg = mockDialogRef.close.mock.calls[0][0]; + expect(closeArg.config).not.toHaveProperty('baseLocale'); + }); }); diff --git a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.ts b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.ts index c51c6043..af3e648e 100644 --- a/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.ts +++ b/apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.ts @@ -3,15 +3,11 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; -import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; -import { MatRadioModule } from '@angular/material/radio'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatChipsModule, type MatChipInputEvent } from '@angular/material/chips'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import type { MatChipInputEvent } from '@angular/material/chips'; import { TranslocoModule, TranslocoService } from '@jsverse/transloco'; import { firstValueFrom } from 'rxjs'; import { isUnderNodeModules, normalizeProtectedTerms, normalizeTag, validateLocale } from '@simoncodes-ca/domain'; @@ -34,14 +30,10 @@ export interface CollectionFormResult { CommonModule, ReactiveFormsModule, MatDialogModule, - MatFormFieldModule, - MatInputModule, MatButtonModule, - MatCheckboxModule, MatIconModule, - MatRadioModule, + MatSlideToggleModule, MatTooltipModule, - MatChipsModule, TranslocoModule, ], templateUrl: './collection-form-dialog.html', @@ -71,15 +63,21 @@ export class CollectionFormDialog implements OnInit { }); readonly addLocaleInput = new FormControl('', { nonNullable: true }); - readonly tagSeparatorKeyCodes = [ENTER, COMMA] as const; + /** The config file name, wrapped in our own `` so hints can set it in mono inside translated prose. */ + readonly configFileMarkup = '.lingo-tracker.json'; readonly tagsList = signal([]); readonly protectedTermsList = signal([]); /** The collection's `protectedTermsFile` pointer, preserved across an edit but not editable here. */ readonly protectedTermsFile = signal(undefined); /** Resolved path of that file, shown read-only so the source of a diff is obvious. */ readonly protectedTermsFilePath = signal(undefined); - /** Terms live in a file, so without a pointer there is nowhere to save them — the chips stay disabled. */ + /** Terms live in a file, so without a pointer there is nowhere to save them — the editor stays hidden. */ readonly canEditProtectedTerms = computed(() => this.protectedTermsFile() !== undefined); + /** + * Tags and protected terms are the rarely-touched part of a collection, so they sit behind a + * disclosure. It opens by itself when there is already something in it to look at. + */ + readonly advancedOpen = signal(false); #originalLocales: string[] = []; /** Tracks whether the user manually toggled read-only, so auto-detection stops overriding it. */ @@ -95,6 +93,29 @@ export class CollectionFormDialog implements OnInit { : TRACKER_TOKENS.COLLECTIONS.DIALOG.CREATE.TITLE; } + /** Whether the entered folder is under node_modules (drives the read-only hint). */ + get isNodeModulesPath(): boolean { + return isUnderNodeModules(this.form.controls.translationsFolder.value); + } + + get showNameError(): boolean { + const control = this.form.controls.name; + return control.hasError('required') && control.touched; + } + + get showFolderError(): boolean { + const control = this.form.controls.translationsFolder; + return control.hasError('required') && control.touched; + } + + /** The one line under the locale chips: what clicking does, what is locked, or what empty means. */ + get localesHintToken(): string { + if (this.isEditMode) return TRACKER_TOKENS.COLLECTIONS.DIALOG.BASEHINTEDIT; + return this.form.controls.locales.length > 0 + ? TRACKER_TOKENS.COLLECTIONS.DIALOG.BASEHINTCREATE + : TRACKER_TOKENS.COLLECTIONS.DIALOG.LOCALESINHERITHINT; + } + ngOnInit(): void { if (this.isEditMode && this.#data.config) { const configLocales = this.#data.config.locales ?? []; @@ -111,6 +132,7 @@ export class CollectionFormDialog implements OnInit { this.protectedTermsList.set(this.#data.config.protectedTerms ?? []); this.protectedTermsFile.set(this.#data.config.protectedTermsFile); this.protectedTermsFilePath.set(this.#data.config.protectedTermsFilePath); + this.advancedOpen.set(this.tagsList().length > 0 || this.protectedTermsList().length > 0); // An existing read-only flag is the user's prior choice — don't let auto-detection override it. this.#readOnlyTouchedByUser = this.#data.config.readOnly !== undefined; @@ -136,9 +158,35 @@ export class CollectionFormDialog implements OnInit { this.#readOnlyTouchedByUser = true; } - /** Whether the entered folder is under node_modules (drives the read-only hint). */ - get isNodeModulesPath(): boolean { - return isUnderNodeModules(this.form.controls.translationsFolder.value); + toggleAdvanced(): void { + this.advancedOpen.update((open) => !open); + } + + /** + * The locale shown as BASE. In edit mode a collection without its own `baseLocale` inherits the + * global one, so the inherited value is what gets marked and locked; only an explicit choice + * is ever written back. + */ + get displayedBaseLocale(): string { + const own = this.form.controls.baseLocale.value; + if (own) return own; + return this.isEditMode ? (this.#data.effectiveBaseLocale ?? '') : ''; + } + + isBaseLocale(locale: string): boolean { + return this.displayedBaseLocale === locale; + } + + /** The base locale is a create-time decision; after that it anchors every checksum and is locked. */ + setBaseLocale(locale: string): void { + if (this.isEditMode) return; + if (!this.form.controls.locales.getRawValue().includes(locale)) return; + this.form.controls.baseLocale.setValue(locale); + } + + canRemoveLocale(index: number): boolean { + const locale = this.form.controls.locales.at(index)?.value; + return !(this.isEditMode && locale === this.displayedBaseLocale); } addLocale(): void { @@ -170,11 +218,42 @@ export class CollectionFormDialog implements OnInit { this.addLocaleInput.setValue(''); } - addCollectionTag(event: MatChipInputEvent): void { - const normalized = normalizeTag(event.value); + /** Leaving the input with a locale typed but not confirmed should not silently drop it. */ + addLocaleIfPending(): void { + if (this.addLocaleInput.value.trim()) { + this.addLocale(); + } + } + + /** Enter or comma commits the typed tag or term, like the Material chip input it replaces. */ + onChipInputKeydown(event: KeyboardEvent, input: HTMLInputElement, kind: 'tag' | 'term'): void { + if (event.key !== 'Enter' && event.key !== ',') return; + event.preventDefault(); + this.commitChipInput(input, kind); + } + + commitChipInput(input: HTMLInputElement, kind: 'tag' | 'term'): void { + if (!input.value.trim()) { + input.value = ''; + return; + } + if (kind === 'tag') { + this.addTagValue(input.value); + } else { + this.addProtectedTermValue(input.value); + } + input.value = ''; + } + + addTagValue(value: string): void { + const normalized = normalizeTag(value); if (normalized && !this.tagsList().includes(normalized)) { this.tagsList.update((tags) => [...tags, normalized]); } + } + + addCollectionTag(event: MatChipInputEvent): void { + this.addTagValue(event.value); event.chipInput?.clear(); } @@ -186,11 +265,15 @@ export class CollectionFormDialog implements OnInit { * Adds a protected term, trimming and deduping case-sensitively while preserving * the entered casing and punctuation (`iPhone`, `Node.js`, `C++` stay verbatim). */ - addProtectedTerm(event: MatChipInputEvent): void { - const [term] = normalizeProtectedTerms([event.value]); + addProtectedTermValue(value: string): void { + const [term] = normalizeProtectedTerms([value]); if (term && !this.protectedTermsList().includes(term)) { this.protectedTermsList.update((terms) => [...terms, term]); } + } + + addProtectedTerm(event: MatChipInputEvent): void { + this.addProtectedTermValue(event.value); event.chipInput?.clear(); } @@ -199,7 +282,7 @@ export class CollectionFormDialog implements OnInit { } removeLocale(index: number): void { - if (this.isEditMode && this.form.controls.locales.at(index).value === this.form.controls.baseLocale.value) { + if (!this.canRemoveLocale(index)) { return; } @@ -217,7 +300,10 @@ export class CollectionFormDialog implements OnInit { } async onSubmit(): Promise { - if (this.form.invalid) return; + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } if (this.isEditMode) { const localesArray = this.form.controls.locales.getRawValue(); diff --git a/apps/tracker/src/app/collections/collections-manager.ts b/apps/tracker/src/app/collections/collections-manager.ts index f57e0d07..cdb9f982 100644 --- a/apps/tracker/src/app/collections/collections-manager.ts +++ b/apps/tracker/src/app/collections/collections-manager.ts @@ -127,7 +127,9 @@ export class CollectionsManager { import('./collection-form-dialog/collection-form-dialog').then((m) => { const dialogRef = this.#dialog.open(m.CollectionFormDialog, { data: { mode: 'create' }, - width: '500px', + panelClass: 'collection-form-dialog-panel', + // Land on the first field, not the close button. + autoFocus: 'input', }); dialogRef.afterClosed().subscribe((result) => { @@ -152,14 +154,21 @@ export class CollectionsManager { return; } + const effectiveBaseLocale = this.store + .collectionEntriesWithLocales() + .find((item) => item.name === name)?.baseLocale; + import('./collection-form-dialog/collection-form-dialog').then((m) => { const dialogRef = this.#dialog.open(m.CollectionFormDialog, { data: { mode: 'edit', name, config, + effectiveBaseLocale, }, - width: '500px', + panelClass: 'collection-form-dialog-panel', + // Land on the first field, not the close button. + autoFocus: 'input', }); dialogRef.afterClosed().subscribe((result) => { diff --git a/apps/tracker/src/i18n/collections/dialog/resource_entries.json b/apps/tracker/src/i18n/collections/dialog/resource_entries.json index 998f5ffa..8ce60828 100644 --- a/apps/tracker/src/i18n/collections/dialog/resource_entries.json +++ b/apps/tracker/src/i18n/collections/dialog/resource_entries.json @@ -287,5 +287,175 @@ "ru": "Для этой коллекции не настроен файл защищённых терминов. Настройте его через CLI, чтобы редактировать термины здесь.", "ja": "このコレクションには保護用語ファイルが設定されていません。ここで用語を編集するには、CLI で設定してください。", "de": "Für diese Sammlung ist keine Datei mit geschützten Begriffen konfiguriert. Richten Sie eine über die CLI ein, um Begriffe hier zu bearbeiten." + }, + "createSubtitle": { + "source": "Point LingoTracker at a folder of translation files and choose the locales it tracks.", + "comment": "One-sentence purpose line under the Create Collection dialog title", + "tags": ["collections"], + "es": "Point LingoTracker at a folder of translation files and choose the locales it tracks.", + "fr-ca": "Point LingoTracker at a folder of translation files and choose the locales it tracks.", + "ru": "Point LingoTracker at a folder of translation files and choose the locales it tracks.", + "ja": "Point LingoTracker at a folder of translation files and choose the locales it tracks.", + "de": "Point LingoTracker at a folder of translation files and choose the locales it tracks." + }, + "sourceTitle": { + "source": "Source", + "comment": "Heading of the dialog group holding the collection name and translations folder", + "tags": ["collections"], + "es": "Source", + "fr-ca": "Source", + "ru": "Source", + "ja": "Source", + "de": "Source" + }, + "sourceHint": { + "source": "Where the translation files live.", + "comment": "Description under the Source group heading", + "tags": ["collections"], + "es": "Where the translation files live.", + "fr-ca": "Where the translation files live.", + "ru": "Where the translation files live.", + "ja": "Where the translation files live.", + "de": "Where the translation files live." + }, + "nameLocked": { + "source": "Names can't be changed after creation.", + "comment": "Hint under the collection name in edit mode, where the name is shown locked", + "tags": ["collections"], + "es": "Names can't be changed after creation.", + "fr-ca": "Names can't be changed after creation.", + "ru": "Names can't be changed after creation.", + "ja": "Names can't be changed after creation.", + "de": "Names can't be changed after creation." + }, + "localesDescription": { + "source": "Every locale this collection ships. Translations are compared against the base locale.", + "comment": "Description under the Locales group heading", + "tags": ["collections"], + "es": "Every locale this collection ships. Translations are compared against the base locale.", + "fr-ca": "Every locale this collection ships. Translations are compared against the base locale.", + "ru": "Every locale this collection ships. Translations are compared against the base locale.", + "ja": "Every locale this collection ships. Translations are compared against the base locale.", + "de": "Every locale this collection ships. Translations are compared against the base locale." + }, + "addLocalePlaceholder": { + "source": "Add locale, e.g. de or pt-br", + "comment": "Placeholder of the inline input that adds a locale chip. de and pt-br are locale codes", + "tags": ["collections"], + "es": "Add locale, e.g. de or pt-br", + "fr-ca": "Add locale, e.g. de or pt-br", + "ru": "Add locale, e.g. de or pt-br", + "ja": "Add locale, e.g. de or pt-br", + "de": "Add locale, e.g. de or pt-br" + }, + "baseHintCreate": { + "source": "Click a locale to make it the base.", + "comment": "Hint under the locale chips while creating a collection", + "tags": ["collections"], + "es": "Click a locale to make it the base.", + "fr-ca": "Click a locale to make it the base.", + "ru": "Click a locale to make it the base.", + "ja": "Click a locale to make it the base.", + "de": "Click a locale to make it the base." + }, + "baseHintEdit": { + "source": "The base locale is fixed after creation. Removing a locale deletes its translations.", + "comment": "Hint under the locale chips while editing a collection", + "tags": ["collections"], + "es": "The base locale is fixed after creation. Removing a locale deletes its translations.", + "fr-ca": "The base locale is fixed after creation. Removing a locale deletes its translations.", + "ru": "The base locale is fixed after creation. Removing a locale deletes its translations.", + "ja": "The base locale is fixed after creation. Removing a locale deletes its translations.", + "de": "The base locale is fixed after creation. Removing a locale deletes its translations." + }, + "localesInheritHint": { + "source": "Leave empty to use the locales from your global config.", + "comment": "Hint under the locale chips when no locale has been added yet", + "tags": ["collections"], + "es": "Leave empty to use the locales from your global config.", + "fr-ca": "Leave empty to use the locales from your global config.", + "ru": "Leave empty to use the locales from your global config.", + "ja": "Leave empty to use the locales from your global config.", + "de": "Leave empty to use the locales from your global config." + }, + "makeBaseAriaX": { + "source": "Make {locale} the base locale", + "comment": "Screen-reader label of a locale chip button; clicking it sets that locale as the base", + "tags": ["collections"], + "es": "Make {locale} the base locale", + "fr-ca": "Make {locale} the base locale", + "ru": "Make {locale} the base locale", + "ja": "Make {locale} the base locale", + "de": "Make {locale} the base locale" + }, + "optionsTitle": { + "source": "Options", + "comment": "Heading of the dialog group holding read-only and metadata settings", + "tags": ["collections"], + "es": "Options", + "fr-ca": "Options", + "ru": "Options", + "ja": "Options", + "de": "Options" + }, + "advancedTitle": { + "source": "Tags & protected terms", + "comment": "Label of the collapsible row that reveals the tags and protected terms editors", + "tags": ["collections"], + "es": "Tags & protected terms", + "fr-ca": "Tags & protected terms", + "ru": "Tags & protected terms", + "ja": "Tags & protected terms", + "de": "Tags & protected terms" + }, + "advancedEmpty": { + "source": "None yet", + "comment": "Summary shown on the collapsed Tags & protected terms row when both are empty", + "tags": ["collections"], + "es": "None yet", + "fr-ca": "None yet", + "ru": "None yet", + "ja": "None yet", + "de": "None yet" + }, + "tagsCountX": { + "source": "{ count, plural, =1 {1 tag} other {{count} tags} }", + "comment": "Summary on the collapsed row: number of collection tags (ICU plural)", + "tags": ["collections"], + "es": "{ count, plural, =1 {1 tag} other {{count} tags} }", + "fr-ca": "{ count, plural, =1 {1 tag} other {{count} tags} }", + "ru": "{ count, plural, =1 {1 tag} other {{count} tags} }", + "ja": "{ count, plural, =1 {1 tag} other {{count} tags} }", + "de": "{ count, plural, =1 {1 tag} other {{count} tags} }" + }, + "termsCountX": { + "source": "{ count, plural, =1 {1 term} other {{count} terms} }", + "comment": "Summary on the collapsed row: number of protected terms (ICU plural)", + "tags": ["collections"], + "es": "{ count, plural, =1 {1 term} other {{count} terms} }", + "fr-ca": "{ count, plural, =1 {1 term} other {{count} terms} }", + "ru": "{ count, plural, =1 {1 term} other {{count} terms} }", + "ja": "{ count, plural, =1 {1 term} other {{count} terms} }", + "de": "{ count, plural, =1 {1 term} other {{count} terms} }" + }, + "editSubtitleX": { + "source": "Changes are saved to {file} in your working tree.", + "comment": "One-sentence line under the Edit Collection dialog title. {{ file }} is the config file name (.lingo-tracker.json), inserted by the app", + "tags": ["collections"], + "es": "Changes are saved to {file} in your working tree.", + "fr-ca": "Changes are saved to {file} in your working tree.", + "ru": "Changes are saved to {file} in your working tree.", + "ja": "Changes are saved to {file} in your working tree.", + "de": "Changes are saved to {file} in your working tree." + }, + "folderHintX": { + "source": "Relative to the project root, next to {file}.", + "comment": "Hint under the translations folder field. {{ file }} is the config file name (.lingo-tracker.json), inserted by the app", + "tags": ["collections"], + "es": "Relative to the project root, next to {file}.", + "fr-ca": "Relative to the project root, next to {file}.", + "ru": "Relative to the project root, next to {file}.", + "ja": "Relative to the project root, next to {file}.", + "de": "Relative to the project root, next to {file}." } } diff --git a/apps/tracker/src/i18n/collections/dialog/tracker_meta.json b/apps/tracker/src/i18n/collections/dialog/tracker_meta.json index ba881c2a..cb2b1047 100644 --- a/apps/tracker/src/i18n/collections/dialog/tracker_meta.json +++ b/apps/tracker/src/i18n/collections/dialog/tracker_meta.json @@ -838,5 +838,515 @@ "baseChecksum": "9d1cfe68880b36bde684f91b1481b2dd", "status": "translated" } + }, + "createSubtitle": { + "en": { + "checksum": "1adedf2ff460c36363970f1243294179" + }, + "es": { + "checksum": "1adedf2ff460c36363970f1243294179", + "baseChecksum": "1adedf2ff460c36363970f1243294179", + "status": "new" + }, + "fr-ca": { + "checksum": "1adedf2ff460c36363970f1243294179", + "baseChecksum": "1adedf2ff460c36363970f1243294179", + "status": "new" + }, + "ru": { + "checksum": "1adedf2ff460c36363970f1243294179", + "baseChecksum": "1adedf2ff460c36363970f1243294179", + "status": "new" + }, + "ja": { + "checksum": "1adedf2ff460c36363970f1243294179", + "baseChecksum": "1adedf2ff460c36363970f1243294179", + "status": "new" + }, + "de": { + "checksum": "1adedf2ff460c36363970f1243294179", + "baseChecksum": "1adedf2ff460c36363970f1243294179", + "status": "new" + } + }, + "sourceTitle": { + "en": { + "checksum": "f31bbdd1b3e85bccd652680e16935819" + }, + "es": { + "checksum": "f31bbdd1b3e85bccd652680e16935819", + "baseChecksum": "f31bbdd1b3e85bccd652680e16935819", + "status": "new" + }, + "fr-ca": { + "checksum": "f31bbdd1b3e85bccd652680e16935819", + "baseChecksum": "f31bbdd1b3e85bccd652680e16935819", + "status": "new" + }, + "ru": { + "checksum": "f31bbdd1b3e85bccd652680e16935819", + "baseChecksum": "f31bbdd1b3e85bccd652680e16935819", + "status": "new" + }, + "ja": { + "checksum": "f31bbdd1b3e85bccd652680e16935819", + "baseChecksum": "f31bbdd1b3e85bccd652680e16935819", + "status": "new" + }, + "de": { + "checksum": "f31bbdd1b3e85bccd652680e16935819", + "baseChecksum": "f31bbdd1b3e85bccd652680e16935819", + "status": "new" + } + }, + "sourceHint": { + "en": { + "checksum": "41f38c6bc495c5146b41fa46a9ac30ae" + }, + "es": { + "checksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "baseChecksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "status": "new" + }, + "fr-ca": { + "checksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "baseChecksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "status": "new" + }, + "ru": { + "checksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "baseChecksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "status": "new" + }, + "ja": { + "checksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "baseChecksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "status": "new" + }, + "de": { + "checksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "baseChecksum": "41f38c6bc495c5146b41fa46a9ac30ae", + "status": "new" + } + }, + "nameLocked": { + "en": { + "checksum": "06fd70b4587f37a9637a82457e49e78e" + }, + "es": { + "checksum": "06fd70b4587f37a9637a82457e49e78e", + "baseChecksum": "06fd70b4587f37a9637a82457e49e78e", + "status": "new" + }, + "fr-ca": { + "checksum": "06fd70b4587f37a9637a82457e49e78e", + "baseChecksum": "06fd70b4587f37a9637a82457e49e78e", + "status": "new" + }, + "ru": { + "checksum": "06fd70b4587f37a9637a82457e49e78e", + "baseChecksum": "06fd70b4587f37a9637a82457e49e78e", + "status": "new" + }, + "ja": { + "checksum": "06fd70b4587f37a9637a82457e49e78e", + "baseChecksum": "06fd70b4587f37a9637a82457e49e78e", + "status": "new" + }, + "de": { + "checksum": "06fd70b4587f37a9637a82457e49e78e", + "baseChecksum": "06fd70b4587f37a9637a82457e49e78e", + "status": "new" + } + }, + "localesDescription": { + "en": { + "checksum": "a2074ef985fe9f48613e3d138984714a" + }, + "es": { + "checksum": "a2074ef985fe9f48613e3d138984714a", + "baseChecksum": "a2074ef985fe9f48613e3d138984714a", + "status": "new" + }, + "fr-ca": { + "checksum": "a2074ef985fe9f48613e3d138984714a", + "baseChecksum": "a2074ef985fe9f48613e3d138984714a", + "status": "new" + }, + "ru": { + "checksum": "a2074ef985fe9f48613e3d138984714a", + "baseChecksum": "a2074ef985fe9f48613e3d138984714a", + "status": "new" + }, + "ja": { + "checksum": "a2074ef985fe9f48613e3d138984714a", + "baseChecksum": "a2074ef985fe9f48613e3d138984714a", + "status": "new" + }, + "de": { + "checksum": "a2074ef985fe9f48613e3d138984714a", + "baseChecksum": "a2074ef985fe9f48613e3d138984714a", + "status": "new" + } + }, + "addLocalePlaceholder": { + "en": { + "checksum": "014a9ed46585c3f71dc9ffb7c0970c37" + }, + "es": { + "checksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "baseChecksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "status": "new" + }, + "fr-ca": { + "checksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "baseChecksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "status": "new" + }, + "ru": { + "checksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "baseChecksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "status": "new" + }, + "ja": { + "checksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "baseChecksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "status": "new" + }, + "de": { + "checksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "baseChecksum": "014a9ed46585c3f71dc9ffb7c0970c37", + "status": "new" + } + }, + "baseHintCreate": { + "en": { + "checksum": "2650085fa76eefa5142bb4a89c2e055d" + }, + "es": { + "checksum": "2650085fa76eefa5142bb4a89c2e055d", + "baseChecksum": "2650085fa76eefa5142bb4a89c2e055d", + "status": "new" + }, + "fr-ca": { + "checksum": "2650085fa76eefa5142bb4a89c2e055d", + "baseChecksum": "2650085fa76eefa5142bb4a89c2e055d", + "status": "new" + }, + "ru": { + "checksum": "2650085fa76eefa5142bb4a89c2e055d", + "baseChecksum": "2650085fa76eefa5142bb4a89c2e055d", + "status": "new" + }, + "ja": { + "checksum": "2650085fa76eefa5142bb4a89c2e055d", + "baseChecksum": "2650085fa76eefa5142bb4a89c2e055d", + "status": "new" + }, + "de": { + "checksum": "2650085fa76eefa5142bb4a89c2e055d", + "baseChecksum": "2650085fa76eefa5142bb4a89c2e055d", + "status": "new" + } + }, + "baseHintEdit": { + "en": { + "checksum": "c2969426433b943d6263980dc9ef0d0e" + }, + "es": { + "checksum": "c2969426433b943d6263980dc9ef0d0e", + "baseChecksum": "c2969426433b943d6263980dc9ef0d0e", + "status": "new" + }, + "fr-ca": { + "checksum": "c2969426433b943d6263980dc9ef0d0e", + "baseChecksum": "c2969426433b943d6263980dc9ef0d0e", + "status": "new" + }, + "ru": { + "checksum": "c2969426433b943d6263980dc9ef0d0e", + "baseChecksum": "c2969426433b943d6263980dc9ef0d0e", + "status": "new" + }, + "ja": { + "checksum": "c2969426433b943d6263980dc9ef0d0e", + "baseChecksum": "c2969426433b943d6263980dc9ef0d0e", + "status": "new" + }, + "de": { + "checksum": "c2969426433b943d6263980dc9ef0d0e", + "baseChecksum": "c2969426433b943d6263980dc9ef0d0e", + "status": "new" + } + }, + "localesInheritHint": { + "en": { + "checksum": "65370462f983426720c428ae6241e29f" + }, + "es": { + "checksum": "65370462f983426720c428ae6241e29f", + "baseChecksum": "65370462f983426720c428ae6241e29f", + "status": "new" + }, + "fr-ca": { + "checksum": "65370462f983426720c428ae6241e29f", + "baseChecksum": "65370462f983426720c428ae6241e29f", + "status": "new" + }, + "ru": { + "checksum": "65370462f983426720c428ae6241e29f", + "baseChecksum": "65370462f983426720c428ae6241e29f", + "status": "new" + }, + "ja": { + "checksum": "65370462f983426720c428ae6241e29f", + "baseChecksum": "65370462f983426720c428ae6241e29f", + "status": "new" + }, + "de": { + "checksum": "65370462f983426720c428ae6241e29f", + "baseChecksum": "65370462f983426720c428ae6241e29f", + "status": "new" + } + }, + "makeBaseAriaX": { + "en": { + "checksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3" + }, + "es": { + "checksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "baseChecksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "status": "new" + }, + "fr-ca": { + "checksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "baseChecksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "status": "new" + }, + "ru": { + "checksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "baseChecksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "status": "new" + }, + "ja": { + "checksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "baseChecksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "status": "new" + }, + "de": { + "checksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "baseChecksum": "c81ef0dd34dfd16a83ffbc9d03e8a3e3", + "status": "new" + } + }, + "optionsTitle": { + "en": { + "checksum": "dae8ace18bdcbcc6ae5aece263e14fe8" + }, + "es": { + "checksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "baseChecksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "status": "new" + }, + "fr-ca": { + "checksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "baseChecksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "status": "new" + }, + "ru": { + "checksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "baseChecksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "status": "new" + }, + "ja": { + "checksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "baseChecksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "status": "new" + }, + "de": { + "checksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "baseChecksum": "dae8ace18bdcbcc6ae5aece263e14fe8", + "status": "new" + } + }, + "advancedTitle": { + "en": { + "checksum": "2f75bc9f3ab86312796527850876d280" + }, + "es": { + "checksum": "2f75bc9f3ab86312796527850876d280", + "baseChecksum": "2f75bc9f3ab86312796527850876d280", + "status": "new" + }, + "fr-ca": { + "checksum": "2f75bc9f3ab86312796527850876d280", + "baseChecksum": "2f75bc9f3ab86312796527850876d280", + "status": "new" + }, + "ru": { + "checksum": "2f75bc9f3ab86312796527850876d280", + "baseChecksum": "2f75bc9f3ab86312796527850876d280", + "status": "new" + }, + "ja": { + "checksum": "2f75bc9f3ab86312796527850876d280", + "baseChecksum": "2f75bc9f3ab86312796527850876d280", + "status": "new" + }, + "de": { + "checksum": "2f75bc9f3ab86312796527850876d280", + "baseChecksum": "2f75bc9f3ab86312796527850876d280", + "status": "new" + } + }, + "advancedEmpty": { + "en": { + "checksum": "4e6d327262eb2924ef4f7abdbb4be4fd" + }, + "es": { + "checksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "baseChecksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "status": "new" + }, + "fr-ca": { + "checksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "baseChecksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "status": "new" + }, + "ru": { + "checksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "baseChecksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "status": "new" + }, + "ja": { + "checksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "baseChecksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "status": "new" + }, + "de": { + "checksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "baseChecksum": "4e6d327262eb2924ef4f7abdbb4be4fd", + "status": "new" + } + }, + "tagsCountX": { + "en": { + "checksum": "e6b166e9e835a44d7c198328c254f2cf" + }, + "es": { + "checksum": "e6b166e9e835a44d7c198328c254f2cf", + "baseChecksum": "e6b166e9e835a44d7c198328c254f2cf", + "status": "new" + }, + "fr-ca": { + "checksum": "e6b166e9e835a44d7c198328c254f2cf", + "baseChecksum": "e6b166e9e835a44d7c198328c254f2cf", + "status": "new" + }, + "ru": { + "checksum": "e6b166e9e835a44d7c198328c254f2cf", + "baseChecksum": "e6b166e9e835a44d7c198328c254f2cf", + "status": "new" + }, + "ja": { + "checksum": "e6b166e9e835a44d7c198328c254f2cf", + "baseChecksum": "e6b166e9e835a44d7c198328c254f2cf", + "status": "new" + }, + "de": { + "checksum": "e6b166e9e835a44d7c198328c254f2cf", + "baseChecksum": "e6b166e9e835a44d7c198328c254f2cf", + "status": "new" + } + }, + "termsCountX": { + "en": { + "checksum": "58391e8881cb8c17e06ecea765b702c7" + }, + "es": { + "checksum": "58391e8881cb8c17e06ecea765b702c7", + "baseChecksum": "58391e8881cb8c17e06ecea765b702c7", + "status": "new" + }, + "fr-ca": { + "checksum": "58391e8881cb8c17e06ecea765b702c7", + "baseChecksum": "58391e8881cb8c17e06ecea765b702c7", + "status": "new" + }, + "ru": { + "checksum": "58391e8881cb8c17e06ecea765b702c7", + "baseChecksum": "58391e8881cb8c17e06ecea765b702c7", + "status": "new" + }, + "ja": { + "checksum": "58391e8881cb8c17e06ecea765b702c7", + "baseChecksum": "58391e8881cb8c17e06ecea765b702c7", + "status": "new" + }, + "de": { + "checksum": "58391e8881cb8c17e06ecea765b702c7", + "baseChecksum": "58391e8881cb8c17e06ecea765b702c7", + "status": "new" + } + }, + "editSubtitleX": { + "en": { + "checksum": "6584587757f7f1a782c1ca82c2669926" + }, + "es": { + "checksum": "6584587757f7f1a782c1ca82c2669926", + "baseChecksum": "6584587757f7f1a782c1ca82c2669926", + "status": "new" + }, + "fr-ca": { + "checksum": "6584587757f7f1a782c1ca82c2669926", + "baseChecksum": "6584587757f7f1a782c1ca82c2669926", + "status": "new" + }, + "ru": { + "checksum": "6584587757f7f1a782c1ca82c2669926", + "baseChecksum": "6584587757f7f1a782c1ca82c2669926", + "status": "new" + }, + "ja": { + "checksum": "6584587757f7f1a782c1ca82c2669926", + "baseChecksum": "6584587757f7f1a782c1ca82c2669926", + "status": "new" + }, + "de": { + "checksum": "6584587757f7f1a782c1ca82c2669926", + "baseChecksum": "6584587757f7f1a782c1ca82c2669926", + "status": "new" + } + }, + "folderHintX": { + "en": { + "checksum": "579ea299de2a24f63342ecd2d17364eb" + }, + "es": { + "checksum": "579ea299de2a24f63342ecd2d17364eb", + "baseChecksum": "579ea299de2a24f63342ecd2d17364eb", + "status": "new" + }, + "fr-ca": { + "checksum": "579ea299de2a24f63342ecd2d17364eb", + "baseChecksum": "579ea299de2a24f63342ecd2d17364eb", + "status": "new" + }, + "ru": { + "checksum": "579ea299de2a24f63342ecd2d17364eb", + "baseChecksum": "579ea299de2a24f63342ecd2d17364eb", + "status": "new" + }, + "ja": { + "checksum": "579ea299de2a24f63342ecd2d17364eb", + "baseChecksum": "579ea299de2a24f63342ecd2d17364eb", + "status": "new" + }, + "de": { + "checksum": "579ea299de2a24f63342ecd2d17364eb", + "baseChecksum": "579ea299de2a24f63342ecd2d17364eb", + "status": "new" + } } } diff --git a/apps/tracker/src/styles.scss b/apps/tracker/src/styles.scss index 744396c7..62ebf2ac 100644 --- a/apps/tracker/src/styles.scss +++ b/apps/tracker/src/styles.scss @@ -125,6 +125,36 @@ mark.search-highlight { } } +// Collection create/edit dialog. Same top rule as the translation editor so the +// two dialogs read as one family; the panel class carries the width because the +// CDK puts it on the overlay pane, outside the component. +.mat-mdc-dialog-container:has(app-collection-form-dialog) { + padding: 0; + border-top: 3px solid var(--color-primary); + box-shadow: var(--shadow-lg); +} + +// Doubled with the pane class so this outranks Material's own `max-height: 100%` +// on the pane, which has no definite height to resolve against and let the +// dialog grow past the viewport. Every layer down to the component inherits the +// viewport-based ceiling, so the body inside is the only thing that scrolls. +.cdk-overlay-pane.collection-form-dialog-panel { + width: 540px; + max-width: min(540px, calc(100vw - 2 * var(--spacing-6))); + max-height: min(90vh, calc(100dvh - 2 * var(--spacing-8))); + + .mat-mdc-dialog-container, + .mat-mdc-dialog-inner-container, + .mat-mdc-dialog-surface, + app-collection-form-dialog { + max-height: inherit; + } + + app-collection-form-dialog { + display: block; + } +} + // Make overlay backdrops slightly darker .cdk-overlay-backdrop, .cdk-overlay-dark-backdrop { From f7e94560d335e6a8383c6912427d67c9642e32fb Mon Sep 17 00:00:00 2001 From: snodel Date: Mon, 14 Sep 2026 14:17:48 -0700 Subject: [PATCH 04/13] docs(tracker): record the design system and collection dialog brief Add the first token-bearing DESIGN.md with its design.json sidecar, derived from the shipped tokens and components, plus the surface brief holding the collection dialog's direction contract. Co-Authored-By: Claude Fable 5.1 --- apps/tracker/.impeccable/design.json | 622 ++++++++++++++++++ ...og-collection-form-dialog-html-71ddd2b8.md | 32 + apps/tracker/DESIGN.md | 368 +++++++++++ 3 files changed, 1022 insertions(+) create mode 100644 apps/tracker/.impeccable/design.json create mode 100644 apps/tracker/.impeccable/surfaces/n-form-dialog-collection-form-dialog-html-71ddd2b8.md create mode 100644 apps/tracker/DESIGN.md diff --git a/apps/tracker/.impeccable/design.json b/apps/tracker/.impeccable/design.json new file mode 100644 index 00000000..341eaca0 --- /dev/null +++ b/apps/tracker/.impeccable/design.json @@ -0,0 +1,622 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-09-14T00:00:00Z", + "title": "Design System: LingoTracker", + "extensions": { + "colorMeta": { + "primary": { + "role": "primary", + "displayName": "Coral / Vermillion", + "canonical": "#d4726a", + "dark": "#e8847c", + "tonalRamp": ["#b04a42", "#c25d55", "#d4726a", "#e8847c", "#f0a09a", "#f5bdb8", "#fadcd9", "#fdf2f1"] + }, + "primary-text": { + "role": "primary", + "displayName": "Coral Text", + "canonical": "#a1443d", + "dark": "#e8847c", + "tonalRamp": ["#7a2f2a", "#a1443d", "#b04a42", "#c25d55", "#d4726a", "#e8847c", "#f0a09a", "#fdf2f1"] + }, + "secondary": { + "role": "secondary", + "displayName": "Deep Sky Blue", + "canonical": "#4a7fb5", + "dark": "#7bafde", + "tonalRamp": ["#1f3f61", "#2d5888", "#3a6a9e", "#4a7fb5", "#5a8db8", "#7bafde", "#9dc4e8", "#eef5fb"] + }, + "focus-ring": { + "role": "secondary", + "displayName": "Focus Sky", + "canonical": "#4a7fb5", + "dark": "#7bafde", + "tonalRamp": ["#1f3f61", "#2d5888", "#3a6a9e", "#4a7fb5", "#5a8db8", "#7bafde", "#9dc4e8", "#eef5fb"] + }, + "warning": { + "role": "tertiary", + "displayName": "Amber Lock", + "canonical": "#d9a441", + "dark": "#e4b454", + "tonalRamp": ["#5c3d10", "#8a5a00", "#a87716", "#c7a86b", "#d9a441", "#e4b454", "#f0bc5e", "#fde8a0"] + }, + "warning-text": { + "role": "tertiary", + "displayName": "Amber Text", + "canonical": "#8a5a00", + "dark": "#e4b454", + "tonalRamp": ["#5c3d10", "#745415", "#8a5a00", "#a87716", "#d9a441", "#e4b454", "#f0bc5e", "#fde8a0"] + }, + "error": { + "role": "tertiary", + "displayName": "Warm Red", + "canonical": "#c95e5e", + "dark": "#d97878", + "tonalRamp": ["#7a3535", "#9a4747", "#c95e5e", "#d97878", "#e39a9a", "#eebcbc", "#f6dcdc", "#fbf0f0"] + }, + "success": { + "role": "tertiary", + "displayName": "Sage Success", + "canonical": "#6da67e", + "dark": "#7dbb8f", + "tonalRamp": ["#2f4f39", "#447353", "#4e8a63", "#6da67e", "#7dbb8f", "#8db580", "#a3c898", "#e9eae1"] + }, + "info": { + "role": "tertiary", + "displayName": "Info Sky", + "canonical": "#5a8db8", + "dark": "#7bafde", + "tonalRamp": ["#1a2d40", "#2d5888", "#3f7aa8", "#5a8db8", "#7bafde", "#9dc4e8", "#c5dcef", "#e6f0f8"] + }, + "parchment": { + "role": "neutral", + "displayName": "Parchment", + "canonical": "#faf8f5", + "dark": "#141210", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "parchment-subtle": { + "role": "neutral", + "displayName": "Parchment Subtle", + "canonical": "#f5f1eb", + "dark": "#1c1814", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "parchment-muted": { + "role": "neutral", + "displayName": "Parchment Muted", + "canonical": "#ede8e0", + "dark": "#2e2923", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "ink": { + "role": "neutral", + "displayName": "Warm Ink", + "canonical": "#2e2923", + "dark": "#f3efe9", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "ink-secondary": { + "role": "neutral", + "displayName": "Ink Secondary", + "canonical": "#5a534a", + "dark": "#d4cdc3", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "ink-tertiary": { + "role": "neutral", + "displayName": "Ink Tertiary (decorative only)", + "canonical": "#a89f93", + "dark": "#7d7569", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "ink-placeholder": { + "role": "neutral", + "displayName": "Placeholder Ink", + "canonical": "#736b61", + "dark": "#8a8175", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#736b61", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#f3efe9", + "#faf8f5" + ] + }, + "ink-on-primary": { + "role": "neutral", + "displayName": "Ink on Coral", + "canonical": "#2e2923", + "dark": "#2e2923", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "border": { + "role": "neutral", + "displayName": "Hairline", + "canonical": "#e5dfd7", + "dark": "#433d36", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f0ebe4", + "#faf8f5" + ] + }, + "border-subtle": { + "role": "neutral", + "displayName": "Hairline Subtle", + "canonical": "#f0ebe4", + "dark": "#2e2923", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f0ebe4", + "#faf8f5" + ] + }, + "border-strong": { + "role": "neutral", + "displayName": "Hairline Strong", + "canonical": "#d4cdc3", + "dark": "#5a534a", + "tonalRamp": [ + "#1c1814", + "#2e2923", + "#433d36", + "#5a534a", + "#7d7569", + "#a89f93", + "#d4cdc3", + "#e5dfd7", + "#f3efe9", + "#faf8f5" + ] + }, + "highlight": { + "role": "tertiary", + "displayName": "Search Highlight", + "canonical": "#fde8a0", + "dark": "#5c3d10", + "tonalRamp": ["#5c3d10", "#745415", "#8a5a00", "#a87716", "#c7a86b", "#e0cfae", "#f0bc5e", "#fde8a0"] + }, + "status-new": { + "role": "tertiary", + "displayName": "Status: New", + "canonical": "#a87716", + "dark": "#e4b454", + "tonalRamp": ["#5b4927", "#745415", "#997a3c", "#a87716", "#c7a86b", "#e0cfae", "#e4b454", "#fde8a0"] + }, + "status-stale": { + "role": "tertiary", + "displayName": "Status: Stale", + "canonical": "#b85c26", + "dark": "#e08a4e", + "tonalRamp": ["#493120", "#865533", "#884720", "#b85c26", "#d29a79", "#e08a4e", "#e6c9b7", "#fdf2f1"] + }, + "status-translated": { + "role": "tertiary", + "displayName": "Status: Translated", + "canonical": "#3f7aa8", + "dark": "#7bafde", + "tonalRamp": ["#1e2225", "#374756", "#3a6d95", "#3f7aa8", "#7bafde", "#bbcbd6", "#e8e9e6", "#eef5fb"] + }, + "status-verified": { + "role": "tertiary", + "displayName": "Status: Verified", + "canonical": "#4e8a63", + "dark": "#7dbb8f", + "tonalRamp": ["#1e231d", "#384b3b", "#447353", "#4e8a63", "#7dbb8f", "#c0d0bf", "#e9eae1", "#faf8f5"] + }, + "wc-coral": { + "role": "tertiary", + "displayName": "Watercolor Coral", + "canonical": "#e8847c", + "dark": "#f0a09a", + "tonalRamp": ["#b04a42", "#c25d55", "#d4726a", "#e8847c", "#f0a09a", "#f5bdb8", "#fadcd9", "#fdf2f1"] + }, + "wc-amber": { + "role": "tertiary", + "displayName": "Watercolor Amber", + "canonical": "#e8a838", + "dark": "#f0bc5e", + "tonalRamp": ["#5c3d10", "#8a5a00", "#a87716", "#d9a441", "#e8a838", "#f0bc5e", "#f6d68f", "#fde8a0"] + }, + "wc-sky": { + "role": "tertiary", + "displayName": "Watercolor Sky", + "canonical": "#7bafde", + "dark": "#9dc4e8", + "tonalRamp": ["#1f3f61", "#2d5888", "#3a6a9e", "#4a7fb5", "#5a8db8", "#7bafde", "#9dc4e8", "#eef5fb"] + }, + "wc-lavender": { + "role": "tertiary", + "displayName": "Watercolor Lavender", + "canonical": "#b892cc", + "dark": "#cbaade", + "tonalRamp": ["#4a3660", "#6b4f87", "#8c6ba8", "#a57fbd", "#b892cc", "#cbaade", "#ddc6ea", "#f2eaf7"] + }, + "wc-sage": { + "role": "tertiary", + "displayName": "Watercolor Sage", + "canonical": "#8db580", + "dark": "#a3c898", + "tonalRamp": ["#2f4f39", "#447353", "#4e8a63", "#6da67e", "#8db580", "#a3c898", "#c0d0bf", "#e9eae1"] + } + }, + "typographyMeta": { + "brand": { + "displayName": "Brand wordmark", + "purpose": "The LingoTracker wordmark in the app header only. Never used for headings or UI." + }, + "display": { + "displayName": "Display", + "purpose": "Page titles such as Collections." + }, + "headline": { + "displayName": "Headline", + "purpose": "Dialog titles, shared by the translation editor and collection form." + }, + "title": { + "displayName": "Title", + "purpose": "Collection card names." + }, + "group-title": { + "displayName": "Group title", + "purpose": "The h3 opening a labelled form group; always followed by a caption hint." + }, + "body": { + "displayName": "Body", + "purpose": "Field values and standard paragraphs." + }, + "body-small": { + "displayName": "Body small", + "purpose": "Dialog subtitles." + }, + "label": { + "displayName": "Label", + "purpose": "Field labels above inputs, disclosure titles, button text." + }, + "caption": { + "displayName": "Caption", + "purpose": "Group hints, field hints, errors, count summaries, switch descriptions." + }, + "tag": { + "displayName": "Chip tag", + "purpose": "The uppercase BASE tag inside a locale chip; the only uppercase text in the system." + }, + "mono-code": { + "displayName": "Mono code", + "purpose": "Locale codes in chips and mono input values." + }, + "mono-path": { + "displayName": "Mono path", + "purpose": "Folder paths on cards, two lines reserved." + } + }, + "shadows": [ + { + "name": "shadow-sm", + "value": "0 1px 3px 0 rgb(44 36 28 / 0.06)", + "purpose": "Card at rest." + }, + { + "name": "shadow-md", + "value": "0 4px 8px -1px rgb(44 36 28 / 0.08), 0 2px 4px -2px rgb(44 36 28 / 0.06)", + "purpose": "Card hover with 2px lift." + }, + { + "name": "shadow-lg", + "value": "0 10px 20px -3px rgb(44 36 28 / 0.1), 0 4px 8px -4px rgb(44 36 28 / 0.08)", + "purpose": "Dialog panels." + }, + { + "name": "shadow-xl", + "value": "0 20px 30px -5px rgb(44 36 28 / 0.12), 0 8px 12px -6px rgb(44 36 28 / 0.08)", + "purpose": "Defined in tokens; unused in shipped surfaces." + }, + { + "name": "focus-glow", + "value": "0 0 0 3px color-mix(in srgb, var(--color-secondary) 22%, transparent)", + "purpose": "Focused text input and chip editor, with border-color: var(--color-secondary)." + }, + { + "name": "error-glow", + "value": "0 0 0 3px color-mix(in srgb, var(--color-error) 18%, transparent)", + "purpose": "Invalid text input and chip editor." + }, + { + "name": "accent-glow", + "value": "0 6px 16px -4px color-mix(in srgb, var(--color-primary) 45%, transparent)", + "purpose": "Hover on the page-level primary Add button." + } + ], + "motion": [ + { + "name": "transition-fast", + "value": "150ms cubic-bezier(0.4, 0, 0.2, 1)", + "purpose": "Colour, border, and shadow state changes on controls." + }, + { + "name": "transition-normal", + "value": "200ms cubic-bezier(0.4, 0, 0.2, 1)", + "purpose": "Row ground changes, chevron rotation, disclosure reveal, card hover lift." + }, + { + "name": "transition-slow", + "value": "300ms cubic-bezier(0.4, 0, 0.2, 1)", + "purpose": "Defined in tokens; reserved." + }, + { + "name": "card-enter", + "value": "0.45s cubic-bezier(0.16, 1, 0.3, 1), 50ms stagger per card", + "purpose": "Collection cards entering the grid; disabled under prefers-reduced-motion." + }, + { + "name": "disclosure-reveal", + "value": "opacity 0->1, translateY(-4px)->0 over transition-normal", + "purpose": "Disclosure panel opening; disabled under prefers-reduced-motion." + } + ], + "breakpoints": [ + { + "name": "sm", + "value": "640px" + }, + { + "name": "md", + "value": "768px" + }, + { + "name": "lg", + "value": "1024px" + }, + { + "name": "xl", + "value": "1280px" + }, + { + "name": "xxl", + "value": "1536px" + } + ], + "darkTheme": { + "selector": "[data-theme='dark'], or :root without data-theme under prefers-color-scheme: dark", + "note": "The neutral scale inverts (neutral-50 becomes #1c1814). Coral primary brightens to #e8847c and primary-text aliases it. Sky brightens to #7bafde. Backgrounds: #141210 page, #1c1814 subtle, #2e2923 muted. Dialog panel lifts to background-subtle while the field well stays on the page tone." + } + }, + "components": [ + { + "name": "Primary Button", + "kind": "button", + "refersTo": "button-primary", + "description": "Material flat button in coral with dark ink; the single primary action in a dialog footer or page header.", + "html": "", + "css": ".ds-btn-primary { font-family: 'Nunito', sans-serif; font-size: 0.875rem; font-weight: 500; line-height: 1; height: 36px; padding: 0 1rem; border: 0; border-radius: var(--border-radius-sm, 0.25rem); background: var(--color-primary, #d4726a); color: var(--color-text-on-primary, #2e2923); cursor: pointer; transition: background 150ms cubic-bezier(0.4,0,0.2,1); } .ds-btn-primary:hover { background: var(--color-primary-hover, #c25d55); } .ds-btn-primary:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 2px; } .ds-btn-primary:disabled { opacity: 0.5; cursor: default; }" + }, + { + "name": "Text Button", + "kind": "button", + "refersTo": "button-text", + "description": "Material text button for Cancel and secondary actions; secondary ink that darkens on hover.", + "html": "", + "css": ".ds-btn-text { font-family: 'Nunito', sans-serif; font-size: 0.875rem; font-weight: 500; line-height: 1; height: 36px; padding: 0 0.75rem; border: 0; border-radius: var(--border-radius-sm, 0.25rem); background: transparent; color: var(--color-text-secondary, #5a534a); cursor: pointer; transition: color 150ms cubic-bezier(0.4,0,0.2,1), background 150ms cubic-bezier(0.4,0,0.2,1); } .ds-btn-text:hover { color: var(--color-text-primary, #2e2923); background: color-mix(in srgb, var(--color-text-primary, #2e2923) 6%, transparent); } .ds-btn-text:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 2px; }" + }, + { + "name": "Labelled Text Input", + "kind": "input", + "refersTo": "text-input", + "description": "Label above a 40px well on the dialog ground; sky border and 22% glow on focus. Mono variant for paths.", + "html": "

Relative to the project root, next to .lingo-tracker.json.

", + "css": ".ds-field { display: flex; flex-direction: column; gap: 0.25rem; font-family: 'Nunito', sans-serif; } .ds-field-label { font-size: 0.875rem; font-weight: 600; line-height: 1.5; color: var(--color-text-primary, #2e2923); } .ds-text-input { display: flex; align-items: center; gap: 0.5rem; height: 40px; padding: 0 0.75rem; border: 1px solid var(--color-border-strong, #d4cdc3); border-radius: var(--border-radius-md, 0.375rem); background: var(--color-background, #faf8f5); color: var(--color-text-primary, #2e2923); transition: border-color 150ms cubic-bezier(0.4,0,0.2,1), box-shadow 150ms cubic-bezier(0.4,0,0.2,1); } .ds-text-input svg { flex: none; color: var(--color-text-tertiary, #a89f93); } .ds-text-input input { flex: 1; min-width: 0; height: 100%; border: 0; padding: 0; background: transparent; color: inherit; font: inherit; font-size: 1rem; outline: none; } .ds-text-input input::placeholder { color: var(--color-text-placeholder, #736b61); } .ds-text-input:focus-within { border-color: var(--color-secondary, #4a7fb5); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary, #4a7fb5) 22%, transparent); } .ds-text-input--mono input { font-family: 'IBM Plex Mono', monospace; font-size: 0.875rem; } .ds-text-input--invalid, .ds-text-input--invalid:focus-within { border-color: var(--color-error, #c95e5e); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error, #c95e5e) 18%, transparent); } .ds-field-hint { margin: 0; font-size: 0.75rem; line-height: 1.5; color: var(--color-text-secondary, #5a534a); } .ds-field-hint code { font-family: 'IBM Plex Mono', monospace; font-size: 0.95em; color: var(--color-text-primary, #2e2923); }" + }, + { + "name": "Locale Chip Editor", + "kind": "chip", + "refersTo": "chip-editor", + "description": "Wrapping well holding locale pills (mono code, sky-tinted BASE chip) and an inline add input; same stroke and focus glow as a text input.", + "html": "
", + "css": ".ds-chip-editor { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding: 0.5rem; border: 1px solid var(--color-border-strong, #d4cdc3); border-radius: var(--border-radius-md, 0.375rem); background: var(--color-background, #faf8f5); font-family: 'Nunito', sans-serif; transition: border-color 150ms cubic-bezier(0.4,0,0.2,1), box-shadow 150ms cubic-bezier(0.4,0,0.2,1); } .ds-chip-editor:focus-within { border-color: var(--color-secondary, #4a7fb5); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary, #4a7fb5) 22%, transparent); } .ds-chip-editor--empty { border-style: dashed; } .ds-chip-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; } .ds-locale-chip { display: inline-flex; align-items: center; border-radius: 9999px; background: var(--color-background-muted, #ede8e0); border: 1px solid transparent; color: var(--color-text-secondary, #5a534a); transition: background 150ms, border-color 150ms, color 150ms; } .ds-locale-chip--base { background: color-mix(in srgb, var(--color-secondary, #4a7fb5) 16%, transparent); border-color: color-mix(in srgb, var(--color-secondary, #4a7fb5) 35%, transparent); color: color-mix(in srgb, var(--color-secondary, #4a7fb5) 45%, var(--color-text-primary, #2e2923)); } .ds-locale-chip-body { display: inline-flex; align-items: center; gap: 0.25rem; padding: 3px 0.5rem 3px 0.75rem; border: 0; border-radius: inherit; background: transparent; color: inherit; font: inherit; line-height: 1.5; cursor: pointer; } .ds-locale-chip-body:hover { color: var(--color-text-primary, #2e2923); } .ds-locale-chip:not(.ds-locale-chip--base) .ds-locale-chip-body:hover { background: color-mix(in srgb, var(--color-secondary, #4a7fb5) 10%, transparent); } .ds-locale-chip-body:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 1px; } .ds-locale-chip-code { font-family: 'IBM Plex Mono', monospace; font-size: 0.875rem; font-weight: 600; } .ds-locale-chip-tag { font-size: 0.625rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; opacity: 0.9; } .ds-locale-chip-remove { display: grid; place-items: center; width: 24px; height: 24px; margin: 0 3px 0 -4px; padding: 0; border: 0; border-radius: 9999px; background: transparent; color: var(--color-text-tertiary, #a89f93); cursor: pointer; transition: color 150ms, background 150ms; } .ds-locale-chip-remove:hover { color: var(--color-error, #c95e5e); background: color-mix(in srgb, var(--color-error, #c95e5e) 12%, transparent); } .ds-locale-chip-remove:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 1px; } .ds-chip-add { display: flex; align-items: center; gap: 0.25rem; flex: 1 1 180px; min-width: 180px; min-height: 30px; padding: 0 0.25rem; color: var(--color-text-tertiary, #a89f93); } .ds-chip-add-input { flex: 1; min-width: 120px; height: 30px; border: 0; padding: 0 0.25rem; background: transparent; color: var(--color-text-primary, #2e2923); font: inherit; font-size: 0.875rem; outline: none; } .ds-chip-add-input::placeholder { color: var(--color-text-placeholder, #736b61); }" + }, + { + "name": "Text Chip", + "kind": "chip", + "refersTo": "chip-text", + "description": "The same pill in its word voice: Nunito on the subtle tone with a hairline, for tags and protected terms.", + "html": "tracker", + "css": ".ds-text-chip { display: inline-flex; align-items: center; border-radius: 9999px; background: var(--color-background-subtle, #f5f1eb); border: 1px solid var(--color-border, #e5dfd7); color: var(--color-text-primary, #2e2923); font-family: 'Nunito', sans-serif; } .ds-text-chip-body { padding: 3px 0.5rem 3px 0.75rem; font-size: 0.875rem; font-weight: 500; line-height: 1.5; } .ds-text-chip-remove { display: grid; place-items: center; width: 24px; height: 24px; margin: 0 3px 0 -4px; padding: 0; border: 0; border-radius: 9999px; background: transparent; color: var(--color-text-tertiary, #a89f93); cursor: pointer; transition: color 150ms, background 150ms; } .ds-text-chip-remove:hover { color: var(--color-error, #c95e5e); background: color-mix(in srgb, var(--color-error, #c95e5e) 12%, transparent); } .ds-text-chip-remove:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 1px; }" + }, + { + "name": "Read-only Switch Row", + "kind": "custom", + "refersTo": "switch-row", + "description": "Subtle panel with an amber lock tile, bold label over a caption, and a toggle; the ground warms when on.", + "html": "", + "css": ".ds-switch-row { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.75rem 1rem; border: 1px solid var(--color-border, #e5dfd7); border-radius: var(--border-radius-lg, 0.5rem); background: var(--color-background-subtle, #f5f1eb); font-family: 'Nunito', sans-serif; cursor: pointer; transition: border-color 200ms cubic-bezier(0.4,0,0.2,1), background 200ms cubic-bezier(0.4,0,0.2,1); } .ds-switch-row--on { border-color: color-mix(in srgb, var(--color-warning, #d9a441) 35%, var(--color-border, #e5dfd7)); background: color-mix(in srgb, var(--color-warning, #d9a441) 6%, var(--color-background-subtle, #f5f1eb)); } .ds-switch-tile { display: grid; place-items: center; flex: none; width: 30px; height: 30px; border-radius: var(--border-radius-md, 0.375rem); background: color-mix(in srgb, var(--color-warning, #d9a441) 14%, transparent); color: var(--color-warning-text, #8a5a00); } .ds-switch-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .ds-switch-label { font-size: 0.875rem; font-weight: 700; line-height: 1.375; color: var(--color-text-primary, #2e2923); } .ds-switch-description { font-size: 0.75rem; line-height: 1.375; color: var(--color-text-secondary, #5a534a); } .ds-switch-control { flex: none; margin-top: 2px; width: 36px; height: 20px; accent-color: var(--color-primary, #d4726a); } .ds-switch-control:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 2px; }" + }, + { + "name": "Disclosure Row", + "kind": "custom", + "refersTo": "disclosure", + "description": "Hairline container with a full-width toggle: leading glyph, title, right-aligned count summary, rotating chevron.", + "html": "
", + "css": ".ds-disclosure { border: 1px solid var(--color-border, #e5dfd7); border-radius: var(--border-radius-lg, 0.5rem); overflow: hidden; font-family: 'Nunito', sans-serif; } .ds-disclosure-toggle { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.5rem 0.75rem 0.5rem 1rem; border: 0; background: transparent; color: var(--color-text-primary, #2e2923); font: inherit; text-align: left; cursor: pointer; transition: background 150ms cubic-bezier(0.4,0,0.2,1); } .ds-disclosure-toggle:hover { background: var(--color-background-subtle, #f5f1eb); } .ds-disclosure-toggle:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: -2px; } .ds-disclosure-icon { flex: none; color: var(--color-text-tertiary, #a89f93); } .ds-disclosure-title { font-size: 0.875rem; font-weight: 600; line-height: 1.5; } .ds-disclosure-summary { margin-left: auto; font-size: 0.75rem; color: var(--color-text-secondary, #5a534a); white-space: nowrap; } .ds-disclosure-chevron { flex: none; color: var(--color-text-tertiary, #a89f93); transition: transform 200ms cubic-bezier(0.4,0,0.2,1); } .ds-disclosure--open .ds-disclosure-chevron { transform: rotate(180deg); }" + }, + { + "name": "Dialog Header", + "kind": "custom", + "refersTo": "dialog-header-tile", + "description": "The shared dialog anatomy: 3px coral top rule, 38px coral tile, headline title, one-line subtitle with inline code, close button, hairline below.", + "html": "

Edit Collection

Changes are saved to .lingo-tracker.json in your working tree.

", + "css": ".ds-dialog { width: 100%; max-width: 540px; border-top: 3px solid var(--color-primary, #d4726a); border-radius: var(--border-radius-lg, 0.5rem); background: var(--color-background, #faf8f5); box-shadow: 0 10px 20px -3px rgb(44 36 28 / 0.1), 0 4px 8px -4px rgb(44 36 28 / 0.08); font-family: 'Nunito', sans-serif; overflow: hidden; } .ds-dialog-header { position: relative; display: flex; align-items: flex-start; gap: 0.75rem; padding: 1.25rem 1.5rem 1rem; border-bottom: 1px solid var(--color-border, #e5dfd7); } .ds-dialog-header-tile { display: grid; place-items: center; flex: none; width: 38px; height: 38px; border-radius: var(--border-radius-lg, 0.5rem); background: color-mix(in srgb, var(--color-primary, #d4726a) 12%, transparent); border: 1px solid color-mix(in srgb, var(--color-primary, #d4726a) 25%, transparent); color: var(--color-primary, #d4726a); } .ds-dialog-header-text { flex: 1; min-width: 0; padding-right: 2rem; } .ds-dialog-title { margin: 0 0 0.25rem; font-size: 1.5rem; font-weight: 600; line-height: 1.25; letter-spacing: -0.01em; color: var(--color-text-primary, #2e2923); } .ds-dialog-subtitle { margin: 0; font-size: 0.875rem; line-height: 1.375; color: var(--color-text-secondary, #5a534a); } .ds-dialog-subtitle code { font-family: 'IBM Plex Mono', monospace; font-size: 0.95em; color: var(--color-text-primary, #2e2923); } .ds-dialog-close { position: absolute; top: 0.75rem; right: 0.75rem; display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 9999px; background: transparent; color: var(--color-text-secondary, #5a534a); cursor: pointer; transition: color 150ms cubic-bezier(0.4,0,0.2,1); } .ds-dialog-close:hover { color: var(--color-text-primary, #2e2923); } .ds-dialog-close:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 2px; } .ds-dialog-footer { display: flex; justify-content: flex-end; gap: 0.5rem; padding: 1rem 1.5rem 1.25rem; border-top: 1px solid var(--color-border, #e5dfd7); } .ds-dialog-footer-cancel { height: 36px; padding: 0 0.75rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--color-text-secondary, #5a534a); font: inherit; font-size: 0.875rem; font-weight: 500; cursor: pointer; } .ds-dialog-footer-cancel:hover { color: var(--color-text-primary, #2e2923); } .ds-dialog-footer-primary { height: 36px; padding: 0 1rem; border: 0; border-radius: 0.25rem; background: var(--color-primary, #d4726a); color: var(--color-text-on-primary, #2e2923); font: inherit; font-size: 0.875rem; font-weight: 500; cursor: pointer; } .ds-dialog-footer-primary:hover { background: var(--color-primary-hover, #c25d55); }" + }, + { + "name": "Collection Card", + "kind": "card", + "refersTo": "card", + "description": "12px-radius parchment card with coral icon tile, bold name, two-line mono path, hairline footer of locale pills; lifts 2px on hover.", + "html": "

trackerResources

apps/tracker/src/i18n

  • en Base
  • es
  • fr-ca
  • +3
", + "css": ".ds-card { display: flex; flex-direction: column; width: 300px; padding: 1.25rem; border: 1px solid var(--color-border, #e5dfd7); border-radius: var(--border-radius-xl, 0.75rem); background: var(--color-background, #faf8f5); box-shadow: 0 1px 3px 0 rgb(44 36 28 / 0.06); font-family: 'Nunito', sans-serif; cursor: pointer; transition: transform 200ms cubic-bezier(0.4,0,0.2,1), box-shadow 200ms cubic-bezier(0.4,0,0.2,1), border-color 200ms cubic-bezier(0.4,0,0.2,1), background 200ms cubic-bezier(0.4,0,0.2,1); } .ds-card:hover { background: color-mix(in srgb, var(--color-primary, #d4726a) 3%, var(--color-background, #faf8f5)); border-color: color-mix(in srgb, var(--color-primary, #d4726a) 40%, var(--color-border, #e5dfd7)); box-shadow: 0 4px 8px -1px rgb(44 36 28 / 0.08), 0 2px 4px -2px rgb(44 36 28 / 0.06); transform: translateY(-2px); } .ds-card:focus-visible { outline: 2px solid var(--focus-ring-color, #4a7fb5); outline-offset: 3px; } .ds-card-header { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.75rem; align-items: start; } .ds-card-icon { display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: var(--border-radius-lg, 0.5rem); background: color-mix(in srgb, var(--color-primary, #d4726a) 12%, transparent); border: 1px solid color-mix(in srgb, var(--color-primary, #d4726a) 25%, transparent); color: var(--color-primary, #d4726a); } .ds-card-name { margin: 0; font-size: 1.125rem; font-weight: 700; letter-spacing: -0.01em; line-height: 1.25; color: var(--color-text-primary, #2e2923); overflow-wrap: anywhere; } .ds-card-path { margin: 0.25rem 0 0; font-family: 'IBM Plex Mono', monospace; font-size: 0.75rem; line-height: 1.45; min-height: calc(2 * 1.45 * 0.75rem); color: var(--color-text-secondary, #5a534a); overflow-wrap: anywhere; } .ds-card-footer { margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border-subtle, #f0ebe4); } .ds-card-chips { display: flex; gap: 0.25rem; margin: 0; padding: 0; list-style: none; } .ds-card-chip { display: inline-flex; align-items: baseline; gap: 0.25rem; padding: 1px 0.5rem; border-radius: 9999px; border: 1px solid transparent; background: var(--color-background-muted, #ede8e0); color: var(--color-text-secondary, #5a534a); font-family: 'IBM Plex Mono', monospace; font-size: 0.75rem; font-weight: 600; line-height: 1.5; } .ds-card-chip--base { background: color-mix(in srgb, var(--color-secondary, #4a7fb5) 16%, transparent); border-color: color-mix(in srgb, var(--color-secondary, #4a7fb5) 35%, transparent); color: color-mix(in srgb, var(--color-secondary, #4a7fb5) 45%, var(--color-text-primary, #2e2923)); } .ds-card-chip--more { background: none; border-color: var(--color-border-strong, #d4cdc3); border-style: dashed; } .ds-card-chip-tag { font-family: 'Nunito', sans-serif; font-size: 0.625rem; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.85; }" + } + ], + "narrative": { + "northStar": "The Watercolor Ledger", + "overview": "LingoTracker is a workbench for translation files that live in a developer's own repository, and its surfaces look like warm paper with careful ink on it. The page is parchment with a faint fractal-noise grain fixed over everything; panels and cards sit one tonal step off the page rather than floating on heavy shadows. Two pigments carry all the meaning: a coral/vermillion primary for the brand mark, the primary action, and the single top rule on every dialog, and a deep sky blue that is reserved for the keyboard focus ring, the input focus glow, and the \"base locale\" tint. Everything else is a warm neutral.\n\nDensity is operational, not editorial. Labels sit above their fields, groups are introduced by a short bold heading and a one-line hint, hints are 12px secondary ink, and anything that is a code or a path (locale codes, folder paths, file names) is set in IBM Plex Mono so it reads as data. The world is built over Angular Material (M2 theme, density -1), but the visible surfaces override Material chrome: dialogs drop their container padding, inputs are hand-drawn wells rather than Material outline fields, chips are the project's own pills.\n\nConfirmed rejections from the shipped code: no hard offset shadows, no decorative gradients except the four-pigment header rule, no all-caps section headings, no Material outline fields inside the dialog family.", + "keyCharacteristics": [ + "Parchment ground with a fixed paper-grain overlay in both themes.", + "One accent (coral) for identity and the primary action; one accent (sky) for focus and \"base\".", + "3px coral top rule on every dialog panel; hairline (1px --color-border) separations everywhere else.", + "Labels above fields; group heading + one-line hint introduces each section.", + "Codes and paths in IBM Plex Mono at the surrounding size; prose in Nunito.", + "Soft, warm-tinted shadows used only for lifted panels and hover; tonal steps convey depth at rest.", + "Light and dark themes share one token set; the dark scale is the light neutral scale inverted." + ], + "rules": [ + { + "name": "The Two-Pigment Rule", + "body": "Coral means identity and the primary action; sky means focus and \"base\". A surface that uses coral for focus, or sky for a button, is off-world.", + "section": "colors" + }, + { + "name": "The Text-Weight Rule", + "body": "Fill tones (primary, warning, ink-tertiary) never set type. Copy uses primary-text, warning-text, or ink-secondary and clears 4.5:1 on its own surface.", + "section": "colors" + }, + { + "name": "The Status-Spine Rule", + "body": "A status colour comes from the status-* tokens and never from a local hex; new and stale are never conflated.", + "section": "colors" + }, + { + "name": "The Mono-Is-Data Rule", + "body": "Anything the file system or the config would spell out verbatim (locale code, folder path, file name, key) is set in IBM Plex Mono. Inside prose it is an inline at 0.95em in ink, with no background, so the line height does not jump.", + "section": "typography" + }, + { + "name": "The Label-Above Rule", + "body": "Form labels sit above their controls as 14px semibold Nunito. No floating labels, no placeholder-as-label.", + "section": "typography" + }, + { + "name": "The Tonal-First Rule", + "body": "A surface at rest shows its depth by tone and hairline, not shadow. Shadows answer to state (hover, lifted overlay, focus).", + "section": "elevation" + }, + { + "name": "The Top-Rule Rule", + "body": "Every dialog panel carries exactly one 3px solid --color-primary top border and zero container padding; the rule is the dialog's only decoration.", + "section": "elevation" + } + ], + "dos": [ + "Do open every dialog with the 3px coral top rule, zero container padding, lg shadow, and the header / hairline / body / hairline / footer column.", + "Do put labels above controls at 14px/600 and hints below at 12px ink-secondary.", + "Do set locale codes, paths, and file names in IBM Plex Mono; inline them in prose as at 0.95em in ink with no background.", + "Do use sky blue for focus (2px outline, or border plus 22% glow on wells) and for the base-locale tint; use coral for the primary action and identity.", + "Do mark locked and read-only states with the amber lock tile (14% amber fill, warning-text ink) so they are unmistakable.", + "Do reuse the locale pill for any locale, the text chip for any word-like tag, and the chip editor for any editable list.", + "Do place surfaces one neutral step off their parent and separate them with 1px hairlines before reaching for a shadow.", + "Do honour prefers-reduced-motion by disabling reveal animations and hover lifts." + ], + "donts": [ + "Don't set type in primary, warning, or ink-tertiary; use the text-weight tokens.", + "Don't use Material outline form fields inside the dialog family; the label-above well is the field.", + "Don't use uppercase or letterspaced text outside the BASE tag inside a chip.", + "Don't use Grechen Fuemen anywhere but the wordmark.", + "Don't add hard offset shadows, gradients on controls, or a second accent rule on a dialog.", + "Don't hard-code a status hue; read the status-* tokens." + ] + } +} diff --git a/apps/tracker/.impeccable/surfaces/n-form-dialog-collection-form-dialog-html-71ddd2b8.md b/apps/tracker/.impeccable/surfaces/n-form-dialog-collection-form-dialog-html-71ddd2b8.md new file mode 100644 index 00000000..57133bd4 --- /dev/null +++ b/apps/tracker/.impeccable/surfaces/n-form-dialog-collection-form-dialog-html-71ddd2b8.md @@ -0,0 +1,32 @@ +--- +version: 1 +slug: "n-form-dialog-collection-form-dialog-html-71ddd2b8" +primary_target: "apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.html" +related_targets: ["apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.ts","apps/tracker/src/app/collections/collection-form-dialog/collection-form-dialog.scss"] +--- + +# Surface: Collection form dialog (create / edit) + +Scope: `apps/tracker/src/app/collections/collection-form-dialog/*`, opened from the Collections manager. Mode: Operate. Refinement of an established surface; the watercolor world and Angular Material component base are inherited, not reopened. + +Audience and job: a developer registering a folder of translation files as a collection, or adjusting an existing one. Occasional task, done with the folder path already in mind. Translators and the i18n owner edit locales and protected terms less often. + +Constraints: every string via Transloco resources; read-only state must be unmistakable; base locale is immutable after creation; protected terms editable only when a terms file is configured; delete stays in the card menu. + +## Direction contract + +THESIS: One dialog, three labelled groups (Source, Locales, Options) that read top to bottom in the order a developer thinks. It refuses the flat stack of equally weighted outlined fields and the radio-list of locales. + +OWN-WORLD: Parchment panel with the 3px coral top rule and header pattern of the translation editor. Nunito labels above fields, IBM Plex Mono for paths and locale codes. Locales are pill chips identical to the Collections card chips, base chip in the sky-blue tint with a BASE tag. Read-only is a switch row on a subtle panel with a warm lock tile. Optional metadata sits in a single disclosure row with counts. + +STORY: The visitor sees where the files live, which locales are tracked and which one is the base, and whether the collection is locked, then creates or saves without scrolling past things that do not apply. + +FIRST VIEWPORT: 540px panel. Header: folder tile, title, one-sentence purpose, close. Body: Source (name, folder with hint), Locales (chip row with inline add input, one hint line), Options (read-only switch row, disclosure "Tags & protected terms"). Footer right-aligned: Cancel, primary "Create collection" / "Save changes". + +FORM: Direction A "Grouped ledger", chosen by the user from three hand-built HTML comps (`.impeccable/mocks/collection-dialog/a-ledger*.png`, approved sidecars). No concept-seed roll: this is a refinement inside an established surface, so no seed key exists. + +Signature interaction: clicking a locale chip in create mode makes it the base; the BASE tag slides to it. In edit mode the base chip carries a lock and its remove control is absent. + +Approved comp: `.impeccable/mocks/collection-dialog/a-ledger-create-light.png` and `a-ledger-edit-light.png`. + +FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance. diff --git a/apps/tracker/DESIGN.md b/apps/tracker/DESIGN.md new file mode 100644 index 00000000..a034cd15 --- /dev/null +++ b/apps/tracker/DESIGN.md @@ -0,0 +1,368 @@ +--- +name: LingoTracker +description: Warm watercolor parchment workbench for translation files; coral accent, sky-blue focus, mono for anything that is a code or a path. +colors: + primary: "#d4726a" + primary-hover: "#c25d55" + primary-active: "#b04a42" + primary-subtle: "#fdf2f1" + primary-text: "#a1443d" + secondary: "#4a7fb5" + secondary-hover: "#3a6a9e" + secondary-active: "#2d5888" + secondary-subtle: "#eef5fb" + parchment: "#faf8f5" + parchment-subtle: "#f5f1eb" + parchment-muted: "#ede8e0" + ink: "#2e2923" + ink-secondary: "#5a534a" + ink-tertiary: "#a89f93" + ink-placeholder: "#736b61" + ink-on-primary: "#2e2923" + border: "#e5dfd7" + border-subtle: "#f0ebe4" + border-strong: "#d4cdc3" + focus-ring: "#4a7fb5" + success: "#6da67e" + warning: "#d9a441" + warning-text: "#8a5a00" + error: "#c95e5e" + info: "#5a8db8" + highlight: "#fde8a0" + status-new: "#a87716" + status-stale: "#b85c26" + status-translated: "#3f7aa8" + status-verified: "#4e8a63" + wc-coral: "#e8847c" + wc-amber: "#e8a838" + wc-sky: "#7bafde" + wc-lavender: "#b892cc" + wc-sage: "#8db580" +typography: + brand: + fontFamily: "Grechen Fuemen, cursive" + display: + fontFamily: "Nunito, sans-serif" + fontSize: "clamp(1.75rem, 2.6vw, 2.25rem)" + fontWeight: 700 + lineHeight: 1.25 + letterSpacing: "-0.02em" + headline: + fontFamily: "Nunito, sans-serif" + fontSize: "1.5rem" + fontWeight: 600 + lineHeight: 1.25 + letterSpacing: "-0.01em" + title: + fontFamily: "Nunito, sans-serif" + fontSize: "1.125rem" + fontWeight: 700 + lineHeight: 1.25 + letterSpacing: "-0.01em" + group-title: + fontFamily: "Nunito, sans-serif" + fontSize: "1rem" + fontWeight: 700 + lineHeight: 1.375 + body: + fontFamily: "Nunito, sans-serif" + fontSize: "1rem" + fontWeight: 400 + lineHeight: 1.5 + body-small: + fontFamily: "Nunito, sans-serif" + fontSize: "0.875rem" + fontWeight: 400 + lineHeight: 1.375 + label: + fontFamily: "Nunito, sans-serif" + fontSize: "0.875rem" + fontWeight: 600 + lineHeight: 1.5 + caption: + fontFamily: "Nunito, sans-serif" + fontSize: "0.75rem" + fontWeight: 400 + lineHeight: 1.375 + tag: + fontFamily: "Nunito, sans-serif" + fontSize: "0.625rem" + fontWeight: 600 + letterSpacing: "0.05em" + mono-code: + fontFamily: "IBM Plex Mono, monospace" + fontSize: "0.875rem" + fontWeight: 600 + mono-path: + fontFamily: "IBM Plex Mono, monospace" + fontSize: "0.75rem" + fontWeight: 400 + lineHeight: 1.45 +rounded: + sm: "0.25rem" + md: "0.375rem" + lg: "0.5rem" + xl: "0.75rem" + full: "9999px" +spacing: + "1": "0.25rem" + "2": "0.5rem" + "3": "0.75rem" + "4": "1rem" + "5": "1.25rem" + "6": "1.5rem" + "8": "2rem" + "10": "2.5rem" + "12": "3rem" + "16": "4rem" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.ink-on-primary}" + rounded: "{rounded.sm}" + typography: "{typography.label}" + button-primary-hover: + backgroundColor: "{colors.primary-hover}" + textColor: "{colors.ink-on-primary}" + button-text: + backgroundColor: "transparent" + textColor: "{colors.ink-secondary}" + typography: "{typography.label}" + button-text-hover: + textColor: "{colors.ink}" + card: + backgroundColor: "{colors.parchment}" + textColor: "{colors.ink}" + rounded: "{rounded.xl}" + padding: "{spacing.5}" + dialog-header-tile: + textColor: "{colors.primary}" + rounded: "{rounded.lg}" + size: "38px" + text-input: + backgroundColor: "{colors.parchment}" + textColor: "{colors.ink}" + rounded: "{rounded.md}" + padding: "0 {spacing.3}" + height: "40px" + typography: "{typography.body}" + text-input-mono: + backgroundColor: "{colors.parchment}" + textColor: "{colors.ink}" + rounded: "{rounded.md}" + height: "40px" + typography: "{typography.mono-code}" + chip-editor: + backgroundColor: "{colors.parchment}" + rounded: "{rounded.md}" + padding: "{spacing.2}" + chip-locale: + backgroundColor: "{colors.parchment-muted}" + textColor: "{colors.ink-secondary}" + rounded: "{rounded.full}" + padding: "3px {spacing.2} 3px {spacing.3}" + typography: "{typography.mono-code}" + chip-text: + backgroundColor: "{colors.parchment-subtle}" + textColor: "{colors.ink}" + rounded: "{rounded.full}" + padding: "3px {spacing.2} 3px {spacing.3}" + typography: "{typography.body-small}" + switch-row: + backgroundColor: "{colors.parchment-subtle}" + textColor: "{colors.ink}" + rounded: "{rounded.lg}" + padding: "{spacing.3} {spacing.4}" + switch-tile: + textColor: "{colors.warning-text}" + rounded: "{rounded.md}" + size: "30px" + disclosure: + backgroundColor: "transparent" + textColor: "{colors.ink}" + rounded: "{rounded.lg}" + padding: "{spacing.2} {spacing.3} {spacing.2} {spacing.4}" +--- + +# Design System: LingoTracker + +## Overview + +**Creative North Star: "The Watercolor Ledger"** + +LingoTracker is a workbench for translation files that live in a developer's own repository, and its surfaces look like warm paper with careful ink on it. The page is parchment with a faint fractal-noise grain fixed over everything; panels and cards sit one tonal step off the page rather than floating on heavy shadows. Two pigments carry all the meaning: a coral/vermillion primary for the brand mark, the primary action, and the single top rule on every dialog, and a deep sky blue that is reserved for the keyboard focus ring, the input focus glow, and the "base locale" tint. Everything else is a warm neutral. + +Density is operational, not editorial. Labels sit above their fields, groups are introduced by a short bold heading and a one-line hint, hints are 12px secondary ink, and anything that is a code or a path (locale codes, folder paths, file names) is set in IBM Plex Mono so it reads as data. The world is built over Angular Material (M2 theme, density -1), but the visible surfaces override Material chrome: dialogs drop their container padding, inputs are hand-drawn wells rather than Material outline fields, chips are the project's own pills. + +Confirmed rejections from the shipped code: no hard offset shadows, no decorative gradients except the four-pigment header rule, no all-caps section headings, no Material outline fields inside the dialog family. + +**Key Characteristics:** +- Parchment ground with a fixed paper-grain overlay in both themes. +- One accent (coral) for identity and the primary action; one accent (sky) for focus and "base". +- 3px coral top rule on every dialog panel; hairline (`1px --color-border`) separations everywhere else. +- Labels above fields; group heading + one-line hint introduces each section. +- Codes and paths in IBM Plex Mono at the surrounding size; prose in Nunito. +- Soft, warm-tinted shadows used only for lifted panels and hover; tonal steps convey depth at rest. +- Light and dark themes share one token set; the dark scale is the light neutral scale inverted. + +## Colors + +A warm parchment neutral scale carrying two pigments, coral and sky, with a quiet four-colour watercolor accent set used only for decoration. + +### Primary +- **Coral / Vermillion** (`{colors.primary}`): the brand pigment. Used for the dialog top rule, filled primary buttons, the folder tile tint on cards and dialog headers (12% fill, 25% border), selected/hover card borders, and the base-locale left rule in the translation editor. Hover and active steps darken (`primary-hover`, `primary-active`). +- **Coral Text** (`{colors.primary-text}`): the text-weight coral. The fill tone only reaches 2.68:1 on a card, so any copy that must read as "primary" uses this instead (5.05:1). In dark theme the brightened primary already clears 4.5:1 and `primary-text` aliases it. +- **Coral Subtle** (`{colors.primary-subtle}`): a near-white blush for selected rows and soft highlights. + +### Secondary +- **Deep Sky Blue** (`{colors.secondary}`): the attention pigment. It is the focus-ring colour (`{colors.focus-ring}` is the same value), the focused-input border and 22% glow, and the "base locale" chip tint (16% fill, 35% border, 45% mixed into the text). It is never a fill for a primary action. +- **Sky Subtle** (`{colors.secondary-subtle}`): info wells and soft secondary backgrounds. + +### Tertiary +- **Amber Warning** (`{colors.warning}` / `{colors.warning-text}`): the "locked" hue. Read-only tiles use a 14% amber fill with `warning-text` ink; the switched-on read-only row mixes 6% amber into its ground and 35% into its border. `warning` itself is fill/border only (about 2.1:1 on parchment) and must never set type. +- **Error** (`{colors.error}`): invalid borders, error hints, the remove-chip hover. +- **Success / Info** (`{colors.success}`, `{colors.info}`): status glyphs and banners. + +### Neutral +- **Parchment** (`{colors.parchment}`): the page and the card surface, the dialog panel in light theme, and the field well. +- **Parchment Subtle** (`{colors.parchment-subtle}`): the switch-row ground, text-chip fill, disclosure hover, and the dialog panel in dark theme (where it lifts one step above the page black). +- **Parchment Muted** (`{colors.parchment-muted}`): locale-chip fill and inline-code background. +- **Ink** (`{colors.ink}`): titles, labels, field values, inline `` in prose. +- **Ink Secondary** (`{colors.ink-secondary}`): subtitles, hints, resting chip text, close/menu icons. +- **Ink Tertiary** (`{colors.ink-tertiary}`): decorative glyphs only (leading icons in inputs, chevrons, remove buttons at rest); at 2.5:1 it is not for text. +- **Ink Placeholder** (`{colors.ink-placeholder}`): placeholder copy, darker than tertiary so it clears 4.5:1. +- **Border / Border Subtle / Border Strong** (`{colors.border}`, `{colors.border-subtle}`, `{colors.border-strong}`): hairlines. Panels and rows use `border`; inner dividers use `border-subtle`; input wells and dashed empty states use `border-strong`. + +### Status spine +- **New** (`{colors.status-new}`), **Stale** (`{colors.status-stale}`), **Translated** (`{colors.status-translated}`), **Verified** (`{colors.status-verified}`): every surface that reports a status reads these tokens, with `-on-dark` variants for the always-dark tooltip. `new` and `stale` keep saturated chip fills; `translated` and `verified` go near-neutral with the hue carried by ink and border only. + +### Watercolor accents +- **Coral, Amber, Sky, Lavender, Sage** (`{colors.wc-coral}` and siblings): decorative pigments for the header rule gradient, empty-state rings, and splashes. Never used for controls or text. + +### Named Rules +**The Two-Pigment Rule.** Coral means identity and the primary action; sky means focus and "base". A surface that uses coral for focus, or sky for a button, is off-world. + +**The Text-Weight Rule.** Fill tones (`primary`, `warning`, `ink-tertiary`) never set type. Copy uses `primary-text`, `warning-text`, or `ink-secondary` and clears 4.5:1 on its own surface. + +**The Status-Spine Rule.** A status colour comes from the `status-*` tokens and never from a local hex; `new` and `stale` are never conflated. + +## Typography + +**Brand Font:** Grechen Fuemen (with cursive fallback); the LingoTracker wordmark in the app header only. +**Body Font:** Nunito (with sans-serif fallback); every heading, label, control, and paragraph. +**Label/Mono Font:** IBM Plex Mono (with monospace fallback); locale codes, folder paths, file names, resource keys. + +**Character:** Nunito's rounded terminals keep a data-heavy tool friendly; Plex Mono's stiffness makes codes and paths read as facts. The pairing is a handwritten ledger with typed entries. Grechen Fuemen never appears outside the wordmark. + +### Hierarchy +- **Display** (700, `clamp(1.75rem, 2.6vw, 2.25rem)`, 1.25, -0.02em): page titles ("Collections"). +- **Headline** (600, 1.5rem, 1.25, -0.01em): dialog titles, shared by the translation editor and the collection form. +- **Title** (700, 1.125rem, 1.25, -0.01em): card names. +- **Group title** (700, 1rem, 1.375): the `h3` that opens a labelled group inside a form (Source, Locales, Options), always followed by a 12px hint. +- **Body** (400, 1rem, 1.5): field values, standard paragraphs. +- **Body small** (400, 0.875rem, 1.375): dialog subtitles, card path-adjacent copy. +- **Label** (600, 0.875rem, 1.5): field labels above inputs, disclosure titles, button text; switch labels step up to 700. +- **Caption** (400, 0.75rem, 1.375–1.5): group hints, field hints, errors, disclosure summaries, switch descriptions. +- **Tag** (600, 0.625rem, +0.05em, uppercase): the BASE tag inside a locale chip; the only uppercase text in the system, and it never leaves the chip. +- **Mono code** (600, 0.875rem): locale codes in chips and mono inputs. +- **Mono path** (400, 0.75rem, 1.45): folder paths on cards; two lines reserved so cards stay flush. + +### Named Rules +**The Mono-Is-Data Rule.** Anything the file system or the config would spell out verbatim (locale code, folder path, file name, key) is set in IBM Plex Mono. Inside prose it is an inline `` at 0.95em in `ink`, with no background, so the line height does not jump. + +**The Label-Above Rule.** Form labels sit above their controls as 14px semibold Nunito. No floating labels, no placeholder-as-label. + +## Layout + +Pages are centred columns capped at 1400px with `--spacing-8` vertical and `--spacing-6` horizontal padding, dropping to `--spacing-6`/`--spacing-4` under 600px. The Collections page is an auto-fill grid of cards at `minmax(min(100%, 300px), 1fr)` with a `--spacing-5` gap. + +Dialogs are fixed-width panels centred by the CDK overlay: the collection form is 540px (`max-width: min(540px, 100vw - 2 * --spacing-6)`, `max-height: min(90vh, 100dvh - 2 * --spacing-8)`); the translation editor is 700px and goes full-screen below 640px. Inside a dialog the column is header / scrolling body / footer, with the body owning the only scroll. Header padding is `--spacing-5 --spacing-6 --spacing-4`; body the same; footer `--spacing-4 --spacing-6 --spacing-5`. + +Rhythm inside a form: groups stack at `--spacing-5`; within a group, heading-to-controls at `--spacing-3`; a field's label, control, and hint at `--spacing-1`; chips and inline controls at `--spacing-2`. Breakpoints follow the mixins in `styles/breakpoints.scss`: 640, 768, 1024, 1280, 1536px, mobile-first. + +## Elevation & Depth + +Depth is tonal first, shadowed second. Panels, wells, and rows are placed one neutral step off their parent (page → card → well in light theme; page black → subtle panel → page-black well in dark theme) and separated by hairlines. Shadows are warm-tinted (`rgb(44 36 28 / …)` in light, black in dark), diffuse, and appear only on lifted panels and hover. + +### Shadow Vocabulary +- **sm** (`0 1px 3px 0 rgb(44 36 28 / 0.06)`): card at rest. +- **md** (`0 4px 8px -1px rgb(44 36 28 / 0.08), 0 2px 4px -2px rgb(44 36 28 / 0.06)`): card hover with a 2px lift. +- **lg** (`0 10px 20px -3px rgb(44 36 28 / 0.1), 0 4px 8px -4px rgb(44 36 28 / 0.08)`): dialog panels. +- **xl**: defined, currently unused. +- **Focus glow** (`0 0 0 3px color-mix(in srgb, --color-secondary 22%, transparent)`): input and chip-editor focus; the error variant uses 18% of `error`. +- **Accent glow** (`0 6px 16px -4px color-mix(in srgb, --color-primary 45%, transparent)`): hover on the page's primary "Add" button only. + +### Named Rules +**The Tonal-First Rule.** A surface at rest shows its depth by tone and hairline, not shadow. Shadows answer to state (hover, lifted overlay, focus). + +**The Top-Rule Rule.** Every dialog panel carries exactly one `3px solid --color-primary` top border and zero container padding; the rule is the dialog's only decoration. + +## Shapes + +Soft, small radii scaled to the object: 4px for inline code, 6px for input wells, chip editors, and small tiles, 8px for cards' icon tiles, switch rows, disclosures, and dialog panels, 12px for collection cards, and full pills for every chip and the chip-add button. Borders are 1px hairlines; empty states (chip editor with no chips, the "+N" overflow chip, a locked value) switch to a dashed `border-strong` stroke rather than changing colour. Icon tiles are squares (38px header/card, 30px switch) with a translucent tint fill and a slightly stronger tint border in the same hue. + +## Components + +### Buttons +- **Shape:** Material button shape (4px), density -1; the page-level "Add Collection" overrides to 8px with `--spacing-2 --spacing-5` padding. +- **Primary:** Material `mat-flat-button color="primary"`: coral fill with dark `ink-on-primary` label, 14px/500. Dark theme keeps the same light coral and the same dark ink. +- **Text:** `mat-button` in `ink-secondary`, hover to `ink`; used for Cancel and the close icon. +- **Hover / Focus:** colour steps via `--transition-fast` (150ms); the page "Add" button gains the accent glow and a 1px lift. Focus is the global 2px sky outline at 2px offset. +- **Footer convention:** right-aligned, Cancel then Primary, `--spacing-2` apart, above a `border` hairline. + +### Chips +- **Locale chip:** full pill, `parchment-muted` fill, transparent 1px border, `ink-secondary` text; code in mono 14px/600 with `3px --spacing-2 3px --spacing-3` padding. Card variant is smaller (12px mono, `1px --spacing-2`). +- **Base variant:** sky tint (16% fill, 35% border, text mixed 45% toward sky) with an uppercase BASE tag at 10px; in edit mode a 14px lock glyph leads the code and the remove control is absent. +- **Text chip (tags, protected terms):** same pill, `parchment-subtle` fill, `border` hairline, Nunito 14px/500 in `ink`. +- **Remove control:** 24px round ghost button in `ink-tertiary`, hover to `error` on a 12% error tint. +- **Interactive chip:** in create mode a locale chip is a button; hover tints non-base chips 10% sky; `aria-pressed` marks the base. + +### Cards / Containers +- **Corner Style:** 12px. +- **Background:** `parchment` (light) or 5% ink mixed into `parchment-subtle` (dark); hover mixes 3–8% coral into the ground. +- **Shadow Strategy:** `sm` at rest, `md` with a 2px lift on hover. +- **Border:** 1px `border`; hover mixes 40% coral into it. Read-only cards swap the icon tile to the amber tint. +- **Internal Padding:** `--spacing-5` (`--spacing-4` under 600px); footer separated by a `border-subtle` hairline holding the locale chips and a trailing arrow. + +### Inputs / Fields +- **Style:** label above (Label role), then a 40px well: 1px `border-strong`, 6px radius, `parchment` ground (`--dialog-well`), optional 18px leading glyph in `ink-tertiary`, `--spacing-2` gap, `0 --spacing-3` padding. Mono variant sets the value in Plex Mono 14px for paths. +- **Focus:** border to `secondary` plus the 22% sky glow, 150ms. +- **Error:** border to `error` with an 18% error glow; a 12px `error` hint with `role="alert"` replaces the normal hint. +- **Locked:** dashed border, transparent ground, `ink-secondary` value, leading lock glyph, and a hint explaining the lock. +- **Hint:** 12px `ink-secondary`; optional 14px leading glyph; file names inside it are inline ``. +- **Page filter field:** the Collections filter uses a coral focus mix instead of sky (see drift note). + +### Chip editor +A wrapping well with the same stroke, radius, ground, and focus glow as a text input, `--spacing-2` padding and gap. Chips come first; the inline add row (18px plus glyph, borderless 30px input, optional 28px pill "Add" button that appears once text is typed) flexes to fill the remaining space at a 180px minimum. Empty editors show a dashed stroke. Tags and protected terms reuse it with text chips. + +### Labelled group +A `section` with an `h3` in Group-title role and an optional 12px `ink-secondary` hint stacked at 2px, then controls at `--spacing-3`. Groups stack at `--spacing-5`. This is how a form reads top to bottom without dividers. + +### Switch row +A `parchment-subtle` panel, 1px `border`, 8px radius, `--spacing-3 --spacing-4` padding: 30px amber lock tile, then a 14px/700 label over a 12px description, then a Material slide toggle. When on, the row mixes 6% amber into its ground and 35% into its border over 200ms. The tile is the lock's colour in every locked context (cards, dialogs). + +### Disclosure row +A 1px `border`, 8px-radius container with a full-width toggle: 18px leading glyph, 14px/600 title, a right-aligned 12px count summary ("2 tags · 0 terms" or "None yet"), and a chevron that rotates 180° over 200ms. The open panel is separated by a `border-subtle` hairline, padded `--spacing-2 --spacing-4 --spacing-4`, and fades in 4px over 200ms (disabled under reduced motion). + +### Dialog anatomy +Panel (see Layout for widths) with zero Material padding, a 3px coral top rule, and `lg` shadow. Header: 38px coral tile with a 19px glyph, Headline title, one-line Body-small subtitle in `ink-secondary`, absolutely positioned close button at `--spacing-3`, hairline below. Body: the scrolling column of labelled groups. Footer: hairline above, right-aligned Cancel + Primary. The translation editor shares the top rule, title, subtitle, close, and hairlines; the collection form adds the header tile. + +## Do's and Don'ts + +### Do: +- **Do** open every dialog with the 3px coral top rule, zero container padding, `lg` shadow, and the header / hairline / body / hairline / footer column. +- **Do** put labels above controls at 14px/600 and hints below at 12px `ink-secondary`. +- **Do** set locale codes, paths, and file names in IBM Plex Mono; inline them in prose as `` at 0.95em in `ink` with no background. +- **Do** use sky blue for focus (2px outline, or border plus 22% glow on wells) and for the base-locale tint; use coral for the primary action and identity. +- **Do** mark locked and read-only states with the amber lock tile (14% amber fill, `warning-text` ink) so they are unmistakable. +- **Do** reuse the locale pill for any locale, the text chip for any word-like tag, and the chip editor for any editable list. +- **Do** place surfaces one neutral step off their parent and separate them with 1px hairlines before reaching for a shadow. +- **Do** honour `prefers-reduced-motion` by disabling reveal animations and hover lifts. + +### Don't: +- **Don't** set type in `primary`, `warning`, or `ink-tertiary`; use the text-weight tokens. +- **Don't** use Material outline form fields inside the dialog family; the label-above well is the field. +- **Don't** use uppercase or letterspaced text outside the BASE tag inside a chip. +- **Don't** use Grechen Fuemen anywhere but the wordmark. +- **Don't** add hard offset shadows, gradients on controls, or a second accent rule on a dialog. +- **Don't** hard-code a status hue; read the `status-*` tokens. From 7ace7e882014553817ae5717661b9e1a60e16087 Mon Sep 17 00:00:00 2001 From: snodel Date: Mon, 14 Sep 2026 14:17:51 -0700 Subject: [PATCH 05/13] chore: ignore impeccable mock and review captures Co-Authored-By: Claude Fable 5.1 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 31fcd7ad..4cf5ccef 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,5 @@ vitest.config.*.timestamp* # Impeccable design skill — machine-local state (config.json is committed) .impeccable/config.local.json .impeccable/live/ +.impeccable/mocks/ +.impeccable/review/ From 5c5287e59c14533e5780520b375c3c71e9d99bf0 Mon Sep 17 00:00:00 2001 From: snodel Date: Wed, 16 Sep 2026 13:59:42 -0700 Subject: [PATCH 06/13] feat(domain): add JavaScript identifier validation Bundle type generation lets the caller name the generated token constant, which has to be a valid JavaScript identifier. The check is a pure regex over a string, so it belongs in domain where the API, the CLI and the Tracker UI can all reach it. Co-Authored-By: Claude Opus 5 (1M context) --- libs/domain/src/index.ts | 1 + libs/domain/src/lib/js-identifier.spec.ts | 39 ++++++++ libs/domain/src/lib/js-identifier.ts | 105 ++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 libs/domain/src/lib/js-identifier.spec.ts create mode 100644 libs/domain/src/lib/js-identifier.ts diff --git a/libs/domain/src/index.ts b/libs/domain/src/index.ts index e5f54083..a9d4f493 100644 --- a/libs/domain/src/index.ts +++ b/libs/domain/src/index.ts @@ -18,3 +18,4 @@ export * from './lib/normalized-levenshtein'; export * from './lib/icu-locale-validation'; export * from './lib/portable-plural-categories'; export * from './lib/icu-arguments'; +export * from './lib/js-identifier'; diff --git a/libs/domain/src/lib/js-identifier.spec.ts b/libs/domain/src/lib/js-identifier.spec.ts new file mode 100644 index 00000000..76d66e6f --- /dev/null +++ b/libs/domain/src/lib/js-identifier.spec.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { isJavaScriptReservedWord, isValidJavaScriptIdentifier } from './js-identifier'; + +describe('isJavaScriptReservedWord', () => { + it('flags ES and TypeScript keywords', () => { + for (const word of ['class', 'const', 'type', 'interface', 'await', 'satisfies']) { + expect(isJavaScriptReservedWord(word)).toBe(true); + } + }); + + it('flags globals that should not be shadowed', () => { + expect(isJavaScriptReservedWord('undefined')).toBe(true); + expect(isJavaScriptReservedWord('NaN')).toBe(true); + }); + + it('is case sensitive and leaves ordinary names alone', () => { + expect(isJavaScriptReservedWord('Type')).toBe(false); + expect(isJavaScriptReservedWord('MY_TOKENS')).toBe(false); + }); +}); + +describe('isValidJavaScriptIdentifier', () => { + it('accepts any casing convention', () => { + for (const name of ['MY_KEYS', 'myKeys', 'MyKeys', '_internal', '$tokens', 'a1']) { + expect(isValidJavaScriptIdentifier(name)).toBe(true); + } + }); + + it('rejects empty and malformed names', () => { + for (const name of ['', '1bad', 'my-key', 'my key', 'ünïcode']) { + expect(isValidJavaScriptIdentifier(name)).toBe(false); + } + }); + + it('rejects reserved words even though they match the character pattern', () => { + expect(isValidJavaScriptIdentifier('type')).toBe(false); + expect(isValidJavaScriptIdentifier('typeTokens')).toBe(true); + }); +}); diff --git a/libs/domain/src/lib/js-identifier.ts b/libs/domain/src/lib/js-identifier.ts new file mode 100644 index 00000000..1389804c --- /dev/null +++ b/libs/domain/src/lib/js-identifier.ts @@ -0,0 +1,105 @@ +/** + * Browser-safe JavaScript identifier rules. + * + * Both the CLI/API (via core's `validateJavaScriptIdentifier`) and the Tracker + * UI's bundle form validate the token constant name. Keeping the rules here + * means the client-side check can never drift from the server-side one. + */ + +/** Matches an ASCII-only JavaScript identifier. Unicode letters are not permitted. */ +export const JS_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + +/** ES2022 + TypeScript contextual keywords that cannot be used as bare identifiers in a const declaration. */ +export const JS_RESERVED_WORDS: ReadonlySet = new Set([ + // ES2022 reserved words + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'let', + 'new', + 'null', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', + 'enum', + 'await', + 'implements', + 'interface', + 'package', + 'private', + 'protected', + 'public', + // TypeScript contextual keywords + 'abstract', + 'as', + 'asserts', + 'async', + 'declare', + 'from', + 'global', + 'infer', + 'is', + 'keyof', + 'module', + 'namespace', + 'never', + 'of', + 'out', + 'override', + 'readonly', + 'require', + 'satisfies', + 'symbol', + 'type', + 'unique', + 'unknown', + 'using', + // Globals that should not be shadowed + 'undefined', + 'Infinity', + 'NaN', +]); + +/** True when `name` is a reserved word that cannot be declared as a bare identifier. */ +export function isJavaScriptReservedWord(name: string): boolean { + return JS_RESERVED_WORDS.has(name); +} + +/** + * True when `name` is a legal, non-reserved ASCII JavaScript identifier. + * + * Accepts any casing: camelCase, PascalCase, SCREAMING_SNAKE_CASE, snake_case. + * Valid: `MY_KEYS`, `myKeys`, `_internal`. Invalid: `1bad`, `my-key`, `class`. + */ +export function isValidJavaScriptIdentifier(name: string): boolean { + return name.length > 0 && JS_IDENTIFIER_PATTERN.test(name) && !isJavaScriptReservedWord(name); +} From 2cd35f251ca8750cd6e4439c903b90d8838ca584 Mon Sep 17 00:00:00 2001 From: snodel Date: Wed, 16 Sep 2026 13:59:56 -0700 Subject: [PATCH 07/13] feat(core): plan, validate and edit bundle definitions Bundles were generate-only and CLI-shaped. Three additions open them up to the API and the Tracker UI without changing what the CLI does: - planBundle: a dry run that never writes. Returns the files it would produce, whether each already exists, keys per locale, key conflicts and one worked example key, so a form can show the consequences of a definition before it is saved. - validateBundleDefinition: returns every problem with a definition rather than throwing on the first, so a form can mark all of its fields at once. - addBundleDefinition / updateBundleDefinition / deleteBundleDefinition: config-file CRUD that validates inside the updater and preserves key order. generateBundle grows an optional onProgress callback and reports keysPerLocale, which is what lets a caller drive a progress bar and a result summary. generateBundleTypes now accepts the definition it should use instead of always reading it back out of the config, so a dry run can type-check a definition that has not been saved. Co-Authored-By: Claude Opus 5 (1M context) --- .../bundle-definition-operations.spec.ts | 253 ++++++++++++ .../bundle/bundle-definition-operations.ts | 166 ++++++++ .../src/lib/bundle/generate-bundle.spec.ts | 129 +++++- libs/core/src/lib/bundle/generate-bundle.ts | 81 +++- libs/core/src/lib/bundle/index.ts | 3 + libs/core/src/lib/bundle/plan-bundle.spec.ts | 384 ++++++++++++++++++ libs/core/src/lib/bundle/plan-bundle.ts | 240 +++++++++++ .../type-generation/generate-types.spec.ts | 51 +++ .../bundle/type-generation/generate-types.ts | 7 +- .../bundle/type-generation/key-transformer.ts | 84 +--- .../bundle/validate-bundle-definition.spec.ts | 222 ++++++++++ .../lib/bundle/validate-bundle-definition.ts | 134 ++++++ libs/core/src/lib/errors/error-messages.ts | 6 + 13 files changed, 1666 insertions(+), 94 deletions(-) create mode 100644 libs/core/src/lib/bundle/bundle-definition-operations.spec.ts create mode 100644 libs/core/src/lib/bundle/bundle-definition-operations.ts create mode 100644 libs/core/src/lib/bundle/plan-bundle.spec.ts create mode 100644 libs/core/src/lib/bundle/plan-bundle.ts create mode 100644 libs/core/src/lib/bundle/validate-bundle-definition.spec.ts create mode 100644 libs/core/src/lib/bundle/validate-bundle-definition.ts diff --git a/libs/core/src/lib/bundle/bundle-definition-operations.spec.ts b/libs/core/src/lib/bundle/bundle-definition-operations.spec.ts new file mode 100644 index 00000000..806d8b1b --- /dev/null +++ b/libs/core/src/lib/bundle/bundle-definition-operations.spec.ts @@ -0,0 +1,253 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import type { BundleDefinition } from '../../config/bundle-definition'; +import type { LingoTrackerConfig } from '../../config/lingo-tracker-config'; +import { CONFIG_FILENAME } from '../../constants'; +import { addBundleDefinition, deleteBundleDefinition, updateBundleDefinition } from './bundle-definition-operations'; + +const baseConfig = (overrides: Partial = {}): LingoTrackerConfig => ({ + exportFolder: 'dist/lingo-export', + importFolder: 'dist/lingo-import', + baseLocale: 'en', + locales: ['en', 'fr'], + collections: { + common: { translationsFolder: './libs/common/i18n' }, + admin: { translationsFolder: './libs/admin/i18n' }, + }, + ...overrides, +}); + +const validDefinition = (overrides: Partial = {}): BundleDefinition => ({ + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: 'All', + ...overrides, +}); + +describe('bundle-definition-operations', () => { + let cwd: string; + + beforeEach(() => { + cwd = mkdtempSync(join(tmpdir(), 'lingo-bundle-ops-')); + }); + + afterEach(() => { + rmSync(cwd, { recursive: true, force: true }); + }); + + const writeConfig = (config: LingoTrackerConfig): void => { + writeFileSync(join(cwd, CONFIG_FILENAME), JSON.stringify(config, null, 2), 'utf8'); + }; + + const readConfig = (): LingoTrackerConfig => JSON.parse(readFileSync(join(cwd, CONFIG_FILENAME), 'utf8')); + + describe('addBundleDefinition', () => { + it('adds a bundle to a config without any bundles', () => { + writeConfig(baseConfig()); + + const result = addBundleDefinition('main', validDefinition(), { cwd }); + + expect(result.message).toBe('Bundle "main" added successfully'); + expect(readConfig().bundles).toEqual({ main: validDefinition() }); + }); + + it('appends after existing bundles and keeps their order', () => { + writeConfig(baseConfig({ bundles: { first: validDefinition(), second: validDefinition() } })); + + addBundleDefinition('third', validDefinition(), { cwd }); + + expect(Object.keys(readConfig().bundles ?? {})).toEqual(['first', 'second', 'third']); + }); + + it('strips undefined optional fields recursively before writing', () => { + writeConfig(baseConfig()); + + addBundleDefinition( + 'main', + { + bundleName: 'main.{locale}', + dist: './dist/i18n', + typeDistFile: undefined, + tokenCasing: undefined, + collections: [ + { + name: 'common', + bundledKeyPrefix: undefined, + mergeStrategy: undefined, + entriesSelectionRules: [ + { matchingPattern: 'apps.*', matchingTags: undefined, matchingTagOperator: undefined }, + ], + }, + ], + }, + { cwd }, + ); + + const raw = readFileSync(join(cwd, CONFIG_FILENAME), 'utf8'); + expect(raw).not.toContain('undefined'); + expect(readConfig().bundles?.['main']).toEqual({ + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: [{ name: 'common', entriesSelectionRules: [{ matchingPattern: 'apps.*' }] }], + }); + }); + + it('trims the key', () => { + writeConfig(baseConfig()); + + addBundleDefinition(' main ', validDefinition(), { cwd }); + + expect(Object.keys(readConfig().bundles ?? {})).toEqual(['main']); + }); + + it('rejects an existing key', () => { + writeConfig(baseConfig({ bundles: { main: validDefinition() } })); + + expect(() => addBundleDefinition('main', validDefinition(), { cwd })).toThrow('Bundle "main" already exists'); + }); + + it('rejects an invalid key without writing', () => { + const config = baseConfig(); + writeConfig(config); + + expect(() => addBundleDefinition('my bundle', validDefinition(), { cwd })).toThrow( + 'Invalid bundle definition: Bundle name may only contain letters, numbers, hyphens and underscores.', + ); + expect(readConfig()).toEqual(config); + }); + + it('rejects an invalid definition and reports every error', () => { + const config = baseConfig(); + writeConfig(config); + + expect(() => + addBundleDefinition( + 'main', + validDefinition({ bundleName: 'main', collections: [{ name: 'ghost', entriesSelectionRules: 'All' }] }), + { cwd }, + ), + ).toThrow( + 'Invalid bundle definition: bundleName must include the {locale} placeholder so each locale gets its own file.; ' + + "Collection 'ghost' does not exist in the configuration.", + ); + expect(readConfig()).toEqual(config); + }); + }); + + describe('updateBundleDefinition', () => { + it('replaces the definition in place and keeps key order', () => { + writeConfig( + baseConfig({ + bundles: { + first: validDefinition(), + second: validDefinition(), + third: validDefinition(), + }, + }), + ); + + const updated = validDefinition({ dist: './out', typeDistFile: './src/tokens.ts' }); + const result = updateBundleDefinition('second', updated, { cwd }); + + expect(result.message).toBe('Bundle "second" updated successfully'); + const bundles = readConfig().bundles ?? {}; + expect(Object.keys(bundles)).toEqual(['first', 'second', 'third']); + expect(bundles['second']).toEqual(updated); + }); + + it('renames the bundle while keeping its position', () => { + writeConfig( + baseConfig({ bundles: { first: validDefinition(), second: validDefinition(), third: validDefinition() } }), + ); + + const result = updateBundleDefinition('second', validDefinition({ dist: './renamed' }), { + cwd, + newKey: 'middle', + }); + + expect(result.message).toBe('Bundle "second" renamed to "middle" and updated successfully'); + const bundles = readConfig().bundles ?? {}; + expect(Object.keys(bundles)).toEqual(['first', 'middle', 'third']); + expect(bundles['middle']?.dist).toBe('./renamed'); + expect(bundles['second']).toBeUndefined(); + }); + + it('treats newKey equal to key as a plain update', () => { + writeConfig(baseConfig({ bundles: { main: validDefinition() } })); + + const result = updateBundleDefinition('main', validDefinition({ dist: './x' }), { cwd, newKey: 'main' }); + + expect(result.message).toBe('Bundle "main" updated successfully'); + expect(readConfig().bundles?.['main']?.dist).toBe('./x'); + }); + + it('throws when the bundle does not exist', () => { + writeConfig(baseConfig({ bundles: { main: validDefinition() } })); + + expect(() => updateBundleDefinition('ghost', validDefinition(), { cwd })).toThrow('Bundle "ghost" not found'); + }); + + it('throws when the bundles section is missing entirely', () => { + writeConfig(baseConfig()); + + expect(() => updateBundleDefinition('main', validDefinition(), { cwd })).toThrow('Bundle "main" not found'); + }); + + it('refuses to rename onto an existing key', () => { + const config = baseConfig({ bundles: { a: validDefinition(), b: validDefinition() } }); + writeConfig(config); + + expect(() => updateBundleDefinition('a', validDefinition(), { cwd, newKey: 'b' })).toThrow( + 'Bundle "b" already exists', + ); + expect(readConfig()).toEqual(config); + }); + + it('validates the new key', () => { + writeConfig(baseConfig({ bundles: { a: validDefinition() } })); + + expect(() => updateBundleDefinition('a', validDefinition(), { cwd, newKey: 'bad key' })).toThrow( + 'Invalid bundle definition:', + ); + }); + + it('validates the definition without writing on failure', () => { + const config = baseConfig({ bundles: { main: validDefinition() } }); + writeConfig(config); + + expect(() => updateBundleDefinition('main', validDefinition({ typeDistFile: 'tokens.js' }), { cwd })).toThrow( + 'Invalid bundle definition: typeDistFile must end with a .ts extension, but got: tokens.js', + ); + expect(readConfig()).toEqual(config); + }); + }); + + describe('deleteBundleDefinition', () => { + it('removes the bundle and keeps the others in order', () => { + writeConfig(baseConfig({ bundles: { a: validDefinition(), b: validDefinition(), c: validDefinition() } })); + + const result = deleteBundleDefinition('b', { cwd }); + + expect(result.message).toBe('Bundle "b" deleted successfully'); + expect(Object.keys(readConfig().bundles ?? {})).toEqual(['a', 'c']); + }); + + it('drops the bundles section when the last bundle is removed', () => { + writeConfig(baseConfig({ bundles: { only: validDefinition() } })); + + deleteBundleDefinition('only', { cwd }); + + expect(readConfig()).not.toHaveProperty('bundles'); + }); + + it('throws when the bundle does not exist', () => { + const config = baseConfig({ bundles: { a: validDefinition() } }); + writeConfig(config); + + expect(() => deleteBundleDefinition('ghost', { cwd })).toThrow('Bundle "ghost" not found'); + expect(readConfig()).toEqual(config); + }); + }); +}); diff --git a/libs/core/src/lib/bundle/bundle-definition-operations.ts b/libs/core/src/lib/bundle/bundle-definition-operations.ts new file mode 100644 index 00000000..b9a9d1fc --- /dev/null +++ b/libs/core/src/lib/bundle/bundle-definition-operations.ts @@ -0,0 +1,166 @@ +/** + * Add / update / delete bundle definitions in `.lingo-tracker.json`. + * + * Every operation validates the definition against the freshly read config + * inside the `updateConfig` updater so the check and the write see the same + * state. Key order in `config.bundles` is preserved on update and rename. + */ + +import type { BundleDefinition, CollectionBundleDefinition, EntrySelectionRule } from '../../config/bundle-definition'; +import type { LingoTrackerConfig } from '../../config/lingo-tracker-config'; +import { updateConfig } from '../config/config-file-operations'; +import { ErrorMessages } from '../errors/error-messages'; +import { validateBundleDefinition, validateBundleKey } from './validate-bundle-definition'; + +export interface BundleDefinitionOperationOptions { + cwd?: string; +} + +export interface UpdateBundleDefinitionOptions extends BundleDefinitionOperationOptions { + /** Rename the bundle to this key while updating it. */ + newKey?: string; +} + +export function addBundleDefinition( + key: string, + definition: BundleDefinition, + options: BundleDefinitionOperationOptions = {}, +): { message: string } { + const bundleKey = assertValidKey(key); + + updateConfig((config) => { + if (config.bundles?.[bundleKey]) { + throw new Error(ErrorMessages.bundleAlreadyExists(bundleKey)); + } + + const cleaned = assertValidDefinition(definition, config); + + return { + ...config, + bundles: { + ...(config.bundles ?? {}), + [bundleKey]: cleaned, + }, + }; + }, options.cwd); + + return { message: `Bundle "${bundleKey}" added successfully` }; +} + +export function updateBundleDefinition( + key: string, + definition: BundleDefinition, + options: UpdateBundleDefinitionOptions = {}, +): { message: string } { + const bundleKey = key.trim(); + const targetKey = options.newKey === undefined ? bundleKey : assertValidKey(options.newKey); + const isRename = targetKey !== bundleKey; + + updateConfig((config) => { + const bundles = config.bundles ?? {}; + + if (!bundles[bundleKey]) { + throw new Error(ErrorMessages.bundleNotFound(bundleKey)); + } + + if (isRename && bundles[targetKey]) { + throw new Error(ErrorMessages.bundleAlreadyExists(targetKey)); + } + + const cleaned = assertValidDefinition(definition, config); + + // Rebuild the record in the original order so a rename keeps its position. + const nextBundles: Record = {}; + for (const [existingKey, existingDefinition] of Object.entries(bundles)) { + if (existingKey === bundleKey) { + nextBundles[targetKey] = cleaned; + } else { + nextBundles[existingKey] = existingDefinition; + } + } + + return { ...config, bundles: nextBundles }; + }, options.cwd); + + return isRename + ? { message: `Bundle "${bundleKey}" renamed to "${targetKey}" and updated successfully` } + : { message: `Bundle "${bundleKey}" updated successfully` }; +} + +export function deleteBundleDefinition( + key: string, + options: BundleDefinitionOperationOptions = {}, +): { message: string } { + const bundleKey = key.trim(); + + updateConfig((config) => { + const bundles = config.bundles ?? {}; + + if (!bundles[bundleKey]) { + throw new Error(ErrorMessages.bundleNotFound(bundleKey)); + } + + const { [bundleKey]: _removed, ...remaining } = bundles; + const next: LingoTrackerConfig = { ...config, bundles: remaining }; + + // Drop the `bundles` key entirely when the last bundle goes, keeping the file minimal. + if (Object.keys(remaining).length === 0) { + delete next.bundles; + } + + return next; + }, options.cwd); + + return { message: `Bundle "${bundleKey}" deleted successfully` }; +} + +function assertValidKey(key: string): string { + const trimmed = key?.trim() ?? ''; + const errors = validateBundleKey(trimmed); + if (errors.length > 0) { + throw new Error(ErrorMessages.invalidBundleDefinition(errors)); + } + return trimmed; +} + +function assertValidDefinition(definition: BundleDefinition, config: LingoTrackerConfig): BundleDefinition { + const cleaned = stripUndefined(definition); + const errors = validateBundleDefinition(cleaned, config); + if (errors.length > 0) { + throw new Error(ErrorMessages.invalidBundleDefinition(errors)); + } + return cleaned; +} + +/** + * Returns a copy of the definition with every `undefined` optional field + * removed (recursively through collections and rules) so nothing spurious is + * serialised into the config file. + */ +function stripUndefined(definition: BundleDefinition): BundleDefinition { + const collections = + definition.collections === 'All' + ? ('All' as const) + : definition.collections.map((collection) => stripCollection(collection)); + + return omitUndefined({ ...definition, collections }); +} + +function stripCollection(collection: CollectionBundleDefinition): CollectionBundleDefinition { + const entriesSelectionRules = + collection.entriesSelectionRules === 'All' + ? ('All' as const) + : collection.entriesSelectionRules.map((rule) => omitUndefined({ ...rule })); + + return omitUndefined({ ...collection, entriesSelectionRules }); +} + +function omitUndefined(value: T): T { + const result: Record = {}; + for (const [field, fieldValue] of Object.entries(value)) { + if (fieldValue !== undefined) { + result[field] = fieldValue; + } + } + return result as T; +} diff --git a/libs/core/src/lib/bundle/generate-bundle.spec.ts b/libs/core/src/lib/bundle/generate-bundle.spec.ts index 194602d3..904d29bc 100644 --- a/libs/core/src/lib/bundle/generate-bundle.spec.ts +++ b/libs/core/src/lib/bundle/generate-bundle.spec.ts @@ -547,7 +547,7 @@ describe('generate-bundle', () => { await generateBundle(params); - expect(generateBundleTypes).toHaveBeenCalledWith('main', mockConfig, 'upperCase', undefined); + expect(generateBundleTypes).toHaveBeenCalledWith('main', mockConfig, 'upperCase', undefined, bundleDefinition); }); it('should not invoke type generation when typeDistFile is missing', async () => { @@ -598,7 +598,7 @@ describe('generate-bundle', () => { await generateBundle(params); - expect(generateBundleTypes).toHaveBeenCalledWith('main', mockConfig, 'upperCase', undefined); + expect(generateBundleTypes).toHaveBeenCalledWith('main', mockConfig, 'upperCase', undefined, bundleDefinition); }); it('should use typeDistFile and not emit a deprecation warning when both typeDist and typeDistFile are present', async () => { @@ -630,7 +630,7 @@ describe('generate-bundle', () => { // generateBundleTypes is mocked here so no real deprecation logic runs. // The no-warn behaviour for the both-keys-present scenario is verified in generate-types.spec.ts. - expect(generateBundleTypes).toHaveBeenCalledWith('main', mockConfig, 'upperCase', undefined); + expect(generateBundleTypes).toHaveBeenCalledWith('main', mockConfig, 'upperCase', undefined, bundleDefinition); }); it('should pass tokenConstantName through to generateBundleTypes', async () => { @@ -659,7 +659,13 @@ describe('generate-bundle', () => { await generateBundle(params); - expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith('main', mockConfig, 'upperCase', 'CUSTOM_TOKENS'); + expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith( + 'main', + mockConfig, + 'upperCase', + 'CUSTOM_TOKENS', + bundleDefinition, + ); }); it('should capture type generation errors in warnings', async () => { @@ -714,7 +720,13 @@ describe('generate-bundle', () => { await generateBundle(params); - expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith('main', mockConfig, 'camelCase', undefined); + expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith( + 'main', + mockConfig, + 'camelCase', + undefined, + bundleDefinition, + ); }); it('should use bundle-level tokenCasing when no CLI override is given', async () => { @@ -732,7 +744,13 @@ describe('generate-bundle', () => { await generateBundle(params); - expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith('main', mockConfig, 'camelCase', undefined); + expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith( + 'main', + mockConfig, + 'camelCase', + undefined, + bundleDefWithCasing, + ); }); it('should use global config tokenCasing when no CLI or bundle-level override is given', async () => { @@ -750,7 +768,104 @@ describe('generate-bundle', () => { await generateBundle(params); - expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith('main', configWithCasing, 'camelCase', undefined); + expect(vi.mocked(generateBundleTypes)).toHaveBeenCalledWith( + 'main', + configWithCasing, + 'camelCase', + undefined, + bundleDefinition, + ); + }); + }); + + describe('onProgress', () => { + const bundleDefinition: BundleDefinition = { + bundleName: 'main.{locale}', + dist: '/dist/bundles', + collections: 'All', + }; + + it('emits one event per locale, in order, with a 1-based index and the output file', async () => { + vi.spyOn(resourceLoader, 'loadCollectionResources').mockReturnValue([{ key: 'buttons.ok', value: 'OK' }]); + const onProgress = vi.fn(); + + await generateBundle({ bundleKey: 'main', bundleDefinition, config: mockConfig, onProgress }); + + expect(onProgress.mock.calls.map((call) => call[0])).toEqual([ + { locale: 'en', index: 1, total: 3, file: path.join('/dist/bundles', 'main.en.json') }, + { locale: 'fr', index: 2, total: 3, file: path.join('/dist/bundles', 'main.fr.json') }, + { locale: 'es', index: 3, total: 3, file: path.join('/dist/bundles', 'main.es.json') }, + ]); + }); + + it('counts the debug-keys locale in total and emits it last', async () => { + vi.spyOn(resourceLoader, 'loadCollectionResources').mockReturnValue([{ key: 'buttons.ok', value: 'OK' }]); + const onProgress = vi.fn(); + + await generateBundle({ + bundleKey: 'main', + bundleDefinition, + config: mockConfig, + locales: ['en', 'fr'], + debugKeysLocale: '99', + onProgress, + }); + + expect(onProgress).toHaveBeenCalledTimes(3); + expect(onProgress.mock.calls.map((call) => call[0].total)).toEqual([3, 3, 3]); + expect(onProgress).toHaveBeenLastCalledWith({ + locale: '99', + index: 3, + total: 3, + file: path.join('/dist/bundles', 'main.99.json'), + }); + }); + + it('still emits for a locale whose bundle turns out empty', async () => { + vi.spyOn(resourceLoader, 'loadCollectionResources').mockReturnValue([]); + const onProgress = vi.fn(); + + await generateBundle({ bundleKey: 'main', bundleDefinition, config: mockConfig, locales: ['en'], onProgress }); + + expect(onProgress).toHaveBeenCalledTimes(1); + }); + }); + + describe('keysPerLocale', () => { + const bundleDefinition: BundleDefinition = { + bundleName: 'main.{locale}', + dist: '/dist/bundles', + collections: 'All', + }; + + it('reports the number of keys written for each processed locale', async () => { + // Two collections ('default' and 'admin') each return the same two keys, + // so the merged bundle holds two keys per locale. + vi.spyOn(resourceLoader, 'loadCollectionResources').mockReturnValue([ + { key: 'buttons.ok', value: 'OK' }, + { key: 'buttons.cancel', value: 'Cancel' }, + ]); + + const result = await generateBundle({ bundleKey: 'main', bundleDefinition, config: mockConfig }); + + expect(result.keysPerLocale).toEqual({ en: 2, fr: 2, es: 2 }); + }); + + it('omits empty locales and includes the debug-keys locale', async () => { + vi.spyOn(resourceLoader, 'loadCollectionResources').mockImplementation((_folder, locale) => + locale === 'fr' ? [] : [{ key: 'buttons.ok', value: 'OK' }], + ); + + const result = await generateBundle({ + bundleKey: 'main', + bundleDefinition, + config: mockConfig, + locales: ['en', 'fr'], + debugKeysLocale: '99', + }); + + expect(result.keysPerLocale).toEqual({ en: 1, '99': 1 }); + expect(result.keysPerLocale).not.toHaveProperty('fr'); }); }); diff --git a/libs/core/src/lib/bundle/generate-bundle.ts b/libs/core/src/lib/bundle/generate-bundle.ts index b948acc2..643146f0 100644 --- a/libs/core/src/lib/bundle/generate-bundle.ts +++ b/libs/core/src/lib/bundle/generate-bundle.ts @@ -45,6 +45,22 @@ export interface GenerateBundleParams { * skipped for this bundle — the keys themselves have no ICU content. */ readonly debugKeysLocale?: string; + /** + * Called at the start of each locale iteration (the debug-keys locale, when + * requested, is included in `total` and emitted last). + */ + readonly onProgress?: (event: BundleProgressEvent) => void; +} + +export interface BundleProgressEvent { + /** Locale about to be processed (the debug-keys locale code for the debug file). */ + readonly locale: string; + /** 1-based position of this locale in the run. */ + readonly index: number; + /** Total number of files this run will attempt, including the debug-keys file. */ + readonly total: number; + /** Output path of the file about to be written. */ + readonly file: string; } export interface GenerateBundleResult { @@ -52,9 +68,27 @@ export interface GenerateBundleResult { readonly filesGenerated: number; readonly warnings: string[]; readonly localesProcessed: string[]; + /** Number of keys written per processed locale (empty locales are omitted). */ + readonly keysPerLocale: Record; readonly typeGenerationResult?: GenerateTypesResult; } +/** + * Optional trace collected while merging collections into a bundle. + * Used by the dry-run planner to report key conflicts and winning origins. + */ +export interface BundleKeyTrace { + /** Final (prefixed) keys that were defined by more than one resource. */ + readonly conflicts: Set; + /** Winning origin per final key. */ + readonly origins: Map; +} + +export interface BundleKeyOrigin { + readonly collectionName: string; + readonly sourceKey: string; +} + /** * Generates translation bundle files for specified bundle configuration * @@ -71,9 +105,11 @@ export async function generateBundle(params: GenerateBundleParams): Promise = {}; // Resolve token casing: CLI override → bundle config → global config → default const resolvedTokenCasing: TokenCasing = @@ -89,8 +125,18 @@ export async function generateBundle(params: GenerateBundleParams): Promise(); + const totalFiles = targetLocales.length + (debugKeysLocale ? 1 : 0); + let progressIndex = 0; for (const locale of targetLocales) { + progressIndex++; + onProgress?.({ + locale, + index: progressIndex, + total: totalFiles, + file: getBundleOutputPath(bundleDefinition, locale), + }); + const bundleData = collectBundleData( bundleDefinition, config, @@ -112,9 +158,18 @@ export async function generateBundle(params: GenerateBundleParams): Promise, + trace?: BundleKeyTrace, ): Record { const bundleData: Record = {}; @@ -198,6 +265,7 @@ function collectBundleData( warnings, cache, collectionConfig.tags, + trace, ); } } else { @@ -219,6 +287,7 @@ function collectBundleData( warnings, cache, collectionConfig.tags, + trace, ); } } @@ -239,6 +308,7 @@ function processCollection( warnings: string[], cache: Map, collectionTags?: string[], + trace?: BundleKeyTrace, ): void { const resources = loadCollectionResources(translationsFolder, locale, baseLocale, cache, collectionTags); const filteredResources = filterResources(resources, collectionDef); @@ -268,14 +338,17 @@ function processCollection( } if (finalKey in bundleData) { + trace?.conflicts.add(finalKey); if (mergeStrategy === 'override') { bundleData[finalKey] = finalValue; + trace?.origins.set(finalKey, { collectionName: collectionDef.name, sourceKey: resource.key }); } // merge (default) - keep existing (first wins) // Skip to next resource since key already exists } else { // New key - add it bundleData[finalKey] = finalValue; + trace?.origins.set(finalKey, { collectionName: collectionDef.name, sourceKey: resource.key }); } } } @@ -308,7 +381,7 @@ function matchesAnyRule(resource: FlatResource, rules: EntrySelectionRule[]): bo /** * Determines output file path for bundle */ -function getBundleOutputPath(bundleDefinition: BundleDefinition, locale: string): string { +export function getBundleOutputPath(bundleDefinition: BundleDefinition, locale: string): string { const fileName = bundleDefinition.bundleName.replace('{locale}', locale); return path.join(bundleDefinition.dist, `${fileName}.json`); } diff --git a/libs/core/src/lib/bundle/index.ts b/libs/core/src/lib/bundle/index.ts index cf5cd1c7..1775ac57 100644 --- a/libs/core/src/lib/bundle/index.ts +++ b/libs/core/src/lib/bundle/index.ts @@ -1,4 +1,7 @@ export * from './generate-bundle'; +export * from './plan-bundle'; +export * from './validate-bundle-definition'; +export * from './bundle-definition-operations'; export * from './pattern-matcher'; export * from './tag-filter'; export * from './hierarchy-builder'; diff --git a/libs/core/src/lib/bundle/plan-bundle.spec.ts b/libs/core/src/lib/bundle/plan-bundle.spec.ts new file mode 100644 index 00000000..988476e0 --- /dev/null +++ b/libs/core/src/lib/bundle/plan-bundle.spec.ts @@ -0,0 +1,384 @@ +import * as fs from 'fs'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { BundleDefinition } from '../../config/bundle-definition'; +import type { LingoTrackerConfig } from '../../config/lingo-tracker-config'; +import { planBundle } from './plan-bundle'; +import type { FlatResource } from './resource-loader'; +import * as resourceLoader from './resource-loader'; +import { generateBundleTypes } from './type-generation/generate-types'; + +vi.mock('fs'); +vi.mock('./resource-loader'); +vi.mock('./type-generation/generate-types'); + +describe('planBundle', () => { + const cwd = '/project'; + let config: LingoTrackerConfig; + + beforeEach(() => { + vi.clearAllMocks(); + + config = { + exportFolder: 'dist/export', + importFolder: 'dist/import', + baseLocale: 'en', + locales: ['en', 'fr'], + collections: { + common: { translationsFolder: '/translations/common' }, + admin: { translationsFolder: '/translations/admin' }, + }, + }; + + vi.spyOn(fs, 'existsSync').mockReturnValue(false); + vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined); + vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + /** Returns resources keyed by translations folder so each collection has distinct content. */ + function mockResourcesByFolder(byFolder: Record): void { + vi.spyOn(resourceLoader, 'loadCollectionResources').mockImplementation((folder) => byFolder[folder] ?? []); + } + + const definition: BundleDefinition = { + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: 'All', + }; + + it('lists one bundle file per locale with resolved paths and exists flags', () => { + mockResourcesByFolder({ '/translations/common': [{ key: 'buttons.ok', value: 'OK' }] }); + vi.mocked(fs.existsSync).mockImplementation((p) => String(p).endsWith('main.en.json')); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd }); + + expect(plan.bundleKey).toBe('main'); + expect(plan.locales).toEqual(['en', 'fr']); + expect(plan.files).toEqual([ + { + path: path.join('./dist/i18n', 'main.en.json'), + absolutePath: path.resolve(cwd, 'dist/i18n/main.en.json'), + kind: 'bundle', + locale: 'en', + exists: true, + keysCount: 1, + }, + { + path: path.join('./dist/i18n', 'main.fr.json'), + absolutePath: path.resolve(cwd, 'dist/i18n/main.fr.json'), + kind: 'bundle', + locale: 'fr', + exists: false, + keysCount: 1, + }, + ]); + expect(plan.keysPerLocale).toEqual({ en: 1, fr: 1 }); + }); + + it('respects a locales subset', () => { + mockResourcesByFolder({ '/translations/common': [{ key: 'a', value: 'A' }] }); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd, locales: ['fr'] }); + + expect(plan.locales).toEqual(['fr']); + expect(plan.files.map((file) => file.locale)).toEqual(['fr']); + expect(plan.keysPerLocale).toEqual({ fr: 1 }); + // The base locale is still consulted for the example key even when not planned. + expect(plan.exampleKey).toEqual({ collectionName: 'common', sourceKey: 'a', bundledKey: 'a' }); + }); + + it('never writes to disk and never calls type generation', () => { + mockResourcesByFolder({ '/translations/common': [{ key: 'a', value: 'A' }] }); + + planBundle({ + bundleKey: 'main', + bundleDefinition: { ...definition, typeDistFile: './src/tokens.ts' }, + config, + cwd, + }); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(fs.mkdirSync).not.toHaveBeenCalled(); + expect(generateBundleTypes).not.toHaveBeenCalled(); + }); + + it('omits the types file when types are not configured', () => { + mockResourcesByFolder({ '/translations/common': [{ key: 'a', value: 'A' }] }); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd }); + + expect(plan.files.filter((file) => file.kind === 'types')).toEqual([]); + expect(plan.exampleKey?.tokenPath).toBeUndefined(); + }); + + it('adds a types file with the base-locale key count when configured', () => { + mockResourcesByFolder({ + '/translations/common': [ + { key: 'a', value: 'A' }, + { key: 'b', value: 'B' }, + ], + }); + + const plan = planBundle({ + bundleKey: 'main', + bundleDefinition: { ...definition, typeDistFile: './src/tokens.ts' }, + config, + cwd, + }); + + const typesFile = plan.files.find((file) => file.kind === 'types'); + expect(typesFile).toEqual({ + path: './src/tokens.ts', + absolutePath: path.resolve(cwd, 'src/tokens.ts'), + kind: 'types', + exists: false, + keysCount: 2, + }); + expect(typesFile?.locale).toBeUndefined(); + }); + + it('warns about empty locales and reports zero keys', () => { + mockResourcesByFolder({}); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd }); + + expect(plan.keysPerLocale).toEqual({ en: 0, fr: 0 }); + expect(plan.warnings).toContain("Bundle 'main' for locale 'en' is empty"); + expect(plan.warnings).toContain("Bundle 'main' for locale 'fr' is empty"); + expect(plan.exampleKey).toBeUndefined(); + }); + + describe('conflicts', () => { + beforeEach(() => { + mockResourcesByFolder({ + '/translations/common': [ + { key: 'shared.title', value: 'Common title' }, + { key: 'common.only', value: 'Only in common' }, + ], + '/translations/admin': [{ key: 'shared.title', value: 'Admin title' }], + }); + }); + + it('records conflicting keys once, regardless of locale count', () => { + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd }); + + expect(plan.conflictsCount).toBe(1); + expect(plan.conflictKeys).toEqual(['shared.title']); + // Conflicting keys are still counted once per locale in the output. + expect(plan.keysPerLocale).toEqual({ en: 2, fr: 2 }); + }); + + it('attributes the example key to the first collection under merge', () => { + const plan = planBundle({ + bundleKey: 'main', + bundleDefinition: { + ...definition, + collections: [ + { name: 'common', entriesSelectionRules: 'All' }, + { name: 'admin', entriesSelectionRules: 'All', mergeStrategy: 'merge' }, + ], + }, + config, + cwd, + }); + + expect(plan.conflictKeys).toEqual(['shared.title']); + expect(plan.exampleKey).toEqual({ + collectionName: 'common', + sourceKey: 'shared.title', + bundledKey: 'shared.title', + }); + }); + + it('attributes the example key to the overriding collection under override', () => { + const plan = planBundle({ + bundleKey: 'main', + bundleDefinition: { + ...definition, + collections: [ + { name: 'common', entriesSelectionRules: 'All' }, + { name: 'admin', entriesSelectionRules: 'All', mergeStrategy: 'override' }, + ], + }, + config, + cwd, + }); + + expect(plan.conflictKeys).toEqual(['shared.title']); + expect(plan.exampleKey).toEqual({ + collectionName: 'admin', + sourceKey: 'shared.title', + bundledKey: 'shared.title', + }); + }); + + it('reports no conflicts when prefixes separate the collections', () => { + const plan = planBundle({ + bundleKey: 'main', + bundleDefinition: { + ...definition, + collections: [ + { name: 'common', entriesSelectionRules: 'All' }, + { name: 'admin', entriesSelectionRules: 'All', bundledKeyPrefix: 'admin' }, + ], + }, + config, + cwd, + }); + + expect(plan.conflictsCount).toBe(0); + expect(plan.conflictKeys).toEqual([]); + expect(plan.keysPerLocale.en).toBe(3); + }); + }); + + describe('hierarchical conflicts', () => { + it('reports a key that is both a leaf and a parent, and warns about it', () => { + mockResourcesByFolder({ + '/translations/common': [ + { key: 'buttons.ok', value: 'OK' }, + { key: 'buttons.ok.label', value: 'OK label' }, + ], + }); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd }); + + expect(plan.hierarchicalConflicts).toEqual(['buttons.ok']); + // It is not a cross-collection conflict, so the existing counter stays clear. + expect(plan.conflictsCount).toBe(0); + expect(plan.conflictKeys).toEqual([]); + expect( + plan.warnings.some((warning) => warning.includes('Hierarchical conflict') && warning.includes('buttons.ok')), + ).toBe(true); + }); + + it('reports a collision introduced by a bundled key prefix', () => { + mockResourcesByFolder({ + '/translations/common': [{ key: 'ok', value: 'OK' }], + '/translations/admin': [{ key: 'ok.label', value: 'OK label' }], + }); + + const plan = planBundle({ + bundleKey: 'main', + bundleDefinition: { + ...definition, + collections: [ + { name: 'common', entriesSelectionRules: 'All', bundledKeyPrefix: 'buttons' }, + { name: 'admin', entriesSelectionRules: 'All', bundledKeyPrefix: 'buttons' }, + ], + }, + config, + cwd, + }); + + expect(plan.hierarchicalConflicts).toEqual(['buttons.ok']); + }); + + it('is empty for a well-formed key set', () => { + mockResourcesByFolder({ + '/translations/common': [ + { key: 'buttons.ok', value: 'OK' }, + { key: 'buttons.cancel', value: 'Cancel' }, + ], + }); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, cwd }); + + expect(plan.hierarchicalConflicts).toEqual([]); + expect(plan.warnings).toEqual([]); + }); + }); + + describe('exampleKey', () => { + beforeEach(() => { + mockResourcesByFolder({ '/translations/common': [{ key: 'buttons.file-upload', value: 'Upload' }] }); + }); + + const prefixed: BundleDefinition = { + ...definition, + collections: [{ name: 'common', entriesSelectionRules: 'All', bundledKeyPrefix: 'shared' }], + }; + + it('includes the prefix in bundledKey but not sourceKey', () => { + const plan = planBundle({ bundleKey: 'main', bundleDefinition: prefixed, config, cwd }); + + expect(plan.exampleKey).toEqual({ + collectionName: 'common', + sourceKey: 'buttons.file-upload', + bundledKey: 'shared.buttons.file-upload', + }); + }); + + it('builds an upperCase tokenPath from the derived constant name by default', () => { + const plan = planBundle({ + bundleKey: 'core-ui', + bundleDefinition: { ...prefixed, typeDistFile: './src/tokens.ts' }, + config, + cwd, + }); + + expect(plan.exampleKey?.tokenPath).toBe('CORE_UI_TOKENS.SHARED.BUTTONS.FILE_UPLOAD'); + }); + + it('honours the casing and constant-name precedence chain (param > definition > config)', () => { + const withTypes: BundleDefinition = { + ...prefixed, + typeDistFile: './src/tokens.ts', + tokenCasing: 'upperCase', + tokenConstantName: 'DEF_TOKENS', + }; + + const fromDefinition = planBundle({ + bundleKey: 'main', + bundleDefinition: withTypes, + config: { ...config, tokenCasing: 'camelCase' }, + cwd, + }); + expect(fromDefinition.exampleKey?.tokenPath).toBe('DEF_TOKENS.SHARED.BUTTONS.FILE_UPLOAD'); + + const fromParams = planBundle({ + bundleKey: 'main', + bundleDefinition: withTypes, + config, + cwd, + tokenCasing: 'camelCase', + tokenConstantName: 'paramTokens', + }); + expect(fromParams.exampleKey?.tokenPath).toBe('paramTokens.shared.buttons.fileUpload'); + + const fromConfig = planBundle({ + bundleKey: 'main', + bundleDefinition: { ...prefixed, typeDistFile: './src/tokens.ts' }, + config: { ...config, tokenCasing: 'camelCase' }, + cwd, + }); + expect(fromConfig.exampleKey?.tokenPath).toBe('MAIN_TOKENS.shared.buttons.fileUpload'); + }); + }); + + it('warns about unknown collections in an explicit list', () => { + mockResourcesByFolder({}); + + const plan = planBundle({ + bundleKey: 'main', + bundleDefinition: { ...definition, collections: [{ name: 'ghost', entriesSelectionRules: 'All' }] }, + config, + cwd, + locales: ['en'], + }); + + expect(plan.warnings).toContain("Collection 'ghost' not found in config"); + }); + + it('defaults cwd to process.cwd() for exists checks', () => { + mockResourcesByFolder({ '/translations/common': [{ key: 'a', value: 'A' }] }); + + const plan = planBundle({ bundleKey: 'main', bundleDefinition: definition, config, locales: ['en'] }); + + expect(plan.files[0]?.absolutePath).toBe(path.resolve(process.cwd(), 'dist/i18n/main.en.json')); + }); +}); diff --git a/libs/core/src/lib/bundle/plan-bundle.ts b/libs/core/src/lib/bundle/plan-bundle.ts new file mode 100644 index 00000000..8362e844 --- /dev/null +++ b/libs/core/src/lib/bundle/plan-bundle.ts @@ -0,0 +1,240 @@ +/** + * Dry-run planner for bundle generation. + * + * `planBundle` computes everything `generateBundle` would do — which files it + * would write, how many keys each locale gets, which keys collide between + * collections — without touching the filesystem. It never calls type + * generation; the types file is only described. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { detectHierarchicalConflicts } from '@simoncodes-ca/domain'; +import { type BundleDefinition, hasTypeDistConfigured, type TokenCasing } from '../../config/bundle-definition'; +import type { LingoTrackerConfig } from '../../config/lingo-tracker-config'; +import type { ResourceEntries } from '../../resource/resource-entry'; +import { type BundleKeyTrace, collectBundleData, getBundleOutputPath } from './generate-bundle'; +import { + bundleKeyToConstantName, + segmentToPropertyName, + splitKeyIntoSegments, +} from './type-generation/key-transformer'; + +export interface PlanBundleParams { + readonly bundleKey: string; + readonly bundleDefinition: BundleDefinition; + readonly config: LingoTrackerConfig; + /** Subset of locales to plan for (defaults to `config.locales`). */ + readonly locales?: string[]; + /** Override for token casing; same precedence as `generateBundle`. */ + readonly tokenCasing?: TokenCasing; + /** Override for the generated constant name; same precedence as `generateBundle`. */ + readonly tokenConstantName?: string; + /** Override for ICU → Transloco transformation; same precedence as `generateBundle`. */ + readonly transformICUToTransloco?: boolean; + /** Base directory used to resolve relative paths for `exists` checks (default: `process.cwd()`). */ + readonly cwd?: string; +} + +export interface BundlePlanFile { + /** Path as configured (relative or absolute), exactly what generation would use. */ + readonly path: string; + /** Path resolved against `cwd`. */ + readonly absolutePath: string; + readonly kind: 'bundle' | 'types'; + /** Present for `bundle` files only. */ + readonly locale?: string; + /** Whether a file already exists at `absolutePath`. */ + readonly exists: boolean; + /** Keys the file would contain. For the types file this is the base-locale key count. */ + readonly keysCount: number; +} + +export interface BundlePlanExampleKey { + readonly collectionName: string; + readonly sourceKey: string; + /** Key as it appears in the bundle output (after `bundledKeyPrefix`). */ + readonly bundledKey: string; + /** Property path in the generated types constant; only when types are configured. */ + readonly tokenPath?: string; +} + +export interface BundlePlan { + readonly bundleKey: string; + readonly locales: string[]; + readonly files: BundlePlanFile[]; + readonly keysPerLocale: Record; + /** Number of keys defined by more than one collection. */ + readonly conflictsCount: number; + /** Keys defined by more than one collection. */ + readonly conflictKeys: string[]; + /** + * Base-locale keys that are both a leaf and a parent, e.g. `buttons.ok` + * alongside `buttons.ok.label`. Generation cannot build a hierarchy from + * these, so a plan that reports any of them describes a bundle that would + * fail. Each one is also echoed in `warnings`. + */ + readonly hierarchicalConflicts: string[]; + readonly exampleKey?: BundlePlanExampleKey; + readonly warnings: string[]; +} + +/** + * Plans a bundle run without writing anything. + */ +export function planBundle(params: PlanBundleParams): BundlePlan { + const { + bundleKey, + bundleDefinition, + config, + locales, + tokenCasing: tokenCasingOverride, + tokenConstantName: tokenConstantNameOverride, + transformICUToTransloco: transformICUToTranslocoOverride, + } = params; + const cwd = params.cwd ?? process.cwd(); + + const resolvedTokenCasing: TokenCasing = + tokenCasingOverride ?? bundleDefinition.tokenCasing ?? config.tokenCasing ?? 'upperCase'; + const resolvedTransformICUToTransloco: boolean = + transformICUToTranslocoOverride ?? + bundleDefinition.transformICUToTransloco ?? + config.transformICUToTransloco ?? + true; + const resolvedConstantName = + tokenConstantNameOverride ?? bundleDefinition.tokenConstantName ?? bundleKeyToConstantName(bundleKey); + + const targetLocales = locales ?? config.locales; + const warnings: string[] = []; + const keysPerLocale: Record = {}; + const files: BundlePlanFile[] = []; + const resourceCache = new Map(); + + // Trace only the base locale: conflicts are a property of the key set, which + // is identical across locales, so tracing every locale would duplicate work. + const trace: BundleKeyTrace = { conflicts: new Set(), origins: new Map() }; + let baseLocaleData: Record | undefined; + + for (const locale of targetLocales) { + const isBaseLocale = locale === config.baseLocale; + const bundleData = collectBundleData( + bundleDefinition, + config, + locale, + warnings, + resolvedTransformICUToTransloco, + resourceCache, + isBaseLocale ? trace : undefined, + ); + + if (isBaseLocale) { + baseLocaleData = bundleData; + } + + const keysCount = Object.keys(bundleData).length; + keysPerLocale[locale] = keysCount; + + if (keysCount === 0) { + warnings.push(`Bundle '${bundleKey}' for locale '${locale}' is empty`); + } + + const outputPath = getBundleOutputPath(bundleDefinition, locale); + files.push(describeFile(outputPath, 'bundle', keysCount, cwd, locale)); + } + + // The base locale drives conflict detection and the example key. When it is + // not among the target locales, collect it once without recording a file. + if (!baseLocaleData) { + baseLocaleData = collectBundleData( + bundleDefinition, + config, + config.baseLocale, + warnings, + resolvedTransformICUToTransloco, + resourceCache, + trace, + ); + } + + const typesConfigured = hasTypeDistConfigured(bundleDefinition); + const baseKeysCount = Object.keys(baseLocaleData).length; + + if (typesConfigured && bundleDefinition.typeDistFile) { + files.push(describeFile(bundleDefinition.typeDistFile, 'types', baseKeysCount, cwd)); + } + + const conflictKeys = Array.from(trace.conflicts).sort(); + + // A key that is both a leaf and a parent makes `buildHierarchy` throw during + // generation, so surface it in the plan rather than letting the run explode. + const hierarchicalConflicts = detectHierarchicalConflicts(Object.keys(baseLocaleData)).sort(); + for (const key of hierarchicalConflicts) { + warnings.push( + `Hierarchical conflict: bundled key '${key}' has a value and child keys; generation would fail. ` + + `Remove the entry or its children from bundle '${bundleKey}'.`, + ); + } + + const exampleKey = pickExampleKey(baseLocaleData, trace, typesConfigured, resolvedConstantName, resolvedTokenCasing); + + return { + bundleKey, + locales: [...targetLocales], + files, + keysPerLocale, + conflictsCount: conflictKeys.length, + conflictKeys, + hierarchicalConflicts, + exampleKey, + warnings, + }; +} + +function describeFile( + filePath: string, + kind: BundlePlanFile['kind'], + keysCount: number, + cwd: string, + locale?: string, +): BundlePlanFile { + const absolutePath = path.resolve(cwd, filePath); + const file: BundlePlanFile = { + path: filePath, + absolutePath, + kind, + exists: fs.existsSync(absolutePath), + keysCount, + }; + return locale === undefined ? file : { ...file, locale }; +} + +function pickExampleKey( + baseLocaleData: Record, + trace: BundleKeyTrace, + typesConfigured: boolean, + constantName: string, + tokenCasing: TokenCasing, +): BundlePlanExampleKey | undefined { + const [firstKey] = Object.keys(baseLocaleData); + if (firstKey === undefined) { + return undefined; + } + + const origin = trace.origins.get(firstKey); + if (!origin) { + return undefined; + } + + const example: BundlePlanExampleKey = { + collectionName: origin.collectionName, + sourceKey: origin.sourceKey, + bundledKey: firstKey, + }; + + if (!typesConfigured) { + return example; + } + + const segments = splitKeyIntoSegments(firstKey).map((segment) => segmentToPropertyName(segment, tokenCasing)); + return { ...example, tokenPath: [constantName, ...segments].join('.') }; +} diff --git a/libs/core/src/lib/bundle/type-generation/generate-types.spec.ts b/libs/core/src/lib/bundle/type-generation/generate-types.spec.ts index dc90b6d4..72192b82 100644 --- a/libs/core/src/lib/bundle/type-generation/generate-types.spec.ts +++ b/libs/core/src/lib/bundle/type-generation/generate-types.spec.ts @@ -184,6 +184,57 @@ describe('generateBundleTypes', () => { }); }); + describe('bundleDefinition parameter', () => { + it('should prefer the passed definition over the one in config', async () => { + vi.mocked(resourceLoader.loadCollectionResources).mockReturnValue([{ key: 'ok', value: 'OK' }]); + + const result = await generateBundleTypes('main', mockConfig, 'upperCase', undefined, { + bundleName: 'main', + dist: 'dist/i18n', + typeDistFile: 'src/generated/other-tokens.ts', + tokenConstantName: 'PASSED_TOKENS', + collections: [{ name: 'common', entriesSelectionRules: 'All', bundledKeyPrefix: 'passed' }], + }); + + expect(result.fileGenerated).toBe(true); + expect(result.typeDistFile).toBe('/abs/src/generated/other-tokens.ts'); + expect(fs.writeFileSync).toHaveBeenCalledWith( + '/abs/src/generated/other-tokens.ts', + expect.stringContaining("OK: 'passed.ok'"), + 'utf-8', + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('export const PASSED_TOKENS'), + 'utf-8', + ); + // Only the passed definition's single collection is loaded, not both from 'All'. + expect(resourceLoader.loadCollectionResources).toHaveBeenCalledTimes(1); + }); + + it('should generate for a bundle key that is absent from config when a definition is passed', async () => { + vi.mocked(resourceLoader.loadCollectionResources).mockReturnValue([{ key: 'ok', value: 'OK' }]); + + const result = await generateBundleTypes('unsaved', mockConfig, 'upperCase', undefined, { + bundleName: 'unsaved', + dist: 'dist/i18n', + typeDistFile: 'src/generated/unsaved.ts', + collections: 'All', + }); + + expect(result.fileGenerated).toBe(true); + expect(result.keysCount).toBe(1); + }); + + it('should fall back to the config definition when none is passed', async () => { + vi.mocked(resourceLoader.loadCollectionResources).mockReturnValue([{ key: 'ok', value: 'OK' }]); + + const result = await generateBundleTypes('main', mockConfig, 'upperCase', undefined, undefined); + + expect(result.typeDistFile).toBe('/abs/src/generated/main-tokens.ts'); + }); + }); + describe('tokenConstantName', () => { it('should use the provided tokenConstantName parameter as the constant name', async () => { vi.mocked(resourceLoader.loadCollectionResources).mockReturnValue([{ key: 'buttons.ok', value: 'OK' }]); diff --git a/libs/core/src/lib/bundle/type-generation/generate-types.ts b/libs/core/src/lib/bundle/type-generation/generate-types.ts index 4281864a..00d9ed06 100644 --- a/libs/core/src/lib/bundle/type-generation/generate-types.ts +++ b/libs/core/src/lib/bundle/type-generation/generate-types.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { LingoTrackerConfig } from '../../../config/lingo-tracker-config'; -import { hasTypeDistConfigured, type TokenCasing } from '../../../config/bundle-definition'; +import { type BundleDefinition, hasTypeDistConfigured, type TokenCasing } from '../../../config/bundle-definition'; import { loadCollectionResources } from '../resource-loader'; import { matchesPattern } from '../pattern-matcher'; import { matchesTags } from '../tag-filter'; @@ -24,8 +24,11 @@ export async function generateBundleTypes( config: LingoTrackerConfig, tokenCasing: TokenCasing = 'upperCase', tokenConstantName?: string, + bundleDefinition?: BundleDefinition, ): Promise { - const bundleDef = config.bundles?.[bundleKey]; + // An explicitly passed definition wins over the one stored in config, so + // callers can generate types for an unsaved or renamed bundle. + const bundleDef = bundleDefinition ?? config.bundles?.[bundleKey]; // Support deprecated 'typeDist' property — read the legacy value without mutating the config object const legacyTypeDist = (bundleDef as unknown as Record)?.['typeDist']; diff --git a/libs/core/src/lib/bundle/type-generation/key-transformer.ts b/libs/core/src/lib/bundle/type-generation/key-transformer.ts index a7b61d63..81f36776 100644 --- a/libs/core/src/lib/bundle/type-generation/key-transformer.ts +++ b/libs/core/src/lib/bundle/type-generation/key-transformer.ts @@ -1,82 +1,4 @@ -// ES2022 + TypeScript contextual keywords that cannot be used as bare identifiers in a const declaration. -const JS_RESERVED_WORDS = new Set([ - // ES2022 reserved words - 'break', - 'case', - 'catch', - 'class', - 'const', - 'continue', - 'debugger', - 'default', - 'delete', - 'do', - 'else', - 'export', - 'extends', - 'false', - 'finally', - 'for', - 'function', - 'if', - 'import', - 'in', - 'instanceof', - 'let', - 'new', - 'null', - 'return', - 'static', - 'super', - 'switch', - 'this', - 'throw', - 'true', - 'try', - 'typeof', - 'var', - 'void', - 'while', - 'with', - 'yield', - 'enum', - 'await', - 'implements', - 'interface', - 'package', - 'private', - 'protected', - 'public', - // TypeScript contextual keywords - 'abstract', - 'as', - 'asserts', - 'async', - 'declare', - 'from', - 'global', - 'infer', - 'is', - 'keyof', - 'module', - 'namespace', - 'never', - 'of', - 'out', - 'override', - 'readonly', - 'require', - 'satisfies', - 'symbol', - 'type', - 'unique', - 'unknown', - 'using', - // Globals that should not be shadowed - 'undefined', - 'Infinity', - 'NaN', -]); +import { isJavaScriptReservedWord, JS_IDENTIFIER_PATTERN } from '@simoncodes-ca/domain'; /** * Validates that a string is a legal JavaScript identifier. @@ -101,11 +23,11 @@ export function validateJavaScriptIdentifier(name: string): string | undefined { } // Remaining characters: letters, digits, underscore, dollar sign - if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) { + if (!JS_IDENTIFIER_PATTERN.test(name)) { return `"${name}" is not a valid JavaScript identifier: may only contain letters, digits, underscores, and dollar signs.`; } - if (JS_RESERVED_WORDS.has(name)) { + if (isJavaScriptReservedWord(name)) { return `"${name}" is a JavaScript reserved word and cannot be used as an identifier.`; } diff --git a/libs/core/src/lib/bundle/validate-bundle-definition.spec.ts b/libs/core/src/lib/bundle/validate-bundle-definition.spec.ts new file mode 100644 index 00000000..75502bd4 --- /dev/null +++ b/libs/core/src/lib/bundle/validate-bundle-definition.spec.ts @@ -0,0 +1,222 @@ +import { describe, expect, it } from 'vitest'; +import type { BundleDefinition } from '../../config/bundle-definition'; +import type { LingoTrackerConfig } from '../../config/lingo-tracker-config'; +import { validateBundleDefinition, validateBundleKey } from './validate-bundle-definition'; + +const config: LingoTrackerConfig = { + exportFolder: 'dist/export', + importFolder: 'dist/import', + baseLocale: 'en', + locales: ['en', 'fr'], + collections: { + common: { translationsFolder: 'libs/common/i18n' }, + admin: { translationsFolder: 'libs/admin/i18n' }, + }, +}; + +const validDefinition = (overrides: Partial = {}): BundleDefinition => ({ + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: 'All', + ...overrides, +}); + +describe('validateBundleKey', () => { + it('accepts letters, numbers, hyphens and underscores', () => { + expect(validateBundleKey('main')).toEqual([]); + expect(validateBundleKey('core-ui_2')).toEqual([]); + }); + + it('rejects an empty or whitespace-only key', () => { + expect(validateBundleKey('')).toEqual(['Bundle name is required.']); + expect(validateBundleKey(' ')).toEqual(['Bundle name is required.']); + }); + + it('rejects keys with other characters', () => { + expect(validateBundleKey('my bundle')).toEqual([ + 'Bundle name may only contain letters, numbers, hyphens and underscores.', + ]); + expect(validateBundleKey('a.b')).toHaveLength(1); + expect(validateBundleKey('a/b')).toHaveLength(1); + }); +}); + +describe('validateBundleDefinition', () => { + it('returns no errors for a valid "All" definition', () => { + expect(validateBundleDefinition(validDefinition(), config)).toEqual([]); + }); + + it('returns no errors for a valid explicit collection list', () => { + const definition = validDefinition({ + collections: [ + { name: 'common', entriesSelectionRules: 'All' }, + { + name: 'admin', + bundledKeyPrefix: 'admin', + mergeStrategy: 'override', + entriesSelectionRules: [{ matchingPattern: 'apps.*', matchingTags: ['ui'], matchingTagOperator: 'Any' }], + }, + ], + typeDistFile: './src/tokens.ts', + tokenConstantName: 'MY_TOKENS', + }); + + expect(validateBundleDefinition(definition, config)).toEqual([]); + }); + + it('reports a missing bundleName', () => { + const errors = validateBundleDefinition(validDefinition({ bundleName: '' }), config); + expect(errors).toContain('bundleName is required.'); + }); + + it('reports a bundleName without the {locale} placeholder', () => { + const errors = validateBundleDefinition(validDefinition({ bundleName: 'main' }), config); + expect(errors).toContain('bundleName must include the {locale} placeholder so each locale gets its own file.'); + }); + + it('reports a missing dist', () => { + const errors = validateBundleDefinition(validDefinition({ dist: ' ' }), config); + expect(errors).toContain('dist (output folder) is required.'); + }); + + it('reports collections that are neither "All" nor a non-empty array', () => { + expect(validateBundleDefinition(validDefinition({ collections: [] }), config)).toContain( + "collections must be 'All' or a non-empty array of collection definitions.", + ); + expect( + validateBundleDefinition(validDefinition({ collections: 'everything' as unknown as 'All' }), config), + ).toContain("collections must be 'All' or a non-empty array of collection definitions."); + }); + + it('reports a typeDistFile that does not end in .ts', () => { + const errors = validateBundleDefinition(validDefinition({ typeDistFile: './src/tokens.js' }), config); + expect(errors).toEqual(['typeDistFile must end with a .ts extension, but got: ./src/tokens.js']); + }); + + it('reports an invalid tokenConstantName', () => { + const errors = validateBundleDefinition(validDefinition({ tokenConstantName: '1bad' }), config); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatch(/^tokenConstantName is invalid: /); + expect(errors[0]).toContain('must start with a letter'); + }); + + it('reports a reserved word used as tokenConstantName', () => { + const errors = validateBundleDefinition(validDefinition({ tokenConstantName: 'class' }), config); + expect(errors[0]).toContain('reserved word'); + }); + + it('reports a collection that does not exist in the configuration', () => { + const errors = validateBundleDefinition( + validDefinition({ collections: [{ name: 'ghost', entriesSelectionRules: 'All' }] }), + config, + ); + expect(errors).toEqual(["Collection 'ghost' does not exist in the configuration."]); + }); + + it('reports a collection without a name', () => { + const errors = validateBundleDefinition( + validDefinition({ collections: [{ name: '', entriesSelectionRules: 'All' }] }), + config, + ); + expect(errors).toEqual(['Collection at index 0 is missing a name.']); + }); + + it('reports an empty entriesSelectionRules array', () => { + const errors = validateBundleDefinition( + validDefinition({ collections: [{ name: 'common', entriesSelectionRules: [] }] }), + config, + ); + expect(errors).toEqual(["Collection 'common': entriesSelectionRules must be 'All' or a non-empty array of rules."]); + }); + + it('reports a rule without a matchingPattern', () => { + const errors = validateBundleDefinition( + validDefinition({ + collections: [{ name: 'common', entriesSelectionRules: [{ matchingPattern: '' }] }], + }), + config, + ); + expect(errors).toEqual(["Collection 'common': rule at index 0 is missing a matchingPattern."]); + }); + + it('reports an invalid matchingTagOperator', () => { + const errors = validateBundleDefinition( + validDefinition({ + collections: [ + { + name: 'common', + entriesSelectionRules: [{ matchingPattern: '*', matchingTagOperator: 'Some' as unknown as 'All' }], + }, + ], + }), + config, + ); + expect(errors).toEqual([ + "Collection 'common': rule at index 0 has an invalid matchingTagOperator 'Some' (expected 'All' or 'Any').", + ]); + }); + + it('reports an invalid mergeStrategy', () => { + const errors = validateBundleDefinition( + validDefinition({ + collections: [{ name: 'common', entriesSelectionRules: 'All', mergeStrategy: 'replace' as unknown as 'merge' }], + }), + config, + ); + expect(errors).toEqual(["Collection 'common': mergeStrategy must be 'merge' or 'override', but got: replace"]); + }); + + it('reports the same collection included twice with the same prefix', () => { + const errors = validateBundleDefinition( + validDefinition({ + collections: [ + { name: 'common', entriesSelectionRules: 'All', bundledKeyPrefix: 'shared' }, + { name: 'common', entriesSelectionRules: [{ matchingPattern: 'x.*' }], bundledKeyPrefix: 'shared' }, + ], + }), + config, + ); + expect(errors).toEqual(["Collection 'common' is included more than once with the same bundledKeyPrefix 'shared'."]); + }); + + it('reports the same collection included twice without a prefix', () => { + const errors = validateBundleDefinition( + validDefinition({ + collections: [ + { name: 'common', entriesSelectionRules: 'All' }, + { name: 'common', entriesSelectionRules: 'All' }, + ], + }), + config, + ); + expect(errors).toEqual(["Collection 'common' is included more than once without a bundledKeyPrefix."]); + }); + + it('allows the same collection twice with different prefixes', () => { + const errors = validateBundleDefinition( + validDefinition({ + collections: [ + { name: 'common', entriesSelectionRules: 'All', bundledKeyPrefix: 'a' }, + { name: 'common', entriesSelectionRules: 'All', bundledKeyPrefix: 'b' }, + ], + }), + config, + ); + expect(errors).toEqual([]); + }); + + it('accumulates every applicable message', () => { + const errors = validateBundleDefinition( + { + bundleName: 'main', + dist: '', + collections: [{ name: 'ghost', entriesSelectionRules: [] }], + typeDistFile: 'tokens.json', + tokenConstantName: 'my-tokens', + }, + config, + ); + + expect(errors).toHaveLength(6); + }); +}); diff --git a/libs/core/src/lib/bundle/validate-bundle-definition.ts b/libs/core/src/lib/bundle/validate-bundle-definition.ts new file mode 100644 index 00000000..c55cbefd --- /dev/null +++ b/libs/core/src/lib/bundle/validate-bundle-definition.ts @@ -0,0 +1,134 @@ +/** + * Pure validation for bundle keys and bundle definitions. + * + * Every function returns the full list of problems found (never throws) so + * callers can surface all messages at once, e.g. in a form. + */ + +import type { BundleDefinition, CollectionBundleDefinition, EntrySelectionRule } from '../../config/bundle-definition'; +import type { LingoTrackerConfig } from '../../config/lingo-tracker-config'; +import { validateJavaScriptIdentifier } from './type-generation/key-transformer'; + +const BUNDLE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/; +const VALID_TAG_OPERATORS: ReadonlySet = new Set(['All', 'Any']); +const VALID_MERGE_STRATEGIES: ReadonlySet = new Set(['merge', 'override']); + +/** + * Validates the key used to identify a bundle in `config.bundles`. + * The key also seeds the generated TypeScript constant name, so it is limited + * to letters, digits, hyphens and underscores. + */ +export function validateBundleKey(key: string): string[] { + const errors: string[] = []; + const trimmed = key?.trim() ?? ''; + + if (trimmed.length === 0) { + errors.push('Bundle name is required.'); + return errors; + } + + if (!BUNDLE_KEY_PATTERN.test(trimmed)) { + errors.push('Bundle name may only contain letters, numbers, hyphens and underscores.'); + } + + return errors; +} + +/** + * Validates a bundle definition against the current configuration. + * Returns every applicable message; an empty array means the definition is valid. + */ +export function validateBundleDefinition(definition: BundleDefinition, config: LingoTrackerConfig): string[] { + const errors: string[] = []; + + const bundleName = definition.bundleName?.trim() ?? ''; + if (bundleName.length === 0) { + errors.push('bundleName is required.'); + } else if (!bundleName.includes('{locale}')) { + errors.push('bundleName must include the {locale} placeholder so each locale gets its own file.'); + } + + if (!definition.dist || definition.dist.trim().length === 0) { + errors.push('dist (output folder) is required.'); + } + + if (definition.collections !== 'All') { + if (!Array.isArray(definition.collections) || definition.collections.length === 0) { + errors.push("collections must be 'All' or a non-empty array of collection definitions."); + } else { + validateCollections(definition.collections, config, errors); + } + } + + if (definition.typeDistFile !== undefined && !definition.typeDistFile.endsWith('.ts')) { + errors.push(`typeDistFile must end with a .ts extension, but got: ${definition.typeDistFile}`); + } + + if (definition.tokenConstantName !== undefined) { + const identifierError = validateJavaScriptIdentifier(definition.tokenConstantName); + if (identifierError) { + errors.push(`tokenConstantName is invalid: ${identifierError}`); + } + } + + return errors; +} + +function validateCollections( + collections: CollectionBundleDefinition[], + config: LingoTrackerConfig, + errors: string[], +): void { + const seenPrefixes = new Map>(); + + collections.forEach((collectionDef, index) => { + const label = collectionDef.name ? `Collection '${collectionDef.name}'` : `Collection at index ${index}`; + + if (!collectionDef.name || collectionDef.name.trim().length === 0) { + errors.push(`Collection at index ${index} is missing a name.`); + } else if (!config.collections?.[collectionDef.name]) { + errors.push(`${label} does not exist in the configuration.`); + } + + const rules = collectionDef.entriesSelectionRules; + if (rules !== 'All') { + if (!Array.isArray(rules) || rules.length === 0) { + errors.push(`${label}: entriesSelectionRules must be 'All' or a non-empty array of rules.`); + } else { + rules.forEach((rule, ruleIndex) => { + validateRule(rule, ruleIndex, label, errors); + }); + } + } + + if (collectionDef.mergeStrategy !== undefined && !VALID_MERGE_STRATEGIES.has(collectionDef.mergeStrategy)) { + errors.push(`${label}: mergeStrategy must be 'merge' or 'override', but got: ${collectionDef.mergeStrategy}`); + } + + if (collectionDef.name) { + const prefixes = seenPrefixes.get(collectionDef.name) ?? new Set(); + const prefix = collectionDef.bundledKeyPrefix || undefined; + if (prefixes.has(prefix)) { + errors.push( + prefix + ? `${label} is included more than once with the same bundledKeyPrefix '${prefix}'.` + : `${label} is included more than once without a bundledKeyPrefix.`, + ); + } + prefixes.add(prefix); + seenPrefixes.set(collectionDef.name, prefixes); + } + }); +} + +function validateRule(rule: EntrySelectionRule, ruleIndex: number, label: string, errors: string[]): void { + if (!rule.matchingPattern || rule.matchingPattern.trim().length === 0) { + errors.push(`${label}: rule at index ${ruleIndex} is missing a matchingPattern.`); + } + + if (rule.matchingTagOperator !== undefined && !VALID_TAG_OPERATORS.has(rule.matchingTagOperator)) { + errors.push( + `${label}: rule at index ${ruleIndex} has an invalid matchingTagOperator '${rule.matchingTagOperator}' (expected 'All' or 'Any').`, + ); + } +} diff --git a/libs/core/src/lib/errors/error-messages.ts b/libs/core/src/lib/errors/error-messages.ts index d3d5019b..9d57cc2f 100644 --- a/libs/core/src/lib/errors/error-messages.ts +++ b/libs/core/src/lib/errors/error-messages.ts @@ -32,4 +32,10 @@ export const ErrorMessages = { cannotModifyBaseLocale: (locale: string) => `Cannot add or remove the base locale "${locale}"`, invalidKey: (key: string, reason: string) => `Invalid resource key "${key}": ${reason}`, + + bundleNotFound: (name: string) => `Bundle "${name}" not found`, + + bundleAlreadyExists: (name: string) => `Bundle "${name}" already exists`, + + invalidBundleDefinition: (errors: string[]) => `Invalid bundle definition: ${errors.join('; ')}`, } as const; From e54618281f6a49e37e3ed04652adf71073c6a952 Mon Sep 17 00:00:00 2001 From: snodel Date: Wed, 16 Sep 2026 14:00:11 -0700 Subject: [PATCH 08/13] feat(data-transfer): add bundle DTOs Contracts for the bundle endpoints, shared by the API and the Tracker UI: the definition itself, create/update payloads, the dry-run request and result, and the generation job snapshot. Plain interfaces with locally declared unions, so the browser never has to reach into core. LingoTrackerConfigDto also gains bundles and projectName; the config mapper was dropping bundles entirely and the UI has no other source for the served project's name. Co-Authored-By: Claude Opus 5 (1M context) --- libs/data-transfer/src/index.ts | 5 ++ .../src/lib/bundle-definition.dto.ts | 74 ++++++++++++++++++ .../src/lib/bundle-dry-run.dto.ts | 75 +++++++++++++++++++ .../src/lib/bundle-generate-job.dto.ts | 52 +++++++++++++ .../src/lib/create-bundle.dto.ts | 9 +++ .../src/lib/lingo-tracker-config.dto.ts | 9 +++ .../src/lib/update-bundle.dto.ts | 9 +++ 7 files changed, 233 insertions(+) create mode 100644 libs/data-transfer/src/lib/bundle-definition.dto.ts create mode 100644 libs/data-transfer/src/lib/bundle-dry-run.dto.ts create mode 100644 libs/data-transfer/src/lib/bundle-generate-job.dto.ts create mode 100644 libs/data-transfer/src/lib/create-bundle.dto.ts create mode 100644 libs/data-transfer/src/lib/update-bundle.dto.ts diff --git a/libs/data-transfer/src/index.ts b/libs/data-transfer/src/index.ts index 594c5a38..d483e5a2 100644 --- a/libs/data-transfer/src/index.ts +++ b/libs/data-transfer/src/index.ts @@ -27,3 +27,8 @@ export * from './lib/translate-locale.dto'; export * from './lib/add-locale.dto'; export * from './lib/add-locale-response.dto'; export * from './lib/remove-locale-response.dto'; +export * from './lib/bundle-definition.dto'; +export * from './lib/create-bundle.dto'; +export * from './lib/update-bundle.dto'; +export * from './lib/bundle-dry-run.dto'; +export * from './lib/bundle-generate-job.dto'; diff --git a/libs/data-transfer/src/lib/bundle-definition.dto.ts b/libs/data-transfer/src/lib/bundle-definition.dto.ts new file mode 100644 index 00000000..cd1b9c43 --- /dev/null +++ b/libs/data-transfer/src/lib/bundle-definition.dto.ts @@ -0,0 +1,74 @@ +/** + * Bundle definition DTOs. Mirror the core `BundleDefinition` shapes without importing from core, + * so they are safe to use from the browser-based Tracker UI. + */ + +/** + * Casing of generated TypeScript token property keys. + * - 'upperCase': SCREAMING_SNAKE_CASE (e.g. FILE_UPLOAD) — default + * - 'camelCase': camelCase (e.g. fileUpload) + */ +export type TokenCasingDto = 'upperCase' | 'camelCase'; + +/** Pattern and tag-based rule selecting which entries of a collection are bundled. */ +export interface EntrySelectionRuleDto { + /** + * Prefix-based key pattern, e.g. "*", "apps.*", "apps.common.*" or an exact key. + */ + matchingPattern: string; + + /** Optional tags to filter by. "*" matches any tagged entry. */ + matchingTags?: string[]; + + /** How multiple tags combine. Ignored when `matchingTags` is omitted. Default: "Any". */ + matchingTagOperator?: 'All' | 'Any'; +} + +/** How a single collection contributes to a bundle. */ +export interface CollectionBundleDefinitionDto { + /** Name of an existing collection. */ + name: string; + + /** Optional prefix prepended to every key from this collection. */ + bundledKeyPrefix?: string; + + /** "All" to include every entry, or a list of selection rules. */ + entriesSelectionRules: 'All' | EntrySelectionRuleDto[]; + + /** + * Conflict resolution when several collections define the same key. + * - "merge": first collection wins (default) + * - "override": this collection overrides earlier values + */ + mergeStrategy?: 'merge' | 'override'; +} + +/** Definition of one bundle output, keyed by bundle name in the project config. */ +export interface BundleDefinitionDto { + /** + * Output file name pattern. Use the `{locale}` placeholder to position the locale, + * e.g. "main.{locale}" → main.en.json. + */ + bundleName: string; + + /** Output directory, absolute or relative to the project root. */ + dist: string; + + /** "All" for every entry of every collection, or per-collection definitions. */ + collections: 'All' | CollectionBundleDefinitionDto[]; + + /** Optional `.ts` file path for generated token type definitions. */ + typeDistFile?: string; + + /** Casing for generated token property keys. Overrides the global setting. */ + tokenCasing?: TokenCasingDto; + + /** + * Custom TypeScript constant name in the type file (e.g. `MY_KEYS`). + * Derived from the bundle key when omitted (e.g. `main` → `MAIN_TOKENS`). + */ + tokenConstantName?: string; + + /** Convert simple ICU placeholders to Transloco `{{ name }}` syntax. Overrides the global setting. */ + transformICUToTransloco?: boolean; +} diff --git a/libs/data-transfer/src/lib/bundle-dry-run.dto.ts b/libs/data-transfer/src/lib/bundle-dry-run.dto.ts new file mode 100644 index 00000000..96d0dc81 --- /dev/null +++ b/libs/data-transfer/src/lib/bundle-dry-run.dto.ts @@ -0,0 +1,75 @@ +import type { BundleDefinitionDto } from './bundle-definition.dto'; + +/** Plans a bundle without writing files. The definition need not be saved in the config. */ +export interface BundleDryRunRequestDto { + /** Bundle key used to derive default names (e.g. the token constant). */ + readonly name: string; + + /** Definition to plan against. */ + readonly bundle: BundleDefinitionDto; + + /** Locales to plan. Defaults to every configured locale. */ + readonly locales?: readonly string[]; +} + +/** One file the bundle would write. */ +export interface BundleDryRunFileDto { + /** Output path relative to the project root. */ + readonly path: string; + + /** Whether this is a translation bundle or the TypeScript types file. */ + readonly kind: 'bundle' | 'types'; + + /** Locale of a `bundle` file. Absent for `types`. */ + readonly locale?: string; + + /** Whether the file already exists on disk. */ + readonly exists: boolean; + + /** Number of keys the file would contain. */ + readonly keysCount: number; +} + +/** Illustrates how one source key is transformed by the bundle. */ +export interface BundleDryRunExampleKeyDto { + readonly collectionName: string; + + /** Key as stored in the collection. */ + readonly sourceKey: string; + + /** Key as written to the bundle (after prefixing). */ + readonly bundledKey: string; + + /** Property path in the generated token constant, when types are enabled. */ + readonly tokenPath?: string; +} + +export interface BundleDryRunResultDto { + readonly name: string; + + /** Locales that were planned. */ + readonly locales: readonly string[]; + + readonly files: readonly BundleDryRunFileDto[]; + + /** Key count per locale. */ + readonly keysPerLocale: Record; + + /** Total number of keys defined by more than one collection. */ + readonly conflictsCount: number; + + /** Conflicting keys. May be capped by the server. */ + readonly conflictKeys: readonly string[]; + + /** + * Keys that are both a leaf and a parent in the bundled key set, e.g. + * `buttons.ok` alongside `buttons.ok.label`. Generation fails on these, + * so a non-empty list means the bundle cannot be built as configured. + * May be capped by the server. + */ + readonly hierarchicalConflicts: readonly string[]; + + readonly exampleKey?: BundleDryRunExampleKeyDto; + + readonly warnings: readonly string[]; +} diff --git a/libs/data-transfer/src/lib/bundle-generate-job.dto.ts b/libs/data-transfer/src/lib/bundle-generate-job.dto.ts new file mode 100644 index 00000000..c9455055 --- /dev/null +++ b/libs/data-transfer/src/lib/bundle-generate-job.dto.ts @@ -0,0 +1,52 @@ +export type BundleGenerateJobStatus = 'pending' | 'running' | 'completed' | 'failed'; + +/** Request body for starting a bundle generation job. */ +export interface GenerateBundleRequestDto { + /** Locales to generate. Defaults to every configured locale. */ + readonly locales?: readonly string[]; +} + +export interface BundleGenerateJobProgressDto { + /** Files written so far. */ + readonly current: number; + + /** Total files to write. */ + readonly total: number; + + /** Path of the file currently being written. */ + readonly currentFile?: string; +} + +export interface BundleGenerateJobResultDto { + /** Paths of the files written, relative to the project root. */ + readonly filesGenerated: readonly string[]; + + /** Key count per locale. */ + readonly keysPerLocale: Record; + + readonly warnings: readonly string[]; + + readonly localesProcessed: readonly string[]; + + /** Path of the generated types file, when types are enabled. */ + readonly typeDistFile?: string; + + /** Number of keys in the generated types file. */ + readonly typesKeysCount?: number; +} + +export interface BundleGenerateJobDto { + readonly jobId: string; + readonly bundleName: string; + readonly status: BundleGenerateJobStatus; + readonly progress: BundleGenerateJobProgressDto; + + /** Present once `status` is `completed`. */ + readonly result?: BundleGenerateJobResultDto; + + /** Present once `status` is `failed`. */ + readonly error?: string; + + readonly startedAt?: string; + readonly completedAt?: string; +} diff --git a/libs/data-transfer/src/lib/create-bundle.dto.ts b/libs/data-transfer/src/lib/create-bundle.dto.ts new file mode 100644 index 00000000..5e8ba29e --- /dev/null +++ b/libs/data-transfer/src/lib/create-bundle.dto.ts @@ -0,0 +1,9 @@ +import type { BundleDefinitionDto } from './bundle-definition.dto'; + +export interface CreateBundleDto { + /** Bundle key in the project config. Must be filesystem-safe and unique. */ + name: string; + + /** Bundle definition */ + bundle: BundleDefinitionDto; +} diff --git a/libs/data-transfer/src/lib/lingo-tracker-config.dto.ts b/libs/data-transfer/src/lib/lingo-tracker-config.dto.ts index 1bc4e31a..e34a87e0 100644 --- a/libs/data-transfer/src/lib/lingo-tracker-config.dto.ts +++ b/libs/data-transfer/src/lib/lingo-tracker-config.dto.ts @@ -1,5 +1,6 @@ import type { TranslationConfigDto } from './translation-config.dto'; import type { LingoTrackerCollectionDto } from './lingo-tracker-collection.dto'; +import type { BundleDefinitionDto, TokenCasingDto } from './bundle-definition.dto'; export interface LingoTrackerConfigDto { exportFolder: string; @@ -7,9 +8,17 @@ export interface LingoTrackerConfigDto { baseLocale: string; locales: string[]; collections: Record; + /** Bundle definitions keyed by bundle name. */ + bundles?: Record; + /** Project-wide default token casing for generated type files. Bundles may override it. */ + tokenCasing?: TokenCasingDto; + /** Project-wide default for ICU → Transloco conversion at bundle time (core default: true). */ + transformICUToTransloco?: boolean; translation?: TranslationConfigDto; /** Resolved global protected terms, read from the protected-terms file. Read-only. */ protectedTerms?: string[]; /** Path of the file the global protected terms are stored in. Read-only; shown in the UI. */ protectedTermsFilePath?: string; + /** Basename of the served workspace folder. Read-only; shown as the home page title. */ + projectName?: string; } diff --git a/libs/data-transfer/src/lib/update-bundle.dto.ts b/libs/data-transfer/src/lib/update-bundle.dto.ts new file mode 100644 index 00000000..0856ddce --- /dev/null +++ b/libs/data-transfer/src/lib/update-bundle.dto.ts @@ -0,0 +1,9 @@ +import type { BundleDefinitionDto } from './bundle-definition.dto'; + +export interface UpdateBundleDto { + /** Optional new bundle name for renaming */ + name?: string; + + /** Updated bundle definition */ + bundle: BundleDefinitionDto; +} From 71a5d03fb8637fe053dd1654aaf6648298ef8831 Mon Sep 17 00:00:00 2001 From: snodel Date: Wed, 16 Sep 2026 14:00:20 -0700 Subject: [PATCH 09/13] feat(api): expose bundle CRUD, dry run and job-based generation POST/PUT/DELETE /api/bundles for the definitions, POST /api/bundles/dry-run for a plan of an unsaved definition, and POST /api/bundles/:name/generate which answers 202 with a job id that GET /api/bundles/jobs/:jobId polls. Generation writes files and can take a while, so it follows the same job-and-poll shape as translation, with a promise queue that keeps runs sequential and eviction of finished jobs. GET /api/config now carries bundles, which the mapper used to drop, and projectName from the API process's working directory. Co-Authored-By: Claude Opus 5 (1M context) --- apps/api/src/app/app.module.ts | 13 +- .../app/bundles/bundle-job.service.spec.ts | 272 ++++++++++++++ .../api/src/app/bundles/bundle-job.service.ts | 150 ++++++++ .../app/bundles/bundles.controller.spec.ts | 345 ++++++++++++++++++ .../api/src/app/bundles/bundles.controller.ts | 234 ++++++++++++ .../src/app/config/config.controller.spec.ts | 15 +- apps/api/src/app/config/config.controller.ts | 3 +- .../api/src/app/mappers/bundle.mapper.spec.ts | 255 +++++++++++++ apps/api/src/app/mappers/bundle.mapper.ts | 193 ++++++++++ .../api/src/app/mappers/config.mapper.spec.ts | 26 +- apps/api/src/app/mappers/config.mapper.ts | 32 +- architecture-docs/api.md | 16 +- architecture-docs/feature-matrix.md | 7 +- 13 files changed, 1543 insertions(+), 18 deletions(-) create mode 100644 apps/api/src/app/bundles/bundle-job.service.spec.ts create mode 100644 apps/api/src/app/bundles/bundle-job.service.ts create mode 100644 apps/api/src/app/bundles/bundles.controller.spec.ts create mode 100644 apps/api/src/app/bundles/bundles.controller.ts create mode 100644 apps/api/src/app/mappers/bundle.mapper.spec.ts create mode 100644 apps/api/src/app/mappers/bundle.mapper.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 09b2636e..4dd60779 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,13 +1,15 @@ import { Logger, Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { ConfigService } from './config/config.service'; -import { ConfigController } from './config/config.controller'; +import { BundleJobService } from './bundles/bundle-job.service'; +import { BundlesController } from './bundles/bundles.controller'; +import { CollectionCacheService } from './cache/collection-cache.service'; import { CollectionsController } from './collections/collections.controller'; -import { ResourcesController } from './collections/resources/resources.controller'; import { FoldersController } from './collections/folders/folders.controller'; import { LocalesController } from './collections/locales/locales.controller'; -import { CollectionCacheService } from './cache/collection-cache.service'; +import { ResourcesController } from './collections/resources/resources.controller'; +import { ConfigController } from './config/config.controller'; +import { ConfigService } from './config/config.service'; import { TranslationJobService } from './translation-job/translation-job.service'; @Module({ @@ -19,8 +21,9 @@ import { TranslationJobService } from './translation-job/translation-job.service ResourcesController, FoldersController, LocalesController, + BundlesController, ], - providers: [AppService, ConfigService, CollectionCacheService, TranslationJobService, Logger], + providers: [AppService, ConfigService, CollectionCacheService, TranslationJobService, BundleJobService, Logger], }) export class AppModule { constructor() { diff --git a/apps/api/src/app/bundles/bundle-job.service.spec.ts b/apps/api/src/app/bundles/bundle-job.service.spec.ts new file mode 100644 index 00000000..79fd0634 --- /dev/null +++ b/apps/api/src/app/bundles/bundle-job.service.spec.ts @@ -0,0 +1,272 @@ +import { Logger } from '@nestjs/common'; +import type { BundleProgressEvent, GenerateBundleParams, GenerateBundleResult } from '@simoncodes-ca/core'; +import { BundleJobService, JOB_RETENTION_MS, MAX_RETAINED_JOBS } from './bundle-job.service'; + +const mockGenerateBundle = jest.fn(); + +jest.mock('@simoncodes-ca/core', () => { + const actual = jest.requireActual('@simoncodes-ca/core'); + return { + ...actual, + generateBundle: (params: unknown) => mockGenerateBundle(params), + }; +}); + +const flush = async (): Promise => { + await new Promise((resolve) => setImmediate(resolve)); +}; + +const bundleDefinition = { + bundleName: '{locale}', + dist: './dist/i18n', + collections: 'All' as const, + typeDistFile: './dist/i18n-types/main.ts', +}; + +const config = { + exportFolder: 'dist/export', + importFolder: 'dist/import', + baseLocale: 'en', + locales: ['en', 'fr'], + collections: { app: { translationsFolder: './i18n' } }, +}; + +const makeResult = (overrides: Partial = {}): GenerateBundleResult => ({ + bundleKey: 'main', + filesGenerated: 2, + warnings: [], + localesProcessed: ['en', 'fr'], + keysPerLocale: { en: 5, fr: 5 }, + typeGenerationResult: { + bundleKey: 'main', + typeDistFile: './dist/i18n-types/main.ts', + keysCount: 5, + fileGenerated: true, + }, + ...overrides, +}); + +const makeParams = (bundleName = 'main') => ({ bundleName, bundleDefinition, config }); + +describe('BundleJobService', () => { + let service: BundleJobService; + let logger: jest.Mocked>; + + beforeEach(() => { + jest.clearAllMocks(); + logger = { error: jest.fn(), log: jest.fn(), warn: jest.fn() }; + service = new BundleJobService(logger as unknown as Logger); + }); + + it('startJob returns an ID and getJob exposes the pending job', () => { + mockGenerateBundle.mockReturnValue(new Promise(() => {})); + + const jobId = service.startJob(makeParams()); + const job = service.getJob(jobId); + + expect(typeof jobId).toBe('string'); + expect(job?.jobId).toBe(jobId); + expect(job?.bundleName).toBe('main'); + expect(job?.status).toBe('pending'); + expect(job?.progress).toEqual({ current: 0, total: 0 }); + }); + + it('getJob returns undefined for an unknown ID', () => { + expect(service.getJob('nope')).toBeUndefined(); + }); + + it('passes the bundle params, locales and an onProgress callback to generateBundle', async () => { + mockGenerateBundle.mockResolvedValue(makeResult()); + + service.startJob({ ...makeParams(), locales: ['fr'] }); + await flush(); + + expect(mockGenerateBundle).toHaveBeenCalledTimes(1); + const params = mockGenerateBundle.mock.calls[0][0] as GenerateBundleParams; + expect(params.bundleKey).toBe('main'); + expect(params.bundleDefinition).toBe(bundleDefinition); + expect(params.config).toBe(config); + expect(params.locales).toEqual(['fr']); + expect(typeof params.onProgress).toBe('function'); + }); + + it('omits locales from the core call when none were requested', async () => { + mockGenerateBundle.mockResolvedValue(makeResult()); + + service.startJob(makeParams()); + await flush(); + + const params = mockGenerateBundle.mock.calls[0][0] as GenerateBundleParams; + expect('locales' in params).toBe(false); + }); + + it('reflects onProgress events in the job snapshot while running', async () => { + let capturedProgress: ((event: BundleProgressEvent) => void) | undefined; + mockGenerateBundle.mockImplementation((params: GenerateBundleParams) => { + capturedProgress = params.onProgress; + return new Promise(() => {}); + }); + + const jobId = service.startJob(makeParams()); + await flush(); + + expect(service.getJob(jobId)?.status).toBe('running'); + expect(service.getJob(jobId)?.startedAt).toEqual(expect.any(String)); + + capturedProgress?.({ locale: 'fr', index: 2, total: 3, file: 'dist/i18n/fr.json' }); + + expect(service.getJob(jobId)?.progress).toEqual({ current: 2, total: 3, currentFile: 'dist/i18n/fr.json' }); + }); + + it('marks the job completed with the mapped result and ISO timestamps', async () => { + mockGenerateBundle.mockImplementation(async (params: GenerateBundleParams) => { + params.onProgress?.({ locale: 'en', index: 1, total: 2, file: 'dist/i18n/en.json' }); + params.onProgress?.({ locale: 'fr', index: 2, total: 2, file: 'dist/i18n/fr.json' }); + return makeResult({ warnings: ['careful'] }); + }); + + const jobId = service.startJob(makeParams()); + await flush(); + + const job = service.getJob(jobId); + expect(job?.status).toBe('completed'); + expect(job?.progress).toEqual({ current: 2, total: 2 }); + expect(job?.result?.localesProcessed).toEqual(['en', 'fr']); + expect(job?.result?.keysPerLocale).toEqual({ en: 5, fr: 5 }); + expect(job?.result?.warnings).toEqual(['careful']); + expect(job?.result?.typeDistFile).toBe('dist/i18n-types/main.ts'); + expect(job?.result?.typesKeysCount).toBe(5); + expect(job?.result?.filesGenerated).toHaveLength(3); + expect(job?.error).toBeUndefined(); + expect(() => new Date(job?.startedAt ?? '').toISOString()).not.toThrow(); + expect(() => new Date(job?.completedAt ?? '').toISOString()).not.toThrow(); + }); + + it('marks the job failed with the error message and logs it', async () => { + mockGenerateBundle.mockRejectedValue(new Error('disk full')); + + const jobId = service.startJob(makeParams()); + await flush(); + + const job = service.getJob(jobId); + expect(job?.status).toBe('failed'); + expect(job?.error).toBe('disk full'); + expect(job?.result).toBeUndefined(); + expect(job?.completedAt).toEqual(expect.any(String)); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('disk full')); + }); + + it('uses a fallback message when the rejection is not an Error', async () => { + mockGenerateBundle.mockRejectedValue('boom'); + + const jobId = service.startJob(makeParams()); + await flush(); + + expect(service.getJob(jobId)?.error).toBe('An unexpected error occurred'); + }); + + it('runs jobs one at a time, in order', async () => { + let resolveFirst: ((value: GenerateBundleResult) => void) | undefined; + mockGenerateBundle + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirst = resolve; + }), + ) + .mockResolvedValueOnce(makeResult({ bundleKey: 'second' })); + + const firstId = service.startJob(makeParams('first')); + const secondId = service.startJob(makeParams('second')); + await flush(); + + expect(mockGenerateBundle).toHaveBeenCalledTimes(1); + expect(service.getJob(firstId)?.status).toBe('running'); + expect(service.getJob(secondId)?.status).toBe('pending'); + + resolveFirst?.(makeResult()); + await flush(); + + expect(mockGenerateBundle).toHaveBeenCalledTimes(2); + expect(service.getJob(firstId)?.status).toBe('completed'); + expect(service.getJob(secondId)?.status).toBe('completed'); + }); + + it('keeps running the queue after a failed job', async () => { + mockGenerateBundle.mockRejectedValueOnce(new Error('first failed')).mockResolvedValueOnce(makeResult()); + + const firstId = service.startJob(makeParams('first')); + const secondId = service.startJob(makeParams('second')); + await flush(); + + expect(service.getJob(firstId)?.status).toBe('failed'); + expect(service.getJob(secondId)?.status).toBe('completed'); + }); + + describe('eviction', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('evicts finished jobs older than the retention window on the next startJob', async () => { + mockGenerateBundle.mockResolvedValue(makeResult()); + + const oldId = service.startJob(makeParams()); + await jest.advanceTimersByTimeAsync(0); + expect(service.getJob(oldId)?.status).toBe('completed'); + + jest.advanceTimersByTime(JOB_RETENTION_MS + 1000); + + const newId = service.startJob(makeParams()); + + expect(service.getJob(oldId)).toBeUndefined(); + expect(service.getJob(newId)).toBeDefined(); + }); + + it('keeps recently finished jobs', async () => { + mockGenerateBundle.mockResolvedValue(makeResult()); + + const recentId = service.startJob(makeParams()); + await jest.advanceTimersByTimeAsync(0); + + jest.advanceTimersByTime(JOB_RETENTION_MS - 1000); + service.startJob(makeParams()); + + expect(service.getJob(recentId)?.status).toBe('completed'); + }); + + it('never evicts a job that has not finished', async () => { + mockGenerateBundle.mockReturnValue(new Promise(() => {})); + + const stuckId = service.startJob(makeParams()); + await jest.advanceTimersByTimeAsync(0); + + jest.advanceTimersByTime(JOB_RETENTION_MS * 2); + service.startJob(makeParams()); + + expect(service.getJob(stuckId)?.status).toBe('running'); + }); + + it('caps retained jobs by evicting the oldest finished ones first', async () => { + mockGenerateBundle.mockResolvedValue(makeResult()); + + const ids: string[] = []; + for (let index = 0; index < MAX_RETAINED_JOBS; index++) { + ids.push(service.startJob(makeParams())); + await jest.advanceTimersByTimeAsync(1); + } + + expect(ids.every((id) => service.getJob(id)?.status === 'completed')).toBe(true); + + const extraId = service.startJob(makeParams()); + + expect(service.getJob(ids[0])).toBeUndefined(); + expect(service.getJob(ids[1])).toBeDefined(); + expect(service.getJob(extraId)).toBeDefined(); + }); + }); +}); diff --git a/apps/api/src/app/bundles/bundle-job.service.ts b/apps/api/src/app/bundles/bundle-job.service.ts new file mode 100644 index 00000000..8bfb75c1 --- /dev/null +++ b/apps/api/src/app/bundles/bundle-job.service.ts @@ -0,0 +1,150 @@ +import { randomUUID } from 'node:crypto'; +import { Injectable, Logger } from '@nestjs/common'; +import type { BundleDefinition, BundleProgressEvent, LingoTrackerConfig } from '@simoncodes-ca/core'; +import { generateBundle } from '@simoncodes-ca/core'; +import type { + BundleGenerateJobDto, + BundleGenerateJobProgressDto, + BundleGenerateJobResultDto, + BundleGenerateJobStatus, +} from '@simoncodes-ca/data-transfer'; +import { mapGenerateBundleResultToJobResult } from '../mappers/bundle.mapper'; + +export interface StartBundleJobParams { + readonly bundleName: string; + readonly bundleDefinition: BundleDefinition; + readonly config: LingoTrackerConfig; + readonly locales?: string[]; +} + +interface BundleJob { + jobId: string; + bundleName: string; + status: BundleGenerateJobStatus; + progress: BundleGenerateJobProgressDto; + result?: BundleGenerateJobResultDto; + error?: string; + startedAt?: Date; + completedAt?: Date; +} + +/** Finished jobs older than this are evicted on the next `startJob`. */ +export const JOB_RETENTION_MS = 30 * 60 * 1000; +/** Upper bound on retained jobs; the oldest finished ones are evicted first. */ +export const MAX_RETAINED_JOBS = 100; + +/** + * Runs bundle generation as fire-and-forget jobs that the caller polls. + * + * Jobs are queued and run one at a time: two bundles may write the same output + * folder, and type generation reads the config from disk, so overlapping runs + * could interleave writes. + */ +@Injectable() +export class BundleJobService { + readonly #logger: Logger; + readonly #jobs = new Map(); + #queue: Promise = Promise.resolve(); + + constructor(logger: Logger) { + this.#logger = logger; + } + + /** Registers a job, schedules it behind any running job, and returns its ID. */ + startJob(params: StartBundleJobParams): string { + this.#evictFinishedJobs(); + + const jobId = randomUUID(); + this.#jobs.set(jobId, { + jobId, + bundleName: params.bundleName, + status: 'pending', + progress: { current: 0, total: 0 }, + }); + + this.#queue = this.#queue.then(() => this.#runJob(jobId, params)); + + return jobId; + } + + /** Returns the current snapshot of a job, or undefined if the job ID is unknown. */ + getJob(jobId: string): BundleGenerateJobDto | undefined { + const job = this.#jobs.get(jobId); + return job ? this.#toDto(job) : undefined; + } + + async #runJob(jobId: string, params: StartBundleJobParams): Promise { + const job = this.#jobs.get(jobId); + if (!job) return; + + job.status = 'running'; + job.startedAt = new Date(); + + const onProgress = (event: BundleProgressEvent): void => { + const running = this.#jobs.get(jobId); + if (!running) return; + running.progress = { current: event.index, total: event.total, currentFile: event.file }; + }; + + try { + const result = await generateBundle({ + bundleKey: params.bundleName, + bundleDefinition: params.bundleDefinition, + config: params.config, + ...(params.locales && { locales: params.locales }), + onProgress, + }); + + const completed = this.#jobs.get(jobId); + if (!completed) return; + + completed.status = 'completed'; + completed.completedAt = new Date(); + completed.progress = { current: completed.progress.total, total: completed.progress.total }; + completed.result = mapGenerateBundleResultToJobResult(result, params.bundleDefinition); + } catch (error: unknown) { + const failed = this.#jobs.get(jobId); + if (!failed) return; + + failed.status = 'failed'; + failed.completedAt = new Date(); + failed.error = error instanceof Error ? error.message : 'An unexpected error occurred'; + + this.#logger.error(`Bundle job ${jobId} (${params.bundleName}) failed: ${failed.error}`); + } + } + + #evictFinishedJobs(): void { + const now = Date.now(); + const finished = [...this.#jobs.values()] + .filter((job) => job.completedAt !== undefined) + .sort((a, b) => (a.completedAt?.getTime() ?? 0) - (b.completedAt?.getTime() ?? 0)); + + for (const job of finished) { + const completedAt = job.completedAt?.getTime() ?? now; + if (now - completedAt > JOB_RETENTION_MS) { + this.#jobs.delete(job.jobId); + } + } + + // `startJob` adds one more after this, so leave room for it. + let excess = this.#jobs.size - (MAX_RETAINED_JOBS - 1); + for (const job of finished) { + if (excess <= 0) break; + if (this.#jobs.delete(job.jobId)) excess--; + } + } + + #toDto(job: BundleJob): BundleGenerateJobDto { + return { + jobId: job.jobId, + bundleName: job.bundleName, + status: job.status, + progress: { ...job.progress }, + ...(job.result && { result: job.result }), + ...(job.error !== undefined && { error: job.error }), + ...(job.startedAt && { startedAt: job.startedAt.toISOString() }), + ...(job.completedAt && { completedAt: job.completedAt.toISOString() }), + }; + } +} diff --git a/apps/api/src/app/bundles/bundles.controller.spec.ts b/apps/api/src/app/bundles/bundles.controller.spec.ts new file mode 100644 index 00000000..60198bde --- /dev/null +++ b/apps/api/src/app/bundles/bundles.controller.spec.ts @@ -0,0 +1,345 @@ +import { ConflictException, HttpException, HttpStatus, NotFoundException } from '@nestjs/common'; +import { Test, type TestingModule } from '@nestjs/testing'; +import type { BundleDefinition, BundlePlan, LingoTrackerConfig } from '@simoncodes-ca/core'; +import * as core from '@simoncodes-ca/core'; +import type { BundleDefinitionDto } from '@simoncodes-ca/data-transfer'; +import type { Response } from 'express'; +import { ConfigService } from '../config/config.service'; +import { BundleJobService } from './bundle-job.service'; +import { BundlesController } from './bundles.controller'; + +jest.mock('@simoncodes-ca/core', () => ({ + addBundleDefinition: jest.fn(), + updateBundleDefinition: jest.fn(), + deleteBundleDefinition: jest.fn(), + planBundle: jest.fn(), + validateBundleKey: jest.fn(() => []), + validateBundleDefinition: jest.fn(() => []), + getBundleOutputPath: jest.fn((definition: BundleDefinition, locale: string) => `${definition.dist}/${locale}.json`), +})); + +const existingDefinition: BundleDefinition = { + bundleName: '{locale}', + dist: './apps/tracker/src/assets/i18n', + collections: [{ name: 'trackerResources', entriesSelectionRules: 'All' }], + typeDistFile: './apps/tracker/src/i18n-types/tracker-resources.ts', +}; + +const config: LingoTrackerConfig = { + exportFolder: 'dist/export', + importFolder: 'dist/import', + baseLocale: 'en', + locales: ['en', 'fr-ca'], + collections: { trackerResources: { translationsFolder: 'apps/tracker/src/i18n' } }, + bundles: { tracker: existingDefinition, other: { ...existingDefinition, dist: './dist/other' } }, +}; + +const requestDefinition: BundleDefinitionDto = { + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: 'All', +}; + +const plan: BundlePlan = { + bundleKey: 'preview', + locales: ['en', 'fr-ca'], + files: [ + { + path: 'dist/i18n/main.en.json', + absolutePath: '/w/dist/i18n/main.en.json', + kind: 'bundle', + locale: 'en', + exists: false, + keysCount: 2, + }, + ], + keysPerLocale: { en: 2, 'fr-ca': 2 }, + conflictsCount: 0, + conflictKeys: [], + hierarchicalConflicts: [], + warnings: [], +}; + +const statusOf = (fn: () => unknown): number => { + try { + fn(); + } catch (error: unknown) { + if (error instanceof HttpException) return error.getStatus(); + throw error; + } + throw new Error('expected an HttpException'); +}; + +const makeResponse = (): { response: Response; status: jest.Mock; json: jest.Mock } => { + const json = jest.fn(); + const status = jest.fn(() => ({ json })); + return { response: { status } as unknown as Response, status, json }; +}; + +describe('BundlesController', () => { + let moduleRef: TestingModule; + let controller: BundlesController; + + const configService = { getConfig: jest.fn() }; + const jobService = { startJob: jest.fn(), getJob: jest.fn() }; + + beforeAll(async () => { + moduleRef = await Test.createTestingModule({ + controllers: [BundlesController], + providers: [ + { provide: ConfigService, useValue: configService }, + { provide: BundleJobService, useValue: jobService }, + ], + }).compile(); + + controller = moduleRef.get(BundlesController); + }); + + beforeEach(() => { + jest.clearAllMocks(); + configService.getConfig.mockReturnValue(config); + (core.validateBundleKey as jest.Mock).mockReturnValue([]); + (core.validateBundleDefinition as jest.Mock).mockReturnValue([]); + }); + + describe('POST /bundles', () => { + it('validates then adds the mapped definition', () => { + (core.addBundleDefinition as jest.Mock).mockReturnValue({ message: 'Bundle "main" added successfully' }); + + const result = controller.createBundle({ + name: ' main ', + bundle: { ...requestDefinition, dist: ' ./dist/i18n ' }, + }); + + expect(result).toEqual({ message: 'Bundle "main" added successfully' }); + expect(core.validateBundleKey).toHaveBeenCalledWith('main'); + expect(core.validateBundleDefinition).toHaveBeenCalledWith( + { bundleName: 'main.{locale}', dist: './dist/i18n', collections: 'All' }, + config, + ); + expect(core.addBundleDefinition).toHaveBeenCalledWith( + 'main', + { bundleName: 'main.{locale}', dist: './dist/i18n', collections: 'All' }, + { cwd: process.cwd() }, + ); + }); + + it('returns 400 with every validation message and does not write', () => { + (core.validateBundleKey as jest.Mock).mockReturnValue(['Bundle name is required.']); + (core.validateBundleDefinition as jest.Mock).mockReturnValue(['dist (output folder) is required.']); + + try { + controller.createBundle({ name: 'x', bundle: requestDefinition }); + throw new Error('expected an HttpException'); + } catch (error: unknown) { + expect(error).toBeInstanceOf(HttpException); + expect((error as HttpException).getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect((error as HttpException).getResponse()).toEqual({ + message: 'Invalid bundle definition', + errors: ['Bundle name is required.', 'dist (output folder) is required.'], + }); + } + expect(core.addBundleDefinition).not.toHaveBeenCalled(); + }); + + it('returns 400 when the body carries no definition', () => { + expect(statusOf(() => controller.createBundle({ name: 'main' } as never))).toBe(HttpStatus.BAD_REQUEST); + expect(statusOf(() => controller.createBundle({ bundle: requestDefinition } as never))).toBe( + HttpStatus.BAD_REQUEST, + ); + }); + + it('returns 409 when core reports the bundle already exists', () => { + (core.addBundleDefinition as jest.Mock).mockImplementation(() => { + throw new Error('Bundle "main" already exists'); + }); + + expect(() => controller.createBundle({ name: 'main', bundle: requestDefinition })).toThrow(ConflictException); + }); + }); + + describe('PUT /bundles/:name', () => { + it('returns 404 when the bundle does not exist', () => { + expect(() => controller.updateBundle('missing', { bundle: requestDefinition })).toThrow(NotFoundException); + expect(core.updateBundleDefinition).not.toHaveBeenCalled(); + }); + + it('updates in place when no rename is requested', () => { + (core.updateBundleDefinition as jest.Mock).mockReturnValue({ message: 'updated' }); + + const result = controller.updateBundle('tracker', { bundle: requestDefinition }); + + expect(result).toEqual({ message: 'updated' }); + expect(core.updateBundleDefinition).toHaveBeenCalledWith( + 'tracker', + { bundleName: 'main.{locale}', dist: './dist/i18n', collections: 'All' }, + { cwd: process.cwd() }, + ); + }); + + it('URI-decodes the name and renames via body.name', () => { + (core.updateBundleDefinition as jest.Mock).mockReturnValue({ message: 'renamed' }); + + controller.updateBundle('tracker', { name: 'tracker-v2', bundle: requestDefinition }); + + expect(core.validateBundleKey).toHaveBeenCalledWith('tracker-v2'); + expect(core.updateBundleDefinition).toHaveBeenCalledWith('tracker', expect.any(Object), { + cwd: process.cwd(), + newKey: 'tracker-v2', + }); + }); + + it('returns 409 when renaming onto an existing bundle', () => { + expect(() => controller.updateBundle('tracker', { name: 'other', bundle: requestDefinition })).toThrow( + ConflictException, + ); + expect(core.updateBundleDefinition).not.toHaveBeenCalled(); + }); + + it('returns 400 when the definition is invalid', () => { + (core.validateBundleDefinition as jest.Mock).mockReturnValue(['bundleName is required.']); + + expect(statusOf(() => controller.updateBundle('tracker', { bundle: requestDefinition }))).toBe( + HttpStatus.BAD_REQUEST, + ); + expect(core.updateBundleDefinition).not.toHaveBeenCalled(); + }); + }); + + describe('DELETE /bundles/:name', () => { + it('deletes an existing bundle', () => { + (core.deleteBundleDefinition as jest.Mock).mockReturnValue({ message: 'deleted' }); + + expect(controller.deleteBundle('tracker')).toEqual({ message: 'deleted' }); + expect(core.deleteBundleDefinition).toHaveBeenCalledWith('tracker', { cwd: process.cwd() }); + }); + + it('returns 404 for an unknown bundle', () => { + expect(() => controller.deleteBundle('missing')).toThrow(NotFoundException); + expect(core.deleteBundleDefinition).not.toHaveBeenCalled(); + }); + + it('maps a core not-found error to 404', () => { + (core.deleteBundleDefinition as jest.Mock).mockImplementation(() => { + throw new Error('Bundle "tracker" not found'); + }); + + expect(() => controller.deleteBundle('tracker')).toThrow(NotFoundException); + }); + }); + + describe('POST /bundles/dry-run', () => { + it('plans the definition from the request body, not the saved one', () => { + (core.planBundle as jest.Mock).mockReturnValue(plan); + + const result = controller.dryRun({ name: 'preview', bundle: requestDefinition, locales: ['en'] }); + + expect(core.planBundle).toHaveBeenCalledWith({ + bundleKey: 'preview', + bundleDefinition: { bundleName: 'main.{locale}', dist: './dist/i18n', collections: 'All' }, + config, + locales: ['en'], + cwd: process.cwd(), + }); + expect(result.name).toBe('preview'); + expect(result.files).toEqual([ + { path: 'dist/i18n/main.en.json', kind: 'bundle', locale: 'en', exists: false, keysCount: 2 }, + ]); + }); + + it('omits locales from the plan when the request has none', () => { + (core.planBundle as jest.Mock).mockReturnValue(plan); + + controller.dryRun({ name: 'preview', bundle: requestDefinition }); + + expect('locales' in (core.planBundle as jest.Mock).mock.calls[0][0]).toBe(false); + }); + + it('returns 400 for an invalid definition without planning', () => { + (core.validateBundleDefinition as jest.Mock).mockReturnValue(['dist (output folder) is required.']); + + expect(statusOf(() => controller.dryRun({ name: 'preview', bundle: requestDefinition }))).toBe( + HttpStatus.BAD_REQUEST, + ); + expect(core.planBundle).not.toHaveBeenCalled(); + }); + + it('returns 400 for a locale outside the project locales', () => { + expect(statusOf(() => controller.dryRun({ name: 'preview', bundle: requestDefinition, locales: ['xx'] }))).toBe( + HttpStatus.BAD_REQUEST, + ); + expect(core.planBundle).not.toHaveBeenCalled(); + }); + }); + + describe('POST /bundles/:name/generate', () => { + it('starts a job and answers 202 with its snapshot', () => { + const snapshot = { jobId: 'job-1', bundleName: 'tracker', status: 'pending', progress: { current: 0, total: 0 } }; + jobService.startJob.mockReturnValue('job-1'); + jobService.getJob.mockReturnValue(snapshot); + const { response, status, json } = makeResponse(); + + controller.generateBundle('tracker', { locales: ['fr-ca'] }, response); + + expect(jobService.startJob).toHaveBeenCalledWith({ + bundleName: 'tracker', + bundleDefinition: existingDefinition, + config, + locales: ['fr-ca'], + }); + expect(status).toHaveBeenCalledWith(HttpStatus.ACCEPTED); + expect(json).toHaveBeenCalledWith(snapshot); + }); + + it('tolerates an empty body', () => { + jobService.startJob.mockReturnValue('job-2'); + jobService.getJob.mockReturnValue({}); + const { response, status } = makeResponse(); + + controller.generateBundle('tracker', undefined as never, response); + + expect(jobService.startJob).toHaveBeenCalledWith({ + bundleName: 'tracker', + bundleDefinition: existingDefinition, + config, + }); + expect(status).toHaveBeenCalledWith(HttpStatus.ACCEPTED); + }); + + it('returns 404 for an unknown bundle', () => { + const { response } = makeResponse(); + + expect(() => controller.generateBundle('missing', {}, response)).toThrow(NotFoundException); + expect(jobService.startJob).not.toHaveBeenCalled(); + }); + + it('returns 400 for a locale outside the project locales', () => { + const { response } = makeResponse(); + + expect(statusOf(() => controller.generateBundle('tracker', { locales: ['en', 'xx'] }, response))).toBe( + HttpStatus.BAD_REQUEST, + ); + expect(jobService.startJob).not.toHaveBeenCalled(); + }); + }); + + describe('GET /bundles/jobs/:jobId', () => { + it('returns the job snapshot', () => { + const snapshot = { + jobId: 'job-1', + bundleName: 'tracker', + status: 'completed', + progress: { current: 2, total: 2 }, + }; + jobService.getJob.mockReturnValue(snapshot); + + expect(controller.getJob('job-1')).toBe(snapshot); + }); + + it('returns 404 for an unknown job', () => { + jobService.getJob.mockReturnValue(undefined); + + expect(() => controller.getJob('nope')).toThrow(NotFoundException); + }); + }); +}); diff --git a/apps/api/src/app/bundles/bundles.controller.ts b/apps/api/src/app/bundles/bundles.controller.ts new file mode 100644 index 00000000..08a54170 --- /dev/null +++ b/apps/api/src/app/bundles/bundles.controller.ts @@ -0,0 +1,234 @@ +import { + Body, + ConflictException, + Controller, + Delete, + Get, + HttpException, + HttpStatus, + NotFoundException, + Param, + Post, + Put, + Res, +} from '@nestjs/common'; +import type { BundleDefinition, LingoTrackerConfig } from '@simoncodes-ca/core'; +import { + addBundleDefinition, + deleteBundleDefinition, + planBundle, + updateBundleDefinition, + validateBundleDefinition, + validateBundleKey, +} from '@simoncodes-ca/core'; +import type { + BundleDefinitionDto, + BundleDryRunRequestDto, + BundleDryRunResultDto, + BundleGenerateJobDto, + CreateBundleDto, + GenerateBundleRequestDto, + UpdateBundleDto, +} from '@simoncodes-ca/data-transfer'; +import type { Response } from 'express'; +import { ConfigService } from '../config/config.service'; +import { mapBundlePlanToDto, mapDtoToBundleDefinition } from '../mappers/bundle.mapper'; +import { BundleJobService } from './bundle-job.service'; + +const INVALID_DEFINITION_MESSAGE = 'Invalid bundle definition'; + +@Controller('bundles') +export class BundlesController { + readonly #configService: ConfigService; + readonly #jobService: BundleJobService; + + constructor(configService: ConfigService, jobService: BundleJobService) { + this.#configService = configService; + this.#jobService = jobService; + } + + /** Plans a bundle from the request body. The definition need not be saved. */ + @Post('dry-run') + dryRun(@Body() body: BundleDryRunRequestDto): BundleDryRunResultDto { + try { + const config = this.#configService.getConfig(); + const name = requireName(body?.name); + const definition = this.#validatedDefinition(name, body?.bundle, config); + const locales = this.#validatedLocales(body?.locales, config); + + const plan = planBundle({ + bundleKey: name, + bundleDefinition: definition, + config, + ...(locales && { locales }), + cwd: process.cwd(), + }); + + return mapBundlePlanToDto(plan); + } catch (error: unknown) { + throw this.#toHttpException(error, 'Error planning bundle'); + } + } + + @Get('jobs/:jobId') + getJob(@Param('jobId') jobId: string): BundleGenerateJobDto { + const job = this.#jobService.getJob(jobId); + + if (job === undefined) { + throw new NotFoundException(`Bundle job "${jobId}" not found`); + } + + return job; + } + + @Post() + createBundle(@Body() body: CreateBundleDto): { message: string } { + try { + const config = this.#configService.getConfig(); + const name = requireName(body?.name); + const definition = this.#validatedDefinition(name, body?.bundle, config); + + return addBundleDefinition(name, definition, { cwd: process.cwd() }); + } catch (error: unknown) { + throw this.#toHttpException(error, 'Error creating bundle'); + } + } + + @Put(':name') + updateBundle(@Param('name') name: string, @Body() body: UpdateBundleDto): { message: string } { + try { + const decodedName = decodeURIComponent(name); + const config = this.#configService.getConfig(); + this.#requireExistingBundle(decodedName, config); + + const newName = typeof body?.name === 'string' && body.name.trim().length > 0 ? body.name.trim() : undefined; + const targetName = newName ?? decodedName; + const definition = this.#validatedDefinition(targetName, body?.bundle, config); + + if (newName !== undefined && newName !== decodedName && config.bundles?.[newName]) { + throw new ConflictException(`Bundle "${newName}" already exists`); + } + + return updateBundleDefinition(decodedName, definition, { + cwd: process.cwd(), + ...(newName !== undefined && newName !== decodedName && { newKey: newName }), + }); + } catch (error: unknown) { + throw this.#toHttpException(error, 'Error updating bundle'); + } + } + + @Delete(':name') + deleteBundle(@Param('name') name: string): { message: string } { + try { + const decodedName = decodeURIComponent(name); + const config = this.#configService.getConfig(); + this.#requireExistingBundle(decodedName, config); + + return deleteBundleDefinition(decodedName, { cwd: process.cwd() }); + } catch (error: unknown) { + throw this.#toHttpException(error, 'Error deleting bundle'); + } + } + + /** Starts a generation job for a saved bundle and answers 202 with the job snapshot. */ + @Post(':name/generate') + generateBundle(@Param('name') name: string, @Body() body: GenerateBundleRequestDto, @Res() response: Response): void { + const decodedName = decodeURIComponent(name); + const config = this.#configService.getConfig(); + const bundleDefinition = this.#requireExistingBundle(decodedName, config); + const locales = this.#validatedLocales(body?.locales, config); + + const jobId = this.#jobService.startJob({ + bundleName: decodedName, + bundleDefinition, + config, + ...(locales && { locales }), + }); + + const job = this.#jobService.getJob(jobId); + response.status(HttpStatus.ACCEPTED).json(job); + } + + #requireExistingBundle(name: string, config: LingoTrackerConfig): BundleDefinition { + const definition = config.bundles?.[name]; + + if (!definition) { + throw new NotFoundException(`Bundle "${name}" not found`); + } + + return definition; + } + + /** Maps and validates a definition body; throws 400 with every message when invalid. */ + #validatedDefinition( + name: string, + dto: BundleDefinitionDto | undefined, + config: LingoTrackerConfig, + ): BundleDefinition { + if (!dto || typeof dto !== 'object') { + throw new HttpException( + { message: INVALID_DEFINITION_MESSAGE, errors: ['bundle definition is required.'] }, + HttpStatus.BAD_REQUEST, + ); + } + + const definition = mapDtoToBundleDefinition(dto); + const errors = [...validateBundleKey(name), ...validateBundleDefinition(definition, config)]; + + if (errors.length > 0) { + throw new HttpException({ message: INVALID_DEFINITION_MESSAGE, errors }, HttpStatus.BAD_REQUEST); + } + + return definition; + } + + /** Accepts an optional locale subset; every entry must be a configured locale. */ + #validatedLocales(locales: readonly string[] | undefined, config: LingoTrackerConfig): string[] | undefined { + if (locales === undefined) { + return undefined; + } + + if (!Array.isArray(locales) || locales.some((locale) => typeof locale !== 'string')) { + throw new HttpException('locales must be an array of strings', HttpStatus.BAD_REQUEST); + } + + const unknown = locales.filter((locale) => !config.locales.includes(locale)); + if (unknown.length > 0) { + throw new HttpException( + `Unknown locale${unknown.length > 1 ? 's' : ''} ${unknown.map((l) => `"${l}"`).join(', ')}: must be defined in the project locales`, + HttpStatus.BAD_REQUEST, + ); + } + + return [...locales]; + } + + #toHttpException(error: unknown, fallback: string): HttpException { + if (error instanceof HttpException) { + return error; + } + + const message = error instanceof Error ? error.message : fallback; + + if (message.includes('not found')) { + return new NotFoundException(message); + } + + if (message.includes('already exists')) { + return new ConflictException(message); + } + + return new HttpException(message, HttpStatus.BAD_REQUEST); + } +} + +function requireName(name: unknown): string { + if (typeof name !== 'string' || name.trim().length === 0) { + throw new HttpException( + { message: INVALID_DEFINITION_MESSAGE, errors: ['Bundle name is required.'] }, + HttpStatus.BAD_REQUEST, + ); + } + return name.trim(); +} diff --git a/apps/api/src/app/config/config.controller.spec.ts b/apps/api/src/app/config/config.controller.spec.ts index c0f11f19..4a395234 100644 --- a/apps/api/src/app/config/config.controller.spec.ts +++ b/apps/api/src/app/config/config.controller.spec.ts @@ -1,9 +1,10 @@ -import { Test, type TestingModule } from '@nestjs/testing'; +import { basename } from 'node:path'; import { HttpException } from '@nestjs/common'; -import { ConfigController } from './config.controller'; -import { ConfigService } from './config.service'; +import { Test, type TestingModule } from '@nestjs/testing'; import { resolveProtectedTermsForConfig, setGlobalProtectedTerms } from '@simoncodes-ca/core'; import * as mapper from '../mappers/config.mapper'; +import { ConfigController } from './config.controller'; +import { ConfigService } from './config.service'; jest.mock('@simoncodes-ca/core', () => ({ setGlobalProtectedTerms: jest.fn(), @@ -59,7 +60,7 @@ describe('ConfigController', () => { const mapSpy = jest.spyOn(mapper, 'mapConfigToDto'); controller.getConfig(); - expect(mapSpy).toHaveBeenCalledWith(baseConfig, resolved); + expect(mapSpy).toHaveBeenCalledWith(baseConfig, resolved, basename(process.cwd())); }); it('exposes the resolved terms and their file path on the DTO', () => { @@ -74,6 +75,12 @@ describe('ConfigController', () => { expect(dto.protectedTerms).toEqual(['iPhone']); expect(dto.protectedTermsFilePath).toBe('/project/.lingo-tracker-protected-terms.json'); }); + + it('exposes the served workspace folder name as projectName', () => { + const dto = controller.getConfig(); + + expect(dto.projectName).toBe(basename(process.cwd())); + }); }); describe('updateConfig', () => { diff --git a/apps/api/src/app/config/config.controller.ts b/apps/api/src/app/config/config.controller.ts index 475b396c..bc8830af 100644 --- a/apps/api/src/app/config/config.controller.ts +++ b/apps/api/src/app/config/config.controller.ts @@ -1,3 +1,4 @@ +import { basename } from 'node:path'; import { Body, Controller, Get, HttpException, HttpStatus, Put } from '@nestjs/common'; import { resolveProtectedTermsForConfig, setGlobalProtectedTerms } from '@simoncodes-ca/core'; import type { LingoTrackerConfigDto, UpdateConfigDto } from '@simoncodes-ca/data-transfer'; @@ -11,7 +12,7 @@ export class ConfigController { @Get() getConfig(): LingoTrackerConfigDto { const config = this.configService.getConfig(); - return mapConfigToDto(config, resolveProtectedTermsForConfig(config)); + return mapConfigToDto(config, resolveProtectedTermsForConfig(config), basename(process.cwd())); } /** diff --git a/apps/api/src/app/mappers/bundle.mapper.spec.ts b/apps/api/src/app/mappers/bundle.mapper.spec.ts new file mode 100644 index 00000000..aacd6452 --- /dev/null +++ b/apps/api/src/app/mappers/bundle.mapper.spec.ts @@ -0,0 +1,255 @@ +import type { BundleDefinition, BundlePlan, GenerateBundleResult } from '@simoncodes-ca/core'; +import type { BundleDefinitionDto } from '@simoncodes-ca/data-transfer'; +import { + MAX_CONFLICT_KEYS, + mapBundleDefinitionToDto, + mapBundlePlanToDto, + mapDtoToBundleDefinition, + mapGenerateBundleResultToJobResult, +} from './bundle.mapper'; + +describe('bundle.mapper', () => { + const fullDefinition: BundleDefinition = { + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: [ + { + name: 'app', + bundledKeyPrefix: 'app', + entriesSelectionRules: [{ matchingPattern: 'apps.*', matchingTags: ['ui'], matchingTagOperator: 'All' }], + mergeStrategy: 'override', + }, + ], + typeDistFile: './dist/i18n-types/main.ts', + tokenCasing: 'camelCase', + tokenConstantName: 'MAIN_KEYS', + transformICUToTransloco: false, + }; + + describe('mapBundleDefinitionToDto', () => { + it('maps every field and copies nested arrays', () => { + const dto = mapBundleDefinitionToDto(fullDefinition); + + expect(dto).toEqual(fullDefinition); + expect(dto.collections).not.toBe(fullDefinition.collections); + const [collection] = dto.collections as Exclude; + const [sourceCollection] = fullDefinition.collections as Exclude; + expect(collection.entriesSelectionRules).not.toBe(sourceCollection.entriesSelectionRules); + }); + + it("passes 'All' through and omits absent optionals", () => { + const dto = mapBundleDefinitionToDto({ bundleName: '{locale}', dist: './dist', collections: 'All' }); + + expect(dto).toEqual({ bundleName: '{locale}', dist: './dist', collections: 'All' }); + expect('typeDistFile' in dto).toBe(false); + expect('transformICUToTransloco' in dto).toBe(false); + }); + }); + + describe('mapDtoToBundleDefinition', () => { + it('trims strings and drops empty optionals', () => { + const dto: BundleDefinitionDto = { + bundleName: ' main.{locale} ', + dist: ' ./dist ', + collections: [ + { + name: ' app ', + bundledKeyPrefix: ' ', + entriesSelectionRules: [{ matchingPattern: ' apps.* ', matchingTags: [' ui ', ''] }], + }, + ], + typeDistFile: '', + tokenConstantName: ' ', + }; + + const definition = mapDtoToBundleDefinition(dto); + + expect(definition).toEqual({ + bundleName: 'main.{locale}', + dist: './dist', + collections: [{ name: 'app', entriesSelectionRules: [{ matchingPattern: 'apps.*', matchingTags: ['ui'] }] }], + }); + expect('typeDistFile' in definition).toBe(false); + expect('tokenConstantName' in definition).toBe(false); + }); + + it('keeps explicit false for transformICUToTransloco and passes All through', () => { + const definition = mapDtoToBundleDefinition({ + bundleName: '{locale}', + dist: './dist', + collections: 'All', + transformICUToTransloco: false, + tokenCasing: 'upperCase', + }); + + expect(definition.transformICUToTransloco).toBe(false); + expect(definition.tokenCasing).toBe('upperCase'); + expect(definition.collections).toBe('All'); + }); + + it('does not alias arrays from the DTO', () => { + const tags = ['ui']; + const dto: BundleDefinitionDto = { + bundleName: '{locale}', + dist: './dist', + collections: [{ name: 'app', entriesSelectionRules: [{ matchingPattern: '*', matchingTags: tags }] }], + }; + + const definition = mapDtoToBundleDefinition(dto); + const [collection] = definition.collections as Exclude; + const [rule] = collection.entriesSelectionRules as Exclude; + + expect(rule.matchingTags).toEqual(['ui']); + expect(rule.matchingTags).not.toBe(tags); + }); + + it('round-trips a full definition', () => { + expect(mapDtoToBundleDefinition(mapBundleDefinitionToDto(fullDefinition))).toEqual(fullDefinition); + }); + }); + + describe('mapBundlePlanToDto', () => { + const plan: BundlePlan = { + bundleKey: 'main', + locales: ['en', 'fr'], + files: [ + { + path: 'dist/en.json', + absolutePath: '/w/dist/en.json', + kind: 'bundle', + locale: 'en', + exists: true, + keysCount: 3, + }, + { path: 'dist/main.ts', absolutePath: '/w/dist/main.ts', kind: 'types', exists: false, keysCount: 3 }, + ], + keysPerLocale: { en: 3, fr: 2 }, + conflictsCount: 60, + conflictKeys: Array.from({ length: 60 }, (_, index) => `key.${index}`), + hierarchicalConflicts: Array.from({ length: 60 }, (_, index) => `parent.${index}`), + exampleKey: { collectionName: 'app', sourceKey: 'a.b', bundledKey: 'a.b', tokenPath: 'MAIN_TOKENS.A.B' }, + warnings: ['w'], + }; + + it('drops absolutePath and keeps locale only on bundle files', () => { + const dto = mapBundlePlanToDto(plan); + + expect(dto.files[0]).toEqual({ path: 'dist/en.json', kind: 'bundle', locale: 'en', exists: true, keysCount: 3 }); + expect(dto.files[1]).toEqual({ path: 'dist/main.ts', kind: 'types', exists: false, keysCount: 3 }); + expect('absolutePath' in dto.files[0]).toBe(false); + expect('locale' in dto.files[1]).toBe(false); + }); + + it('caps conflictKeys but reports the full conflictsCount', () => { + const dto = mapBundlePlanToDto(plan); + + expect(dto.conflictKeys).toHaveLength(MAX_CONFLICT_KEYS); + expect(dto.conflictsCount).toBe(60); + }); + + it('caps hierarchicalConflicts the same way as conflictKeys', () => { + const dto = mapBundlePlanToDto(plan); + + expect(dto.hierarchicalConflicts).toHaveLength(MAX_CONFLICT_KEYS); + expect(dto.hierarchicalConflicts[0]).toBe('parent.0'); + }); + + it('maps name, locales, exampleKey and warnings', () => { + const dto = mapBundlePlanToDto(plan); + + expect(dto.name).toBe('main'); + expect(dto.locales).toEqual(['en', 'fr']); + expect(dto.exampleKey).toEqual(plan.exampleKey); + expect(dto.warnings).toEqual(['w']); + expect(dto.keysPerLocale).toEqual({ en: 3, fr: 2 }); + }); + + it('omits exampleKey when the plan has none', () => { + const dto = mapBundlePlanToDto({ ...plan, exampleKey: undefined }); + + expect('exampleKey' in dto).toBe(false); + }); + }); + + describe('mapGenerateBundleResultToJobResult', () => { + const definition: BundleDefinition = { + bundleName: 'main.{locale}', + dist: './dist/i18n', + collections: 'All', + typeDistFile: './dist/types/main.ts', + }; + + const result: GenerateBundleResult = { + bundleKey: 'main', + filesGenerated: 2, + warnings: ['Bundle empty for de'], + localesProcessed: ['en', 'fr'], + keysPerLocale: { en: 4, fr: 4 }, + typeGenerationResult: { + bundleKey: 'main', + typeDistFile: './dist/types/main.ts', + keysCount: 4, + fileGenerated: true, + }, + }; + + it('rebuilds written paths from processed locales and appends the types file', () => { + const dto = mapGenerateBundleResultToJobResult(result, definition, '/workspace'); + + expect(dto.filesGenerated).toEqual(['dist/i18n/main.en.json', 'dist/i18n/main.fr.json', 'dist/types/main.ts']); + expect(dto.typeDistFile).toBe('dist/types/main.ts'); + expect(dto.typesKeysCount).toBe(4); + expect(dto.localesProcessed).toEqual(['en', 'fr']); + expect(dto.keysPerLocale).toEqual({ en: 4, fr: 4 }); + expect(dto.warnings).toEqual(['Bundle empty for de']); + }); + + it('makes an absolute types path relative to cwd', () => { + const dto = mapGenerateBundleResultToJobResult( + { + ...result, + typeGenerationResult: { + ...result.typeGenerationResult, + typeDistFile: '/workspace/dist/types/main.ts', + } as never, + }, + definition, + '/workspace', + ); + + expect(dto.typeDistFile).toBe('dist/types/main.ts'); + expect(dto.filesGenerated).toContain('dist/types/main.ts'); + }); + + it('leaves paths outside cwd untouched', () => { + const dto = mapGenerateBundleResultToJobResult( + { + ...result, + typeGenerationResult: { ...result.typeGenerationResult, typeDistFile: '/elsewhere/types/main.ts' } as never, + }, + definition, + '/workspace', + ); + + expect(dto.typeDistFile).toBe('/elsewhere/types/main.ts'); + }); + + it('omits type fields when no types file was written', () => { + const dto = mapGenerateBundleResultToJobResult( + { ...result, typeGenerationResult: { ...result.typeGenerationResult, fileGenerated: false } as never }, + definition, + ); + + expect(dto.filesGenerated).toHaveLength(2); + expect('typeDistFile' in dto).toBe(false); + expect('typesKeysCount' in dto).toBe(false); + }); + + it('omits type fields when type generation did not run', () => { + const dto = mapGenerateBundleResultToJobResult({ ...result, typeGenerationResult: undefined }, definition); + + expect(dto.filesGenerated).toHaveLength(2); + expect(dto.typeDistFile).toBeUndefined(); + }); + }); +}); diff --git a/apps/api/src/app/mappers/bundle.mapper.ts b/apps/api/src/app/mappers/bundle.mapper.ts new file mode 100644 index 00000000..0c948358 --- /dev/null +++ b/apps/api/src/app/mappers/bundle.mapper.ts @@ -0,0 +1,193 @@ +import * as path from 'node:path'; +import type { + BundleDefinition, + BundlePlan, + CollectionBundleDefinition, + EntrySelectionRule, + GenerateBundleResult, +} from '@simoncodes-ca/core'; +import { getBundleOutputPath } from '@simoncodes-ca/core'; +import type { + BundleDefinitionDto, + BundleDryRunResultDto, + BundleGenerateJobResultDto, + CollectionBundleDefinitionDto, + EntrySelectionRuleDto, +} from '@simoncodes-ca/data-transfer'; + +/** Upper bound on conflict keys returned by a dry run so huge bundles stay cheap to serialise. */ +export const MAX_CONFLICT_KEYS = 50; + +export function mapBundleDefinitionToDto(definition: BundleDefinition): BundleDefinitionDto { + const dto: BundleDefinitionDto = { + bundleName: definition.bundleName, + dist: definition.dist, + collections: + definition.collections === 'All' + ? 'All' + : definition.collections.map((collection) => mapCollectionDefinitionToDto(collection)), + }; + + if (definition.typeDistFile !== undefined) dto.typeDistFile = definition.typeDistFile; + if (definition.tokenCasing !== undefined) dto.tokenCasing = definition.tokenCasing; + if (definition.tokenConstantName !== undefined) dto.tokenConstantName = definition.tokenConstantName; + if (definition.transformICUToTransloco !== undefined) { + dto.transformICUToTransloco = definition.transformICUToTransloco; + } + + return dto; +} + +function mapCollectionDefinitionToDto(collection: CollectionBundleDefinition): CollectionBundleDefinitionDto { + const dto: CollectionBundleDefinitionDto = { + name: collection.name, + entriesSelectionRules: + collection.entriesSelectionRules === 'All' + ? 'All' + : collection.entriesSelectionRules.map((rule) => mapRuleToDto(rule)), + }; + + if (collection.bundledKeyPrefix !== undefined) dto.bundledKeyPrefix = collection.bundledKeyPrefix; + if (collection.mergeStrategy !== undefined) dto.mergeStrategy = collection.mergeStrategy; + + return dto; +} + +function mapRuleToDto(rule: EntrySelectionRule): EntrySelectionRuleDto { + const dto: EntrySelectionRuleDto = { matchingPattern: rule.matchingPattern }; + + if (rule.matchingTags !== undefined) dto.matchingTags = [...rule.matchingTags]; + if (rule.matchingTagOperator !== undefined) dto.matchingTagOperator = rule.matchingTagOperator; + + return dto; +} + +/** + * Maps an incoming DTO to a core definition. Strings are trimmed and empty or + * undefined optionals are dropped so nothing spurious lands in the config file. + * Structural validation (required fields, unknown collections, …) is left to + * `validateBundleDefinition`; this mapper only normalises what it is given. + */ +export function mapDtoToBundleDefinition(dto: BundleDefinitionDto): BundleDefinition { + const definition: BundleDefinition = { + bundleName: trimString(dto.bundleName), + dist: trimString(dto.dist), + collections: Array.isArray(dto.collections) + ? dto.collections.map((collection) => mapDtoToCollectionDefinition(collection)) + : dto.collections, + }; + + const typeDistFile = optionalString(dto.typeDistFile); + if (typeDistFile !== undefined) definition.typeDistFile = typeDistFile; + + if (dto.tokenCasing !== undefined) definition.tokenCasing = dto.tokenCasing; + + const tokenConstantName = optionalString(dto.tokenConstantName); + if (tokenConstantName !== undefined) definition.tokenConstantName = tokenConstantName; + + if (typeof dto.transformICUToTransloco === 'boolean') { + definition.transformICUToTransloco = dto.transformICUToTransloco; + } + + return definition; +} + +function mapDtoToCollectionDefinition(dto: CollectionBundleDefinitionDto): CollectionBundleDefinition { + const collection: CollectionBundleDefinition = { + name: trimString(dto.name), + entriesSelectionRules: Array.isArray(dto.entriesSelectionRules) + ? dto.entriesSelectionRules.map((rule) => mapDtoToRule(rule)) + : dto.entriesSelectionRules, + }; + + const bundledKeyPrefix = optionalString(dto.bundledKeyPrefix); + if (bundledKeyPrefix !== undefined) collection.bundledKeyPrefix = bundledKeyPrefix; + + if (dto.mergeStrategy !== undefined) collection.mergeStrategy = dto.mergeStrategy; + + return collection; +} + +function mapDtoToRule(dto: EntrySelectionRuleDto): EntrySelectionRule { + const rule: EntrySelectionRule = { matchingPattern: trimString(dto.matchingPattern) }; + + if (Array.isArray(dto.matchingTags)) { + const tags = dto.matchingTags.map((tag) => trimString(tag)).filter((tag) => tag.length > 0); + if (tags.length > 0) rule.matchingTags = tags; + } + + if (dto.matchingTagOperator !== undefined) rule.matchingTagOperator = dto.matchingTagOperator; + + return rule; +} + +function trimString(value: unknown): string { + return typeof value === 'string' ? value.trim() : ''; +} + +function optionalString(value: unknown): string | undefined { + const trimmed = trimString(value); + return trimmed.length > 0 ? trimmed : undefined; +} + +/** Maps a dry-run plan to its DTO, dropping absolute paths and capping the conflict lists. */ +export function mapBundlePlanToDto(plan: BundlePlan): BundleDryRunResultDto { + return { + name: plan.bundleKey, + locales: [...plan.locales], + files: plan.files.map((file) => ({ + path: file.path, + kind: file.kind, + ...(file.locale !== undefined && { locale: file.locale }), + exists: file.exists, + keysCount: file.keysCount, + })), + keysPerLocale: { ...plan.keysPerLocale }, + conflictsCount: plan.conflictsCount, + conflictKeys: plan.conflictKeys.slice(0, MAX_CONFLICT_KEYS), + hierarchicalConflicts: plan.hierarchicalConflicts.slice(0, MAX_CONFLICT_KEYS), + ...(plan.exampleKey && { exampleKey: { ...plan.exampleKey } }), + warnings: [...plan.warnings], + }; +} + +/** + * Maps a finished `generateBundle` run to the job result DTO. Core reports a + * file count; the DTO carries paths, rebuilt from the processed locales and the + * bundle definition so the UI can list what was written. Core resolves the + * types file to an absolute path, so every path is normalised relative to `cwd`. + */ +export function mapGenerateBundleResultToJobResult( + result: GenerateBundleResult, + bundleDefinition: BundleDefinition, + cwd: string = process.cwd(), +): BundleGenerateJobResultDto { + const filesGenerated = result.localesProcessed.map((locale) => + toProjectRelative(getBundleOutputPath(bundleDefinition, locale), cwd), + ); + const types = result.typeGenerationResult; + const typeDistFile = + types?.fileGenerated && types.typeDistFile ? toProjectRelative(types.typeDistFile, cwd) : undefined; + + if (typeDistFile !== undefined) { + filesGenerated.push(typeDistFile); + } + + return { + filesGenerated, + keysPerLocale: { ...result.keysPerLocale }, + warnings: [...result.warnings], + localesProcessed: [...result.localesProcessed], + ...(typeDistFile !== undefined && { typeDistFile }), + ...(types?.fileGenerated && { typesKeysCount: types.keysCount }), + }; +} + +/** Returns `filePath` relative to `cwd` (posix separators) when it lives inside `cwd`; otherwise unchanged. */ +function toProjectRelative(filePath: string, cwd: string): string { + const normalized = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : path.normalize(filePath); + if (normalized.startsWith('..')) { + return filePath; + } + return normalized.split(path.sep).join('/'); +} diff --git a/apps/api/src/app/mappers/config.mapper.spec.ts b/apps/api/src/app/mappers/config.mapper.spec.ts index b177cae3..00e49483 100644 --- a/apps/api/src/app/mappers/config.mapper.spec.ts +++ b/apps/api/src/app/mappers/config.mapper.spec.ts @@ -1,6 +1,6 @@ import type { LingoTrackerConfig, ResolvedProtectedTerms } from '@simoncodes-ca/core'; -import { mapConfigToDto, mapDtoToConfigUpdate } from './config.mapper'; import { mapCollectionToDto, mapDtoToCollection } from './collection.mapper'; +import { mapConfigToDto, mapDtoToConfigUpdate } from './config.mapper'; describe('config.mapper', () => { describe('mapConfigToDto', () => { @@ -66,6 +66,30 @@ describe('config.mapper', () => { expect(dto.collections.app.protectedTerms).toBeUndefined(); expect(dto.protectedTermsFilePath).toBe('/project/.lingo-tracker-protected-terms.json'); }); + + it('maps bundle definitions by name', () => { + const dto = mapConfigToDto({ + ...config, + bundles: { + main: { bundleName: '{locale}', dist: './dist/i18n', collections: 'All', typeDistFile: './dist/main.ts' }, + }, + }); + + expect(dto.bundles).toEqual({ + main: { bundleName: '{locale}', dist: './dist/i18n', collections: 'All', typeDistFile: './dist/main.ts' }, + }); + }); + + it('omits bundles when the config has none', () => { + const dto = mapConfigToDto(config); + + expect('bundles' in dto).toBe(false); + }); + + it('exposes projectName only when provided', () => { + expect(mapConfigToDto(config, undefined, 'lingo-tracker').projectName).toBe('lingo-tracker'); + expect('projectName' in mapConfigToDto(config)).toBe(false); + }); }); describe('mapDtoToConfigUpdate', () => { diff --git a/apps/api/src/app/mappers/config.mapper.ts b/apps/api/src/app/mappers/config.mapper.ts index 50da457a..3119866b 100644 --- a/apps/api/src/app/mappers/config.mapper.ts +++ b/apps/api/src/app/mappers/config.mapper.ts @@ -1,5 +1,16 @@ -import type { LingoTrackerConfig, LingoTrackerCollection, ResolvedProtectedTerms } from '@simoncodes-ca/core'; -import type { LingoTrackerConfigDto, LingoTrackerCollectionDto, UpdateConfigDto } from '@simoncodes-ca/data-transfer'; +import type { + BundleDefinition, + LingoTrackerCollection, + LingoTrackerConfig, + ResolvedProtectedTerms, +} from '@simoncodes-ca/core'; +import type { + BundleDefinitionDto, + LingoTrackerCollectionDto, + LingoTrackerConfigDto, + UpdateConfigDto, +} from '@simoncodes-ca/data-transfer'; +import { mapBundleDefinitionToDto } from './bundle.mapper'; import { mapCollectionToDto } from './collection.mapper'; function mapConfigCollections( @@ -11,20 +22,33 @@ function mapConfigCollections( ); } +function mapConfigBundles(bundles: Record): Record { + return Object.fromEntries(Object.entries(bundles).map(([name, bundle]) => [name, mapBundleDefinitionToDto(bundle)])); +} + /** * Maps config to its DTO. `resolved` carries the protected terms already read from disk - * by the caller — the mapper itself stays free of file I/O. + * by the caller — the mapper itself stays free of file I/O. `projectName` is the served + * workspace folder name, supplied by the controller for the same reason. */ -export function mapConfigToDto(config: LingoTrackerConfig, resolved?: ResolvedProtectedTerms): LingoTrackerConfigDto { +export function mapConfigToDto( + config: LingoTrackerConfig, + resolved?: ResolvedProtectedTerms, + projectName?: string, +): LingoTrackerConfigDto { return { exportFolder: config.exportFolder, importFolder: config.importFolder, baseLocale: config.baseLocale, locales: [...config.locales], collections: mapConfigCollections(config.collections, resolved), + ...(config.bundles && { bundles: mapConfigBundles(config.bundles) }), + ...(config.tokenCasing && { tokenCasing: config.tokenCasing }), + ...(config.transformICUToTransloco !== undefined && { transformICUToTransloco: config.transformICUToTransloco }), translation: config.translation, protectedTerms: resolved?.globalTerms.length ? [...resolved.globalTerms] : undefined, protectedTermsFilePath: resolved?.globalFilePath, + ...(projectName && { projectName }), }; } diff --git a/architecture-docs/api.md b/architecture-docs/api.md index 9d066040..2ed5eab3 100644 --- a/architecture-docs/api.md +++ b/architecture-docs/api.md @@ -76,6 +76,19 @@ All paths are relative to the `/api` global prefix. URL path parameters that con | `POST` | `/collections/:collectionName/locales` | Add a locale to a collection (clears cache) | `AddLocaleDto` | `AddLocaleResponseDto` | | `DELETE` | `/collections/:collectionName/locales/:locale` | Remove a locale from a collection (clears cache) | — | `RemoveLocaleResponseDto` | +### Bundles + +Bundle definitions live under `bundles` in `.lingo-tracker.json` and are exposed on `GET /config`. Generation runs as an async job (one at a time, in order) that the client polls, mirroring the translation job flow. + +| Method | Path | Purpose | Request DTO | Response DTO | +|--------|------|---------|-------------|--------------| +| `POST` | `/bundles` | Create a [bundle](glossary.md#bundle) definition. Validation failures return 400 `{ message, errors[] }`; a duplicate name returns 409. | `CreateBundleDto` | `{ message: string }` | +| `PUT` | `/bundles/:name` | Replace a bundle definition, optionally renaming it via `name` in the body. 404 when missing, 400 when invalid, 409 when the new name is taken. | `UpdateBundleDto` | `{ message: string }` | +| `DELETE` | `/bundles/:name` | Remove a bundle definition (404 when missing) | — | `{ message: string }` | +| `POST` | `/bundles/dry-run` | Plan a bundle from the request body without writing anything. The definition does not have to be saved, so the UI can preview unsaved edits. | `BundleDryRunRequestDto` | `BundleDryRunResultDto` | +| `POST` | `/bundles/:name/generate` | Fire-and-forget: start a generation job for a saved bundle. Optional `locales` must be a subset of the project locales (400 otherwise). | `GenerateBundleRequestDto` | `BundleGenerateJobDto` (202 Accepted) | +| `GET` | `/bundles/jobs/:jobId` | Poll a bundle generation job by ID | — | `BundleGenerateJobDto` | + --- ## Component Diagram @@ -331,7 +344,8 @@ For the entity types that mappers transform, see [domain-and-data-model.md](doma | `resource-tree.mapper.ts` | `ResourceTreeNode` → `ResourceTreeDto` | Flattens `folderPathSegments[]` array to a dot-delimited `path` string; merges `source` (base locale value) into the `translations` record keyed by the base locale string; extracts per-locale `status` from the `metadata` record | | `resource-tree.mapper.ts` | `ResourceTreeEntry` → `ResourceSummaryDto` | Identifies the base locale by the absence of `status` and `baseChecksum` in the metadata entry; produces a flat `{ key, translations, status, comment, tags, inheritedTags }` shape. The `inheritedTags` field carries the parent collection's `tags` so the UI can render them distinctly without re-reading the config. | | `collection.mapper.ts` | `LingoTrackerCollectionDto` ↔ `LingoTrackerCollection` | Bidirectional; shallow clone of `locales[]` and `tags[]` arrays to prevent aliasing. Carries the `protectedTermsFile` setting in both directions. Drops resolved `protectedTerms` on the way back to config, because terms live in a file and the controller writes them there separately. | -| `config.mapper.ts` | `LingoTrackerConfig` → `LingoTrackerConfigDto` | Delegates collection mapping to `collection.mapper`; shallow clone of `locales[]`. Takes an optional `ResolvedProtectedTerms` from the controller, so the mapper itself reads no files. | +| `config.mapper.ts` | `LingoTrackerConfig` → `LingoTrackerConfigDto` | Delegates collection mapping to `collection.mapper` and bundle mapping to `bundle.mapper`; shallow clone of `locales[]`. Takes an optional `ResolvedProtectedTerms` and `projectName` (basename of the API's working directory) from the controller, so the mapper itself reads no files. | +| `bundle.mapper.ts` | `BundleDefinitionDto` ↔ `BundleDefinition`; `BundlePlan` → `BundleDryRunResultDto`; `GenerateBundleResult` → `BundleGenerateJobResultDto` | Bidirectional definition mapping trims strings and drops empty optionals so nothing spurious is written to the config. The plan mapper drops `absolutePath` and caps `conflictKeys` at 50. The job-result mapper rebuilds written file paths from `localesProcessed` plus the types file. | | `search-result.mapper.ts` | `SearchResult` → `SearchResultDto` | Structurally identical types; mapper exists for explicit API boundary documentation | **Why does `config.mapper.ts` take resolved terms as an argument?** Protected terms live in JSON files outside `.lingo-tracker.json`. Building the DTO therefore requires reading the filesystem. diff --git a/architecture-docs/feature-matrix.md b/architecture-docs/feature-matrix.md index 1c15a3b3..b773ab14 100644 --- a/architecture-docs/feature-matrix.md +++ b/architecture-docs/feature-matrix.md @@ -57,7 +57,10 @@ Each cell shows whether the operation is supported (`Yes`), not supported (`—` | Filter by status | — | — | Yes (status filter dropdown) | | Filter by locale | — | — | Yes (locale filter dropdown) | | **Bundle / Export / Import** | | | | -| Generate bundle (locale JSON + optional TS types) | Yes (`bundle`) | — | — | +| Define bundles (create / edit / delete) | — (edit `.lingo-tracker.json`) | Yes (`POST /bundles`, `PUT /bundles/:name`, `DELETE /bundles/:name`) | Yes (bundle form dialog on the home page) | +| Preview a bundle without writing (dry run) | — | Yes (`POST /bundles/dry-run`) | Yes (live preview column in the bundle dialog) | +| Generate bundle (locale JSON + optional TS types) | Yes (`bundle`) | Yes (`POST /bundles/:name/generate`, async job) | Yes (Generate / Generate all on the home page) | +| Poll bundle generation job status | — | Yes (`GET /bundles/jobs/:jobId`) | Yes (inline progress on the bundle card) | | Export resources (XLIFF or JSON) | Yes (`export`) | — | — | | Import translations (XLIFF or JSON) | Yes (`import`) | — | — | | **[Protected Terms](glossary.md#protected-term)** | | | | @@ -79,6 +82,7 @@ Each cell shows whether the operation is supported (`Yes`), not supported (`—` - **In-memory collection cache** — `CollectionCacheService` is an API-only system. It holds a single-slot, incrementally-updated in-memory tree of the active collection, making browsing fast for the Tracker UI. The CLI bypasses the cache entirely and reads files directly on every invocation. See [`api.md — Collection Cache`](api.md#collection-cache). - **Async translation jobs** — The `TranslationJobService` (fire-and-forget job map with UUID-based polling) is API-only. The CLI's `translate-locale` command runs synchronously in-process and prints progress inline. +- **Async bundle jobs** — `BundleJobService` runs bundle generation through a sequential job queue with UUID-based polling. The CLI's `bundle` command runs synchronously in-process. See [`bundle-generation.md`](bundle-generation.md) for the shared pipeline. - **Batch resource creation** — The `POST /collections/:name/resources` endpoint accepts an array of `CreateResourceDto` objects. The CLI's `add-resource` and the UI's editor dialog only create one resource at a time. - **Rename collection** — Renaming exists only on the API and UI. The CLI's `edit-collection` command manages collection-level tags only; renaming a collection requires editing `.lingo-tracker.json` manually or using the UI. The `PUT` endpoint (and therefore the UI edit dialog) also handles adding and removing locales — it diffs the submitted locale list against the existing config and internally calls `addLocaleToCollection` / `removeLocaleFromCollection` as needed. @@ -86,7 +90,6 @@ Each cell shows whether the operation is supported (`Yes`), not supported (`—` - **`init`** — Project bootstrapping (creating `.lingo-tracker.json`) is a CLI-only operation. The API and UI require a config file to already exist before they can start. - **`install-skill`** — Generates a `.claude/` AI skill file templated to the current repository. No equivalent in the API or UI. -- **`bundle`** — Generating deployment locale JSON files and TypeScript token constants is a CLI-only pipeline. See [`bundle-generation.md`](bundle-generation.md). - **`export` / `import`** — XLIFF and JSON import/export workflows for integration with external translation agencies are CLI-only. - **`validate`** — The CI validation gate (exit code 1 on failures) is CLI-only. The UI shows per-resource status visually but does not produce a machine-readable validation report. - **`normalize`** — Repair of checksum drift, backfilling of missing locale entries, and cleanup of empty folders is CLI-only. From b55194444195f48cb9bdd172daa475f16e4a3768 Mon Sep 17 00:00:00 2001 From: snodel Date: Wed, 16 Sep 2026 14:00:31 -0700 Subject: [PATCH 10/13] feat(tracker): manage and generate bundles from the collections page The collections page becomes a split view: collections on the left, a sticky column of bundle cards on the right. A bundle can be created, edited, deleted, generated on its own or with every other one, and its run reports progress and then a result strip inside its own card, so the developer never leaves the page to find out what was written. Hovering or focusing a bundle draws a coral connector from each collection it consumes into a single port on the bundle card, so several inputs visibly converge on one output. The connector layer sits behind the cards, which lets a line cross the inner column and re-emerge rather than scribbling over another card's content; the lines stand down once the layout stacks and there is no gutter to route through. The bundle form is a master-detail dialog: a sections rail, the form itself, and a live preview column driven by a debounced dry run that shows the files the definition would write, the counts, and one worked example key. Under 1200px the preview folds into a disclosure at the bottom of the pane and under 800px the rail becomes a horizontal strip. Run state is mirrored to session storage. Generating the tracker bundle rewrites the Tracker's own i18n assets, which reloads the dev server page mid-run, and without the mirror the result strip vanished the moment it was earned. Co-Authored-By: Claude Opus 5 (1M context) --- .../collections/bundle-card/bundle-card.html | 201 +++ .../collections/bundle-card/bundle-card.scss | 647 +++++++++ .../bundle-card/bundle-card.spec.ts | 215 +++ .../collections/bundle-card/bundle-card.ts | 160 ++ .../bundle-form-dialog-data.ts | 21 + .../bundle-form-dialog.html | 817 +++++++++++ .../bundle-form-dialog.scss | 1281 +++++++++++++++++ .../bundle-form-dialog.spec.ts | 501 +++++++ .../bundle-form-dialog/bundle-form-dialog.ts | 733 ++++++++++ .../bundle-form-dialog/segmented-control.scss | 69 + .../bundle-form-dialog/segmented-control.ts | 126 ++ .../app/collections/collections-manager.html | 402 ++++-- .../app/collections/collections-manager.scss | 445 +++++- .../collections/collections-manager.spec.ts | 172 ++- .../app/collections/collections-manager.ts | 329 ++++- .../services/collections-api.service.ts | 51 + .../collections/store/collections.store.ts | 2 + .../features/with-bundles.feature.spec.ts | 423 ++++++ .../store/features/with-bundles.feature.ts | 388 +++++ .../i18n/bundles/card/resource_entries.json | 102 ++ .../src/i18n/bundles/card/tracker_meta.json | 302 ++++ .../dialog/collection/resource_entries.json | 212 +++ .../dialog/collection/tracker_meta.json | 632 ++++++++ .../dialog/collections/resource_entries.json | 82 ++ .../dialog/collections/tracker_meta.json | 242 ++++ .../dialog/footer/resource_entries.json | 52 + .../bundles/dialog/footer/tracker_meta.json | 152 ++ .../dialog/options/resource_entries.json | 32 + .../bundles/dialog/options/tracker_meta.json | 92 ++ .../dialog/output/resource_entries.json | 92 ++ .../bundles/dialog/output/tracker_meta.json | 272 ++++ .../dialog/preview/resource_entries.json | 172 +++ .../bundles/dialog/preview/tracker_meta.json | 512 +++++++ .../i18n/bundles/dialog/resource_entries.json | 92 ++ .../dialog/sections/resource_entries.json | 142 ++ .../bundles/dialog/sections/tracker_meta.json | 422 ++++++ .../src/i18n/bundles/dialog/tracker_meta.json | 272 ++++ .../dialog/types/resource_entries.json | 82 ++ .../bundles/dialog/types/tracker_meta.json | 242 ++++ .../dialog/validation/resource_entries.json | 122 ++ .../dialog/validation/tracker_meta.json | 362 +++++ .../i18n/bundles/empty/resource_entries.json | 32 + .../src/i18n/bundles/empty/tracker_meta.json | 92 ++ .../src/i18n/bundles/resource_entries.json | 62 + .../i18n/bundles/run/resource_entries.json | 152 ++ .../src/i18n/bundles/run/tracker_meta.json | 452 ++++++ .../i18n/bundles/toast/resource_entries.json | 42 + .../src/i18n/bundles/toast/tracker_meta.json | 122 ++ .../src/i18n/bundles/tracker_meta.json | 182 +++ .../i18n/collections/resource_entries.json | 16 +- .../src/i18n/collections/tracker_meta.json | 52 +- apps/tracker/src/styles.scss | 26 + .../src/testing/transloco-testing.module.ts | 44 + 53 files changed, 12745 insertions(+), 196 deletions(-) create mode 100644 apps/tracker/src/app/collections/bundle-card/bundle-card.html create mode 100644 apps/tracker/src/app/collections/bundle-card/bundle-card.scss create mode 100644 apps/tracker/src/app/collections/bundle-card/bundle-card.spec.ts create mode 100644 apps/tracker/src/app/collections/bundle-card/bundle-card.ts create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog-data.ts create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.html create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.scss create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.spec.ts create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.ts create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.scss create mode 100644 apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.ts create mode 100644 apps/tracker/src/app/collections/store/features/with-bundles.feature.spec.ts create mode 100644 apps/tracker/src/app/collections/store/features/with-bundles.feature.ts create mode 100644 apps/tracker/src/i18n/bundles/card/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/card/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/collection/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/collection/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/collections/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/collections/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/footer/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/footer/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/options/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/options/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/output/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/output/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/preview/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/preview/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/sections/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/sections/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/types/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/types/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/validation/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/dialog/validation/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/empty/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/empty/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/run/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/run/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/toast/resource_entries.json create mode 100644 apps/tracker/src/i18n/bundles/toast/tracker_meta.json create mode 100644 apps/tracker/src/i18n/bundles/tracker_meta.json diff --git a/apps/tracker/src/app/collections/bundle-card/bundle-card.html b/apps/tracker/src/app/collections/bundle-card/bundle-card.html new file mode 100644 index 00000000..e985aac5 --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-card/bundle-card.html @@ -0,0 +1,201 @@ +
+ + +
+

{{ entry().name }}

+

+ {{ outputPath().prefix }}@if (outputPath().placeholder) {{{ outputPath().placeholder }}}{{ outputPath().suffix }} +

+
+ +
+ + + + + + +
+
+ + +
+
+ +
{{ TOKENS.BUNDLES.CARD.COLLECTIONSLABEL | transloco }}
+
+
    + @if (includesAllCollections()) { +
  • + + {{ TOKENS.BUNDLES.CARD.ALLCOLLECTIONSX | transloco: { count: collectionNames().length } }} +
  • + } @else { + @for (name of collectionNames(); track name) { +
  • + + {{ name }} +
  • + } + } +
+
+
+ +
+ +
{{ TOKENS.BUNDLES.CARD.TYPESLABEL | transloco }}
+
+ @if (typeDistFile(); as file) { + {{ file }} + } @else { + {{ TOKENS.BUNDLES.CARD.TYPESOFF | transloco }} + } +
+
+
+ + +@if (isRunning()) { +
+ + + {{ TOKENS.BUNDLES.RUN.GENERATING | transloco }} + @if (currentFileName(); as file) { + {{ TOKENS.BUNDLES.RUN.WRITING | transloco }} {{ file }} + } + @if (progress(); as p) { + @if (p.total > 0) { + {{ TOKENS.BUNDLES.RUN.LOCALESPROGRESSX | transloco: { current: p.current, total: p.total } }} + } + } + + +
+} @else if (isCompleted()) { +
+
+ + + + {{ TOKENS.BUNDLES.RUN.GENERATED | transloco }} + {{ TOKENS.BUNDLES.RUN.FILESX | transloco: { count: filesCount() } }} + {{ TOKENS.BUNDLES.RUN.KEYSPERLOCALEX | transloco: { count: keysPerLocale() } }} + @if (hasWarnings()) { + {{ TOKENS.BUNDLES.RUN.WARNINGSX | transloco: { count: warningsCount() } }} + } + + + +
+ + +
+
+ + @if (detailsOpen()) { +
+

{{ TOKENS.BUNDLES.RUN.FILESWRITTEN | transloco }}

+
    + @for (item of resultFiles(); track item.locale) { +
  • + {{ item.file }} + @if (item.keys !== undefined) { + {{ TOKENS.BUNDLES.RUN.KEYSX | transloco: { count: item.keys } }} + } +
  • + } + @if (result()?.typeDistFile; as typesFile) { +
  • + {{ typesFile }} + @if (result()?.typesKeysCount; as typesKeys) { + {{ TOKENS.BUNDLES.RUN.KEYSX | transloco: { count: typesKeys } }} + } +
  • + } +
+ + @if (hasWarnings()) { +

{{ TOKENS.BUNDLES.RUN.WARNINGSHEADING | transloco }}

+
    + @for (warning of result()?.warnings; track $index) { +
  • {{ warning }}
  • + } +
+ } +
+ } +
+} @else if (isFailed()) { +
+ + + {{ TOKENS.BUNDLES.RUN.FAILED | transloco }} + @if (run()?.error; as message) { + {{ message }} + } + + +
+} + + diff --git a/apps/tracker/src/app/collections/bundle-card/bundle-card.scss b/apps/tracker/src/app/collections/bundle-card/bundle-card.scss new file mode 100644 index 00000000..e8025fa9 --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-card/bundle-card.scss @@ -0,0 +1,647 @@ +// ============================================================================= +// Bundle card — compact card for the home page bundles column. +// Shares the collection card anatomy; --card-surface* come from the page host. +// ============================================================================= + +:host { + background: var(--card-surface, var(--color-background)); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-xl); + box-shadow: var(--shadow-sm); + display: flex; + flex-direction: column; + padding: var(--spacing-4); + position: relative; + transition: + transform var(--transition-normal), + box-shadow var(--transition-normal), + border-color var(--transition-normal), + background var(--transition-normal); +} + +:host(.bundle-card--linked) { + background: var(--card-surface-hover, color-mix(in srgb, var(--color-primary) 3%, var(--color-background))); + border-color: color-mix(in srgb, var(--color-primary) 40%, var(--color-border)); + box-shadow: var(--shadow-md); + transform: translateY(-2px); + + .tile { + background: color-mix(in srgb, var(--color-primary) 20%, transparent); + border-color: color-mix(in srgb, var(--color-primary) 45%, transparent); + } +} + +// --------------------------------------------------------------------------- +// Header: tile, name + output path, menu +// --------------------------------------------------------------------------- +.card-header { + align-items: start; + display: grid; + gap: var(--spacing-3); + grid-template-columns: auto minmax(0, 1fr) auto; +} + +.tile { + align-items: center; + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent); + border-radius: var(--border-radius-lg); + display: flex; + flex: none; + height: 38px; + justify-content: center; + transition: background var(--transition-normal), border-color var(--transition-normal); + width: 38px; + + mat-icon { + color: var(--color-primary); + font-size: 19px; + height: 19px; + width: 19px; + } +} + +.card-heading { + min-width: 0; + padding-top: 1px; +} + +.bundle-name { + color: var(--color-text-primary); + font-family: var(--font-family-body); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + letter-spacing: -0.01em; + line-height: var(--line-height-tight); + margin: 0; + overflow-wrap: anywhere; +} + +.output-path { + color: var(--color-text-secondary); + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + line-height: 1.45; + margin: var(--spacing-1) 0 0; + overflow-wrap: anywhere; + + .var { + color: var(--color-text-primary); + font-weight: var(--font-weight-semibold); + } +} + +.card-actions { + flex: none; + margin: calc(-1 * var(--spacing-2)) calc(-1 * var(--spacing-2)) 0 0; +} + +.icon-button { + align-items: center; + background: none; + border: 0; + border-radius: var(--border-radius-full); + color: var(--color-text-secondary); + cursor: pointer; + display: grid; + height: 40px; + padding: 0; + place-items: center; + transition: color var(--transition-fast), background var(--transition-fast); + width: 40px; + + &:hover { + background: var(--color-background-subtle); + color: var(--color-text-primary); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + mat-icon { + font-size: 20px; + height: 20px; + width: 20px; + } +} + +// --------------------------------------------------------------------------- +// Meta rows: Collections chips, Types path +// --------------------------------------------------------------------------- +.meta { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + margin: var(--spacing-3) 0 0; +} + +.meta-row { + align-items: center; + display: flex; + gap: var(--spacing-2); + min-width: 0; +} + +.meta-icon { + color: var(--color-text-tertiary); + flex: none; + font-size: 16px; + height: 16px; + width: 16px; +} + +.meta-label { + color: var(--color-text-secondary); + flex: none; + font-size: var(--font-size-xs); + margin: 0; + width: 66px; +} + +.meta-value { + color: var(--color-text-secondary); + font-size: var(--font-size-xs); + margin: 0; + min-width: 0; + + .mono { + color: var(--color-text-primary); + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + overflow-wrap: anywhere; + } + + .muted { + color: var(--color-text-secondary); + } +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-2) var(--spacing-1); + list-style: none; + margin: 0; + min-width: 0; + padding: 0; +} + +// Text chip in its "soft" voice: Nunito, subtle ground, hairline. Coral text-weight when linked. +.chip { + align-items: center; + background: var(--color-background-subtle); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-full); + color: var(--color-text-primary); + display: inline-flex; + font-family: var(--font-family-body); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + gap: 5px; + line-height: 1.5; + padding: 1px var(--spacing-2) 1px 6px; + transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); + white-space: nowrap; + + mat-icon { + color: var(--color-text-secondary); + flex: none; + font-size: 14px; + height: 14px; + width: 14px; + } + + &--linked { + background: color-mix(in srgb, var(--color-primary) 8%, transparent); + border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-border)); + color: var(--color-primary-text); + + mat-icon { + color: var(--color-primary-text); + } + } +} + +// --------------------------------------------------------------------------- +// Result strips +// --------------------------------------------------------------------------- +.result { + border-radius: var(--border-radius-md); + font-size: var(--font-size-xs); + line-height: 1.45; + margin-top: var(--spacing-4); + overflow: hidden; + position: relative; + + mat-icon { + flex: none; + font-size: 16px; + height: 16px; + width: 16px; + } + + .mono { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + } + + // Clauses carry their own leading separator and never break internally. A wrapped + // line therefore starts with "· 3 warnings" instead of stranding a bare "·" at the + // end of the line above it, which is what a run of inline separators does. + .fig { + white-space: nowrap; + + & + .fig::before { + content: '·'; + margin: 0 5px; + opacity: 0.6; + } + } + + // A failure message is prose, not a figure, so it may break like prose. + .fig--prose { + min-width: 0; + overflow-wrap: anywhere; + white-space: normal; + } + + b { + font-weight: var(--font-weight-bold); + } +} + +// A flex row, not an inline run: Angular strips the whitespace between the clause +// spans, so inline layout has no break opportunity between them and the sentence +// runs straight under the controls instead of wrapping. +.result-text { + align-items: baseline; + display: flex; + flex: 1; + flex-wrap: wrap; + min-width: 0; + row-gap: 2px; +} + +.result-line, +.result--run, +.result--failed { + align-items: flex-start; + display: flex; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-3); +} + +// The glyph and the controls sit on the first line's optical centre; only the copy +// grows downwards. Centring them instead leaves both drifting whenever a locale wraps. +.result-line > mat-icon, +.result--run > mat-icon, +.result--failed > mat-icon, +.result-controls { + margin-top: 1px; +} + +.result-controls { + align-items: center; + display: flex; + flex: none; + gap: var(--spacing-1); +} + +.result--run { + background: var(--color-background-subtle); + border: 1px solid var(--color-border); + color: var(--color-text-secondary); + padding-bottom: calc(var(--spacing-2) + 2px); + + .mono { + color: var(--color-text-primary); + } +} + +.bar { + background: color-mix(in srgb, var(--color-primary) 18%, transparent); + bottom: 0; + height: 2px; + left: 0; + position: absolute; + right: 0; + + i { + background: var(--color-primary); + border-radius: 0 2px 2px 0; + display: block; + height: 100%; + transition: width var(--transition-slow); + } +} + +.result--ok { + background: color-mix(in srgb, var(--color-success) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent); + color: color-mix(in srgb, var(--color-success) 55%, var(--color-text-primary)); +} + +.result--warn { + background: color-mix(in srgb, var(--color-warning) 9%, transparent); + border-color: color-mix(in srgb, var(--color-warning) 26%, transparent); + color: var(--color-warning-text); +} + +.result--failed { + background: color-mix(in srgb, var(--color-error) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent); + color: color-mix(in srgb, var(--color-error) 55%, var(--color-text-primary)); +} + +.details-toggle { + align-items: center; + background: none; + border: 0; + border-radius: var(--border-radius-sm); + color: inherit; + cursor: pointer; + display: inline-flex; + font: inherit; + font-weight: var(--font-weight-semibold); + gap: 2px; + padding: 0 2px; + white-space: nowrap; + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + .chevron { + transition: transform var(--transition-normal); + } + + .result--open & .chevron { + transform: rotate(180deg); + } +} + +// Dismiss is the strip's own chrome, so it reads quieter than Details and sits +// outermost, against the edge it removes. +.dismiss { + align-items: center; + background: none; + border: 0; + border-radius: var(--border-radius-sm); + color: inherit; + cursor: pointer; + display: inline-flex; + justify-content: center; + height: 20px; + opacity: 0.65; + padding: 0; + transition: opacity var(--transition-fast); + width: 20px; + + &:hover { + opacity: 1; + } + + &:focus-visible { + opacity: 1; + outline: 2px solid var(--focus-ring-color); + outline-offset: 2px; + } + + mat-icon { + font-size: 15px; + height: 15px; + width: 15px; + } +} + +.result-details { + animation: details-reveal var(--transition-normal) both; + border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent); + color: var(--color-text-secondary); + display: flex; + flex-direction: column; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-3) var(--spacing-3); +} + +@keyframes details-reveal { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: none; + } +} + +.details-heading { + color: var(--color-text-primary); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-bold); + margin: 0; +} + +.details-list { + display: flex; + flex-direction: column; + gap: 2px; + list-style: none; + margin: 0; + padding: 0; + + li { + align-items: baseline; + display: flex; + gap: var(--spacing-3); + justify-content: space-between; + min-width: 0; + } + + .mono { + color: var(--color-text-primary); + min-width: 0; + overflow-wrap: anywhere; + } + + .num { + flex: none; + } + + // Generator warnings are full paragraphs of prose. The card is a summary + // surface, so show the opening lines of each (full text on hover) and leave + // the rest to the CLI rather than letting one warning run the whole column. + &--warnings li { + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + color: var(--color-warning-text); + display: -webkit-box; + line-height: var(--line-height-snug); + list-style: none; + max-width: 62ch; + overflow: hidden; + overflow-wrap: anywhere; + + // Marker kept as inline content so it stays inside the clamped box. + &::before { + content: '•'; + margin-right: var(--spacing-2); + } + } +} + +// --------------------------------------------------------------------------- +// Footer: locale count + Generate +// --------------------------------------------------------------------------- +.card-footer { + align-items: center; + border-top: 1px solid var(--color-border-subtle); + display: flex; + gap: var(--spacing-3); + justify-content: space-between; + margin-top: var(--spacing-4); + padding-top: var(--spacing-3); +} + +.footer-meta { + align-items: center; + color: var(--color-text-secondary); + display: flex; + font-size: var(--font-size-xs); + gap: var(--spacing-3); + min-width: 0; + + > span { + align-items: center; + display: inline-flex; + gap: 4px; + white-space: nowrap; + } + + mat-icon { + color: var(--color-text-tertiary); + font-size: 15px; + height: 15px; + width: 15px; + } +} + +.num { + font-variant-numeric: tabular-nums; +} + +// Hand-rolled outline button (the comp's .btn.outline.sm), a real + + +
+ + + + +
+ @switch (activeKind()) { + + @case ('output') { +
+
+

{{ TOKENS.BUNDLES.DIALOG.SECTIONS.OUTPUT | transloco }}

+

{{ TOKENS.BUNDLES.DIALOG.OUTPUT.DESCRIPTION | transloco }}

+
+
+ +
+ + @if (isEditMode) { +
+ + {{ form.controls.name.value }} +
+

+ } @else { +
+ +
+ @if (showError(form.controls.name, 'required')) { + + } @else if (showError(form.controls.name, 'pattern')) { + + } @else if (showError(form.controls.name, 'nameExists')) { + + } + } +
+ +
+
+ +
+ + +
+ @if (showError(form.controls.dist)) { + + } +
+ +
+ +
+ + +
+ @if (showError(form.controls.bundleName, 'required')) { + + } @else if (showError(form.controls.bundleName, 'missingLocale')) { + + } +
+
+ + @if (patternFiles().length > 0) { +

+ + +

+ } + } + + + @case ('collections') { +
+
+

{{ TOKENS.BUNDLES.DIALOG.SECTIONS.COLLECTIONS | transloco }}

+

{{ TOKENS.BUNDLES.DIALOG.COLLECTIONS.DESCRIPTION | transloco }}

+
+
+ +
+ +
+ {{ TOKENS.BUNDLES.DIALOG.COLLECTIONS.INCLUDEALLLABEL | transloco }} + + {{ TOKENS.BUNDLES.DIALOG.COLLECTIONS.INCLUDEALLHINTX | transloco: { count: allCollectionNames().length } }} + +
+ +
+ + @if (!form.controls.allCollections.value) { +
    + @for (group of form.controls.collections.controls; track group; let index = $index) { +
  • + +
  • + } +
  • + + + @for (name of availableCollections(); track name) { + + } + +
  • +
+ @if (showError(form.controls.collections, 'collectionsEmpty')) { + + } + } + } + + + @case ('collection') { + @if (activeCollectionGroup(); as group) { +
+
+
+

{{ group.controls.name.value }}

+

{{ TOKENS.BUNDLES.DIALOG.COLLECTION.DESCRIPTION | transloco }}

+
+ +
+ +
+
+ +
+ +
+
+
+ {{ TOKENS.BUNDLES.DIALOG.COLLECTION.CONFLICTSLABEL | transloco }} + +
+
+ +
+ {{ TOKENS.BUNDLES.DIALOG.COLLECTION.ENTRIESLABEL | transloco }} + +
+ + @if (!group.controls.allEntries.value) { +
+ + {{ TOKENS.BUNDLES.DIALOG.COLLECTION.RULESLABEL | transloco }} + {{ TOKENS.BUNDLES.DIALOG.COLLECTION.RULESHINT | transloco }} + + + @if (group.controls.rules.length > 0) { + + } + +
+ @for (rule of group.controls.rules.controls; track rule; let ruleIndex = $index) { +
+
+ +
+ +
+ @for (tag of rule.controls.matchingTags.value; track tag; let tagIndex = $index) { + @if (tagIndex > 0) { + + } + + {{ tag }} + + + } + @if (rule.controls.matchingTags.value.length === 0) { + + } + +
+ + +
+ } +
+ + + + @if (showError(group.controls.rules, 'rulesEmpty')) { + + } @else if (submitAttempted() && group.controls.rules.invalid) { + + } +
+ } +
+ } + } + + + @case ('types') { +
+
+

{{ TOKENS.BUNDLES.DIALOG.SECTIONS.TYPES | transloco }}

+

{{ TOKENS.BUNDLES.DIALOG.TYPES.DESCRIPTION | transloco }}

+
+
+ +
+ +
+ {{ TOKENS.BUNDLES.DIALOG.TYPES.SWITCHLABEL | transloco }} + +
+ +
+ + @if (form.controls.typesEnabled.value) { +
+
+ +
+ + +
+ @if (showError(form.controls.typeDistFile, 'required')) { + + } @else if (showError(form.controls.typeDistFile, 'notTypeScript')) { + + } +
+ +
+
+ {{ TOKENS.BUNDLES.DIALOG.TYPES.CASINGLABEL | transloco }} + + @if (form.controls.tokenCasing.value === 'inherit') { +

{{ TOKENS.BUNDLES.DIALOG.INHERITHINT | transloco }}

+ } @else { +

+ +

+ } +
+
+ +
+ +
+ @if (showError(form.controls.tokenConstantName, 'invalidIdentifier')) { + + } @else { +

{{ TOKENS.BUNDLES.DIALOG.TYPES.CONSTANTHINT | transloco }}

+ } +
+
+
+ } + } + + + @case ('options') { +
+
+

{{ TOKENS.BUNDLES.DIALOG.SECTIONS.OPTIONS | transloco }}

+

{{ TOKENS.BUNDLES.DIALOG.OPTIONS.DESCRIPTION | transloco }}

+
+
+ +
+ +
+ {{ TOKENS.BUNDLES.DIALOG.OPTIONS.ICULABEL | transloco }} + +
+ +
+ @if (form.controls.transformICUToTransloco.value === 'inherit') { +

+ + {{ TOKENS.BUNDLES.DIALOG.INHERITHINT | transloco }} +

+ } @else { +

+ + + {{ TOKENS.BUNDLES.DIALOG.OVERRIDEHINTX | transloco: { value: (projectIcuTransform() ? TOKENS.BUNDLES.DIALOG.ON : TOKENS.BUNDLES.DIALOG.OFF) | transloco } }} + +

+ } + } + } + + +
+ + @if (previewOpen()) { +
+ +
+ } +
+
+ + + +
+ + +
+ + + + +
+
+ + + +
+

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.WILLWRITE | transloco }}

+ @if (previewTree().length === 0) { +

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.WAITING | transloco }}

+ } @else { +
    + @for (folder of previewTree(); track folder.path) { +
  • + + + + @for (part of folder.pathParts; track $index) { + {{ part }} + } + / + + +
      + @for (file of folder.files; track file.name) { +
    • + + + @for (part of file.nameParts; track $index) { + {{ part }} + } + + @if (file.exists === false) { + {{ TOKENS.BUNDLES.DIALOG.PREVIEW.NEW | transloco }} + } +
    • + } +
    +
  • + } +
+ } +
+ +
+

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.DRYRUN | transloco }}

+ @if (previewStatus() === 'error') { +

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.COUNTSUNAVAILABLE | transloco }}

+ } @else if (dryRun(); as result) { +
+
{{ TOKENS.BUNDLES.DIALOG.SECTIONS.COLLECTIONS | transloco }}
+
{{ collectionCount() }}
+
{{ TOKENS.BUNDLES.DIALOG.PREVIEW.KEYSPERLOCALE | transloco }}
+
≈ {{ keysPerLocale() }}
+
{{ TOKENS.BUNDLES.DIALOG.PREVIEW.CONFLICTS | transloco }}
+
+ {{ result.conflictsCount }} + @if (result.conflictsCount > 0 && hasOverride()) { + · {{ TOKENS.BUNDLES.DIALOG.PREVIEW.RESOLVEDBYOVERRIDE | transloco }} + } +
+
{{ TOKENS.BUNDLES.DIALOG.PREVIEW.LOCALES | transloco }}
+
{{ result.locales.length }}
+
+ @if (hierarchicalConflicts().length > 0) { + + } +

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.CAPTION | transloco }}

+ } @else { + + } +
+ + @if (dryRun(); as result) { + @if (previewStatus() !== 'error') { +
+

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.EXAMPLEKEY | transloco }}

+ @if (result.exampleKey; as example) { +
+
+ {{ example.sourceKey }} + {{ TOKENS.BUNDLES.DIALOG.PREVIEW.EXAMPLEFROMX | transloco: { collection: example.collectionName } }} +
+
+ + {{ example.bundledKey }} + @if (example.bundledKey === example.sourceKey) { + {{ TOKENS.BUNDLES.DIALOG.PREVIEW.EXAMPLEUNCHANGED | transloco }} + } +
+ @if (example.tokenPath) { +
+ + + @for (part of tokenPathParts(); track $index) { + {{ part }} + } + +
+ } +
+ } @else { +

{{ TOKENS.BUNDLES.DIALOG.PREVIEW.EXAMPLENOEXAMPLE | transloco }}

+ } +
+ } + } +
diff --git a/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.scss b/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.scss new file mode 100644 index 00000000..4356b58e --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.scss @@ -0,0 +1,1281 @@ +// The outer box (width, top rule, zero container padding) is owned by +// `.bundle-form-dialog-panel` in styles.scss, because the CDK appends the +// overlay pane outside this component. Everything here is the inside. +:host { + display: block; + max-height: inherit; + // Same surface pairing as the collection dialog: the panel is one step lighter + // than the page so the wells read as set into it; the rail and preview column + // sit one step back again so the pane is the lit part of the room. + --dialog-surface: var(--color-background); + --dialog-well: var(--color-background); + --dialog-aside: var(--color-background-subtle); + --dialog-active: var(--color-background); +} + +:host-context([data-theme='dark']) { + --dialog-surface: var(--color-background-subtle); + --dialog-aside: var(--color-background); + --dialog-active: var(--color-background-muted); +} + +@media (prefers-color-scheme: dark) { + :host-context(:root:not([data-theme='light']):not([data-theme='dark'])) { + --dialog-surface: var(--color-background-subtle); + --dialog-aside: var(--color-background); + --dialog-active: var(--color-background-muted); + } +} + +.dialog-wrapper { + display: flex; + flex-direction: column; + max-height: inherit; + background: var(--dialog-surface); +} + +code { + font-family: var(--font-family-mono); + font-size: 0.95em; + color: var(--color-text-primary); +} + +.num { + font-variant-numeric: tabular-nums; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Header (inherited from the collection dialog) +// ───────────────────────────────────────────────────────────────────────────── + +.dialog-header { + position: relative; + flex-shrink: 0; + display: flex; + align-items: flex-start; + gap: var(--spacing-3); + padding: var(--spacing-5) var(--spacing-6) var(--spacing-4); + border-bottom: 1px solid var(--color-border); +} + +.dialog-header-tile { + display: grid; + place-items: center; + flex: none; + width: 38px; + height: 38px; + border-radius: var(--border-radius-lg); + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent); + color: var(--color-primary); + + mat-icon { + font-size: 19px; + width: 19px; + height: 19px; + } +} + +.dialog-header-text { + flex: 1; + min-width: 0; + padding-right: var(--spacing-8); +} + +.dialog-title { + margin: 0 0 var(--spacing-1); + font-size: var(--font-size-2xl); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-tight); + letter-spacing: -0.01em; + color: var(--color-text-primary); +} + +.dialog-subtitle { + margin: 0; + font-size: var(--font-size-sm); + line-height: var(--line-height-snug); + color: var(--color-text-secondary); +} + +.dialog-close { + position: absolute; + top: var(--spacing-3); + right: var(--spacing-3); + color: var(--color-text-secondary); + transition: color var(--transition-fast); + + &:hover { + color: var(--color-text-primary); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Master-detail body: rail / pane / preview +// ───────────────────────────────────────────────────────────────────────────── + +.md { + flex: 1 1 auto; + min-height: 0; + display: grid; + grid-template-columns: 240px minmax(0, 1fr) 320px; + grid-template-rows: minmax(0, 1fr); + // The comp's 520px body; tall enough that switching panes never resizes the dialog. + min-height: 520px; + margin: 0; +} + +.rail, +.pane, +.prev { + min-height: 0; + overflow-y: auto; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Rail +// ───────────────────────────────────────────────────────────────────────────── + +.rail { + display: flex; + flex-direction: column; + gap: 2px; + padding: var(--spacing-3) 10px var(--spacing-3) var(--spacing-3); + background: var(--dialog-aside); + border-right: 1px solid var(--color-border); +} + +.rail-item { + display: flex; + align-items: center; + gap: 9px; + width: 100%; + padding: var(--spacing-2) 10px; + border: 0; + border-radius: var(--border-radius-md); + background: transparent; + color: var(--color-text-primary); + font: inherit; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + text-align: left; + cursor: pointer; + transition: + background var(--transition-fast), + box-shadow var(--transition-fast), + color var(--transition-fast); + + > mat-icon { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); + transition: color var(--transition-fast); + } + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--dialog-active) 60%, transparent); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &--on { + background: var(--dialog-active); + box-shadow: var(--shadow-sm); + + > mat-icon { + color: var(--color-primary); + } + } + + &--child { + padding-left: var(--spacing-6); + font-weight: var(--font-weight-medium); + + > mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &.rail-item--on { + font-weight: var(--font-weight-bold); + } + } + + &--add { + padding-left: var(--spacing-6); + color: var(--color-text-secondary); + + > mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: var(--color-text-secondary); + } + + &:disabled { + cursor: default; + opacity: 0.6; + } + } +} + +.rail-stack { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1 1 auto; +} + +.rail-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rail-sub { + display: block; + margin-top: 1px; + font-family: var(--font-family-mono); + font-size: 11px; + font-weight: var(--font-weight-normal); + color: var(--color-text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 170px; + + &--prose { + font-family: inherit; + } + + // Placeholder text: present so the row height never changes, but clearly not a value yet. + &--empty { + color: var(--color-text-tertiary); + } +} + +.rail-count { + flex: none; + margin-left: auto; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + white-space: nowrap; +} + +.rail-dot { + flex: none; + width: 6px; + height: 6px; + margin-left: auto; + border-radius: var(--border-radius-full); + background: var(--color-error); +} + +.rail-rule { + height: 1px; + margin: var(--spacing-2) 6px; + background: var(--color-border); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Pane +// ───────────────────────────────────────────────────────────────────────────── + +.pane { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + padding: var(--spacing-5) var(--spacing-6) var(--spacing-3); +} + +.pane-section { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.pane-head { + display: flex; + align-items: flex-start; + gap: var(--spacing-3); +} + +.pane-head-text { + flex: 1; + min-width: 0; + + h3 { + margin: 0; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-snug); + letter-spacing: -0.01em; + color: var(--color-text-primary); + overflow-wrap: anywhere; + } + + p { + margin: 2px 0 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-snug); + color: var(--color-text-secondary); + } +} + +.pane-head-action { + flex: none; + height: 32px; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} + +.row-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-3); +} + +.row-3-2 { + display: grid; + grid-template-columns: 3fr 2fr; + gap: var(--spacing-3); +} + +// Types sub-fields hang off the switch row with a left hairline, like the comp. +.sub-fields { + display: flex; + flex-direction: column; + gap: var(--spacing-3); + margin-left: 14px; + padding-left: 14px; + border-left: 1px solid var(--color-border); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Fields (inherited from the collection dialog) +// ───────────────────────────────────────────────────────────────────────────── + +.field { + display: flex; + flex-direction: column; + gap: var(--spacing-1); + min-width: 0; +} + +.field-label { + display: flex; + align-items: baseline; + gap: 6px; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-normal); + color: var(--color-text-primary); +} + +.field-label-opt { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); +} + +.text-input { + display: flex; + align-items: center; + gap: var(--spacing-2); + height: 40px; + padding: 0 var(--spacing-3); + border: 1px solid var(--color-border-strong); + border-radius: var(--border-radius-md); + background: var(--dialog-well); + color: var(--color-text-primary); + transition: + border-color var(--transition-fast), + box-shadow var(--transition-fast); + + > mat-icon { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); + } + + input { + flex: 1; + min-width: 0; + height: 100%; + border: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; + font-size: var(--font-size-base); + outline: none; + + &::placeholder { + color: var(--color-text-placeholder); + } + } + + &:focus-within { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary) 22%, transparent); + } + + &--mono input { + font-family: var(--font-family-mono); + font-size: var(--font-size-sm); + } + + &--compact { + height: 36px; + } + + &--invalid, + &--invalid:focus-within { + border-color: var(--color-error); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 18%, transparent); + } + + &--locked { + border-style: dashed; + background: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-base); + } +} + +// The `.json` tail sits inside the pattern well so the user types only the stem. +.text-input-trail { + flex: none; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); +} + +.field-hint, +.field-error { + margin: 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-normal); +} + +.field-hint { + color: var(--color-text-secondary); + + &--icon { + display: flex; + align-items: flex-start; + gap: var(--spacing-1); + + mat-icon { + flex: none; + font-size: 14px; + width: 14px; + height: 14px; + margin-top: 2px; + color: var(--color-text-tertiary); + } + } +} + +.field-error { + color: var(--color-error); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Switch rows (coral tile variant of the collection dialog's) +// ───────────────────────────────────────────────────────────────────────────── + +.switch-row { + display: flex; + align-items: flex-start; + gap: var(--spacing-3); + padding: var(--spacing-3) var(--spacing-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + background: var(--color-background-subtle); + transition: + border-color var(--transition-normal), + background var(--transition-normal); + + &--on { + border-color: color-mix(in srgb, var(--color-primary) 35%, var(--color-border)); + background: color-mix(in srgb, var(--color-primary) 5%, var(--color-background-subtle)); + } +} + +.switch-tile { + display: grid; + place-items: center; + flex: none; + width: 30px; + height: 30px; + border-radius: var(--border-radius-md); + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + color: var(--color-primary); + + mat-icon { + font-size: 17px; + width: 17px; + height: 17px; + } +} + +.switch-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.switch-label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-snug); + color: var(--color-text-primary); +} + +.switch-description { + font-size: var(--font-size-xs); + line-height: var(--line-height-snug); + color: var(--color-text-secondary); +} + +.switch-control { + flex: none; + margin-top: 2px; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Collections summary list +// ───────────────────────────────────────────────────────────────────────────── + +.coll-list { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + margin: 0; + padding: 0; + list-style: none; +} + +.coll-row { + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + background: var(--dialog-well); + overflow: hidden; + + &--add { + border-style: dashed; + border-color: var(--color-border-strong); + background: none; + + .coll-row-button { + color: var(--color-text-secondary); + } + + .coll-row-lead { + color: var(--color-text-secondary); + } + } +} + +.coll-row-button { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px var(--spacing-3) 10px 14px; + border: 0; + background: transparent; + color: var(--color-text-primary); + font: inherit; + text-align: left; + cursor: pointer; + transition: background var(--transition-fast); + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--color-background-subtle) 60%, transparent); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &:disabled { + cursor: default; + opacity: 0.7; + } +} + +.coll-row-lead { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); +} + +.coll-row-name { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.coll-row-summary { + margin-left: auto; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.coll-row-chevron { + flex: none; + color: var(--color-text-tertiary); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Rules table +// ───────────────────────────────────────────────────────────────────────────── + +.rules-head, +.rule { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(160px, 1fr) 40px; + gap: var(--spacing-2); + align-items: center; +} + +.rules-head { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); +} + +.rules { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +.rule-remove { + color: var(--color-text-tertiary); + + &:hover { + color: var(--color-error); + } +} + +.rule-add { + display: inline-flex; + align-items: center; + align-self: flex-start; + gap: var(--spacing-1); + padding: var(--spacing-1) var(--spacing-2); + margin-left: calc(-1 * var(--spacing-2)); + border: 0; + border-radius: var(--border-radius-md); + background: transparent; + color: var(--color-text-secondary); + font: inherit; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + cursor: pointer; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &:hover { + color: var(--color-text-primary); + background: var(--color-background-subtle); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 1px; + } +} + +// Tags well: chips with the any/all operator between them, and an inline input. +.tags-well { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-1); + min-height: 36px; + padding: 3px var(--spacing-2); + border: 1px solid var(--color-border-strong); + border-radius: var(--border-radius-md); + background: var(--dialog-well); + transition: + border-color var(--transition-fast), + box-shadow var(--transition-fast); + + &:focus-within { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary) 22%, transparent); + } + + &--empty { + border-style: dashed; + } +} + +.tags-well-icon { + flex: none; + font-size: 16px; + width: 16px; + height: 16px; + color: var(--color-text-tertiary); +} + +.tags-input { + flex: 1 1 60px; + min-width: 60px; + height: 26px; + border: 0; + padding: 0 var(--spacing-1); + background: transparent; + color: var(--color-text-primary); + font: inherit; + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + outline: none; + + &::placeholder { + font-family: var(--font-family-body); + color: var(--color-text-placeholder); + } +} + +.tag-chip { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 0 2px 0 var(--spacing-2); + border-radius: var(--border-radius-full); + border: 1px solid var(--color-border); + background: var(--color-background-subtle); + color: var(--color-text-primary); + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + line-height: 1.6; +} + +.tag-chip-remove { + display: grid; + place-items: center; + width: 20px; + height: 20px; + padding: 0; + border: 0; + border-radius: var(--border-radius-full); + background: transparent; + color: var(--color-text-tertiary); + cursor: pointer; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } + + &:hover { + color: var(--color-error); + background: color-mix(in srgb, var(--color-error) 12%, transparent); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 1px; + } +} + +.tag-operator { + padding: 0 2px; + border: 0; + border-radius: var(--border-radius-sm); + background: transparent; + color: var(--color-text-secondary); + font: inherit; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + cursor: pointer; + + &:hover { + color: var(--color-secondary-active); + text-decoration: underline; + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: 1px; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Preview (column and disclosure share every block style) +// ───────────────────────────────────────────────────────────────────────────── + +.prev.col { + display: flex; + flex-direction: column; + gap: var(--spacing-5); + padding: var(--spacing-5) var(--spacing-5) var(--spacing-5); + background: var(--dialog-aside); + border-left: 1px solid var(--color-border); +} + +.preview { + transition: opacity var(--transition-normal); + + &--stale { + opacity: 0.6; + } +} + +.preview-block { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + + h4 { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: var(--color-text-tertiary); + } + } +} + +.preview-note, +.preview-caption { + margin: 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-normal); + color: var(--color-text-secondary); +} + +// Hierarchical key collisions make generation throw, so the preview states it +// in the error tint rather than letting the dry run read as a clean pass. +.preview-error { + display: flex; + align-items: flex-start; + gap: var(--spacing-1); + margin: 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-normal); + color: var(--color-error); + + mat-icon { + flex: none; + width: 14px; + height: 14px; + font-size: 14px; + line-height: 14px; + } + + span { + overflow-wrap: anywhere; + } +} + +.tree, +.tree ul { + display: flex; + flex-direction: column; + gap: 3px; + margin: 0; + padding: 0; + list-style: none; + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + color: var(--color-text-primary); +} + +.tree-folder { + display: flex; + flex-direction: column; + gap: 3px; +} + +.tree-dir { + display: flex; + align-items: center; + gap: 5px; + color: var(--color-text-secondary); + overflow-wrap: break-word; + + mat-icon { + flex: none; + font-size: 14px; + width: 14px; + height: 14px; + color: var(--color-text-tertiary); + } +} + +.tree-dir-path { + min-width: 0; + overflow-wrap: break-word; +} + +.tree-file { + display: flex; + align-items: center; + gap: 6px; + padding-left: 21px; + + mat-icon { + flex: none; + font-size: 14px; + width: 14px; + height: 14px; + color: var(--color-text-tertiary); + } +} + +.tree-file-name { + min-width: 0; + overflow-wrap: break-word; +} + +.tree-new { + padding: 0 6px; + border-radius: var(--border-radius-full); + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + color: var(--color-primary-text); + font-family: var(--font-family-body); + font-size: 10px; + font-weight: var(--font-weight-semibold); + letter-spacing: 0.02em; +} + +.kv { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px var(--spacing-3); + margin: 0; + font-size: var(--font-size-xs); + + dt { + margin: 0; + color: var(--color-text-secondary); + } + + dd { + margin: 0; + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + } +} + +.kv--skeleton dd { + color: var(--color-text-tertiary); +} + +.example { + display: flex; + flex-direction: column; + gap: 4px; + padding: var(--spacing-2) 10px; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + background: var(--dialog-well); + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); +} + +.example-line { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0 6px; + color: var(--color-text-secondary); + + b { + color: var(--color-text-primary); + font-weight: var(--font-weight-semibold); + // Long token paths break at the hints between segments, never mid-identifier. + overflow-wrap: break-word; + } + + mat-icon { + flex: none; + font-size: 14px; + width: 14px; + height: 14px; + color: var(--color-text-tertiary); + } + + // The note wraps under the key, indented past the glyph. + > span { + flex-basis: 100%; + padding-left: 20px; + } + + &:first-child > span { + padding-left: 0; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Disclosure (collection dialog recipe); only shown below 1200px +// ───────────────────────────────────────────────────────────────────────────── + +.disclosure { + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + overflow: hidden; +} + +.preview-disclosure { + display: none; + margin-top: auto; +} + +.disclosure-toggle { + display: flex; + align-items: center; + gap: var(--spacing-2); + width: 100%; + padding: var(--spacing-2) var(--spacing-3) var(--spacing-2) var(--spacing-4); + border: 0; + background: transparent; + color: var(--color-text-primary); + font: inherit; + text-align: left; + cursor: pointer; + transition: background var(--transition-fast); + + &:hover { + background: var(--color-background-subtle); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } +} + +.disclosure-icon { + flex: none; + font-size: 18px; + width: 18px; + height: 18px; + color: var(--color-text-tertiary); +} + +.disclosure-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + line-height: 1.5; +} + +.disclosure-summary { + margin-left: auto; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + white-space: nowrap; +} + +.disclosure-chevron { + flex: none; + color: var(--color-text-tertiary); + transition: transform var(--transition-normal); + + .disclosure--open & { + transform: rotate(180deg); + } +} + +.disclosure-panel { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + padding: var(--spacing-3) var(--spacing-4) var(--spacing-4); + border-top: 1px solid var(--color-border-subtle); + animation: disclosure-reveal var(--transition-normal) both; +} + +@keyframes disclosure-reveal { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: none; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Footer +// ───────────────────────────────────────────────────────────────────────────── + +.dialog-footer { + flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-4) var(--spacing-6) var(--spacing-5); + border-top: 1px solid var(--color-border); +} + +.footer-meta { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + + mat-icon { + flex: none; + font-size: 14px; + width: 14px; + height: 14px; + color: var(--color-success); + } + + // The affirmative glyph is earned: while the form is invalid it stays a quiet outline. + .footer-meta-icon--idle { + color: var(--color-text-tertiary); + } +} + +.footer-spacer { + flex: 1; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Responsive +// ───────────────────────────────────────────────────────────────────────────── + +// Under 1200px the preview column goes and its blocks reappear in the disclosure +// at the bottom of the pane. +@media (max-width: 1199px) { + .md { + grid-template-columns: 240px minmax(0, 1fr); + } + + .prev.col { + display: none; + } + + .preview-disclosure { + display: block; + } +} + +// Under 800px the rail becomes a horizontal strip of sections above the pane. +@media (max-width: 799px) { + .md { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); + min-height: 0; + } + + .rail { + flex-direction: row; + align-items: stretch; + gap: var(--spacing-1); + flex-wrap: wrap; + row-gap: var(--spacing-1); + padding: var(--spacing-2) var(--spacing-3); + border-right: 0; + border-bottom: 1px solid var(--color-border); + } + + .rail-item { + width: auto; + flex: none; + + &--child, + &--add { + padding-left: 10px; + } + } + + .rail-stack { + flex: none; + } + + .rail-sub { + max-width: 120px; + } + + .rail-count, + .rail-dot { + margin-left: 0; + } + + .rail-rule { + flex-basis: 100%; + width: auto; + height: 1px; + margin: 4px 0; + } + + .row-2, + .row-3-2 { + grid-template-columns: minmax(0, 1fr); + } + + .rules-head { + display: none; + } + + .rule { + grid-template-columns: minmax(0, 1fr) 40px; + + .tags-well { + grid-column: 1 / -1; + } + } +} + +@media (prefers-reduced-motion: reduce) { + .disclosure-panel { + animation: none; + } + + .disclosure-chevron, + .preview, + .rail-item, + .switch-row { + transition: none; + } +} diff --git a/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.spec.ts b/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.spec.ts new file mode 100644 index 00000000..23657380 --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.spec.ts @@ -0,0 +1,501 @@ +import { signal } from '@angular/core'; +import { type ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import type { BundleDefinitionDto, BundleDryRunResultDto, LingoTrackerConfigDto } from '@simoncodes-ca/data-transfer'; +import { of, throwError } from 'rxjs'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getTranslocoTestingModule } from '../../../testing/transloco-testing.module'; +import { CollectionsApiService } from '../services/collections-api.service'; +import { CollectionsStore } from '../store/collections.store'; +import { BundleFormDialog } from './bundle-form-dialog'; +import type { BundleFormDialogData } from './bundle-form-dialog-data'; + +const config: LingoTrackerConfigDto = { + exportFolder: './export', + importFolder: './import', + baseLocale: 'en', + locales: ['en', 'fr-ca', 'es'], + collections: { + trackerResources: { translationsFolder: './apps/tracker/src/i18n' }, + mockDesignSystem: { translationsFolder: './libs/ds/i18n' }, + TestDataPlayground: { translationsFolder: './tmp/i18n' }, + }, +}; + +const trackerBundle: BundleDefinitionDto = { + bundleName: '{locale}', + dist: './apps/tracker/src/assets/i18n', + collections: [{ name: 'trackerResources', entriesSelectionRules: 'All' }], + typeDistFile: './apps/tracker/src/i18n-types/tracker-resources.ts', + transformICUToTransloco: true, +}; + +const dryRunResult: BundleDryRunResultDto = { + name: 'admin', + locales: ['en', 'fr-ca', 'es'], + files: [ + { path: 'dist/i18n/admin.en.json', kind: 'bundle', locale: 'en', exists: false, keysCount: 12 }, + { path: 'dist/i18n/admin.fr-ca.json', kind: 'bundle', locale: 'fr-ca', exists: false, keysCount: 12 }, + { path: 'dist/i18n/admin.es.json', kind: 'bundle', locale: 'es', exists: true, keysCount: 12 }, + ], + keysPerLocale: { en: 12, 'fr-ca': 12, es: 12 }, + conflictsCount: 0, + conflictKeys: [], + hierarchicalConflicts: [], + exampleKey: { collectionName: 'trackerResources', sourceKey: 'a.b', bundledKey: 'a.b' }, + warnings: [], +}; + +interface Harness { + fixture: ComponentFixture; + component: BundleFormDialog; + dialogRef: { close: ReturnType }; + api: { dryRunBundle: ReturnType }; +} + +const buildTestBed = async (data: BundleFormDialogData): Promise => { + const dialogRef = { close: vi.fn() }; + const api = { dryRunBundle: vi.fn().mockReturnValue(of(dryRunResult)) }; + const store = { + config: signal(config), + collectionEntries: signal( + Object.entries(config.collections).map(([name, collection]) => ({ name, config: collection })), + ), + bundleEntries: signal([{ name: 'tracker', definition: trackerBundle }]), + }; + + await TestBed.configureTestingModule({ + imports: [BundleFormDialog, NoopAnimationsModule, getTranslocoTestingModule()], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: MatDialogRef, useValue: dialogRef }, + { provide: CollectionsApiService, useValue: api }, + { provide: CollectionsStore, useValue: store }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(BundleFormDialog); + fixture.detectChanges(); + return { fixture, component: fixture.componentInstance, dialogRef, api }; +}; + +const fillOutput = (component: BundleFormDialog): void => { + component.form.controls.name.setValue('admin'); + component.form.controls.dist.setValue('./dist/i18n'); + component.form.controls.bundleName.setValue('admin.{locale}'); +}; + +describe('BundleFormDialog — create mode', () => { + let harness: Harness; + let component: BundleFormDialog; + + beforeEach(async () => { + TestBed.resetTestingModule(); + harness = await buildTestBed({ mode: 'create' }); + component = harness.component; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + expect(component.isEditMode).toBe(false); + }); + + it('should start with the first collection pane open, seeded with the first collection', () => { + expect(component.form.controls.collections.length).toBe(1); + expect(component.form.controls.collections.at(0).controls.name.value).toBe('trackerResources'); + expect(component.activeSection()).toBe('coll:0'); + expect(component.activeKind()).toBe('collection'); + }); + + it('should open the Collections pane when the bundle includes every collection', async () => { + TestBed.resetTestingModule(); + const built = await buildTestBed({ + mode: 'create', + bundle: { bundleName: '{locale}', dist: './dist', collections: 'All' }, + }); + expect(built.component.activeSection()).toBe('collections'); + expect(built.component.form.controls.collections.length).toBe(0); + }); + + it('should keep the name editable', () => { + expect(component.form.controls.name.disabled).toBe(false); + }); + + it('should reject a name that collides with an existing bundle', () => { + component.form.controls.name.setValue('tracker'); + expect(component.form.controls.name.hasError('nameExists')).toBe(true); + + component.form.controls.name.setValue('admin'); + expect(component.form.controls.name.errors).toBeNull(); + }); + + it('should reject names outside the letters, numbers, hyphens and underscores charset', () => { + component.form.controls.name.setValue('my bundle!'); + expect(component.form.controls.name.hasError('pattern')).toBe(true); + }); + + it('should require {locale} in the file name pattern', () => { + component.form.controls.bundleName.setValue('admin'); + expect(component.form.controls.bundleName.hasError('missingLocale')).toBe(true); + + component.form.controls.bundleName.setValue('admin.{locale}'); + expect(component.form.controls.bundleName.errors).toBeNull(); + }); + + it('should require a .ts type file only while types are on', () => { + const typeFile = component.form.controls.typeDistFile; + expect(typeFile.errors).toBeNull(); + + component.form.controls.typesEnabled.setValue(true); + expect(typeFile.hasError('required')).toBe(true); + + typeFile.setValue('./dist/types/admin.js'); + expect(typeFile.hasError('notTypeScript')).toBe(true); + + typeFile.setValue('./dist/types/admin.ts'); + expect(typeFile.errors).toBeNull(); + + typeFile.setValue('./dist/types/admin.js'); + component.form.controls.typesEnabled.setValue(false); + expect(typeFile.errors).toBeNull(); + }); + + it('should require a valid JavaScript identifier for the constant name', () => { + const constant = component.form.controls.tokenConstantName; + constant.setValue('1BAD'); + expect(constant.hasError('invalidIdentifier')).toBe(true); + + constant.setValue('ADMIN_TOKENS'); + expect(constant.errors).toBeNull(); + + constant.setValue(''); + expect(constant.errors).toBeNull(); + }); + + it('should reject reserved words the server would reject, so the dialog never closes on a 400', () => { + const constant = component.form.controls.tokenConstantName; + + for (const reserved of ['type', 'class', 'interface', 'await', 'undefined']) { + constant.setValue(reserved); + expect(constant.hasError('invalidIdentifier')).toBe(true); + } + + constant.setValue('typeTokens'); + expect(constant.errors).toBeNull(); + }); + + it('should require at least one collection unless every collection is included', () => { + component.removeCollection(0); + expect(component.form.controls.collections.hasError('collectionsEmpty')).toBe(true); + expect(component.activeSection()).toBe('collections'); + + component.form.controls.allCollections.setValue(true); + expect(component.form.controls.collections.errors).toBeNull(); + }); + + it('should require at least one rule unless the collection takes all entries', () => { + const group = component.form.controls.collections.at(0); + expect(group.controls.rules.errors).toBeNull(); + + group.controls.allEntries.setValue(false); + expect(group.controls.rules.hasError('rulesEmpty')).toBe(true); + + component.addRule(group); + expect(group.controls.rules.errors).toBeNull(); + expect(group.controls.rules.at(0).controls.matchingPattern.hasError('required')).toBe(true); + + component.removeRule(group, 0); + expect(group.controls.rules.hasError('rulesEmpty')).toBe(true); + }); + + it('should add a collection from the remaining ones and open its pane', () => { + expect(component.availableCollections()).toEqual(['mockDesignSystem', 'TestDataPlayground']); + + component.addCollection('mockDesignSystem'); + + expect(component.form.controls.collections.length).toBe(2); + expect(component.activeSection()).toBe('coll:1'); + expect(component.availableCollections()).toEqual(['TestDataPlayground']); + }); + + it('should flag an invalid section in the rail once it has been touched', () => { + expect(component.sectionErrors().has('output')).toBe(false); + + component.form.controls.bundleName.setValue('admin'); + component.form.controls.bundleName.markAsTouched(); + + expect(component.sectionErrors().has('output')).toBe(true); + }); + + it('should add, normalize and remove rule tags and toggle the operator', () => { + const group = component.form.controls.collections.at(0); + group.controls.allEntries.setValue(false); + component.addRule(group); + const rule = group.controls.rules.at(0); + const input = { value: ' Admin UI ' } as HTMLInputElement; + + component.commitTagInput(rule, input); + expect(rule.controls.matchingTags.value).toEqual(['admin-ui']); + expect(input.value).toBe(''); + + component.commitTagInput(rule, { value: 'admin-ui' } as HTMLInputElement); + expect(rule.controls.matchingTags.value).toEqual(['admin-ui']); + + component.toggleTagOperator(rule); + expect(rule.controls.matchingTagOperator.value).toBe('All'); + + component.removeRuleTag(rule, 'admin-ui'); + expect(rule.controls.matchingTags.value).toEqual([]); + }); + + it('should not close when submitted invalid, and land on the first section with errors', () => { + component.onSubmit(); + + expect(harness.dialogRef.close).not.toHaveBeenCalled(); + expect(component.submitAttempted()).toBe(true); + expect(component.activeSection()).toBe('output'); + }); + + it('should close with a definition that omits empty optional fields and maps inherit to undefined', () => { + fillOutput(component); + const group = component.form.controls.collections.at(0); + group.controls.allEntries.setValue(false); + component.addRule(group); + group.controls.rules.at(0).controls.matchingPattern.setValue('apps.admin.*'); + component.addCollection('mockDesignSystem'); + const second = component.form.controls.collections.at(1); + second.controls.bundledKeyPrefix.setValue('ds'); + second.controls.mergeStrategy.setValue('override'); + component.form.controls.typesEnabled.setValue(true); + component.form.controls.typeDistFile.setValue('./dist/i18n-types/admin.ts'); + + component.onSubmit(); + + expect(harness.dialogRef.close).toHaveBeenCalledWith({ + name: 'admin', + bundle: { + bundleName: 'admin.{locale}', + dist: './dist/i18n', + collections: [ + { name: 'trackerResources', entriesSelectionRules: [{ matchingPattern: 'apps.admin.*' }] }, + { name: 'mockDesignSystem', bundledKeyPrefix: 'ds', entriesSelectionRules: 'All', mergeStrategy: 'override' }, + ], + typeDistFile: './dist/i18n-types/admin.ts', + }, + }); + const bundle = harness.dialogRef.close.mock.calls[0][0].bundle as BundleDefinitionDto; + expect(bundle).not.toHaveProperty('tokenCasing'); + expect(bundle).not.toHaveProperty('tokenConstantName'); + expect(bundle).not.toHaveProperty('transformICUToTransloco'); + }); + + it('should send "All" for collections and explicit casing and ICU choices when set', () => { + fillOutput(component); + component.form.controls.allCollections.setValue(true); + component.form.controls.typesEnabled.setValue(true); + component.form.controls.typeDistFile.setValue('./dist/i18n-types/admin.ts'); + component.form.controls.tokenCasing.setValue('camelCase'); + component.form.controls.tokenConstantName.setValue('ADMIN_KEYS'); + component.setIcu(false); + + component.onSubmit(); + + expect(harness.dialogRef.close).toHaveBeenCalledWith({ + name: 'admin', + bundle: { + bundleName: 'admin.{locale}', + dist: './dist/i18n', + collections: 'All', + typeDistFile: './dist/i18n-types/admin.ts', + tokenCasing: 'camelCase', + tokenConstantName: 'ADMIN_KEYS', + transformICUToTransloco: false, + }, + }); + }); + + it('should close with undefined on cancel', () => { + component.onCancel(); + expect(harness.dialogRef.close).toHaveBeenCalledWith(undefined); + }); +}); + +describe('BundleFormDialog — dry run', () => { + beforeEach(() => { + vi.useFakeTimers(); + TestBed.resetTestingModule(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should debounce edits and call the API once with the current definition', async () => { + const { component, api } = await buildTestBed({ mode: 'create' }); + fillOutput(component); + component.form.controls.dist.setValue('./dist/i18n'); + expect(api.dryRunBundle).not.toHaveBeenCalled(); + expect(component.previewStale()).toBe(true); + + vi.advanceTimersByTime(299); + expect(api.dryRunBundle).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(1); + expect(api.dryRunBundle).toHaveBeenCalledTimes(1); + expect(api.dryRunBundle).toHaveBeenCalledWith({ + name: 'admin', + bundle: expect.objectContaining({ bundleName: 'admin.{locale}', dist: './dist/i18n' }), + }); + expect(component.dryRun()).toEqual(dryRunResult); + expect(component.previewStatus()).toBe('ready'); + expect(component.previewStale()).toBe(false); + expect(component.previewFileCount()).toBe(3); + expect(component.keysPerLocale()).toBe(12); + }); + + it('should not call the API until name, folder and pattern are all present', async () => { + const { component, api } = await buildTestBed({ mode: 'create' }); + component.form.controls.name.setValue('admin'); + vi.advanceTimersByTime(300); + + expect(api.dryRunBundle).not.toHaveBeenCalled(); + expect(component.previewStatus()).toBe('waiting'); + }); + + it('should fall back to the client-side tree when the dry run fails', async () => { + const { component, api } = await buildTestBed({ mode: 'create' }); + api.dryRunBundle.mockReturnValue(throwError(() => new Error('boom'))); + fillOutput(component); + component.form.controls.typesEnabled.setValue(true); + component.form.controls.typeDistFile.setValue('./dist/i18n-types/admin.ts'); + vi.advanceTimersByTime(300); + + expect(component.previewStatus()).toBe('error'); + expect(component.dryRun()).toBeUndefined(); + expect( + component.previewTree().map((folder) => ({ + path: folder.path, + files: folder.files.map((file) => ({ name: file.name, kind: file.kind, exists: file.exists })), + })), + ).toEqual([ + { + path: 'dist/i18n', + files: [ + { name: 'admin.en.json', kind: 'bundle', exists: undefined }, + { name: 'admin.fr-ca.json', kind: 'bundle', exists: undefined }, + { name: 'admin.es.json', kind: 'bundle', exists: undefined }, + ], + }, + { path: 'dist/i18n-types', files: [{ name: 'admin.ts', kind: 'types', exists: undefined }] }, + ]); + }); + + it('should split tree paths and names after separators so they wrap between segments', async () => { + const { component, api } = await buildTestBed({ mode: 'create' }); + api.dryRunBundle.mockReturnValue(throwError(() => new Error('boom'))); + fillOutput(component); + vi.advanceTimersByTime(300); + + const folder = component.previewTree()[0]; + expect(folder).toBeDefined(); + expect(folder?.pathParts).toEqual(['dist/', 'i18n']); + expect(folder?.files[0]?.nameParts).toEqual(['admin.', 'en.', 'json']); + expect(folder?.files[0]?.nameParts.join('')).toBe(folder?.files[0]?.name); + }); + + it('should split the example token path after separators', async () => { + const { component } = await buildTestBed({ mode: 'create' }); + fillOutput(component); + vi.advanceTimersByTime(300); + + component.dryRun.set({ + ...dryRunResult, + exampleKey: { + collectionName: 'trackerResources', + sourceKey: 'app.skipToMainContent', + bundledKey: 'app.skipToMainContent', + tokenPath: 'TRACKER_TOKENS.APP.SKIPTOMAINCONTENT', + }, + }); + + expect(component.tokenPathParts()).toEqual(['TRACKER_', 'TOKENS.', 'APP.', 'SKIPTOMAINCONTENT']); + expect(component.tokenPathParts().join('')).toBe('TRACKER_TOKENS.APP.SKIPTOMAINCONTENT'); + }); + + it('should surface hierarchical key collisions as an error in the preview', async () => { + const { component, fixture } = await buildTestBed({ mode: 'create' }); + fillOutput(component); + vi.advanceTimersByTime(300); + fixture.detectChanges(); + + expect((fixture.nativeElement as HTMLElement).querySelector('[data-testid="hierarchical-conflicts"]')).toBeNull(); + + component.dryRun.set({ ...dryRunResult, hierarchicalConflicts: ['buttons.ok', 'menu.file'] }); + fixture.detectChanges(); + + expect(component.hierarchicalConflicts()).toEqual(['buttons.ok', 'menu.file']); + expect(component.hierarchicalConflictList()).toBe('buttons.ok, menu.file'); + + // The message itself is translated (asserted via the token), so the test checks + // that the error line appears at all and that it is announced. + const error = (fixture.nativeElement as HTMLElement).querySelector('[data-testid="hierarchical-conflicts"]'); + expect(error).toBeTruthy(); + expect(error?.getAttribute('role')).toBe('alert'); + }); +}); + +describe('BundleFormDialog — edit mode', () => { + let harness: Harness; + let component: BundleFormDialog; + + beforeEach(async () => { + TestBed.resetTestingModule(); + harness = await buildTestBed({ mode: 'edit', name: 'tracker', bundle: trackerBundle }); + component = harness.component; + }); + + it('should open on the Output section', () => { + expect(component.isEditMode).toBe(true); + expect(component.activeSection()).toBe('output'); + }); + + it('should lock the name and skip the collision check against itself', () => { + expect(component.form.controls.name.disabled).toBe(true); + expect(component.form.controls.name.value).toBe('tracker'); + expect(component.form.controls.name.errors).toBeNull(); + expect(harness.fixture.nativeElement.querySelector('[data-testid="name-locked"]')).toBeTruthy(); + }); + + it('should pre-populate the definition', () => { + const raw = component.form.getRawValue(); + expect(raw.dist).toBe('./apps/tracker/src/assets/i18n'); + expect(raw.bundleName).toBe('{locale}'); + expect(raw.allCollections).toBe(false); + expect(raw.collections).toHaveLength(1); + expect(raw.collections[0]?.allEntries).toBe(true); + expect(raw.typesEnabled).toBe(true); + expect(raw.typeDistFile).toBe('./apps/tracker/src/i18n-types/tracker-resources.ts'); + expect(raw.tokenCasing).toBe('inherit'); + expect(raw.transformICUToTransloco).toBe('on'); + }); + + it('should keep the name in the result even though the control is disabled', () => { + component.onSubmit(); + + expect(harness.dialogRef.close).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'tracker', + bundle: expect.objectContaining({ collections: trackerBundle.collections, transformICUToTransloco: true }), + }), + ); + }); + + it('should collapse an ICU choice equal to the project default (on) back to inherit', () => { + component.setIcu(true); + expect(component.form.controls.transformICUToTransloco.value).toBe('inherit'); + + component.setIcu(false); + expect(component.form.controls.transformICUToTransloco.value).toBe('off'); + }); +}); diff --git a/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.ts b/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.ts new file mode 100644 index 00000000..26a62d9d --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-form-dialog/bundle-form-dialog.ts @@ -0,0 +1,733 @@ +import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, signal } from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { NgTemplateOutlet } from '@angular/common'; +import { + type AbstractControl, + FormArray, + FormControl, + FormGroup, + ReactiveFormsModule, + type ValidationErrors, + type ValidatorFn, + Validators, +} from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslocoPipe } from '@jsverse/transloco'; +import { catchError, debounceTime, map, of, startWith, switchMap, tap } from 'rxjs'; +import { isValidJavaScriptIdentifier, normalizeTag } from '@simoncodes-ca/domain'; +import type { + BundleDefinitionDto, + BundleDryRunRequestDto, + BundleDryRunResultDto, + CollectionBundleDefinitionDto, + EntrySelectionRuleDto, + TokenCasingDto, +} from '@simoncodes-ca/data-transfer'; +import { TRACKER_TOKENS } from '../../../i18n-types/tracker-resources'; +import { CollectionsApiService } from '../services/collections-api.service'; +import { CollectionsStore } from '../store/collections.store'; +import type { BundleFormDialogData, BundleFormResult } from './bundle-form-dialog-data'; +import { SegmentedControl, type SegmentOption } from './segmented-control'; + +export type BundleSection = 'output' | 'collections' | `coll:${number}` | 'types' | 'options'; +export type MergeStrategy = 'merge' | 'override'; +export type TagOperator = 'Any' | 'All'; +export type TokenCasingChoice = 'inherit' | TokenCasingDto; +export type IcuChoice = 'inherit' | 'on' | 'off'; + +export type RuleGroup = FormGroup<{ + matchingPattern: FormControl; + matchingTags: FormControl; + matchingTagOperator: FormControl; +}>; + +export type CollectionGroup = FormGroup<{ + name: FormControl; + bundledKeyPrefix: FormControl; + mergeStrategy: FormControl; + allEntries: FormControl; + rules: FormArray; +}>; + +/** One folder of the "Will write" tree. */ +export interface PreviewFolder { + readonly path: string; + /** `path` split after each separator so the template can offer break opportunities. */ + readonly pathParts: readonly string[]; + readonly files: readonly PreviewFile[]; +} + +export interface PreviewFile { + readonly name: string; + /** `name` split after each separator so the template can offer break opportunities. */ + readonly nameParts: readonly string[]; + readonly kind: 'bundle' | 'types'; + /** `undefined` when the dry run failed and existence is unknown. */ + readonly exists: boolean | undefined; +} + +export type PreviewStatus = 'waiting' | 'loading' | 'ready' | 'error'; + +const LOCALE_PLACEHOLDER = '{locale}'; +const NAME_PATTERN = /^[A-Za-z0-9_-]+$/; + +const escapeHtml = (value: string): string => + value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + +const code = (value: string): string => `${escapeHtml(value)}`; + +/** Mirrors core's `bundleKeyToConstantName` without importing core into the browser. */ +const deriveConstantName = (bundleKey: string): string => `${bundleKey.replace(/-/g, '_').toUpperCase()}_TOKENS`; + +/** Split after each separator so long paths break between segments instead of mid-identifier. */ +const splitAfterSeparators = (value: string, separators: RegExp): readonly string[] => + value.length === 0 ? [] : value.split(separators); + +const PATH_SEPARATORS = /(?<=[._\-/])/; +const TOKEN_SEPARATORS = /(?<=[._])/; + +const stripDotSlash = (path: string): string => path.replace(/^\.\//, '').replace(/\/+$/, ''); + +@Component({ + selector: 'app-bundle-form-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgTemplateOutlet, + ReactiveFormsModule, + MatButtonModule, + MatIconModule, + MatMenuModule, + MatSlideToggleModule, + MatTooltipModule, + TranslocoPipe, + SegmentedControl, + ], + templateUrl: './bundle-form-dialog.html', + styleUrl: './bundle-form-dialog.scss', +}) +export class BundleFormDialog { + readonly #dialogRef = inject(MatDialogRef); + readonly #data = inject(MAT_DIALOG_DATA); + readonly #api = inject(CollectionsApiService); + readonly #destroyRef = inject(DestroyRef); + readonly store = inject(CollectionsStore); + + readonly TOKENS = TRACKER_TOKENS; + readonly isEditMode = this.#data.mode === 'edit'; + readonly configFileMarkup = '.lingo-tracker.json'; + readonly bundlesKeyMarkup = 'bundles'; + readonly tsExtensionMarkup = '.ts'; + /** Braces as entities so messageformat never reads the examples as arguments. */ + readonly icuExampleMarkup = '{count}'; + readonly translocoExampleMarkup = '{{count}}'; + readonly localePlaceholder = LOCALE_PLACEHOLDER; + /** Rail sub-labels always render; these stand in until the matching field is filled. */ + readonly placeholderPattern = TRACKER_TOKENS.BUNDLES.DIALOG.SECTIONS.OUTPUTPLACEHOLDER; + readonly placeholderTypeFile = TRACKER_TOKENS.BUNDLES.DIALOG.SECTIONS.TYPESPLACEHOLDER; + + readonly mergeOptions: readonly SegmentOption[] = [ + { value: 'merge', label: TRACKER_TOKENS.BUNDLES.DIALOG.COLLECTION.FIRSTWINS }, + { value: 'override', label: TRACKER_TOKENS.BUNDLES.DIALOG.COLLECTION.THISOVERRIDES }, + ]; + readonly entriesOptions: readonly SegmentOption[] = [ + { value: true, label: TRACKER_TOKENS.BUNDLES.DIALOG.COLLECTION.ALLENTRIES, icon: 'done_all' }, + { value: false, label: TRACKER_TOKENS.BUNDLES.DIALOG.COLLECTION.ONLYMATCHING, icon: 'filter_alt' }, + ]; + readonly casingOptions: readonly SegmentOption[] = [ + { value: 'upperCase', text: 'UPPER_CASE' }, + { value: 'camelCase', text: 'camelCase' }, + ]; + + readonly form = new FormGroup({ + name: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.pattern(NAME_PATTERN), this.#uniqueNameValidator()], + }), + dist: new FormControl('', { nonNullable: true, validators: [Validators.required] }), + bundleName: new FormControl('', { + nonNullable: true, + validators: [Validators.required, localePlaceholderValidator], + }), + allCollections: new FormControl(false, { nonNullable: true }), + collections: new FormArray([], { validators: [collectionsRequiredValidator] }), + typesEnabled: new FormControl(false, { nonNullable: true }), + typeDistFile: new FormControl('', { nonNullable: true, validators: [typeFileValidator] }), + tokenCasing: new FormControl('inherit', { nonNullable: true }), + tokenConstantName: new FormControl('', { nonNullable: true, validators: [identifierValidator] }), + transformICUToTransloco: new FormControl('inherit', { nonNullable: true }), + }); + + /** + * Reactive forms are not signals. Every form event (value, status, touched, submit) bumps this + * signal so the computeds below re-read the form. + */ + readonly #formTick = toSignal(this.form.events.pipe(map(() => Symbol())), { initialValue: Symbol() }); + + readonly activeSection = signal(this.#initialSection()); + readonly submitAttempted = signal(false); + readonly previewOpen = signal(false); + + readonly dryRun = signal(undefined); + readonly previewStatus = signal('waiting'); + /** True from the first keystroke until the next dry run lands. */ + readonly previewStale = signal(false); + + readonly activeKind = computed<'output' | 'collections' | 'collection' | 'types' | 'options'>(() => { + const section = this.activeSection(); + return section.startsWith('coll:') ? 'collection' : (section as Exclude); + }); + + readonly activeCollectionIndex = computed(() => { + const section = this.activeSection(); + return section.startsWith('coll:') ? Number(section.slice(5)) : -1; + }); + + readonly activeCollectionGroup = computed(() => { + this.#formTick(); + const index = this.activeCollectionIndex(); + return index >= 0 ? this.form.controls.collections.at(index) : undefined; + }); + + /** Names of every configured collection, in config order. */ + readonly allCollectionNames = computed(() => this.store.collectionEntries().map((entry) => entry.name)); + + /** Collections not yet in the bundle — what the "Add a collection" menu offers. */ + readonly availableCollections = computed(() => { + this.#formTick(); + const used = new Set(this.form.controls.collections.controls.map((group) => group.controls.name.value)); + return this.allCollectionNames().filter((name) => !used.has(name)); + }); + + readonly projectLocales = computed(() => this.store.config()?.locales ?? []); + readonly projectBaseLocale = computed(() => this.store.config()?.baseLocale ?? ''); + readonly projectTokenCasing = computed(() => this.store.config()?.tokenCasing ?? 'upperCase'); + readonly projectIcuTransform = computed(() => this.store.config()?.transformICUToTransloco ?? true); + + readonly collectionCount = computed(() => { + this.#formTick(); + return this.form.controls.allCollections.value + ? this.allCollectionNames().length + : this.form.controls.collections.length; + }); + + /** Rail summary under Output: `dist/pattern.json`, or nothing until both are typed. */ + readonly outputSummary = computed(() => { + this.#formTick(); + const { dist, bundleName } = this.form.getRawValue(); + if (!dist.trim() && !bundleName.trim()) return ''; + return `${stripDotSlash(dist.trim())}/${bundleName.trim()}.json`; + }); + + readonly typeFileName = computed(() => { + this.#formTick(); + const { typesEnabled, typeDistFile } = this.form.getRawValue(); + if (!typesEnabled) return ''; + return typeDistFile.trim().split('/').pop() ?? ''; + }); + + readonly derivedConstantName = computed(() => { + this.#formTick(); + return deriveConstantName(this.form.getRawValue().name.trim() || 'bundle'); + }); + + readonly patternFiles = computed(() => { + this.#formTick(); + const pattern = this.form.getRawValue().bundleName.trim(); + if (!pattern) return []; + return this.projectLocales().map((locale) => `${pattern.replace(LOCALE_PLACEHOLDER, locale)}.json`); + }); + + readonly writesHintParams = computed(() => { + const files = this.patternFiles(); + return { examples: files.slice(0, 2).map(code).join(', '), count: files.length }; + }); + + /** Sections whose fields are invalid and worth flagging in the rail. */ + readonly sectionErrors = computed>(() => { + this.#formTick(); + const flag = (control: AbstractControl): boolean => + control.invalid && (control.touched || control.dirty || this.submitAttempted()); + const errors = new Set(); + const controls = this.form.controls; + + if ([controls.name, controls.dist, controls.bundleName].some(flag)) errors.add('output'); + if (flag(controls.collections) && controls.collections.hasError('collectionsEmpty')) errors.add('collections'); + controls.collections.controls.forEach((group, index) => { + if (flag(group)) errors.add(`coll:${index}`); + }); + if ([controls.typeDistFile, controls.tokenConstantName].some(flag)) errors.add('types'); + return errors; + }); + + /** Client-side tree from the form alone; used while waiting and when the dry run fails. */ + readonly localTree = computed(() => { + this.#formTick(); + const raw = this.form.getRawValue(); + const files: { path: string; kind: 'bundle' | 'types' }[] = this.patternFiles().map((file) => ({ + path: `${stripDotSlash(raw.dist.trim())}/${file}`, + kind: 'bundle', + })); + if (raw.typesEnabled && raw.typeDistFile.trim()) { + files.push({ path: stripDotSlash(raw.typeDistFile.trim()), kind: 'types' }); + } + return groupIntoFolders(files.map((file) => ({ ...file, exists: undefined }))); + }); + + readonly previewTree = computed(() => { + const result = this.dryRun(); + if (!result || this.previewStatus() === 'error') return this.localTree(); + return groupIntoFolders( + result.files.map((file) => ({ path: stripDotSlash(file.path), kind: file.kind, exists: file.exists })), + ); + }); + + readonly previewFileCount = computed(() => + this.previewTree().reduce((total, folder) => total + folder.files.length, 0), + ); + + /** + * Bundled keys that are both a leaf and a parent (e.g. `buttons.ok` next to + * `buttons.ok.label`). Generation throws on these, so the preview must show + * them as an error rather than letting the bundle look ready to ship. + */ + readonly hierarchicalConflicts = computed(() => { + if (this.previewStatus() === 'error') return []; + return this.dryRun()?.hierarchicalConflicts ?? []; + }); + + /** The colliding keys as one readable list for the preview error line. */ + readonly hierarchicalConflictList = computed(() => this.hierarchicalConflicts().join(', ')); + + readonly keysPerLocale = computed(() => { + const result = this.dryRun(); + if (!result) return undefined; + const base = result.keysPerLocale[this.projectBaseLocale()]; + if (base !== undefined) return base; + return Math.max(0, ...Object.values(result.keysPerLocale)); + }); + + /** What the ICU switch shows: the explicit choice, or the project default while inheriting. */ + readonly icuChecked = computed(() => { + this.#formTick(); + const choice = this.form.controls.transformICUToTransloco.value; + return choice === 'inherit' ? this.projectIcuTransform() : choice === 'on'; + }); + + /** Example token path split after `.` and `_`, so it wraps between segments, never mid-identifier. */ + readonly tokenPathParts = computed(() => { + const tokenPath = this.dryRun()?.exampleKey?.tokenPath; + if (!tokenPath) return []; + return splitAfterSeparators(tokenPath, TOKEN_SEPARATORS); + }); + + readonly hasOverride = computed(() => { + this.#formTick(); + return this.form.controls.collections.controls.some((group) => group.controls.mergeStrategy.value === 'override'); + }); + + constructor() { + this.#populate(); + this.#wireDependentValidation(); + this.#wireDryRun(); + } + + // ───────────────────────────── navigation ───────────────────────────── + + activate(section: BundleSection): void { + this.activeSection.set(section); + } + + isActive(section: BundleSection): boolean { + return this.activeSection() === section; + } + + activateCollection(index: number): void { + this.activate(`coll:${index}`); + } + + isActiveCollection(index: number): boolean { + return this.activeCollectionIndex() === index; + } + + hasCollectionError(index: number): boolean { + return this.sectionErrors().has(`coll:${index}`); + } + + /** Wraps user text in `` for the innerHTML hints, escaped so a prefix can never inject markup. */ + codeMarkup(value: string): string { + return code(value); + } + + /** A choice equal to the project default is redundant, so it collapses back to inherit. */ + setIcu(checked: boolean): void { + const control = this.form.controls.transformICUToTransloco; + control.setValue(checked === this.projectIcuTransform() ? 'inherit' : checked ? 'on' : 'off'); + control.markAsDirty(); + control.markAsTouched(); + } + + togglePreview(): void { + this.previewOpen.update((open) => !open); + } + + // ───────────────────────────── collections ───────────────────────────── + + addCollection(name: string): void { + const collections = this.form.controls.collections; + collections.push(this.#buildCollectionGroup({ name, entriesSelectionRules: 'All' })); + collections.markAsDirty(); + this.activate(`coll:${collections.length - 1}`); + } + + removeCollection(index: number): void { + const collections = this.form.controls.collections; + if (index < 0 || index >= collections.length) return; + collections.removeAt(index); + collections.markAsDirty(); + collections.markAsTouched(); + this.activate('collections'); + } + + onAllCollectionsToggle(): void { + if (this.form.controls.allCollections.value && this.activeKind() === 'collection') { + this.activate('collections'); + } + } + + /** The one-line summary shown on a collection row and in the rail. */ + ruleCount(group: CollectionGroup): number { + return group.controls.rules.length; + } + + // ───────────────────────────── rules ───────────────────────────── + + addRule(group: CollectionGroup): void { + group.controls.rules.push(this.#buildRuleGroup({ matchingPattern: '' })); + group.controls.rules.markAsDirty(); + } + + removeRule(group: CollectionGroup, index: number): void { + group.controls.rules.removeAt(index); + group.controls.rules.markAsDirty(); + group.controls.rules.markAsTouched(); + } + + onTagInputKeydown(event: KeyboardEvent, rule: RuleGroup, input: HTMLInputElement): void { + if (event.key === 'Backspace' && !input.value) { + const tags = rule.controls.matchingTags.value; + if (tags.length > 0) this.removeRuleTag(rule, tags[tags.length - 1]); + return; + } + if (event.key !== 'Enter' && event.key !== ',') return; + event.preventDefault(); + this.commitTagInput(rule, input); + } + + commitTagInput(rule: RuleGroup, input: HTMLInputElement): void { + const normalized = normalizeTag(input.value); + input.value = ''; + if (!normalized) return; + const control = rule.controls.matchingTags; + if (control.value.includes(normalized)) return; + control.setValue([...control.value, normalized]); + control.markAsDirty(); + } + + removeRuleTag(rule: RuleGroup, tag: string): void { + const control = rule.controls.matchingTags; + control.setValue(control.value.filter((existing) => existing !== tag)); + control.markAsDirty(); + } + + toggleTagOperator(rule: RuleGroup): void { + const control = rule.controls.matchingTagOperator; + control.setValue(control.value === 'Any' ? 'All' : 'Any'); + control.markAsDirty(); + } + + // ───────────────────────────── errors ───────────────────────────── + + showError(control: AbstractControl, error?: string): boolean { + if (!(control.touched || control.dirty || this.submitAttempted())) return false; + return error ? control.hasError(error) : control.invalid; + } + + // ───────────────────────────── submit ───────────────────────────── + + onCancel(): void { + this.#dialogRef.close(undefined); + } + + onSubmit(): void { + this.submitAttempted.set(true); + if (this.form.invalid) { + this.form.markAllAsTouched(); + this.#revealFirstError(); + return; + } + this.#dialogRef.close(this.#buildResult()); + } + + // ───────────────────────────── private ───────────────────────────── + + #initialSection(): BundleSection { + if (this.#data.mode === 'edit') return 'output'; + const collections = this.#data.bundle?.collections; + return Array.isArray(collections) && collections.length > 0 ? 'coll:0' : 'collections'; + } + + #populate(): void { + const bundle = this.#data.bundle; + const name = this.#data.name ?? ''; + const collections = bundle?.collections; + + this.form.patchValue({ + name, + dist: bundle?.dist ?? '', + bundleName: bundle?.bundleName ?? '', + allCollections: collections === 'All', + typesEnabled: Boolean(bundle?.typeDistFile), + typeDistFile: bundle?.typeDistFile ?? '', + tokenCasing: bundle?.tokenCasing ?? 'inherit', + tokenConstantName: bundle?.tokenConstantName ?? '', + transformICUToTransloco: + bundle?.transformICUToTransloco === undefined ? 'inherit' : bundle.transformICUToTransloco ? 'on' : 'off', + }); + + if (Array.isArray(collections)) { + for (const collection of collections) { + this.form.controls.collections.push(this.#buildCollectionGroup(collection), { emitEvent: false }); + } + } + + // In create mode the first collection pane opens straight away when there is one; a brand + // new bundle starts with its first collection so the user lands on something to fill in. + if (!this.isEditMode && this.form.controls.collections.length === 0 && !this.form.controls.allCollections.value) { + const [first] = this.allCollectionNames(); + if (first) { + this.form.controls.collections.push(this.#buildCollectionGroup({ name: first, entriesSelectionRules: 'All' }), { + emitEvent: false, + }); + this.activeSection.set('coll:0'); + } + } + + if (this.isEditMode && name) { + this.form.controls.name.disable({ emitEvent: false }); + } + + // Cross-control validators ran before their siblings had values; settle them now. + this.form.controls.typeDistFile.updateValueAndValidity({ emitEvent: false }); + this.form.controls.collections.updateValueAndValidity({ emitEvent: false }); + } + + #buildCollectionGroup(collection: CollectionBundleDefinitionDto): CollectionGroup { + const rules = Array.isArray(collection.entriesSelectionRules) ? collection.entriesSelectionRules : []; + const group: CollectionGroup = new FormGroup({ + name: new FormControl(collection.name, { nonNullable: true, validators: [Validators.required] }), + bundledKeyPrefix: new FormControl(collection.bundledKeyPrefix ?? '', { nonNullable: true }), + mergeStrategy: new FormControl(collection.mergeStrategy ?? 'merge', { nonNullable: true }), + allEntries: new FormControl(collection.entriesSelectionRules === 'All', { nonNullable: true }), + rules: new FormArray( + rules.map((rule) => this.#buildRuleGroup(rule)), + { validators: [rulesRequiredValidator] }, + ), + }); + // The rules validator reads `allEntries`, which it could not see before the group existed; + // settle it now and re-run it on every toggle. + group.controls.rules.updateValueAndValidity({ emitEvent: false }); + group.controls.allEntries.valueChanges + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe(() => group.controls.rules.updateValueAndValidity()); + return group; + } + + #buildRuleGroup(rule: EntrySelectionRuleDto): RuleGroup { + return new FormGroup({ + matchingPattern: new FormControl(rule.matchingPattern, { + nonNullable: true, + validators: [Validators.required], + }), + matchingTags: new FormControl([...(rule.matchingTags ?? [])], { nonNullable: true }), + matchingTagOperator: new FormControl(rule.matchingTagOperator ?? 'Any', { nonNullable: true }), + }); + } + + #uniqueNameValidator(): ValidatorFn { + return (control) => { + if (this.#data.mode === 'edit') return null; + const value = String(control.value ?? '').trim(); + if (!value) return null; + const taken = this.store.bundleEntries().some((entry) => entry.name === value); + return taken ? { nameExists: { name: value } } : null; + }; + } + + /** + * Validators that read a sibling control do not re-run on their own when the sibling changes; + * these subscriptions nudge them. + */ + #wireDependentValidation(): void { + const controls = this.form.controls; + controls.typesEnabled.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => { + controls.typeDistFile.updateValueAndValidity(); + controls.tokenConstantName.updateValueAndValidity(); + }); + controls.allCollections.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => { + controls.collections.updateValueAndValidity(); + }); + } + + #wireDryRun(): void { + this.form.valueChanges + .pipe( + startWith(null), + tap(() => this.previewStale.set(true)), + debounceTime(300), + map(() => this.#buildDryRunRequest()), + switchMap((request) => { + if (!request) { + return of({ status: 'waiting' as const, result: undefined }); + } + this.previewStatus.update((status) => (status === 'ready' ? status : 'loading')); + return this.#api.dryRunBundle(request).pipe( + map((result) => ({ status: 'ready' as const, result })), + catchError(() => of({ status: 'error' as const, result: undefined })), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(({ status, result }) => { + this.previewStatus.set(status); + if (status !== 'error') this.dryRun.set(result); + this.previewStale.set(false); + }); + } + + /** The dry run needs a name, a folder and a pattern; anything less would only 400. */ + #buildDryRunRequest(): BundleDryRunRequestDto | undefined { + const raw = this.form.getRawValue(); + const name = raw.name.trim(); + if (!name || !raw.dist.trim() || !raw.bundleName.trim()) return undefined; + return { name, bundle: this.#buildDefinition() }; + } + + #buildDefinition(): BundleDefinitionDto { + const raw = this.form.getRawValue(); + const collections: BundleDefinitionDto['collections'] = raw.allCollections + ? 'All' + : raw.collections.map((collection) => { + const prefix = collection.bundledKeyPrefix.trim(); + const entriesSelectionRules: CollectionBundleDefinitionDto['entriesSelectionRules'] = collection.allEntries + ? 'All' + : collection.rules.map((rule) => ({ + matchingPattern: rule.matchingPattern.trim(), + ...(rule.matchingTags.length > 0 + ? { matchingTags: [...rule.matchingTags], matchingTagOperator: rule.matchingTagOperator } + : {}), + })); + return { + name: collection.name, + ...(prefix ? { bundledKeyPrefix: prefix } : {}), + entriesSelectionRules, + ...(collection.mergeStrategy === 'override' ? { mergeStrategy: 'override' as const } : {}), + }; + }); + + const typeDistFile = raw.typeDistFile.trim(); + const tokenConstantName = raw.tokenConstantName.trim(); + const typesOn = raw.typesEnabled && typeDistFile.length > 0; + + return { + bundleName: raw.bundleName.trim(), + dist: raw.dist.trim(), + collections, + ...(typesOn ? { typeDistFile } : {}), + ...(typesOn && raw.tokenCasing !== 'inherit' ? { tokenCasing: raw.tokenCasing } : {}), + ...(typesOn && tokenConstantName ? { tokenConstantName } : {}), + ...(raw.transformICUToTransloco !== 'inherit' + ? { transformICUToTransloco: raw.transformICUToTransloco === 'on' } + : {}), + }; + } + + #buildResult(): BundleFormResult { + return { name: this.form.getRawValue().name.trim(), bundle: this.#buildDefinition() }; + } + + /** After a failed submit, land on the first section that has something to fix. */ + #revealFirstError(): void { + const order: BundleSection[] = [ + 'output', + 'collections', + ...this.form.controls.collections.controls.map((_, index) => `coll:${index}` as const), + 'types', + ]; + const errors = this.sectionErrors(); + const first = order.find((section) => errors.has(section)); + if (first) this.activate(first); + } +} + +// ───────────────────────────── validators & helpers ───────────────────────────── + +function localePlaceholderValidator(control: AbstractControl): ValidationErrors | null { + const value = String(control.value ?? ''); + if (!value.trim()) return null; + return value.includes(LOCALE_PLACEHOLDER) ? null : { missingLocale: true }; +} + +function typeFileValidator(control: AbstractControl): ValidationErrors | null { + const parent = control.parent; + const enabled = parent?.get('typesEnabled')?.value === true; + if (!enabled) return null; + const value = String(control.value ?? '').trim(); + if (!value) return { required: true }; + return value.endsWith('.ts') ? null : { notTypeScript: true }; +} + +/** + * Mirrors core's `validateJavaScriptIdentifier` exactly: the shared rules live in + * `@simoncodes-ca/domain` so the client check can never be weaker than the server's, + * which would let the dialog close on a name the API then rejects with a 400. + */ +function identifierValidator(control: AbstractControl): ValidationErrors | null { + const value = String(control.value ?? '').trim(); + if (!value) return null; + return isValidJavaScriptIdentifier(value) ? null : { invalidIdentifier: true }; +} + +function collectionsRequiredValidator(control: AbstractControl): ValidationErrors | null { + const parent = control.parent; + const all = parent?.get('allCollections')?.value === true; + if (all) return null; + return control instanceof FormArray && control.length === 0 ? { collectionsEmpty: true } : null; +} + +function rulesRequiredValidator(control: AbstractControl): ValidationErrors | null { + const parent = control.parent; + const all = parent?.get('allEntries')?.value === true; + if (all) return null; + return control instanceof FormArray && control.length === 0 ? { rulesEmpty: true } : null; +} + +function groupIntoFolders(files: readonly { path: string; kind: 'bundle' | 'types'; exists: boolean | undefined }[]) { + const folders = new Map(); + for (const file of files) { + const slash = file.path.lastIndexOf('/'); + const folder = slash >= 0 ? file.path.slice(0, slash) : ''; + const name = slash >= 0 ? file.path.slice(slash + 1) : file.path; + const list = folders.get(folder) ?? []; + list.push({ name, nameParts: splitAfterSeparators(name, PATH_SEPARATORS), kind: file.kind, exists: file.exists }); + folders.set(folder, list); + } + return [...folders.entries()].map( + ([path, list]) => + ({ path, pathParts: splitAfterSeparators(path, PATH_SEPARATORS), files: list }) satisfies PreviewFolder, + ); +} diff --git a/apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.scss b/apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.scss new file mode 100644 index 00000000..66471ede --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.scss @@ -0,0 +1,69 @@ +// Segmented control: parchment-muted pills inside a hairline well. Colours and +// focus glow follow the dialog's text wells so the two read as one family. +:host { + display: inline-flex; + gap: 2px; + padding: 2px; + border: 1px solid var(--color-border-strong); + border-radius: var(--border-radius-md); + background: var(--dialog-well, var(--color-background)); + transition: + border-color var(--transition-fast), + box-shadow var(--transition-fast); +} + +:host(:focus-within) { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-secondary) 22%, transparent); +} + +:host(.segmented--disabled) { + opacity: 0.6; +} + +.segment { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + padding: 5px var(--spacing-3); + border: 0; + border-radius: var(--border-radius-sm); + background: transparent; + color: var(--color-text-secondary); + font: inherit; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + line-height: 1.4; + white-space: nowrap; + cursor: pointer; + transition: + background var(--transition-fast), + color var(--transition-fast); + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &:hover:not(:disabled) { + color: var(--color-text-primary); + } + + &:focus-visible { + outline: 2px solid var(--focus-ring-color); + outline-offset: -2px; + } + + &--on { + background: var(--color-background-muted); + color: var(--color-text-primary); + font-weight: var(--font-weight-semibold); + } +} + +:host(.segmented--mono) .segment { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); +} diff --git a/apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.ts b/apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.ts new file mode 100644 index 00000000..9a8b91a2 --- /dev/null +++ b/apps/tracker/src/app/collections/bundle-form-dialog/segmented-control.ts @@ -0,0 +1,126 @@ +import { ChangeDetectionStrategy, Component, ElementRef, forwardRef, inject, input, signal } from '@angular/core'; +import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'; +import { MatIconModule } from '@angular/material/icon'; +import { TranslocoPipe } from '@jsverse/transloco'; + +export type SegmentValue = string | boolean; + +export interface SegmentOption { + readonly value: T; + /** Transloco token for the visible label. Omit for technical literals and set `text`. */ + readonly label?: string; + /** Untranslated literal label (identifiers such as `camelCase`). */ + readonly text?: string; + readonly icon?: string; +} + +/** + * A hand-rolled segmented control: parchment-muted pills inside a hairline well, exposed as a + * radio group so arrow keys move the selection the way a native radio set does. Plugs into + * reactive forms through ControlValueAccessor. + */ +@Component({ + selector: 'app-segmented-control', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [MatIconModule, TranslocoPipe], + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SegmentedControl), multi: true }], + host: { + class: 'segmented', + role: 'radiogroup', + '[class.segmented--mono]': 'mono()', + '[class.segmented--disabled]': 'disabled()', + '[attr.aria-labelledby]': 'labelledBy() || null', + '(keydown)': 'onKeydown($event)', + }, + template: ` + @for (option of options(); track option.value; let index = $index) { + + } + `, + styleUrl: './segmented-control.scss', +}) +export class SegmentedControl implements ControlValueAccessor { + readonly #host = inject>(ElementRef); + + readonly options = input.required(); + readonly mono = input(false); + readonly labelledBy = input(''); + + readonly value = signal(undefined); + readonly disabled = signal(false); + + #onChange: (value: SegmentValue) => void = () => undefined; + #onTouched: () => void = () => undefined; + + /** Only the checked pill (or the first, when nothing is checked) is in the tab order. */ + tabIndexFor(optionValue: SegmentValue, index: number): number { + const current = this.value(); + const hasSelection = this.options().some((option) => option.value === current); + if (hasSelection) return optionValue === current ? 0 : -1; + return index === 0 ? 0 : -1; + } + + select(next: SegmentValue): void { + if (this.disabled() || next === this.value()) { + this.#onTouched(); + return; + } + this.value.set(next); + this.#onChange(next); + this.#onTouched(); + } + + onKeydown(event: KeyboardEvent): void { + const forward = event.key === 'ArrowRight' || event.key === 'ArrowDown'; + const backward = event.key === 'ArrowLeft' || event.key === 'ArrowUp'; + if (!forward && !backward) return; + event.preventDefault(); + + const options = this.options(); + if (options.length === 0) return; + const currentIndex = options.findIndex((option) => option.value === this.value()); + const step = forward ? 1 : -1; + const nextIndex = (currentIndex + step + options.length) % options.length; + const nextOption = options[nextIndex]; + if (!nextOption) return; + + this.select(nextOption.value); + const button = this.#host.nativeElement.querySelector(`[data-index="${nextIndex}"]`); + button?.focus(); + } + + writeValue(value: SegmentValue | undefined): void { + this.value.set(value); + } + + registerOnChange(fn: (value: SegmentValue) => void): void { + this.#onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.#onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled.set(isDisabled); + } +} diff --git a/apps/tracker/src/app/collections/collections-manager.html b/apps/tracker/src/app/collections/collections-manager.html index eb81b8d5..94d807c7 100644 --- a/apps/tracker/src/app/collections/collections-manager.html +++ b/apps/tracker/src/app/collections/collections-manager.html @@ -2,53 +2,24 @@