Skip to content

feat(frontend): app shell with resizable chat drawer (M6.3) - #71

Merged
vgtray merged 29 commits into
mainfrom
feat/36-app-shell
Apr 22, 2026
Merged

feat(frontend): app shell with resizable chat drawer (M6.3)#71
vgtray merged 29 commits into
mainfrom
feat/36-app-shell

Conversation

@vgtray

@vgtray vgtray commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces the unique layout hosting the full demo (issue #36): sticky topbar + CSS grid main area with docked resizable chat drawer.

  • AppShell (frontend/src/app/AppShell.tsx) — viewport host, topbar + main grid, Cmd/Ctrl+K global handler with input bail-out
  • TopBar (frontend/src/app/TopBar.tsx) — AriaMark + equipment selector (P-01 / P-02 / Tank-A / Tank-B) + KpiBar placeholder slot (filled M7.2) + time-based shift indicator (A/B/C) + MetaStrip UNIT/CELL/REV + toggle button
  • Drawer (frontend/src/app/Drawer.tsx) — docked resizable 360-640px, pointer drag + keyboard separator (Arrow/Home/End), ARIA-wired (role="separator", aria-valuenow/min/max, toggle has aria-expanded/aria-controls). Intentionally distinct from the overlay-modal Drawer primitive in the DS.
  • routes (frontend/src/app/routes.tsx) — //control-room ; /control-room (RequireAuth + AppShell + Outlet) ; /login, /data, /design preserved ; catch-all → /control-room
  • ControlRoomPage (frontend/src/pages/ControlRoomPage.tsx) — placeholder
  • useLocalStorage (frontend/src/lib/useLocalStorage.ts) — SSR-safe typed hook with storage event cross-tab sync, quota/JSON errors swallowed

Drawer state (aria.chatDrawer) and equipment (aria.selectedEquipment) persist across reloads. Width is clamped 360-640 defensively at read-time (sanitize()).

Design discipline

  • Zero new deps
  • Zero hex literal outside tokens.css (palette §2)
  • Icons only via design-system/icons (Icons.PanelRightOpen/Close) with stroke-width 1.5 (§7)
  • Motion via --ds-motion-* tokens — no framer-motion runtime for the shell itself; tokens already fall to 0ms under prefers-reduced-motion (§6)
  • No §9 anti-patterns (no gradients, no glassmorphism, no rounded-2xl, no glow, no GSAP/Lenis/SplitText)

Acceptance (issue #36)

  • Resize drawer persists across reloads
  • Cmd/Ctrl+K toggles drawer, bails out when typing in input/textarea/select/contenteditable
  • Viewport min 1280×800 supported
  • Topbar never scrolls (sticky top-0)

Test plan

  • npm run typecheck — green
  • npm run build — green (439.85 kB / 139.93 kB gz)
  • npm run check (Biome) — green, 31 files, 0 errors
  • Manual: open /control-room, resize drawer, reload, verify width/open persist
  • Manual: Cmd/Ctrl+K toggles drawer ; does nothing when focus is in an input
  • Manual: keyboard on resize separator (Arrow/Home/End) adjusts width

Notes for reviewer

Three DS-related nits flagged by QA for follow-up (out of scope for this PR):

  • LoginPage redirect target is still /data, to realign once ControlRoomPage has content
  • LoginPage uses off-DS classes (pre-existing, 43 occurrences to refactor before demo)
  • Default credentials admin/admin123 hardcoded in LoginPage (dev convenience, guard with import.meta.env.DEV before demo)

Will open separate issues for these.

Closes #36

Introduce the unique layout that hosts the full demo:

- `AppShell`: flex viewport host with sticky topbar + CSS grid main area
  (control room left / docked chat drawer right)
- `TopBar`: AriaMark + equipment selector (P-01/P-02/Tank-A/Tank-B),
  KpiBar placeholder slot (filled in M7.2), time-based shift indicator
  (A/B/C), MetaStrip with UNIT/CELL/REV
- `Drawer`: docked resizable chat drawer (360-640px), pointer drag +
  keyboard (arrows/Home/End) on a window-splitter separator, ARIA-wired
- `routes`: `/` -> `/control-room`, RequireAuth + AppShell + Outlet;
  `/login`, `/data`, `/design` preserved; catch-all -> `/control-room`
- `ControlRoomPage`: placeholder shell (SectionHeader + Hairline)
- `useLocalStorage`: SSR-safe typed hook with cross-tab sync
- Drawer state (`aria.chatDrawer`) and equipment (`aria.selectedEquipment`)
  persist across reloads, width clamped 360-640 defensively
- Cmd/Ctrl+K toggles the drawer, bails out when typing in input/select/
  textarea/contenteditable

Zero new deps, tokens-only (no hex literals), icons via DS wrapper,
motion via `--ds-motion-*` tokens (respects prefers-reduced-motion).

Closes #36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the M6.3 frontend “app shell” layout for the demo: a sticky top bar plus a CSS-grid main area with a docked, resizable chat drawer, and updates routing so /control-room becomes the default authenticated landing page.

Changes:

  • Introduces AppShell + TopBar + docked resizable Drawer (pointer + keyboard resize, ARIA-wired) and persists drawer/equipment state via useLocalStorage.
  • Adds /control-room route (protected) and redirects / + catch-all to it; keeps /login, /data, /design.
  • Adds a placeholder ControlRoomPage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
frontend/src/pages/ControlRoomPage.tsx Adds placeholder content for the new control-room route.
frontend/src/lib/useLocalStorage.ts Adds typed localStorage hook with SSR-safe read and cross-tab sync.
frontend/src/app/routes.tsx Defines new routing structure and default redirect to /control-room.
frontend/src/app/TopBar.tsx Implements the sticky top bar UI and drawer toggle control.
frontend/src/app/Drawer.tsx Implements docked resizable chat drawer with pointer + keyboard interactions.
frontend/src/app/AppShell.tsx Composes top bar + main grid + drawer; adds global Cmd/Ctrl+K handler and persistence wiring.
frontend/src/App.tsx Switches App to use the new centralized AppRoutes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +40 to +50
function onStorage(e: StorageEvent) {
if (e.key !== keyRef.current || e.newValue === null) return;
try {
setStored(JSON.parse(e.newValue) as T);
} catch {
// ignore
}
}
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useLocalStorage ignores storage events where e.newValue === null (e.g., another tab calls removeItem/clear). That means state will not actually stay in sync across tabs in that case, despite the hook’s contract. Consider resetting to initialValue (or re-running readValue()) when newValue is null, and also handling key changes if the hook is used with a dynamic key.

Suggested change
function onStorage(e: StorageEvent) {
if (e.key !== keyRef.current || e.newValue === null) return;
try {
setStored(JSON.parse(e.newValue) as T);
} catch {
// ignore
}
}
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);
setStored(readValue());
}, [readValue]);
useEffect(() => {
function onStorage(e: StorageEvent) {
if (e.storageArea !== window.localStorage) return;
if (e.key !== null && e.key !== keyRef.current) return;
setStored(readValue());
}
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, [readValue]);

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +66
useEffect(
() => () => {
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp);
},
[onPointerMove, onPointerUp],
);

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect cleanup removes pointer listeners but doesn’t reset document.body.style.cursor / userSelect if the component unmounts mid-drag (e.g., route change while resizing). This can leave the whole app stuck in col-resize cursor / userSelect: none. Update the cleanup (and/or onPointerUp) to always restore the body styles and clear draggingRef when tearing down.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +57
setDrawer((prev) => ({ ...sanitizeDrawer(prev), open: !prev.open }));
}, [setDrawer]);

const setDrawerWidth = useCallback(
(width: number) => {
setDrawer((prev) => ({ ...sanitizeDrawer(prev), width }));

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleDrawer sanitizes prev but then computes the next open value from the unsanitized prev.open (open: !prev.open). If the persisted value is non-boolean (corrupt localStorage), toggling can behave inconsistently with safeDrawer. Compute the toggle from the sanitized value instead (and similarly clamp width when persisting in setDrawerWidth so localStorage doesn’t store out-of-range widths if onWidthChange is ever called with an invalid value).

Suggested change
setDrawer((prev) => ({ ...sanitizeDrawer(prev), open: !prev.open }));
}, [setDrawer]);
const setDrawerWidth = useCallback(
(width: number) => {
setDrawer((prev) => ({ ...sanitizeDrawer(prev), width }));
setDrawer((prev) => {
const safePrev = sanitizeDrawer(prev);
return { ...safePrev, open: !safePrev.open };
});
}, [setDrawer]);
const setDrawerWidth = useCallback(
(width: number) => {
setDrawer((prev) => {
const safePrev = sanitizeDrawer(prev);
const safeWidth = Math.max(
DRAWER_MIN_WIDTH,
Math.min(DRAWER_MAX_WIDTH, Math.round(width ?? DRAWER_DEFAULT_WIDTH)),
);
return { ...safePrev, width: safeWidth };
});

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +114
<DrawerIcon className="size-4" />
<KbdKey className="ml-1">⌘K</KbdKey>
</button>

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shortcut hint is always rendered as “⌘K”, but the handler uses Ctrl+K on non-Mac platforms. This is misleading for Windows/Linux users; consider rendering “CtrlK” (or “Ctrl+K”) when metaKey isn’t the primary modifier, or making the handler accept either Ctrl or ⌘ and updating the label accordingly.

Copilot uses AI. Check for mistakes.
zestones and others added 19 commits April 22, 2026 20:50
…humain

11 m24 toolspy 3 tools contexte humain
…n-peuvent-être-skippés

13 m26 toolspy 2 tools production peuvent être skippés
…discovery-call_tool

14 m27 mcpclient singleton auto discovery call tool
Replace the flat hardcoded equipment select with a SCADA-style
command-palette that reflects the real DB model (Enterprise → Site →
Area → Line → Cell) via `GET /api/v1/hierarchy/tree`.

- `EquipmentPicker`: trigger button with bracketed breadcrumb
  (`[EQ] cellName · SITE · AREA · LINE`) + `⌘⇧E` shortcut. Popover
  with search (token-split, case-insensitive), tree grouped by line,
  keyboard navigation (↑↓ Home End wrap, Enter commit, Esc restore),
  StatusRail per row, disabled cells greyed. Full ARIA (dialog+modal,
  listbox/option, activedescendant, expanded/haspopup/controls).
- `lib/hierarchy`: ISA-95 DTO types, `useHierarchyTree` TanStack query
  (staleTime 60s), `flattenTree/searchCells/groupByLine`, `findCell`
  helpers. `EquipmentSelection` now carries `{lineId, lineName, cellId,
  cellName, siteName, areaName}` for live MetaStrip binding.
- `TopBar`: MetaStrip binds to the live selection (`SITE / LINE / REV`
  instead of hardcoded `UNIT / CELL / REV`).
- `AppShell`: `aria.selectedEquipment` shape migrated; old string
  payload ("P-01") is dropped via a new `validator` option on
  `useLocalStorage`, which also filters out stale shapes arriving on
  cross-tab `storage` events. Auto-seeds the first enabled cell when
  no selection exists.

Zero new deps. All tokens-only (no hex literals). Icons via DS wrapper.
Motion via `Motion.fadeInUp` + `--ds-motion-*` tokens.

`npm run typecheck` / `npm run build` / `npm run check` all green.
Bundle 453.33 kB / 143.36 kB gzipped (+3 kB vs baseline).
@vgtray

vgtray commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator Author

Update — hierarchical equipment picker

The flat <select> in the topbar has been replaced by a SCADA-style command-palette that reflects the real ISA-95 DB model (Enterprise → Site → Area → Line → Cell) via GET /api/v1/hierarchy/tree.

What changed

  • EquipmentPicker trigger + popover with search (token AND, case-insensitive), tree grouped by line, keyboard nav (↑↓ Home End wrap, Enter commit, Esc restore focus), StatusRail per row, disabled cells greyed. Full ARIA (dialog+modal, listbox/option, aria-activedescendant).
  • ⌘⇧E / Ctrl+Shift+E global shortcut with input/textarea/select/contenteditable bail-out.
  • lib/hierarchy.ts (new): ISA-95 DTO types, useHierarchyTree TanStack query (staleTime 60 s), helpers (flattenTree, searchCells, groupByLine, findCell). EquipmentSelection carries {lineId, lineName, cellId, cellName, siteName, areaName}.
  • TopBar: MetaStrip binds to the live selection (SITE / LINE / REV).
  • AppShell: aria.selectedEquipment shape migrated; old string payload ("P-01") dropped via a new validator option on useLocalStorage, which also filters out stale shapes arriving on cross-tab storage events. First-enabled cell auto-seeded when no selection exists.

Compliance

  • Zero new deps
  • Zero hex literal outside tokens.css
  • Icons only via design-system/icons
  • Motion via Motion.fadeInUp + --ds-motion-* tokens
  • §9 anti-patterns: none hit

Gates

  • npm run typecheck
  • npm run build ✓ — 453.33 kB JS / 143.36 kB gzipped (+3 kB vs baseline)
  • npm run check (Biome) ✓

zestones and others added 3 commits April 22, 2026 21:51
- Introduced unit tests for the MCPClient singleton and its interaction with FastMCP.
- Added tests for the MCP to Anthropic schema adapter to ensure correct transformation of tool descriptors.
- Implemented smoke tests for ARIA MCP tool registration to verify the registration of KPI and signal tools.
- Created unit tests for datetime helper functions to validate timezone-aware parsing.
- Added threshold evaluation tests to ensure correct behavior of threshold breach logic.
- Developed tests for merging structured data in the knowledge base module.
- Implemented validation tests for the KbRepository to ensure upsert operations maintain data integrity.
- Added contract tests for the EquipmentKB domain model to validate the expected JSON structure.
- Created unit tests for WorkOrder schemas to validate new fields and status transitions.
feat: unit tests for MCPClient, schema adapter, and work order schemas
zestones and others added 6 commits April 22, 2026 22:05
…r_-déclarés-dans-les-agents

16 m29 UI tools generative render  déclarés dans les agents
Introduce the unique layout that hosts the full demo:

- `AppShell`: flex viewport host with sticky topbar + CSS grid main area
  (control room left / docked chat drawer right)
- `TopBar`: AriaMark + equipment selector (P-01/P-02/Tank-A/Tank-B),
  KpiBar placeholder slot (filled in M7.2), time-based shift indicator
  (A/B/C), MetaStrip with UNIT/CELL/REV
- `Drawer`: docked resizable chat drawer (360-640px), pointer drag +
  keyboard (arrows/Home/End) on a window-splitter separator, ARIA-wired
- `routes`: `/` -> `/control-room`, RequireAuth + AppShell + Outlet;
  `/login`, `/data`, `/design` preserved; catch-all -> `/control-room`
- `ControlRoomPage`: placeholder shell (SectionHeader + Hairline)
- `useLocalStorage`: SSR-safe typed hook with cross-tab sync
- Drawer state (`aria.chatDrawer`) and equipment (`aria.selectedEquipment`)
  persist across reloads, width clamped 360-640 defensively
- Cmd/Ctrl+K toggles the drawer, bails out when typing in input/select/
  textarea/contenteditable

Zero new deps, tokens-only (no hex literals), icons via DS wrapper,
motion via `--ds-motion-*` tokens (respects prefers-reduced-motion).

Closes #36
Replace the flat hardcoded equipment select with a SCADA-style
command-palette that reflects the real DB model (Enterprise → Site →
Area → Line → Cell) via `GET /api/v1/hierarchy/tree`.

- `EquipmentPicker`: trigger button with bracketed breadcrumb
  (`[EQ] cellName · SITE · AREA · LINE`) + `⌘⇧E` shortcut. Popover
  with search (token-split, case-insensitive), tree grouped by line,
  keyboard navigation (↑↓ Home End wrap, Enter commit, Esc restore),
  StatusRail per row, disabled cells greyed. Full ARIA (dialog+modal,
  listbox/option, activedescendant, expanded/haspopup/controls).
- `lib/hierarchy`: ISA-95 DTO types, `useHierarchyTree` TanStack query
  (staleTime 60s), `flattenTree/searchCells/groupByLine`, `findCell`
  helpers. `EquipmentSelection` now carries `{lineId, lineName, cellId,
  cellName, siteName, areaName}` for live MetaStrip binding.
- `TopBar`: MetaStrip binds to the live selection (`SITE / LINE / REV`
  instead of hardcoded `UNIT / CELL / REV`).
- `AppShell`: `aria.selectedEquipment` shape migrated; old string
  payload ("P-01") is dropped via a new `validator` option on
  `useLocalStorage`, which also filters out stale shapes arriving on
  cross-tab `storage` events. Auto-seeds the first enabled cell when
  no selection exists.

Zero new deps. All tokens-only (no hex literals). Icons via DS wrapper.
Motion via `Motion.fadeInUp` + `--ds-motion-*` tokens.

`npm run typecheck` / `npm run build` / `npm run check` all green.
Bundle 453.33 kB / 143.36 kB gzipped (+3 kB vs baseline).
…t picker

Pivot the visual identity from SCADA/Bloomberg-Terminal ("Editorial
Industrial Telemetry" v1) to Linear/Vercel/Stripe-calm ("Operator-calm"
v2). Refactor the equipment picker from a flat line-grouped list to a
5-level ISA-95 Plant Structure tree.

Design pivot (DESIGN_PLAN_v2.md):
- Palette: warm neutrals, blue accent (#3478f6 dark / #2563eb light),
  no more cyan. Dark + light mode first-class via ThemeProvider +
  ThemeToggle (System/Dark/Light), pre-mount script in index.html for
  zero flash.
- Typography: Inter everywhere, JetBrains Mono reserved to numerics /
  IDs / kbd / code only. Normal-case everywhere; no more mono-caps
  +0.08em as default signature.
- Radii: collapsed from 5 to 3 levels (sm 6 / md 10 / lg 14). No more
  `rounded-full` except StatusDot.
- Signatures: bracketed labels removed ("[ CONTROL ROOM ]" → "Control
  room"), registration marks removed ("UNIT / D-02 · CELL / 02.01" →
  "P-02 · Apr 22, 2026" meta-lines). Grain overlay, CRT scanline, and
  default status rail on all cards all removed. Shadow overlay token
  reintroduced for floating surfaces only.
- Legacy `styles/dark.css` + `styles/light.css` dropped (unused tokens
  concurrent with v2). `--ds-accent-glow`, `--ds-radius-xs/xl`,
  `--ds-text-display`, Badge `tag` prop all removed.

Equipment picker tree:
- 5-level ISA-95 tree (Enterprise → Site → Area → Line → Cell) with
  chevrons, monochrome level icons (Building2/MapPin/Layers/GitBranch/
  Cpu), right-aligned counts on containers.
- Auto-expand chain to current selection on open (or full expand when
  no selection). Live search auto-expands matching branches; siblings
  remain visible dimmed for hierarchical context.
- Keyboard: ↑/↓ navigate all visible nodes, →/← expand/collapse-or-
  ascend, Enter commit cell or toggle container, Home/End jump, Esc
  close. Full ARIA tree pattern (role=tree/treeitem, aria-level,
  aria-expanded, aria-selected, aria-activedescendant).
- Popover resized 460×520 to accommodate indent depth. localStorage
  persistence, cross-tab validator, auto-seed, ⌘⇧E shortcut all
  preserved.

Zero new deps. All quality gates green:
- typecheck ✓
- build ✓ (461.30 kB / 145.62 kB gzip)
- check (biome) ✓ (35 files, 0 errors)

Known follow-up: 5 Lucide icons imported directly in EquipmentPicker
bypass the design-system/icons wrapper (scope strict on that file). To
route through the DS wrapper in a follow-up issue.

Follows DESIGN_PLAN_v2.md §1-§12. Follow-ups tracked in issues.
@vgtray

vgtray commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator Author

Update 3 — operator-calm design pivot + ISA-95 tree view (final for this PR)

Two big changes squashed into commit `aa67504`:

1. Design pivot (DESIGN_PLAN_v2 "Operator-calm")

Visual identity moved from SCADA / Bloomberg Terminal to Linear / Vercel / Stripe-calm (user feedback on the previous round).

  • Palette: warm neutrals + blue accent (`#3478f6` dark / `#2563eb` light). No more cyan.
  • Light mode first-class — new `ThemeProvider` + `ThemeToggle` (System / Dark / Light) with pre-mount script in `index.html` for zero-flash.
  • Typography: Inter everywhere, JetBrains Mono reserved to numerics / IDs / kbd / code only. Normal-case across the app — no more mono-caps `+0.08em` signature.
  • Radii collapsed from 5 to 3 levels (`sm 6 / md 10 / lg 14`).
  • Removed: grain SVG overlay (`body::before`), bracketed labels (`[ CONTROL ROOM ]` etc.), registration marks (`UNIT / D-02 · CELL / 02.01`), CRT scanline, default status rail on all cards, `--ds-accent-glow`, `--ds-radius-xs/xl`, `--ds-text-display`, Badge `tag` prop.
  • Added: `--ds-bg-hover`, `--ds-accent-ring`, `--ds-accent-soft`, `--ds-status-info`, `--ds-shadow-overlay`, full `[data-theme="light"]` token block.
  • Dropped legacy `styles/dark.css` + `styles/light.css` (unused tokens concurrent with v2).
  • New `DESIGN_PLAN_v2.md`, v1 archived as `DESIGN_PLAN_v1_deprecated.md`.

2. Equipment picker — full ISA-95 tree view

The picker moved from flat line-grouped list to the 5-level ISA-95 Plant Structure pattern (Enterprise → Site → Area → Line → Cell).

  • Chevrons expand/collapse, monochrome level icons (`Building2` / `MapPin` / `Layers` / `GitBranch` / `Cpu`), right-aligned counts on containers.
  • Auto-expands the chain to the current selection on open; full expand when no selection.
  • Live search auto-expands matching branches; non-match siblings remain visible dimmed for hierarchical context (Linear/command-palette pattern).
  • Keyboard: `↑/↓` all visible nodes · `→/←` expand/collapse-or-ascend · `Enter` commit or toggle · `Home`/`End` jump · `Esc` close.
  • Full ARIA tree pattern: `role=tree/treeitem`, `aria-level`, `aria-expanded`, `aria-selected`, `aria-activedescendant`.
  • Popover resized 460×520 to accommodate 5-level indent.
  • localStorage persistence, cross-tab validator, auto-seed first cell, `⌘⇧E` shortcut — all preserved.

Gates

  • `npm run typecheck` ✓
  • `npm run build` ✓ (461.30 kB / 145.62 kB gzipped)
  • `npm run check` (Biome) ✓ (35 files, 0 errors)

Follow-ups opened

QA status

QA round not run on this iteration — user choice (visual validation in loop). A follow-up QA sweep can be done before merge if desired.

@vgtray
vgtray merged commit 8e46865 into main Apr 22, 2026
9 checks passed
@vgtray
vgtray deleted the feat/36-app-shell branch April 22, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M6.3 — App shell (topbar + control room area + chat drawer)

3 participants