@undevy-org/tablewright 0.1.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/es/components/dashboard/ReportSummaryCard.js +67 -0
  4. package/dist/es/components/data-table/AppliedStateChips.js +113 -0
  5. package/dist/es/components/data-table/ColumnFilterPopover.js +548 -0
  6. package/dist/es/components/data-table/ColumnHeaderMenu.js +144 -0
  7. package/dist/es/components/data-table/CompoundFilterPopover.js +508 -0
  8. package/dist/es/components/data-table/CopyButton.js +35 -0
  9. package/dist/es/components/data-table/CopyableText.js +26 -0
  10. package/dist/es/components/data-table/DataTableShell.js +145 -0
  11. package/dist/es/components/data-table/DrawerActionGroup.js +59 -0
  12. package/dist/es/components/data-table/DrawerEntityCard.js +21 -0
  13. package/dist/es/components/data-table/DrawerField.js +10 -0
  14. package/dist/es/components/data-table/DrawerFlagItem.js +20 -0
  15. package/dist/es/components/data-table/DrawerSection.js +26 -0
  16. package/dist/es/components/data-table/DrawerShell.js +141 -0
  17. package/dist/es/components/data-table/DrawerSummaryCard.js +11 -0
  18. package/dist/es/components/data-table/EmptyState.js +14 -0
  19. package/dist/es/components/data-table/ExpiryTimestampCell.js +43 -0
  20. package/dist/es/components/data-table/FilterChip.js +47 -0
  21. package/dist/es/components/data-table/FilterFormPanel.js +357 -0
  22. package/dist/es/components/data-table/FilterFormToggle.js +77 -0
  23. package/dist/es/components/data-table/FilterPopoverFooter.js +19 -0
  24. package/dist/es/components/data-table/FilterToolbar.js +131 -0
  25. package/dist/es/components/data-table/FlatColumnToggle.js +46 -0
  26. package/dist/es/components/data-table/GapIndicator.js +29 -0
  27. package/dist/es/components/data-table/GroupedColumnToggle.js +112 -0
  28. package/dist/es/components/data-table/RowActionsMenu.js +74 -0
  29. package/dist/es/components/data-table/RowControlCell.js +104 -0
  30. package/dist/es/components/data-table/SortableHeader.js +102 -0
  31. package/dist/es/components/data-table/TableFooter.js +132 -0
  32. package/dist/es/components/data-table/TableLayout.js +14 -0
  33. package/dist/es/components/data-table/TimestampCell.js +16 -0
  34. package/dist/es/components/data-table/TwoPanelMenu.js +64 -0
  35. package/dist/es/components/data-table/ViewChip.js +25 -0
  36. package/dist/es/components/data-table/cells/AmountCell.js +127 -0
  37. package/dist/es/components/data-table/cells/ConfirmedAmountCell.js +115 -0
  38. package/dist/es/components/data-table/cells/CurrencyAmountCell.js +26 -0
  39. package/dist/es/components/data-table/cells/DualLineCell.js +59 -0
  40. package/dist/es/components/data-table/cells/EnumBadgeCell.js +9 -0
  41. package/dist/es/components/data-table/cells/MonoIdCell.js +31 -0
  42. package/dist/es/components/data-table/hooks/use-column-resize.js +39 -0
  43. package/dist/es/components/data-table/hooks/use-row-drag.js +40 -0
  44. package/dist/es/components/data-table/hooks/use-table-pagination.js +14 -0
  45. package/dist/es/components/data-table/hooks/use-table-sort.js +35 -0
  46. package/dist/es/components/data-table/managed-filters.js +34 -0
  47. package/dist/es/components/data-table/sticky-utils.js +25 -0
  48. package/dist/es/components/layout/AppShell.js +113 -0
  49. package/dist/es/components/layout/Sidebar.js +128 -0
  50. package/dist/es/components/layout/SidebarAccountMenu.js +129 -0
  51. package/dist/es/components/layout/SidebarUser.js +38 -0
  52. package/dist/es/components/layout/TopBar.js +25 -0
  53. package/dist/es/components/layout/app-shell-context.js +9 -0
  54. package/dist/es/components/ui/badge.js +31 -0
  55. package/dist/es/components/ui/button-footer.js +25 -0
  56. package/dist/es/components/ui/button.js +44 -0
  57. package/dist/es/components/ui/checkbox.js +24 -0
  58. package/dist/es/components/ui/combobox.js +179 -0
  59. package/dist/es/components/ui/dialog.js +80 -0
  60. package/dist/es/components/ui/dropdown-menu.js +66 -0
  61. package/dist/es/components/ui/input.js +24 -0
  62. package/dist/es/components/ui/popover.js +33 -0
  63. package/dist/es/components/ui/segmented-control.js +60 -0
  64. package/dist/es/components/ui/select.js +66 -0
  65. package/dist/es/components/ui/tabs.js +41 -0
  66. package/dist/es/components/ui/theme-switch.js +47 -0
  67. package/dist/es/constants/drawer.js +4 -0
  68. package/dist/es/context/drawer-expand-context.js +10 -0
  69. package/dist/es/context/portal-container-context.js +10 -0
  70. package/dist/es/hooks/table-orchestration-store.js +165 -0
  71. package/dist/es/hooks/use-scroll-activity.js +33 -0
  72. package/dist/es/hooks/useRowDragOverride.js +18 -0
  73. package/dist/es/hooks/useTableOrchestration.js +452 -0
  74. package/dist/es/index.js +199 -0
  75. package/dist/es/lib/column-gaps.js +23 -0
  76. package/dist/es/lib/filter-stats.js +67 -0
  77. package/dist/es/lib/format-enum.js +6 -0
  78. package/dist/es/lib/format-timestamp.js +37 -0
  79. package/dist/es/lib/style-scope.js +4 -0
  80. package/dist/es/lib/utils.js +8 -0
  81. package/dist/fonts.css +5 -0
  82. package/dist/preflight.css +502 -0
  83. package/dist/styles.css +1731 -0
  84. package/dist/tablewright.cjs +5473 -0
  85. package/dist/tablewright.css +1579 -0
  86. package/dist/tokens.css +150 -0
  87. package/dist/types/components/dashboard/ReportSummaryCard.d.ts +15 -0
  88. package/dist/types/components/data-table/AppliedStateChips.d.ts +28 -0
  89. package/dist/types/components/data-table/ColumnFilterPopover.d.ts +14 -0
  90. package/dist/types/components/data-table/ColumnHeaderMenu.d.ts +27 -0
  91. package/dist/types/components/data-table/CompoundFilterPopover.d.ts +14 -0
  92. package/dist/types/components/data-table/CopyButton.d.ts +8 -0
  93. package/dist/types/components/data-table/CopyableText.d.ts +10 -0
  94. package/dist/types/components/data-table/DataTableShell.d.ts +30 -0
  95. package/dist/types/components/data-table/DrawerActionGroup.d.ts +8 -0
  96. package/dist/types/components/data-table/DrawerEntityCard.d.ts +13 -0
  97. package/dist/types/components/data-table/DrawerField.d.ts +7 -0
  98. package/dist/types/components/data-table/DrawerFlagItem.d.ts +10 -0
  99. package/dist/types/components/data-table/DrawerSection.d.ts +8 -0
  100. package/dist/types/components/data-table/DrawerShell.d.ts +33 -0
  101. package/dist/types/components/data-table/DrawerSummaryCard.d.ts +7 -0
  102. package/dist/types/components/data-table/EmptyState.d.ts +6 -0
  103. package/dist/types/components/data-table/ExpiryTimestampCell.d.ts +6 -0
  104. package/dist/types/components/data-table/FilterChip.d.ts +8 -0
  105. package/dist/types/components/data-table/FilterFormPanel.d.ts +34 -0
  106. package/dist/types/components/data-table/FilterFormToggle.d.ts +18 -0
  107. package/dist/types/components/data-table/FilterPopoverFooter.d.ts +9 -0
  108. package/dist/types/components/data-table/FilterToolbar.d.ts +29 -0
  109. package/dist/types/components/data-table/FlatColumnToggle.d.ts +10 -0
  110. package/dist/types/components/data-table/GapIndicator.d.ts +11 -0
  111. package/dist/types/components/data-table/GroupedColumnToggle.d.ts +12 -0
  112. package/dist/types/components/data-table/RowActionsMenu.d.ts +10 -0
  113. package/dist/types/components/data-table/RowControlCell.d.ts +16 -0
  114. package/dist/types/components/data-table/SortableHeader.d.ts +16 -0
  115. package/dist/types/components/data-table/TableFooter.d.ts +31 -0
  116. package/dist/types/components/data-table/TableLayout.d.ts +12 -0
  117. package/dist/types/components/data-table/TimestampCell.d.ts +3 -0
  118. package/dist/types/components/data-table/TwoPanelMenu.d.ts +22 -0
  119. package/dist/types/components/data-table/ViewChip.d.ts +6 -0
  120. package/dist/types/components/data-table/cells/AmountCell.d.ts +11 -0
  121. package/dist/types/components/data-table/cells/ConfirmedAmountCell.d.ts +10 -0
  122. package/dist/types/components/data-table/cells/CurrencyAmountCell.d.ts +8 -0
  123. package/dist/types/components/data-table/cells/DualLineCell.d.ts +44 -0
  124. package/dist/types/components/data-table/cells/EnumBadgeCell.d.ts +9 -0
  125. package/dist/types/components/data-table/cells/MonoIdCell.d.ts +19 -0
  126. package/dist/types/components/data-table/filter-types.d.ts +79 -0
  127. package/dist/types/components/data-table/hooks/use-column-resize.d.ts +7 -0
  128. package/dist/types/components/data-table/hooks/use-row-drag.d.ts +14 -0
  129. package/dist/types/components/data-table/hooks/use-table-pagination.d.ts +10 -0
  130. package/dist/types/components/data-table/hooks/use-table-sort.d.ts +6 -0
  131. package/dist/types/components/data-table/managed-filters.d.ts +18 -0
  132. package/dist/types/components/data-table/sticky-utils.d.ts +4 -0
  133. package/dist/types/components/data-table/types.d.ts +35 -0
  134. package/dist/types/components/layout/AppShell.d.ts +23 -0
  135. package/dist/types/components/layout/Sidebar.d.ts +16 -0
  136. package/dist/types/components/layout/SidebarAccountMenu.d.ts +20 -0
  137. package/dist/types/components/layout/SidebarUser.d.ts +8 -0
  138. package/dist/types/components/layout/TopBar.d.ts +7 -0
  139. package/dist/types/components/layout/app-shell-context.d.ts +8 -0
  140. package/dist/types/components/layout/index.d.ts +10 -0
  141. package/dist/types/components/layout/types.d.ts +11 -0
  142. package/dist/types/components/ui/badge.d.ts +9 -0
  143. package/dist/types/components/ui/button-footer.d.ts +7 -0
  144. package/dist/types/components/ui/button.d.ts +11 -0
  145. package/dist/types/components/ui/checkbox.d.ts +4 -0
  146. package/dist/types/components/ui/combobox.d.ts +92 -0
  147. package/dist/types/components/ui/dialog.d.ts +15 -0
  148. package/dist/types/components/ui/dropdown-menu.d.ts +10 -0
  149. package/dist/types/components/ui/input.d.ts +6 -0
  150. package/dist/types/components/ui/popover.d.ts +7 -0
  151. package/dist/types/components/ui/segmented-control.d.ts +17 -0
  152. package/dist/types/components/ui/select.d.ts +8 -0
  153. package/dist/types/components/ui/tabs.d.ts +10 -0
  154. package/dist/types/components/ui/theme-switch.d.ts +12 -0
  155. package/dist/types/constants/drawer.d.ts +1 -0
  156. package/dist/types/context/drawer-expand-context.d.ts +7 -0
  157. package/dist/types/context/portal-container-context.d.ts +14 -0
  158. package/dist/types/hooks/table-orchestration-store.d.ts +75 -0
  159. package/dist/types/hooks/use-scroll-activity.d.ts +1 -0
  160. package/dist/types/hooks/useDemoTheme.d.ts +8 -0
  161. package/dist/types/hooks/useRowDragOverride.d.ts +6 -0
  162. package/dist/types/hooks/useTableOrchestration.d.ts +115 -0
  163. package/dist/types/index.d.ts +107 -0
  164. package/dist/types/lib/column-gaps.d.ts +3 -0
  165. package/dist/types/lib/filter-stats.d.ts +2 -0
  166. package/dist/types/lib/format-enum.d.ts +1 -0
  167. package/dist/types/lib/format-timestamp.d.ts +4 -0
  168. package/dist/types/lib/style-scope.d.ts +19 -0
  169. package/dist/types/lib/utils.d.ts +2 -0
  170. package/package.json +114 -0
@@ -0,0 +1,144 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "../ui/dropdown-menu.js";
4
+ import { ChevronDown, ListFilter, EyeOff, ArrowUp, ArrowDown, Check } from "lucide-react";
5
+ const DEFAULT_LABELS = {
6
+ sort: "Sort",
7
+ sortByColumn: (label) => `Sort by ${label}`,
8
+ ascending: "Ascending",
9
+ descending: "Descending",
10
+ filterByColumn: "Filter by this column",
11
+ hideColumn: "Hide column"
12
+ };
13
+ const sectionLabelClass = "px-2 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--text-tertiary)] first:pt-1";
14
+ const itemClass = "gap-2.5 rounded-md px-2.5 py-2";
15
+ const iconClass = "h-3.5 w-3.5 shrink-0 text-[var(--text-secondary)]";
16
+ function SortItems({
17
+ label,
18
+ sorted,
19
+ onSort,
20
+ labels
21
+ }) {
22
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
23
+ /* @__PURE__ */ jsx("div", { className: sectionLabelClass, children: label ? labels.sortByColumn(label) : labels.sort }),
24
+ /* @__PURE__ */ jsxs(
25
+ DropdownMenuItem,
26
+ {
27
+ className: `${itemClass}${sorted === "asc" ? " bg-[var(--bg-tertiary)]" : ""}`,
28
+ onClick: () => onSort("asc"),
29
+ children: [
30
+ /* @__PURE__ */ jsx(ArrowUp, { className: iconClass }),
31
+ labels.ascending,
32
+ sorted === "asc" && /* @__PURE__ */ jsx(Check, { className: "ml-auto h-3 w-3 text-[var(--text-tertiary)]" })
33
+ ]
34
+ }
35
+ ),
36
+ /* @__PURE__ */ jsxs(
37
+ DropdownMenuItem,
38
+ {
39
+ className: `${itemClass}${sorted === "desc" ? " bg-[var(--bg-tertiary)]" : ""}`,
40
+ onClick: () => onSort("desc"),
41
+ children: [
42
+ /* @__PURE__ */ jsx(ArrowDown, { className: iconClass }),
43
+ labels.descending,
44
+ sorted === "desc" && /* @__PURE__ */ jsx(Check, { className: "ml-auto h-3 w-3 text-[var(--text-tertiary)]" })
45
+ ]
46
+ }
47
+ )
48
+ ] });
49
+ }
50
+ function ColumnHeaderMenu({
51
+ label,
52
+ primaryLabel,
53
+ secondaryLabel,
54
+ sorted,
55
+ onSort,
56
+ primarySorted,
57
+ onPrimarySort,
58
+ secondarySorted,
59
+ onSecondarySort,
60
+ hasFilter,
61
+ onFilterClick,
62
+ onHide,
63
+ labels: labelOverrides,
64
+ menuWidth = "w-[200px]"
65
+ }) {
66
+ const [open, setOpen] = useState(false);
67
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
68
+ const isStacked = !!primaryLabel;
69
+ const isSingleSortable = !!label && !!onSort;
70
+ const isSortable = isStacked || isSingleSortable;
71
+ const hasActions = hasFilter || !!onHide;
72
+ const labelContent = isStacked ? /* @__PURE__ */ jsxs("span", { className: "flex flex-col leading-none", children: [
73
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-primary)]", children: [
74
+ primaryLabel,
75
+ primarySorted === "asc" && /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 shrink-0" }),
76
+ primarySorted === "desc" && /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 shrink-0" })
77
+ ] }),
78
+ /* @__PURE__ */ jsxs("span", { className: "mt-1 inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-[0.06em] text-[var(--text-tertiary)]", children: [
79
+ secondaryLabel,
80
+ secondarySorted === "asc" && /* @__PURE__ */ jsx(ArrowUp, { className: "h-2.5 w-2.5 shrink-0" }),
81
+ secondarySorted === "desc" && /* @__PURE__ */ jsx(ArrowDown, { className: "h-2.5 w-2.5 shrink-0" })
82
+ ] })
83
+ ] }) : /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-primary)]", children: [
84
+ label,
85
+ sorted === "asc" && /* @__PURE__ */ jsx(ArrowUp, { className: "h-3 w-3 shrink-0" }),
86
+ sorted === "desc" && /* @__PURE__ */ jsx(ArrowDown, { className: "h-3 w-3 shrink-0" })
87
+ ] });
88
+ return /* @__PURE__ */ jsxs(DropdownMenu, { open, onOpenChange: setOpen, children: [
89
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": true, className: "pointer-events-none invisible flex items-center gap-1.5 py-1", children: [
90
+ labelContent,
91
+ /* @__PURE__ */ jsx("span", { className: "h-3 w-3 shrink-0" })
92
+ ] }),
93
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
94
+ "button",
95
+ {
96
+ type: "button",
97
+ className: `absolute inset-0 flex cursor-pointer select-none items-center gap-1.5 px-4 text-left transition-colors hover:bg-[var(--bg-hover)]${open ? " bg-[var(--bg-hover)]" : ""}`,
98
+ onClick: (e) => e.stopPropagation(),
99
+ children: [
100
+ labelContent,
101
+ /* @__PURE__ */ jsx(
102
+ ChevronDown,
103
+ {
104
+ className: `h-3 w-3 shrink-0 text-[var(--text-tertiary)] transition-transform duration-200${open ? " rotate-180" : ""}`
105
+ }
106
+ )
107
+ ]
108
+ }
109
+ ) }),
110
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: `${menuWidth} p-1.5`, children: [
111
+ isSingleSortable && /* @__PURE__ */ jsx(SortItems, { sorted: sorted ?? false, onSort, labels }),
112
+ isStacked && onPrimarySort && /* @__PURE__ */ jsx(
113
+ SortItems,
114
+ {
115
+ label: primaryLabel,
116
+ sorted: primarySorted ?? false,
117
+ onSort: onPrimarySort,
118
+ labels
119
+ }
120
+ ),
121
+ isStacked && onSecondarySort && /* @__PURE__ */ jsx(
122
+ SortItems,
123
+ {
124
+ label: secondaryLabel,
125
+ sorted: secondarySorted ?? false,
126
+ onSort: onSecondarySort,
127
+ labels
128
+ }
129
+ ),
130
+ isSortable && hasActions && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-[var(--border-subtle)]" }),
131
+ hasFilter && onFilterClick && /* @__PURE__ */ jsxs(DropdownMenuItem, { className: itemClass, onClick: onFilterClick, children: [
132
+ /* @__PURE__ */ jsx(ListFilter, { className: iconClass }),
133
+ labels.filterByColumn
134
+ ] }),
135
+ onHide && /* @__PURE__ */ jsxs(DropdownMenuItem, { className: itemClass, onClick: onHide, children: [
136
+ /* @__PURE__ */ jsx(EyeOff, { className: iconClass }),
137
+ labels.hideColumn
138
+ ] })
139
+ ] })
140
+ ] });
141
+ }
142
+ export {
143
+ ColumnHeaderMenu
144
+ };
@@ -0,0 +1,508 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { Input } from "../ui/input.js";
4
+ import { Checkbox } from "../ui/checkbox.js";
5
+ import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
6
+ import { FilterChip } from "./FilterChip.js";
7
+ import { FilterPopoverFooter } from "./FilterPopoverFooter.js";
8
+ const DEFAULT_LABELS = {
9
+ searchPlaceholder: "Search...",
10
+ addPlaceholder: "Add...",
11
+ contains: "Contains",
12
+ chipMin: "min",
13
+ chipMax: "max",
14
+ statsAllPrefix: "All:",
15
+ statsFilteredPrefix: "Filt:"
16
+ };
17
+ function isCompoundEmpty(value) {
18
+ if (typeof value !== "object" || Array.isArray(value) || value === null) return true;
19
+ const obj = value;
20
+ return Object.keys(obj).every((k) => isSubValueEmpty(obj[k]));
21
+ }
22
+ function isSubValueEmpty(value) {
23
+ if (value === void 0 || value === null) return true;
24
+ if (Array.isArray(value)) return value.length === 0;
25
+ if (typeof value === "string") return value.length === 0;
26
+ if (typeof value === "object") {
27
+ const obj = value;
28
+ return obj.from === void 0 && obj.to === void 0;
29
+ }
30
+ return true;
31
+ }
32
+ function initDraft(sub, compound) {
33
+ const val = compound[sub.key];
34
+ if (sub.type === "number-range") {
35
+ const range = typeof val === "object" && !Array.isArray(val) ? val : {};
36
+ return {
37
+ type: "number-range",
38
+ from: range.from !== void 0 ? String(range.from) : "",
39
+ to: range.to !== void 0 ? String(range.to) : ""
40
+ };
41
+ }
42
+ if (sub.type === "enum") {
43
+ return { type: "enum", selected: Array.isArray(val) ? val : [] };
44
+ }
45
+ return {
46
+ type: "text",
47
+ tags: Array.isArray(val) ? val : [],
48
+ input: ""
49
+ };
50
+ }
51
+ function draftToValue(draft) {
52
+ if (draft.type === "number-range") {
53
+ const from = draft.from.trim() !== "" ? Number(draft.from) : void 0;
54
+ const to = draft.to.trim() !== "" ? Number(draft.to) : void 0;
55
+ if (from === void 0 && to === void 0) return void 0;
56
+ return { from, to };
57
+ }
58
+ if (draft.type === "enum") {
59
+ return draft.selected.length > 0 ? draft.selected : void 0;
60
+ }
61
+ const trimmed = draft.input.trim();
62
+ const all = trimmed.length > 0 && !draft.tags.includes(trimmed) ? [...draft.tags, trimmed] : [...draft.tags];
63
+ return all.length > 0 ? all : void 0;
64
+ }
65
+ function formatSubDisplay(sub, val) {
66
+ if (isSubValueEmpty(val)) return null;
67
+ if (sub.type === "number-range" && typeof val === "object" && !Array.isArray(val)) {
68
+ const { from, to } = val;
69
+ const label = sub.label.replace(/\s+/g, " ").split(" ").pop() ?? sub.label;
70
+ if (from !== void 0 && to !== void 0) return `${label}: ${from}–${to}`;
71
+ if (from !== void 0) return `${label}: ≥${from}`;
72
+ if (to !== void 0) return `${label}: ≤${to}`;
73
+ }
74
+ if (sub.type === "enum" && Array.isArray(val) && val.length > 0) {
75
+ if (val.length === 1) {
76
+ const label = sub.options?.find((o) => o.value === val[0])?.label ?? val[0];
77
+ return `${sub.label}: ${label}`;
78
+ }
79
+ return `${sub.label}: ${val.length} selected`;
80
+ }
81
+ if (Array.isArray(val) && val.length > 0) {
82
+ if (val.length === 1) {
83
+ const display = val[0].length > 15 ? `${val[0].slice(0, 15)}…` : val[0];
84
+ return `${sub.label}: ${display}`;
85
+ }
86
+ return `${sub.label}: ${val.length} values`;
87
+ }
88
+ return null;
89
+ }
90
+ function CompoundFilterPopover({
91
+ config,
92
+ filter,
93
+ onChange,
94
+ onRemove,
95
+ initialOpen = false,
96
+ chipClassName,
97
+ columnFilterStats,
98
+ requestOpen,
99
+ onRequestOpenHandled
100
+ }) {
101
+ const subFilters = config.subFilters ?? [];
102
+ const compound = typeof filter.value === "object" && !Array.isArray(filter.value) ? filter.value : {};
103
+ const [isOpen, setIsOpen] = useState(false);
104
+ const onRequestOpenHandledRef = useRef(onRequestOpenHandled);
105
+ useEffect(() => {
106
+ onRequestOpenHandledRef.current = onRequestOpenHandled;
107
+ });
108
+ const [drafts, setDrafts] = useState(() => {
109
+ const d = {};
110
+ for (const sub of subFilters) d[sub.key] = initDraft(sub, compound);
111
+ return d;
112
+ });
113
+ useEffect(() => {
114
+ if (!initialOpen) return;
115
+ const id = setTimeout(() => setIsOpen(true), 0);
116
+ return () => clearTimeout(id);
117
+ }, []);
118
+ useEffect(() => {
119
+ if (!requestOpen) return;
120
+ const id = setTimeout(() => {
121
+ setIsOpen(true);
122
+ onRequestOpenHandledRef.current?.();
123
+ }, 0);
124
+ return () => clearTimeout(id);
125
+ }, [requestOpen]);
126
+ const handleOpenChange = (nextOpen) => {
127
+ if (!nextOpen) {
128
+ if (isCompoundEmpty(filter.value)) {
129
+ onRemove();
130
+ return;
131
+ }
132
+ const d = {};
133
+ for (const sub of subFilters) d[sub.key] = initDraft(sub, compound);
134
+ setDrafts(d);
135
+ }
136
+ setIsOpen(nextOpen);
137
+ };
138
+ const handleApply = () => {
139
+ const result = {};
140
+ for (const sub of subFilters) {
141
+ const val = draftToValue(drafts[sub.key]);
142
+ if (val !== void 0) result[sub.key] = val;
143
+ }
144
+ if (Object.keys(result).length === 0) {
145
+ onRemove();
146
+ } else {
147
+ onChange(result);
148
+ }
149
+ setIsOpen(false);
150
+ };
151
+ const handleClearAll = () => {
152
+ const d = {};
153
+ for (const sub of subFilters) {
154
+ d[sub.key] = sub.type === "number-range" ? { type: "number-range", from: "", to: "" } : sub.type === "enum" ? { type: "enum", selected: [] } : { type: "text", tags: [], input: "" };
155
+ }
156
+ setDrafts(d);
157
+ };
158
+ const handleResetSection = (key) => {
159
+ setDrafts((prev) => {
160
+ const sub = subFilters.find((s) => s.key === key);
161
+ if (!sub) return prev;
162
+ return {
163
+ ...prev,
164
+ [key]: sub.type === "number-range" ? { type: "number-range", from: "", to: "" } : sub.type === "enum" ? { type: "enum", selected: [] } : { type: "text", tags: [], input: "" }
165
+ };
166
+ });
167
+ };
168
+ const getDisplayValue = () => {
169
+ if (isCompoundEmpty(filter.value)) return void 0;
170
+ const parts = [];
171
+ for (const sub of subFilters) {
172
+ const display = formatSubDisplay(sub, compound[sub.key]);
173
+ if (display) parts.push(display);
174
+ }
175
+ if (parts.length === 0) return void 0;
176
+ const joined = parts.join(" & ");
177
+ return joined.length > 30 ? `${parts.length} filters` : joined;
178
+ };
179
+ const updateDraft = (key, updater) => {
180
+ setDrafts((prev) => ({ ...prev, [key]: updater(prev[key]) }));
181
+ };
182
+ const renderNumberRangeSection = (sub, draft) => {
183
+ const statsKey = `${filter.columnId}:${sub.key}`;
184
+ const stats = columnFilterStats?.[statsKey];
185
+ const allNum = stats?.all?.type === "number-range" ? stats.all : null;
186
+ const filtNum = stats?.filtered?.type === "number-range" ? stats.filtered : null;
187
+ const chipCls = (active) => active ? "rounded border border-[var(--tag-blue-text)]/20 bg-[var(--tag-blue-bg)] px-1.5 py-0.5 text-[11px] text-[var(--tag-blue-text)] cursor-pointer transition-colors" : "rounded border border-[var(--border-subtle)] bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-primary)] hover:bg-[var(--bg-hover)] cursor-pointer transition-colors";
188
+ const renderNumRow = (label, numStats) => {
189
+ if (!numStats) return null;
190
+ const { min, max } = numStats;
191
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
192
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-[var(--text-tertiary)] whitespace-nowrap min-w-[40px]", children: label }),
193
+ /* @__PURE__ */ jsxs(
194
+ "button",
195
+ {
196
+ type: "button",
197
+ className: chipCls(draft.from === String(min)),
198
+ onClick: () => updateDraft(sub.key, (d) => ({ ...d, from: String(min) })),
199
+ children: [
200
+ DEFAULT_LABELS.chipMin,
201
+ " ",
202
+ min.toLocaleString()
203
+ ]
204
+ }
205
+ ),
206
+ /* @__PURE__ */ jsxs(
207
+ "button",
208
+ {
209
+ type: "button",
210
+ className: chipCls(draft.to === String(max)),
211
+ onClick: () => updateDraft(sub.key, (d) => ({ ...d, to: String(max) })),
212
+ children: [
213
+ DEFAULT_LABELS.chipMax,
214
+ " ",
215
+ max.toLocaleString()
216
+ ]
217
+ }
218
+ )
219
+ ] });
220
+ };
221
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
222
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
223
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1", children: [
224
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block", children: DEFAULT_LABELS.chipMin }),
225
+ /* @__PURE__ */ jsx(
226
+ Input,
227
+ {
228
+ type: "number",
229
+ autoFocus: subFilters.indexOf(sub) === 0,
230
+ placeholder: allNum ? String(allNum.min) : "0",
231
+ value: draft.from,
232
+ onChange: (e) => updateDraft(sub.key, (prev) => ({
233
+ ...prev,
234
+ from: e.target.value
235
+ })),
236
+ onKeyDown: (e) => {
237
+ if (e.key === "Enter") {
238
+ e.preventDefault();
239
+ handleApply();
240
+ }
241
+ }
242
+ }
243
+ )
244
+ ] }),
245
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1", children: [
246
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block", children: DEFAULT_LABELS.chipMax }),
247
+ /* @__PURE__ */ jsx(
248
+ Input,
249
+ {
250
+ type: "number",
251
+ placeholder: allNum ? String(allNum.max) : "0",
252
+ value: draft.to,
253
+ onChange: (e) => updateDraft(sub.key, (prev) => ({
254
+ ...prev,
255
+ to: e.target.value
256
+ })),
257
+ onKeyDown: (e) => {
258
+ if (e.key === "Enter") {
259
+ e.preventDefault();
260
+ handleApply();
261
+ }
262
+ }
263
+ }
264
+ )
265
+ ] })
266
+ ] }),
267
+ allNum && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] mt-3 pt-2 space-y-2", children: [
268
+ renderNumRow(DEFAULT_LABELS.statsAllPrefix, allNum),
269
+ filtNum && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderNumRow(DEFAULT_LABELS.statsFilteredPrefix, filtNum) })
270
+ ] })
271
+ ] });
272
+ };
273
+ const renderTextSection = (sub, draft) => {
274
+ const statsKey = `${filter.columnId}:${sub.key}`;
275
+ const stats = columnFilterStats?.[statsKey];
276
+ const allText = stats?.all?.type === "text" ? stats.all : null;
277
+ const filtText = stats?.filtered?.type === "text" ? stats.filtered : null;
278
+ const inTagsCls = "rounded border border-[var(--tag-blue-text)]/20 bg-[var(--tag-blue-bg)] px-1.5 py-0.5 text-[11px] text-[var(--tag-blue-text)] opacity-60 cursor-pointer transition-colors";
279
+ const chipCls = (active) => active ? "rounded border border-[var(--tag-blue-text)]/20 bg-[var(--tag-blue-bg)] px-1.5 py-0.5 text-[11px] text-[var(--tag-blue-text)] cursor-pointer transition-colors" : "rounded border border-[var(--border-subtle)] bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[11px] text-[var(--text-primary)] hover:bg-[var(--bg-hover)] cursor-pointer transition-colors";
280
+ const toggleTag = (s) => {
281
+ updateDraft(sub.key, (d) => {
282
+ const td = d;
283
+ const tags = td.tags.includes(s) ? td.tags.filter((t) => t !== s) : [...td.tags, s];
284
+ return { ...td, tags };
285
+ });
286
+ };
287
+ const addTag = (raw) => {
288
+ const trimmed = raw.trim();
289
+ if (trimmed.length === 0 || draft.tags.includes(trimmed)) return false;
290
+ updateDraft(sub.key, (d) => {
291
+ const td = d;
292
+ return { ...td, tags: [...td.tags, trimmed], input: "" };
293
+ });
294
+ return true;
295
+ };
296
+ const applyWithPendingInput = () => {
297
+ const result = {};
298
+ for (const sf of subFilters) {
299
+ if (sf.key === sub.key) {
300
+ const trimmed = draft.input.trim();
301
+ const tags = trimmed.length > 0 && !draft.tags.includes(trimmed) ? [...draft.tags, trimmed] : [...draft.tags];
302
+ if (tags.length > 0) result[sf.key] = tags;
303
+ } else {
304
+ const val = draftToValue(drafts[sf.key]);
305
+ if (val !== void 0) result[sf.key] = val;
306
+ }
307
+ }
308
+ if (Object.keys(result).length === 0) {
309
+ onRemove();
310
+ } else {
311
+ onChange(result);
312
+ }
313
+ setIsOpen(false);
314
+ };
315
+ const handleTextKeyDown = (e) => {
316
+ const isModEnter = e.key === "Enter" && (e.metaKey || e.ctrlKey);
317
+ if (isModEnter) {
318
+ e.preventDefault();
319
+ applyWithPendingInput();
320
+ return;
321
+ }
322
+ if (e.key === "Enter") {
323
+ e.preventDefault();
324
+ if (draft.input.trim().length === 0) {
325
+ handleApply();
326
+ } else if (draft.tags.length === 0) {
327
+ addTag(draft.input);
328
+ applyWithPendingInput();
329
+ } else {
330
+ addTag(draft.input);
331
+ }
332
+ return;
333
+ }
334
+ if (e.key === "Backspace" && draft.input.length === 0 && draft.tags.length > 0) {
335
+ updateDraft(sub.key, (d) => {
336
+ const td = d;
337
+ return { ...td, tags: td.tags.slice(0, -1) };
338
+ });
339
+ }
340
+ };
341
+ const truncate = (s, max = 18) => s.length > max ? s.slice(0, max) + "…" : s;
342
+ const renderTextRow = (label, textStats) => {
343
+ if (!textStats) return null;
344
+ const { samples, totalUnique } = textStats;
345
+ const remaining = totalUnique - samples.length;
346
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
347
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-[var(--text-tertiary)] whitespace-nowrap min-w-[40px]", children: label }),
348
+ samples.map((s) => /* @__PURE__ */ jsx(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ className: draft.tags.includes(s) ? inTagsCls : chipCls(false),
353
+ onClick: () => toggleTag(s),
354
+ children: truncate(s)
355
+ },
356
+ s
357
+ )),
358
+ remaining > 0 && /* @__PURE__ */ jsxs("span", { className: "rounded bg-[var(--bg-secondary)] px-1.5 py-0.5 text-[9px] text-[var(--text-tertiary)]", children: [
359
+ "+",
360
+ remaining
361
+ ] })
362
+ ] });
363
+ };
364
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
365
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-semibold uppercase tracking-[0.1em] text-[var(--text-tertiary)] block mb-2", children: [
366
+ DEFAULT_LABELS.contains,
367
+ draft.tags.length >= 2 && /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] font-semibold text-[var(--tag-blue-text)] bg-[var(--tag-blue-bg)] rounded px-1 py-0.5 uppercase", children: "OR" })
368
+ ] }),
369
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--bg-surface)] px-2 py-1.5 focus-within:border-[var(--border-focus)]", children: [
370
+ draft.tags.map((tag) => /* @__PURE__ */ jsxs(
371
+ "span",
372
+ {
373
+ className: "inline-flex items-center gap-0.5 rounded bg-[var(--tag-blue-bg)] px-1.5 py-0.5 text-[11px] text-[var(--tag-blue-text)] max-w-[120px]",
374
+ children: [
375
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: tag }),
376
+ /* @__PURE__ */ jsx(
377
+ "button",
378
+ {
379
+ type: "button",
380
+ className: "ml-0.5 text-[var(--tag-blue-text)]/60 hover:text-[var(--tag-blue-text)] text-xs leading-none",
381
+ onClick: () => updateDraft(sub.key, (d) => {
382
+ const td = d;
383
+ return { ...td, tags: td.tags.filter((t) => t !== tag) };
384
+ }),
385
+ children: "✕"
386
+ }
387
+ )
388
+ ]
389
+ },
390
+ tag
391
+ )),
392
+ /* @__PURE__ */ jsx(
393
+ "input",
394
+ {
395
+ autoFocus: subFilters.indexOf(sub) === 0,
396
+ className: "flex-1 min-w-[50px] bg-transparent text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-tertiary)]",
397
+ placeholder: draft.tags.length === 0 ? DEFAULT_LABELS.searchPlaceholder : DEFAULT_LABELS.addPlaceholder,
398
+ value: draft.input,
399
+ onChange: (e) => updateDraft(sub.key, (prev) => ({ ...prev, input: e.target.value })),
400
+ onKeyDown: handleTextKeyDown
401
+ }
402
+ )
403
+ ] }),
404
+ allText && /* @__PURE__ */ jsxs("div", { className: "border-t border-[var(--border-subtle)] mt-3 pt-2 space-y-2", children: [
405
+ renderTextRow(DEFAULT_LABELS.statsAllPrefix, allText),
406
+ filtText && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border-subtle)] pt-1.5", children: renderTextRow(DEFAULT_LABELS.statsFilteredPrefix, filtText) })
407
+ ] })
408
+ ] });
409
+ };
410
+ const renderEnumSection = (sub, draft) => {
411
+ const statsKey = `${filter.columnId}:${sub.key}`;
412
+ const stats = columnFilterStats?.[statsKey];
413
+ const allEnum = stats?.all?.type === "enum" ? stats.all : null;
414
+ const filtEnum = stats?.filtered?.type === "enum" ? stats.filtered : null;
415
+ return /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: sub.options?.map((option) => {
416
+ const allCount = allEnum?.counts[option.value];
417
+ const filtCount = filtEnum?.counts[option.value];
418
+ return /* @__PURE__ */ jsxs(
419
+ "label",
420
+ {
421
+ className: "flex items-center gap-2 px-2 py-1.5 hover:bg-[var(--bg-secondary)] rounded-md cursor-pointer transition-colors",
422
+ children: [
423
+ /* @__PURE__ */ jsx(
424
+ Checkbox,
425
+ {
426
+ checked: draft.selected.includes(option.value),
427
+ onCheckedChange: (checked) => {
428
+ if (checked === "indeterminate") return;
429
+ updateDraft(sub.key, (d) => {
430
+ const ed = d;
431
+ return {
432
+ ...ed,
433
+ selected: checked ? [...ed.selected, option.value] : ed.selected.filter((v) => v !== option.value)
434
+ };
435
+ });
436
+ }
437
+ }
438
+ ),
439
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-[var(--text-primary)]", children: option.label }),
440
+ allCount !== void 0 && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-[var(--text-tertiary)] tabular-nums", children: filtCount !== void 0 ? `${filtCount} / ${allCount}` : allCount })
441
+ ]
442
+ },
443
+ option.value
444
+ );
445
+ }) });
446
+ };
447
+ const hasSectionValue = (key) => {
448
+ const draft = drafts[key];
449
+ if (!draft) return false;
450
+ if (draft.type === "number-range") return draft.from.trim() !== "" || draft.to.trim() !== "";
451
+ if (draft.type === "enum") return draft.selected.length > 0;
452
+ return draft.tags.length > 0 || draft.input.trim().length > 0;
453
+ };
454
+ return /* @__PURE__ */ jsxs(Popover, { open: isOpen, onOpenChange: handleOpenChange, children: [
455
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
456
+ FilterChip,
457
+ {
458
+ label: config.label,
459
+ value: getDisplayValue(),
460
+ onRemove,
461
+ active: !!getDisplayValue(),
462
+ className: chipClassName
463
+ }
464
+ ) }),
465
+ /* @__PURE__ */ jsxs(
466
+ PopoverContent,
467
+ {
468
+ align: "start",
469
+ collisionPadding: 8,
470
+ className: "w-auto p-0 [box-shadow:var(--shadow-menu)] border-[var(--border-subtle)] flex flex-col max-h-[min(var(--radix-popover-content-available-height),520px)] overflow-hidden",
471
+ children: [
472
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 min-h-0 overflow-hidden", children: subFilters.map((sub, idx) => /* @__PURE__ */ jsxs(
473
+ "div",
474
+ {
475
+ className: `p-3 w-60 flex flex-col min-h-0 overflow-y-auto overscroll-contain ${idx > 0 ? "border-l border-[var(--border-subtle)]" : ""}`,
476
+ children: [
477
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
478
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
479
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold text-[var(--text-primary)]", children: sub.label }),
480
+ sub.badge && /* @__PURE__ */ jsx("span", { className: "text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-tertiary)]", children: sub.badge })
481
+ ] }),
482
+ hasSectionValue(sub.key) && /* @__PURE__ */ jsx(
483
+ "button",
484
+ {
485
+ type: "button",
486
+ className: "text-[11px] text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors",
487
+ onClick: () => handleResetSection(sub.key),
488
+ title: `Reset ${sub.label}`,
489
+ children: "✕"
490
+ }
491
+ )
492
+ ] }),
493
+ drafts[sub.key]?.type === "number-range" && renderNumberRangeSection(sub, drafts[sub.key]),
494
+ drafts[sub.key]?.type === "text" && renderTextSection(sub, drafts[sub.key]),
495
+ drafts[sub.key]?.type === "enum" && renderEnumSection(sub, drafts[sub.key])
496
+ ]
497
+ },
498
+ sub.key
499
+ )) }),
500
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 bg-[var(--bg-surface)]", children: /* @__PURE__ */ jsx(FilterPopoverFooter, { onApply: handleApply, onClear: handleClearAll }) })
501
+ ]
502
+ }
503
+ )
504
+ ] });
505
+ }
506
+ export {
507
+ CompoundFilterPopover
508
+ };
@@ -0,0 +1,35 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Check, Copy } from "lucide-react";
4
+ import { cn } from "../../lib/utils.js";
5
+ function CopyButton({
6
+ text,
7
+ className,
8
+ iconClassName,
9
+ title = "Copy value"
10
+ }) {
11
+ const [copied, setCopied] = useState(false);
12
+ const handleCopy = (event) => {
13
+ event.stopPropagation();
14
+ void navigator.clipboard.writeText(text);
15
+ setCopied(true);
16
+ window.setTimeout(() => setCopied(false), 1500);
17
+ };
18
+ return /* @__PURE__ */ jsx(
19
+ "button",
20
+ {
21
+ type: "button",
22
+ className: cn(
23
+ "ml-1.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-[var(--text-tertiary)] opacity-0 transition-all hover:text-[var(--text-primary)] focus-visible:opacity-100 group-hover:opacity-100",
24
+ className
25
+ ),
26
+ onClick: handleCopy,
27
+ title,
28
+ "aria-label": copied ? "Copied" : title,
29
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: cn("h-3 w-3 text-[var(--tag-green-text)]", iconClassName) }) : /* @__PURE__ */ jsx(Copy, { className: cn("h-3 w-3", iconClassName) })
30
+ }
31
+ );
32
+ }
33
+ export {
34
+ CopyButton
35
+ };