20 KiB
Raw Blame History

name description tags
sage-frontend Sage platform frontend — shell architecture, UX design methodology, deployment workflow, theme system, menu/navigation patterns, and bricks-compatible implementation.
sage
frontend
shell
deployment
ux-design
theme
bricks
menu

Sage Frontend: Shell, Design & Deployment

When to Use

  • Designing new Sage UI features or redesigning existing ones
  • Modifying shell layout (index.ui, global_menu.ui, sidebar, topbar)
  • Deploying UI changes to production
  • Working with theme CSS/JS (dark/light mode, shell_theme.*)
  • Adding/modifying navigation menu items with role-based visibility
  • Building bricks-compatible UI files (.ui, .dspy)

Architecture Overview

Sage shell files live at the sage level, not in modules:

File Location Purpose
index.ui sage/wwwroot/ Shell layout (header + sidebar + main content area)
global_menu.ui sage/wwwroot/ Global navigation menu (all modules, role-based visibility)
shell_theme.js sage/wwwroot/ Theme toggle, sidebar collapse, SPA router, sageReloadMenu()
shell_theme.css sage/wwwroot/ Theme CSS variables, dark/light overrides

Module responsibility: Each module only handles its own content when its menu items are clicked. No module owns the shell layout or global menu.

.sage-main uses overflow: hidden — Each page is responsible for its own scrolling via VScrollPanel with css: "filler".

Design Methodology (Phased Approval — MANDATORY)

Phase 1: System Analysis

  1. Map all modules: scan pkgs/*/wwwroot/*.ui, pkgs/*/json/*.json
  2. Identify user roles from RBAC module, role-permission mappings, menu templates
  3. Classify pages: Dashboard, List/CRUD, Form/Edit, Detail, Settings
  4. Audit pain points: layout inconsistencies, color fragmentation, navigation issues

Phase 2: Design Proposal (PRESENT TO USER — DO NOT IMPLEMENT)

Present as terminal text (ASCII diagrams + tables):

  1. Design Philosophy + core principles + color system
  2. Global Layout (ASCII wireframe)
  3. Key Interface Mockups (dashboard, list, form, detail)
  4. Bricks Widget Mapping Table
  5. File Organization Plan
  6. Implementation Phases
  7. Open Questions

STOP HERE. Wait for user feedback before coding.

Phase 3: Implementation (ONLY AFTER APPROVAL)

Follow bricks-framework conventions strictly. Maintain backward compatibility.

See references/design-methodology.md for full workflow detail and references/design-system.md for color system, spacing, and widget mapping.

Design System (Dark Theme Default)

Color System

Background: #0B1120 (global), #111827 (panel), #1E293B (card), #334155 (hover)
Borders:    #334155 (strong), #1E293B (subtle)
Text:       #F1F5F9 (primary), #94A3B8 (secondary), #64748B (tertiary)
Brand:      #3B82F6 (primary), #22C55E (success), #F59E0B (warning), #EF4444 (danger)

Spacing

Container: 24px | Card: 20px | Between sections: 16px | Between elements: 12px | Tight: 8px

Card Pattern

VBox: bgcolor:#1E293B, borderRadius:12px, padding:20px, border:1px solid #334155

See references/design-system.md for full widget mapping table and responsive layout patterns.

Deployment Workflow

Git Strategy

sage/wwwroot/ is tracked in git (removed from .gitignore). Only real files go in git. Module symlinks are created by build.sh and MUST be gitignored.

Git-tracked: index.ui, global_menu.ui, imgs/, other real .ui/.dspy files Gitignored: ALL module symlinks (created by build.sh), .DS_Store, *.bak, *.swp

Deploy Steps

# 1. Edit source files
vim ~/repos/sage/wwwroot/index.ui

# 2. Commit to sage repo
cd ~/repos/sage && git add wwwroot/ && git commit -m "feat: update shell" && git push

# 3. Deploy to production
cd ~/sage && git pull

# 4. Restart
cd ~/sage && ./stop.sh && ./start.sh

Shell Theme Files

  • shell_theme.css and shell_theme.js live directly in sage/wwwroot/ (NOT symlinked from dashboard_for_sage — that module is deprecated).
  • Auto-discovered by ahserver's cssfiles()/jsfiles() — NO header.tmpl modification needed.
  • NEVER modify bricks/dist/header.tmpl for Sage-specific themes.
  • Verify the file is actually served: curl -sI https://<host>/shell_theme.js must return 200. Broken symlinks produce 404 silently — the page still loads but sidebar toggle, theme switch, and menu reload all fail.

See references/deployment-workflow.md for full git strategy, build.sh details, and deployment pitfalls.

Menu & Navigation

global_menu.ui Structure

  • Items use comma-prefix formatting (not trailing commas)
  • Each item targets app.sage_main_content
  • Jinja2 conditions for role-based visibility (use with caution)

Role-Based Menu Pattern

{% set roles = get_user_roles(get_user()) %}
{% set role_str = roles|join(',') %}
{% set is_customer = 'customer.' in role_str %}
{% set is_customer_admin = 'customer.admin' in roles %}

Sidebar Toggle

sageToggleSidebar() in shell_theme.js calls both CSS class toggle AND bricks.getWidgetById('global_nav_menu').toggle_collapse(). Menu reload (sageReloadMenu()) must re-apply collapsed state.

CRITICAL: sageReloadMenu() must set sidebar.el.style.width for BOTH states. The original code only set width when collapsed (64px), leaving the expanded state (240px) unset. After destroying and rebuilding the menu (sidebar.el.innerHTML = ''), the CSS transition: width 0.3s ease on .sage-sidebar can be interrupted, leaving the inline width stuck at a narrow value. Menu text widgets use the bricks filler CSS class which has overflow: hidden — when the sidebar is narrower than expected, text labels get clipped to ~1 character.

Correct pattern after menu rebuild:

sidebar.el.style.width = isCollapsed ? '64px' : '240px';
updateSidebarIcon(isCollapsed);
if (isCollapsed) {
    var menu = bricks.getWidgetById('global_nav_menu', bricks.app);
    if (menu && menu.collapse) menu.collapse();
}

See references/shell-theme-js-deploy.md for the symlink gotcha.

Dynamic Menu Reload

Event: user_logined (NOT sage_login). Fires after login to refresh menu with role-based items.

See references/menu-patterns.md for role hierarchy detection, nested conditions, and customer role examples.

Theme System & CSS Overrides

CRITICAL: bricks.css Light Defaults Override Dark Theme

When the dark shell loads CRUD pages from modules, bricks.css default light styles apply. shell_theme.css must include [data-theme="dark"] overrides for ALL bricks.css component classes:

  • .tabular, .tabular-header-row, .tabular-row, .tabular-cell
  • .popup, .modal, .message, .titlebar
  • .inputbox, .auto-textarea
  • .htoolbar, .vtoolbar, .toolbar-button
  • .accordion-item, .card, .tabpanel
  • .llm_msg, .user_msg, pre

CRITICAL: Verify BOTH Light AND Dark Modes After Any CSS Change

Common failure: fix dark mode but break light mode (light text on light bg). Checklist:

  1. Switch to light → CRUD text must be dark
  2. Switch to dark → CRUD text must be light
  3. Check popups and forms in both

Quick Entry Button Styling

Buttons linking to other modules: bgcolor: "#1E293B", border: "1px solid #475569", color: "#FFFFFF", fontWeight: 600. Avoid bgcolor: "#334155".

Critical Pitfalls

  1. DO NOT implement during design phase — present wireframes first, wait for approval
  2. All designs must be bricks-compatible — no invented properties, no style nesting, correct actiontype
  3. shell_theme.css/js are now real files in sage/wwwroot/ — dashboard_for_sage module is deprecated. Edits and commits go to sage repo.
  4. Don't put shell files in dashboard_for_sage — shell lives at sage/wwwroot/
  5. Module symlinks are created by build.sh — NEVER add to git
  6. shell.ui no longer exists — index.ui IS the shell layout
  7. Static assets must exist in the repo — missing assets cause 401 → JS runtime errors cascade
  8. urlwidget containers should NOT have ids — id belongs in loaded UI file's root widget
  9. Only include Sage modules in global_menu.ui — external modules cause 401 errors. See references/module-membership.md
  10. Shell top bar must include language switcher and window dock — check historical top.ui for required components
  11. Menu navigation must trigger Router hook — menu_clicked() must call bricks.Router._onReplace() or F5 refresh won't restore page
  12. user_logined event fires AFTER Router._restore() — handler must check if Router already loaded a page before overriding with dashboard
  13. RBAC-aware navigation — menu conditions are for UX (hiding items), not security. Path-level RBAC remains primary access control.
  14. Documentation pages pattern: Copy markdown to wwwroot → MarkdownViewer UI → add RBAC permissions → menu entry points to .ui page
  15. Dashboard stats need real data sources — identify existing .dspy endpoints; don't propose hardcoded values
  16. Maintain backward compatibility — new shell wraps existing module pages, doesn't replace them
  17. Jinja2 comma management — conditional blocks in .ui files produce JSON. Each {% if %} block must independently start with , and the preceding widget must end with } (no comma). Never produce },,{ or },]. Mentally render every role combination before committing.
  18. Owner and Reseller are equivalent — always check both: 'owner.*' in roles or 'reseller.*' in roles
  19. ALL-users menu items go after {% endif %} — not inside either {% if is_customer %} or {% else %} branch. Items inside a branch are only visible to that role group.
  20. Menu URL trailing slash causes RBAC 403 — if a menu item URL ends with / (e.g. /dapi/downapp/), the registered permission path must also include the trailing slash variant. RBAC does exact match. See sage-rbac-auth skill for details.
  21. Bricks Image widget: CSS targets the <img> tag directly — an Image widget renders as a raw <img>, NOT a wrapper div. CSS like .logo img { height: 50px } silently fails because .logo IS the <img>. Use .logo { height: 50px } instead. Verify: document.querySelector('.logo').tagName returns "IMG". Symptom: images render at full native resolution (e.g. 2405×777 logo fills entire viewport). See sage-platform: references/multi-tenant-routing.md.
  22. .filler CSS class has overflow: hidden — root VBox with css:"filler" blocks scrolling when content exceeds viewport. Add "overflow":"auto" to VBox options. Inline style takes precedence over the class. Symptom: tall landing pages appear cut off; scrollHeight == clientHeight despite content being 3000+px.

21b. bricks urlwidget bind format: url goes inside options — buildUrlwidgetHandler copies desc.options via objcopy(desc.options||{}). A bare {"url": "..."} on the bind is IGNORED. Correct: {"actiontype":"urlwidget","target":"root.center","options":{"url":"{{entire_url('/path.ui')}}"}}. Wrong: {"actiontype":"urlwidget","target":"root.center","url":"..."}. See references/bricks-bind-patterns.md.\n\n21c. cross-widget binds need root.<id> or app.<id> — bricks get_by_id uses fromw.dom_element.querySelector('#'+id) — descendant-only search. A widget deep in a dropdown can't find a top-level sibling. Use target:"root.center" to start from root. Test with bricks.getWidgetById('root.center', bricks.app) in console.\n\n21d. Language switching: getlang() doesn't exist in bricks — bricks only has bricks.get_current_language() (reads navigator.language). For multi-language tenant pages, use localStorage.setItem('hermes-lang',lang) + location.href redirect. The entry index_carousel.ui should check localStorage on ready and redirect if non-default lang is set. See references/tenant-language-switching.md.\n\n22. Tenant CSS dual-loading: TWO files, both must match — bricks auto-loads BOTH tenant/tenant.css (global) AND <domain>/tenant.css (tenant-specific). The global file loads FIRST, the tenant-specific file SECOND. Both share selectors like .tenant-hero, .login-btn, .nav-link:hover. If you only update the domain-specific file, the global file's values may still leak through where !important isn't used. Always grep BOTH files and update them together. Verify with document.querySelectorAll('link[rel="stylesheet"]') in browser console to see which CSS files are actually loaded.

  1. height: 100% on absolute children needs explicit parent height, not min-height — when a parent uses position: absolute + min-height, and children use position: absolute; height: 100%, the children's height: 100% resolves against the parent's height property (which may be unspecified), NOT min-height. Children collapse to 0px. Fix: use height instead of min-height on the absolute-positioned parent. Symptom: card content exists in DOM but all getBoundingClientRect().height values are 0.

  2. Browser CSS cache survives across navigations — the cloud browser (Browserbase) caches CSS aggressively. After deploying CSS changes, the browser may keep serving stale cached copies. Verify the server file first with curl, then in the browser force-reload the stylesheet: var l=document.querySelector('link[href*="tenant.css"]'); l.href=l.href.split('?')[0]+'?t='+Date.now(). Do not re-navigate expecting fresh CSS — the stylesheet URL has not changed so the browser reuses its cache.

  3. bricks auto-sets data-theme="dark" — force light with MutationObserver — bricks framework detects OS prefers-color-scheme: dark and sets data-theme="dark" on <html> AFTER the ready event fires. An inline setAttribute in the same ready handler gets overwritten. Fix: inject a theme.js script that (a) sets data-theme="light" immediately, (b) uses MutationObserver on <html> attributes to block bricks from reverting it, and (c) checks ?theme=dark query param for opt-in dark mode. Inject via index_carousel.ui binds BEFORE carousel.js. See references/tenant-theme-forcing.md for full script, color palette, and pitfalls.

  4. bricks vcontainer/hcontainer overrides CSS dimensions — always use !important — bricks wraps every VBox/HBox in a <div class="vcontainer"> or <div class="hcontainer"> with flexbox layout. These classes set width:100% and/or display:flex which override CSS class-based rules. When building widgets that require position:absolute (carousel cards, flip inners) or fixed dimensions, use !important: position:absolute!important; width:280px!important; height:360px!important. For absolute children inside a bricks flex parent, also override the parent track to display:block!important. See references/bricks-carousel-flip.md.

  5. bricks strips custom data-* attrs; cssText+!important silently fails; urlwidget renders async — carousel.js triple pitfall. When building carousel/flip-card widgets: (a) data-pos set in JSON options is NOT rendered by bricks — assign via JS with c.setAttribute('data-pos', String(i-mid)). (b) element.style.cssText = 'position:absolute!important;...' silently fails — the browser rejects the entire string when !important is present. Use individual element.style.position = 'absolute' without !important (inline styles already have highest specificity). (c) urlwidget content loads AFTER the parent page's ready event, so querySelectorAll('.carousel-card') returns empty when carousel.js first runs. Wrap init in setTimeout(init, 500) retry: var cards=document.querySelectorAll('.carousel-card'); if(!cards.length){setTimeout(init,500);return;}. See references/bricks-carousel-flip.md.

  6. bricks widget development: adding new widget types — creating a new bricks widget (e.g. FlipCard, Carousel) requires extending JsWidget/Layout, implementing create(), registering via Factory.register(), adding to build.sh SOURCES, and adding CSS to bricks.css. NEVER call async widgetBuild in the synchronous constructor — breaks the page silently. See references/bricks-widget-development.md.

  7. Menu text clipped to 1 char after toggle: sidebar width stuck narrow — menu item text widgets use CSS class filler (overflow: hidden). If the sidebar inline width is stuck at ~64px (collapsed width) while the menu is expanded, text labels get clipped to ~1 character. Root cause: sageReloadMenu() not resetting width for expanded state, or CSS transition: width 0.3s ease being interrupted by innerHTML = '' during menu rebuild. Fix: always set sidebar.el.style.width = isCollapsed ? '64px' : '240px' after menu rebuild. Check shell_theme.js is actually being served (symlink may be broken — see references/shell-theme-js-deploy.md).

Shell Deployment Checklist (After Any UI Module Update)

  1. Verify shell entry point intact: sage/wwwroot/index.ui has sage_sidebar HBox
  2. Rebuild: cd ~/repos/sage && ./build.sh
  3. Verify global_menu.ui targets app.sage_main_content
  4. Verify theme files are auto-discovered (in wwwroot root, not blocked by RBAC)
  5. Restart Sage: ./stop.sh && ./start.sh
  6. Test: unauthenticated view, login flow, role-based items, logout flow

Common Failure Modes

  • Module index.ui overwrites shell index.ui during build.sh
  • Sidebar CSS class missing (shell_theme.css not loaded)
  • RBAC blocks shell_theme.js (must be any role in load_path.py)

References

  • references/design-methodology.md — Full phased design workflow and approval process

  • references/design-system.md — Color system, spacing, bricks widget mapping table

  • references/deployment-workflow.md — Full git strategy, build.sh, deployment pitfalls

  • references/menu-patterns.md — Role-based menu, Jinja2 conditions, customer roles

  • references/module-membership.md — Definitive list of Sage vs external modules

  • references/customer-role-menu.md — Customer role menu structure and feature mapping

  • references/sms-login-flow.md — SMS login architecture

  • references/shell-architecture.md — Shell layout structure and key widget IDs

  • references/dashboard-optimization.md — Role-based UI, stat card trends, chart consolidation, Jinja2 comma pitfalls

  • references/sage-module-inventory.md — Module inventory for design analysis

  • references/tenant-css-verification.md — Tenant CSS dual-loading, cache busting, height vs min-height pitfall

  • references/tenant-landing-theming.md — Dual-theme landing page design, aligning to reference SPA, card height fix, color palette

  • references/tenant-theme-forcing.md — MutationObserver pattern to force light theme, bricks dark auto-override, color palette

  • references/bricks-carousel-flip.md — Carousel 3D flip cards, hover-flip case cards, products dropdown — all as bricks CSS-only widgets

  • references/bricks-widget-development.md — How to add new widget types (FlipCard, Carousel) to the bricks framework; constructor pitfalls

  • references/product-landing-pages.md — Product sub-page template, floating chat icon, navigation URL pattern

  • references/bricks-bind-patterns.md — urlwidget options format, target resolution, PopupWindow, direct navigation\n- references/tenant-language-switching.md — Multi-language static pages via localStorage + redirect\n\n## Related Skills

  • bricks-framework — Dynamic menu reload, widget properties, actiontype values

  • module-development-spec — Module wwwroot structure and routing

  • build-script-modularization — Build scripts for bricks and other assets

  • sage-rbac-auth — Authentication flow, session management

  • sage-cache-sync — Cross-process cache invalidation patterns