@timbal-ai/timbal-react 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +33 -0
  3. package/dist/app.cjs +884 -642
  4. package/dist/app.d.cts +4 -4
  5. package/dist/app.d.ts +4 -4
  6. package/dist/app.esm.js +6 -6
  7. package/dist/{chart-artifact-2OTDTRwM.d.ts → chart-artifact-BYl5C-dk.d.ts} +90 -29
  8. package/dist/{chart-artifact-CS3qyGIY.d.cts → chart-artifact-Dpt4t5sf.d.cts} +90 -29
  9. package/dist/{chat-ClmzWzCX.d.cts → chat-DDsp-Vzz.d.cts} +1 -1
  10. package/dist/{chat-ClmzWzCX.d.ts → chat-DDsp-Vzz.d.ts} +1 -1
  11. package/dist/chat.cjs +26 -26
  12. package/dist/chat.d.cts +3 -3
  13. package/dist/chat.d.ts +3 -3
  14. package/dist/chat.esm.js +3 -3
  15. package/dist/{chunk-TZI3ID3C.esm.js → chunk-24B4I4XC.esm.js} +3 -3
  16. package/dist/{chunk-QIABF4KB.esm.js → chunk-ELEY66OH.esm.js} +2 -2
  17. package/dist/{chunk-WMKPT5BV.esm.js → chunk-HSL36SJ4.esm.js} +6 -6
  18. package/dist/chunk-JJOO4PR5.esm.js +391 -0
  19. package/dist/{chunk-AZL2WANO.esm.js → chunk-MBS7XHV2.esm.js} +28 -28
  20. package/dist/{chunk-5ECRZ5O7.esm.js → chunk-NO5AWNWT.esm.js} +224 -57
  21. package/dist/{chunk-ZNYAETFD.esm.js → chunk-R4RQT2XQ.esm.js} +2 -2
  22. package/dist/{chunk-JYDJRGDE.esm.js → chunk-TMP7RIA7.esm.js} +2 -2
  23. package/dist/{chunk-SZDYIRMB.esm.js → chunk-UVPXH4MB.esm.js} +647 -532
  24. package/dist/{chunk-IGHBLJV3.esm.js → chunk-WQIQW7EM.esm.js} +3 -2
  25. package/dist/{chunk-B4XAC4G7.esm.js → chunk-YYEI6XME.esm.js} +361 -527
  26. package/dist/{circular-progress-CDsJwIPF.d.cts → circular-progress-B9nnwzCu.d.cts} +1 -1
  27. package/dist/{circular-progress-CDsJwIPF.d.ts → circular-progress-B9nnwzCu.d.ts} +1 -1
  28. package/dist/cli/timbal-ui-lint.mjs +503 -0
  29. package/dist/index.cjs +1358 -856
  30. package/dist/index.d.cts +9 -8
  31. package/dist/index.d.ts +9 -8
  32. package/dist/index.esm.js +40 -20
  33. package/dist/{kanban-U5xNe9py.d.cts → kanban-FFBeaZPS.d.cts} +4 -4
  34. package/dist/{kanban-U5xNe9py.d.ts → kanban-FFBeaZPS.d.ts} +4 -4
  35. package/dist/{layout-B8r6Jbat.d.ts → layout-CuKeSY74.d.ts} +1 -1
  36. package/dist/{layout-Cu7Ijn04.d.cts → layout-PzVwkJyL.d.cts} +1 -1
  37. package/dist/site.cjs +71 -0
  38. package/dist/site.d.cts +15 -1
  39. package/dist/site.d.ts +15 -1
  40. package/dist/site.esm.js +12 -311
  41. package/dist/studio.cjs +31 -31
  42. package/dist/studio.d.cts +2 -2
  43. package/dist/studio.d.ts +2 -2
  44. package/dist/studio.esm.js +7 -7
  45. package/dist/{timbal-v2-button-B7vPs7gg.d.ts → timbal-v2-button-DCAZNyUx.d.cts} +1 -1
  46. package/dist/{timbal-v2-button-B7vPs7gg.d.cts → timbal-v2-button-DCAZNyUx.d.ts} +1 -1
  47. package/dist/ui.cjs +77 -77
  48. package/dist/ui.d.cts +3 -3
  49. package/dist/ui.d.ts +3 -3
  50. package/dist/ui.esm.js +15 -15
  51. package/dist/{welcome-NXZlcihe.d.cts → welcome-B00oH5Io.d.cts} +1 -1
  52. package/dist/{welcome-DduQAC4K.d.ts → welcome-DU-4NTjZ.d.ts} +1 -1
  53. package/package.json +13 -3
@@ -4,32 +4,36 @@ import {
4
4
  ShellInsetProvider,
5
5
  studioChromeShellStyle,
6
6
  studioSidebarWidthTransition
7
- } from "./chunk-QIABF4KB.esm.js";
7
+ } from "./chunk-ELEY66OH.esm.js";
8
8
  import {
9
9
  ChartArtifactView,
10
10
  LineAreaChart,
11
11
  Thread,
12
12
  TimbalRuntimeProvider,
13
+ formatCompact,
13
14
  monotoneAreaPath,
14
15
  monotoneLinePath,
15
16
  studioIntegrationCardClass,
16
17
  studioTopbarPillHeightClass,
17
18
  toNum
18
- } from "./chunk-IGHBLJV3.esm.js";
19
+ } from "./chunk-WQIQW7EM.esm.js";
19
20
  import {
20
21
  Checkbox,
21
22
  CopyButton,
22
23
  Popover,
23
24
  PopoverContent,
24
25
  PopoverTrigger,
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
25
31
  Skeleton
26
- } from "./chunk-5ECRZ5O7.esm.js";
32
+ } from "./chunk-NO5AWNWT.esm.js";
27
33
  import {
28
34
  PillSegmentedTabs
29
- } from "./chunk-ZNYAETFD.esm.js";
35
+ } from "./chunk-R4RQT2XQ.esm.js";
30
36
  import {
31
- Avatar,
32
- AvatarFallback,
33
37
  Button,
34
38
  Dialog,
35
39
  DialogContent,
@@ -41,7 +45,7 @@ import {
41
45
  TIMBAL_V2_SWITCH_TRACK_OFF,
42
46
  TimbalV2Button,
43
47
  controlClass
44
- } from "./chunk-AZL2WANO.esm.js";
48
+ } from "./chunk-MBS7XHV2.esm.js";
45
49
  import {
46
50
  cn
47
51
  } from "./chunk-EDEKQYSU.esm.js";
@@ -220,7 +224,11 @@ var HOUSE_RULES = [
220
224
  {
221
225
  id: "compose-from-blocks",
222
226
  rule: "Build from premade blocks (MetricRow, MetricChartCard, DataTable, IntegrationCard). Drop to raw primitives only when no block fits.",
223
- why: "Slop appears the moment generation falls below the curated block layer."
227
+ why: "Slop appears the moment generation falls below the curated block layer.",
228
+ // "Should have used a block" is a judgement about absence, not a textual
229
+ // pattern — no high-precision deterministic check exists, so this stays
230
+ // prompt-only rather than risk false-positives blocking valid UIs.
231
+ enforcement: "prompt-only"
224
232
  },
225
233
  {
226
234
  id: "use-kit-controls",
@@ -301,7 +309,7 @@ The content region is a **padded scroll area** by default \u2014 great for stack
301
309
  - Give the filling child **\`min-h-0 flex-1\`** (or \`h-full\`) so its own scroll/footer resolves \u2014 e.g. \`<TimbalChat className="min-h-0 flex-1" />\`, or a two-pane row where each pane is \`min-h-0 overflow-y-auto\`.
302
310
 
303
311
  \`\`\`tsx
304
- <AppShell contentFill topbar={<div className="flex justify-end p-4"><ModeToggle /></div>}>
312
+ <AppShell contentFill> {/* no global topbar / theme switch */}
305
313
  <Page fill> {/* headerless: omit title */}
306
314
  <TimbalChat workforceId="\u2026" className="min-h-0 flex-1" />
307
315
  </Page>
@@ -321,7 +329,7 @@ Presentational groups \u2014 import from the package root, not from these paths:
321
329
 
322
330
  | Folder | Components |
323
331
  |--------|------------|
324
- | \`data/\` | \`MetricRow\`, \`MetricChartCard\`, \`MetricTile\`, \`DataTable\`, \`FilterBar\`, \`FilterField\`, \`ChartPanel\` |
332
+ | \`data/\` | \`MetricRow\`, \`MetricChartCard\`, \`MetricTile\`, \`DataTable\`, \`FilterBar\`, \`FilterField\`, \`FilterDropdown\`, \`ChartPanel\` |
325
333
  | \`integrations/\` | \`IntegrationCard\`, \`ConnectionRow\`, \`ConnectionRowList\`, \`IntegrationsEmptyState\`, \`PlanBadge\` |
326
334
  | \`settings/\` | \`SettingsSection\`, \`FieldRow\`, \`DangerZone\`, \`FloatingUnsavedChangesBar\` |
327
335
  | \`surfaces/\` | \`StatTile\`, \`InfoCard\`, \`AlertCard\`, \`CatalogCard\`, \`ResourceCard\`, \`DescriptionList\`, \`ExpandableSection\`, \`StatusDot\`, \`StatusBadge\`, \`EmptyState\` |
@@ -390,6 +398,7 @@ The cause of slop is dropping **below** the curated block layer into raw primiti
390
398
  | \`StatusBadge\` | Status pill: \`tone\` (\`default\`\\|\`primary\`\\|\`success\`\\|\`warn\`\\|\`danger\`\\|\`muted\`), children. Use \`danger\` for critical/error severity. |
391
399
  | \`FilterBar\` | Horizontal filter row \u2014 bottom-aligns controls. Mix \`SearchInput\` with labeled \`FilterField\` + \`Select\` (or \`Field\` + \`Select\`); labels sit above, control baselines match. |
392
400
  | \`FilterField\` | Optional label wrapper for a filter control inside \`FilterBar\` (severity, status, \u2026). Omit \`label\` for search-only fields. |
401
+ | \`FilterDropdown\` | Single-button **multi-facet** filter popover for dense list/table views \u2014 **data-driven**: pass \`fields\` describing your **actual columns** (each \`{ id, label, type }\` where \`type\` is \`multiselect\` \\| \`text\` \\| \`daterange\` \\| \`numeric\`; \`multiselect\` takes \`options: [{ value, label, hint?, icon? }]\`). State is keyed by field \`id\` \u2014 controlled (\`value\` + \`onChange\`) or uncontrolled (\`defaultValue\`). Renders **removable active-filter pills** next to the trigger by default (\`showActiveChips\`); wire \`onChange\` to actually filter your rows. **Always derive \`fields\` from the table's columns/data; never ship the default example facets.** Use when one \`FilterBar\` row isn't enough. |
393
402
  | \`SearchInput\` | Filter field with consistent app styling. |
394
403
  | \`DataTable\` | Sortable table: \`columns\`, \`rows\`, \`getRowKey\`, optional \`sort\` / \`onSortChange\`, \`emptyTitle\`, \`showRowCount\`, \`caption\`, \`truncate: true\` on columns with long text. **Scales:** \`pageSize\` (built-in client pager), \`selectable\` + \`onSelectionChange\` (checkbox column for bulk actions), \`loading\` (skeleton rows). \`onRowClick\` for row \u2192 detail (open a \`Sheet\`). |
395
404
  | \`Avatar\` / \`AvatarFallback\` | User initials: \`variant="secondary"\` (or \`primary\` / \`chart\` alias) on **both** \`Avatar\` and \`AvatarFallback\` \u2014 same chrome as catalog **Action** buttons (\`Button variant="secondary"\`: elevated gradient, \`border-border\`, \`shadow-card\`, \`text-foreground\`). Never dark primary CTA fill or raw \`bg-blue-600\`. |
@@ -404,6 +413,8 @@ The cause of slop is dropping **below** the curated block layer into raw primiti
404
413
 
405
414
  Charts run on **recharts** with shadcn \`ChartContainer\` / \`ChartTooltipContent\` chrome (see \`src/ui/chart.tsx\`). Series colors default to \`--chart-1..6\`; override those CSS tokens to rebrand every chart.
406
415
 
416
+ > **React 19 requirement \u2014 do not hand-roll SVG charts to work around this.** recharts under React 19 crashes (\`Cannot assign to read only property 'lanes'\`, blank route) when \`immer\` resolves to **11.0.0**. The fix is a dependency override in the app's \`package.json\` \u2014 \`"overrides": { "immer": ">=11.0.1" }\` (Yarn: \`"resolutions"\`) \u2014 **not** a code change. Always keep using \`LineAreaChart\` / \`PieChart\` / \`ChartPanel\`; never replace them with raw SVG/CSS charts.
417
+
407
418
  | Component | Use for |
408
419
  |-----------|---------|
409
420
  | \`LineAreaChart\` | Cartesian engine (shadcn-style chrome). Bar fills use theme gradients automatically. Props: \`data\`, \`xKey\`, \`series: [{ dataKey, label?, color? }]\`, \`variant\` (\`area\`\\|\`line\`\\|\`bar\`), \`orientation\` (\`horizontal\` for horizontal bars), \`stacked\`, \`curve\`, \`dots\`, \`gridLines\`, \`tooltipIndicator\`, \`layout\` (\`flush\` \u2014 hides axes by default; category + values on hover tooltip), \`showXAxis\` / \`showYAxis\` to opt back in, \`clipTicks\` (truncates long axis labels when axes are on), \`height\`, \`showLegend\`, \`formatX\`, \`formatValue\`, \`ariaLabel\`. |
@@ -448,6 +459,21 @@ Charts run on **recharts** with shadcn \`ChartContainer\` / \`ChartTooltipConten
448
459
  | \`Banner\` | Page-level announcement bar: \`tone\` (\`default\`\\|\`primary\`\\|\`success\`\\|\`warn\`\\|\`danger\`), \`icon\`, \`title\`, body as children, right-aligned \`actions\`, \`onDismiss\` (renders the dismiss X). For in-form/field messages use \`InfoCard\` or \`Alert\` instead. |
449
460
  | \`Timeline\` | Vertical event log: \`items: [{ id, title, description?, meta?, tone?, icon? }]\`. Presentational \u2014 pass already-formatted timestamps in \`meta\`. |
450
461
 
462
+ #### More \`/ui\` primitives (import from \`/ui\` or the package root)
463
+
464
+ These ship in the same design system but aren't re-exported from \`/app\`. Reach for them before hand-rolling \u2014 they're all dependency-free and on the shared tokens / control surface.
465
+
466
+ | Component | Use for |
467
+ |-----------|---------|
468
+ | \`Stepper\` | Ordered step indicator for wizards / onboarding (horizontal or vertical; complete / active / upcoming states). |
469
+ | \`Rating\` | Star rating \u2014 interactive (keyboard + hover preview) or \`readOnly\`; controlled or uncontrolled. |
470
+ | \`NumberField\` | Numeric input with \u2212/+ steppers on the control surface; clamps to \`min\`/\`max\`, steps by \`step\`. |
471
+ | \`TagInput\` | Chips / token input; commits on Enter/comma, removes on Backspace, optional \`dedupe\`/\`max\`. |
472
+ | \`AvatarGroup\` | Overlapping avatar stack with an optional \`+N\` overflow chip (\`max\`, \`spacing\`). |
473
+ | \`CircularProgress\` | Lightweight SVG progress ring \u2014 determinate (optional center label) or indeterminate. |
474
+ | \`CopyButton\` | Click-to-copy with a transient check confirmation; icon-only or with a label. |
475
+ | \`Snippet\` | Single-line code / command on the elevated surface with a built-in copy button. |
476
+
451
477
  Studio chrome (\`StudioSidebar\`, \`ModeToggle\`, \u2026) lives in \`@timbal-ai/timbal-react/studio\` \u2014 optional, not required for every dashboard.
452
478
 
453
479
  ### Block recipes \u2014 compose these (don't clone wholesale)
@@ -514,6 +540,7 @@ import {
514
540
  DataTable,
515
541
  FilterBar,
516
542
  FilterField,
543
+ FilterDropdown,
517
544
  AlertCard,
518
545
  CatalogCard,
519
546
  } from "@timbal-ai/timbal-react/app";
@@ -568,6 +595,8 @@ var GRADIENT_DIRECTIONS = /* @__PURE__ */ new Set([
568
595
  var ICON_IMPORT_RE = /from\s+["']lucide-react["']/;
569
596
  var RAW_CONTROL_SURFACE_RE = /\bborder-input\b/;
570
597
  var COLORED_HOVER_RE = /\bhover:(?:bg|from|to|via)-(?:primary|destructive|success|warn|danger|blue|emerald|green|amber|red|indigo|violet|purple|pink|rose|sky|cyan|teal|lime|yellow|orange|fuchsia)\b/;
598
+ var TREND_CONTEXT_RE = /\b(?:trend|delta|TrendingUp|TrendingDown|ArrowUp|ArrowDown|ArrowUpRight|ArrowDownRight|MoveUp|MoveDown)\b|[+\-]\d+(?:\.\d+)?\s*%/;
599
+ var TREND_COLOR_RE = /\b(?:text|bg|border)-(?:success|destructive|emerald|green|lime|teal|red|rose|orange|amber)(?:-\d{2,3})?(?:\/\d{1,3})?\b/;
571
600
  var RESERVED_GRADIENT_SET = new Set(RESERVED_GRADIENT_TOKENS);
572
601
  function stripVariants(util) {
573
602
  return util.replace(/^(?:[a-z-]+:)*/, "");
@@ -611,6 +640,16 @@ function lintGeneratedUi(source, options = {}) {
611
640
  if (cardMatch) {
612
641
  const isSelfClosing = /\/>/.test(line) && line.indexOf(cardMatch[0]) < line.indexOf("/>");
613
642
  if (!isSelfClosing) {
643
+ if (openCards.length > 0) {
644
+ const parentCard = openCards[openCards.length - 1];
645
+ findings.push({
646
+ rule: "no-card-in-card",
647
+ severity: "warn",
648
+ line: lineNo,
649
+ message: `Card inside card. A <${cardMatch[1]}> is nested inside the <${parentCard.type}> opened on L${parentCard.line}. Double borders/shadows add no information \u2014 group with spacing or a <Section> instead.`,
650
+ snippet: line.trim().slice(0, 120)
651
+ });
652
+ }
614
653
  openCards.push({ type: cardMatch[1], line: lineNo });
615
654
  }
616
655
  }
@@ -683,6 +722,15 @@ function lintGeneratedUi(source, options = {}) {
683
722
  snippet: line.trim().slice(0, 120)
684
723
  });
685
724
  }
725
+ if (TREND_CONTEXT_RE.test(line) && TREND_COLOR_RE.test(line)) {
726
+ findings.push({
727
+ rule: "neutral-trend",
728
+ severity: "warn",
729
+ line: lineNo,
730
+ message: "Colored trend indicator. House style: don't tint deltas green/red on every metric \u2014 show a trend only when the change is the point, and keep it muted (text-muted-foreground).",
731
+ snippet: line.trim().slice(0, 120)
732
+ });
733
+ }
686
734
  if (BOLD_VALUE_RE.test(line)) {
687
735
  findings.push({
688
736
  rule: "bold-metric",
@@ -1648,7 +1696,11 @@ function useAppDensityClass(key, override) {
1648
1696
  }
1649
1697
 
1650
1698
  // src/charts/sparkline.tsx
1651
- import { useId } from "react";
1699
+ import {
1700
+ useId,
1701
+ useRef,
1702
+ useState
1703
+ } from "react";
1652
1704
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1653
1705
  var Sparkline = ({
1654
1706
  data,
@@ -1659,30 +1711,36 @@ var Sparkline = ({
1659
1711
  height = 28,
1660
1712
  strokeWidth = 1.5,
1661
1713
  className,
1662
- ariaLabel = "Trend"
1714
+ ariaLabel = "Trend",
1715
+ interactive = false,
1716
+ labels,
1717
+ formatValue
1663
1718
  }) => {
1664
1719
  const uid = useId();
1720
+ const containerRef = useRef(null);
1721
+ const [activeIndex, setActiveIndex] = useState(null);
1665
1722
  const values = data.map((d) => typeof d === "number" ? d : toNum(d[dataKey]));
1666
1723
  if (values.length === 0) {
1667
1724
  return /* @__PURE__ */ jsx3("span", { className: cn("inline-block", className), style: { width, height } });
1668
1725
  }
1669
- const pad = strokeWidth + 1;
1726
+ const padX = 0;
1727
+ const padY = strokeWidth + 1;
1670
1728
  const min = Math.min(...values);
1671
1729
  const max = Math.max(...values);
1672
1730
  const range = max - min || 1;
1673
- const innerW = width - pad * 2;
1674
- const innerH = height - pad * 2;
1731
+ const innerW = width - padX * 2;
1732
+ const innerH = height - padY * 2;
1675
1733
  const points = values.map((v, i) => ({
1676
- x: pad + (values.length > 1 ? i / (values.length - 1) * innerW : innerW / 2),
1677
- y: pad + innerH - (v - min) / range * innerH
1734
+ x: padX + (values.length > 1 ? i / (values.length - 1) * innerW : innerW / 2),
1735
+ y: padY + innerH - (v - min) / range * innerH
1678
1736
  }));
1679
- return /* @__PURE__ */ jsxs2(
1737
+ const svg = /* @__PURE__ */ jsxs2(
1680
1738
  "svg",
1681
1739
  {
1682
1740
  width,
1683
1741
  height,
1684
1742
  viewBox: `0 0 ${width} ${height}`,
1685
- className: cn("block", className),
1743
+ className: cn("block", interactive ? "h-full w-full" : className),
1686
1744
  role: "img",
1687
1745
  "aria-label": ariaLabel,
1688
1746
  preserveAspectRatio: "none",
@@ -1692,7 +1750,7 @@ var Sparkline = ({
1692
1750
  /* @__PURE__ */ jsx3("stop", { offset: "0%", style: { stopColor: color, stopOpacity: 0.25 } }),
1693
1751
  /* @__PURE__ */ jsx3("stop", { offset: "100%", style: { stopColor: color, stopOpacity: 0 } })
1694
1752
  ] }) }),
1695
- /* @__PURE__ */ jsx3("path", { d: monotoneAreaPath(points, height - pad), fill: `url(#${uid}-spark)` })
1753
+ /* @__PURE__ */ jsx3("path", { d: monotoneAreaPath(points, height - padY), fill: `url(#${uid}-spark)` })
1696
1754
  ] }),
1697
1755
  /* @__PURE__ */ jsx3(
1698
1756
  "path",
@@ -1704,7 +1762,81 @@ var Sparkline = ({
1704
1762
  strokeLinecap: "round",
1705
1763
  strokeLinejoin: "round"
1706
1764
  }
1707
- )
1765
+ ),
1766
+ interactive && activeIndex != null && points[activeIndex] ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1767
+ /* @__PURE__ */ jsx3(
1768
+ "line",
1769
+ {
1770
+ x1: points[activeIndex].x,
1771
+ x2: points[activeIndex].x,
1772
+ y1: 0,
1773
+ y2: height,
1774
+ stroke: color,
1775
+ strokeWidth: 1,
1776
+ strokeOpacity: 0.3,
1777
+ vectorEffect: "non-scaling-stroke"
1778
+ }
1779
+ ),
1780
+ /* @__PURE__ */ jsx3(
1781
+ "circle",
1782
+ {
1783
+ cx: points[activeIndex].x,
1784
+ cy: points[activeIndex].y,
1785
+ r: 2.75,
1786
+ fill: color,
1787
+ stroke: "var(--background, #fff)",
1788
+ strokeWidth: 1.5,
1789
+ vectorEffect: "non-scaling-stroke"
1790
+ }
1791
+ )
1792
+ ] }) : null
1793
+ ]
1794
+ }
1795
+ );
1796
+ if (!interactive) return svg;
1797
+ const onMove = (e) => {
1798
+ const rect = e.currentTarget.getBoundingClientRect();
1799
+ if (rect.width === 0) return;
1800
+ const fraction = (e.clientX - rect.left) / rect.width;
1801
+ const index = Math.max(
1802
+ 0,
1803
+ Math.min(values.length - 1, Math.round(fraction * (values.length - 1)))
1804
+ );
1805
+ setActiveIndex(index);
1806
+ };
1807
+ const active = activeIndex != null ? points[activeIndex] : null;
1808
+ const formattedValue = activeIndex != null ? formatValue ? formatValue(values[activeIndex], activeIndex) : formatCompact(values[activeIndex]) : null;
1809
+ return /* @__PURE__ */ jsxs2(
1810
+ "span",
1811
+ {
1812
+ ref: containerRef,
1813
+ className: cn("relative block touch-none", className),
1814
+ style: { width: "100%", height: "100%" },
1815
+ onPointerMove: onMove,
1816
+ onPointerLeave: () => setActiveIndex(null),
1817
+ children: [
1818
+ svg,
1819
+ active ? /* @__PURE__ */ jsxs2(
1820
+ "span",
1821
+ {
1822
+ "aria-hidden": true,
1823
+ className: cn(
1824
+ "pointer-events-none absolute z-30 -translate-x-1/2 -translate-y-full whitespace-nowrap",
1825
+ "rounded-xl border px-3 py-2 text-[11px] font-medium leading-none tabular-nums shadow-[0_12px_40px_-10px_rgba(0,0,0,0.55)]",
1826
+ "border-white/10 bg-gradient-to-b from-neutral-800 to-neutral-950 text-white",
1827
+ "dark:border-black/10 dark:from-white dark:to-neutral-100 dark:text-neutral-900"
1828
+ ),
1829
+ style: {
1830
+ left: `${active.x / width * 100}%`,
1831
+ top: `${active.y / height * 100}%`,
1832
+ marginTop: -8
1833
+ },
1834
+ children: [
1835
+ labels?.[activeIndex] != null ? /* @__PURE__ */ jsx3("span", { className: "mr-1.5 text-neutral-300 dark:text-neutral-500", children: labels[activeIndex] }) : null,
1836
+ /* @__PURE__ */ jsx3("span", { children: formattedValue })
1837
+ ]
1838
+ }
1839
+ ) : null
1708
1840
  ]
1709
1841
  }
1710
1842
  );
@@ -1770,6 +1902,15 @@ var inlineTrendToneClass = {
1770
1902
  down: "text-rose-500/90 dark:text-rose-400/95 font-medium",
1771
1903
  neutral: "text-muted-foreground/80"
1772
1904
  };
1905
+ var sparklineToneColor = {
1906
+ up: "var(--primary, #3b82f6)",
1907
+ down: "var(--destructive, #f43f5e)",
1908
+ neutral: "var(--muted-foreground, #64748b)"
1909
+ };
1910
+ var sparklineBandBleed = {
1911
+ default: "-mx-4 -mb-3 h-10",
1912
+ compact: "-mx-3 -mb-2 h-8"
1913
+ };
1773
1914
  var activeToneClass = {
1774
1915
  default: "bg-foreground dark:bg-white",
1775
1916
  primary: "bg-primary",
@@ -1799,8 +1940,10 @@ var MetricTile = ({
1799
1940
  ariaLabel,
1800
1941
  className
1801
1942
  }) => {
1943
+ const density = useAppDensity();
1802
1944
  const metricTileBaseClass = useAppDensityClass("metricTile");
1803
1945
  const hasSparkline = Boolean(sparkline || sparklineData);
1946
+ const bandBleed = sparklineBandBleed[density === "compact" ? "compact" : "default"];
1804
1947
  const content = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1805
1948
  active ? /* @__PURE__ */ jsx5(
1806
1949
  "span",
@@ -1812,17 +1955,6 @@ var MetricTile = ({
1812
1955
  )
1813
1956
  }
1814
1957
  ) : null,
1815
- hasSparkline ? /* @__PURE__ */ jsx5("div", { className: "absolute inset-x-0 bottom-0.5 h-9 w-full overflow-hidden pointer-events-none z-0 opacity-45 dark:opacity-35 select-none", children: sparkline ?? /* @__PURE__ */ jsx5(
1816
- Sparkline,
1817
- {
1818
- data: sparklineData,
1819
- width: 160,
1820
- height: 36,
1821
- className: "w-full h-full",
1822
- color: trendTone === "up" ? "var(--primary, #3b82f6)" : trendTone === "down" ? "var(--destructive, #f43f5e)" : "var(--muted-foreground, #64748b)",
1823
- ...sparklineConfig
1824
- }
1825
- ) }) : null,
1826
1958
  /* @__PURE__ */ jsxs4("div", { className: "relative z-10 flex flex-col gap-1 w-full text-left", children: [
1827
1959
  /* @__PURE__ */ jsx5("span", { className: "text-xs font-semibold text-muted-foreground/80 tracking-tight", children: label }),
1828
1960
  /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-2", children: [
@@ -1841,7 +1973,28 @@ var MetricTile = ({
1841
1973
  }
1842
1974
  ) : null
1843
1975
  ] })
1844
- ] })
1976
+ ] }),
1977
+ hasSparkline ? /* @__PURE__ */ jsx5(
1978
+ "div",
1979
+ {
1980
+ className: cn(
1981
+ "relative z-10 mt-2",
1982
+ bandBleed
1983
+ ),
1984
+ children: sparkline ?? /* @__PURE__ */ jsx5(
1985
+ Sparkline,
1986
+ {
1987
+ data: sparklineData,
1988
+ width: 160,
1989
+ height: 40,
1990
+ interactive: true,
1991
+ className: "h-full w-full opacity-90",
1992
+ color: sparklineToneColor[trendTone],
1993
+ ...sparklineConfig
1994
+ }
1995
+ )
1996
+ }
1997
+ ) : null
1845
1998
  ] });
1846
1999
  const divider = showDivider ? metricCellDividerClass : void 0;
1847
2000
  if (onSelect) {
@@ -1893,7 +2046,7 @@ function useAppShellNav() {
1893
2046
 
1894
2047
  // src/app/layout/AppShell.tsx
1895
2048
  import { motion, useReducedMotion } from "motion/react";
1896
- import { useCallback, useEffect, useMemo, useState } from "react";
2049
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
1897
2050
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1898
2051
  var floatingTriggerClass = cn(
1899
2052
  "aui-app-shell-chat-trigger-fixed fixed z-50 rounded-full px-5 py-2.5 text-sm font-medium shadow-card-elevated",
@@ -1917,7 +2070,7 @@ var AppShellBody = ({
1917
2070
  insetExpanded,
1918
2071
  children
1919
2072
  }) => {
1920
- const [isMobile, setIsMobile] = useState(() => {
2073
+ const [isMobile, setIsMobile] = useState2(() => {
1921
2074
  if (typeof window === "undefined") return false;
1922
2075
  return window.innerWidth < 768;
1923
2076
  });
@@ -1997,7 +2150,7 @@ var AppShell = ({
1997
2150
  }) => {
1998
2151
  const topbarContent = topbar ?? header;
1999
2152
  const hasChat = Boolean(chat);
2000
- const [uncontrolledNavOpen, setUncontrolledNavOpen] = useState(defaultNavOpen);
2153
+ const [uncontrolledNavOpen, setUncontrolledNavOpen] = useState2(defaultNavOpen);
2001
2154
  const isNavControlled = navOpenProp !== void 0;
2002
2155
  const navOpen = isNavControlled ? navOpenProp : uncontrolledNavOpen;
2003
2156
  const setNavOpen = useCallback(
@@ -2012,7 +2165,7 @@ var AppShell = ({
2012
2165
  () => ({ open: navOpen, setOpen: setNavOpen, toggle: toggleNav }),
2013
2166
  [navOpen, setNavOpen, toggleNav]
2014
2167
  );
2015
- const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultChatOpen);
2168
+ const [uncontrolledOpen, setUncontrolledOpen] = useState2(defaultChatOpen);
2016
2169
  const isChatControlled = chatOpenProp !== void 0;
2017
2170
  const chatOpen = isChatControlled ? chatOpenProp : uncontrolledOpen;
2018
2171
  const setChatOpen = useCallback(
@@ -2027,7 +2180,7 @@ var AppShell = ({
2027
2180
  const toggleChat = useCallback(() => {
2028
2181
  setChatOpen(!chatOpen);
2029
2182
  }, [chatOpen, setChatOpen]);
2030
- const [insetPaddingPx, setInsetPaddingPx] = useState(
2183
+ const [insetPaddingPx, setInsetPaddingPx] = useState2(
2031
2184
  sidebar ? SIDEBAR_INSET_PX_EXPANDED : 0
2032
2185
  );
2033
2186
  const reportShellInset = useCallback((insetPx) => {
@@ -2540,7 +2693,7 @@ var StatusBadge = ({
2540
2693
  "span",
2541
2694
  {
2542
2695
  className: cn(
2543
- "aui-app-status-badge inline-flex items-center gap-1.5 rounded-full px-2 py-0.5",
2696
+ "aui-app-status-badge inline-flex w-fit shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5",
2544
2697
  "text-xs font-medium leading-none ring-1 ring-inset",
2545
2698
  statusBadgeToneClass[tone],
2546
2699
  className
@@ -2755,7 +2908,7 @@ var DescriptionList = ({
2755
2908
  );
2756
2909
 
2757
2910
  // src/app/surfaces/ExpandableSection.tsx
2758
- import { useId as useId2, useState as useState2 } from "react";
2911
+ import { useId as useId2, useState as useState3 } from "react";
2759
2912
  import { AnimatePresence, motion as motion2, useReducedMotion as useReducedMotion2 } from "motion/react";
2760
2913
  import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
2761
2914
  var Chevron = ({ open }) => /* @__PURE__ */ jsx24(
@@ -2787,7 +2940,7 @@ var ExpandableSection = ({
2787
2940
  }) => {
2788
2941
  const reduceMotion = useReducedMotion2();
2789
2942
  const panelId = useId2();
2790
- const [internalOpen, setInternalOpen] = useState2(defaultOpen);
2943
+ const [internalOpen, setInternalOpen] = useState3(defaultOpen);
2791
2944
  const open = openProp ?? internalOpen;
2792
2945
  const toggle = () => {
2793
2946
  if (openProp == null) setInternalOpen((o) => !o);
@@ -3147,7 +3300,7 @@ var FieldRow = ({
3147
3300
  };
3148
3301
 
3149
3302
  // src/app/settings/FloatingUnsavedChangesBar.tsx
3150
- import { useEffect as useEffect2, useState as useState3 } from "react";
3303
+ import { useEffect as useEffect2, useState as useState4 } from "react";
3151
3304
  import { createPortal } from "react-dom";
3152
3305
  import { AnimatePresence as AnimatePresence2, motion as motion3, useReducedMotion as useReducedMotion3 } from "motion/react";
3153
3306
  import { jsx as jsx30, jsxs as jsxs24 } from "react/jsx-runtime";
@@ -3164,7 +3317,7 @@ var FloatingUnsavedChangesBar = ({
3164
3317
  className
3165
3318
  }) => {
3166
3319
  const reduceMotion = useReducedMotion3();
3167
- const [mounted, setMounted] = useState3(false);
3320
+ const [mounted, setMounted] = useState4(false);
3168
3321
  useEffect2(() => setMounted(true), []);
3169
3322
  if (!mounted || typeof document === "undefined") return null;
3170
3323
  return createPortal(
@@ -3770,505 +3923,467 @@ var FilterField = ({
3770
3923
  };
3771
3924
 
3772
3925
  // src/app/data/FilterDropdown.tsx
3773
- import { useState as useState4, useMemo as useMemo2, useEffect as useEffect3 } from "react";
3774
- import {
3775
- CalendarIcon,
3776
- ChevronDownIcon as ChevronDownIcon2,
3777
- ChevronRightIcon,
3778
- CircleDollarSignIcon,
3779
- ListFilterIcon,
3780
- SearchIcon as SearchIcon2,
3781
- TrendingUpIcon,
3782
- UserIcon,
3783
- WalletIcon
3784
- } from "lucide-react";
3926
+ import { useEffect as useEffect3, useMemo as useMemo2, useState as useState5 } from "react";
3927
+ import { ChevronRightIcon, ListFilterIcon, XIcon as XIcon2 } from "lucide-react";
3785
3928
  import { jsx as jsx47, jsxs as jsxs36 } from "react/jsx-runtime";
3786
- var DEFAULT_CONTACTS = [
3787
- { id: "1", name: "Pedro Olivares Sanchez", email: "polivares@timbal.ai", initials: "PE" },
3788
- { id: "2", name: "John Doe", email: "john@example.com", initials: "JD" },
3789
- { id: "3", name: "Sarah Smith", email: "sarah@example.com", initials: "SS" }
3929
+ var DEFAULT_PRESETS = [
3930
+ { id: "last_7_days", label: "Last 7 days" },
3931
+ { id: "last_30_days", label: "Last 30 days" },
3932
+ { id: "last_90_days", label: "Last 90 days" },
3933
+ { id: "this_month", label: "This month" },
3934
+ { id: "this_year", label: "This year" },
3935
+ { id: "custom", label: "Custom range" }
3790
3936
  ];
3791
- var OPERATORS = [
3792
- { id: "greater_than", label: "Greater than..." },
3793
- { id: "less_than", label: "Less than..." },
3794
- { id: "equals", label: "Equals..." }
3937
+ var DEFAULT_OPERATORS = [
3938
+ { id: "gt", label: "Greater than" },
3939
+ { id: "lt", label: "Less than" },
3940
+ { id: "eq", label: "Equals" }
3795
3941
  ];
3942
+ function asArray(v) {
3943
+ return Array.isArray(v) ? v : [];
3944
+ }
3945
+ function asText(v) {
3946
+ return typeof v === "string" ? v : "";
3947
+ }
3948
+ function asDate(v) {
3949
+ return v && !Array.isArray(v) && typeof v === "object" && "preset" in v ? v : { preset: null };
3950
+ }
3951
+ function asNumeric(v) {
3952
+ return v && !Array.isArray(v) && typeof v === "object" && "operator" in v ? v : { operator: "gt", value: "" };
3953
+ }
3954
+ var OPERATOR_SYMBOL = {
3955
+ gt: ">",
3956
+ lt: "<",
3957
+ eq: "="
3958
+ };
3796
3959
  function FilterDropdown({
3797
- filters,
3798
- onFiltersChange,
3799
- initialFilters,
3800
- contacts = DEFAULT_CONTACTS,
3960
+ fields,
3961
+ value,
3962
+ defaultValue,
3963
+ onChange,
3964
+ label = "Filter",
3965
+ align = "start",
3966
+ showActiveChips = true,
3801
3967
  className
3802
3968
  }) {
3803
- const [isOpen, setIsOpen] = useState4(false);
3804
- const [activeMenu, setActiveMenu] = useState4("contact");
3805
- const [isMobile, setIsMobile] = useState4(false);
3969
+ const [isOpen, setIsOpen] = useState5(false);
3970
+ const [activeId, setActiveId] = useState5(fields[0]?.id ?? null);
3971
+ const [isMobile, setIsMobile] = useState5(false);
3972
+ const isControlled = value !== void 0;
3973
+ const [internal, setInternal] = useState5(defaultValue ?? {});
3974
+ const values = isControlled ? value : internal;
3806
3975
  useEffect3(() => {
3807
3976
  const checkMobile = () => setIsMobile(window.innerWidth < 768);
3808
3977
  checkMobile();
3809
3978
  window.addEventListener("resize", checkMobile);
3810
3979
  return () => window.removeEventListener("resize", checkMobile);
3811
3980
  }, []);
3812
- const [selectedContacts, setSelectedContacts] = useState4(
3813
- filters?.contacts ?? initialFilters?.contacts ?? []
3814
- );
3815
- const [walletInput, setWalletInput] = useState4(filters?.walletAddress ?? initialFilters?.walletAddress ?? "");
3816
- const [appliedWallet, setAppliedWallet] = useState4(filters?.walletAddress ?? initialFilters?.walletAddress ?? "");
3817
- const [selectedDatePreset, setSelectedDatePreset] = useState4(
3818
- filters?.lastInvoiceDate ?? initialFilters?.lastInvoiceDate ?? null
3819
- );
3820
- const [customDateFrom, setCustomDateFrom] = useState4(
3821
- filters?.customDateRange?.from ?? initialFilters?.customDateRange?.from ?? ""
3822
- );
3823
- const [customDateTo, setCustomDateTo] = useState4(
3824
- filters?.customDateRange?.to ?? initialFilters?.customDateRange?.to ?? ""
3825
- );
3826
- const [ltvOperator, setLtvOperator] = useState4(
3827
- filters?.lifetimeValue?.operator ?? initialFilters?.lifetimeValue?.operator ?? "greater_than"
3828
- );
3829
- const [ltvValue, setLtvValue] = useState4(filters?.lifetimeValue?.value ?? initialFilters?.lifetimeValue?.value ?? "");
3830
- const [isLtvOperatorOpen, setLtvOperatorOpen] = useState4(false);
3831
- const [outstandingOperator, setOutstandingOperator] = useState4(
3832
- filters?.outstanding?.operator ?? initialFilters?.outstanding?.operator ?? "greater_than"
3833
- );
3834
- const [outstandingValue, setOutstandingValue] = useState4(filters?.outstanding?.value ?? initialFilters?.outstanding?.value ?? "");
3835
- const [isOutstandingOperatorOpen, setOutstandingOperatorOpen] = useState4(false);
3836
3981
  useEffect3(() => {
3837
- if (filters) {
3838
- setSelectedContacts(filters.contacts ?? []);
3839
- setWalletInput(filters.walletAddress ?? "");
3840
- setAppliedWallet(filters.walletAddress ?? "");
3841
- setSelectedDatePreset(filters.lastInvoiceDate ?? null);
3842
- setCustomDateFrom(filters.customDateRange?.from ?? "");
3843
- setCustomDateTo(filters.customDateRange?.to ?? "");
3844
- setLtvOperator(filters.lifetimeValue?.operator ?? "greater_than");
3845
- setLtvValue(filters.lifetimeValue?.value ?? "");
3846
- setOutstandingOperator(filters.outstanding?.operator ?? "greater_than");
3847
- setOutstandingValue(filters.outstanding?.value ?? "");
3982
+ if (!fields.some((f) => f.id === activeId)) {
3983
+ setActiveId(fields[0]?.id ?? null);
3848
3984
  }
3849
- }, [filters]);
3850
- const [contactSearch, setContactSearch] = useState4("");
3851
- const [walletSearch, setWalletSearch] = useState4("");
3852
- const refDate = useMemo2(() => new Date(2026, 5, 26), []);
3853
- const presets = useMemo2(() => {
3854
- const year = refDate.getFullYear();
3855
- const month = refDate.getMonth();
3856
- const lastMonthDate = new Date(year, month - 1, 1);
3857
- const lastMonthLabel = lastMonthDate.toLocaleDateString("en-US", { month: "short", year: "numeric" });
3858
- const thisMonthLabel = refDate.toLocaleDateString("en-US", { month: "short", year: "numeric" });
3859
- const thisQuarter = Math.floor(month / 3) + 1;
3860
- const thisQuarterLabel = `Q${thisQuarter} ${year}`;
3861
- const lastQuarter = thisQuarter === 1 ? 4 : thisQuarter - 1;
3862
- const lastQuarterYear = thisQuarter === 1 ? year - 1 : year;
3863
- const lastQuarterLabel = `Q${lastQuarter} ${lastQuarterYear}`;
3864
- const thisYearLabel = `${year}`;
3865
- const last30 = new Date(refDate);
3866
- last30.setDate(refDate.getDate() - 30);
3867
- const formatDate = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
3868
- const last30Label = `${formatDate(last30)} - ${formatDate(refDate)}`;
3869
- const last90 = new Date(refDate);
3870
- last90.setDate(refDate.getDate() - 90);
3871
- const last90Label = `${formatDate(last90)} - ${formatDate(refDate)}`;
3872
- return [
3873
- { id: "last_month", label: "Last month", date: lastMonthLabel },
3874
- { id: "this_month", label: "This month", date: thisMonthLabel },
3875
- { id: "this_quarter", label: "This quarter", date: thisQuarterLabel },
3876
- { id: "last_quarter", label: "Last quarter", date: lastQuarterLabel },
3877
- { id: "this_year", label: "This year", date: thisYearLabel },
3878
- { id: "last_30_days", label: "Last 30 days", date: last30Label },
3879
- { id: "last_90_days", label: "Last 90 days", date: last90Label },
3880
- { id: "custom", label: "Custom range", date: "" }
3881
- ];
3882
- }, [refDate]);
3883
- const filteredContacts = useMemo2(() => {
3884
- if (!contactSearch) return contacts;
3885
- const query = contactSearch.toLowerCase();
3886
- return contacts.filter(
3887
- (c) => c.name.toLowerCase().includes(query) || c.email.toLowerCase().includes(query)
3888
- );
3889
- }, [contacts, contactSearch]);
3890
- const handleContactToggle = (contactName) => {
3891
- const next = selectedContacts.includes(contactName) ? selectedContacts.filter((name) => name !== contactName) : [...selectedContacts, contactName];
3892
- setSelectedContacts(next);
3893
- notifyChanges({ contacts: next });
3894
- setIsOpen(false);
3895
- };
3896
- const handleWalletApply = () => {
3897
- setAppliedWallet(walletInput);
3898
- notifyChanges({ walletAddress: walletInput });
3899
- setIsOpen(false);
3985
+ }, [fields, activeId]);
3986
+ const commit = (id, next) => {
3987
+ const merged = { ...values, [id]: next };
3988
+ if (!isControlled) setInternal(merged);
3989
+ onChange?.(merged);
3900
3990
  };
3901
- const handleWalletClear = () => {
3902
- setWalletInput("");
3903
- setAppliedWallet("");
3904
- notifyChanges({ walletAddress: "" });
3905
- setIsOpen(false);
3991
+ const clearAll = () => {
3992
+ if (!isControlled) setInternal({});
3993
+ onChange?.({});
3906
3994
  };
3907
- const handleDatePresetSelect = (presetId) => {
3908
- setSelectedDatePreset(presetId);
3909
- if (presetId !== "custom") {
3910
- notifyChanges({ lastInvoiceDate: presetId, customDateRange: void 0 });
3911
- setIsOpen(false);
3995
+ const activeIdx = fields.findIndex((f) => f.id === activeId);
3996
+ const activeField = activeIdx >= 0 ? fields[activeIdx] : void 0;
3997
+ const chips = [];
3998
+ for (const field of fields) {
3999
+ const v = values[field.id];
4000
+ if (field.type === "multiselect") {
4001
+ const selected = asArray(v);
4002
+ for (const optionValue of selected) {
4003
+ const opt = field.options?.find((o) => o.value === optionValue);
4004
+ chips.push({
4005
+ id: `${field.id}:${optionValue}`,
4006
+ label: `${field.label}: ${opt?.label ?? optionValue}`,
4007
+ remove: () => commit(field.id, selected.filter((x) => x !== optionValue))
4008
+ });
4009
+ }
4010
+ } else if (field.type === "text") {
4011
+ const text = asText(v);
4012
+ if (text) {
4013
+ chips.push({
4014
+ id: field.id,
4015
+ label: `${field.label}: ${text}`,
4016
+ remove: () => commit(field.id, "")
4017
+ });
4018
+ }
4019
+ } else if (field.type === "numeric") {
4020
+ const n = asNumeric(v);
4021
+ if (n.value) {
4022
+ chips.push({
4023
+ id: field.id,
4024
+ label: `${field.label} ${OPERATOR_SYMBOL[n.operator]} ${n.value}`,
4025
+ remove: () => commit(field.id, null)
4026
+ });
4027
+ }
4028
+ } else if (field.type === "daterange") {
4029
+ const d = asDate(v);
4030
+ if (d.preset) {
4031
+ const presetLabel = d.preset === "custom" ? `${d.from || "\u2026"} \u2013 ${d.to || "\u2026"}` : (field.presets ?? DEFAULT_PRESETS).find((p) => p.id === d.preset)?.label ?? d.preset;
4032
+ chips.push({
4033
+ id: field.id,
4034
+ label: `${field.label}: ${presetLabel}`,
4035
+ remove: () => commit(field.id, { preset: null })
4036
+ });
4037
+ }
3912
4038
  }
3913
- };
3914
- const handleCustomDateApply = () => {
3915
- notifyChanges({
3916
- lastInvoiceDate: "custom",
3917
- customDateRange: { from: customDateFrom, to: customDateTo }
3918
- });
3919
- setIsOpen(false);
3920
- };
3921
- const handleLtvApply = () => {
3922
- notifyChanges({
3923
- lifetimeValue: ltvValue ? { operator: ltvOperator, value: ltvValue } : null
3924
- });
3925
- setIsOpen(false);
3926
- };
3927
- const handleLtvClear = () => {
3928
- setLtvValue("");
3929
- notifyChanges({ lifetimeValue: null });
3930
- setIsOpen(false);
3931
- };
3932
- const handleOutstandingApply = () => {
3933
- notifyChanges({
3934
- outstanding: outstandingValue ? { operator: outstandingOperator, value: outstandingValue } : null
3935
- });
3936
- setIsOpen(false);
3937
- };
3938
- const handleOutstandingClear = () => {
3939
- setOutstandingValue("");
3940
- notifyChanges({ outstanding: null });
3941
- setIsOpen(false);
3942
- };
3943
- const notifyChanges = (overrides) => {
3944
- const current = {
3945
- contacts: selectedContacts,
3946
- walletAddress: appliedWallet,
3947
- lastInvoiceDate: selectedDatePreset,
3948
- customDateRange: customDateFrom || customDateTo ? { from: customDateFrom, to: customDateTo } : void 0,
3949
- lifetimeValue: ltvValue ? { operator: ltvOperator, value: ltvValue } : null,
3950
- outstanding: outstandingValue ? { operator: outstandingOperator, value: outstandingValue } : null,
3951
- ...overrides
3952
- };
3953
- onFiltersChange?.(current);
3954
- };
3955
- const menuItems = [
3956
- { id: "contact", label: "Contact", icon: /* @__PURE__ */ jsx47(UserIcon, { className: "size-4" }) },
3957
- { id: "wallet", label: "Wallet address", icon: /* @__PURE__ */ jsx47(WalletIcon, { className: "size-4" }) },
3958
- { id: "date", label: "Last invoice date", icon: /* @__PURE__ */ jsx47(CalendarIcon, { className: "size-4" }) },
3959
- { id: "ltv", label: "Lifetime value", icon: /* @__PURE__ */ jsx47(TrendingUpIcon, { className: "size-4" }) },
3960
- { id: "outstanding", label: "Outstanding", icon: /* @__PURE__ */ jsx47(CircleDollarSignIcon, { className: "size-4" }) }
3961
- ];
3962
- const activeIdx = menuItems.findIndex((item) => item.id === activeMenu);
3963
- return /* @__PURE__ */ jsx47("div", { className: cn("inline-block", className), children: /* @__PURE__ */ jsxs36(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [
3964
- /* @__PURE__ */ jsx47(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx47(
3965
- Button,
4039
+ }
4040
+ return /* @__PURE__ */ jsxs36("div", { className: cn("flex flex-wrap items-center gap-2", className), children: [
4041
+ /* @__PURE__ */ jsxs36(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [
4042
+ /* @__PURE__ */ jsx47(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx47(
4043
+ Button,
4044
+ {
4045
+ variant: "outline",
4046
+ size: "sm",
4047
+ className: "border-dashed font-medium text-muted-foreground hover:text-foreground",
4048
+ iconLeading: /* @__PURE__ */ jsx47(ListFilterIcon, { className: "size-4" }),
4049
+ children: label
4050
+ }
4051
+ ) }),
4052
+ /* @__PURE__ */ jsx47(
4053
+ PopoverContent,
4054
+ {
4055
+ variant: "list",
4056
+ align,
4057
+ className: "overflow-visible border-none bg-transparent p-0 shadow-none max-w-[calc(100vw-32px)] md:max-w-none",
4058
+ children: /* @__PURE__ */ jsxs36("div", { className: "relative flex flex-col md:flex-row items-stretch md:items-start w-[calc(100vw-32px)] max-w-[340px] md:w-auto md:max-w-none", children: [
4059
+ /* @__PURE__ */ jsx47("div", { className: "w-full md:w-56 rounded-xl border border-border bg-popover p-1.5 shadow-lg", children: fields.map((field) => {
4060
+ const isActive = activeId === field.id;
4061
+ return /* @__PURE__ */ jsxs36(
4062
+ "button",
4063
+ {
4064
+ type: "button",
4065
+ className: cn(
4066
+ "flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm text-left transition-colors outline-none",
4067
+ isActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4068
+ ),
4069
+ onMouseEnter: () => !isMobile && setActiveId(field.id),
4070
+ onClick: () => setActiveId(field.id),
4071
+ children: [
4072
+ /* @__PURE__ */ jsxs36("span", { className: "flex items-center gap-2", children: [
4073
+ field.icon,
4074
+ /* @__PURE__ */ jsx47("span", { children: field.label })
4075
+ ] }),
4076
+ /* @__PURE__ */ jsx47(ChevronRightIcon, { className: "size-4 text-muted-foreground/50" })
4077
+ ]
4078
+ },
4079
+ field.id
4080
+ );
4081
+ }) }),
4082
+ activeField && /* @__PURE__ */ jsx47(
4083
+ "div",
4084
+ {
4085
+ className: "relative left-0 mt-2 w-full md:absolute md:left-[calc(100%+6px)] md:w-80 rounded-xl border border-border bg-popover p-3 shadow-lg transition-all duration-150 md:mt-0",
4086
+ style: isMobile ? {} : { top: `${activeIdx * 36 + 6}px` },
4087
+ children: /* @__PURE__ */ jsx47(
4088
+ FilterFieldControl,
4089
+ {
4090
+ field: activeField,
4091
+ value: values[activeField.id],
4092
+ onChange: (next) => commit(activeField.id, next),
4093
+ onClose: () => setIsOpen(false)
4094
+ }
4095
+ )
4096
+ }
4097
+ )
4098
+ ] })
4099
+ }
4100
+ )
4101
+ ] }),
4102
+ showActiveChips && chips.map((chip) => /* @__PURE__ */ jsx47(FilterChip, { label: chip.label, onRemove: chip.remove }, chip.id)),
4103
+ showActiveChips && chips.length > 0 && /* @__PURE__ */ jsx47(
4104
+ "button",
3966
4105
  {
3967
- variant: "outline",
3968
- size: "sm",
3969
- shape: "pill",
3970
- className: "border-dashed font-medium text-muted-foreground hover:text-foreground",
3971
- iconLeading: /* @__PURE__ */ jsx47(ListFilterIcon, { className: "size-4" }),
3972
- children: "Filter"
4106
+ type: "button",
4107
+ onClick: clearAll,
4108
+ className: "rounded-full px-3 py-1 text-sm font-medium text-muted-foreground outline-none transition-colors hover:text-foreground",
4109
+ children: "Clear all"
3973
4110
  }
3974
- ) }),
4111
+ )
4112
+ ] });
4113
+ }
4114
+ function FilterChip({ label, onRemove }) {
4115
+ return /* @__PURE__ */ jsxs36("span", { className: "inline-flex h-9 items-center gap-1.5 rounded-full border border-border bg-muted/40 pl-3 pr-1.5 text-sm font-medium text-foreground", children: [
4116
+ /* @__PURE__ */ jsx47("span", { className: "truncate", children: label }),
3975
4117
  /* @__PURE__ */ jsx47(
3976
- PopoverContent,
4118
+ "button",
3977
4119
  {
3978
- variant: "list",
3979
- align: "start",
3980
- className: "overflow-visible border-none bg-transparent p-0 shadow-none max-w-[calc(100vw-32px)] md:max-w-none",
3981
- children: /* @__PURE__ */ jsxs36("div", { className: "relative flex flex-col md:flex-row items-stretch md:items-start w-[calc(100vw-32px)] max-w-[340px] md:w-auto md:max-w-none", children: [
3982
- /* @__PURE__ */ jsx47("div", { className: "w-full md:w-56 rounded-xl border border-border bg-popover p-1.5 shadow-lg", children: menuItems.map((item) => {
3983
- const isActive = activeMenu === item.id;
3984
- return /* @__PURE__ */ jsxs36(
3985
- "button",
3986
- {
3987
- type: "button",
3988
- className: cn(
3989
- "flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm text-left transition-colors outline-none",
3990
- isActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
3991
- ),
3992
- onMouseEnter: () => !isMobile && setActiveMenu(item.id),
3993
- onClick: () => setActiveMenu(item.id),
3994
- children: [
3995
- /* @__PURE__ */ jsxs36("span", { className: "flex items-center gap-2", children: [
3996
- item.icon,
3997
- /* @__PURE__ */ jsx47("span", { children: item.label })
3998
- ] }),
3999
- /* @__PURE__ */ jsx47(ChevronRightIcon, { className: "size-4 text-muted-foreground/50" })
4000
- ]
4001
- },
4002
- item.id
4003
- );
4004
- }) }),
4005
- activeMenu && /* @__PURE__ */ jsxs36(
4006
- "div",
4120
+ type: "button",
4121
+ onClick: onRemove,
4122
+ "aria-label": `Remove ${label}`,
4123
+ className: "flex size-5 items-center justify-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground",
4124
+ children: /* @__PURE__ */ jsx47(XIcon2, { className: "size-3.5" })
4125
+ }
4126
+ )
4127
+ ] });
4128
+ }
4129
+ function FilterFieldControl({
4130
+ field,
4131
+ value,
4132
+ onChange,
4133
+ onClose
4134
+ }) {
4135
+ switch (field.type) {
4136
+ case "multiselect":
4137
+ return /* @__PURE__ */ jsx47(MultiSelectControl, { field, value: asArray(value), onChange });
4138
+ case "text":
4139
+ return /* @__PURE__ */ jsx47(TextControl, { field, value: asText(value), onChange, onClose });
4140
+ case "daterange":
4141
+ return /* @__PURE__ */ jsx47(DateRangeControl, { field, value: asDate(value), onChange, onClose });
4142
+ case "numeric":
4143
+ return /* @__PURE__ */ jsx47(NumericControl, { field, value: asNumeric(value), onChange, onClose });
4144
+ default:
4145
+ return null;
4146
+ }
4147
+ }
4148
+ function MultiSelectControl({
4149
+ field,
4150
+ value,
4151
+ onChange
4152
+ }) {
4153
+ const options = field.options ?? [];
4154
+ const [search, setSearch] = useState5("");
4155
+ const searchable = field.searchable ?? options.length > 8;
4156
+ const filtered = useMemo2(() => {
4157
+ if (!search) return options;
4158
+ const q = search.toLowerCase();
4159
+ return options.filter(
4160
+ (o) => o.label.toLowerCase().includes(q) || o.hint?.toLowerCase().includes(q)
4161
+ );
4162
+ }, [options, search]);
4163
+ const toggle = (optionValue) => {
4164
+ onChange(
4165
+ value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]
4166
+ );
4167
+ };
4168
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4169
+ searchable && /* @__PURE__ */ jsx47(
4170
+ SearchInput,
4171
+ {
4172
+ placeholder: field.searchPlaceholder ?? "Search\u2026",
4173
+ value: search,
4174
+ onChange: (e) => setSearch(e.target.value),
4175
+ className: "w-full min-w-0"
4176
+ }
4177
+ ),
4178
+ /* @__PURE__ */ jsx47("div", { className: "flex max-h-48 flex-col gap-1 overflow-y-auto pr-1", children: filtered.length === 0 ? /* @__PURE__ */ jsx47("p", { className: "py-4 text-center text-xs text-muted-foreground", children: "No options found" }) : filtered.map((option) => /* @__PURE__ */ jsxs36(
4179
+ "label",
4180
+ {
4181
+ className: "flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-muted/50",
4182
+ children: [
4183
+ /* @__PURE__ */ jsx47(
4184
+ Checkbox,
4007
4185
  {
4008
- className: "relative left-0 mt-2 w-full md:absolute md:left-[calc(100%+6px)] md:w-80 rounded-xl border border-border bg-popover p-3 shadow-lg transition-all duration-150 md:mt-0",
4009
- style: isMobile ? {} : { top: `${activeIdx * 36 + 6}px` },
4010
- children: [
4011
- activeMenu === "contact" && /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4012
- /* @__PURE__ */ jsxs36("div", { className: "relative flex items-center", children: [
4013
- /* @__PURE__ */ jsx47(SearchIcon2, { className: "absolute left-2.5 size-4 text-muted-foreground/60" }),
4014
- /* @__PURE__ */ jsx47(
4015
- "input",
4016
- {
4017
- type: "text",
4018
- placeholder: "Search by name or email...",
4019
- value: contactSearch,
4020
- onChange: (e) => setContactSearch(e.target.value),
4021
- className: "w-full rounded-lg border border-border bg-background py-1.5 pl-8 pr-3 text-sm outline-none placeholder:text-muted-foreground/60 focus:border-border"
4022
- }
4023
- )
4024
- ] }),
4025
- /* @__PURE__ */ jsx47("div", { className: "max-h-48 overflow-y-auto flex flex-col gap-1 pr-1", children: filteredContacts.length === 0 ? /* @__PURE__ */ jsx47("p", { className: "py-4 text-center text-xs text-muted-foreground", children: "No contacts found" }) : filteredContacts.map((contact) => {
4026
- const isChecked = selectedContacts.includes(contact.name);
4027
- return /* @__PURE__ */ jsxs36(
4028
- "label",
4029
- {
4030
- className: "flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-muted/50 transition-colors",
4031
- children: [
4032
- /* @__PURE__ */ jsx47(
4033
- Checkbox,
4034
- {
4035
- checked: isChecked,
4036
- onCheckedChange: () => handleContactToggle(contact.name)
4037
- }
4038
- ),
4039
- /* @__PURE__ */ jsx47(Avatar, { variant: "secondary", children: /* @__PURE__ */ jsx47(AvatarFallback, { children: contact.initials }) }),
4040
- /* @__PURE__ */ jsx47("span", { className: "text-sm font-medium text-foreground", children: contact.name })
4041
- ]
4042
- },
4043
- contact.id
4044
- );
4045
- }) })
4046
- ] }),
4047
- activeMenu === "wallet" && /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4048
- /* @__PURE__ */ jsx47("div", { className: "relative flex items-center", children: /* @__PURE__ */ jsx47(
4049
- "input",
4050
- {
4051
- type: "text",
4052
- placeholder: "Search by wallet...",
4053
- value: walletInput,
4054
- onChange: (e) => setWalletInput(e.target.value),
4055
- className: "w-full rounded-lg border border-border bg-background px-3 py-1.5 text-sm outline-none placeholder:text-muted-foreground/60"
4056
- }
4057
- ) }),
4058
- /* @__PURE__ */ jsxs36("div", { className: "flex items-center justify-end gap-2 pt-1 border-t border-border/40", children: [
4059
- /* @__PURE__ */ jsx47(
4060
- Button,
4061
- {
4062
- variant: "ghost",
4063
- size: "sm",
4064
- onClick: handleWalletClear,
4065
- className: "text-muted-foreground hover:text-foreground h-8 px-3",
4066
- children: "Clear"
4067
- }
4068
- ),
4069
- /* @__PURE__ */ jsx47(
4070
- Button,
4071
- {
4072
- color: "primary",
4073
- size: "sm",
4074
- onClick: handleWalletApply,
4075
- className: "h-8 px-3",
4076
- children: "Apply"
4077
- }
4078
- )
4079
- ] })
4080
- ] }),
4081
- activeMenu === "date" && /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-1", children: [
4082
- presets.map((preset) => {
4083
- const isSelected = selectedDatePreset === preset.id;
4084
- return /* @__PURE__ */ jsxs36(
4085
- "button",
4086
- {
4087
- type: "button",
4088
- onClick: () => handleDatePresetSelect(preset.id),
4089
- className: cn(
4090
- "flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-sm text-left transition-colors outline-none",
4091
- isSelected ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4092
- ),
4093
- children: [
4094
- /* @__PURE__ */ jsx47("span", { children: preset.label }),
4095
- preset.date && /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground/70", children: preset.date })
4096
- ]
4097
- },
4098
- preset.id
4099
- );
4100
- }),
4101
- selectedDatePreset === "custom" && /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2 mt-2 pt-2 border-t border-border/40", children: [
4102
- /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4103
- /* @__PURE__ */ jsx47(
4104
- "input",
4105
- {
4106
- type: "date",
4107
- value: customDateFrom,
4108
- onChange: (e) => setCustomDateFrom(e.target.value),
4109
- className: "w-full rounded-lg border border-border bg-background px-2 py-1 text-xs outline-none"
4110
- }
4111
- ),
4112
- /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground", children: "to" }),
4113
- /* @__PURE__ */ jsx47(
4114
- "input",
4115
- {
4116
- type: "date",
4117
- value: customDateTo,
4118
- onChange: (e) => setCustomDateTo(e.target.value),
4119
- className: "w-full rounded-lg border border-border bg-background px-2 py-1 text-xs outline-none"
4120
- }
4121
- )
4122
- ] }),
4123
- /* @__PURE__ */ jsx47("div", { className: "flex justify-end gap-2", children: /* @__PURE__ */ jsx47(
4124
- Button,
4125
- {
4126
- color: "primary",
4127
- size: "sm",
4128
- onClick: handleCustomDateApply,
4129
- className: "h-7 text-xs px-2.5",
4130
- children: "Apply"
4131
- }
4132
- ) })
4133
- ] })
4134
- ] }),
4135
- activeMenu === "ltv" && /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4136
- /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4137
- /* @__PURE__ */ jsxs36("div", { className: "relative shrink-0", children: [
4138
- /* @__PURE__ */ jsxs36(
4139
- "button",
4140
- {
4141
- type: "button",
4142
- onClick: () => setLtvOperatorOpen(!isLtvOperatorOpen),
4143
- className: "flex h-9 items-center gap-1 rounded-lg border border-border bg-background px-2.5 text-xs font-normal text-muted-foreground hover:bg-muted/50 hover:text-foreground outline-none whitespace-nowrap",
4144
- children: [
4145
- /* @__PURE__ */ jsx47("span", { children: OPERATORS.find((op) => op.id === ltvOperator)?.label.replace("...", "") }),
4146
- /* @__PURE__ */ jsx47(ChevronDownIcon2, { className: "size-3" })
4147
- ]
4148
- }
4149
- ),
4150
- isLtvOperatorOpen && /* @__PURE__ */ jsx47("div", { className: "absolute left-0 top-full z-50 mt-1 w-32 rounded-lg border border-border bg-popover p-1 shadow-md", children: OPERATORS.map((op) => /* @__PURE__ */ jsx47(
4151
- "button",
4152
- {
4153
- type: "button",
4154
- onClick: () => {
4155
- setLtvOperator(op.id);
4156
- setLtvOperatorOpen(false);
4157
- },
4158
- className: "w-full rounded-md px-2 py-1 text-left text-xs text-foreground hover:bg-muted outline-none",
4159
- children: op.label
4160
- },
4161
- op.id
4162
- )) })
4163
- ] }),
4164
- /* @__PURE__ */ jsx47(
4165
- "input",
4166
- {
4167
- type: "text",
4168
- placeholder: "0.00",
4169
- value: ltvValue,
4170
- onChange: (e) => setLtvValue(e.target.value),
4171
- className: "h-9 flex-1 min-w-0 rounded-lg border border-border bg-background px-3 py-1 text-sm outline-none placeholder:text-muted-foreground/60"
4172
- }
4173
- )
4174
- ] }),
4175
- /* @__PURE__ */ jsxs36("div", { className: "flex items-center justify-end gap-2 pt-1 border-t border-border/40", children: [
4176
- /* @__PURE__ */ jsx47(
4177
- Button,
4178
- {
4179
- variant: "ghost",
4180
- size: "sm",
4181
- onClick: handleLtvClear,
4182
- className: "text-muted-foreground hover:text-foreground h-8 px-3",
4183
- children: "Clear"
4184
- }
4185
- ),
4186
- /* @__PURE__ */ jsx47(
4187
- Button,
4188
- {
4189
- color: "primary",
4190
- size: "sm",
4191
- onClick: handleLtvApply,
4192
- className: "h-8 px-3",
4193
- children: "Apply"
4194
- }
4195
- )
4196
- ] })
4197
- ] }),
4198
- activeMenu === "outstanding" && /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4199
- /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4200
- /* @__PURE__ */ jsxs36("div", { className: "relative shrink-0", children: [
4201
- /* @__PURE__ */ jsxs36(
4202
- "button",
4203
- {
4204
- type: "button",
4205
- onClick: () => setOutstandingOperatorOpen(!isOutstandingOperatorOpen),
4206
- className: "flex h-9 items-center gap-1 rounded-lg border border-border bg-background px-2.5 text-xs font-normal text-muted-foreground hover:bg-muted/50 hover:text-foreground outline-none whitespace-nowrap",
4207
- children: [
4208
- /* @__PURE__ */ jsx47("span", { children: OPERATORS.find((op) => op.id === outstandingOperator)?.label.replace("...", "") }),
4209
- /* @__PURE__ */ jsx47(ChevronDownIcon2, { className: "size-3" })
4210
- ]
4211
- }
4212
- ),
4213
- isOutstandingOperatorOpen && /* @__PURE__ */ jsx47("div", { className: "absolute left-0 top-full z-50 mt-1 w-32 rounded-lg border border-border bg-popover p-1 shadow-md", children: OPERATORS.map((op) => /* @__PURE__ */ jsx47(
4214
- "button",
4215
- {
4216
- type: "button",
4217
- onClick: () => {
4218
- setOutstandingOperator(op.id);
4219
- setOutstandingOperatorOpen(false);
4220
- },
4221
- className: "w-full rounded-md px-2 py-1 text-left text-xs text-foreground hover:bg-muted outline-none",
4222
- children: op.label
4223
- },
4224
- op.id
4225
- )) })
4226
- ] }),
4227
- /* @__PURE__ */ jsx47(
4228
- "input",
4229
- {
4230
- type: "text",
4231
- placeholder: "0.00",
4232
- value: outstandingValue,
4233
- onChange: (e) => setOutstandingValue(e.target.value),
4234
- className: "h-9 flex-1 min-w-0 rounded-lg border border-border bg-background px-3 py-1 text-sm outline-none placeholder:text-muted-foreground/60"
4235
- }
4236
- )
4237
- ] }),
4238
- /* @__PURE__ */ jsxs36("div", { className: "flex items-center justify-end gap-2 pt-1 border-t border-border/40", children: [
4239
- /* @__PURE__ */ jsx47(
4240
- Button,
4241
- {
4242
- variant: "ghost",
4243
- size: "sm",
4244
- onClick: handleOutstandingClear,
4245
- className: "text-muted-foreground hover:text-foreground h-8 px-3",
4246
- children: "Clear"
4247
- }
4248
- ),
4249
- /* @__PURE__ */ jsx47(
4250
- Button,
4251
- {
4252
- color: "primary",
4253
- size: "sm",
4254
- onClick: handleOutstandingApply,
4255
- className: "h-8 px-3",
4256
- children: "Apply"
4257
- }
4258
- )
4259
- ] })
4260
- ] })
4261
- ]
4186
+ checked: value.includes(option.value),
4187
+ onCheckedChange: () => toggle(option.value)
4262
4188
  }
4263
- )
4264
- ] })
4189
+ ),
4190
+ option.icon,
4191
+ /* @__PURE__ */ jsxs36("span", { className: "flex min-w-0 flex-col", children: [
4192
+ /* @__PURE__ */ jsx47("span", { className: "truncate text-sm font-medium text-foreground", children: option.label }),
4193
+ option.hint && /* @__PURE__ */ jsx47("span", { className: "truncate text-xs text-muted-foreground", children: option.hint })
4194
+ ] })
4195
+ ]
4196
+ },
4197
+ option.value
4198
+ )) })
4199
+ ] });
4200
+ }
4201
+ function TextControl({
4202
+ field,
4203
+ value,
4204
+ onChange,
4205
+ onClose
4206
+ }) {
4207
+ const [draft, setDraft] = useState5(value);
4208
+ useEffect3(() => setDraft(value), [value]);
4209
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4210
+ /* @__PURE__ */ jsx47(
4211
+ "input",
4212
+ {
4213
+ type: "text",
4214
+ placeholder: field.placeholder ?? "Type a value\u2026",
4215
+ value: draft,
4216
+ onChange: (e) => setDraft(e.target.value),
4217
+ onKeyDown: (e) => {
4218
+ if (e.key === "Enter") {
4219
+ onChange(draft);
4220
+ onClose();
4221
+ }
4222
+ },
4223
+ className: controlClass({ size: "sm" }, "w-full")
4224
+ }
4225
+ ),
4226
+ /* @__PURE__ */ jsx47(
4227
+ ApplyClear,
4228
+ {
4229
+ onClear: () => {
4230
+ setDraft("");
4231
+ onChange("");
4232
+ onClose();
4233
+ },
4234
+ onApply: () => {
4235
+ onChange(draft);
4236
+ onClose();
4237
+ }
4265
4238
  }
4266
4239
  )
4267
- ] }) });
4240
+ ] });
4241
+ }
4242
+ function DateRangeControl({
4243
+ field,
4244
+ value,
4245
+ onChange,
4246
+ onClose
4247
+ }) {
4248
+ const presets = field.presets ?? DEFAULT_PRESETS;
4249
+ const [from, setFrom] = useState5(value.from ?? "");
4250
+ const [to, setTo] = useState5(value.to ?? "");
4251
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-1", children: [
4252
+ presets.map((preset) => {
4253
+ const isSelected = value.preset === preset.id;
4254
+ return /* @__PURE__ */ jsxs36(
4255
+ "button",
4256
+ {
4257
+ type: "button",
4258
+ onClick: () => {
4259
+ if (preset.id === "custom") {
4260
+ onChange({ preset: "custom", from, to });
4261
+ } else {
4262
+ onChange({ preset: preset.id });
4263
+ onClose();
4264
+ }
4265
+ },
4266
+ className: cn(
4267
+ "flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-left text-sm transition-colors outline-none",
4268
+ isSelected ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4269
+ ),
4270
+ children: [
4271
+ /* @__PURE__ */ jsx47("span", { children: preset.label }),
4272
+ preset.hint && /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground/70", children: preset.hint })
4273
+ ]
4274
+ },
4275
+ preset.id
4276
+ );
4277
+ }),
4278
+ value.preset === "custom" && /* @__PURE__ */ jsxs36("div", { className: "mt-2 flex flex-col gap-2 border-t border-border/40 pt-2", children: [
4279
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4280
+ /* @__PURE__ */ jsx47(
4281
+ "input",
4282
+ {
4283
+ type: "date",
4284
+ value: from,
4285
+ onChange: (e) => setFrom(e.target.value),
4286
+ className: controlClass({ size: "sm" }, "w-full text-xs")
4287
+ }
4288
+ ),
4289
+ /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground", children: "to" }),
4290
+ /* @__PURE__ */ jsx47(
4291
+ "input",
4292
+ {
4293
+ type: "date",
4294
+ value: to,
4295
+ onChange: (e) => setTo(e.target.value),
4296
+ className: controlClass({ size: "sm" }, "w-full text-xs")
4297
+ }
4298
+ )
4299
+ ] }),
4300
+ /* @__PURE__ */ jsx47("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx47(
4301
+ Button,
4302
+ {
4303
+ color: "primary",
4304
+ size: "sm",
4305
+ className: "h-8 px-3",
4306
+ onClick: () => {
4307
+ onChange({ preset: "custom", from, to });
4308
+ onClose();
4309
+ },
4310
+ children: "Apply"
4311
+ }
4312
+ ) })
4313
+ ] })
4314
+ ] });
4315
+ }
4316
+ function NumericControl({
4317
+ field,
4318
+ value,
4319
+ onChange,
4320
+ onClose
4321
+ }) {
4322
+ const operators = field.operators ?? DEFAULT_OPERATORS;
4323
+ const [operator, setOperator] = useState5(value.operator);
4324
+ const [draft, setDraft] = useState5(value.value);
4325
+ useEffect3(() => {
4326
+ setOperator(value.operator);
4327
+ setDraft(value.value);
4328
+ }, [value.operator, value.value]);
4329
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4330
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4331
+ /* @__PURE__ */ jsxs36(Select, { value: operator, onValueChange: (v) => setOperator(v), children: [
4332
+ /* @__PURE__ */ jsx47(SelectTrigger, { size: "sm", className: "shrink-0", children: /* @__PURE__ */ jsx47(SelectValue, {}) }),
4333
+ /* @__PURE__ */ jsx47(SelectContent, { children: operators.map((op) => /* @__PURE__ */ jsx47(SelectItem, { value: op.id, children: op.label }, op.id)) })
4334
+ ] }),
4335
+ /* @__PURE__ */ jsx47(
4336
+ "input",
4337
+ {
4338
+ type: "text",
4339
+ inputMode: "decimal",
4340
+ placeholder: field.placeholder ?? "0.00",
4341
+ value: draft,
4342
+ onChange: (e) => setDraft(e.target.value),
4343
+ onKeyDown: (e) => {
4344
+ if (e.key === "Enter") {
4345
+ onChange(draft ? { operator, value: draft } : null);
4346
+ onClose();
4347
+ }
4348
+ },
4349
+ className: controlClass({ size: "sm" }, "min-w-0 flex-1")
4350
+ }
4351
+ )
4352
+ ] }),
4353
+ /* @__PURE__ */ jsx47(
4354
+ ApplyClear,
4355
+ {
4356
+ onClear: () => {
4357
+ setDraft("");
4358
+ onChange(null);
4359
+ onClose();
4360
+ },
4361
+ onApply: () => {
4362
+ onChange(draft ? { operator, value: draft } : null);
4363
+ onClose();
4364
+ }
4365
+ }
4366
+ )
4367
+ ] });
4368
+ }
4369
+ function ApplyClear({ onClear, onApply }) {
4370
+ return /* @__PURE__ */ jsxs36("div", { className: "flex items-center justify-end gap-2 border-t border-border/40 pt-1", children: [
4371
+ /* @__PURE__ */ jsx47(
4372
+ Button,
4373
+ {
4374
+ variant: "ghost",
4375
+ size: "sm",
4376
+ onClick: onClear,
4377
+ className: "h-8 px-3 text-muted-foreground hover:text-foreground",
4378
+ children: "Clear"
4379
+ }
4380
+ ),
4381
+ /* @__PURE__ */ jsx47(Button, { color: "primary", size: "sm", onClick: onApply, className: "h-8 px-3", children: "Apply" })
4382
+ ] });
4268
4383
  }
4269
4384
 
4270
4385
  // src/app/data/DataTable.tsx
4271
- import { useEffect as useEffect4, useMemo as useMemo3, useState as useState5 } from "react";
4386
+ import { useEffect as useEffect4, useMemo as useMemo3, useState as useState6 } from "react";
4272
4387
  import {
4273
4388
  ArrowDownIcon,
4274
4389
  ArrowUpDownIcon,
@@ -4356,7 +4471,7 @@ function DataTable({
4356
4471
  defaultPageIndex = 0,
4357
4472
  onPageChange
4358
4473
  }) {
4359
- const [uncontrolledSort, setUncontrolledSort] = useState5(
4474
+ const [uncontrolledSort, setUncontrolledSort] = useState6(
4360
4475
  defaultSort
4361
4476
  );
4362
4477
  const isSortControlled = sortProp !== void 0;
@@ -4367,7 +4482,7 @@ function DataTable({
4367
4482
  }
4368
4483
  onSortChange?.(next);
4369
4484
  };
4370
- const [uncontrolledSelected, setUncontrolledSelected] = useState5(
4485
+ const [uncontrolledSelected, setUncontrolledSelected] = useState6(
4371
4486
  defaultSelectedKeys ?? []
4372
4487
  );
4373
4488
  const isSelectionControlled = selectedKeysProp !== void 0;
@@ -4379,7 +4494,7 @@ function DataTable({
4379
4494
  }
4380
4495
  onSelectionChange?.(next);
4381
4496
  };
4382
- const [uncontrolledPage, setUncontrolledPage] = useState5(defaultPageIndex);
4497
+ const [uncontrolledPage, setUncontrolledPage] = useState6(defaultPageIndex);
4383
4498
  const isPageControlled = pageIndexProp !== void 0;
4384
4499
  const rawPageIndex = isPageControlled ? pageIndexProp : uncontrolledPage;
4385
4500
  const setPage = (next) => {
@@ -4683,7 +4798,7 @@ var ChartPanel = ({
4683
4798
  };
4684
4799
 
4685
4800
  // src/app/data/MetricRow.tsx
4686
- import { useId as useId10, useState as useState6 } from "react";
4801
+ import { useId as useId10, useState as useState7 } from "react";
4687
4802
  import { jsx as jsx50, jsxs as jsxs39 } from "react/jsx-runtime";
4688
4803
  var MetricRow = ({
4689
4804
  title,
@@ -4701,7 +4816,7 @@ var MetricRow = ({
4701
4816
  const metricTileClass = useAppDensityClass("metricTile");
4702
4817
  const titleId = useId10();
4703
4818
  const selectable = onMetricChange != null || activeMetricId != null;
4704
- const [internalId, setInternalId] = useState6(
4819
+ const [internalId, setInternalId] = useState7(
4705
4820
  defaultActiveMetricId ?? metrics[0]?.id
4706
4821
  );
4707
4822
  const activeId = activeMetricId ?? internalId;
@@ -4775,7 +4890,7 @@ var MetricRow = ({
4775
4890
  };
4776
4891
 
4777
4892
  // src/app/data/MetricChartCard.tsx
4778
- import { useId as useId11, useState as useState7 } from "react";
4893
+ import { useId as useId11, useState as useState8 } from "react";
4779
4894
  import { jsx as jsx51, jsxs as jsxs40 } from "react/jsx-runtime";
4780
4895
  var MetricChartCard = ({
4781
4896
  title,
@@ -4801,7 +4916,7 @@ var MetricChartCard = ({
4801
4916
  const metricChartRegionClass = useAppDensityClass("metricChartRegion");
4802
4917
  const metricTileClass = useAppDensityClass("metricTile");
4803
4918
  const titleId = useId11();
4804
- const [internalId, setInternalId] = useState7(
4919
+ const [internalId, setInternalId] = useState8(
4805
4920
  defaultActiveMetricId ?? metrics[0]?.id
4806
4921
  );
4807
4922
  const activeId = activeMetricId ?? internalId;
@@ -4913,9 +5028,9 @@ var MetricChartCard = ({
4913
5028
  };
4914
5029
 
4915
5030
  // src/hooks/use-live-query.ts
4916
- import { useCallback as useCallback2, useEffect as useEffect5, useRef, useState as useState8 } from "react";
5031
+ import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef2, useState as useState9 } from "react";
4917
5032
  function useInterval(callback, delayMs) {
4918
- const saved = useRef(callback);
5033
+ const saved = useRef2(callback);
4919
5034
  useEffect5(() => {
4920
5035
  saved.current = callback;
4921
5036
  }, [callback]);
@@ -4932,18 +5047,18 @@ function useLiveQuery(fetcher, options = {}) {
4932
5047
  immediate = true,
4933
5048
  refetchOnFocus = true
4934
5049
  } = options;
4935
- const [data, setData] = useState8(void 0);
4936
- const [error, setError] = useState8(void 0);
4937
- const [loading, setLoading] = useState8(enabled);
4938
- const [refreshing, setRefreshing] = useState8(false);
4939
- const [lastUpdated, setLastUpdated] = useState8(null);
4940
- const fetcherRef = useRef(fetcher);
5050
+ const [data, setData] = useState9(void 0);
5051
+ const [error, setError] = useState9(void 0);
5052
+ const [loading, setLoading] = useState9(enabled);
5053
+ const [refreshing, setRefreshing] = useState9(false);
5054
+ const [lastUpdated, setLastUpdated] = useState9(null);
5055
+ const fetcherRef = useRef2(fetcher);
4941
5056
  useEffect5(() => {
4942
5057
  fetcherRef.current = fetcher;
4943
5058
  }, [fetcher]);
4944
- const mounted = useRef(true);
4945
- const requestId = useRef(0);
4946
- const hasData = useRef(false);
5059
+ const mounted = useRef2(true);
5060
+ const requestId = useRef2(0);
5061
+ const hasData = useRef2(false);
4947
5062
  useEffect5(() => {
4948
5063
  mounted.current = true;
4949
5064
  return () => {