@sincpro/web-ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -2
  3. package/dist/components/assist/AssistMark.d.ts +64 -0
  4. package/dist/components/assist/AssistMark.js +108 -0
  5. package/dist/components/assist/AssistPending.d.ts +24 -0
  6. package/dist/components/assist/AssistPending.js +26 -0
  7. package/dist/components/assist/AssistSuggestion.d.ts +42 -0
  8. package/dist/components/assist/AssistSuggestion.js +55 -0
  9. package/dist/components/assist/ChangeReview.d.ts +56 -0
  10. package/dist/components/assist/ChangeReview.js +121 -0
  11. package/dist/components/assist/assist.d.ts +77 -0
  12. package/dist/components/assist/assist.js +87 -0
  13. package/dist/components/assist/index.d.ts +5 -0
  14. package/dist/components/assist/index.js +7 -0
  15. package/dist/components/chatter/Chatter.d.ts +161 -0
  16. package/dist/components/chatter/Chatter.js +232 -0
  17. package/dist/components/chatter/Messages.d.ts +66 -0
  18. package/dist/components/chatter/Messages.js +43 -0
  19. package/dist/components/chatter/Timeline.d.ts +21 -0
  20. package/dist/components/chatter/Timeline.js +30 -0
  21. package/dist/components/chatter/index.d.ts +3 -0
  22. package/dist/components/chatter/index.js +4 -0
  23. package/dist/components/form/Form.d.ts +52 -0
  24. package/dist/components/form/Form.js +49 -0
  25. package/dist/components/form/Sheet.d.ts +43 -0
  26. package/dist/components/form/Sheet.js +83 -0
  27. package/dist/components/form/index.d.ts +2 -0
  28. package/dist/components/form/index.js +4 -0
  29. package/dist/components/frame/Chart.d.ts +13 -0
  30. package/dist/components/frame/Chart.js +24 -0
  31. package/dist/components/frame/FrameView.d.ts +33 -0
  32. package/dist/components/frame/FrameView.js +125 -0
  33. package/dist/components/frame/index.d.ts +3 -0
  34. package/dist/components/frame/index.js +3 -0
  35. package/dist/components/frame/model.d.ts +43 -0
  36. package/dist/components/frame/model.js +101 -0
  37. package/dist/components/index.d.ts +10 -0
  38. package/dist/components/index.js +11 -0
  39. package/dist/components/lines/LineItems.d.ts +74 -0
  40. package/dist/components/lines/LineItems.js +229 -0
  41. package/dist/components/lines/index.d.ts +1 -0
  42. package/dist/components/lines/index.js +2 -0
  43. package/dist/components/lines/lines.d.ts +23 -0
  44. package/dist/components/lines/lines.js +26 -0
  45. package/dist/components/listing/ColumnsMenu.d.ts +20 -0
  46. package/dist/components/listing/ColumnsMenu.js +46 -0
  47. package/dist/components/listing/GroupingLevels.d.ts +35 -0
  48. package/dist/components/listing/GroupingLevels.js +45 -0
  49. package/dist/components/listing/Listing.d.ts +27 -0
  50. package/dist/components/listing/Listing.js +86 -0
  51. package/dist/components/listing/ListingBar.d.ts +73 -0
  52. package/dist/components/listing/ListingBar.js +105 -0
  53. package/dist/components/listing/Pagination.d.ts +36 -0
  54. package/dist/components/listing/Pagination.js +69 -0
  55. package/dist/components/listing/RecordCard.d.ts +33 -0
  56. package/dist/components/listing/RecordCard.js +22 -0
  57. package/dist/components/listing/RecordLink.d.ts +7 -0
  58. package/dist/components/listing/RecordLink.js +22 -0
  59. package/dist/components/listing/Row.d.ts +106 -0
  60. package/dist/components/listing/Row.js +122 -0
  61. package/dist/components/listing/SortMenu.d.ts +62 -0
  62. package/dist/components/listing/SortMenu.js +105 -0
  63. package/dist/components/listing/ViewSwitcher.d.ts +24 -0
  64. package/dist/components/listing/ViewSwitcher.js +16 -0
  65. package/dist/components/listing/columns.d.ts +47 -0
  66. package/dist/components/listing/columns.js +72 -0
  67. package/dist/components/listing/grouping.d.ts +35 -0
  68. package/dist/components/listing/grouping.js +47 -0
  69. package/dist/components/listing/index.d.ts +17 -0
  70. package/dist/components/listing/index.js +20 -0
  71. package/dist/components/listing/modelColumns.d.ts +50 -0
  72. package/dist/components/listing/modelColumns.js +103 -0
  73. package/dist/components/listing/pages.d.ts +78 -0
  74. package/dist/components/listing/pages.js +135 -0
  75. package/dist/components/listing/selection.d.ts +21 -0
  76. package/dist/components/listing/selection.js +43 -0
  77. package/dist/components/listing/set.d.ts +48 -0
  78. package/dist/components/listing/set.js +24 -0
  79. package/dist/components/listing/tree.d.ts +28 -0
  80. package/dist/components/listing/tree.js +23 -0
  81. package/dist/components/navigation/CommandPalette.d.ts +40 -0
  82. package/dist/components/navigation/CommandPalette.js +114 -0
  83. package/dist/components/navigation/NavigationTree.d.ts +31 -0
  84. package/dist/components/navigation/NavigationTree.js +79 -0
  85. package/dist/components/navigation/commands.d.ts +30 -0
  86. package/dist/components/navigation/commands.js +30 -0
  87. package/dist/components/navigation/index.d.ts +3 -0
  88. package/dist/components/navigation/index.js +4 -0
  89. package/dist/components/record/Breadcrumb.d.ts +42 -0
  90. package/dist/components/record/Breadcrumb.js +98 -0
  91. package/dist/components/record/DirtyActions.d.ts +26 -0
  92. package/dist/components/record/DirtyActions.js +22 -0
  93. package/dist/components/record/Pager.d.ts +18 -0
  94. package/dist/components/record/Pager.js +33 -0
  95. package/dist/components/record/RecordHeader.d.ts +71 -0
  96. package/dist/components/record/RecordHeader.js +72 -0
  97. package/dist/components/record/RecordSkeleton.d.ts +3 -0
  98. package/dist/components/record/RecordSkeleton.js +21 -0
  99. package/dist/components/record/SmartButtons.d.ts +35 -0
  100. package/dist/components/record/SmartButtons.js +38 -0
  101. package/dist/components/record/StatusBar.d.ts +51 -0
  102. package/dist/components/record/StatusBar.js +168 -0
  103. package/dist/components/record/ThumbBar.d.ts +36 -0
  104. package/dist/components/record/ThumbBar.js +86 -0
  105. package/dist/components/record/index.d.ts +6 -0
  106. package/dist/components/record/index.js +8 -0
  107. package/dist/components/search/CriteriaBuilder.d.ts +58 -0
  108. package/dist/components/search/CriteriaBuilder.js +278 -0
  109. package/dist/components/search/CriteriaChips.d.ts +22 -0
  110. package/dist/components/search/CriteriaChips.js +64 -0
  111. package/dist/components/search/FilterBuilder.d.ts +142 -0
  112. package/dist/components/search/FilterBuilder.js +421 -0
  113. package/dist/components/search/JoinWord.d.ts +8 -0
  114. package/dist/components/search/JoinWord.js +47 -0
  115. package/dist/components/search/JsonEditor.d.ts +19 -0
  116. package/dist/components/search/JsonEditor.js +78 -0
  117. package/dist/components/search/SearchBar.d.ts +78 -0
  118. package/dist/components/search/SearchBar.js +387 -0
  119. package/dist/components/search/criteria.d.ts +116 -0
  120. package/dist/components/search/criteria.js +159 -0
  121. package/dist/components/search/datePeriods.d.ts +122 -0
  122. package/dist/components/search/datePeriods.js +134 -0
  123. package/dist/components/search/dateRanges.d.ts +54 -0
  124. package/dist/components/search/dateRanges.js +89 -0
  125. package/dist/components/search/index.d.ts +9 -0
  126. package/dist/components/search/index.js +20 -0
  127. package/dist/components/search/model.d.ts +54 -0
  128. package/dist/components/search/model.js +37 -0
  129. package/dist/components/search/query.d.ts +166 -0
  130. package/dist/components/search/query.js +436 -0
  131. package/dist/components/shell/AppFrame.d.ts +14 -0
  132. package/dist/components/shell/AppFrame.js +80 -0
  133. package/dist/components/shell/TopBar.d.ts +73 -0
  134. package/dist/components/shell/TopBar.js +54 -0
  135. package/dist/components/shell/headerSlot.d.ts +17 -0
  136. package/dist/components/shell/headerSlot.js +21 -0
  137. package/dist/components/shell/index.d.ts +2 -0
  138. package/dist/components/shell/index.js +3 -0
  139. package/dist/foundations/aria.d.ts +14 -0
  140. package/dist/foundations/aria.js +18 -0
  141. package/dist/foundations/calendar.d.ts +28 -0
  142. package/dist/foundations/calendar.js +183 -0
  143. package/dist/foundations/copy.d.ts +619 -0
  144. package/dist/foundations/copy.es.d.ts +2 -0
  145. package/dist/foundations/copy.es.js +377 -0
  146. package/dist/foundations/copy.js +472 -0
  147. package/dist/foundations/density.d.ts +16 -0
  148. package/dist/foundations/density.js +21 -0
  149. package/dist/foundations/grouping.d.ts +281 -0
  150. package/dist/foundations/grouping.js +0 -0
  151. package/dist/foundations/index.d.ts +14 -0
  152. package/dist/foundations/index.js +30 -0
  153. package/dist/foundations/layout.d.ts +9 -0
  154. package/dist/foundations/layout.js +9 -0
  155. package/dist/foundations/pivot.d.ts +222 -0
  156. package/dist/foundations/pivot.js +198 -0
  157. package/dist/foundations/pivotSheet.d.ts +61 -0
  158. package/dist/foundations/pivotSheet.js +0 -0
  159. package/dist/foundations/slots.d.ts +67 -0
  160. package/dist/foundations/slots.js +63 -0
  161. package/dist/foundations/sticky.d.ts +53 -0
  162. package/dist/foundations/sticky.js +150 -0
  163. package/dist/foundations/surface.d.ts +9 -0
  164. package/dist/foundations/surface.js +26 -0
  165. package/dist/foundations/theme.d.ts +42 -0
  166. package/dist/foundations/theme.js +88 -0
  167. package/dist/foundations/types.d.ts +23 -0
  168. package/dist/foundations/types.js +50 -0
  169. package/dist/foundations/variants.d.ts +19 -0
  170. package/dist/foundations/variants.js +48 -0
  171. package/dist/foundations/xlsx.d.ts +86 -0
  172. package/dist/foundations/xlsx.js +0 -0
  173. package/dist/index.d.ts +19 -0
  174. package/dist/index.js +19 -0
  175. package/dist/primitives/block/Block.d.ts +66 -0
  176. package/dist/primitives/block/Block.js +34 -0
  177. package/dist/primitives/block/Line.d.ts +18 -0
  178. package/dist/primitives/block/Line.js +45 -0
  179. package/dist/primitives/block/SectionGrid.d.ts +30 -0
  180. package/dist/primitives/block/SectionGrid.js +29 -0
  181. package/dist/primitives/block/Steps.d.ts +76 -0
  182. package/dist/primitives/block/Steps.js +43 -0
  183. package/dist/primitives/block/index.d.ts +4 -0
  184. package/dist/primitives/block/index.js +7 -0
  185. package/dist/primitives/button/Button.d.ts +137 -0
  186. package/dist/primitives/button/Button.js +234 -0
  187. package/dist/primitives/button/index.d.ts +1 -0
  188. package/dist/primitives/button/index.js +2 -0
  189. package/dist/primitives/cell/CellStack.d.ts +47 -0
  190. package/dist/primitives/cell/CellStack.js +33 -0
  191. package/dist/primitives/cell/index.d.ts +1 -0
  192. package/dist/primitives/cell/index.js +3 -0
  193. package/dist/primitives/choice/ComboBox.d.ts +16 -0
  194. package/dist/primitives/choice/ComboBox.js +52 -0
  195. package/dist/primitives/choice/RadioGroup.d.ts +18 -0
  196. package/dist/primitives/choice/RadioGroup.js +50 -0
  197. package/dist/primitives/choice/SearchField.d.ts +24 -0
  198. package/dist/primitives/choice/SearchField.js +27 -0
  199. package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
  200. package/dist/primitives/choice/SegmentedControl.js +69 -0
  201. package/dist/primitives/choice/Slider.d.ts +16 -0
  202. package/dist/primitives/choice/Slider.js +46 -0
  203. package/dist/primitives/choice/Switch.d.ts +14 -0
  204. package/dist/primitives/choice/Switch.js +65 -0
  205. package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
  206. package/dist/primitives/choice/ThemeSwitcher.js +25 -0
  207. package/dist/primitives/choice/index.d.ts +8 -0
  208. package/dist/primitives/choice/index.js +11 -0
  209. package/dist/primitives/choice/options.d.ts +28 -0
  210. package/dist/primitives/choice/options.js +6 -0
  211. package/dist/primitives/date/Calendar.d.ts +31 -0
  212. package/dist/primitives/date/Calendar.js +110 -0
  213. package/dist/primitives/date/DatePicker.d.ts +34 -0
  214. package/dist/primitives/date/DatePicker.js +99 -0
  215. package/dist/primitives/date/index.d.ts +2 -0
  216. package/dist/primitives/date/index.js +4 -0
  217. package/dist/primitives/dialog/Dialog.d.ts +118 -0
  218. package/dist/primitives/dialog/Dialog.js +121 -0
  219. package/dist/primitives/dialog/index.d.ts +1 -0
  220. package/dist/primitives/dialog/index.js +2 -0
  221. package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
  222. package/dist/primitives/disclosure/Disclosure.js +52 -0
  223. package/dist/primitives/disclosure/index.d.ts +1 -0
  224. package/dist/primitives/disclosure/index.js +2 -0
  225. package/dist/primitives/field/Checks.d.ts +57 -0
  226. package/dist/primitives/field/Checks.js +72 -0
  227. package/dist/primitives/field/DropZone.d.ts +42 -0
  228. package/dist/primitives/field/DropZone.js +52 -0
  229. package/dist/primitives/field/Field.d.ts +166 -0
  230. package/dist/primitives/field/Field.js +240 -0
  231. package/dist/primitives/field/Suggest.d.ts +48 -0
  232. package/dist/primitives/field/Suggest.js +43 -0
  233. package/dist/primitives/field/aria.d.ts +61 -0
  234. package/dist/primitives/field/aria.js +70 -0
  235. package/dist/primitives/field/context.d.ts +30 -0
  236. package/dist/primitives/field/context.js +13 -0
  237. package/dist/primitives/field/index.d.ts +7 -0
  238. package/dist/primitives/field/index.js +11 -0
  239. package/dist/primitives/field/layout.d.ts +24 -0
  240. package/dist/primitives/field/layout.js +10 -0
  241. package/dist/primitives/field/mode.d.ts +9 -0
  242. package/dist/primitives/field/mode.js +13 -0
  243. package/dist/primitives/hint/Hint.d.ts +31 -0
  244. package/dist/primitives/hint/Hint.js +42 -0
  245. package/dist/primitives/hint/index.d.ts +1 -0
  246. package/dist/primitives/hint/index.js +1 -0
  247. package/dist/primitives/index.d.ts +19 -0
  248. package/dist/primitives/index.js +20 -0
  249. package/dist/primitives/menu/Menu.d.ts +100 -0
  250. package/dist/primitives/menu/Menu.js +59 -0
  251. package/dist/primitives/menu/index.d.ts +1 -0
  252. package/dist/primitives/menu/index.js +2 -0
  253. package/dist/primitives/notice/Callout.d.ts +49 -0
  254. package/dist/primitives/notice/Callout.js +28 -0
  255. package/dist/primitives/notice/Notice.d.ts +45 -0
  256. package/dist/primitives/notice/Notice.js +61 -0
  257. package/dist/primitives/notice/Toast.d.ts +55 -0
  258. package/dist/primitives/notice/Toast.js +142 -0
  259. package/dist/primitives/notice/index.d.ts +3 -0
  260. package/dist/primitives/notice/index.js +5 -0
  261. package/dist/primitives/overlay/Popover.d.ts +6 -0
  262. package/dist/primitives/overlay/Popover.js +27 -0
  263. package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
  264. package/dist/primitives/overlay/PopoverTrigger.js +38 -0
  265. package/dist/primitives/overlay/Tooltip.d.ts +38 -0
  266. package/dist/primitives/overlay/Tooltip.js +74 -0
  267. package/dist/primitives/overlay/index.d.ts +3 -0
  268. package/dist/primitives/overlay/index.js +5 -0
  269. package/dist/primitives/picker/Picker.d.ts +48 -0
  270. package/dist/primitives/picker/Picker.js +150 -0
  271. package/dist/primitives/picker/index.d.ts +1 -0
  272. package/dist/primitives/picker/index.js +2 -0
  273. package/dist/primitives/surface/Peek.d.ts +60 -0
  274. package/dist/primitives/surface/Peek.js +71 -0
  275. package/dist/primitives/surface/index.d.ts +1 -0
  276. package/dist/primitives/surface/index.js +2 -0
  277. package/dist/primitives/table/CellEditor.d.ts +14 -0
  278. package/dist/primitives/table/CellEditor.js +83 -0
  279. package/dist/primitives/table/DataTable.d.ts +135 -0
  280. package/dist/primitives/table/DataTable.js +244 -0
  281. package/dist/primitives/table/EditableTable.d.ts +96 -0
  282. package/dist/primitives/table/EditableTable.js +423 -0
  283. package/dist/primitives/table/PivotTable.d.ts +14 -0
  284. package/dist/primitives/table/PivotTable.js +0 -0
  285. package/dist/primitives/table/ReportTable.d.ts +38 -0
  286. package/dist/primitives/table/ReportTable.js +106 -0
  287. package/dist/primitives/table/RowActions.d.ts +16 -0
  288. package/dist/primitives/table/RowActions.js +28 -0
  289. package/dist/primitives/table/SheetTable.d.ts +70 -0
  290. package/dist/primitives/table/SheetTable.js +197 -0
  291. package/dist/primitives/table/Table.d.ts +106 -0
  292. package/dist/primitives/table/Table.js +116 -0
  293. package/dist/primitives/table/index.d.ts +7 -0
  294. package/dist/primitives/table/index.js +9 -0
  295. package/dist/primitives/tabs/Tabs.d.ts +91 -0
  296. package/dist/primitives/tabs/Tabs.js +56 -0
  297. package/dist/primitives/tabs/index.d.ts +1 -0
  298. package/dist/primitives/tabs/index.js +1 -0
  299. package/dist/primitives/tag/Tag.d.ts +36 -0
  300. package/dist/primitives/tag/Tag.js +68 -0
  301. package/dist/primitives/tag/Tags.d.ts +57 -0
  302. package/dist/primitives/tag/Tags.js +104 -0
  303. package/dist/primitives/tag/TagsField.d.ts +32 -0
  304. package/dist/primitives/tag/TagsField.js +182 -0
  305. package/dist/primitives/tag/choices.d.ts +38 -0
  306. package/dist/primitives/tag/choices.js +26 -0
  307. package/dist/primitives/tag/index.d.ts +4 -0
  308. package/dist/primitives/tag/index.js +8 -0
  309. package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
  310. package/dist/primitives/toolbar/Toolbar.js +36 -0
  311. package/dist/primitives/toolbar/index.d.ts +1 -0
  312. package/dist/primitives/toolbar/index.js +2 -0
  313. package/dist/primitives/value/Copyable.d.ts +37 -0
  314. package/dist/primitives/value/Copyable.js +50 -0
  315. package/dist/primitives/value/Fecha.d.ts +12 -0
  316. package/dist/primitives/value/Fecha.js +80 -0
  317. package/dist/primitives/value/Meter.d.ts +29 -0
  318. package/dist/primitives/value/Meter.js +46 -0
  319. package/dist/primitives/value/Metric.d.ts +42 -0
  320. package/dist/primitives/value/Metric.js +38 -0
  321. package/dist/primitives/value/Person.d.ts +90 -0
  322. package/dist/primitives/value/Person.js +208 -0
  323. package/dist/primitives/value/Relation.d.ts +69 -0
  324. package/dist/primitives/value/Relation.js +57 -0
  325. package/dist/primitives/value/Totals.d.ts +26 -0
  326. package/dist/primitives/value/Totals.js +4 -0
  327. package/dist/primitives/value/Value.d.ts +77 -0
  328. package/dist/primitives/value/Value.js +119 -0
  329. package/dist/primitives/value/index.d.ts +8 -0
  330. package/dist/primitives/value/index.js +9 -0
  331. package/dist/tokens.css +1025 -0
  332. package/dist/views/index.d.ts +2 -0
  333. package/dist/views/index.js +3 -0
  334. package/dist/views/list/List.d.ts +113 -0
  335. package/dist/views/list/List.js +97 -0
  336. package/dist/views/list/ListView.d.ts +6 -0
  337. package/dist/views/list/ListView.js +269 -0
  338. package/dist/views/list/columns.d.ts +18 -0
  339. package/dist/views/list/columns.js +27 -0
  340. package/dist/views/list/derive.d.ts +76 -0
  341. package/dist/views/list/derive.js +181 -0
  342. package/dist/views/list/index.d.ts +2 -0
  343. package/dist/views/list/index.js +4 -0
  344. package/dist/views/list/presentations.d.ts +39 -0
  345. package/dist/views/list/presentations.js +103 -0
  346. package/dist/views/list/types.d.ts +272 -0
  347. package/dist/views/list/types.js +1 -0
  348. package/dist/views/list/useNarrowList.d.ts +17 -0
  349. package/dist/views/list/useNarrowList.js +32 -0
  350. package/dist/views/record/Record.d.ts +126 -0
  351. package/dist/views/record/Record.js +141 -0
  352. package/dist/views/record/index.d.ts +1 -0
  353. package/dist/views/record/index.js +2 -0
  354. package/package.json +109 -4
@@ -0,0 +1,421 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { JoinWord } from "./JoinWord.js";
4
+ import { JsonEditor } from "./JsonEditor.js";
5
+ import { fieldLabel, filterableNames, } from "./model.js";
6
+ import { opLabel, sayCondition } from "./query.js";
7
+ import { currentCopy, fillCopy, useUiCopy } from "../../foundations/copy.js";
8
+ import { useSurface } from "../../foundations/surface.js";
9
+ import { Button } from "../../primitives/button/index.js";
10
+ import { FolderTree, Plus, Trash2 } from "lucide-react";
11
+ import { useState } from "react";
12
+ /* ====================================================================== *
13
+ * UN FILTRO QUE ALGUIEN ARMA, DE LO QUE EL MODELO DICE QUE ACEPTA.
14
+ *
15
+ * Los recortes con nombre de al lado —«vencidas», «con entregas pendientes»— son las
16
+ * preguntas que alguien ya pensó. Esto es para las demás: elegir un campo, un
17
+ * operador, escribir un valor — agruparlos con Y o con O, anidar un grupo
18
+ * adentro de otro tantas veces como haga falta, o escribir directamente el
19
+ * JSON para copiar o pegar una criteria entera.
20
+ *
21
+ * **Nada acá está escrito a mano.** Los campos salen de la definición del
22
+ * modelo que manda el servidor, y también los operadores de cada uno: un
23
+ * campo de texto ofrece «contiene», uno numérico ofrece «>», uno que admite
24
+ * vacío ofrece «está vacío». Agregar una columna la hace aparecer acá, con
25
+ * sus operadores correctos, sin tocar este archivo.
26
+ *
27
+ * **Un árbol, no una lista.** Cada grupo —la raíz incluida— tiene su propio
28
+ * «todas / cualquiera» y sus propios hijos: una condición, o otro grupo
29
+ * anidado adentro. El ícono del árbol agrega un grupo nuevo al lado de la
30
+ * fila que se tocó; el `+` agrega otra condición.
31
+ *
32
+ * **Un solo componente renderiza y construye — y siempre hay un JSON al
33
+ * lado.** El árbol y el editor de código son dos vistas de la MISMA
34
+ * criteria, sincronizadas en las dos direcciones: tocar el árbol reescribe
35
+ * el JSON, y escribir un JSON reconocible rehace el árbol. Lo único que el
36
+ * árbol no sabe construir ni mostrar es un NOT: si el filtro trae uno en
37
+ * cualquier nivel, se dibuja de sólo lectura con `ExpressionView` en su
38
+ * lugar, y el JSON sigue siendo la manera de tocarlo.
39
+ * ====================================================================== */
40
+ /* Cómo se llama un operador para quien lo lee: `copy.ops`, que es la MISMA tabla que usa la
41
+ barra al armar la pregunta. Dos tablas es que la misma comparación se llame de dos maneras
42
+ en la misma pantalla. */
43
+ /** Qué operadores no llevan valor: preguntar si algo está vacío no necesita escribir nada. */
44
+ const WITHOUT_VALUE = new Set(["is null"]);
45
+ /** Qué operadores comparan contra DOS valores. «Entre el 1 y el 31» es la pregunta que todo
46
+ * el mundo hace sobre fechas, y escribirla como dos reglas sueltas es la forma de
47
+ * equivocarse con uno de los dos extremos. */
48
+ const WITH_RANGE = new Set(["between"]);
49
+ /** Qué clase de input pide un valor, leído del tipo del campo. */
50
+ function inputType(type) {
51
+ if (type === "integer" || type === "number")
52
+ return "number";
53
+ if (type === "date")
54
+ return "date";
55
+ if (type === "datetime")
56
+ return "datetime-local";
57
+ return "text";
58
+ }
59
+ function isCondition(value) {
60
+ return (typeof value === "object" &&
61
+ value !== null &&
62
+ !Array.isArray(value) &&
63
+ typeof value.field === "string" &&
64
+ typeof value.operator === "string" &&
65
+ "value" in value);
66
+ }
67
+ /** Si esto es una de las formas que el filtro reconoce — condición, o grupo `all`/`any`/
68
+ * `negate`. Un JSON válido que no es ninguna de éstas —`{}`, `{"foo": 1}`, un array suelto—
69
+ * no es un filtro que este componente sepa dibujar, y se trata como error de forma en vez de
70
+ * intentar mostrar cualquier cosa. */
71
+ export function isRecognizedFilter(value) {
72
+ if (isCondition(value))
73
+ return true;
74
+ if (typeof value !== "object" || value === null)
75
+ return false;
76
+ const record = value;
77
+ if (Array.isArray(record.all))
78
+ return true;
79
+ if (Array.isArray(record.any))
80
+ return true;
81
+ if ("negate" in record)
82
+ return true;
83
+ return false;
84
+ }
85
+ export function blankCondition(model, first) {
86
+ const field = first ?? "";
87
+ return {
88
+ kind: "condition",
89
+ field,
90
+ operator: model.fields[field]?.ops[0] ?? "=",
91
+ value: "",
92
+ };
93
+ }
94
+ export function blankGroup(model, first) {
95
+ return { kind: "group", join: "all", children: [blankCondition(model, first)] };
96
+ }
97
+ /** El árbol como el filtro que viaja.
98
+ *
99
+ * in condición sola
100
+ * out esa condición, sin envolver
101
+ *
102
+ * in grupo con dos hijos, join «any»
103
+ * out {any: [ … , … ]}
104
+ *
105
+ * Un grupo de un solo hijo no se envuelve —es ese hijo—, y un grupo sin hijos desaparece: ni
106
+ * «todas» ni «cualquiera» vacío es un filtro que alguien pidió. La regla vale en cualquier
107
+ * nivel, no sólo en la raíz, así que anidar un grupo de una condición sola es lo mismo que no
108
+ * haberlo anidado. */
109
+ export function treeToFilter(node) {
110
+ if (node.kind === "condition") {
111
+ if (node.field === "")
112
+ return undefined;
113
+ if (WITHOUT_VALUE.has(node.operator)) {
114
+ return { field: node.field, operator: node.operator, value: true };
115
+ }
116
+ if (WITH_RANGE.has(node.operator)) {
117
+ /* Un rango a medio escribir no es medio rango: mientras falte un extremo, la regla no
118
+ viaja. */
119
+ if (node.value === "" || (node.upper ?? "") === "")
120
+ return undefined;
121
+ return { field: node.field, operator: node.operator, value: [node.value, node.upper] };
122
+ }
123
+ /* **Una regla sin valor no filtra.** Viajaba con `value: ""`, y eso no es «todavía no
124
+ escribí nada»: contra un `=` es «el campo es la cadena vacía», que vacía la lista
125
+ mientras alguien sigue tipeando, y contra un `like` es «contiene nada», que la deja
126
+ igual. Para preguntar por lo que está vacío está el operador «está vacío», que el
127
+ modelo publica en los campos que lo admiten — explícito, y no por omisión. */
128
+ if (node.value === "")
129
+ return undefined;
130
+ return { field: node.field, operator: node.operator, value: node.value };
131
+ }
132
+ const kept = node.children
133
+ .map(treeToFilter)
134
+ .filter((one) => one !== undefined);
135
+ if (kept.length === 0)
136
+ return undefined;
137
+ /* «Ninguna» niega el grupo ENTERO y no cada hijo por su cuenta: «ni vencida ni
138
+ grande» es `no (vencida o grande)`. Y un grupo de uno tampoco se desarma
139
+ acá —desarmarlo perdería la negación—, que es lo mismo que hace la barra. */
140
+ if (node.join === "not") {
141
+ return { negate: kept.length === 1 ? kept[0] : { any: kept } };
142
+ }
143
+ if (kept.length === 1)
144
+ return kept[0];
145
+ return node.join === "any" ? { any: kept } : { all: kept };
146
+ }
147
+ /** Una condición de vuelta a una fila editable: el valor vuelve a texto, que es lo único que
148
+ * un `<input>` sabe mostrar. Una lista —el valor de un `in`— se escribe separada por comas. */
149
+ function conditionToTree(condition) {
150
+ if (WITH_RANGE.has(condition.operator) && Array.isArray(condition.value)) {
151
+ const [from, to] = condition.value;
152
+ return {
153
+ kind: "condition",
154
+ field: condition.field,
155
+ operator: condition.operator,
156
+ value: String(from ?? ""),
157
+ upper: String(to ?? ""),
158
+ };
159
+ }
160
+ return {
161
+ kind: "condition",
162
+ field: condition.field,
163
+ operator: condition.operator,
164
+ value: WITHOUT_VALUE.has(condition.operator)
165
+ ? ""
166
+ : Array.isArray(condition.value)
167
+ ? condition.value.join(",")
168
+ : String(condition.value ?? ""),
169
+ };
170
+ }
171
+ /** El filtro leído de vuelta a un árbol.
172
+ *
173
+ * in {field: "name", operator: "=", value: "labs"}
174
+ * out TreeCondition
175
+ *
176
+ * in {any: [{all: [cond1, cond2]}, cond3]} grupo adentro de grupo
177
+ * out TreeGroup(any) con un TreeGroup(all) y una condición como hijos
178
+ *
179
+ * in {negate: {any: [cond1, cond2]}}
180
+ * out TreeGroup(not) con las dos condiciones como hijos
181
+ *
182
+ * El `negate` de un `any` se abre en el grupo «ninguna» y no queda como un grupo
183
+ * adentro de otro: es lo que `treeToFilter` vuelve a escribir, así que ida y
184
+ * vuelta dan lo mismo. Lo que NO se puede leer —un objeto que no es ni una
185
+ * condición ni ninguna de las tres formas— devuelve `null`, y ahí el componente
186
+ * lo muestra de sólo lectura sin dejar de poder tocarlo como JSON.
187
+ */
188
+ export function treeFromFilter(filter) {
189
+ if (isCondition(filter))
190
+ return conditionToTree(filter);
191
+ if (typeof filter !== "object" || filter === null)
192
+ return null;
193
+ const record = filter;
194
+ if ("negate" in record) {
195
+ const inside = record.negate;
196
+ /* Un `negate` de un `any` son los hijos del grupo negado; cualquier otra cosa
197
+ es un solo hijo. */
198
+ const raws = typeof inside === "object" &&
199
+ inside !== null &&
200
+ Array.isArray(inside.any)
201
+ ? inside.any
202
+ : [inside];
203
+ const children = readChildren(raws);
204
+ return children === null ? null : { kind: "group", join: "not", children };
205
+ }
206
+ const join = Array.isArray(record.all)
207
+ ? "all"
208
+ : Array.isArray(record.any)
209
+ ? "any"
210
+ : null;
211
+ if (join === null)
212
+ return null;
213
+ const children = readChildren(record[join]);
214
+ return children === null ? null : { kind: "group", join, children };
215
+ }
216
+ /** Los hijos de un grupo, o `null` si alguno no se puede leer: medio árbol es
217
+ * peor que ninguno, porque dibuja algo que no es lo que filtra. */
218
+ function readChildren(raws) {
219
+ const children = [];
220
+ for (const raw of raws) {
221
+ const child = treeFromFilter(raw);
222
+ if (child === null)
223
+ return null;
224
+ children.push(child);
225
+ }
226
+ return children;
227
+ }
228
+ function stringifyFilter(filter) {
229
+ return filter === undefined ? "" : JSON.stringify(filter, null, 2);
230
+ }
231
+ /** Un valor de condición, legible: una lista se junta con comas, y lo que no tiene nada se
232
+ * dice en palabras en vez de mostrar `true`/`undefined` sueltos. */
233
+ function describeValue(value, operator) {
234
+ const copy = currentCopy();
235
+ if (Array.isArray(value)) {
236
+ return operator !== undefined && WITH_RANGE.has(operator)
237
+ ? value.join(` ${copy.search.and} `)
238
+ : value.join(", ");
239
+ }
240
+ /* Las mismas dos palabras que la tabla pone en una celda booleana: un `sí` que
241
+ no es el `sí` de al lado es una segunda manera de decir lo mismo. */
242
+ if (typeof value === "boolean")
243
+ return value ? copy.table.yes : copy.table.no;
244
+ return String(value ?? "");
245
+ }
246
+ /** El filtro dicho en una línea, para una ficha o un rótulo.
247
+ *
248
+ * in {field: "row_count", operator: ">", value: 1000}, con el modelo
249
+ * out 'filas mayor que 1000'
250
+ *
251
+ * in {all: [cond1, cond2]}
252
+ * out 'el nombre contiene labs y filas mayor que 1000'
253
+ *
254
+ * in {any: [cond1, cond2]}
255
+ * out 'el nombre es a o el nombre es b'
256
+ *
257
+ * Cada condición la dice `sayCondition`, que es la misma que usa la barra: sin
258
+ * eso, lo escrito en la barra y lo armado en el diálogo se leen distinto aunque
259
+ * filtren igual, y nadie sabe cuál de las dos frases es la que manda.
260
+ *
261
+ * **El modelo es el que sabe cómo se llama un campo.** Sin él queda el nombre
262
+ * crudo —`row_count`—, que es lo mejor que se puede decir sin preguntarle a
263
+ * nadie.
264
+ *
265
+ * Un grupo anidado se encierra entre paréntesis, que es lo que evita que «a y (b o c)» se
266
+ * lea como «(a y b) o c». Para más de tres condiciones se corta: una ficha que no entra en
267
+ * la barra no dice nada — para eso está abrir el constructor y verlo entero. */
268
+ export function describeFilter(node, model) {
269
+ return say(node, model, 0);
270
+ }
271
+ function say(node, model, depth) {
272
+ const copy = currentCopy();
273
+ if ("all" in node || "any" in node) {
274
+ const items = "all" in node ? node.all : node.any;
275
+ const glue = "all" in node ? copy.search.and : copy.search.or;
276
+ const said = items.slice(0, 3).map((child) => say(child, model, depth + 1));
277
+ if (items.length > 3) {
278
+ said.push(fillCopy(copy.search.andMore, { count: items.length - 3 }));
279
+ }
280
+ const joined = said.join(` ${glue} `);
281
+ return depth === 0 || said.length === 1 ? joined : `(${joined})`;
282
+ }
283
+ if ("negate" in node)
284
+ return `${copy.search.notWord} ${say(node.negate, model, depth + 1)}`;
285
+ const label = model === undefined ? node.field : fieldLabel(model, node.field, copy.locale);
286
+ return sayCondition(label, node.operator, describeValue(node.value, node.operator));
287
+ }
288
+ /** El filtro dibujado tal como es, recursivo: una condición es una línea, `all`/`any` son un
289
+ * grupo con su rótulo y sus hijos adentro, `negate` es «no» envolviendo al que niega.
290
+ *
291
+ * Es la mitad de sólo lectura de este archivo, y también sirve sola: mostrar qué dice un
292
+ * filtro que ya está puesto no necesita abrir el constructor. */
293
+ export function ExpressionView({ node, model, depth = 0, }) {
294
+ const surface = useSurface();
295
+ const copy = useUiCopy();
296
+ const box = surface === "chrome" ? "border-chrome-ink/20" : "border-ink/15";
297
+ const indent = depth === 0 ? "" : "ml-4";
298
+ if ("all" in node || "any" in node) {
299
+ /* LAS MISMAS TRES PALABRAS QUE LA BARRA. Un grupo que acá se llama «coincide
300
+ con alguna» y en la ficha «cualquiera» son dos vocabularios para una sola
301
+ cosa, y el que los lee tiene que aprender los dos. */
302
+ const label = "all" in node ? copy.search.joins.all : copy.search.joins.any;
303
+ const items = "all" in node ? node.all : node.any;
304
+ return (_jsxs("div", { className: `flex flex-col gap-1 border-l-2 pl-2 ${box} ${indent}`, children: [_jsx("p", { className: "text-micro text-muted", children: label }), items.map((child, index) => (_jsx(ExpressionView, { node: child, ...(model === undefined ? {} : { model }), depth: depth + 1 }, index)))] }));
305
+ }
306
+ if ("negate" in node) {
307
+ return (_jsxs("div", { className: `flex flex-col gap-1 border-l-2 pl-2 ${box} ${indent}`, children: [_jsx("p", { className: "text-micro text-muted", children: copy.search.joins.none }), _jsx(ExpressionView, { node: node.negate, ...(model === undefined ? {} : { model }), depth: depth + 1 })] }));
308
+ }
309
+ /* Y la misma frase que la ficha: una condición se lee igual esté donde esté. */
310
+ return _jsx("p", { className: `text-meta ${indent}`, children: describeFilter(node, model) });
311
+ }
312
+ /* ---------------------------------------------------------------------- *
313
+ * UN GRUPO, DIBUJADO — la pieza que se llama a sí misma para anidar.
314
+ * ---------------------------------------------------------------------- */
315
+ export function WhereTree({ model, group, onChange, depth = 0, focus, }) {
316
+ const names = filterableNames(model);
317
+ const copy = useUiCopy();
318
+ const surface = useSurface();
319
+ const box = surface === "chrome"
320
+ ? "border-chrome-ink/20 bg-chrome text-chrome-ink"
321
+ : "border-ink/15 bg-surface text-ink";
322
+ function setChild(index, next) {
323
+ onChange({
324
+ ...group,
325
+ children: group.children.map((c, at) => (at === index ? next : c)),
326
+ });
327
+ }
328
+ function addConditionAfter(index) {
329
+ onChange({
330
+ ...group,
331
+ children: [
332
+ ...group.children.slice(0, index + 1),
333
+ blankCondition(model, names[0]),
334
+ ...group.children.slice(index + 1),
335
+ ],
336
+ });
337
+ }
338
+ function addGroupAfter(index) {
339
+ onChange({
340
+ ...group,
341
+ children: [
342
+ ...group.children.slice(0, index + 1),
343
+ blankGroup(model, names[0]),
344
+ ...group.children.slice(index + 1),
345
+ ],
346
+ });
347
+ }
348
+ function removeAt(index) {
349
+ onChange({ ...group, children: group.children.filter((_, at) => at !== index) });
350
+ }
351
+ return (_jsxs("div", { className: `flex min-w-0 flex-col gap-2 rounded-control border p-2 ${box}`, children: [_jsxs("p", { className: "flex flex-wrap items-baseline gap-1 text-meta", children: [copy.search.askJoin, _jsx(JoinWord, { join: group.join, onChange: (join) => onChange({ ...group, join }) })] }), group.children.map((child, index) => child.kind === "group" ? (_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(WhereTree, { model: model, group: child, onChange: (next) => setChild(index, next), depth: depth + 1, ...(focus === undefined ? {} : { focus }) }) }), _jsx("button", { type: "button", "aria-label": copy.builder.dropGroup, disabled: group.children.length === 1, onClick: () => removeAt(index), className: "mt-2 text-muted disabled:opacity-30", children: _jsx(Trash2, { className: "size-4" }) })] }, index)) : (_jsxs("div", {
352
+ /* La que se vino a ver se marca y se trae a la vista. El anillo y no
353
+ un fondo: el fondo compite con el `select` que hay que tocar. */
354
+ ref: focus !== undefined && child.field === focus
355
+ ? (node) => node?.scrollIntoView({ block: "nearest" })
356
+ : undefined, className: `flex min-w-0 flex-wrap items-center gap-2 rounded-control ${focus !== undefined && child.field === focus
357
+ ? "-m-1 p-1 ring-1 ring-primary/60"
358
+ : ""}`, children: [_jsx("select", { value: child.field, onChange: (event) => setChild(index, {
359
+ ...child,
360
+ field: event.target.value,
361
+ operator: model.fields[event.target.value]?.ops.includes(child.operator)
362
+ ? child.operator
363
+ : (model.fields[event.target.value]?.ops[0] ?? "="),
364
+ }), className: `min-w-0 flex-1 basis-40 rounded-control border px-2 py-1 text-meta ${box}`, children: names.map((name) => (_jsx("option", { value: name, children: name }, name))) }), _jsx("select", { value: child.operator, onChange: (event) => setChild(index, { ...child, operator: event.target.value }), className: `min-w-0 flex-1 basis-36 rounded-control border px-2 py-1 text-meta ${box}`, children: (model.fields[child.field]?.ops ?? []).map((operator) => (_jsx("option", { value: operator, children: opLabel(operator) }, operator))) }), WITHOUT_VALUE.has(child.operator) ? (_jsx("span", { className: "flex-1 text-meta text-muted", children: copy.builder.noValue })) : WITH_RANGE.has(child.operator) ? (_jsxs("span", { className: "flex min-w-0 flex-1 basis-40 items-center gap-1", children: [_jsx("input", { type: inputType(model.fields[child.field]?.type ?? "text"), value: child.value, onChange: (event) => setChild(index, { ...child, value: event.target.value }), placeholder: copy.builder.from, className: `min-w-0 flex-1 rounded-control border px-2 py-1 text-meta ${box}` }), _jsx("span", { className: "shrink-0 text-micro text-muted", children: copy.search.and }), _jsx("input", { type: inputType(model.fields[child.field]?.type ?? "text"), value: child.upper ?? "", onChange: (event) => setChild(index, { ...child, upper: event.target.value }), placeholder: copy.builder.to, className: `min-w-0 flex-1 rounded-control border px-2 py-1 text-meta ${box}` })] })) : (_jsx("input", { type: inputType(model.fields[child.field]?.type ?? "text"), value: child.value, onChange: (event) => setChild(index, { ...child, value: event.target.value }), placeholder: copy.builder.value, className: `min-w-0 flex-1 basis-40 rounded-control border px-2 py-1 text-meta ${box}` })), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": copy.builder.addRule, onClick: () => addConditionAfter(index), className: "text-muted hover:text-ink", children: _jsx(Plus, { className: "size-4" }) }), _jsx("button", { type: "button", "aria-label": copy.builder.addNested, onClick: () => addGroupAfter(index), className: "text-muted hover:text-ink", children: _jsx(FolderTree, { className: "size-4" }) }), _jsx("button", { type: "button", "aria-label": copy.builder.dropRule, disabled: group.children.length === 1, onClick: () => removeAt(index), className: "text-muted disabled:opacity-30", children: _jsx(Trash2, { className: "size-4" }) })] })] }, index)))] }));
365
+ }
366
+ export function FilterBuilder({ model, initial, onApply, onCancel, }) {
367
+ const names = filterableNames(model);
368
+ const copy = useUiCopy();
369
+ const decomposed = initial === undefined ? null : treeFromFilter(initial);
370
+ const [tree, setTree] = useState(() => decomposed !== null && decomposed.kind === "group"
371
+ ? decomposed
372
+ : decomposed !== null
373
+ ? { kind: "group", join: "all", children: [decomposed] }
374
+ : blankGroup(model, names[0]));
375
+ const [lockedView, setLockedView] = useState(initial !== undefined && decomposed === null ? initial : null);
376
+ const [rawText, setRawText] = useState(() => stringifyFilter(initial ?? treeToFilter(tree)));
377
+ const [rawError, setRawError] = useState(null);
378
+ /* Tocar el árbol reescribe el JSON para que muestre exactamente lo que el árbol ahora dice,
379
+ y suelta cualquier vista de sólo lectura que hubiera quedado. */
380
+ function updateTree(next) {
381
+ setTree(next);
382
+ setLockedView(null);
383
+ setRawText(stringifyFilter(treeToFilter(next)));
384
+ setRawError(null);
385
+ }
386
+ /* Escribir en el JSON intenta lo mismo al revés: si lo tipeado no trae un `negate` en
387
+ ningún nivel, el árbol se rehace para mostrarlo — Y y O, a cualquier profundidad. Si trae
388
+ uno, se dibuja de sólo lectura, pero el JSON sigue siendo lo que se va a aplicar. Si no
389
+ es ninguna de las dos —a medio escribir, o no es un filtro— no se toca lo que ya había. */
390
+ function updateRaw(text) {
391
+ setRawText(text);
392
+ if (text.trim() === "") {
393
+ setRawError(null);
394
+ setLockedView(null);
395
+ setTree(blankGroup(model, names[0]));
396
+ return;
397
+ }
398
+ let parsed;
399
+ try {
400
+ parsed = JSON.parse(text);
401
+ }
402
+ catch {
403
+ setRawError(currentCopy().builder.invalidJson);
404
+ return;
405
+ }
406
+ if (!isRecognizedFilter(parsed)) {
407
+ setRawError(currentCopy().builder.notFilter);
408
+ return;
409
+ }
410
+ setRawError(null);
411
+ const node = treeFromFilter(parsed);
412
+ if (node !== null) {
413
+ setTree(node.kind === "group" ? node : { kind: "group", join: "all", children: [node] });
414
+ setLockedView(null);
415
+ }
416
+ else {
417
+ setLockedView(parsed);
418
+ }
419
+ }
420
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 p-3", children: [_jsx("div", { className: "max-h-80 min-w-0 overflow-auto", children: lockedView !== null ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-meta text-muted", children: copy.search.unreadable }), _jsx(ExpressionView, { node: lockedView, model: model })] })) : (_jsx(WhereTree, { model: model, group: tree, onChange: updateTree })) }), _jsx(JsonEditor, { value: rawText, onChange: updateRaw, error: rawError, label: copy.builder.jsonFilter }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { role: "primary", disabled: rawError !== null, onClick: () => onApply(rawText.trim() === "" ? undefined : JSON.parse(rawText)), children: copy.builder.add }), onCancel === undefined ? null : (_jsx(Button, { role: "ghost", onClick: onCancel, children: copy.builder.cancel }))] })] }));
421
+ }
@@ -0,0 +1,8 @@
1
+ import type { CriteriaJoin } from "./criteria.js";
2
+ import type { Surface } from "../../foundations/types.js";
3
+ export declare function JoinWord({ join, onChange, surface, }: {
4
+ join: CriteriaJoin;
5
+ /** Sin esto la palabra es un valor: dice cómo junta, y no se toca. */
6
+ onChange?: (next: CriteriaJoin) => void;
7
+ surface?: Surface;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import { bySurface, useSurface } from "../../foundations/surface.js";
5
+ import { Tags } from "../../primitives/tag/Tags.js";
6
+ import { useState } from "react";
7
+ /* ==================================================================== *
8
+ * CÓMO SE JUNTA UN NODO, en una palabra que se toca.
9
+ *
10
+ * Todas, cualquiera, ninguna. Son las tres y no dos: sin «ninguna», negar algo
11
+ * sólo se podía escribir en el JSON, y una pregunta que el editor no sabe
12
+ * construir es una pregunta que la mayoría no va a hacer.
13
+ *
14
+ * ── Una palabra, no un símbolo ──────────────────────────────────────
15
+ *
16
+ * «Y» y «O» son de quien escribe consultas; «todas» y «cualquiera» son de quien
17
+ * busca. Y son las de la copia, así que cambiarlas las cambia en los dos lados
18
+ * a la vez.
19
+ *
20
+ * ── Se abre en sus opciones, no rota ────────────────────────────────
21
+ *
22
+ * Con tres, un botón que alterna no dice cuáles hay ni cuál está puesta: hay
23
+ * que apretarlo para averiguarlo, y averiguarlo cambia lo que filtra.
24
+ *
25
+ * Vive acá y no adentro de la barra porque el constructor pregunta lo mismo. Un
26
+ * `<select>` allá y una lista acá son dos controles para una decisión, y el que
27
+ * aprendió uno no reconoce el otro.
28
+ * ==================================================================== */
29
+ const OPTIONS = ["all", "any", "not"];
30
+ export function JoinWord({ join, onChange, surface, }) {
31
+ const sup = useSurface(surface);
32
+ const copy = useUiCopy();
33
+ const [open, setOpen] = useState(false);
34
+ const word = copy.search.joins[join === "not" ? "none" : join];
35
+ if (onChange === undefined) {
36
+ return _jsx("span", { className: "shrink-0 text-micro text-muted", children: word });
37
+ }
38
+ return (_jsxs("span", { className: "relative inline-flex shrink-0", children: [_jsx("button", { type: "button", onClick: () => setOpen((current) => !current), "aria-expanded": open, title: copy.search.switchJoin, className: `shrink-0 rounded-control px-1 text-micro font-medium underline decoration-dotted underline-offset-4 decoration-current/40 transition-colors hover:decoration-current focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${bySurface(sup, "text-muted hover:text-ink", "text-chrome-muted hover:text-chrome-ink")}`, children: word }), open ? (_jsx("span", { className: "absolute top-full left-0 z-40 mt-1 rounded-panel border border-line-strong bg-surface p-1.5 shadow-float", children: _jsx(Tags, { stack: true, surface: "canvas", label: copy.search.switchJoin, items: OPTIONS.map((one) => ({
39
+ id: one,
40
+ label: copy.search.joins[one === "not" ? "none" : one],
41
+ selected: one === join,
42
+ onSelect: () => {
43
+ onChange(one);
44
+ setOpen(false);
45
+ },
46
+ })) }) })) : null] }));
47
+ }
@@ -0,0 +1,19 @@
1
+ /** Un JSON acomodado, o el mismo texto si todavía no se puede leer.
2
+ *
3
+ * in '{"all":[{"field":"a","operator":"=","value":1}]}'
4
+ * out el mismo, con sangría de dos espacios y un campo por línea
5
+ *
6
+ * Separado del componente para que se pueda probar sin dibujar nada, que es lo único que
7
+ * tiene lógica acá adentro. */
8
+ export declare function formatJson(text: string): string;
9
+ export declare function JsonEditor({ value, onChange, error, height, label, }: {
10
+ value: string;
11
+ onChange: (next: string) => void;
12
+ /** Qué está mal con lo escrito más allá de la sintaxis — que sea JSON válido pero no un
13
+ * filtro, por ejemplo. Los errores de sintaxis los marca el editor, en su lugar exacto. */
14
+ error?: string | null;
15
+ height?: string;
16
+ /** Cómo se llama este editor. Sin esto dice sólo que es un editor de código,
17
+ * que es lo único que se puede decir sin saber qué se escribe adentro. */
18
+ label?: string;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { json, jsonParseLinter } from "@codemirror/lang-json";
4
+ import { linter, lintGutter } from "@codemirror/lint";
5
+ import { useUiCopy } from "../../foundations/copy.js";
6
+ import { useSurface } from "../../foundations/surface.js";
7
+ import { useTheme } from "../../foundations/theme.js";
8
+ import { Button } from "../../primitives/button/index.js";
9
+ import CodeMirror, { EditorView, Prec } from "@uiw/react-codemirror";
10
+ import { Wand2 } from "lucide-react";
11
+ /* ====================================================================== *
12
+ * UN JSON QUE SE ESCRIBE A MANO, EN UN EDITOR DE VERDAD.
13
+ *
14
+ * CodeMirror y no un `<textarea>` con colores encima: eso último llegó hasta
15
+ * donde llegan cincuenta líneas —pintar las comillas— y se quedaba corto en
16
+ * todo lo demás, que es lo que hace usable escribir un filtro a mano:
17
+ * números de línea para saber dónde está el error, plegado para no perderse
18
+ * en un `all` de seis condiciones, y el subrayado en el carácter exacto
19
+ * donde el JSON se rompe en vez de un cartel que dice que algo está mal.
20
+ *
21
+ * El `linter` de abajo es el del propio paquete de JSON: marca el error
22
+ * mientras se escribe. El aviso de afuera —`error`— sigue existiendo porque
23
+ * dice otra cosa: que el JSON está bien escrito pero no es un filtro.
24
+ * ====================================================================== */
25
+ /** Un JSON acomodado, o el mismo texto si todavía no se puede leer.
26
+ *
27
+ * in '{"all":[{"field":"a","operator":"=","value":1}]}'
28
+ * out el mismo, con sangría de dos espacios y un campo por línea
29
+ *
30
+ * Separado del componente para que se pueda probar sin dibujar nada, que es lo único que
31
+ * tiene lógica acá adentro. */
32
+ export function formatJson(text) {
33
+ try {
34
+ return JSON.stringify(JSON.parse(text), null, 2);
35
+ }
36
+ catch {
37
+ return text;
38
+ }
39
+ }
40
+ /* The built-in `light`/`dark` themes pick the SYNTAX colours; this layer paints the box from
41
+ the system's tokens, so the editor sits on the same surface as the fields around it in
42
+ either theme instead of on a hard-coded one. */
43
+ const SURFACE_THEME = EditorView.theme({
44
+ "&": { backgroundColor: "var(--color-surface)", color: "var(--color-ink)" },
45
+ ".cm-gutters": {
46
+ backgroundColor: "var(--color-ground)",
47
+ color: "var(--color-muted)",
48
+ borderRightStyle: "solid",
49
+ borderRightWidth: "thin",
50
+ borderRightColor: "var(--color-line)",
51
+ },
52
+ ".cm-activeLine, .cm-activeLineGutter": {
53
+ backgroundColor: "color-mix(in srgb, var(--color-ink) 5%, transparent)",
54
+ },
55
+ ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--color-ink)" },
56
+ });
57
+ export function JsonEditor({ value, onChange, error, height = "16rem", label, }) {
58
+ const copy = useUiCopy();
59
+ const surface = useSurface();
60
+ const { resolved } = useTheme();
61
+ /* The chrome is dark in both themes; the canvas follows the theme. */
62
+ const dark = surface === "chrome" || resolved === "dark";
63
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-micro text-muted", children: label ?? copy.builder.editor }), _jsxs(Button, { role: "ghost", size: "sm", disabled: value.trim() === "", onClick: () => onChange(formatJson(value)), children: [_jsx(Wand2, { className: "size-3" }), " ", copy.builder.format] })] }), _jsx("div", { className: `overflow-hidden rounded-control border ${error == null ? "border-ink/15" : "border-danger"}`, children: _jsx(CodeMirror, { value: value, onChange: onChange, height: height, theme: dark ? "dark" : "light", extensions: [
64
+ json(),
65
+ linter(jsonParseLinter()),
66
+ lintGutter(),
67
+ ...(surface === "chrome" ? [] : [Prec.highest(SURFACE_THEME)]),
68
+ ], basicSetup: {
69
+ lineNumbers: true,
70
+ foldGutter: true,
71
+ highlightActiveLine: true,
72
+ autocompletion: false,
73
+ /* El resaltado del par de llaves es lo que hace legible un filtro anidado: sin él,
74
+ en un `any` adentro de un `all` hay que contar corchetes con el dedo. */
75
+ bracketMatching: true,
76
+ closeBrackets: true,
77
+ } }) }), error == null ? null : _jsx("p", { className: "text-micro text-danger", children: error })] }));
78
+ }
@@ -0,0 +1,78 @@
1
+ import { type CriteriaJoin, type CriteriaPath, type SearchCriteria } from "./criteria.js";
2
+ import type { ModelDefinition } from "./model.js";
3
+ import type { Surface } from "../../foundations/types.js";
4
+ import type { MenuGroup } from "../../primitives/menu/Menu.js";
5
+ import { type ReactNode } from "react";
6
+ /** Dónde se puede buscar. Lo declara el dominio, no esta pieza. */
7
+ export interface SearchFacet {
8
+ id: string;
9
+ /** Cómo se lee en la sugerencia y en la ficha: «nombre», «columnas». */
10
+ label: string;
11
+ }
12
+ /**
13
+ * La CONJUNCIÓN entre dos recortes: «y» o «o».
14
+ *
15
+ * Con `onToggle` se puede tocar, y eso no es un adorno: «vencidas Y de este
16
+ * mes» y «vencidas O de este mes» son dos listas completamente distintas, y
17
+ * hasta acá cuál de las dos era estaba decidido por dentro y no se podía cambiar
18
+ * sin abrir el constructor. Sin `onToggle` es una palabra y nada más — que es lo
19
+ * correcto entre dos ejes, donde la Y no es una opción.
20
+ */
21
+ /**
22
+ * Lo que la barra necesita. Está aparte de la función porque es el contrato: lo
23
+ * que una lista tiene que darle para que la barra pregunte, y nada más.
24
+ */
25
+ export type SearchBarProps = {
26
+ facets: readonly SearchFacet[];
27
+ /**
28
+ * El modelo, cuando la lista lo tiene.
29
+ *
30
+ * Es lo que convierte la barra de «buscar un texto en un campo» a «hacer una
31
+ * pregunta»: de acá salen el tipo de cada campo —y con él qué se le puede
32
+ * escribir—, los operadores que acepta, sus valores posibles si es un enum, y
33
+ * por qué columna se busca un relacional. Sin esto, `facets` sigue funcionando
34
+ * como siempre: cada faceta es un campo de texto.
35
+ */
36
+ model?: ModelDefinition;
37
+ /**
38
+ * TODO lo que la barra tiene puesto: los niveles de agrupado y los filtros, en
39
+ * una sola lista de instancias.
40
+ *
41
+ * Una sola y no tres porque son lo mismo con clases distintas —ver
42
+ * `criteria.ts`—: lo que entra por el numeral, lo que se escribe a mano, lo
43
+ * que un filtro declarado trae con nombre y lo que sale del constructor son
44
+ * todas instancias, y por eso todas se leen, se tocan y se sacan igual.
45
+ */
46
+ criteria: readonly SearchCriteria[];
47
+ onCriteriaChange: (next: SearchCriteria[]) => void;
48
+ placeholder?: string;
49
+ /** Lo que el panel OFRECE. Lo puesto no sale de acá: sale de `criteria`. */
50
+ filters?: readonly MenuGroup[];
51
+ /**
52
+ * El editor de niveles de agrupado: `<GroupingLevels …/>`.
53
+ *
54
+ * Va en la MISMA columna del panel donde están los filtros, porque es la misma
55
+ * pregunta —«qué parte de esto estoy mirando»— y separarlos obliga a abrir dos
56
+ * cosas para armar un recorte sin poder ver los dos estados a la vez.
57
+ */
58
+ grouping?: ReactNode;
59
+ /**
60
+ * Alguien escribió `#and`, `#or` o `#not` y la barra abrió un nodo: esto avisa
61
+ * cuál, por si quien compone quiere hacer algo más con eso.
62
+ */
63
+ onOpenGroup?: (join: CriteriaJoin) => void;
64
+ favorites?: readonly MenuGroup[];
65
+ onFilterSelect?: (id: string) => void;
66
+ onFavoriteSelect?: (id: string) => void;
67
+ /**
68
+ * Abre el constructor. Recibe la RUTA de la instancia que se tocó, cuando se
69
+ * tocó una: el constructor abre ahí, en vez de abrir en la primera regla y
70
+ * obligar a buscar la que uno venía mirando entre doce.
71
+ */
72
+ onOpenBuilder?: (path?: CriteriaPath) => void;
73
+ /** Lo que va pegado al campo, a su derecha. */
74
+ trailing?: ReactNode;
75
+ /** Por default la hereda de la superficie que la envuelve. */
76
+ surface?: Surface;
77
+ };
78
+ export declare function SearchBar({ facets, model, criteria, onCriteriaChange, placeholder, filters, grouping, onOpenGroup, favorites, onFilterSelect, onFavoriteSelect, onOpenBuilder, trailing, surface, }: SearchBarProps): import("react").JSX.Element;