@timbal-ai/timbal-react 1.5.0 → 1.6.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 (52) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +25 -0
  3. package/dist/app.cjs +855 -640
  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-C2pZQsaP.d.ts} +81 -29
  8. package/dist/{chart-artifact-CS3qyGIY.d.cts → chart-artifact-VAqgH-My.d.cts} +81 -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-SZDYIRMB.esm.js → chunk-6SQMTBPL.esm.js} +618 -530
  17. package/dist/{chunk-QIABF4KB.esm.js → chunk-ELEY66OH.esm.js} +2 -2
  18. package/dist/{chunk-WMKPT5BV.esm.js → chunk-HSL36SJ4.esm.js} +6 -6
  19. package/dist/chunk-JJOO4PR5.esm.js +391 -0
  20. package/dist/{chunk-AZL2WANO.esm.js → chunk-MBS7XHV2.esm.js} +28 -28
  21. package/dist/{chunk-5ECRZ5O7.esm.js → chunk-NO5AWNWT.esm.js} +224 -57
  22. package/dist/{chunk-ZNYAETFD.esm.js → chunk-R4RQT2XQ.esm.js} +2 -2
  23. package/dist/{chunk-JYDJRGDE.esm.js → chunk-TMP7RIA7.esm.js} +2 -2
  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/index.cjs +1327 -852
  29. package/dist/index.d.cts +9 -8
  30. package/dist/index.d.ts +9 -8
  31. package/dist/index.esm.js +40 -20
  32. package/dist/{kanban-U5xNe9py.d.cts → kanban-FFBeaZPS.d.cts} +4 -4
  33. package/dist/{kanban-U5xNe9py.d.ts → kanban-FFBeaZPS.d.ts} +4 -4
  34. package/dist/{layout-B8r6Jbat.d.ts → layout-CuKeSY74.d.ts} +1 -1
  35. package/dist/{layout-Cu7Ijn04.d.cts → layout-PzVwkJyL.d.cts} +1 -1
  36. package/dist/site.cjs +71 -0
  37. package/dist/site.d.cts +15 -1
  38. package/dist/site.d.ts +15 -1
  39. package/dist/site.esm.js +12 -311
  40. package/dist/studio.cjs +31 -31
  41. package/dist/studio.d.cts +2 -2
  42. package/dist/studio.d.ts +2 -2
  43. package/dist/studio.esm.js +7 -7
  44. package/dist/{timbal-v2-button-B7vPs7gg.d.ts → timbal-v2-button-DCAZNyUx.d.cts} +1 -1
  45. package/dist/{timbal-v2-button-B7vPs7gg.d.cts → timbal-v2-button-DCAZNyUx.d.ts} +1 -1
  46. package/dist/ui.cjs +77 -77
  47. package/dist/ui.d.cts +3 -3
  48. package/dist/ui.d.ts +3 -3
  49. package/dist/ui.esm.js +15 -15
  50. package/dist/{welcome-NXZlcihe.d.cts → welcome-B00oH5Io.d.cts} +1 -1
  51. package/dist/{welcome-DduQAC4K.d.ts → welcome-DU-4NTjZ.d.ts} +1 -1
  52. package/package.json +1 -1
@@ -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";
@@ -321,7 +325,7 @@ Presentational groups \u2014 import from the package root, not from these paths:
321
325
 
322
326
  | Folder | Components |
323
327
  |--------|------------|
324
- | \`data/\` | \`MetricRow\`, \`MetricChartCard\`, \`MetricTile\`, \`DataTable\`, \`FilterBar\`, \`FilterField\`, \`ChartPanel\` |
328
+ | \`data/\` | \`MetricRow\`, \`MetricChartCard\`, \`MetricTile\`, \`DataTable\`, \`FilterBar\`, \`FilterField\`, \`FilterDropdown\`, \`ChartPanel\` |
325
329
  | \`integrations/\` | \`IntegrationCard\`, \`ConnectionRow\`, \`ConnectionRowList\`, \`IntegrationsEmptyState\`, \`PlanBadge\` |
326
330
  | \`settings/\` | \`SettingsSection\`, \`FieldRow\`, \`DangerZone\`, \`FloatingUnsavedChangesBar\` |
327
331
  | \`surfaces/\` | \`StatTile\`, \`InfoCard\`, \`AlertCard\`, \`CatalogCard\`, \`ResourceCard\`, \`DescriptionList\`, \`ExpandableSection\`, \`StatusDot\`, \`StatusBadge\`, \`EmptyState\` |
@@ -390,6 +394,7 @@ The cause of slop is dropping **below** the curated block layer into raw primiti
390
394
  | \`StatusBadge\` | Status pill: \`tone\` (\`default\`\\|\`primary\`\\|\`success\`\\|\`warn\`\\|\`danger\`\\|\`muted\`), children. Use \`danger\` for critical/error severity. |
391
395
  | \`FilterBar\` | Horizontal filter row \u2014 bottom-aligns controls. Mix \`SearchInput\` with labeled \`FilterField\` + \`Select\` (or \`Field\` + \`Select\`); labels sit above, control baselines match. |
392
396
  | \`FilterField\` | Optional label wrapper for a filter control inside \`FilterBar\` (severity, status, \u2026). Omit \`label\` for search-only fields. |
397
+ | \`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
398
  | \`SearchInput\` | Filter field with consistent app styling. |
394
399
  | \`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
400
  | \`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\`. |
@@ -448,6 +453,21 @@ Charts run on **recharts** with shadcn \`ChartContainer\` / \`ChartTooltipConten
448
453
  | \`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
454
  | \`Timeline\` | Vertical event log: \`items: [{ id, title, description?, meta?, tone?, icon? }]\`. Presentational \u2014 pass already-formatted timestamps in \`meta\`. |
450
455
 
456
+ #### More \`/ui\` primitives (import from \`/ui\` or the package root)
457
+
458
+ 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.
459
+
460
+ | Component | Use for |
461
+ |-----------|---------|
462
+ | \`Stepper\` | Ordered step indicator for wizards / onboarding (horizontal or vertical; complete / active / upcoming states). |
463
+ | \`Rating\` | Star rating \u2014 interactive (keyboard + hover preview) or \`readOnly\`; controlled or uncontrolled. |
464
+ | \`NumberField\` | Numeric input with \u2212/+ steppers on the control surface; clamps to \`min\`/\`max\`, steps by \`step\`. |
465
+ | \`TagInput\` | Chips / token input; commits on Enter/comma, removes on Backspace, optional \`dedupe\`/\`max\`. |
466
+ | \`AvatarGroup\` | Overlapping avatar stack with an optional \`+N\` overflow chip (\`max\`, \`spacing\`). |
467
+ | \`CircularProgress\` | Lightweight SVG progress ring \u2014 determinate (optional center label) or indeterminate. |
468
+ | \`CopyButton\` | Click-to-copy with a transient check confirmation; icon-only or with a label. |
469
+ | \`Snippet\` | Single-line code / command on the elevated surface with a built-in copy button. |
470
+
451
471
  Studio chrome (\`StudioSidebar\`, \`ModeToggle\`, \u2026) lives in \`@timbal-ai/timbal-react/studio\` \u2014 optional, not required for every dashboard.
452
472
 
453
473
  ### Block recipes \u2014 compose these (don't clone wholesale)
@@ -514,6 +534,7 @@ import {
514
534
  DataTable,
515
535
  FilterBar,
516
536
  FilterField,
537
+ FilterDropdown,
517
538
  AlertCard,
518
539
  CatalogCard,
519
540
  } from "@timbal-ai/timbal-react/app";
@@ -1648,7 +1669,11 @@ function useAppDensityClass(key, override) {
1648
1669
  }
1649
1670
 
1650
1671
  // src/charts/sparkline.tsx
1651
- import { useId } from "react";
1672
+ import {
1673
+ useId,
1674
+ useRef,
1675
+ useState
1676
+ } from "react";
1652
1677
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1653
1678
  var Sparkline = ({
1654
1679
  data,
@@ -1659,30 +1684,36 @@ var Sparkline = ({
1659
1684
  height = 28,
1660
1685
  strokeWidth = 1.5,
1661
1686
  className,
1662
- ariaLabel = "Trend"
1687
+ ariaLabel = "Trend",
1688
+ interactive = false,
1689
+ labels,
1690
+ formatValue
1663
1691
  }) => {
1664
1692
  const uid = useId();
1693
+ const containerRef = useRef(null);
1694
+ const [activeIndex, setActiveIndex] = useState(null);
1665
1695
  const values = data.map((d) => typeof d === "number" ? d : toNum(d[dataKey]));
1666
1696
  if (values.length === 0) {
1667
1697
  return /* @__PURE__ */ jsx3("span", { className: cn("inline-block", className), style: { width, height } });
1668
1698
  }
1669
- const pad = strokeWidth + 1;
1699
+ const padX = 0;
1700
+ const padY = strokeWidth + 1;
1670
1701
  const min = Math.min(...values);
1671
1702
  const max = Math.max(...values);
1672
1703
  const range = max - min || 1;
1673
- const innerW = width - pad * 2;
1674
- const innerH = height - pad * 2;
1704
+ const innerW = width - padX * 2;
1705
+ const innerH = height - padY * 2;
1675
1706
  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
1707
+ x: padX + (values.length > 1 ? i / (values.length - 1) * innerW : innerW / 2),
1708
+ y: padY + innerH - (v - min) / range * innerH
1678
1709
  }));
1679
- return /* @__PURE__ */ jsxs2(
1710
+ const svg = /* @__PURE__ */ jsxs2(
1680
1711
  "svg",
1681
1712
  {
1682
1713
  width,
1683
1714
  height,
1684
1715
  viewBox: `0 0 ${width} ${height}`,
1685
- className: cn("block", className),
1716
+ className: cn("block", interactive ? "h-full w-full" : className),
1686
1717
  role: "img",
1687
1718
  "aria-label": ariaLabel,
1688
1719
  preserveAspectRatio: "none",
@@ -1692,7 +1723,7 @@ var Sparkline = ({
1692
1723
  /* @__PURE__ */ jsx3("stop", { offset: "0%", style: { stopColor: color, stopOpacity: 0.25 } }),
1693
1724
  /* @__PURE__ */ jsx3("stop", { offset: "100%", style: { stopColor: color, stopOpacity: 0 } })
1694
1725
  ] }) }),
1695
- /* @__PURE__ */ jsx3("path", { d: monotoneAreaPath(points, height - pad), fill: `url(#${uid}-spark)` })
1726
+ /* @__PURE__ */ jsx3("path", { d: monotoneAreaPath(points, height - padY), fill: `url(#${uid}-spark)` })
1696
1727
  ] }),
1697
1728
  /* @__PURE__ */ jsx3(
1698
1729
  "path",
@@ -1704,7 +1735,81 @@ var Sparkline = ({
1704
1735
  strokeLinecap: "round",
1705
1736
  strokeLinejoin: "round"
1706
1737
  }
1707
- )
1738
+ ),
1739
+ interactive && activeIndex != null && points[activeIndex] ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1740
+ /* @__PURE__ */ jsx3(
1741
+ "line",
1742
+ {
1743
+ x1: points[activeIndex].x,
1744
+ x2: points[activeIndex].x,
1745
+ y1: 0,
1746
+ y2: height,
1747
+ stroke: color,
1748
+ strokeWidth: 1,
1749
+ strokeOpacity: 0.3,
1750
+ vectorEffect: "non-scaling-stroke"
1751
+ }
1752
+ ),
1753
+ /* @__PURE__ */ jsx3(
1754
+ "circle",
1755
+ {
1756
+ cx: points[activeIndex].x,
1757
+ cy: points[activeIndex].y,
1758
+ r: 2.75,
1759
+ fill: color,
1760
+ stroke: "var(--background, #fff)",
1761
+ strokeWidth: 1.5,
1762
+ vectorEffect: "non-scaling-stroke"
1763
+ }
1764
+ )
1765
+ ] }) : null
1766
+ ]
1767
+ }
1768
+ );
1769
+ if (!interactive) return svg;
1770
+ const onMove = (e) => {
1771
+ const rect = e.currentTarget.getBoundingClientRect();
1772
+ if (rect.width === 0) return;
1773
+ const fraction = (e.clientX - rect.left) / rect.width;
1774
+ const index = Math.max(
1775
+ 0,
1776
+ Math.min(values.length - 1, Math.round(fraction * (values.length - 1)))
1777
+ );
1778
+ setActiveIndex(index);
1779
+ };
1780
+ const active = activeIndex != null ? points[activeIndex] : null;
1781
+ const formattedValue = activeIndex != null ? formatValue ? formatValue(values[activeIndex], activeIndex) : formatCompact(values[activeIndex]) : null;
1782
+ return /* @__PURE__ */ jsxs2(
1783
+ "span",
1784
+ {
1785
+ ref: containerRef,
1786
+ className: cn("relative block touch-none", className),
1787
+ style: { width: "100%", height: "100%" },
1788
+ onPointerMove: onMove,
1789
+ onPointerLeave: () => setActiveIndex(null),
1790
+ children: [
1791
+ svg,
1792
+ active ? /* @__PURE__ */ jsxs2(
1793
+ "span",
1794
+ {
1795
+ "aria-hidden": true,
1796
+ className: cn(
1797
+ "pointer-events-none absolute z-30 -translate-x-1/2 -translate-y-full whitespace-nowrap",
1798
+ "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)]",
1799
+ "border-white/10 bg-gradient-to-b from-neutral-800 to-neutral-950 text-white",
1800
+ "dark:border-black/10 dark:from-white dark:to-neutral-100 dark:text-neutral-900"
1801
+ ),
1802
+ style: {
1803
+ left: `${active.x / width * 100}%`,
1804
+ top: `${active.y / height * 100}%`,
1805
+ marginTop: -8
1806
+ },
1807
+ children: [
1808
+ labels?.[activeIndex] != null ? /* @__PURE__ */ jsx3("span", { className: "mr-1.5 text-neutral-300 dark:text-neutral-500", children: labels[activeIndex] }) : null,
1809
+ /* @__PURE__ */ jsx3("span", { children: formattedValue })
1810
+ ]
1811
+ }
1812
+ ) : null
1708
1813
  ]
1709
1814
  }
1710
1815
  );
@@ -1770,6 +1875,15 @@ var inlineTrendToneClass = {
1770
1875
  down: "text-rose-500/90 dark:text-rose-400/95 font-medium",
1771
1876
  neutral: "text-muted-foreground/80"
1772
1877
  };
1878
+ var sparklineToneColor = {
1879
+ up: "var(--primary, #3b82f6)",
1880
+ down: "var(--destructive, #f43f5e)",
1881
+ neutral: "var(--muted-foreground, #64748b)"
1882
+ };
1883
+ var sparklineBandBleed = {
1884
+ default: "-mx-4 -mb-3 h-10",
1885
+ compact: "-mx-3 -mb-2 h-8"
1886
+ };
1773
1887
  var activeToneClass = {
1774
1888
  default: "bg-foreground dark:bg-white",
1775
1889
  primary: "bg-primary",
@@ -1799,8 +1913,10 @@ var MetricTile = ({
1799
1913
  ariaLabel,
1800
1914
  className
1801
1915
  }) => {
1916
+ const density = useAppDensity();
1802
1917
  const metricTileBaseClass = useAppDensityClass("metricTile");
1803
1918
  const hasSparkline = Boolean(sparkline || sparklineData);
1919
+ const bandBleed = sparklineBandBleed[density === "compact" ? "compact" : "default"];
1804
1920
  const content = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1805
1921
  active ? /* @__PURE__ */ jsx5(
1806
1922
  "span",
@@ -1812,17 +1928,6 @@ var MetricTile = ({
1812
1928
  )
1813
1929
  }
1814
1930
  ) : 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
1931
  /* @__PURE__ */ jsxs4("div", { className: "relative z-10 flex flex-col gap-1 w-full text-left", children: [
1827
1932
  /* @__PURE__ */ jsx5("span", { className: "text-xs font-semibold text-muted-foreground/80 tracking-tight", children: label }),
1828
1933
  /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-2", children: [
@@ -1841,7 +1946,28 @@ var MetricTile = ({
1841
1946
  }
1842
1947
  ) : null
1843
1948
  ] })
1844
- ] })
1949
+ ] }),
1950
+ hasSparkline ? /* @__PURE__ */ jsx5(
1951
+ "div",
1952
+ {
1953
+ className: cn(
1954
+ "relative z-10 mt-2",
1955
+ bandBleed
1956
+ ),
1957
+ children: sparkline ?? /* @__PURE__ */ jsx5(
1958
+ Sparkline,
1959
+ {
1960
+ data: sparklineData,
1961
+ width: 160,
1962
+ height: 40,
1963
+ interactive: true,
1964
+ className: "h-full w-full opacity-90",
1965
+ color: sparklineToneColor[trendTone],
1966
+ ...sparklineConfig
1967
+ }
1968
+ )
1969
+ }
1970
+ ) : null
1845
1971
  ] });
1846
1972
  const divider = showDivider ? metricCellDividerClass : void 0;
1847
1973
  if (onSelect) {
@@ -1893,7 +2019,7 @@ function useAppShellNav() {
1893
2019
 
1894
2020
  // src/app/layout/AppShell.tsx
1895
2021
  import { motion, useReducedMotion } from "motion/react";
1896
- import { useCallback, useEffect, useMemo, useState } from "react";
2022
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
1897
2023
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1898
2024
  var floatingTriggerClass = cn(
1899
2025
  "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 +2043,7 @@ var AppShellBody = ({
1917
2043
  insetExpanded,
1918
2044
  children
1919
2045
  }) => {
1920
- const [isMobile, setIsMobile] = useState(() => {
2046
+ const [isMobile, setIsMobile] = useState2(() => {
1921
2047
  if (typeof window === "undefined") return false;
1922
2048
  return window.innerWidth < 768;
1923
2049
  });
@@ -1997,7 +2123,7 @@ var AppShell = ({
1997
2123
  }) => {
1998
2124
  const topbarContent = topbar ?? header;
1999
2125
  const hasChat = Boolean(chat);
2000
- const [uncontrolledNavOpen, setUncontrolledNavOpen] = useState(defaultNavOpen);
2126
+ const [uncontrolledNavOpen, setUncontrolledNavOpen] = useState2(defaultNavOpen);
2001
2127
  const isNavControlled = navOpenProp !== void 0;
2002
2128
  const navOpen = isNavControlled ? navOpenProp : uncontrolledNavOpen;
2003
2129
  const setNavOpen = useCallback(
@@ -2012,7 +2138,7 @@ var AppShell = ({
2012
2138
  () => ({ open: navOpen, setOpen: setNavOpen, toggle: toggleNav }),
2013
2139
  [navOpen, setNavOpen, toggleNav]
2014
2140
  );
2015
- const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultChatOpen);
2141
+ const [uncontrolledOpen, setUncontrolledOpen] = useState2(defaultChatOpen);
2016
2142
  const isChatControlled = chatOpenProp !== void 0;
2017
2143
  const chatOpen = isChatControlled ? chatOpenProp : uncontrolledOpen;
2018
2144
  const setChatOpen = useCallback(
@@ -2027,7 +2153,7 @@ var AppShell = ({
2027
2153
  const toggleChat = useCallback(() => {
2028
2154
  setChatOpen(!chatOpen);
2029
2155
  }, [chatOpen, setChatOpen]);
2030
- const [insetPaddingPx, setInsetPaddingPx] = useState(
2156
+ const [insetPaddingPx, setInsetPaddingPx] = useState2(
2031
2157
  sidebar ? SIDEBAR_INSET_PX_EXPANDED : 0
2032
2158
  );
2033
2159
  const reportShellInset = useCallback((insetPx) => {
@@ -2540,7 +2666,7 @@ var StatusBadge = ({
2540
2666
  "span",
2541
2667
  {
2542
2668
  className: cn(
2543
- "aui-app-status-badge inline-flex items-center gap-1.5 rounded-full px-2 py-0.5",
2669
+ "aui-app-status-badge inline-flex w-fit shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5",
2544
2670
  "text-xs font-medium leading-none ring-1 ring-inset",
2545
2671
  statusBadgeToneClass[tone],
2546
2672
  className
@@ -2755,7 +2881,7 @@ var DescriptionList = ({
2755
2881
  );
2756
2882
 
2757
2883
  // src/app/surfaces/ExpandableSection.tsx
2758
- import { useId as useId2, useState as useState2 } from "react";
2884
+ import { useId as useId2, useState as useState3 } from "react";
2759
2885
  import { AnimatePresence, motion as motion2, useReducedMotion as useReducedMotion2 } from "motion/react";
2760
2886
  import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
2761
2887
  var Chevron = ({ open }) => /* @__PURE__ */ jsx24(
@@ -2787,7 +2913,7 @@ var ExpandableSection = ({
2787
2913
  }) => {
2788
2914
  const reduceMotion = useReducedMotion2();
2789
2915
  const panelId = useId2();
2790
- const [internalOpen, setInternalOpen] = useState2(defaultOpen);
2916
+ const [internalOpen, setInternalOpen] = useState3(defaultOpen);
2791
2917
  const open = openProp ?? internalOpen;
2792
2918
  const toggle = () => {
2793
2919
  if (openProp == null) setInternalOpen((o) => !o);
@@ -3147,7 +3273,7 @@ var FieldRow = ({
3147
3273
  };
3148
3274
 
3149
3275
  // src/app/settings/FloatingUnsavedChangesBar.tsx
3150
- import { useEffect as useEffect2, useState as useState3 } from "react";
3276
+ import { useEffect as useEffect2, useState as useState4 } from "react";
3151
3277
  import { createPortal } from "react-dom";
3152
3278
  import { AnimatePresence as AnimatePresence2, motion as motion3, useReducedMotion as useReducedMotion3 } from "motion/react";
3153
3279
  import { jsx as jsx30, jsxs as jsxs24 } from "react/jsx-runtime";
@@ -3164,7 +3290,7 @@ var FloatingUnsavedChangesBar = ({
3164
3290
  className
3165
3291
  }) => {
3166
3292
  const reduceMotion = useReducedMotion3();
3167
- const [mounted, setMounted] = useState3(false);
3293
+ const [mounted, setMounted] = useState4(false);
3168
3294
  useEffect2(() => setMounted(true), []);
3169
3295
  if (!mounted || typeof document === "undefined") return null;
3170
3296
  return createPortal(
@@ -3770,505 +3896,467 @@ var FilterField = ({
3770
3896
  };
3771
3897
 
3772
3898
  // 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";
3899
+ import { useEffect as useEffect3, useMemo as useMemo2, useState as useState5 } from "react";
3900
+ import { ChevronRightIcon, ListFilterIcon, XIcon as XIcon2 } from "lucide-react";
3785
3901
  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" }
3902
+ var DEFAULT_PRESETS = [
3903
+ { id: "last_7_days", label: "Last 7 days" },
3904
+ { id: "last_30_days", label: "Last 30 days" },
3905
+ { id: "last_90_days", label: "Last 90 days" },
3906
+ { id: "this_month", label: "This month" },
3907
+ { id: "this_year", label: "This year" },
3908
+ { id: "custom", label: "Custom range" }
3790
3909
  ];
3791
- var OPERATORS = [
3792
- { id: "greater_than", label: "Greater than..." },
3793
- { id: "less_than", label: "Less than..." },
3794
- { id: "equals", label: "Equals..." }
3910
+ var DEFAULT_OPERATORS = [
3911
+ { id: "gt", label: "Greater than" },
3912
+ { id: "lt", label: "Less than" },
3913
+ { id: "eq", label: "Equals" }
3795
3914
  ];
3915
+ function asArray(v) {
3916
+ return Array.isArray(v) ? v : [];
3917
+ }
3918
+ function asText(v) {
3919
+ return typeof v === "string" ? v : "";
3920
+ }
3921
+ function asDate(v) {
3922
+ return v && !Array.isArray(v) && typeof v === "object" && "preset" in v ? v : { preset: null };
3923
+ }
3924
+ function asNumeric(v) {
3925
+ return v && !Array.isArray(v) && typeof v === "object" && "operator" in v ? v : { operator: "gt", value: "" };
3926
+ }
3927
+ var OPERATOR_SYMBOL = {
3928
+ gt: ">",
3929
+ lt: "<",
3930
+ eq: "="
3931
+ };
3796
3932
  function FilterDropdown({
3797
- filters,
3798
- onFiltersChange,
3799
- initialFilters,
3800
- contacts = DEFAULT_CONTACTS,
3933
+ fields,
3934
+ value,
3935
+ defaultValue,
3936
+ onChange,
3937
+ label = "Filter",
3938
+ align = "start",
3939
+ showActiveChips = true,
3801
3940
  className
3802
3941
  }) {
3803
- const [isOpen, setIsOpen] = useState4(false);
3804
- const [activeMenu, setActiveMenu] = useState4("contact");
3805
- const [isMobile, setIsMobile] = useState4(false);
3942
+ const [isOpen, setIsOpen] = useState5(false);
3943
+ const [activeId, setActiveId] = useState5(fields[0]?.id ?? null);
3944
+ const [isMobile, setIsMobile] = useState5(false);
3945
+ const isControlled = value !== void 0;
3946
+ const [internal, setInternal] = useState5(defaultValue ?? {});
3947
+ const values = isControlled ? value : internal;
3806
3948
  useEffect3(() => {
3807
3949
  const checkMobile = () => setIsMobile(window.innerWidth < 768);
3808
3950
  checkMobile();
3809
3951
  window.addEventListener("resize", checkMobile);
3810
3952
  return () => window.removeEventListener("resize", checkMobile);
3811
3953
  }, []);
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
3954
  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 ?? "");
3955
+ if (!fields.some((f) => f.id === activeId)) {
3956
+ setActiveId(fields[0]?.id ?? null);
3848
3957
  }
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);
3958
+ }, [fields, activeId]);
3959
+ const commit = (id, next) => {
3960
+ const merged = { ...values, [id]: next };
3961
+ if (!isControlled) setInternal(merged);
3962
+ onChange?.(merged);
3895
3963
  };
3896
- const handleWalletApply = () => {
3897
- setAppliedWallet(walletInput);
3898
- notifyChanges({ walletAddress: walletInput });
3899
- setIsOpen(false);
3964
+ const clearAll = () => {
3965
+ if (!isControlled) setInternal({});
3966
+ onChange?.({});
3900
3967
  };
3901
- const handleWalletClear = () => {
3902
- setWalletInput("");
3903
- setAppliedWallet("");
3904
- notifyChanges({ walletAddress: "" });
3905
- setIsOpen(false);
3906
- };
3907
- const handleDatePresetSelect = (presetId) => {
3908
- setSelectedDatePreset(presetId);
3909
- if (presetId !== "custom") {
3910
- notifyChanges({ lastInvoiceDate: presetId, customDateRange: void 0 });
3911
- setIsOpen(false);
3968
+ const activeIdx = fields.findIndex((f) => f.id === activeId);
3969
+ const activeField = activeIdx >= 0 ? fields[activeIdx] : void 0;
3970
+ const chips = [];
3971
+ for (const field of fields) {
3972
+ const v = values[field.id];
3973
+ if (field.type === "multiselect") {
3974
+ const selected = asArray(v);
3975
+ for (const optionValue of selected) {
3976
+ const opt = field.options?.find((o) => o.value === optionValue);
3977
+ chips.push({
3978
+ id: `${field.id}:${optionValue}`,
3979
+ label: `${field.label}: ${opt?.label ?? optionValue}`,
3980
+ remove: () => commit(field.id, selected.filter((x) => x !== optionValue))
3981
+ });
3982
+ }
3983
+ } else if (field.type === "text") {
3984
+ const text = asText(v);
3985
+ if (text) {
3986
+ chips.push({
3987
+ id: field.id,
3988
+ label: `${field.label}: ${text}`,
3989
+ remove: () => commit(field.id, "")
3990
+ });
3991
+ }
3992
+ } else if (field.type === "numeric") {
3993
+ const n = asNumeric(v);
3994
+ if (n.value) {
3995
+ chips.push({
3996
+ id: field.id,
3997
+ label: `${field.label} ${OPERATOR_SYMBOL[n.operator]} ${n.value}`,
3998
+ remove: () => commit(field.id, null)
3999
+ });
4000
+ }
4001
+ } else if (field.type === "daterange") {
4002
+ const d = asDate(v);
4003
+ if (d.preset) {
4004
+ 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;
4005
+ chips.push({
4006
+ id: field.id,
4007
+ label: `${field.label}: ${presetLabel}`,
4008
+ remove: () => commit(field.id, { preset: null })
4009
+ });
4010
+ }
3912
4011
  }
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,
4012
+ }
4013
+ return /* @__PURE__ */ jsxs36("div", { className: cn("flex flex-wrap items-center gap-2", className), children: [
4014
+ /* @__PURE__ */ jsxs36(Popover, { open: isOpen, onOpenChange: setIsOpen, children: [
4015
+ /* @__PURE__ */ jsx47(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx47(
4016
+ Button,
4017
+ {
4018
+ variant: "outline",
4019
+ size: "sm",
4020
+ className: "border-dashed font-medium text-muted-foreground hover:text-foreground",
4021
+ iconLeading: /* @__PURE__ */ jsx47(ListFilterIcon, { className: "size-4" }),
4022
+ children: label
4023
+ }
4024
+ ) }),
4025
+ /* @__PURE__ */ jsx47(
4026
+ PopoverContent,
4027
+ {
4028
+ variant: "list",
4029
+ align,
4030
+ className: "overflow-visible border-none bg-transparent p-0 shadow-none max-w-[calc(100vw-32px)] md:max-w-none",
4031
+ 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: [
4032
+ /* @__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) => {
4033
+ const isActive = activeId === field.id;
4034
+ return /* @__PURE__ */ jsxs36(
4035
+ "button",
4036
+ {
4037
+ type: "button",
4038
+ className: cn(
4039
+ "flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm text-left transition-colors outline-none",
4040
+ isActive ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4041
+ ),
4042
+ onMouseEnter: () => !isMobile && setActiveId(field.id),
4043
+ onClick: () => setActiveId(field.id),
4044
+ children: [
4045
+ /* @__PURE__ */ jsxs36("span", { className: "flex items-center gap-2", children: [
4046
+ field.icon,
4047
+ /* @__PURE__ */ jsx47("span", { children: field.label })
4048
+ ] }),
4049
+ /* @__PURE__ */ jsx47(ChevronRightIcon, { className: "size-4 text-muted-foreground/50" })
4050
+ ]
4051
+ },
4052
+ field.id
4053
+ );
4054
+ }) }),
4055
+ activeField && /* @__PURE__ */ jsx47(
4056
+ "div",
4057
+ {
4058
+ 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",
4059
+ style: isMobile ? {} : { top: `${activeIdx * 36 + 6}px` },
4060
+ children: /* @__PURE__ */ jsx47(
4061
+ FilterFieldControl,
4062
+ {
4063
+ field: activeField,
4064
+ value: values[activeField.id],
4065
+ onChange: (next) => commit(activeField.id, next),
4066
+ onClose: () => setIsOpen(false)
4067
+ }
4068
+ )
4069
+ }
4070
+ )
4071
+ ] })
4072
+ }
4073
+ )
4074
+ ] }),
4075
+ showActiveChips && chips.map((chip) => /* @__PURE__ */ jsx47(FilterChip, { label: chip.label, onRemove: chip.remove }, chip.id)),
4076
+ showActiveChips && chips.length > 0 && /* @__PURE__ */ jsx47(
4077
+ "button",
3966
4078
  {
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"
4079
+ type: "button",
4080
+ onClick: clearAll,
4081
+ className: "rounded-full px-3 py-1 text-sm font-medium text-muted-foreground outline-none transition-colors hover:text-foreground",
4082
+ children: "Clear all"
3973
4083
  }
3974
- ) }),
4084
+ )
4085
+ ] });
4086
+ }
4087
+ function FilterChip({ label, onRemove }) {
4088
+ 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: [
4089
+ /* @__PURE__ */ jsx47("span", { className: "truncate", children: label }),
3975
4090
  /* @__PURE__ */ jsx47(
3976
- PopoverContent,
4091
+ "button",
3977
4092
  {
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",
4093
+ type: "button",
4094
+ onClick: onRemove,
4095
+ "aria-label": `Remove ${label}`,
4096
+ className: "flex size-5 items-center justify-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground",
4097
+ children: /* @__PURE__ */ jsx47(XIcon2, { className: "size-3.5" })
4098
+ }
4099
+ )
4100
+ ] });
4101
+ }
4102
+ function FilterFieldControl({
4103
+ field,
4104
+ value,
4105
+ onChange,
4106
+ onClose
4107
+ }) {
4108
+ switch (field.type) {
4109
+ case "multiselect":
4110
+ return /* @__PURE__ */ jsx47(MultiSelectControl, { field, value: asArray(value), onChange });
4111
+ case "text":
4112
+ return /* @__PURE__ */ jsx47(TextControl, { field, value: asText(value), onChange, onClose });
4113
+ case "daterange":
4114
+ return /* @__PURE__ */ jsx47(DateRangeControl, { field, value: asDate(value), onChange, onClose });
4115
+ case "numeric":
4116
+ return /* @__PURE__ */ jsx47(NumericControl, { field, value: asNumeric(value), onChange, onClose });
4117
+ default:
4118
+ return null;
4119
+ }
4120
+ }
4121
+ function MultiSelectControl({
4122
+ field,
4123
+ value,
4124
+ onChange
4125
+ }) {
4126
+ const options = field.options ?? [];
4127
+ const [search, setSearch] = useState5("");
4128
+ const searchable = field.searchable ?? options.length > 8;
4129
+ const filtered = useMemo2(() => {
4130
+ if (!search) return options;
4131
+ const q = search.toLowerCase();
4132
+ return options.filter(
4133
+ (o) => o.label.toLowerCase().includes(q) || o.hint?.toLowerCase().includes(q)
4134
+ );
4135
+ }, [options, search]);
4136
+ const toggle = (optionValue) => {
4137
+ onChange(
4138
+ value.includes(optionValue) ? value.filter((v) => v !== optionValue) : [...value, optionValue]
4139
+ );
4140
+ };
4141
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4142
+ searchable && /* @__PURE__ */ jsx47(
4143
+ SearchInput,
4144
+ {
4145
+ placeholder: field.searchPlaceholder ?? "Search\u2026",
4146
+ value: search,
4147
+ onChange: (e) => setSearch(e.target.value),
4148
+ className: "w-full min-w-0"
4149
+ }
4150
+ ),
4151
+ /* @__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(
4152
+ "label",
4153
+ {
4154
+ className: "flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-muted/50",
4155
+ children: [
4156
+ /* @__PURE__ */ jsx47(
4157
+ Checkbox,
4007
4158
  {
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
- ]
4159
+ checked: value.includes(option.value),
4160
+ onCheckedChange: () => toggle(option.value)
4262
4161
  }
4263
- )
4264
- ] })
4162
+ ),
4163
+ option.icon,
4164
+ /* @__PURE__ */ jsxs36("span", { className: "flex min-w-0 flex-col", children: [
4165
+ /* @__PURE__ */ jsx47("span", { className: "truncate text-sm font-medium text-foreground", children: option.label }),
4166
+ option.hint && /* @__PURE__ */ jsx47("span", { className: "truncate text-xs text-muted-foreground", children: option.hint })
4167
+ ] })
4168
+ ]
4169
+ },
4170
+ option.value
4171
+ )) })
4172
+ ] });
4173
+ }
4174
+ function TextControl({
4175
+ field,
4176
+ value,
4177
+ onChange,
4178
+ onClose
4179
+ }) {
4180
+ const [draft, setDraft] = useState5(value);
4181
+ useEffect3(() => setDraft(value), [value]);
4182
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4183
+ /* @__PURE__ */ jsx47(
4184
+ "input",
4185
+ {
4186
+ type: "text",
4187
+ placeholder: field.placeholder ?? "Type a value\u2026",
4188
+ value: draft,
4189
+ onChange: (e) => setDraft(e.target.value),
4190
+ onKeyDown: (e) => {
4191
+ if (e.key === "Enter") {
4192
+ onChange(draft);
4193
+ onClose();
4194
+ }
4195
+ },
4196
+ className: controlClass({ size: "sm" }, "w-full")
4197
+ }
4198
+ ),
4199
+ /* @__PURE__ */ jsx47(
4200
+ ApplyClear,
4201
+ {
4202
+ onClear: () => {
4203
+ setDraft("");
4204
+ onChange("");
4205
+ onClose();
4206
+ },
4207
+ onApply: () => {
4208
+ onChange(draft);
4209
+ onClose();
4210
+ }
4265
4211
  }
4266
4212
  )
4267
- ] }) });
4213
+ ] });
4214
+ }
4215
+ function DateRangeControl({
4216
+ field,
4217
+ value,
4218
+ onChange,
4219
+ onClose
4220
+ }) {
4221
+ const presets = field.presets ?? DEFAULT_PRESETS;
4222
+ const [from, setFrom] = useState5(value.from ?? "");
4223
+ const [to, setTo] = useState5(value.to ?? "");
4224
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-1", children: [
4225
+ presets.map((preset) => {
4226
+ const isSelected = value.preset === preset.id;
4227
+ return /* @__PURE__ */ jsxs36(
4228
+ "button",
4229
+ {
4230
+ type: "button",
4231
+ onClick: () => {
4232
+ if (preset.id === "custom") {
4233
+ onChange({ preset: "custom", from, to });
4234
+ } else {
4235
+ onChange({ preset: preset.id });
4236
+ onClose();
4237
+ }
4238
+ },
4239
+ className: cn(
4240
+ "flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-left text-sm transition-colors outline-none",
4241
+ isSelected ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4242
+ ),
4243
+ children: [
4244
+ /* @__PURE__ */ jsx47("span", { children: preset.label }),
4245
+ preset.hint && /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground/70", children: preset.hint })
4246
+ ]
4247
+ },
4248
+ preset.id
4249
+ );
4250
+ }),
4251
+ value.preset === "custom" && /* @__PURE__ */ jsxs36("div", { className: "mt-2 flex flex-col gap-2 border-t border-border/40 pt-2", children: [
4252
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4253
+ /* @__PURE__ */ jsx47(
4254
+ "input",
4255
+ {
4256
+ type: "date",
4257
+ value: from,
4258
+ onChange: (e) => setFrom(e.target.value),
4259
+ className: controlClass({ size: "sm" }, "w-full text-xs")
4260
+ }
4261
+ ),
4262
+ /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground", children: "to" }),
4263
+ /* @__PURE__ */ jsx47(
4264
+ "input",
4265
+ {
4266
+ type: "date",
4267
+ value: to,
4268
+ onChange: (e) => setTo(e.target.value),
4269
+ className: controlClass({ size: "sm" }, "w-full text-xs")
4270
+ }
4271
+ )
4272
+ ] }),
4273
+ /* @__PURE__ */ jsx47("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx47(
4274
+ Button,
4275
+ {
4276
+ color: "primary",
4277
+ size: "sm",
4278
+ className: "h-8 px-3",
4279
+ onClick: () => {
4280
+ onChange({ preset: "custom", from, to });
4281
+ onClose();
4282
+ },
4283
+ children: "Apply"
4284
+ }
4285
+ ) })
4286
+ ] })
4287
+ ] });
4288
+ }
4289
+ function NumericControl({
4290
+ field,
4291
+ value,
4292
+ onChange,
4293
+ onClose
4294
+ }) {
4295
+ const operators = field.operators ?? DEFAULT_OPERATORS;
4296
+ const [operator, setOperator] = useState5(value.operator);
4297
+ const [draft, setDraft] = useState5(value.value);
4298
+ useEffect3(() => {
4299
+ setOperator(value.operator);
4300
+ setDraft(value.value);
4301
+ }, [value.operator, value.value]);
4302
+ return /* @__PURE__ */ jsxs36("div", { className: "flex flex-col gap-2.5", children: [
4303
+ /* @__PURE__ */ jsxs36("div", { className: "flex items-center gap-2", children: [
4304
+ /* @__PURE__ */ jsxs36(Select, { value: operator, onValueChange: (v) => setOperator(v), children: [
4305
+ /* @__PURE__ */ jsx47(SelectTrigger, { size: "sm", className: "shrink-0", children: /* @__PURE__ */ jsx47(SelectValue, {}) }),
4306
+ /* @__PURE__ */ jsx47(SelectContent, { children: operators.map((op) => /* @__PURE__ */ jsx47(SelectItem, { value: op.id, children: op.label }, op.id)) })
4307
+ ] }),
4308
+ /* @__PURE__ */ jsx47(
4309
+ "input",
4310
+ {
4311
+ type: "text",
4312
+ inputMode: "decimal",
4313
+ placeholder: field.placeholder ?? "0.00",
4314
+ value: draft,
4315
+ onChange: (e) => setDraft(e.target.value),
4316
+ onKeyDown: (e) => {
4317
+ if (e.key === "Enter") {
4318
+ onChange(draft ? { operator, value: draft } : null);
4319
+ onClose();
4320
+ }
4321
+ },
4322
+ className: controlClass({ size: "sm" }, "min-w-0 flex-1")
4323
+ }
4324
+ )
4325
+ ] }),
4326
+ /* @__PURE__ */ jsx47(
4327
+ ApplyClear,
4328
+ {
4329
+ onClear: () => {
4330
+ setDraft("");
4331
+ onChange(null);
4332
+ onClose();
4333
+ },
4334
+ onApply: () => {
4335
+ onChange(draft ? { operator, value: draft } : null);
4336
+ onClose();
4337
+ }
4338
+ }
4339
+ )
4340
+ ] });
4341
+ }
4342
+ function ApplyClear({ onClear, onApply }) {
4343
+ return /* @__PURE__ */ jsxs36("div", { className: "flex items-center justify-end gap-2 border-t border-border/40 pt-1", children: [
4344
+ /* @__PURE__ */ jsx47(
4345
+ Button,
4346
+ {
4347
+ variant: "ghost",
4348
+ size: "sm",
4349
+ onClick: onClear,
4350
+ className: "h-8 px-3 text-muted-foreground hover:text-foreground",
4351
+ children: "Clear"
4352
+ }
4353
+ ),
4354
+ /* @__PURE__ */ jsx47(Button, { color: "primary", size: "sm", onClick: onApply, className: "h-8 px-3", children: "Apply" })
4355
+ ] });
4268
4356
  }
4269
4357
 
4270
4358
  // src/app/data/DataTable.tsx
4271
- import { useEffect as useEffect4, useMemo as useMemo3, useState as useState5 } from "react";
4359
+ import { useEffect as useEffect4, useMemo as useMemo3, useState as useState6 } from "react";
4272
4360
  import {
4273
4361
  ArrowDownIcon,
4274
4362
  ArrowUpDownIcon,
@@ -4356,7 +4444,7 @@ function DataTable({
4356
4444
  defaultPageIndex = 0,
4357
4445
  onPageChange
4358
4446
  }) {
4359
- const [uncontrolledSort, setUncontrolledSort] = useState5(
4447
+ const [uncontrolledSort, setUncontrolledSort] = useState6(
4360
4448
  defaultSort
4361
4449
  );
4362
4450
  const isSortControlled = sortProp !== void 0;
@@ -4367,7 +4455,7 @@ function DataTable({
4367
4455
  }
4368
4456
  onSortChange?.(next);
4369
4457
  };
4370
- const [uncontrolledSelected, setUncontrolledSelected] = useState5(
4458
+ const [uncontrolledSelected, setUncontrolledSelected] = useState6(
4371
4459
  defaultSelectedKeys ?? []
4372
4460
  );
4373
4461
  const isSelectionControlled = selectedKeysProp !== void 0;
@@ -4379,7 +4467,7 @@ function DataTable({
4379
4467
  }
4380
4468
  onSelectionChange?.(next);
4381
4469
  };
4382
- const [uncontrolledPage, setUncontrolledPage] = useState5(defaultPageIndex);
4470
+ const [uncontrolledPage, setUncontrolledPage] = useState6(defaultPageIndex);
4383
4471
  const isPageControlled = pageIndexProp !== void 0;
4384
4472
  const rawPageIndex = isPageControlled ? pageIndexProp : uncontrolledPage;
4385
4473
  const setPage = (next) => {
@@ -4683,7 +4771,7 @@ var ChartPanel = ({
4683
4771
  };
4684
4772
 
4685
4773
  // src/app/data/MetricRow.tsx
4686
- import { useId as useId10, useState as useState6 } from "react";
4774
+ import { useId as useId10, useState as useState7 } from "react";
4687
4775
  import { jsx as jsx50, jsxs as jsxs39 } from "react/jsx-runtime";
4688
4776
  var MetricRow = ({
4689
4777
  title,
@@ -4701,7 +4789,7 @@ var MetricRow = ({
4701
4789
  const metricTileClass = useAppDensityClass("metricTile");
4702
4790
  const titleId = useId10();
4703
4791
  const selectable = onMetricChange != null || activeMetricId != null;
4704
- const [internalId, setInternalId] = useState6(
4792
+ const [internalId, setInternalId] = useState7(
4705
4793
  defaultActiveMetricId ?? metrics[0]?.id
4706
4794
  );
4707
4795
  const activeId = activeMetricId ?? internalId;
@@ -4775,7 +4863,7 @@ var MetricRow = ({
4775
4863
  };
4776
4864
 
4777
4865
  // src/app/data/MetricChartCard.tsx
4778
- import { useId as useId11, useState as useState7 } from "react";
4866
+ import { useId as useId11, useState as useState8 } from "react";
4779
4867
  import { jsx as jsx51, jsxs as jsxs40 } from "react/jsx-runtime";
4780
4868
  var MetricChartCard = ({
4781
4869
  title,
@@ -4801,7 +4889,7 @@ var MetricChartCard = ({
4801
4889
  const metricChartRegionClass = useAppDensityClass("metricChartRegion");
4802
4890
  const metricTileClass = useAppDensityClass("metricTile");
4803
4891
  const titleId = useId11();
4804
- const [internalId, setInternalId] = useState7(
4892
+ const [internalId, setInternalId] = useState8(
4805
4893
  defaultActiveMetricId ?? metrics[0]?.id
4806
4894
  );
4807
4895
  const activeId = activeMetricId ?? internalId;
@@ -4913,9 +5001,9 @@ var MetricChartCard = ({
4913
5001
  };
4914
5002
 
4915
5003
  // src/hooks/use-live-query.ts
4916
- import { useCallback as useCallback2, useEffect as useEffect5, useRef, useState as useState8 } from "react";
5004
+ import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef2, useState as useState9 } from "react";
4917
5005
  function useInterval(callback, delayMs) {
4918
- const saved = useRef(callback);
5006
+ const saved = useRef2(callback);
4919
5007
  useEffect5(() => {
4920
5008
  saved.current = callback;
4921
5009
  }, [callback]);
@@ -4932,18 +5020,18 @@ function useLiveQuery(fetcher, options = {}) {
4932
5020
  immediate = true,
4933
5021
  refetchOnFocus = true
4934
5022
  } = 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);
5023
+ const [data, setData] = useState9(void 0);
5024
+ const [error, setError] = useState9(void 0);
5025
+ const [loading, setLoading] = useState9(enabled);
5026
+ const [refreshing, setRefreshing] = useState9(false);
5027
+ const [lastUpdated, setLastUpdated] = useState9(null);
5028
+ const fetcherRef = useRef2(fetcher);
4941
5029
  useEffect5(() => {
4942
5030
  fetcherRef.current = fetcher;
4943
5031
  }, [fetcher]);
4944
- const mounted = useRef(true);
4945
- const requestId = useRef(0);
4946
- const hasData = useRef(false);
5032
+ const mounted = useRef2(true);
5033
+ const requestId = useRef2(0);
5034
+ const hasData = useRef2(false);
4947
5035
  useEffect5(() => {
4948
5036
  mounted.current = true;
4949
5037
  return () => {