@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.
@@ -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
+ }