diff --git a/docs/superpowers/plans/2026-04-03-mobile-first-layout-redesign.md b/docs/superpowers/plans/2026-04-03-mobile-first-layout-redesign.md new file mode 100644 index 00000000..1136169d --- /dev/null +++ b/docs/superpowers/plans/2026-04-03-mobile-first-layout-redesign.md @@ -0,0 +1,658 @@ +# Mobile-First Layout Redesign — Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make all three Blazor SSR layouts (AppLayout, PublicLayout, ManageLayout) fully functional on mobile with proper navigation, accessibility, and touch support. + +**Architecture:** All changes are in Blazor `.razor` files and vanilla JS/CSS — no React changes. Mobile behavior is driven by CSS class toggling from JS, matching the existing codebase pattern. The `md:` breakpoint (768px) is the universal mobile/desktop threshold. + +**Tech Stack:** Blazor SSR (Razor components), vanilla JavaScript, Tailwind CSS v4, inline SVG icons. + +**Spec:** `docs/superpowers/specs/2026-04-03-mobile-first-layout-redesign.md` + +--- + +## File Structure + +| File | Responsibility | +| ---- | -------------- | +| `framework/SimpleModule.Blazor/Components/Layout/AppLayout.razor` | Add mobile header + backdrop HTML elements, ARIA attributes | +| `framework/SimpleModule.Blazor/Components/Layout/PublicLayout.razor` | Add hamburger + full-screen overlay with accordion nav, ARIA | +| `framework/SimpleModule.Blazor/Components/Layout/ManageLayout.razor` | Switch to `md:` breakpoint, add mobile tab bar markup | +| `framework/SimpleModule.Blazor/Components/ManageNav.razor` | Add `Horizontal` parameter, responsive class variants | +| `template/SimpleModule.Host/wwwroot/js/shell.js` | Mobile sidebar toggle, swipe gestures, scroll lock, escape key, matchMedia cleanup | +| `packages/SimpleModule.Theme.Default/theme.css` | Hide collapse toggle on mobile, scrollbar-hide, tab underline styles, prefers-reduced-motion | + +--- + +## Chunk 1: Theme CSS + shell.js Foundation + +### Task 1: Add mobile CSS utilities to theme.css + +**Files:** +- Modify: `packages/SimpleModule.Theme.Default/theme.css:582-614` + +- [ ] **Step 1: Hide sidebar collapse toggle on mobile** + +In the `.app-sidebar-toggle` rule (line 583), remove `flex` from the `@apply` directive and add `display: none` to hide on mobile. Then show on desktop with `display: flex`: + +```css +.app-sidebar-toggle { + @apply fixed bottom-4 z-50 w-7 h-7 rounded-full bg-surface border border-border items-center justify-center text-text-muted hover:text-text hover:bg-surface-raised cursor-pointer transition-all duration-200; + left: 1rem; + display: none; +} +``` + +Note: `flex` was removed from `@apply` because `display: none` needs to win on mobile. On desktop, `display: flex` is set explicitly. + +In the `@media (min-width: 768px)` block, add: +```css +.app-sidebar-toggle { + display: flex; +} +``` + +- [ ] **Step 2: Add scrollbar-hide utility** + +After the existing `.table-responsive` block (around line 339), add inside `@layer components`: + +```css +/* --- Scrollbar hide (for horizontal tab bars) --- */ +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} +.scrollbar-hide::-webkit-scrollbar { + display: none; +} +``` + +- [ ] **Step 3: Add manage tab styles** + +After the scrollbar-hide block, add inside `@layer components`: + +```css +/* --- Manage account tab bar --- */ +.manage-tab { + @apply inline-flex items-center gap-2 px-4 py-2.5 text-sm whitespace-nowrap text-text-muted no-underline transition-colors duration-150 border-b-2 border-transparent shrink-0; +} +.manage-tab:hover { + @apply text-text; +} +.manage-tab-active { + @apply inline-flex items-center gap-2 px-4 py-2.5 text-sm whitespace-nowrap text-primary no-underline font-semibold border-b-2 border-primary shrink-0; +} +``` + +- [ ] **Step 4: Add prefers-reduced-motion rule** + +At the end of the file, before the closing, add: + +```css +/* --- Reduced motion --- */ +@media (prefers-reduced-motion: reduce) { + .app-sidebar, + .app-sidebar-backdrop, + .app-sidebar-toggle, + .public-overlay { + transition: none !important; + } +} +``` + +- [ ] **Step 5: Add public overlay styles** + +Inside the `@media (min-width: 768px)` block within the App Layout section, after `.app-mobile-header { display: none; }`: + +```css +.public-overlay { + display: none; +} +``` + +And in `@layer components` (outside the media query), add: + +```css +/* --- Public layout mobile overlay --- */ +.public-overlay { + @apply fixed inset-0 z-50 bg-surface; + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; +} +.public-overlay.open { + opacity: 1; + pointer-events: auto; +} +``` + +- [ ] **Step 6: Verify CSS builds** + +Run: `npm run build` +Expected: Build succeeds with no errors. + +- [ ] **Step 7: Commit** + +```bash +git add packages/SimpleModule.Theme.Default/theme.css +git commit -m "feat: add mobile CSS utilities for layout redesign" +``` + +--- + +### Task 2: Add mobile sidebar logic to shell.js + +**Files:** +- Modify: `template/SimpleModule.Host/wwwroot/js/shell.js` + +- [ ] **Step 1: Add mobile sidebar toggle function** + +Append to `shell.js` after the existing user dropdown code: + +```javascript +// ── Mobile sidebar ────────────────────────────────────── +(function () { + var sidebar = document.getElementById('app-sidebar'); + var backdrop = document.getElementById('app-sidebar-backdrop'); + var hamburger = document.getElementById('app-mobile-hamburger'); + if (!sidebar) return; + + function openSidebar() { + sidebar.classList.add('app-sidebar-open'); + if (backdrop) backdrop.classList.add('visible'); + if (hamburger) { + hamburger.setAttribute('aria-expanded', 'true'); + hamburger.setAttribute('aria-label', 'Close navigation'); + } + document.body.style.overflow = 'hidden'; + } + + function closeSidebar() { + sidebar.classList.remove('app-sidebar-open'); + if (backdrop) backdrop.classList.remove('visible'); + if (hamburger) { + hamburger.setAttribute('aria-expanded', 'false'); + hamburger.setAttribute('aria-label', 'Open navigation'); + } + document.body.style.overflow = ''; + } + + function isMobile() { + return !window.matchMedia('(min-width: 768px)').matches; + } + + // Public API + window.openMobileSidebar = openSidebar; + window.closeMobileSidebar = closeSidebar; + + // Backdrop click + if (backdrop) { + backdrop.addEventListener('click', closeSidebar); + } + + // Escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && isMobile() && sidebar.classList.contains('app-sidebar-open')) { + closeSidebar(); + } + }); + + // Close on desktop resize + var mql = window.matchMedia('(min-width: 768px)'); + mql.addEventListener('change', function (e) { + if (e.matches) { + closeSidebar(); + } + }); + + // Swipe gesture + var touchStartX = 0; + var touchStartY = 0; + var swiping = false; + + document.addEventListener('touchstart', function (e) { + var touch = e.touches[0]; + touchStartX = touch.clientX; + touchStartY = touch.clientY; + swiping = false; + // Only detect edge swipe when sidebar is closed + if (!sidebar.classList.contains('app-sidebar-open') && touchStartX <= 25) { + swiping = true; + } + // Detect swipe-to-close on sidebar or backdrop + if (sidebar.classList.contains('app-sidebar-open')) { + swiping = true; + } + }, { passive: true }); + + document.addEventListener('touchend', function (e) { + if (!swiping || !isMobile()) return; + var touch = e.changedTouches[0]; + var dx = touch.clientX - touchStartX; + var dy = Math.abs(touch.clientY - touchStartY); + // Ignore vertical scrolls + if (dy > Math.abs(dx)) return; + var isOpen = sidebar.classList.contains('app-sidebar-open'); + if (!isOpen && dx > 50 && touchStartX <= 25) { + openSidebar(); + } else if (isOpen && dx < -50) { + closeSidebar(); + } + }, { passive: true }); +})(); +``` + +- [ ] **Step 2: Verify JS is syntactically valid** + +Run: `node -c template/SimpleModule.Host/wwwroot/js/shell.js` +Expected: No syntax errors. + +- [ ] **Step 3: Commit** + +```bash +git add template/SimpleModule.Host/wwwroot/js/shell.js +git commit -m "feat: add mobile sidebar toggle, swipe gestures, and accessibility handlers" +``` + +--- + +## Chunk 2: AppLayout Mobile Header + Backdrop + +### Task 3: Add mobile header and backdrop to AppLayout.razor + +**Files:** +- Modify: `framework/SimpleModule.Blazor/Components/Layout/AppLayout.razor` + +- [ ] **Step 1: Add mobile header before the sidebar** + +Insert before `` tag** + +Insert right after `` (line 80) and before the sidebar collapse toggle button: + +```html + +
+``` + +- [ ] **Step 3: Update sidebar collapse toggle to be hidden on mobile** + +Change the existing toggle button (line 83): +```html +