@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,229 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { commitDraft, filterLines, planRemoval, } from "./lines.js";
4
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
5
+ import { Button, IconButton } from "../../primitives/button/Button.js";
6
+ import { SearchField } from "../../primitives/choice/SearchField.js";
7
+ import { Dialog } from "../../primitives/dialog/Dialog.js";
8
+ import { FieldIdsContext } from "../../primitives/field/context.js";
9
+ import { useFormMode } from "../../primitives/field/mode.js";
10
+ import { Empty } from "../../primitives/notice/Notice.js";
11
+ import { useToast } from "../../primitives/notice/Toast.js";
12
+ import { EditableTable, } from "../../primitives/table/EditableTable.js";
13
+ import { Totals } from "../../primitives/value/Totals.js";
14
+ import { Pencil, Plus, Trash2 } from "lucide-react";
15
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
16
+ /** The notice is optional: without a `ToastProvider` the line is still removed, just
17
+ * without the way back — a missing provider must not break a form. */
18
+ function useOptionalToast() {
19
+ try {
20
+ return useToast();
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function plain(v) {
27
+ return v === null ? "" : String(v);
28
+ }
29
+ /**
30
+ * The lines of a document inside its record: a count and a search above, the lines
31
+ * (edited in place or in a popup editor), and the totals under them on the right.
32
+ *
33
+ * Context: the record used to compose this by hand — a `Table`, its rows, a
34
+ * `Totals` — and every module did it a little differently: where «add» went, whether
35
+ * removing asked first, what an empty document said. One component, built on
36
+ * `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
37
+ */
38
+ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", }) {
39
+ const copy = useUiCopy();
40
+ const toast = useOptionalToast();
41
+ const read = useFormMode(mode) === "read";
42
+ const root = useRef(null);
43
+ const searchLabelId = useId();
44
+ const formId = useId();
45
+ const [query, setQuery] = useState("");
46
+ const [session, setSession] = useState(null);
47
+ const [open, setOpen] = useState(false);
48
+ /* The draft the form submits, read from a ref: Enter in a number field commits
49
+ the typed text and submits in the same keystroke, before a re-render. */
50
+ const draftRef = useRef(null);
51
+ /* Where focus goes after a removal: the button that was pressed is gone with its
52
+ row, and focus falling to the page is losing the place. */
53
+ const focusNext = useRef(null);
54
+ const canChange = !read && onChange !== undefined;
55
+ const canAdd = !read && onAdd !== undefined && newLine !== undefined;
56
+ const canRemove = !read && onRemove !== undefined;
57
+ const canEdit = canChange && editor !== undefined;
58
+ const labelOf = (line) => {
59
+ if (lineLabel !== undefined)
60
+ return lineLabel(line);
61
+ const first = columns[0];
62
+ if (first === undefined)
63
+ return getLineId(line);
64
+ return first.text?.(line) ?? plain(first.value(line));
65
+ };
66
+ const searchable = lines.length > searchFrom;
67
+ const q = searchable ? query.trim() : "";
68
+ const shown = useMemo(() => filterLines(lines, q, (line) => searchText !== undefined
69
+ ? searchText(line)
70
+ : columns.map((c) => c.text?.(line) ?? plain(c.value(line))).join(" "), copy.locale), [lines, columns, searchText, q, copy.locale]);
71
+ useEffect(() => {
72
+ const target = focusNext.current;
73
+ if (target === null)
74
+ return;
75
+ focusNext.current = null;
76
+ const scope = root.current;
77
+ if (scope === null)
78
+ return;
79
+ /* The next line's first button on a desk; on a phone its card, which has none. */
80
+ const next = target.id === null
81
+ ? null
82
+ : (scope.querySelector(`[data-line="${CSS.escape(target.id)}"] button`) ?? scope.querySelector(`[data-row-id="${CSS.escape(target.id)}"]`));
83
+ (next ?? scope.querySelector("[data-line-add]"))?.focus();
84
+ }, [lines]);
85
+ /* ── the editor ─────────────────────────────────────────────────── */
86
+ const openEditor = (id, draft) => {
87
+ draftRef.current = draft;
88
+ setSession({ id, draft, tried: false });
89
+ setOpen(true);
90
+ };
91
+ const setDraft = (next) => {
92
+ draftRef.current = next;
93
+ setSession((s) => (s === null ? s : { ...s, draft: next }));
94
+ };
95
+ const save = () => {
96
+ const draft = draftRef.current;
97
+ if (session === null || draft === null)
98
+ return;
99
+ const commit = commitDraft(session.id, draft, isValid);
100
+ if (commit.kind === "invalid") {
101
+ setSession({ ...session, draft, tried: true });
102
+ return;
103
+ }
104
+ if (commit.kind === "add") {
105
+ /* A new line is shown even if it does not match the search: adding one and
106
+ not seeing it is the worst answer. */
107
+ setQuery("");
108
+ onAdd?.(commit.line);
109
+ }
110
+ else
111
+ onChange?.(commit.id, commit.line);
112
+ setOpen(false);
113
+ };
114
+ const add = () => {
115
+ if (newLine === undefined)
116
+ return;
117
+ const blank = newLine();
118
+ if (editor === undefined) {
119
+ setQuery("");
120
+ onAdd?.(blank);
121
+ }
122
+ else
123
+ openEditor(null, blank);
124
+ };
125
+ const remove = (line) => {
126
+ const id = getLineId(line);
127
+ const { index, focus } = planRemoval(lines, shown, id, getLineId);
128
+ focusNext.current = { id: focus };
129
+ onRemove?.(id);
130
+ toast?.show({
131
+ message: fillCopy(copy.lineItems.removed, { name: labelOf(line) }),
132
+ ...(onRestore === undefined
133
+ ? {}
134
+ : {
135
+ action: {
136
+ label: copy.lineItems.undo,
137
+ onSelect: () => onRestore(line, index),
138
+ },
139
+ }),
140
+ });
141
+ };
142
+ /* ── the table ──────────────────────────────────────────────────── */
143
+ const tableColumns = columns.map((c) => ({
144
+ id: c.id,
145
+ header: c.header,
146
+ ...(c.width === undefined ? {} : { width: c.width }),
147
+ ...(c.numeric === undefined ? {} : { numeric: c.numeric }),
148
+ ...(c.stack === undefined ? {} : { stack: c.stack }),
149
+ ...(c.brief === undefined ? {} : { brief: c.brief }),
150
+ render: (line) => {
151
+ const drawn = c.cell === undefined ? plain(c.value(line)) : c.cell(line);
152
+ /* An amount never breaks in two: «Bs.» over «12.500,00» reads as two values. */
153
+ return c.numeric === true ? _jsx("span", { className: "whitespace-nowrap", children: drawn }) : drawn;
154
+ },
155
+ ...(c.edit === undefined || !canChange
156
+ ? {}
157
+ : {
158
+ /* The control takes the column's width and does not impose its own: an input's
159
+ natural width is twenty characters, and in an auto-sized table that pushes
160
+ the amounts off the card the moment a row goes into editing. */
161
+ edit: (line, set) => (_jsx("div", { className: "contain-inline-size", children: c.edit?.(line, set) })),
162
+ }),
163
+ }));
164
+ if (canEdit || canRemove) {
165
+ tableColumns.push({
166
+ id: "\u0000actions",
167
+ header: "",
168
+ width: 1,
169
+ /* On a card the press opens the editor, and the editor removes: the buttons
170
+ would be two more targets on a small screen. Without an editor the trash
171
+ is the only way to remove, so it stays — at the card's top right, lit. */
172
+ stack: canEdit ? "hidden" : "trail",
173
+ render: (line) => {
174
+ const id = getLineId(line);
175
+ const nameId = `${formId}-line-${id}`;
176
+ return (
177
+ /* Hidden until the row is hovered or holds focus, like `RowActions`: a pencil
178
+ and a trash can lit on each of twenty lines are forty invitations. They
179
+ fade instead of unmounting, so Tab still reaches them. Pressing them does
180
+ not put the row into in-place editing. */
181
+ _jsxs("span", { "data-line": id, className: "flex items-center justify-end gap-0.5 whitespace-nowrap opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 in-data-stacked:opacity-100", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { id: nameId, className: "sr-only", children: labelOf(line) }), canEdit ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openEditor(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
182
+ },
183
+ });
184
+ }
185
+ const addButton = canAdd ? (_jsx(Button, { role: "secondary", size: "sm", icon: Plus, onClick: add, "data-line-add": "", children: copy.lines.add })) : null;
186
+ const count = q === ""
187
+ ? fillCopy(lines.length === 1 ? copy.lineItems.one : copy.lineItems.many, {
188
+ count: lines.length.toLocaleString(copy.locale),
189
+ })
190
+ : fillCopy(copy.lineItems.shown, {
191
+ shown: shown.length.toLocaleString(copy.locale),
192
+ count: lines.length.toLocaleString(copy.locale),
193
+ });
194
+ return (_jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 ? (
195
+ /* Said, with its way out: an empty document is a question («what is sold
196
+ here?»), not a blank box. In read mode there is no way out to offer. */
197
+ _jsx(Empty, { align: "start", sentence: copy.lines.empty, action: addButton ?? undefined })) : (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 px-1", children: [searchable ? (_jsxs("div", { className: "w-full max-w-(--container-search) min-w-0 flex-1", children: [_jsx("span", { id: searchLabelId, className: "sr-only", children: copy.lines.search }), _jsx(FieldIdsContext.Provider, { value: {
198
+ id: `${searchLabelId}-input`,
199
+ labelId: searchLabelId,
200
+ describedBy: undefined,
201
+ invalid: false,
202
+ }, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), addButton === null ? null : _jsx("span", { className: "ml-auto", children: addButton })] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(canEdit
203
+ ? {
204
+ onOpen: (id) => {
205
+ const line = lines.find((l) => getLineId(l) === id);
206
+ if (line !== undefined)
207
+ openEditor(id, line);
208
+ },
209
+ }
210
+ : {}), ...(canChange && columns.some((c) => c.edit !== undefined)
211
+ ? { onChange: (id, next) => onChange?.(id, next) }
212
+ : {}) })] })), totals === undefined || totals.length === 0 ? null : (
213
+ /* Inset like the table's cells, so the last figure lines up with the amounts. */
214
+ _jsx("div", { className: "px-4", children: _jsx(Totals, { rows: totals }) })), editor === undefined || session === null ? null : (_jsx(Dialog, { open: open, width: "sm", title: session.id === null ? copy.lineItems.newLine : copy.lines.edit, ...(context === undefined ? {} : { context }), ...(session.id === null ? {} : { subtitle: labelOf(session.draft) }), onClose: () => setOpen(false), footer: _jsxs(_Fragment, { children: [session.id === null || !canRemove ? null : (
215
+ /* Apart, at the other end from «Listo»: removing is not the way the
216
+ form is meant to close. */
217
+ _jsx("span", { className: "mr-auto", children: _jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", onClick: () => {
218
+ const line = lines.find((l) => getLineId(l) === session.id);
219
+ setOpen(false);
220
+ if (line !== undefined)
221
+ remove(line);
222
+ } }) })), _jsx(Button, { role: "ghost", size: "sm", onClick: () => setOpen(false), children: copy.dialog.cancel }), _jsx(Button, { role: "primary", size: "sm", type: "submit", form: formId, children: copy.lines.done })] }), children: _jsx("form", { id: formId, noValidate: true, onSubmit: (e) => {
223
+ e.preventDefault();
224
+ save();
225
+ }, className: "flex flex-col gap-3", children: editor(session.draft, setDraft, {
226
+ tried: session.tried,
227
+ isNew: session.id === null,
228
+ }) }) }))] }));
229
+ }
@@ -0,0 +1 @@
1
+ export { type LineEditorState, type LineItemColumn, LineItems, type LineItemsProps, } from "./LineItems.js";
@@ -0,0 +1,2 @@
1
+ /* LINES — the sub-list of lines inside a record (Odoo's one2many): an invoice's, an order's. */
2
+ export { LineItems, } from "./LineItems.js";
@@ -0,0 +1,23 @@
1
+ /** The lines whose text contains the query, case and locale aware. An empty query is all. */
2
+ export declare function filterLines<T>(lines: readonly T[], query: string, textOf: (line: T) => string, locale: string): readonly T[];
3
+ /**
4
+ * What removing a line needs to know BEFORE it is gone: its position among all the lines (so
5
+ * «Deshacer» puts it back exactly there) and the visible line that inherits the focus — the
6
+ * next one, or the previous one when it was the last. `null` when none is left.
7
+ */
8
+ export declare function planRemoval<T>(lines: readonly T[], shown: readonly T[], id: string, getId: (line: T) => string): {
9
+ index: number;
10
+ focus: string | null;
11
+ };
12
+ /** What «Listo» does with the draft: nothing yet (it is not valid), add it, or replace a line. */
13
+ export type DraftCommit<T> = {
14
+ kind: "invalid";
15
+ } | {
16
+ kind: "add";
17
+ line: T;
18
+ } | {
19
+ kind: "change";
20
+ id: string;
21
+ line: T;
22
+ };
23
+ export declare function commitDraft<T>(id: string | null, draft: T, isValid?: (draft: T) => boolean): DraftCommit<T>;
@@ -0,0 +1,26 @@
1
+ /* The decisions of `LineItems` that do not need a screen: which lines a search leaves, where a
2
+ removed line was and who takes the focus after it, and what «Listo» hands to the owner of
3
+ the lines. Pure, so a test reads them without a browser. */
4
+ /** The lines whose text contains the query, case and locale aware. An empty query is all. */
5
+ export function filterLines(lines, query, textOf, locale) {
6
+ const q = query.trim().toLocaleLowerCase(locale);
7
+ if (q === "")
8
+ return lines;
9
+ return lines.filter((line) => textOf(line).toLocaleLowerCase(locale).includes(q));
10
+ }
11
+ /**
12
+ * What removing a line needs to know BEFORE it is gone: its position among all the lines (so
13
+ * «Deshacer» puts it back exactly there) and the visible line that inherits the focus — the
14
+ * next one, or the previous one when it was the last. `null` when none is left.
15
+ */
16
+ export function planRemoval(lines, shown, id, getId) {
17
+ const index = lines.findIndex((l) => getId(l) === id);
18
+ const at = shown.findIndex((l) => getId(l) === id);
19
+ const neighbour = shown[at + 1] ?? (at > 0 ? shown[at - 1] : undefined);
20
+ return { index, focus: neighbour === undefined ? null : getId(neighbour) };
21
+ }
22
+ export function commitDraft(id, draft, isValid) {
23
+ if (isValid !== undefined && !isValid(draft))
24
+ return { kind: "invalid" };
25
+ return id === null ? { kind: "add", line: draft } : { kind: "change", id, line: draft };
26
+ }
@@ -0,0 +1,20 @@
1
+ import type { ColumnControl, ColumnSpec } from "./columns.js";
2
+ import type { Surface } from "../../foundations/types.js";
3
+ /**
4
+ * The Odoo columns submenu: required fields stay checked and cannot be
5
+ * unchecked; optional fields toggle. It stays open while toggling so hiding
6
+ * three columns is three clicks, not three open-close round trips.
7
+ *
8
+ * Drawn only when the set declared optional columns. A menu of required-only
9
+ * fields would be a list of things you cannot do.
10
+ */
11
+ export declare function ColumnsMenu({ control, surface, }: {
12
+ control: ColumnControl;
13
+ surface?: Surface;
14
+ }): import("react").JSX.Element | null;
15
+ export declare function specsFromColumns(columns: readonly {
16
+ id: string;
17
+ header: string;
18
+ optional?: boolean;
19
+ defaultHidden?: boolean;
20
+ }[], fallbackOptional: boolean): ColumnSpec[];
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import { Menu } from "../../primitives/menu/Menu.js";
5
+ import { Columns3 } from "lucide-react";
6
+ /**
7
+ * The Odoo columns submenu: required fields stay checked and cannot be
8
+ * unchecked; optional fields toggle. It stays open while toggling so hiding
9
+ * three columns is three clicks, not three open-close round trips.
10
+ *
11
+ * Drawn only when the set declared optional columns. A menu of required-only
12
+ * fields would be a list of things you cannot do.
13
+ */
14
+ export function ColumnsMenu({ control, surface, }) {
15
+ const copy = useUiCopy();
16
+ if (!control.hasOptional)
17
+ return null;
18
+ const requiredOptions = control.required.map((column) => ({
19
+ id: column.id,
20
+ label: column.label,
21
+ checked: true,
22
+ disabled: true,
23
+ hint: copy.frame.required,
24
+ }));
25
+ const optionalOptions = control.optional.map((column) => ({
26
+ id: column.id,
27
+ label: column.label,
28
+ checked: control.isVisible(column.id),
29
+ }));
30
+ const groups = requiredOptions.length === 0
31
+ ? [{ label: copy.frame.show, options: optionalOptions }]
32
+ : [
33
+ { label: copy.frame.required, options: requiredOptions },
34
+ { label: copy.frame.optional, options: optionalOptions },
35
+ ];
36
+ const hiddenOptional = control.optional.some((column) => !control.isVisible(column.id));
37
+ return (_jsx(Menu, { label: copy.frame.columns, icon: Columns3, groups: groups, onSelect: control.toggle, closeOnSelect: false, active: hiddenOptional, ...(surface === undefined ? {} : { surface }), size: "sm" }));
38
+ }
39
+ export function specsFromColumns(columns, fallbackOptional) {
40
+ return columns.map((column) => ({
41
+ id: column.id,
42
+ label: column.header,
43
+ optional: column.optional ?? fallbackOptional,
44
+ ...(column.defaultHidden === true ? { defaultHidden: true } : {}),
45
+ }));
46
+ }
@@ -0,0 +1,35 @@
1
+ import type { GroupingControl } from "./grouping.js";
2
+ import { type GroupableField, type GroupGrain } from "../../foundations/grouping.js";
3
+ /**
4
+ * «AGRUPAR POR», adentro del panel de recortes.
5
+ *
6
+ * Context: va en la MISMA columna donde están los filtros y los favoritos, y no
7
+ * en un control aparte. Filtrar y agrupar son la misma pregunta —«qué parte de
8
+ * esto estoy mirando»— y separarlos obliga a abrir dos cosas para armar un
9
+ * recorte, sin poder ver los dos estados a la vez.
10
+ *
11
+ * ── Una fecha se DESPLIEGA, no se elige ─────────────────────────────
12
+ *
13
+ * Un campo común es una opción: se tilda y listo. Un campo de fecha no, porque
14
+ * «agrupar por fecha» no quiere decir nada hasta decir con qué corte — y los
15
+ * cortes no son excluyentes: «por mes y adentro por día» es una pregunta
16
+ * legítima y frecuente. Así que una fecha se abre y ofrece sus granos como
17
+ * CASILLAS, que se tildan de a varias.
18
+ *
19
+ * ── Y se ordenan solas ──────────────────────────────────────────────
20
+ *
21
+ * Tildar «día» y después «mes» da «mes › día», nunca al revés. No es una
22
+ * preferencia: un día entero vive adentro de un solo mes, así que partir primero
23
+ * por día deja un nivel de mes que no parte nada. La dirección la pone el
24
+ * calendario y por eso la pone el sistema — ver `normalizeLevels`.
25
+ */
26
+ export type GroupingLevelsProps = {
27
+ /** Por qué campos se puede partir. Sin ninguno no se dibuja nada: una columna
28
+ * vacía en el panel es peor que una columna que no está. */
29
+ fields: readonly GroupableField[];
30
+ control: GroupingControl;
31
+ /** Cómo se llama cada grano, cuando el producto lo dice con otras palabras.
32
+ * Sin esto, las de la copia en el idioma activo. */
33
+ grainLabel?: (grain: GroupGrain) => string;
34
+ };
35
+ export declare function GroupingLevels({ fields, control, grainLabel }: GroupingLevelsProps): import("react").JSX.Element | null;
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { grainName } from "../../foundations/calendar.js";
4
+ import { useUiCopy } from "../../foundations/copy.js";
5
+ import { GRAIN_ORDER, levelKey, } from "../../foundations/grouping.js";
6
+ import { ChevronDown, ChevronRight, ChevronUp, X } from "lucide-react";
7
+ import { useState } from "react";
8
+ function fieldLabel(fields, field) {
9
+ return fields.find((one) => one.field === field)?.label ?? field;
10
+ }
11
+ export function GroupingLevels({ fields, control, grainLabel }) {
12
+ const copy = useUiCopy();
13
+ /* Qué fechas están desplegadas. Arranca desplegada la que ya tenga algún grano
14
+ puesto: si no, lo puesto queda escondido detrás de un chevron y hay que
15
+ adivinar dónde. */
16
+ const [opened, setOpened] = useState(() => control.levels.filter((one) => one.grain !== undefined).map((one) => one.field));
17
+ if (fields.length === 0)
18
+ return null;
19
+ const placed = control.levels;
20
+ const grain = (one) => grainLabel?.(one) ?? grainName(one);
21
+ const isOpen = (field) => opened.includes(field);
22
+ const toggleOpen = (field) => setOpened((current) => current.includes(field) ? current.filter((one) => one !== field) : [...current, field]);
23
+ return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [placed.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsx("ol", { className: "flex flex-wrap gap-1 px-1 pb-1.5", children: placed.map((level, index) => (_jsx("li", { children: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-pill border border-accent/40 bg-accent/18 py-0.5 pr-0.5 pl-1.5 text-micro text-ink", children: [_jsx("span", { className: "tabular-nums opacity-70", children: index + 1 }), _jsxs("span", { className: "max-w-32 truncate", children: [fieldLabel(fields, level.field), level.grain === undefined ? "" : ` · ${grain(level.grain)}`] }), _jsx("button", { type: "button", onClick: () => control.move(level.field, -1), disabled: index === 0, "aria-label": copy.grouping.up, title: copy.grouping.up, className: "rounded-pill p-0.5 hover:bg-primary/15 disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronUp, { className: "size-3", strokeWidth: 2.5 }) }), _jsx("button", { type: "button", onClick: () => control.move(level.field, 1), disabled: index === placed.length - 1, "aria-label": copy.grouping.down, title: copy.grouping.down, className: "rounded-pill p-0.5 hover:bg-primary/15 disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronDown, { className: "size-3", strokeWidth: 2.5 }) }), _jsx("button", { type: "button", onClick: () => control.remove(level.field, level.grain), "aria-label": copy.grouping.remove, title: copy.grouping.remove, className: "rounded-pill p-0.5 hover:bg-primary/15", children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) })] }) }, levelKey(level)))) }), _jsx("div", { className: "mb-1 h-px bg-line", role: "separator" })] })), fields.map((one) => {
24
+ const grains = one.grains;
25
+ /* Un campo común: una casilla y nada más. */
26
+ if (grains === undefined || grains.length === 0) {
27
+ const on = control.has(one.field);
28
+ return (_jsxs("button", { type: "button", onClick: () => control.toggle(one.field), "aria-pressed": on, className: `flex items-center gap-2 rounded-control px-1.5 py-1 text-left transition-colors duration-(--duration-instant) hover:bg-primary/8 ${on ? "bg-primary/6" : ""}`, children: [_jsx(Mark, { on: on }), _jsx("span", { className: `min-w-0 truncate text-meta ${on ? "font-medium text-ink" : "text-ink"}`, children: one.label })] }, one.field));
29
+ }
30
+ /* Una FECHA: se despliega y ofrece sus cortes como casillas. */
31
+ const open = isOpen(one.field);
32
+ const puestos = placed.filter((level) => level.field === one.field);
33
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("button", { type: "button", onClick: () => toggleOpen(one.field), "aria-expanded": open, className: "flex items-center gap-1.5 rounded-control px-1.5 py-1 text-left transition-colors duration-(--duration-instant) hover:bg-primary/8", children: [_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: `min-w-0 flex-1 truncate text-meta ${puestos.length > 0 ? "font-medium text-ink" : "text-ink"}`, children: one.label }), puestos.length === 0 ? null : (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-accent/40 bg-accent/18 px-2 py-0.5 text-micro tabular-nums text-ink", children: puestos.length }))] }), !open ? null : (_jsx("div", { className: "flex flex-col pl-(--spacing-indent)", children: GRAIN_ORDER.filter((g) => grains.includes(g)).map((g) => {
34
+ const on = control.has(one.field, g);
35
+ return (_jsxs("button", { type: "button", onClick: () => control.toggle(one.field, g), "aria-pressed": on, className: `flex items-center gap-2 rounded-control px-1.5 py-1 text-left transition-colors duration-(--duration-instant) hover:bg-primary/8 ${on ? "bg-primary/6" : ""}`, children: [_jsx(Mark, { on: on }), _jsx("span", { className: `min-w-0 truncate text-meta ${on ? "font-medium text-ink" : "text-muted"}`, children: grain(g) })] }, g));
36
+ }) }))] }, one.field));
37
+ }), placed.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 h-px bg-line", role: "separator" }), _jsx("button", { type: "button", onClick: () => control.clear(), className: "rounded-control px-2 py-1 text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-ink/4 hover:text-ink", children: copy.grouping.clear })] }))] }));
38
+ }
39
+ /** La marca de tildado. La columna existe SIEMPRE, tildada o no: sin ella,
40
+ * tildar corre el texto y la lista salta. Apagada se dibuja el aro, la misma
41
+ * marca que el panel de filtros, para que las dos columnas se lean como
42
+ * casillas y no como texto suelto con un hueco a la izquierda. */
43
+ function Mark({ on }) {
44
+ return (_jsx("span", { className: "flex size-(--size-icon-sm) shrink-0 items-center justify-center", children: on ? (_jsx("span", { className: "size-1.5 rounded-pill bg-primary" })) : (_jsx("span", { className: "size-3 rounded-pill border border-line-strong" })) }));
45
+ }
@@ -0,0 +1,27 @@
1
+ import type { Surface } from "../../foundations/types.js";
2
+ import type { ReactNode } from "react";
3
+ export declare function Listing({ bar, metrics, sticky, surface, children, }: {
4
+ /** The `ListingBar`: actions, title, search, pagination and views. */
5
+ bar?: ReactNode;
6
+ /**
7
+ * La barra se queda arriba al scrollear, debajo de lo que ya esté pegado.
8
+ *
9
+ * Es opt-in y no el default porque el fondo tiene que ser el de lo que hay
10
+ * DETRÁS, y eso depende de dónde esté puesta: el arquetipo de lista vive
11
+ * sobre el piso de la página, pero un `FrameView` —«el vistazo» de un
12
+ * registro— vive sobre el PAPEL blanco, y ahí la banda color piso se ve como
13
+ * una mancha. Se descubrió comparando historias, no razonando.
14
+ */
15
+ sticky?: boolean;
16
+ /** Sobre qué está puesta la lista. Sólo se usa para el fondo de la banda que
17
+ * se pega: `chrome` pinta el mismo azul que ya pinta la barra —o sea,
18
+ * invisible— y cualquier otra pinta el PISO, que es lo que hay detrás de una
19
+ * lista de página. Las que viven adentro de algo blanco —un cajón, una vista
20
+ * previa— son compactas, y en compacto la barra no se pega. */
21
+ surface?: Surface;
22
+ /** The metrics that also narrow down. They go BELOW the bar and above the
23
+ * rows: they belong to the set I am looking at, not to the frame that frames
24
+ * it — above the bar they read as part of the navigation. */
25
+ metrics?: ReactNode;
26
+ children: ReactNode;
27
+ }): import("react").JSX.Element;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useHeaderSlot } from "../shell/headerSlot.js";
4
+ import { byDensity, useDensity } from "../../foundations/density.js";
5
+ import { useStickyLayer } from "../../foundations/sticky.js";
6
+ import { createPortal } from "react-dom";
7
+ /* ==================================================================== *
8
+ * LISTING — the shape of a screen of N records. A single one, for all of them.
9
+ *
10
+ * The request was literal: «in every list window don't forget that it has
11
+ * [Search (Filters)] [List]». Up to here every view built that by hand —one
12
+ * put the totals on top, another the search box below the upload panel, another
13
+ * had no search box at all— and the result is that two lists of the same
14
+ * product get used differently.
15
+ *
16
+ * Four bands, always in the same order:
17
+ *
18
+ * bar crumbs · search · pagination
19
+ * actions · title · columns · views
20
+ * body the rows, in a card with a border
21
+ *
22
+ * ── The list goes in a card and the background stays behind ─────────
23
+ *
24
+ * It is what separates an application from a document. Without the card the
25
+ * rows float over nothing and the screen reads as running text; the card puts
26
+ * a boundary around the set, and that boundary is what makes it read as «one
27
+ * thing with elements inside».
28
+ *
29
+ * ── Two densities, and both are needed ──────────────────────────────
30
+ *
31
+ * `table` aligned, sortable columns. It wins when there is width: the data
32
+ * gets compared across rows without reading, which is what a table
33
+ * exists for.
34
+ * `rows` two lines per record, big name and metadata underneath. It wins
35
+ * when there is NO width —a narrow panel, the preview— because a
36
+ * six-column table in 380 px is not a table, it is an accordion.
37
+ *
38
+ * It is not an aesthetic preference: it is the available width. That is why
39
+ * the consumer passes it and this piece does not choose it — only whoever
40
+ * composes knows which surface it is going to land on.
41
+ *
42
+ * ── The row lays itself out; nobody tells it how ────────────────────
43
+ *
44
+ * WHICH presentation is the consumer's call. How a row arranges itself inside
45
+ * the one that was chosen is nobody's: there is no width at which a row «turns
46
+ * narrow», and no breakpoint written anywhere. Every zone asks for what it
47
+ * needs —the name `--container-name`, the tags `--container-tags`, the
48
+ * metadata what it says— and the line does what a line does: what does not fit
49
+ * wraps, and the name keeps the first one.
50
+ *
51
+ * **That is why it holds up with a checkbox, an icon, actions, no tags, or a
52
+ * metadata three times longer.** None of that is declared anywhere: they all
53
+ * take their width from the same line, so a list that selects wraps a little
54
+ * earlier than one that does not, on its own. Asking the density instead —a
55
+ * decision per SURFACE— got it wrong in the one place it mattered: a list in a
56
+ * 480 px drawer is comfortable —it draws its title, its counters, its full
57
+ * padding— and narrow at the same time, and the name was left with whatever
58
+ * was over between a metadata that did not shrink and a tag column of 8 rem:
59
+ * about a dozen pixels, «ve…».
60
+ * ==================================================================== */
61
+ export function Listing({ bar, metrics, sticky = false, surface, children, }) {
62
+ const d = useDensity();
63
+ /* La barra se PEGA arriba y las filas se van debajo de ella.
64
+ Context: con 187 filas, scrollear para ver la última dejaba sin buscador,
65
+ sin paginador y sin el conmutador de presentación — o sea que recortar
66
+ después de mirar obligaba a subir de nuevo. Se pega debajo de lo que ya
67
+ esté pegado (la barra de la aplicación) y empuja la pila para el
68
+ encabezado de la tabla, que se pega debajo de ella. Ver `system/sticky.tsx`. */
69
+ const band = useStickyLayer();
70
+ /* En compacto NO se pega, y no es por falta de espacio: compacto es un cajón
71
+ o una vista previa, o sea una caja corta con su propia cabecera y su propio
72
+ scroll. Una banda pegada ahí se pinta encima del papel blanco del cajón —se
73
+ ve como una mancha— para resolver un scroll que en cinco filas no existe.
74
+ Es la misma razón por la que `ListingBar` tampoco dibuja la identidad ahí. */
75
+ const pegada = sticky && d === "comfortable";
76
+ /* With a sidebar the bar is the screen's header: it goes in the frame's full-width slot,
77
+ above the sidebar (see `shell/headerSlot.ts`), and the frame does the sticking. */
78
+ const slot = useHeaderSlot();
79
+ const inSlot = pegada && slot !== null;
80
+ return (
81
+ /* Context: the box of rows carries no border. What separates it from the
82
+ background is its white and its radius; a frame around a list that is
83
+ already inside a framed panel is a nested rectangle, and nested
84
+ rectangles are what make a product read like a cloud console. */
85
+ _jsxs("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: [inSlot ? (createPortal(bar, slot)) : (_jsx("div", { ref: band.ref, className: `min-w-0 ${pegada ? `sticky z-30 ${surface === "chrome" ? "bg-chrome" : "bg-ground"}` : ""}`, style: pegada ? band.self : undefined, children: bar })), inSlot && metrics !== undefined ? _jsx("div", { className: "pt-2", children: metrics }) : metrics, _jsx("div", { className: "min-w-0 overflow-clip rounded-box bg-surface", style: pegada && !inSlot ? band.below : undefined, children: children })] }));
86
+ }
@@ -0,0 +1,73 @@
1
+ import type { Surface } from "../../foundations/types.js";
2
+ import type { ReactNode } from "react";
3
+ /** THE BAR — two rows, two questions.
4
+ *
5
+ * ┌──────────┬──────────────────────────────┬──────────────┐
6
+ * │ crumbs │ 🔍 search… ⚙3 │ 1–8 of 187 ‹ › │
7
+ * ├──────────┼──────────────────────────────┼──────────────┤
8
+ * │ [New] │ Facturas · 180 · 7 · 8 │ [columns] [▤][▦] │
9
+ * └──────────┴──────────────────────────────┴──────────────┘
10
+ * WHERE / HOW MUCH I SEE HOW I LOOK
11
+ *
12
+ * Pagination answers how much of the filtered set is on screen, so it stays
13
+ * with the search. Columns and the presentation switcher answer how the set
14
+ * is looked at, so they sit with the actions — not next to the page range.
15
+ * Putting the four of them on the search line is how the field and «1–80»
16
+ * painted over each other the moment the window shrank.
17
+ *
18
+ * ── Why it is two lines tall ────────────────────────────────────────
19
+ *
20
+ * Because the three things that go on the left do not fit in one: a
21
+ * comfortable button is 34 px tall, a list title may have to wrap onto two
22
+ * lines, and below it goes a sentence saying what the list is made of. Forcing
23
+ * it into a single line means shrinking the button —the defect we already
24
+ * fixed— or truncating the title, which is worse: a list that cannot be named
25
+ * in full is indistinguishable from the one next to it.
26
+ *
27
+ * ── The selection transforms the bar, it does not add one ───────────
28
+ *
29
+ * When rows get selected, the identity zone turns into «3 elegidas ✕» plus the
30
+ * actions that apply. **In the same place and without pushing anything.** A
31
+ * bar that appears shifts the list downwards right when somebody is ticking
32
+ * checkboxes, and the row they were about to tick is no longer where it was.
33
+ *
34
+ * The actions on the left do NOT go away: «Nuevo» stays available with three
35
+ * rows selected, because selecting is not entering a mode. */
36
+ export declare function ListingBar({ actions, title, description, counters, selection, search, pagination, views, crumbs, surface, }: {
37
+ /** What starts something new. It stays visible even when there is a selection. */
38
+ actions?: ReactNode;
39
+ title?: string;
40
+ /** One sentence: what this list is made of, or what can be done with it. It
41
+ * goes below the title and that is why it can be a real sentence. */
42
+ description?: string;
43
+ /** The totals per category. The ones in the pagination say how many there
44
+ * are; these say what they are made of, which is another question. */
45
+ counters?: readonly {
46
+ label: string;
47
+ value: number | string;
48
+ }[];
49
+ /** When there are selected rows. It replaces the identity, in the same place. */
50
+ selection?: {
51
+ count: number;
52
+ onClear?: () => void;
53
+ actions?: ReactNode;
54
+ };
55
+ search?: ReactNode;
56
+ pagination?: ReactNode;
57
+ /** Columns menu and presentation switcher. They sit on the actions row, on
58
+ * the right: they are how I look at the set, not how many of it I see. */
59
+ views?: ReactNode;
60
+ /** Where I come from. It goes above the row and only on the chrome. */
61
+ crumbs?: ReactNode;
62
+ /** `chrome` paints the whole band navy and everything inside inherits it:
63
+ * the search box, the pagination and the switcher. See `system/surface.tsx`. */
64
+ surface?: Surface;
65
+ }): import("react").JSX.Element;
66
+ export type ListIdentity = {
67
+ title?: string;
68
+ description?: string;
69
+ counters?: readonly {
70
+ label: string;
71
+ value: number | string;
72
+ }[];
73
+ };