@fayz-ai/ui 0.9.0-next.1 → 0.10.0

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.
Files changed (156) hide show
  1. package/README.md +2 -2
  2. package/dist/analytics/AnalyticsCard.d.ts +10 -0
  3. package/dist/analytics/AnalyticsCard.d.ts.map +1 -0
  4. package/dist/analytics/AnalyticsControls.d.ts +18 -0
  5. package/dist/analytics/AnalyticsControls.d.ts.map +1 -0
  6. package/dist/analytics/AnalyticsGrid.d.ts +16 -0
  7. package/dist/analytics/AnalyticsGrid.d.ts.map +1 -0
  8. package/dist/analytics/context.d.ts +58 -0
  9. package/dist/analytics/context.d.ts.map +1 -0
  10. package/dist/analytics/derive.d.ts +81 -0
  11. package/dist/analytics/derive.d.ts.map +1 -0
  12. package/dist/analytics/index.d.ts +13 -0
  13. package/dist/analytics/index.d.ts.map +1 -0
  14. package/dist/analytics/range.d.ts +22 -0
  15. package/dist/analytics/range.d.ts.map +1 -0
  16. package/dist/{chunk-PVIB724L.js → chunk-22EVFMS3.js} +5 -3
  17. package/dist/chunk-22EVFMS3.js.map +1 -0
  18. package/dist/{chunk-G6OKZ7WL.js → chunk-6V5MGYXH.js} +13 -9
  19. package/dist/chunk-6V5MGYXH.js.map +1 -0
  20. package/dist/{chunk-KV3MHB6N.js → chunk-H63TOSQ7.js} +196 -53
  21. package/dist/chunk-H63TOSQ7.js.map +1 -0
  22. package/dist/{chunk-OUPJDGKE.js → chunk-I6CPKAOD.js} +134 -37
  23. package/dist/chunk-I6CPKAOD.js.map +1 -0
  24. package/dist/{chunk-YRVE4FOD.js → chunk-OKZ6WQB3.js} +3 -3
  25. package/dist/chunk-OKZ6WQB3.js.map +1 -0
  26. package/dist/{chunk-S5KNM4D2.js → chunk-SEIY2F5F.js} +235 -107
  27. package/dist/chunk-SEIY2F5F.js.map +1 -0
  28. package/dist/{chunk-75EFDLST.js → chunk-VAKWUUVC.js} +157 -6
  29. package/dist/chunk-VAKWUUVC.js.map +1 -0
  30. package/dist/dashboard/OnboardingWidget.d.ts +2 -14
  31. package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
  32. package/dist/dashboard/SidebarOnboarding.d.ts +28 -0
  33. package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -0
  34. package/dist/dashboard/index.d.ts +2 -0
  35. package/dist/dashboard/index.d.ts.map +1 -1
  36. package/dist/dashboard/index.js +4 -4
  37. package/dist/dashboard/types.d.ts +18 -0
  38. package/dist/dashboard/types.d.ts.map +1 -1
  39. package/dist/dashboard/useOnboardingProgress.d.ts +28 -0
  40. package/dist/dashboard/useOnboardingProgress.d.ts.map +1 -0
  41. package/dist/icons.js +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +960 -14
  45. package/dist/index.js.map +1 -1
  46. package/dist/layout/AppShell.d.ts +14 -1
  47. package/dist/layout/AppShell.d.ts.map +1 -1
  48. package/dist/layout/ModulePage.d.ts +11 -2
  49. package/dist/layout/ModulePage.d.ts.map +1 -1
  50. package/dist/layout/Sidebar.d.ts +8 -1
  51. package/dist/layout/Sidebar.d.ts.map +1 -1
  52. package/dist/layout/index.js +4 -4
  53. package/dist/lib/cn.d.ts.map +1 -0
  54. package/dist/preset.d.ts +3 -0
  55. package/dist/preset.d.ts.map +1 -0
  56. package/dist/primitives/data-table.d.ts +19 -4
  57. package/dist/primitives/data-table.d.ts.map +1 -1
  58. package/dist/primitives/date-picker.d.ts +4 -2
  59. package/dist/primitives/date-picker.d.ts.map +1 -1
  60. package/dist/primitives/filter-chip.d.ts +26 -0
  61. package/dist/primitives/filter-chip.d.ts.map +1 -0
  62. package/dist/primitives/header-tabs.d.ts +32 -0
  63. package/dist/primitives/header-tabs.d.ts.map +1 -0
  64. package/dist/primitives/hint.d.ts +25 -0
  65. package/dist/primitives/hint.d.ts.map +1 -0
  66. package/dist/primitives/index.d.ts +7 -0
  67. package/dist/primitives/index.d.ts.map +1 -1
  68. package/dist/primitives/index.js +4 -4
  69. package/dist/primitives/list-view.d.ts +14 -1
  70. package/dist/primitives/list-view.d.ts.map +1 -1
  71. package/dist/primitives/page-header.d.ts +23 -0
  72. package/dist/primitives/page-header.d.ts.map +1 -0
  73. package/dist/primitives/stat-tiles.d.ts +30 -0
  74. package/dist/primitives/stat-tiles.d.ts.map +1 -0
  75. package/dist/primitives/switch.d.ts +11 -0
  76. package/dist/primitives/switch.d.ts.map +1 -0
  77. package/dist/stores/index.d.ts +1 -0
  78. package/dist/stores/index.d.ts.map +1 -1
  79. package/dist/stores/page-header.store.d.ts +69 -0
  80. package/dist/stores/page-header.store.d.ts.map +1 -0
  81. package/package.json +4 -13
  82. package/dist/chunk-75EFDLST.js.map +0 -1
  83. package/dist/chunk-G6OKZ7WL.js.map +0 -1
  84. package/dist/chunk-KV3MHB6N.js.map +0 -1
  85. package/dist/chunk-OUPJDGKE.js.map +0 -1
  86. package/dist/chunk-PVIB724L.js.map +0 -1
  87. package/dist/chunk-S5KNM4D2.js.map +0 -1
  88. package/dist/chunk-YRVE4FOD.js.map +0 -1
  89. package/dist/utils/cn.d.ts.map +0 -1
  90. package/src/dashboard/ChartWidget.tsx +0 -50
  91. package/src/dashboard/ChartWidgetInner.tsx +0 -83
  92. package/src/dashboard/DashboardCanvas.tsx +0 -210
  93. package/src/dashboard/DashboardCustomizeMenu.tsx +0 -53
  94. package/src/dashboard/DashboardGrid.tsx +0 -35
  95. package/src/dashboard/DashboardRange.tsx +0 -62
  96. package/src/dashboard/GoldCard.tsx +0 -38
  97. package/src/dashboard/KpiCard.tsx +0 -238
  98. package/src/dashboard/OnboardingWidget.tsx +0 -140
  99. package/src/dashboard/TableWidget.tsx +0 -51
  100. package/src/dashboard/define.ts +0 -53
  101. package/src/dashboard/icon.ts +0 -13
  102. package/src/dashboard/index.ts +0 -38
  103. package/src/dashboard/layout.ts +0 -50
  104. package/src/dashboard/preferences.ts +0 -71
  105. package/src/dashboard/types.ts +0 -41
  106. package/src/icons.ts +0 -66
  107. package/src/index.ts +0 -238
  108. package/src/layout/AppShell.tsx +0 -987
  109. package/src/layout/ContentSplit.tsx +0 -153
  110. package/src/layout/ModulePage.tsx +0 -466
  111. package/src/layout/PageTransition.tsx +0 -57
  112. package/src/layout/SaveBar.tsx +0 -247
  113. package/src/layout/Sidebar.tsx +0 -535
  114. package/src/layout/Topbar.tsx +0 -462
  115. package/src/layout/index.ts +0 -39
  116. package/src/layout/useCrossModuleBack.ts +0 -30
  117. package/src/primitives/Avatar.tsx +0 -48
  118. package/src/primitives/SearchCombobox.tsx +0 -216
  119. package/src/primitives/badge.tsx +0 -68
  120. package/src/primitives/breadcrumb.tsx +0 -43
  121. package/src/primitives/button.tsx +0 -60
  122. package/src/primitives/card.tsx +0 -79
  123. package/src/primitives/checkbox.tsx +0 -53
  124. package/src/primitives/confirm-dialog.tsx +0 -125
  125. package/src/primitives/currency-input.tsx +0 -121
  126. package/src/primitives/data-table.tsx +0 -160
  127. package/src/primitives/date-picker.tsx +0 -225
  128. package/src/primitives/dropdown.tsx +0 -196
  129. package/src/primitives/index.ts +0 -87
  130. package/src/primitives/input.tsx +0 -50
  131. package/src/primitives/list-view.tsx +0 -252
  132. package/src/primitives/markdown-editor.tsx +0 -75
  133. package/src/primitives/markdown.tsx +0 -169
  134. package/src/primitives/modal.tsx +0 -156
  135. package/src/primitives/phone-link.tsx +0 -138
  136. package/src/primitives/popover.tsx +0 -30
  137. package/src/primitives/search-select.tsx +0 -314
  138. package/src/primitives/segmented-control.tsx +0 -66
  139. package/src/primitives/select.tsx +0 -157
  140. package/src/primitives/separator.tsx +0 -29
  141. package/src/primitives/sheet.tsx +0 -181
  142. package/src/primitives/skeleton.tsx +0 -46
  143. package/src/primitives/tabs.tsx +0 -53
  144. package/src/primitives/time-picker.tsx +0 -148
  145. package/src/primitives/toast.tsx +0 -46
  146. package/src/primitives/tooltip.tsx +0 -28
  147. package/src/stores/index.ts +0 -10
  148. package/src/stores/layout.store.ts +0 -31
  149. package/src/stores/theme.store.ts +0 -457
  150. package/src/styles.css +0 -461
  151. package/src/tailwind.ts +0 -43
  152. package/src/theme/index.ts +0 -94
  153. package/src/theme/preset.ts +0 -209
  154. package/src/theme/presets.ts +0 -106
  155. package/src/utils/cn.ts +0 -6
  156. /package/dist/{utils → lib}/cn.d.ts +0 -0
@@ -1,210 +0,0 @@
1
- import * as React from 'react'
2
- import { EyeOff } from 'lucide-react'
3
- import {
4
- usePluginRuntime,
5
- getDashboardWidgets,
6
- resolvePluginComponent,
7
- hashRouterAdapter,
8
- type DashboardLayoutConfig,
9
- type DashboardSurface,
10
- type DashboardWidgetDef,
11
- } from '@fayz-ai/core'
12
- import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../primitives/card'
13
- import { DashboardGrid, type DashboardGridItem } from './DashboardGrid'
14
- import { resolveDashboardLayout, type LaidOutWidget } from './layout'
15
- import { useDashboardPreferences } from './preferences'
16
- import { DashboardRangeProvider, DashboardRangeControl, type DashboardRange } from './DashboardRange'
17
- import { DashboardCustomizeMenu } from './DashboardCustomizeMenu'
18
-
19
- /** Wrap a widget so a hide button fades in on hover (top-right corner). */
20
- function HoverHide({ id, onHide, children }: { id: string; onHide?: (id: string) => void; children: React.ReactNode }) {
21
- if (!onHide) return <>{children}</>
22
- return (
23
- <div className="group relative h-full">
24
- {children}
25
- <button
26
- type="button"
27
- onClick={() => onHide(id)}
28
- title="Hide widget"
29
- aria-label="Hide widget"
30
- className="absolute right-2 top-2 z-10 inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-background/90 text-muted-foreground opacity-0 shadow-sm transition-opacity hover:text-foreground group-hover:opacity-100"
31
- >
32
- <EyeOff className="h-3.5 w-3.5" />
33
- </button>
34
- </div>
35
- )
36
- }
37
-
38
- function toGridItems(laidOut: LaidOutWidget[], onHide?: (id: string) => void): DashboardGridItem[] {
39
- return laidOut.map(({ def, span }) => {
40
- const Component = resolvePluginComponent(def)
41
- const node = Component ? <Component {...(def.props ?? {})} /> : null
42
- return { id: def.id, span, node: <HoverHide id={def.id} onHide={onHide}>{node}</HoverHide> }
43
- })
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Navigation — widgets (e.g. onboarding) call useDashboardNavigate() to route.
48
- // Defaults to the shared hash router so widgets work without explicit wiring.
49
- // ---------------------------------------------------------------------------
50
-
51
- const DashboardNavigateContext = React.createContext<(path: string) => void>((path) =>
52
- hashRouterAdapter().navigate(path),
53
- )
54
-
55
- export const DashboardNavigateProvider = DashboardNavigateContext.Provider
56
-
57
- export function useDashboardNavigate(): (path: string) => void {
58
- return React.useContext(DashboardNavigateContext)
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Shared widget grid — used by DashboardCanvas (registry) and by standalone
63
- // callers (e.g. the dashboard plugin's legacy DashboardPage shim). Resolves the
64
- // layout, then renders each widget by span.
65
- // ---------------------------------------------------------------------------
66
-
67
- export function WidgetGrid({ widgets, appLayout, surface }: {
68
- widgets: DashboardWidgetDef[]
69
- appLayout?: DashboardLayoutConfig
70
- surface: DashboardSurface
71
- }) {
72
- const { prefs, setVisible } = useDashboardPreferences(surface)
73
- const laidOut = resolveDashboardLayout(widgets, appLayout, prefs ?? undefined)
74
- return <DashboardGrid items={toGridItems(laidOut, (id) => setVisible(id, false))} />
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // DashboardCanvas — the registry-driven renderer. Serves the global app home
79
- // (surface="home", all domains) and a plugin's own overview (surface="plugin-home"
80
- // + domain). Widgets a plugin registers once appear on both.
81
- // ---------------------------------------------------------------------------
82
-
83
- export interface DashboardCanvasProps {
84
- surface: DashboardSurface
85
- /** Restrict to one plugin's widgets (used for plugin-home). */
86
- domain?: string
87
- title?: string
88
- subtitle?: string
89
- /** Render the in-content title/subtitle. Default: true. */
90
- showHeader?: boolean
91
- /** App-level curation of which widgets show, their order and span. */
92
- appLayout?: DashboardLayoutConfig
93
- /** App-level widgets injected into this surface's grid without a plugin — e.g.
94
- * an "Ask the assistant" CTA. Merged with the registry widgets, then laid out
95
- * (respects each widget's span/defaultOrder). Filtered to this surface. */
96
- extraWidgets?: DashboardWidgetDef[]
97
- /** Show a shared time-range control (sticky, top-right). Widgets read it via
98
- * useDashboardRange(). Pass `true` for defaults or an options object. */
99
- range?: boolean | DashboardRangeOptions
100
- /** Show the "Customize" menu so users can choose which widgets appear
101
- * (persisted per surface). Default: true. */
102
- customizable?: boolean
103
- /** Label for the customize menu trigger. */
104
- customizeLabel?: string
105
- /** Initial visibility before user prefs: 'default' honors each widget's
106
- * `defaultVisible` (used by the global home to curate a starting set); 'all'
107
- * shows every eligible widget. Defaults to 'all' on a plugin-home, 'default'
108
- * on the global home. */
109
- initialVisibility?: 'default' | 'all'
110
- /** Route handler for widgets that navigate (defaults to the hash router). */
111
- onNavigate?: (path: string) => void
112
- className?: string
113
- }
114
-
115
- export interface DashboardRangeOptions {
116
- options?: DashboardRange[]
117
- default?: DashboardRange
118
- onChange?: (range: DashboardRange) => void
119
- }
120
-
121
- export function DashboardCanvas({
122
- surface, domain, title, subtitle, showHeader = true, appLayout, extraWidgets, range,
123
- customizable = true, customizeLabel, initialVisibility, onNavigate, className,
124
- }: DashboardCanvasProps) {
125
- const runtime = usePluginRuntime()
126
- const registryWidgets = getDashboardWidgets(runtime, { surface, domain })
127
- const widgets = React.useMemo<DashboardWidgetDef[]>(() => {
128
- const extra = (extraWidgets ?? []).filter((w) => {
129
- if (!(w.surfaces ?? ['home', 'plugin-home']).includes(surface)) return false
130
- return !domain || w.domain === domain
131
- })
132
- return extra.length ? [...registryWidgets, ...extra] : registryWidgets
133
- }, [registryWidgets, extraWidgets, surface, domain])
134
- const rangeOpts: DashboardRangeOptions | null = range ? (range === true ? {} : range) : null
135
-
136
- const forceAll = (initialVisibility ?? (surface === 'plugin-home' ? 'all' : 'default')) === 'all'
137
- const { prefs, setVisible, reset } = useDashboardPreferences(surface, domain ?? 'home')
138
- const laidOut = resolveDashboardLayout(widgets, appLayout, prefs ?? undefined, forceAll)
139
- const visibleIds = React.useMemo(() => new Set(laidOut.map((w) => w.def.id)), [laidOut])
140
-
141
- const hasControls = Boolean(rangeOpts) || (customizable && widgets.length > 0)
142
- const headerBlock = showHeader && (title || subtitle) ? (
143
- <div>
144
- {title ? <h1 className="text-2xl font-semibold tracking-tight">{title}</h1> : null}
145
- {subtitle ? <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p> : null}
146
- </div>
147
- ) : null
148
-
149
- // Controls sit inline on the header row (right-aligned) — no separate band, so
150
- // there's no empty gap above the content.
151
- const topRow = (headerBlock || hasControls) ? (
152
- <div className="flex items-start justify-between gap-4">
153
- {headerBlock ?? <span />}
154
- {hasControls ? (
155
- <div className="flex shrink-0 items-center gap-2">
156
- {customizable && widgets.length > 0 ? (
157
- <DashboardCustomizeMenu
158
- widgets={widgets}
159
- visibleIds={visibleIds}
160
- onToggle={setVisible}
161
- onReset={reset}
162
- label={customizeLabel}
163
- />
164
- ) : null}
165
- {rangeOpts ? <DashboardRangeControl /> : null}
166
- </div>
167
- ) : null}
168
- </div>
169
- ) : null
170
-
171
- const content = (
172
- <div className={className ?? 'space-y-6 p-6'}>
173
- {topRow}
174
-
175
- {widgets.length === 0 ? (
176
- <Card>
177
- <CardHeader>
178
- <CardTitle>Welcome!</CardTitle>
179
- <CardDescription>Your dashboard will show key metrics and information here.</CardDescription>
180
- </CardHeader>
181
- <CardContent>
182
- <p className="text-sm text-muted-foreground">
183
- Enable plugins or register dashboard widgets to populate this view.
184
- </p>
185
- </CardContent>
186
- </Card>
187
- ) : laidOut.length === 0 ? (
188
- <Card>
189
- <CardContent className="p-6 text-sm text-muted-foreground">
190
- All widgets are hidden. Use “Customize” to choose what to show.
191
- </CardContent>
192
- </Card>
193
- ) : (
194
- <DashboardGrid items={toGridItems(laidOut)} />
195
- )}
196
- </div>
197
- )
198
-
199
- const navWrapped = onNavigate
200
- ? <DashboardNavigateProvider value={onNavigate}>{content}</DashboardNavigateProvider>
201
- : content
202
-
203
- return rangeOpts
204
- ? (
205
- <DashboardRangeProvider options={rangeOpts.options} defaultRange={rangeOpts.default} onChange={rangeOpts.onChange}>
206
- {navWrapped}
207
- </DashboardRangeProvider>
208
- )
209
- : navWrapped
210
- }
@@ -1,53 +0,0 @@
1
- import * as React from 'react'
2
- import { SlidersHorizontal, RotateCcw } from 'lucide-react'
3
- import type { DashboardWidgetDef } from '@fayz-ai/core'
4
- import { useTranslation } from '@fayz-ai/core'
5
- import { Button } from '../primitives/button'
6
- import {
7
- Dropdown, DropdownTrigger, DropdownContent, DropdownLabel, DropdownSeparator, DropdownCheckboxItem, DropdownItem,
8
- } from '../primitives/dropdown'
9
-
10
- export interface DashboardCustomizeMenuProps {
11
- /** Full set of registered widgets for this surface. */
12
- widgets: DashboardWidgetDef[]
13
- /** Ids currently visible. */
14
- visibleIds: Set<string>
15
- onToggle: (widgetId: string, visible: boolean) => void
16
- onReset: () => void
17
- label?: string
18
- }
19
-
20
- /** Gear menu to choose which widgets appear on the dashboard (persisted). */
21
- export function DashboardCustomizeMenu({ widgets, visibleIds, onToggle, onReset, label }: DashboardCustomizeMenuProps) {
22
- const t = useTranslation()
23
- if (widgets.length === 0) return null
24
- return (
25
- <Dropdown>
26
- <DropdownTrigger asChild>
27
- <Button variant="outline" size="sm" className="gap-1.5">
28
- <SlidersHorizontal className="h-3.5 w-3.5" />
29
- {label ?? 'Customize'}
30
- </Button>
31
- </DropdownTrigger>
32
- <DropdownContent align="end" className="max-h-[70vh] w-60 overflow-y-auto">
33
- <DropdownLabel>{label ?? 'Show widgets'}</DropdownLabel>
34
- <DropdownSeparator />
35
- {widgets.map((w) => (
36
- <DropdownCheckboxItem
37
- key={w.id}
38
- checked={visibleIds.has(w.id)}
39
- onCheckedChange={(checked) => onToggle(w.id, Boolean(checked))}
40
- onSelect={(e) => e.preventDefault()}
41
- >
42
- {t(w.title)}
43
- </DropdownCheckboxItem>
44
- ))}
45
- <DropdownSeparator />
46
- <DropdownItem onSelect={() => onReset()}>
47
- <RotateCcw className="mr-2 h-3.5 w-3.5" />
48
- {'Reset to defaults'}
49
- </DropdownItem>
50
- </DropdownContent>
51
- </Dropdown>
52
- )
53
- }
@@ -1,35 +0,0 @@
1
- import * as React from 'react'
2
- import { cn } from '../utils/cn'
3
-
4
- export interface DashboardGridItem {
5
- id: string
6
- /** Column span on the 4-col grid (defaults to 1). */
7
- span?: number
8
- node: React.ReactNode
9
- }
10
-
11
- /** Literal span → Tailwind classes (kept literal so the JIT compiler picks them up). */
12
- const SPAN_CLASS: Record<number, string> = {
13
- 1: '',
14
- 2: 'sm:col-span-2',
15
- 3: 'sm:col-span-2 lg:col-span-3',
16
- 4: 'sm:col-span-2 lg:col-span-4',
17
- }
18
-
19
- export interface DashboardGridProps {
20
- items: DashboardGridItem[]
21
- className?: string
22
- }
23
-
24
- /** Responsive 4-col dashboard grid — the layout every dashboard shares. */
25
- export function DashboardGrid({ items, className }: DashboardGridProps) {
26
- return (
27
- <div className={cn('grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4', className)}>
28
- {items.map((item) => (
29
- <div key={item.id} className={SPAN_CLASS[Math.min(Math.max(item.span ?? 1, 1), 4)] ?? ''}>
30
- {item.node}
31
- </div>
32
- ))}
33
- </div>
34
- )
35
- }
@@ -1,62 +0,0 @@
1
- import * as React from 'react'
2
- import { SegmentedControl } from '../primitives/segmented-control'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Shared dashboard time range. The canvas provides it; widgets read it via
6
- // useDashboardRange() and refetch/filter their data accordingly.
7
- // ---------------------------------------------------------------------------
8
-
9
- export type DashboardRange = string
10
-
11
- export const DEFAULT_RANGE_OPTIONS: DashboardRange[] = ['7d', '30d', '90d']
12
-
13
- interface DashboardRangeContextValue {
14
- range: DashboardRange
15
- setRange: (range: DashboardRange) => void
16
- options: DashboardRange[]
17
- }
18
-
19
- const DashboardRangeContext = React.createContext<DashboardRangeContextValue | null>(null)
20
-
21
- /**
22
- * Read the shared dashboard range. Returns null when no range control is
23
- * mounted, so widgets can stay range-agnostic.
24
- */
25
- export function useDashboardRange(): DashboardRangeContextValue | null {
26
- return React.useContext(DashboardRangeContext)
27
- }
28
-
29
- export interface DashboardRangeProviderProps {
30
- options?: DashboardRange[]
31
- defaultRange?: DashboardRange
32
- onChange?: (range: DashboardRange) => void
33
- children: React.ReactNode
34
- }
35
-
36
- export function DashboardRangeProvider({
37
- options = DEFAULT_RANGE_OPTIONS, defaultRange, onChange, children,
38
- }: DashboardRangeProviderProps) {
39
- const [range, setRangeState] = React.useState<DashboardRange>(defaultRange ?? options[1] ?? options[0] ?? '30d')
40
- const setRange = React.useCallback((r: DashboardRange) => {
41
- setRangeState(r)
42
- onChange?.(r)
43
- }, [onChange])
44
- const value = React.useMemo(() => ({ range, setRange, options }), [range, setRange, options])
45
- return <DashboardRangeContext.Provider value={value}>{children}</DashboardRangeContext.Provider>
46
- }
47
-
48
- /** Segmented 7d / 30d / 90d control bound to the shared dashboard range.
49
- * Thin wrapper over the canonical SegmentedControl primitive. */
50
- export function DashboardRangeControl({ className }: { className?: string }) {
51
- const ctx = useDashboardRange()
52
- if (!ctx) return null
53
- return (
54
- <SegmentedControl
55
- options={ctx.options}
56
- value={ctx.range}
57
- onChange={ctx.setRange}
58
- className={className}
59
- aria-label="Time range"
60
- />
61
- )
62
- }
@@ -1,38 +0,0 @@
1
- import * as React from 'react'
2
- import { cn } from '../utils/cn'
3
-
4
- export interface GoldCardProps extends React.HTMLAttributes<HTMLDivElement> {
5
- /**
6
- * Brand-colored hero variant — `bg-primary text-primary-foreground` with a
7
- * soft branded shadow. Use for the headline card (e.g. a balance hero). When
8
- * false (default) it's a plain surface card (`bg-card` + border).
9
- */
10
- branded?: boolean
11
- }
12
-
13
- /**
14
- * GoldCard — a generic, reusable rounded card wrapper for dashboard surfaces.
15
- *
16
- * The `branded` variant paints the card in the app's primary color so a single
17
- * hero (balance, headline KPI, CTA) pops against the neutral grid. Both variants
18
- * are internally full-width so they reflow cleanly inside the responsive
19
- * DashboardGrid (full-width on mobile, cell-width on desktop).
20
- */
21
- export const GoldCard = React.forwardRef<HTMLDivElement, GoldCardProps>(
22
- ({ branded = false, className, children, ...rest }, ref) => (
23
- <div
24
- ref={ref}
25
- className={cn(
26
- 'h-full rounded-3xl p-5',
27
- branded
28
- ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
29
- : 'border border-border bg-card text-card-foreground shadow-sm',
30
- className,
31
- )}
32
- {...rest}
33
- >
34
- {children}
35
- </div>
36
- ),
37
- )
38
- GoldCard.displayName = 'GoldCard'
@@ -1,238 +0,0 @@
1
- import * as React from 'react'
2
- import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
3
- import { cn } from '../utils/cn'
4
- import { Card, CardContent } from '../primitives/card'
5
- import { renderIcon } from './icon'
6
- import type { KpiCurrency, IconRef, KpiFormat, KpiValue, KpiTrend } from './types'
7
-
8
- export function formatKpiValue(value: number, format?: KpiFormat, currency?: KpiCurrency): string {
9
- const locale = currency?.locale ?? 'pt-BR'
10
- switch (format) {
11
- case 'currency':
12
- return new Intl.NumberFormat(locale, { style: 'currency', currency: currency?.code ?? 'BRL', maximumFractionDigits: 0 }).format(value)
13
- case 'percent':
14
- return `${new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)}%`
15
- case 'duration':
16
- return `${Math.round(value)} min`
17
- default:
18
- return new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)
19
- }
20
- }
21
-
22
- function deriveTrend(current: number, previous: number | undefined, explicit: KpiTrend | undefined): KpiTrend | undefined {
23
- if (explicit) return explicit
24
- if (previous == null) return undefined
25
- return current > previous ? 'up' : current < previous ? 'down' : 'neutral'
26
- }
27
-
28
- function TrendChip({ trend, delta, invert }: { trend: KpiTrend; delta: number | null; invert?: boolean }) {
29
- if (trend === 'neutral') return null
30
- const good = invert ? trend === 'down' : trend === 'up'
31
- const Icon = trend === 'up' ? TrendingUp : TrendingDown
32
- return (
33
- <span className={cn(
34
- 'inline-flex items-center gap-0.5 text-xs font-medium',
35
- good ? 'text-emerald-600' : 'text-rose-600',
36
- )}>
37
- <Icon className="h-3 w-3" />
38
- {delta != null ? `${delta > 0 ? '+' : ''}${delta}%` : null}
39
- </span>
40
- )
41
- }
42
-
43
- /** Trend-aware accent: green when good, red when bad, theme-info when flat. */
44
- function trendColor(trend: KpiTrend | undefined, invert?: boolean): string {
45
- if (!trend || trend === 'neutral') return 'hsl(var(--info))'
46
- const good = invert ? trend === 'down' : trend === 'up'
47
- return good ? '#10b981' : '#f43f5e'
48
- }
49
-
50
- /** Tiny dependency-free SVG sparkline (oldest → newest), with a soft area fill. */
51
- function Sparkline({ data, color }: { data: number[]; color: string }) {
52
- const gid = React.useId()
53
- if (!data || data.length < 2) return null
54
- const W = 96, H = 32, P = 3
55
- const min = Math.min(...data)
56
- const max = Math.max(...data)
57
- const span = max - min || 1
58
- const step = (W - P * 2) / (data.length - 1)
59
- const pts = data.map((d, i) => {
60
- const x = P + i * step
61
- const y = P + (H - P * 2) * (1 - (d - min) / span)
62
- return [x, y] as const
63
- })
64
- const line = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`).join(' ')
65
- const area = `${line} L${pts[pts.length - 1][0].toFixed(1)},${H} L${pts[0][0].toFixed(1)},${H} Z`
66
- return (
67
- <svg viewBox={`0 0 ${W} ${H}`} className="h-8 w-24 shrink-0 overflow-visible" preserveAspectRatio="none" aria-hidden>
68
- <defs>
69
- <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
70
- <stop offset="0%" stopColor={color} stopOpacity="0.22" />
71
- <stop offset="100%" stopColor={color} stopOpacity="0" />
72
- </linearGradient>
73
- </defs>
74
- <path d={area} fill={`url(#${gid})`} />
75
- <path d={line} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
76
- </svg>
77
- )
78
- }
79
-
80
- /** Semicircular radial gauge showing `value` as a fraction of `goal`. */
81
- function Gauge({ value, goal, color, children }: { value: number; goal: number; color: string; children: React.ReactNode }) {
82
- const pct = goal > 0 ? Math.min(1, Math.max(0, value / goal)) : 0
83
- const R = 40, CX = 50, CY = 46, SW = 9
84
- const len = Math.PI * R
85
- const d = `M ${CX - R} ${CY} A ${R} ${R} 0 0 1 ${CX + R} ${CY}`
86
- return (
87
- <div className="relative mx-auto w-full max-w-[160px]">
88
- <svg viewBox="0 0 100 52" className="w-full" aria-hidden>
89
- <path d={d} fill="none" stroke="hsl(var(--muted))" strokeWidth={SW} strokeLinecap="round" />
90
- <path
91
- d={d} fill="none" stroke={color} strokeWidth={SW} strokeLinecap="round"
92
- strokeDasharray={len} strokeDashoffset={len * (1 - pct)}
93
- />
94
- </svg>
95
- <div className="absolute inset-x-0 bottom-0 text-center text-2xl font-semibold tracking-tight text-foreground">
96
- {children}
97
- </div>
98
- </div>
99
- )
100
- }
101
-
102
- export interface KpiCardProps {
103
- label: string
104
- icon?: IconRef
105
- /** Pre-formatted/sync value. Provide this OR `compute`. */
106
- value?: React.ReactNode
107
- /** Numeric value formatted via `format`. Ignored when `value` is set. */
108
- amount?: number
109
- /** Async loader; the card shows a skeleton until it resolves. */
110
- compute?: () => Promise<KpiValue>
111
- format?: KpiFormat
112
- currency?: KpiCurrency
113
- sub?: React.ReactNode
114
- /** Trend chip inputs (for sync usage). */
115
- current?: number
116
- previous?: number
117
- trend?: KpiTrend
118
- /** For cost-style metrics where down is good (e.g. CPA). */
119
- invertTrend?: boolean
120
- /** Inline sparkline series (oldest → newest). Async usage: return via KpiValue.spark. */
121
- spark?: number[]
122
- /** Render the card as a radial gauge toward this goal value. */
123
- goal?: number
124
- /** Accent color for the sparkline/gauge. Defaults to a trend-aware color. */
125
- accent?: string
126
- loading?: boolean
127
- }
128
-
129
- /**
130
- * Canonical KPI card — consolidates the trend-chip card from plugin-marketing
131
- * and the async MetricCard from plugin-dashboard. Use directly, or via a
132
- * registered KPI widget (see defineKpiWidget).
133
- */
134
- export function KpiCard(props: KpiCardProps) {
135
- const { label, icon, sub, format, currency, invertTrend } = props
136
- const [computed, setComputed] = React.useState<KpiValue | null>(null)
137
- const [errored, setErrored] = React.useState(false)
138
-
139
- React.useEffect(() => {
140
- if (!props.compute) return
141
- let cancelled = false
142
- props.compute()
143
- .then((v) => { if (!cancelled) setComputed(v) })
144
- .catch(() => { if (!cancelled) setErrored(true) })
145
- return () => { cancelled = true }
146
- }, [props.compute])
147
-
148
- const isAsync = Boolean(props.compute)
149
- const loading = props.loading || (isAsync && computed === null && !errored)
150
-
151
- let display: React.ReactNode = props.value
152
- let current = props.current
153
- let previous = props.previous
154
- let trend = props.trend
155
- let unit: string | undefined
156
- let spark = props.spark
157
-
158
- if (isAsync && computed) {
159
- display = formatKpiValue(computed.value, format, currency)
160
- current = computed.value
161
- previous = computed.previousValue
162
- trend = computed.trend
163
- unit = computed.unit
164
- spark = computed.spark ?? spark
165
- } else if (display == null && props.amount != null) {
166
- display = formatKpiValue(props.amount, format, currency)
167
- current = current ?? props.amount
168
- }
169
-
170
- const resolvedTrend = current != null ? deriveTrend(current, previous, trend) : trend
171
- const delta = current != null && previous != null && previous !== 0
172
- ? Math.round(((current - previous) / previous) * 100)
173
- : null
174
- const accent = props.accent ?? trendColor(resolvedTrend, invertTrend)
175
-
176
- const valueText = (
177
- <>
178
- {display}
179
- {unit ? <span className="ml-1 text-sm font-normal text-muted-foreground">{unit}</span> : null}
180
- </>
181
- )
182
- const trendChip = resolvedTrend ? <TrendChip trend={resolvedTrend} delta={delta} invert={invertTrend} /> : null
183
-
184
- // Pick a body layout: gauge (goal set) → sparkline (series set) → plain.
185
- const isGauge = props.goal != null && current != null
186
- const hasSpark = Boolean(spark && spark.length >= 2)
187
-
188
- let body: React.ReactNode
189
- if (errored) {
190
- body = <span className="text-sm text-muted-foreground">—</span>
191
- } else if (loading) {
192
- body = isGauge
193
- ? <div className="mx-auto h-16 w-32 animate-pulse rounded bg-muted" />
194
- : <div className="h-7 w-20 animate-pulse rounded bg-muted" />
195
- } else if (isGauge) {
196
- body = (
197
- <div>
198
- <Gauge value={current!} goal={props.goal!} color={accent}>{valueText}</Gauge>
199
- <div className="mt-1 flex items-center justify-center gap-2 text-xs text-muted-foreground">
200
- {trendChip}
201
- <span>{sub ?? `out of ${formatKpiValue(props.goal!, format, currency)}`}</span>
202
- </div>
203
- </div>
204
- )
205
- } else if (hasSpark) {
206
- body = (
207
- <div className="flex items-end justify-between gap-3">
208
- <div className="min-w-0">
209
- <div className="text-2xl font-semibold tracking-tight text-foreground">{valueText}</div>
210
- {trendChip ? <div className="mt-0.5">{trendChip}</div> : null}
211
- </div>
212
- <Sparkline data={spark!} color={accent} />
213
- </div>
214
- )
215
- } else {
216
- body = (
217
- <div className="flex items-baseline justify-between gap-2">
218
- <span className="text-2xl font-semibold tracking-tight text-foreground">{valueText}</span>
219
- {trendChip ? <span className="mb-0.5">{trendChip}</span> : null}
220
- </div>
221
- )
222
- }
223
-
224
- return (
225
- // h-full so cards fill the (stretch) grid cell and every card in a row matches
226
- // the tallest — labels/subtitles of different lengths no longer misalign them.
227
- <Card className="h-full">
228
- <CardContent className="flex h-full flex-col p-4">
229
- <div className="mb-2 flex items-center justify-between gap-2">
230
- <span className="text-sm text-muted-foreground">{label}</span>
231
- <span className="text-muted-foreground">{renderIcon(icon)}</span>
232
- </div>
233
- {body}
234
- {sub && !isGauge ? <p className="mt-0.5 text-xs text-muted-foreground">{sub}</p> : null}
235
- </CardContent>
236
- </Card>
237
- )
238
- }