diff --git a/.claude/commands/redesign-sidebar.md b/.claude/commands/redesign-sidebar.md
new file mode 100644
index 00000000..a5f811b3
--- /dev/null
+++ b/.claude/commands/redesign-sidebar.md
@@ -0,0 +1,501 @@
+---
+description: Redesign the app UI from a top nav bar to a modern SaaS vertical sidebar layout
+---
+
+# Redesign: Top Nav to Vertical Sidebar
+
+Transform this Vue 3 app from a horizontal top-nav layout into a modern SaaS-style layout with a fixed 240px dark vertical sidebar. Follow the steps below exactly. All `.vue` file changes **must** be delegated to the `vue-expert` subagent via the Agent tool.
+
+---
+
+## Target Layout
+
+```
+┌──────────────────────┬────────────────────────────────────────┐
+│ Brand Name │ │
+│ Subtitle │ [router-view — page content] │
+├──────────────────────│ │
+│ ○ Overview │ │
+│ ○ Inventory │ │
+│ ○ Orders │ │
+│ ○ Finance │ │
+│ ○ Demand Forecast │ │
+│ ○ Reports │ │
+├──────────────────────│ │
+│ FILTERS │ │
+│ Time Period ▾ │ │
+│ Location ▾ │ │
+│ Category ▾ │ │
+│ Status ▾ │ │
+│ [ Reset ] │ │
+├──────────────────────│ │
+│ 🌐 English │ │
+│ [JD] Jane Doe │ │
+└──────────────────────┴────────────────────────────────────────┘
+ 240px fixed, #0f172a flex:1, margin-left:240px
+```
+
+---
+
+## Design Tokens (do not deviate)
+
+| Element | Value |
+|---------|-------|
+| Sidebar width | `240px` |
+| Sidebar background | `#0f172a` |
+| Sidebar position | `fixed; left:0; top:0; bottom:0; z-index:100; overflow-y:auto` |
+| Logo zone height | `64px` |
+| Active nav background | `#2563eb` |
+| Nav link default color | `rgba(255,255,255,0.65)` |
+| Nav link active color | `#ffffff` |
+| Nav link hover background | `rgba(255,255,255,0.07)` |
+| Dividers | `1px solid rgba(255,255,255,0.08)` |
+| Filter select background | `rgba(255,255,255,0.08)` |
+| Filter select border | `1px solid rgba(255,255,255,0.12)` |
+| Filter select color | `rgba(255,255,255,0.85)` |
+| Filter label color | `rgba(255,255,255,0.4)` |
+| Main content margin | `margin-left:240px` |
+| Main content padding | `padding:1.5rem 2rem` |
+
+---
+
+## Step 1 — Read Current Files
+
+Before touching anything, read these four files:
+- `client/src/App.vue`
+- `client/src/components/FilterBar.vue`
+- `client/src/components/LanguageSwitcher.vue`
+- `client/src/components/ProfileMenu.vue`
+
+Note the exact structure of each — especially:
+- The nav route paths and their `t()` translation keys in `App.vue`
+- The event names emitted by `ProfileMenu.vue` (`show-profile-details`, `show-tasks`)
+- The CSS class names for dropdowns in `LanguageSwitcher.vue` and `ProfileMenu.vue`
+- The task management refs/methods in `App.vue` script (keep them all)
+
+---
+
+## Step 2 — Create AppSidebar.vue (via vue-expert)
+
+Use the Agent tool with `subagent_type: "vue-expert"` to create `client/src/components/AppSidebar.vue`.
+
+Provide this full spec to vue-expert:
+
+---
+**File to create:** `client/src/components/AppSidebar.vue`
+
+**Purpose:** Fixed left sidebar containing logo, navigation, global filters, language switcher, and profile menu. Replaces the `.top-nav` header and wraps `FilterBar`, `LanguageSwitcher`, and `ProfileMenu`.
+
+**Imports needed:**
+- `FilterBar` from `'./FilterBar.vue'`
+- `LanguageSwitcher` from `'./LanguageSwitcher.vue'`
+- `ProfileMenu` from `'./ProfileMenu.vue'`
+- `useI18n` from `'../composables/useI18n'`
+- `RouterLink` and `useRoute` from `'vue-router'`
+
+**Emits:** `['show-profile-details', 'show-tasks']`
+
+**Setup:**
+```js
+const { t } = useI18n()
+const route = useRoute()
+```
+
+**Template structure:**
+```html
+
+
+
+```
+
+**Scoped styles:**
+```css
+.app-sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 240px;
+ background: #0f172a;
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+}
+
+.sidebar-logo {
+ height: 64px;
+ min-height: 64px;
+ padding: 0 1.25rem;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.sidebar-brand {
+ font-size: 0.938rem;
+ font-weight: 700;
+ color: #ffffff;
+ letter-spacing: -0.02em;
+ line-height: 1.2;
+}
+
+.sidebar-tagline {
+ font-size: 0.688rem;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 0.125rem;
+ line-height: 1.3;
+}
+
+.sidebar-nav {
+ padding: 0.75rem;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ padding: 0.5rem 0.75rem;
+ border-radius: 6px;
+ color: rgba(255, 255, 255, 0.65);
+ font-size: 0.875rem;
+ font-weight: 500;
+ text-decoration: none;
+ transition: background 0.15s ease, color 0.15s ease;
+}
+
+.nav-link:hover {
+ background: rgba(255, 255, 255, 0.07);
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.nav-link.active {
+ background: #2563eb;
+ color: #ffffff;
+}
+
+.sidebar-filters {
+ padding: 0.75rem 0;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.sidebar-filters-label {
+ font-size: 0.688rem;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.35);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ padding: 0 1.25rem;
+ margin-bottom: 0.5rem;
+}
+
+.sidebar-bottom {
+ padding: 0.75rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+/* Make language and profile buttons full-width and fit dark sidebar */
+.sidebar-bottom :deep(.language-button),
+.sidebar-bottom :deep(.profile-button) {
+ width: 100%;
+ background: rgba(255, 255, 255, 0.06);
+ border-color: rgba(255, 255, 255, 0.12);
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.sidebar-bottom :deep(.language-button):hover,
+.sidebar-bottom :deep(.profile-button):hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.2);
+ color: #ffffff;
+}
+
+.sidebar-bottom :deep(.lang-name),
+.sidebar-bottom :deep(.profile-name) {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.sidebar-bottom :deep(.chevron-icon) {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* Open dropdowns upward and to the right to avoid sidebar clipping */
+.sidebar-bottom :deep(.dropdown-menu) {
+ bottom: calc(100% + 0.5rem);
+ top: auto;
+ left: 0;
+ right: auto;
+ min-width: 200px;
+}
+```
+---
+
+## Step 3 — Modify App.vue (via vue-expert)
+
+Use the Agent tool with `subagent_type: "vue-expert"` to modify `client/src/App.vue`.
+
+Provide this full spec to vue-expert:
+
+---
+**File to modify:** `client/src/App.vue`
+
+**Template — replace the entire `` block with:**
+```html
+
+
+
+```
+
+**Script changes:**
+- Add `import AppSidebar from './components/AppSidebar.vue'`
+- Add `AppSidebar` to the `components` object
+- Remove `import FilterBar` (now inside AppSidebar)
+- Remove `import LanguageSwitcher` (now inside AppSidebar)
+- Remove `import ProfileMenu` (now inside AppSidebar)
+- Remove `FilterBar`, `LanguageSwitcher`, `ProfileMenu` from the `components` object
+- Keep ALL task management logic: `showProfileDetails`, `showTasks`, `tasks`, `addTask`, `deleteTask`, `toggleTask`
+- Remove `useI18n` and `useAuth` imports only if they are no longer referenced in the script after the above changes
+
+**CSS — in the global unscoped `
diff --git a/client/src/components/FilterBar.vue b/client/src/components/FilterBar.vue
index 9e12b694..1ba362a4 100644
--- a/client/src/components/FilterBar.vue
+++ b/client/src/components/FilterBar.vue
@@ -64,6 +64,7 @@
+ Reset
@@ -102,84 +103,94 @@ export default {
diff --git a/client/src/composables/useSidebar.js b/client/src/composables/useSidebar.js
new file mode 100644
index 00000000..a99a0dad
--- /dev/null
+++ b/client/src/composables/useSidebar.js
@@ -0,0 +1,14 @@
+import { ref, watch } from 'vue'
+
+const STORAGE_KEY = 'sidebar-collapsed'
+const isCollapsed = ref(localStorage.getItem(STORAGE_KEY) === 'true')
+
+watch(isCollapsed, (val) => {
+ localStorage.setItem(STORAGE_KEY, String(val))
+})
+
+export function useSidebar() {
+ const toggle = () => { isCollapsed.value = !isCollapsed.value }
+ const expand = () => { isCollapsed.value = false }
+ return { isCollapsed, toggle, expand }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..58698bcb
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,76 @@
+{
+ "name": "inventory-management",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "@playwright/test": "^1.60.0",
+ "playwright": "^1.60.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
+ "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.60.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.60.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..f0350af7
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "devDependencies": {
+ "@playwright/test": "^1.60.0",
+ "playwright": "^1.60.0"
+ }
+}
diff --git a/scripts/verify_sidebar.js b/scripts/verify_sidebar.js
new file mode 100644
index 00000000..f4b9a48d
--- /dev/null
+++ b/scripts/verify_sidebar.js
@@ -0,0 +1,198 @@
+const { chromium } = require('/Users/robbfournier/anthropic-basecamp/inventory-management/node_modules/playwright');
+const path = require('path');
+const fs = require('fs');
+
+const screenshotDir = '/tmp/sidebar_verify';
+if (!fs.existsSync(screenshotDir)) fs.mkdirSync(screenshotDir, { recursive: true });
+
+(async () => {
+ const browser = await chromium.launch({ headless: true });
+ const page = await browser.newPage();
+ await page.setViewportSize({ width: 1440, height: 900 });
+
+ // ---- Step 1 + 2: Navigate and inspect initial state ----
+ await page.goto('http://localhost:3000', { waitUntil: 'networkidle' });
+ await page.screenshot({ path: `${screenshotDir}/01_home.png`, fullPage: false });
+
+ // Check for sidebar
+ const sidebar = await page.$('[class*="sidebar"], nav[class*="side"], aside, .sidebar, #sidebar');
+ const topNav = await page.$('header, nav[class*="top"], .top-nav, .navbar');
+ const sidebarBg = sidebar ? await sidebar.evaluate(el => getComputedStyle(el).backgroundColor) : null;
+ const sidebarWidth = sidebar ? await sidebar.evaluate(el => el.offsetWidth) : null;
+ const sidebarLeft = sidebar ? await sidebar.evaluate(el => el.getBoundingClientRect().left) : null;
+
+ console.log('=== STEP 2: Initial State ===');
+ console.log('Sidebar element found:', !!sidebar);
+ console.log('Sidebar background color:', sidebarBg);
+ console.log('Sidebar width:', sidebarWidth);
+ console.log('Sidebar left position:', sidebarLeft);
+ console.log('Top nav / header found:', !!topNav);
+
+ // Check sidebar color matches dark (#0f172a)
+ // rgb(15, 23, 42) = #0f172a
+ if (sidebarBg) {
+ const isDark = sidebarBg.includes('15, 23, 42') || sidebarBg === '#0f172a';
+ console.log('Sidebar is dark (#0f172a):', isDark);
+ }
+
+ // Check main content positioning
+ const mainContent = await page.$('main, .main-content, #main, [class*="main"]');
+ const mainLeft = mainContent ? await mainContent.evaluate(el => el.getBoundingClientRect().left) : null;
+ console.log('Main content left offset:', mainLeft, '(should be >= 240 if sidebar is there)');
+
+ // ---- Step 3: Click Inventory ----
+ const inventoryLink = await page.$('a[href*="inventory"], a:has-text("Inventory")');
+ if (inventoryLink) {
+ await inventoryLink.click();
+ await page.waitForTimeout(500);
+ } else {
+ console.log('WARNING: Could not find Inventory nav link');
+ // Try text
+ await page.click('text=Inventory');
+ await page.waitForTimeout(500);
+ }
+
+ const urlAfterInventory = page.url();
+ await page.screenshot({ path: `${screenshotDir}/03_inventory.png`, fullPage: false });
+
+ // Check active link color
+ const activeLink = await page.$('a.router-link-active, a.active, [aria-current="page"]');
+ const activeLinkColor = activeLink ? await activeLink.evaluate(el => getComputedStyle(el).color) : null;
+ const activeLinkBg = activeLink ? await activeLink.evaluate(el => getComputedStyle(el).backgroundColor) : null;
+
+ console.log('\n=== STEP 4: Inventory Active State ===');
+ console.log('URL after clicking Inventory:', urlAfterInventory);
+ console.log('Active link color:', activeLinkColor);
+ console.log('Active link background:', activeLinkBg);
+
+ // ---- Step 5: Click Orders ----
+ try {
+ await page.click('a[href*="orders"], a:has-text("Orders")');
+ await page.waitForTimeout(500);
+ } catch {
+ console.log('WARNING: Could not click Orders link directly');
+ }
+
+ const urlAfterOrders = page.url();
+ await page.screenshot({ path: `${screenshotDir}/05_orders.png`, fullPage: false });
+
+ console.log('\n=== STEP 6: Orders Active State ===');
+ console.log('URL after clicking Orders:', urlAfterOrders);
+
+ const activeAfterOrders = await page.$('a.router-link-active, a.active, [aria-current="page"]');
+ const ordersActiveBg = activeAfterOrders ? await activeAfterOrders.evaluate(el => getComputedStyle(el).backgroundColor) : null;
+ console.log('Orders active link background:', ordersActiveBg);
+
+ // ---- Step 7: Check filter dropdowns ----
+ await page.goto('http://localhost:3000', { waitUntil: 'networkidle' });
+ await page.waitForTimeout(300);
+
+ const filterLabels = ['Time Period', 'Location', 'Category', 'Status', 'Warehouse'];
+ console.log('\n=== STEP 7: Filter Dropdowns ===');
+ for (const label of filterLabels) {
+ const found = await page.$(`text=${label}`);
+ if (found) {
+ const rect = await found.evaluate(el => {
+ const r = el.getBoundingClientRect();
+ return { left: r.left, top: r.top, width: r.width };
+ });
+ console.log(`Filter "${label}" found at left=${rect.left}, top=${rect.top}`);
+ } else {
+ console.log(`Filter "${label}": NOT FOUND`);
+ }
+ }
+
+ // ---- Step 8: Language switcher ----
+ await page.screenshot({ path: `${screenshotDir}/07_filters.png`, fullPage: false });
+
+ // Find language switcher - often at bottom of sidebar
+ const langBtn = await page.$('button[class*="lang"], button[class*="locale"], [class*="language"], button:has-text("EN"), button:has-text("English")');
+ console.log('\n=== STEP 8: Language Switcher ===');
+ console.log('Language switcher found:', !!langBtn);
+
+ if (langBtn) {
+ await langBtn.click();
+ await page.waitForTimeout(400);
+ await page.screenshot({ path: `${screenshotDir}/08_lang_dropdown.png`, fullPage: false });
+
+ // Check if dropdown is within viewport
+ const dropdown = await page.$('[class*="dropdown"], [class*="menu"], [role="listbox"], [role="menu"]');
+ if (dropdown) {
+ const rect = await dropdown.evaluate(el => el.getBoundingClientRect());
+ console.log('Language dropdown left position:', rect.left, '(should be >= 0 to not clip)');
+ console.log('Language dropdown is clipping:', rect.left < 0);
+ }
+
+ // Close by pressing Escape
+ await page.keyboard.press('Escape');
+ await page.waitForTimeout(300);
+ } else {
+ // Take screenshot anyway to see what's at bottom of sidebar
+ await page.screenshot({ path: `${screenshotDir}/08_lang_dropdown.png`, fullPage: false });
+ console.log('Could not find language switcher button - screenshot taken for manual inspection');
+ }
+
+ // ---- Step 9: Profile button ----
+ const profileBtn = await page.$('button[class*="profile"], button[class*="user"], [class*="avatar"], button[class*="account"]');
+ console.log('\n=== STEP 9: Profile Button ===');
+ console.log('Profile button found:', !!profileBtn);
+
+ if (profileBtn) {
+ await profileBtn.click();
+ await page.waitForTimeout(400);
+ await page.screenshot({ path: `${screenshotDir}/09_profile_dropdown.png`, fullPage: false });
+
+ const profileDropdown = await page.$('[class*="dropdown"], [class*="menu"], [role="listbox"], [role="menu"]');
+ if (profileDropdown) {
+ const rect = await profileDropdown.evaluate(el => el.getBoundingClientRect());
+ console.log('Profile dropdown left position:', rect.left, '(should be >= 0 to not clip)');
+ console.log('Profile dropdown is clipping:', rect.left < 0);
+ }
+
+ await page.keyboard.press('Escape');
+ await page.waitForTimeout(300);
+ } else {
+ await page.screenshot({ path: `${screenshotDir}/09_profile_dropdown.png`, fullPage: false });
+ console.log('Could not find profile button - screenshot taken for manual inspection');
+ }
+
+ // ---- Step 10: Final screenshot ----
+ await page.goto('http://localhost:3000', { waitUntil: 'networkidle' });
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${screenshotDir}/10_final.png`, fullPage: false });
+
+ // Final layout check
+ const sidebarFinal = await page.$('[class*="sidebar"], aside, .sidebar, #sidebar');
+ const sidebarWidthFinal = sidebarFinal ? await sidebarFinal.evaluate(el => el.offsetWidth) : null;
+ const sidebarColorFinal = sidebarFinal ? await sidebarFinal.evaluate(el => getComputedStyle(el).backgroundColor) : null;
+
+ console.log('\n=== STEP 10: Final State ===');
+ console.log('Sidebar width:', sidebarWidthFinal);
+ console.log('Sidebar background:', sidebarColorFinal);
+
+ // Dump all classes on page to help debug
+ const allNavLinks = await page.$$eval('nav a, aside a, .sidebar a', els =>
+ els.map(el => ({
+ text: el.textContent.trim().substring(0, 30),
+ href: el.getAttribute('href'),
+ classes: el.className
+ }))
+ );
+ console.log('\n=== All sidebar/nav links found ===');
+ allNavLinks.forEach(l => console.log(` "${l.text}" href="${l.href}" class="${l.classes}"`));
+
+ // Report on layout issues
+ console.log('\n=== LAYOUT ISSUE SUMMARY ===');
+ if (!sidebar) {
+ console.log('ISSUE: No sidebar element found with expected selectors');
+ }
+ if (sidebarWidth && (sidebarWidth < 200 || sidebarWidth > 300)) {
+ console.log(`ISSUE: Sidebar width is ${sidebarWidth}px, expected ~240px`);
+ }
+ if (mainLeft !== null && mainLeft < 200) {
+ console.log(`ISSUE: Main content starts at ${mainLeft}px, may be hidden behind sidebar`);
+ }
+
+ console.log('\nScreenshots saved to:', screenshotDir);
+ await browser.close();
+})();