Add Puppeteer scripts for navigation testing and debugging

- Created nav-debug.cjs to log anchor tags and simulate clicks on the Voice navigation link, capturing click events and page navigation.
- Added nav-test.cjs to test the Voice link click and log the URL at various intervals, capturing any page errors.
- Introduced nav-test2.cjs to check the presence of specific elements on the /voice/ page and log any console errors.
- Implemented nav-test4019.cjs to monitor network requests and responses related to the Voice navigation, verifying button presence and click functionality.
This commit is contained in:
asepharyana
2026-08-15 19:23:17 +07:00
parent 1b56212d1a
commit 3c2c1c3b15
9 changed files with 789 additions and 38 deletions
+48
View File
@@ -0,0 +1,48 @@
const puppeteer = require("puppeteer-core");
(async () => {
const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox","--disable-setuid-sandbox"] });
const page = await browser.newPage();
page.on("pageerror", (e) => console.log("PAGEERROR:", e.message));
await page.goto("http://127.0.0.1:4017/dashboard/", { waitUntil: "domcontentloaded", timeout: 30000 });
await new Promise((r) => setTimeout(r, 2500));
// Find every <a> tag and log its href + event listeners
const anchors = await page.$$eval("a", els => els.map(a => ({
href: a.getAttribute("href"),
hasClick: a.hasAttribute("onClick"),
outer: a.outerHTML.substring(0, 200)
})));
console.log("ALL ANCHORS:", JSON.stringify(anchors));
// Inject error catches
await page.evaluate(() => {
window.__clicks = [];
window.__events = [];
document.addEventListener("click", (e) => window.__clicks.push({ target: e.target.tagName, dp: e.defaultPrevented, url: location.href }), true);
document.addEventListener("click", (e) => {
const t = e.target.closest("a");
if (t) {
window.__events.push({ targetTag: t.tagName, href: t.getAttribute("href"), dp: e.defaultPrevented });
}
}, false);
});
// Click the Voice nav anchor
await page.evaluate(() => {
const v = Array.from(document.querySelectorAll('a[aria-label]')).find(a=>a.getAttribute('aria-label')==='Voice');
if (v) v.click();
});
await new Promise((r) => setTimeout(r, 3000));
console.log("URL after click:", page.url());
console.log("history.length:", await page.evaluate(() => history.length));
console.log("CLICKS (capture phase):", JSON.stringify(await page.evaluate(() => window.__clicks)));
console.log("CLICKS (bubble phase):", JSON.stringify(await page.evaluate(() => window.__clicks)));
// Force navigate and confirm destination
await page.evaluate(() => { window.location.href = "/voice/"; });
await new Promise((r) => setTimeout(r, 2000));
console.log("FORCED to /voice/ ->", page.url());
await browser.close();
})().catch((e) => { console.error("FAIL:", e.message); process.exit(1); });
+27
View File
@@ -0,0 +1,27 @@
const puppeteer = require("puppeteer-core");
(async () => {
const browser = await puppeteer.launch({
executablePath: "/usr/bin/google-chrome",
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
const errs = [];
page.on("pageerror", (e) => errs.push("PAGEERROR: " + e.message));
await page.goto("http://localhost:3000/dashboard/", { waitUntil: "domcontentloaded", timeout: 30000 });
await new Promise((r) => setTimeout(r, 2500));
const info = await page.evaluate(() => {
const v = Array.from(document.querySelectorAll('a[aria-label]')).find((a) => a.getAttribute("aria-label") === "Voice");
const r = v.getBoundingClientRect();
return { cx: r.x + r.width / 2, cy: r.y + r.height / 2 };
});
await page.mouse.click(info.cx, info.cy);
for (const t of [500, 1000, 2000, 3500]) {
await new Promise((r) => setTimeout(r, t === 500 ? 500 : t - (t === 1000 ? 500 : t === 2000 ? 1000 : 2000)));
console.log(`url @${t}ms:`, page.url());
}
console.log("ERRORS:", errs.slice(0, 10).join(" | ") || "none");
await browser.close();
})().catch((e) => { console.error("SCRIPT FAIL:", e); process.exit(1); });
+18
View File
@@ -0,0 +1,18 @@
const puppeteer = require("puppeteer-core");
(async () => {
const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox","--disable-setuid-sandbox"] });
const page = await browser.newPage();
const errs = [];
page.on("pageerror", (e) => errs.push("PAGEERR: " + e.message));
page.on("console", (m) => { if (m.type() === "error" || m.type()==="warning") errs.push("CONS["+m.type()+"]: " + m.text().slice(0,100)); });
await page.goto("http://127.0.0.1:4019/voice/", { waitUntil: "domcontentloaded", timeout: 30000 });
await new Promise((r) => setTimeout(r, 4000));
const snap = await page.evaluate(() => ({
url: location.href,
hasVoice: !!Array.from(document.querySelectorAll("*")).find(e => e.textContent && e.textContent.includes("Live speakers")),
hasPicker: !!Array.from(document.querySelectorAll("select"))[0] || false,
}));
console.log("DIRECT /voice/ load:", JSON.stringify(snap));
console.log("ERRORS:", errs.slice(0,15).join("\n") || "none");
await browser.close();
})().catch((e) => { console.error("FAIL:", e.message); process.exit(1); });
+26
View File
@@ -0,0 +1,26 @@
const puppeteer = require("puppeteer-core");
(async () => {
const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox","--disable-setuid-sandbox"] });
const page = await browser.newPage();
const net = [];
page.on("request", (r) => { const u = r.url(); if (u.includes("_rsc") || u.includes("/voice")) net.push("REQ " + r.method() + " " + u); });
page.on("response", (r) => { const u = r.url(); if (u.includes("_rsc") || u.includes("/voice")) net.push("RES " + r.status() + " " + u); });
page.on("pageerror", (e) => console.log("PAGEERR:", e.message));
await page.goto("http://127.0.0.1:4019/dashboard/", { waitUntil: "domcontentloaded", timeout: 30000 });
await new Promise((r) => setTimeout(r, 2500));
const info = await page.evaluate(() => {
const b = document.querySelector('button[aria-label="Voice"]');
const a = document.querySelector('a[aria-label="Voice"]');
return { buttonPresent: !!b, aPresent: !!a };
});
console.log("NAV button present:", info.buttonPresent, "| a present:", info.aPresent);
await page.evaluate(() => {
const b = document.querySelector('button[aria-label="Voice"]');
if (b) b.click();
});
await new Promise((r) => setTimeout(r, 3000));
console.log("URL after click:", page.url(), "| history:", await page.evaluate(() => history.length));
await browser.close();
})().catch((e) => { console.error("FAIL:", e.message); process.exit(1); });
+1
View File
@@ -29,6 +29,7 @@
"@types/react-dom": "^19",
"@types/three": "^0.180.0",
"babel-plugin-react-compiler": "1.0.0",
"puppeteer-core": "^25.7.0",
"tailwindcss": "^4",
"typescript": "^5"
}
+198 -2
View File
@@ -60,6 +60,9 @@ importers:
babel-plugin-react-compiler:
specifier: 1.0.0
version: 1.0.0
puppeteer-core:
specifier: ^25.7.0
version: 25.7.0
tailwindcss:
specifier: ^4
version: 4.3.3
@@ -427,6 +430,19 @@ packages:
cpu: [x64]
os: [win32]
'@puppeteer/browsers@3.2.0':
resolution: {integrity: sha512-LlBrE8oqGfU7b1Nk2d5Q1SbuPhZxTj0cJEMDPEws28OjNMELlflekmPPuf4FnK03x0ZRjKaYwJElUcKK4kyqJA==}
engines: {node: '>=22.12.0'}
hasBin: true
peerDependencies:
proxy-agent: '>=8.0.1'
yauzl: ^2.10.0 || ^3.4.0
peerDependenciesMeta:
proxy-agent:
optional: true
yauzl:
optional: true
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -548,6 +564,14 @@ packages:
'@webgpu/types@0.1.71':
resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==}
ansi-regex@6.3.0:
resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
engines: {node: '>=12'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
babel-plugin-react-compiler@1.0.0:
resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
@@ -564,9 +588,19 @@ packages:
caniuse-lite@1.0.30001806:
resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
chromium-bidi@17.0.2:
resolution: {integrity: sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==}
engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0'}
peerDependencies:
devtools-protocol: '*'
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
cliui@9.0.1:
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
engines: {node: '>=20'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -594,9 +628,15 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
devtools-protocol@0.0.1666840:
resolution: {integrity: sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==}
electron-to-chromium@1.5.398:
resolution: {integrity: sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
enhanced-resolve@5.24.4:
resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==}
engines: {node: '>=10.13.0'}
@@ -626,6 +666,14 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -734,6 +782,13 @@ packages:
meshoptimizer@0.22.0:
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
modern-tar@0.8.4:
resolution: {integrity: sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==}
engines: {node: '>=18.0.0'}
motion-dom@12.43.0:
resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==}
@@ -804,6 +859,10 @@ packages:
resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==}
engines: {node: ^10 || ^12 || >=14}
puppeteer-core@25.7.0:
resolution: {integrity: sha512-wgBBj7dU5ceGyoT2PCrJpkYOhxPY8mDOmcSKZP92Cj5GgqQ3kv/UxzawnOLxiYuupe4bDf/yiQm3bzs/1nI0rQ==}
engines: {node: '>=22.12.0'}
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -833,6 +892,18 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string-width@8.2.2:
resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
engines: {node: '>=20'}
strip-ansi@7.2.0:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -867,6 +938,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typed-query-selector@2.12.2:
resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -886,9 +960,43 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
webdriver-bidi-protocol@0.4.2:
resolution: {integrity: sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
ws@8.21.3:
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
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
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yargs-parser@22.0.0:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yargs@18.1.0:
resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -1190,6 +1298,11 @@ snapshots:
'@next/swc-win32-x64-msvc@16.2.12':
optional: true
'@puppeteer/browsers@3.2.0':
dependencies:
modern-tar: 0.8.4
yargs: 18.1.0
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -1293,6 +1406,10 @@ snapshots:
'@webgpu/types@0.1.71': {}
ansi-regex@6.3.0: {}
ansi-styles@6.2.3: {}
babel-plugin-react-compiler@1.0.0:
dependencies:
'@babel/types': 7.29.7
@@ -1310,8 +1427,20 @@ snapshots:
caniuse-lite@1.0.30001806: {}
chromium-bidi@17.0.2(devtools-protocol@0.0.1666840):
dependencies:
devtools-protocol: 0.0.1666840
mitt: 3.0.1
zod: 3.25.76
client-only@0.0.1: {}
cliui@9.0.1:
dependencies:
string-width: 7.2.0
strip-ansi: 7.2.0
wrap-ansi: 9.0.2
clsx@2.1.1: {}
convert-source-map@2.0.0:
@@ -1328,16 +1457,19 @@ snapshots:
detect-libc@2.1.2: {}
devtools-protocol@0.0.1666840: {}
electron-to-chromium@1.5.398:
optional: true
emoji-regex@10.6.0: {}
enhanced-resolve@5.24.4:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.3
escalade@3.2.0:
optional: true
escalade@3.2.0: {}
fflate@0.8.3: {}
@@ -1353,6 +1485,10 @@ snapshots:
gensync@1.0.0-beta.2:
optional: true
get-caller-file@2.0.5: {}
get-east-asian-width@1.6.0: {}
graceful-fs@4.2.11: {}
jiti@2.7.0: {}
@@ -1430,6 +1566,10 @@ snapshots:
meshoptimizer@0.22.0: {}
mitt@3.0.1: {}
modern-tar@0.8.4: {}
motion-dom@12.43.0:
dependencies:
motion-utils: 12.39.0
@@ -1496,6 +1636,20 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
puppeteer-core@25.7.0:
dependencies:
'@puppeteer/browsers': 3.2.0
chromium-bidi: 17.0.2(devtools-protocol@0.0.1666840)
devtools-protocol: 0.0.1666840
typed-query-selector: 2.12.2
webdriver-bidi-protocol: 0.4.2
ws: 8.21.3
transitivePeerDependencies:
- bufferutil
- proxy-agent
- utf-8-validate
- yauzl
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
@@ -1545,6 +1699,21 @@ snapshots:
source-map-js@1.2.1: {}
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
string-width@8.2.2:
dependencies:
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.3.0
styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.4):
dependencies:
client-only: 0.0.1
@@ -1568,6 +1737,8 @@ snapshots:
tslib@2.8.1: {}
typed-query-selector@2.12.2: {}
typescript@5.9.3: {}
undici-types@6.21.0: {}
@@ -1583,5 +1754,30 @@ snapshots:
dependencies:
react: 19.2.4
webdriver-bidi-protocol@0.4.2: {}
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3
string-width: 7.2.0
strip-ansi: 7.2.0
ws@8.21.3: {}
y18n@5.0.8: {}
yallist@3.1.1:
optional: true
yargs-parser@22.0.0: {}
yargs@18.1.0:
dependencies:
cliui: 9.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
string-width: 8.2.2
y18n: 5.0.8
yargs-parser: 22.0.0
zod@3.25.76: {}
@@ -221,7 +221,7 @@ export function AmbientCanvas({
<div
ref={mountRef}
aria-hidden
className="fixed inset-0 -z-10 overflow-hidden"
className="fixed inset-0 -z-10 overflow-hidden pointer-events-none select-none"
style={{
background:
"radial-gradient(120% 90% at 50% 0%, oklch(0.2 0.04 70 / 0.5), oklch(0.1 0.015 70) 60%)",
@@ -1,50 +1,67 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import { LayoutDashboard } from "lucide-react";
import { navItems, isActivePath } from "@/lib/navigation";
import { cn } from "@/lib/utils";
import {
Tooltip,
} from "@/components/primitives/tooltip";
function NavItem({
href,
label,
active,
Icon,
}: {
href: string;
label: string;
active: boolean;
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}) {
const router = useRouter();
return (
<button
type="button"
aria-label={label}
aria-current={active ? "page" : undefined}
onClick={() => router.push(href)}
className={cn(
"group relative flex size-11 items-center justify-center rounded-[13px] transition-all",
active
? "bg-signal/15 text-signal"
: "text-ink-faint hover:bg-white/5 hover:text-ink-soft",
)}
>
{active && (
<span className="absolute -left-3 h-6 w-1 rounded-full bg-signal shadow-[0_0_12px_var(--color-signal-glow)]" />
)}
<Icon className="size-[18px]" strokeWidth={active ? 2.4 : 2} />
</button>
);
}
export function NavRail() {
const pathname = usePathname();
const path = pathname ?? "/";
return (
<nav className="glass m-3 mr-0 flex w-[68px] flex-col items-center gap-1 rounded-[18px] py-4">
<Link
<NavItem
href="/dashboard"
className="mb-3 flex size-11 items-center justify-center rounded-[14px] bg-signal/15 text-signal glow-signal"
aria-label="GMW home"
>
<span className="display text-xl">G</span>
</Link>
label="Dashboard"
active={path === "/dashboard" || path === "/dashboard/"}
Icon={LayoutDashboard}
/>
<div className="flex flex-1 flex-col gap-1">
{navItems.map((item) => {
const active = isActivePath(pathname, item.matchPrefix);
const Icon = item.icon;
return (
<Tooltip key={item.href} label={item.label} side="bottom">
<Link
href={item.href}
aria-label={item.label}
aria-current={active ? "page" : undefined}
className={cn(
"group relative flex size-11 items-center justify-center rounded-[13px] transition-all",
active
? "bg-signal/15 text-signal"
: "text-ink-faint hover:bg-white/5 hover:text-ink-soft",
)}
>
{active && (
<span className="absolute -left-3 h-6 w-1 rounded-full bg-signal shadow-[0_0_12px_var(--color-signal-glow)]" />
)}
<Icon className="size-[18px]" strokeWidth={active ? 2.4 : 2} />
</Link>
</Tooltip>
);
})}
{navItems.map((item) => (
<NavItem
key={item.href}
href={item.href}
label={item.label}
active={isActivePath(path, item.matchPrefix)}
Icon={item.icon}
/>
))}
</div>
</nav>
);
}