@astra-kit/ui 0.0.0-stage → 1.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 +21 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +330 -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 +58 -0
  55. package/dist/components/dropdown-menu.js +135 -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 +17 -0
  177. package/dist/constants/dropdown-menu.js +48 -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,875 @@
1
+ /**
2
+ * Gramática pura (sin React) para una barra de filtro estilo GitHub Issues.
3
+ *
4
+ * La barra acepta una query de texto con tokens `clave:valor` más texto libre.
5
+ * Este módulo tokeniza la query, la parsea contra una configuración de
6
+ * qualifiers y la traduce a los tres canales de filtrado que ya consume la
7
+ * tabla: búsqueda fuzzy por descripción, exclusiones de descripción y filtros
8
+ * avanzados por columna (`DataTableColumnFilterValue`).
9
+ */
10
+ const DIACRITICS_PATTERN = /[̀-ͯ]/g;
11
+ const YEAR_MONTH_SLUG_PATTERN = /^(\d{4})-(0[1-9]|1[0-2])$/;
12
+ const RANGE_SEPARATOR = "..";
13
+ /** Slug de valor para los modos de presencia/rango mes-año sin cota. */
14
+ export const YEAR_MONTH_NO_VALUE_SLUG = "sin-fechas";
15
+ export const YEAR_MONTH_HAS_VALUE_SLUG = "con-fechas";
16
+ export const PRESENCE_TRUE_SLUG = "si";
17
+ export const PRESENCE_FALSE_SLUG = "no";
18
+ /**
19
+ * GitHub-style meta-keys to filter by presence/absence of ANY field:
20
+ * `tiene:<field>` (the field has a value) and `no:<field>` (the field is empty).
21
+ * The token value is the key of the target qualifier (e.g. `tiene:enviados`,
22
+ * `no:carpeta`). They unify presence across every kind into a single syntax,
23
+ * instead of a `<field>:si/no` per qualifier.
24
+ */
25
+ export const PRESENCE_HAS_META_KEY = "tiene";
26
+ export const PRESENCE_NO_META_KEY = "no";
27
+ /** Identificador de "sin carpeta asignada" en un qualifier de carpeta. */
28
+ export const UNASSIGNED_FOLDER_FILTER_VALUE = "__unassigned__";
29
+ /** Normaliza una clave o slug: sin acentos, minúsculas, recortado. */
30
+ export function normalizeFilterSlug(value) {
31
+ return value.normalize("NFD").replace(DIACRITICS_PATTERN, "").toLocaleLowerCase().trim();
32
+ }
33
+ function stripSurroundingQuotes(value) {
34
+ if (value.length >= 2) {
35
+ const first = value[0];
36
+ const last = value[value.length - 1];
37
+ if ((first === "\"" || first === "'") && first === last) {
38
+ return value.slice(1, -1);
39
+ }
40
+ }
41
+ // Comilla de apertura sin cierre (token a medio tipear).
42
+ if (value.startsWith("\"") || value.startsWith("'")) {
43
+ return value.slice(1);
44
+ }
45
+ return value;
46
+ }
47
+ /**
48
+ * Envuelve en comillas un valor que, sin comillas, se re-tokenizaría distinto:
49
+ * espacios, un `:` (se interpretaría como `clave:valor`) o un `-` inicial (se
50
+ * interpretaría como exclusión). Mantiene la query como fuente de verdad sin
51
+ * pérdida entre la barra unificada y los controles clásicos.
52
+ */
53
+ function quoteTokenIfNeeded(value) {
54
+ return /[\s:]/.test(value) || value.startsWith("-") ? `"${value}"` : value;
55
+ }
56
+ /**
57
+ * Serializa el texto libre de descripción citando, palabra por palabra, los
58
+ * segmentos que se parsearían como un token (por ejemplo `total:100`), de modo
59
+ * que un filtro de descripción no se transforme en un filtro avanzado al
60
+ * re-parsear la query.
61
+ */
62
+ function serializeDescriptionFilter(descriptionFilter) {
63
+ return descriptionFilter.split(/\s+/).filter((word) => word.length > 0).map(quoteTokenIfNeeded).join(" ");
64
+ }
65
+ /**
66
+ * Divide la query en tokens respetando comillas (`"..."` / `'...'` agrupan
67
+ * espacios). Conserva los índices de cada token para que el autocompletado
68
+ * pueda ubicar el token bajo el caret.
69
+ */
70
+ export function tokenizeFilterQuery(query) {
71
+ const tokens = [];
72
+ let index = 0;
73
+ const { length } = query;
74
+ while (index < length) {
75
+ if (/\s/.test(query[index])) {
76
+ index += 1;
77
+ continue;
78
+ }
79
+ const startIndex = index;
80
+ let quoteChar = null;
81
+ while (index < length) {
82
+ const character = query[index];
83
+ if (quoteChar) {
84
+ if (character === quoteChar) {
85
+ quoteChar = null;
86
+ }
87
+ index += 1;
88
+ continue;
89
+ }
90
+ if (character === "\"" || character === "'") {
91
+ quoteChar = character;
92
+ index += 1;
93
+ continue;
94
+ }
95
+ if (/\s/.test(character)) {
96
+ break;
97
+ }
98
+ index += 1;
99
+ }
100
+ const raw = query.slice(startIndex, index);
101
+ tokens.push(buildToken(raw, startIndex, index));
102
+ }
103
+ return tokens;
104
+ }
105
+ function findKeyColonIndex(text) {
106
+ for (let position = 0; position < text.length; position += 1) {
107
+ const character = text[position];
108
+ if (character === "\"" || character === "'") {
109
+ // Una comilla antes del `:` significa que no hay clave (es texto libre).
110
+ return -1;
111
+ }
112
+ if (character === ":") {
113
+ return position;
114
+ }
115
+ }
116
+ return -1;
117
+ }
118
+ function buildToken(raw, startIndex, endIndex) {
119
+ // A leading `-` marks exclusion, even when it is the only character: this way,
120
+ // when starting an exclusion from the bar (`-`), the token is already negated
121
+ // and autocompletion can offer the fields to exclude. A lone `-` is ignored
122
+ // when parsing (empty value) and filters nothing.
123
+ const negated = raw.startsWith("-");
124
+ const remainder = negated ? raw.slice(1) : raw;
125
+ const colonIndex = findKeyColonIndex(remainder);
126
+ if (colonIndex === -1) {
127
+ return {
128
+ endIndex,
129
+ hasColon: false,
130
+ negated,
131
+ raw,
132
+ rawKey: null,
133
+ startIndex,
134
+ value: stripSurroundingQuotes(remainder)
135
+ };
136
+ }
137
+ return {
138
+ endIndex,
139
+ hasColon: true,
140
+ negated,
141
+ raw,
142
+ rawKey: remainder.slice(0, colonIndex),
143
+ startIndex,
144
+ value: stripSurroundingQuotes(remainder.slice(colonIndex + 1))
145
+ };
146
+ }
147
+ function buildQualifierLookup(configs) {
148
+ const lookup = new Map();
149
+ for (const config of configs) {
150
+ if (config.kind === "text" || !config.key) {
151
+ continue;
152
+ }
153
+ lookup.set(normalizeFilterSlug(config.key), config);
154
+ for (const alias of config.aliases ?? []) {
155
+ lookup.set(normalizeFilterSlug(alias), config);
156
+ }
157
+ }
158
+ return lookup;
159
+ }
160
+ function parseNumber(value) {
161
+ const normalized = value.trim();
162
+ if (!normalized) {
163
+ return null;
164
+ }
165
+ const parsed = Number(normalized);
166
+ return Number.isFinite(parsed) ? parsed : null;
167
+ }
168
+ /**
169
+ * Parsea el valor de un qualifier numérico. Acepta comparadores (`>`, `>=`,
170
+ * `<`, `<=`, `=`), igualdad implícita (`N`) y rangos (`A..B`, `A..`, `..B`).
171
+ * `>` y `>=` son ambos inclusivos para preservar la semántica del matcher
172
+ * existente (`value < min → false`); `<` y `<=` mapean a `max`.
173
+ */
174
+ function parseNumberRangeValue(value) {
175
+ const normalized = value.trim();
176
+ if (!normalized) {
177
+ return null;
178
+ }
179
+ if (normalized.includes(RANGE_SEPARATOR)) {
180
+ const [rawMin, rawMax] = normalized.split(RANGE_SEPARATOR, 2);
181
+ const hasMin = rawMin.trim().length > 0;
182
+ const hasMax = rawMax.trim().length > 0;
183
+ if (!hasMin && !hasMax) {
184
+ return null;
185
+ }
186
+ const min = hasMin ? parseNumber(rawMin) : null;
187
+ const max = hasMax ? parseNumber(rawMax) : null;
188
+ if (hasMin && min == null || hasMax && max == null) {
189
+ return null;
190
+ }
191
+ // Un rango invertido (`100..50`) no puede matchear ninguna fila: es inválido.
192
+ if (min != null && max != null && min > max) {
193
+ return null;
194
+ }
195
+ return {
196
+ kind: "numberRange",
197
+ ...max != null ? { max } : {},
198
+ ...min != null ? { min } : {}
199
+ };
200
+ }
201
+ if (normalized.startsWith(">=") || normalized.startsWith(">")) {
202
+ const min = parseNumber(normalized.replace(/^>=?/, ""));
203
+ return min == null ? null : {
204
+ kind: "numberRange",
205
+ min
206
+ };
207
+ }
208
+ if (normalized.startsWith("<=") || normalized.startsWith("<")) {
209
+ const max = parseNumber(normalized.replace(/^<=?/, ""));
210
+ return max == null ? null : {
211
+ kind: "numberRange",
212
+ max
213
+ };
214
+ }
215
+ const exact = parseNumber(normalized.replace(/^=/, ""));
216
+ return exact == null ? null : {
217
+ kind: "numberRange",
218
+ max: exact,
219
+ min: exact
220
+ };
221
+ }
222
+ /** Convierte un slug `AAAA-MM` a su valor numérico comparable `AAAAMM`. */
223
+ export function parseYearMonthSlug(value) {
224
+ const match = YEAR_MONTH_SLUG_PATTERN.exec(value.trim());
225
+ if (!match) {
226
+ return null;
227
+ }
228
+ const [, year, month] = match;
229
+ return Number(`${year}${month}`);
230
+ }
231
+ /** Formatea un valor numérico `AAAAMM` de vuelta a su slug `AAAA-MM`. */
232
+ export function formatYearMonthSlug(value) {
233
+ const year = Math.floor(value / 100);
234
+ const month = value % 100;
235
+ return `${year}-${String(month).padStart(2, "0")}`;
236
+ }
237
+ function parseYearMonthRangeValue(value) {
238
+ const normalized = normalizeFilterSlug(value);
239
+ if (!normalized) {
240
+ return null;
241
+ }
242
+ if (normalized === YEAR_MONTH_NO_VALUE_SLUG) {
243
+ return {
244
+ kind: "yearMonthRange",
245
+ mode: "noValue"
246
+ };
247
+ }
248
+ if (normalized === YEAR_MONTH_HAS_VALUE_SLUG) {
249
+ return {
250
+ kind: "yearMonthRange",
251
+ mode: "hasValue"
252
+ };
253
+ }
254
+ // Comparators `>=`/`>` (from) and `<=`/`<` (to), analogous to the numeric
255
+ // ones, to bound dates with a single endpoint without writing an `A..` range.
256
+ if (normalized.startsWith(">=") || normalized.startsWith(">")) {
257
+ const min = parseYearMonthSlug(normalized.replace(/^>=?/, ""));
258
+ return min == null ? null : {
259
+ kind: "yearMonthRange",
260
+ min,
261
+ mode: "from"
262
+ };
263
+ }
264
+ if (normalized.startsWith("<=") || normalized.startsWith("<")) {
265
+ const max = parseYearMonthSlug(normalized.replace(/^<=?/, ""));
266
+ return max == null ? null : {
267
+ kind: "yearMonthRange",
268
+ max,
269
+ mode: "to"
270
+ };
271
+ }
272
+ if (normalized.includes(RANGE_SEPARATOR)) {
273
+ const [rawFrom, rawTo] = normalized.split(RANGE_SEPARATOR, 2);
274
+ const hasFrom = rawFrom.trim().length > 0;
275
+ const hasTo = rawTo.trim().length > 0;
276
+ if (!hasFrom && !hasTo) {
277
+ return null;
278
+ }
279
+ const min = hasFrom ? parseYearMonthSlug(rawFrom) : null;
280
+ const max = hasTo ? parseYearMonthSlug(rawTo) : null;
281
+ if (hasFrom && min == null || hasTo && max == null) {
282
+ return null;
283
+ }
284
+ if (min != null && max != null) {
285
+ // Rango invertido (`2026-12..2026-06`): imposible de matchear, inválido.
286
+ if (min > max) {
287
+ return null;
288
+ }
289
+ return {
290
+ kind: "yearMonthRange",
291
+ max,
292
+ min,
293
+ mode: "range"
294
+ };
295
+ }
296
+ if (min != null) {
297
+ return {
298
+ kind: "yearMonthRange",
299
+ min,
300
+ mode: "from"
301
+ };
302
+ }
303
+ return {
304
+ kind: "yearMonthRange",
305
+ max,
306
+ mode: "to"
307
+ };
308
+ }
309
+ const exact = parseYearMonthSlug(normalized);
310
+ if (exact == null) {
311
+ return null;
312
+ }
313
+ return {
314
+ kind: "yearMonthRange",
315
+ max: exact,
316
+ min: exact,
317
+ mode: "range"
318
+ };
319
+ }
320
+ function buildColumnFilterValue(config, value) {
321
+ if (config.kind === "numberRange") {
322
+ return parseNumberRangeValue(value);
323
+ }
324
+ if (config.kind === "yearMonthRange") {
325
+ return parseYearMonthRangeValue(value);
326
+ }
327
+ if (config.kind === "enum") {
328
+ // A leading `@` mention-style prefix is tolerated (e.g. `prestamista:@vero`).
329
+ const normalized = normalizeFilterSlug(value).replace(/^@/, "");
330
+ const option = (config.options ?? []).find((candidate) => normalizeFilterSlug(candidate.slug) === normalized);
331
+ return option ? {
332
+ kind: "enum",
333
+ value: option.value
334
+ } : null;
335
+ }
336
+ if (config.kind === "presence") {
337
+ const normalized = normalizeFilterSlug(value);
338
+ if (normalized === PRESENCE_TRUE_SLUG) {
339
+ return {
340
+ kind: "presence",
341
+ value: "hasValue"
342
+ };
343
+ }
344
+ if (normalized === PRESENCE_FALSE_SLUG) {
345
+ return {
346
+ kind: "presence",
347
+ value: "noValue"
348
+ };
349
+ }
350
+ return null;
351
+ }
352
+ return null;
353
+ }
354
+ /**
355
+ * Parses the value of a text qualifier (`textMatch`) with glob syntax. Accepts
356
+ * presence (`si`/`no`) and `*` wildcards: `text*` starts with, `*text` ends
357
+ * with, `*text*` contains, and `text` (no wildcard) is exact equality. The text
358
+ * is normalized (no accents, lowercase) to compare case/accent-insensitively.
359
+ */
360
+ export function parseTextMatchValue(value) {
361
+ const trimmed = value.trim();
362
+ if (!trimmed) {
363
+ return null;
364
+ }
365
+ const normalizedSlug = normalizeFilterSlug(trimmed);
366
+ if (normalizedSlug === PRESENCE_TRUE_SLUG) {
367
+ return {
368
+ kind: "textMatch",
369
+ op: "has"
370
+ };
371
+ }
372
+ if (normalizedSlug === PRESENCE_FALSE_SLUG) {
373
+ return {
374
+ kind: "textMatch",
375
+ op: "notHas"
376
+ };
377
+ }
378
+ const startsWithStar = trimmed.startsWith("*");
379
+ const endsWithStar = trimmed.endsWith("*");
380
+ if (startsWithStar && endsWithStar) {
381
+ const text = normalizeFilterSlug(trimmed.slice(1, -1));
382
+ return text ? {
383
+ kind: "textMatch",
384
+ op: "contains",
385
+ text
386
+ } : null;
387
+ }
388
+ if (endsWithStar) {
389
+ const text = normalizeFilterSlug(trimmed.slice(0, -1));
390
+ return text ? {
391
+ kind: "textMatch",
392
+ op: "startsWith",
393
+ text
394
+ } : null;
395
+ }
396
+ if (startsWithStar) {
397
+ const text = normalizeFilterSlug(trimmed.slice(1));
398
+ return text ? {
399
+ kind: "textMatch",
400
+ op: "endsWith",
401
+ text
402
+ } : null;
403
+ }
404
+ const text = normalizeFilterSlug(trimmed);
405
+ return text ? {
406
+ kind: "textMatch",
407
+ op: "equals",
408
+ text
409
+ } : null;
410
+ }
411
+ /**
412
+ * Parsea el valor de un qualifier de carpeta (`folder`) contra sus opciones
413
+ * dinámicas (slug→folderId). `sin-carpeta` mapea al sentinel de "sin asignar".
414
+ */
415
+ export function parseFolderValue(config, value) {
416
+ // Se tolera un `@` inicial al estilo "mención" (p. ej. `carpeta:@hogar`),
417
+ // igual que el parser de enum, para que completar el token tipeando/Enter
418
+ // coincida con la sugerencia que filtra strip-eando ese `@`.
419
+ const normalized = normalizeFilterSlug(value).replace(/^@/, "");
420
+ if (!normalized) {
421
+ return null;
422
+ }
423
+ const option = (config.options ?? []).find((candidate) => normalizeFilterSlug(candidate.slug) === normalized);
424
+ return option ? {
425
+ kind: "folder",
426
+ folderId: option.value
427
+ } : null;
428
+ }
429
+ /** Construye el valor estructurado de un qualifier según su kind. */
430
+ function buildAppliedFilterValue(config, value) {
431
+ if (config.kind === "textMatch") {
432
+ return parseTextMatchValue(value);
433
+ }
434
+ if (config.kind === "folder") {
435
+ return parseFolderValue(config, value);
436
+ }
437
+ return buildColumnFilterValue(config, value);
438
+ }
439
+ /** Discrimina un valor que vive en el path de columnas de TanStack. */
440
+ function isColumnFilterValue(value) {
441
+ return value.kind !== "textMatch" && value.kind !== "folder";
442
+ }
443
+ function mergeColumnFilterValue(existing, next) {
444
+ if (existing && existing.kind === "numberRange" && next.kind === "numberRange") {
445
+ // Qualifiers repetidos se ANDean: nos quedamos con la cota más ajustada
446
+ // (mayor mínimo, menor máximo) para que el resultado no dependa del orden.
447
+ const mergedMin = existing.min != null && next.min != null ? Math.max(existing.min, next.min) : next.min ?? existing.min;
448
+ const mergedMax = existing.max != null && next.max != null ? Math.min(existing.max, next.max) : next.max ?? existing.max;
449
+ return {
450
+ kind: "numberRange",
451
+ ...mergedMax != null ? { max: mergedMax } : {},
452
+ ...mergedMin != null ? { min: mergedMin } : {}
453
+ };
454
+ }
455
+ if (existing && existing.kind === "yearMonthRange" && next.kind === "yearMonthRange") {
456
+ const isBounded = (mode) => mode === "range" || mode === "from" || mode === "to";
457
+ // Los modos de presencia (con/sin fechas) no combinan con cotas: gana el
458
+ // último. Para range/from/to se intersectan las cotas (mayor desde, menor
459
+ // hasta), igual que `numberRange`, en vez de descartar la primera cota.
460
+ if (!isBounded(existing.mode) || !isBounded(next.mode)) {
461
+ return next;
462
+ }
463
+ const mergedMin = existing.min != null && next.min != null ? Math.max(existing.min, next.min) : next.min ?? existing.min;
464
+ const mergedMax = existing.max != null && next.max != null ? Math.min(existing.max, next.max) : next.max ?? existing.max;
465
+ const mode = mergedMin != null && mergedMax != null ? "range" : mergedMin != null ? "from" : "to";
466
+ return {
467
+ kind: "yearMonthRange",
468
+ mode,
469
+ ...mergedMax != null ? { max: mergedMax } : {},
470
+ ...mergedMin != null ? { min: mergedMin } : {}
471
+ };
472
+ }
473
+ return next;
474
+ }
475
+ /**
476
+ * Detecta un rango imposible (`min > max`), por ejemplo tras combinar
477
+ * `total:>100 total:<50`. El matcher rechazaría toda fila, así que se reporta el
478
+ * token como inválido en vez de aplicar un filtro vacío.
479
+ */
480
+ function isImpossibleRange(value) {
481
+ if (value.kind === "numberRange") {
482
+ return value.min != null && value.max != null && value.min > value.max;
483
+ }
484
+ if (value.kind === "yearMonthRange") {
485
+ return value.mode === "range" && value.min != null && value.max != null && value.min > value.max;
486
+ }
487
+ return false;
488
+ }
489
+ /** Quita duplicados de exclusiones comparando por su forma normalizada. */
490
+ function dedupeByNormalizedSlug(values) {
491
+ const seenKeys = new Set();
492
+ const dedupedValues = [];
493
+ for (const value of values) {
494
+ const normalizedKey = normalizeFilterSlug(value);
495
+ if (!normalizedKey || seenKeys.has(normalizedKey)) {
496
+ continue;
497
+ }
498
+ seenKeys.add(normalizedKey);
499
+ dedupedValues.push(value);
500
+ }
501
+ return dedupedValues;
502
+ }
503
+ /** Parsea la query completa contra la configuración de qualifiers. */
504
+ export function parseFilterQuery(query, configs) {
505
+ const tokens = tokenizeFilterQuery(query);
506
+ const lookup = buildQualifierLookup(configs);
507
+ const descriptionParts = [];
508
+ const excludedDescriptionFilters = [];
509
+ const advancedFiltersByColumn = {};
510
+ const appliedFilters = [];
511
+ // Índice en `appliedFilters` del último filtro de rango (no negado) por clave,
512
+ // para mergear y validar rangos repetidos de forma uniforme tengan o no columna.
513
+ const rangeAppliedIndexByKey = new Map();
514
+ const invalidTokens = [];
515
+ for (const token of tokens) {
516
+ if (token.hasColon && token.rawKey) {
517
+ const normalizedKey = normalizeFilterSlug(token.rawKey);
518
+ // Presence meta-keys (`tiene:<field>` / `no:<field>`): the value is the
519
+ // key of the target field. They produce a presence filter that the domain
520
+ // predicate evaluates per field; they do not go through the column path.
521
+ if (normalizedKey === PRESENCE_HAS_META_KEY || normalizedKey === PRESENCE_NO_META_KEY) {
522
+ const fieldKey = normalizeFilterSlug(token.value ?? "");
523
+ if (!fieldKey) {
524
+ continue;
525
+ }
526
+ const fieldConfig = lookup.get(fieldKey);
527
+ if (!fieldConfig || fieldConfig.supportsPresence === false) {
528
+ invalidTokens.push({
529
+ raw: token.raw,
530
+ reason: "invalidValue"
531
+ });
532
+ continue;
533
+ }
534
+ // Token negation (`-tiene:x`) flips the meaning, leaving a single
535
+ // canonical presence filter (without residual negation).
536
+ const hasValue = normalizedKey === PRESENCE_HAS_META_KEY !== token.negated;
537
+ appliedFilters.push({
538
+ key: fieldConfig.key,
539
+ negated: false,
540
+ value: {
541
+ kind: "presence",
542
+ value: hasValue ? "hasValue" : "noValue"
543
+ }
544
+ });
545
+ continue;
546
+ }
547
+ const config = lookup.get(normalizedKey);
548
+ if (!config) {
549
+ invalidTokens.push({
550
+ raw: token.raw,
551
+ reason: "unknownKey"
552
+ });
553
+ const freeText = token.negated ? token.raw.slice(1) : token.raw;
554
+ if (token.negated) {
555
+ excludedDescriptionFilters.push(freeText.trim());
556
+ } else if (freeText.trim()) {
557
+ descriptionParts.push(freeText.trim());
558
+ }
559
+ continue;
560
+ }
561
+ const value = token.value ?? "";
562
+ const filterValue = buildAppliedFilterValue(config, value);
563
+ if (!filterValue) {
564
+ if (value.trim()) {
565
+ invalidTokens.push({
566
+ raw: token.raw,
567
+ reason: "invalidValue"
568
+ });
569
+ }
570
+ continue;
571
+ }
572
+ if (isColumnFilterValue(filterValue)) {
573
+ const isRange = filterValue.kind === "numberRange" || filterValue.kind === "yearMonthRange";
574
+ // Los rangos repetidos de una misma clave (con o sin columna) se mergean
575
+ // y validan igual: una combinación imposible (p. ej.
576
+ // `subtotal:>1000 subtotal:<50`) se reporta inválida y conserva la cota
577
+ // previa, en vez de filtrar todas las filas en silencio.
578
+ if (!token.negated && isRange) {
579
+ const existingIndex = rangeAppliedIndexByKey.get(config.key);
580
+ if (existingIndex != null) {
581
+ const existingValue = appliedFilters[existingIndex].value;
582
+ const mergedValue = mergeColumnFilterValue(existingValue, filterValue);
583
+ if (isImpossibleRange(mergedValue)) {
584
+ invalidTokens.push({
585
+ raw: token.raw,
586
+ reason: "invalidValue"
587
+ });
588
+ continue;
589
+ }
590
+ appliedFilters[existingIndex] = {
591
+ ...appliedFilters[existingIndex],
592
+ value: mergedValue
593
+ };
594
+ if (config.columnId) {
595
+ advancedFiltersByColumn[config.columnId] = mergedValue;
596
+ }
597
+ continue;
598
+ }
599
+ appliedFilters.push({
600
+ key: config.key,
601
+ negated: false,
602
+ value: filterValue
603
+ });
604
+ rangeAppliedIndexByKey.set(config.key, appliedFilters.length - 1);
605
+ if (config.columnId) {
606
+ advancedFiltersByColumn[config.columnId] = filterValue;
607
+ }
608
+ continue;
609
+ }
610
+ // enum/presence (not range) or negated: added without merging. The
611
+ // non-negated ones with a column are projected to the classic TanStack
612
+ // path.
613
+ appliedFilters.push({
614
+ key: config.key,
615
+ negated: token.negated,
616
+ value: filterValue
617
+ });
618
+ // Presence (`<field>:si/no`) is unified with the meta-keys and resolved
619
+ // by the domain predicate, so it is never projected to a column.
620
+ if (!token.negated && config.columnId && filterValue.kind !== "presence") {
621
+ advancedFiltersByColumn[config.columnId] = mergeColumnFilterValue(advancedFiltersByColumn[config.columnId], filterValue);
622
+ }
623
+ continue;
624
+ }
625
+ // textMatch / folder: sin columna y sin merge de rango.
626
+ appliedFilters.push({
627
+ key: config.key,
628
+ negated: token.negated,
629
+ value: filterValue
630
+ });
631
+ continue;
632
+ }
633
+ const text = (token.value ?? "").trim();
634
+ if (!text) {
635
+ continue;
636
+ }
637
+ if (token.negated) {
638
+ excludedDescriptionFilters.push(text);
639
+ } else {
640
+ descriptionParts.push(text);
641
+ }
642
+ }
643
+ return {
644
+ advancedFiltersByColumn,
645
+ appliedFilters,
646
+ descriptionFilter: descriptionParts.join(" "),
647
+ excludedDescriptionFilters: dedupeByNormalizedSlug(excludedDescriptionFilters),
648
+ invalidTokens
649
+ };
650
+ }
651
+ function serializeNumberRange(key, value) {
652
+ const { min, max } = value;
653
+ if (min != null && max != null) {
654
+ return min === max ? `${key}:=${min}` : `${key}:${min}..${max}`;
655
+ }
656
+ if (min != null) {
657
+ return `${key}:>=${min}`;
658
+ }
659
+ if (max != null) {
660
+ return `${key}:<=${max}`;
661
+ }
662
+ return null;
663
+ }
664
+ function serializeYearMonthRange(key, value) {
665
+ if (value.mode === "noValue") {
666
+ return `${key}:${YEAR_MONTH_NO_VALUE_SLUG}`;
667
+ }
668
+ if (value.mode === "hasValue") {
669
+ return `${key}:${YEAR_MONTH_HAS_VALUE_SLUG}`;
670
+ }
671
+ if (value.mode === "from" && value.min != null) {
672
+ return `${key}:${formatYearMonthSlug(value.min)}..`;
673
+ }
674
+ if (value.mode === "to" && value.max != null) {
675
+ return `${key}:..${formatYearMonthSlug(value.max)}`;
676
+ }
677
+ if (value.mode === "range" && value.min != null && value.max != null) {
678
+ if (value.min === value.max) {
679
+ return `${key}:${formatYearMonthSlug(value.min)}`;
680
+ }
681
+ return `${key}:${formatYearMonthSlug(value.min)}..${formatYearMonthSlug(value.max)}`;
682
+ }
683
+ return null;
684
+ }
685
+ function serializeTextMatch(key, value) {
686
+ if (value.op === "has") {
687
+ return `${key}:${PRESENCE_TRUE_SLUG}`;
688
+ }
689
+ if (value.op === "notHas") {
690
+ return `${key}:${PRESENCE_FALSE_SLUG}`;
691
+ }
692
+ const text = value.text ?? "";
693
+ const decorated = value.op === "startsWith" ? `${text}*` : value.op === "endsWith" ? `*${text}` : value.op === "contains" ? `*${text}*` : text;
694
+ return `${key}:${quoteTokenIfNeeded(decorated)}`;
695
+ }
696
+ function serializeFolderValue(config, value) {
697
+ const option = (config.options ?? []).find((candidate) => candidate.value === value.folderId);
698
+ return option ? `${config.key}:${option.slug}` : null;
699
+ }
700
+ function serializeAppliedFilterValue(config, value) {
701
+ if (value.kind === "numberRange") {
702
+ return serializeNumberRange(config.key, value);
703
+ }
704
+ if (value.kind === "yearMonthRange") {
705
+ return serializeYearMonthRange(config.key, value);
706
+ }
707
+ if (value.kind === "enum") {
708
+ const option = (config.options ?? []).find((candidate) => candidate.value === value.value);
709
+ return option ? `${config.key}:${option.slug}` : null;
710
+ }
711
+ if (value.kind === "textMatch") {
712
+ return serializeTextMatch(config.key, value);
713
+ }
714
+ if (value.kind === "folder") {
715
+ return serializeFolderValue(config, value);
716
+ }
717
+ // Presence: canonical form with meta-keys (`tiene:<field>` / `no:<field>`).
718
+ const metaKey = value.value === "hasValue" ? PRESENCE_HAS_META_KEY : PRESENCE_NO_META_KEY;
719
+ return `${metaKey}:${config.key}`;
720
+ }
721
+ /**
722
+ * `true` when the filter is already emitted by the classic serialization from
723
+ * `advancedFiltersByColumn` (not negated, with a column, and of a column kind),
724
+ * so it is not serialized twice while also iterating `appliedFilters`.
725
+ *
726
+ * `textMatch` and `folder` never live in the column map, so they are never
727
+ * covered. Presence is special: parsing keeps it only in `appliedFilters`, but a
728
+ * consumer (the advanced dialog) may also place it in the column map. It counts
729
+ * as covered only when that same column actually holds a value, so a presence
730
+ * that lives solely in `appliedFilters` is still serialized once and one that
731
+ * lives in both is not duplicated.
732
+ */
733
+ function isCoveredByColumnSerialization(appliedFilter, config, advancedFiltersByColumn) {
734
+ if (appliedFilter.negated || config.columnId == null || appliedFilter.value.kind === "textMatch" || appliedFilter.value.kind === "folder") {
735
+ return false;
736
+ }
737
+ if (appliedFilter.value.kind === "presence") {
738
+ return advancedFiltersByColumn[config.columnId] != null;
739
+ }
740
+ return true;
741
+ }
742
+ /** Reconstruye una query canónica a partir de un resultado parseado. */
743
+ export function serializeFilterQuery(parsed, configs) {
744
+ const segments = [];
745
+ const configsByKey = new Map(configs.map((config) => [normalizeFilterSlug(config.key), config]));
746
+ const descriptionFilter = parsed.descriptionFilter.trim();
747
+ if (descriptionFilter) {
748
+ segments.push(serializeDescriptionFilter(descriptionFilter));
749
+ }
750
+ // Classic per-column filters (config order), for compatibility with consumers
751
+ // that build the parsed result only from `advancedFiltersByColumn`.
752
+ for (const config of configs) {
753
+ if (!config.columnId) {
754
+ continue;
755
+ }
756
+ const value = parsed.advancedFiltersByColumn[config.columnId];
757
+ if (!value) {
758
+ continue;
759
+ }
760
+ const serialized = serializeAppliedFilterValue(config, value);
761
+ if (serialized) {
762
+ segments.push(serialized);
763
+ }
764
+ }
765
+ // Extras the column path does not cover: negated, textMatch, folder, and
766
+ // qualifiers without a column.
767
+ for (const appliedFilter of parsed.appliedFilters ?? []) {
768
+ const config = configsByKey.get(normalizeFilterSlug(appliedFilter.key));
769
+ if (!config || isCoveredByColumnSerialization(appliedFilter, config, parsed.advancedFiltersByColumn)) {
770
+ continue;
771
+ }
772
+ const serialized = serializeAppliedFilterValue(config, appliedFilter.value);
773
+ if (serialized) {
774
+ segments.push(appliedFilter.negated ? `-${serialized}` : serialized);
775
+ }
776
+ }
777
+ for (const excluded of parsed.excludedDescriptionFilters) {
778
+ const trimmed = excluded.trim();
779
+ if (trimmed) {
780
+ segments.push(`-${quoteTokenIfNeeded(trimmed)}`);
781
+ }
782
+ }
783
+ return segments.join(" ");
784
+ }
785
+ /**
786
+ * Returns the character ranges of the VALUES (whatever comes after the `:`) that
787
+ * are valid for their qualifier, to highlight them in the bar. An invalid,
788
+ * unknown, or incomplete value produces no range (no highlight). Covers the
789
+ * presence meta-keys (`tiene:`/`no:` with an existing field) and the normal
790
+ * qualifiers (value parseable per its kind: enum, range, text, etc.).
791
+ */
792
+ export function getValueHighlightRanges(query, configs) {
793
+ const tokens = tokenizeFilterQuery(query);
794
+ const lookup = buildQualifierLookup(configs);
795
+ const ranges = [];
796
+ for (const token of tokens) {
797
+ if (!token.hasColon || !token.rawKey) {
798
+ continue;
799
+ }
800
+ const negationOffset = token.negated ? 1 : 0;
801
+ const colonIndex = findKeyColonIndex(token.raw.slice(negationOffset));
802
+ if (colonIndex === -1) {
803
+ continue;
804
+ }
805
+ const valueStart = token.startIndex + negationOffset + colonIndex + 1;
806
+ // No value yet (`key:`): nothing to highlight.
807
+ if (valueStart >= token.endIndex) {
808
+ continue;
809
+ }
810
+ const normalizedKey = normalizeFilterSlug(token.rawKey);
811
+ const rawValue = token.value ?? "";
812
+ let isValid = false;
813
+ if (normalizedKey === PRESENCE_HAS_META_KEY || normalizedKey === PRESENCE_NO_META_KEY) {
814
+ // A meta-key's value is the key of an existing field.
815
+ isValid = lookup.has(normalizeFilterSlug(rawValue));
816
+ } else {
817
+ const config = lookup.get(normalizedKey);
818
+ isValid = config != null && buildAppliedFilterValue(config, rawValue) != null;
819
+ }
820
+ if (isValid) {
821
+ ranges.push({
822
+ end: token.endIndex,
823
+ start: valueStart
824
+ });
825
+ }
826
+ }
827
+ return ranges;
828
+ }
829
+ /**
830
+ * Describe el token bajo el caret para alimentar el autocompletado. Decide si
831
+ * el usuario está tipeando una clave (sin `:`) o el valor de una clave.
832
+ */
833
+ export function getActiveFilterToken(query, caretIndex) {
834
+ const tokens = tokenizeFilterQuery(query);
835
+ const activeToken = tokens.find((token) => caretIndex >= token.startIndex && caretIndex <= token.endIndex);
836
+ if (!activeToken) {
837
+ return {
838
+ keyPart: "",
839
+ mode: "key",
840
+ negated: false,
841
+ replaceEnd: caretIndex,
842
+ replaceStart: caretIndex,
843
+ resolvedKey: null,
844
+ tokenStart: caretIndex,
845
+ valuePart: ""
846
+ };
847
+ }
848
+ const negationOffset = activeToken.negated ? 1 : 0;
849
+ const contentStart = activeToken.startIndex + negationOffset;
850
+ const remainder = activeToken.raw.slice(negationOffset);
851
+ const colonIndex = findKeyColonIndex(remainder);
852
+ if (colonIndex === -1) {
853
+ return {
854
+ keyPart: remainder,
855
+ mode: "key",
856
+ negated: activeToken.negated,
857
+ replaceEnd: activeToken.endIndex,
858
+ replaceStart: activeToken.startIndex,
859
+ resolvedKey: null,
860
+ tokenStart: activeToken.startIndex,
861
+ valuePart: ""
862
+ };
863
+ }
864
+ const valueStart = contentStart + colonIndex + 1;
865
+ return {
866
+ keyPart: remainder.slice(0, colonIndex),
867
+ mode: "value",
868
+ negated: activeToken.negated,
869
+ replaceEnd: activeToken.endIndex,
870
+ replaceStart: valueStart,
871
+ resolvedKey: remainder.slice(0, colonIndex),
872
+ tokenStart: activeToken.startIndex,
873
+ valuePart: remainder.slice(colonIndex + 1)
874
+ };
875
+ }