diff --git a/examples/playground/src/layouts/dashboard.tsx b/examples/playground/src/layouts/dashboard.tsx
new file mode 100644
index 0000000..e2d02cb
--- /dev/null
+++ b/examples/playground/src/layouts/dashboard.tsx
@@ -0,0 +1,24 @@
+/**
+ * dashboard 布局的真实渲染骨架 —— 数据看板
+ * 槽位:stats(顶部指标条)/ content(主内容)
+ */
+import React from 'react'
+import type { LayoutRenderProps } from 'vibe-registry'
+
+export function DashboardLayout({ slots, blocks, page }: LayoutRenderProps): React.ReactElement {
+ const stats = (blocks['stats'] as React.ReactNode[] | undefined) ?? []
+ const content = (blocks['content'] as React.ReactNode[] | undefined) ?? []
+
+ return (
+
+
+ {page.title ?? 'Dashboard'}
+ layout: {slots.join(' + ')}
+
+
+
+ )
+}
diff --git a/examples/playground/src/main.tsx b/examples/playground/src/main.tsx
index 285f3ca..09db5ed 100644
--- a/examples/playground/src/main.tsx
+++ b/examples/playground/src/main.tsx
@@ -1,6 +1,5 @@
/**
- * playground 入口:画廊 + 真实页面渲染(类 Vite 首页体验)
- * 演示:import 即注册 → setup 业务 → PageView 渲染
+ * playground 入口:模板画廊(预览即真实)+ 业务实装示例
*/
import React from 'react'
import { createRoot } from 'react-dom/client'
@@ -28,21 +27,26 @@ function App(): React.ReactElement {
vibe-registry playground
- import 即注册 · 页面只管业务 · 预览即真实
+ 布局模板 · 引入即注册 · 页面只管业务 · 预览即真实
+ {/* ① 模板画廊:每卡真实渲染,点选看完整预览 */}
+
+
+ {/* ② 业务实装:同一布局,页面只管业务(订单列表) */}
- 真实渲染:订单列表页(layout: admin)
+ 业务实装示例:订单列表页(复用 admin 布局)
+
+ 页面只声明 layout:'admin' + blocks;数据来自 setup —— 与画廊里的 demo 用同一渲染内核
+
{data ? (
) : (
setup 加载中…
)}
-
-
)
}
diff --git a/examples/playground/src/registry.ts b/examples/playground/src/registry.ts
index 2b8a156..740eedd 100644
--- a/examples/playground/src/registry.ts
+++ b/examples/playground/src/registry.ts
@@ -4,21 +4,103 @@
*/
import { registerLayout, registerWidget } from 'vibe-registry'
import { AdminLayout } from './layouts/admin.tsx'
-import { PageHeader, DataTable, NavMenu } from './widgets.tsx'
+import { DashboardLayout } from './layouts/dashboard.tsx'
+import { PageHeader, DataTable, NavMenu, StatGrid } from './widgets.tsx'
+
+// ── 布局注册(带真实渲染骨架 + demo 预览数据)──────────────────
-// ── 布局注册(带真实渲染骨架)──────────────────────────────
registerLayout({
kind: 'layout',
id: 'admin',
slots: ['sidebar', 'header', 'content', 'footer'],
- description: '后台管理骨架',
+ description: '后台管理骨架(侧栏+顶栏+内容)',
render: AdminLayout,
preview: {
- title: 'Admin · 后台管理',
+ title: 'Admin 后台管理',
demoBlocks: [
- { slot: 'sidebar', widget: 'nav-menu', props: { items: [{ label: '订单', active: true }, { label: '商品' }] } },
- { slot: 'header', widget: 'page-header', props: { title: '订单管理', subtitle: '全部订单' } },
- { slot: 'content', widget: 'data-table' },
+ { slot: 'sidebar', widget: 'nav-menu', props: { items: [{ label: '订单', active: true }, { label: '商品' }, { label: '客户' }] } },
+ { slot: 'header', widget: 'page-header', props: { title: '订单管理', subtitle: '近 30 天全部订单' } },
+ {
+ slot: 'content',
+ widget: 'data-table',
+ props: {
+ columns: [
+ { key: 'id', title: '订单号' },
+ { key: 'customer', title: '客户' },
+ { key: 'amount', title: '金额' },
+ { key: 'status', title: '状态' },
+ ],
+ rows: [
+ { id: 'A1001', customer: '张三', amount: '¥1,299', status: '已支付' },
+ { id: 'A1002', customer: '李四', amount: '¥88', status: '待发货' },
+ ],
+ },
+ },
+ ],
+ },
+})
+
+registerLayout({
+ kind: 'layout',
+ id: 'dashboard',
+ slots: ['stats', 'content'],
+ description: '数据看板(顶部指标 + 主内容)',
+ render: DashboardLayout,
+ preview: {
+ title: 'Dashboard 数据看板',
+ demoBlocks: [
+ {
+ slot: 'stats',
+ widget: 'stat-grid',
+ props: {
+ stats: [
+ { label: '今日订单', value: 128, delta: '+12%' },
+ { label: '销售额', value: '¥45,200', delta: '+8%' },
+ { label: '失败率', value: '1.2%', delta: '-0.3%' },
+ ],
+ },
+ },
+ {
+ slot: 'content',
+ widget: 'data-table',
+ props: {
+ columns: [
+ { key: 'name', title: '指标' },
+ { key: 'value', title: '数值' },
+ ],
+ rows: [
+ { name: '订单转化率', value: '3.4%' },
+ { name: '客单价', value: '¥353' },
+ ],
+ },
+ },
+ ],
+ },
+})
+
+registerLayout({
+ kind: 'layout',
+ id: 'app-shell',
+ slots: ['header', 'content'],
+ description: '通用外壳(顶栏+内容),无自定义 render → 走缺省兜底',
+ preview: {
+ title: 'App Shell 通用外壳',
+ demoBlocks: [
+ { slot: 'header', widget: 'page-header', props: { title: '欢迎使用', subtitle: '无 render 的布局由 DefaultLayout 兜底' } },
+ {
+ slot: 'content',
+ widget: 'data-table',
+ props: {
+ columns: [
+ { key: 'k', title: '项' },
+ { key: 'v', title: '值' },
+ ],
+ rows: [
+ { k: '布局', v: 'app-shell' },
+ { k: '渲染', v: 'DefaultLayout 兜底' },
+ ],
+ },
+ },
],
},
})
@@ -42,3 +124,9 @@ registerWidget({
component: DataTable,
description: '通用数据表格(列定义来自声明 props)',
})
+registerWidget({
+ kind: 'widget',
+ id: 'stat-grid',
+ component: StatGrid,
+ description: '指标格栅(仪表盘统计)',
+})
diff --git a/examples/playground/src/widgets.tsx b/examples/playground/src/widgets.tsx
index 93a0347..30284d4 100644
--- a/examples/playground/src/widgets.tsx
+++ b/examples/playground/src/widgets.tsx
@@ -70,3 +70,33 @@ export function NavMenu({ items = [] }: { items?: Array<{ label: string; active?
),
)
}
+
+/** 统计卡片(仪表盘用) */
+export function StatCard({
+ label,
+ value,
+ delta,
+}: {
+ label?: string
+ value?: unknown
+ delta?: string
+}): React.ReactElement {
+ return React.createElement(
+ 'div',
+ { style: { border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, background: '#fff' } },
+ React.createElement('div', { style: { fontSize: 12, color: '#6b7280' } }, label ?? ''),
+ React.createElement('div', { style: { fontSize: 24, fontWeight: 700, margin: '4px 0' } }, String(value ?? '-')),
+ delta ? React.createElement('div', { style: { fontSize: 12, color: delta.startsWith('-') ? '#dc2626' : '#16a34a' } }, delta) : null,
+ )
+}
+
+/** 指标格子容器(dashboard 布局的内容区 demo 用) */
+export function StatGrid({ stats = [] }: { stats?: Array<{ label?: string; value?: unknown; delta?: string }> }): React.ReactElement {
+ return React.createElement(
+ 'div',
+ { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12 } },
+ ...(stats as Array<{ label?: string; value?: unknown; delta?: string }>).map((s, i) =>
+ React.createElement(StatCard, { key: i, label: s.label, value: s.value, delta: s.delta }),
+ ),
+ )
+}
diff --git a/src/render.ts b/src/render.ts
index 47439e1..6f52819 100644
--- a/src/render.ts
+++ b/src/render.ts
@@ -99,51 +99,109 @@ export function DefaultLayout({ slots, blocks }: LayoutRenderProps): React.React
)
}
-/* ── 画廊:列出全部注册布局 + live 预览(类 Vite 首页)────────── */
+/* ── 画廊:布局模板 live 预览(类 Vite 首页,点选查看) ─────────── */
export interface GalleryProps {
readonly registry: RegistryApi
readonly pages?: readonly PageDeclaration[]
}
-function PreviewShell({ title, children }: { title: string; children?: React.ReactNode }): React.ReactElement {
+/** 由布局的 preview.demoBlocks 构造一个可渲染的 demo 页声明 */
+function demoPageFrom(layout: LayoutRegistryEntry): PageDeclaration | null {
+ const demo = layout.preview?.demoBlocks ?? []
+ if (demo.length === 0) return null
+ return {
+ layout: layout.id,
+ title: layout.preview?.title ?? layout.id,
+ blocks: demo.map((d) => ({ widget: d.widget, slot: d.slot, props: d.props })),
+ }
+}
+
+/** 缩略预览:把真实渲染缩放进卡片(预览即真实) */
+function Thumb({ children, onClick }: { children?: React.ReactNode; onClick?: () => void }): React.ReactElement {
return React.createElement(
'div',
- { style: { border: '1px solid #e5e7eb', borderRadius: 8, padding: 12, background: '#fff' } },
- React.createElement('h3', { style: { margin: '0 0 8px', fontSize: 14 } }, title),
- React.createElement('div', { style: { fontSize: 12, color: '#6b7280' } }, children),
+ {
+ onClick,
+ style: {
+ width: 420,
+ height: 260,
+ overflow: 'hidden',
+ border: '1px solid #e5e7eb',
+ borderRadius: 8,
+ cursor: onClick ? 'pointer' : 'default',
+ position: 'relative',
+ background: '#fff',
+ },
+ },
+ React.createElement(
+ 'div',
+ { style: { width: 1050, transform: 'scale(0.4)', transformOrigin: 'top left' } },
+ children,
+ ),
)
}
export function LayoutGallery({ registry }: GalleryProps): React.ReactElement {
const layouts = registry.listAll().filter((e): e is LayoutRegistryEntry => e.kind === 'layout')
const widgets = registry.listAll().filter((e) => e.kind === 'widget')
- const items = layouts.map((layout) => {
- const demo = layout.preview?.demoBlocks ?? []
+ const [selected, setSelected] = React.useState(null)
+
+ const cards = layouts.map((layout) => {
+ const page = demoPageFrom(layout)
return React.createElement(
- PreviewShell,
- { key: layout.id, title: `${layout.preview?.title ?? layout.id} (${layout.id})` },
+ 'div',
+ { key: layout.id, style: { display: 'flex', flexDirection: 'column', gap: 6 } },
+ React.createElement(
+ Thumb,
+ { onClick: () => setSelected(layout) },
+ page
+ ? React.createElement(PageView, { page, registry, data: {} })
+ : React.createElement('div', { style: { padding: 12, color: '#9ca3af' } }, '(无 demoBlocks)'),
+ ),
React.createElement(
'div',
- {},
- React.createElement('div', {}, `slots: ${layout.slots.join(' → ')}`),
- demo.length
- ? React.createElement(
- 'ul',
- {},
- demo.map((d, i) =>
- React.createElement('li', { key: i }, `${d.slot} ← widget:${d.widget} ${d.props ? JSON.stringify(d.props) : ''}`),
- ),
- )
- : React.createElement('div', {}, '(无 demo)'),
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
+ React.createElement(
+ 'div',
+ {},
+ React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, layout.preview?.title ?? layout.id),
+ React.createElement('div', { style: { fontSize: 11, color: '#9ca3af' } }, `id: ${layout.id} · slots: ${layout.slots.join(' → ')}`),
+ ),
+ React.createElement(
+ 'button',
+ {
+ onClick: () => setSelected(layout),
+ style: { fontSize: 12, padding: '3px 10px', borderRadius: 6, border: '1px solid #d1d5db', background: '#fff', cursor: 'pointer' },
+ },
+ '预览',
+ ),
),
)
})
+
+ const detail = selected ? demoPageFrom(selected) : null
+ const detailEl = selected
+ ? React.createElement(
+ 'div',
+ { style: { marginTop: 20 } },
+ React.createElement('h3', {}, `${selected.preview?.title ?? selected.id} · 完整预览`),
+ detail
+ ? React.createElement(
+ 'div',
+ { style: { border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' } },
+ React.createElement(PageView, { page: detail, registry, data: {} }),
+ )
+ : React.createElement('div', {}, '(此布局无 demo)'),
+ )
+ : null
+
return React.createElement(
'div',
- { style: { fontFamily: 'system-ui, sans-serif', maxWidth: 720, margin: '0 auto', padding: 24 } },
- React.createElement('h1', { style: { fontSize: 20 } }, 'vibe-registry · 布局画廊'),
- React.createElement('p', { style: { color: '#6b7280' } }, `已注册:${layouts.length} 布局 · ${widgets.length} widget`),
- React.createElement('div', { style: { display: 'grid', gap: 12 } }, ...items),
+ { style: { fontFamily: 'system-ui, sans-serif', maxWidth: 960, margin: '0 auto', padding: 24 } },
+ React.createElement('h1', { style: { fontSize: 20 } }, 'vibe-registry · 布局模板'),
+ React.createElement('p', { style: { color: '#6b7280' } }, `已注册 ${layouts.length} 个布局模板 · ${widgets.length} 个 widget · 点击卡片预览(预览即真实)`),
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(430px, 1fr))', gap: 16 } }, ...cards),
+ detailEl,
)
}