2026-05-14 Gate 1 local. Third slice of the POS Section Rework arc (Narong spec item 9): the master-detail "Point of Sale" submenu shell. A new pos/layout.tsx gates the section and renders a left nav rail — Dashboard / Registers / Sessions / Orders — copying the NIX-OS-87 Settings pattern. The dark sidebar's "Register" item is untouched. No migration.
pos/layout.tsx gates the whole section on nix_cafe.pos.view once, then hands off to a client PosShell that renders the "Point of Sale" header + the PosNav rail + a pos-detail content pane.?config=N escape hatch — the in-shell register terminal is full-bleed (its own -m-8 cancels the layout padding) and must NOT get the rail. A Next.js layout.tsx can't read searchParams, so PosShell is a thin client wrapper: when ?config= is present it passes children straight through with no header/rail/wrapper — the terminal renders exactly as before./pos) · Registers (/pos/registers) · Sessions (/pos/sessions) · Orders (/pos/orders), active row from usePathname(), mobile collapses to a "← All of Point of Sale" back link./pos/registers and /pos/orders re-export the existing registers-admin and Orders pages so they render inside the shell; /pos/sessions is a placeholder for Slice 4's real session history.<h1> from pos-landing.tsx, the page's empty state, and the loading skeleton; the shell owns the single header.registers-client.tsx hardcoded router.push("/settings/registers?…"), which would have bounced a /pos/registers user back to Settings. Now uses window.location.pathname so it works wherever the component is mounted.Normal sub-route (/cafe/pos, /pos/registers, …) ?config=N register terminal
┌──────────────────────────────────────────┐ ┌──────────────────────────────┐
│ Point of Sale │ │ (full-bleed POS terminal — │
│ Registers, sessions, and orders… │ │ PosShell returns children │
│ ┌──────────┬───────────────────────────┐ │ │ bare, no header / rail / │
│ │ Dashboard│ pos-detail │ │ │ wrapper, so its own -m-8 │
│ │ Registers│ (the page's content │ │ │ still cancels the layout │
│ │ Sessions │ renders here) │ │ │ padding exactly as before) │
│ │ Orders │ │ │ │ │
│ └──────────┴───────────────────────────┘ │ └──────────────────────────────┘
└──────────────────────────────────────────┘
pos/layout.tsx (server) → requirePermission('nix_cafe.pos.view')
→ {children}
pos/_components/pos-shell.tsx → useSearchParams().get('config') != null
→ if register view: return <>{children}> (bare)
→ else: header + + {children}
pos/_components/pos-nav.tsx → the 4-item rail, active row from usePathname()
✓ Sessions placeholder renders via react-dom/server ✓ All Slice 3 source files present ✓ pos/layout.tsx gates on nix_cafe.pos.view + wraps PosShell in Suspense ✓ PosShell passes the ?config= register view through bare (no rail/header) ✓ PosNav rail has the 4 sub-items with correct hrefs ✓ Registers + Orders sub-routes re-export the existing pages ✓ Section h1 removed from the landing pages (the shell owns it now) ✓ registers-client shop selector is path-relative (works under /pos/registers) PosShell / PosNav depend on Next navigation hooks so they can't be renderToString'd standalone, and /cafe/pos is authed (can't render locally without the SSO stack). Gate 1 therefore renders the one hook-free component (the Sessions placeholder) + makes structural assertions against the source. Gate 2 does the real behavioural pass on prod — rail renders + navigates, ?config= still full-bleed, sub-routes load inside the shell.
new:
app/(authed)/pos/layout.tsx section shell + perm gate
app/(authed)/pos/_components/pos-shell.tsx client: rail+header / ?config= passthrough
app/(authed)/pos/_components/pos-nav.tsx client: the 4-item rail
app/(authed)/pos/registers/page.tsx re-exports settings/registers/page
app/(authed)/pos/orders/page.tsx re-exports orders/page
app/(authed)/pos/sessions/page.tsx placeholder (Slice 4 builds the real one)
edited:
app/(authed)/pos/pos-landing.tsx dropped its own "Point of Sale" h1
app/(authed)/pos/page.tsx dropped the h1 from the empty state
app/(authed)/pos/loading.tsx dropped the fake-h1 skeleton block
app/(authed)/settings/registers/registers-client.tsx
shop selector now path-relative
Gate 2 (prod, get-coffee + lumiere):
• /cafe/pos renders the header + 4-item rail; Dashboard is the landing
• click Registers / Sessions / Orders — rail stays, detail pane swaps,
each sub-route loads inside the shell
• /cafe/pos?config=N still renders the full-bleed terminal (no rail)
• 59/59 regression (r1-2-landing + r7 in the blast radius)
Carry-overs for Slice 5 (the memo already scopes these):
• /pos/registers re-exports the Settings page, which still
requirePermission('nix_cafe.settings.view') — combined with the
layout's nix_cafe.pos.view that's a stricter gate than the old
/cafe/settings/registers. Slice 5 settles permission gating.
• Slice 5 also drops the Registers row from the Settings nav + adds a
redirect stub, and relocates Orders off the top-level sidebar.