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?{' '}
+
+ Register here
+
+
+
+
- );
-};
\ 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)',
+ }}
+ />
+
-
- Register
-
+
+
+ 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?{' '}
-
- Login here
+
+
+ Confirm Password *
+
+ setConfirmPassword(e.target.value)}
+ placeholder="Confirm your password"
+ required
+ bg="bg.panel"
+ borderColor="border"
+ _focus={{
+ borderColor: 'brand.solid',
+ boxShadow: '0 0 0 1px var(--chakra-colors-brand-solid)',
+ }}
+ />
+ {passwordMismatch && (
+
+ Passwords do not match
+
+ )}
+
+
+
+ Register
-
-
+
+
+ Already have an account?{' '}
+
+ Login here
+
+
+
+
- );
-};
\ 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 (
+
+
+
+ {({ acceptedFiles }) => {
+ if (acceptedFiles.length === 1) {
+ return {acceptedFiles[0].name}
+ }
+ if (acceptedFiles.length > 1) {
+ return {acceptedFiles.length} files
+ }
+ return {placeholder}
+ }}
+
+
+
+ )
+ }
+)
+
+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 (
+
+ )
+}
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 (
+
+
+ {props.value}
+
+
+ )
+ }
+)
+
+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
+
+
+
+ colorMode === 'dark' && toggleColorMode()}
+ flex={1}
+ >
+
+ Light Mode
+
+
+ colorMode === 'light' && toggleColorMode()}
+ flex={1}
+ >
+
+ Dark Mode
+
+
+
+
+
+ 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
+
+ Primary Action
+
+
+
+ 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 const ToggleIndicator = ChakraToggle.Indicator
diff --git a/thingconnect.pulse.client/src/components/ui/tooltip.tsx b/thingconnect.pulse.client/src/components/ui/tooltip.tsx
index 6156874..a67aaf3 100644
--- a/thingconnect.pulse.client/src/components/ui/tooltip.tsx
+++ b/thingconnect.pulse.client/src/components/ui/tooltip.tsx
@@ -1,5 +1,5 @@
-import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
-import * as React from "react"
+import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'
+import * as React from 'react'
export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean
@@ -10,35 +10,38 @@ export interface TooltipProps extends ChakraTooltip.RootProps {
disabled?: boolean
}
-export const Tooltip = function Tooltip({ ref, ...props }: TooltipProps & { ref?: React.RefObject }) {
- const {
- showArrow,
- children,
- disabled,
- portalled = true,
- content,
- contentProps,
- portalRef,
- ...rest
- } = props
+export const Tooltip = function Tooltip({
+ ref,
+ ...props
+}: TooltipProps & { ref?: React.RefObject }) {
+ const {
+ showArrow,
+ children,
+ disabled,
+ portalled = true,
+ content,
+ contentProps,
+ portalRef,
+ ...rest
+ } = props
- if (disabled) return children
+ if (disabled) return children
- return (
-
- {children}
-
-
-
- {showArrow && (
-
-
-
- )}
- {content}
-
-
-
-
- )
- }
+ return (
+
+ {children}
+
+
+
+ {showArrow && (
+
+
+
+ )}
+ {content}
+
+
+
+
+ )
+}
diff --git a/thingconnect.pulse.client/src/lib/color-mode.ts b/thingconnect.pulse.client/src/lib/color-mode.ts
index d6fe59d..1f71afb 100644
--- a/thingconnect.pulse.client/src/lib/color-mode.ts
+++ b/thingconnect.pulse.client/src/lib/color-mode.ts
@@ -1,27 +1,27 @@
-import { useTheme } from "next-themes";
+import { useTheme } from 'next-themes'
-export type ColorMode = "light" | "dark";
+export type ColorMode = 'light' | 'dark'
export interface UseColorModeReturn {
- colorMode: ColorMode;
- setColorMode: (colorMode: ColorMode) => void;
- toggleColorMode: () => void;
+ colorMode: ColorMode
+ setColorMode: (colorMode: ColorMode) => void
+ toggleColorMode: () => void
}
export function useColorMode(): UseColorModeReturn {
- const { resolvedTheme, setTheme, forcedTheme } = useTheme();
- const colorMode = forcedTheme || resolvedTheme;
+ const { resolvedTheme, setTheme, forcedTheme } = useTheme()
+ const colorMode = forcedTheme || resolvedTheme
const toggleColorMode = () => {
- setTheme(resolvedTheme === "dark" ? "light" : "dark");
- };
+ setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
+ }
return {
colorMode: colorMode as ColorMode,
setColorMode: setTheme,
toggleColorMode,
- };
+ }
}
export function useColorModeValue(light: T, dark: T) {
- const { colorMode } = useColorMode();
- return colorMode === "dark" ? dark : light;
-}
\ No newline at end of file
+ const { colorMode } = useColorMode()
+ return colorMode === 'dark' ? dark : light
+}
diff --git a/thingconnect.pulse.client/src/lib/toaster.ts b/thingconnect.pulse.client/src/lib/toaster.ts
index d3a6b0f..4ca24d7 100644
--- a/thingconnect.pulse.client/src/lib/toaster.ts
+++ b/thingconnect.pulse.client/src/lib/toaster.ts
@@ -1,6 +1,6 @@
-import { createToaster } from "@chakra-ui/react";
+import { createToaster } from '@chakra-ui/react'
export const toaster = createToaster({
- placement: "bottom-end",
+ placement: 'bottom-end',
pauseOnPageIdle: true,
-});
\ No newline at end of file
+})
diff --git a/thingconnect.pulse.client/src/main.tsx b/thingconnect.pulse.client/src/main.tsx
index a797eeb..82f855e 100644
--- a/thingconnect.pulse.client/src/main.tsx
+++ b/thingconnect.pulse.client/src/main.tsx
@@ -1,12 +1,16 @@
import { StrictMode } from 'react'
-import { Provider } from "@/components/ui/provider"
+import { Provider } from '@/components/ui/provider'
import { createRoot } from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
+import './components/ui/theme/global.css'
createRoot(document.getElementById('root')!).render(
-
+
+
+
)
diff --git a/thingconnect.pulse.client/src/pages/DashboardPage.tsx b/thingconnect.pulse.client/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..fc5110e
--- /dev/null
+++ b/thingconnect.pulse.client/src/pages/DashboardPage.tsx
@@ -0,0 +1,48 @@
+import { Box, Button, Heading, Text, VStack, HStack, Link } from '@chakra-ui/react'
+import { Link as RouterLink } from 'react-router-dom'
+import { Logo } from '@/components/ui/logo'
+import { authService } from '@/services/authService'
+
+function DashboardPage() {
+ const handleLogout = () => {
+ authService.logout()
+ window.location.href = '/login'
+ }
+
+ return (
+
+
+
+
+
+ Welcome to Pulse
+
+
+ You are successfully logged in! This is where your main application content will go.
+
+
+
+
+
+ Enterprise Dashboard
+
+
+ Your authenticated dashboard content goes here. The theme automatically adapts to light
+ and dark modes using Atlassian Design System principles.
+
+
+ View Theme Showcase →
+
+
+
+
+
+ Logout
+
+
+
+
+ )
+}
+
+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
+
+
+ {colorMode === 'light' ? 'Dark' : 'Light'}
+
+
+
+ {/* 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()