@astra-kit/ui 0.0.0-stage → 0.0.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 (304) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +384 -2
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/assets/fonts/geist-LICENSE.txt +93 -0
  6. package/assets/fonts/geist-latin-variable.woff2 +0 -0
  7. package/assets/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  8. package/assets/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
  9. package/assets/fonts/ibmplexmono-LICENSE.txt +93 -0
  10. package/assets/fonts/inter-LICENSE.txt +93 -0
  11. package/assets/fonts/inter-latin-variable.woff2 +0 -0
  12. package/dist/adapters/unpic-image.d.ts +3 -0
  13. package/dist/adapters/unpic-image.js +98 -0
  14. package/dist/components/account-menu.d.ts +67 -0
  15. package/dist/components/account-menu.js +440 -0
  16. package/dist/components/action-button.d.ts +25 -0
  17. package/dist/components/action-button.js +264 -0
  18. package/dist/components/alert.d.ts +31 -0
  19. package/dist/components/alert.js +503 -0
  20. package/dist/components/animated-collapse.d.ts +19 -0
  21. package/dist/components/animated-collapse.js +170 -0
  22. package/dist/components/animated-count.d.ts +13 -0
  23. package/dist/components/animated-count.js +105 -0
  24. package/dist/components/animated-list-item.d.ts +25 -0
  25. package/dist/components/animated-list-item.js +134 -0
  26. package/dist/components/animated-theme-toggler.d.ts +12 -0
  27. package/dist/components/animated-theme-toggler.js +200 -0
  28. package/dist/components/avatar.d.ts +65 -0
  29. package/dist/components/avatar.js +463 -0
  30. package/dist/components/badge.d.ts +21 -0
  31. package/dist/components/badge.js +297 -0
  32. package/dist/components/button-interaction.d.ts +23 -0
  33. package/dist/components/button-interaction.js +22 -0
  34. package/dist/components/button-label.d.ts +8 -0
  35. package/dist/components/button-label.js +203 -0
  36. package/dist/components/button.d.ts +23 -0
  37. package/dist/components/button.js +145 -0
  38. package/dist/components/calendar.d.ts +47 -0
  39. package/dist/components/calendar.js +155 -0
  40. package/dist/components/card.d.ts +38 -0
  41. package/dist/components/card.js +904 -0
  42. package/dist/components/carousel.d.ts +71 -0
  43. package/dist/components/carousel.js +585 -0
  44. package/dist/components/checkbox.d.ts +15 -0
  45. package/dist/components/checkbox.js +324 -0
  46. package/dist/components/control-button.d.ts +16 -0
  47. package/dist/components/control-button.js +215 -0
  48. package/dist/components/data-table.d.ts +118 -0
  49. package/dist/components/data-table.js +895 -0
  50. package/dist/components/dialog-surface.d.ts +49 -0
  51. package/dist/components/dialog-surface.js +220 -0
  52. package/dist/components/dialog.d.ts +92 -0
  53. package/dist/components/dialog.js +320 -0
  54. package/dist/components/dropdown-menu.d.ts +57 -0
  55. package/dist/components/dropdown-menu.js +107 -0
  56. package/dist/components/field.d.ts +53 -0
  57. package/dist/components/field.js +710 -0
  58. package/dist/components/file-upload.d.ts +88 -0
  59. package/dist/components/file-upload.js +776 -0
  60. package/dist/components/filter-presets-bar.d.ts +73 -0
  61. package/dist/components/filter-presets-bar.js +510 -0
  62. package/dist/components/filter-query-bar.d.ts +21 -0
  63. package/dist/components/filter-query-bar.js +881 -0
  64. package/dist/components/filter-query-grammar.d.ts +188 -0
  65. package/dist/components/filter-query-grammar.js +875 -0
  66. package/dist/components/form.d.ts +29 -0
  67. package/dist/components/form.js +330 -0
  68. package/dist/components/highlighter.d.ts +15 -0
  69. package/dist/components/highlighter.js +152 -0
  70. package/dist/components/hover-card.d.ts +50 -0
  71. package/dist/components/hover-card.js +80 -0
  72. package/dist/components/input-group.d.ts +16 -0
  73. package/dist/components/input-group.js +277 -0
  74. package/dist/components/input.d.ts +17 -0
  75. package/dist/components/input.js +120 -0
  76. package/dist/components/label.d.ts +4 -0
  77. package/dist/components/label.js +43 -0
  78. package/dist/components/link.d.ts +9 -0
  79. package/dist/components/link.js +43 -0
  80. package/dist/components/morph-text.d.ts +14 -0
  81. package/dist/components/morph-text.js +210 -0
  82. package/dist/components/pagination.d.ts +59 -0
  83. package/dist/components/pagination.js +190 -0
  84. package/dist/components/popover.d.ts +17 -0
  85. package/dist/components/popover.js +40 -0
  86. package/dist/components/presence-swap.d.ts +22 -0
  87. package/dist/components/presence-swap.js +209 -0
  88. package/dist/components/primitives/alert-dialog.d.ts +68 -0
  89. package/dist/components/primitives/alert-dialog.js +629 -0
  90. package/dist/components/primitives/avatar.d.ts +46 -0
  91. package/dist/components/primitives/avatar.js +265 -0
  92. package/dist/components/primitives/calendar.d.ts +37 -0
  93. package/dist/components/primitives/calendar.js +1250 -0
  94. package/dist/components/primitives/carousel.d.ts +106 -0
  95. package/dist/components/primitives/carousel.js +928 -0
  96. package/dist/components/primitives/dialog.d.ts +62 -0
  97. package/dist/components/primitives/dialog.js +490 -0
  98. package/dist/components/primitives/dropdown-menu-items.d.ts +59 -0
  99. package/dist/components/primitives/dropdown-menu-items.js +265 -0
  100. package/dist/components/primitives/dropdown-menu.d.ts +64 -0
  101. package/dist/components/primitives/dropdown-menu.js +560 -0
  102. package/dist/components/primitives/hover-card.d.ts +50 -0
  103. package/dist/components/primitives/hover-card.js +507 -0
  104. package/dist/components/primitives/pagination.d.ts +65 -0
  105. package/dist/components/primitives/pagination.js +373 -0
  106. package/dist/components/primitives/popover.d.ts +22 -0
  107. package/dist/components/primitives/popover.js +213 -0
  108. package/dist/components/primitives/select.d.ts +37 -0
  109. package/dist/components/primitives/select.js +428 -0
  110. package/dist/components/primitives/tooltip.d.ts +30 -0
  111. package/dist/components/primitives/tooltip.js +277 -0
  112. package/dist/components/progress-ring.d.ts +20 -0
  113. package/dist/components/progress-ring.js +53 -0
  114. package/dist/components/radio-group.d.ts +34 -0
  115. package/dist/components/radio-group.js +392 -0
  116. package/dist/components/reaction-button.d.ts +30 -0
  117. package/dist/components/reaction-button.js +189 -0
  118. package/dist/components/rich-link-editor.d.ts +25 -0
  119. package/dist/components/rich-link-editor.js +320 -0
  120. package/dist/components/rich-markdown-content.d.ts +10 -0
  121. package/dist/components/rich-markdown-content.js +56 -0
  122. package/dist/components/rich-text-content.d.ts +19 -0
  123. package/dist/components/rich-text-content.js +26 -0
  124. package/dist/components/select.d.ts +55 -0
  125. package/dist/components/select.js +188 -0
  126. package/dist/components/separator.d.ts +3 -0
  127. package/dist/components/separator.js +48 -0
  128. package/dist/components/sheet.d.ts +19 -0
  129. package/dist/components/sheet.js +420 -0
  130. package/dist/components/sidebar-panel-context.d.ts +17 -0
  131. package/dist/components/sidebar-panel-context.js +14 -0
  132. package/dist/components/sidebar.d.ts +195 -0
  133. package/dist/components/sidebar.js +2041 -0
  134. package/dist/components/skeleton.d.ts +23 -0
  135. package/dist/components/skeleton.js +334 -0
  136. package/dist/components/split-button.d.ts +36 -0
  137. package/dist/components/split-button.js +306 -0
  138. package/dist/components/svgs/whatsappIcon.d.ts +3 -0
  139. package/dist/components/svgs/whatsappIcon.js +14 -0
  140. package/dist/components/switch.d.ts +13 -0
  141. package/dist/components/switch.js +444 -0
  142. package/dist/components/table.d.ts +10 -0
  143. package/dist/components/table.js +298 -0
  144. package/dist/components/tabs.d.ts +36 -0
  145. package/dist/components/tabs.js +636 -0
  146. package/dist/components/textarea.d.ts +16 -0
  147. package/dist/components/textarea.js +111 -0
  148. package/dist/components/themed-toaster.d.ts +7 -0
  149. package/dist/components/themed-toaster.js +86 -0
  150. package/dist/components/toaster.d.ts +12 -0
  151. package/dist/components/toaster.js +62 -0
  152. package/dist/components/tooltip.d.ts +43 -0
  153. package/dist/components/tooltip.js +65 -0
  154. package/dist/components/typing-animation.d.ts +34 -0
  155. package/dist/components/typing-animation.js +389 -0
  156. package/dist/constants/action-button.d.ts +20 -0
  157. package/dist/constants/action-button.js +17 -0
  158. package/dist/constants/alert.d.ts +16 -0
  159. package/dist/constants/alert.js +13 -0
  160. package/dist/constants/avatar.d.ts +38 -0
  161. package/dist/constants/avatar.js +32 -0
  162. package/dist/constants/badge.d.ts +6 -0
  163. package/dist/constants/badge.js +6 -0
  164. package/dist/constants/button.d.ts +16 -0
  165. package/dist/constants/button.js +16 -0
  166. package/dist/constants/calendar.d.ts +36 -0
  167. package/dist/constants/calendar.js +34 -0
  168. package/dist/constants/card.d.ts +23 -0
  169. package/dist/constants/card.js +20 -0
  170. package/dist/constants/carousel.d.ts +55 -0
  171. package/dist/constants/carousel.js +74 -0
  172. package/dist/constants/checkbox.d.ts +17 -0
  173. package/dist/constants/checkbox.js +17 -0
  174. package/dist/constants/dialog.d.ts +11 -0
  175. package/dist/constants/dialog.js +10 -0
  176. package/dist/constants/dropdown-menu.d.ts +10 -0
  177. package/dist/constants/dropdown-menu.js +37 -0
  178. package/dist/constants/field.d.ts +24 -0
  179. package/dist/constants/field.js +24 -0
  180. package/dist/constants/hover-card.d.ts +37 -0
  181. package/dist/constants/hover-card.js +85 -0
  182. package/dist/constants/morph-text.d.ts +13 -0
  183. package/dist/constants/morph-text.js +13 -0
  184. package/dist/constants/motion.d.ts +12 -0
  185. package/dist/constants/motion.js +12 -0
  186. package/dist/constants/pagination.d.ts +20 -0
  187. package/dist/constants/pagination.js +20 -0
  188. package/dist/constants/popover.d.ts +7 -0
  189. package/dist/constants/popover.js +33 -0
  190. package/dist/constants/radio-group.d.ts +10 -0
  191. package/dist/constants/radio-group.js +10 -0
  192. package/dist/constants/select.d.ts +7 -0
  193. package/dist/constants/select.js +34 -0
  194. package/dist/constants/sidebar.d.ts +9 -0
  195. package/dist/constants/sidebar.js +17 -0
  196. package/dist/constants/skeleton.d.ts +17 -0
  197. package/dist/constants/skeleton.js +17 -0
  198. package/dist/constants/split-button.d.ts +7 -0
  199. package/dist/constants/split-button.js +5 -0
  200. package/dist/constants/switch.d.ts +33 -0
  201. package/dist/constants/switch.js +41 -0
  202. package/dist/constants/tabs.d.ts +17 -0
  203. package/dist/constants/tabs.js +17 -0
  204. package/dist/constants/toast.d.ts +3 -0
  205. package/dist/constants/toast.js +3 -0
  206. package/dist/constants/tooltip.d.ts +13 -0
  207. package/dist/constants/tooltip.js +52 -0
  208. package/dist/hooks/use-horizontal-swipe.d.ts +23 -0
  209. package/dist/hooks/use-horizontal-swipe.js +80 -0
  210. package/dist/hooks/use-is-hydrated.d.ts +6 -0
  211. package/dist/hooks/use-is-hydrated.js +20 -0
  212. package/dist/hooks/use-mobile.d.ts +1 -0
  213. package/dist/hooks/use-mobile.js +19 -0
  214. package/dist/hooks/use-prefers-reduced-motion.d.ts +2 -0
  215. package/dist/hooks/use-prefers-reduced-motion.js +27 -0
  216. package/dist/hooks/use-rich-link-editor.d.ts +55 -0
  217. package/dist/hooks/use-rich-link-editor.js +515 -0
  218. package/dist/hooks/use-sidebar-persistence.d.ts +6 -0
  219. package/dist/hooks/use-sidebar-persistence.js +52 -0
  220. package/dist/hooks/use-today.d.ts +7 -0
  221. package/dist/hooks/use-today.js +50 -0
  222. package/dist/hooks/use-viewer-time-zone.d.ts +6 -0
  223. package/dist/hooks/use-viewer-time-zone.js +20 -0
  224. package/dist/hooks.d.ts +11 -0
  225. package/dist/hooks.js +11 -0
  226. package/dist/index.d.ts +71 -0
  227. package/dist/index.js +69 -0
  228. package/dist/lib/browser-clipboard.d.ts +13 -0
  229. package/dist/lib/browser-clipboard.js +48 -0
  230. package/dist/lib/column-filter-value.d.ts +12 -0
  231. package/dist/lib/column-filter-value.js +52 -0
  232. package/dist/lib/file-acceptance.d.ts +43 -0
  233. package/dist/lib/file-acceptance.js +107 -0
  234. package/dist/lib/format-file-size.d.ts +16 -0
  235. package/dist/lib/format-file-size.js +34 -0
  236. package/dist/lib/fuzzy-search.d.ts +17 -0
  237. package/dist/lib/fuzzy-search.js +183 -0
  238. package/dist/lib/horizontal-swipe.d.ts +23 -0
  239. package/dist/lib/horizontal-swipe.js +29 -0
  240. package/dist/lib/image-dimensions.d.ts +2 -0
  241. package/dist/lib/image-dimensions.js +7 -0
  242. package/dist/lib/name-initials.d.ts +7 -0
  243. package/dist/lib/name-initials.js +11 -0
  244. package/dist/lib/rich-text/link-markdown-constants.d.ts +147 -0
  245. package/dist/lib/rich-text/link-markdown-constants.js +458 -0
  246. package/dist/lib/rich-text/link-markdown-types.d.ts +53 -0
  247. package/dist/lib/rich-text/link-markdown-types.js +1 -0
  248. package/dist/lib/rich-text/link-markdown.d.ts +193 -0
  249. package/dist/lib/rich-text/link-markdown.js +592 -0
  250. package/dist/lib/rich-text/rich-markdown.d.ts +41 -0
  251. package/dist/lib/rich-text/rich-markdown.js +105 -0
  252. package/dist/lib/rich-text/rich-text-editor-dom.d.ts +26 -0
  253. package/dist/lib/rich-text/rich-text-editor-dom.js +79 -0
  254. package/dist/lib/toast-layer.d.ts +24 -0
  255. package/dist/lib/toast-layer.js +76 -0
  256. package/dist/lib/utils.d.ts +2 -0
  257. package/dist/lib/utils.js +2 -0
  258. package/dist/lib/year-month-filter.d.ts +16 -0
  259. package/dist/lib/year-month-filter.js +46 -0
  260. package/dist/motion/arc-floating-slot.d.ts +40 -0
  261. package/dist/motion/arc-floating-slot.js +177 -0
  262. package/dist/motion/arc-press.d.ts +6 -0
  263. package/dist/motion/arc-press.js +59 -0
  264. package/dist/motion/arc-swap.d.ts +22 -0
  265. package/dist/motion/arc-swap.js +49 -0
  266. package/dist/motion/arc-tokens.d.ts +53 -0
  267. package/dist/motion/arc-tokens.js +73 -0
  268. package/dist/motion/glide-indicator.d.ts +69 -0
  269. package/dist/motion/glide-indicator.js +309 -0
  270. package/dist/motion/glide-slot.d.ts +9 -0
  271. package/dist/motion/glide-slot.js +71 -0
  272. package/dist/motion/motion-slot.d.ts +12 -0
  273. package/dist/motion/motion-slot.js +376 -0
  274. package/dist/motion/overlay-warmth.d.ts +24 -0
  275. package/dist/motion/overlay-warmth.js +70 -0
  276. package/dist/motion/presence.d.ts +40 -0
  277. package/dist/motion/presence.js +101 -0
  278. package/dist/motion/surface-keyframes.d.ts +24 -0
  279. package/dist/motion/surface-keyframes.js +141 -0
  280. package/dist/motion/tokens.d.ts +81 -0
  281. package/dist/motion/tokens.js +67 -0
  282. package/dist/motion/use-attached-element.d.ts +7 -0
  283. package/dist/motion/use-attached-element.js +22 -0
  284. package/dist/motion/use-morph-width.d.ts +13 -0
  285. package/dist/motion/use-morph-width.js +59 -0
  286. package/dist/next.d.ts +14 -0
  287. package/dist/next.js +79 -0
  288. package/dist/providers/astra-ui-provider.d.ts +27 -0
  289. package/dist/providers/astra-ui-provider.js +100 -0
  290. package/dist/providers/native-provider.d.ts +4 -0
  291. package/dist/providers/native-provider.js +45 -0
  292. package/dist/sonner.d.ts +7 -0
  293. package/dist/sonner.js +8 -0
  294. package/dist/styles.css +2 -0
  295. package/dist/tanstack.d.ts +11 -0
  296. package/dist/tanstack.js +50 -0
  297. package/dist/theme.d.ts +3 -0
  298. package/dist/theme.js +1 -0
  299. package/dist/types/data-table.d.ts +27 -0
  300. package/dist/types/data-table.js +1 -0
  301. package/fonts.css +5 -0
  302. package/package.json +450 -4
  303. package/styles.css +4 -0
  304. package/theme.css +248 -0
@@ -0,0 +1,895 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
4
+ import { CircleAlert, ChevronDown, ChevronRight, Ellipsis, Eraser, X } from "lucide-react";
5
+ import { cn } from "../lib/utils.js";
6
+ import { matchesColumnFilterValue, normalizeFilterToken } from "../lib/column-filter-value.js";
7
+ import { MotionSlot } from "../motion/motion-slot.js";
8
+ import { ControlButton } from "./control-button.js";
9
+ import { Badge } from "./badge.js";
10
+ import { FilterQueryBar } from "./filter-query-bar.js";
11
+ import { parseFilterQuery, serializeFilterQuery } from "./filter-query-grammar.js";
12
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./primitives/dropdown-menu.js";
13
+ import { DropdownMenuItems } from "./primitives/dropdown-menu-items.js";
14
+ import { Input } from "./input.js";
15
+ import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "./table.js";
16
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
+ const CLEAR_FILTER_ARIA_LABEL = "Limpiar filtro";
18
+ const ACTIVE_EXCLUSIONS_SR_LABEL = "Filtros de exclusión activos";
19
+ const HIDE_EXCLUDE_FILTERS_ARIA_LABEL = "Ocultar filtros de exclusión";
20
+ const EMPTY_EXCLUDE_FILTER_ERROR_MESSAGE = "Ingresá un texto para excluir.";
21
+ const DUPLICATE_EXCLUDE_FILTER_ERROR_MESSAGE = "Esa exclusión ya está activa.";
22
+ const EXCLUDED_ROWS_SUMMARY_LABEL = "Total excluidas";
23
+ const CLEAR_ALL_EXCLUSIONS_ARIA_LABEL = "Quitar todas las exclusiones";
24
+ const CLEAR_ALL_EXCLUSIONS_FROM_INPUT_ARIA_LABEL = "Limpiar filtros excluidos";
25
+ const REVERSE_FILTER_PENDING_MESSAGE = "Estás escribiendo una exclusión. Presioná Enter para aplicarla.";
26
+ /** Filters a column with the structured values the query bar writes, or plain text. */
27
+ const columnFilterValueFn = (row, columnId, filterValue) => matchesColumnFilterValue(row.getValue(columnId), filterValue);
28
+ /** Mirrors TanStack's column id resolution: explicit id, else the accessor key with dots as `_`. */
29
+ function getColumnDefId(column) {
30
+ if (column.id) return column.id;
31
+ const accessorKey = column.accessorKey;
32
+ return typeof accessorKey === "string" ? accessorKey.replaceAll(".", "_") : undefined;
33
+ }
34
+ /**
35
+ * Gives the columns targeted by query qualifiers a filter that understands the query bar's
36
+ * structured values. TanStack would otherwise infer one from the data (`inNumberRange` for
37
+ * numbers, which crashes on an object). A consumer's own `filterFn` always wins.
38
+ */
39
+ function withQueryFilterFns(columns, targetedColumnIds) {
40
+ if (targetedColumnIds.size === 0) return columns;
41
+ return columns.map((column) => {
42
+ if ("columns" in column && column.columns) {
43
+ return {
44
+ ...column,
45
+ columns: withQueryFilterFns(column.columns, targetedColumnIds)
46
+ };
47
+ }
48
+ const columnId = getColumnDefId(column);
49
+ if (column.filterFn != null || columnId == null || !targetedColumnIds.has(columnId)) {
50
+ return column;
51
+ }
52
+ return {
53
+ ...column,
54
+ filterFn: columnFilterValueFn
55
+ };
56
+ });
57
+ }
58
+ function getTableFilterValue(filterValue, showExcludeFilterToggle) {
59
+ const normalizedFilterValue = filterValue.trimStart();
60
+ if (showExcludeFilterToggle && normalizedFilterValue.startsWith("-")) {
61
+ return "";
62
+ }
63
+ return filterValue;
64
+ }
65
+ function areColumnFiltersEqual(leftFilters, rightFilters) {
66
+ if (leftFilters.length !== rightFilters.length) {
67
+ return false;
68
+ }
69
+ return leftFilters.every((leftFilter, index) => leftFilter.id === rightFilters[index]?.id && leftFilter.value === rightFilters[index]?.value);
70
+ }
71
+ function areStringArraysEqual(leftValues, rightValues) {
72
+ if (leftValues.length !== rightValues.length) {
73
+ return false;
74
+ }
75
+ return leftValues.every((value, index) => value === rightValues[index]);
76
+ }
77
+ function areColumnFilterValuesEqual(leftValue, rightValue) {
78
+ if (leftValue.kind !== rightValue.kind) {
79
+ return false;
80
+ }
81
+ if (leftValue.kind === "numberRange" && rightValue.kind === "numberRange") {
82
+ return leftValue.min === rightValue.min && leftValue.max === rightValue.max;
83
+ }
84
+ if (leftValue.kind === "yearMonthRange" && rightValue.kind === "yearMonthRange") {
85
+ return leftValue.mode === rightValue.mode && leftValue.min === rightValue.min && leftValue.max === rightValue.max;
86
+ }
87
+ if (leftValue.kind === "enum" && rightValue.kind === "enum") {
88
+ return leftValue.value === rightValue.value;
89
+ }
90
+ if (leftValue.kind === "presence" && rightValue.kind === "presence") {
91
+ return leftValue.value === rightValue.value;
92
+ }
93
+ return false;
94
+ }
95
+ function areAdvancedFilterMapsEqual(leftMap, rightMap) {
96
+ const leftKeys = Object.keys(leftMap);
97
+ const rightKeys = Object.keys(rightMap);
98
+ if (leftKeys.length !== rightKeys.length) {
99
+ return false;
100
+ }
101
+ return leftKeys.every((key) => {
102
+ const rightValue = rightMap[key];
103
+ return rightValue != null && areColumnFilterValuesEqual(leftMap[key], rightValue);
104
+ });
105
+ }
106
+ export function DataTable({ columns, data, emptyMessage, toolbarActions, onVisibleRowsChange, onCellClick, getRowClassName, sorting: controlledSorting, onSortingChange, columnVisibility: controlledColumnVisibility, onColumnVisibilityChange, filterColumnId, filterLabel = "Filtrar", filterPlaceholder = "Filtrar...", filterExtraContent, filterValue: controlledFilterValue, onFilterValueChange, queryFilterConfig, queryFilterPlaceholder, queryFilterLabel, onAppliedFiltersChange, queryFilterControlsRef, queryFilterTrailingAction, rowGroups, showExcludeFilterToggle = false, excludeFilterValues: controlledExcludeFilterValues, onExcludeFilterValuesChange, excludeFilterPlaceholder = "Excluir por descripción", excludeFilterLabel = "Filtro de exclusión", excludeFilterToggleLabel = "Mostrar filtros de exclusión", excludeFilterRowsCountByValue, excludeFilterUniqueRowsCount, showColumnVisibilityToggle = false, columnVisibilityButtonLabel = "Columnas", columnVisibilityMenuLabel = "Mostrar columnas", columnVisibilityMenuExtraItems, sortingBadgeLabelOverrides, sortingBadgeIgnoredColumnIds, selectAllColumnsLabel = "Mostrar todas", deselectAllColumnsLabel = "Ocultar todas", hideableColumnsDefaultVisibility }) {
107
+ const [uncontrolledSorting, setUncontrolledSorting] = React.useState([]);
108
+ const [columnFilters, setColumnFilters] = React.useState([]);
109
+ const [uncontrolledFilterValue, setUncontrolledFilterValue] = React.useState("");
110
+ const [filterQueryDraft, setFilterQueryDraft] = React.useState("");
111
+ // Filtros estructurados de la última query parseada. Se preservan para que la
112
+ // re-sincronización canónica (al perder foco) NO descarte los qualifiers sin
113
+ // columna (textMatch/folder) ni los negados, que no viven en
114
+ // `advancedFiltersAppliedByColumn` y quedarían como filtros activos invisibles.
115
+ const [appliedFiltersDraft, setAppliedFiltersDraft] = React.useState([]);
116
+ const [isQueryFilterFocused, setIsQueryFilterFocused] = React.useState(false);
117
+ const [uncontrolledExcludeFilterValues, setUncontrolledExcludeFilterValues] = React.useState([]);
118
+ const [excludeFilterInputValue, setExcludeFilterInputValue] = React.useState("");
119
+ const [excludeFilterErrorMessage, setExcludeFilterErrorMessage] = React.useState(null);
120
+ const [isExcludeFilterInputVisible, setIsExcludeFilterInputVisible] = React.useState(false);
121
+ const [uncontrolledColumnVisibility, setUncontrolledColumnVisibility] = React.useState({});
122
+ const [advancedFiltersAppliedByColumn, setAdvancedFiltersAppliedByColumn] = React.useState({});
123
+ const sorting = controlledSorting ?? uncontrolledSorting;
124
+ const columnVisibility = controlledColumnVisibility ?? uncontrolledColumnVisibility;
125
+ const shouldShowQueryFilter = queryFilterConfig != null;
126
+ const isFilterValueControlled = controlledFilterValue != null;
127
+ const isExcludeFilterValuesControlled = controlledExcludeFilterValues != null;
128
+ const resolvedFilterValue = isFilterValueControlled ? controlledFilterValue : uncontrolledFilterValue;
129
+ const resolvedExcludeFilterValues = isExcludeFilterValuesControlled ? controlledExcludeFilterValues : uncontrolledExcludeFilterValues;
130
+ const hasActiveExcludeFilterValues = resolvedExcludeFilterValues.length > 0;
131
+ const tableFilterValue = getTableFilterValue(resolvedFilterValue, showExcludeFilterToggle);
132
+ const hasPendingReverseFilter = showExcludeFilterToggle && resolvedFilterValue.trimStart().startsWith("-");
133
+ const advancedFilterColumnIds = React.useMemo(() => {
134
+ const columnIds = new Set();
135
+ for (const queryQualifier of queryFilterConfig ?? []) {
136
+ if (queryQualifier.columnId) {
137
+ columnIds.add(queryQualifier.columnId);
138
+ }
139
+ }
140
+ return columnIds;
141
+ }, [queryFilterConfig]);
142
+ const handleExcludeFilterValuesChange = React.useCallback((nextExcludeFilterValues) => {
143
+ if (!isExcludeFilterValuesControlled) {
144
+ setUncontrolledExcludeFilterValues(nextExcludeFilterValues);
145
+ }
146
+ onExcludeFilterValuesChange?.(nextExcludeFilterValues);
147
+ }, [isExcludeFilterValuesControlled, onExcludeFilterValuesChange]);
148
+ // La barra de query es la fuente de verdad mientras se tipea: parsea el texto
149
+ // y escribe en los mismos canales que usan los controles clásicos (descripción,
150
+ // exclusiones y avanzados), de modo que ambas vistas queden sincronizadas.
151
+ const handleQueryFilterChange = React.useCallback((nextQuery) => {
152
+ // Update urgente: mantiene el input y el dropdown de sugerencias
153
+ // respondiendo de inmediato a cada tecla.
154
+ setFilterQueryDraft(nextQuery);
155
+ if (!queryFilterConfig) {
156
+ return;
157
+ }
158
+ const parsedQuery = parseFilterQuery(nextQuery, queryFilterConfig);
159
+ // El re-filtrado vuelve a renderizar toda la tabla (celdas con popovers,
160
+ // tooltips y menús por fila), lo que bloquearía el commit del dropdown si
161
+ // corriera urgente. Se difiere como transición de baja prioridad para que
162
+ // el tipeo y las sugerencias no se cuelguen.
163
+ React.startTransition(() => {
164
+ // Cada canal se actualiza solo si cambió. Propagar valores iguales
165
+ // (p. ej. una lista de exclusiones vacía nueva en cada tecla) dispararía
166
+ // re-cálculos de memos y un re-render extra de todas las filas sin
167
+ // ningún cambio observable.
168
+ if (parsedQuery.descriptionFilter !== resolvedFilterValue) {
169
+ if (!isFilterValueControlled) {
170
+ setUncontrolledFilterValue(parsedQuery.descriptionFilter);
171
+ }
172
+ onFilterValueChange?.(parsedQuery.descriptionFilter);
173
+ }
174
+ if (!areStringArraysEqual(parsedQuery.excludedDescriptionFilters, resolvedExcludeFilterValues)) {
175
+ handleExcludeFilterValuesChange(parsedQuery.excludedDescriptionFilters);
176
+ }
177
+ if (!areAdvancedFilterMapsEqual(parsedQuery.advancedFiltersByColumn, advancedFiltersAppliedByColumn)) {
178
+ setAdvancedFiltersAppliedByColumn(parsedQuery.advancedFiltersByColumn);
179
+ }
180
+ setAppliedFiltersDraft(parsedQuery.appliedFilters);
181
+ onAppliedFiltersChange?.(parsedQuery.appliedFilters);
182
+ });
183
+ }, [
184
+ advancedFiltersAppliedByColumn,
185
+ handleExcludeFilterValuesChange,
186
+ isFilterValueControlled,
187
+ onAppliedFiltersChange,
188
+ onFilterValueChange,
189
+ queryFilterConfig,
190
+ resolvedExcludeFilterValues,
191
+ resolvedFilterValue
192
+ ]);
193
+ // Forma canónica de los filtros activos, para reflejarlos en la barra de query.
194
+ // Se serializa desde los filtros aplicados id-backed (no desde el texto de la
195
+ // barra), de modo que un filtro de carpeta apunte siempre por `folderId`: si la
196
+ // carpeta se renombra, su slug se re-deriva del nombre nuevo; si desaparece, el
197
+ // filtro se descarta. Definida antes del efecto de reparse para que este pueda
198
+ // re-propagar la forma id-backed cuando cambia la config.
199
+ const canonicalQueryFromFilters = React.useMemo(() => {
200
+ if (!queryFilterConfig) {
201
+ return "";
202
+ }
203
+ return serializeFilterQuery({
204
+ advancedFiltersByColumn: advancedFiltersAppliedByColumn,
205
+ appliedFilters: appliedFiltersDraft,
206
+ descriptionFilter: tableFilterValue,
207
+ excludedDescriptionFilters: resolvedExcludeFilterValues,
208
+ invalidTokens: []
209
+ }, queryFilterConfig);
210
+ }, [
211
+ advancedFiltersAppliedByColumn,
212
+ appliedFiltersDraft,
213
+ queryFilterConfig,
214
+ resolvedExcludeFilterValues,
215
+ tableFilterValue
216
+ ]);
217
+ // Firma estructural de la config de qualifiers: dispara la reconciliación solo
218
+ // cuando su CONTENIDO cambia (no en cada render con una referencia de array
219
+ // nueva), evitando re-propagar la forma canónica mientras el usuario tipea.
220
+ const queryFilterConfigSignature = React.useMemo(() => queryFilterConfig ? JSON.stringify(queryFilterConfig) : "", [queryFilterConfig]);
221
+ const previousQueryFilterConfigSignatureRef = React.useRef(null);
222
+ // Si la config de qualifiers cambia (p. ej. se renombra una carpeta y su opción
223
+ // pasa de `carpeta:hogar` a `carpeta:casa`, o se borra y desaparece), se
224
+ // re-propaga la forma canónica id-backed contra la NUEVA config en vez de
225
+ // re-parsear el texto con el slug viejo. Así un filtro de carpeta seleccionado
226
+ // sobrevive a renames y colisiones de slug (se vuelve a serializar por
227
+ // `folderId`), mientras que un filtro realmente huérfano (cuyo `folderId` ya no
228
+ // existe) se descarta del predicado en vez de seguir filtrando de forma
229
+ // invisible.
230
+ React.useEffect(() => {
231
+ const previousSignature = previousQueryFilterConfigSignatureRef.current;
232
+ previousQueryFilterConfigSignatureRef.current = queryFilterConfigSignature;
233
+ // Primer render o config sin contenido: nada que reconciliar todavía.
234
+ if (previousSignature === null) {
235
+ return;
236
+ }
237
+ if (!queryFilterConfig || !filterQueryDraft || previousSignature === queryFilterConfigSignature) {
238
+ return;
239
+ }
240
+ handleQueryFilterChange(canonicalQueryFromFilters);
241
+ // Solo debe re-correr cuando cambia el contenido de la config; el resto de los
242
+ // valores se leen como estado actual y no como disparadores para no re-parsear
243
+ // en cada tecla.
244
+ // eslint-disable-next-line react-hooks/exhaustive-deps
245
+ }, [queryFilterConfigSignature]);
246
+ // Reemplaza los filtros de carpeta de la barra por un único `carpeta:<id>` (o
247
+ // ninguno si `folderId` es `null`), preservando el resto. Lo usa el consumidor
248
+ // al clickear un chip de carpeta, para que la barra sea la única fuente de
249
+ // verdad y no queden filtros de carpeta inconsistentes.
250
+ const setSingleFolderFilter = React.useCallback((folderId) => {
251
+ if (!queryFilterConfig) {
252
+ return;
253
+ }
254
+ const folderQualifierKey = queryFilterConfig.find((config) => config.kind === "folder")?.key;
255
+ if (!folderQualifierKey) {
256
+ return;
257
+ }
258
+ const parsedQuery = parseFilterQuery(filterQueryDraft, queryFilterConfig);
259
+ // Limpia TODO filtro que apunte a la key de carpeta —tanto `carpeta:<id>`
260
+ // (kind "folder") como la presencia `tiene:carpeta`/`no:carpeta`—, para que
261
+ // el chip sea la única fuente de verdad y no queden filtros contradictorios
262
+ // (p. ej. `no:carpeta` + `carpeta:hogar`, que oculta todas las filas).
263
+ const nonFolderFilters = parsedQuery.appliedFilters.filter((appliedFilter) => appliedFilter.value.kind !== "folder" && appliedFilter.key !== folderQualifierKey);
264
+ const nextAppliedFilters = folderId == null ? nonFolderFilters : [...nonFolderFilters, {
265
+ key: folderQualifierKey,
266
+ negated: false,
267
+ value: {
268
+ folderId,
269
+ kind: "folder"
270
+ }
271
+ }];
272
+ handleQueryFilterChange(serializeFilterQuery({
273
+ ...parsedQuery,
274
+ appliedFilters: nextAppliedFilters
275
+ }, queryFilterConfig));
276
+ }, [
277
+ filterQueryDraft,
278
+ handleQueryFilterChange,
279
+ queryFilterConfig
280
+ ]);
281
+ React.useEffect(() => {
282
+ if (!queryFilterControlsRef) {
283
+ return;
284
+ }
285
+ queryFilterControlsRef.current = {
286
+ getQueryText: () => filterQueryDraft,
287
+ setQueryText: handleQueryFilterChange,
288
+ setSingleFolderFilter
289
+ };
290
+ return () => {
291
+ queryFilterControlsRef.current = null;
292
+ };
293
+ }, [
294
+ filterQueryDraft,
295
+ handleQueryFilterChange,
296
+ queryFilterControlsRef,
297
+ setSingleFolderFilter
298
+ ]);
299
+ const addExcludeFilterValue = React.useCallback((rawExcludeFilterValue, options = {}) => {
300
+ const normalizedCandidate = normalizeFilterToken(rawExcludeFilterValue);
301
+ if (!normalizedCandidate) {
302
+ setExcludeFilterErrorMessage(EMPTY_EXCLUDE_FILTER_ERROR_MESSAGE);
303
+ return false;
304
+ }
305
+ const hasDuplicateExcludeFilter = resolvedExcludeFilterValues.some((excludeFilterValue) => normalizeFilterToken(excludeFilterValue) === normalizedCandidate);
306
+ if (hasDuplicateExcludeFilter) {
307
+ setExcludeFilterErrorMessage(DUPLICATE_EXCLUDE_FILTER_ERROR_MESSAGE);
308
+ return false;
309
+ }
310
+ handleExcludeFilterValuesChange([...resolvedExcludeFilterValues, rawExcludeFilterValue.trim()]);
311
+ if (options.clearExcludeInputValue ?? true) {
312
+ setExcludeFilterInputValue("");
313
+ }
314
+ setExcludeFilterErrorMessage(null);
315
+ return true;
316
+ }, [handleExcludeFilterValuesChange, resolvedExcludeFilterValues]);
317
+ const removeExcludeFilterValue = React.useCallback((excludeFilterValueToRemove) => {
318
+ handleExcludeFilterValuesChange(resolvedExcludeFilterValues.filter((excludeFilterValue) => excludeFilterValue !== excludeFilterValueToRemove));
319
+ }, [handleExcludeFilterValuesChange, resolvedExcludeFilterValues]);
320
+ const handleClearAllExcludeFilters = React.useCallback(() => {
321
+ handleExcludeFilterValuesChange([]);
322
+ setExcludeFilterInputValue("");
323
+ setExcludeFilterErrorMessage(null);
324
+ }, [handleExcludeFilterValuesChange]);
325
+ const handleSortingChange = React.useCallback((updater) => {
326
+ const nextSorting = typeof updater === "function" ? updater(sorting) : updater;
327
+ if (controlledSorting == null) {
328
+ setUncontrolledSorting(nextSorting);
329
+ }
330
+ onSortingChange?.(nextSorting);
331
+ }, [
332
+ controlledSorting,
333
+ onSortingChange,
334
+ sorting
335
+ ]);
336
+ const handleColumnVisibilityChange = React.useCallback((updater) => {
337
+ if (controlledColumnVisibility == null) {
338
+ setUncontrolledColumnVisibility(updater);
339
+ }
340
+ onColumnVisibilityChange?.(updater);
341
+ }, [controlledColumnVisibility, onColumnVisibilityChange]);
342
+ // TanStack Table manages internal reactive state through this hook.
343
+ const tableColumns = React.useMemo(() => withQueryFilterFns(columns, advancedFilterColumnIds), [columns, advancedFilterColumnIds]);
344
+ const table = useReactTable({
345
+ columns: tableColumns,
346
+ data,
347
+ // La tabla no pagina (no hay `getPaginationRowModel`). Con el auto-reset por
348
+ // defecto, cada cambio de filtro dispara `resetPageIndex` → `setPagination`,
349
+ // que vuelve a renderizar y re-dispara el reset en bucle: "Maximum update
350
+ // depth exceeded" y la página colgada al tipear. Se desactivan los
351
+ // auto-resets porque no hay estado de paginado/expansión que reiniciar.
352
+ autoResetPageIndex: false,
353
+ autoResetExpanded: false,
354
+ getCoreRowModel: getCoreRowModel(),
355
+ getFilteredRowModel: getFilteredRowModel(),
356
+ getSortedRowModel: getSortedRowModel(),
357
+ onColumnFiltersChange: setColumnFilters,
358
+ onSortingChange: handleSortingChange,
359
+ onColumnVisibilityChange: handleColumnVisibilityChange,
360
+ state: {
361
+ columnFilters,
362
+ columnVisibility,
363
+ sorting
364
+ }
365
+ });
366
+ const hideableColumns = table.getAllLeafColumns().filter((column) => column.getCanHide());
367
+ const getHideableColumnDefaultVisibility = React.useCallback((columnId) => hideableColumnsDefaultVisibility?.[columnId] ?? true, [hideableColumnsDefaultVisibility]);
368
+ const areHideableColumnsAtDefaultVisibility = hideableColumns.every((column) => column.getIsVisible() === getHideableColumnDefaultVisibility(column.id));
369
+ const areSomeHideableColumnsVisible = hideableColumns.some((column) => column.getIsVisible());
370
+ const shouldShowColumnVisibilityToggle = showColumnVisibilityToggle && hideableColumns.length > 0;
371
+ const hasModifiedColumnVisibility = shouldShowColumnVisibilityToggle && !areHideableColumnsAtDefaultVisibility;
372
+ const hasToolbarChanges = hasModifiedColumnVisibility;
373
+ const shouldShowToolbarActions = shouldShowColumnVisibilityToggle || toolbarActions != null;
374
+ const shouldShowToolbar = shouldShowQueryFilter || Boolean(filterColumnId) || shouldShowToolbarActions;
375
+ const filterColumn = filterColumnId ? table.getColumn(filterColumnId) : undefined;
376
+ const activeSortingEntry = sorting.find((sortingEntry) => !sortingBadgeIgnoredColumnIds?.has(sortingEntry.id));
377
+ const activeSortingColumn = activeSortingEntry ? table.getColumn(activeSortingEntry.id) : undefined;
378
+ const activeSortingColumnMeta = activeSortingColumn?.columnDef.meta;
379
+ const activeSortingColumnLabel = (activeSortingEntry ? sortingBadgeLabelOverrides?.[activeSortingEntry.id] : undefined) ?? activeSortingColumnMeta?.label ?? (typeof activeSortingColumn?.columnDef.header === "string" ? activeSortingColumn.columnDef.header : activeSortingEntry?.id);
380
+ const shouldShowSortingBadge = activeSortingEntry != null && activeSortingColumnLabel != null;
381
+ const activeSortingDirectionSymbol = activeSortingEntry?.desc ? "↓" : "↑";
382
+ const activeSortingDirectionLabel = activeSortingEntry?.desc ? "descendente" : "ascendente";
383
+ const footerGroups = table.getFooterGroups();
384
+ const hasFooterContent = footerGroups.some((footerGroup) => footerGroup.headers.some((footer) => !footer.isPlaceholder && footer.column.columnDef.footer != null));
385
+ const handleResetSorting = React.useCallback(() => {
386
+ handleSortingChange([]);
387
+ }, [handleSortingChange]);
388
+ const handleClearMainFilter = React.useCallback(() => {
389
+ if (!isFilterValueControlled) {
390
+ setUncontrolledFilterValue("");
391
+ }
392
+ onFilterValueChange?.("");
393
+ filterColumn?.setFilterValue("");
394
+ }, [
395
+ filterColumn,
396
+ isFilterValueControlled,
397
+ onFilterValueChange
398
+ ]);
399
+ React.useEffect(() => {
400
+ if (!filterColumn) {
401
+ return;
402
+ }
403
+ const currentFilterValue = String(filterColumn.getFilterValue() ?? "");
404
+ if (currentFilterValue === tableFilterValue) {
405
+ return;
406
+ }
407
+ filterColumn.setFilterValue(tableFilterValue);
408
+ }, [filterColumn, tableFilterValue]);
409
+ // Mientras la barra está enfocada, el usuario es la fuente de verdad y no se
410
+ // pisa su texto; al perder el foco (o al editar los controles clásicos) la
411
+ // barra se re-sincroniza con la forma canónica de los filtros activos.
412
+ React.useEffect(() => {
413
+ if (!queryFilterConfig || isQueryFilterFocused) {
414
+ return;
415
+ }
416
+ setFilterQueryDraft((previousDraft) => previousDraft === canonicalQueryFromFilters ? previousDraft : canonicalQueryFromFilters);
417
+ }, [
418
+ canonicalQueryFromFilters,
419
+ isQueryFilterFocused,
420
+ queryFilterConfig
421
+ ]);
422
+ React.useEffect(() => {
423
+ if (advancedFilterColumnIds.size === 0 && Object.keys(advancedFiltersAppliedByColumn).length === 0) {
424
+ return;
425
+ }
426
+ setColumnFilters((previousColumnFilters) => {
427
+ const nextColumnFilters = previousColumnFilters.filter((columnFilter) => !advancedFilterColumnIds.has(columnFilter.id));
428
+ for (const [columnId, filterValue] of Object.entries(advancedFiltersAppliedByColumn)) {
429
+ nextColumnFilters.push({
430
+ id: columnId,
431
+ value: filterValue
432
+ });
433
+ }
434
+ if (areColumnFiltersEqual(previousColumnFilters, nextColumnFilters)) {
435
+ return previousColumnFilters;
436
+ }
437
+ return nextColumnFilters;
438
+ });
439
+ }, [advancedFilterColumnIds, advancedFiltersAppliedByColumn]);
440
+ React.useEffect(() => {
441
+ onVisibleRowsChange?.(table.getRowModel().rows.map((row) => row.original));
442
+ }, [
443
+ columnFilters,
444
+ columnVisibility,
445
+ data,
446
+ onVisibleRowsChange,
447
+ sorting,
448
+ table
449
+ ]);
450
+ const sortingBadgeNode = shouldShowSortingBadge ? /* @__PURE__ */ _jsxs(Badge, {
451
+ "aria-live": "polite",
452
+ className: "inline-flex w-fit items-center gap-1.5",
453
+ tone: "neutral",
454
+ children: [
455
+ /* @__PURE__ */ _jsx("span", { children: `Ordenado por: ${activeSortingColumnLabel} ${activeSortingDirectionSymbol}` }),
456
+ /* @__PURE__ */ _jsx("span", {
457
+ className: "sr-only",
458
+ children: `Orden ${activeSortingDirectionLabel}`
459
+ }),
460
+ /* @__PURE__ */ _jsx("button", {
461
+ "aria-label": "Quitar orden",
462
+ className: "inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
463
+ onClick: handleResetSorting,
464
+ type: "button",
465
+ children: /* @__PURE__ */ _jsx(X, {
466
+ "aria-hidden": "true",
467
+ className: "size-3"
468
+ })
469
+ })
470
+ ]
471
+ }) : null;
472
+ const filterExtraContentNode = filterExtraContent ? /* @__PURE__ */ _jsx("div", {
473
+ className: "grid gap-1",
474
+ children: filterExtraContent
475
+ }) : null;
476
+ const tableRowModelRows = table.getRowModel().rows;
477
+ const visibleLeafColumnsCount = table.getVisibleLeafColumns().length;
478
+ // El cuerpo de la tabla es la parte más cara de renderizar (cientos de celdas
479
+ // con popovers, tooltips y menús). Se memoiza para que los cambios de estado
480
+ // locales del toolbar —el draft de la barra de query, el foco, los inputs de
481
+ // filtros— no vuelvan a renderizar todas las filas. Solo se recalcula cuando
482
+ // cambia el row model (datos/filtros/orden), la visibilidad de columnas o los
483
+ // callbacks de fila. Sin esto, cada tecla disparaba varios re-render completos
484
+ // del cuerpo y colgaba la página. Se sigue el patrón de memoización de
485
+ // TanStack: no se depende de la identidad de `table`, que cambia en cada render.
486
+ const tableBodyRows = React.useMemo(() => {
487
+ // Con agrupado activo, el conteo por grupo se calcula sobre el row model
488
+ // visible (post filtros/orden) para que el encabezado refleje lo que se ve.
489
+ const groupRowCounts = new Map();
490
+ if (rowGroups) {
491
+ for (const row of tableRowModelRows) {
492
+ const groupKey = rowGroups.getGroupKey(row.original);
493
+ groupRowCounts.set(groupKey, (groupRowCounts.get(groupKey) ?? 0) + 1);
494
+ }
495
+ }
496
+ // Partición por grupo: garantiza un único bloque por clave aunque el row
497
+ // model llegue intercalado (orden manual, sorting sin la columna fantasma,
498
+ // etc.). El orden interno de cada grupo respeta el row model; el orden de
499
+ // los grupos usa getGroupSortValue (del primer row) o la primera aparición.
500
+ let rowsToRender = tableRowModelRows;
501
+ if (rowGroups) {
502
+ const rowsByGroupKey = new Map();
503
+ for (const row of tableRowModelRows) {
504
+ const groupKey = rowGroups.getGroupKey(row.original);
505
+ const groupRows = rowsByGroupKey.get(groupKey);
506
+ if (groupRows) {
507
+ groupRows.push(row);
508
+ } else {
509
+ rowsByGroupKey.set(groupKey, [row]);
510
+ }
511
+ }
512
+ const groupEntries = [...rowsByGroupKey.values()];
513
+ const getGroupSortValue = rowGroups.getGroupSortValue;
514
+ if (getGroupSortValue) {
515
+ groupEntries.sort((leftRows, rightRows) => {
516
+ const leftValue = getGroupSortValue(leftRows[0].original);
517
+ const rightValue = getGroupSortValue(rightRows[0].original);
518
+ if (leftValue === rightValue) {
519
+ return 0;
520
+ }
521
+ return leftValue < rightValue ? -1 : 1;
522
+ });
523
+ }
524
+ rowsToRender = groupEntries.flat();
525
+ }
526
+ const renderedRows = [];
527
+ let previousGroupKey = null;
528
+ for (const row of rowsToRender) {
529
+ if (rowGroups) {
530
+ const groupKey = rowGroups.getGroupKey(row.original);
531
+ const isGroupCollapsed = rowGroups.collapsedGroupKeys?.has(groupKey) ?? false;
532
+ if (groupKey !== previousGroupKey) {
533
+ const groupHeaderContent = rowGroups.renderGroupHeader(groupKey, groupRowCounts.get(groupKey) ?? 0);
534
+ const handleGroupToggle = rowGroups.onGroupToggle;
535
+ renderedRows.push(/* @__PURE__ */ _jsx(TableRow, {
536
+ className: "hover:bg-transparent",
537
+ "data-slot": "table-group-header-row",
538
+ children: /* @__PURE__ */ _jsx(TableCell, {
539
+ className: "bg-muted/40",
540
+ colSpan: Math.max(visibleLeafColumnsCount, 1),
541
+ children: handleGroupToggle ? /* @__PURE__ */ _jsxs("button", {
542
+ "aria-expanded": !isGroupCollapsed,
543
+ className: "flex w-full items-center gap-1.5 text-left",
544
+ onClick: () => handleGroupToggle(groupKey),
545
+ type: "button",
546
+ children: [/* @__PURE__ */ _jsx(MotionSlot, {
547
+ kind: "rotate",
548
+ children: /* @__PURE__ */ _jsx(ChevronRight, {
549
+ "aria-hidden": "true",
550
+ className: cn("size-4 shrink-0", !isGroupCollapsed && "rotate-90")
551
+ })
552
+ }), groupHeaderContent]
553
+ }) : groupHeaderContent
554
+ })
555
+ }, `group-header-${groupKey}-${row.id}`));
556
+ previousGroupKey = groupKey;
557
+ }
558
+ // Grupo colapsado: solo queda el encabezado; sus filas no se renderizan.
559
+ if (isGroupCollapsed) {
560
+ continue;
561
+ }
562
+ }
563
+ const rowClassName = getRowClassName?.(row.original);
564
+ renderedRows.push(/* @__PURE__ */ _jsx(TableRow, {
565
+ className: rowClassName,
566
+ children: row.getVisibleCells().map((cell) => {
567
+ const columnMeta = cell.column.columnDef.meta;
568
+ const handleCellClick = onCellClick && columnMeta?.isClickable ? (event) => {
569
+ onCellClick(event, row.original, cell.column.id);
570
+ } : undefined;
571
+ return /* @__PURE__ */ _jsx(TableCell, {
572
+ className: columnMeta?.cellClassName,
573
+ "data-label": columnMeta?.label,
574
+ onClick: handleCellClick,
575
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
576
+ }, cell.id);
577
+ })
578
+ }, row.id));
579
+ }
580
+ return renderedRows;
581
+ // `columnVisibility` se incluye a propósito: alterna qué celdas devuelve
582
+ // `row.getVisibleCells()` sin cambiar la referencia de `tableRowModelRows`,
583
+ // por lo que es necesaria para invalidar la memo al mostrar/ocultar columnas
584
+ // aunque el linter no la vea referenciada léxicamente.
585
+ // `emptyMessage` se mantiene FUERA de esta memo a propósito: cambia al pasar
586
+ // de "sin filtro" a "con filtro" y, si estuviera acá, invalidaría la memo y
587
+ // re-renderizaría todas las filas en la primera tecla aunque no cambien.
588
+ // eslint-disable-next-line react-hooks/exhaustive-deps
589
+ }, [
590
+ columnVisibility,
591
+ getRowClassName,
592
+ onCellClick,
593
+ rowGroups,
594
+ tableRowModelRows,
595
+ visibleLeafColumnsCount
596
+ ]);
597
+ const tableBodyContent = tableBodyRows.length ? tableBodyRows : /* @__PURE__ */ _jsx(TableRow, { children: /* @__PURE__ */ _jsx(TableCell, {
598
+ className: "h-24 text-center",
599
+ colSpan: Math.max(visibleLeafColumnsCount, 1),
600
+ children: emptyMessage
601
+ }) });
602
+ return /* @__PURE__ */ _jsxs("div", {
603
+ className: "grid gap-4",
604
+ children: [shouldShowToolbar ? /* @__PURE__ */ _jsxs("div", {
605
+ className: "grid gap-2",
606
+ children: [shouldShowQueryFilter ? /* @__PURE__ */ _jsx(FilterQueryBar, {
607
+ ariaLabel: queryFilterLabel,
608
+ configs: queryFilterConfig,
609
+ onFocusChange: setIsQueryFilterFocused,
610
+ onValueChange: handleQueryFilterChange,
611
+ placeholder: queryFilterPlaceholder,
612
+ trailingAction: queryFilterTrailingAction,
613
+ value: filterQueryDraft
614
+ }) : null, /* @__PURE__ */ _jsxs("div", {
615
+ className: "flex flex-wrap items-center gap-3",
616
+ children: [filterColumnId ? /* @__PURE__ */ _jsxs("div", {
617
+ className: "grid w-full max-w-sm gap-2",
618
+ children: [
619
+ !shouldShowQueryFilter ? /* @__PURE__ */ _jsxs(_Fragment, { children: [
620
+ /* @__PURE__ */ _jsxs("div", {
621
+ className: "relative w-full",
622
+ children: [/* @__PURE__ */ _jsx(Input, {
623
+ "aria-label": filterLabel,
624
+ className: `w-full pr-16 ${hasPendingReverseFilter ? "text-red-400" : ""}`,
625
+ onChange: (event) => {
626
+ const nextFilterValue = event.target.value;
627
+ if (!isFilterValueControlled) {
628
+ setUncontrolledFilterValue(nextFilterValue);
629
+ }
630
+ onFilterValueChange?.(nextFilterValue);
631
+ filterColumn?.setFilterValue(getTableFilterValue(nextFilterValue, showExcludeFilterToggle));
632
+ },
633
+ onKeyDown: (event) => {
634
+ if (event.key !== "Enter") {
635
+ return;
636
+ }
637
+ const trimmedResolvedFilterValue = resolvedFilterValue.trimStart();
638
+ if (!showExcludeFilterToggle || !trimmedResolvedFilterValue.startsWith("-")) {
639
+ return;
640
+ }
641
+ event.preventDefault();
642
+ const excludeFilterCandidate = trimmedResolvedFilterValue.slice(1);
643
+ const wasExcludeFilterAdded = addExcludeFilterValue(excludeFilterCandidate, { clearExcludeInputValue: false });
644
+ if (wasExcludeFilterAdded) {
645
+ handleClearMainFilter();
646
+ }
647
+ },
648
+ placeholder: filterPlaceholder,
649
+ type: "text",
650
+ value: resolvedFilterValue
651
+ }), /* @__PURE__ */ _jsxs("div", {
652
+ className: "absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5",
653
+ children: [resolvedFilterValue ? /* @__PURE__ */ _jsx(ControlButton, {
654
+ "aria-label": CLEAR_FILTER_ARIA_LABEL,
655
+ className: "active:-translate-y-0",
656
+ onClick: handleClearMainFilter,
657
+ size: "icon-xs",
658
+ type: "button",
659
+ variant: "ghost",
660
+ children: /* @__PURE__ */ _jsx(X, { "aria-hidden": "true" })
661
+ }) : null, showExcludeFilterToggle ? /* @__PURE__ */ _jsxs(ControlButton, {
662
+ "aria-expanded": isExcludeFilterInputVisible,
663
+ "aria-label": isExcludeFilterInputVisible ? HIDE_EXCLUDE_FILTERS_ARIA_LABEL : excludeFilterToggleLabel,
664
+ className: "relative",
665
+ onClick: () => {
666
+ setIsExcludeFilterInputVisible((previousState) => {
667
+ const nextState = !previousState;
668
+ if (!nextState) {
669
+ setExcludeFilterInputValue("");
670
+ setExcludeFilterErrorMessage(null);
671
+ }
672
+ return nextState;
673
+ });
674
+ },
675
+ size: "icon-xs",
676
+ type: "button",
677
+ variant: "ghost",
678
+ children: [/* @__PURE__ */ _jsx(Ellipsis, { "aria-hidden": "true" }), hasActiveExcludeFilterValues ? /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx("span", {
679
+ "aria-hidden": "true",
680
+ className: "absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive ring-2 ring-background"
681
+ }), /* @__PURE__ */ _jsx("span", {
682
+ className: "sr-only",
683
+ children: ACTIVE_EXCLUSIONS_SR_LABEL
684
+ })] }) : null]
685
+ }) : null]
686
+ })]
687
+ }),
688
+ showExcludeFilterToggle && isExcludeFilterInputVisible ? /* @__PURE__ */ _jsxs("div", {
689
+ className: "grid gap-1",
690
+ children: [/* @__PURE__ */ _jsxs("div", {
691
+ className: "flex items-center gap-1",
692
+ children: [/* @__PURE__ */ _jsx(Input, {
693
+ "aria-invalid": excludeFilterErrorMessage != null,
694
+ "aria-label": excludeFilterLabel,
695
+ onChange: (event) => {
696
+ setExcludeFilterInputValue(event.target.value);
697
+ if (excludeFilterErrorMessage != null) {
698
+ setExcludeFilterErrorMessage(null);
699
+ }
700
+ },
701
+ onKeyDown: (event) => {
702
+ if (event.key !== "Enter") {
703
+ return;
704
+ }
705
+ event.preventDefault();
706
+ addExcludeFilterValue(excludeFilterInputValue);
707
+ },
708
+ placeholder: excludeFilterPlaceholder,
709
+ type: "text",
710
+ value: excludeFilterInputValue
711
+ }), /* @__PURE__ */ _jsx(ControlButton, {
712
+ "aria-label": CLEAR_ALL_EXCLUSIONS_FROM_INPUT_ARIA_LABEL,
713
+ className: "shrink-0",
714
+ disabled: !hasActiveExcludeFilterValues,
715
+ onClick: handleClearAllExcludeFilters,
716
+ size: "icon-xs",
717
+ type: "button",
718
+ variant: "ghost",
719
+ children: /* @__PURE__ */ _jsx(Eraser, { "aria-hidden": "true" })
720
+ })]
721
+ }), excludeFilterErrorMessage ? /* @__PURE__ */ _jsx("p", {
722
+ "aria-live": "polite",
723
+ className: "text-sm text-destructive",
724
+ children: excludeFilterErrorMessage
725
+ }) : null]
726
+ }) : null,
727
+ hasPendingReverseFilter ? /* @__PURE__ */ _jsxs("div", {
728
+ "aria-live": "polite",
729
+ className: "rounded-md border border-border/80 bg-muted/40 px-3 py-2 text-muted-foreground",
730
+ role: "status",
731
+ children: [/* @__PURE__ */ _jsxs("div", {
732
+ className: "flex items-center gap-2 text-sm font-medium text-foreground/80",
733
+ children: [/* @__PURE__ */ _jsx(CircleAlert, {
734
+ "aria-hidden": "true",
735
+ className: "size-4"
736
+ }), /* @__PURE__ */ _jsx("span", { children: "Exclusión pendiente" })]
737
+ }), /* @__PURE__ */ _jsx("p", {
738
+ className: "mt-1 text-xs",
739
+ children: REVERSE_FILTER_PENDING_MESSAGE
740
+ })]
741
+ }) : null
742
+ ] }) : null,
743
+ showExcludeFilterToggle && resolvedExcludeFilterValues.length > 0 ? /* @__PURE__ */ _jsxs("div", {
744
+ className: "flex flex-wrap items-center gap-2",
745
+ children: [excludeFilterUniqueRowsCount != null ? /* @__PURE__ */ _jsxs(Badge, {
746
+ "aria-label": `Total de filas excluidas únicas: ${excludeFilterUniqueRowsCount}`,
747
+ className: "inline-flex h-6 items-center gap-1.5 px-2.5",
748
+ tone: "danger",
749
+ children: [
750
+ /* @__PURE__ */ _jsx("span", { children: `${EXCLUDED_ROWS_SUMMARY_LABEL}:` }),
751
+ /* @__PURE__ */ _jsx("span", {
752
+ className: "inline-flex min-w-5 items-center justify-center rounded-full bg-destructive/20 px-1.5 text-[10px] leading-4 font-medium",
753
+ children: excludeFilterUniqueRowsCount
754
+ }),
755
+ /* @__PURE__ */ _jsx("button", {
756
+ "aria-label": CLEAR_ALL_EXCLUSIONS_ARIA_LABEL,
757
+ className: "inline-flex size-4 items-center justify-center rounded-sm text-destructive transition-colors hover:bg-destructive/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40",
758
+ onClick: handleClearAllExcludeFilters,
759
+ type: "button",
760
+ children: /* @__PURE__ */ _jsx(X, {
761
+ "aria-hidden": "true",
762
+ className: "size-3"
763
+ })
764
+ })
765
+ ]
766
+ }) : null, resolvedExcludeFilterValues.map((excludeFilterValue) => {
767
+ const excludedRowsCount = excludeFilterRowsCountByValue?.[excludeFilterValue];
768
+ return /* @__PURE__ */ _jsxs(Badge, {
769
+ className: "inline-flex h-6 items-center gap-1.5 px-2.5",
770
+ tone: "danger",
771
+ children: [
772
+ /* @__PURE__ */ _jsx("span", { children: `− ${excludeFilterValue}` }),
773
+ excludedRowsCount != null ? /* @__PURE__ */ _jsx("span", {
774
+ "aria-label": `Filas excluidas por ${excludeFilterValue}: ${excludedRowsCount}`,
775
+ className: "inline-flex min-w-5 items-center justify-center rounded-full bg-destructive/20 px-1.5 text-[10px] leading-4 font-medium",
776
+ children: excludedRowsCount
777
+ }) : null,
778
+ /* @__PURE__ */ _jsx("button", {
779
+ "aria-label": `Quitar exclusión ${excludeFilterValue}`,
780
+ className: "inline-flex size-4 items-center justify-center rounded-sm text-destructive transition-colors hover:bg-destructive/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40",
781
+ onClick: () => removeExcludeFilterValue(excludeFilterValue),
782
+ type: "button",
783
+ children: /* @__PURE__ */ _jsx(X, {
784
+ "aria-hidden": "true",
785
+ className: "size-3"
786
+ })
787
+ })
788
+ ]
789
+ }, excludeFilterValue);
790
+ })]
791
+ }) : null,
792
+ sortingBadgeNode,
793
+ filterExtraContentNode
794
+ ]
795
+ }) : null, shouldShowToolbarActions ? /* @__PURE__ */ _jsxs("div", {
796
+ className: "ml-auto flex flex-wrap items-center gap-2",
797
+ children: [toolbarActions, shouldShowColumnVisibilityToggle ? /* @__PURE__ */ _jsxs(DropdownMenu, { children: [/* @__PURE__ */ _jsx(DropdownMenuTrigger, {
798
+ asChild: true,
799
+ children: /* @__PURE__ */ _jsxs(ControlButton, {
800
+ "aria-label": columnVisibilityButtonLabel,
801
+ className: "relative aria-expanded:*:data-[slot=data-table-columns-chevron]:rotate-180",
802
+ type: "button",
803
+ variant: "outline",
804
+ children: [
805
+ columnVisibilityButtonLabel,
806
+ /* @__PURE__ */ _jsx(MotionSlot, {
807
+ kind: "rotate",
808
+ children: /* @__PURE__ */ _jsx(ChevronDown, {
809
+ "aria-hidden": "true",
810
+ "data-slot": "data-table-columns-chevron"
811
+ })
812
+ }),
813
+ hasToolbarChanges ? /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx("span", {
814
+ "aria-hidden": "true",
815
+ className: "absolute -right-0.5 -top-0.5 size-2 rounded-full bg-destructive ring-2 ring-background"
816
+ }), /* @__PURE__ */ _jsx("span", {
817
+ className: "sr-only",
818
+ children: "Columnas modificadas"
819
+ })] }) : null
820
+ ]
821
+ })
822
+ }), /* @__PURE__ */ _jsxs(DropdownMenuContent, {
823
+ align: "end",
824
+ children: [
825
+ columnVisibilityMenuExtraItems?.length ? /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx(DropdownMenuItems, { items: columnVisibilityMenuExtraItems }), /* @__PURE__ */ _jsx(DropdownMenuSeparator, {})] }) : null,
826
+ /* @__PURE__ */ _jsx(DropdownMenuLabel, { children: columnVisibilityMenuLabel }),
827
+ /* @__PURE__ */ _jsx(DropdownMenuItem, {
828
+ disabled: areHideableColumnsAtDefaultVisibility,
829
+ onSelect: (event) => {
830
+ event.preventDefault();
831
+ hideableColumns.forEach((column) => {
832
+ column.toggleVisibility(getHideableColumnDefaultVisibility(column.id));
833
+ });
834
+ },
835
+ children: selectAllColumnsLabel
836
+ }),
837
+ /* @__PURE__ */ _jsx(DropdownMenuItem, {
838
+ disabled: !areSomeHideableColumnsVisible,
839
+ onSelect: (event) => {
840
+ event.preventDefault();
841
+ hideableColumns.forEach((column) => {
842
+ column.toggleVisibility(false);
843
+ });
844
+ },
845
+ children: deselectAllColumnsLabel
846
+ }),
847
+ /* @__PURE__ */ _jsx(DropdownMenuSeparator, {}),
848
+ hideableColumns.map((column) => {
849
+ const columnMeta = column.columnDef.meta;
850
+ const isColumnVisible = column.getIsVisible();
851
+ const label = columnMeta?.label ?? (typeof column.columnDef.header === "string" ? column.columnDef.header : column.id);
852
+ return /* @__PURE__ */ _jsxs(DropdownMenuCheckboxItem, {
853
+ checked: isColumnVisible,
854
+ onSelect: (event) => {
855
+ event.preventDefault();
856
+ },
857
+ onCheckedChange: (nextVisible) => {
858
+ column.toggleVisibility(Boolean(nextVisible));
859
+ },
860
+ children: [label, !isColumnVisible ? /* @__PURE__ */ _jsxs(_Fragment, { children: [/* @__PURE__ */ _jsx("span", {
861
+ "aria-hidden": "true",
862
+ className: "absolute right-2 top-1.5 size-2 rounded-full bg-destructive ring-2 ring-background"
863
+ }), /* @__PURE__ */ _jsx("span", {
864
+ className: "sr-only",
865
+ children: "Columna deseleccionada"
866
+ })] }) : null]
867
+ }, column.id);
868
+ })
869
+ ]
870
+ })] }) : null]
871
+ }) : null]
872
+ })]
873
+ }) : null, /* @__PURE__ */ _jsx("div", {
874
+ className: "relative w-full overflow-x-auto",
875
+ children: /* @__PURE__ */ _jsxs(Table, { children: [
876
+ /* @__PURE__ */ _jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ _jsx(TableRow, { children: headerGroup.headers.map((header) => {
877
+ const columnMeta = header.column.columnDef.meta;
878
+ return /* @__PURE__ */ _jsx(TableHead, {
879
+ className: columnMeta?.headerClassName,
880
+ onClick: columnMeta?.onHeaderClick,
881
+ children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())
882
+ }, header.id);
883
+ }) }, headerGroup.id)) }),
884
+ /* @__PURE__ */ _jsx(TableBody, { children: tableBodyContent }),
885
+ hasFooterContent ? /* @__PURE__ */ _jsx(TableFooter, { children: footerGroups.map((footerGroup) => /* @__PURE__ */ _jsx(TableRow, {
886
+ className: "hover:bg-transparent",
887
+ children: footerGroup.headers.map((footer) => /* @__PURE__ */ _jsx(TableCell, {
888
+ "data-label": footer.column.columnDef.meta?.label,
889
+ children: footer.isPlaceholder ? null : flexRender(footer.column.columnDef.footer, footer.getContext())
890
+ }, footer.id))
891
+ }, footerGroup.id)) }) : null
892
+ ] })
893
+ })]
894
+ });
895
+ }