@fayz-ai/ui 0.21.0 → 0.23.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 (96) hide show
  1. package/dist/analytics/AnalyticsCard.d.ts.map +1 -1
  2. package/dist/analytics/AnalyticsControls.d.ts.map +1 -1
  3. package/dist/analytics/DateRangeControl.d.ts +14 -0
  4. package/dist/analytics/DateRangeControl.d.ts.map +1 -0
  5. package/dist/analytics/StatementTable.d.ts +22 -0
  6. package/dist/analytics/StatementTable.d.ts.map +1 -0
  7. package/dist/analytics/context.d.ts +8 -0
  8. package/dist/analytics/context.d.ts.map +1 -1
  9. package/dist/analytics/derive.d.ts +13 -0
  10. package/dist/analytics/derive.d.ts.map +1 -1
  11. package/dist/analytics/index.d.ts +7 -1
  12. package/dist/analytics/index.d.ts.map +1 -1
  13. package/dist/analytics/statement.d.ts +58 -0
  14. package/dist/analytics/statement.d.ts.map +1 -0
  15. package/dist/{chunk-E44WOPCR.js → chunk-HO2P2XB4.js} +428 -58
  16. package/dist/chunk-HO2P2XB4.js.map +1 -0
  17. package/dist/{chunk-P3YTL3MF.js → chunk-L4TXDKMN.js} +5 -5
  18. package/dist/chunk-L4TXDKMN.js.map +1 -0
  19. package/dist/{chunk-AIUHVTVK.js → chunk-OWN3GFDG.js} +5 -3
  20. package/dist/chunk-OWN3GFDG.js.map +1 -0
  21. package/dist/{chunk-EGHEU3U5.js → chunk-RRNMNFLF.js} +492 -789
  22. package/dist/chunk-RRNMNFLF.js.map +1 -0
  23. package/dist/chunk-UP7WVKSU.js +631 -0
  24. package/dist/chunk-UP7WVKSU.js.map +1 -0
  25. package/dist/{chunk-OYH7CEMB.js → chunk-XEUIYSW2.js} +330 -366
  26. package/dist/chunk-XEUIYSW2.js.map +1 -0
  27. package/dist/{chunk-MBGTFL4I.js → chunk-XKFYX3DG.js} +39 -8
  28. package/dist/chunk-XKFYX3DG.js.map +1 -0
  29. package/dist/dashboard/OnboardingWidget.d.ts +4 -1
  30. package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
  31. package/dist/dashboard/SidebarOnboarding.d.ts +11 -1
  32. package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -1
  33. package/dist/dashboard/index.js +4 -4
  34. package/dist/icons.js +1 -1
  35. package/dist/index.d.ts +3 -3
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1109 -166
  38. package/dist/index.js.map +1 -1
  39. package/dist/layout/AppShell.d.ts +7 -1
  40. package/dist/layout/AppShell.d.ts.map +1 -1
  41. package/dist/layout/ModulePage.d.ts.map +1 -1
  42. package/dist/layout/SaveBar.d.ts.map +1 -1
  43. package/dist/layout/ShellSearch.d.ts +53 -0
  44. package/dist/layout/ShellSearch.d.ts.map +1 -0
  45. package/dist/layout/Sidebar.d.ts +10 -6
  46. package/dist/layout/Sidebar.d.ts.map +1 -1
  47. package/dist/layout/Takeover.d.ts.map +1 -1
  48. package/dist/layout/index.d.ts +1 -0
  49. package/dist/layout/index.d.ts.map +1 -1
  50. package/dist/layout/index.js +4 -4
  51. package/dist/primitives/data-table.d.ts.map +1 -1
  52. package/dist/primitives/date-range-chip.d.ts +32 -0
  53. package/dist/primitives/date-range-chip.d.ts.map +1 -0
  54. package/dist/primitives/header-tabs.d.ts +7 -1
  55. package/dist/primitives/header-tabs.d.ts.map +1 -1
  56. package/dist/primitives/index.d.ts +2 -1
  57. package/dist/primitives/index.d.ts.map +1 -1
  58. package/dist/primitives/index.js +5 -5
  59. package/dist/primitives/list-view.d.ts +43 -1
  60. package/dist/primitives/list-view.d.ts.map +1 -1
  61. package/dist/primitives/modal.d.ts +2 -0
  62. package/dist/primitives/modal.d.ts.map +1 -1
  63. package/dist/primitives/page-actions.d.ts +14 -0
  64. package/dist/primitives/page-actions.d.ts.map +1 -1
  65. package/dist/primitives/page-toolbar.d.ts +16 -1
  66. package/dist/primitives/page-toolbar.d.ts.map +1 -1
  67. package/dist/primitives/search-select.d.ts +3 -1
  68. package/dist/primitives/search-select.d.ts.map +1 -1
  69. package/dist/primitives/segmented-control.d.ts.map +1 -1
  70. package/dist/primitives/stock-entry-form.d.ts +4 -1
  71. package/dist/primitives/stock-entry-form.d.ts.map +1 -1
  72. package/dist/stores/layout.store.d.ts +20 -1
  73. package/dist/stores/layout.store.d.ts.map +1 -1
  74. package/dist/styles.css +133 -0
  75. package/dist/tour/PracticeOverlay.d.ts +12 -0
  76. package/dist/tour/PracticeOverlay.d.ts.map +1 -0
  77. package/dist/tour/TourSpotlight.d.ts.map +1 -1
  78. package/dist/tour/confetti.d.ts +17 -0
  79. package/dist/tour/confetti.d.ts.map +1 -0
  80. package/dist/tour/index.d.ts +3 -0
  81. package/dist/tour/index.d.ts.map +1 -1
  82. package/dist/tour/practice.store.d.ts +28 -0
  83. package/dist/tour/practice.store.d.ts.map +1 -0
  84. package/dist/tour/tour.store.d.ts +4 -0
  85. package/dist/tour/tour.store.d.ts.map +1 -1
  86. package/dist/tour/types.d.ts +17 -0
  87. package/dist/tour/types.d.ts.map +1 -1
  88. package/package.json +2 -2
  89. package/dist/chunk-AIUHVTVK.js.map +0 -1
  90. package/dist/chunk-E44WOPCR.js.map +0 -1
  91. package/dist/chunk-EGHEU3U5.js.map +0 -1
  92. package/dist/chunk-HKGTF2YA.js +0 -117
  93. package/dist/chunk-HKGTF2YA.js.map +0 -1
  94. package/dist/chunk-MBGTFL4I.js.map +0 -1
  95. package/dist/chunk-OYH7CEMB.js.map +0 -1
  96. package/dist/chunk-P3YTL3MF.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,23 +1,24 @@
1
- import { Popover, PopoverTrigger, PopoverContent, Input, Hint, DatePicker } from './chunk-OYH7CEMB.js';
2
- export { Badge, Breadcrumb, BulkActionBar, ChevronPager, CurrencyInput, DatePicker, DotBadge, DraftCommitBar, FilterCheckBox, FilterCheckList, FilterCheckRow, FilterChip, FilterSection, Hint, Input, ListView, Markdown, MarkdownEditor, MiniCalendar, Modal, ModalBody, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, MultiSelect, PageActionBar, PhoneField, PhoneLink, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, RefreshButton, RowActions, SearchCombobox, SearchField, SearchSelect, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, StatTiles, StockEntryForm, Tabs, TabsContent, TabsList, TabsTrigger, Thumbnail, TimePicker, ToastProvider, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, WHATSAPP_BRAND_COLOR, WhatsAppIcon, badgeVariants, formatPhone, normalizePhone, renderMarkdown, setRowActionsLabel, toLocalISODate, toast, useRowSelection } from './chunk-OYH7CEMB.js';
3
- export { AppShellSkeleton, ContentSplit, ContentSplitTrigger, PageTrail, Takeover, navigateToCrumb, usePageTrail, usePageTrailParent, useTakeoverClose } from './chunk-P3YTL3MF.js';
4
- export { AppShell, BackStyleProvider, HeaderTabs, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeader, PageHeaderActions, PageHeaderCrumb, PageRail, PageTabs, PageTransition, RailNavItem, SaveBar, SaveBarProvider, SegmentedControl, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SubpageHeader, Topbar, bottomNavReachesAll, useBackHandler, useBackStyle, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition, useRailRow, useSaveBar } from './chunk-EGHEU3U5.js';
1
+ import { Input, DatePicker } from './chunk-XEUIYSW2.js';
2
+ export { Badge, Breadcrumb, BulkActionBar, ChevronPager, CurrencyInput, DatePicker, DateRangeChip, DotBadge, DraftCommitBar, FilterCheckBox, FilterCheckList, FilterCheckRow, FilterChip, FilterSection, Input, ListView, Markdown, MarkdownEditor, MiniCalendar, Modal, ModalBody, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalOverlay, ModalPortal, ModalTitle, ModalTrigger, MultiSelect, PhoneField, PhoneLink, RefreshButton, RowActions, SearchCombobox, SearchField, SearchSelect, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, StatTiles, StockEntryForm, Tabs, TabsContent, TabsList, TabsTrigger, Thumbnail, TimePicker, ToastProvider, WHATSAPP_BRAND_COLOR, WhatsAppIcon, badgeVariants, formatPhone, normalizePhone, renderMarkdown, setRowActionsLabel, toLocalISODate, toast, useRowSelection } from './chunk-XEUIYSW2.js';
3
+ export { AppShellSkeleton, ContentSplit, ContentSplitTrigger, PageTrail, Takeover, navigateToCrumb, usePageTrail, usePageTrailParent, useTakeoverClose } from './chunk-L4TXDKMN.js';
4
+ export { BackStyleProvider, HeaderTabs, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageRail, PageTabs, PageTransition, RailNavItem, SegmentedControl, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SubpageHeader, useBackStyle, useModuleLayout, useNavTransition } from './chunk-UP7WVKSU.js';
5
5
  export { ThemeProvider, createFayzTheme, createTheme, darkTheme, fayzThemePresets, lightTheme, useTheme, useThemeStore } from './chunk-VDAYIQX4.js';
6
- import { ChartWidget, Sparkline, DashboardGrid } from './chunk-MBGTFL4I.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-MBGTFL4I.js';
8
- import { Skeleton, ToolbarButton, PageToolbar, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, Card, CardContent, RangeControl, DropdownSeparator } from './chunk-E44WOPCR.js';
9
- export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DataTable, Dropdown, DropdownCheckboxItem, DropdownContent, DropdownGroup, DropdownItem, DropdownLabel, DropdownPortal, DropdownRadioGroup, DropdownRadioItem, DropdownSeparator, DropdownShortcut, DropdownSub, DropdownSubContent, DropdownSubTrigger, DropdownTrigger, PageToolbar, RangeControl, Skeleton, TableSkeleton, ToolbarAction, ToolbarButton, WIDE_QUERY, useIsWide } from './chunk-E44WOPCR.js';
10
- export { PageHeaderOutlet, PageTitlePortal, useHasPageActions, useHasPageHeaderBack, useHasPageHeaderHost, useHasPageHeaderOutlet, useLayoutStore, usePageHeader, usePageHeaderClaimed, usePageHeaderHost, useRegisterPageActions } from './chunk-HKGTF2YA.js';
6
+ import { ChartWidget, Sparkline, DashboardGrid } from './chunk-XKFYX3DG.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-XKFYX3DG.js';
8
+ import { Skeleton, useToolbarSurface, useToolbarPanelClose, Popover, PopoverTrigger, ToolbarButton, PopoverContent, PageToolbar, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, RangeControl, DropdownSeparator, Card, Hint, CardContent, Checkbox } from './chunk-HO2P2XB4.js';
9
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DataTable, Dropdown, DropdownCheckboxItem, DropdownContent, DropdownGroup, DropdownItem, DropdownLabel, DropdownPortal, DropdownRadioGroup, DropdownRadioItem, DropdownSeparator, DropdownShortcut, DropdownSub, DropdownSubContent, DropdownSubTrigger, DropdownTrigger, Hint, PageActionBar, PageToolbar, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, RangeControl, Skeleton, TableSkeleton, ToolbarAction, ToolbarButton, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, WIDE_QUERY, useIsWide, useToolbarPanelClose, useToolbarSurface } from './chunk-HO2P2XB4.js';
10
+ export { AppShell, PageHeader, PageHeaderCrumb, PageHeaderOutlet, PageTitlePortal, SaveBar, SaveBarProvider, ShellSearch, Sidebar, Topbar, bottomNavReachesAll, useBackHandler, useHasPageActions, useHasPageHeaderBack, useHasPageHeaderHost, useHasPageHeaderOutlet, useLayoutStore, useModuleHeaderActionsSlot, useModuleHeaderSlot, usePageHeader, usePageHeaderClaimed, usePageHeaderHost, useRailRow, useRegisterPageActions, useSaveBar } from './chunk-RRNMNFLF.js';
11
11
  import { errorMessage } from './chunk-OB2ULDBD.js';
12
12
  export { Wizard, WizardField, errorMessage, useWizard } from './chunk-OB2ULDBD.js';
13
13
  export { ConfirmDialog, ImageDrop, OptionCards, OptionChips, StepProgress, Switch } from './chunk-GLRLWBBF.js';
14
14
  import { cn, Button } from './chunk-OKZ6WQB3.js';
15
15
  export { Button, buttonVariants, cn } from './chunk-OKZ6WQB3.js';
16
- export { BRAND_MARKS, ChefControlMark, ICON_MAP, getLucideIcon, registerIcons } from './chunk-AIUHVTVK.js';
16
+ export { BRAND_MARKS, ChefControlMark, ICON_MAP, getLucideIcon, registerIcons } from './chunk-OWN3GFDG.js';
17
17
  import * as React2 from 'react';
18
+ import React2__default from 'react';
18
19
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
19
20
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
20
- import { Construction, DatabaseZap, AlertCircle, SlidersHorizontal, Search, ChevronRight, GitCompareArrows, Check, X, TrendingUp, TrendingDown, Info, ArrowUpRight } from 'lucide-react';
21
+ import { Construction, DatabaseZap, AlertCircle, Search, ChevronRight, SlidersHorizontal, GitCompareArrows, Check, X, TrendingUp, TrendingDown, Info, ArrowUpRight } from 'lucide-react';
21
22
  import { useTranslation, getCurrentLocale } from '@fayz-ai/core';
22
23
  import { createPortal } from 'react-dom';
23
24
  import { create } from 'zustand';
@@ -223,17 +224,18 @@ function formatBucket(value, grain, locale = "pt-BR") {
223
224
  if (value == null) return "\u2014";
224
225
  const d = new Date(String(value));
225
226
  if (Number.isNaN(d.getTime())) return String(value);
227
+ const utc = { timeZone: "UTC" };
226
228
  switch (grain) {
227
229
  case "hour":
228
230
  return d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
229
231
  case "month":
230
- return d.toLocaleDateString(locale, { month: "short", year: "2-digit" });
232
+ return d.toLocaleDateString(locale, { ...utc, month: "short", year: "2-digit" });
231
233
  case "quarter":
232
- return `Q${Math.floor(d.getMonth() / 3) + 1} ${d.getFullYear()}`;
234
+ return `Q${Math.floor(d.getUTCMonth() / 3) + 1} ${d.getUTCFullYear()}`;
233
235
  case "year":
234
- return String(d.getFullYear());
236
+ return String(d.getUTCFullYear());
235
237
  default:
236
- return d.toLocaleDateString(locale, { day: "2-digit", month: "short" });
238
+ return d.toLocaleDateString(locale, { ...utc, day: "2-digit", month: "short" });
237
239
  }
238
240
  }
239
241
  function toBreakdown(rows, categoryKey, measureKey, format, currency, limit) {
@@ -536,8 +538,454 @@ function useAnalyticsCardRows(card) {
536
538
  return rows.slice(0, limit);
537
539
  }, [provider, state, card]);
538
540
  }
541
+ var STATEMENT_ROW_CAP = 5e3;
542
+ function useAnalyticsAllRows(card, enabled = true) {
543
+ const fetchRows = useAnalyticsCardRows(card);
544
+ const { state } = useAnalyticsContext();
545
+ const [rows, setRows] = React2.useState([]);
546
+ const [loading, setLoading] = React2.useState(enabled);
547
+ const [error, setError] = React2.useState(null);
548
+ const key = JSON.stringify([
549
+ card.id,
550
+ state.dateRange,
551
+ state.filters,
552
+ state.search,
553
+ state.sortBy,
554
+ state.sortDir
555
+ ]);
556
+ React2.useEffect(() => {
557
+ if (!enabled) {
558
+ setLoading(false);
559
+ return;
560
+ }
561
+ let live = true;
562
+ setLoading(true);
563
+ setError(null);
564
+ fetchRows(STATEMENT_ROW_CAP).then((r) => {
565
+ if (live) {
566
+ setRows(r);
567
+ setLoading(false);
568
+ }
569
+ }).catch((e) => {
570
+ if (live) {
571
+ setError(errorMessage(e));
572
+ setLoading(false);
573
+ }
574
+ });
575
+ return () => {
576
+ live = false;
577
+ };
578
+ }, [key, enabled]);
579
+ return { rows, loading, error };
580
+ }
539
581
  var EXPORT_PAGE_SIZE = 1e3;
540
582
  var EXPORT_ROW_CAP = 2e4;
583
+
584
+ // src/analytics/statement.ts
585
+ function compareCodes(a, b) {
586
+ const pa = a.split(".");
587
+ const pb = b.split(".");
588
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
589
+ const sa = pa[i];
590
+ const sb = pb[i];
591
+ if (sa === void 0) return -1;
592
+ if (sb === void 0) return 1;
593
+ const na = Number(sa);
594
+ const nb = Number(sb);
595
+ if (Number.isFinite(na) && Number.isFinite(nb)) {
596
+ if (na !== nb) return na - nb;
597
+ } else if (sa !== sb) {
598
+ return sa < sb ? -1 : 1;
599
+ }
600
+ }
601
+ return 0;
602
+ }
603
+ function parentCode(code) {
604
+ const i = code.lastIndexOf(".");
605
+ return i < 0 ? "" : code.slice(0, i);
606
+ }
607
+ var UNCLASSIFIED = "\0sem-classificacao";
608
+ function emptyNode(code, label, synthetic) {
609
+ return {
610
+ key: code || label,
611
+ code,
612
+ label,
613
+ depth: code ? code.split(".").length - 1 : 0,
614
+ synthetic,
615
+ own: {},
616
+ rollup: {},
617
+ total: 0,
618
+ children: []
619
+ };
620
+ }
621
+ function add(bag, period, value) {
622
+ bag[period] = (bag[period] ?? 0) + value;
623
+ }
624
+ function rollup(node, periods) {
625
+ node.children.sort((a, b) => compareCodes(a.code, b.code));
626
+ node.rollup = { ...node.own };
627
+ for (const child of node.children) {
628
+ rollup(child, periods);
629
+ for (const p of periods) {
630
+ if (child.rollup[p]) add(node.rollup, p, child.rollup[p]);
631
+ }
632
+ }
633
+ node.total = periods.reduce((acc, p) => acc + (node.rollup[p] ?? 0), 0);
634
+ }
635
+ function buildStatement(rows, def) {
636
+ const periodSet = /* @__PURE__ */ new Set();
637
+ const bySection = /* @__PURE__ */ new Map();
638
+ for (const s of def.sections) bySection.set(s.value, /* @__PURE__ */ new Map());
639
+ for (const row of rows) {
640
+ const section = String(row[def.sectionKey] ?? "");
641
+ const bucket = bySection.get(section);
642
+ if (!bucket) continue;
643
+ const period = String(row[def.periodKey] ?? "");
644
+ const estrutura = !period;
645
+ if (!estrutura) periodSet.add(period);
646
+ const value = Number(row[def.valueKey] ?? 0);
647
+ if (!Number.isFinite(value)) continue;
648
+ const code = String(row[def.codeKey] ?? "").trim();
649
+ const label = String(row[def.labelKey] ?? "").trim();
650
+ if (estrutura) {
651
+ if (!code) continue;
652
+ let node2 = bucket.get(code);
653
+ if (!node2) {
654
+ node2 = emptyNode(code, label || code, false);
655
+ bucket.set(code, node2);
656
+ } else {
657
+ node2.synthetic = false;
658
+ if (label) node2.label = label;
659
+ }
660
+ continue;
661
+ }
662
+ if (!code) {
663
+ const spec = def.sections.find((sec) => sec.value === section);
664
+ if (!spec?.unclassifiedLabel) continue;
665
+ let node2 = bucket.get(UNCLASSIFIED);
666
+ if (!node2) {
667
+ node2 = emptyNode("", spec.unclassifiedLabel, false);
668
+ node2.key = `${section}${UNCLASSIFIED}`;
669
+ bucket.set(UNCLASSIFIED, node2);
670
+ }
671
+ add(node2.own, period, value);
672
+ continue;
673
+ }
674
+ let node = bucket.get(code);
675
+ if (!node) {
676
+ node = emptyNode(code, label || code, false);
677
+ bucket.set(code, node);
678
+ } else if (node.synthetic) {
679
+ node.synthetic = false;
680
+ if (label) node.label = label;
681
+ }
682
+ add(node.own, period, value);
683
+ }
684
+ const periods = [...periodSet].sort();
685
+ const lines = [];
686
+ const sectionTotals = /* @__PURE__ */ new Map();
687
+ for (const spec of def.sections) {
688
+ const bucket = bySection.get(spec.value);
689
+ for (const code of [...bucket.keys()]) {
690
+ if (code === UNCLASSIFIED) continue;
691
+ let parent = parentCode(code);
692
+ while (parent && !bucket.has(parent)) {
693
+ bucket.set(parent, emptyNode(parent, parent, true));
694
+ parent = parentCode(parent);
695
+ }
696
+ }
697
+ const roots = [];
698
+ for (const [code, node] of bucket) {
699
+ if (code === UNCLASSIFIED) continue;
700
+ const parent = parentCode(code);
701
+ if (parent && bucket.has(parent)) bucket.get(parent).children.push(node);
702
+ else roots.push(node);
703
+ }
704
+ const totals = {};
705
+ for (const node of roots) {
706
+ rollup(node, periods);
707
+ for (const p of periods) if (node.rollup[p]) add(totals, p, node.rollup[p]);
708
+ }
709
+ const orphan = bucket.get(UNCLASSIFIED);
710
+ if (orphan) {
711
+ rollup(orphan, periods);
712
+ for (const p of periods) if (orphan.rollup[p]) add(totals, p, orphan.rollup[p]);
713
+ }
714
+ roots.sort((a, b) => compareCodes(a.code, b.code));
715
+ const nodes = orphan ? [...roots, orphan] : roots;
716
+ const total = periods.reduce((acc, p) => acc + (totals[p] ?? 0), 0);
717
+ sectionTotals.set(spec.value, { totals, total });
718
+ lines.push({ kind: "section", section: spec.value, label: spec.label, nodes, totals, total });
719
+ }
720
+ const out = [];
721
+ for (const line of def.lines) {
722
+ if (line.kind === "section") {
723
+ const block = lines.find((l) => l.kind === "section" && l.section === line.section);
724
+ if (block) out.push(block);
725
+ continue;
726
+ }
727
+ const totals = {};
728
+ for (const [section, sign] of Object.entries(line.of)) {
729
+ const s = sectionTotals.get(section);
730
+ if (!s) continue;
731
+ for (const p of periods) if (s.totals[p]) add(totals, p, s.totals[p] * sign);
732
+ }
733
+ out.push({
734
+ kind: "total",
735
+ label: line.label,
736
+ emphasis: line.emphasis,
737
+ totals,
738
+ total: periods.reduce((acc, p) => acc + (totals[p] ?? 0), 0)
739
+ });
740
+ }
741
+ return { periods, lines: out };
742
+ }
743
+ function flattenNodes(nodes, open, out = []) {
744
+ for (const node of nodes) {
745
+ out.push(node);
746
+ if (node.children.length && open(node)) flattenNodes(node.children, open, out);
747
+ }
748
+ return out;
749
+ }
750
+ function defaultOpenKeys(lines, depth) {
751
+ const open = /* @__PURE__ */ new Set();
752
+ const walk = (nodes) => {
753
+ for (const n of nodes) {
754
+ if (n.depth < depth) open.add(n.key);
755
+ walk(n.children);
756
+ }
757
+ };
758
+ for (const line of lines) if (line.kind === "section") walk(line.nodes);
759
+ return open;
760
+ }
761
+ var EMPTY = "\u2014";
762
+ function cell(value, currency) {
763
+ if (value === void 0) return EMPTY;
764
+ return formatAnalyticsValue(value, "currency", currency);
765
+ }
766
+ function Row({
767
+ label,
768
+ code,
769
+ depth,
770
+ values,
771
+ total,
772
+ periods,
773
+ currency,
774
+ expandable,
775
+ open,
776
+ onToggle,
777
+ tone,
778
+ synthetic
779
+ }) {
780
+ return /* @__PURE__ */ jsxs(
781
+ "tr",
782
+ {
783
+ onClick: expandable ? onToggle : void 0,
784
+ className: cn(
785
+ "border-b border-border/50 last:border-b-0",
786
+ tone === "strong" && "bg-muted/60 font-semibold",
787
+ tone === "rule" && "border-t-2 border-foreground/70 bg-muted/40 font-semibold",
788
+ tone === "section" && "bg-muted/30 font-medium",
789
+ !tone && "hover:bg-muted/30",
790
+ expandable && "cursor-pointer select-none"
791
+ ),
792
+ children: [
793
+ /* @__PURE__ */ jsx(
794
+ "th",
795
+ {
796
+ scope: "row",
797
+ className: "sticky left-0 z-10 w-full bg-inherit px-3 py-2 text-left font-inherit",
798
+ style: { paddingLeft: `${12 + depth * 16}px` },
799
+ children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
800
+ expandable ? /* @__PURE__ */ jsxs(
801
+ "button",
802
+ {
803
+ type: "button",
804
+ tabIndex: -1,
805
+ onClick: (e) => {
806
+ e.stopPropagation();
807
+ onToggle?.();
808
+ },
809
+ "aria-expanded": open,
810
+ className: "-ml-1 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
811
+ children: [
812
+ /* @__PURE__ */ jsx(
813
+ ChevronRight,
814
+ {
815
+ className: cn("h-3.5 w-3.5 transition-transform", open && "rotate-90"),
816
+ "aria-hidden": true
817
+ }
818
+ ),
819
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
820
+ open ? "Recolher" : "Expandir",
821
+ " ",
822
+ label
823
+ ] })
824
+ ]
825
+ }
826
+ ) : /* @__PURE__ */ jsx("span", { className: "inline-block w-[18px]", "aria-hidden": true }),
827
+ code ? /* @__PURE__ */ jsx("span", { className: "shrink-0 font-mono text-[11px] tabular-nums text-muted-foreground", children: code }) : null,
828
+ label && label !== code ? /* @__PURE__ */ jsx("span", { className: cn("truncate", synthetic && "text-muted-foreground"), title: label, children: label }) : null
829
+ ] })
830
+ }
831
+ ),
832
+ periods.map((p) => /* @__PURE__ */ jsx("td", { className: "w-px whitespace-nowrap px-3 py-2 text-right tabular-nums", children: cell(values[p], currency) }, p)),
833
+ /* @__PURE__ */ jsx("td", { className: "w-px whitespace-nowrap border-l border-border px-3 py-2 text-right font-medium tabular-nums", children: cell(total || void 0, currency) })
834
+ ]
835
+ }
836
+ );
837
+ }
838
+ function StatementSkeleton({ rows, dense }) {
839
+ const cols = 4;
840
+ return /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border", "aria-hidden": true, children: [
841
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 border-b bg-muted/50 px-3 py-2.5", children: [
842
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-24" }),
843
+ /* @__PURE__ */ jsx("div", { className: "ml-auto flex gap-3", children: Array.from({ length: cols }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-16" }, i)) })
844
+ ] }),
845
+ Array.from({ length: rows }).map((_, i) => {
846
+ const total = i === 0 || i === Math.floor(rows / 2);
847
+ return /* @__PURE__ */ jsxs(
848
+ "div",
849
+ {
850
+ className: cn(
851
+ "flex items-center gap-3 border-b border-border/50 px-3 last:border-b-0",
852
+ dense ? "py-1.5" : "py-2.5",
853
+ total && "bg-muted/40"
854
+ ),
855
+ children: [
856
+ /* @__PURE__ */ jsx(
857
+ Skeleton,
858
+ {
859
+ className: cn("h-3.5", total ? "w-48" : "w-36"),
860
+ style: { marginLeft: total ? 0 : `${i % 3 * 16}px` }
861
+ }
862
+ ),
863
+ /* @__PURE__ */ jsx("div", { className: "ml-auto flex gap-3", children: Array.from({ length: cols }).map((_2, c) => /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-16" }, c)) })
864
+ ]
865
+ },
866
+ i
867
+ );
868
+ })
869
+ ] });
870
+ }
871
+ function StatementTable({
872
+ rows,
873
+ def,
874
+ currency,
875
+ formatPeriod,
876
+ totalLabel = "Total",
877
+ accountLabel = "Conta",
878
+ emptyMessage = "Sem lan\xE7amentos no per\xEDodo.",
879
+ className,
880
+ dense,
881
+ loading,
882
+ skeletonRows
883
+ }) {
884
+ const statement = React2__default.useMemo(() => buildStatement(rows, def), [rows, def]);
885
+ const [open, setOpen] = React2__default.useState(() => /* @__PURE__ */ new Set());
886
+ const [touched, setTouched] = React2__default.useState(false);
887
+ React2__default.useEffect(() => {
888
+ if (touched) return;
889
+ setOpen(defaultOpenKeys(statement.lines, def.defaultDepth ?? 1));
890
+ }, [statement, def.defaultDepth, touched]);
891
+ const toggle = React2__default.useCallback((key) => {
892
+ setTouched(true);
893
+ setOpen((prev) => {
894
+ const next = new Set(prev);
895
+ if (next.has(key)) next.delete(key);
896
+ else next.add(key);
897
+ return next;
898
+ });
899
+ }, []);
900
+ const setAll = React2__default.useCallback((expand) => {
901
+ setTouched(true);
902
+ if (!expand) {
903
+ setOpen(/* @__PURE__ */ new Set());
904
+ return;
905
+ }
906
+ const all = /* @__PURE__ */ new Set();
907
+ const walk = (nodes) => {
908
+ for (const n of nodes) {
909
+ all.add(n.key);
910
+ walk(n.children);
911
+ }
912
+ };
913
+ for (const line of statement.lines) if (line.kind === "section") walk(line.nodes);
914
+ setOpen(all);
915
+ }, [statement]);
916
+ const { periods, lines } = statement;
917
+ if (loading) {
918
+ return /* @__PURE__ */ jsx(StatementSkeleton, { rows: skeletonRows ?? (dense ? 5 : 10), dense });
919
+ }
920
+ if (periods.length === 0) {
921
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-[120px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground", children: emptyMessage });
922
+ }
923
+ const isOpen = (n) => open.has(n.key);
924
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
925
+ !dense ? /* @__PURE__ */ jsxs("div", { className: "no-print flex gap-2", children: [
926
+ /* @__PURE__ */ jsx(
927
+ "button",
928
+ {
929
+ type: "button",
930
+ onClick: () => setAll(true),
931
+ className: "rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted",
932
+ children: "Expandir tudo"
933
+ }
934
+ ),
935
+ /* @__PURE__ */ jsx(
936
+ "button",
937
+ {
938
+ type: "button",
939
+ onClick: () => setAll(false),
940
+ className: "rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-muted",
941
+ children: "Recolher tudo"
942
+ }
943
+ )
944
+ ] }) : null,
945
+ /* @__PURE__ */ jsx("div", { className: "print-flow overflow-x-auto rounded-lg border", children: /* @__PURE__ */ jsxs("table", { className: cn("w-full border-collapse", dense ? "text-xs" : "text-sm"), children: [
946
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b bg-muted/50", children: [
947
+ /* @__PURE__ */ jsx("th", { scope: "col", className: "sticky left-0 z-10 w-full bg-muted/50 px-3 py-2 text-left text-xs font-medium uppercase tracking-wide text-muted-foreground", children: accountLabel }),
948
+ periods.map((p) => /* @__PURE__ */ jsx("th", { scope: "col", className: "w-px whitespace-nowrap px-3 py-2 text-right text-xs font-medium uppercase tracking-wide text-muted-foreground", children: formatPeriod ? formatPeriod(p) : p }, p)),
949
+ /* @__PURE__ */ jsx("th", { scope: "col", className: "w-px whitespace-nowrap border-l border-border px-3 py-2 text-right text-xs font-medium uppercase tracking-wide text-muted-foreground", children: totalLabel })
950
+ ] }) }),
951
+ /* @__PURE__ */ jsx("tbody", { children: lines.map((line, i) => {
952
+ if (line.kind === "total") {
953
+ return /* @__PURE__ */ jsx(
954
+ Row,
955
+ {
956
+ label: line.label,
957
+ depth: 0,
958
+ values: line.totals,
959
+ total: line.total,
960
+ periods,
961
+ currency,
962
+ tone: line.emphasis ?? "strong"
963
+ },
964
+ `total-${i}-${line.label}`
965
+ );
966
+ }
967
+ const visible = flattenNodes(line.nodes, isOpen);
968
+ return /* @__PURE__ */ jsx(React2__default.Fragment, { children: visible.map((node) => /* @__PURE__ */ jsx(
969
+ Row,
970
+ {
971
+ label: node.label,
972
+ code: node.code || void 0,
973
+ depth: node.depth,
974
+ values: node.rollup,
975
+ total: node.total,
976
+ periods,
977
+ currency,
978
+ expandable: node.children.length > 0,
979
+ open: isOpen(node),
980
+ onToggle: () => toggle(node.key),
981
+ synthetic: node.synthetic
982
+ },
983
+ node.key
984
+ )) }, `section-${line.section}`);
985
+ }) })
986
+ ] }) })
987
+ ] });
988
+ }
541
989
  var CHART_TYPES = { line: "line", bar: "bar", area: "area", donut: "pie" };
542
990
  function CardChrome({
543
991
  card,
@@ -657,7 +1105,7 @@ function AnalyticsCard({ card, className, hideOpen }) {
657
1105
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Not connected yet" })
658
1106
  ] });
659
1107
  }
660
- if (loading && !result) {
1108
+ if (loading) {
661
1109
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
662
1110
  /* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-24" }),
663
1111
  /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-16" })
@@ -717,6 +1165,19 @@ function AnalyticsCard({ card, className, hideOpen }) {
717
1165
  }
718
1166
  case "table":
719
1167
  return /* @__PURE__ */ jsx(MiniTable, { rows, card, currency, limit: card.viz.limit ?? 5 });
1168
+ case "statement": {
1169
+ if (!card.viz.statement) return /* @__PURE__ */ jsx(EmptyState, {});
1170
+ return /* @__PURE__ */ jsx(
1171
+ StatementTable,
1172
+ {
1173
+ rows,
1174
+ def: { ...card.viz.statement, defaultDepth: 0 },
1175
+ currency,
1176
+ formatPeriod: (p) => formatBucket(p, "month"),
1177
+ dense: true
1178
+ }
1179
+ );
1180
+ }
720
1181
  case "funnel": {
721
1182
  const measure = measures[0];
722
1183
  if (!measure || !category) return /* @__PURE__ */ jsx(EmptyState, {});
@@ -876,22 +1337,25 @@ function FilterValues({
876
1337
  return /* @__PURE__ */ jsx("p", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: t("analytics.filters.noOptions") });
877
1338
  }
878
1339
  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: [
879
- /* @__PURE__ */ jsx(
880
- "input",
1340
+ multi ? /* @__PURE__ */ jsx(
1341
+ Checkbox,
881
1342
  {
882
- type: multi ? "checkbox" : "radio",
883
- name: filter.key,
884
1343
  checked: selected.has(o.value),
1344
+ "aria-label": o.label,
885
1345
  onChange: () => {
886
- if (!multi) {
887
- onChange(filter.key, selected.has(o.value) ? void 0 : o.value);
888
- return;
889
- }
890
1346
  const next = new Set(selected);
891
1347
  if (next.has(o.value)) next.delete(o.value);
892
1348
  else next.add(o.value);
893
1349
  onChange(filter.key, [...next]);
894
- },
1350
+ }
1351
+ }
1352
+ ) : /* @__PURE__ */ jsx(
1353
+ "input",
1354
+ {
1355
+ type: "radio",
1356
+ name: filter.key,
1357
+ checked: selected.has(o.value),
1358
+ onChange: () => onChange(filter.key, selected.has(o.value) ? void 0 : o.value),
895
1359
  className: "h-3.5 w-3.5"
896
1360
  }
897
1361
  ),
@@ -901,13 +1365,16 @@ function FilterValues({
901
1365
  function AnalyticsFilterPicker({ filters }) {
902
1366
  const t = useTranslation();
903
1367
  const { state, setFilters, clearFilters } = useAnalyticsContext();
1368
+ const surface = useToolbarSurface();
1369
+ const inline = surface === "panel";
1370
+ const closePanel = useToolbarPanelClose();
904
1371
  const [open, setOpen] = React2.useState(false);
905
1372
  const [search, setSearch] = React2.useState("");
906
1373
  const [openGroup, setOpenGroup] = React2.useState(null);
907
1374
  const [draft, setDraft] = React2.useState(state.filters);
908
1375
  React2.useEffect(() => {
909
- if (open) setDraft(state.filters);
910
- }, [open, state.filters]);
1376
+ if (open || inline) setDraft(state.filters);
1377
+ }, [open, inline, state.filters]);
911
1378
  const groups = React2.useMemo(() => {
912
1379
  const q = search.trim().toLowerCase();
913
1380
  const matching = q ? filters.filter((f) => f.label.toLowerCase().includes(q)) : filters;
@@ -920,39 +1387,36 @@ function AnalyticsFilterPicker({ filters }) {
920
1387
  }, [filters, search, t]);
921
1388
  const activeCount = Object.keys(state.filters).length;
922
1389
  if (filters.length === 0) return null;
923
- return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
924
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(ToolbarButton, { icon: SlidersHorizontal, caret: false, children: [
925
- t("analytics.filters"),
926
- activeCount > 0 ? /* @__PURE__ */ jsx("span", { className: "ml-0.5 rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground", children: activeCount }) : null
1390
+ const panel = /* @__PURE__ */ jsxs(Fragment, { children: [
1391
+ /* @__PURE__ */ jsx("div", { className: inline ? "pb-2" : "border-b p-2", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1392
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1393
+ /* @__PURE__ */ jsx(
1394
+ Input,
1395
+ {
1396
+ value: search,
1397
+ onChange: (e) => setSearch(e.target.value),
1398
+ placeholder: t("analytics.filters.search"),
1399
+ className: "h-8 pl-8 text-sm"
1400
+ }
1401
+ )
927
1402
  ] }) }),
928
- /* @__PURE__ */ jsxs(PopoverContent, { align: "end", className: "w-80 p-0", children: [
929
- /* @__PURE__ */ jsx("div", { className: "border-b p-2", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
930
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
931
- /* @__PURE__ */ jsx(
932
- Input,
933
- {
934
- value: search,
935
- onChange: (e) => setSearch(e.target.value),
936
- placeholder: t("analytics.filters.search"),
937
- className: "h-8 pl-8 text-sm"
938
- }
939
- )
940
- ] }) }),
941
- /* @__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: t("analytics.filters.none") }) : groups.map(([group, groupFilters]) => /* @__PURE__ */ jsxs("div", { children: [
942
- /* @__PURE__ */ jsxs(
1403
+ /* @__PURE__ */ jsx("div", { className: inline ? "min-h-0 flex-1 overflow-y-auto" : "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: t("analytics.filters.none") }) : groups.map(([group, groupFilters]) => {
1404
+ const expanded = inline || openGroup === group;
1405
+ return /* @__PURE__ */ jsxs("div", { children: [
1406
+ inline ? /* @__PURE__ */ jsx("p", { className: "px-1 pb-1 pt-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group }) : /* @__PURE__ */ jsxs(
943
1407
  "button",
944
1408
  {
945
1409
  type: "button",
946
1410
  onClick: () => setOpenGroup((g) => g === group ? null : group),
947
- "aria-expanded": openGroup === group,
1411
+ "aria-expanded": expanded,
948
1412
  className: "flex w-full items-center justify-between rounded-md px-2 py-2 text-sm hover:bg-muted",
949
1413
  children: [
950
1414
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: group }),
951
- /* @__PURE__ */ jsx(ChevronRight, { className: cn("h-3.5 w-3.5 text-muted-foreground transition-transform", openGroup === group && "rotate-90") })
1415
+ /* @__PURE__ */ jsx(ChevronRight, { className: cn("h-3.5 w-3.5 text-muted-foreground transition-transform", expanded && "rotate-90") })
952
1416
  ]
953
1417
  }
954
1418
  ),
955
- openGroup === group ? /* @__PURE__ */ jsx("div", { className: "space-y-3 px-2 pb-3 pt-1", children: groupFilters.map((f) => /* @__PURE__ */ jsxs("div", { children: [
1419
+ expanded ? /* @__PURE__ */ jsx("div", { className: inline ? "space-y-4 px-1 pb-2" : "space-y-3 px-2 pb-3 pt-1", children: groupFilters.map((f) => /* @__PURE__ */ jsxs("div", { children: [
956
1420
  /* @__PURE__ */ jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: f.label }),
957
1421
  /* @__PURE__ */ jsx(
958
1422
  FilterValues,
@@ -968,35 +1432,79 @@ function AnalyticsFilterPicker({ filters }) {
968
1432
  }
969
1433
  )
970
1434
  ] }, f.key)) }) : null
971
- ] }, group)) }),
972
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-t p-2", children: [
973
- /* @__PURE__ */ jsx(
974
- Button,
975
- {
976
- variant: "ghost",
977
- size: "sm",
978
- onClick: () => {
979
- setDraft({});
980
- clearFilters();
981
- setOpen(false);
982
- },
983
- children: t("analytics.filters.clear")
984
- }
985
- ),
986
- /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => {
987
- setFilters(draft);
988
- setOpen(false);
989
- }, children: t("analytics.filters.apply") })
990
- ] })
1435
+ ] }, group);
1436
+ }) }),
1437
+ /* @__PURE__ */ jsxs("div", { className: cn(
1438
+ "flex items-center justify-between gap-2 border-t",
1439
+ inline ? "mt-2 pt-3" : "p-2"
1440
+ ), children: [
1441
+ /* @__PURE__ */ jsx(
1442
+ Button,
1443
+ {
1444
+ variant: "ghost",
1445
+ size: "sm",
1446
+ onClick: () => {
1447
+ setDraft({});
1448
+ clearFilters();
1449
+ setOpen(false);
1450
+ closePanel();
1451
+ },
1452
+ children: t("analytics.filters.clear")
1453
+ }
1454
+ ),
1455
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => {
1456
+ setFilters(draft);
1457
+ setOpen(false);
1458
+ closePanel();
1459
+ }, children: t("analytics.filters.apply") })
991
1460
  ] })
992
1461
  ] });
1462
+ if (inline) return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-col", children: panel });
1463
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
1464
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(ToolbarButton, { icon: SlidersHorizontal, caret: false, children: [
1465
+ t("analytics.filters"),
1466
+ activeCount > 0 ? /* @__PURE__ */ jsx("span", { className: "ml-0.5 rounded-full bg-primary px-1.5 text-[10px] font-semibold text-primary-foreground", children: activeCount }) : null
1467
+ ] }) }),
1468
+ /* @__PURE__ */ jsx(PopoverContent, { align: "end", className: "w-80 p-0", children: panel })
1469
+ ] });
993
1470
  }
994
1471
  function AnalyticsRangeControl() {
995
1472
  const t = useTranslation();
1473
+ const surface = useToolbarSurface();
996
1474
  const locale = getCurrentLocale();
997
1475
  const { state, preset, setPreset, setDateRange } = useAnalyticsContext();
998
1476
  const custom = preset === "custom";
999
1477
  const span = formatSpan(state.dateRange.from, state.dateRange.to, locale);
1478
+ if (surface === "panel") {
1479
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1480
+ /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1", children: [
1481
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: t("analytics.range.from") }),
1482
+ /* @__PURE__ */ jsx(
1483
+ DatePicker,
1484
+ {
1485
+ value: state.dateRange.from,
1486
+ onChange: (from) => setDateRange({
1487
+ from,
1488
+ to: from > state.dateRange.to ? from : state.dateRange.to
1489
+ })
1490
+ }
1491
+ )
1492
+ ] }),
1493
+ /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1", children: [
1494
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: t("analytics.range.to") }),
1495
+ /* @__PURE__ */ jsx(
1496
+ DatePicker,
1497
+ {
1498
+ value: state.dateRange.to,
1499
+ onChange: (to) => setDateRange({
1500
+ from: to < state.dateRange.from ? to : state.dateRange.from,
1501
+ to
1502
+ })
1503
+ }
1504
+ )
1505
+ ] })
1506
+ ] });
1507
+ }
1000
1508
  return /* @__PURE__ */ jsx(
1001
1509
  RangeControl,
1002
1510
  {
@@ -1087,24 +1595,122 @@ function ActiveFilterChip({ label, value, filterKey }) {
1087
1595
  const t = useTranslation();
1088
1596
  const { setFilter } = useAnalyticsContext();
1089
1597
  const text = Array.isArray(value) ? value.length === 1 ? String(value[0]) : t("analytics.filters.selected", { count: value.length }) : String(value);
1090
- return /* @__PURE__ */ jsxs("span", { className: "inline-flex h-[26px] shrink-0 items-center gap-1 rounded-full border border-primary/30 bg-primary/10 py-1 pl-2.5 pr-1 text-xs font-medium text-primary", children: [
1091
- /* @__PURE__ */ jsxs("span", { className: "opacity-70", children: [
1092
- label,
1093
- ":"
1094
- ] }),
1095
- /* @__PURE__ */ jsx("span", { className: "max-w-[10rem] truncate", children: text }),
1096
- /* @__PURE__ */ jsx(
1097
- "button",
1098
- {
1099
- type: "button",
1100
- onClick: () => setFilter(filterKey, void 0),
1101
- "aria-label": `${t("analytics.filters.remove")}: ${label}`,
1102
- className: "rounded-full p-0.5 transition-colors hover:bg-primary/20",
1103
- children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
1104
- }
1105
- )
1106
- ] });
1598
+ return (
1599
+ // `min-w-0` no lugar de `shrink-0`: na faixa estreita do celular a pastilha
1600
+ // inteira não cabia, e o que sobrava fora da tela era justamente o × — um
1601
+ // filtro visível que não se consegue tirar é pior do que um escondido.
1602
+ // Encolhendo, quem encurta é o VALOR (truncate), e o botão de remover é
1603
+ // `shrink-0`, então sobrevive a qualquer largura.
1604
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex h-[26px] min-w-0 items-center gap-1 rounded-full border border-primary/30 bg-primary/10 py-1 pl-2.5 pr-1 text-xs font-medium text-primary", children: [
1605
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 opacity-70", children: [
1606
+ label,
1607
+ ":"
1608
+ ] }),
1609
+ /* @__PURE__ */ jsx("span", { className: "max-w-[10rem] truncate", children: text }),
1610
+ /* @__PURE__ */ jsx(
1611
+ "button",
1612
+ {
1613
+ type: "button",
1614
+ onClick: () => setFilter(filterKey, void 0),
1615
+ "aria-label": `${t("analytics.filters.remove")}: ${label}`,
1616
+ className: "shrink-0 rounded-full p-0.5 transition-colors hover:bg-primary/20",
1617
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
1618
+ }
1619
+ )
1620
+ ] })
1621
+ );
1622
+ }
1623
+ var DATE_RANGE_PRESETS = [
1624
+ "today",
1625
+ "yesterday",
1626
+ "last_7_days",
1627
+ "last_30_days",
1628
+ "last_90_days",
1629
+ "this_month",
1630
+ "last_month",
1631
+ "this_year"
1632
+ ];
1633
+ function formatSpan2(from, to, locale) {
1634
+ const fmt = new Intl.DateTimeFormat(locale, { day: "2-digit", month: "short" });
1635
+ const a = fmt.format(/* @__PURE__ */ new Date(`${from}T12:00:00`));
1636
+ const b = fmt.format(/* @__PURE__ */ new Date(`${to}T12:00:00`));
1637
+ return a === b ? a : `${a} \u2013 ${b}`;
1638
+ }
1639
+ function DateRangeControl({
1640
+ value,
1641
+ onChange,
1642
+ preset = "custom",
1643
+ onPresetChange,
1644
+ presets = DATE_RANGE_PRESETS
1645
+ }) {
1646
+ const t = useTranslation();
1647
+ const locale = getCurrentLocale();
1648
+ const custom = preset === "custom";
1649
+ const pick = (p) => {
1650
+ onPresetChange?.(p);
1651
+ onChange(resolveRangePreset(p));
1652
+ };
1653
+ return /* @__PURE__ */ jsx(
1654
+ RangeControl,
1655
+ {
1656
+ options: presets.map((p) => ({ value: p, label: t(`analytics.range.${p}`) })),
1657
+ value: preset,
1658
+ onChange: (p) => pick(p),
1659
+ label: custom ? t("analytics.range.custom") : void 0,
1660
+ span: custom ? void 0 : formatSpan2(value.from, value.to, locale),
1661
+ extra: /* @__PURE__ */ jsxs(Fragment, { children: [
1662
+ /* @__PURE__ */ jsx(DropdownSeparator, {}),
1663
+ /* @__PURE__ */ jsxs(DropdownItem, { onSelect: () => {
1664
+ onPresetChange?.("custom");
1665
+ onChange({ ...value });
1666
+ }, children: [
1667
+ t("analytics.range.custom"),
1668
+ custom ? /* @__PURE__ */ jsx(Check, { className: "ml-auto h-3.5 w-3.5 shrink-0 pl-1" }) : null
1669
+ ] })
1670
+ ] }),
1671
+ children: custom ? /* @__PURE__ */ jsxs(Fragment, { children: [
1672
+ /* @__PURE__ */ jsx(
1673
+ DatePicker,
1674
+ {
1675
+ variant: "toolbar",
1676
+ value: value.from,
1677
+ onChange: (from) => onChange({ from, to: from > value.to ? from : value.to })
1678
+ }
1679
+ ),
1680
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "\u2013" }),
1681
+ /* @__PURE__ */ jsx(
1682
+ DatePicker,
1683
+ {
1684
+ variant: "toolbar",
1685
+ value: value.to,
1686
+ onChange: (to) => onChange({ from: to < value.from ? to : value.from, to })
1687
+ }
1688
+ )
1689
+ ] }) : null
1690
+ }
1691
+ );
1107
1692
  }
1693
+ var usePracticeStore = create((set, get) => ({
1694
+ title: null,
1695
+ steps: [],
1696
+ done: [],
1697
+ start: (title, steps) => {
1698
+ if (!steps.length) return;
1699
+ set({ title, steps, done: [] });
1700
+ },
1701
+ markDone: (anchor) => {
1702
+ const { steps, done } = get();
1703
+ if (!steps.some((s) => s.anchor === anchor)) return;
1704
+ if (done.includes(anchor)) return;
1705
+ set({ done: [...done, anchor] });
1706
+ },
1707
+ stop: () => set({ title: null, steps: [], done: [] })
1708
+ }));
1709
+ function nextPracticeStep(steps, done) {
1710
+ return steps.find((s) => !done.includes(s.anchor));
1711
+ }
1712
+
1713
+ // src/tour/tour.store.ts
1108
1714
  var navigator;
1109
1715
  function setTourNavigator(fn) {
1110
1716
  navigator = fn;
@@ -1116,14 +1722,19 @@ var useTourStore = create((set, get) => ({
1116
1722
  tour: null,
1117
1723
  index: 0,
1118
1724
  lastOutcome: null,
1725
+ lastTour: null,
1119
1726
  start: (tour) => {
1120
1727
  if (!tour.stops.length) return;
1121
- set({ tour, index: 0, lastOutcome: null });
1728
+ usePracticeStore.getState().stop();
1729
+ set({ tour, index: 0, lastOutcome: null, lastTour: tour });
1122
1730
  },
1123
1731
  next: () => {
1124
1732
  const { tour, index } = get();
1125
1733
  if (!tour) return;
1126
1734
  if (index >= tour.stops.length - 1) {
1735
+ if (tour.practice?.length) {
1736
+ usePracticeStore.getState().start(tour.title ?? "", tour.practice);
1737
+ }
1127
1738
  get().stop(true);
1128
1739
  return;
1129
1740
  }
@@ -1135,11 +1746,105 @@ var useTourStore = create((set, get) => ({
1135
1746
  tour: null,
1136
1747
  index: 0,
1137
1748
  lastOutcome: s.tour ? { tourId: s.tour.id, completed } : s.lastOutcome
1138
- }))
1749
+ })),
1750
+ restart: () => {
1751
+ const t = get().lastTour;
1752
+ if (t) get().start(t);
1753
+ }
1139
1754
  }));
1140
1755
  function currentStop(state) {
1141
1756
  return state.tour?.stops[state.index];
1142
1757
  }
1758
+
1759
+ // src/tour/confetti.ts
1760
+ function fireConfetti(opts) {
1761
+ if (typeof document === "undefined") return;
1762
+ if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
1763
+ const canvas = document.createElement("canvas");
1764
+ const dpr = window.devicePixelRatio || 1;
1765
+ const w = window.innerWidth;
1766
+ const h = window.innerHeight;
1767
+ canvas.width = w * dpr;
1768
+ canvas.height = h * dpr;
1769
+ canvas.style.cssText = `position:fixed;inset:0;width:${w}px;height:${h}px;pointer-events:none;z-index:200`;
1770
+ document.body.appendChild(canvas);
1771
+ const ctx = canvas.getContext("2d");
1772
+ if (!ctx) {
1773
+ canvas.remove();
1774
+ return;
1775
+ }
1776
+ ctx.scale(dpr, dpr);
1777
+ const css = getComputedStyle(document.documentElement);
1778
+ const prim = (css.getPropertyValue("--primary") || "35 92% 50%").trim();
1779
+ const colors = [`hsl(${prim})`, "#22c55e", "#f59e0b", "#ec4899", "#3b82f6", "#ffffff"];
1780
+ const n = opts?.count ?? 130;
1781
+ const rnd = (a, b) => a + Math.random() * (b - a);
1782
+ const fromBottom = !!opts?.fromBottom;
1783
+ const ox = opts?.origin?.x ?? w / 2;
1784
+ const oy = opts?.origin?.y ?? h * 0.32;
1785
+ const parts = Array.from({ length: fromBottom ? Math.max(n, 180) : n }, () => {
1786
+ if (fromBottom) {
1787
+ return {
1788
+ x: rnd(0, w),
1789
+ y: h + rnd(0, 40),
1790
+ vx: rnd(-3, 3),
1791
+ vy: -rnd(13, 22),
1792
+ // straight up, hard
1793
+ rot: rnd(0, Math.PI * 2),
1794
+ vr: rnd(-0.3, 0.3),
1795
+ size: rnd(6, 12),
1796
+ color: colors[Math.random() * colors.length | 0],
1797
+ life: 0
1798
+ };
1799
+ }
1800
+ const angle = rnd(-Math.PI, 0);
1801
+ const speed = rnd(6, 15);
1802
+ return {
1803
+ x: ox,
1804
+ y: oy,
1805
+ vx: Math.cos(angle) * speed * rnd(0.6, 1.4),
1806
+ vy: Math.sin(angle) * speed - rnd(2, 6),
1807
+ rot: rnd(0, Math.PI * 2),
1808
+ vr: rnd(-0.3, 0.3),
1809
+ size: rnd(6, 11),
1810
+ color: colors[Math.random() * colors.length | 0],
1811
+ life: 0
1812
+ };
1813
+ });
1814
+ const GRAVITY = 0.32;
1815
+ const DRAG = 0.99;
1816
+ const MAX_LIFE = 140;
1817
+ let raf = 0;
1818
+ const tick = () => {
1819
+ ctx.clearRect(0, 0, w, h);
1820
+ let alive = 0;
1821
+ for (const p of parts) {
1822
+ if (p.life > MAX_LIFE) continue;
1823
+ alive++;
1824
+ p.life++;
1825
+ p.vx *= DRAG;
1826
+ p.vy = p.vy * DRAG + GRAVITY;
1827
+ p.x += p.vx;
1828
+ p.y += p.vy;
1829
+ p.rot += p.vr;
1830
+ const fade = Math.max(0, 1 - p.life / MAX_LIFE);
1831
+ ctx.save();
1832
+ ctx.translate(p.x, p.y);
1833
+ ctx.rotate(p.rot);
1834
+ ctx.globalAlpha = fade;
1835
+ ctx.fillStyle = p.color;
1836
+ ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6);
1837
+ ctx.restore();
1838
+ }
1839
+ if (alive > 0) {
1840
+ raf = requestAnimationFrame(tick);
1841
+ } else {
1842
+ cancelAnimationFrame(raf);
1843
+ canvas.remove();
1844
+ }
1845
+ };
1846
+ raf = requestAnimationFrame(tick);
1847
+ }
1143
1848
  var DEFAULT_LABELS = {
1144
1849
  next: "Pr\xF3ximo",
1145
1850
  waiting: "Fa\xE7a agora \u2014 eu espero",
@@ -1157,6 +1862,27 @@ function readRect(el) {
1157
1862
  const r = el.getBoundingClientRect();
1158
1863
  return { top: r.top - PAD, left: r.left - PAD, width: r.width + PAD * 2, height: r.height + PAD * 2 };
1159
1864
  }
1865
+ var CURSOR_TRAVEL_MS = 650;
1866
+ function pointFor(rect) {
1867
+ return { x: rect.left + rect.width * 0.62, y: rect.top + rect.height * 0.62 };
1868
+ }
1869
+ function prefersReducedMotion() {
1870
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1871
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1872
+ }
1873
+ function exemptRightInset() {
1874
+ if (typeof document === "undefined") return 0;
1875
+ let inset = 0;
1876
+ for (const el of Array.from(document.querySelectorAll("[data-tour-exempt]"))) {
1877
+ const r = el.getBoundingClientRect();
1878
+ if (r.width === 0 || r.height === 0) continue;
1879
+ if (r.right < window.innerWidth - 8) continue;
1880
+ if (r.width > window.innerWidth * 0.7) continue;
1881
+ if (r.height < window.innerHeight * 0.5) continue;
1882
+ inset = Math.max(inset, window.innerWidth - r.left);
1883
+ }
1884
+ return Math.round(inset);
1885
+ }
1160
1886
  function TourSpotlight({ labels, className }) {
1161
1887
  const l = { ...DEFAULT_LABELS, ...labels };
1162
1888
  const tour = useTourStore((s) => s.tour);
@@ -1164,21 +1890,65 @@ function TourSpotlight({ labels, className }) {
1164
1890
  const next = useTourStore((s) => s.next);
1165
1891
  const prev = useTourStore((s) => s.prev);
1166
1892
  const stop = useTourStore((s) => s.stop);
1893
+ const lastOutcome = useTourStore((s) => s.lastOutcome);
1894
+ const firedFor = React2.useRef(null);
1895
+ React2.useEffect(() => {
1896
+ if (!lastOutcome?.completed) return;
1897
+ if (firedFor.current === lastOutcome.tourId) return;
1898
+ firedFor.current = lastOutcome.tourId;
1899
+ if (usePracticeStore.getState().steps.length > 0) return;
1900
+ const origin = rect ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : void 0;
1901
+ fireConfetti({ origin });
1902
+ }, [lastOutcome]);
1167
1903
  const stopDef = tour?.stops[index];
1168
1904
  const [rect, setRect] = React2.useState(null);
1169
1905
  const [missing, setMissing] = React2.useState(false);
1906
+ const [cursor, setCursor] = React2.useState(null);
1907
+ const [arrived, setArrived] = React2.useState(false);
1908
+ const [engaged, setEngaged] = React2.useState(false);
1909
+ const [rightInset, setRightInset] = React2.useState(0);
1910
+ React2.useEffect(() => {
1911
+ if (!tour) return;
1912
+ const sync = () => setRightInset(exemptRightInset());
1913
+ sync();
1914
+ const interval = window.setInterval(sync, 500);
1915
+ window.addEventListener("resize", sync);
1916
+ return () => {
1917
+ window.clearInterval(interval);
1918
+ window.removeEventListener("resize", sync);
1919
+ };
1920
+ }, [tour?.id]);
1170
1921
  React2.useEffect(() => {
1171
1922
  if (!stopDef) return;
1172
1923
  setRect(null);
1173
1924
  setMissing(false);
1925
+ setArrived(false);
1926
+ setEngaged(false);
1174
1927
  if (stopDef.route) getTourNavigator()?.(stopDef.route);
1175
1928
  let raf = 0;
1929
+ let travel = 0;
1176
1930
  const deadline = Date.now() + ANCHOR_TIMEOUT_MS;
1177
1931
  const hunt = () => {
1178
1932
  const el = document.querySelector(`[data-tour~="${CSS.escape(stopDef.anchor)}"]`);
1179
1933
  if (el) {
1180
1934
  el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
1181
- setRect(readRect(el));
1935
+ const found = readRect(el);
1936
+ const land = () => {
1937
+ const at = document.querySelector(`[data-tour~="${CSS.escape(stopDef.anchor)}"]`);
1938
+ const settled = at ? readRect(at) : found;
1939
+ setRect(settled);
1940
+ setCursor(pointFor(settled));
1941
+ setArrived(true);
1942
+ };
1943
+ if (prefersReducedMotion()) {
1944
+ land();
1945
+ return;
1946
+ }
1947
+ setCursor((prev2) => prev2 ?? { x: window.innerWidth / 2, y: window.innerHeight / 2 });
1948
+ raf = requestAnimationFrame(() => {
1949
+ raf = requestAnimationFrame(() => setCursor(pointFor(found)));
1950
+ });
1951
+ travel = window.setTimeout(land, CURSOR_TRAVEL_MS + 40);
1182
1952
  return;
1183
1953
  }
1184
1954
  if (Date.now() > deadline) {
@@ -1188,10 +1958,13 @@ function TourSpotlight({ labels, className }) {
1188
1958
  raf = requestAnimationFrame(hunt);
1189
1959
  };
1190
1960
  raf = requestAnimationFrame(hunt);
1191
- return () => cancelAnimationFrame(raf);
1961
+ return () => {
1962
+ cancelAnimationFrame(raf);
1963
+ window.clearTimeout(travel);
1964
+ };
1192
1965
  }, [stopDef?.anchor, stopDef?.route, tour?.id]);
1193
1966
  React2.useEffect(() => {
1194
- if (!stopDef || missing) return;
1967
+ if (!stopDef || missing || !arrived) return;
1195
1968
  const anchor = stopDef.anchor;
1196
1969
  const handover = Boolean(stopDef.waitForUser);
1197
1970
  let frame = 0;
@@ -1200,7 +1973,9 @@ function TourSpotlight({ labels, className }) {
1200
1973
  const el = document.querySelector(`[data-tour~="${CSS.escape(anchor)}"]`);
1201
1974
  if (el) {
1202
1975
  absent = 0;
1203
- setRect(readRect(el));
1976
+ const r = readRect(el);
1977
+ setRect(r);
1978
+ setCursor(pointFor(r));
1204
1979
  return;
1205
1980
  }
1206
1981
  if (!handover) return;
@@ -1220,7 +1995,49 @@ function TourSpotlight({ labels, className }) {
1220
1995
  window.removeEventListener("scroll", onFrame, true);
1221
1996
  window.removeEventListener("resize", onFrame);
1222
1997
  };
1223
- }, [stopDef?.anchor, stopDef?.waitForUser, missing, stop]);
1998
+ }, [stopDef?.anchor, stopDef?.waitForUser, missing, arrived, stop]);
1999
+ React2.useEffect(() => {
2000
+ if (tour) return;
2001
+ setCursor(null);
2002
+ setArrived(false);
2003
+ setRect(null);
2004
+ setMissing(false);
2005
+ }, [tour]);
2006
+ React2.useEffect(() => {
2007
+ if (!stopDef || missing) return;
2008
+ const anchor = stopDef.anchor;
2009
+ const within = (node) => {
2010
+ const el = document.querySelector(`[data-tour~="${CSS.escape(anchor)}"]`);
2011
+ return !!el && node instanceof Node && el.contains(node);
2012
+ };
2013
+ const editable = (node) => node instanceof Element && !!node.closest('input, textarea, select, [contenteditable="true"]');
2014
+ const onDown = (e) => {
2015
+ if (!within(e.target)) return;
2016
+ if (editable(e.target)) setEngaged(true);
2017
+ };
2018
+ const onFocus = (e) => {
2019
+ if (within(e.target) && editable(e.target)) setEngaged(true);
2020
+ };
2021
+ const onClick = (e) => {
2022
+ if (!within(e.target) || editable(e.target)) return;
2023
+ useTourStore.getState().next();
2024
+ };
2025
+ const onOut = () => {
2026
+ window.setTimeout(() => {
2027
+ if (!within(document.activeElement)) setEngaged(false);
2028
+ }, 0);
2029
+ };
2030
+ document.addEventListener("pointerdown", onDown, true);
2031
+ document.addEventListener("focusin", onFocus, true);
2032
+ document.addEventListener("click", onClick, true);
2033
+ document.addEventListener("focusout", onOut, true);
2034
+ return () => {
2035
+ document.removeEventListener("pointerdown", onDown, true);
2036
+ document.removeEventListener("focusin", onFocus, true);
2037
+ document.removeEventListener("click", onClick, true);
2038
+ document.removeEventListener("focusout", onOut, true);
2039
+ };
2040
+ }, [stopDef?.anchor, missing]);
1224
2041
  React2.useEffect(() => {
1225
2042
  if (!tour) return;
1226
2043
  const onKey = (e) => {
@@ -1234,99 +2051,125 @@ function TourSpotlight({ labels, className }) {
1234
2051
  if (!tour || !stopDef || typeof document === "undefined") return null;
1235
2052
  const total = tour.stops.length;
1236
2053
  const isLast = index === total - 1;
1237
- const bubble = placeBubble(rect, stopDef.placement);
1238
- const body = /* @__PURE__ */ jsxs("div", { className: cn("pointer-events-none fixed inset-0 z-[120]", className), role: "dialog", "aria-modal": "false", children: [
1239
- rect && !missing ? /* @__PURE__ */ jsxs(Fragment, { children: [
1240
- /* @__PURE__ */ jsx(Dim, { style: { top: 0, left: 0, width: "100%", height: Math.max(0, rect.top) }, onClick: () => stop(false) }),
1241
- /* @__PURE__ */ jsx(Dim, { style: { top: rect.top, left: 0, width: Math.max(0, rect.left), height: rect.height }, onClick: () => stop(false) }),
1242
- /* @__PURE__ */ jsx(
1243
- Dim,
1244
- {
1245
- style: { top: rect.top, left: rect.left + rect.width, right: 0, height: rect.height },
1246
- onClick: () => stop(false)
1247
- }
1248
- ),
1249
- /* @__PURE__ */ jsx(Dim, { style: { top: rect.top + rect.height, left: 0, width: "100%", bottom: 0 }, onClick: () => stop(false) }),
1250
- /* @__PURE__ */ jsx(
1251
- "div",
1252
- {
1253
- "aria-hidden": true,
1254
- className: "fz-spotlight-ring pointer-events-none absolute motion-safe:transition-[top,left,width,height] motion-safe:duration-300",
1255
- style: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
1256
- }
1257
- )
1258
- ] }) : /* @__PURE__ */ jsx(Dim, { style: { inset: 0 }, onClick: () => stop(false) }),
2054
+ const bubble = placeBubble(rect, stopDef.placement, rightInset);
2055
+ const body = (
2056
+ // `right` leaves the exempt column (the assistant narrating this tour)
2057
+ // outside the overlay entirely bright, readable, clickable.
1259
2058
  /* @__PURE__ */ jsxs(
1260
2059
  "div",
1261
2060
  {
1262
- className: cn(
1263
- "pointer-events-auto absolute w-[300px] max-w-[calc(100vw-24px)] rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-2xl",
1264
- "motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-95 motion-safe:duration-200"
1265
- ),
1266
- style: bubble,
2061
+ className: cn("pointer-events-none fixed inset-y-0 left-0 z-[120]", className),
2062
+ style: { right: rightInset },
2063
+ role: "dialog",
2064
+ "aria-modal": "false",
1267
2065
  children: [
1268
- /* @__PURE__ */ jsx(
1269
- "button",
1270
- {
1271
- type: "button",
1272
- onClick: () => stop(false),
1273
- "aria-label": l.skip,
1274
- className: "absolute right-2 top-2 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
1275
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
1276
- }
1277
- ),
1278
- /* @__PURE__ */ jsx("p", { className: "pr-6 text-sm font-semibold leading-snug", children: stopDef.title }),
1279
- (missing ? l.notFound : stopDef.body) && /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-[13px] leading-relaxed text-muted-foreground", children: missing ? l.notFound : stopDef.body }),
1280
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-2", children: [
1281
- /* @__PURE__ */ jsx("span", { className: "text-[11px] tabular-nums text-muted-foreground", children: l.stepOf.replace("{current}", String(index + 1)).replace("{total}", String(total)) }),
1282
- /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1283
- stopDef.waitForUser && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-primary", children: [
1284
- /* @__PURE__ */ jsxs("span", { className: "relative flex h-1.5 w-1.5", children: [
1285
- /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary/70" }),
1286
- /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-primary" })
1287
- ] }),
1288
- l.waiting
1289
- ] }),
1290
- index > 0 && !stopDef.waitForUser && /* @__PURE__ */ jsx(
1291
- "button",
1292
- {
1293
- type: "button",
1294
- onClick: prev,
1295
- className: "rounded-button px-2.5 py-1.5 text-[12.5px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
1296
- children: l.back
1297
- }
1298
- ),
1299
- !stopDef.waitForUser && /* @__PURE__ */ jsx(
1300
- "button",
2066
+ rect && !missing ? /* @__PURE__ */ jsxs(Fragment, { children: [
2067
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: 0, left: 0, width: "100%", height: Math.max(0, rect.top) } }),
2068
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: rect.top, left: 0, width: Math.max(0, rect.left), height: rect.height } }),
2069
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: rect.top, left: rect.left + rect.width, right: 0, height: rect.height } }),
2070
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: rect.top + rect.height, left: 0, width: "100%", bottom: 0 } }),
2071
+ /* @__PURE__ */ jsx(
2072
+ "div",
1301
2073
  {
1302
- type: "button",
1303
- onClick: next,
1304
- className: "rounded-button bg-primary px-3 py-1.5 text-[12.5px] font-semibold text-primary-foreground shadow-button-primary transition-colors hover:bg-primary/90",
1305
- children: isLast ? l.done : l.next
2074
+ "aria-hidden": true,
2075
+ className: "fz-spotlight-ring pointer-events-none absolute motion-safe:transition-[top,left,width,height] motion-safe:duration-300",
2076
+ style: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
1306
2077
  }
1307
2078
  )
1308
- ] })
2079
+ ] }) : /* @__PURE__ */ jsx(Dim, { style: { inset: 0 } }),
2080
+ cursor && !missing && !engaged && /* @__PURE__ */ jsx(
2081
+ "div",
2082
+ {
2083
+ "aria-hidden": true,
2084
+ className: cn(
2085
+ "fz-tour-cursor",
2086
+ !arrived && "fz-tour-cursor-travel",
2087
+ arrived && "fz-tour-cursor-arrived",
2088
+ arrived && stopDef.waitForUser && "fz-tour-cursor-nudge"
2089
+ ),
2090
+ style: { transform: `translate3d(${cursor.x}px, ${cursor.y}px, 0)` },
2091
+ children: /* @__PURE__ */ jsx("svg", { width: "34", height: "34", viewBox: "0 0 24 24", fill: "hsl(var(--primary))", stroke: "white", strokeWidth: "1.4", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M5.5 3.2v17.6c0 .45.54.67.85.35l4.4-4.4a.5.5 0 0 1 .36-.15h6.24c.45 0 .67-.54.35-.85L6.35 2.85a.5.5 0 0 0-.85.35Z" }) })
2092
+ }
2093
+ ),
2094
+ (arrived || missing) && !engaged && /* @__PURE__ */ jsxs(
2095
+ "div",
2096
+ {
2097
+ className: cn(
2098
+ "pointer-events-auto absolute w-[300px] max-w-[calc(100vw-24px)] rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-2xl",
2099
+ "motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-95 motion-safe:duration-200"
2100
+ ),
2101
+ style: bubble,
2102
+ children: [
2103
+ /* @__PURE__ */ jsx(
2104
+ "button",
2105
+ {
2106
+ type: "button",
2107
+ onClick: () => stop(false),
2108
+ "aria-label": l.skip,
2109
+ className: "absolute right-2 top-2 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
2110
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2111
+ }
2112
+ ),
2113
+ /* @__PURE__ */ jsx("p", { className: "pr-6 text-sm font-semibold leading-snug", children: stopDef.title }),
2114
+ (missing ? l.notFound : stopDef.body) && /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-[13px] leading-relaxed text-muted-foreground", children: missing ? l.notFound : stopDef.body }),
2115
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-2", children: [
2116
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] tabular-nums text-muted-foreground", children: l.stepOf.replace("{current}", String(index + 1)).replace("{total}", String(total)) }),
2117
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
2118
+ stopDef.waitForUser && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-primary", children: [
2119
+ /* @__PURE__ */ jsxs("span", { className: "relative flex h-1.5 w-1.5", children: [
2120
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary/70" }),
2121
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-primary" })
2122
+ ] }),
2123
+ l.waiting
2124
+ ] }),
2125
+ index > 0 && !stopDef.waitForUser && /* @__PURE__ */ jsx(
2126
+ "button",
2127
+ {
2128
+ type: "button",
2129
+ onClick: prev,
2130
+ className: "rounded-button px-2.5 py-1.5 text-[12.5px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
2131
+ children: l.back
2132
+ }
2133
+ ),
2134
+ !stopDef.waitForUser && /* @__PURE__ */ jsx(
2135
+ "button",
2136
+ {
2137
+ type: "button",
2138
+ onClick: next,
2139
+ className: "rounded-button bg-primary px-3 py-1.5 text-[12.5px] font-semibold text-primary-foreground shadow-button-primary transition-colors hover:bg-primary/90",
2140
+ children: isLast ? l.done : l.next
2141
+ }
2142
+ )
2143
+ ] })
2144
+ ]
2145
+ }
2146
+ )
1309
2147
  ]
1310
2148
  }
1311
2149
  )
1312
- ] });
2150
+ );
1313
2151
  return createPortal(body, document.body);
1314
2152
  }
1315
- function Dim({ style, onClick }) {
2153
+ function Dim({ style, soft }) {
1316
2154
  return /* @__PURE__ */ jsx(
1317
2155
  "div",
1318
2156
  {
1319
- onClick,
1320
- className: "pointer-events-auto absolute motion-safe:transition-all motion-safe:duration-300",
1321
- style: { backgroundColor: "rgb(0 0 0 / 0.55)", ...style }
2157
+ className: cn(
2158
+ "absolute motion-safe:transition-all motion-safe:duration-300",
2159
+ // Hard dim still swallows the click (so a stray click cannot press a
2160
+ // covered button) — it just no longer STOPS the tour. Soft is fully
2161
+ // click-through: the person is working inside the hole's overflow.
2162
+ soft ? "pointer-events-none" : "pointer-events-auto"
2163
+ ),
2164
+ style: { backgroundColor: soft ? "rgb(0 0 0 / 0.12)" : "rgb(0 0 0 / 0.55)", ...style }
1322
2165
  }
1323
2166
  );
1324
2167
  }
1325
- function placeBubble(rect, placement) {
2168
+ function placeBubble(rect, placement, rightInset = 0) {
1326
2169
  if (typeof window === "undefined" || !rect) {
1327
2170
  return { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
1328
2171
  }
1329
- const vw = window.innerWidth;
2172
+ const vw = window.innerWidth - rightInset;
1330
2173
  const vh = window.innerHeight;
1331
2174
  const belowRoom = vh - (rect.top + rect.height);
1332
2175
  const rightRoom = vw - (rect.left + rect.width);
@@ -1351,7 +2194,107 @@ function placeBubble(rect, placement) {
1351
2194
  function tourAnchor(anchor) {
1352
2195
  return { "data-tour": anchor };
1353
2196
  }
2197
+ function PracticeOverlay() {
2198
+ const title = usePracticeStore((s) => s.title);
2199
+ const steps = usePracticeStore((s) => s.steps);
2200
+ const done = usePracticeStore((s) => s.done);
2201
+ const markDone = usePracticeStore((s) => s.markDone);
2202
+ const stop = usePracticeStore((s) => s.stop);
2203
+ const active = !!title && steps.length > 0;
2204
+ const next = active ? nextPracticeStep(steps, done) : void 0;
2205
+ const [rects, setRects] = React2.useState({});
2206
+ React2.useEffect(() => {
2207
+ if (!active) return;
2208
+ const measure = () => {
2209
+ const out = {};
2210
+ for (const s of steps) {
2211
+ const el = document.querySelector(`[data-tour~="${CSS.escape(s.anchor)}"]`);
2212
+ if (el) {
2213
+ const r = el.getBoundingClientRect();
2214
+ out[s.anchor] = { top: r.top, left: r.left };
2215
+ } else {
2216
+ out[s.anchor] = null;
2217
+ }
2218
+ }
2219
+ setRects(out);
2220
+ };
2221
+ measure();
2222
+ const interval = window.setInterval(measure, 300);
2223
+ window.addEventListener("scroll", measure, true);
2224
+ window.addEventListener("resize", measure);
2225
+ return () => {
2226
+ window.clearInterval(interval);
2227
+ window.removeEventListener("scroll", measure, true);
2228
+ window.removeEventListener("resize", measure);
2229
+ };
2230
+ }, [active, steps]);
2231
+ React2.useEffect(() => {
2232
+ if (!active) return;
2233
+ const within = (anchor, node) => {
2234
+ const el = document.querySelector(`[data-tour~="${CSS.escape(anchor)}"]`);
2235
+ return !!el && node instanceof Node && el.contains(node);
2236
+ };
2237
+ const hit = (e) => {
2238
+ for (const s of steps) {
2239
+ if (done.includes(s.anchor)) continue;
2240
+ if (within(s.anchor, e.target)) {
2241
+ markDone(s.anchor);
2242
+ break;
2243
+ }
2244
+ }
2245
+ };
2246
+ document.addEventListener("pointerdown", hit, true);
2247
+ document.addEventListener("input", hit, true);
2248
+ document.addEventListener("change", hit, true);
2249
+ return () => {
2250
+ document.removeEventListener("pointerdown", hit, true);
2251
+ document.removeEventListener("input", hit, true);
2252
+ document.removeEventListener("change", hit, true);
2253
+ };
2254
+ }, [active, steps, done, markDone]);
2255
+ const finaleFired = React2.useRef(false);
2256
+ React2.useEffect(() => {
2257
+ if (!active) {
2258
+ finaleFired.current = false;
2259
+ return;
2260
+ }
2261
+ const finalStep = steps.find((s) => s.final) ?? steps[steps.length - 1];
2262
+ if (finalStep && done.includes(finalStep.anchor) && !finaleFired.current) {
2263
+ finaleFired.current = true;
2264
+ fireConfetti({ fromBottom: true });
2265
+ window.setTimeout(() => stop(), 1400);
2266
+ }
2267
+ }, [active, done, steps, stop]);
2268
+ if (!active || typeof document === "undefined") return null;
2269
+ const body = /* @__PURE__ */ jsx("div", { className: "pointer-events-none fixed inset-0 z-[118]", children: steps.map((s, i) => {
2270
+ const rect = rects[s.anchor];
2271
+ if (!rect) return null;
2272
+ const isDone = done.includes(s.anchor);
2273
+ const isNext = next?.anchor === s.anchor;
2274
+ return /* @__PURE__ */ jsx(Footprint, { n: i + 1, label: s.label, top: rect.top, left: rect.left, done: isDone, next: isNext }, s.anchor);
2275
+ }) });
2276
+ return createPortal(body, document.body);
2277
+ }
2278
+ function Footprint({ n, label, top, left, done, next }) {
2279
+ return /* @__PURE__ */ jsxs(
2280
+ "div",
2281
+ {
2282
+ className: "absolute flex items-center gap-1.5 transition-all duration-300",
2283
+ style: { top: Math.max(4, top - 12), left: Math.max(4, left - 12) },
2284
+ children: [
2285
+ /* @__PURE__ */ jsx(
2286
+ "span",
2287
+ {
2288
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[12px] font-bold shadow-lg ring-2 ring-background " + (done ? "bg-primary text-primary-foreground" : next ? "fz-practice-pulse bg-primary text-primary-foreground" : "bg-card text-foreground"),
2289
+ children: done ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : n
2290
+ }
2291
+ ),
2292
+ next && !done && /* @__PURE__ */ jsx("span", { className: "rounded-md bg-primary px-2 py-1 text-[11.5px] font-medium text-primary-foreground shadow-lg", children: label })
2293
+ ]
2294
+ }
2295
+ );
2296
+ }
1354
2297
 
1355
- export { AnalyticsCard, AnalyticsContextProvider, AnalyticsControls, AnalyticsFilterPicker, AnalyticsGrid, Avatar, AvatarFallback, AvatarImage, BATCH_WINDOW_MS, TourSpotlight, autoGrain, collectFilters, comparisonRange, createBatchingProvider, currentStop, deltaPercent, formatAnalyticsValue, formatBucket, formatCompact, formatDuration, getTourNavigator, isEngineMissing, num, parseDay, rangeLengthDays, resolveRangePreset, resolveRatios, resolveRowRatios, setTourNavigator, toBreakdown, toChartData, toDayString, toSpark, tourAnchor, trendOf, useAnalyticsCard, useAnalyticsCardRows, useAnalyticsContext, useAnalyticsContextOptional, useTourStore, vizCategory, vizMeasures };
2298
+ export { AnalyticsCard, AnalyticsContextProvider, AnalyticsControls, AnalyticsFilterPicker, AnalyticsGrid, Avatar, AvatarFallback, AvatarImage, BATCH_WINDOW_MS, DATE_RANGE_PRESETS, DateRangeControl, PracticeOverlay, STATEMENT_ROW_CAP, StatementTable, TourSpotlight, autoGrain, buildStatement, collectFilters, compareCodes, comparisonRange, createBatchingProvider, currentStop, defaultOpenKeys, deltaPercent, fireConfetti, flattenNodes, formatAnalyticsValue, formatBucket, formatCompact, formatDuration, getTourNavigator, isEngineMissing, nextPracticeStep, num, parentCode, parseDay, rangeLengthDays, resolveRangePreset, resolveRatios, resolveRowRatios, setTourNavigator, toBreakdown, toChartData, toDayString, toSpark, tourAnchor, trendOf, useAnalyticsAllRows, useAnalyticsCard, useAnalyticsCardRows, useAnalyticsContext, useAnalyticsContextOptional, usePracticeStore, useTourStore, vizCategory, vizMeasures };
1356
2299
  //# sourceMappingURL=index.js.map
1357
2300
  //# sourceMappingURL=index.js.map