@asteby/metacore-runtime-react 18.16.1 → 18.17.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 +30 -0
- package/dist/dashboard-grid.d.ts +6 -0
- package/dist/dashboard-grid.d.ts.map +1 -0
- package/dist/dashboard-grid.js +126 -0
- package/dist/dashboard-types.d.ts +130 -0
- package/dist/dashboard-types.d.ts.map +1 -0
- package/dist/dashboard-types.js +7 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/widgets/renderers.d.ts +19 -0
- package/dist/widgets/renderers.d.ts.map +1 -0
- package/dist/widgets/renderers.js +83 -0
- package/dist/widgets/widget-card.d.ts +34 -0
- package/dist/widgets/widget-card.d.ts.map +1 -0
- package/dist/widgets/widget-card.js +30 -0
- package/dist/widgets/widget-format.d.ts +42 -0
- package/dist/widgets/widget-format.d.ts.map +1 -0
- package/dist/widgets/widget-format.js +138 -0
- package/dist/widgets/widget-renderer.d.ts +34 -0
- package/dist/widgets/widget-renderer.d.ts.map +1 -0
- package/dist/widgets/widget-renderer.js +83 -0
- package/package.json +2 -1
- package/src/__tests__/dashboard-grid.test.tsx +222 -0
- package/src/dashboard-grid.tsx +205 -0
- package/src/dashboard-types.ts +178 -0
- package/src/index.ts +56 -0
- package/src/widgets/renderers.tsx +351 -0
- package/src/widgets/widget-card.tsx +125 -0
- package/src/widgets/widget-format.ts +181 -0
- package/src/widgets/widget-renderer.tsx +200 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Per-widget dispatcher: maps a spec.kind to its built-in renderer (or a
|
|
2
|
+
// federated <Slot> for kind:"custom"), wrapped in an error boundary so a single
|
|
3
|
+
// broken widget renders its own error card instead of tumbling the grid.
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { Slot } from '../slot'
|
|
7
|
+
import type { DashboardWidgetSpec, WidgetData, WidgetSize } from '../dashboard-types'
|
|
8
|
+
import { WidgetCard, WidgetError } from './widget-card'
|
|
9
|
+
import {
|
|
10
|
+
StatWidget,
|
|
11
|
+
BarWidget,
|
|
12
|
+
LineWidget,
|
|
13
|
+
AreaWidget,
|
|
14
|
+
PieWidget,
|
|
15
|
+
DonutWidget,
|
|
16
|
+
ListWidget,
|
|
17
|
+
ProgressWidget,
|
|
18
|
+
type WidgetRenderProps,
|
|
19
|
+
} from './renderers'
|
|
20
|
+
|
|
21
|
+
/** Maps a widget size to its column span in the 4-col grid. */
|
|
22
|
+
export const SIZE_SPAN: Record<WidgetSize, number> = {
|
|
23
|
+
sm: 1,
|
|
24
|
+
md: 2,
|
|
25
|
+
lg: 3,
|
|
26
|
+
full: 4,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Tailwind col-span class per size (static → scanned in this package build).
|
|
30
|
+
* Grid is 2 cols on mobile, 4 on lg: sm=quarter, md=half, lg=¾, full=row. */
|
|
31
|
+
export const SIZE_CLASS: Record<WidgetSize, string> = {
|
|
32
|
+
sm: 'col-span-1 lg:col-span-1',
|
|
33
|
+
md: 'col-span-2 lg:col-span-2',
|
|
34
|
+
lg: 'col-span-2 lg:col-span-3',
|
|
35
|
+
full: 'col-span-2 lg:col-span-4',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Kinds that render a chart/list and want extra vertical room (2 grid rows).
|
|
39
|
+
* Stat/progress stay a single compact row so KPIs read like KPIs, not slabs. */
|
|
40
|
+
const TALL_KINDS = new Set(['bar', 'line', 'area', 'pie', 'donut', 'list', 'custom'])
|
|
41
|
+
|
|
42
|
+
/** Default footprint when a spec omits `size`: charts go half-width, stats a
|
|
43
|
+
* quarter — so a tablero of mixed widgets packs densely without manual sizing. */
|
|
44
|
+
export function defaultSize(spec: DashboardWidgetSpec): WidgetSize {
|
|
45
|
+
if (spec.size) return spec.size
|
|
46
|
+
return TALL_KINDS.has(spec.kind) ? 'md' : 'sm'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Combined col + row span for a widget's grid cell. Row-span drives the
|
|
50
|
+
* height contrast (chart=2, stat=1) that makes the layout feel designed. */
|
|
51
|
+
export function spanClass(spec: DashboardWidgetSpec): string {
|
|
52
|
+
const col = SIZE_CLASS[defaultSize(spec)]
|
|
53
|
+
return `${col} ${TALL_KINDS.has(spec.kind) ? 'row-span-2' : 'row-span-1'}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const RENDERERS: Record<
|
|
57
|
+
string,
|
|
58
|
+
(p: WidgetRenderProps) => React.ReactElement
|
|
59
|
+
> = {
|
|
60
|
+
stat: StatWidget,
|
|
61
|
+
bar: BarWidget,
|
|
62
|
+
line: LineWidget,
|
|
63
|
+
area: AreaWidget,
|
|
64
|
+
pie: PieWidget,
|
|
65
|
+
donut: DonutWidget,
|
|
66
|
+
list: ListWidget,
|
|
67
|
+
progress: ProgressWidget,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface BoundaryProps {
|
|
71
|
+
spec: DashboardWidgetSpec
|
|
72
|
+
message: string
|
|
73
|
+
children: React.ReactNode
|
|
74
|
+
}
|
|
75
|
+
interface BoundaryState {
|
|
76
|
+
error: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Isolated boundary: a throwing widget renders its own error card. */
|
|
80
|
+
class WidgetErrorBoundary extends React.Component<BoundaryProps, BoundaryState> {
|
|
81
|
+
state: BoundaryState = { error: false }
|
|
82
|
+
static getDerivedStateFromError(): BoundaryState {
|
|
83
|
+
return { error: true }
|
|
84
|
+
}
|
|
85
|
+
render() {
|
|
86
|
+
if (this.state.error) {
|
|
87
|
+
return (
|
|
88
|
+
<WidgetCard
|
|
89
|
+
data-testid={`widget-${this.props.spec.key}`}
|
|
90
|
+
title={this.props.spec.title}
|
|
91
|
+
subtitle={this.props.spec.subtitle}
|
|
92
|
+
icon={this.props.spec.icon}
|
|
93
|
+
accent={this.props.spec.accent}
|
|
94
|
+
>
|
|
95
|
+
<WidgetError message={this.props.message} />
|
|
96
|
+
</WidgetCard>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
return this.props.children
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface WidgetRendererProps {
|
|
104
|
+
spec: DashboardWidgetSpec
|
|
105
|
+
data?: WidgetData
|
|
106
|
+
locale?: string
|
|
107
|
+
currency?: string
|
|
108
|
+
/** Translated empty fallback for this widget. */
|
|
109
|
+
emptyText: string
|
|
110
|
+
/** Translated error message for this widget. */
|
|
111
|
+
errorText: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Renders one widget. `kind:"custom"` defers to the federated slot
|
|
116
|
+
* (`spec.slot ?? 'dashboard.widgets'`) inside the same card chrome so it
|
|
117
|
+
* combines with the declarative widgets.
|
|
118
|
+
*/
|
|
119
|
+
export function WidgetRenderer({
|
|
120
|
+
spec,
|
|
121
|
+
data,
|
|
122
|
+
locale,
|
|
123
|
+
currency,
|
|
124
|
+
emptyText,
|
|
125
|
+
errorText,
|
|
126
|
+
}: WidgetRendererProps) {
|
|
127
|
+
let body: React.ReactNode
|
|
128
|
+
if (spec.kind === 'custom') {
|
|
129
|
+
body = (
|
|
130
|
+
<WidgetCard
|
|
131
|
+
data-testid={`widget-${spec.key}`}
|
|
132
|
+
title={spec.title}
|
|
133
|
+
subtitle={spec.subtitle}
|
|
134
|
+
icon={spec.icon}
|
|
135
|
+
accent={spec.accent}
|
|
136
|
+
>
|
|
137
|
+
<Slot
|
|
138
|
+
name={spec.slot ?? 'dashboard.widgets'}
|
|
139
|
+
props={{ spec, data, locale, currency }}
|
|
140
|
+
fallback={
|
|
141
|
+
<div className="flex flex-1 items-center justify-center py-6 text-xs text-muted-foreground">
|
|
142
|
+
{emptyText}
|
|
143
|
+
</div>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
</WidgetCard>
|
|
147
|
+
)
|
|
148
|
+
} else {
|
|
149
|
+
const Renderer = RENDERERS[spec.kind]
|
|
150
|
+
body = Renderer ? (
|
|
151
|
+
<Renderer
|
|
152
|
+
spec={spec}
|
|
153
|
+
data={data}
|
|
154
|
+
locale={locale}
|
|
155
|
+
currency={currency}
|
|
156
|
+
emptyText={emptyText}
|
|
157
|
+
/>
|
|
158
|
+
) : (
|
|
159
|
+
<WidgetCard
|
|
160
|
+
data-testid={`widget-${spec.key}`}
|
|
161
|
+
title={spec.title}
|
|
162
|
+
subtitle={spec.subtitle}
|
|
163
|
+
icon={spec.icon}
|
|
164
|
+
accent={spec.accent}
|
|
165
|
+
>
|
|
166
|
+
<WidgetError message={errorText} />
|
|
167
|
+
</WidgetCard>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<WidgetErrorBoundary spec={spec} message={errorText}>
|
|
173
|
+
{body}
|
|
174
|
+
</WidgetErrorBoundary>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Skeleton placeholder shown per widget while data loads. */
|
|
179
|
+
export function WidgetSkeleton({ spec }: { spec: DashboardWidgetSpec }) {
|
|
180
|
+
const isChart =
|
|
181
|
+
spec.kind !== 'stat' && spec.kind !== 'progress' && spec.kind !== 'custom'
|
|
182
|
+
return (
|
|
183
|
+
<WidgetCard
|
|
184
|
+
data-testid={`widget-skeleton-${spec.key}`}
|
|
185
|
+
title={spec.title}
|
|
186
|
+
subtitle={spec.subtitle}
|
|
187
|
+
icon={spec.icon}
|
|
188
|
+
accent={spec.accent}
|
|
189
|
+
>
|
|
190
|
+
{isChart ? (
|
|
191
|
+
<div className="h-[132px] w-full animate-pulse rounded-md bg-muted" />
|
|
192
|
+
) : (
|
|
193
|
+
<div className="flex flex-col gap-2">
|
|
194
|
+
<div className="h-8 w-2/3 animate-pulse rounded bg-muted" />
|
|
195
|
+
<div className="h-2 w-full animate-pulse rounded bg-muted/60" />
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</WidgetCard>
|
|
199
|
+
)
|
|
200
|
+
}
|