diff --git a/.gitignore b/.gitignore index f2b1774..1e34506 100644 --- a/.gitignore +++ b/.gitignore @@ -419,3 +419,6 @@ FodyWeavers.xsd *.msix *.msm *.msp + +# AI +CLAUDE.md diff --git a/docs/adr/0003-design-token-management.md b/docs/adr/0003-design-token-management.md new file mode 100644 index 0000000..ed843fb --- /dev/null +++ b/docs/adr/0003-design-token-management.md @@ -0,0 +1,98 @@ +# 3. Design token management with Figma Tokens Studio and Vite + +Date: 2025-11-19 + +## Status + +Accepted + +## Context + +The Summer EBT Self-Service Portal needs to support customization for 50+ states while +maintaining a consistent design system foundation. Each state requires its own visual identity +(colors, typography, spacing) while adhering to USWDS (U.S. Web Design System) standards and +federal accessibility requirements. + +Key requirements: +- **Multi-state branding**: Each state needs customizable theme tokens (DC, CA, TX, etc.) +- **Designer-developer workflow**: Designers work in Figma, developers need automated sync +- **USWDS compliance**: Must integrate with USWDS theming system +- **Scalability**: Clear workflow that scales across 50+ state implementations + +Without automated token management, each state implementation would require manual +translation of design decisions into code, creating maintenance burden and design-code drift. + +## Decision + +We will use **Figma Tokens Studio** for token management with a **Vite-based custom USWDS compiler**. + +**Core Architecture**: +- **Token Source**: Figma Tokens Studio plugin with GitHub sync to `design/states/{state}.json` +- **Token Organization**: One JSON file per state following W3C Design Tokens specification +- **Transformation**: Node.js scripts convert tokens to USWDS SCSS variables and inject fonts +- **Compilation**: Vite compiles USWDS with state-specific theme tokens using modern build tooling +- **Optimization**: Lightning CSS, sass-embedded, PurgeCSS for production bundle optimization + +**Workflow**: `Figma → Tokens Studio → GitHub → JSON → Transformation Scripts → SCSS → Vite → CSS Bundle` + +## Alternatives Considered + +### Alternative 1: @uswds/compile (Official Gulp-based tooling) +**Why rejected**: Gulp is a legacy build tool with slower build times. USWDS documentation +explicitly supports custom compilers. Vite unifies our toolchain and provides modern +performance optimizations while maintaining full USWDS compatibility. + +### Alternative 2: Style Dictionary +**Why rejected**: Requires manual Figma export workflow (no GitHub sync). For our web-only +use case, the platform-agnostic capabilities add complexity without benefit. Figma Tokens +Studio's direct GitHub integration eliminates manual export steps. + +### Alternative 3: CSS Custom Properties at Runtime +**Why rejected**: USWDS requires IE11 support, which lacks CSS custom property support. +Build-time compilation is more performant and aligns with USWDS architecture. + +### Alternative 4: Manual SCSS Variables +**Why rejected**: Does not scale to 50+ state implementations. Manual synchronization is +error-prone and creates a bottleneck in the design-development workflow. Automated pipeline +is essential for maintaining consistency and velocity. + +## Consequences + +### Positive +- **Single source of truth**: Figma becomes the authoritative source for all design tokens +- **Automated workflow**: Design changes sync to code automatically via GitHub +- **USWDS compatibility**: Full compliance with USWDS theming and browser requirements +- **Scalable to 50+ states**: Same pattern replicable across all state implementations +- **Modern developer experience**: Vite with HMR for rapid iteration +- **Production optimization**: Automated bundle optimization with Lightning CSS and PurgeCSS + +### Negative +- **Custom tooling maintenance**: Token transformation scripts require ongoing maintenance +- **Third-party dependency**: Reliance on Figma Tokens Studio plugin +- **Learning curve**: Team must understand token transformation and Vite configuration + +### Risks and Mitigation +**Risk**: Figma Tokens Studio plugin abandonment +**Mitigation**: JSON format is portable; migration path to Style Dictionary exists + +**Risk**: USWDS version changes breaking compatibility +**Mitigation**: Custom compiler follows official USWDS patterns documented by USWDS team + +**Risk**: State-specific token conflicts +**Mitigation**: Clear naming conventions and USWDS namespace prevent collisions + +## References + +**Implementation Details**: See `docs/design-token-system.md` for technical specifications, +performance metrics, and implementation guide. + +**Key Documentation**: +- [USWDS Custom Compiler Guide](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) +- [Figma Tokens Studio Documentation](https://docs.tokens.studio/) + +## Related ADRs +- **ADR 0002**: Adopt Clean Architecture + +## Notes +This ADR documents the production-ready implementation for DC as the first state. The pattern +will be replicated for all subsequent state implementations (CA, TX, FL, etc.). diff --git a/package.json b/package.json new file mode 100644 index 0000000..62c9c84 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "sebt-portal", + "version": "1.0.0", + "private": true, + "description": "Summer EBT (SUN Bucks) Self-Service Portal - Root package for convenience scripts", + "scripts": { + "web:dev": "pnpm --filter @sebt/web dev", + "web:build": "pnpm --filter @sebt/web build", + "web:build:dc": "pnpm --filter @sebt/web build:dc", + "web:build:co": "pnpm --filter @sebt/web build:co", + "web:tokens": "pnpm --filter @sebt/web tokens", + "web:tokens:dc": "pnpm --filter @sebt/web tokens:dc", + "web:tokens:co": "pnpm --filter @sebt/web tokens:co", + "web:preview": "pnpm --filter @sebt/web preview", + "api:run": "dotnet watch --project src/SEBT.Portal.Api", + "api:build": "dotnet build", + "api:test": "dotnet test", + "api:restore": "dotnet restore", + "dev": "concurrently -n \"API,Web\" -c \"blue,green\" \"pnpm api:run\" \"pnpm web:dev\"", + "dev:web-only": "pnpm web:dev", + "install:web": "pnpm --filter @sebt/web install", + "install:all": "pnpm install" + }, + "devDependencies": { + "concurrently": "^9.2.1" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.0.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..2ad7e3e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1871 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + + src/SEBT.Portal.Web: + dependencies: + '@uswds/uswds': + specifier: ^3.8.0 + version: 3.13.0 + devDependencies: + '@fullhuman/postcss-purgecss': + specifier: ^7.0.2 + version: 7.0.2(postcss@8.5.6) + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + lightningcss: + specifier: ^1.30.2 + version: 1.30.2 + purgecss: + specifier: ^7.0.2 + version: 7.0.2 + sass-embedded: + specifier: ^1.93.3 + version: 1.93.3 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.2 + version: 7.2.4(@types/node@24.10.1)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3) + vite-plugin-static-copy: + specifier: ^3.1.4 + version: 3.1.4(vite@7.2.4(@types/node@24.10.1)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3)) + +packages: + + '@bufbuild/protobuf@2.10.1': + resolution: {integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fullhuman/postcss-purgecss@7.0.2': + resolution: {integrity: sha512-U4zAXNaVztbDxO9EdcLp51F3UxxYsb/7DN89rFxFJhfk2Wua2pvw2Kf3HdspbPhW/wpHjSjsxWYoIlbTgRSjbQ==} + peerDependencies: + postcss: ^8.0.0 + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@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] + + '@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] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@uswds/uswds@3.13.0': + resolution: {integrity: sha512-8P494gmXv/0sm09ExSdj8wAMjGLnM7UMRY/XgsMIRKnWfDXG+TyuCOKIuD4lqs+gLvSmi1nTQKyd0c0/A7VWJQ==} + engines: {node: '>= 4'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-builder@0.2.0: + resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + element-closest@2.0.2: + resolution: {integrity: sha512-QCqAWP3kwj8Gz9UXncVXQGdrhnWxD8SQBSeZp5pOsyCcQ6RpL738L1/tfuwBiMi6F1fYkxqPnBrFBR4L+f49Cg==} + engines: {node: '>=4.0.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + keyboardevent-key-polyfill@1.1.0: + resolution: {integrity: sha512-NTDqo7XhzL1fqmUzYroiyK2qGua7sOMzLav35BfNA/mPUSCtw8pZghHFMTYR9JdnJ23IQz695FcaM6EE6bpbFQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + + lit@3.3.1: + resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} + + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + + matches-selector@1.2.0: + resolution: {integrity: sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + purgecss@7.0.2: + resolution: {integrity: sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ==} + hasBin: true + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + receptor@1.0.0: + resolution: {integrity: sha512-yvVEqVQDNzEmGkluCkEdbKSXqZb3WGxotI/VukXIQ+4/BXEeXVjWtmC6jWaR1BIsmEAGYQy3OTaNgDj2Svr01w==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sass-embedded-all-unknown@1.93.3: + resolution: {integrity: sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.93.3: + resolution: {integrity: sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.93.3: + resolution: {integrity: sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.93.3: + resolution: {integrity: sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.93.3: + resolution: {integrity: sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.93.3: + resolution: {integrity: sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.93.3: + resolution: {integrity: sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.93.3: + resolution: {integrity: sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-arm@1.93.3: + resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-musl-arm64@1.93.3: + resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-musl-arm@1.93.3: + resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-musl-riscv64@1.93.3: + resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + + sass-embedded-linux-musl-x64@1.93.3: + resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + + sass-embedded-linux-riscv64@1.93.3: + resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + + sass-embedded-linux-x64@1.93.3: + resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + + sass-embedded-unknown-all@1.93.3: + resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.93.3: + resolution: {integrity: sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.93.3: + resolution: {integrity: sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.93.3: + resolution: {integrity: sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==} + engines: {node: '>=16.0.0'} + hasBin: true + + sass@1.93.3: + resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} + engines: {node: '>=14.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.1.3: + resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} + engines: {node: '>=16.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + vite-plugin-static-copy@3.1.4: + resolution: {integrity: sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + + vite@7.2.4: + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@bufbuild/protobuf@2.10.1': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@fullhuman/postcss-purgecss@7.0.2(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + purgecss: 7.0.2 + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + '@types/trusted-types@2.0.7': {} + + '@uswds/uswds@3.13.0': + dependencies: + lit: 3.3.1 + receptor: 1.0.0 + optionalDependencies: + sass-embedded-linux-x64: 1.93.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-builder@0.2.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorjs.io@0.5.2: {} + + commander@12.1.0: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + eastasianwidth@0.2.0: {} + + element-closest@2.0.2: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.1.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.1 + + has-flag@4.0.0: {} + + immutable@5.1.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + keyboardevent-key-polyfill@1.1.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.1: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + + lru-cache@11.2.2: {} + + matches-selector@1.2.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + optional: true + + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minipass@7.1.2: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: + optional: true + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + p-map@7.0.4: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + purgecss@7.0.2: + dependencies: + commander: 12.1.0 + glob: 11.1.0 + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: + optional: true + + receptor@1.0.0: + dependencies: + element-closest: 2.0.2 + keyboardevent-key-polyfill: 1.1.0 + matches-selector: 1.2.0 + object-assign: 4.1.1 + + require-directory@2.1.1: {} + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sass-embedded-all-unknown@1.93.3: + dependencies: + sass: 1.93.3 + optional: true + + sass-embedded-android-arm64@1.93.3: + optional: true + + sass-embedded-android-arm@1.93.3: + optional: true + + sass-embedded-android-riscv64@1.93.3: + optional: true + + sass-embedded-android-x64@1.93.3: + optional: true + + sass-embedded-darwin-arm64@1.93.3: + optional: true + + sass-embedded-darwin-x64@1.93.3: + optional: true + + sass-embedded-linux-arm64@1.93.3: + optional: true + + sass-embedded-linux-arm@1.93.3: + optional: true + + sass-embedded-linux-musl-arm64@1.93.3: + optional: true + + sass-embedded-linux-musl-arm@1.93.3: + optional: true + + sass-embedded-linux-musl-riscv64@1.93.3: + optional: true + + sass-embedded-linux-musl-x64@1.93.3: + optional: true + + sass-embedded-linux-riscv64@1.93.3: + optional: true + + sass-embedded-linux-x64@1.93.3: + optional: true + + sass-embedded-unknown-all@1.93.3: + dependencies: + sass: 1.93.3 + optional: true + + sass-embedded-win32-arm64@1.93.3: + optional: true + + sass-embedded-win32-x64@1.93.3: + optional: true + + sass-embedded@1.93.3: + dependencies: + '@bufbuild/protobuf': 2.10.1 + buffer-builder: 0.2.0 + colorjs.io: 0.5.2 + immutable: 5.1.4 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.93.3 + sass-embedded-android-arm: 1.93.3 + sass-embedded-android-arm64: 1.93.3 + sass-embedded-android-riscv64: 1.93.3 + sass-embedded-android-x64: 1.93.3 + sass-embedded-darwin-arm64: 1.93.3 + sass-embedded-darwin-x64: 1.93.3 + sass-embedded-linux-arm: 1.93.3 + sass-embedded-linux-arm64: 1.93.3 + sass-embedded-linux-musl-arm: 1.93.3 + sass-embedded-linux-musl-arm64: 1.93.3 + sass-embedded-linux-musl-riscv64: 1.93.3 + sass-embedded-linux-musl-x64: 1.93.3 + sass-embedded-linux-riscv64: 1.93.3 + sass-embedded-linux-x64: 1.93.3 + sass-embedded-unknown-all: 1.93.3 + sass-embedded-win32-arm64: 1.93.3 + sass-embedded-win32-x64: 1.93.3 + + sass@1.93.3: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.1.3 + + sync-message-port@1.1.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + util-deprecate@1.0.2: {} + + varint@6.0.0: {} + + vite-plugin-static-copy@3.1.4(vite@7.2.4(@types/node@24.10.1)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3)): + dependencies: + chokidar: 3.6.0 + p-map: 7.0.4 + picocolors: 1.1.1 + tinyglobby: 0.2.15 + vite: 7.2.4(@types/node@24.10.1)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3) + + vite@7.2.4(@types/node@24.10.1)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.93.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + lightningcss: 1.30.2 + sass: 1.93.3 + sass-embedded: 1.93.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3d33d91 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +# pnpm workspace configuration +# Defines packages in this repository for pnpm --filter to work +# +# Future packages can be added here as the project grows: +# - 'src/SEBT.Portal.Admin' # Admin dashboard (future) +# - 'src/SEBT.Portal.Shared' # Shared UI components (future) +# - 'packages/*' # Shared packages (future) + +packages: + - 'src/SEBT.Portal.Web' diff --git a/src/SEBT.Portal.Api/SEBT.Portal.Api.csproj b/src/SEBT.Portal.Api/SEBT.Portal.Api.csproj index 32ab2fa..50c6341 100644 --- a/src/SEBT.Portal.Api/SEBT.Portal.Api.csproj +++ b/src/SEBT.Portal.Api/SEBT.Portal.Api.csproj @@ -1,28 +1,82 @@ - - net8.0 - enable - enable - Linux + + net10.0 + enable + enable + Linux + + + $(MSBuildProjectDirectory)/../SEBT.Portal.Web + $(FrontendRoot)/dist + $(MSBuildProjectDirectory)/../.. + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - .dockerignore - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + .dockerignore + + + + + + + + + + + + \ No newline at end of file diff --git a/src/SEBT.Portal.Web/.env.example b/src/SEBT.Portal.Web/.env.example new file mode 100644 index 0000000..137a26d --- /dev/null +++ b/src/SEBT.Portal.Web/.env.example @@ -0,0 +1,5 @@ +# State configuration for local development +# Copy this file to .env and set your preferred state + +# Valid values: dc, co +STATE=dc diff --git a/src/SEBT.Portal.Web/.gitignore b/src/SEBT.Portal.Web/.gitignore new file mode 100644 index 0000000..466874e --- /dev/null +++ b/src/SEBT.Portal.Web/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Build outputs +public/uswds.css + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment files +.env +.env.local + +# USWDS generated files (auto-generated during build from design/states/*.json) +# Only ignore state-specific auto-generated theme files (dc, co, etc.) +# Manual files like _uswds-theme.scss and _uswds-theme-custom-styles.scss should be committed +sass/_uswds-theme-dc.scss +sass/_uswds-theme-co.scss +sass/_uswds-theme-entry.scss diff --git a/src/SEBT.Portal.Web/README.md b/src/SEBT.Portal.Web/README.md new file mode 100644 index 0000000..ebfcefc --- /dev/null +++ b/src/SEBT.Portal.Web/README.md @@ -0,0 +1,154 @@ +# SEBT Portal Web + +Frontend for the Summer EBT Self-Service Portal using USWDS with Figma design tokens. + +## Quick Start + +```bash +# Install dependencies +pnpm install + +# Configure your local state +cp .env.example .env +# Edit .env to set STATE=dc or STATE=co + +# Start dev server (http://localhost:5173) +pnpm dev + +# Build for production +pnpm build + +# Preview production build +pnpm preview +``` + +## Tech Stack + +- **Framework**: Vite 7 + TypeScript +- **Design System**: USWDS 3.8 +- **CSS**: Sass with Lightning CSS +- **Design Tokens**: Figma Tokens Studio → GitHub sync + +## Design Token Workflow + +Each state deployment uses a single token file - tokens are generated automatically during build. + +``` +Figma → Tokens Studio Plugin → design/states/{state}.json (committed to git) + ↓ + pnpm build:dc (auto-generates during build) + ↓ + sass/_uswds-theme-dc.scss (gitignored) + ↓ + Vite build → Compiled CSS +``` + +### Building for Production + +```bash +# Build for specific state (auto-generates tokens) +pnpm web:build:dc # DC deployment +pnpm web:build:co # Colorado deployment + +# Or use environment variable +STATE=co pnpm web:build + +# Default build uses DC +pnpm web:build +``` + +### Local Development + +**State Configuration:** +Your local state is configured via `.env` file. Vite automatically loads this file. + +```bash +# .env file +STATE=dc # or co + +# Start dev server - uses state from .env +pnpm dev + +# Override state temporarily +STATE=co pnpm dev +``` + +**Manual Token Generation (Development Only):** + +```bash +# Regenerate tokens when you update design/states/*.json +pnpm tokens:dc +pnpm tokens:co + +# Or use the script directly +node scripts/tokens-to-scss.js dc + +# Smart caching: only regenerates if state JSON changed +``` + +## Project Structure + +``` +src/SEBT.Portal.Web/ +├── design/states/ # Figma design tokens (JSON) +│ ├── dc.json # District of Columbia +│ └── co.json # Colorado (when added) +├── sass/ # USWDS theme configuration +│ ├── _uswds-theme-dc.scss # Auto-generated DC theme +│ └── _uswds-theme.scss # Theme loader +├── scripts/ +│ └── tokens-to-scss.js # Token transformation script +├── src/ +│ ├── main.ts # App entry point +│ └── styles.scss # Sass entry point +├── public/ # Static assets (auto-copied) +├── index.html # HTML entry point +└── vite.config.ts # Build configuration +``` + +## Key Features + +- ✅ **Multi-state support**: Generate SCSS for any state (DC, CO, etc.) +- ✅ **Smart token caching**: Instant builds when tokens unchanged +- ✅ **Lightning CSS**: 5-10x faster CSS processing +- ✅ **sass-embedded**: 10-30% faster Sass compilation +- ✅ **HMR**: Instant hot module replacement +- ✅ **USWDS compliant**: Follows official custom compiler pattern + +## Multi-State Deployment + +Each state is a **separate deployment** with its own build. The build process automatically generates the correct SCSS for that state. + +**Deployment Architecture:** +``` +dc.portal.sebt.gov → pnpm web:build:dc → Uses design/states/dc.json +co.portal.sebt.gov → pnpm web:build:co → Uses design/states/co.json +``` + +**CI/CD Integration:** +```yaml +# Example GitHub Actions workflow +- name: Build DC Portal + run: pnpm web:build:dc # Auto-generates tokens from dc.json + +- name: Build CO Portal + run: pnpm web:build:co # Auto-generates tokens from co.json +``` + +**What's committed to Git:** +- ✅ `design/states/*.json` - Source of truth for design tokens +- ✅ `scripts/tokens-to-scss.js` - Token transformation script +- ❌ `sass/_uswds-theme-*.scss` - Auto-generated, gitignored + +## Documentation + +- [Token Management ADR](../../docs/adr/0003-design-token-management-with-figma-and-vite.md) +- [USWDS Custom Compiler Guide](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) +- [Figma Tokens Studio Docs](https://docs.tokens.studio/) + +## Build Performance + +- Build time: ~3s (with optimizations) +- CSS output: ~532 KB (minified + prefixed) +- Token transformation: <100ms (cached), ~200ms (regeneration) +- Browser support: IE11, Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ diff --git a/src/SEBT.Portal.Web/index.html b/src/SEBT.Portal.Web/index.html new file mode 100644 index 0000000..e257d1c --- /dev/null +++ b/src/SEBT.Portal.Web/index.html @@ -0,0 +1,146 @@ + + + + + + SEBT Portal - USWDS Implementation Test + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+

+ USWDS Implementation with Figma Design Tokens +

+ +
+
+

✓ Implementation Working

+

+ USWDS is successfully compiled with DC state theme tokens from + Figma. DC theme primary color (mint-cool-60v) is applied to + buttons and links. +

+
+
+ +
+

Build Workflow

+
    +
  1. + Figma Tokens Studio → Export design tokens to + design/states/dc.json +
  2. +
  3. + Token Transformation → Transform tokens to USWDS theme + variables in sass/_uswds-theme-dc.scss +
  4. +
  5. + Vite → Compile USWDS Sass with DC theme and bundle with Autoprefixer +
  6. +
  7. + HMR → Instant hot module replacement for rapid development +
  8. +
+
+ +
+

USWDS Components with DC Theme

+ +
+ + + +
+ +
+
+

+ DC Theme Active: Primary color is + mint-cool-60v (#0f6460), Typography uses Urbanist font family + from Figma tokens. +

+
+
+
+
+
+
+ +
+ +
+ + + + + diff --git a/src/SEBT.Portal.Web/package.json b/src/SEBT.Portal.Web/package.json new file mode 100644 index 0000000..8b0a84a --- /dev/null +++ b/src/SEBT.Portal.Web/package.json @@ -0,0 +1,26 @@ +{ + "name": "@sebt/web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "predev": "node scripts/tokens-to-scss.js && node scripts/inject-fonts.js", + "dev": "vite", + "prebuild": "node scripts/validate-tokens.js && node scripts/tokens-to-scss.js && node scripts/inject-fonts.js && node scripts/generate-token-types.js", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@uswds/uswds": "^3.8.0" + }, + "devDependencies": { + "@fullhuman/postcss-purgecss": "^7.0.2", + "@types/node": "^24.10.1", + "lightningcss": "^1.30.2", + "purgecss": "^7.0.2", + "sass-embedded": "^1.93.3", + "typescript": "~5.9.3", + "vite": "^7.2.2", + "vite-plugin-static-copy": "^3.1.4" + } +} diff --git a/src/SEBT.Portal.Web/sass/_uswds-theme-custom-styles.scss b/src/SEBT.Portal.Web/sass/_uswds-theme-custom-styles.scss new file mode 100644 index 0000000..bb83602 --- /dev/null +++ b/src/SEBT.Portal.Web/sass/_uswds-theme-custom-styles.scss @@ -0,0 +1,18 @@ +/* +======================================== +SEBT Portal - Custom USWDS Styles +======================================== +Project-specific style overrides and custom components. +Loaded after USWDS core to allow safe overrides. + +Structure: +1. Custom utility classes +2. Component overrides +3. Project-specific components +4. State-specific theming + +References: +- USWDS Documentation: https://designsystem.digital.gov/ +- Theme Configuration: ../sass/_uswds-theme.scss +- DC Theme Variables: ../sass/_uswds-theme-dc.scss (auto-generated from Figma tokens) +*/ diff --git a/src/SEBT.Portal.Web/sass/_uswds-theme.scss b/src/SEBT.Portal.Web/sass/_uswds-theme.scss new file mode 100644 index 0000000..bbdf63c --- /dev/null +++ b/src/SEBT.Portal.Web/sass/_uswds-theme.scss @@ -0,0 +1,16 @@ +/* +---------------------------------------- +USWDS Theme Configuration +---------------------------------------- +This file loads USWDS with state-specific theme settings. +State theme is loaded via auto-generated _uswds-theme-entry.scss +which is regenerated during build based on STATE environment variable. + +Build commands: + pnpm build # Defaults to DC + STATE=dc pnpm build + STATE=co pnpm build +*/ + +// Forward the auto-generated theme entry (loads state-specific theme + USWDS core) +@forward "uswds-theme-entry"; diff --git a/src/SEBT.Portal.Web/scripts/generate-token-types.js b/src/SEBT.Portal.Web/scripts/generate-token-types.js new file mode 100755 index 0000000..d3b59c6 --- /dev/null +++ b/src/SEBT.Portal.Web/scripts/generate-token-types.js @@ -0,0 +1,226 @@ +#!/usr/bin/env node +/** + * TypeScript Type Definitions Generator + * + * Auto-generates TypeScript types from design tokens for IDE autocomplete and type safety. + * + * Usage: + * node generate-token-types.js # Generates types for DC (default) + * node generate-token-types.js ca # Generates types for California + * STATE=tx node generate-token-types.js # Environment variable + * + * Output: + * src/types/design-tokens.d.ts - TypeScript type definitions + * + * Features: + * - Type-safe access to design tokens in TypeScript + * - IDE autocomplete for token values + * - Compile-time validation of token usage + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = join(__dirname, '..'); + +/** + * Convert token name to TypeScript-friendly property name + * theme-color-primary-vivid → colorPrimaryVivid + */ +function toCamelCase(str) { + return str + .replace(/^theme-/, '') + .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +/** + * Determine TypeScript type from token $type + */ +function getTypeScriptType(tokenType) { + switch (tokenType) { + case 'color': + return 'string'; // USWDS color reference or hex + case 'fontFamilies': + return 'string'; + case 'dimension': + case 'sizing': + return 'string | number'; + case 'fontWeights': + return 'number | string'; + case 'lineHeights': + return 'number | string'; + case 'number': + return 'number'; + case 'boolean': + return 'boolean'; + default: + return 'string'; + } +} + +/** + * Process theme object and generate TypeScript interface properties + */ +function processThemeObject(obj, prefix = '') { + const properties = []; + + for (const [key, value] of Object.entries(obj)) { + if (value && typeof value === 'object' && '$value' in value) { + // This is a token with a value + const tokenName = prefix ? `${prefix}-${key}` : key; + const propName = toCamelCase(tokenName); + const tsType = getTypeScriptType(value.$type); + const description = value.$description || `Token: ${tokenName}`; + + properties.push({ + name: propName, + type: tsType, + description, + originalName: tokenName + }); + } else if (value && typeof value === 'object' && !('$value' in value)) { + // Nested object - recurse + const nestedPrefix = prefix ? `${prefix}-${key}` : key; + properties.push(...processThemeObject(value, nestedPrefix)); + } + } + + return properties; +} + +/** + * Generate TypeScript interface from properties + */ +function generateInterface(state, properties) { + const interfaceName = `${state.toUpperCase()}Theme`; + const lines = []; + + lines.push('/**'); + lines.push(` * Design tokens for ${state.toUpperCase()} state`); + lines.push(` * Auto-generated from design/states/${state}.json`); + lines.push(' * DO NOT EDIT DIRECTLY - Regenerated from design tokens'); + lines.push(' */'); + lines.push(`export interface ${interfaceName} {`); + + // Sort properties alphabetically for better readability + const sortedProps = [...properties].sort((a, b) => a.name.localeCompare(b.name)); + + sortedProps.forEach(({ name, type, description }) => { + lines.push(` /** ${description} */`); + lines.push(` ${name}: ${type};`); + lines.push(''); + }); + + lines.push('}'); + + return lines.join('\n'); +} + +/** + * Generate token value mapping (for runtime access) + */ +function generateTokenMap(state, properties) { + const constName = `${state.toUpperCase()}_TOKENS`; + const lines = []; + + lines.push('/**'); + lines.push(` * Runtime token map for ${state.toUpperCase()} state`); + lines.push(` * Maps property names to original SCSS variable names`); + lines.push(' */'); + lines.push(`export const ${constName}: Record = {`); + + // Sort properties alphabetically + const sortedProps = [...properties].sort((a, b) => a.name.localeCompare(b.name)); + + sortedProps.forEach(({ name, originalName }, index) => { + const comma = index < sortedProps.length - 1 ? ',' : ''; + lines.push(` ${name}: '$${originalName}'${comma}`); + }); + + lines.push('};'); + + return lines.join('\n'); +} + +/** + * Main generation function + */ +function generateTypes(state) { + const startTime = performance.now(); + const tokenPath = join(projectRoot, `design/states/${state}.json`); + const outputDir = join(projectRoot, 'src/types'); + const outputPath = join(outputDir, 'design-tokens.d.ts'); + + if (!existsSync(tokenPath)) { + throw new Error(`Token file not found: ${tokenPath}`); + } + + console.log(`Reading tokens: ${tokenPath}`); + const tokenData = JSON.parse(readFileSync(tokenPath, 'utf8')); + + if (!tokenData.theme) { + throw new Error(`No "theme" object found in ${state}.json`); + } + + console.log(`✅ Processing theme tokens for ${state.toUpperCase()}`); + + // Extract properties + const properties = processThemeObject(tokenData.theme); + console.log(`✅ Extracted ${properties.length} token properties`); + + // Generate TypeScript content + const content = `// design-tokens.d.ts +// Auto-generated TypeScript definitions for design tokens +// Source: design/states/${state}.json +// DO NOT EDIT DIRECTLY - Regenerated from design tokens on build + +${generateInterface(state, properties)} + +${generateTokenMap(state, properties)} + +/** + * Example usage: + * + * import type { ${state.toUpperCase()}Theme } from './types/design-tokens'; + * import { ${state.toUpperCase()}_TOKENS } from './types/design-tokens'; + * + * // Type-safe access + * const primaryColor: ${state.toUpperCase()}Theme['colorPrimaryVivid'] = '#0f6460'; + * + * // Runtime SCSS variable name + * const scssVar = ${state.toUpperCase()}_TOKENS.colorPrimaryVivid; // '$theme-color-primary-vivid' + */ +`; + + // Write to file + mkdirSync(outputDir, { recursive: true }); + writeFileSync(outputPath, content, 'utf8'); + + const elapsedTime = (performance.now() - startTime).toFixed(2); + + console.log(`✅ Generated TypeScript definitions`); + console.log(` ${outputPath}`); + console.log(`\n📊 Type Generation Metrics:`); + console.log(` Generation time: ${elapsedTime}ms`); + console.log(` Properties: ${properties.length}`); + console.log(` Interface: ${state.toUpperCase()}Theme`); + console.log(` Token map: ${state.toUpperCase()}_TOKENS\n`); + + return { success: true, properties: properties.length, time: parseFloat(elapsedTime) }; +} + +function main() { + try { + const args = process.argv.slice(2); + const state = (args[0] || process.env.STATE || 'dc').toLowerCase(); + + generateTypes(state); + } catch (error) { + console.error(`❌ Error: ${error.message}`); + process.exit(1); + } +} + +main(); diff --git a/src/SEBT.Portal.Web/scripts/inject-fonts.js b/src/SEBT.Portal.Web/scripts/inject-fonts.js new file mode 100644 index 0000000..6198641 --- /dev/null +++ b/src/SEBT.Portal.Web/scripts/inject-fonts.js @@ -0,0 +1,162 @@ +#!/usr/bin/env node +/** + * Inject State-Specific Fonts into HTML + * + * Auto-detects custom fonts from design tokens and injects Google Fonts links into index.html. + * Runs during prebuild to ensure fonts match the current state's design tokens. + * + * Usage: + * node inject-fonts.js # Defaults to DC + * node inject-fonts.js co # Colorado state + * STATE=tx node inject-fonts.js # Environment variable + * + * Workflow: + * 1. Read design/states/{state}.json + * 2. Extract font families from theme tokens + * 3. Generate Google Fonts URLs with appropriate weights + * 4. Inject tags into index.html at STATE_FONTS_PLACEHOLDER + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = join(__dirname, '..'); + +const GOOGLE_FONTS_BASE = 'https://fonts.googleapis.com/css2'; +const DEFAULT_WEIGHTS = [300, 400, 700]; +const DEFAULT_STYLES = ['normal', 'italic']; + +const GOOGLE_FONTS = new Set([ + 'urbanist', + 'public sans', + 'roboto', + 'open sans', + 'lato', + 'montserrat', + 'raleway', + 'poppins', + 'inter', + 'work sans', + 'nunito' +]); + +function extractFonts(tokensJson, state) { + const fonts = new Set(); + + if (!tokensJson.theme) { + console.warn(`⚠️ No theme object found in ${state}.json`); + return fonts; + } + + const theme = tokensJson.theme; + + if (theme['theme-font-type-sans']?.$value) { + const fontName = theme['theme-font-type-sans'].$value.replace(/'/g, '').toLowerCase(); + fonts.add(fontName); + } + + if (theme['theme-font-type-serif']?.$value) { + const fontName = theme['theme-font-type-serif'].$value.replace(/'/g, '').toLowerCase(); + fonts.add(fontName); + } + + return fonts; +} + +function isGoogleFont(fontName) { + return GOOGLE_FONTS.has(fontName.toLowerCase()); +} + +function generateGoogleFontsUrl(fontName) { + const capitalized = fontName + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join('+'); + + const weightSpecs = []; + DEFAULT_STYLES.forEach((style, styleIdx) => { + DEFAULT_WEIGHTS.forEach(weight => { + weightSpecs.push(`${styleIdx},${weight}`); + }); + }); + + return `${GOOGLE_FONTS_BASE}?family=${capitalized}:ital,wght@${weightSpecs.join(';')}&display=swap`; +} + +function generateFontLinks(fonts) { + if (fonts.size === 0) { + return ''; + } + + const googleFonts = Array.from(fonts).filter(isGoogleFont); + + if (googleFonts.length === 0) { + return ``; + } + + const lines = [ + '', + '', + '' + ]; + + googleFonts.forEach(font => { + const url = generateGoogleFontsUrl(font); + const capitalizedName = font + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + lines.push(``); + lines.push(``); + }); + + return lines.map(line => ` ${line}`).join('\n'); +} + +function injectFonts(state) { + const tokensPath = join(projectRoot, 'design/states', `${state}.json`); + const htmlPath = join(projectRoot, 'index.html'); + + console.log(`Reading tokens: ${tokensPath}`); + const tokensJson = JSON.parse(readFileSync(tokensPath, 'utf8')); + + const fonts = extractFonts(tokensJson, state); + console.log(`✅ Found ${fonts.size} custom font(s): ${Array.from(fonts).join(', ')}`); + + const fontLinks = generateFontLinks(fonts); + + let html = readFileSync(htmlPath, 'utf8'); + + const placeholder = ''; + const startMarker = ''; + const endMarker = ''; + + if (html.includes(placeholder)) { + html = html.replace(placeholder, `${startMarker}\n${fontLinks}\n ${endMarker}`); + } else if (html.includes(startMarker) && html.includes(endMarker)) { + const regex = new RegExp(`${startMarker}[\\s\\S]*?${endMarker}`, 'g'); + html = html.replace(regex, `${startMarker}\n${fontLinks}\n ${endMarker}`); + } else { + throw new Error(`Neither placeholder nor markers found in index.html. Expected: ${placeholder} or ${startMarker}...${endMarker}`); + } + + writeFileSync(htmlPath, html, 'utf8'); + console.log(`✅ Injected fonts for ${state.toUpperCase()} into index.html\n`); +} + +function main() { + try { + const args = process.argv.slice(2); + const state = (args[0] || process.env.STATE || 'dc').toLowerCase(); + + injectFonts(state); + } catch (error) { + console.error('❌ Font injection failed:', error.message); + process.exit(1); + } +} + +main(); diff --git a/src/SEBT.Portal.Web/scripts/tokens-to-scss.js b/src/SEBT.Portal.Web/scripts/tokens-to-scss.js new file mode 100644 index 0000000..da99c83 --- /dev/null +++ b/src/SEBT.Portal.Web/scripts/tokens-to-scss.js @@ -0,0 +1,264 @@ +#!/usr/bin/env node +/** + * Convert Figma Tokens Studio JSON to USWDS SCSS Variables + * + * Transforms state-specific design tokens from Figma into USWDS theme variables. + * Auto-runs during build via 'prebuild' script - generates only the needed state. + * + * Usage: + * node tokens-to-scss.js # Defaults to DC + * node tokens-to-scss.js ca # California state + * STATE=tx node tokens-to-scss.js # Environment variable + * + * Features: + * - Smart caching: Only regenerates if source JSON changed + * - USWDS compatible: Generates proper theme variable format + * - Build integration: Auto-runs during pnpm build + * + * Workflow: + * 1. Read design/states/{state}.json (source of truth) + * 2. Check if transformation needed (timestamp-based) + * 3. Extract 'theme' object (USWDS has 'system' tokens built-in) + * 4. Convert to SCSS variables with USWDS naming conventions + * 5. Output to sass/_uswds-theme-{state}.scss (gitignored) + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = join(__dirname, '..'); + +const USWDS_PREFIXES = new Set([ + 'color', 'font', 'link', 'focus', 'button', + 'global', 'style', 'text' +]); + +const CUSTOM_TYPEFACES = { + urbanist: { + displayName: 'Urbanist', + capHeight: 364 + }, + 'public-sans': { + displayName: 'Public Sans', + capHeight: 364 + } +}; + +function getStatePaths(state) { + const statesDir = join(projectRoot, 'design/states'); + const sassDir = join(projectRoot, 'sass'); + + return { + input: join(statesDir, `${state}.json`), + output: join(sassDir, `_uswds-theme-${state}.scss`), + sassDir + }; +} + +function shouldInjectColorPrefix(tokenName, tokenType) { + if (!tokenName.startsWith('theme-') || tokenType !== 'color') { + return false; + } + + for (const prefix of USWDS_PREFIXES) { + if (tokenName.includes(`-${prefix}-`)) { + return false; + } + } + + return true; +} + +function normalizeFontValue(value, varName) { + if (!varName.includes('font-type') && !varName.includes('font-role')) { + return value; + } + + for (const typeface of Object.keys(CUSTOM_TYPEFACES)) { + const capitalized = `'${typeface.charAt(0).toUpperCase() + typeface.slice(1)}'`; + if (value === capitalized) { + return `'${typeface}'`; + } + } + + return value; +} + +function generateTypefaceTokens() { + if (Object.keys(CUSTOM_TYPEFACES).length === 0) { + return ''; + } + + const tokens = Object.entries(CUSTOM_TYPEFACES) + .map(([key, { displayName, capHeight }]) => + ` ${key}: (\n display-name: "${displayName}",\n cap-height: ${capHeight}px\n )` + ) + .join(',\n'); + + return `\n// Custom typeface tokens for fonts not included in USWDS by default\n$theme-typeface-tokens: (\n${tokens}\n);\n`; +} + +function toScssVariableName(name) { + return name.startsWith('$') ? name : `$${name}`; +} + +function toScssValue(value) { + if (typeof value === 'string') { + if (value.startsWith('{') && value.endsWith('}')) { + const tokenName = value.slice(1, -1); + return `'${tokenName}'`; + } + const cleaned = value.replace(/'/g, ''); + return `'${cleaned}'`; + } + return value; +} + +function processThemeObject(obj, prefix = '') { + const variables = []; + + for (const [key, value] of Object.entries(obj)) { + if (value && typeof value === 'object' && '$value' in value) { + let tokenName = prefix ? `${prefix}-${key}` : key; + + if (shouldInjectColorPrefix(tokenName, value.$type)) { + tokenName = tokenName.replace('theme-', 'theme-color-'); + } + + const scssName = toScssVariableName(tokenName); + const scssValue = toScssValue(value.$value); + + variables.push({ + name: scssName, + value: scssValue, + description: value.$description || '' + }); + } else if (value && typeof value === 'object' && !('$value' in value)) { + const nestedPrefix = prefix ? `${prefix}-${key}` : key; + variables.push(...processThemeObject(value, nestedPrefix)); + } + } + + return variables; +} + +function needsRegeneration(inputPath, outputPath) { + if (!existsSync(outputPath)) { + return true; + } + + const inputStats = statSync(inputPath); + const outputStats = statSync(outputPath); + + return inputStats.mtimeMs > outputStats.mtimeMs; +} + +function formatScssVariable({ name, value, description }) { + value = normalizeFontValue(value, name); + const comment = description + ? ` // ${description.split('\n')[0].trim()}` + : ''; + return `${name}: ${value};${comment}`; +} + +function processState(state) { + const paths = getStatePaths(state); + + if (!existsSync(paths.input)) { + throw new Error(`State token file not found: ${paths.input}`); + } + + if (!needsRegeneration(paths.input, paths.output)) { + console.log(`⚡ Tokens unchanged for ${state.toUpperCase()}\n ${paths.input} → ${paths.output}`); + return { state, cached: true, success: true }; + } + + console.log(`Reading: ${paths.input}`); + const stateJson = JSON.parse(readFileSync(paths.input, 'utf8')); + + if (!stateJson.theme) { + throw new Error(`No "theme" object found in ${state}.json`); + } + + console.log(`✅ Found theme object for ${state.toUpperCase()}`); + + const variables = processThemeObject(stateJson.theme); + console.log(`✅ Extracted ${variables.length} theme variables`); + + const scssContent = `// _uswds-theme-${state}.scss +// Auto-generated USWDS theme variables from Figma Tokens Studio +// Source: design/states/${state}.json (theme object only) +// DO NOT EDIT DIRECTLY - This file is regenerated from design tokens +// +// Usage: Import this file before importing USWDS to customize the theme +// @import 'uswds-theme-${state}'; +// @use 'uswds'; +${generateTypefaceTokens()} +` + variables.map(formatScssVariable).join('\n') + '\n'; + + mkdirSync(paths.sassDir, { recursive: true }); + writeFileSync(paths.output, scssContent, 'utf8'); + + console.log(`✅ Generated ${variables.length} variables for ${state.toUpperCase()}`); + console.log(` ${paths.output}`); + + const entryPath = join(paths.sassDir, '_uswds-theme-entry.scss'); + const entryContent = `/* +---------------------------------------- +USWDS Theme Entry Point (Auto-Generated) +---------------------------------------- +This file is auto-generated during build to load the correct state theme. +DO NOT EDIT DIRECTLY - Regenerated from design tokens on every build. + +Current state: ${state.toUpperCase()} +Source: design/states/${state}.json +*/ + +// Load ${state.toUpperCase()} theme variables +@use "uswds-theme-${state}"; + +// Load USWDS core with theme suppression +// Path is relative to loadPaths in vite.config.ts +@use "uswds-core" with ( + // Suppress release notes + $theme-show-notifications: false +); +`; + + if (existsSync(entryPath)) { + const currentEntry = readFileSync(entryPath, 'utf8'); + if (currentEntry.includes(`Current state: ${state.toUpperCase()}`)) { + console.log(`⚡ Entry file unchanged for ${state.toUpperCase()}`); + console.log(` ${entryPath}\n`); + return { state, cached: false, success: true, count: variables.length }; + } + } + + writeFileSync(entryPath, entryContent, 'utf8'); + console.log(`✅ Generated theme entry for ${state.toUpperCase()}`); + console.log(` ${entryPath}\n`); + + return { state, cached: false, success: true, count: variables.length }; +} + +function main() { + try { + const args = process.argv.slice(2); + const state = (args[0] || process.env.STATE || 'dc').toLowerCase(); + + try { + processState(state); + } catch (error) { + console.error(`❌ Error: ${error.message}`); + process.exit(1); + } + + } catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/src/SEBT.Portal.Web/scripts/validate-tokens.js b/src/SEBT.Portal.Web/scripts/validate-tokens.js new file mode 100755 index 0000000..7e6c351 --- /dev/null +++ b/src/SEBT.Portal.Web/scripts/validate-tokens.js @@ -0,0 +1,317 @@ +#!/usr/bin/env node +/** + * Design Token Validation Script + * + * Validates design token structure to catch errors early in the build pipeline. + * Ensures token files conform to expected format and USWDS requirements. + * + * Usage: + * node validate-tokens.js # Validates DC (default) + * node validate-tokens.js ca # Validates California + * STATE=tx node validate-tokens.js # Environment variable + * + * Validation Rules: + * 1. Required 'theme' object exists + * 2. Color tokens use valid USWDS color references + * 3. Font tokens reference valid typefaces + * 4. All token values are properly formatted + * 5. No circular references + * + * Exit Codes: + * 0 - All validations passed + * 1 - Validation errors found + */ + +import { readFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = join(__dirname, '..'); + +// USWDS color palette (subset for validation) +const USWDS_COLORS = new Set([ + 'red', 'red-warm', 'red-cool', 'red-vivid', + 'orange', 'orange-warm', 'orange-vivid', + 'gold', 'yellow', + 'green', 'green-warm', 'green-cool', 'green-vivid', + 'mint', 'mint-cool', 'mint-vivid', + 'cyan', 'cyan-vivid', + 'blue', 'blue-warm', 'blue-cool', 'blue-vivid', + 'indigo', 'indigo-warm', 'indigo-cool', 'indigo-vivid', + 'violet', 'violet-warm', 'violet-vivid', + 'magenta', 'magenta-vivid', + 'gray', 'gray-warm', 'gray-cool', + 'black', 'white' +]); + +const USWDS_COLOR_GRADES = new Set([ + '5', '10', '20', '30', '40', '50', '60', '70', '80', '90', + '5v', '10v', '20v', '30v', '40v', '50v', '60v', '70v', '80v' +]); + +// Valid custom typefaces (can be extended) +const VALID_TYPEFACES = new Set([ + 'urbanist', + 'public-sans', + 'source-sans-pro', + 'merriweather', + 'roboto-mono' +]); + +class ValidationError { + constructor(path, message, severity = 'error') { + this.path = path; + this.message = message; + this.severity = severity; + } + + toString() { + const icon = this.severity === 'error' ? '❌' : '⚠️'; + return `${icon} ${this.path}: ${this.message}`; + } +} + +class TokenValidator { + constructor(state) { + this.state = state; + this.errors = []; + this.warnings = []; + this.tokenRefs = new Set(); + this.resolvedRefs = new Set(); + } + + // Validate color token reference + validateColorReference(value, path) { + // Extract color name from reference like {mint-cool-60v} + const refMatch = value.match(/^\{([^}]+)\}$/); + if (!refMatch) { + // Direct color values are also valid (hex, rgb, etc.) + if (!/^(#[0-9a-fA-F]{3,8}|rgb|hsl)/.test(value)) { + this.warnings.push( + new ValidationError( + path, + `Color value "${value}" is not a USWDS reference or standard color format`, + 'warning' + ) + ); + } + return; + } + + const colorRef = refMatch[1]; + this.tokenRefs.add(colorRef); + + // Parse USWDS color reference: color-family-grade + const parts = colorRef.split('-'); + if (parts.length < 2) { + this.errors.push( + new ValidationError( + path, + `Invalid color reference format: "${colorRef}" (expected: family-grade or family-variant-grade)` + ) + ); + return; + } + + const grade = parts[parts.length - 1]; + const family = parts.slice(0, -1).join('-'); + + if (!USWDS_COLORS.has(family)) { + this.errors.push( + new ValidationError( + path, + `Unknown USWDS color family: "${family}" in reference "${colorRef}"` + ) + ); + } + + if (!USWDS_COLOR_GRADES.has(grade)) { + this.errors.push( + new ValidationError( + path, + `Invalid USWDS color grade: "${grade}" in reference "${colorRef}" (valid: 5-90, 5v-80v)` + ) + ); + } + } + + // Validate font token reference + validateFontReference(value, path) { + // Remove quotes and lowercase + const font = value.replace(/['"]/g, '').toLowerCase(); + + // Check if it's a custom typeface + if (!VALID_TYPEFACES.has(font)) { + this.warnings.push( + new ValidationError( + path, + `Font "${font}" not in validated typeface list. Ensure it's properly configured.`, + 'warning' + ) + ); + } + } + + // Validate a single token + validateToken(token, path) { + if (!token || typeof token !== 'object') { + this.errors.push( + new ValidationError(path, 'Token must be an object') + ); + return; + } + + if (!('$value' in token)) { + // Not a leaf token, skip + return; + } + + const { $value, $type, $description } = token; + + // Validate based on type + if ($type === 'color') { + this.validateColorReference($value, path); + } else if ($type === 'fontFamilies' || path.includes('font-type')) { + this.validateFontReference($value, path); + } else if ($type === 'dimension' || $type === 'sizing') { + // Validate dimension format + if (typeof $value !== 'string' && typeof $value !== 'number') { + this.errors.push( + new ValidationError( + path, + `Dimension value must be string or number, got: ${typeof $value}` + ) + ); + } + } + + // Warn if no description for important tokens + if (!$description && (path.includes('primary') || path.includes('secondary'))) { + this.warnings.push( + new ValidationError( + path, + 'Important token missing $description', + 'warning' + ) + ); + } + } + + // Recursively validate theme object + validateThemeObject(obj, prefix = 'theme') { + for (const [key, value] of Object.entries(obj)) { + const path = `${prefix}.${key}`; + + if (value && typeof value === 'object') { + if ('$value' in value) { + // This is a token + this.validateToken(value, path); + } else { + // Nested object + this.validateThemeObject(value, path); + } + } + } + } + + // Main validation entry point + validate(tokenData) { + const startTime = performance.now(); + + // 1. Required structure + if (!tokenData.theme) { + this.errors.push( + new ValidationError( + 'root', + 'Missing required "theme" object. USWDS requires theme tokens.' + ) + ); + return this.getResults(startTime); + } + + // 2. Validate theme structure + this.validateThemeObject(tokenData.theme); + + // 3. Check for potential circular references + const unresolvedRefs = [...this.tokenRefs].filter( + ref => !this.resolvedRefs.has(ref) + ); + if (unresolvedRefs.length > 0) { + this.warnings.push( + new ValidationError( + 'theme', + `Unresolved token references: ${unresolvedRefs.join(', ')}. Ensure these are defined in USWDS or system tokens.`, + 'warning' + ) + ); + } + + return this.getResults(startTime); + } + + getResults(startTime) { + const elapsedTime = (performance.now() - startTime).toFixed(2); + return { + state: this.state, + errors: this.errors, + warnings: this.warnings, + tokenCount: this.tokenRefs.size, + validationTime: parseFloat(elapsedTime), + success: this.errors.length === 0 + }; + } +} + +function validateState(state) { + const tokenPath = join(projectRoot, `design/states/${state}.json`); + + if (!existsSync(tokenPath)) { + console.error(`❌ Token file not found: ${tokenPath}`); + return { success: false }; + } + + console.log(`Validating: ${tokenPath}`); + + try { + const tokenData = JSON.parse(readFileSync(tokenPath, 'utf8')); + const validator = new TokenValidator(state); + const results = validator.validate(tokenData); + + // Print results + console.log(`\n📊 Validation Results (${state.toUpperCase()}):`); + console.log(` Validation time: ${results.validationTime}ms`); + console.log(` Token references: ${results.tokenCount}`); + + if (results.errors.length > 0) { + console.log(`\n❌ Errors (${results.errors.length}):`); + results.errors.forEach(err => console.log(` ${err.toString()}`)); + } + + if (results.warnings.length > 0) { + console.log(`\n⚠️ Warnings (${results.warnings.length}):`); + results.warnings.forEach(warn => console.log(` ${warn.toString()}`)); + } + + if (results.errors.length === 0) { + console.log(`\n✅ Validation passed for ${state.toUpperCase()}`); + } else { + console.log(`\n❌ Validation failed for ${state.toUpperCase()}`); + } + + return results; + } catch (error) { + console.error(`❌ Failed to parse token file: ${error.message}`); + return { success: false }; + } +} + +function main() { + const args = process.argv.slice(2); + const state = (args[0] || process.env.STATE || 'dc').toLowerCase(); + + const results = validateState(state); + process.exit(results.success ? 0 : 1); +} + +main(); diff --git a/src/SEBT.Portal.Web/src/main.ts b/src/SEBT.Portal.Web/src/main.ts new file mode 100644 index 0000000..2bd695e --- /dev/null +++ b/src/SEBT.Portal.Web/src/main.ts @@ -0,0 +1,16 @@ +/** + * SEBT Portal - Main Entry Point + * + * This is the main TypeScript entry point for the SEBT Self-Service Portal. + * It initializes the application and imports USWDS styles. + */ + +import './styles.scss'; + +// Mark CSS as loaded to prevent FOUC +document.body.classList.add('css-loaded'); + +// Development-only logging +if (import.meta.env.DEV) { + console.log('🎨 SEBT Portal initialized with USWDS'); +} diff --git a/src/SEBT.Portal.Web/src/styles.scss b/src/SEBT.Portal.Web/src/styles.scss new file mode 100644 index 0000000..47b045b --- /dev/null +++ b/src/SEBT.Portal.Web/src/styles.scss @@ -0,0 +1,16 @@ +/** + * Main Styles Entry Point (USWDS Custom Compiler) + * + * This file is imported by src/main.ts and compiled by Vite. + * Following official USWDS custom compiler pattern: + * https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/ + * + * Structure: + * 1. Load project's USWDS settings (_uswds-theme.scss) + * 2. Load USWDS source code + * 3. Load project's custom styles + */ + +@forward '../sass/uswds-theme'; +@forward 'uswds'; +@forward '../sass/uswds-theme-custom-styles'; diff --git a/src/SEBT.Portal.Web/src/types/design-tokens.d.ts b/src/SEBT.Portal.Web/src/types/design-tokens.d.ts new file mode 100644 index 0000000..c6769cf --- /dev/null +++ b/src/SEBT.Portal.Web/src/types/design-tokens.d.ts @@ -0,0 +1,170 @@ +// design-tokens.d.ts +// Auto-generated TypeScript definitions for design tokens +// Source: design/states/dc.json +// DO NOT EDIT DIRECTLY - Regenerated from design tokens on build + +/** + * Design tokens for DC state + * Auto-generated from design/states/dc.json + * DO NOT EDIT DIRECTLY - Regenerated from design tokens + */ +export interface DCTheme { + /** Token: theme-button-border-radius */ + buttonBorderRadius: string; + + /** Family tokens designate the overall color set and do not map to explicit values. Therefore, they are blank. */ + colorPrimaryFamily: string; + + /** Token: theme-focus-color */ + focusColor: string; + + /** Token: theme-font-role-alt */ + fontRoleAlt: string; + + /** Token: theme-font-role-heading */ + fontRoleHeading: string; + + /** In your HTML files, add a reference to the JavaScript and/or CSS files provided by the font hosting service. https://fonts.google.com/specimen/Urbanist + +In your settings configuration, tell $theme-typeface-tokens to create a new typeface token. In the code example, we are creating a new typeface token: + + $theme-typeface-tokens: ( + "Urbanist": ( + "display-name": "Urbanist", + "cap-height": 364px + ), + ), + +Source: https://designsystem.digital.gov/design-tokens/typesetting/font-family/#adding-fonts-to-uswds-2 */ + fontTypeSans: string; + + /** In your HTML files, add a reference to the JavaScript and/or CSS files provided by the font hosting service. https://fonts.google.com/specimen/Urbanist + +In your settings configuration, tell $theme-typeface-tokens to create a new typeface token. In the code example, we are creating a new typeface token: + + $theme-typeface-tokens: ( + "Urbanist": ( + "display-name": "Urbanist", + "cap-height": 364px + ), + ), + +Source: https://designsystem.digital.gov/design-tokens/typesetting/font-family/#adding-fonts-to-uswds-2 */ + fontTypeSerif: string; + + /** Token: theme-global-content-styles */ + globalContentStyles: boolean; + + /** Token: theme-global-link-styles */ + globalLinkStyles: boolean; + + /** Token: theme-global-paragraph-styles */ + globalParagraphStyles: boolean; + + /** Token: theme-link-color */ + linkColor: string; + + /** Token: theme-primary */ + primary: string; + + /** Token: theme-primary-dark */ + primaryDark: string; + + /** Token: theme-primary-darker */ + primaryDarker: string; + + /** Token: theme-primary-light */ + primaryLight: string; + + /** Token: theme-primary-lighter */ + primaryLighter: string; + + /** Token: theme-primary-lightest */ + primaryLightest: string; + + /** Token: theme-primary-vivid */ + primaryVivid: string; + + /** Token: theme-secondary */ + secondary: string; + + /** Token: theme-secondary-dark */ + secondaryDark: string; + + /** Token: theme-secondary-darker */ + secondaryDarker: string; + + /** Family tokens designate the overall color set and do not map to explicit values. Therefore, they are blank. */ + secondaryFamily: string; + + /** Token: theme-secondary-light */ + secondaryLight: string; + + /** Token: theme-secondary-lighter */ + secondaryLighter: string; + + /** Token: theme-secondary-vivid */ + secondaryVivid: string; + + /** Not well documented but necessary to make type size and line heights consistent with .usa-prose */ + styleBodyElement: boolean; + + /** Setting measures to 'none' prevents text from having a fixed width and allows it to flow. */ + textMeasure: string | number; + + /** Setting measures to 'none' prevents text from having a fixed width and allows it to flow. */ + textMeasureNarrow: string | number; + + /** Setting measures to 'none' prevents text from having a fixed width and allows it to flow. */ + textMeasureWide: string | number; + +} + +/** + * Runtime token map for DC state + * Maps property names to original SCSS variable names + */ +export const DC_TOKENS: Record = { + buttonBorderRadius: '$theme-button-border-radius', + colorPrimaryFamily: '$theme-color-primary-family', + focusColor: '$theme-focus-color', + fontRoleAlt: '$theme-font-role-alt', + fontRoleHeading: '$theme-font-role-heading', + fontTypeSans: '$theme-font-type-sans', + fontTypeSerif: '$theme-font-type-serif', + globalContentStyles: '$theme-global-content-styles', + globalLinkStyles: '$theme-global-link-styles', + globalParagraphStyles: '$theme-global-paragraph-styles', + linkColor: '$theme-link-color', + primary: '$theme-primary', + primaryDark: '$theme-primary-dark', + primaryDarker: '$theme-primary-darker', + primaryLight: '$theme-primary-light', + primaryLighter: '$theme-primary-lighter', + primaryLightest: '$theme-primary-lightest', + primaryVivid: '$theme-primary-vivid', + secondary: '$theme-secondary', + secondaryDark: '$theme-secondary-dark', + secondaryDarker: '$theme-secondary-darker', + secondaryFamily: '$theme-secondary-family', + secondaryLight: '$theme-secondary-light', + secondaryLighter: '$theme-secondary-lighter', + secondaryVivid: '$theme-secondary-vivid', + styleBodyElement: '$theme-style-body-element', + textMeasure: '$theme-text-measure', + textMeasureNarrow: '$theme-text-measure-narrow', + textMeasureWide: '$theme-text-measure-wide' +}; + +/** + * Example usage: + * + * import type { DCTheme } from './types/design-tokens'; + * import { DC_TOKENS } from './types/design-tokens'; + * + * // Type-safe access + * const primaryColor: DCTheme['colorPrimaryVivid'] = '#0f6460'; + * + * // Runtime SCSS variable name + * const scssVar = DC_TOKENS.colorPrimaryVivid; // '$theme-color-primary-vivid' + */ diff --git a/src/SEBT.Portal.Web/tsconfig.json b/src/SEBT.Portal.Web/tsconfig.json new file mode 100644 index 0000000..4ba8dd9 --- /dev/null +++ b/src/SEBT.Portal.Web/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/src/SEBT.Portal.Web/vite.config.ts b/src/SEBT.Portal.Web/vite.config.ts new file mode 100644 index 0000000..4f1590d --- /dev/null +++ b/src/SEBT.Portal.Web/vite.config.ts @@ -0,0 +1,194 @@ +import { defineConfig, type Plugin } from 'vite'; +import { resolve } from 'path'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; + +interface PurgeCSSResult { + css: string; + rejectedCss?: string; +} + +interface PurgeCSSClass { + purge: (options: { + content: string[]; + css: { raw: string }[]; + safelist?: { + standard?: RegExp[]; + deep?: RegExp[]; + greedy?: RegExp[]; + }; + defaultExtractor?: (content: string) => string[]; + }) => Promise; +} + +interface PurgeCSSModule { + PurgeCSS: new () => PurgeCSSClass; +} + +interface OutputFile { + type: 'asset' | 'chunk'; + source?: string | Uint8Array; + code?: string; +} + +/** + * Vite Configuration for SEBT Portal Web + * + * USWDS Custom Compiler Setup (following official USWDS docs): + * https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/ + * + * This replaces @uswds/compile with Vite's native capabilities: + * 1. Sass compilation with Lightning CSS (faster than PostCSS/Autoprefixer) + * 2. Static asset copying (fonts, images, JS) + * 3. TypeScript compilation + * 4. Dev server with HMR + * 5. PurgeCSS for production builds (removes unused USWDS CSS) + * + * Note: Figma token transformation runs via prebuild hook in package.json + */ + +/** + * PurgeCSS Plugin for Vite + * Removes unused CSS from USWDS in production builds + * Pattern-based safelisting ensures scalability as app grows + * + * Performance: Reduces bundle ~12% (577KB → 505KB) while maintaining + * zero-maintenance compatibility with all current and future USWDS components + */ +function purgeCSSPlugin(): Plugin { + let PurgeCSSConstructor: PurgeCSSModule['PurgeCSS'] | null = null; + + return { + name: 'vite-plugin-purgecss', + apply: 'build', + async buildStart() { + const module = await import('purgecss/lib/purgecss.js') as PurgeCSSModule; + PurgeCSSConstructor = module.PurgeCSS; + }, + async generateBundle(_options, bundle) { + if (!PurgeCSSConstructor) { + throw new Error('PurgeCSS not loaded'); + } + + const purgecss = new PurgeCSSConstructor(); + + for (const [fileName, file] of Object.entries(bundle)) { + const isAsset = file.type === 'asset'; + const hasSource = 'source' in file; + const isStringSource = hasSource && typeof file.source === 'string'; + + if (fileName.endsWith('.css') && isAsset && isStringSource) { + const cssSource = file.source as string; + + const purged = await purgecss.purge({ + content: [ + resolve(__dirname, 'index.html'), + resolve(__dirname, 'src/**/*.ts'), + resolve(__dirname, 'src/**/*.tsx'), + resolve(__dirname, 'src/**/*.js'), + resolve(__dirname, 'src/**/*.jsx') + ], + css: [{ raw: cssSource }], + safelist: { + standard: [ + /^usa-/, + /^grid-/, + /^tablet:/, + /^desktop:/, + /^mobile:/, + /^mobile-lg:/, + /^widescreen:/, + ], + deep: [ + /usa-/, + ], + greedy: [ + /:hover/, + /:focus/, + /:active/, + /:visited/, + /:before/, + /:after/, + ] + }, + defaultExtractor: (content: string): string[] => { + const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []; + const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || []; + return [...broadMatches, ...innerMatches]; + } + }); + + if (purged && purged[0] && 'source' in file) { + (file as OutputFile).source = purged[0].css; + } + } + } + } + }; +} + +export default defineConfig({ + build: { + outDir: 'dist', + assetsDir: 'assets', + sourcemap: true, + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html') + } + } + }, + + server: { + port: 5173, + strictPort: false, + open: true + }, + + css: { + transformer: 'lightningcss', + lightningcss: { + targets: { + ie: 11, + chrome: 90, + firefox: 88, + safari: 14, + edge: 90 + } + }, + preprocessorOptions: { + scss: { + loadPaths: [ + resolve(__dirname, 'node_modules/@uswds/uswds/packages'), + resolve(__dirname, 'node_modules'), + resolve(__dirname, 'sass') + ] + } + } + }, + + plugins: [ + viteStaticCopy({ + targets: [ + // Fonts: Skipped - Using Google Fonts (Urbanist) from design tokens + // Saves ~5.9MB by not copying unused USWDS font families + // (Public Sans, Roboto Mono, Merriweather, Source Sans Pro) + + // Images: Copy only USWDS icon sprite (essential for USWDS components) + // Saves ~9.9MB by skipping Material Icons (deprecated), USA Icons, hero images + { + src: 'node_modules/@uswds/uswds/dist/img/sprite.svg', + dest: 'img' + }, + + // JavaScript: Copy USWDS initialization script (required for interactive components) + { + src: 'node_modules/@uswds/uswds/dist/js/uswds-init.min.js', + dest: 'js' + } + ] + }), + purgeCSSPlugin() + ], + + publicDir: 'public' +});