diff --git a/examples/app-showcase/objectstack.config.ts b/examples/app-showcase/objectstack.config.ts index f1ea0032e9..b46c606acb 100644 --- a/examples/app-showcase/objectstack.config.ts +++ b/examples/app-showcase/objectstack.config.ts @@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js'; import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js'; import { allReports } from './src/reports/index.js'; import { allActions } from './src/actions/index.js'; -import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage } from './src/pages/index.js'; +import { StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, AccountCockpitPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage } from './src/pages/index.js'; import { allFlows } from './src/flows/index.js'; import { allWebhooks } from './src/webhooks/index.js'; import { allHooks } from './src/hooks/index.js'; @@ -154,7 +154,7 @@ export default defineStack({ apps: [ShowcaseApp], portals: allPortals, views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews], - pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage], + pages: [StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, AccountCockpitPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage], dashboards: [ChartGalleryDashboard, OpsDashboard], books: allBooks, datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset], diff --git a/examples/app-showcase/src/apps/index.ts b/examples/app-showcase/src/apps/index.ts index fc59bc1699..3e7f07d295 100644 --- a/examples/app-showcase/src/apps/index.ts +++ b/examples/app-showcase/src/apps/index.ts @@ -3,9 +3,18 @@ import { App } from '@objectstack/spec/ui'; /** - * Showcase app — navigation that links to every surface so a human can click - * through the whole gallery: the data objects, the Task view gallery, the - * Chart Gallery dashboard, the reports, and the component-gallery home page. + * Showcase app — navigation organized to TEACH page authoring. + * + * "Start Here" is the default landing: it explains the two axes every page has — + * `type` (the surface role) and `kind` (the authoring model) — and links to the + * canonical example of each kind. The first three groups (Workspace / Data Model + * / Analytics) are the app *working* like a real product. The "Authoring · *" + * groups are the page-authoring gallery, split BY KIND so a learner can see + * which model fits which scenario: + * • Structured (full/slotted) — declarative regions, no code (the default) + * • Visualizations — one list/interface page per record view-type + * • HTML — constrained JSX, composed, parsed-never-executed + * • React — real React, executed, interactive (trusted tier) */ export const ShowcaseApp = App.create({ name: 'showcase_app', @@ -14,6 +23,7 @@ export const ShowcaseApp = App.create({ branding: { primaryColor: '#7C3AED' }, navigation: [ + { id: 'nav_start_here', type: 'page', pageName: 'showcase_start_here', label: 'Start Here', icon: 'compass' }, { id: 'grp_workspace', type: 'group', @@ -59,18 +69,12 @@ export const ShowcaseApp = App.create({ ], }, { - id: 'grp_pages', + id: 'grp_auth_structured', type: 'group', - label: 'Pages', - icon: 'layout', + label: 'Authoring · Structured', + icon: 'layout-template', children: [ { id: 'nav_gallery', type: 'page', pageName: 'showcase_component_gallery', label: 'Component Gallery', icon: 'layout-template' }, - { id: 'nav_command_center_jsx', type: 'page', pageName: 'showcase_command_center_jsx', label: 'Command Center (HTML)', icon: 'code' }, - { id: 'nav_crm_workbench', type: 'page', pageName: 'showcase_crm_workbench', label: 'CRM Workbench (React)', icon: 'layout-dashboard' }, - { id: 'nav_inquiry_triage', type: 'page', pageName: 'showcase_inquiry_triage', label: 'Inquiry Triage (React)', icon: 'inbox' }, - { id: 'nav_account_cockpit', type: 'page', pageName: 'showcase_account_cockpit', label: 'Account Cockpit (React)', icon: 'satellite' }, - { id: 'nav_invoice_console', type: 'page', pageName: 'showcase_invoice_console', label: 'Invoice Console (React)', icon: 'receipt' }, - { id: 'nav_task_desk', type: 'page', pageName: 'showcase_task_desk', label: 'Task Desk (React)', icon: 'panel-right-open' }, { id: 'nav_styling_gallery', type: 'page', pageName: 'showcase_styling_gallery', label: 'Styling (ADR-0065)', icon: 'palette' }, { id: 'nav_page_variables', type: 'page', pageName: 'showcase_page_variables', label: 'Page Variables', icon: 'mouse-pointer-click' }, { id: 'nav_contact_form', type: 'page', pageName: 'showcase_contact_form', label: 'Contact Form', icon: 'mail-plus' }, @@ -79,6 +83,14 @@ export const ShowcaseApp = App.create({ { id: 'nav_task_workbench', type: 'page', pageName: 'showcase_task_workbench', label: 'Task Workbench', icon: 'sliders-horizontal' }, { id: 'nav_task_triage', type: 'page', pageName: 'showcase_task_triage', label: 'Task Triage (Tabs)', icon: 'layout-list' }, { id: 'nav_active_projects', type: 'page', pageName: 'showcase_active_projects', label: 'Active Projects', icon: 'folder-kanban' }, + ], + }, + { + id: 'grp_auth_viz', + type: 'group', + label: 'Authoring · Visualizations', + icon: 'layout-grid', + children: [ { id: 'nav_task_all_views', type: 'page', pageName: 'showcase_task_all_views', label: 'All Views', icon: 'layout-grid' }, { id: 'nav_task_board', type: 'page', pageName: 'showcase_task_board', label: 'Task Board', icon: 'columns-3' }, { id: 'nav_task_calendar', type: 'page', pageName: 'showcase_task_calendar', label: 'Task Calendar', icon: 'calendar' }, @@ -88,5 +100,26 @@ export const ShowcaseApp = App.create({ { id: 'nav_task_map', type: 'page', pageName: 'showcase_task_map', label: 'Work Map', icon: 'map-pin' }, ], }, + { + id: 'grp_auth_html', + type: 'group', + label: 'Authoring · HTML', + icon: 'code', + children: [ + { id: 'nav_command_center_jsx', type: 'page', pageName: 'showcase_command_center_jsx', label: 'Command Center', icon: 'code' }, + ], + }, + { + id: 'grp_auth_react', + type: 'group', + label: 'Authoring · React', + icon: 'zap', + children: [ + { id: 'nav_crm_workbench', type: 'page', pageName: 'showcase_crm_workbench', label: 'CRM Workbench · master/detail', icon: 'layout-dashboard' }, + { id: 'nav_task_desk', type: 'page', pageName: 'showcase_task_desk', label: 'Task Desk · drawer & modal', icon: 'panel-right-open' }, + { id: 'nav_account_cockpit', type: 'page', pageName: 'showcase_account_cockpit', label: 'Account Cockpit · live rollup', icon: 'satellite' }, + { id: 'nav_renewals_pipeline', type: 'page', pageName: 'showcase_renewals_pipeline', label: 'Renewals Pipeline · record blocks', icon: 'refresh-cw' }, + ], + }, ], }); diff --git a/examples/app-showcase/src/pages/index.ts b/examples/app-showcase/src/pages/index.ts index 18259d52ab..69c239fce4 100644 --- a/examples/app-showcase/src/pages/index.ts +++ b/examples/app-showcase/src/pages/index.ts @@ -2,6 +2,7 @@ import { definePage } from '@objectstack/spec/ui'; +export { StartHerePage } from './start-here.page.js'; export { ProjectWorkspacePage } from './project-workspace.page.js'; export { ProjectDetailPage } from './project-detail.page.js'; export { TaskWorkbenchPage } from './task-workbench.page.js'; @@ -17,9 +18,7 @@ export { StylingGalleryPage } from './styling-gallery.page.js'; export { CommandCenterPage } from './command-center.page.js'; export { CommandCenterJsxPage } from './command-center-jsx.page.js'; export { CrmWorkbenchPage } from './crm-workbench.page.js'; -export { InquiryTriagePage } from './inquiry-triage.page.js'; export { AccountCockpitPage } from './account-cockpit.page.js'; -export { InvoiceConsolePage } from './invoice-console.page.js'; export { TaskDeskPage } from './task-desk.page.js'; export { PageVariablesPage } from './page-variables.page.js'; export { ContactFormPage } from './contact-form.page.js'; @@ -46,7 +45,7 @@ export const ComponentGalleryPage = definePage({ label: 'Component Gallery', type: 'home', template: 'header-sidebar-main', - isDefault: true, + isDefault: false, kind: 'full', regions: [ { diff --git a/examples/app-showcase/src/pages/inquiry-triage.page.ts b/examples/app-showcase/src/pages/inquiry-triage.page.ts deleted file mode 100644 index d21218d93c..0000000000 --- a/examples/app-showcase/src/pages/inquiry-triage.page.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { definePage } from '@objectstack/spec/ui'; - -/** - * Inquiry Triage Inbox — a `kind:'react'` business scenario (ADR-0081). - * - * A support/lead-triage queue: status TABS with live counts filter a real - * ``, and a detail panel with one-click status actions persists via - * `useAdapter().update` and refreshes the list + counts. - * - * Styling (ADR-0065): no Tailwind — page chrome is inline `style={{}}` with - * `hsl(var(--token))` theme colors; the data component brings its own styling. - */ -export const InquiryTriagePage = definePage({ - name: 'showcase_inquiry_triage', - label: 'Inquiry Triage (React)', - type: 'home', - kind: 'react', - source: ` -function Page() { - const adapter = useAdapter(); - const [tab, setTab] = React.useState('all'); - const [sel, setSel] = React.useState(null); - const [reload, setReload] = React.useState(0); - const [counts, setCounts] = React.useState({ all: 0, new: 0, contacted: 0, closed: 0 }); - - const refresh = React.useCallback(async () => { - if (!adapter) return; - const res = await adapter.find('showcase_inquiry', { top: 500 }); - const rows = Array.isArray(res) ? res : (res && res.records) || []; - setCounts({ - all: rows.length, - new: rows.filter((r) => r.status === 'new').length, - contacted: rows.filter((r) => r.status === 'contacted').length, - closed: rows.filter((r) => r.status === 'closed').length, - }); - }, [adapter]); - React.useEffect(() => { refresh(); }, [refresh, reload]); - - const setStatus = async (status) => { - if (!adapter || !sel) return; - await adapter.update('showcase_inquiry', sel.id, { status }); - setSel(null); - setReload((k) => k + 1); - }; - - const TABS = [['all', 'All'], ['new', 'New'], ['contacted', 'Contacted'], ['closed', 'Closed']]; - const filters = tab === 'all' ? undefined : ['status', '=', tab]; - const STATUS_COLOR = { new: 'hsl(217 91% 60%)', contacted: 'hsl(38 92% 50%)', closed: 'hsl(142 70% 45%)' }; - - const card = { background: 'hsl(var(--card))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)' }; - const tabBtn = (active) => ({ - display: 'flex', alignItems: 'center', gap: 8, borderRadius: 9999, padding: '6px 16px', fontSize: 14, fontWeight: 600, - border: '1px solid ' + (active ? 'transparent' : 'hsl(var(--border))'), cursor: 'pointer', - background: active ? 'hsl(var(--primary))' : 'transparent', - color: active ? 'hsl(var(--primary-foreground))' : 'hsl(var(--muted-foreground))', - }); - - return ( -
-
-

Inquiry Triage

-

A support queue over showcase_inquiry — tabs filter a real <ListView>; one click moves an inquiry's status.

-
- -
- {TABS.map(([k, label]) => ( - - ))} -
- -
-
- setSel(r)} /> -
-
- {sel ? ( -
-
-
-

{sel.name}

- {sel.status} -
-

{sel.company} · {sel.email}

-
-

{sel.message || 'No message.'}

-
- - -
-
- ) : ( -
-
📥
-

Select an inquiry to triage.

-
- )} -
-
-
- ); -}`, -}); diff --git a/examples/app-showcase/src/pages/invoice-console.page.ts b/examples/app-showcase/src/pages/invoice-console.page.ts deleted file mode 100644 index 4f32faf0a7..0000000000 --- a/examples/app-showcase/src/pages/invoice-console.page.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { definePage } from '@objectstack/spec/ui'; - -/** - * Invoice Console — a `kind:'react'` business scenario (ADR-0081). - * - * Accounts-receivable workbench over `showcase_invoice`: aggregate KPIs, a - * status segmented control filtering a real ``, an `` - * editor, and a one-click "Mark Paid" collect action. - * - * Styling (ADR-0065): no Tailwind — inline `style={{}}` with `hsl(var(--token))`. - */ -export const InvoiceConsolePage = definePage({ - name: 'showcase_invoice_console', - label: 'Invoice Console (React)', - type: 'home', - kind: 'react', - source: ` -function Page() { - const adapter = useAdapter(); - const [status, setStatus] = React.useState('all'); - const [sel, setSel] = React.useState(null); - const [mode, setMode] = React.useState('edit'); - const [reload, setReload] = React.useState(0); - const [kpi, setKpi] = React.useState({ count: 0, draft: 0, sent: 0, paid: 0 }); - - React.useEffect(() => { - let alive = true; - (async () => { - if (!adapter) return; - const res = await adapter.find('showcase_invoice', { top: 500 }); - const rows = Array.isArray(res) ? res : (res && res.records) || []; - if (alive) setKpi({ - count: rows.length, - draft: rows.filter((r) => r.status === 'draft').length, - sent: rows.filter((r) => r.status === 'sent').length, - paid: rows.filter((r) => r.status === 'paid').length, - }); - })(); - return () => { alive = false; }; - }, [adapter, reload]); - - const afterWrite = () => { setSel(null); setMode('edit'); setReload((k) => k + 1); }; - const markPaid = async () => { if (!adapter || !sel) return; await adapter.update('showcase_invoice', sel.id, { status: 'paid' }); afterWrite(); }; - const openNew = () => { setSel(null); setMode('create'); }; - - const FILTERS = [['all', 'All'], ['draft', 'Draft'], ['sent', 'Sent'], ['paid', 'Paid'], ['void', 'Void']]; - const filters = status === 'all' ? undefined : ['status', '=', status]; - const editing = mode === 'create' || sel; - - const card = { background: 'hsl(var(--card))', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)' }; - const eyebrow = { fontSize: 12, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em', color: 'hsl(var(--muted-foreground))' }; - const Kpi = ({ label, value, accent }) => ( -
-
{label}
-
{value}
-
- ); - const pill = (active) => ({ - borderRadius: 9999, padding: '4px 14px', fontSize: 14, fontWeight: 600, cursor: 'pointer', - border: '1px solid ' + (active ? 'transparent' : 'hsl(var(--border))'), - background: active ? 'hsl(var(--primary))' : 'transparent', - color: active ? 'hsl(var(--primary-foreground))' : 'hsl(var(--muted-foreground))', - }); - - return ( -
-
-
-

Invoice Console

-

Accounts receivable over showcase_invoice — aggregate, filter, edit, and collect.

-
- -
- -
- - - - -
- -
- {FILTERS.map(([k, label]) => ( - - ))} -
- -
-
- { setSel(r); setMode('edit'); }} /> -
-
- {editing ? ( - - setSel(null)} /> - {mode === 'edit' && sel && sel.status !== 'paid' ? ( - - ) : null} - - ) : ( -
-
🧾
-

Select an invoice, or create one.

-
- )} -
-
-
- ); -}`, -}); diff --git a/examples/app-showcase/src/pages/start-here.page.ts b/examples/app-showcase/src/pages/start-here.page.ts new file mode 100644 index 0000000000..8f687d594f --- /dev/null +++ b/examples/app-showcase/src/pages/start-here.page.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { definePage } from '@objectstack/spec/ui'; + +/** + * Start Here — the showcase's teaching index and default landing. + * + * A page has TWO orthogonal axes: + * • type — the surface ROLE: home · record · list · app + * • kind — the authoring MODEL: full/slotted · html · react + * + * This page teaches the `kind` decision (the capability that keeps iterating) and + * links to the canonical example of each. It is itself a `kind:'html'` page — + * constrained JSX composed of registered components + safe HTML, parsed-never- + * executed (ADR-0080), styled with inline `style` + theme tokens (ADR-0065), no + * Tailwind. So it doubles as the second HTML-tier example. + */ +export const StartHerePage = definePage({ + name: 'showcase_start_here', + label: 'Start Here', + type: 'home', + kind: 'html', + isDefault: true, + source: ` + + + +
ObjectStack Showcase
+
Pick the right page authoring model
+
Every page has two axes — its type (the surface role: home · record · list · app) and its kind (how you author it). Choose the simplest kind that expresses what the page needs.
+
+ + +
Decision tree
+ +
1
+
Is it a standard record / list / home you can compose from the component catalogue? → use full / slotted (declarative regions, no code).
+
+ +
2
+
Need free-form custom layout, but no interactivity? → use html (constrained JSX, parsed-never-executed, safe).
+
+ +
3
+
Need real interactivity — cross-component state, master/detail, drawers, live filters? → use react (real React, executed; trusted tier).
+
+
+ +
+ + +
full / slotted
+
Structured regions
+
Compose catalogue components into named regions/slots. No code. The default for record, list, and home layouts.
+
→ Component Gallery
+
+ + +
styling
+
Scoped style-objects (ADR-0065)
+
Style any block with a per-breakpoint style object compiled to scoped CSS — never Tailwind classes in metadata.
+
→ Styling Gallery
+
+ + +
html
+
Composed, no JS
+
Free-form layout as constrained JSX text — registered components + safe HTML, parsed-never-executed. Safe, OSS default.
+
→ Command Center (HTML)
+
+ + +
react
+
Interactive (executed)
+
Real React — hooks, handlers, live useAdapter queries — composing the platform's data blocks into stateful business UIs.
+
→ CRM Workbench
+
+ +
+
`, +});