@fayz-ai/ui 0.9.0 → 0.10.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.
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 +6 -15
  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/tailwind.ts +0 -43
  151. package/src/theme/index.ts +0 -94
  152. package/src/theme/preset.ts +0 -209
  153. package/src/theme/presets.ts +0 -106
  154. package/src/utils/cn.ts +0 -6
  155. /package/dist/{utils → lib}/cn.d.ts +0 -0
  156. /package/{src → dist}/styles.css +0 -0
package/dist/index.js CHANGED
@@ -1,17 +1,21 @@
1
- export { Badge, Breadcrumb, Checkbox, ConfirmDialog, CurrencyInput, DatePicker, Input, ListView, Markdown, MarkdownEditor, Modal, ModalBody, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, PhoneLink, Popover, PopoverContent, PopoverTrigger, SearchCombobox, SearchSelect, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Tabs, TabsContent, TabsList, TabsTrigger, TimePicker, ToastProvider, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, normalizePhone, renderMarkdown, toast } from './chunk-S5KNM4D2.js';
2
- export { AppShell, BackStyleProvider, ContentSplit, ContentSplitTrigger, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageTransition, Sidebar, SubpageHeader, Topbar, useBackStyle, useLayoutStore, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition } from './chunk-KV3MHB6N.js';
3
- export { SaveBar, SaveBarProvider, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, useBackHandler, useSaveBar } from './chunk-75EFDLST.js';
1
+ import { Popover, PopoverTrigger, PopoverContent, Input, Hint } from './chunk-SEIY2F5F.js';
2
+ export { Badge, Breadcrumb, Checkbox, ConfirmDialog, CurrencyInput, DatePicker, FilterChip, Hint, Input, ListView, Markdown, MarkdownEditor, Modal, ModalBody, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, PhoneLink, Popover, PopoverContent, PopoverTrigger, SearchCombobox, SearchSelect, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, StatTiles, Switch, Tabs, TabsContent, TabsList, TabsTrigger, TimePicker, ToastProvider, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, normalizePhone, renderMarkdown, toast } from './chunk-SEIY2F5F.js';
3
+ export { AppShell, BackStyleProvider, ContentSplit, ContentSplitTrigger, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageTransition, Sidebar, SubpageHeader, Topbar, useBackStyle, useLayoutStore, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition } from './chunk-H63TOSQ7.js';
4
+ export { HeaderTabs, PageHeader, PageHeaderCrumb, PageHeaderOutlet, SaveBar, SaveBarProvider, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, useBackHandler, useHasPageHeaderHost, useHasPageHeaderOutlet, usePageHeader, usePageHeaderHost, usePageHeaderSlot, useSaveBar } from './chunk-VAKWUUVC.js';
4
5
  export { ThemeProvider, createFayzTheme, createTheme, darkTheme, fayzThemePresets, lightTheme, useTheme, useThemeStore } from './chunk-VDAYIQX4.js';
5
- export { ChartWidget, DEFAULT_RANGE_OPTIONS, DashboardCanvas, DashboardCustomizeMenu, DashboardGrid, DashboardNavigateProvider, DashboardRangeControl, DashboardRangeProvider, GoldCard, KpiCard, OnboardingWidget, TableWidget, WidgetGrid, defineChartWidget, defineCustomWidget, defineKpiWidget, defineOnboardingWidget, defineTableWidget, formatKpiValue, renderIcon, resolveDashboardLayout, useDashboardNavigate, useDashboardPreferences, useDashboardRange } from './chunk-OUPJDGKE.js';
6
- export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DataTable, Dropdown, DropdownCheckboxItem, DropdownContent, DropdownGroup, DropdownItem, DropdownLabel, DropdownPortal, DropdownRadioGroup, DropdownRadioItem, DropdownSeparator, DropdownShortcut, DropdownSub, DropdownSubContent, DropdownSubTrigger, DropdownTrigger, SegmentedControl, Skeleton, TableSkeleton } from './chunk-G6OKZ7WL.js';
7
- import { cn } from './chunk-YRVE4FOD.js';
8
- export { Button, buttonVariants, cn } from './chunk-YRVE4FOD.js';
9
- export { ICON_MAP, getLucideIcon, registerIcons } from './chunk-PVIB724L.js';
10
- import * as React from 'react';
6
+ import { ChartWidget, DashboardGrid } from './chunk-I6CPKAOD.js';
7
+ export { ChartWidget, DEFAULT_RANGE_OPTIONS, DashboardCanvas, DashboardCustomizeMenu, DashboardGrid, DashboardNavigateProvider, DashboardRangeControl, DashboardRangeProvider, GoldCard, KpiCard, OnboardingWidget, SidebarOnboarding, TableWidget, WidgetGrid, defineChartWidget, defineCustomWidget, defineKpiWidget, defineOnboardingWidget, defineTableWidget, formatKpiValue, renderIcon, resolveDashboardLayout, useDashboardNavigate, useDashboardPreferences, useDashboardRange, useOnboardingProgress } from './chunk-I6CPKAOD.js';
8
+ import { Skeleton, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, Card, CardContent } from './chunk-6V5MGYXH.js';
9
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DataTable, Dropdown, DropdownCheckboxItem, DropdownContent, DropdownGroup, DropdownItem, DropdownLabel, DropdownPortal, DropdownRadioGroup, DropdownRadioItem, DropdownSeparator, DropdownShortcut, DropdownSub, DropdownSubContent, DropdownSubTrigger, DropdownTrigger, SegmentedControl, Skeleton, TableSkeleton } from './chunk-6V5MGYXH.js';
10
+ import { cn, Button } from './chunk-OKZ6WQB3.js';
11
+ export { Button, buttonVariants, cn } from './chunk-OKZ6WQB3.js';
12
+ export { ICON_MAP, getLucideIcon, registerIcons } from './chunk-22EVFMS3.js';
13
+ import * as React2 from 'react';
11
14
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
12
- import { jsx } from 'react/jsx-runtime';
15
+ import { jsx, jsxs } from 'react/jsx-runtime';
16
+ import { Construction, DatabaseZap, AlertCircle, SlidersHorizontal, Search, ChevronRight, Calendar, GitCompareArrows, RefreshCw, X, TrendingUp, TrendingDown, Info, ArrowUpRight } from 'lucide-react';
13
17
 
14
- var Avatar = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
18
+ var Avatar = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
15
19
  AvatarPrimitive.Root,
16
20
  {
17
21
  ref,
@@ -23,7 +27,7 @@ var Avatar = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
23
27
  }
24
28
  ));
25
29
  Avatar.displayName = AvatarPrimitive.Root.displayName;
26
- var AvatarImage = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
30
+ var AvatarImage = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
27
31
  AvatarPrimitive.Image,
28
32
  {
29
33
  ref,
@@ -32,7 +36,7 @@ var AvatarImage = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
32
36
  }
33
37
  ));
34
38
  AvatarImage.displayName = AvatarPrimitive.Image.displayName;
35
- var AvatarFallback = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
39
+ var AvatarFallback = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
36
40
  AvatarPrimitive.Fallback,
37
41
  {
38
42
  ref,
@@ -45,6 +49,948 @@ var AvatarFallback = React.forwardRef(({ className, ...props }, ref) => /* @__PU
45
49
  ));
46
50
  AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
47
51
 
48
- export { Avatar, AvatarFallback, AvatarImage };
52
+ // src/analytics/range.ts
53
+ function toDayString(d) {
54
+ const y = d.getFullYear();
55
+ const m = String(d.getMonth() + 1).padStart(2, "0");
56
+ const day = String(d.getDate()).padStart(2, "0");
57
+ return `${y}-${m}-${day}`;
58
+ }
59
+ function parseDay(s) {
60
+ const [y, m, d] = s.split("-").map(Number);
61
+ return new Date(y ?? 1970, (m ?? 1) - 1, d ?? 1, 12, 0, 0, 0);
62
+ }
63
+ function shiftDays(d, days) {
64
+ const next = new Date(d);
65
+ next.setDate(next.getDate() + days);
66
+ return next;
67
+ }
68
+ function rangeLengthDays(range) {
69
+ const from = parseDay(range.from);
70
+ const to = parseDay(range.to);
71
+ return Math.max(1, Math.round((to.getTime() - from.getTime()) / 864e5) + 1);
72
+ }
73
+ function resolveRangePreset(preset, today = /* @__PURE__ */ new Date()) {
74
+ const base = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12);
75
+ switch (preset) {
76
+ case "today":
77
+ return { from: toDayString(base), to: toDayString(base) };
78
+ case "yesterday": {
79
+ const y = shiftDays(base, -1);
80
+ return { from: toDayString(y), to: toDayString(y) };
81
+ }
82
+ // "Last 7 days" includes today, so it spans today minus six — not minus
83
+ // seven, which would be eight days of data under a seven-day label.
84
+ case "last_7_days":
85
+ return { from: toDayString(shiftDays(base, -6)), to: toDayString(base) };
86
+ case "last_30_days":
87
+ return { from: toDayString(shiftDays(base, -29)), to: toDayString(base) };
88
+ case "last_90_days":
89
+ return { from: toDayString(shiftDays(base, -89)), to: toDayString(base) };
90
+ case "this_month":
91
+ return {
92
+ from: toDayString(new Date(base.getFullYear(), base.getMonth(), 1, 12)),
93
+ to: toDayString(base)
94
+ };
95
+ case "last_month": {
96
+ const first = new Date(base.getFullYear(), base.getMonth() - 1, 1, 12);
97
+ const last = new Date(base.getFullYear(), base.getMonth(), 0, 12);
98
+ return { from: toDayString(first), to: toDayString(last) };
99
+ }
100
+ case "this_quarter": {
101
+ const q = Math.floor(base.getMonth() / 3);
102
+ return {
103
+ from: toDayString(new Date(base.getFullYear(), q * 3, 1, 12)),
104
+ to: toDayString(base)
105
+ };
106
+ }
107
+ case "this_year":
108
+ return { from: toDayString(new Date(base.getFullYear(), 0, 1, 12)), to: toDayString(base) };
109
+ default:
110
+ return { from: toDayString(shiftDays(base, -29)), to: toDayString(base) };
111
+ }
112
+ }
113
+ function comparisonRange(range, compare) {
114
+ if (!compare || compare === "none") return null;
115
+ if (compare === "previous_year") {
116
+ const from = parseDay(range.from);
117
+ const to = parseDay(range.to);
118
+ from.setFullYear(from.getFullYear() - 1);
119
+ to.setFullYear(to.getFullYear() - 1);
120
+ return { from: toDayString(from), to: toDayString(to) };
121
+ }
122
+ const days = rangeLengthDays(range);
123
+ const prevTo = shiftDays(parseDay(range.from), -1);
124
+ const prevFrom = shiftDays(prevTo, -(days - 1));
125
+ return { from: toDayString(prevFrom), to: toDayString(prevTo) };
126
+ }
127
+ function autoGrain(range) {
128
+ const days = rangeLengthDays(range);
129
+ if (days <= 2) return "hour";
130
+ if (days <= 62) return "day";
131
+ if (days <= 180) return "week";
132
+ return "month";
133
+ }
134
+
135
+ // src/analytics/derive.ts
136
+ function errorMessage(err) {
137
+ if (err instanceof Error) return err.message;
138
+ if (typeof err === "string") return err;
139
+ if (err && typeof err === "object") {
140
+ const e = err;
141
+ const parts = [e.message, e.details, e.hint].filter((p) => typeof p === "string" && p.trim().length > 0);
142
+ if (parts.length > 0) return [...new Set(parts)].join(" \u2014 ");
143
+ try {
144
+ return JSON.stringify(err);
145
+ } catch {
146
+ return "Unknown error";
147
+ }
148
+ }
149
+ return String(err);
150
+ }
151
+ function isEngineMissing(err) {
152
+ if (!err || typeof err !== "object") return false;
153
+ const e = err;
154
+ const code = typeof e.code === "string" ? e.code : "";
155
+ if (["42883", "42P01", "PGRST202"].includes(code)) return true;
156
+ const message = typeof e.message === "string" ? e.message.toLowerCase() : "";
157
+ return message.includes("analytics_run") || message.includes("plg_shop_rep_");
158
+ }
159
+ function num(value) {
160
+ if (value == null) return 0;
161
+ const n = typeof value === "number" ? value : Number(value);
162
+ return Number.isFinite(n) ? n : 0;
163
+ }
164
+ function resolveRatios(summary, measures = []) {
165
+ const out = { ...summary ?? {} };
166
+ for (const m of measures) {
167
+ if (m.agg !== "ratio" || !m.ratioOf) continue;
168
+ const [numeratorKey, denominatorKey] = m.ratioOf;
169
+ const denominator = num(out[denominatorKey]);
170
+ out[m.key] = denominator === 0 ? 0 : num(out[numeratorKey]) / denominator * (m.scale ?? 1);
171
+ }
172
+ return out;
173
+ }
174
+ function resolveRowRatios(rows, measures = []) {
175
+ const ratios = measures.filter((m) => m.agg === "ratio" && m.ratioOf);
176
+ if (ratios.length === 0) return rows;
177
+ return rows.map((row) => ({ ...row, ...resolveRatios(row, ratios) }));
178
+ }
179
+ function deltaPercent(current, previous) {
180
+ if (previous == null || previous === 0) return null;
181
+ return (current - previous) / Math.abs(previous) * 100;
182
+ }
183
+ function trendOf(current, previous) {
184
+ if (previous == null) return void 0;
185
+ if (current > previous) return "up";
186
+ if (current < previous) return "down";
187
+ return "neutral";
188
+ }
189
+ function formatCompact(value, locale = "pt-BR") {
190
+ return new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }).format(value);
191
+ }
192
+ function formatAnalyticsValue(value, format, currency, opts) {
193
+ const locale = currency?.locale ?? "pt-BR";
194
+ if (format === "date") {
195
+ if (!value) return "\u2014";
196
+ const d = new Date(String(value));
197
+ return Number.isNaN(d.getTime()) ? String(value) : d.toLocaleDateString(locale);
198
+ }
199
+ if (format === "text" || typeof value === "string" && format == null) {
200
+ return value == null || value === "" ? "\u2014" : String(value);
201
+ }
202
+ const n = num(value);
203
+ switch (format) {
204
+ case "currency":
205
+ return opts?.compact && Math.abs(n) >= 1e4 ? `${currency?.symbol ?? "R$"}${formatCompact(n, locale)}` : new Intl.NumberFormat(locale, {
206
+ style: "currency",
207
+ currency: currency?.code ?? "BRL",
208
+ maximumFractionDigits: 2
209
+ }).format(n);
210
+ case "percent":
211
+ return `${new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(n)}%`;
212
+ case "duration":
213
+ return formatDuration(n);
214
+ default:
215
+ return opts?.compact && Math.abs(n) >= 1e4 ? formatCompact(n, locale) : new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(n);
216
+ }
217
+ }
218
+ function formatDuration(hours) {
219
+ if (!Number.isFinite(hours) || hours <= 0) return "0s";
220
+ const totalMinutes = Math.round(hours * 60);
221
+ if (totalMinutes < 1) return `${Math.round(hours * 3600)}s`;
222
+ if (totalMinutes < 60) return `${totalMinutes}min`;
223
+ const h = Math.floor(totalMinutes / 60);
224
+ const m = totalMinutes % 60;
225
+ if (h < 24) return m ? `${h}h ${m}min` : `${h}h`;
226
+ const d = Math.floor(h / 24);
227
+ return `${d}d ${h % 24}h`;
228
+ }
229
+ function formatBucket(value, grain, locale = "pt-BR") {
230
+ if (value == null) return "\u2014";
231
+ const d = new Date(String(value));
232
+ if (Number.isNaN(d.getTime())) return String(value);
233
+ switch (grain) {
234
+ case "hour":
235
+ return d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
236
+ case "month":
237
+ return d.toLocaleDateString(locale, { month: "short", year: "2-digit" });
238
+ case "quarter":
239
+ return `Q${Math.floor(d.getMonth() / 3) + 1} ${d.getFullYear()}`;
240
+ case "year":
241
+ return String(d.getFullYear());
242
+ default:
243
+ return d.toLocaleDateString(locale, { day: "2-digit", month: "short" });
244
+ }
245
+ }
246
+ function toBreakdown(rows, categoryKey, measureKey, format, currency, limit) {
247
+ const mapped = rows.map((row) => ({
248
+ label: row[categoryKey] == null || row[categoryKey] === "" ? "\u2014" : String(row[categoryKey]),
249
+ value: num(row[measureKey])
250
+ }));
251
+ const max = mapped.reduce((m, r) => Math.max(m, Math.abs(r.value)), 0);
252
+ const sliced = limit ? mapped.slice(0, limit) : mapped;
253
+ return sliced.map((r) => ({
254
+ ...r,
255
+ share: max === 0 ? 0 : Math.abs(r.value) / max,
256
+ formatted: formatAnalyticsValue(r.value, format, currency)
257
+ }));
258
+ }
259
+ function vizMeasures(card) {
260
+ const all = card.query.measures ?? [];
261
+ const wanted = card.viz.measures;
262
+ if (!wanted?.length) return all;
263
+ return wanted.map((key) => all.find((m) => m.key === key)).filter((m) => Boolean(m));
264
+ }
265
+ function vizCategory(card) {
266
+ const dims = card.query.dimensions ?? [];
267
+ if (card.viz.categoryKey) return dims.find((d) => d.key === card.viz.categoryKey) ?? dims[0];
268
+ return dims[0];
269
+ }
270
+ function toChartData(rows, categoryKey, grain, locale = "pt-BR") {
271
+ return rows.map((row) => {
272
+ const out = {};
273
+ for (const [k, v] of Object.entries(row)) {
274
+ if (k === categoryKey) continue;
275
+ out[k] = typeof v === "number" ? v : num(v);
276
+ }
277
+ out[categoryKey] = grain ? formatBucket(row[categoryKey], grain, locale) : row[categoryKey] == null || row[categoryKey] === "" ? "\u2014" : String(row[categoryKey]);
278
+ return out;
279
+ });
280
+ }
281
+ function toSpark(result, measureKey) {
282
+ if (!result?.rows?.length || result.rows.length < 2) return void 0;
283
+ return result.rows.map((row) => num(row[measureKey]));
284
+ }
285
+ var AnalyticsContext = React2.createContext(null);
286
+ function useAnalyticsContext() {
287
+ const ctx = React2.useContext(AnalyticsContext);
288
+ if (!ctx) throw new Error("useAnalyticsContext must be used inside <AnalyticsContextProvider>");
289
+ return ctx;
290
+ }
291
+ function useAnalyticsContextOptional() {
292
+ return React2.useContext(AnalyticsContext);
293
+ }
294
+ function AnalyticsContextProvider({
295
+ provider,
296
+ currency,
297
+ defaultPreset = "last_30_days",
298
+ defaultCompare = "previous_period",
299
+ defaultPageSize = 50,
300
+ onOpenCard,
301
+ createdBy,
302
+ children
303
+ }) {
304
+ const [preset, setPresetState] = React2.useState(defaultPreset);
305
+ const [refreshToken, setRefreshToken] = React2.useState(0);
306
+ const [state, setState] = React2.useState(() => ({
307
+ dateRange: resolveRangePreset(defaultPreset),
308
+ compare: defaultCompare,
309
+ filters: {},
310
+ page: 1,
311
+ pageSize: defaultPageSize
312
+ }));
313
+ const setPreset = React2.useCallback((next) => {
314
+ setPresetState(next);
315
+ if (next !== "custom") {
316
+ setState((s) => ({ ...s, dateRange: resolveRangePreset(next), page: 1 }));
317
+ }
318
+ }, []);
319
+ const setDateRange = React2.useCallback((dateRange) => {
320
+ setPresetState("custom");
321
+ setState((s) => ({ ...s, dateRange, page: 1 }));
322
+ }, []);
323
+ const setCompare = React2.useCallback((compare) => {
324
+ setState((s) => ({ ...s, compare }));
325
+ }, []);
326
+ const setFilter = React2.useCallback((key, value2) => {
327
+ setState((s) => {
328
+ const filters = { ...s.filters };
329
+ if (value2 == null || value2 === "" || Array.isArray(value2) && value2.length === 0) delete filters[key];
330
+ else filters[key] = value2;
331
+ return { ...s, filters, page: 1 };
332
+ });
333
+ }, []);
334
+ const setFilters = React2.useCallback((filters) => {
335
+ setState((s) => ({ ...s, filters, page: 1 }));
336
+ }, []);
337
+ const clearFilters = React2.useCallback(() => {
338
+ setState((s) => ({ ...s, filters: {}, page: 1 }));
339
+ }, []);
340
+ const setPage = React2.useCallback((page) => {
341
+ setState((s) => ({ ...s, page: Math.max(1, page) }));
342
+ }, []);
343
+ const setPageSize = React2.useCallback((pageSize) => {
344
+ setState((s) => ({ ...s, pageSize, page: 1 }));
345
+ }, []);
346
+ const setSort = React2.useCallback((sortBy, sortDir) => {
347
+ setState((s) => ({ ...s, sortBy, sortDir, page: 1 }));
348
+ }, []);
349
+ const setSearch = React2.useCallback((search) => {
350
+ setState((s) => ({ ...s, search, page: 1 }));
351
+ }, []);
352
+ const refresh = React2.useCallback(() => setRefreshToken((t) => t + 1), []);
353
+ const deduped = React2.useMemo(() => {
354
+ const inflight = /* @__PURE__ */ new Map();
355
+ return {
356
+ run(card, queryState) {
357
+ const key = JSON.stringify([card.id, queryState, refreshToken]);
358
+ const existing = inflight.get(key);
359
+ if (existing) return existing;
360
+ const promise = provider.run(card, queryState).finally(() => {
361
+ inflight.delete(key);
362
+ });
363
+ inflight.set(key, promise);
364
+ return promise;
365
+ }
366
+ };
367
+ }, [provider, refreshToken]);
368
+ const value = React2.useMemo(() => ({
369
+ provider: deduped,
370
+ currency: currency ?? { code: "BRL", locale: "pt-BR", symbol: "R$" },
371
+ state,
372
+ preset,
373
+ setPreset,
374
+ setDateRange,
375
+ setCompare,
376
+ setFilter,
377
+ setFilters,
378
+ clearFilters,
379
+ setPage,
380
+ setPageSize,
381
+ setSort,
382
+ setSearch,
383
+ refreshToken,
384
+ refresh,
385
+ onOpenCard,
386
+ createdBy
387
+ }), [
388
+ deduped,
389
+ currency,
390
+ state,
391
+ preset,
392
+ setPreset,
393
+ setDateRange,
394
+ setCompare,
395
+ setFilter,
396
+ setFilters,
397
+ clearFilters,
398
+ setPage,
399
+ setPageSize,
400
+ setSort,
401
+ setSearch,
402
+ refreshToken,
403
+ refresh,
404
+ onOpenCard,
405
+ createdBy
406
+ ]);
407
+ return /* @__PURE__ */ jsx(AnalyticsContext.Provider, { value, children });
408
+ }
409
+ function withResolvedGrain(card, range) {
410
+ const dims = card.query.dimensions;
411
+ if (!dims?.some((d) => d.grain === "auto")) return card;
412
+ const grain = autoGrain(range);
413
+ return {
414
+ ...card,
415
+ query: {
416
+ ...card.query,
417
+ dimensions: dims.map((d) => d.grain === "auto" ? { ...d, grain } : d)
418
+ }
419
+ };
420
+ }
421
+ function useAnalyticsCard(card, options) {
422
+ const { provider, state, refreshToken } = useAnalyticsContext();
423
+ const [result, setResult] = React2.useState(null);
424
+ const [loading, setLoading] = React2.useState(false);
425
+ const [error, setError] = React2.useState(null);
426
+ const [engineMissing, setEngineMissing] = React2.useState(false);
427
+ const enabled = (options?.enabled ?? true) && card.available !== false;
428
+ const merged = React2.useMemo(
429
+ () => ({ ...state, ...options?.stateOverride }),
430
+ [state, options?.stateOverride]
431
+ );
432
+ const resolved = React2.useMemo(() => withResolvedGrain(card, merged.dateRange), [card, merged.dateRange]);
433
+ const key = React2.useMemo(
434
+ () => JSON.stringify([resolved.id, merged, refreshToken]),
435
+ [resolved.id, merged, refreshToken]
436
+ );
437
+ React2.useEffect(() => {
438
+ if (!enabled) {
439
+ setResult(null);
440
+ return;
441
+ }
442
+ let cancelled = false;
443
+ setLoading(true);
444
+ setError(null);
445
+ setEngineMissing(false);
446
+ const comparison = resolved.compare ? comparisonRange(merged.dateRange, merged.compare) : null;
447
+ Promise.all([
448
+ provider.run(resolved, merged),
449
+ comparison ? provider.run(resolved, { ...merged, dateRange: comparison }).catch(() => null) : Promise.resolve(null)
450
+ ]).then(([current, previous]) => {
451
+ if (cancelled) return;
452
+ setResult({
453
+ ...current,
454
+ rows: resolveRowRatios(current.rows, resolved.query.measures),
455
+ previous: previous ? { rows: previous.rows, summary: previous.summary } : void 0
456
+ });
457
+ }).catch((err) => {
458
+ if (cancelled) return;
459
+ setResult(null);
460
+ setError(errorMessage(err));
461
+ setEngineMissing(isEngineMissing(err));
462
+ }).finally(() => {
463
+ if (!cancelled) setLoading(false);
464
+ });
465
+ return () => {
466
+ cancelled = true;
467
+ };
468
+ }, [key, enabled, provider]);
469
+ const refetch = React2.useCallback(() => {
470
+ setResult(null);
471
+ setLoading(true);
472
+ }, []);
473
+ const summary = React2.useMemo(
474
+ () => resolveRatios(result?.summary, resolved.query.measures),
475
+ [result?.summary, resolved.query.measures]
476
+ );
477
+ const previousSummary = React2.useMemo(
478
+ () => result?.previous ? resolveRatios(result.previous.summary, resolved.query.measures) : null,
479
+ [result?.previous, resolved.query.measures]
480
+ );
481
+ return { result, summary, previousSummary, loading, error, engineMissing, refetch };
482
+ }
483
+ var CHART_TYPES = { line: "line", bar: "bar", area: "area", donut: "pie" };
484
+ function CardChrome({
485
+ card,
486
+ hideOpen,
487
+ children,
488
+ dense
489
+ }) {
490
+ const { onOpenCard } = useAnalyticsContext();
491
+ const canOpen = Boolean(onOpenCard) && !hideOpen;
492
+ return /* @__PURE__ */ jsxs(Card, { className: "group flex h-full flex-col", children: [
493
+ /* @__PURE__ */ jsxs("div", { className: cn("flex items-start justify-between gap-2", dense ? "px-4 pt-3" : "px-5 pt-4"), children: [
494
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
495
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-muted-foreground", children: card.name }),
496
+ card.description ? /* @__PURE__ */ jsx(Hint, { label: card.description, side: "top", children: /* @__PURE__ */ jsx(
497
+ "span",
498
+ {
499
+ tabIndex: 0,
500
+ role: "note",
501
+ "aria-label": card.description,
502
+ className: "shrink-0 text-muted-foreground/60 hover:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
503
+ children: /* @__PURE__ */ jsx(Info, { className: "h-3.5 w-3.5" })
504
+ }
505
+ ) }) : null
506
+ ] }),
507
+ canOpen ? /* @__PURE__ */ jsx(Hint, { label: "Open as report", side: "top", children: /* @__PURE__ */ jsx(
508
+ "button",
509
+ {
510
+ type: "button",
511
+ onClick: () => onOpenCard?.(card),
512
+ "aria-label": `Open ${card.name} as a report`,
513
+ className: "shrink-0 rounded-md p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100",
514
+ children: /* @__PURE__ */ jsx(ArrowUpRight, { className: "h-4 w-4" })
515
+ }
516
+ ) }) : null
517
+ ] }),
518
+ /* @__PURE__ */ jsx(CardContent, { className: cn("flex-1", dense ? "p-4 pt-2" : "p-5 pt-2"), children })
519
+ ] });
520
+ }
521
+ function Spark({ data, color }) {
522
+ const gid = React2.useId();
523
+ if (data.length < 2) return null;
524
+ const W = 120, H = 28, P = 2;
525
+ const min = Math.min(...data);
526
+ const max = Math.max(...data);
527
+ const span = max - min || 1;
528
+ const step = (W - P * 2) / (data.length - 1);
529
+ const pts = data.map((d, i) => [P + i * step, P + (H - P * 2) * (1 - (d - min) / span)]);
530
+ const line = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" ");
531
+ const area = `${line} L${pts[pts.length - 1][0].toFixed(1)},${H} L${pts[0][0].toFixed(1)},${H} Z`;
532
+ return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${W} ${H}`, className: "h-7 w-full", preserveAspectRatio: "none", "aria-hidden": true, children: [
533
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
534
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.22" }),
535
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
536
+ ] }) }),
537
+ /* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
538
+ /* @__PURE__ */ jsx("path", { d: line, fill: "none", stroke: color, strokeWidth: "1.5", vectorEffect: "non-scaling-stroke", strokeLinejoin: "round" })
539
+ ] });
540
+ }
541
+ function TrendChip({ current, previous }) {
542
+ const delta = deltaPercent(current, previous);
543
+ const trend = trendOf(current, previous);
544
+ if (!trend || trend === "neutral" || delta == null) {
545
+ return /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" });
546
+ }
547
+ const Icon = trend === "up" ? TrendingUp : TrendingDown;
548
+ return /* @__PURE__ */ jsxs("span", { className: cn(
549
+ "inline-flex items-center gap-0.5 text-xs font-medium",
550
+ trend === "up" ? "text-emerald-600" : "text-rose-600"
551
+ ), children: [
552
+ /* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
553
+ `${delta > 0 ? "+" : ""}${delta.toFixed(1)}%`
554
+ ] });
555
+ }
556
+ function BreakdownList({
557
+ rows,
558
+ categoryKey,
559
+ measure,
560
+ currency,
561
+ limit
562
+ }) {
563
+ const items = toBreakdown(rows, categoryKey, measure.key, measure.format, currency, limit);
564
+ if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
565
+ return /* @__PURE__ */ jsx("ul", { className: "space-y-2.5", children: items.map((item, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-3 text-sm", children: [
566
+ /* @__PURE__ */ jsx("span", { className: "w-2/5 truncate text-muted-foreground", title: item.label, children: item.label }),
567
+ /* @__PURE__ */ jsx("span", { className: "h-1.5 flex-1 overflow-hidden rounded-full bg-muted", role: "presentation", children: /* @__PURE__ */ jsx(
568
+ "span",
569
+ {
570
+ className: "block h-full rounded-full bg-[hsl(var(--info))]",
571
+ style: { width: `${Math.max(item.share * 100, item.value === 0 ? 0 : 2)}%` }
572
+ }
573
+ ) }),
574
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 tabular-nums font-medium", children: item.formatted })
575
+ ] }, `${item.label}-${i}`)) });
576
+ }
577
+ function MiniTable({
578
+ rows,
579
+ card,
580
+ currency,
581
+ limit
582
+ }) {
583
+ const dims = card.query.dimensions ?? [];
584
+ const measures = vizMeasures(card);
585
+ const cols = [
586
+ ...dims.map((d) => ({ key: d.key, label: d.label, format: d.format, align: "left" })),
587
+ ...measures.map((m) => ({ key: m.key, label: m.label, format: m.format, align: "right" }))
588
+ ];
589
+ const shown = limit ? rows.slice(0, limit) : rows;
590
+ if (shown.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
591
+ return /* @__PURE__ */ jsx("div", { className: "-mx-1 overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
592
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b text-xs text-muted-foreground", children: cols.map((c) => /* @__PURE__ */ jsx("th", { className: cn("px-1 pb-2 font-medium", c.align === "right" ? "text-right" : "text-left"), children: c.label }, c.key)) }) }),
593
+ /* @__PURE__ */ jsx("tbody", { children: shown.map((row, i) => /* @__PURE__ */ jsx("tr", { className: "border-b border-border/50 last:border-0", children: cols.map((c) => /* @__PURE__ */ jsx("td", { className: cn("px-1 py-2", c.align === "right" ? "text-right tabular-nums" : "truncate"), children: formatAnalyticsValue(row[c.key], c.format, currency) }, c.key)) }, i)) })
594
+ ] }) });
595
+ }
596
+ function EmptyState({ label = "No data for this date range" }) {
597
+ return /* @__PURE__ */ jsx("div", { className: "flex h-full min-h-[80px] items-center justify-center text-center text-sm text-muted-foreground", children: label });
598
+ }
599
+ function AnalyticsCard({ card, className, hideOpen }) {
600
+ const { currency } = useAnalyticsContext();
601
+ const { result, summary, previousSummary, loading, error, engineMissing } = useAnalyticsCard(card);
602
+ const measures = vizMeasures(card);
603
+ const category = vizCategory(card);
604
+ const rows = result?.rows ?? [];
605
+ const body = (() => {
606
+ if (card.available === false) {
607
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-[80px] flex-col items-center justify-center gap-2 text-center", children: [
608
+ /* @__PURE__ */ jsx(Construction, { className: "h-5 w-5 text-muted-foreground" }),
609
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Not connected yet" })
610
+ ] });
611
+ }
612
+ if (loading && !result) {
613
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
614
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-24" }),
615
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-16" })
616
+ ] });
617
+ }
618
+ if (engineMissing) {
619
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-[80px] flex-col items-center justify-center gap-1.5 text-center", children: [
620
+ /* @__PURE__ */ jsx(DatabaseZap, { className: "h-5 w-5 text-muted-foreground" }),
621
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium", children: "Analytics engine not installed" }),
622
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "Run migration 0039 on this project." })
623
+ ] });
624
+ }
625
+ if (error) {
626
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-[60px] items-start gap-1.5 text-xs text-destructive", children: [
627
+ /* @__PURE__ */ jsx(AlertCircle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
628
+ /* @__PURE__ */ jsx("span", { className: "break-words", children: error })
629
+ ] });
630
+ }
631
+ switch (card.viz.type) {
632
+ case "kpi": {
633
+ const measure = measures[0];
634
+ if (!measure) return /* @__PURE__ */ jsx(EmptyState, {});
635
+ const value = num(summary[measure.key]);
636
+ const previous = previousSummary ? num(previousSummary[measure.key]) : null;
637
+ const spark = card.viz.spark ? toSpark(result, measure.key) : void 0;
638
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col justify-between gap-2", children: [
639
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
640
+ /* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold tracking-tight tabular-nums", children: formatAnalyticsValue(value, measure.format, currency, { compact: true }) }),
641
+ /* @__PURE__ */ jsx(TrendChip, { current: value, previous })
642
+ ] }),
643
+ spark ? /* @__PURE__ */ jsx(Spark, { data: spark, color: "hsl(var(--info))" }) : null
644
+ ] });
645
+ }
646
+ case "breakdown": {
647
+ const measure = measures[0];
648
+ if (!measure || !category) return /* @__PURE__ */ jsx(EmptyState, {});
649
+ return /* @__PURE__ */ jsx(
650
+ BreakdownList,
651
+ {
652
+ rows,
653
+ categoryKey: category.key,
654
+ measure,
655
+ currency,
656
+ limit: card.viz.limit
657
+ }
658
+ );
659
+ }
660
+ case "table":
661
+ return /* @__PURE__ */ jsx(MiniTable, { rows, card, currency, limit: card.viz.limit ?? 5 });
662
+ case "funnel": {
663
+ const measure = measures[0];
664
+ if (!measure || !category) return /* @__PURE__ */ jsx(EmptyState, {});
665
+ const first = num(rows[0]?.[measure.key]) || 1;
666
+ return /* @__PURE__ */ jsx("ul", { className: "space-y-2.5", children: rows.map((row, i) => {
667
+ const value = num(row[measure.key]);
668
+ return /* @__PURE__ */ jsxs("li", { className: "text-sm", children: [
669
+ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-baseline justify-between gap-2", children: [
670
+ /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: String(row[category.key] ?? "\u2014") }),
671
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 tabular-nums font-medium", children: [
672
+ formatAnalyticsValue(value, measure.format, currency),
673
+ /* @__PURE__ */ jsxs("span", { className: "ml-1.5 text-xs text-muted-foreground", children: [
674
+ (value / first * 100).toFixed(0),
675
+ "%"
676
+ ] })
677
+ ] })
678
+ ] }),
679
+ /* @__PURE__ */ jsx("span", { className: "block h-1.5 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx("span", { className: "block h-full rounded-full bg-[hsl(var(--info))]", style: { width: `${value / first * 100}%` } }) })
680
+ ] }, i);
681
+ }) });
682
+ }
683
+ default: {
684
+ if (rows.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
685
+ if (!category) return /* @__PURE__ */ jsx(EmptyState, {});
686
+ const chartType = CHART_TYPES[card.viz.type] ?? "line";
687
+ const data = toChartData(rows, category.key, category.grain, currency.locale);
688
+ const series = measures.map((m) => ({ dataKey: m.key, label: m.label }));
689
+ return /* @__PURE__ */ jsx(
690
+ ChartWidget,
691
+ {
692
+ type: chartType,
693
+ data,
694
+ series,
695
+ categoryKey: category.key,
696
+ height: card.viz.height ?? 200,
697
+ className: "border-0 bg-transparent shadow-none [&>div]:p-0"
698
+ }
699
+ );
700
+ }
701
+ }
702
+ })();
703
+ const dense = card.viz.type === "kpi";
704
+ return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(CardChrome, { card, hideOpen, dense, children: body }) });
705
+ }
706
+ function defaultSpan(card) {
707
+ if (card.dashboard?.span) return card.dashboard.span;
708
+ switch (card.viz.type) {
709
+ case "kpi":
710
+ return 1;
711
+ case "table":
712
+ return 4;
713
+ default:
714
+ return 2;
715
+ }
716
+ }
717
+ function AnalyticsGrid({ cards, className }) {
718
+ const items = React2.useMemo(
719
+ () => cards.map((card) => ({
720
+ id: card.id,
721
+ span: defaultSpan(card),
722
+ node: /* @__PURE__ */ jsx(AnalyticsCard, { card })
723
+ })),
724
+ [cards]
725
+ );
726
+ return /* @__PURE__ */ jsx(DashboardGrid, { items, className });
727
+ }
728
+ function collectFilters(cards) {
729
+ const byKey = /* @__PURE__ */ new Map();
730
+ for (const card of cards) {
731
+ for (const filter of card.query.filters ?? []) {
732
+ if (!byKey.has(filter.key)) byKey.set(filter.key, filter);
733
+ }
734
+ }
735
+ return [...byKey.values()];
736
+ }
737
+ var PRESETS = [
738
+ { value: "today", label: "Today" },
739
+ { value: "yesterday", label: "Yesterday" },
740
+ { value: "last_7_days", label: "Last 7 days" },
741
+ { value: "last_30_days", label: "Last 30 days" },
742
+ { value: "last_90_days", label: "Last 90 days" },
743
+ { value: "this_month", label: "This month" },
744
+ { value: "last_month", label: "Last month" },
745
+ { value: "this_quarter", label: "This quarter" },
746
+ { value: "this_year", label: "This year" }
747
+ ];
748
+ var COMPARE_OPTIONS = [
749
+ { value: "previous_period", label: "Previous period" },
750
+ { value: "previous_year", label: "Previous year" },
751
+ { value: "none", label: "No comparison" }
752
+ ];
753
+ var UNGROUPED = "General";
754
+ var ControlButton = React2.forwardRef(function ControlButton2({ icon: Icon, children, ...props }, ref) {
755
+ return /* @__PURE__ */ jsxs(Button, { ref, variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs font-normal", ...props, children: [
756
+ Icon ? /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }) : null,
757
+ children
758
+ ] });
759
+ });
760
+ function useFilterOptions(filter) {
761
+ const [options, setOptions] = React2.useState(
762
+ Array.isArray(filter.options) ? filter.options : []
763
+ );
764
+ React2.useEffect(() => {
765
+ if (typeof filter.options !== "function") {
766
+ setOptions(Array.isArray(filter.options) ? filter.options : []);
767
+ return;
768
+ }
769
+ let cancelled = false;
770
+ void filter.options().then((next) => {
771
+ if (!cancelled) setOptions(next);
772
+ }).catch(() => {
773
+ });
774
+ return () => {
775
+ cancelled = true;
776
+ };
777
+ }, [filter.options]);
778
+ return options;
779
+ }
780
+ function FilterValues({
781
+ filter,
782
+ draft,
783
+ onChange
784
+ }) {
785
+ const options = useFilterOptions(filter);
786
+ const current = draft[filter.key];
787
+ if (filter.type === "text" || filter.type === "number") {
788
+ return /* @__PURE__ */ jsx(
789
+ Input,
790
+ {
791
+ type: filter.type === "number" ? "number" : "text",
792
+ value: current == null ? "" : String(current),
793
+ placeholder: filter.placeholder,
794
+ onChange: (e) => onChange(filter.key, e.target.value),
795
+ className: "h-8 text-sm"
796
+ }
797
+ );
798
+ }
799
+ if (filter.type === "boolean") {
800
+ return /* @__PURE__ */ jsx("div", { className: "space-y-1", children: [{ label: "Yes", value: "true" }, { label: "No", value: "false" }].map((o) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted", children: [
801
+ /* @__PURE__ */ jsx(
802
+ "input",
803
+ {
804
+ type: "radio",
805
+ name: filter.key,
806
+ checked: String(current) === o.value,
807
+ onChange: () => onChange(filter.key, o.value),
808
+ className: "h-3.5 w-3.5"
809
+ }
810
+ ),
811
+ o.label
812
+ ] }, o.value)) });
813
+ }
814
+ const multi = filter.type === "multiselect";
815
+ const selected = new Set(Array.isArray(current) ? current.map(String) : current != null ? [String(current)] : []);
816
+ if (options.length === 0) {
817
+ return /* @__PURE__ */ jsx("p", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: "No options available" });
818
+ }
819
+ return /* @__PURE__ */ jsx("div", { className: "max-h-56 space-y-0.5 overflow-y-auto", children: options.map((o) => /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted", children: [
820
+ /* @__PURE__ */ jsx(
821
+ "input",
822
+ {
823
+ type: multi ? "checkbox" : "radio",
824
+ name: filter.key,
825
+ checked: selected.has(o.value),
826
+ onChange: () => {
827
+ if (!multi) {
828
+ onChange(filter.key, selected.has(o.value) ? void 0 : o.value);
829
+ return;
830
+ }
831
+ const next = new Set(selected);
832
+ if (next.has(o.value)) next.delete(o.value);
833
+ else next.add(o.value);
834
+ onChange(filter.key, [...next]);
835
+ },
836
+ className: "h-3.5 w-3.5"
837
+ }
838
+ ),
839
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: o.label })
840
+ ] }, o.value)) });
841
+ }
842
+ function AnalyticsFilterPicker({ filters }) {
843
+ const { state, setFilters, clearFilters } = useAnalyticsContext();
844
+ const [open, setOpen] = React2.useState(false);
845
+ const [search, setSearch] = React2.useState("");
846
+ const [openGroup, setOpenGroup] = React2.useState(null);
847
+ const [draft, setDraft] = React2.useState(state.filters);
848
+ React2.useEffect(() => {
849
+ if (open) setDraft(state.filters);
850
+ }, [open, state.filters]);
851
+ const groups = React2.useMemo(() => {
852
+ const q = search.trim().toLowerCase();
853
+ const matching = q ? filters.filter((f) => f.label.toLowerCase().includes(q)) : filters;
854
+ const map = /* @__PURE__ */ new Map();
855
+ for (const f of matching) {
856
+ const key = f.group ?? UNGROUPED;
857
+ map.set(key, [...map.get(key) ?? [], f]);
858
+ }
859
+ return [...map.entries()].sort(([a], [b]) => a.localeCompare(b));
860
+ }, [filters, search]);
861
+ const activeCount = Object.keys(state.filters).length;
862
+ if (filters.length === 0) return null;
863
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
864
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs font-normal", children: [
865
+ /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }),
866
+ "Filters",
867
+ activeCount > 0 ? /* @__PURE__ */ jsx("span", { className: "ml-0.5 rounded-full bg-primary px-1.5 text-[10px] font-medium text-primary-foreground", children: activeCount }) : null
868
+ ] }) }),
869
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "w-80 p-0", children: [
870
+ /* @__PURE__ */ jsx("div", { className: "border-b p-2", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
871
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
872
+ /* @__PURE__ */ jsx(
873
+ Input,
874
+ {
875
+ value: search,
876
+ onChange: (e) => setSearch(e.target.value),
877
+ placeholder: "Search filters",
878
+ className: "h-8 pl-8 text-sm"
879
+ }
880
+ )
881
+ ] }) }),
882
+ /* @__PURE__ */ jsx("div", { className: "max-h-80 overflow-y-auto p-1", children: groups.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-sm text-muted-foreground", children: "No filters match" }) : groups.map(([group, groupFilters]) => /* @__PURE__ */ jsxs("div", { children: [
883
+ /* @__PURE__ */ jsxs(
884
+ "button",
885
+ {
886
+ type: "button",
887
+ onClick: () => setOpenGroup((g) => g === group ? null : group),
888
+ "aria-expanded": openGroup === group,
889
+ className: "flex w-full items-center justify-between rounded-md px-2 py-2 text-sm hover:bg-muted",
890
+ children: [
891
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: group }),
892
+ /* @__PURE__ */ jsx(ChevronRight, { className: cn("h-3.5 w-3.5 text-muted-foreground transition-transform", openGroup === group && "rotate-90") })
893
+ ]
894
+ }
895
+ ),
896
+ openGroup === group ? /* @__PURE__ */ jsx("div", { className: "space-y-3 px-2 pb-3 pt-1", children: groupFilters.map((f) => /* @__PURE__ */ jsxs("div", { children: [
897
+ /* @__PURE__ */ jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: f.label }),
898
+ /* @__PURE__ */ jsx(
899
+ FilterValues,
900
+ {
901
+ filter: f,
902
+ draft,
903
+ onChange: (key, value) => setDraft((d) => {
904
+ const next = { ...d };
905
+ if (value == null || value === "" || Array.isArray(value) && value.length === 0) delete next[key];
906
+ else next[key] = value;
907
+ return next;
908
+ })
909
+ }
910
+ )
911
+ ] }, f.key)) }) : null
912
+ ] }, group)) }),
913
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-t p-2", children: [
914
+ /* @__PURE__ */ jsx(
915
+ Button,
916
+ {
917
+ variant: "ghost",
918
+ size: "sm",
919
+ className: "h-8 text-xs",
920
+ onClick: () => {
921
+ setDraft({});
922
+ clearFilters();
923
+ setOpen(false);
924
+ },
925
+ children: "Clear"
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx(Button, { size: "sm", className: "h-8 text-xs", onClick: () => {
929
+ setFilters(draft);
930
+ setOpen(false);
931
+ }, children: "Apply" })
932
+ ] })
933
+ ] })
934
+ ] });
935
+ }
936
+ function AnalyticsControls({ filters = [], showCompare = true, actions, className }) {
937
+ const { state, preset, setPreset, setCompare, refresh } = useAnalyticsContext();
938
+ const presetLabel = PRESETS.find((p) => p.value === preset)?.label ?? `${state.dateRange.from} \u2013 ${state.dateRange.to}`;
939
+ const compareLabel = COMPARE_OPTIONS.find((c) => c.value === (state.compare ?? "none"))?.label;
940
+ const activeFilters = Object.entries(state.filters);
941
+ const filterLabel = (key) => filters.find((f) => f.key === key)?.label ?? key;
942
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-wrap items-center gap-2", className), children: [
943
+ /* @__PURE__ */ jsxs(Dropdown, { children: [
944
+ /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsx(ControlButton, { icon: Calendar, children: presetLabel }) }),
945
+ /* @__PURE__ */ jsx(DropdownContent, { align: "start", className: "w-48", children: PRESETS.map((p) => /* @__PURE__ */ jsx(DropdownItem, { onSelect: () => setPreset(p.value), children: p.label }, p.value)) })
946
+ ] }),
947
+ showCompare ? /* @__PURE__ */ jsxs(Dropdown, { children: [
948
+ /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(ControlButton, { icon: GitCompareArrows, children: [
949
+ "Compare: ",
950
+ compareLabel
951
+ ] }) }),
952
+ /* @__PURE__ */ jsx(DropdownContent, { align: "start", className: "w-48", children: COMPARE_OPTIONS.map((c) => /* @__PURE__ */ jsx(DropdownItem, { onSelect: () => setCompare(c.value), children: c.label }, c.value)) })
953
+ ] }) : null,
954
+ /* @__PURE__ */ jsx(AnalyticsFilterPicker, { filters }),
955
+ activeFilters.map(([key, value]) => /* @__PURE__ */ jsx(ActiveFilterChip, { label: filterLabel(key), value, filterKey: key }, key)),
956
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
957
+ actions,
958
+ /* @__PURE__ */ jsx(
959
+ Button,
960
+ {
961
+ variant: "outline",
962
+ size: "sm",
963
+ onClick: refresh,
964
+ "aria-label": "Refresh",
965
+ className: "h-8 w-8 p-0",
966
+ children: /* @__PURE__ */ jsx(RefreshCw, { className: "h-3.5 w-3.5" })
967
+ }
968
+ )
969
+ ] })
970
+ ] });
971
+ }
972
+ function ActiveFilterChip({ label, value, filterKey }) {
973
+ const { setFilter } = useAnalyticsContext();
974
+ const text = Array.isArray(value) ? `${value.length} selected` : String(value);
975
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full border bg-muted/50 py-1 pl-2.5 pr-1 text-xs", children: [
976
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
977
+ label,
978
+ ":"
979
+ ] }),
980
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: text }),
981
+ /* @__PURE__ */ jsx(
982
+ "button",
983
+ {
984
+ type: "button",
985
+ onClick: () => setFilter(filterKey, void 0),
986
+ "aria-label": `Remove ${label} filter`,
987
+ className: "rounded-full p-0.5 hover:bg-muted",
988
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
989
+ }
990
+ )
991
+ ] });
992
+ }
993
+
994
+ export { AnalyticsCard, AnalyticsContextProvider, AnalyticsControls, AnalyticsFilterPicker, AnalyticsGrid, Avatar, AvatarFallback, AvatarImage, autoGrain, collectFilters, comparisonRange, deltaPercent, errorMessage, formatAnalyticsValue, formatBucket, formatCompact, formatDuration, isEngineMissing, num, parseDay, rangeLengthDays, resolveRangePreset, resolveRatios, resolveRowRatios, toBreakdown, toChartData, toDayString, toSpark, trendOf, useAnalyticsCard, useAnalyticsContext, useAnalyticsContextOptional, vizCategory, vizMeasures };
49
995
  //# sourceMappingURL=index.js.map
50
996
  //# sourceMappingURL=index.js.map