@reopt-ai/opt-ui 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/COMPONENT_CATALOG.md +3000 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/chunk-3GWWZKX7.js +38 -0
- package/dist/chunk-AFF2HPE5.cjs +5008 -0
- package/dist/chunk-ELWICXYY.js +4745 -0
- package/dist/chunk-N4NDU5ET.cjs +4745 -0
- package/dist/chunk-ONE3C5RV.cjs +38 -0
- package/dist/chunk-QWBHD54V.js +3218 -0
- package/dist/chunk-RBM2RNC2.js +5008 -0
- package/dist/chunk-X5WCXJAF.cjs +3218 -0
- package/dist/core/index.cjs +324 -0
- package/dist/core/index.d.cts +5 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +324 -0
- package/dist/docs/01-getting-started.md +129 -0
- package/dist/docs/02-components/01-core.md +1841 -0
- package/dist/docs/02-components/02-visuals.md +11 -0
- package/dist/docs/02-components/03-shells.md +1361 -0
- package/dist/docs/02-components/04-surfaces.md +11 -0
- package/dist/docs/02-components/index.md +106 -0
- package/dist/docs/03-recipes/01-forms.md +471 -0
- package/dist/docs/03-recipes/02-dashboards.md +397 -0
- package/dist/docs/03-recipes/03-layouts.md +424 -0
- package/dist/docs/04-theming.md +232 -0
- package/dist/docs/05-migration/01-breaking-changes.md +117 -0
- package/dist/docs/05-migration/02-formstore.md +336 -0
- package/dist/docs/06-troubleshooting.md +119 -0
- package/dist/docs/index.md +71 -0
- package/dist/id-registry.cjs +1875 -0
- package/dist/id-registry.d.cts +27 -0
- package/dist/id-registry.d.ts +27 -0
- package/dist/id-registry.js +1875 -0
- package/dist/id-registry.json +3799 -0
- package/dist/index-BZ_lBlO1.d.ts +474 -0
- package/dist/index-BuvxoWHf.d.cts +474 -0
- package/dist/index-DlAcuvQp.d.cts +1686 -0
- package/dist/index-Slu5hOj1.d.ts +1686 -0
- package/dist/index.cjs +9959 -0
- package/dist/index.d.cts +2351 -0
- package/dist/index.d.ts +2351 -0
- package/dist/index.js +9959 -0
- package/dist/meta.cjs +6898 -0
- package/dist/meta.d.cts +36 -0
- package/dist/meta.d.ts +36 -0
- package/dist/meta.js +6869 -0
- package/dist/shells/index.cjs +65 -0
- package/dist/shells/index.d.cts +5 -0
- package/dist/shells/index.d.ts +5 -0
- package/dist/shells/index.js +65 -0
- package/dist/tailwind.css +401 -0
- package/dist/theme/presets/default.css +355 -0
- package/dist/theme/presets/minimal.css +354 -0
- package/dist/theme/presets/mono-dark.css +354 -0
- package/dist/theme/presets/natural.css +181 -0
- package/dist/theme/presets/pro.css +354 -0
- package/dist/types-D4-0lwaE.d.cts +298 -0
- package/dist/types-D4-0lwaE.d.ts +298 -0
- package/dist/visuals/index.cjs +4 -0
- package/dist/visuals/index.d.cts +1 -0
- package/dist/visuals/index.d.ts +1 -0
- package/dist/visuals/index.js +4 -0
- package/package.json +165 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Dashboard Recipes"
|
|
3
|
+
description: "opt-ui 대시보드 조합 패턴 3종 — KPI, Analytics, Activity"
|
|
4
|
+
related:
|
|
5
|
+
links:
|
|
6
|
+
- ../02-components/02-visuals.md
|
|
7
|
+
- ../02-components/03-shells.md
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<!-- AI agent hint: 차트 컴포넌트는 반드시 ChartContainer로 감쌀 것. ChartDataPoint[]의 name 필드는 필수. -->
|
|
11
|
+
|
|
12
|
+
# Dashboard Recipes
|
|
13
|
+
|
|
14
|
+
Copy-paste-ready dashboard composition patterns.
|
|
15
|
+
All imports from `@reopt-ai/opt-ui`.
|
|
16
|
+
|
|
17
|
+
## 1. KPI Dashboard
|
|
18
|
+
|
|
19
|
+
SummaryRow for top-level stats, TrendChart for time series, DataTable for details.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
"use client";
|
|
23
|
+
|
|
24
|
+
import { useState } from "react";
|
|
25
|
+
import {
|
|
26
|
+
SurfaceLayout,
|
|
27
|
+
PageHeader,
|
|
28
|
+
SummaryRow,
|
|
29
|
+
TrendChart,
|
|
30
|
+
DataTable,
|
|
31
|
+
Button,
|
|
32
|
+
type StatCard as StatCardType,
|
|
33
|
+
type ChartDataPoint,
|
|
34
|
+
type ChartSeriesDef,
|
|
35
|
+
type ColumnDef,
|
|
36
|
+
} from "@reopt-ai/opt-ui";
|
|
37
|
+
|
|
38
|
+
interface SalesRow {
|
|
39
|
+
id: string;
|
|
40
|
+
product: string;
|
|
41
|
+
revenue: number;
|
|
42
|
+
units: number;
|
|
43
|
+
growth: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stats: StatCardType[] = [
|
|
47
|
+
{
|
|
48
|
+
id: "revenue",
|
|
49
|
+
title: "매출",
|
|
50
|
+
value: "₩45.2M",
|
|
51
|
+
change: "+12.5%",
|
|
52
|
+
trend: "up",
|
|
53
|
+
},
|
|
54
|
+
{ id: "orders", title: "주문", value: "1,234", change: "+8.2%", trend: "up" },
|
|
55
|
+
{
|
|
56
|
+
id: "aov",
|
|
57
|
+
title: "객단가",
|
|
58
|
+
value: "₩36,600",
|
|
59
|
+
change: "-2.1%",
|
|
60
|
+
trend: "down",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "refund",
|
|
64
|
+
title: "환불률",
|
|
65
|
+
value: "3.2%",
|
|
66
|
+
change: "-0.5%",
|
|
67
|
+
trend: "up",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const trendData: ChartDataPoint[] = [
|
|
72
|
+
{ name: "1월", revenue: 32000, orders: 890 },
|
|
73
|
+
{ name: "2월", revenue: 35000, orders: 920 },
|
|
74
|
+
{ name: "3월", revenue: 38000, orders: 1010 },
|
|
75
|
+
{ name: "4월", revenue: 41000, orders: 1100 },
|
|
76
|
+
{ name: "5월", revenue: 45200, orders: 1234 },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const trendSeries: ChartSeriesDef[] = [
|
|
80
|
+
{ dataKey: "revenue", name: "매출" },
|
|
81
|
+
{ dataKey: "orders", name: "주문" },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const rows: SalesRow[] = [
|
|
85
|
+
{
|
|
86
|
+
id: "1",
|
|
87
|
+
product: "Pro Plan",
|
|
88
|
+
revenue: 18200000,
|
|
89
|
+
units: 340,
|
|
90
|
+
growth: "+15%",
|
|
91
|
+
},
|
|
92
|
+
{ id: "2", product: "Starter", revenue: 12500000, units: 520, growth: "+8%" },
|
|
93
|
+
{
|
|
94
|
+
id: "3",
|
|
95
|
+
product: "Enterprise",
|
|
96
|
+
revenue: 14500000,
|
|
97
|
+
units: 90,
|
|
98
|
+
growth: "+22%",
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const columns: ColumnDef<SalesRow>[] = [
|
|
103
|
+
{ id: "product", header: "상품", accessor: "product" },
|
|
104
|
+
{
|
|
105
|
+
id: "revenue",
|
|
106
|
+
header: "매출",
|
|
107
|
+
accessor: (r) => `₩${(r.revenue / 1e6).toFixed(1)}M`,
|
|
108
|
+
},
|
|
109
|
+
{ id: "units", header: "판매", accessor: "units", align: "right" },
|
|
110
|
+
{ id: "growth", header: "성장률", accessor: "growth", align: "right" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export function KpiDashboard() {
|
|
114
|
+
return (
|
|
115
|
+
<SurfaceLayout>
|
|
116
|
+
<PageHeader
|
|
117
|
+
title="매출 대시보드"
|
|
118
|
+
actions={<Button variant="secondary">리포트 다운로드</Button>}
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
<SummaryRow stats={stats} columns={4} />
|
|
122
|
+
|
|
123
|
+
<TrendChart
|
|
124
|
+
title="월별 추세"
|
|
125
|
+
description="매출 및 주문 수 추이"
|
|
126
|
+
data={trendData}
|
|
127
|
+
series={trendSeries}
|
|
128
|
+
height={280}
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
<DataTable
|
|
132
|
+
data={rows}
|
|
133
|
+
columns={columns}
|
|
134
|
+
keyExtractor={(r) => r.id}
|
|
135
|
+
title="상품별 실적"
|
|
136
|
+
sortable
|
|
137
|
+
/>
|
|
138
|
+
</SurfaceLayout>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 2. Analytics Overview
|
|
144
|
+
|
|
145
|
+
TimeRangeSelector for date filtering, LineChart and BarChart side by side, FilterBar for segmentation.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
"use client";
|
|
149
|
+
|
|
150
|
+
import { useState } from "react";
|
|
151
|
+
import {
|
|
152
|
+
SurfaceLayout,
|
|
153
|
+
PageHeader,
|
|
154
|
+
TimeRangeSelector,
|
|
155
|
+
ChartContainer,
|
|
156
|
+
LineChart,
|
|
157
|
+
BarChart,
|
|
158
|
+
FilterBar,
|
|
159
|
+
type ChartDataPoint,
|
|
160
|
+
type ChartSeriesDef,
|
|
161
|
+
type FilterGroupDef,
|
|
162
|
+
type DateRange,
|
|
163
|
+
} from "@reopt-ai/opt-ui";
|
|
164
|
+
|
|
165
|
+
const pageViewData: ChartDataPoint[] = [
|
|
166
|
+
{ name: "Mon", views: 1200, visitors: 800 },
|
|
167
|
+
{ name: "Tue", views: 1400, visitors: 950 },
|
|
168
|
+
{ name: "Wed", views: 1100, visitors: 750 },
|
|
169
|
+
{ name: "Thu", views: 1600, visitors: 1100 },
|
|
170
|
+
{ name: "Fri", views: 1800, visitors: 1250 },
|
|
171
|
+
{ name: "Sat", views: 900, visitors: 600 },
|
|
172
|
+
{ name: "Sun", views: 700, visitors: 450 },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const lineSeries: ChartSeriesDef[] = [
|
|
176
|
+
{ dataKey: "views", name: "페이지뷰" },
|
|
177
|
+
{ dataKey: "visitors", name: "방문자" },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const channelData: ChartDataPoint[] = [
|
|
181
|
+
{ name: "Organic", sessions: 4500 },
|
|
182
|
+
{ name: "Direct", sessions: 3200 },
|
|
183
|
+
{ name: "Social", sessions: 2100 },
|
|
184
|
+
{ name: "Referral", sessions: 1800 },
|
|
185
|
+
{ name: "Paid", sessions: 1200 },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const barSeries: ChartSeriesDef[] = [{ dataKey: "sessions", name: "세션" }];
|
|
189
|
+
|
|
190
|
+
const filters: FilterGroupDef[] = [
|
|
191
|
+
{
|
|
192
|
+
id: "channel",
|
|
193
|
+
label: "채널",
|
|
194
|
+
type: "select",
|
|
195
|
+
options: [
|
|
196
|
+
{ id: "all", label: "전체", value: "all" },
|
|
197
|
+
{ id: "organic", label: "Organic", value: "organic" },
|
|
198
|
+
{ id: "paid", label: "Paid", value: "paid" },
|
|
199
|
+
],
|
|
200
|
+
value: "all",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "device",
|
|
204
|
+
label: "디바이스",
|
|
205
|
+
type: "multi-select",
|
|
206
|
+
options: [
|
|
207
|
+
{ id: "desktop", label: "Desktop", value: "desktop" },
|
|
208
|
+
{ id: "mobile", label: "Mobile", value: "mobile" },
|
|
209
|
+
{ id: "tablet", label: "Tablet", value: "tablet" },
|
|
210
|
+
],
|
|
211
|
+
value: [],
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
export function AnalyticsOverview() {
|
|
216
|
+
const [timeRange, setTimeRange] = useState<DateRange>({
|
|
217
|
+
start: null,
|
|
218
|
+
end: null,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<SurfaceLayout>
|
|
223
|
+
<PageHeader
|
|
224
|
+
title="분석 개요"
|
|
225
|
+
description="트래픽 및 채널 분석"
|
|
226
|
+
actions={
|
|
227
|
+
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
<FilterBar
|
|
232
|
+
filters={filters}
|
|
233
|
+
onFilterChange={(id, value) => console.log(id, value)}
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<div className="gap-group grid grid-cols-1 lg:grid-cols-2">
|
|
237
|
+
<div className="gap-element flex flex-col">
|
|
238
|
+
<h3 className="text-text-primary text-sm font-medium">일별 트래픽</h3>
|
|
239
|
+
<ChartContainer height={280}>
|
|
240
|
+
<LineChart data={pageViewData} series={lineSeries} />
|
|
241
|
+
</ChartContainer>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div className="gap-element flex flex-col">
|
|
245
|
+
<h3 className="text-text-primary text-sm font-medium">채널별 세션</h3>
|
|
246
|
+
<ChartContainer height={280}>
|
|
247
|
+
<BarChart data={channelData} series={barSeries} />
|
|
248
|
+
</ChartContainer>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</SurfaceLayout>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## 3. Activity Dashboard
|
|
257
|
+
|
|
258
|
+
ActivityFeed for recent events, TaskList for action items, MetricsCard grid for quick stats.
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
"use client";
|
|
262
|
+
|
|
263
|
+
import {
|
|
264
|
+
SurfaceLayout,
|
|
265
|
+
PageHeader,
|
|
266
|
+
ActivityFeed,
|
|
267
|
+
TaskList,
|
|
268
|
+
MetricsCard,
|
|
269
|
+
Button,
|
|
270
|
+
type ActivityDef,
|
|
271
|
+
type TaskDef,
|
|
272
|
+
type TeamMemberDef,
|
|
273
|
+
} from "@reopt-ai/opt-ui";
|
|
274
|
+
|
|
275
|
+
const actor: TeamMemberDef = {
|
|
276
|
+
id: "u1",
|
|
277
|
+
name: "김개발",
|
|
278
|
+
email: "dev@example.com",
|
|
279
|
+
role: "Engineer",
|
|
280
|
+
status: "online",
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const activities: ActivityDef[] = [
|
|
284
|
+
{
|
|
285
|
+
id: "a1",
|
|
286
|
+
type: "deployed",
|
|
287
|
+
title: "v2.4.0 프로덕션 배포",
|
|
288
|
+
actor,
|
|
289
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "a2",
|
|
293
|
+
type: "merged",
|
|
294
|
+
title: "feat: 대시보드 리팩토링 PR 병합",
|
|
295
|
+
actor,
|
|
296
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 120).toISOString(),
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "a3",
|
|
300
|
+
type: "completed",
|
|
301
|
+
title: "API 마이그레이션 완료",
|
|
302
|
+
description: "REST → GraphQL 전환 작업",
|
|
303
|
+
actor,
|
|
304
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 300).toISOString(),
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const tasks: TaskDef[] = [
|
|
309
|
+
{
|
|
310
|
+
id: "t1",
|
|
311
|
+
title: "인증 토큰 만료 처리",
|
|
312
|
+
status: "in-progress",
|
|
313
|
+
priority: "high",
|
|
314
|
+
assignee: actor,
|
|
315
|
+
dueDate: "2026-04-05",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "t2",
|
|
319
|
+
title: "대시보드 성능 최적화",
|
|
320
|
+
status: "todo",
|
|
321
|
+
priority: "medium",
|
|
322
|
+
tags: ["performance"],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: "t3",
|
|
326
|
+
title: "E2E 테스트 추가",
|
|
327
|
+
description: "핵심 플로우 커버리지 확보",
|
|
328
|
+
status: "todo",
|
|
329
|
+
priority: "low",
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
export function ActivityDashboard() {
|
|
334
|
+
return (
|
|
335
|
+
<SurfaceLayout>
|
|
336
|
+
<PageHeader
|
|
337
|
+
title="활동 대시보드"
|
|
338
|
+
actions={<Button variant="secondary">전체 보기</Button>}
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
<div className="gap-group grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
|
342
|
+
<MetricsCard
|
|
343
|
+
title="오늘 배포"
|
|
344
|
+
value={3}
|
|
345
|
+
change="+2"
|
|
346
|
+
trend="up"
|
|
347
|
+
sparklineData={[1, 0, 2, 1, 3]}
|
|
348
|
+
/>
|
|
349
|
+
<MetricsCard
|
|
350
|
+
title="열린 PR"
|
|
351
|
+
value={7}
|
|
352
|
+
change="-1"
|
|
353
|
+
trend="down"
|
|
354
|
+
sparklineData={[9, 8, 10, 8, 7]}
|
|
355
|
+
/>
|
|
356
|
+
<MetricsCard
|
|
357
|
+
title="완료 태스크"
|
|
358
|
+
value={12}
|
|
359
|
+
change="+4"
|
|
360
|
+
trend="up"
|
|
361
|
+
sparklineData={[5, 6, 8, 10, 12]}
|
|
362
|
+
/>
|
|
363
|
+
<MetricsCard
|
|
364
|
+
title="미해결 이슈"
|
|
365
|
+
value={23}
|
|
366
|
+
trend="neutral"
|
|
367
|
+
sparklineData={[25, 24, 23, 24, 23]}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div className="gap-section grid grid-cols-1 lg:grid-cols-2">
|
|
372
|
+
<ActivityFeed activities={activities} maxItems={5} />
|
|
373
|
+
<TaskList
|
|
374
|
+
tasks={tasks}
|
|
375
|
+
onTaskToggle={(id) => console.log("toggle", id)}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
</SurfaceLayout>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Composition Notes
|
|
384
|
+
|
|
385
|
+
| Pattern | Key Components | Data Types |
|
|
386
|
+
| ------------------- | ---------------------------------------- | ---------------------------------------- |
|
|
387
|
+
| KPI stats row | `SummaryRow` | `StatCard[]` (aliased as `StatCardType`) |
|
|
388
|
+
| Trend visualization | `TrendChart` or `ChartContainer` + chart | `ChartDataPoint[]` + `ChartSeriesDef[]` |
|
|
389
|
+
| Tabular detail | `DataTable` | `ColumnDef<T>[]` + data array |
|
|
390
|
+
| Date filtering | `TimeRangeSelector` | `DateRange` |
|
|
391
|
+
| Segment filtering | `FilterBar` | `FilterGroupDef[]` |
|
|
392
|
+
| Quick metrics | `MetricsCard` | inline props + `sparklineData: number[]` |
|
|
393
|
+
| Activity timeline | `ActivityFeed` | `ActivityDef[]` |
|
|
394
|
+
| Task tracking | `TaskList` | `TaskDef[]` |
|
|
395
|
+
|
|
396
|
+
All page-level layouts use `SurfaceLayout` for consistent `gap-section` spacing.
|
|
397
|
+
Use `gap-group` for grid gaps and `gap-element` for tightly spaced items.
|