Files
rmutr/docs/superpowers/specs/2026-07-07-sidebar-icon-rail-design.md
2026-07-07 00:05:34 +07:00

6.6 KiB

Sidebar collapse-to-icon-rail — Design Spec

Problem

The main sidebar (app-menu-bar) is a fixed 260px-wide column with icons + Thai text labels for every menu item. The only existing toggle (hamburger button in the toolbar) hides the entire sidebar off-screen via a negative margin-left — there is no "shrink to icon-only rail" mode like the reference screenshots the user provided (a narrow ~64-72px column showing just icons, with a small >> affordance to re-expand).

Goal

Repurpose the existing hamburger toggle so it switches the sidebar between:

  • Full mode (current behavior): 260px wide, icons + text, current accordion sub-menus.
  • Rail mode (new): ~64-72px wide, icons only. Leaf items still navigate on click. Items with children (accordion groups) show a floating flyout popup (triggered by hover/click) listing the children as full-text links.

The hamburger no longer hides the sidebar entirely — it only toggles between these two width states. State persists across reloads via localStorage.

Current architecture (relevant files)

  • src/app/layout/main-layout/main-layout.component.{ts,html} — owns isExpanedMenu: boolean, applies margin-left: -280px to hide the sidebar, listens to (expanedMenu) from the toolbar.
  • src/app/layout/components/tool-bar/tool-bar.component.ts — hamburger <button class="menu-toggle-btn" (click)="toggleMenu()">, emits expanedMenu event.
  • src/app/layout/components/menu-bar/menu-bar.component.{ts,html,scss} — sidebar shell. menu-bar.component.html:1 hardcodes style="min-width: 260px; max-width: 260px; ...". Iterates menus (= routes from navigator.ts) and renders app-head-menu / app-basic-menu / app-collapsable / divider per item.type.
  • src/app/layout/components/menu/head-menu/* — flat top-level leaf link (icon + text).
  • src/app/layout/components/menu/basic-menu/* — leaf link, used at any nesting depth.
  • src/app/layout/components/menu/collapsable/* — expandable parent; recurses the same *ngFor type-switch over item.children. Existing accordion expand/collapse state (isCollapsed) is per-group and unrelated to the new rail-mode toggle.
  • src/app/core/data/navigator.ts — the SeItem[] data array driving the whole menu (id, title, type, icon, children, link, ischildActive, ...). Child items conventionally get icon: 'fiber_manual_record', styled down to a 7px dot via collapsable.component.scss — i.e. children have no meaningful icon of their own, which is why rail mode must present them via a text flyout rather than shrinking them to an icon.

Design

1. State: isRailMode

  • Add isRailMode: boolean to MainLayoutComponent, replacing the current use of isExpanedMenu for the hide/show margin trick. Initialize from localStorage.getItem('sidebar_rail_mode') === 'true' on construction; write back on every toggle.
  • ToolBarComponent's existing hamburger button keeps emitting the same expanedMenu (or a renamed but equivalent) event; MainLayoutComponent flips isRailMode and persists it.
  • Pass isRailMode into MenuBarComponent as an @Input(). MenuBarComponent passes it straight through to app-head-menu / app-basic-menu / app-collapsable as an @Input() on each, since the same three components recurse at every nesting depth.

2. Sidebar shell width

  • menu-bar.component.html: replace the hardcoded inline min-width/max-width: 260px with an [ngClass]/[ngStyle] binding driven by isRailMode (260px vs. ~68px), plus a CSS transition: width 0.2s ease (or reuse SEAnimations conventions already used for the accordion expand) so the resize animates smoothly.
  • Header block (logo + "มทร.รัตนโกสินทร์ / ระบบแผนงานและงบประมาณ"): wrap the text block in *ngIf="!isRailMode"; keep the logo icon visible and centered in both modes.
  • Divider items: hide entirely in rail mode (*ngIf="!isRailMode" on the divider <div>), since a horizontal rule with no label doesn't carry meaning at that width.

3. Leaf items (head-menu, basic-menu)

  • Title <span> wrapped in *ngIf="!isRailMode".
  • When isRailMode is true, add a native title="{{item.title}}" attribute on the icon button/row so hovering shows the label as a browser tooltip — cheap, no new UI component needed.
  • Click behavior unchanged (still navigates via existing link/routerLink handling).

4. Parent items with children (collapsable)

  • When isRailMode is true, the component no longer renders its children inline/expanded in the document flow. Instead:
    • The parent icon becomes the trigger for a flyout popup.
    • Flyout opens on mouseenter of the icon (with a short close delay on mouseleave from both the icon and the popup, so the user can move the cursor into the popup) and also toggles on click, for touch/keyboard accessibility.
    • Flyout is a small absolutely-positioned panel anchored to the right of the icon (use Angular CDK Overlay, already a project dependency, with a connectedPosition anchored to the trigger element — avoids manual z-index/positioning math and handles viewport-edge flipping for free).
    • Flyout content: the same recursive item list (title text + click-through), reusing app-basic-menu/nested app-collapsable in their normal (non-rail) rendering, since inside the flyout there's room for full labels.
    • Closes on: selecting an item (navigation), clicking outside, or Escape.
  • Active-group indicator: if any descendant matches the existing ischildActive flag, render a small colored dot/border accent on the parent's icon in rail mode (reusing the existing active-state accent color already used for the expanded active row) so the user can tell which group the current page belongs to without opening the flyout.

5. Out of scope

  • No new/second toggle button — the existing hamburger is the only control, per the approved design.
  • No responsive/mobile breakpoint behavior (none exists today; not part of this change).
  • No changes to navigator.ts data (icons, children, structure) — this is purely a rendering-mode change layered on the existing data-driven menu.

Testing

  • Manual verification via the run/browser flow: toggle rail mode, confirm width animates, confirm leaf items navigate, confirm a collapsable item's flyout opens/closes correctly (hover and click), confirm the active-group indicator shows on the right icon when a child route is active, confirm state survives a full page reload (localStorage).
  • No existing automated test suite covers the layout/menu components (none found during exploration) — this change ships without new automated tests, consistent with the rest of the layout code.