diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0b7e1ce..70f86d7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,15 +4,24 @@ "Bash(npm run build:*)", "WebFetch(domain:chakra-ui.com)", "Bash(npm run lint)", + "Bash(git branch:*)", "Bash(git checkout:*)", + "mcp__chakra-ui__get_theme", + "mcp__chakra-ui__customize_theme", + "Bash(npx @chakra-ui/cli typegen:*)", "Bash(npm run dev:*)", - "Bash(dotnet build)", - "WebFetch(domain:www.chakra-ui.com)", + "Bash(dotnet run:*)", + "Bash(cp:*)", + "WebFetch(domain:atlassian.design)", + "WebFetch(domain:atlaskit.atlassian.com)", + "mcp__chakra-ui__get_component_props", + "mcp__puppeteer__puppeteer_screenshot", + "Bash(npm install:*)", "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(gh pr create:*)", - "Bash(gh pr view:*)" + "Bash(mkdir:*)", + "mcp__chakra-ui__list_components", + "mcp__chakra-ui__get_component_example" ], "deny": [] } diff --git a/CLAUDE.md b/CLAUDE.md index 48ad95f..e35bbdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,24 +21,37 @@ ThingConnect.Pulse is a full-stack web application with: ## Development Commands -### Frontend (run from `thingconnect.pulse.client/`) +**IMPORTANT**: For development, run backend and frontend servers separately for optimal development experience. + +### Frontend Development (run from `thingconnect.pulse.client/`) ```bash -npm run dev # Start development server (Vite) +npm run dev # Start Vite development server (https://localhost:49812 or similar) npm run build # Build for production (TypeScript compile + Vite build) npm run lint # Run ESLint npm run format # Format code with Prettier npm run preview # Preview production build ``` -### Backend (run from `ThingConnect.Pulse.Server/`) +### Backend Development (run from `ThingConnect.Pulse.Server/`) ```bash -dotnet run # Start development server -dotnet build # Build the project +dotnet run # Start ASP.NET Core API server (https://localhost:7286) +dotnet build # Build the backend project +dotnet watch # Start with hot reload for development ``` -### Full Stack (run from solution root) +### Development Workflow +1. **Terminal 1**: Start backend server from `ThingConnect.Pulse.Server/` + ```bash + dotnet run + ``` +2. **Terminal 2**: Start frontend server from `thingconnect.pulse.client/` + ```bash + npm run dev + ``` + +### Production Build (run from solution root) ```bash -dotnet run --project ThingConnect.Pulse.Server # Starts both backend and frontend via SPA proxy +dotnet run --project ThingConnect.Pulse.Server # Serves built frontend via SPA proxy ``` ## Development Setup diff --git a/ThingConnect.Pulse.Server/Program.cs b/ThingConnect.Pulse.Server/Program.cs index 0dc93cd..c324263 100644 --- a/ThingConnect.Pulse.Server/Program.cs +++ b/ThingConnect.Pulse.Server/Program.cs @@ -42,18 +42,6 @@ public static void Main(string[] args) }; }); - // CORS - Restrict to specific origins for security - builder.Services.AddCors(options => - { - options.AddPolicy("AllowReactApp", policy => - { - policy.WithOrigins("https://localhost:5173", "http://localhost:5173", "https://localhost:49813", "http://localhost:49813") - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials(); - }); - }); - builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); @@ -61,27 +49,31 @@ public static void Main(string[] args) var app = builder.Build(); - app.UseDefaultFiles(); - app.UseStaticFiles(); - // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } + else + { + // Only serve static files in production + app.UseDefaultFiles(); + app.UseStaticFiles(); + } app.UseHttpsRedirection(); - app.UseCors("AllowReactApp"); - app.UseAuthentication(); app.UseAuthorization(); - app.MapControllers(); - app.MapFallbackToFile("/index.html"); + // Only serve SPA fallback in production + if (!app.Environment.IsDevelopment()) + { + app.MapFallbackToFile("/index.html"); + } app.Run(); } diff --git a/ThingConnect.Pulse.Server/Properties/launchSettings.json b/ThingConnect.Pulse.Server/Properties/launchSettings.json index 4b8b570..6ebb27a 100644 --- a/ThingConnect.Pulse.Server/Properties/launchSettings.json +++ b/ThingConnect.Pulse.Server/Properties/launchSettings.json @@ -5,8 +5,7 @@ "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5099" @@ -16,8 +15,7 @@ "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7286;http://localhost:5099" @@ -27,8 +25,7 @@ "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + "ASPNETCORE_ENVIRONMENT": "Development" } }, "Container (Dockerfile)": { diff --git a/ThingConnect.Pulse.Server/pulse.db b/ThingConnect.Pulse.Server/pulse.db index 524e85f..cc104b9 100644 Binary files a/ThingConnect.Pulse.Server/pulse.db and b/ThingConnect.Pulse.Server/pulse.db differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..22a494f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1913 @@ +{ + "name": "ThingConnect.Pulse", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "puppeteer": "^24.15.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz", + "integrity": "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==", + "dev": true, + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dev": true, + "optional": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, + "node_modules/bare-events": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "dev": true, + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", + "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", + "dev": true, + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.15.0.tgz", + "integrity": "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.15.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.15.0.tgz", + "integrity": "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "optional": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true + }, + "@puppeteer/browsers": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz", + "integrity": "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==", + "dev": true, + "requires": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", + "yargs": "^17.7.2" + } + }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dev": true, + "optional": true, + "requires": { + "undici-types": "~7.10.0" + } + }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, + "bare-events": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "dev": true, + "optional": true + }, + "bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "dev": true, + "optional": true, + "requires": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + } + }, + "bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "dev": true, + "optional": true + }, + "bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "optional": true, + "requires": { + "bare-os": "^3.0.1" + } + }, + "bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "dev": true, + "optional": true, + "requires": { + "streamx": "^2.21.0" + } + }, + "basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chromium-bidi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", + "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", + "dev": true, + "requires": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "requires": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + } + }, + "data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true + }, + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + } + }, + "devtools-protocol": { + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + } + }, + "pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "puppeteer": { + "version": "24.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.15.0.tgz", + "integrity": "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.15.0", + "typed-query-selector": "^2.12.0" + } + }, + "puppeteer-core": { + "version": "24.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.15.0.tgz", + "integrity": "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg==", + "dev": true, + "requires": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "dev": true, + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "dev": true, + "requires": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true + }, + "undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "optional": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cbfa284 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "puppeteer": "^24.15.0" + } +} diff --git a/thingconnect.pulse.client/.claude/settings.local.json b/thingconnect.pulse.client/.claude/settings.local.json new file mode 100644 index 0000000..3ddc117 --- /dev/null +++ b/thingconnect.pulse.client/.claude/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:atlassian.design)", + "Bash(npm run build:*)", + "mcp__chakra-ui__get_theme", + "mcp__chakra-ui__v2_to_v3_code_review", + "mcp__chakra-ui__get_component_example", + "mcp__chakra-ui__get_component_props", + "Bash(npm run dev:*)", + "Bash(npm run lint)", + "Bash(npm run format:*)", + "Bash(git add:*)", + "Bash(git push:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/thingconnect.pulse.client/eslint.config.js b/thingconnect.pulse.client/eslint.config.js index 51afcb7..7b2a0ce 100644 --- a/thingconnect.pulse.client/eslint.config.js +++ b/thingconnect.pulse.client/eslint.config.js @@ -9,26 +9,26 @@ import prettier from 'eslint-config-prettier' import { globalIgnores } from 'eslint/config' export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - // ...tseslint.configs.strictTypeChecked, // optional strict rules - reactX.configs['recommended-typescript'], - reactDom.configs.recommended, - reactRefresh.configs.vite, - prettier, - ...pluginQuery.configs['flat/recommended'], - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + // ...tseslint.configs.strictTypeChecked, // optional strict rules + reactX.configs['recommended-typescript'], + reactDom.configs.recommended, + reactRefresh.configs.vite, + prettier, + ...pluginQuery.configs['flat/recommended'], + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, }, + }, ]) diff --git a/thingconnect.pulse.client/index.html b/thingconnect.pulse.client/index.html index 7fcdf32..fc481dd 100644 --- a/thingconnect.pulse.client/index.html +++ b/thingconnect.pulse.client/index.html @@ -2,9 +2,11 @@ - + - ThingConnect - Pulse + ThingConnect Pulse + +
diff --git a/thingconnect.pulse.client/package-lock.json b/thingconnect.pulse.client/package-lock.json index 437115a..a205997 100644 --- a/thingconnect.pulse.client/package-lock.json +++ b/thingconnect.pulse.client/package-lock.json @@ -12,15 +12,18 @@ "@emotion/react": "^11.14.0", "@tanstack/react-query": "^5.84.1", "axios": "^1.11.0", + "lodash": "^4.17.21", "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "react-router-dom": "^7.8.0" }, "devDependencies": { "@chakra-ui/cli": "^3.24.0", "@eslint/js": "^9.30.1", "@tanstack/eslint-plugin-query": "^5.83.1", + "@types/lodash": "^4.17.20", "@types/node": "^20", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -1913,6 +1916,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3400,6 +3409,14 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4678,6 +4695,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5301,6 +5323,42 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-router": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz", + "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", + "dependencies": { + "react-router": "7.8.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5445,6 +5503,11 @@ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "dev": true }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7451,6 +7514,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true + }, "@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -8700,6 +8769,11 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" + }, "cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -9591,6 +9665,11 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9997,6 +10076,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-router": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", + "requires": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + } + }, + "react-router-dom": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz", + "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", + "requires": { + "react-router": "7.8.0" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -10093,6 +10189,11 @@ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "dev": true }, + "set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/thingconnect.pulse.client/package.json b/thingconnect.pulse.client/package.json index 5124d8d..48401aa 100644 --- a/thingconnect.pulse.client/package.json +++ b/thingconnect.pulse.client/package.json @@ -16,15 +16,18 @@ "@emotion/react": "^11.14.0", "@tanstack/react-query": "^5.84.1", "axios": "^1.11.0", + "lodash": "^4.17.21", "next-themes": "^0.4.6", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "react-router-dom": "^7.8.0" }, "devDependencies": { "@chakra-ui/cli": "^3.24.0", "@eslint/js": "^9.30.1", "@tanstack/eslint-plugin-query": "^5.83.1", + "@types/lodash": "^4.17.20", "@types/node": "^20", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", diff --git a/thingconnect.pulse.client/public/thingconnect-icon.svg b/thingconnect.pulse.client/public/thingconnect-icon.svg new file mode 100644 index 0000000..18a3c6c --- /dev/null +++ b/thingconnect.pulse.client/public/thingconnect-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/thingconnect.pulse.client/src/App.tsx b/thingconnect.pulse.client/src/App.tsx index 3b885df..0c48a3d 100644 --- a/thingconnect.pulse.client/src/App.tsx +++ b/thingconnect.pulse.client/src/App.tsx @@ -1,3 +1,87 @@ +<<<<<<< HEAD +import { useState, useEffect } from 'react' +import { Routes, Route, Navigate } from 'react-router-dom' +import { AuthScreen } from './components/auth/AuthScreen' +import { AppLayout } from './components/layout/AppLayout' +import { authService } from './services/authService' +import { Box, Text } from '@chakra-ui/react' +import DashboardPage from './pages/DashboardPage' +import ThemeShowcasePage from './pages/ThemeShowcasePage' +import ProtectedRoute from './components/routing/ProtectedRoute' + +function App() { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const checkAuth = () => { + try { + const isLoggedIn = authService.isAuthenticated() + setIsAuthenticated(isLoggedIn) + } catch (error) { + console.error('Auth check failed:', error) + setIsAuthenticated(false) + } finally { + setIsLoading(false) + } + } + + void checkAuth() + }, []) + + const handleAuthSuccess = () => { + setIsAuthenticated(true) + } + + if (isLoading) { + return ( + + + Loading... + + + ) + } + + return ( + + + + + ) : ( + + ) + } + /> + + + + } + /> + + + + } + /> + : + } + /> + + ) +======= import { useState, useEffect, useCallback } from 'react'; import { Box, Spinner, Center, Flex, Text, HStack, Button, Avatar } from '@chakra-ui/react'; import { AdminSetupLanding } from './components/setup/AdminSetupLanding'; @@ -134,6 +218,7 @@ function App() { ); +>>>>>>> 8970e5ce26ad4454c134a92623c57322e7f1d8b1 } export default App; diff --git a/thingconnect.pulse.client/src/assets/ThingConnect Icon.png b/thingconnect.pulse.client/src/assets/ThingConnect Icon.png new file mode 100644 index 0000000..f60cbf1 Binary files /dev/null and b/thingconnect.pulse.client/src/assets/ThingConnect Icon.png differ diff --git a/thingconnect.pulse.client/src/assets/ThingConnect Icon.svg b/thingconnect.pulse.client/src/assets/ThingConnect Icon.svg new file mode 100644 index 0000000..18a3c6c --- /dev/null +++ b/thingconnect.pulse.client/src/assets/ThingConnect Icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/thingconnect.pulse.client/src/assets/ThingConnect Logo.png b/thingconnect.pulse.client/src/assets/ThingConnect Logo.png new file mode 100644 index 0000000..e03cb1a Binary files /dev/null and b/thingconnect.pulse.client/src/assets/ThingConnect Logo.png differ diff --git a/thingconnect.pulse.client/src/assets/ThingConnect Logo.svg b/thingconnect.pulse.client/src/assets/ThingConnect Logo.svg new file mode 100644 index 0000000..c1393c9 --- /dev/null +++ b/thingconnect.pulse.client/src/assets/ThingConnect Logo.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/thingconnect.pulse.client/src/components/auth/AuthScreen.tsx b/thingconnect.pulse.client/src/components/auth/AuthScreen.tsx index f8dd103..8c45088 100644 --- a/thingconnect.pulse.client/src/components/auth/AuthScreen.tsx +++ b/thingconnect.pulse.client/src/components/auth/AuthScreen.tsx @@ -1,48 +1,44 @@ -import { useState } from 'react'; -import { LoginForm } from './LoginForm'; -import { RegisterForm } from './RegisterForm'; -import { authService } from '../../services/authService'; +import { useState } from 'react' +import { LoginForm } from './LoginForm' +import { RegisterForm } from './RegisterForm' +import { authService } from '../../services/authService' interface AuthScreenProps { - onAuthSuccess: () => void; + onAuthSuccess: () => void } export const AuthScreen = ({ onAuthSuccess }: AuthScreenProps) => { - const [isLogin, setIsLogin] = useState(true); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const [isLogin, setIsLogin] = useState(true) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) const handleLogin = async (username: string, password: string) => { - setIsLoading(true); - setError(null); - + setIsLoading(true) + setError(null) + try { - await authService.login(username, password); - onAuthSuccess(); + await authService.login(username, password) + onAuthSuccess() } catch (err: unknown) { - setError( - (err as { response?: { data?: string } })?.response?.data || 'Login failed' - ); + setError((err as { response?: { data?: string } })?.response?.data || 'Login failed') } finally { - setIsLoading(false); + setIsLoading(false) } - }; + } const handleRegister = async (username: string, email: string, password: string) => { - setIsLoading(true); - setError(null); - + setIsLoading(true) + setError(null) + try { - await authService.register(username, email, password); - onAuthSuccess(); + await authService.register(username, email, password) + onAuthSuccess() } catch (err: unknown) { - setError( - (err as { response?: { data?: string } })?.response?.data || 'Registration failed' - ); + setError((err as { response?: { data?: string } })?.response?.data || 'Registration failed') } finally { - setIsLoading(false); + setIsLoading(false) } - }; + } return ( <> @@ -50,8 +46,8 @@ export const AuthScreen = ({ onAuthSuccess }: AuthScreenProps) => { { - setIsLogin(false); - setError(null); + setIsLogin(false) + setError(null) }} isLoading={isLoading} error={error} @@ -60,13 +56,13 @@ export const AuthScreen = ({ onAuthSuccess }: AuthScreenProps) => { { - setIsLogin(true); - setError(null); + setIsLogin(true) + setError(null) }} isLoading={isLoading} error={error} /> )} - ); -}; \ No newline at end of file + ) +} diff --git a/thingconnect.pulse.client/src/components/auth/LoginForm.tsx b/thingconnect.pulse.client/src/components/auth/LoginForm.tsx index d32ff1b..9ff73a6 100644 --- a/thingconnect.pulse.client/src/components/auth/LoginForm.tsx +++ b/thingconnect.pulse.client/src/components/auth/LoginForm.tsx @@ -1,78 +1,120 @@ -import { useState } from 'react'; -import { - Box, - Button, - Input, - Stack, - Text, -} from '@chakra-ui/react'; +import { useState } from 'react' +import { Box, Button, Input, Stack, Text, VStack } from '@chakra-ui/react' +import { Logo } from '../ui/logo' interface LoginFormProps { - onLogin: (username: string, password: string) => Promise; - onSwitchToRegister: () => void; - isLoading: boolean; - error: string | null; + onLogin: (username: string, password: string) => Promise + onSwitchToRegister: () => void + isLoading: boolean + error: string | null } export const LoginForm = ({ onLogin, onSwitchToRegister, isLoading, error }: LoginFormProps) => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - void onLogin(username, password); - }; + e.preventDefault() + void onLogin(username, password) + } return ( - - - Login - - {error && ( - - {error} - - )} - - - Username * - setUsername(e.target.value)} - placeholder="Enter your username" - required - /> + + + + + + Login to Pulse + - - Password * - setPassword(e.target.value)} - placeholder="Enter your password" - required - /> - + + {error && ( + + {error} + + )} + + + + Username * + + setUsername(e.target.value)} + placeholder="Enter your username" + required + bg="color.background.default" + borderColor="border" + borderRadius="base" + p="100" + _focus={{ + borderColor: 'brand.solid', + boxShadow: '0 0 0 2px var(--chakra-colors-brand-200)', + }} + /> + - + + + Password * + + setPassword(e.target.value)} + placeholder="Enter your password" + required + bg="color.background.default" + borderColor="border" + borderRadius="base" + p="100" + _focus={{ + borderColor: 'brand.solid', + boxShadow: '0 0 0 2px var(--chakra-colors-brand-200)', + }} + /> + - - Don't have an account?{' '} - - - + + + Don't have an account?{' '} + + + + - ); -}; \ No newline at end of file + ) +} diff --git a/thingconnect.pulse.client/src/components/auth/RegisterForm.tsx b/thingconnect.pulse.client/src/components/auth/RegisterForm.tsx index cadedd1..c854493 100644 --- a/thingconnect.pulse.client/src/components/auth/RegisterForm.tsx +++ b/thingconnect.pulse.client/src/components/auth/RegisterForm.tsx @@ -1,115 +1,162 @@ -import { useState } from 'react'; -import { - Box, - Button, - Input, - Stack, - Text, -} from '@chakra-ui/react'; +import { useState } from 'react' +import { Box, Button, Input, Stack, Text, VStack } from '@chakra-ui/react' +import { Logo } from '../ui/logo' interface RegisterFormProps { - onRegister: (username: string, email: string, password: string) => Promise; - onSwitchToLogin: () => void; - isLoading: boolean; - error: string | null; + onRegister: (username: string, email: string, password: string) => Promise + onSwitchToLogin: () => void + isLoading: boolean + error: string | null } -export const RegisterForm = ({ onRegister, onSwitchToLogin, isLoading, error }: RegisterFormProps) => { - const [username, setUsername] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); +export const RegisterForm = ({ + onRegister, + onSwitchToLogin, + isLoading, + error, +}: RegisterFormProps) => { + const [username, setUsername] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - + e.preventDefault() + if (password !== confirmPassword) { - return; + return } - - void onRegister(username, email, password); - }; - const passwordMismatch = password !== confirmPassword && confirmPassword !== ''; + void onRegister(username, email, password) + } - return ( - - - Register - - {error && ( - - {error} - - )} + const passwordMismatch = password !== confirmPassword && confirmPassword !== '' - - Username * - setUsername(e.target.value)} - placeholder="Choose a username" - required - /> + return ( + + + + + + Join Pulse + - - Email * - setEmail(e.target.value)} - placeholder="Enter your email" - required - /> - + + {error && ( + + {error} + + )} - - Password * - setPassword(e.target.value)} - placeholder="Create a password" - required - /> - + + + Username * + + setUsername(e.target.value)} + placeholder="Choose a username" + required + bg="bg.panel" + borderColor="border" + _focus={{ + borderColor: 'brand.solid', + boxShadow: '0 0 0 1px var(--chakra-colors-brand-solid)', + }} + /> + - - Confirm Password * - setConfirmPassword(e.target.value)} - placeholder="Confirm your password" - required - /> - {passwordMismatch && ( - - Passwords do not match + + + Email * - )} - + setEmail(e.target.value)} + placeholder="Enter your email" + required + bg="bg.panel" + borderColor="border" + _focus={{ + borderColor: 'brand.solid', + boxShadow: '0 0 0 1px var(--chakra-colors-brand-solid)', + }} + /> + - + + + Password * + + setPassword(e.target.value)} + placeholder="Create a password" + required + bg="bg.panel" + borderColor="border" + _focus={{ + borderColor: 'brand.solid', + boxShadow: '0 0 0 1px var(--chakra-colors-brand-solid)', + }} + /> + - - Already have an account?{' '} - - - + + + Already have an account?{' '} + + + + - ); -}; \ No newline at end of file + ) +} diff --git a/thingconnect.pulse.client/src/components/layout/AppLayout.tsx b/thingconnect.pulse.client/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..1f104db --- /dev/null +++ b/thingconnect.pulse.client/src/components/layout/AppLayout.tsx @@ -0,0 +1,41 @@ +import { Box, Flex, HStack } from '@chakra-ui/react' +import { ColorModeButton } from '../ui/color-mode' +import { Logo } from '../ui/logo' +import type { ReactNode } from 'react' + +interface AppLayoutProps { + children: ReactNode + showHeader?: boolean +} + +export function AppLayout({ children, showHeader = true }: AppLayoutProps) { + return ( + + {showHeader && ( + + + + {/* Show full logo on larger screens, icon on mobile */} + + + + + + + + .Pulse + + + + + + + + )} + + + {children} + + + ) +} diff --git a/thingconnect.pulse.client/src/components/routing/ProtectedRoute.tsx b/thingconnect.pulse.client/src/components/routing/ProtectedRoute.tsx new file mode 100644 index 0000000..7fc8c37 --- /dev/null +++ b/thingconnect.pulse.client/src/components/routing/ProtectedRoute.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react' +import { Navigate } from 'react-router-dom' +import { AppLayout } from '@/components/layout/AppLayout' + +interface ProtectedRouteProps { + isAuthenticated: boolean + children: ReactNode +} + +function ProtectedRoute({ isAuthenticated, children }: ProtectedRouteProps) { + if (!isAuthenticated) { + return + } + + return {children} +} + +export default ProtectedRoute diff --git a/thingconnect.pulse.client/src/components/ui/Button.tsx b/thingconnect.pulse.client/src/components/ui/Button.tsx new file mode 100644 index 0000000..4d055fb --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/Button.tsx @@ -0,0 +1,28 @@ +import { Button as ChakraButton, ButtonProps as ChakraButtonProps } from '@chakra-ui/react' + +export interface ButtonProps extends ChakraButtonProps { + variant?: 'primary' | 'secondary' | 'danger' +} + +export function Button({ variant = 'primary', children, ...props }: ButtonProps) { + const variantProps = { + primary: { + colorPalette: 'blue', + variant: 'solid', + }, + secondary: { + colorPalette: 'gray', + variant: 'outline', + }, + danger: { + colorPalette: 'red', + variant: 'solid', + }, + }[variant] + + return ( + + {children} + + ) +} diff --git a/thingconnect.pulse.client/src/components/ui/CLAUDE.md b/thingconnect.pulse.client/src/components/ui/CLAUDE.md new file mode 100644 index 0000000..a19697a --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/CLAUDE.md @@ -0,0 +1,196 @@ +# Chakra UI Theme & Customization Guide + +This document provides comprehensive guidance for working with Chakra UI theming and customization in the ThingConnect.Pulse project. + +## Theme Structure + +### Tokens (Core Theme Tokens) + +Non-semantic tokens that define the foundational design values: + +- Colors: `gray.50`, `red.500`, `blue.100`, etc. +- Spacing, fonts, shadows, radii, and other design primitives + +### Semantic Tokens + +Context-aware tokens that automatically work for light and dark mode. **Always prefer semantic tokens over hard-coded color values**. + +Available semantic tokens: + +- **Background**: `bg`, `bg.subtle`, `bg.muted`, `bg.emphasized`, `bg.inverted`, `bg.panel` +- **Foreground**: `fg`, `fg.muted`, `fg.subtle`, `fg.inverted` +- **Borders**: `border`, `border.muted`, `border.subtle`, `border.emphasized` +- **Status Colors**: `*.error`, `*.warning`, `*.success`, `*.info` (where \* = bg, fg, border) +- **Color Palettes**: Each color has semantic variants: `contrast`, `fg`, `subtle`, `muted`, `emphasized`, `solid`, `focusRing` + +### Text Styles + +Consistent text styling that combines font size, font weight, and line height: + +- Sizes: `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl` +- Special: `label` for form labels + +### Layer Styles + +Consistent container styling for common UI patterns: + +- **Fill styles**: `fill.muted`, `fill.subtle`, `fill.surface`, `fill.solid` +- **Outline styles**: `outline.subtle`, `outline.solid` +- **Indicators**: `indicator.bottom`, `indicator.top`, `indicator.start`, `indicator.end` +- **States**: `disabled`, `none` + +### Animation Styles + +Shorthand for animation patterns (YOU MUST specify duration and ease when using): + +- `slide-fade-in`, `slide-fade-out` +- `scale-fade-in`, `scale-fade-out` + +## Theme Customization + +### Adding Custom Colors + +```typescript +import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react' + +const config = defineConfig({ + theme: { + tokens: { + colors: { + brand: { + 50: { value: '#e6f2ff' }, + 100: { value: '#bfdeff' }, + 200: { value: '#99caff' }, + 500: { value: '#0080ff' }, + 950: { value: '#001a33' }, + }, + }, + }, + semanticTokens: { + colors: { + brand: { + solid: { value: '{colors.brand.500}' }, + contrast: { value: '{colors.brand.100}' }, + fg: { + value: { + _light: '{colors.brand.700}', + _dark: '{colors.brand.600}', + }, + }, + muted: { value: '{colors.brand.100}' }, + subtle: { value: '{colors.brand.200}' }, + emphasized: { value: '{colors.brand.300}' }, + focusRing: { value: '{colors.brand.500}' }, + }, + }, + }, + }, +}) + +export const system = createSystem(defaultConfig, config) +``` + +**Important**: For new colors, YOU MUST create matching semantic tokens: `solid`, `contrast`, `fg`, `muted`, `subtle`, `emphasized`, `focusRing`. + +### Adding Custom Text Styles + +```typescript +const config = defineConfig({ + theme: { + textStyles: { + heading: { + value: { + fontSize: '24px', + fontWeight: 'bold', + lineHeight: '1.2', + }, + }, + body: { + value: { + fontSize: '16px', + fontWeight: 'normal', + lineHeight: '1.5', + }, + }, + }, + }, +}) +``` + +### Adding Custom Layer Styles + +```typescript +const config = defineConfig({ + theme: { + layerStyles: { + card: { + value: { + background: 'bg.panel', + borderRadius: 'md', + padding: '4', + boxShadow: 'sm', + }, + }, + panel: { + value: { + background: 'bg.subtle', + border: '1px solid', + borderColor: 'border.muted', + borderRadius: 'lg', + padding: '6', + }, + }, + }, + }, +}) +``` + +## TypeScript Support + +To get proper autocompletion and type safety, YOU MUST run the typegen command after making theme changes: + +```bash +npx @chakra-ui/cli typegen src/components/ui/theme/index.ts +``` + +This generates TypeScript definitions for your custom theme tokens. + +## Best Practices + +1. **Use Semantic Tokens**: Always prefer semantic tokens over raw color values + - ✅ `color="fg.muted"` + - ❌ `color="gray.600"` + +2. **Dark Mode Support**: Semantic tokens automatically handle light/dark mode + - Define light/dark variants using `_light` and `_dark` properties + +3. **Consistent Spacing**: Use the predefined spacing tokens + - `padding="4"`, `margin="2"`, `gap="6"` + +4. **Text Consistency**: Use textStyles for consistent typography + - `textStyle="lg"` instead of manual fontSize/lineHeight + +5. **Layer Styles for Containers**: Use layerStyles for consistent container patterns + - `layerStyle="card"` for card-like containers + +6. **Animation Consistency**: Always specify duration and ease with animationStyles + ```typescript + + ``` + +## Current Theme Structure + +The project uses an Atlassian-inspired design system with: + +- **Brand colors**: ThingConnect blue palette +- **Accent colors**: Teal secondary palette +- **Status colors**: Success (green), warning (yellow), danger (red) +- **Neutral grays**: Clean, accessible gray scale +- **Atlassian typography**: Clean, readable font hierarchy +- **Enterprise layer styles**: Card, panel, header, modal, button variants + +All theme files are located in `src/components/ui/theme/`. diff --git a/thingconnect.pulse.client/src/components/ui/Card.tsx b/thingconnect.pulse.client/src/components/ui/Card.tsx new file mode 100644 index 0000000..0065138 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/Card.tsx @@ -0,0 +1,33 @@ +import { Box, BoxProps } from '@chakra-ui/react' +import { ReactNode } from 'react' + +export interface CardProps extends BoxProps { + children: ReactNode + variant?: 'elevated' | 'outline' | 'ghost' +} + +export function Card({ children, variant = 'elevated', ...props }: CardProps) { + const variantStyles = { + elevated: { + boxShadow: 'md', + borderWidth: '1px', + borderColor: 'transparent', + }, + outline: { + boxShadow: 'none', + borderWidth: '1px', + borderColor: 'border.default', + }, + ghost: { + boxShadow: 'none', + borderWidth: '0', + bg: 'transparent', + }, + }[variant] + + return ( + + {children} + + ) +} diff --git a/thingconnect.pulse.client/src/components/ui/Input.tsx b/thingconnect.pulse.client/src/components/ui/Input.tsx new file mode 100644 index 0000000..c5ff9ed --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/Input.tsx @@ -0,0 +1,23 @@ +import { + Input as ChakraInput, + InputProps as ChakraInputProps, + FormControl, + FormLabel, + FormErrorMessage, +} from '@chakra-ui/react' +import { forwardRef } from 'react' + +export interface InputProps extends ChakraInputProps { + label?: string + error?: string +} + +export const Input = forwardRef(({ label, error, ...props }, ref) => { + return ( + + {label && {label}} + + {error && {error}} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/accordion.tsx b/thingconnect.pulse.client/src/components/ui/accordion.tsx new file mode 100644 index 0000000..7b6cf6f --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/accordion.tsx @@ -0,0 +1,45 @@ +import { Accordion, HStack } from '@chakra-ui/react' +import * as React from 'react' +import { LuChevronDown } from 'react-icons/lu' + +interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps { + indicatorPlacement?: 'start' | 'end' +} + +export const AccordionItemTrigger = React.forwardRef( + function AccordionItemTrigger(props, ref) { + const { children, indicatorPlacement = 'end', ...rest } = props + return ( + + {indicatorPlacement === 'start' && ( + + + + )} + + {children} + + {indicatorPlacement === 'end' && ( + + + + )} + + ) + } +) + +interface AccordionItemContentProps extends Accordion.ItemContentProps {} + +export const AccordionItemContent = React.forwardRef( + function AccordionItemContent(props, ref) { + return ( + + + + ) + } +) + +export const AccordionRoot = Accordion.Root +export const AccordionItem = Accordion.Item diff --git a/thingconnect.pulse.client/src/components/ui/action-bar.tsx b/thingconnect.pulse.client/src/components/ui/action-bar.tsx new file mode 100644 index 0000000..e80f94a --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/action-bar.tsx @@ -0,0 +1,39 @@ +import { ActionBar, Portal } from '@chakra-ui/react' +import { CloseButton } from './close-button' +import * as React from 'react' + +interface ActionBarContentProps extends ActionBar.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const ActionBarContent = React.forwardRef( + function ActionBarContent(props, ref) { + const { children, portalled = true, portalRef, ...rest } = props + + return ( + + + + {children} + + + + ) + } +) + +export const ActionBarCloseTrigger = React.forwardRef< + HTMLButtonElement, + ActionBar.CloseTriggerProps +>(function ActionBarCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const ActionBarRoot = ActionBar.Root +export const ActionBarSelectionTrigger = ActionBar.SelectionTrigger +export const ActionBarSeparator = ActionBar.Separator diff --git a/thingconnect.pulse.client/src/components/ui/alert.tsx b/thingconnect.pulse.client/src/components/ui/alert.tsx new file mode 100644 index 0000000..9b6c149 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/alert.tsx @@ -0,0 +1,27 @@ +import { Alert as ChakraAlert } from '@chakra-ui/react' +import * as React from 'react' + +export interface AlertProps extends Omit { + startElement?: React.ReactNode + endElement?: React.ReactNode + title?: React.ReactNode + icon?: React.ReactElement +} + +export const Alert = React.forwardRef(function Alert(props, ref) { + const { title, children, icon, startElement, endElement, ...rest } = props + return ( + + {startElement || {icon}} + {children ? ( + + {title} + {children} + + ) : ( + {title} + )} + {endElement} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/avatar.tsx b/thingconnect.pulse.client/src/components/ui/avatar.tsx new file mode 100644 index 0000000..4291033 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/avatar.tsx @@ -0,0 +1,26 @@ +import { Avatar as ChakraAvatar, AvatarGroup as ChakraAvatarGroup } from '@chakra-ui/react' +import * as React from 'react' + +type ImageProps = React.ImgHTMLAttributes + +export interface AvatarProps extends ChakraAvatar.RootProps { + name?: string + src?: string + srcSet?: string + loading?: ImageProps['loading'] + icon?: React.ReactElement + fallback?: React.ReactNode +} + +export const Avatar = React.forwardRef(function Avatar(props, ref) { + const { name, src, srcSet, loading, icon, fallback, children, ...rest } = props + return ( + + {icon || fallback} + + {children} + + ) +}) + +export const AvatarGroup = ChakraAvatarGroup diff --git a/thingconnect.pulse.client/src/components/ui/blockquote.tsx b/thingconnect.pulse.client/src/components/ui/blockquote.tsx new file mode 100644 index 0000000..28e9059 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/blockquote.tsx @@ -0,0 +1,29 @@ +import { Blockquote as ChakraBlockquote } from '@chakra-ui/react' +import * as React from 'react' + +export interface BlockquoteProps extends ChakraBlockquote.RootProps { + cite?: React.ReactNode + citeUrl?: string + icon?: React.ReactNode + showDash?: boolean +} + +export const Blockquote = React.forwardRef( + function Blockquote(props, ref) { + const { children, cite, citeUrl, showDash, icon, ...rest } = props + + return ( + + {icon} + {children} + {cite && ( + + {showDash ? <>— : null} {cite} + + )} + + ) + } +) + +export const BlockquoteIcon = ChakraBlockquote.Icon diff --git a/thingconnect.pulse.client/src/components/ui/breadcrumb.tsx b/thingconnect.pulse.client/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..675a1eb --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/breadcrumb.tsx @@ -0,0 +1,35 @@ +import { Breadcrumb, type SystemStyleObject } from '@chakra-ui/react' +import * as React from 'react' + +export interface BreadcrumbRootProps extends Breadcrumb.RootProps { + separator?: React.ReactNode + separatorGap?: SystemStyleObject['gap'] +} + +export const BreadcrumbRoot = React.forwardRef( + function BreadcrumbRoot(props, ref) { + const { separator, separatorGap, children, ...rest } = props + + const validChildren = React.Children.toArray(children).filter(React.isValidElement) + + return ( + + + {validChildren.map((child, index) => { + const last = index === validChildren.length - 1 + return ( + + {child} + {!last && {separator}} + + ) + })} + + + ) + } +) + +export const BreadcrumbLink = Breadcrumb.Link +export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink +export const BreadcrumbEllipsis = Breadcrumb.Ellipsis diff --git a/thingconnect.pulse.client/src/components/ui/checkbox-card.tsx b/thingconnect.pulse.client/src/components/ui/checkbox-card.tsx new file mode 100644 index 0000000..033d5b1 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/checkbox-card.tsx @@ -0,0 +1,53 @@ +import { CheckboxCard as ChakraCheckboxCard } from '@chakra-ui/react' +import * as React from 'react' + +export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps { + icon?: React.ReactElement + label?: React.ReactNode + description?: React.ReactNode + addon?: React.ReactNode + indicator?: React.ReactNode | null + indicatorPlacement?: 'start' | 'end' | 'inside' + inputProps?: React.InputHTMLAttributes +} + +export const CheckboxCard = React.forwardRef( + function CheckboxCard(props, ref) { + const { + inputProps, + label, + description, + icon, + addon, + indicator = , + indicatorPlacement = 'end', + ...rest + } = props + + const hasContent = label || description || icon + const ContentWrapper = indicator ? ChakraCheckboxCard.Content : React.Fragment + + return ( + + + + {indicatorPlacement === 'start' && indicator} + {hasContent && ( + + {icon} + {label && {label}} + {description && ( + {description} + )} + {indicatorPlacement === 'inside' && indicator} + + )} + {indicatorPlacement === 'end' && indicator} + + {addon && {addon}} + + ) + } +) + +export const CheckboxCardIndicator = ChakraCheckboxCard.Indicator diff --git a/thingconnect.pulse.client/src/components/ui/checkbox.tsx b/thingconnect.pulse.client/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..640daf2 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/checkbox.tsx @@ -0,0 +1,21 @@ +import { Checkbox as ChakraCheckbox } from '@chakra-ui/react' +import * as React from 'react' + +export interface CheckboxProps extends ChakraCheckbox.RootProps { + icon?: React.ReactNode + inputProps?: React.InputHTMLAttributes + rootRef?: React.RefObject +} + +export const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + const { icon, children, inputProps, rootRef, ...rest } = props + return ( + + + {icon || } + {children != null && {children}} + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/clipboard.tsx b/thingconnect.pulse.client/src/components/ui/clipboard.tsx new file mode 100644 index 0000000..db26b80 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/clipboard.tsx @@ -0,0 +1,99 @@ +import type { ButtonProps, InputProps } from '@chakra-ui/react' +import { Button, Clipboard as ChakraClipboard, IconButton, Input } from '@chakra-ui/react' +import * as React from 'react' +import { LuCheck, LuClipboard, LuLink } from 'react-icons/lu' + +const ClipboardIcon = React.forwardRef( + function ClipboardIcon(props, ref) { + return ( + } {...props} ref={ref}> + + + ) + } +) + +const ClipboardCopyText = React.forwardRef( + function ClipboardCopyText(props, ref) { + return ( + + Copy + + ) + } +) + +export const ClipboardLabel = React.forwardRef( + function ClipboardLabel(props, ref) { + return ( + + ) + } +) + +export const ClipboardButton = React.forwardRef( + function ClipboardButton(props, ref) { + return ( + + + + ) + } +) + +export const ClipboardLink = React.forwardRef( + function ClipboardLink(props, ref) { + return ( + + + + ) + } +) + +export const ClipboardIconButton = React.forwardRef( + function ClipboardIconButton(props, ref) { + return ( + + + + + + + ) + } +) + +export const ClipboardInput = React.forwardRef( + function ClipboardInputElement(props, ref) { + return ( + + + + ) + } +) + +export const ClipboardRoot = ChakraClipboard.Root diff --git a/thingconnect.pulse.client/src/components/ui/close-button.tsx b/thingconnect.pulse.client/src/components/ui/close-button.tsx new file mode 100644 index 0000000..f834ed9 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/close-button.tsx @@ -0,0 +1,16 @@ +import type { ButtonProps } from '@chakra-ui/react' +import { IconButton as ChakraIconButton } from '@chakra-ui/react' +import * as React from 'react' +import { LuX } from 'react-icons/lu' + +export type CloseButtonProps = ButtonProps + +export const CloseButton = React.forwardRef( + function CloseButton(props, ref) { + return ( + + {props.children ?? } + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/color-mode.tsx b/thingconnect.pulse.client/src/components/ui/color-mode.tsx index f86a744..fb9051a 100644 --- a/thingconnect.pulse.client/src/components/ui/color-mode.tsx +++ b/thingconnect.pulse.client/src/components/ui/color-mode.tsx @@ -1,76 +1,103 @@ -"use client" +'use client' -import type { IconButtonProps, SpanProps } from "@chakra-ui/react" -import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react" -import { ThemeProvider } from "next-themes" -import type { ThemeProviderProps } from "next-themes" -import * as React from "react" -import { LuMoon, LuSun } from "react-icons/lu" -import { useColorMode } from "../../lib/color-mode" +import type { IconButtonProps, SpanProps } from '@chakra-ui/react' +import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react' +import { ThemeProvider, useTheme } from 'next-themes' +import type { ThemeProviderProps } from 'next-themes' +import * as React from 'react' +import { LuMoon, LuSun } from 'react-icons/lu' -export type ColorModeProviderProps = ThemeProviderProps +export interface ColorModeProviderProps extends ThemeProviderProps {} export function ColorModeProvider(props: ColorModeProviderProps) { - return ( - - ) + return } -export function ColorModeIcon() { - const { colorMode } = useColorMode() - return colorMode === "dark" ? : +export type ColorMode = 'light' | 'dark' + +export interface UseColorModeReturn { + colorMode: ColorMode + setColorMode: (colorMode: ColorMode) => void + toggleColorMode: () => void +} + +export function useColorMode(): UseColorModeReturn { + const { resolvedTheme, setTheme, forcedTheme } = useTheme() + const colorMode = forcedTheme || resolvedTheme + const toggleColorMode = () => { + setTheme(resolvedTheme === 'dark' ? 'light' : 'dark') + } + return { + colorMode: colorMode as ColorMode, + setColorMode: setTheme, + toggleColorMode, + } } -type ColorModeButtonProps = Omit +export function useColorModeValue(light: T, dark: T) { + const { colorMode } = useColorMode() + return colorMode === 'dark' ? dark : light +} -export const ColorModeButton = function ColorModeButton({ ref, ...props }: ColorModeButtonProps & { ref?: React.RefObject }) { - const { toggleColorMode } = useColorMode() - return ( - }> - - - - - ) +export function ColorModeIcon() { + const { colorMode } = useColorMode() + return colorMode === 'dark' ? : } -export const LightMode = function LightMode({ ref, ...props }: SpanProps & { ref?: React.RefObject }) { +interface ColorModeButtonProps extends Omit {} + +export const ColorModeButton = React.forwardRef( + function ColorModeButton(props, ref) { + const { toggleColorMode } = useColorMode() return ( - + }> + + + + ) } +) -export const DarkMode = function DarkMode({ ref, ...props }: SpanProps & { ref?: React.RefObject }) { +export const LightMode = React.forwardRef( + function LightMode(props, ref) { return ( ) } +) + +export const DarkMode = React.forwardRef(function DarkMode(props, ref) { + return ( + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/color-picker.tsx b/thingconnect.pulse.client/src/components/ui/color-picker.tsx new file mode 100644 index 0000000..a2124d2 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/color-picker.tsx @@ -0,0 +1,201 @@ +import type { IconButtonProps, StackProps } from '@chakra-ui/react' +import { + ColorPicker as ChakraColorPicker, + For, + IconButton, + Portal, + Span, + Stack, + Text, + VStack, +} from '@chakra-ui/react' +import * as React from 'react' +import { LuCheck, LuPipette } from 'react-icons/lu' + +export const ColorPickerTrigger = React.forwardRef< + HTMLButtonElement, + ChakraColorPicker.TriggerProps & { fitContent?: boolean } +>(function ColorPickerTrigger(props, ref) { + const { fitContent, ...rest } = props + return ( + + {props.children || } + + ) +}) + +export const ColorPickerInput = React.forwardRef< + HTMLInputElement, + Omit +>(function ColorHexInput(props, ref) { + return +}) + +interface ColorPickerContentProps extends ChakraColorPicker.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const ColorPickerContent = React.forwardRef( + function ColorPickerContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) + } +) + +export const ColorPickerInlineContent = React.forwardRef< + HTMLDivElement, + ChakraColorPicker.ContentProps +>(function ColorPickerInlineContent(props, ref) { + return ( + + ) +}) + +export const ColorPickerSliders = React.forwardRef( + function ColorPickerSliders(props, ref) { + return ( + + + + + ) + } +) + +export const ColorPickerArea = React.forwardRef( + function ColorPickerArea(props, ref) { + return ( + + + + + ) + } +) + +export const ColorPickerEyeDropper = React.forwardRef( + function ColorPickerEyeDropper(props, ref) { + return ( + + + + + + ) + } +) + +export const ColorPickerChannelSlider = React.forwardRef< + HTMLDivElement, + ChakraColorPicker.ChannelSliderProps +>(function ColorPickerSlider(props, ref) { + return ( + + + + + + ) +}) + +export const ColorPickerSwatchTrigger = React.forwardRef< + HTMLButtonElement, + ChakraColorPicker.SwatchTriggerProps & { + swatchSize?: ChakraColorPicker.SwatchTriggerProps['boxSize'] + } +>(function ColorPickerSwatchTrigger(props, ref) { + const { swatchSize, children, ...rest } = props + return ( + + {children || ( + + + + + + )} + + ) +}) + +export const ColorPickerRoot = React.forwardRef( + function ColorPickerRoot(props, ref) { + return ( + + {props.children} + + + ) + } +) + +const formatMap = { + rgba: ['red', 'green', 'blue', 'alpha'], + hsla: ['hue', 'saturation', 'lightness', 'alpha'], + hsba: ['hue', 'saturation', 'brightness', 'alpha'], + hexa: ['hex', 'alpha'], +} as const + +export const ColorPickerChannelInputs = React.forwardRef< + HTMLDivElement, + ChakraColorPicker.ViewProps +>(function ColorPickerChannelInputs(props, ref) { + const channels = formatMap[props.format] + return ( + + {channels.map((channel) => ( + + + + {channel.charAt(0).toUpperCase()} + + + ))} + + ) +}) + +export const ColorPickerChannelSliders = React.forwardRef< + HTMLDivElement, + ChakraColorPicker.ViewProps +>(function ColorPickerChannelSliders(props, ref) { + const channels = formatMap[props.format] + return ( + + + {(channel) => ( + + + {channel} + + + + )} + + + ) +}) + +export const ColorPickerLabel = ChakraColorPicker.Label +export const ColorPickerControl = ChakraColorPicker.Control +export const ColorPickerValueText = ChakraColorPicker.ValueText +export const ColorPickerValueSwatch = ChakraColorPicker.ValueSwatch +export const ColorPickerChannelInput = ChakraColorPicker.ChannelInput +export const ColorPickerSwatchGroup = ChakraColorPicker.SwatchGroup diff --git a/thingconnect.pulse.client/src/components/ui/data-list.tsx b/thingconnect.pulse.client/src/components/ui/data-list.tsx new file mode 100644 index 0000000..84264f9 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/data-list.tsx @@ -0,0 +1,28 @@ +import { DataList as ChakraDataList } from '@chakra-ui/react' +import { InfoTip } from './toggle-tip' +import * as React from 'react' + +export const DataListRoot = ChakraDataList.Root + +interface ItemProps extends ChakraDataList.ItemProps { + label: React.ReactNode + value: React.ReactNode + info?: React.ReactNode + grow?: boolean +} + +export const DataListItem = React.forwardRef( + function DataListItem(props, ref) { + const { label, info, value, children, grow, ...rest } = props + return ( + + + {label} + {info && {info}} + + {value} + {children} + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/dialog.tsx b/thingconnect.pulse.client/src/components/ui/dialog.tsx new file mode 100644 index 0000000..a986e10 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/dialog.tsx @@ -0,0 +1,49 @@ +import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react' +import { CloseButton } from './close-button' +import * as React from 'react' + +interface DialogContentProps extends ChakraDialog.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + backdrop?: boolean +} + +export const DialogContent = React.forwardRef( + function DialogContent(props, ref) { + const { children, portalled = true, portalRef, backdrop = true, ...rest } = props + + return ( + + {backdrop && } + + + {children} + + + + ) + } +) + +export const DialogCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDialog.CloseTriggerProps +>(function DialogCloseTrigger(props, ref) { + return ( + + + {props.children} + + + ) +}) + +export const DialogRoot = ChakraDialog.Root +export const DialogFooter = ChakraDialog.Footer +export const DialogHeader = ChakraDialog.Header +export const DialogBody = ChakraDialog.Body +export const DialogBackdrop = ChakraDialog.Backdrop +export const DialogTitle = ChakraDialog.Title +export const DialogDescription = ChakraDialog.Description +export const DialogTrigger = ChakraDialog.Trigger +export const DialogActionTrigger = ChakraDialog.ActionTrigger diff --git a/thingconnect.pulse.client/src/components/ui/drawer.tsx b/thingconnect.pulse.client/src/components/ui/drawer.tsx new file mode 100644 index 0000000..cd07cb5 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/drawer.tsx @@ -0,0 +1,45 @@ +import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react' +import { CloseButton } from './close-button' +import * as React from 'react' + +interface DrawerContentProps extends ChakraDrawer.ContentProps { + portalled?: boolean + portalRef?: React.RefObject + offset?: ChakraDrawer.ContentProps['padding'] +} + +export const DrawerContent = React.forwardRef( + function DrawerContent(props, ref) { + const { children, portalled = true, portalRef, offset, ...rest } = props + return ( + + + + {children} + + + + ) + } +) + +export const DrawerCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDrawer.CloseTriggerProps +>(function DrawerCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const DrawerTrigger = ChakraDrawer.Trigger +export const DrawerRoot = ChakraDrawer.Root +export const DrawerFooter = ChakraDrawer.Footer +export const DrawerHeader = ChakraDrawer.Header +export const DrawerBody = ChakraDrawer.Body +export const DrawerBackdrop = ChakraDrawer.Backdrop +export const DrawerDescription = ChakraDrawer.Description +export const DrawerTitle = ChakraDrawer.Title +export const DrawerActionTrigger = ChakraDrawer.ActionTrigger diff --git a/thingconnect.pulse.client/src/components/ui/empty-state.tsx b/thingconnect.pulse.client/src/components/ui/empty-state.tsx new file mode 100644 index 0000000..c146311 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/empty-state.tsx @@ -0,0 +1,30 @@ +import { EmptyState as ChakraEmptyState, VStack } from '@chakra-ui/react' +import * as React from 'react' + +export interface EmptyStateProps extends ChakraEmptyState.RootProps { + title: string + description?: string + icon?: React.ReactNode +} + +export const EmptyState = React.forwardRef( + function EmptyState(props, ref) { + const { title, description, icon, children, ...rest } = props + return ( + + + {icon && {icon}} + {description ? ( + + {title} + {description} + + ) : ( + {title} + )} + {children} + + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/field.tsx b/thingconnect.pulse.client/src/components/ui/field.tsx new file mode 100644 index 0000000..abffff3 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/field.tsx @@ -0,0 +1,26 @@ +import { Field as ChakraField } from '@chakra-ui/react' +import * as React from 'react' + +export interface FieldProps extends Omit { + label?: React.ReactNode + helperText?: React.ReactNode + errorText?: React.ReactNode + optionalText?: React.ReactNode +} + +export const Field = React.forwardRef(function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = props + return ( + + {label && ( + + {label} + + + )} + {children} + {helperText && {helperText}} + {errorText && {errorText}} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/file-upload.tsx b/thingconnect.pulse.client/src/components/ui/file-upload.tsx new file mode 100644 index 0000000..8943789 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/file-upload.tsx @@ -0,0 +1,153 @@ +'use client' + +import type { ButtonProps, RecipeProps } from '@chakra-ui/react' +import { + Button, + FileUpload as ChakraFileUpload, + Icon, + IconButton, + Span, + Text, + useFileUploadContext, + useRecipe, +} from '@chakra-ui/react' +import * as React from 'react' +import { LuFile, LuUpload, LuX } from 'react-icons/lu' + +export interface FileUploadRootProps extends ChakraFileUpload.RootProps { + inputProps?: React.InputHTMLAttributes +} + +export const FileUploadRoot = React.forwardRef( + function FileUploadRoot(props, ref) { + const { children, inputProps, ...rest } = props + return ( + + + {children} + + ) + } +) + +export interface FileUploadDropzoneProps extends ChakraFileUpload.DropzoneProps { + label: React.ReactNode + description?: React.ReactNode +} + +export const FileUploadDropzone = React.forwardRef( + function FileUploadDropzone(props, ref) { + const { children, label, description, ...rest } = props + return ( + + + + + +
{label}
+ {description && {description}} +
+ {children} +
+ ) + } +) + +interface VisibilityProps { + showSize?: boolean + clearable?: boolean +} + +interface FileUploadItemProps extends VisibilityProps { + file: File +} + +const FileUploadItem = React.forwardRef( + function FileUploadItem(props, ref) { + const { file, showSize, clearable } = props + return ( + + + + + + + + {showSize ? ( + + + + + ) : ( + + )} + + {clearable && ( + + + + + + )} + + ) + } +) + +interface FileUploadListProps extends VisibilityProps, ChakraFileUpload.ItemGroupProps { + files?: File[] +} + +export const FileUploadList = React.forwardRef( + function FileUploadList(props, ref) { + const { showSize, clearable, files, ...rest } = props + + const fileUpload = useFileUploadContext() + const acceptedFiles = files ?? fileUpload.acceptedFiles + + if (acceptedFiles.length === 0) return null + + return ( + + {acceptedFiles.map((file) => ( + + ))} + + ) + } +) + +type Assign = Omit & U + +interface FileInputProps extends Assign> { + placeholder?: React.ReactNode +} + +export const FileInput = React.forwardRef( + function FileInput(props, ref) { + const inputRecipe = useRecipe({ key: 'input' }) + const [recipeProps, restProps] = inputRecipe.splitVariantProps(props) + const { placeholder = 'Select file(s)', ...rest } = restProps + return ( + + + + ) + } +) + +export const FileUploadLabel = ChakraFileUpload.Label +export const FileUploadClearTrigger = ChakraFileUpload.ClearTrigger +export const FileUploadTrigger = ChakraFileUpload.Trigger diff --git a/thingconnect.pulse.client/src/components/ui/hover-card.tsx b/thingconnect.pulse.client/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..b49e035 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/hover-card.tsx @@ -0,0 +1,34 @@ +import { HoverCard, Portal } from '@chakra-ui/react' +import * as React from 'react' + +interface HoverCardContentProps extends HoverCard.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const HoverCardContent = React.forwardRef( + function HoverCardContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + + return ( + + + + + + ) + } +) + +export const HoverCardArrow = React.forwardRef( + function HoverCardArrow(props, ref) { + return ( + + + + ) + } +) + +export const HoverCardRoot = HoverCard.Root +export const HoverCardTrigger = HoverCard.Trigger diff --git a/thingconnect.pulse.client/src/components/ui/index.ts b/thingconnect.pulse.client/src/components/ui/index.ts new file mode 100644 index 0000000..6d1b092 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/index.ts @@ -0,0 +1,3 @@ +export { Button } from './Button' +export { Input } from './Input' +export { Card } from './Card' diff --git a/thingconnect.pulse.client/src/components/ui/input-group.tsx b/thingconnect.pulse.client/src/components/ui/input-group.tsx new file mode 100644 index 0000000..698950a --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/input-group.tsx @@ -0,0 +1,52 @@ +import type { BoxProps, InputElementProps } from '@chakra-ui/react' +import { Group, InputElement } from '@chakra-ui/react' +import * as React from 'react' + +export interface InputGroupProps extends BoxProps { + startElementProps?: InputElementProps + endElementProps?: InputElementProps + startElement?: React.ReactNode + endElement?: React.ReactNode + children: React.ReactElement + startOffset?: InputElementProps['paddingStart'] + endOffset?: InputElementProps['paddingEnd'] +} + +export const InputGroup = React.forwardRef( + function InputGroup(props, ref) { + const { + startElement, + startElementProps, + endElement, + endElementProps, + children, + startOffset = '6px', + endOffset = '6px', + ...rest + } = props + + const child = React.Children.only>(children) + + return ( + + {startElement && ( + + {startElement} + + )} + {React.cloneElement(child, { + ...(startElement && { + ps: `calc(var(--input-height) - ${startOffset})`, + }), + ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), + ...children.props, + })} + {endElement && ( + + {endElement} + + )} + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/link-button.tsx b/thingconnect.pulse.client/src/components/ui/link-button.tsx new file mode 100644 index 0000000..c13c1d8 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/link-button.tsx @@ -0,0 +1,11 @@ +'use client' + +import type { HTMLChakraProps, RecipeProps } from '@chakra-ui/react' +import { createRecipeContext } from '@chakra-ui/react' + +export interface LinkButtonProps extends HTMLChakraProps<'a', RecipeProps<'button'>> {} + +const { withContext } = createRecipeContext({ key: 'button' }) + +// Replace "a" with your framework's link component +export const LinkButton = withContext('a') diff --git a/thingconnect.pulse.client/src/components/ui/logo.tsx b/thingconnect.pulse.client/src/components/ui/logo.tsx new file mode 100644 index 0000000..a967aac --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/logo.tsx @@ -0,0 +1,31 @@ +import { Image, type ImageProps } from '@chakra-ui/react' +import ThingConnectLogo from '../../assets/ThingConnect Logo.svg' +import ThingConnectIcon from '../../assets/ThingConnect Icon.svg' + +interface LogoProps extends Omit { + variant?: 'full' | 'icon' + size?: 'sm' | 'md' | 'lg' | 'xl' +} + +const sizeMap = { + sm: { full: '30px', icon: '24px' }, + md: { full: '40px', icon: '32px' }, + lg: { full: '50px', icon: '40px' }, + xl: { full: '60px', icon: '48px' }, +} + +export function Logo({ variant = 'full', size = 'md', ...props }: LogoProps) { + const logoSrc = variant === 'full' ? ThingConnectLogo : ThingConnectIcon + const logoAlt = variant === 'full' ? 'ThingConnect Logo' : 'ThingConnect' + const logoHeight = sizeMap[size][variant] + + return ( + {logoAlt} + ) +} diff --git a/thingconnect.pulse.client/src/components/ui/menu.tsx b/thingconnect.pulse.client/src/components/ui/menu.tsx new file mode 100644 index 0000000..bac0b3f --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/menu.tsx @@ -0,0 +1,103 @@ +'use client' + +import { AbsoluteCenter, Menu as ChakraMenu, Portal } from '@chakra-ui/react' +import * as React from 'react' +import { LuCheck, LuChevronRight } from 'react-icons/lu' + +interface MenuContentProps extends ChakraMenu.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const MenuContent = React.forwardRef( + function MenuContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) + } +) + +export const MenuArrow = React.forwardRef( + function MenuArrow(props, ref) { + return ( + + + + ) + } +) + +export const MenuCheckboxItem = React.forwardRef( + function MenuCheckboxItem(props, ref) { + return ( + + + + + + + {props.children} + + ) + } +) + +export const MenuRadioItem = React.forwardRef( + function MenuRadioItem(props, ref) { + const { children, ...rest } = props + return ( + + + + + + + {children} + + ) + } +) + +export const MenuItemGroup = React.forwardRef( + function MenuItemGroup(props, ref) { + const { title, children, ...rest } = props + return ( + + {title && {title}} + {children} + + ) + } +) + +export interface MenuTriggerItemProps extends ChakraMenu.ItemProps { + startIcon?: React.ReactNode +} + +export const MenuTriggerItem = React.forwardRef( + function MenuTriggerItem(props, ref) { + const { startIcon, children, ...rest } = props + return ( + + {startIcon} + {children} + + + ) + } +) + +export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup +export const MenuContextTrigger = ChakraMenu.ContextTrigger +export const MenuRoot = ChakraMenu.Root +export const MenuSeparator = ChakraMenu.Separator + +export const MenuItem = ChakraMenu.Item +export const MenuItemText = ChakraMenu.ItemText +export const MenuItemCommand = ChakraMenu.ItemCommand +export const MenuTrigger = ChakraMenu.Trigger diff --git a/thingconnect.pulse.client/src/components/ui/native-select.tsx b/thingconnect.pulse.client/src/components/ui/native-select.tsx new file mode 100644 index 0000000..3ae56e8 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/native-select.tsx @@ -0,0 +1,53 @@ +'use client' + +import { NativeSelect as Select } from '@chakra-ui/react' +import * as React from 'react' + +interface NativeSelectRootProps extends Select.RootProps { + icon?: React.ReactNode +} + +export const NativeSelectRoot = React.forwardRef( + function NativeSelect(props, ref) { + const { icon, children, ...rest } = props + return ( + + {children} + {icon} + + ) + } +) + +interface NativeSelectItem { + value: string + label: string + disabled?: boolean +} + +interface NativeSelectFieldProps extends Select.FieldProps { + items?: Array +} + +export const NativeSelectField = React.forwardRef( + function NativeSelectField(props, ref) { + const { items: itemsProp, children, ...rest } = props + + const items = React.useMemo( + () => + itemsProp?.map((item) => (typeof item === 'string' ? { label: item, value: item } : item)), + [itemsProp] + ) + + return ( + + {children} + {items?.map((item) => ( + + ))} + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/number-input.tsx b/thingconnect.pulse.client/src/components/ui/number-input.tsx new file mode 100644 index 0000000..36f22a4 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/number-input.tsx @@ -0,0 +1,23 @@ +import { NumberInput as ChakraNumberInput } from '@chakra-ui/react' +import * as React from 'react' + +export interface NumberInputProps extends ChakraNumberInput.RootProps {} + +export const NumberInputRoot = React.forwardRef( + function NumberInput(props, ref) { + const { children, ...rest } = props + return ( + + {children} + + + + + + ) + } +) + +export const NumberInputField = ChakraNumberInput.Input +export const NumberInputScrubber = ChakraNumberInput.Scrubber +export const NumberInputLabel = ChakraNumberInput.Label diff --git a/thingconnect.pulse.client/src/components/ui/pagination.tsx b/thingconnect.pulse.client/src/components/ui/pagination.tsx new file mode 100644 index 0000000..cbf13f9 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/pagination.tsx @@ -0,0 +1,188 @@ +'use client' + +import type { ButtonProps, TextProps } from '@chakra-ui/react' +import { + Button, + Pagination as ChakraPagination, + IconButton, + Text, + createContext, + usePaginationContext, +} from '@chakra-ui/react' +import * as React from 'react' +import { HiChevronLeft, HiChevronRight, HiMiniEllipsisHorizontal } from 'react-icons/hi2' +import { LinkButton } from './link-button' + +interface ButtonVariantMap { + current: ButtonProps['variant'] + default: ButtonProps['variant'] + ellipsis: ButtonProps['variant'] +} + +type PaginationVariant = 'outline' | 'solid' | 'subtle' + +interface ButtonVariantContext { + size: ButtonProps['size'] + variantMap: ButtonVariantMap + getHref?: (page: number) => string +} + +const [RootPropsProvider, useRootProps] = createContext({ + name: 'RootPropsProvider', +}) + +export interface PaginationRootProps extends Omit { + size?: ButtonProps['size'] + variant?: PaginationVariant + getHref?: (page: number) => string +} + +const variantMap: Record = { + outline: { default: 'ghost', ellipsis: 'plain', current: 'outline' }, + solid: { default: 'outline', ellipsis: 'outline', current: 'solid' }, + subtle: { default: 'ghost', ellipsis: 'plain', current: 'subtle' }, +} + +export const PaginationRoot = React.forwardRef( + function PaginationRoot(props, ref) { + const { size = 'sm', variant = 'outline', getHref, ...rest } = props + return ( + + + + ) + } +) + +export const PaginationEllipsis = React.forwardRef( + function PaginationEllipsis(props, ref) { + const { size, variantMap } = useRootProps() + return ( + + + + ) + } +) + +export const PaginationItem = React.forwardRef( + function PaginationItem(props, ref) { + const { page } = usePaginationContext() + const { size, variantMap, getHref } = useRootProps() + + const current = page === props.value + const variant = current ? variantMap.current : variantMap.default + + if (getHref) { + return ( + + {props.value} + + ) + } + + return ( + + + + ) + } +) + +export const PaginationPrevTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPagination.PrevTriggerProps +>(function PaginationPrevTrigger(props, ref) { + const { size, variantMap, getHref } = useRootProps() + const { previousPage } = usePaginationContext() + + if (getHref) { + return ( + + + + ) + } + + return ( + + + + + + ) +}) + +export const PaginationNextTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPagination.NextTriggerProps +>(function PaginationNextTrigger(props, ref) { + const { size, variantMap, getHref } = useRootProps() + const { nextPage } = usePaginationContext() + + if (getHref) { + return ( + + + + ) + } + + return ( + + + + + + ) +}) + +export const PaginationItems = (props: React.HTMLAttributes) => { + return ( + + {({ pages }) => + pages.map((page, index) => { + return page.type === 'ellipsis' ? ( + + ) : ( + + ) + }) + } + + ) +} + +interface PageTextProps extends TextProps { + format?: 'short' | 'compact' | 'long' +} + +export const PaginationPageText = React.forwardRef( + function PaginationPageText(props, ref) { + const { format = 'compact', ...rest } = props + const { page, totalPages, pageRange, count } = usePaginationContext() + const content = React.useMemo(() => { + if (format === 'short') return `${page} / ${totalPages}` + if (format === 'compact') return `${page} of ${totalPages}` + return `${pageRange.start + 1} - ${Math.min(pageRange.end, count)} of ${count}` + }, [format, page, totalPages, pageRange, count]) + + return ( + + {content} + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/password-input.tsx b/thingconnect.pulse.client/src/components/ui/password-input.tsx new file mode 100644 index 0000000..4014f18 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/password-input.tsx @@ -0,0 +1,146 @@ +'use client' + +import type { ButtonProps, GroupProps, InputProps, StackProps } from '@chakra-ui/react' +import { + Box, + HStack, + IconButton, + Input, + InputGroup, + Stack, + mergeRefs, + useControllableState, +} from '@chakra-ui/react' +import * as React from 'react' +import { LuEye, LuEyeOff } from 'react-icons/lu' + +export interface PasswordVisibilityProps { + /** + * The default visibility state of the password input. + */ + defaultVisible?: boolean + /** + * The controlled visibility state of the password input. + */ + visible?: boolean + /** + * Callback invoked when the visibility state changes. + */ + onVisibleChange?: (visible: boolean) => void + /** + * Custom icons for the visibility toggle button. + */ + visibilityIcon?: { on: React.ReactNode; off: React.ReactNode } +} + +export interface PasswordInputProps extends InputProps, PasswordVisibilityProps { + rootProps?: GroupProps +} + +export const PasswordInput = React.forwardRef( + function PasswordInput(props, ref) { + const { + rootProps, + defaultVisible, + visible: visibleProp, + onVisibleChange, + visibilityIcon = { on: , off: }, + ...rest + } = props + + const [visible, setVisible] = useControllableState({ + value: visibleProp, + defaultValue: defaultVisible || false, + onChange: onVisibleChange, + }) + + const inputRef = React.useRef(null) + + return ( + { + if (rest.disabled) return + if (e.button !== 0) return + e.preventDefault() + setVisible(!visible) + }} + > + {visible ? visibilityIcon.off : visibilityIcon.on} + + } + {...rootProps} + > + + + ) + } +) + +const VisibilityTrigger = React.forwardRef( + function VisibilityTrigger(props, ref) { + return ( + + ) + } +) + +interface PasswordStrengthMeterProps extends StackProps { + max?: number + value: number +} + +export const PasswordStrengthMeter = React.forwardRef( + function PasswordStrengthMeter(props, ref) { + const { max = 4, value, ...rest } = props + + const percent = (value / max) * 100 + const { label, colorPalette } = getColorPalette(percent) + + return ( + + + {Array.from({ length: max }).map((_, index) => ( + + ))} + + {label && {label}} + + ) + } +) + +function getColorPalette(percent: number) { + switch (true) { + case percent < 33: + return { label: 'Low', colorPalette: 'red' } + case percent < 66: + return { label: 'Medium', colorPalette: 'orange' } + default: + return { label: 'High', colorPalette: 'green' } + } +} diff --git a/thingconnect.pulse.client/src/components/ui/pin-input.tsx b/thingconnect.pulse.client/src/components/ui/pin-input.tsx new file mode 100644 index 0000000..af57461 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/pin-input.tsx @@ -0,0 +1,27 @@ +import { PinInput as ChakraPinInput, Group } from '@chakra-ui/react' +import * as React from 'react' + +export interface PinInputProps extends ChakraPinInput.RootProps { + rootRef?: React.RefObject + count?: number + inputProps?: React.InputHTMLAttributes + attached?: boolean +} + +export const PinInput = React.forwardRef( + function PinInput(props, ref) { + const { count = 4, inputProps, rootRef, attached, ...rest } = props + return ( + + + + + {Array.from({ length: count }).map((_, index) => ( + + ))} + + + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/popover.tsx b/thingconnect.pulse.client/src/components/ui/popover.tsx new file mode 100644 index 0000000..6a2dad4 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/popover.tsx @@ -0,0 +1,57 @@ +import { Popover as ChakraPopover, Portal } from '@chakra-ui/react' +import { CloseButton } from './close-button' +import * as React from 'react' + +interface PopoverContentProps extends ChakraPopover.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const PopoverContent = React.forwardRef( + function PopoverContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) + } +) + +export const PopoverArrow = React.forwardRef( + function PopoverArrow(props, ref) { + return ( + + + + ) + } +) + +export const PopoverCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraPopover.CloseTriggerProps +>(function PopoverCloseTrigger(props, ref) { + return ( + + + + ) +}) + +export const PopoverTitle = ChakraPopover.Title +export const PopoverDescription = ChakraPopover.Description +export const PopoverFooter = ChakraPopover.Footer +export const PopoverHeader = ChakraPopover.Header +export const PopoverRoot = ChakraPopover.Root +export const PopoverBody = ChakraPopover.Body +export const PopoverTrigger = ChakraPopover.Trigger diff --git a/thingconnect.pulse.client/src/components/ui/progress-circle.tsx b/thingconnect.pulse.client/src/components/ui/progress-circle.tsx new file mode 100644 index 0000000..324c2bc --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/progress-circle.tsx @@ -0,0 +1,33 @@ +import type { SystemStyleObject } from '@chakra-ui/react' +import { AbsoluteCenter, ProgressCircle as ChakraProgressCircle } from '@chakra-ui/react' +import * as React from 'react' + +interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps { + trackColor?: SystemStyleObject['stroke'] + cap?: SystemStyleObject['strokeLinecap'] +} + +export const ProgressCircleRing = React.forwardRef( + function ProgressCircleRing(props, ref) { + const { trackColor, cap, color, ...rest } = props + return ( + + + + + ) + } +) + +export const ProgressCircleValueText = React.forwardRef< + HTMLDivElement, + ChakraProgressCircle.ValueTextProps +>(function ProgressCircleValueText(props, ref) { + return ( + + + + ) +}) + +export const ProgressCircleRoot = ChakraProgressCircle.Root diff --git a/thingconnect.pulse.client/src/components/ui/progress.tsx b/thingconnect.pulse.client/src/components/ui/progress.tsx new file mode 100644 index 0000000..cdc6671 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/progress.tsx @@ -0,0 +1,32 @@ +import { Progress as ChakraProgress } from '@chakra-ui/react' +import { InfoTip } from './toggle-tip' +import * as React from 'react' + +export const ProgressBar = React.forwardRef( + function ProgressBar(props, ref) { + return ( + + + + ) + } +) + +export interface ProgressLabelProps extends ChakraProgress.LabelProps { + info?: React.ReactNode +} + +export const ProgressLabel = React.forwardRef( + function ProgressLabel(props, ref) { + const { children, info, ...rest } = props + return ( + + {children} + {info && {info}} + + ) + } +) + +export const ProgressRoot = ChakraProgress.Root +export const ProgressValueText = ChakraProgress.ValueText diff --git a/thingconnect.pulse.client/src/components/ui/prose.tsx b/thingconnect.pulse.client/src/components/ui/prose.tsx new file mode 100644 index 0000000..3d0552b --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/prose.tsx @@ -0,0 +1,275 @@ +'use client' + +import { chakra } from '@chakra-ui/react' + +const TRAILING_PSEUDO_REGEX = /(::?[\w-]+(?:\([^)]*\))?)+$/ +const EXCLUDE_CLASSNAME = '.not-prose' +function inWhere(selector: T): T { + const rebuiltSelector = selector.startsWith('& ') ? selector.slice(2) : selector + const match = selector.match(TRAILING_PSEUDO_REGEX) + const pseudo = match ? match[0] : '' + const base = match ? selector.slice(0, -match[0].length) : rebuiltSelector + return `& :where(${base}):not(${EXCLUDE_CLASSNAME}, ${EXCLUDE_CLASSNAME} *)${pseudo}` as T +} + +export const Prose = chakra('div', { + base: { + color: 'fg.muted', + maxWidth: '65ch', + fontSize: 'sm', + lineHeight: '1.7em', + [inWhere('& p')]: { + marginTop: '1em', + marginBottom: '1em', + }, + [inWhere('& blockquote')]: { + marginTop: '1.285em', + marginBottom: '1.285em', + paddingInline: '1.285em', + borderInlineStartWidth: '0.25em', + color: 'fg', + }, + [inWhere('& a')]: { + color: 'fg', + textDecoration: 'underline', + textUnderlineOffset: '3px', + textDecorationThickness: '2px', + textDecorationColor: 'border.muted', + fontWeight: '500', + }, + [inWhere('& strong')]: { + fontWeight: '600', + }, + [inWhere('& a strong')]: { + color: 'inherit', + }, + [inWhere('& h1')]: { + fontSize: '2.15em', + letterSpacing: '-0.02em', + marginTop: '0', + marginBottom: '0.8em', + lineHeight: '1.2em', + }, + [inWhere('& h2')]: { + fontSize: '1.4em', + letterSpacing: '-0.02em', + marginTop: '1.6em', + marginBottom: '0.8em', + lineHeight: '1.4em', + }, + [inWhere('& h3')]: { + fontSize: '1.285em', + letterSpacing: '-0.01em', + marginTop: '1.5em', + marginBottom: '0.4em', + lineHeight: '1.5em', + }, + [inWhere('& h4')]: { + marginTop: '1.4em', + marginBottom: '0.5em', + letterSpacing: '-0.01em', + lineHeight: '1.5em', + }, + [inWhere('& img')]: { + marginTop: '1.7em', + marginBottom: '1.7em', + borderRadius: 'lg', + boxShadow: 'inset', + }, + [inWhere('& picture')]: { + marginTop: '1.7em', + marginBottom: '1.7em', + }, + [inWhere('& picture > img')]: { + marginTop: '0', + marginBottom: '0', + }, + [inWhere('& video')]: { + marginTop: '1.7em', + marginBottom: '1.7em', + }, + [inWhere('& kbd')]: { + fontSize: '0.85em', + borderRadius: 'xs', + paddingTop: '0.15em', + paddingBottom: '0.15em', + paddingInlineEnd: '0.35em', + paddingInlineStart: '0.35em', + fontFamily: 'inherit', + color: 'fg.muted', + '--shadow': 'colors.border', + boxShadow: '0 0 0 1px var(--shadow),0 1px 0 1px var(--shadow)', + }, + [inWhere('& code')]: { + fontSize: '0.925em', + letterSpacing: '-0.01em', + borderRadius: 'md', + borderWidth: '1px', + padding: '0.25em', + }, + [inWhere('& pre code')]: { + fontSize: 'inherit', + letterSpacing: 'inherit', + borderWidth: 'inherit', + padding: '0', + }, + [inWhere('& h2 code')]: { + fontSize: '0.9em', + }, + [inWhere('& h3 code')]: { + fontSize: '0.8em', + }, + [inWhere('& pre')]: { + backgroundColor: 'bg.subtle', + marginTop: '1.6em', + marginBottom: '1.6em', + borderRadius: 'md', + fontSize: '0.9em', + paddingTop: '0.65em', + paddingBottom: '0.65em', + paddingInlineEnd: '1em', + paddingInlineStart: '1em', + overflowX: 'auto', + fontWeight: '400', + }, + [inWhere('& ol')]: { + marginTop: '1em', + marginBottom: '1em', + paddingInlineStart: '1.5em', + }, + [inWhere('& ul')]: { + marginTop: '1em', + marginBottom: '1em', + paddingInlineStart: '1.5em', + }, + [inWhere('& li')]: { + marginTop: '0.285em', + marginBottom: '0.285em', + }, + [inWhere('& ol > li')]: { + paddingInlineStart: '0.4em', + listStyleType: 'decimal', + '&::marker': { + color: 'fg.muted', + }, + }, + [inWhere('& ul > li')]: { + paddingInlineStart: '0.4em', + listStyleType: 'disc', + '&::marker': { + color: 'fg.muted', + }, + }, + [inWhere('& > ul > li p')]: { + marginTop: '0.5em', + marginBottom: '0.5em', + }, + [inWhere('& > ul > li > p:first-of-type')]: { + marginTop: '1em', + }, + [inWhere('& > ul > li > p:last-of-type')]: { + marginBottom: '1em', + }, + [inWhere('& > ol > li > p:first-of-type')]: { + marginTop: '1em', + }, + [inWhere('& > ol > li > p:last-of-type')]: { + marginBottom: '1em', + }, + [inWhere('& ul ul, ul ol, ol ul, ol ol')]: { + marginTop: '0.5em', + marginBottom: '0.5em', + }, + [inWhere('& dl')]: { + marginTop: '1em', + marginBottom: '1em', + }, + [inWhere('& dt')]: { + fontWeight: '600', + marginTop: '1em', + }, + [inWhere('& dd')]: { + marginTop: '0.285em', + paddingInlineStart: '1.5em', + }, + [inWhere('& hr')]: { + marginTop: '2.25em', + marginBottom: '2.25em', + }, + [inWhere('& :is(h1,h2,h3,h4,h5,hr) + *')]: { + marginTop: '0', + }, + [inWhere('& table')]: { + width: '100%', + tableLayout: 'auto', + textAlign: 'start', + lineHeight: '1.5em', + marginTop: '2em', + marginBottom: '2em', + }, + [inWhere('& thead')]: { + borderBottomWidth: '1px', + color: 'fg', + }, + [inWhere('& tbody tr')]: { + borderBottomWidth: '1px', + borderBottomColor: 'border', + }, + [inWhere('& thead th')]: { + paddingInlineEnd: '1em', + paddingBottom: '0.65em', + paddingInlineStart: '1em', + fontWeight: 'medium', + textAlign: 'start', + }, + [inWhere('& thead th:first-of-type')]: { + paddingInlineStart: '0', + }, + [inWhere('& thead th:last-of-type')]: { + paddingInlineEnd: '0', + }, + [inWhere('& tbody td, tfoot td')]: { + paddingTop: '0.65em', + paddingInlineEnd: '1em', + paddingBottom: '0.65em', + paddingInlineStart: '1em', + }, + [inWhere('& tbody td:first-of-type, tfoot td:first-of-type')]: { + paddingInlineStart: '0', + }, + [inWhere('& tbody td:last-of-type, tfoot td:last-of-type')]: { + paddingInlineEnd: '0', + }, + [inWhere('& figure')]: { + marginTop: '1.625em', + marginBottom: '1.625em', + }, + [inWhere('& figure > *')]: { + marginTop: '0', + marginBottom: '0', + }, + [inWhere('& figcaption')]: { + fontSize: '0.85em', + lineHeight: '1.25em', + marginTop: '0.85em', + color: 'fg.muted', + }, + [inWhere('& h1, h2, h3, h4')]: { + color: 'fg', + fontWeight: '600', + }, + }, + variants: { + size: { + md: { + fontSize: 'sm', + }, + lg: { + fontSize: 'md', + }, + }, + }, + defaultVariants: { + size: 'md', + }, +}) diff --git a/thingconnect.pulse.client/src/components/ui/provider.tsx b/thingconnect.pulse.client/src/components/ui/provider.tsx index fd0331b..4ab35b2 100644 --- a/thingconnect.pulse.client/src/components/ui/provider.tsx +++ b/thingconnect.pulse.client/src/components/ui/provider.tsx @@ -1,14 +1,12 @@ -"use client" +'use client' -import { ChakraProvider, defaultSystem } from "@chakra-ui/react" -import { - ColorModeProvider, - type ColorModeProviderProps, -} from "./color-mode" +import { ChakraProvider } from '@chakra-ui/react' +import { ColorModeProvider, type ColorModeProviderProps } from './color-mode' +import { system } from './theme' export function Provider(props: ColorModeProviderProps) { return ( - + ) diff --git a/thingconnect.pulse.client/src/components/ui/qr-code.tsx b/thingconnect.pulse.client/src/components/ui/qr-code.tsx new file mode 100644 index 0000000..e943494 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/qr-code.tsx @@ -0,0 +1,20 @@ +import { QrCode as ChakraQrCode } from '@chakra-ui/react' +import * as React from 'react' + +export interface QrCodeProps extends Omit { + fill?: string + overlay?: React.ReactNode +} + +export const QrCode = React.forwardRef(function QrCode(props, ref) { + const { children, fill, overlay, ...rest } = props + return ( + + + + + {overlay} + {children && {children}} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/radio-card.tsx b/thingconnect.pulse.client/src/components/ui/radio-card.tsx new file mode 100644 index 0000000..9aab10b --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/radio-card.tsx @@ -0,0 +1,53 @@ +import { RadioCard } from '@chakra-ui/react' +import * as React from 'react' + +interface RadioCardItemProps extends RadioCard.ItemProps { + icon?: React.ReactElement + label?: React.ReactNode + description?: React.ReactNode + addon?: React.ReactNode + indicator?: React.ReactNode | null + indicatorPlacement?: 'start' | 'end' | 'inside' + inputProps?: React.InputHTMLAttributes +} + +export const RadioCardItem = React.forwardRef( + function RadioCardItem(props, ref) { + const { + inputProps, + label, + description, + addon, + icon, + indicator = , + indicatorPlacement = 'end', + ...rest + } = props + + const hasContent = label || description || icon + const ContentWrapper = indicator ? RadioCard.ItemContent : React.Fragment + + return ( + + + + {indicatorPlacement === 'start' && indicator} + {hasContent && ( + + {icon} + {label && {label}} + {description && {description}} + {indicatorPlacement === 'inside' && indicator} + + )} + {indicatorPlacement === 'end' && indicator} + + {addon && {addon}} + + ) + } +) + +export const RadioCardRoot = RadioCard.Root +export const RadioCardLabel = RadioCard.Label +export const RadioCardItemIndicator = RadioCard.ItemIndicator diff --git a/thingconnect.pulse.client/src/components/ui/radio.tsx b/thingconnect.pulse.client/src/components/ui/radio.tsx new file mode 100644 index 0000000..142d847 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/radio.tsx @@ -0,0 +1,20 @@ +import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react' +import * as React from 'react' + +export interface RadioProps extends ChakraRadioGroup.ItemProps { + rootRef?: React.RefObject + inputProps?: React.InputHTMLAttributes +} + +export const Radio = React.forwardRef(function Radio(props, ref) { + const { children, inputProps, rootRef, ...rest } = props + return ( + + + + {children && {children}} + + ) +}) + +export const RadioGroup = ChakraRadioGroup.Root diff --git a/thingconnect.pulse.client/src/components/ui/rating.tsx b/thingconnect.pulse.client/src/components/ui/rating.tsx new file mode 100644 index 0000000..fd05933 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/rating.tsx @@ -0,0 +1,25 @@ +import { RatingGroup } from '@chakra-ui/react' +import * as React from 'react' + +export interface RatingProps extends RatingGroup.RootProps { + icon?: React.ReactElement + count?: number + label?: React.ReactNode +} + +export const Rating = React.forwardRef(function Rating(props, ref) { + const { icon, count = 5, label, ...rest } = props + return ( + + {label && {label}} + + + {Array.from({ length: count }).map((_, index) => ( + + + + ))} + + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/segmented-control.tsx b/thingconnect.pulse.client/src/components/ui/segmented-control.tsx new file mode 100644 index 0000000..46295fe --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/segmented-control.tsx @@ -0,0 +1,42 @@ +'use client' + +import { For, SegmentGroup } from '@chakra-ui/react' +import * as React from 'react' + +interface Item { + value: string + label: React.ReactNode + disabled?: boolean +} + +export interface SegmentedControlProps extends SegmentGroup.RootProps { + items: Array +} + +function normalize(items: Array): Item[] { + return items.map((item) => { + if (typeof item === 'string') return { value: item, label: item } + return item + }) +} + +export const SegmentedControl = React.forwardRef( + function SegmentedControl(props, ref) { + const { items, ...rest } = props + const data = React.useMemo(() => normalize(items), [items]) + + return ( + + + + {(item) => ( + + {item.label} + + + )} + + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/select.tsx b/thingconnect.pulse.client/src/components/ui/select.tsx new file mode 100644 index 0000000..7aa29ae --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/select.tsx @@ -0,0 +1,134 @@ +'use client' + +import type { CollectionItem } from '@chakra-ui/react' +import { Select as ChakraSelect, Portal } from '@chakra-ui/react' +import { CloseButton } from './close-button' +import * as React from 'react' + +interface SelectTriggerProps extends ChakraSelect.ControlProps { + clearable?: boolean +} + +export const SelectTrigger = React.forwardRef( + function SelectTrigger(props, ref) { + const { children, clearable, ...rest } = props + return ( + + {children} + + {clearable && } + + + + ) + } +) + +const SelectClearTrigger = React.forwardRef( + function SelectClearTrigger(props, ref) { + return ( + + + + ) + } +) + +interface SelectContentProps extends ChakraSelect.ContentProps { + portalled?: boolean + portalRef?: React.RefObject +} + +export const SelectContent = React.forwardRef( + function SelectContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) + } +) + +export const SelectItem = React.forwardRef( + function SelectItem(props, ref) { + const { item, children, ...rest } = props + return ( + + {children} + + + ) + } +) + +interface SelectValueTextProps extends Omit { + children?(items: CollectionItem[]): React.ReactNode +} + +export const SelectValueText = React.forwardRef( + function SelectValueText(props, ref) { + const { children, ...rest } = props + return ( + + + {(select) => { + const items = select.selectedItems + if (items.length === 0) return props.placeholder + if (children) return children(items) + if (items.length === 1) return select.collection.stringifyItem(items[0]) + return `${items.length} selected` + }} + + + ) + } +) + +export const SelectRoot = React.forwardRef( + function SelectRoot(props, ref) { + return ( + + {props.asChild ? ( + props.children + ) : ( + <> + + {props.children} + + )} + + ) + } +) as ChakraSelect.RootComponent + +interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps { + label: React.ReactNode +} + +export const SelectItemGroup = React.forwardRef( + function SelectItemGroup(props, ref) { + const { children, label, ...rest } = props + return ( + + {label} + {children} + + ) + } +) + +export const SelectLabel = ChakraSelect.Label +export const SelectItemText = ChakraSelect.ItemText diff --git a/thingconnect.pulse.client/src/components/ui/skeleton.tsx b/thingconnect.pulse.client/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..76b729e --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/skeleton.tsx @@ -0,0 +1,37 @@ +import type { SkeletonProps as ChakraSkeletonProps, CircleProps } from '@chakra-ui/react' +import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react' +import * as React from 'react' + +export interface SkeletonCircleProps extends ChakraSkeletonProps { + size?: CircleProps['size'] +} + +export const SkeletonCircle = React.forwardRef( + function SkeletonCircle(props, ref) { + const { size, ...rest } = props + return ( + + + + ) + } +) + +export interface SkeletonTextProps extends ChakraSkeletonProps { + noOfLines?: number +} + +export const SkeletonText = React.forwardRef( + function SkeletonText(props, ref) { + const { noOfLines = 3, gap, ...rest } = props + return ( + + {Array.from({ length: noOfLines }).map((_, index) => ( + + ))} + + ) + } +) + +export const Skeleton = ChakraSkeleton diff --git a/thingconnect.pulse.client/src/components/ui/slider.tsx b/thingconnect.pulse.client/src/components/ui/slider.tsx new file mode 100644 index 0000000..4d935a7 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/slider.tsx @@ -0,0 +1,78 @@ +import { Slider as ChakraSlider, For, HStack } from '@chakra-ui/react' +import * as React from 'react' + +export interface SliderProps extends ChakraSlider.RootProps { + marks?: Array + label?: React.ReactNode + showValue?: boolean +} + +export const Slider = React.forwardRef(function Slider(props, ref) { + const { marks: marksProp, label, showValue, ...rest } = props + const value = props.defaultValue ?? props.value + + const marks = marksProp?.map((mark) => { + if (typeof mark === 'number') return { value: mark, label: undefined } + return mark + }) + + const hasMarkLabel = !!marks?.some((mark) => mark.label) + + return ( + + {label && !showValue && {label}} + {label && showValue && ( + + {label} + + + )} + + + + + + + + + ) +}) + +function SliderThumbs(props: { value?: number[] }) { + const { value } = props + return ( + + {(_, index) => ( + + + + )} + + ) +} + +interface SliderMarksProps { + marks?: Array +} + +const SliderMarks = React.forwardRef( + function SliderMarks(props, ref) { + const { marks } = props + if (!marks?.length) return null + + return ( + + {marks.map((mark, index) => { + const value = typeof mark === 'number' ? mark : mark.value + const label = typeof mark === 'number' ? undefined : mark.label + return ( + + + {label} + + ) + })} + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/stat.tsx b/thingconnect.pulse.client/src/components/ui/stat.tsx new file mode 100644 index 0000000..03bfee0 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/stat.tsx @@ -0,0 +1,61 @@ +import { Badge, type BadgeProps, Stat as ChakraStat, FormatNumber } from '@chakra-ui/react' +import { InfoTip } from './toggle-tip' +import * as React from 'react' + +interface StatLabelProps extends ChakraStat.LabelProps { + info?: React.ReactNode +} + +export const StatLabel = React.forwardRef( + function StatLabel(props, ref) { + const { info, children, ...rest } = props + return ( + + {children} + {info && {info}} + + ) + } +) + +interface StatValueTextProps extends ChakraStat.ValueTextProps { + value?: number + formatOptions?: Intl.NumberFormatOptions +} + +export const StatValueText = React.forwardRef( + function StatValueText(props, ref) { + const { value, formatOptions, children, ...rest } = props + return ( + + {children || (value != null && )} + + ) + } +) + +export const StatUpTrend = React.forwardRef( + function StatUpTrend(props, ref) { + return ( + + + {props.children} + + ) + } +) + +export const StatDownTrend = React.forwardRef( + function StatDownTrend(props, ref) { + return ( + + + {props.children} + + ) + } +) + +export const StatRoot = ChakraStat.Root +export const StatHelpText = ChakraStat.HelpText +export const StatValueUnit = ChakraStat.ValueUnit diff --git a/thingconnect.pulse.client/src/components/ui/status.tsx b/thingconnect.pulse.client/src/components/ui/status.tsx new file mode 100644 index 0000000..ae91d39 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/status.tsx @@ -0,0 +1,27 @@ +import type { ColorPalette } from '@chakra-ui/react' +import { Status as ChakraStatus } from '@chakra-ui/react' +import * as React from 'react' + +type StatusValue = 'success' | 'error' | 'warning' | 'info' + +export interface StatusProps extends ChakraStatus.RootProps { + value?: StatusValue +} + +const statusMap: Record = { + success: 'green', + error: 'red', + warning: 'orange', + info: 'blue', +} + +export const Status = React.forwardRef(function Status(props, ref) { + const { children, value = 'info', ...rest } = props + const colorPalette = rest.colorPalette ?? statusMap[value] + return ( + + + {children} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/stepper-input.tsx b/thingconnect.pulse.client/src/components/ui/stepper-input.tsx new file mode 100644 index 0000000..4e884bc --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/stepper-input.tsx @@ -0,0 +1,47 @@ +import { HStack, IconButton, NumberInput } from '@chakra-ui/react' +import * as React from 'react' +import { LuMinus, LuPlus } from 'react-icons/lu' + +export interface StepperInputProps extends NumberInput.RootProps { + label?: React.ReactNode +} + +export const StepperInput = React.forwardRef( + function StepperInput(props, ref) { + const { label, ...rest } = props + return ( + + {label && {label}} + + + + + + + ) + } +) + +const DecrementTrigger = React.forwardRef( + function DecrementTrigger(props, ref) { + return ( + + + + + + ) + } +) + +const IncrementTrigger = React.forwardRef( + function IncrementTrigger(props, ref) { + return ( + + + + + + ) + } +) diff --git a/thingconnect.pulse.client/src/components/ui/steps.tsx b/thingconnect.pulse.client/src/components/ui/steps.tsx new file mode 100644 index 0000000..48a0620 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/steps.tsx @@ -0,0 +1,77 @@ +import { Box, Steps as ChakraSteps } from '@chakra-ui/react' +import * as React from 'react' +import { LuCheck } from 'react-icons/lu' + +interface StepInfoProps { + title?: React.ReactNode + description?: React.ReactNode +} + +export interface StepsItemProps extends Omit, StepInfoProps { + completedIcon?: React.ReactNode + icon?: React.ReactNode +} + +export const StepsItem = React.forwardRef( + function StepsItem(props, ref) { + const { title, description, completedIcon, icon, ...rest } = props + return ( + + + + } + incomplete={icon || } + /> + + + + + + ) + } +) + +const StepInfo = (props: StepInfoProps) => { + const { title, description } = props + + if (title && description) { + return ( + + {title} + {description} + + ) + } + + return ( + <> + {title && {title}} + {description && {description}} + + ) +} + +interface StepsIndicatorProps { + completedIcon: React.ReactNode + icon?: React.ReactNode +} + +export const StepsIndicator = React.forwardRef( + function StepsIndicator(props, ref) { + const { icon = , completedIcon } = props + return ( + + + + ) + } +) + +export const StepsList = ChakraSteps.List +export const StepsRoot = ChakraSteps.Root +export const StepsContent = ChakraSteps.Content +export const StepsCompletedContent = ChakraSteps.CompletedContent + +export const StepsNextTrigger = ChakraSteps.NextTrigger +export const StepsPrevTrigger = ChakraSteps.PrevTrigger diff --git a/thingconnect.pulse.client/src/components/ui/switch.tsx b/thingconnect.pulse.client/src/components/ui/switch.tsx new file mode 100644 index 0000000..d43c2d7 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/switch.tsx @@ -0,0 +1,32 @@ +import { Switch as ChakraSwitch } from '@chakra-ui/react' +import * as React from 'react' + +export interface SwitchProps extends ChakraSwitch.RootProps { + inputProps?: React.InputHTMLAttributes + rootRef?: React.RefObject + trackLabel?: { on: React.ReactNode; off: React.ReactNode } + thumbLabel?: { on: React.ReactNode; off: React.ReactNode } +} + +export const Switch = React.forwardRef(function Switch(props, ref) { + const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } = props + + return ( + + + + + {thumbLabel && ( + + {thumbLabel?.on} + + )} + + {trackLabel && ( + {trackLabel.on} + )} + + {children != null && {children}} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/tag.tsx b/thingconnect.pulse.client/src/components/ui/tag.tsx new file mode 100644 index 0000000..d6a13fc --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/tag.tsx @@ -0,0 +1,26 @@ +import { Tag as ChakraTag } from '@chakra-ui/react' +import * as React from 'react' + +export interface TagProps extends ChakraTag.RootProps { + startElement?: React.ReactNode + endElement?: React.ReactNode + onClose?: VoidFunction + closable?: boolean +} + +export const Tag = React.forwardRef(function Tag(props, ref) { + const { startElement, endElement, onClose, closable = !!onClose, children, ...rest } = props + + return ( + + {startElement && {startElement}} + {children} + {endElement && {endElement}} + {closable && ( + + + + )} + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/theme-toggle.tsx b/thingconnect.pulse.client/src/components/ui/theme-toggle.tsx new file mode 100644 index 0000000..de9e7b4 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/theme-toggle.tsx @@ -0,0 +1,175 @@ +'use client' + +import { Button, HStack, VStack, Text, Box, Heading, Stack } from '@chakra-ui/react' +import { ColorModeButton } from './color-mode' +import { LuPalette, LuSun, LuMoon } from 'react-icons/lu' +import { useColorMode } from '../../lib/color-mode' + +export function ThemeToggle() { + const { colorMode, toggleColorMode } = useColorMode() + + return ( + + + + + Theme Settings + + + + Switch between light and dark themes for the best viewing experience + + + + + + + + + + + Current theme: + + + {colorMode === 'light' ? 'Light' : 'Dark'} + + + + + + ) +} + +export function EnterpriseThemeShowcase() { + return ( + + + + ThingConnect.Pulse + + + Enterprise Theme System with Light & Dark Mode Support + + + + + + + {/* Brand Colors Card */} + + + Brand Colors + + + Primary + + + Success + + + Warning + + + Danger + + + + + + {/* Typography Card */} + + + Typography + + Heading Large + Body Medium Text + Caption Text + Overline Text + + + + + + {/* Layout Examples */} + + + + Section Style + + + This uses the enterprise.section layer style for content areas. + + + + + + Header Style + + + This uses the enterprise.header layer style for navigation areas. + + + + + + + 🎨 Professional enterprise theme with semantic color tokens that automatically adapt to + light and dark modes + + + + ) +} diff --git a/thingconnect.pulse.client/src/components/ui/theme/CLAUDE.md b/thingconnect.pulse.client/src/components/ui/theme/CLAUDE.md new file mode 100644 index 0000000..2b13d29 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/theme/CLAUDE.md @@ -0,0 +1,335 @@ +# ThingConnect.Pulse Theme System + +This document describes the complete Chakra UI theme configuration for ThingConnect.Pulse, based on Atlassian Design System principles. + +## Theme Structure + +The theme is split across multiple files for better organization: + +- `index.ts` - Main theme configuration with semantic tokens, typography, and layer styles +- `colors.ts` - All color definitions using `defineTokens.colors()` + +## Color System + +### Core Color Palettes + +The theme includes comprehensive color palettes with specific shade ranges: + +#### Blue (Primary Brand Color) + +- **Range**: 100-1000 (no 50) +- **Key shades**: + - `blue.500` (#388BFF) - Primary blue + - `blue.700` (#0C66E4) - Dark accent + - `blue.900` (#09326C) - Darkest + +#### Red (Status/Error Color) + +- **Range**: 100-1000 +- **Key shades**: + - `red.500` (#F15B50) - Primary red + - `red.600` (#E2483D) - Medium dark + - `red.1000` (#42221F) - Darkest + +#### Green (Success Color) + +- **Range**: 100-1000 +- **Key shades**: + - `green.500` (#2ABB7F) - Success green + - `green.400` (#4BCE97) - Light success + +#### Other Colors + +- **Yellow**: 100-1000 (warning states) +- **Purple**: 100-1000 (accent color) +- **Teal**: 100-1000 (secondary accent) +- **Orange**: 100-1000 (additional status) +- **Gray**: 100-1000 (neutrals with fine-grained steps) + +#### Special Palettes + +- **Neutrals**: 0, 100-1100, plus alpha variants (100A-500A) +- **Dark Neutrals**: -100 to 1100, plus alpha variants +- **White/Black Alpha**: 50-950 (transparency variants) + +### Semantic Color Tokens + +Each color family has semantic variants that adapt to light/dark mode: + +```typescript +// Example for blue +blue: { + solid: { value: '{colors.blue.500}' }, // Solid background + contrast: { value: 'white' }, // Contrasting text + fg: { // Foreground text + value: { + _light: '{colors.blue.600}', + _dark: '{colors.blue.400}', + }, + }, + muted: { // Muted background + value: { + _light: '{colors.blue.100}', + _dark: '{colors.blue.900}', + }, + }, + subtle: { /* ... */ }, // Subtle background + emphasized: { /* ... */ }, // Emphasized state + focusRing: { value: '{colors.blue.500}' }, // Focus indicators +} +``` + +### Global Semantic Tokens + +Atlassian-style surface tokens for consistent theming: + +- `color.background.default` - Main background +- `color.background.sunken` - Recessed areas +- `color.background.raised` - Elevated surfaces +- `color.background.overlay` - Modal/overlay backgrounds + +Legacy tokens for compatibility: + +- `bg`, `bg.panel` - Background surfaces +- `fg`, `fg.muted`, `fg.subtle` - Text colors +- `border`, `border.muted`, `border.subtle` - Border colors + +## Typography System + +### Font Stack + +All text uses Atlassian Sans with system fallbacks: + +``` +'Atlassian Sans', ui-sans-serif, -apple-system, BlinkMacSystemFont, +'Segoe UI', Roboto, Helvetica, Arial, sans-serif +``` + +### Text Styles + +#### Heading Scales + +- `ui.heading.xxlarge` - 72px, weight 600 +- `ui.heading.xlarge` - 60px, weight 600 +- `ui.heading.large` - 48px, weight 600 +- `ui.heading.medium` - 36px, weight 500 +- `ui.heading.small` - 30px, weight 500 +- `ui.heading.xsmall` - 24px, weight 500 +- `ui.heading.xxsmall` - 20px, weight 500 + +#### Body Text + +- `ui.body.large` - 18px, line-height 1.556 +- `ui.body.medium` - 16px, line-height 1.5 (default) +- `ui.body.small` - 14px, line-height 1.429 + +#### Utility Styles + +- `ui.helper` - 12px helper text +- `ui.code` - 14px monospace code + +### Font Sizes + +Precise scale from 2xs (10px) to 7xl (72px): + +- `2xs`: 0.625rem (10px) +- `xs`: 0.75rem (12px) +- `sm`: 0.875rem (14px) +- `md`: 1rem (16px) - default +- `lg`: 1.125rem (18px) +- `xl`: 1.25rem (20px) +- `2xl`: 1.5rem (24px) +- `3xl`: 1.875rem (30px) +- `4xl`: 2.25rem (36px) +- `5xl`: 3rem (48px) +- `6xl`: 3.75rem (60px) +- `7xl`: 4.5rem (72px) + +## Spacing System + +Atlassian Design System 4px grid-based spacing: + +- `025`: 0.125rem (2px) - minimal +- `050`: 0.25rem (4px) - space.050 +- `075`: 0.375rem (6px) - space.075 +- `100`: 0.5rem (8px) - space.100 +- `150`: 0.75rem (12px) - space.150 +- `200`: 1rem (16px) - space.200 +- `250`: 1.25rem (20px) - space.250 +- `300`: 1.5rem (24px) - space.300 +- `400`: 2rem (32px) - space.400 +- `500`: 2.5rem (40px) - space.500 +- `600`: 3rem (48px) - space.600 +- `800`: 4rem (64px) - space.800 +- `1000`: 5rem (80px) - space.1000 + +## Border Radius + +Atlassian-inspired radius scale: + +- `none`: 0 +- `sm`: 2px +- `base`: 3px (Atlassian button standard) +- `md`: 4px +- `lg`: 6px +- `xl`: 8px +- `2xl`: 12px +- `3xl`: 16px +- `full`: 9999px + +## Shadows + +Atlassian elevation system: + +- `raised`: Subtle card elevation +- `overlay`: Modal/popover shadows +- `xs`, `sm`, `base`, `md`, `lg`: Progressive elevation +- Dark mode variants: `raised.dark`, `overlay.dark` + +## Gradients + +Subtle brand gradients: + +- `brand.subtle`: Blue gradient +- `accent.subtle`: Teal to green +- `surface.overlay`: Dark overlay +- `surface.overlay.light`: Light overlay + +## Layer Styles + +### Atlassian Component Styles + +#### Cards & Panels + +```typescript +'atlassian.card': { + bg: 'color.background.raised', + borderRadius: 'base', + border: '1px solid', + borderColor: 'border.muted', + boxShadow: 'raised', + p: '200', // 16px +} + +'atlassian.panel': { + bg: 'color.background.sunken', + borderRadius: 'base', + border: '1px solid', + borderColor: 'border.subtle', + p: '300', // 24px +} +``` + +#### Buttons + +```typescript +'atlassian.button.primary': { + bg: 'blue.500', + color: 'white', + borderRadius: 'base', + px: '200', + py: '100', + minHeight: '32px', +} + +'atlassian.button.secondary': { + bg: 'transparent', + color: 'blue.500', + border: '1px solid', + borderColor: 'blue.500', + borderRadius: 'base', + px: '200', + py: '100', + minHeight: '32px', +} +``` + +#### Layout Elements + +```typescript +'atlassian.header': { + bg: 'color.background.default', + borderBottom: '1px solid', + borderColor: 'border.muted', + px: '200', + py: '150', + minHeight: '44px', +} + +'atlassian.modal': { + bg: 'color.background.overlay', + borderRadius: 'lg', + boxShadow: 'overlay', + p: '300', +} +``` + +## Usage Guidelines + +### Color Usage + +1. **Always prefer semantic tokens** over raw color values + - ✅ `bg="blue.subtle"` + - ❌ `bg="blue.100"` + +2. **Use appropriate color families** + - Blue: Primary actions, links + - Red: Errors, destructive actions + - Green: Success states + - Yellow: Warnings + - Purple: Special features + +3. **Leverage automatic light/dark mode** + - Semantic tokens automatically adapt + - Use `_light` and `_dark` variants when needed + +### Typography Usage + +1. **Use text styles for consistency** + - ✅ `textStyle="ui.body.medium"` + - ❌ Manual fontSize/lineHeight + +2. **Maintain hierarchy** + - Headings: xxlarge → xxsmall + - Body: large → small + - Helper text: ui.helper + +### Spacing Usage + +1. **Follow the 4px grid** + - Use spacing tokens: `p="200"` (16px) + - Avoid arbitrary values + +2. **Consistent patterns** + - Cards: padding `200` (16px) + - Panels: padding `300` (24px) + - Headers: padding `150`/`200` (12px/16px) + +### Layer Style Usage + +1. **Use Atlassian layer styles** + - `layerStyle="atlassian.card"` for cards + - `layerStyle="atlassian.button.primary"` for primary buttons + +2. **Maintain touch targets** + - Minimum 32px height for interactive elements + - Consistent 44px header height + +## Dark Mode Support + +The theme automatically supports dark mode through: + +- Semantic tokens with `_light`/`_dark` variants +- Global CSS that respects `colorScheme` +- Automatic color inversion for appropriate elements + +## File Organization + +``` +src/components/ui/theme/ +├── index.ts # Main theme config +├── colors.ts # Color definitions +└── CLAUDE.md # This documentation +``` + +The theme is exported as `system` from `index.ts` and should be used with Chakra UI's Provider. diff --git a/thingconnect.pulse.client/src/components/ui/theme/README.md b/thingconnect.pulse.client/src/components/ui/theme/README.md new file mode 100644 index 0000000..3301f44 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/theme/README.md @@ -0,0 +1,188 @@ +# ThingConnect.Pulse Theme System + +This directory contains the enterprise-focused theme configuration for ThingConnect.Pulse, built with Chakra UI v3, incorporating authentic ThingConnect brand colors, and following Atlassian Design System principles. + +## Design System Foundation + +### 🏗️ Atlassian Design System Integration + +- **Design Tokens**: Follows Atlassian's token naming conventions and structure +- **4px Grid System**: Consistent spacing based on Atlassian's rhythm +- **Surface System**: Semantic background colors (default, sunken, raised, overlay) +- **Elevation**: Shadow system with light/dark mode variations +- **Typography**: 2025 typography refresh with bolder fonts and improved readability + +### 🎨 ThingConnect Brand Colors + +- **Primary Brand**: `#076bb3` - The signature ThingConnect blue extracted from logo +- **Secondary Accent**: `#16a5a4` - The distinctive ThingConnect teal from logo +- **Supporting Colors**: Dark variants `#124771` and `#097a7d` for depth and contrast +- **Semantic Colors**: Automatic light/dark mode support with semantic tokens +- **Status Colors**: Success, warning, danger, and neutral color schemes +- **Accessibility**: WCAG compliant color contrasts + +### 🌙 Dark/Light Mode Support + +- Seamless theme switching with `next-themes` +- Automatic system preference detection +- Smooth transitions between modes +- Enterprise-optimized color schemes for both modes + +### 📐 Typography System + +- Inter font family for modern, readable text +- Comprehensive text styles (display, heading, body, caption, overline) +- Consistent spacing and line heights +- Professional letter spacing + +### 🧱 Layout Styles + +Pre-built layer styles for common enterprise UI patterns: + +- `enterprise.card` - Card containers with proper shadows and borders +- `enterprise.section` - Content sections with subtle backgrounds +- `enterprise.header` - Navigation and header areas +- `enterprise.sidebar` - Sidebar layouts with borders + +## Usage + +### Atlassian-Style Usage + +```tsx +import { Box, Text, Heading } from '@chakra-ui/react' + +function MyComponent() { + return ( + + Enterprise Title + + Professional content with Atlassian design principles + + + ) +} +``` + +### Legacy Enterprise Usage (Still Supported) + +```tsx +function LegacyComponent() { + return ( + + Legacy Title + + Backward compatible with previous theme + + + ) +} +``` + +### Color Palette Usage + +```tsx +// Use semantic tokens that automatically adapt to light/dark mode + + + + Themed container + +``` + +### Theme Toggle + +```tsx +import { ColorModeButton } from "@/components/ui/color-mode" +import { ThemeToggle } from "@/components/ui/theme-toggle" + +// Simple toggle button + + +// Full theme selection UI + +``` + +## File Structure + +``` +src/theme/ +├── index.ts # Main theme configuration +├── global.css # Global CSS styles +└── README.md # This documentation +``` + +## Customization + +To modify the theme: + +1. Edit `src/theme/index.ts` to adjust colors, typography, or layout styles +2. Run the typegen command to update TypeScript types: + ```bash + npx @chakra-ui/cli typegen src/theme/index.ts + ``` +3. Restart your development server + +## Color Tokens + +### ThingConnect Brand Colors + +- `brand.50` through `brand.950` - Full ThingConnect blue palette based on `#076bb3` +- `brand.solid` - Primary ThingConnect blue (`#076bb3`) +- `brand.fg` - Text color that adapts to light/dark mode +- `brand.muted` - Subtle background color +- `brand.emphasized` - Highlighted background color + +### ThingConnect Accent Colors + +- `accent.50` through `accent.950` - Full ThingConnect teal palette based on `#16a5a4` +- `accent.solid` - Secondary ThingConnect teal (`#16a5a4`) +- `accent.fg` - Accent text color that adapts to light/dark mode +- `accent.muted` - Subtle accent background color +- `accent.emphasized` - Highlighted accent background color + +### Semantic Colors + +- `bg` - Main background color +- `bg.panel` - Panel/card background +- `bg.muted` - Muted background +- `fg` - Primary text color +- `fg.muted` - Secondary text color +- `border` - Default border color +- `border.subtle` - Subtle border color + +## Typography Styles + +### Display Text + +- `display.large` - 7xl, bold, tight spacing +- `display.medium` - 6xl, bold, tight spacing +- `display.small` - 5xl, bold, comfortable spacing + +### Headings + +- `heading.large` - 4xl, semibold +- `heading.medium` - 3xl, semibold +- `heading.small` - 2xl, semibold + +### Body Text + +- `body.large` - lg, normal weight +- `body.medium` - md, normal weight +- `body.small` - sm, normal weight + +### Utility Text + +- `caption` - xs, medium weight, tracked +- `overline` - 2xs, bold, uppercase, widely tracked + +## Development + +The theme system includes automatic TypeScript type generation. After modifying the theme, run: + +```bash +npx @chakra-ui/cli typegen src/theme/index.ts +``` + +This ensures full type safety and autocompletion for all custom tokens and styles. diff --git a/thingconnect.pulse.client/src/components/ui/theme/colors.ts b/thingconnect.pulse.client/src/components/ui/theme/colors.ts new file mode 100644 index 0000000..596ad20 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/theme/colors.ts @@ -0,0 +1,560 @@ +import { defineTokens } from '@chakra-ui/react' + +export const colors = defineTokens.colors({ + whiteAlpha: { + '50': { + value: 'rgba(255, 255, 255, 0.04)', + }, + '100': { + value: 'rgba(255, 255, 255, 0.06)', + }, + '200': { + value: 'rgba(255, 255, 255, 0.08)', + }, + '300': { + value: 'rgba(255, 255, 255, 0.16)', + }, + '400': { + value: 'rgba(255, 255, 255, 0.24)', + }, + '500': { + value: 'rgba(255, 255, 255, 0.36)', + }, + '600': { + value: 'rgba(255, 255, 255, 0.48)', + }, + '700': { + value: 'rgba(255, 255, 255, 0.64)', + }, + '800': { + value: 'rgba(255, 255, 255, 0.80)', + }, + '900': { + value: 'rgba(255, 255, 255, 0.92)', + }, + '950': { + value: 'rgba(255, 255, 255, 0.95)', + }, + }, + blackAlpha: { + '50': { + value: 'rgba(0, 0, 0, 0.04)', + }, + '100': { + value: 'rgba(0, 0, 0, 0.06)', + }, + '200': { + value: 'rgba(0, 0, 0, 0.08)', + }, + '300': { + value: 'rgba(0, 0, 0, 0.16)', + }, + '400': { + value: 'rgba(0, 0, 0, 0.24)', + }, + '500': { + value: 'rgba(0, 0, 0, 0.36)', + }, + '600': { + value: 'rgba(0, 0, 0, 0.48)', + }, + '700': { + value: 'rgba(0, 0, 0, 0.64)', + }, + '800': { + value: 'rgba(0, 0, 0, 0.80)', + }, + '900': { + value: 'rgba(0, 0, 0, 0.92)', + }, + '950': { + value: 'rgba(0, 0, 0, 0.95)', + }, + }, + gray: { + '100': { + value: '#F7F8F9', + }, + '150': { + value: '#F5F6F7', + }, + '200': { + value: '#F1F2F4', + }, + '250': { + value: '#BFC1C4', + }, + '300': { + value: '#DCDFE4', + }, + '350': { + value: '#A9ABAF', + }, + '400': { + value: '#B3B9C4', + }, + '450': { + value: '#8C8F97', + }, + '500': { + value: '#596773', + }, + '550': { + value: '#7E8188', + }, + '600': { + value: '#454F59', + }, + '650': { + value: '#505258', + }, + '700': { + value: '#38414A', + }, + '750': { + value: '#242528', + }, + '800': { + value: '#22272B', + }, + '900': { + value: '#161A1D', + }, + '1000': { + value: '#101214', + }, + }, + red: { + '100': { + value: '#FFECEB', + }, + '200': { + value: '#FFD5D2', + }, + '300': { + value: '#FD9891', + }, + '400': { + value: '#F87168', + }, + '500': { + value: '#F15B50', + }, + '600': { + value: '#E2483D', + }, + '650': { + value: '#C9372C', + }, + '700': { + value: '#991919', + }, + '800': { + value: '#AE2E24', + }, + '900': { + value: '#5D1F1A', + }, + '1000': { + value: '#42221F', + }, + }, + orange: { + '100': { + value: '#FFF3EB', + }, + '200': { + value: '#FEDEC8', + }, + '300': { + value: '#FEC195', + }, + '400': { + value: '#FEA362', + }, + '500': { + value: '#F38A3F', + }, + '600': { + value: '#E56910', + }, + '700': { + value: '#C25100', + }, + '800': { + value: '#A54800', + }, + '900': { + value: '#702E00', + }, + '1000': { + value: '#38291E', + }, + }, + yellow: { + '100': { + value: '#FFF7D6', + }, + '200': { + value: '#F8E6A0', + }, + '300': { + value: '#F5CD47', + }, + '350': { + value: '#FBC828', + }, + '400': { + value: '#E2B203', + }, + '500': { + value: '#CF9F02', + }, + '600': { + value: '#B38600', + }, + '700': { + value: '#946F00', + }, + '800': { + value: '#7F5F01', + }, + '900': { + value: '#533F04', + }, + '1000': { + value: '#332E1B', + }, + }, + lime: { + '100': { + value: '#EFFFD6', + }, + '200': { + value: '#D3F1A7', + }, + '300': { + value: '#B3DF72', + }, + '400': { + value: '#94C748', + }, + '500': { + value: '#82B536', + }, + '600': { + value: '#6A9A23', + }, + '700': { + value: '#5B7F24', + }, + '800': { + value: '#4C6B1F', + }, + '900': { + value: '#37471F', + }, + '1000': { + value: '#28311B', + }, + }, + green: { + '100': { + value: '#DCFFF1', + }, + '200': { + value: '#BAF3DB', + }, + '300': { + value: '#7EE2B8', + }, + '400': { + value: '#4BCE97', + }, + '500': { + value: '#2ABB7F', + }, + '600': { + value: '#22A06B', + }, + '700': { + value: '#1F845A', + }, + '800': { + value: '#216E4E', + }, + '900': { + value: '#164B35', + }, + '1000': { + value: '#1C3329', + }, + }, + teal: { + '100': { + value: '#E7F9FF', + }, + '200': { + value: '#C6EDFB', + }, + '300': { + value: '#9DD9EE', + }, + '400': { + value: '#6CC3E0', + }, + '500': { + value: '#42B2D7', + }, + '600': { + value: '#2898BD', + }, + '700': { + value: '#227D9B', + }, + '800': { + value: '#206A83', + }, + '900': { + value: '#164555', + }, + '1000': { + value: '#1E3137', + }, + }, + blue: { + '100': { + value: '#E9F2FF', + }, + '200': { + value: '#CCE0FF', + }, + '300': { + value: '#85B8FF', + }, + '325': { + value: '#8FB8F6', + }, + '375': { + value: '#669DF1', + }, + '400': { + value: '#579DFF', + }, + '450': { + value: '#4688EC', + }, + '500': { + value: '#388BFF', + }, + '600': { + value: '#1D7AFC', + }, + '700': { + value: '#0C66E4', + }, + '725': { + value: '#1868DB', + }, + '775': { + value: '#1558BC', + }, + '800': { + value: '#0055CC', + }, + '900': { + value: '#09326C', + }, + '1000': { + value: '#1C2B41', + }, + }, + purple: { + '100': { + value: '#F3F0FF', + }, + '200': { + value: '#DFD8FD', + }, + '300': { + value: '#B8ACF6', + }, + '400': { + value: '#9F8FEF', + }, + '500': { + value: '#8F7EE7', + }, + '600': { + value: '#8270DB', + }, + '700': { + value: '#6E5DC6', + }, + '800': { + value: '#5E4DB2', + }, + '900': { + value: '#352C63', + }, + '1000': { + value: '#2B273F', + }, + }, + magenta: { + '100': { + value: '#FFECF8', + }, + '200': { + value: '#FDD0EC', + }, + '300': { + value: '#F797D2', + }, + '400': { + value: '#E774BB', + }, + '500': { + value: '#DA62AC', + }, + '600': { + value: '#CD519D', + }, + '700': { + value: '#AE4787', + }, + '800': { + value: '#943D73', + }, + '900': { + value: '#50253F', + }, + '1000': { + value: '#3D2232', + }, + }, + neutrals: { + '0': { + value: '#FFFFFF', + }, + '100': { + value: '#F7F8F9', + }, + '200': { + value: '#F1F2F4', + }, + '300': { + value: '#DCDFE4', + }, + '400': { + value: '#B3B9C4', + }, + '500': { + value: '#8590A2', + }, + '600': { + value: '#758195', + }, + '700': { + value: '#626F86', + }, + '800': { + value: '#44546F', + }, + '900': { + value: '#2C3E5D', + }, + '1000': { + value: '#172B4D', + }, + '1100': { + value: '#091E42', + }, + '100A': { + value: '#091E4208', + }, + '200A': { + value: '#091E420F', + }, + '300A': { + value: '#091E4224', + }, + '400A': { + value: '#091E424F', + }, + '500A': { + value: '#091E427D', + }, + }, + darkneutrals: { + '-100': { + value: '#101214', + }, + '0': { + value: '#161A1D', + }, + '100': { + value: '#1D2125', + }, + '200': { + value: '#22272B', + }, + '250': { + value: '#282E33', + }, + '300': { + value: '#2C333A', + }, + '350': { + value: '#38414A', + }, + '400': { + value: '#454F59', + }, + '500': { + value: '#596773', + }, + '600': { + value: '#738496', + }, + '700': { + value: '#8C9BAB', + }, + '800': { + value: '#9FADBC', + }, + '900': { + value: '#B6C2CF', + }, + '1000': { + value: '#C7D1DB', + }, + '1100': { + value: '#DEE4EA', + }, + '-100A': { + value: '#03040442', + }, + '100A': { + value: '#BCD6F00A', + }, + '200A': { + value: '#A1BDD914', + }, + '250A': { + value: '#C8E1F91A', + }, + '300A': { + value: '#A6C5E229', + }, + '350A': { + value: '#C3DEFE33', + }, + '400A': { + value: '#BFDBF847', + }, + '500A': { + value: '#9BB4CA80', + }, + }, +}) diff --git a/thingconnect.pulse.client/src/components/ui/theme/global.css b/thingconnect.pulse.client/src/components/ui/theme/global.css new file mode 100644 index 0000000..83a73b2 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/theme/global.css @@ -0,0 +1,46 @@ +/* Enterprise Theme Global Styles */ + +/* Font smoothing for better text rendering */ +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Improve focus visibility for accessibility */ +*:focus-visible { + outline: 2px solid var(--chakra-colors-brand-solid); + outline-offset: 2px; +} + +/* Smooth transitions for theme switching */ +* { + transition: + background-color 0.2s ease-in-out, + border-color 0.2s ease-in-out, + color 0.2s ease-in-out; +} + +/* Improve scrollbar styling in webkit browsers */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--chakra-colors-bg-muted); +} + +::-webkit-scrollbar-thumb { + background: var(--chakra-colors-border-subtle); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--chakra-colors-border); +} + +/* Better selection colors */ +::selection { + background: var(--chakra-colors-brand-emphasized); + color: var(--chakra-colors-brand-contrast); +} diff --git a/thingconnect.pulse.client/src/components/ui/theme/index.ts b/thingconnect.pulse.client/src/components/ui/theme/index.ts new file mode 100644 index 0000000..c244562 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/theme/index.ts @@ -0,0 +1,569 @@ +import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react' +import { colors } from './colors' + +const config = defineConfig({ + theme: { + tokens: { + colors: colors, + fonts: { + heading: { + value: + "'Atlassian Sans', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", + }, + body: { + value: + "'Atlassian Sans', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", + }, + mono: { + value: "SF Mono, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", + }, + }, + fontSizes: { + '2xs': { value: '0.625rem' }, // 10px + xs: { value: '0.75rem' }, // 12px + sm: { value: '0.875rem' }, // 14px + md: { value: '1rem' }, // 16px + lg: { value: '1.125rem' }, // 18px + xl: { value: '1.25rem' }, // 20px + '2xl': { value: '1.5rem' }, // 24px + '3xl': { value: '1.875rem' }, // 30px + '4xl': { value: '2.25rem' }, // 36px + '5xl': { value: '3rem' }, // 48px + '6xl': { value: '3.75rem' }, // 60px + '7xl': { value: '4.5rem' }, // 72px + }, + spacing: { + // Atlassian Design System 4px grid spacing tokens + '025': { value: '0.125rem' }, // 2px - minimal spacing + '050': { value: '0.25rem' }, // 4px - space.050 + '075': { value: '0.375rem' }, // 6px - space.075 + '100': { value: '0.5rem' }, // 8px - space.100 + '150': { value: '0.75rem' }, // 12px - space.150 + '200': { value: '1rem' }, // 16px - space.200 + '250': { value: '1.25rem' }, // 20px - space.250 + '300': { value: '1.5rem' }, // 24px - space.300 + '400': { value: '2rem' }, // 32px - space.400 + '500': { value: '2.5rem' }, // 40px - space.500 + '600': { value: '3rem' }, // 48px - space.600 + '800': { value: '4rem' }, // 64px - space.800 + '1000': { value: '5rem' }, // 80px - space.1000 + }, + radii: { + none: { value: '0' }, + sm: { value: '0.125rem' }, // 2px + base: { value: '0.1875rem' }, // 3px - Atlassian button border radius + md: { value: '0.25rem' }, // 4px + lg: { value: '0.375rem' }, // 6px + xl: { value: '0.5rem' }, // 8px + '2xl': { value: '0.75rem' }, // 12px + '3xl': { value: '1rem' }, // 16px + full: { value: '9999px' }, + }, + shadows: { + // Atlassian-inspired shadow system - subtle and clean + raised: { value: '0 1px 1px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31)' }, + overlay: { value: '0 8px 16px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31)' }, + 'raised.dark': { value: '0 1px 1px rgba(0, 0, 0, 0.25), 0 0 1px rgba(0, 0, 0, 0.31)' }, + 'overlay.dark': { value: '0 8px 16px rgba(0, 0, 0, 0.36), 0 0 1px rgba(0, 0, 0, 0.31)' }, + // Atlassian elevation shadows + xs: { value: '0 1px 1px rgba(9, 30, 66, 0.25)' }, + sm: { value: '0 1px 1px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31)' }, + base: { value: '0 4px 8px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31)' }, + md: { value: '0 8px 16px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31)' }, + lg: { value: '0 16px 24px rgba(9, 30, 66, 0.25), 0 0 1px rgba(9, 30, 66, 0.31)' }, + }, + gradients: { + // Atlassian subtle gradients for overlays and backgrounds + 'brand.subtle': { value: 'linear-gradient(135deg, #0052cc 0%, #0065ff 100%)' }, + 'accent.subtle': { value: 'linear-gradient(135deg, #00b8d9 0%, #36b37e 100%)' }, + 'surface.overlay': { value: 'linear-gradient(rgba(9, 30, 66, 0), rgba(9, 30, 66, 0.6))' }, + 'surface.overlay.light': { + value: 'linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.9))', + }, + }, + }, + semanticTokens: { + colors: { + // Blue semantic tokens + blue: { + solid: { value: '{colors.blue.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.blue.600}', + _dark: '{colors.blue.400}', + }, + }, + muted: { + value: { + _light: '{colors.blue.100}', + _dark: '{colors.blue.900}', + }, + }, + subtle: { + value: { + _light: '{colors.blue.100}', + _dark: '{colors.blue.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.blue.200}', + _dark: '{colors.blue.800}', + }, + }, + focusRing: { value: '{colors.blue.500}' }, + }, + // Teal semantic tokens + teal: { + solid: { value: '{colors.teal.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.teal.600}', + _dark: '{colors.teal.400}', + }, + }, + muted: { + value: { + _light: '{colors.teal.50}', + _dark: '{colors.teal.1000}', + }, + }, + subtle: { + value: { + _light: '{colors.teal.100}', + _dark: '{colors.teal.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.teal.200}', + _dark: '{colors.teal.800}', + }, + }, + focusRing: { value: '{colors.teal.500}' }, + }, + // Green semantic tokens + green: { + solid: { value: '{colors.green.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.green.600}', + _dark: '{colors.green.400}', + }, + }, + muted: { + value: { + _light: '{colors.green.50}', + _dark: '{colors.green.1000}', + }, + }, + subtle: { + value: { + _light: '{colors.green.100}', + _dark: '{colors.green.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.green.200}', + _dark: '{colors.green.800}', + }, + }, + focusRing: { value: '{colors.green.500}' }, + }, + // Yellow semantic tokens + yellow: { + solid: { value: '{colors.yellow.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.yellow.600}', + _dark: '{colors.yellow.400}', + }, + }, + muted: { + value: { + _light: '{colors.yellow.50}', + _dark: '{colors.yellow.1000}', + }, + }, + subtle: { + value: { + _light: '{colors.yellow.100}', + _dark: '{colors.yellow.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.yellow.200}', + _dark: '{colors.yellow.800}', + }, + }, + focusRing: { value: '{colors.yellow.500}' }, + }, + // Red semantic tokens + red: { + solid: { value: '{colors.red.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.red.600}', + _dark: '{colors.red.400}', + }, + }, + muted: { + value: { + _light: '{colors.red.50}', + _dark: '{colors.red.1000}', + }, + }, + subtle: { + value: { + _light: '{colors.red.100}', + _dark: '{colors.red.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.red.200}', + _dark: '{colors.red.800}', + }, + }, + focusRing: { value: '{colors.red.500}' }, + }, + // Purple semantic tokens + purple: { + solid: { value: '{colors.purple.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.purple.600}', + _dark: '{colors.purple.400}', + }, + }, + muted: { + value: { + _light: '{colors.purple.50}', + _dark: '{colors.purple.1000}', + }, + }, + subtle: { + value: { + _light: '{colors.purple.100}', + _dark: '{colors.purple.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.purple.200}', + _dark: '{colors.purple.800}', + }, + }, + focusRing: { value: '{colors.purple.500}' }, + }, + // Neutral semantic tokens + neutral: { + solid: { value: '{colors.neutral.500}' }, + contrast: { value: 'white' }, + fg: { + value: { + _light: '{colors.neutral.600}', + _dark: '{colors.neutral.400}', + }, + }, + muted: { + value: { + _light: '{colors.neutral.50}', + _dark: '{colors.neutral.950}', + }, + }, + subtle: { + value: { + _light: '{colors.neutral.100}', + _dark: '{colors.neutral.900}', + }, + }, + emphasized: { + value: { + _light: '{colors.neutral.200}', + _dark: '{colors.neutral.800}', + }, + }, + focusRing: { value: '{colors.neutral.500}' }, + }, + // Atlassian-style surface semantic tokens using neutral palette + 'color.background.default': { + value: { + _light: '{colors.neutral.0}', // N0 - Pure white + _dark: '{colors.neutral.1000}', // N1000 - Darkest background + }, + }, + 'color.background.sunken': { + value: { + _light: '{colors.neutral.20}', // N20 - Light gray sunken + _dark: '{colors.blue.1000}', // B1000 - Dark blue sunken + }, + }, + 'color.background.raised': { + value: { + _light: '{colors.neutral.0}', // N0 - Pure white raised + _dark: '{colors.blue.800}', // B800 - Dark blue raised + }, + }, + 'color.background.overlay': { + value: { + _light: '{colors.neutral.0}', // N0 - Pure white overlay + _dark: '{colors.blue.700}', // B700 - Dark blue overlay + }, + }, + // Legacy background tokens for compatibility with Atlassian neutrals + bg: { + value: { + _light: '{colors.neutral.20}', // N20 - Light gray background + _dark: '{colors.neutral.1000}', // N1000 - Dark background + }, + }, + 'bg.panel': { + value: { + _light: '{colors.neutral.0}', // N0 - Pure white + _dark: '{colors.neutral.900}', // N900 - Dark panel + }, + }, + fg: { + value: { + _light: '{colors.neutral.1000}', // N1000 - Dark text on light + _dark: '{colors.neutral.0}', // N0 - Light text on dark + }, + }, + 'fg.muted': { + value: { + _light: '{colors.neutral.600}', // N600 - Muted text + _dark: '{colors.neutral.400}', // N400 - Muted text dark + }, + }, + 'fg.subtle': { + value: { + _light: '{colors.neutral.500}', // N500 - Subtle text + _dark: '{colors.neutral.500}', // N500 - Subtle text + }, + }, + border: { + value: { + _light: '{colors.neutral.40}', // N40 - Light border + _dark: '{colors.neutral.700}', // N700 - Dark border + }, + }, + 'border.muted': { + value: { + _light: '{colors.neutral.30}', // N30 - Muted border + _dark: '{colors.neutral.800}', // N800 - Muted dark border + }, + }, + 'border.subtle': { + value: { + _light: '{colors.neutral.50}', // N50 - Subtle border + _dark: '{colors.neutral.600}', // N600 - Subtle dark border + }, + }, + }, + }, + textStyles: { + // Atlassian typography scale - clean, accessible typography + 'ui.heading.xxlarge': { + value: { + fontSize: '7xl', // 72px + fontWeight: '600', // Atlassian medium weight + lineHeight: '1.125', + letterSpacing: '-0.01em', + }, + }, + 'ui.heading.xlarge': { + value: { + fontSize: '6xl', // 60px + fontWeight: '600', + lineHeight: '1.133', + letterSpacing: '-0.008em', + }, + }, + 'ui.heading.large': { + value: { + fontSize: '5xl', // 48px + fontWeight: '600', + lineHeight: '1.167', + letterSpacing: '-0.006em', + }, + }, + 'ui.heading.medium': { + value: { + fontSize: '4xl', // 36px + fontWeight: '500', // Atlassian medium + lineHeight: '1.222', + letterSpacing: '-0.004em', + }, + }, + 'ui.heading.small': { + value: { + fontSize: '3xl', // 30px + fontWeight: '500', + lineHeight: '1.267', + letterSpacing: '-0.002em', + }, + }, + 'ui.heading.xsmall': { + value: { + fontSize: '2xl', // 24px + fontWeight: '500', + lineHeight: '1.333', + }, + }, + 'ui.heading.xxsmall': { + value: { + fontSize: 'xl', // 20px + fontWeight: '500', + lineHeight: '1.4', + }, + }, + // Atlassian body text styles - optimized for readability + 'ui.body.large': { + value: { + fontSize: 'lg', // 18px + fontWeight: '400', + lineHeight: '1.556', // ~28px line height + }, + }, + 'ui.body.medium': { + value: { + fontSize: 'md', // 16px + fontWeight: '400', + lineHeight: '1.5', // 24px line height + }, + }, + 'ui.body.small': { + value: { + fontSize: 'sm', // 14px + fontWeight: '400', + lineHeight: '1.429', // ~20px line height + }, + }, + // Helper text styles + 'ui.helper': { + value: { + fontSize: 'xs', // 12px + fontWeight: '400', + lineHeight: '1.333', // 16px line height + }, + }, + // Code text style + 'ui.code': { + value: { + fontSize: 'sm', // 14px + fontWeight: '400', + lineHeight: '1.571', // ~22px + fontFamily: 'mono', + }, + }, + }, + layerStyles: { + // Atlassian surface styles - clean and accessible + 'atlassian.card': { + value: { + bg: 'color.background.raised', + borderRadius: 'base', // 3px - Atlassian border radius + border: '1px solid', + borderColor: 'border.muted', + boxShadow: 'raised', + p: '200', // 16px - consistent spacing + }, + }, + 'atlassian.panel': { + value: { + bg: 'color.background.sunken', + borderRadius: 'base', + border: '1px solid', + borderColor: 'border.subtle', + p: '300', // 24px + }, + }, + 'atlassian.header': { + value: { + bg: 'color.background.default', + borderBottom: '1px solid', + borderColor: 'border.muted', + px: '200', // 16px + py: '150', // 12px + minHeight: '44px', // Atlassian touch target + }, + }, + 'atlassian.modal': { + value: { + bg: 'color.background.overlay', + borderRadius: 'lg', // Larger radius for modals + boxShadow: 'overlay', + p: '300', // 24px + }, + }, + 'atlassian.button.primary': { + value: { + bg: 'blue.500', + color: 'white', + borderRadius: 'base', // 3px + px: '200', // 16px + py: '100', // 8px + minHeight: '32px', // Atlassian button height + }, + }, + 'atlassian.button.secondary': { + value: { + bg: 'transparent', + color: 'blue.500', + border: '1px solid', + borderColor: 'blue.500', + borderRadius: 'base', // 3px + px: '200', // 16px + py: '100', // 8px + minHeight: '32px', // Atlassian button height + }, + }, + // Legacy styles for backward compatibility + 'enterprise.card': { + value: { + bg: 'color.background.raised', + borderRadius: 'base', + boxShadow: 'raised', + p: '300', + }, + }, + 'enterprise.header': { + value: { + bg: 'color.background.default', + borderBottom: '1px solid', + borderColor: 'border', + px: '300', + py: '200', + }, + }, + }, + }, + globalCss: { + html: { + colorScheme: 'light dark', + }, + body: { + bg: 'bg', + color: 'fg', + fontFamily: 'body', + lineHeight: '1.6', + }, + '*': { + borderColor: 'border', + }, + '*::placeholder': { + color: 'fg.muted', + }, + }, +}) + +export const system = createSystem(defaultConfig, config) diff --git a/thingconnect.pulse.client/src/components/ui/timeline.tsx b/thingconnect.pulse.client/src/components/ui/timeline.tsx new file mode 100644 index 0000000..b8dd3d1 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/timeline.tsx @@ -0,0 +1,20 @@ +import { Timeline as ChakraTimeline } from '@chakra-ui/react' +import * as React from 'react' + +export const TimelineConnector = React.forwardRef( + function TimelineConnector(props, ref) { + return ( + + + + + ) + } +) + +export const TimelineRoot = ChakraTimeline.Root +export const TimelineContent = ChakraTimeline.Content +export const TimelineItem = ChakraTimeline.Item +export const TimelineIndicator = ChakraTimeline.Indicator +export const TimelineTitle = ChakraTimeline.Title +export const TimelineDescription = ChakraTimeline.Description diff --git a/thingconnect.pulse.client/src/components/ui/toaster.tsx b/thingconnect.pulse.client/src/components/ui/toaster.tsx index f46100b..0a4b7c3 100644 --- a/thingconnect.pulse.client/src/components/ui/toaster.tsx +++ b/thingconnect.pulse.client/src/components/ui/toaster.tsx @@ -1,34 +1,24 @@ -"use client" +'use client' -import { - Toaster as ChakraToaster, - Portal, - Spinner, - Stack, - Toast, -} from "@chakra-ui/react" -import { toaster } from "../../lib/toaster" +import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast } from '@chakra-ui/react' +import { toaster } from '../../lib/toaster' export const Toaster = () => { return ( - + {(toast) => ( - - {toast.type === "loading" ? ( + + {toast.type === 'loading' ? ( ) : ( )} {toast.title && {toast.title}} - {toast.description && ( - {toast.description} - )} + {toast.description && {toast.description}} - {toast.action && ( - {toast.action.label} - )} + {toast.action && {toast.action.label}} {toast.closable && } )} diff --git a/thingconnect.pulse.client/src/components/ui/toggle-tip.tsx b/thingconnect.pulse.client/src/components/ui/toggle-tip.tsx new file mode 100644 index 0000000..32773b5 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/toggle-tip.tsx @@ -0,0 +1,54 @@ +import { + Popover as ChakraPopover, + IconButton, + type IconButtonProps, + Portal, +} from '@chakra-ui/react' +import * as React from 'react' +import { HiOutlineInformationCircle } from 'react-icons/hi' + +export interface ToggleTipProps extends ChakraPopover.RootProps { + showArrow?: boolean + portalled?: boolean + portalRef?: React.RefObject + content?: React.ReactNode +} + +export const ToggleTip = React.forwardRef( + function ToggleTip(props, ref) { + const { showArrow, children, portalled = true, content, portalRef, ...rest } = props + + return ( + + {children} + + + + {showArrow && ( + + + + )} + {content} + + + + + ) + } +) + +export interface InfoTipProps extends Partial { + buttonProps?: IconButtonProps | undefined +} + +export const InfoTip = React.forwardRef(function InfoTip(props, ref) { + const { children, buttonProps, ...rest } = props + return ( + + + + + + ) +}) diff --git a/thingconnect.pulse.client/src/components/ui/toggle.tsx b/thingconnect.pulse.client/src/components/ui/toggle.tsx new file mode 100644 index 0000000..0e02aa9 --- /dev/null +++ b/thingconnect.pulse.client/src/components/ui/toggle.tsx @@ -0,0 +1,44 @@ +'use client' + +import type { ButtonProps } from '@chakra-ui/react' +import { Button, Toggle as ChakraToggle, useToggleContext } from '@chakra-ui/react' +import * as React from 'react' + +interface ToggleProps extends ChakraToggle.RootProps { + variant?: keyof typeof variantMap + size?: ButtonProps['size'] +} + +const variantMap = { + solid: { on: 'solid', off: 'outline' }, + surface: { on: 'surface', off: 'outline' }, + subtle: { on: 'subtle', off: 'ghost' }, + ghost: { on: 'subtle', off: 'ghost' }, +} as const + +export const Toggle = React.forwardRef(function Toggle(props, ref) { + const { variant = 'subtle', size, children, ...rest } = props + const variantConfig = variantMap[variant] + + return ( + + + {children} + + + ) +}) + +interface ToggleBaseButtonProps extends Omit { + variant: Record<'on' | 'off', ButtonProps['variant']> +} + +const ToggleBaseButton = React.forwardRef( + function ToggleBaseButton(props, ref) { + const toggle = useToggleContext() + const { variant, ...rest } = props + return +
+ + + ) +} + +export default DashboardPage diff --git a/thingconnect.pulse.client/src/pages/ThemeShowcasePage.tsx b/thingconnect.pulse.client/src/pages/ThemeShowcasePage.tsx new file mode 100644 index 0000000..11f1847 --- /dev/null +++ b/thingconnect.pulse.client/src/pages/ThemeShowcasePage.tsx @@ -0,0 +1,50 @@ +import { Box, Heading, Stack, Button } from '@chakra-ui/react' +import { useColorMode } from '@/components/ui/color-mode' +import { Tooltip } from '@/components/ui/tooltip' +import _ from 'lodash' + +const ThemeShowcasePage = () => { + const { colorMode, toggleColorMode } = useColorMode() + const colorFamilies = ['blue', 'red', 'green', 'yellow', 'teal', 'purple', 'neutral'] + + return ( + + + {/* Header */} + + + Color Palette + + + + + {/* Color Grid - Columns like screenshot */} + + + {colorFamilies.map((family) => ( + + {_.range(100, 1000, 100).map((value) => ( + + + + ))} + + ))} + + + + + ) +} + +export default ThemeShowcasePage diff --git a/thingconnect.pulse.client/src/services/authService.ts b/thingconnect.pulse.client/src/services/authService.ts index d777183..99e73eb 100644 --- a/thingconnect.pulse.client/src/services/authService.ts +++ b/thingconnect.pulse.client/src/services/authService.ts @@ -1,69 +1,71 @@ -import axios from 'axios'; +import axios from 'axios' const API_BASE_URL = '/api'; export interface AuthResponse { - token: string; - username: string; - email: string; + token: string + username: string + email: string } export interface LoginRequest { - username: string; - password: string; + username: string + password: string } export interface RegisterRequest { - username: string; - email: string; - password: string; + username: string + email: string + password: string } export interface UserProfile { - id: number; - username: string; - email: string; - createdAt: string; + id: number + username: string + email: string + createdAt: string } class AuthService { - private token: string | null = null; + private token: string | null = null constructor() { - this.token = localStorage.getItem('token'); - this.setupAxiosInterceptors(); + this.token = localStorage.getItem('token') + this.setupAxiosInterceptors() } private setupAxiosInterceptors() { axios.interceptors.request.use((config) => { if (this.token) { - config.headers.Authorization = `Bearer ${this.token}`; + config.headers.Authorization = `Bearer ${this.token}` } - return config; - }); + return config + }) axios.interceptors.response.use( (response) => response, (error: unknown) => { if ((error as { response?: { status?: number } })?.response?.status === 401) { - this.logout(); + this.logout() } - return Promise.reject(error); + return Promise.reject( + new Error((error as { message?: string })?.message || 'An error occurred') + ) } - ); + ) } async login(username: string, password: string): Promise { const response = await axios.post(`${API_BASE_URL}/auth/login`, { username, password, - }); + }) - const { token } = response.data; - this.token = token; - localStorage.setItem('token', token); + const { token } = response.data + this.token = token + localStorage.setItem('token', token) - return response.data; + return response.data } async register(username: string, email: string, password: string): Promise { @@ -71,32 +73,32 @@ class AuthService { username, email, password, - }); + }) - const { token } = response.data; - this.token = token; - localStorage.setItem('token', token); + const { token } = response.data + this.token = token + localStorage.setItem('token', token) - return response.data; + return response.data } async getUserProfile(): Promise { - const response = await axios.get(`${API_BASE_URL}/user/profile`); - return response.data; + const response = await axios.get(`${API_BASE_URL}/user/profile`) + return response.data } logout(): void { - this.token = null; - localStorage.removeItem('token'); + this.token = null + localStorage.removeItem('token') } isAuthenticated(): boolean { - return this.token !== null; + return this.token !== null } getToken(): string | null { - return this.token; + return this.token } } -export const authService = new AuthService(); \ No newline at end of file +export const authService = new AuthService()