Skip to content

Commit 87d1e37

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(showcase): SDUI big-screen "Command Center" page + bump vendored console (#2242)
A full-bleed, theme-following "交付运营数据大屏 / Delivery Operations Command Center" page demonstrating that SDUI pages can compose a data-screen / 大屏: KPI hero strip, gradient bar/area charts, a container-scaled donut, and a live work-queue grid — theme-adaptive (light dashboard <-> dark 大屏), with a cohesive chart palette via a per-page --chart-1..5 override. Wired into nav: Analytics -> Command Center (大屏). Bumps .objectui-sha -> c9a3fc8db (objectstack-ai/objectui#1922) for the three renderer capabilities the page uses: full-bleed width='full' pages, container-scaled donuts, and gradient bar/area fills. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5ba52b0 commit 87d1e37

5 files changed

Lines changed: 185 additions & 3 deletions

File tree

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
aa50dc07f55ba8328f5858695678b9f4c67c81f4
1+
c9a3fc8dba928408612ef763d5aa833857fa4bbd

examples/app-showcase/objectstack.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js';
2222
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js';
2323
import { allReports } from './src/reports/index.js';
2424
import { allActions } from './src/actions/index.js';
25-
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage } from './src/pages/index.js';
25+
import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage } from './src/pages/index.js';
2626
import { allFlows } from './src/flows/index.js';
2727
import { allWebhooks } from './src/webhooks/index.js';
2828
import { allHooks } from './src/hooks/index.js';
@@ -156,7 +156,7 @@ export default defineStack({
156156
apps: [ShowcaseApp],
157157
portals: allPortals,
158158
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
159-
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage],
159+
pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage],
160160
dashboards: [ChartGalleryDashboard, OpsDashboard],
161161
books: allBooks,
162162
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],

examples/app-showcase/src/apps/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const ShowcaseApp = App.create({
4949
label: 'Analytics',
5050
icon: 'chart-bar',
5151
children: [
52+
{ id: 'nav_command_center', type: 'page', pageName: 'showcase_command_center', label: 'Command Center (大屏)', icon: 'monitor-dot' },
5253
{ id: 'nav_ops', type: 'dashboard', dashboardName: 'showcase_ops_dashboard', label: 'Delivery Operations', icon: 'gauge' },
5354
{ id: 'nav_charts', type: 'dashboard', dashboardName: 'showcase_chart_gallery', label: 'Chart Gallery', icon: 'layout-dashboard' },
5455
{ id: 'nav_report_tabular', type: 'object', objectName: 'showcase_task', viewName: 'tabular', label: 'Task List', icon: 'table' },
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Operations Command Center (大屏) — a dense, full-bleed "big screen" built as a
7+
* pure SDUI micro-page that FOLLOWS the console theme (light ↔ dark).
8+
*
9+
* Density: object-chart's ChartContainer is `h-[350px]` by default, but it
10+
* honours a consumer className (→ AdvancedChartImpl → ChartContainer), so each
11+
* chart node carries a `responsiveStyles` height that shrinks it toward the
12+
* ~280px floor. Compact title + KPI strip + tight 2-col rows then fit the whole
13+
* board — KPIs, five charts and the work queue — without a long scroll.
14+
*
15+
* Theme-adaptive: every colour is a theme token (`hsl(var(--card))` panels,
16+
* `hsl(var(--foreground))` text, `hsl(var(--border))`). The root overrides
17+
* `--chart-1..5` with one cohesive mid-lightness ramp (reads on light AND dark);
18+
* KPI numbers reuse it.
19+
*
20+
* Layout note: object-chart must sit in a `display: block` panel, and the root
21+
* column needs `align-items: stretch` or children shrink to content width.
22+
*/
23+
24+
const CHART_RAMP = {
25+
'--chart-1': '192 86% 46%',
26+
'--chart-2': '214 84% 56%',
27+
'--chart-3': '256 72% 62%',
28+
'--chart-4': '168 76% 42%',
29+
'--chart-5': '322 72% 56%',
30+
};
31+
32+
function head(id: string, title: string, accent: string, badge?: string): any {
33+
return {
34+
id: id + '_h', type: 'flex',
35+
responsiveStyles: { large: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px', paddingBottom: '7px', borderBottom: '1px solid hsl(var(--border))' } },
36+
properties: {
37+
children: [
38+
{ id: id + '_bar', type: 'flex', responsiveStyles: { large: { width: '4px', height: '13px', borderRadius: '2px', background: accent, boxShadow: '0 0 9px ' + accent } }, properties: { children: [] } },
39+
{ id: id + '_t', type: 'element:text', responsiveStyles: { large: { fontSize: '13px', fontWeight: '700', letterSpacing: '0.03em', color: 'hsl(var(--foreground))' } }, properties: { content: title } },
40+
...(badge
41+
? [
42+
{ id: id + '_sp', type: 'flex', responsiveStyles: { large: { flex: '1 1 auto' } }, properties: { children: [] } },
43+
{ id: id + '_b', type: 'element:text', responsiveStyles: { large: { fontSize: '10px', fontWeight: '700', color: 'hsl(var(--chart-2))', background: 'hsl(var(--chart-2) / 0.12)', border: '1px solid hsl(var(--chart-2) / 0.4)', borderRadius: '999px', padding: '2px 9px' } }, properties: { content: badge } },
44+
]
45+
: []),
46+
],
47+
},
48+
};
49+
}
50+
51+
function panel(o: { id: string; title?: string; accent: string; badge?: string; minHeight?: string; span?: string; pad?: string; child: any }): any {
52+
return {
53+
id: o.id, type: 'flex',
54+
responsiveStyles: {
55+
large: {
56+
display: 'block', minWidth: '0', minHeight: o.minHeight ?? '0px',
57+
...(o.span ? { gridColumn: o.span } : {}),
58+
padding: o.pad ?? '12px 14px 12px', borderRadius: '14px',
59+
background: 'hsl(var(--card))',
60+
border: '1px solid hsl(var(--border))',
61+
boxShadow: '0 14px 34px -24px rgba(2,6,23,0.45), inset 0 1px 0 hsl(var(--foreground) / 0.04)',
62+
},
63+
small: { padding: '12px' },
64+
},
65+
properties: { children: [...(o.title ? [head(o.id, o.title, o.accent, o.badge)] : []), o.child] },
66+
};
67+
}
68+
69+
function band(id: string, cols: number, children: any[], gap = '12px'): any {
70+
return {
71+
id, type: 'flex',
72+
responsiveStyles: {
73+
large: { display: 'grid', gridTemplateColumns: 'repeat(' + cols + ', minmax(0,1fr))', gap, alignItems: 'stretch' },
74+
medium: { gridTemplateColumns: 'repeat(2, minmax(0,1fr))' },
75+
small: { gridTemplateColumns: '1fr' },
76+
},
77+
properties: { children },
78+
};
79+
}
80+
81+
function stat(id: string, value: string, label: string, chartVar: string): any {
82+
return {
83+
id, type: 'flex',
84+
responsiveStyles: { large: { display: 'flex', flexDirection: 'column', gap: '1px', padding: '2px 4px' } },
85+
properties: {
86+
children: [
87+
{ id: id + '_v', type: 'element:text', responsiveStyles: { large: { fontSize: '30px', fontWeight: '800', lineHeight: '1.05', color: 'hsl(var(' + chartVar + '))', fontVariantNumeric: 'tabular-nums' } }, properties: { content: value } },
88+
{ id: id + '_l', type: 'element:text', responsiveStyles: { large: { fontSize: '11px', fontWeight: '500', letterSpacing: '0.04em', color: 'hsl(var(--muted-foreground))' } }, properties: { content: label } },
89+
],
90+
},
91+
};
92+
}
93+
94+
// Each chart node carries a height → shrinks the ChartContainer toward its
95+
// ~280px floor (denser than the default h-[350px]).
96+
function chart(id: string, chartType: string, dataset: string, dimensions: string[], values: string[]): any {
97+
return { id, type: 'object-chart', responsiveStyles: { large: { width: '100%', minWidth: '0', height: '200px' } }, properties: { dataset, dimensions, values, chartType } };
98+
}
99+
100+
const A = { c1: 'hsl(var(--chart-1))', c2: 'hsl(var(--chart-2))', c3: 'hsl(var(--chart-3))', c4: 'hsl(var(--chart-4))', c5: 'hsl(var(--chart-5))' };
101+
102+
export const CommandCenterPage = definePage({
103+
name: 'showcase_command_center',
104+
label: 'Command Center (大屏)',
105+
type: 'app',
106+
template: 'default',
107+
kind: 'full',
108+
isDefault: false,
109+
regions: [
110+
{
111+
name: 'main',
112+
width: 'full',
113+
components: [
114+
{
115+
id: 'cc_root',
116+
type: 'flex',
117+
responsiveStyles: {
118+
large: {
119+
...CHART_RAMP,
120+
minHeight: '100%', width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: '10px',
121+
padding: '10px 20px 18px',
122+
background:
123+
'radial-gradient(1200px 520px at 50% -16%, hsl(var(--chart-1) / 0.10) 0%, transparent 60%), ' +
124+
'hsl(var(--background))',
125+
color: 'hsl(var(--foreground))',
126+
},
127+
small: { padding: '12px', gap: '10px' },
128+
},
129+
properties: {
130+
children: [
131+
// ── Title (compact) ─────────────────────────────────────────
132+
{
133+
id: 'cc_titlebar', type: 'flex',
134+
responsiveStyles: { large: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px', padding: '0' } },
135+
properties: {
136+
children: [
137+
{ id: 'cc_title', type: 'element:text', responsiveStyles: { large: { fontSize: '23px', fontWeight: '800', letterSpacing: '0.36em', color: 'hsl(var(--foreground))', textShadow: '0 0 22px hsl(var(--chart-1) / 0.45)' }, small: { fontSize: '17px', letterSpacing: '0.14em' } }, properties: { content: '交 付 运 营 数 据 大 屏' } },
138+
{ id: 'cc_subtitle', type: 'element:text', responsiveStyles: { large: { fontSize: '10px', fontWeight: '600', letterSpacing: '0.34em', color: 'hsl(var(--muted-foreground))' } }, properties: { content: 'DELIVERY OPERATIONS · COMMAND CENTER' } },
139+
],
140+
},
141+
},
142+
143+
// ── KPI hero strip — 6 metrics across the full width ─────────
144+
panel({
145+
id: 'cc_kpi', accent: A.c3, pad: '10px 16px',
146+
child: band('cc_kpi_grid', 6, [
147+
stat('cc_k1', '5', '项目 Projects', '--chart-1'),
148+
stat('cc_k2', '10', '任务 Tasks', '--chart-2'),
149+
stat('cc_k3', '13', '客户 Accounts', '--chart-3'),
150+
stat('cc_k4', '8', '待办 Open', '--chart-4'),
151+
stat('cc_k5', '2', '复审 Review', '--chart-5'),
152+
stat('cc_k6', '1.09M', '预算 Budget', '--chart-1'),
153+
], '8px'),
154+
}),
155+
156+
// ── Row 1 — trend (wide) + status ───────────────────────────
157+
band('cc_r1', 3, [
158+
panel({ id: 'cc_throughput', title: '任务吞吐趋势 (月)', accent: A.c2, span: 'span 2', child: chart('cc_thr_c', 'area', 'showcase_task_metrics', ['created_at'], ['task_count']) }),
159+
panel({ id: 'cc_status', title: '任务状态分布', accent: A.c1, child: chart('cc_status_c', 'bar', 'showcase_task_metrics', ['status'], ['task_count']) }),
160+
]),
161+
162+
// ── Row 2 — three charts ────────────────────────────────────
163+
band('cc_r2', 3, [
164+
panel({ id: 'cc_priority', title: '优先级分布', accent: A.c5, child: chart('cc_pri_c', 'bar', 'showcase_task_metrics', ['priority'], ['task_count']) }),
165+
panel({ id: 'cc_budget', title: '预算 vs 支出 (按客户)', accent: A.c4, child: chart('cc_bud_c', 'bar', 'showcase_project_metrics', ['account'], ['budget_sum', 'spent_sum']) }),
166+
panel({ id: 'cc_health', title: '项目健康度', accent: A.c4, child: chart('cc_health_c', 'donut', 'showcase_project_metrics', ['health'], ['project_count']) }),
167+
]),
168+
169+
// ── Row 3 — work queue, full width, compact ─────────────────
170+
panel({
171+
id: 'cc_queue', title: '待审核列表 · Work Queue', accent: A.c1, badge: '审批中', pad: '12px 14px 8px',
172+
child: { id: 'cc_queue_g', type: 'object-grid', responsiveStyles: { large: { minWidth: '0', display: 'block' } }, properties: { objectName: 'showcase_task', columns: ['title', 'project', 'status', 'priority', 'due_date'] } },
173+
}),
174+
],
175+
},
176+
},
177+
],
178+
},
179+
],
180+
});

examples/app-showcase/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { NewProjectWizardPage } from './new-project-wizard.page.js';
1414
export { MyWorkPage } from './my-work.page.js';
1515
export { SettingsPage } from './settings.page.js';
1616
export { StylingGalleryPage } from './styling-gallery.page.js';
17+
export { CommandCenterPage } from './command-center.page.js';
1718
export {
1819
TaskBoardPage,
1920
TaskCalendarPage,

0 commit comments

Comments
 (0)