Skip to content

Commit b136f7c

Browse files
os-zhuangclaude
andauthored
fix(showcase): drop Tailwind from kind:'react' pages — inline styles + pre-styled overlays (ADR-0065) (#2494)
A react/html page's `source` is runtime metadata, so the console's build-time Tailwind never scans it — utility classes that don't also appear in the console's own source silently produce NO CSS (exactly the ADR-0065 failure: styling that "works only by coincidence"). The Task Desk modal proved it: a hand-rolled `bg-black/50` backdrop rendered transparent, the form floated as unstyled black boxes, and light-theme `text-slate-900` headings were invisible on the dark theme. Rewrite all six kind:'react' showcase pages to use ZERO Tailwind: - Overlays use the platform's own `<ObjectForm formType="drawer"|"modal">` variant (pre-styled Sheet/Dialog with backdrop + animation), driven by `open`/`onOpenChange` — NOT hand-rolled `fixed inset-0` overlays. - Page chrome (headers, KPI cards, tabs, badges, empty states) uses inline `style={{}}` with `hsl(var(--token))` theme colors — real CSS, theme-aware, always generated regardless of the build's content scan. Pages: task-desk, crm-workbench, inquiry-triage, account-cockpit, invoice-console, renewals-pipeline. Browser-verified on a real `objectstack serve` console: titles now legible, Task Desk drawer/modal render the pre-styled ObjectForm overlay (proper backdrop + card), CRM KPI cards and Inquiry tabs/badges all theme-correct. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 127ee94 commit b136f7c

6 files changed

Lines changed: 195 additions & 262 deletions

File tree

examples/app-showcase/src/pages/account-cockpit.page.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { definePage } from '@objectstack/spec/ui';
55
/**
66
* Account Cockpit — a `kind:'react'` business scenario (ADR-0081).
77
*
8-
* A customer-360 cockpit: a live-search account list (React text input drives
9-
* the real `<ListView>`'s `filters`), a selected account edited in a real
10-
* `<ObjectForm>`, and a related-data strip that aggregates the account's
11-
* projects and invoices via cross-object `useAdapter` queries. Demonstrates
12-
* live search, master-detail edit, and cross-object roll-ups together.
8+
* Customer-360 over `showcase_account`: a live search filters a real
9+
* `<ListView>`, selecting an account loads it into an `<ObjectForm>` editor and
10+
* rolls up related projects & invoices via `useAdapter()` cross-object queries.
11+
*
12+
* Styling (ADR-0065): no Tailwind — inline `style={{}}` with `hsl(var(--token))`.
1313
*/
1414
export const AccountCockpitPage = definePage({
1515
name: 'showcase_account_cockpit',
@@ -38,49 +38,49 @@ function Page() {
3838
}, [adapter, sel, reload]);
3939
4040
const filters = q.trim() ? ['name', 'contains', q.trim()] : undefined;
41+
const card = { background: 'hsl(var(--card))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)' };
4142
const Stat = ({ label, value, accent }) => (
42-
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3">
43-
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">{label}</div>
44-
<div className={'mt-1 text-2xl font-bold ' + (accent || 'text-slate-900')}>{value}</div>
43+
<div style={{ background: 'hsl(var(--muted))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)', padding: 12 }}>
44+
<div style={{ fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em', color: 'hsl(var(--muted-foreground))' }}>{label}</div>
45+
<div style={{ marginTop: 4, fontSize: 24, fontWeight: 700, color: accent || 'hsl(var(--foreground))' }}>{value}</div>
4546
</div>
4647
);
4748
4849
return (
49-
<div className="mx-auto max-w-6xl space-y-5 p-8">
50-
<header className="flex items-end justify-between gap-4">
50+
<div style={{ maxWidth: 1152, margin: '0 auto', padding: 32, display: 'flex', flexDirection: 'column', gap: 20 }}>
51+
<header style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 16 }}>
5152
<div>
52-
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Account Cockpit</h1>
53-
<p className="mt-1 text-sm text-slate-500">Customer-360 over <code>showcase_account</code> — search, edit, and roll up related projects &amp; invoices.</p>
53+
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 700, letterSpacing: '-0.01em', color: 'hsl(var(--foreground))' }}>Account Cockpit</h1>
54+
<p style={{ marginTop: 4, fontSize: 14, color: 'hsl(var(--muted-foreground))' }}>Customer-360 over <code>showcase_account</code> — search, edit, and roll up related projects &amp; invoices.</p>
5455
</div>
55-
<input value={q} onChange={(e) => { setQ(e.target.value); setSel(null); }}
56-
placeholder="Search accounts…"
57-
className="w-64 rounded-lg border border-slate-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none" />
56+
<input value={q} onChange={(e) => { setQ(e.target.value); setSel(null); }} placeholder="Search accounts…"
57+
style={{ width: 256, borderRadius: 'var(--radius)', border: '1px solid hsl(var(--border))', background: 'hsl(var(--background))', color: 'hsl(var(--foreground))', padding: '8px 12px', fontSize: 14, outline: 'none' }} />
5858
</header>
5959
60-
<div className="grid grid-cols-5 gap-6">
61-
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
60+
<div style={{ display: 'grid', gridTemplateColumns: '3fr 2fr', gap: 24, alignItems: 'start' }}>
61+
<section style={{ ...card, padding: 8 }}>
6262
<ListView key={q + ':' + reload} objectName="showcase_account"
6363
fields={['name', 'industry', 'status', 'annual_revenue']} filters={filters}
6464
navigation={{ mode: 'none' }} onRowClick={(r) => setSel(r)} />
6565
</section>
66-
<section className="col-span-2 space-y-4">
66+
<section style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
6767
{sel ? (
6868
<React.Fragment>
69-
<div className="grid grid-cols-3 gap-3">
69+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
7070
<Stat label="Projects" value={related.projects} />
7171
<Stat label="Invoices" value={related.invoices} />
72-
<Stat label="Open AR" value={related.openInvoices} accent="text-amber-600" />
72+
<Stat label="Open AR" value={related.openInvoices} accent="hsl(38 92% 50%)" />
7373
</div>
74-
<div className="rounded-xl border border-slate-200 bg-white p-5">
74+
<div style={{ ...card, padding: 20 }}>
7575
<ObjectForm key={sel.id + ':' + reload} objectName="showcase_account" mode="edit"
7676
recordId={sel.id} onSuccess={() => { setSel(null); setReload((k) => k + 1); }}
7777
onCancel={() => setSel(null)} />
7878
</div>
7979
</React.Fragment>
8080
) : (
81-
<div className="flex h-full min-h-[240px] flex-col items-center justify-center rounded-xl border border-slate-200 bg-white text-center text-slate-400">
82-
<div className="text-4xl">🛰️</div>
83-
<p className="mt-2 text-sm">Search and select an account.</p>
81+
<div style={{ ...card, display: 'flex', minHeight: 240, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: 'hsl(var(--muted-foreground))' }}>
82+
<div style={{ fontSize: 32 }}>🛰️</div>
83+
<p style={{ marginTop: 8, fontSize: 14 }}>Search and select an account.</p>
8484
</div>
8585
)}
8686
</section>

examples/app-showcase/src/pages/crm-workbench.page.ts

Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,15 @@ import { definePage } from '@objectstack/spec/ui';
55
/**
66
* CRM Workbench — a `kind:'react'` page (ADR-0081, the TRUSTED tier).
77
*
8-
* The whole page is REAL React executed at render by `@object-ui/react-runtime`
9-
* (hooks, event handlers, arbitrary JS) — NOT the constrained parse-never-execute
10-
* `kind:'html'` tier. It demonstrates exactly what a real customer business UI
11-
* needs: a master/detail workbench that COMPOSES the platform's real data
12-
* components — `<ListView>` (the object table) and `<ObjectForm>` — with React
13-
* state to wire complex interaction the fixed page schema cannot express:
8+
* Real React executed at render (hooks, handlers, arbitrary JS) composing the
9+
* platform's real data components — `<ListView>` + `<ObjectForm>` — into a
10+
* master/detail workbench with a live KPI strip.
1411
*
15-
* - click a project row → load that record into the editor (controlled `recordId`)
16-
* - save the form → refresh the list (`onSuccess` bumps a remount key)
17-
* - "New project" → the same editor in create mode
18-
* - a live KPI strip → `useAdapter()` queries the object directly
19-
*
20-
* The injected scope exposes `React`, `useAdapter`, and the curated public data
21-
* blocks as PascalCase components (`ListView`, `ObjectForm`, `ObjectMetric`, …),
22-
* each a real registered renderer. Layout is plain HTML + Tailwind.
23-
*
24-
* NOTE: `kind:'react'` executes author code, so it is gated by the host
25-
* capability `CAP_REACT_PAGES`, which defaults ON (the platform trusts its
26-
* reviewed, draft-gated authors). A deployment that does not trust its authors
27-
* turns it off server-side with `OS_PAGE_REACT=off`, in which case
28-
* this page renders a "disabled on this deployment" notice instead of executing.
12+
* Styling (ADR-0065): page source is runtime metadata, so the console's
13+
* build-time Tailwind never scans it — utility classes silently no-op. So the
14+
* page uses ZERO Tailwind: layout/chrome is inline `style={{}}` with
15+
* `hsl(var(--token))` theme colors (real CSS, theme-aware), and the data
16+
* components bring their own compiled styling.
2917
*/
3018
export const CrmWorkbenchPage = definePage({
3119
name: 'showcase_crm_workbench',
@@ -48,64 +36,50 @@ function Page() {
4836
setStats({ total: rows.length, active: rows.filter((r) => r.status === 'active').length });
4937
} catch (e) { /* ignore in demo */ }
5038
}, [adapter]);
51-
5239
React.useEffect(() => { refreshStats(); }, [refreshStats, reloadKey]);
5340
5441
const openNew = () => { setSelected(null); setMode('create'); };
5542
const onRowClick = (rec) => { setSelected(rec); setMode('edit'); };
5643
const afterSave = () => { setSelected(null); setMode('edit'); setReloadKey((k) => k + 1); };
57-
5844
const editing = mode === 'create' || selected;
5945
46+
const card = { background: 'hsl(var(--card))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)', padding: 16 };
47+
const eyebrow = { fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em', color: 'hsl(var(--muted-foreground))' };
48+
const big = { marginTop: 4, fontSize: 30, fontWeight: 700, color: 'hsl(var(--foreground))' };
49+
6050
return (
61-
<div className="mx-auto max-w-6xl space-y-6 p-8">
62-
<header className="flex items-center justify-between">
51+
<div style={{ maxWidth: 1152, margin: '0 auto', padding: 32, display: 'flex', flexDirection: 'column', gap: 24 }}>
52+
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
6353
<div>
64-
<h1 className="text-2xl font-bold tracking-tight text-slate-900">CRM Workbench</h1>
65-
<p className="mt-1 text-sm text-slate-500">Master/detail over <code>showcase_project</code> — real <code>&lt;ListView&gt;</code> + <code>&lt;ObjectForm&gt;</code> wired with React state.</p>
54+
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 700, letterSpacing: '-0.01em', color: 'hsl(var(--foreground))' }}>CRM Workbench</h1>
55+
<p style={{ marginTop: 4, fontSize: 14, color: 'hsl(var(--muted-foreground))' }}>Master/detail over <code>showcase_project</code> — real <code>&lt;ListView&gt;</code> + <code>&lt;ObjectForm&gt;</code> wired with React state.</p>
6656
</div>
67-
<button onClick={openNew} className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500">+ New project</button>
57+
<button onClick={openNew} style={{ flexShrink: 0, borderRadius: 'var(--radius)', background: 'hsl(var(--primary))', color: 'hsl(var(--primary-foreground))', padding: '8px 16px', fontSize: 14, fontWeight: 600, border: 'none', cursor: 'pointer' }}>+ New project</button>
6858
</header>
6959
70-
<div className="grid grid-cols-3 gap-4">
71-
<div className="rounded-xl border border-slate-200 bg-white p-4">
72-
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">Total projects</div>
73-
<div className="mt-1 text-3xl font-bold text-slate-900">{stats.total}</div>
74-
</div>
75-
<div className="rounded-xl border border-slate-200 bg-white p-4">
76-
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">Active</div>
77-
<div className="mt-1 text-3xl font-bold text-emerald-600">{stats.active}</div>
78-
</div>
79-
<div className="rounded-xl border border-slate-200 bg-white p-4">
80-
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">Editing</div>
81-
<div className="mt-1 truncate text-lg font-semibold text-slate-700">{mode === 'create' ? 'New project' : selected ? (selected.name || selected.id) : '—'}</div>
82-
</div>
60+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
61+
<div style={card}><div style={eyebrow}>Total projects</div><div style={big}>{stats.total}</div></div>
62+
<div style={card}><div style={eyebrow}>Active</div><div style={{ ...big, color: 'hsl(142 70% 45%)' }}>{stats.active}</div></div>
63+
<div style={card}><div style={eyebrow}>Editing</div><div style={{ marginTop: 4, fontSize: 18, fontWeight: 600, color: 'hsl(var(--foreground))', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{mode === 'create' ? 'New project' : selected ? (selected.name || selected.id) : '—'}</div></div>
8364
</div>
8465
85-
<div className="grid grid-cols-5 gap-6">
86-
<section className="col-span-3 rounded-xl border border-slate-200 bg-white p-2">
87-
<ListView
88-
key={reloadKey}
89-
objectName="showcase_project"
66+
<div style={{ display: 'grid', gridTemplateColumns: '3fr 2fr', gap: 24, alignItems: 'start' }}>
67+
<section style={{ ...card, padding: 8 }}>
68+
<ListView key={reloadKey} objectName="showcase_project"
9069
fields={['name', 'status', 'health', 'budget', 'owner']}
91-
navigation={{ mode: 'none' }}
92-
onRowClick={onRowClick}
93-
/>
70+
navigation={{ mode: 'none' }} onRowClick={onRowClick} />
9471
</section>
95-
<section className="col-span-2 rounded-xl border border-slate-200 bg-white p-5">
72+
<section style={{ ...card, padding: 20 }}>
9673
{editing ? (
9774
<ObjectForm
9875
key={(mode === 'create' ? 'new' : selected && selected.id) + ':' + reloadKey}
99-
objectName="showcase_project"
100-
mode={mode}
76+
objectName="showcase_project" mode={mode}
10177
recordId={mode === 'edit' && selected ? selected.id : undefined}
102-
onSuccess={afterSave}
103-
onCancel={() => { setSelected(null); }}
104-
/>
78+
onSuccess={afterSave} onCancel={() => { setSelected(null); }} />
10579
) : (
106-
<div className="flex h-full min-h-[240px] flex-col items-center justify-center text-center text-slate-400">
107-
<div className="text-4xl">🗂️</div>
108-
<p className="mt-2 text-sm">Select a project to edit, or create a new one.</p>
80+
<div style={{ display: 'flex', minHeight: 240, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: 'hsl(var(--muted-foreground))' }}>
81+
<div style={{ fontSize: 32 }}>🗂️</div>
82+
<p style={{ marginTop: 8, fontSize: 14 }}>Select a project to edit, or create a new one.</p>
10983
</div>
11084
)}
11185
</section>

0 commit comments

Comments
 (0)