6.6 KiB
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}— ownsisExpanedMenu: boolean, appliesmargin-left: -280pxto 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()">, emitsexpanedMenuevent.src/app/layout/components/menu-bar/menu-bar.component.{ts,html,scss}— sidebar shell.menu-bar.component.html:1hardcodesstyle="min-width: 260px; max-width: 260px; ...". Iteratesmenus(=routesfromnavigator.ts) and rendersapp-head-menu/app-basic-menu/app-collapsable/ divider peritem.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*ngFortype-switch overitem.children. Existing accordion expand/collapse state (isCollapsed) is per-group and unrelated to the new rail-mode toggle.src/app/core/data/navigator.ts— theSeItem[]data array driving the whole menu (id, title, type, icon, children, link, ischildActive, ...). Child items conventionally geticon: 'fiber_manual_record', styled down to a 7px dot viacollapsable.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: booleantoMainLayoutComponent, replacing the current use ofisExpanedMenufor the hide/show margin trick. Initialize fromlocalStorage.getItem('sidebar_rail_mode') === 'true'on construction; write back on every toggle. ToolBarComponent's existing hamburger button keeps emitting the sameexpanedMenu(or a renamed but equivalent) event;MainLayoutComponentflipsisRailModeand persists it.- Pass
isRailModeintoMenuBarComponentas an@Input().MenuBarComponentpasses it straight through toapp-head-menu/app-basic-menu/app-collapsableas 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 inlinemin-width/max-width: 260pxwith an[ngClass]/[ngStyle]binding driven byisRailMode(260px vs. ~68px), plus a CSStransition: width 0.2s ease(or reuseSEAnimationsconventions 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
isRailModeis true, add a nativetitle="{{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/routerLinkhandling).
4. Parent items with children (collapsable)
- When
isRailModeis 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
mouseenterof the icon (with a short close delay onmouseleavefrom 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
connectedPositionanchored 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/nestedapp-collapsablein 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
ischildActiveflag, 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.tsdata (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 acollapsableitem'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.