Skip to content

Commit 18c8886

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(showcase): live bare-variant KPIs + brand palette on Command Center 大屏 (#2244)
Adopt the objectui data-screen primitives (objectstack-ai/objectui#1923): - KPI hero strip → live object-metric 'variant:bare' (was hand-typed static element:text numbers); compact '0.0a' on the budget → '1.1M'. - charts → object-chart 'colors' brand palette (drops the --chart-1..5 CSS-var override hack) + first-class 'donut' chartType. Bumps vendored console (.objectui-sha) to the objectui commit carrying those renderer capabilities. Theme-following (light dashboard / dark 大屏) unchanged. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8130996 commit 18c8886

2 files changed

Lines changed: 66 additions & 73 deletions

File tree

.objectui-sha

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

examples/app-showcase/src/pages/command-center.page.ts

Lines changed: 65 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,41 @@
33
import { definePage } from '@objectstack/spec/ui';
44

55
/**
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).
6+
* Operations Command Center (大屏) — a full-bleed SDUI "big screen" that follows
7+
* the console theme (light ↔ dark). It exercises the data-screen customization
8+
* primitives added to objectui (#1922 + follow-up):
9+
* • object-metric `variant:'bare'` → live KPIs as big tinted numbers (no card
10+
* chrome) — data-bound, not hand-typed.
11+
* • object-chart `colors` → one cohesive brand palette across every chart
12+
* (replaces the old `--chart-1..5` override hack).
13+
* • object-chart `chartType:'donut'` → first-class (was an untyped string).
14+
* • full-bleed page + container-scaled donuts + gradient bars/areas.
815
*
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.
16+
* Composition: centred title → full-width KPI hero strip (6 live metrics) →
17+
* two equal chart rows (throughput trend spans 2) → work queue on its own
18+
* full-width row (height never knocks a chart row out of alignment).
1419
*
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.
20+
* Theme-adaptive: panels/text/hairlines are theme tokens (`hsl(var(--card))` …).
21+
* Layout note: object-chart must sit in a `display:block` panel (a flex child
22+
* collapses to width:0 and recharts won't draw).
2223
*/
2324

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-
};
25+
// One cohesive brand palette, mid-lightness so it reads on light AND dark.
26+
const PALETTE = ['hsl(192 86% 46%)', 'hsl(214 84% 56%)', 'hsl(256 72% 62%)', 'hsl(168 76% 42%)', 'hsl(322 72% 56%)'];
27+
const A = { c1: PALETTE[0], c2: PALETTE[1], c3: PALETTE[2], c4: PALETTE[3], c5: PALETTE[4] };
3128

3229
function head(id: string, title: string, accent: string, badge?: string): any {
3330
return {
3431
id: id + '_h', type: 'flex',
35-
responsiveStyles: { large: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px', paddingBottom: '7px', borderBottom: '1px solid hsl(var(--border))' } },
32+
responsiveStyles: { large: { display: 'flex', alignItems: 'center', gap: '9px', marginBottom: '12px', paddingBottom: '10px', borderBottom: '1px solid hsl(var(--border))' } },
3633
properties: {
3734
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 } },
35+
{ id: id + '_bar', type: 'flex', responsiveStyles: { large: { width: '4px', height: '15px', borderRadius: '2px', background: accent, boxShadow: '0 0 10px ' + accent } }, properties: { children: [] } },
36+
{ id: id + '_t', type: 'element:text', responsiveStyles: { large: { fontSize: '14px', fontWeight: '700', letterSpacing: '0.03em', color: 'hsl(var(--foreground))' } }, properties: { content: title } },
4037
...(badge
4138
? [
4239
{ 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 } },
40+
{ id: id + '_b', type: 'element:text', responsiveStyles: { large: { fontSize: '11px', fontWeight: '700', color: A.c2, background: 'hsl(214 84% 56% / 0.12)', border: '1px solid hsl(214 84% 56% / 0.4)', borderRadius: '999px', padding: '3px 11px' } }, properties: { content: badge } },
4441
]
4542
: []),
4643
],
@@ -53,20 +50,20 @@ function panel(o: { id: string; title?: string; accent: string; badge?: string;
5350
id: o.id, type: 'flex',
5451
responsiveStyles: {
5552
large: {
56-
display: 'block', minWidth: '0', minHeight: o.minHeight ?? '0px',
53+
display: 'block', minWidth: '0', minHeight: o.minHeight ?? '240px',
5754
...(o.span ? { gridColumn: o.span } : {}),
58-
padding: o.pad ?? '12px 14px 12px', borderRadius: '14px',
55+
padding: o.pad ?? '15px 17px 17px', borderRadius: '16px',
5956
background: 'hsl(var(--card))',
6057
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)',
58+
boxShadow: '0 16px 40px -24px rgba(2,6,23,0.45), inset 0 1px 0 hsl(var(--foreground) / 0.04)',
6259
},
63-
small: { padding: '12px' },
60+
small: { padding: '12px', minHeight: '200px' },
6461
},
6562
properties: { children: [...(o.title ? [head(o.id, o.title, o.accent, o.badge)] : []), o.child] },
6663
};
6764
}
6865

69-
function band(id: string, cols: number, children: any[], gap = '12px'): any {
66+
function band(id: string, cols: number, children: any[], gap = '16px'): any {
7067
return {
7168
id, type: 'flex',
7269
responsiveStyles: {
@@ -78,26 +75,21 @@ function band(id: string, cols: number, children: any[], gap = '12px'): any {
7875
};
7976
}
8077

81-
function stat(id: string, value: string, label: string, chartVar: string): any {
78+
/** A LIVE KPI — object-metric in the new `bare` variant (big tinted number + label). */
79+
function kpi(id: string, object: string, label: string, colorVariant: string, aggregate: any, filter?: any, format?: string): any {
8280
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-
},
81+
id, type: 'object-metric',
82+
responsiveStyles: { large: { minWidth: '0' } },
83+
properties: { objectName: object, label, colorVariant, variant: 'bare', aggregate, ...(filter ? { filter } : {}), ...(format ? { format } : {}) },
9184
};
9285
}
9386

94-
// Each chart node carries a height → shrinks the ChartContainer toward its
95-
// ~280px floor (denser than the default h-[350px]).
87+
/** A dataset-bound chart with the shared brand palette. */
9688
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 } };
89+
return { id, type: 'object-chart', responsiveStyles: { large: { width: '100%', minWidth: '0' } }, properties: { dataset, dimensions, values, chartType, colors: PALETTE } };
9890
}
9991

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))' };
92+
const CHART_H = '376px';
10193

10294
export const CommandCenterPage = definePage({
10395
name: 'showcase_command_center',
@@ -116,59 +108,60 @@ export const CommandCenterPage = definePage({
116108
type: 'flex',
117109
responsiveStyles: {
118110
large: {
119-
...CHART_RAMP,
120-
minHeight: '100%', width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: '10px',
121-
padding: '10px 20px 18px',
111+
minHeight: '100%', display: 'flex', flexDirection: 'column', gap: '16px',
112+
padding: '22px 26px 32px',
122113
background:
123-
'radial-gradient(1200px 520px at 50% -16%, hsl(var(--chart-1) / 0.10) 0%, transparent 60%), ' +
114+
'radial-gradient(1200px 540px at 50% -14%, hsl(192 86% 46% / 0.10) 0%, transparent 60%), ' +
115+
'radial-gradient(900px 460px at 100% 0%, hsl(256 72% 62% / 0.08) 0%, transparent 55%), ' +
124116
'hsl(var(--background))',
125117
color: 'hsl(var(--foreground))',
126118
},
127-
small: { padding: '12px', gap: '10px' },
119+
small: { padding: '14px 12px 24px', gap: '12px' },
128120
},
129121
properties: {
130122
children: [
131-
// ── Title (compact) ─────────────────────────────────────────
123+
// ── Title ────────────────────────────────────────────────────
132124
{
133125
id: 'cc_titlebar', type: 'flex',
134-
responsiveStyles: { large: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px', padding: '0' } },
126+
responsiveStyles: { large: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '5px', padding: '2px 0 2px' } },
135127
properties: {
136128
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' } },
129+
{ id: 'cc_title', type: 'element:text', responsiveStyles: { large: { fontSize: '27px', fontWeight: '800', letterSpacing: '0.4em', color: 'hsl(var(--foreground))', textShadow: '0 0 26px hsl(192 86% 46% / 0.45)' }, small: { fontSize: '18px', letterSpacing: '0.16em' } }, properties: { content: '交 付 运 营 数 据 大 屏' } },
130+
{ id: 'cc_subtitle', type: 'element:text', responsiveStyles: { large: { fontSize: '11px', fontWeight: '600', letterSpacing: '0.36em', color: 'hsl(var(--muted-foreground))' } }, properties: { content: 'DELIVERY OPERATIONS · COMMAND CENTER' } },
131+
{ id: 'cc_rule', type: 'flex', responsiveStyles: { large: { width: 'min(640px, 60%)', height: '1px', marginTop: '4px', background: 'linear-gradient(90deg, transparent, hsl(192 86% 46% / 0.55), transparent)' } }, properties: { children: [] } },
139132
],
140133
},
141134
},
142135

143-
// ── KPI hero strip — 6 metrics across the full width ─────────
136+
// ── KPI hero strip — 6 LIVE metrics (bare variant) ───────────
144137
panel({
145-
id: 'cc_kpi', accent: A.c3, pad: '10px 16px',
138+
id: 'cc_kpi', title: '核心指标 · Key Metrics', accent: A.c3, minHeight: '0px', pad: '14px 18px 16px',
146139
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'),
140+
kpi('cc_k1', 'showcase_project', '活跃项目 Active', 'blue', { field: 'id', function: 'count' }, { status: 'active' }),
141+
kpi('cc_k2', 'showcase_task', '待办任务 Open', 'teal', { field: 'id', function: 'count' }, { status: { $ne: 'done' } }),
142+
kpi('cc_k3', 'showcase_task', '待复审 Review', 'purple', { field: 'id', function: 'count' }, { status: 'in_review' }),
143+
kpi('cc_k4', 'showcase_project', '风险项目 At-Risk', 'danger', { field: 'id', function: 'count' }, { health: 'red' }),
144+
kpi('cc_k5', 'showcase_account', '客户 Accounts', 'orange', { field: 'id', function: 'count' }),
145+
kpi('cc_k6', 'showcase_project', '总预算 Budget', 'success', { field: 'budget', function: 'sum' }, undefined, '0.0a'),
146+
], '10px'),
154147
}),
155148

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']) }),
149+
// ── Row A — three equal chart panels ─────────────────────────
150+
band('cc_rowA', 3, [
151+
panel({ id: 'cc_status', title: '任务状态分布', accent: A.c1, minHeight: CHART_H, child: chart('cc_status_c', 'bar', 'showcase_task_metrics', ['status'], ['task_count']) }),
152+
panel({ id: 'cc_health', title: '项目健康度', accent: A.c4, minHeight: CHART_H, child: chart('cc_health_c', 'donut', 'showcase_project_metrics', ['health'], ['project_count']) }),
153+
panel({ id: 'cc_priority', title: '优先级分布', accent: A.c5, minHeight: CHART_H, child: chart('cc_pri_c', 'bar', 'showcase_task_metrics', ['priority'], ['task_count']) }),
160154
]),
161155

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']) }),
156+
// ── Row B — wide trend (span 2) + spend ──────────────────────
157+
band('cc_rowB', 3, [
158+
panel({ id: 'cc_throughput', title: '任务吞吐趋势 (月)', accent: A.c2, span: 'span 2', minHeight: CHART_H, child: chart('cc_thr_c', 'area', 'showcase_task_metrics', ['created_at'], ['task_count']) }),
159+
panel({ id: 'cc_budget', title: '预算 vs 支出 (按客户)', accent: A.c4, minHeight: CHART_H, child: chart('cc_bud_c', 'bar', 'showcase_project_metrics', ['account'], ['budget_sum', 'spent_sum']) }),
167160
]),
168161

169-
// ── Row 3work queue, full width, compact ─────────────────
162+
// ── Work queueits own full-width row at the bottom ────────
170163
panel({
171-
id: 'cc_queue', title: '待审核列表 · Work Queue', accent: A.c1, badge: '审批中', pad: '12px 14px 8px',
164+
id: 'cc_queue', title: '待审核列表 · Work Queue', accent: A.c1, badge: '审批中', minHeight: '0px',
172165
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'] } },
173166
}),
174167
],

0 commit comments

Comments
 (0)