@fayz-ai/ui 0.21.0 → 0.22.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 (50) hide show
  1. package/dist/{chunk-P3YTL3MF.js → chunk-7G3O54CC.js} +3 -3
  2. package/dist/{chunk-P3YTL3MF.js.map → chunk-7G3O54CC.js.map} +1 -1
  3. package/dist/{chunk-AIUHVTVK.js → chunk-DHNUZF2X.js} +4 -3
  4. package/dist/chunk-DHNUZF2X.js.map +1 -0
  5. package/dist/{chunk-EGHEU3U5.js → chunk-PQUS22JE.js} +19 -5
  6. package/dist/chunk-PQUS22JE.js.map +1 -0
  7. package/dist/{chunk-MBGTFL4I.js → chunk-SUSW4LSW.js} +37 -6
  8. package/dist/chunk-SUSW4LSW.js.map +1 -0
  9. package/dist/{chunk-OYH7CEMB.js → chunk-ZSOT7KTE.js} +12 -8
  10. package/dist/chunk-ZSOT7KTE.js.map +1 -0
  11. package/dist/dashboard/OnboardingWidget.d.ts +4 -1
  12. package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
  13. package/dist/dashboard/SidebarOnboarding.d.ts +11 -1
  14. package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -1
  15. package/dist/dashboard/index.js +2 -2
  16. package/dist/icons.js +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +445 -87
  20. package/dist/index.js.map +1 -1
  21. package/dist/layout/ModulePage.d.ts.map +1 -1
  22. package/dist/layout/SaveBar.d.ts.map +1 -1
  23. package/dist/layout/Sidebar.d.ts.map +1 -1
  24. package/dist/layout/index.js +3 -3
  25. package/dist/primitives/header-tabs.d.ts +7 -1
  26. package/dist/primitives/header-tabs.d.ts.map +1 -1
  27. package/dist/primitives/index.js +3 -3
  28. package/dist/primitives/search-select.d.ts +3 -1
  29. package/dist/primitives/search-select.d.ts.map +1 -1
  30. package/dist/primitives/stock-entry-form.d.ts +4 -1
  31. package/dist/primitives/stock-entry-form.d.ts.map +1 -1
  32. package/dist/styles.css +75 -0
  33. package/dist/tour/PracticeOverlay.d.ts +12 -0
  34. package/dist/tour/PracticeOverlay.d.ts.map +1 -0
  35. package/dist/tour/TourSpotlight.d.ts.map +1 -1
  36. package/dist/tour/confetti.d.ts +17 -0
  37. package/dist/tour/confetti.d.ts.map +1 -0
  38. package/dist/tour/index.d.ts +3 -0
  39. package/dist/tour/index.d.ts.map +1 -1
  40. package/dist/tour/practice.store.d.ts +28 -0
  41. package/dist/tour/practice.store.d.ts.map +1 -0
  42. package/dist/tour/tour.store.d.ts +4 -0
  43. package/dist/tour/tour.store.d.ts.map +1 -1
  44. package/dist/tour/types.d.ts +17 -0
  45. package/dist/tour/types.d.ts.map +1 -1
  46. package/package.json +2 -2
  47. package/dist/chunk-AIUHVTVK.js.map +0 -1
  48. package/dist/chunk-EGHEU3U5.js.map +0 -1
  49. package/dist/chunk-MBGTFL4I.js.map +0 -1
  50. package/dist/chunk-OYH7CEMB.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
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 { Popover, PopoverTrigger, PopoverContent, Input, Hint, DatePicker } from './chunk-ZSOT7KTE.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-ZSOT7KTE.js';
3
+ export { AppShellSkeleton, ContentSplit, ContentSplitTrigger, PageTrail, Takeover, navigateToCrumb, usePageTrail, usePageTrailParent, useTakeoverClose } from './chunk-7G3O54CC.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-PQUS22JE.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';
6
+ import { ChartWidget, Sparkline, DashboardGrid } from './chunk-SUSW4LSW.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-SUSW4LSW.js';
8
8
  import { Skeleton, ToolbarButton, PageToolbar, Dropdown, DropdownTrigger, DropdownContent, DropdownItem, Card, CardContent, RangeControl, DropdownSeparator } from './chunk-E44WOPCR.js';
9
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
10
  export { PageHeaderOutlet, PageTitlePortal, useHasPageActions, useHasPageHeaderBack, useHasPageHeaderHost, useHasPageHeaderOutlet, useLayoutStore, usePageHeader, usePageHeaderClaimed, usePageHeaderHost, useRegisterPageActions } from './chunk-HKGTF2YA.js';
@@ -13,7 +13,7 @@ export { Wizard, WizardField, errorMessage, useWizard } from './chunk-OB2ULDBD.j
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-DHNUZF2X.js';
17
17
  import * as React2 from 'react';
18
18
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
19
19
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -1105,6 +1105,27 @@ function ActiveFilterChip({ label, value, filterKey }) {
1105
1105
  )
1106
1106
  ] });
1107
1107
  }
1108
+ var usePracticeStore = create((set, get) => ({
1109
+ title: null,
1110
+ steps: [],
1111
+ done: [],
1112
+ start: (title, steps) => {
1113
+ if (!steps.length) return;
1114
+ set({ title, steps, done: [] });
1115
+ },
1116
+ markDone: (anchor) => {
1117
+ const { steps, done } = get();
1118
+ if (!steps.some((s) => s.anchor === anchor)) return;
1119
+ if (done.includes(anchor)) return;
1120
+ set({ done: [...done, anchor] });
1121
+ },
1122
+ stop: () => set({ title: null, steps: [], done: [] })
1123
+ }));
1124
+ function nextPracticeStep(steps, done) {
1125
+ return steps.find((s) => !done.includes(s.anchor));
1126
+ }
1127
+
1128
+ // src/tour/tour.store.ts
1108
1129
  var navigator;
1109
1130
  function setTourNavigator(fn) {
1110
1131
  navigator = fn;
@@ -1116,14 +1137,19 @@ var useTourStore = create((set, get) => ({
1116
1137
  tour: null,
1117
1138
  index: 0,
1118
1139
  lastOutcome: null,
1140
+ lastTour: null,
1119
1141
  start: (tour) => {
1120
1142
  if (!tour.stops.length) return;
1121
- set({ tour, index: 0, lastOutcome: null });
1143
+ usePracticeStore.getState().stop();
1144
+ set({ tour, index: 0, lastOutcome: null, lastTour: tour });
1122
1145
  },
1123
1146
  next: () => {
1124
1147
  const { tour, index } = get();
1125
1148
  if (!tour) return;
1126
1149
  if (index >= tour.stops.length - 1) {
1150
+ if (tour.practice?.length) {
1151
+ usePracticeStore.getState().start(tour.title ?? "", tour.practice);
1152
+ }
1127
1153
  get().stop(true);
1128
1154
  return;
1129
1155
  }
@@ -1135,11 +1161,105 @@ var useTourStore = create((set, get) => ({
1135
1161
  tour: null,
1136
1162
  index: 0,
1137
1163
  lastOutcome: s.tour ? { tourId: s.tour.id, completed } : s.lastOutcome
1138
- }))
1164
+ })),
1165
+ restart: () => {
1166
+ const t = get().lastTour;
1167
+ if (t) get().start(t);
1168
+ }
1139
1169
  }));
1140
1170
  function currentStop(state) {
1141
1171
  return state.tour?.stops[state.index];
1142
1172
  }
1173
+
1174
+ // src/tour/confetti.ts
1175
+ function fireConfetti(opts) {
1176
+ if (typeof document === "undefined") return;
1177
+ if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
1178
+ const canvas = document.createElement("canvas");
1179
+ const dpr = window.devicePixelRatio || 1;
1180
+ const w = window.innerWidth;
1181
+ const h = window.innerHeight;
1182
+ canvas.width = w * dpr;
1183
+ canvas.height = h * dpr;
1184
+ canvas.style.cssText = `position:fixed;inset:0;width:${w}px;height:${h}px;pointer-events:none;z-index:200`;
1185
+ document.body.appendChild(canvas);
1186
+ const ctx = canvas.getContext("2d");
1187
+ if (!ctx) {
1188
+ canvas.remove();
1189
+ return;
1190
+ }
1191
+ ctx.scale(dpr, dpr);
1192
+ const css = getComputedStyle(document.documentElement);
1193
+ const prim = (css.getPropertyValue("--primary") || "35 92% 50%").trim();
1194
+ const colors = [`hsl(${prim})`, "#22c55e", "#f59e0b", "#ec4899", "#3b82f6", "#ffffff"];
1195
+ const n = opts?.count ?? 130;
1196
+ const rnd = (a, b) => a + Math.random() * (b - a);
1197
+ const fromBottom = !!opts?.fromBottom;
1198
+ const ox = opts?.origin?.x ?? w / 2;
1199
+ const oy = opts?.origin?.y ?? h * 0.32;
1200
+ const parts = Array.from({ length: fromBottom ? Math.max(n, 180) : n }, () => {
1201
+ if (fromBottom) {
1202
+ return {
1203
+ x: rnd(0, w),
1204
+ y: h + rnd(0, 40),
1205
+ vx: rnd(-3, 3),
1206
+ vy: -rnd(13, 22),
1207
+ // straight up, hard
1208
+ rot: rnd(0, Math.PI * 2),
1209
+ vr: rnd(-0.3, 0.3),
1210
+ size: rnd(6, 12),
1211
+ color: colors[Math.random() * colors.length | 0],
1212
+ life: 0
1213
+ };
1214
+ }
1215
+ const angle = rnd(-Math.PI, 0);
1216
+ const speed = rnd(6, 15);
1217
+ return {
1218
+ x: ox,
1219
+ y: oy,
1220
+ vx: Math.cos(angle) * speed * rnd(0.6, 1.4),
1221
+ vy: Math.sin(angle) * speed - rnd(2, 6),
1222
+ rot: rnd(0, Math.PI * 2),
1223
+ vr: rnd(-0.3, 0.3),
1224
+ size: rnd(6, 11),
1225
+ color: colors[Math.random() * colors.length | 0],
1226
+ life: 0
1227
+ };
1228
+ });
1229
+ const GRAVITY = 0.32;
1230
+ const DRAG = 0.99;
1231
+ const MAX_LIFE = 140;
1232
+ let raf = 0;
1233
+ const tick = () => {
1234
+ ctx.clearRect(0, 0, w, h);
1235
+ let alive = 0;
1236
+ for (const p of parts) {
1237
+ if (p.life > MAX_LIFE) continue;
1238
+ alive++;
1239
+ p.life++;
1240
+ p.vx *= DRAG;
1241
+ p.vy = p.vy * DRAG + GRAVITY;
1242
+ p.x += p.vx;
1243
+ p.y += p.vy;
1244
+ p.rot += p.vr;
1245
+ const fade = Math.max(0, 1 - p.life / MAX_LIFE);
1246
+ ctx.save();
1247
+ ctx.translate(p.x, p.y);
1248
+ ctx.rotate(p.rot);
1249
+ ctx.globalAlpha = fade;
1250
+ ctx.fillStyle = p.color;
1251
+ ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6);
1252
+ ctx.restore();
1253
+ }
1254
+ if (alive > 0) {
1255
+ raf = requestAnimationFrame(tick);
1256
+ } else {
1257
+ cancelAnimationFrame(raf);
1258
+ canvas.remove();
1259
+ }
1260
+ };
1261
+ raf = requestAnimationFrame(tick);
1262
+ }
1143
1263
  var DEFAULT_LABELS = {
1144
1264
  next: "Pr\xF3ximo",
1145
1265
  waiting: "Fa\xE7a agora \u2014 eu espero",
@@ -1157,6 +1277,27 @@ function readRect(el) {
1157
1277
  const r = el.getBoundingClientRect();
1158
1278
  return { top: r.top - PAD, left: r.left - PAD, width: r.width + PAD * 2, height: r.height + PAD * 2 };
1159
1279
  }
1280
+ var CURSOR_TRAVEL_MS = 650;
1281
+ function pointFor(rect) {
1282
+ return { x: rect.left + rect.width * 0.62, y: rect.top + rect.height * 0.62 };
1283
+ }
1284
+ function prefersReducedMotion() {
1285
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1286
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1287
+ }
1288
+ function exemptRightInset() {
1289
+ if (typeof document === "undefined") return 0;
1290
+ let inset = 0;
1291
+ for (const el of Array.from(document.querySelectorAll("[data-tour-exempt]"))) {
1292
+ const r = el.getBoundingClientRect();
1293
+ if (r.width === 0 || r.height === 0) continue;
1294
+ if (r.right < window.innerWidth - 8) continue;
1295
+ if (r.width > window.innerWidth * 0.7) continue;
1296
+ if (r.height < window.innerHeight * 0.5) continue;
1297
+ inset = Math.max(inset, window.innerWidth - r.left);
1298
+ }
1299
+ return Math.round(inset);
1300
+ }
1160
1301
  function TourSpotlight({ labels, className }) {
1161
1302
  const l = { ...DEFAULT_LABELS, ...labels };
1162
1303
  const tour = useTourStore((s) => s.tour);
@@ -1164,21 +1305,65 @@ function TourSpotlight({ labels, className }) {
1164
1305
  const next = useTourStore((s) => s.next);
1165
1306
  const prev = useTourStore((s) => s.prev);
1166
1307
  const stop = useTourStore((s) => s.stop);
1308
+ const lastOutcome = useTourStore((s) => s.lastOutcome);
1309
+ const firedFor = React2.useRef(null);
1310
+ React2.useEffect(() => {
1311
+ if (!lastOutcome?.completed) return;
1312
+ if (firedFor.current === lastOutcome.tourId) return;
1313
+ firedFor.current = lastOutcome.tourId;
1314
+ if (usePracticeStore.getState().steps.length > 0) return;
1315
+ const origin = rect ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : void 0;
1316
+ fireConfetti({ origin });
1317
+ }, [lastOutcome]);
1167
1318
  const stopDef = tour?.stops[index];
1168
1319
  const [rect, setRect] = React2.useState(null);
1169
1320
  const [missing, setMissing] = React2.useState(false);
1321
+ const [cursor, setCursor] = React2.useState(null);
1322
+ const [arrived, setArrived] = React2.useState(false);
1323
+ const [engaged, setEngaged] = React2.useState(false);
1324
+ const [rightInset, setRightInset] = React2.useState(0);
1325
+ React2.useEffect(() => {
1326
+ if (!tour) return;
1327
+ const sync = () => setRightInset(exemptRightInset());
1328
+ sync();
1329
+ const interval = window.setInterval(sync, 500);
1330
+ window.addEventListener("resize", sync);
1331
+ return () => {
1332
+ window.clearInterval(interval);
1333
+ window.removeEventListener("resize", sync);
1334
+ };
1335
+ }, [tour?.id]);
1170
1336
  React2.useEffect(() => {
1171
1337
  if (!stopDef) return;
1172
1338
  setRect(null);
1173
1339
  setMissing(false);
1340
+ setArrived(false);
1341
+ setEngaged(false);
1174
1342
  if (stopDef.route) getTourNavigator()?.(stopDef.route);
1175
1343
  let raf = 0;
1344
+ let travel = 0;
1176
1345
  const deadline = Date.now() + ANCHOR_TIMEOUT_MS;
1177
1346
  const hunt = () => {
1178
1347
  const el = document.querySelector(`[data-tour~="${CSS.escape(stopDef.anchor)}"]`);
1179
1348
  if (el) {
1180
1349
  el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
1181
- setRect(readRect(el));
1350
+ const found = readRect(el);
1351
+ const land = () => {
1352
+ const at = document.querySelector(`[data-tour~="${CSS.escape(stopDef.anchor)}"]`);
1353
+ const settled = at ? readRect(at) : found;
1354
+ setRect(settled);
1355
+ setCursor(pointFor(settled));
1356
+ setArrived(true);
1357
+ };
1358
+ if (prefersReducedMotion()) {
1359
+ land();
1360
+ return;
1361
+ }
1362
+ setCursor((prev2) => prev2 ?? { x: window.innerWidth / 2, y: window.innerHeight / 2 });
1363
+ raf = requestAnimationFrame(() => {
1364
+ raf = requestAnimationFrame(() => setCursor(pointFor(found)));
1365
+ });
1366
+ travel = window.setTimeout(land, CURSOR_TRAVEL_MS + 40);
1182
1367
  return;
1183
1368
  }
1184
1369
  if (Date.now() > deadline) {
@@ -1188,10 +1373,13 @@ function TourSpotlight({ labels, className }) {
1188
1373
  raf = requestAnimationFrame(hunt);
1189
1374
  };
1190
1375
  raf = requestAnimationFrame(hunt);
1191
- return () => cancelAnimationFrame(raf);
1376
+ return () => {
1377
+ cancelAnimationFrame(raf);
1378
+ window.clearTimeout(travel);
1379
+ };
1192
1380
  }, [stopDef?.anchor, stopDef?.route, tour?.id]);
1193
1381
  React2.useEffect(() => {
1194
- if (!stopDef || missing) return;
1382
+ if (!stopDef || missing || !arrived) return;
1195
1383
  const anchor = stopDef.anchor;
1196
1384
  const handover = Boolean(stopDef.waitForUser);
1197
1385
  let frame = 0;
@@ -1200,7 +1388,9 @@ function TourSpotlight({ labels, className }) {
1200
1388
  const el = document.querySelector(`[data-tour~="${CSS.escape(anchor)}"]`);
1201
1389
  if (el) {
1202
1390
  absent = 0;
1203
- setRect(readRect(el));
1391
+ const r = readRect(el);
1392
+ setRect(r);
1393
+ setCursor(pointFor(r));
1204
1394
  return;
1205
1395
  }
1206
1396
  if (!handover) return;
@@ -1220,7 +1410,49 @@ function TourSpotlight({ labels, className }) {
1220
1410
  window.removeEventListener("scroll", onFrame, true);
1221
1411
  window.removeEventListener("resize", onFrame);
1222
1412
  };
1223
- }, [stopDef?.anchor, stopDef?.waitForUser, missing, stop]);
1413
+ }, [stopDef?.anchor, stopDef?.waitForUser, missing, arrived, stop]);
1414
+ React2.useEffect(() => {
1415
+ if (tour) return;
1416
+ setCursor(null);
1417
+ setArrived(false);
1418
+ setRect(null);
1419
+ setMissing(false);
1420
+ }, [tour]);
1421
+ React2.useEffect(() => {
1422
+ if (!stopDef || missing) return;
1423
+ const anchor = stopDef.anchor;
1424
+ const within = (node) => {
1425
+ const el = document.querySelector(`[data-tour~="${CSS.escape(anchor)}"]`);
1426
+ return !!el && node instanceof Node && el.contains(node);
1427
+ };
1428
+ const editable = (node) => node instanceof Element && !!node.closest('input, textarea, select, [contenteditable="true"]');
1429
+ const onDown = (e) => {
1430
+ if (!within(e.target)) return;
1431
+ if (editable(e.target)) setEngaged(true);
1432
+ };
1433
+ const onFocus = (e) => {
1434
+ if (within(e.target) && editable(e.target)) setEngaged(true);
1435
+ };
1436
+ const onClick = (e) => {
1437
+ if (!within(e.target) || editable(e.target)) return;
1438
+ useTourStore.getState().next();
1439
+ };
1440
+ const onOut = () => {
1441
+ window.setTimeout(() => {
1442
+ if (!within(document.activeElement)) setEngaged(false);
1443
+ }, 0);
1444
+ };
1445
+ document.addEventListener("pointerdown", onDown, true);
1446
+ document.addEventListener("focusin", onFocus, true);
1447
+ document.addEventListener("click", onClick, true);
1448
+ document.addEventListener("focusout", onOut, true);
1449
+ return () => {
1450
+ document.removeEventListener("pointerdown", onDown, true);
1451
+ document.removeEventListener("focusin", onFocus, true);
1452
+ document.removeEventListener("click", onClick, true);
1453
+ document.removeEventListener("focusout", onOut, true);
1454
+ };
1455
+ }, [stopDef?.anchor, missing]);
1224
1456
  React2.useEffect(() => {
1225
1457
  if (!tour) return;
1226
1458
  const onKey = (e) => {
@@ -1234,99 +1466,125 @@ function TourSpotlight({ labels, className }) {
1234
1466
  if (!tour || !stopDef || typeof document === "undefined") return null;
1235
1467
  const total = tour.stops.length;
1236
1468
  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) }),
1469
+ const bubble = placeBubble(rect, stopDef.placement, rightInset);
1470
+ const body = (
1471
+ // `right` leaves the exempt column (the assistant narrating this tour)
1472
+ // outside the overlay entirely bright, readable, clickable.
1259
1473
  /* @__PURE__ */ jsxs(
1260
1474
  "div",
1261
1475
  {
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,
1476
+ className: cn("pointer-events-none fixed inset-y-0 left-0 z-[120]", className),
1477
+ style: { right: rightInset },
1478
+ role: "dialog",
1479
+ "aria-modal": "false",
1267
1480
  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",
1481
+ rect && !missing ? /* @__PURE__ */ jsxs(Fragment, { children: [
1482
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: 0, left: 0, width: "100%", height: Math.max(0, rect.top) } }),
1483
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: rect.top, left: 0, width: Math.max(0, rect.left), height: rect.height } }),
1484
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: rect.top, left: rect.left + rect.width, right: 0, height: rect.height } }),
1485
+ /* @__PURE__ */ jsx(Dim, { soft: engaged, style: { top: rect.top + rect.height, left: 0, width: "100%", bottom: 0 } }),
1486
+ /* @__PURE__ */ jsx(
1487
+ "div",
1301
1488
  {
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
1489
+ "aria-hidden": true,
1490
+ className: "fz-spotlight-ring pointer-events-none absolute motion-safe:transition-[top,left,width,height] motion-safe:duration-300",
1491
+ style: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
1306
1492
  }
1307
1493
  )
1308
- ] })
1494
+ ] }) : /* @__PURE__ */ jsx(Dim, { style: { inset: 0 } }),
1495
+ cursor && !missing && !engaged && /* @__PURE__ */ jsx(
1496
+ "div",
1497
+ {
1498
+ "aria-hidden": true,
1499
+ className: cn(
1500
+ "fz-tour-cursor",
1501
+ !arrived && "fz-tour-cursor-travel",
1502
+ arrived && "fz-tour-cursor-arrived",
1503
+ arrived && stopDef.waitForUser && "fz-tour-cursor-nudge"
1504
+ ),
1505
+ style: { transform: `translate3d(${cursor.x}px, ${cursor.y}px, 0)` },
1506
+ 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" }) })
1507
+ }
1508
+ ),
1509
+ (arrived || missing) && !engaged && /* @__PURE__ */ jsxs(
1510
+ "div",
1511
+ {
1512
+ className: cn(
1513
+ "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",
1514
+ "motion-safe:animate-in motion-safe:fade-in motion-safe:zoom-in-95 motion-safe:duration-200"
1515
+ ),
1516
+ style: bubble,
1517
+ children: [
1518
+ /* @__PURE__ */ jsx(
1519
+ "button",
1520
+ {
1521
+ type: "button",
1522
+ onClick: () => stop(false),
1523
+ "aria-label": l.skip,
1524
+ className: "absolute right-2 top-2 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
1525
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
1526
+ }
1527
+ ),
1528
+ /* @__PURE__ */ jsx("p", { className: "pr-6 text-sm font-semibold leading-snug", children: stopDef.title }),
1529
+ (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 }),
1530
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-2", children: [
1531
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] tabular-nums text-muted-foreground", children: l.stepOf.replace("{current}", String(index + 1)).replace("{total}", String(total)) }),
1532
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1533
+ stopDef.waitForUser && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-[12px] font-medium text-primary", children: [
1534
+ /* @__PURE__ */ jsxs("span", { className: "relative flex h-1.5 w-1.5", children: [
1535
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary/70" }),
1536
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-1.5 w-1.5 rounded-full bg-primary" })
1537
+ ] }),
1538
+ l.waiting
1539
+ ] }),
1540
+ index > 0 && !stopDef.waitForUser && /* @__PURE__ */ jsx(
1541
+ "button",
1542
+ {
1543
+ type: "button",
1544
+ onClick: prev,
1545
+ 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",
1546
+ children: l.back
1547
+ }
1548
+ ),
1549
+ !stopDef.waitForUser && /* @__PURE__ */ jsx(
1550
+ "button",
1551
+ {
1552
+ type: "button",
1553
+ onClick: next,
1554
+ 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",
1555
+ children: isLast ? l.done : l.next
1556
+ }
1557
+ )
1558
+ ] })
1559
+ ]
1560
+ }
1561
+ )
1309
1562
  ]
1310
1563
  }
1311
1564
  )
1312
- ] });
1565
+ );
1313
1566
  return createPortal(body, document.body);
1314
1567
  }
1315
- function Dim({ style, onClick }) {
1568
+ function Dim({ style, soft }) {
1316
1569
  return /* @__PURE__ */ jsx(
1317
1570
  "div",
1318
1571
  {
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 }
1572
+ className: cn(
1573
+ "absolute motion-safe:transition-all motion-safe:duration-300",
1574
+ // Hard dim still swallows the click (so a stray click cannot press a
1575
+ // covered button) — it just no longer STOPS the tour. Soft is fully
1576
+ // click-through: the person is working inside the hole's overflow.
1577
+ soft ? "pointer-events-none" : "pointer-events-auto"
1578
+ ),
1579
+ style: { backgroundColor: soft ? "rgb(0 0 0 / 0.12)" : "rgb(0 0 0 / 0.55)", ...style }
1322
1580
  }
1323
1581
  );
1324
1582
  }
1325
- function placeBubble(rect, placement) {
1583
+ function placeBubble(rect, placement, rightInset = 0) {
1326
1584
  if (typeof window === "undefined" || !rect) {
1327
1585
  return { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
1328
1586
  }
1329
- const vw = window.innerWidth;
1587
+ const vw = window.innerWidth - rightInset;
1330
1588
  const vh = window.innerHeight;
1331
1589
  const belowRoom = vh - (rect.top + rect.height);
1332
1590
  const rightRoom = vw - (rect.left + rect.width);
@@ -1351,7 +1609,107 @@ function placeBubble(rect, placement) {
1351
1609
  function tourAnchor(anchor) {
1352
1610
  return { "data-tour": anchor };
1353
1611
  }
1612
+ function PracticeOverlay() {
1613
+ const title = usePracticeStore((s) => s.title);
1614
+ const steps = usePracticeStore((s) => s.steps);
1615
+ const done = usePracticeStore((s) => s.done);
1616
+ const markDone = usePracticeStore((s) => s.markDone);
1617
+ const stop = usePracticeStore((s) => s.stop);
1618
+ const active = !!title && steps.length > 0;
1619
+ const next = active ? nextPracticeStep(steps, done) : void 0;
1620
+ const [rects, setRects] = React2.useState({});
1621
+ React2.useEffect(() => {
1622
+ if (!active) return;
1623
+ const measure = () => {
1624
+ const out = {};
1625
+ for (const s of steps) {
1626
+ const el = document.querySelector(`[data-tour~="${CSS.escape(s.anchor)}"]`);
1627
+ if (el) {
1628
+ const r = el.getBoundingClientRect();
1629
+ out[s.anchor] = { top: r.top, left: r.left };
1630
+ } else {
1631
+ out[s.anchor] = null;
1632
+ }
1633
+ }
1634
+ setRects(out);
1635
+ };
1636
+ measure();
1637
+ const interval = window.setInterval(measure, 300);
1638
+ window.addEventListener("scroll", measure, true);
1639
+ window.addEventListener("resize", measure);
1640
+ return () => {
1641
+ window.clearInterval(interval);
1642
+ window.removeEventListener("scroll", measure, true);
1643
+ window.removeEventListener("resize", measure);
1644
+ };
1645
+ }, [active, steps]);
1646
+ React2.useEffect(() => {
1647
+ if (!active) return;
1648
+ const within = (anchor, node) => {
1649
+ const el = document.querySelector(`[data-tour~="${CSS.escape(anchor)}"]`);
1650
+ return !!el && node instanceof Node && el.contains(node);
1651
+ };
1652
+ const hit = (e) => {
1653
+ for (const s of steps) {
1654
+ if (done.includes(s.anchor)) continue;
1655
+ if (within(s.anchor, e.target)) {
1656
+ markDone(s.anchor);
1657
+ break;
1658
+ }
1659
+ }
1660
+ };
1661
+ document.addEventListener("pointerdown", hit, true);
1662
+ document.addEventListener("input", hit, true);
1663
+ document.addEventListener("change", hit, true);
1664
+ return () => {
1665
+ document.removeEventListener("pointerdown", hit, true);
1666
+ document.removeEventListener("input", hit, true);
1667
+ document.removeEventListener("change", hit, true);
1668
+ };
1669
+ }, [active, steps, done, markDone]);
1670
+ const finaleFired = React2.useRef(false);
1671
+ React2.useEffect(() => {
1672
+ if (!active) {
1673
+ finaleFired.current = false;
1674
+ return;
1675
+ }
1676
+ const finalStep = steps.find((s) => s.final) ?? steps[steps.length - 1];
1677
+ if (finalStep && done.includes(finalStep.anchor) && !finaleFired.current) {
1678
+ finaleFired.current = true;
1679
+ fireConfetti({ fromBottom: true });
1680
+ window.setTimeout(() => stop(), 1400);
1681
+ }
1682
+ }, [active, done, steps, stop]);
1683
+ if (!active || typeof document === "undefined") return null;
1684
+ const body = /* @__PURE__ */ jsx("div", { className: "pointer-events-none fixed inset-0 z-[118]", children: steps.map((s, i) => {
1685
+ const rect = rects[s.anchor];
1686
+ if (!rect) return null;
1687
+ const isDone = done.includes(s.anchor);
1688
+ const isNext = next?.anchor === s.anchor;
1689
+ return /* @__PURE__ */ jsx(Footprint, { n: i + 1, label: s.label, top: rect.top, left: rect.left, done: isDone, next: isNext }, s.anchor);
1690
+ }) });
1691
+ return createPortal(body, document.body);
1692
+ }
1693
+ function Footprint({ n, label, top, left, done, next }) {
1694
+ return /* @__PURE__ */ jsxs(
1695
+ "div",
1696
+ {
1697
+ className: "absolute flex items-center gap-1.5 transition-all duration-300",
1698
+ style: { top: Math.max(4, top - 12), left: Math.max(4, left - 12) },
1699
+ children: [
1700
+ /* @__PURE__ */ jsx(
1701
+ "span",
1702
+ {
1703
+ 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"),
1704
+ children: done ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : n
1705
+ }
1706
+ ),
1707
+ 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 })
1708
+ ]
1709
+ }
1710
+ );
1711
+ }
1354
1712
 
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 };
1713
+ export { AnalyticsCard, AnalyticsContextProvider, AnalyticsControls, AnalyticsFilterPicker, AnalyticsGrid, Avatar, AvatarFallback, AvatarImage, BATCH_WINDOW_MS, PracticeOverlay, TourSpotlight, autoGrain, collectFilters, comparisonRange, createBatchingProvider, currentStop, deltaPercent, fireConfetti, formatAnalyticsValue, formatBucket, formatCompact, formatDuration, getTourNavigator, isEngineMissing, nextPracticeStep, num, parseDay, rangeLengthDays, resolveRangePreset, resolveRatios, resolveRowRatios, setTourNavigator, toBreakdown, toChartData, toDayString, toSpark, tourAnchor, trendOf, useAnalyticsCard, useAnalyticsCardRows, useAnalyticsContext, useAnalyticsContextOptional, usePracticeStore, useTourStore, vizCategory, vizMeasures };
1356
1714
  //# sourceMappingURL=index.js.map
1357
1715
  //# sourceMappingURL=index.js.map