20 KiB
| 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, 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
- Map all modules: scan
pkgs/*/wwwroot/*.ui,pkgs/*/json/*.json - Identify user roles from RBAC module, role-permission mappings, menu templates
- Classify pages: Dashboard, List/CRUD, Form/Edit, Detail, Settings
- 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):
- Design Philosophy + core principles + color system
- Global Layout (ASCII wireframe)
- Key Interface Mockups (dashboard, list, form, detail)
- Bricks Widget Mapping Table
- File Organization Plan
- Implementation Phases
- 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.cssandshell_theme.jslive directly insage/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.tmplfor Sage-specific themes. - Verify the file is actually served:
curl -sI https://<host>/shell_theme.jsmust 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:
- Switch to light → CRUD text must be dark
- Switch to dark → CRUD text must be light
- 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
- DO NOT implement during design phase — present wireframes first, wait for approval
- All designs must be bricks-compatible — no invented properties, no
stylenesting, correctactiontype - shell_theme.css/js are now real files in
sage/wwwroot/— dashboard_for_sage module is deprecated. Edits and commits go to sage repo. - Don't put shell files in dashboard_for_sage — shell lives at
sage/wwwroot/ - Module symlinks are created by build.sh — NEVER add to git
- shell.ui no longer exists —
index.uiIS the shell layout - Static assets must exist in the repo — missing assets cause 401 → JS runtime errors cascade
- urlwidget containers should NOT have ids — id belongs in loaded UI file's root widget
- Only include Sage modules in global_menu.ui — external modules cause 401 errors. See
references/module-membership.md - Shell top bar must include language switcher and window dock — check historical
top.uifor required components - Menu navigation must trigger Router hook —
menu_clicked()must callbricks.Router._onReplace()or F5 refresh won't restore page - user_logined event fires AFTER Router._restore() — handler must check if Router already loaded a page before overriding with dashboard
- RBAC-aware navigation — menu conditions are for UX (hiding items), not security. Path-level RBAC remains primary access control.
- Documentation pages pattern: Copy markdown to wwwroot → MarkdownViewer UI → add RBAC permissions → menu entry points to .ui page
- Dashboard stats need real data sources — identify existing .dspy endpoints; don't propose hardcoded values
- Maintain backward compatibility — new shell wraps existing module pages, doesn't replace them
- 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. - Owner and Reseller are equivalent — always check both:
'owner.*' in roles or 'reseller.*' in roles - 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. - 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. Seesage-rbac-authskill for details. - Bricks Image widget: CSS targets the
<img>tag directly — anImagewidget renders as a raw<img>, NOT a wrapper div. CSS like.logo img { height: 50px }silently fails because.logoIS the<img>. Use.logo { height: 50px }instead. Verify:document.querySelector('.logo').tagNamereturns"IMG". Symptom: images render at full native resolution (e.g. 2405×777 logo fills entire viewport). Seesage-platform: references/multi-tenant-routing.md. .fillerCSS class hasoverflow: hidden— root VBox withcss:"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 == clientHeightdespite 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.
-
height: 100%on absolute children needs explicit parentheight, notmin-height— when a parent usesposition: absolute+min-height, and children useposition: absolute; height: 100%, the children'sheight: 100%resolves against the parent'sheightproperty (which may be unspecified), NOTmin-height. Children collapse to 0px. Fix: useheightinstead ofmin-heighton the absolute-positioned parent. Symptom: card content exists in DOM but allgetBoundingClientRect().heightvalues are 0. -
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. -
bricks auto-sets
data-theme="dark"— force light with MutationObserver — bricks framework detects OSprefers-color-scheme: darkand setsdata-theme="dark"on<html>AFTER the ready event fires. An inlinesetAttributein the same ready handler gets overwritten. Fix: inject atheme.jsscript that (a) setsdata-theme="light"immediately, (b) usesMutationObserveron<html>attributes to block bricks from reverting it, and (c) checks?theme=darkquery param for opt-in dark mode. Inject viaindex_carousel.uibinds BEFORE carousel.js. Seereferences/tenant-theme-forcing.mdfor full script, color palette, and pitfalls. -
bricks
vcontainer/hcontaineroverrides CSS dimensions — always use!important— bricks wraps every VBox/HBox in a<div class="vcontainer">or<div class="hcontainer">with flexbox layout. These classes setwidth:100%and/ordisplay:flexwhich override CSS class-based rules. When building widgets that requireposition: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 todisplay:block!important. Seereferences/bricks-carousel-flip.md. -
bricks strips custom
data-*attrs;cssText+!importantsilently fails; urlwidget renders async — carousel.js triple pitfall. When building carousel/flip-card widgets: (a)data-posset in JSONoptionsis NOT rendered by bricks — assign via JS withc.setAttribute('data-pos', String(i-mid)). (b)element.style.cssText = 'position:absolute!important;...'silently fails — the browser rejects the entire string when!importantis present. Use individualelement.style.position = 'absolute'without!important(inline styles already have highest specificity). (c)urlwidgetcontent loads AFTER the parent page'sreadyevent, soquerySelectorAll('.carousel-card')returns empty when carousel.js first runs. Wrap init insetTimeout(init, 500)retry:var cards=document.querySelectorAll('.carousel-card'); if(!cards.length){setTimeout(init,500);return;}. Seereferences/bricks-carousel-flip.md. -
bricks widget development: adding new widget types — creating a new bricks widget (e.g. FlipCard, Carousel) requires extending
JsWidget/Layout, implementingcreate(), registering viaFactory.register(), adding tobuild.shSOURCES, and adding CSS tobricks.css. NEVER call asyncwidgetBuildin the synchronous constructor — breaks the page silently. Seereferences/bricks-widget-development.md. -
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 CSStransition: width 0.3s easebeing interrupted byinnerHTML = ''during menu rebuild. Fix: always setsidebar.el.style.width = isCollapsed ? '64px' : '240px'after menu rebuild. Checkshell_theme.jsis actually being served (symlink may be broken — seereferences/shell-theme-js-deploy.md).
Shell Deployment Checklist (After Any UI Module Update)
- Verify shell entry point intact:
sage/wwwroot/index.uihassage_sidebarHBox - Rebuild:
cd ~/repos/sage && ./build.sh - Verify global_menu.ui targets
app.sage_main_content - Verify theme files are auto-discovered (in wwwroot root, not blocked by RBAC)
- Restart Sage:
./stop.sh && ./start.sh - 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
anyrole 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