@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.
- package/dist/analytics/AnalyticsCard.d.ts.map +1 -1
- package/dist/analytics/AnalyticsControls.d.ts.map +1 -1
- package/dist/analytics/DateRangeControl.d.ts +14 -0
- package/dist/analytics/DateRangeControl.d.ts.map +1 -0
- package/dist/analytics/StatementTable.d.ts +22 -0
- package/dist/analytics/StatementTable.d.ts.map +1 -0
- package/dist/analytics/context.d.ts +8 -0
- package/dist/analytics/context.d.ts.map +1 -1
- package/dist/analytics/derive.d.ts +13 -0
- package/dist/analytics/derive.d.ts.map +1 -1
- package/dist/analytics/index.d.ts +7 -1
- package/dist/analytics/index.d.ts.map +1 -1
- package/dist/analytics/statement.d.ts +58 -0
- package/dist/analytics/statement.d.ts.map +1 -0
- package/dist/{chunk-E44WOPCR.js → chunk-HO2P2XB4.js} +428 -58
- package/dist/chunk-HO2P2XB4.js.map +1 -0
- package/dist/{chunk-P3YTL3MF.js → chunk-L4TXDKMN.js} +5 -5
- package/dist/chunk-L4TXDKMN.js.map +1 -0
- package/dist/{chunk-AIUHVTVK.js → chunk-OWN3GFDG.js} +5 -3
- package/dist/chunk-OWN3GFDG.js.map +1 -0
- package/dist/{chunk-EGHEU3U5.js → chunk-RRNMNFLF.js} +492 -789
- package/dist/chunk-RRNMNFLF.js.map +1 -0
- package/dist/chunk-UP7WVKSU.js +631 -0
- package/dist/chunk-UP7WVKSU.js.map +1 -0
- package/dist/{chunk-OYH7CEMB.js → chunk-XEUIYSW2.js} +330 -366
- package/dist/chunk-XEUIYSW2.js.map +1 -0
- package/dist/{chunk-MBGTFL4I.js → chunk-XKFYX3DG.js} +39 -8
- package/dist/chunk-XKFYX3DG.js.map +1 -0
- package/dist/dashboard/OnboardingWidget.d.ts +4 -1
- package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
- package/dist/dashboard/SidebarOnboarding.d.ts +11 -1
- package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -1
- package/dist/dashboard/index.js +4 -4
- package/dist/icons.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1109 -166
- package/dist/index.js.map +1 -1
- package/dist/layout/AppShell.d.ts +7 -1
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/ModulePage.d.ts.map +1 -1
- package/dist/layout/SaveBar.d.ts.map +1 -1
- package/dist/layout/ShellSearch.d.ts +53 -0
- package/dist/layout/ShellSearch.d.ts.map +1 -0
- package/dist/layout/Sidebar.d.ts +10 -6
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Takeover.d.ts.map +1 -1
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +4 -4
- package/dist/primitives/data-table.d.ts.map +1 -1
- package/dist/primitives/date-range-chip.d.ts +32 -0
- package/dist/primitives/date-range-chip.d.ts.map +1 -0
- package/dist/primitives/header-tabs.d.ts +7 -1
- package/dist/primitives/header-tabs.d.ts.map +1 -1
- package/dist/primitives/index.d.ts +2 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +5 -5
- package/dist/primitives/list-view.d.ts +43 -1
- package/dist/primitives/list-view.d.ts.map +1 -1
- package/dist/primitives/modal.d.ts +2 -0
- package/dist/primitives/modal.d.ts.map +1 -1
- package/dist/primitives/page-actions.d.ts +14 -0
- package/dist/primitives/page-actions.d.ts.map +1 -1
- package/dist/primitives/page-toolbar.d.ts +16 -1
- package/dist/primitives/page-toolbar.d.ts.map +1 -1
- package/dist/primitives/search-select.d.ts +3 -1
- package/dist/primitives/search-select.d.ts.map +1 -1
- package/dist/primitives/segmented-control.d.ts.map +1 -1
- package/dist/primitives/stock-entry-form.d.ts +4 -1
- package/dist/primitives/stock-entry-form.d.ts.map +1 -1
- package/dist/stores/layout.store.d.ts +20 -1
- package/dist/stores/layout.store.d.ts.map +1 -1
- package/dist/styles.css +133 -0
- package/dist/tour/PracticeOverlay.d.ts +12 -0
- package/dist/tour/PracticeOverlay.d.ts.map +1 -0
- package/dist/tour/TourSpotlight.d.ts.map +1 -1
- package/dist/tour/confetti.d.ts +17 -0
- package/dist/tour/confetti.d.ts.map +1 -0
- package/dist/tour/index.d.ts +3 -0
- package/dist/tour/index.d.ts.map +1 -1
- package/dist/tour/practice.store.d.ts +28 -0
- package/dist/tour/practice.store.d.ts.map +1 -0
- package/dist/tour/tour.store.d.ts +4 -0
- package/dist/tour/tour.store.d.ts.map +1 -1
- package/dist/tour/types.d.ts +17 -0
- package/dist/tour/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-AIUHVTVK.js.map +0 -1
- package/dist/chunk-E44WOPCR.js.map +0 -1
- package/dist/chunk-EGHEU3U5.js.map +0 -1
- package/dist/chunk-HKGTF2YA.js +0 -117
- package/dist/chunk-HKGTF2YA.js.map +0 -1
- package/dist/chunk-MBGTFL4I.js.map +0 -1
- package/dist/chunk-OYH7CEMB.js.map +0 -1
- package/dist/chunk-P3YTL3MF.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { Badge, Breadcrumb, BulkActionBar, ChevronPager, CurrencyInput, DatePicker, DotBadge, DraftCommitBar, FilterCheckBox, FilterCheckList, FilterCheckRow, FilterChip, FilterSection,
|
|
3
|
-
export { AppShellSkeleton, ContentSplit, ContentSplitTrigger, PageTrail, Takeover, navigateToCrumb, usePageTrail, usePageTrailParent, useTakeoverClose } from './chunk-
|
|
4
|
-
export {
|
|
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-
|
|
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-
|
|
8
|
-
import { Skeleton, ToolbarButton, PageToolbar, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, Card,
|
|
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-
|
|
10
|
-
export { PageHeaderOutlet, PageTitlePortal, useHasPageActions, useHasPageHeaderBack, useHasPageHeaderHost, useHasPageHeaderOutlet, useLayoutStore, usePageHeader, usePageHeaderClaimed, usePageHeaderHost, useRegisterPageActions } from './chunk-
|
|
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-
|
|
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,
|
|
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.
|
|
234
|
+
return `Q${Math.floor(d.getUTCMonth() / 3) + 1} ${d.getUTCFullYear()}`;
|
|
233
235
|
case "year":
|
|
234
|
-
return String(d.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
924
|
-
/* @__PURE__ */ jsx(
|
|
925
|
-
|
|
926
|
-
|
|
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__ */
|
|
929
|
-
|
|
930
|
-
|
|
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":
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
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
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
/* @__PURE__ */
|
|
1097
|
-
"
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 () =>
|
|
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
|
-
|
|
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 =
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
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__ */
|
|
1269
|
-
"
|
|
1270
|
-
{
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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,
|
|
2153
|
+
function Dim({ style, soft }) {
|
|
1316
2154
|
return /* @__PURE__ */ jsx(
|
|
1317
2155
|
"div",
|
|
1318
2156
|
{
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|