From a6ba382241adea1feb21a6a7a32dfc1368af799b Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 4 Aug 2026 12:11:25 +0800 Subject: [PATCH] feat(tenant): multi-tenant domain routing endpoint + shell_theme dedup + dim_test path --- load_path.py | 2 + wwwroot/get_domain_info.dspy | 13 + wwwroot/shell_theme.css | 844 ++++++++++++++++++++++++++++++++++- wwwroot/shell_theme.js | 264 ++++++++++- 4 files changed, 1121 insertions(+), 2 deletions(-) create mode 100644 wwwroot/get_domain_info.dspy mode change 120000 => 100644 wwwroot/shell_theme.css mode change 120000 => 100644 wwwroot/shell_theme.js diff --git a/load_path.py b/load_path.py index 44f70da4..c9d9cb3c 100644 --- a/load_path.py +++ b/load_path.py @@ -10,6 +10,7 @@ paths="""/ any /i18n_getmsgs any /tenant any /tenant/index.ui any +/get_domain_info.dspy any /uapi logined /uapi/uapi logined /uapi/uapi/delete_uapi.dspy logined @@ -54,6 +55,7 @@ paths="""/ any /llmage/show_same_catelog_llm.ui logined /llmage/model_estimate.dspy logined /llmage/model_plaza.ui logined +/llmage/dim_test.ui logined /llmage/plaza_model_cards.ui logined /llmage/api/get_plaza_models.dspy logined /llmage/api/plaza_categories.dspy logined diff --git a/wwwroot/get_domain_info.dspy b/wwwroot/get_domain_info.dspy new file mode 100644 index 00000000..d948bfcd --- /dev/null +++ b/wwwroot/get_domain_info.dspy @@ -0,0 +1,13 @@ +# 多租户域名路由:解析当前请求域名对应的租户/分销商 +env = request._run_ns +host = request.headers.get('X-Forwarded-Host') or request.host +host = host.split(':')[0] +try: + rec = await env.get_domain_by_host(request, host) +except Exception as e: + return json.dumps({'success': False, 'domain': host, 'error': str(e)}, ensure_ascii=False) +if rec: + result = {'success': True, 'domain': host, 'resellerid': rec.resellerid, 'orgid': rec.orgid} +else: + result = {'success': False, 'domain': host, 'error': 'Domain not found'} +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/shell_theme.css b/wwwroot/shell_theme.css deleted file mode 120000 index fe14dae1..00000000 --- a/wwwroot/shell_theme.css +++ /dev/null @@ -1 +0,0 @@ -../../dashboard_for_sage/wwwroot/shell_theme.css \ No newline at end of file diff --git a/wwwroot/shell_theme.css b/wwwroot/shell_theme.css new file mode 100644 index 00000000..8028d26e --- /dev/null +++ b/wwwroot/shell_theme.css @@ -0,0 +1,843 @@ +/* Sage Modern UI Theme System + Dark/Light theme switching via CSS custom properties + Applied to element data-theme attribute +*/ + +/* ===== Dark Theme (default) ===== */ +:root, +[data-theme="dark"] { + --sage-bg-primary: #0B1120; + --sage-bg-secondary: #111827; + --sage-bg-card: #1E293B; + --sage-bg-hover: #334155; + --sage-bg-input: #0F172A; + --sage-bg-toolbar: #111827; + + --sage-border-primary: #334155; + --sage-border-weak: #1E293B; + --sage-border-focus: #3B82F6; + + --sage-text-primary: #F1F5F9; + --sage-text-secondary: #94A3B8; + --sage-text-muted: #64748B; + --sage-text-inverse: #0F172A; + + --sage-brand: #3B82F6; + --sage-brand-hover: #2563EB; + --sage-success: #22C55E; + --sage-warning: #F59E0B; + --sage-danger: #EF4444; + --sage-info: #06B6D4; + + --sage-sidebar-width: 240px; + --sage-sidebar-collapsed-width: 64px; + --sage-topbar-height: 56px; + --sage-radius: 12px; + --sage-radius-sm: 8px; + + --sage-shadow-card: 0 1px 3px rgba(0,0,0,0.3); + --sage-shadow-lg: 0 4px 12px rgba(0,0,0,0.4); +} + +/* ===== Light Theme ===== */ +[data-theme="light"] { + --sage-bg-primary: #F8FAFC; + --sage-bg-secondary: #FFFFFF; + --sage-bg-card: #FFFFFF; + --sage-bg-hover: #F1F5F9; + --sage-bg-input: #F8FAFC; + --sage-bg-toolbar: #FFFFFF; + + --sage-border-primary: #E2E8F0; + --sage-border-weak: #F1F5F9; + --sage-border-focus: #3B82F6; + + --sage-text-primary: #0F172A; + --sage-text-secondary: #475569; + --sage-text-muted: #94A3B8; + --sage-text-inverse: #FFFFFF; + + --sage-brand: #3B82F6; + --sage-brand-hover: #2563EB; + --sage-success: #16A34A; + --sage-warning: #D97706; + --sage-danger: #DC2626; + --sage-info: #0891B2; + + --sage-shadow-card: 0 1px 3px rgba(0,0,0,0.08); + --sage-shadow-lg: 0 4px 12px rgba(0,0,0,0.1); +} + +/* ===== Global Overrides ===== */ +body { + margin: 0; + padding: 0; + background-color: var(--sage-bg-primary); + color: var(--sage-text-primary); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + transition: background-color 0.2s ease, color 0.2s ease; +} + +/* Ensure theme variables override bricks.css hardcoded values */ +[data-theme="dark"] body { + background-color: var(--sage-bg-primary); + color: var(--sage-text-primary); +} + +[data-theme="light"] body { + background-color: var(--sage-bg-primary); + color: var(--sage-text-primary); +} + +/* ===== Shell Layout ===== */ +.sage-shell { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: var(--sage-bg-primary); + color: var(--sage-text-primary); + transition: background-color 0.2s ease, color 0.2s ease; +} + +.sage-topbar { + height: var(--sage-topbar-height); + background-color: var(--sage-bg-secondary); + border-bottom: 1px solid var(--sage-border-primary); + display: flex; + align-items: center; + padding: 0 16px; + gap: 12px; + flex-shrink: 0; + z-index: 100; +} + +.sage-body { + display: flex; + flex: 1; + overflow: hidden; +} + +.sage-sidebar { + width: var(--sage-sidebar-width); + background-color: var(--sage-bg-secondary); + border-right: 1px solid var(--sage-border-primary); + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + flex-shrink: 0; + transition: width 0.3s ease, transform 0.3s ease; + position: relative; + z-index: 50; +} + +.sage-sidebar.collapsed { + width: var(--sage-sidebar-collapsed-width); +} + +.sage-sidebar.collapsed .sidebar-text { + opacity: 0; + width: 0; + overflow: hidden; +} + +/* Menu collapsed state - hide text labels, center icons */ +.menu-collapsed .filler { + display: none !important; +} +.menu-collapsed .menuitem { + justify-content: center; + padding: 8px 0; +} +.menu-collapsed .menuitem:hover { + background-color: var(--sage-bg-hover); +} + +/* Mobile: sidebar as overlay */ +@media (max-width: 768px) { + .sage-sidebar { + position: fixed; + left: 0; + top: var(--sage-topbar-height); + height: calc(100vh - var(--sage-topbar-height)); + transform: translateX(0); + box-shadow: var(--sage-shadow-lg); + } + + .sage-sidebar.collapsed { + transform: translateX(-100%); + width: var(--sage-sidebar-width); /* Keep full width when hidden on mobile */ + } + + .sage-main { + width: 100%; + } +} + +.sage-main { + flex: 1; + overflow: hidden; + overflow-x: hidden; + padding: 24px; + background-color: var(--sage-bg-primary); + min-width: 0; /* Prevent flex item from overflowing */ + min-height: 0; /* Allow flex children to shrink below content size */ + transition: margin-left 0.3s ease; +} + +/* Responsive padding for mobile */ +@media (max-width: 768px) { + .sage-main { + padding: 16px; + } +} + +@media (max-width: 480px) { + .sage-main { + padding: 12px; + } +} + +/* ===== Stat Cards ===== */ +.stat-card { + background-color: var(--sage-bg-card); + border-radius: var(--sage-radius); + padding: 20px; + border: 1px solid var(--sage-border-primary); + box-shadow: var(--sage-shadow-card); + transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.2s ease; + cursor: pointer; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--sage-shadow-lg); +} + +.stat-card .stat-icon { + width: 40px; + height: 40px; + border-radius: var(--sage-radius-sm); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; +} + +.stat-card .stat-value { + font-size: 28px; + font-weight: 700; + color: var(--sage-text-primary); + line-height: 1.2; +} + +.stat-card .stat-label { + font-size: 14px; + color: var(--sage-text-secondary); + margin-top: 4px; +} + +.stat-card .stat-trend { + font-size: 13px; + margin-top: 8px; +} + +.stat-card .stat-trend.up { + color: var(--sage-success); +} + +.stat-card .stat-trend.down { + color: var(--sage-danger); +} + +/* ===== Quick Links ===== */ +.quick-link { + background-color: var(--sage-bg-card); + border-radius: var(--sage-radius); + padding: 20px; + border: 1px solid var(--sage-border-primary); + box-shadow: var(--sage-shadow-card); + transition: transform 0.15s ease, box-shadow 0.15s ease, background-color 0.2s ease; + cursor: pointer; + text-align: center; +} + +.quick-link:hover { + transform: translateY(-2px); + box-shadow: var(--sage-shadow-lg); + border-color: var(--sage-brand); +} + +/* ===== Section Headers ===== */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.section-title { + font-size: 18px; + font-weight: 600; + color: var(--sage-text-primary); +} + +/* ===== Buttons ===== */ +.sage-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--sage-radius-sm); + border: none; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.15s ease; +} + +.sage-btn-primary { + background-color: var(--sage-brand); + color: #FFFFFF; +} + +.sage-btn-primary:hover { + background-color: var(--sage-brand-hover); +} + +.sage-btn-ghost { + background-color: transparent; + color: var(--sage-text-secondary); + border: 1px solid var(--sage-border-primary); +} + +.sage-btn-ghost:hover { + background-color: var(--sage-bg-hover); + color: var(--sage-text-primary); +} + +/* ===== Theme Toggle Button ===== */ +.theme-toggle { + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid var(--sage-border-primary); + background-color: transparent; + color: var(--sage-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.theme-toggle:hover { + background-color: var(--sage-bg-hover); + color: var(--sage-text-primary); +} + +/* ===== Sidebar Toggle ===== */ +.sidebar-toggle { + width: 36px; + height: 36px; + border-radius: var(--sage-radius-sm); + border: 1px solid var(--sage-border-primary); + background-color: transparent; + color: var(--sage-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s ease; +} + +.sidebar-toggle:hover { + background-color: var(--sage-bg-hover); + color: var(--sage-text-primary); +} + +/* ===== Breadcrumb ===== */ +.breadcrumb { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--sage-text-secondary); +} + +.breadcrumb a { + color: var(--sage-text-secondary); + text-decoration: none; +} + +.breadcrumb a:hover { + color: var(--sage-brand); +} + +.breadcrumb .separator { + color: var(--sage-text-muted); +} + +.breadcrumb .current { + color: var(--sage-text-primary); + font-weight: 500; +} + +/* ===== DataViewer Overrides for theme ===== */ +.tabular-row { + cursor: pointer; +} + +[data-theme="dark"] .dataviewer-toolbar, +[data-theme="dark"] .tabular, +[data-theme="dark"] .data-row, +[data-theme="dark"] body { + background-color: var(--sage-bg-primary); + color: var(--sage-text-primary); +} + +[data-theme="dark"] .tabular-header-row { + background-color: var(--sage-bg-secondary); + color: var(--sage-text-secondary); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .tabular-row { + color: var(--sage-text-primary); +} + +[data-theme="dark"] .tabular-row:nth-child(odd) { + background-color: var(--sage-bg-card); +} + +[data-theme="dark"] .tabular-row:nth-child(even) { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] .tabular-row-selected { + background-color: var(--sage-bg-hover) !important; +} +[data-theme="dark"] .tabular-row-selected, +[data-theme="dark"] .tabular-row-selected .tabular-cell { + color: var(--sage-brand) !important; +} + +/* Light theme selected row */ +[data-theme="light"] .tabular-row-selected { + background-color: #E0F2FE !important; +} +[data-theme="light"] .tabular-row-selected, +[data-theme="light"] .tabular-row-selected .tabular-cell { + color: #0369A1 !important; +} + +[data-theme="dark"] .tabular-cell { + border-color: var(--sage-border-weak); + color: var(--sage-text-primary); +} + +[data-theme="dark"] .popup, +[data-theme="dark"] .modal, +[data-theme="dark"] .message { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .titlebar { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] .inputbox { + background-color: var(--sage-bg-input); + color: var(--sage-text-primary); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .htoolbar, +[data-theme="dark"] .vtoolbar { + background-color: var(--sage-bg-toolbar); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .accordion-item { + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .accordion-item:nth-child(odd) { + background-color: var(--sage-bg-card); +} + +[data-theme="dark"] .accordion-item:nth-child(even) { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] .accordion-item-header { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] .accordion-item-info { + background-color: var(--sage-bg-card); +} + +[data-theme="dark"] .accordion-item-selected { + background-color: var(--sage-bg-hover); +} + +[data-theme="dark"] .accordion-item-info-selected { + background-color: var(--sage-bg-hover); +} + +[data-theme="dark"] .card { + background-color: var(--sage-bg-card); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .subcard { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] .curpos { + background-color: var(--sage-bg-hover); +} + +[data-theme="dark"] .selected { + background-color: var(--sage-bg-hover); +} + +[data-theme="dark"] .tabpanel { + background-color: var(--sage-bg-card); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .tabpanel-content { + background-color: var(--sage-bg-secondary); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .toolbar-button { + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .toolbar-button-active { + background-color: var(--sage-bg-hover); +} + +[data-theme="dark"] .auto-textarea { + background-color: var(--sage-bg-input); + color: var(--sage-text-primary); + border-color: var(--sage-border-primary); +} + +[data-theme="dark"] .droparea { + border-color: var(--sage-border-primary); + color: var(--sage-text-secondary); +} + +[data-theme="dark"] .droparea:hover { + border-color: var(--sage-brand); + color: var(--sage-brand); + background: var(--sage-bg-hover); +} + +[data-theme="dark"] .thinking-content { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] .resp-error { + background-color: rgba(239, 68, 68, 0.15); + color: var(--sage-danger); +} + +[data-theme="dark"] .resp-content { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); +} + +[data-theme="dark"] .llm_title { + background-color: var(--sage-bg-hover); +} + +[data-theme="dark"] .progress-container { + background-color: var(--sage-bg-secondary); +} + +[data-theme="dark"] pre { + background-color: var(--sage-bg-secondary); + color: var(--sage-text-primary); +} + +[data-theme="dark"] .llm_msg { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5); +} + +[data-theme="dark"] .user_msg { + background-color: var(--sage-bg-hover); + color: var(--sage-text-primary); +} + +[data-theme="light"] .dataviewer-toolbar, +[data-theme="light"] .tabular, +[data-theme="light"] .data-row, +[data-theme="light"] body { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); +} + +[data-theme="light"] .tabular-header-row { + background-color: var(--sage-bg-hover); + color: var(--sage-text-secondary); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .tabular-row { + color: var(--sage-text-primary); +} + +[data-theme="light"] .tabular-row:nth-child(odd) { + background-color: #f9fafb; +} + +[data-theme="light"] .tabular-row:nth-child(even) { + background-color: var(--sage-bg-card); +} + +[data-theme="light"] .tabular-row-selected { + color: var(--sage-brand-hover); +} + +[data-theme="light"] .tabular-cell { + border-color: var(--sage-border-weak); + color: var(--sage-text-primary); +} + +[data-theme="light"] .popup, +[data-theme="light"] .modal, +[data-theme="light"] .message { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .titlebar { + background-color: var(--sage-bg-secondary); +} + +[data-theme="light"] .inputbox { + background-color: var(--sage-bg-input); + color: var(--sage-text-primary); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .htoolbar, +[data-theme="light"] .vtoolbar { + background-color: var(--sage-bg-toolbar); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .accordion-item { + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .accordion-item:nth-child(odd) { + background-color: var(--sage-bg-card); +} + +[data-theme="light"] .accordion-item:nth-child(even) { + background-color: #f9fafb; +} + +[data-theme="light"] .accordion-item-header { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .accordion-item-info { + background-color: var(--sage-bg-card); +} + +[data-theme="light"] .accordion-item-selected { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .accordion-item-info-selected { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .card { + background-color: var(--sage-bg-card); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .subcard { + background-color: #f9fafb; +} + +[data-theme="light"] .curpos { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .selected { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .tabpanel { + background-color: var(--sage-bg-card); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .tabpanel-content { + background-color: var(--sage-bg-secondary); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .toolbar-button { + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .toolbar-button-active { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .auto-textarea { + background-color: var(--sage-bg-input); + color: var(--sage-text-primary); + border-color: var(--sage-border-primary); +} + +[data-theme="light"] .droparea { + border-color: var(--sage-border-primary); + color: var(--sage-text-secondary); +} + +[data-theme="light"] .droparea:hover { + border-color: var(--sage-brand); + color: var(--sage-brand); + background: var(--sage-bg-hover); +} + +[data-theme="light"] .thinking-content { + background-color: var(--sage-bg-secondary); +} + +[data-theme="light"] .resp-error { + background-color: rgba(239, 68, 68, 0.1); + color: var(--sage-danger); +} + +[data-theme="light"] .resp-content { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); +} + +[data-theme="light"] .llm_title { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] .progress-container { + background-color: var(--sage-bg-hover); +} + +[data-theme="light"] pre { + background-color: var(--sage-bg-secondary); + color: var(--sage-text-primary); +} + +[data-theme="light"] .llm_msg { + background-color: var(--sage-bg-card); + color: var(--sage-text-primary); +} + +[data-theme="light"] .user_msg { + background-color: var(--sage-bg-hover); + color: var(--sage-text-primary); +} + +[data-theme="light"] .tabular { background-color: #f5f5f5; color: #1E293B; } +[data-theme="light"] .tabular-header-row { background-color: #E2E8F0; color: #0F172A; font-weight: 600; } +[data-theme="light"] .tabular-header-row .tabular-cell { color: #0F172A; } +[data-theme="light"] .tabular-row { color: #1E293B; } +[data-theme="light"] .tabular-row:nth-child(odd) { background-color: #FFFFFF; } +[data-theme="light"] .tabular-row:nth-child(even) { background-color: #F8FAFC; } +[data-theme="light"] .tabular-cell { border-color: #E2E8F0; color: #1E293B; } +[data-theme="light"] .card { color: #1E293B; } +[data-theme="light"] .inputbox { color: #1E293B; background-color: #FFFFFF; } +[data-theme="light"] .popup, [data-theme="light"] .modal { color: #1E293B; } +[data-theme="light"] .accordion-item-info { color: #475569; } +[data-theme="light"] .toolbar-button { color: #334155; } +[data-theme="light"] .message { color: #1E293B; } + +/* ===== Menu Overrides ===== */ +[data-theme="dark"] .menu-item { + color: var(--sage-text-secondary); +} + +[data-theme="dark"] .menu-item:hover, +[data-theme="dark"] .menu-item.active { + background-color: var(--sage-bg-hover); + color: var(--sage-text-primary); +} + +[data-theme="light"] .menu-item { + color: var(--sage-text-secondary); +} + +[data-theme="light"] .menu-item:hover, +[data-theme="light"] .menu-item.active { + background-color: var(--sage-bg-hover); + color: var(--sage-brand); +} + +/* ===== Utility Classes ===== */ +.sage-brand-title { + color: var(--sage-text-primary); + font-weight: bold; +} + +.sage-text-secondary { + color: var(--sage-text-secondary); +} + +/* ===== Responsive - Topbar ===== */ +@media (max-width: 768px) { + .sage-topbar { + padding: 0 8px; + gap: 6px; + } + .sage-brand-title { + font-size: 16px; + } +} + +@media (max-width: 480px) { + .sage-brand-title { + display: none; + } +} + +/* ===== Responsive - Stat Cards ===== */ +@media (max-width: 768px) { + .stat-card { + padding: 14px; + } + .stat-card .stat-value { + font-size: 22px; + } + .stat-card .stat-label { + font-size: 12px; + } + .stat-card .stat-icon { + width: 32px; + height: 32px; + margin-bottom: 8px; + } + .quick-link { + padding: 14px; + } + .section-title { + font-size: 16px; + } +} diff --git a/wwwroot/shell_theme.js b/wwwroot/shell_theme.js deleted file mode 120000 index af0c87ec..00000000 --- a/wwwroot/shell_theme.js +++ /dev/null @@ -1 +0,0 @@ -../../dashboard_for_sage/wwwroot/shell_theme.js \ No newline at end of file diff --git a/wwwroot/shell_theme.js b/wwwroot/shell_theme.js new file mode 100644 index 00000000..367a7227 --- /dev/null +++ b/wwwroot/shell_theme.js @@ -0,0 +1,263 @@ +/* Sage Modern UI Shell Theme & Layout Controller + Handles: theme switching (dark/light), sidebar collapse, localStorage persistence +*/ +(function() { + 'use strict'; + + var THEME_KEY = 'sage_ui_theme'; + var SIDEBAR_KEY = 'sage_sidebar_collapsed'; + + // Initialize theme on page load + function initTheme() { + var saved = null; + try { saved = localStorage.getItem(THEME_KEY); } catch(e) {} + var theme = saved || 'dark'; + document.documentElement.setAttribute('data-theme', theme); + updateThemeIcon(theme); + } + + // Toggle between dark and light + function toggleTheme() { + var current = document.documentElement.getAttribute('data-theme') || 'dark'; + var next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + try { localStorage.setItem(THEME_KEY, next); } catch(e) {} + updateThemeIcon(next); + } + + // Update the theme toggle icon based on current theme + function updateThemeIcon(theme) { + var btn = document.getElementById('theme_toggle_btn'); + if (!btn) return; + if (theme === 'light') { + // Show moon icon (switch to dark) + btn.innerHTML = ''; + } else { + // Show sun icon (switch to light) + btn.innerHTML = ''; + } + } + + // Initialize sidebar state + function initSidebar() { + var collapsed = false; + try { collapsed = localStorage.getItem(SIDEBAR_KEY) === 'true'; } catch(e) {} + + // Auto-collapse on mobile + if (isMobile()) { + collapsed = true; + } + + var sidebar = document.getElementById('sage_sidebar'); + if (sidebar && collapsed) { + sidebar.classList.add('collapsed'); + sidebar.style.width = '64px'; + } + } + + // Handle window resize - auto-collapse on mobile + function handleResize() { + var sidebar = document.getElementById('sage_sidebar'); + if (!sidebar) return; + + if (isMobile() && !sidebar.classList.contains('collapsed')) { + sidebar.classList.add('collapsed'); + sidebar.style.width = '64px'; + try { localStorage.setItem(SIDEBAR_KEY, 'true'); } catch(e) {} + updateSidebarIcon(true); + } + } + + // Check if we're on mobile viewport + function isMobile() { + return window.innerWidth <= 768; + } + + // Close sidebar when clicking outside on mobile + function setupMobileOverlay() { + document.addEventListener('click', function(e) { + if (!isMobile()) return; + var sidebar = document.getElementById('sage_sidebar'); + var toggleBtn = document.getElementById('sidebar_toggle_btn'); + if (!sidebar || sidebar.classList.contains('collapsed')) return; + // If click is outside sidebar and toggle button, close sidebar + if (!sidebar.contains(e.target) && (!toggleBtn || !toggleBtn.contains(e.target))) { + sidebar.classList.add('collapsed'); + sidebar.style.width = '64px'; + try { localStorage.setItem(SIDEBAR_KEY, 'true'); } catch(ex) {} + updateSidebarIcon(true); + } + }); + } + + function updateSidebarIcon(isCollapsed) { + var btn = document.getElementById('sidebar_toggle_btn'); + if (!btn) return; + if (isCollapsed) { + btn.innerHTML = ''; + } else { + btn.innerHTML = ''; + } + } + + // Toggle sidebar collapse + function toggleSidebar() { + var sidebar = document.getElementById('sage_sidebar'); + if (!sidebar) return; + sidebar.classList.toggle('collapsed'); + var isCollapsed = sidebar.classList.contains('collapsed'); + try { localStorage.setItem(SIDEBAR_KEY, isCollapsed); } catch(e) {} + updateSidebarIcon(isCollapsed); + // Override inline width style (bricks sets inline style which beats CSS class) + sidebar.style.width = isCollapsed ? '64px' : '240px'; + // Toggle Menu widget collapse state + if (typeof bricks !== 'undefined') { + var menu = bricks.getWidgetById('global_nav_menu', bricks.app); + if (menu && menu.toggle_collapse) { + menu.toggle_collapse(); + } + } + } + + // Initialize SPA Router + function initRouter() { + if (typeof bricks === 'undefined' || !bricks.Router) { + console.log('[Shell] Router not available'); + return; + } + bricks.Router.init({ + targets: [ + { id: 'sage_main_content', param: 'page' } + ] + }); + } + + // Called by user_logined event — skip dashboard if Router has a saved route + window.sageOnLogin = async function(dashboardUrl) { + var target = bricks.getWidgetById('sage_main_content', bricks.app); + if (!target) return; + + var hasRoute = false; + if (bricks.Router && bricks.Router._enabled) { + hasRoute = bricks.Router.current('sage_main_content') + || new URLSearchParams(window.location.search).has('page'); + } + + if (hasRoute) { + console.log('[Shell] Router has route, skip dashboard load'); + return; + } + + // No route — load dashboard via Router + if (dashboardUrl && bricks.Router && bricks.Router._enabled) { + bricks.Router.navigate('sage_main_content', dashboardUrl); + } else if (dashboardUrl) { + var desc = { widgettype: 'urlwidget', options: { url: dashboardUrl } }; + var w = await bricks.widgetBuild(desc, bricks.app); + if (w) { + target.clear_widgets(); + target.add_widget(w); + } + } + }; + + // Reload global menu after login/logout + window.sageReloadMenu = async function() { + if (typeof bricks === 'undefined') return; + var sidebar = bricks.getWidgetById('sage_sidebar', bricks.app); + if (!sidebar) { + console.log('[Shell] sage_sidebar not found'); + return; + } + // Check if sidebar is currently collapsed before clearing + var isCollapsed = sidebar.el && sidebar.el.classList.contains('collapsed'); + // Clear existing children + sidebar.subwidgets.forEach(function(w) { w.destroy && w.destroy(); }); + sidebar.subwidgets = []; + sidebar.el.innerHTML = ''; + // Rebuild menu urlwidget + var menuUrl = bricks.app.baseUrl + '/global_menu.ui?_webbricks_=1'; + var desc = { + "widgettype": "urlwidget", + "options": { "url": menuUrl } + }; + try { + var w = await bricks.widgetBuild(desc, sidebar); + if (w) { + sidebar.addSubWidget(w); + // Re-apply sidebar width and collapsed state to newly built menu + 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(); + } + } + console.log('[Shell] Menu reloaded'); + } + } catch(e) { + console.log('[Shell] Menu reload error:', e); + } + }; + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + initTheme(); + initSidebar(); + initRouter(); + setupMobileOverlay(); + window.addEventListener('resize', handleResize); + }); + } else { + initTheme(); + initSidebar(); + initRouter(); + setupMobileOverlay(); + window.addEventListener('resize', handleResize); + } + + // Bricks widgets render asynchronously after DOMContentLoaded. + // Retry updateThemeIcon until the button element exists. + (function retryThemeIcon() { + var btn = document.getElementById('theme_toggle_btn'); + if (btn) { + var theme = document.documentElement.getAttribute('data-theme') || 'dark'; + updateThemeIcon(theme); + } else { + setTimeout(retryThemeIcon, 200); + } + })(); + + // Retry sidebar icon init until the button element exists + (function retrySidebarIcon() { + var sidebar = document.getElementById('sage_sidebar'); + var btn = document.getElementById('sidebar_toggle_btn'); + if (btn && sidebar) { + var isCollapsed = sidebar.classList.contains('collapsed'); + updateSidebarIcon(isCollapsed); + // Sync inline width with collapsed state (bricks may have overridden it) + sidebar.style.width = isCollapsed ? '64px' : '240px'; + // Apply collapsed state to Menu if sidebar was already collapsed + if (isCollapsed && typeof bricks !== 'undefined') { + var menu = bricks.getWidgetById('global_nav_menu', bricks.app); + if (menu && menu.collapse) { + menu.collapse(); + } else { + setTimeout(retrySidebarIcon, 200); + return; + } + } + } else { + setTimeout(retrySidebarIcon, 200); + } + })(); + + // Expose global functions for bricks bind access + window.sageToggleTheme = toggleTheme; + window.sageToggleSidebar = toggleSidebar; + window.sageInitTheme = initTheme; + window.sageInitSidebar = initSidebar; + +})();