diff --git a/.claude/launch.json b/.claude/launch.json index 42db145..6bf0d19 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,9 +4,8 @@ { "name": "admin-solid", "runtimeExecutable": "sh", - "runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev -- --port $PORT"], - "port": 3002, - "autoPort": true + "runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev -- --port 3002"], + "port": 3002 } ] } diff --git a/package-lock.json b/package-lock.json index de28e60..4fb7976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.3.2", + "@tailwindcss/vite": "^4.2.2", + "lucide-solid": "^1.0.1", "solid-js": "^1.9.5", + "tailwindcss": "^4.2.2", "vinxi": "^0.5.7" }, "devDependencies": { @@ -355,7 +358,6 @@ "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" @@ -367,7 +369,6 @@ "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -378,7 +379,6 @@ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -968,7 +968,6 @@ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", @@ -2311,6 +2310,272 @@ "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", "license": "CC0-1.0" }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@tanstack/directive-functions-plugin": { "version": "1.121.21", "resolved": "https://registry.npmjs.org/@tanstack/directive-functions-plugin/-/directive-functions-plugin-1.121.21.tgz", @@ -2391,7 +2656,6 @@ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4009,6 +4273,19 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -5019,7 +5296,6 @@ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -5056,7 +5332,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5077,7 +5352,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5098,7 +5372,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5119,7 +5392,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5140,7 +5412,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5161,7 +5432,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5182,7 +5452,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5203,7 +5472,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5224,7 +5492,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5245,7 +5512,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5266,7 +5532,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5401,6 +5666,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-solid": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lucide-solid/-/lucide-solid-1.0.1.tgz", + "integrity": "sha512-fpo0UmEHdEmdvydbFS9lQUqQ9rqSqWXMY4w3n9tVr9FqBBWJB0YI/lTMSxCspHs706ZzHIS/1YEnDhLOIP+m+A==", + "license": "ISC", + "peerDependencies": { + "solid-js": "^1.4.7" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -7355,6 +7629,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.1.tgz", + "integrity": "sha512-b+u3CEM6FjDHru+nhUSjDofpWSBp2rINziJWgApm72wwGasQ/wKXftZe4tI2Y5HPv6OpzXSZHOFq87H4vfsgsw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "7.5.11", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", diff --git a/package.json b/package.json index 52c761c..c61e686 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.3.2", + "@tailwindcss/vite": "^4.2.2", + "lucide-solid": "^1.0.1", "solid-js": "^1.9.5", + "tailwindcss": "^4.2.2", "vinxi": "^0.5.7" }, "engines": { diff --git a/scripts/cleanup-css.mjs b/scripts/cleanup-css.mjs new file mode 100644 index 0000000..14cc98a --- /dev/null +++ b/scripts/cleanup-css.mjs @@ -0,0 +1,167 @@ +#!/usr/bin/env node +/** + * Replace old hand-written CSS class names with Tailwind v4 equivalents. + * SAFE version: only replaces inside class="..." attribute values. + * + * Run: node scripts/cleanup-css.mjs + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { globSync } from 'glob'; + +// Order matters: most-specific (multi-word) patterns first. +// Each entry: [old-class-string, new-tailwind-classes] +const REPLACEMENTS = [ + // ── Page structure ────────────────────────────────────────────── + ['page-hero-card page-actions', 'mb-6 flex items-start justify-between gap-4'], + ['page-actions-right', 'flex items-center gap-2'], + ['page-actions', 'mb-6 flex items-start justify-between gap-4'], + ['page-title-block', 'flex flex-col gap-1'], + ['page-title', 'text-2xl font-bold text-gray-900'], + ['page-subtitle', 'mt-1 text-sm text-gray-500'], + + // ── Card ──────────────────────────────────────────────────────── + ['card', 'rounded-xl border border-gray-200 bg-white shadow-sm'], + + // ── Buttons (specific before generic) ─────────────────────────── + ['btn navy', 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'], + ['btn primary', 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'], + ['btn danger', 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors'], + ['btn sm', 'inline-flex items-center rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 transition-colors'], + ['btn', 'inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors'], + + // ── Table ──────────────────────────────────────────────────────── + ['table-wrap', 'overflow-x-auto'], + ['list-table', 'w-full text-sm'], + ['perm-table', 'w-full text-sm'], + ['table-actions', 'flex items-center justify-end gap-1'], + ['align-right', 'text-right'], + ['row-selected', 'bg-orange-50'], + + // ── Icon action buttons ────────────────────────────────────────── + ['action-icon-btn danger', 'rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm'], + ['action-icon-btn', 'rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm'], + + // ── Status chips (specific before generic) ─────────────────────── + ['status-chip active', 'inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700'], + ['status-chip pending', 'inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-700'], + ['status-chip inactive', 'inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500'], + ['status-chip', 'inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600'], + + // ── Error / notice ─────────────────────────────────────────────── + ['error-box', 'mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'], + + // ── Form sections ──────────────────────────────────────────────── + ['role-form-section', 'mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm'], + ['field-grid-2', 'mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2'], + ['module-picker', 'mt-3 flex flex-wrap gap-2'], + ['module-chip selected', 'flex cursor-pointer items-center gap-1.5 rounded-full border border-orange-300 bg-orange-50 px-3 py-1.5 text-xs font-medium text-orange-700'], + ['module-chip', 'flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300'], + + // ── Legacy tab nav (now handled by AdminShell) ──────────────────── + ['admin-link-tabs', 'hidden'], + ['admin-link-tab active', 'hidden'], + ['admin-link-tab', 'hidden'], +]; + +/** + * Replace old CSS class names only inside JSX class attribute values. + * Handles: + * class="foo bar baz" + * class={'foo bar baz'} + * class={`foo bar baz`} + * class={condition ? 'foo' : 'bar'} + * + * We do this by extracting the string portion of class attributes and + * replacing only within those extracted portions. + */ +function replaceInClassAttributes(src) { + let changed = false; + + // Process class="..." and class='...' (static strings) + src = src.replace(/class=["']([^"']*)["']/g, (match, classes) => { + let replaced = classes; + for (const [from, to] of REPLACEMENTS) { + // Replace whole-word occurrences within the class string + // Use word boundaries to avoid partial matches + const re = new RegExp(`(? { + let replaced = classes; + for (const [from, to] of REPLACEMENTS) { + const re = new RegExp(`(? { + let replaced = classes; + for (const [from, to] of REPLACEMENTS) { + const re = new RegExp(`(? { + // Replace inside single/double-quoted string literals within the expression + const replaced = match.replace(/'([^']*)'/g, (m, inner) => { + let r = inner; + for (const [from, to] of REPLACEMENTS) { + const re = new RegExp(`(? { + // skip JSX attributes within the expression (avoid double replacement) + let r = inner; + for (const [from, to] of REPLACEMENTS) { + const re = new RegExp(`(? = [ ]; const PAGE_TITLES: Array<{ prefix: string; title: string }> = [ - { prefix: '/admin/workspace', title: 'Dashboard Workspace' }, - { prefix: '/admin/settings', title: 'Settings' }, - { prefix: '/admin/role-modules', title: 'Role Modules' }, - { prefix: '/admin/modules', title: 'Module Management' }, - { prefix: '/admin/responses', title: 'Lead Responses' }, - { prefix: '/admin/applications', title: 'Applications' }, - { prefix: '/admin/financial', title: 'Financial Management' }, - { prefix: '/admin/help', title: 'Support Management' }, - { prefix: '/admin/verification-status', title: 'Verification Status' }, - { prefix: '/admin/verification', title: 'Verification Review' }, + { prefix: '/admin/employees', title: 'Employee Management' }, + { prefix: '/admin/department', title: 'Department Management' }, + { prefix: '/admin/designation', title: 'Designation Management' }, + { prefix: '/admin/roles', title: 'Internal Role Management' }, + { prefix: '/admin/runtime-roles', title: 'External Role Management' }, + { prefix: '/admin/onboarding-schemas', title: 'External Onboarding Management' }, + { prefix: '/admin/internal-dashboard-management', title: 'Internal Dashboard Management' }, + { prefix: '/admin/external-dashboard-management', title: 'External Dashboard Management' }, + { prefix: '/admin/role-ui-configs', title: 'External Dashboard Management' }, { prefix: '/admin/approval', title: 'Approval Management' }, { prefix: '/admin/users', title: 'Users Management' }, { prefix: '/admin/company', title: 'Company Management' }, - { prefix: '/admin/customer', title: 'Customer Management' }, { prefix: '/admin/candidate', title: 'Candidate Management' }, + { prefix: '/admin/customer', title: 'Customer Management' }, { prefix: '/admin/photographer', title: 'Photographer Management' }, { prefix: '/admin/makeup-artist', title: 'Makeup Artist Management' }, { prefix: '/admin/tutors', title: 'Tutors Management' }, { prefix: '/admin/developers', title: 'Developers Management' }, + { prefix: '/admin/video-editors', title: 'Video Editor Management' }, + { prefix: '/admin/fitness-trainers', title: 'Fitness Trainer Management' }, + { prefix: '/admin/catering-services', title: 'Catering Services Management' }, { prefix: '/admin/graphic-designers', title: 'Graphics Designer Management' }, + { prefix: '/admin/social-media-managers', title: 'Social Media Manager Management' }, { prefix: '/admin/jobs', title: 'Jobs Management' }, { prefix: '/admin/leads', title: 'Leads Management' }, - { prefix: '/admin/requirements', title: 'Requirement Request' }, { prefix: '/admin/pricing', title: 'Pricing Management' }, - { prefix: '/admin/invoice', title: 'Invoice Management' }, { prefix: '/admin/credit', title: 'Credit Management' }, - { prefix: '/admin/ledger', title: 'Ledger Management' }, + { prefix: '/admin/coupon', title: 'Coupon Management' }, + { prefix: '/admin/discount', title: 'Discount Management' }, + { prefix: '/admin/tax', title: 'Tax Management' }, + { prefix: '/admin/order', title: 'Order Management' }, + { prefix: '/admin/invoice', title: 'Invoice Management' }, + { prefix: '/admin/review', title: 'Review Management' }, + { prefix: '/admin/support', title: 'Support Management' }, + { prefix: '/admin/kb', title: 'Knowledge Base Management' }, + { prefix: '/admin/notifications', title: 'Notifications' }, { prefix: '/admin/report', title: 'Report Management' }, - { prefix: '/admin/employees', title: 'Employee Management' }, - { prefix: '/admin/roles', title: 'Internal Role Management' }, - { prefix: '/admin/external-role-management', title: 'External Role Management' }, - { prefix: '/admin/internal-role-management', title: 'Internal Role Management' }, - { prefix: '/admin/runtime-roles', title: 'External Role Management' }, - { prefix: '/admin/onboarding-management', title: 'External Onboarding Management' }, - { prefix: '/admin/onboarding-schemas', title: 'External Onboarding Management' }, - { prefix: '/admin/kb', title: 'KB Management' }, + { prefix: '/admin/ledger', title: 'Ledger Management' }, + { prefix: '/admin/workspace', title: 'Dashboard Workspace' }, { prefix: '/admin', title: 'Dashboard' }, ]; @@ -96,6 +98,8 @@ export default function AdminShell(props: { children: JSX.Element }) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [checkedSession, setCheckedSession] = createSignal(false); + const [adminName, setAdminName] = createSignal('Admin'); + const [adminRole, setAdminRole] = createSignal('Super Admin'); const tabs = createMemo(() => { const path = location.pathname; @@ -121,9 +125,6 @@ export default function AdminShell(props: { children: JSX.Element }) { }); onMount(() => { - // ?_preview=1 or sessionStorage flag — bypass auth for UI testing without a live backend. - // Sets the session cookie AND a sessionStorage flag so all subsequent pages in this tab - // also skip the API check without needing ?_preview=1 in every URL. const isLocalDev = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); @@ -144,7 +145,6 @@ export default function AdminShell(props: { children: JSX.Element }) { navigate(`/login?from=${from}`, { replace: true }); return; } - try { const accessToken = typeof sessionStorage !== 'undefined' @@ -160,9 +160,9 @@ export default function AdminShell(props: { children: JSX.Element }) { credentials: 'include', }); const payload = await response.json().catch(() => ({})); - if (!response.ok || isExternalIdentity(payload)) { - throw new Error('Unauthorized'); - } + if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized'); + if (payload?.full_name) setAdminName(payload.full_name); + if (payload?.role?.name) setAdminRole(payload.role.name); setCheckedSession(true); } catch { clearAdminSession(); @@ -177,77 +177,109 @@ export default function AdminShell(props: { children: JSX.Element }) { const onLogout = async () => { await fetch('/api/gateway/users/auth/logout', { method: 'POST', - headers: { - Accept: 'application/json', - 'x-portal-target': 'admin', - }, + headers: { Accept: 'application/json', 'x-portal-target': 'admin' }, credentials: 'include', }).catch(() => {}); - clearAdminSession(); if (typeof sessionStorage !== 'undefined') { sessionStorage.removeItem('nxtgauge_admin_access_token'); + sessionStorage.removeItem('nxtgauge_admin_preview'); } navigate('/login', { replace: true }); }; + const initials = () => adminName().charAt(0).toUpperCase() || 'A'; + return ( -
-
-
-
- NXTGAUGE +
+ {/* ── Fixed Header ── */} +
+ {/* Left: logo + page title */} +
+
+ NXTGAUGE
-

{pageTitle()}

+

{pageTitle()}

-
- - - + + {/* Avatar + name */} +
+ + + {/* Logout */} + +
+ {/* ── Body: sidebar + main (fixed, below header) ── */} {checkedSession() ? ( -
- -
- {tabs().length > 0 ? ( -
- +
+ {/* Sidebar */} + + + {/* Main content */} +
+ {/* Sub-tabs (shown for multi-tab sections) */} + {tabs().length > 0 && ( +
+ {tabs().map((tab) => ( + + {tab.label} + + ))}
- ) : null} -
{props.children}
+ )} + {props.children}
) : ( -
-
-
-

Checking session...

-
+ /* Session check loading state */ +
+
+
+

Checking session…

)} diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index 5d19c96..b655d61 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -1,94 +1,140 @@ import { A, useLocation } from '@solidjs/router'; -import { sidebarCollapsed, toggleSidebar } from '~/lib/sidebar-state'; +import { createSignal } from 'solid-js'; -type LinkItem = { legacyHref: string; href: string; label: string; icon: string; aliasPrefix?: string }; +type LinkItem = { + href: string; + label: string; + icon: string; + aliasPrefix?: string; +}; const links: LinkItem[] = [ - { legacyHref: '/', href: '/admin', label: 'Dashboard', icon: 'dashboard.svg' }, - { legacyHref: '/roles?scope=internal', href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg' }, - { legacyHref: '/runtime-roles', href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' }, - { legacyHref: '/onboarding-management', href: '/admin/onboarding-schemas', label: 'External Onboarding Management', icon: 'reviews.svg' }, - { legacyHref: '/internal-dashboard-management', href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' }, - { legacyHref: '/external-dashboard-management', href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' }, - { legacyHref: '/approval', href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' }, - { legacyHref: '/department', href: '/admin/department', label: 'Department Management', icon: 'department.svg' }, - { legacyHref: '/designation', href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' }, - { legacyHref: '/employees', href: '/admin/employees', label: 'Employee Management', icon: 'users.svg' }, - { legacyHref: '/users', href: '/admin/users', label: 'Users Management', icon: 'users.svg' }, - { legacyHref: '/company', href: '/admin/company', label: 'Company Management', icon: 'company.svg' }, - { legacyHref: '/candidate', href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg' }, - { legacyHref: '/customer', href: '/admin/customer', label: 'Customer Management', icon: 'users.svg' }, - { legacyHref: '/photographer', href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg' }, - { legacyHref: '/makeup-artist', href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg' }, - { legacyHref: '/tutors', href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg' }, - { legacyHref: '/developers', href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg' }, - { legacyHref: '/video-editors', href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg' }, - { legacyHref: '/fitness-trainers', href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg' }, - { legacyHref: '/catering-services', href: '/admin/catering-services', label: 'Catering Services Management', icon: 'company.svg' }, - { legacyHref: '/graphic-designers', href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: 'developers.svg' }, - { legacyHref: '/social-media-managers', href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: 'developers.svg' }, - { legacyHref: '/jobs', href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg' }, - { legacyHref: '/leads', href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg' }, - { legacyHref: '/pricing', href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg' }, - { legacyHref: '/credit', href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg' }, - { legacyHref: '/coupon', href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg' }, - { legacyHref: '/discount', href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg' }, - { legacyHref: '/tax', href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg' }, - { legacyHref: '/order', href: '/admin/order', label: 'Order Management', icon: 'order.svg' }, - { legacyHref: '/invoice', href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg' }, - { legacyHref: '/review', href: '/admin/review', label: 'Review Management', icon: 'reviews.svg' }, - { legacyHref: '/help', href: '/admin/support', label: 'Support Management', icon: 'support.svg' }, - { legacyHref: '/report', href: '/admin/report', label: 'Report Management', icon: 'report.svg' }, - { legacyHref: '/ledger', href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg' }, - { legacyHref: '/kb', href: '/admin/kb', label: 'KB Management', icon: 'reviews.svg' }, - { legacyHref: '/notifications', href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg' }, + { href: '/admin', label: 'Dashboard', icon: 'dashboard.svg' }, + { href: '/admin/department', label: 'Department Management', icon: 'department.svg' }, + { href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' }, + { href: '/admin/employees', label: 'Employee Management', icon: 'users.svg' }, + { href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg' }, + { href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' }, + { href: '/admin/onboarding-schemas', label: 'External Onboarding Management', icon: 'reviews.svg' }, + { href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' }, + { href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' }, + { href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' }, + { href: '/admin/users', label: 'Users Management', icon: 'users.svg' }, + { href: '/admin/company', label: 'Company Management', icon: 'company.svg' }, + { href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg' }, + { href: '/admin/customer', label: 'Customer Management', icon: 'users.svg' }, + { href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg' }, + { href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg' }, + { href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg' }, + { href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg' }, + { href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg' }, + { href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg' }, + { href: '/admin/catering-services', label: 'Catering Services Management', icon: 'company.svg' }, + { href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: 'developers.svg' }, + { href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: 'developers.svg' }, + { href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg' }, + { href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg' }, + { href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg' }, + { href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg' }, + { href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg' }, + { href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg' }, + { href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg' }, + { href: '/admin/order', label: 'Order Management', icon: 'order.svg' }, + { href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg' }, + { href: '/admin/review', label: 'Review Management', icon: 'reviews.svg' }, + { href: '/admin/support', label: 'Support Management', icon: 'support.svg' }, + { href: '/admin/kb', label: 'Knowledge Base Management', icon: 'reviews.svg' }, + { href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg' }, + { href: '/admin/report', label: 'Report Management', icon: 'report.svg' }, + { href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg' }, ]; export default function AdminSidebar() { const location = useLocation(); - const collapsed = sidebarCollapsed; + const [collapsed, setCollapsed] = createSignal(false); - const isLinkActive = (href: string, aliasPrefix?: string) => { - const pathOnly = href.split('?')[0] || href; - if (pathOnly === '/admin') return location.pathname === '/admin'; + const isActive = (href: string, aliasPrefix?: string) => { + if (href === '/admin') return location.pathname === '/admin'; if (aliasPrefix && location.pathname.startsWith(aliasPrefix)) return true; - return location.pathname === pathOnly || location.pathname.startsWith(`${pathOnly}/`); + return location.pathname === href || location.pathname.startsWith(`${href}/`); }; return ( -