@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,423 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import { byDensity, useDensity } from "../../foundations/density.js";
5
+ import { pathKey, pileIsOpen, pileItems, pilesOf, } from "../../foundations/grouping.js";
6
+ import { HANDHELD_WIDTH } from "../../foundations/layout.js";
7
+ import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
8
+ import { IconButton } from "../button/Button.js";
9
+ import { Plus, Trash2 } from "lucide-react";
10
+ import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
11
+ import { useKeyboard } from "react-aria";
12
+ /**
13
+ * Where each column goes in the stacked card, defaults resolved: a declared `stack`
14
+ * wins; otherwise the first undeclared column is the title (unless one was declared),
15
+ * the last undeclared numeric one is the amount (unless one was declared), and the rest
16
+ * are detail.
17
+ */
18
+ export function stackPlaces(columns) {
19
+ const titleAt = columns.some((c) => c.stack === "title")
20
+ ? -1
21
+ : columns.findIndex((c) => c.stack === undefined);
22
+ const amountAt = columns.some((c) => c.stack === "amount")
23
+ ? -1
24
+ : columns.findLastIndex((c, i) => c.stack === undefined && c.numeric === true && i !== titleAt);
25
+ return columns.map((c, i) => c.stack ?? (i === titleAt ? "title" : i === amountAt ? "amount" : "detail"));
26
+ }
27
+ /** A brief that says nothing is left out of the line, separator included. */
28
+ function says(node) {
29
+ return node !== null && node !== undefined && node !== false && node !== "";
30
+ }
31
+ /** The label of a group band — its field, its value, how many rows it holds. The same
32
+ * in the table's band and the stacked list's. */
33
+ function pileTag(pile, locale) {
34
+ return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted/70", children: pile.field }), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: pile.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: pile.count.toLocaleString(locale) })] }));
35
+ }
36
+ /** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
37
+ * box and does not move the cells. */
38
+ const RING = "outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary forced-colors:focus-visible:outline-[Highlight]";
39
+ /** What can take the focus in a row. Filtered again in `firstControl`: React Aria's
40
+ * hidden form mirrors (a `<select>` behind a `Select`) match this and are not stops. */
41
+ const FOCUSABLE = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), a[href], [tabindex]';
42
+ /** The first control of a row being edited: the first one in an editable cell, else
43
+ * anything focusable in the row (a column without `edit` may still hold a button). */
44
+ function firstControl(row) {
45
+ const stops = (scope) => [...row.querySelectorAll(scope)].find((el) => el.tabIndex >= 0 && el.closest('[aria-hidden="true"], [inert]') === null) ?? null;
46
+ return stops(`[data-edit] :is(${FOCUSABLE})`) ?? stops(FOCUSABLE);
47
+ }
48
+ /** Keys a control inside the row already answers with `Enter`: a button presses, a
49
+ * link follows, a text area breaks the line, an open list picks its option. */
50
+ function ownsEnter(target) {
51
+ return (target instanceof Element &&
52
+ target.closest('button, a[href], textarea, [role="button"], [role="combobox"][aria-expanded="true"]') !== null);
53
+ }
54
+ /**
55
+ * One body row. Its own component so that it can hold React Aria's `useKeyboard`
56
+ * (a hook per row) and the timer that decides whether the focus really left it.
57
+ * The same row is a `<tr>` in the table and an `<li>` in the stacked list: one
58
+ * keyboard model, two shapes.
59
+ */
60
+ function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, children, }) {
61
+ const leaving = useRef(null);
62
+ const stay = () => {
63
+ if (leaving.current !== null)
64
+ clearTimeout(leaving.current);
65
+ leaving.current = null;
66
+ };
67
+ useEffect(() => stay, []);
68
+ /* React Aria's `useKeyboard` stops the propagation of what it handles and lets
69
+ the rest through (`continuePropagation`), so an `Escape` the row did not use
70
+ still reaches the dialog or the drawer around the table. */
71
+ const { keyboardProps } = useKeyboard({
72
+ isDisabled: !editable,
73
+ onKeyDown: (e) => {
74
+ if (!editing) {
75
+ /* Only on the row itself: a link drawn inside a reading cell keeps its
76
+ own `Enter`. */
77
+ if (e.target !== e.currentTarget)
78
+ return e.continuePropagation();
79
+ if (e.key === "Enter" || e.key === "F2") {
80
+ e.preventDefault();
81
+ onStart(true);
82
+ return;
83
+ }
84
+ if (e.key === "ArrowDown" ||
85
+ e.key === "ArrowUp" ||
86
+ e.key === "Home" ||
87
+ e.key === "End") {
88
+ e.preventDefault();
89
+ onMove(e.key);
90
+ return;
91
+ }
92
+ return e.continuePropagation();
93
+ }
94
+ if (e.key === "Escape") {
95
+ e.preventDefault();
96
+ onCancel();
97
+ return;
98
+ }
99
+ e.continuePropagation();
100
+ },
101
+ });
102
+ const props = {
103
+ ...keyboardProps,
104
+ /* `Enter` commits in the CAPTURE phase: React Aria's number field answers
105
+ `Enter` itself and stops it, so it would never bubble up to the row. Taking
106
+ it on the way down —and only from the row's own cells, not from a list
107
+ open in a portal— commits from any control. Focusing the row first lets
108
+ that field write the value it was holding (it commits on blur). */
109
+ onKeyDownCapture: editing
110
+ ? (e) => {
111
+ if (e.key !== "Enter" || e.nativeEvent.isComposing)
112
+ return;
113
+ if (!(e.target instanceof Node) || !e.currentTarget.contains(e.target))
114
+ return;
115
+ if (ownsEnter(e.target))
116
+ return;
117
+ e.preventDefault();
118
+ e.stopPropagation();
119
+ onCommit(true);
120
+ }
121
+ : undefined,
122
+ ...(editable
123
+ ? {
124
+ /* While it is being edited the row itself is not a stop: `Shift+Tab`
125
+ from its first control leaves it (and commits) instead of landing on
126
+ the row it is already in. */
127
+ tabIndex: tabbable && !editing ? 0 : -1,
128
+ "aria-keyshortcuts": editing ? undefined : "Enter F2",
129
+ onFocus: () => {
130
+ stay();
131
+ onFocusRow();
132
+ },
133
+ onBlur: editing
134
+ ? () => {
135
+ /* Commit only if the focus left the WHOLE row — and the row is
136
+ not only its DOM. A `Select`'s list opens in a portal, outside
137
+ the `<tr>`, so "the next focused element is not inside" would
138
+ close the row the moment its list opened. The focus event of a
139
+ portal still bubbles through React's tree back to this row, so
140
+ the commit waits one tick: if the focus landed anywhere that
141
+ belongs to the row, `onFocus` above cancels it. */
142
+ stay();
143
+ leaving.current = setTimeout(() => {
144
+ leaving.current = null;
145
+ onCommit(false);
146
+ }, 0);
147
+ }
148
+ : undefined,
149
+ onClick: editing ? undefined : () => onStart(false),
150
+ }
151
+ : {}),
152
+ className,
153
+ "data-row-id": rowId,
154
+ };
155
+ return as === "li" ? (_jsx("li", { ref: rowRef, ...props, children: children })) : (_jsx("tr", { ref: rowRef, ...props, children: children }));
156
+ }
157
+ /**
158
+ * A table whose rows are edited in place, one row at a time.
159
+ *
160
+ * Mouse: touching a row opens it. Keyboard: the rows are one tab stop, the arrows walk
161
+ * them, `Enter` / `F2` open one, `Tab` walks its controls, `Enter` commits and `Escape`
162
+ * puts the row back as it was; both return the focus to the row. Leaving the row
163
+ * (click outside, tabbing past its last control) commits.
164
+ *
165
+ * Narrower than `--container-handheld`, each row is a stacked card (see the file's
166
+ * header): the same keys, and `onOpen` —when given— instead of editing in place.
167
+ */
168
+ export function EditableTable({ rows, columns, getRowId, onChange, onRemove, onAdd, addLabel,
169
+ /** The footer with the aggregates, already formatted. `null` on the columns
170
+ * that do not add up. Whoever composes computes it, because whoever composes
171
+ * knows what adding up means in their domain —a percentage is not summed, it
172
+ * is averaged. */
173
+ totals, empty, grouping,
174
+ /** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
175
+ * compone, por el mismo motivo que `totals`: sumar significa cosas distintas
176
+ * según el dominio, y un porcentaje se promedia, no se suma. */
177
+ groupTotal, height, layout = "auto", onOpen, }) {
178
+ const copy = useUiCopy();
179
+ const d = useDensity();
180
+ const box = useScrollBox(height === undefined ? {} : { height });
181
+ const [editing, setEditing] = useState(null);
182
+ /* The row that holds the one tab stop of the body: the last one focused. */
183
+ const [active, setActive] = useState(null);
184
+ const rowNodes = useRef(new Map());
185
+ /* The row as it was when editing began, to put it back on `Escape`; `touched` says
186
+ whether any control wrote to it since, so a cancel without changes changes nothing. */
187
+ const before = useRef(null);
188
+ /* Set when the keyboard opened the row: its first control takes the focus once
189
+ the controls exist. A click opens it without moving the focus. */
190
+ const focusOnOpen = useRef(null);
191
+ /* La secuencia que se dibuja: montones y filas intercalados. Sin agrupado, las
192
+ filas tal cual — una tabla que no agrupa no paga nada por poder hacerlo. */
193
+ const drawn = useMemo(() => {
194
+ const piles = pilesOf(rows, grouping);
195
+ if (piles === null)
196
+ return rows.map((row) => ({ row }));
197
+ const out = [];
198
+ const walk = (list) => {
199
+ for (const pile of list) {
200
+ out.push({ pile });
201
+ if (!pileIsOpen(pile, grouping))
202
+ continue;
203
+ if (pile.piles.length > 0)
204
+ walk(pile.piles);
205
+ else
206
+ for (const row of pile.items)
207
+ out.push({ row });
208
+ }
209
+ };
210
+ walk(piles);
211
+ return out;
212
+ }, [rows, grouping]);
213
+ /* The width of the box decides, not the window's: a drawer on a wide screen is as
214
+ narrow as a phone. Measured before paint, so a phone never sees the table first. */
215
+ const frame = useRef(null);
216
+ const [narrow, setNarrow] = useState(false);
217
+ useLayoutEffect(() => {
218
+ const el = frame.current;
219
+ if (layout !== "auto" || el === null)
220
+ return;
221
+ /* A box that measures zero is hidden (a closed tab): it keeps what it had. */
222
+ const measure = (width) => {
223
+ if (width > 0)
224
+ setNarrow(width < HANDHELD_WIDTH);
225
+ };
226
+ measure(el.getBoundingClientRect().width);
227
+ if (typeof ResizeObserver === "undefined")
228
+ return;
229
+ const watch = new ResizeObserver(([entry]) => {
230
+ if (entry !== undefined)
231
+ measure(entry.contentRect.width);
232
+ });
233
+ watch.observe(el);
234
+ return () => watch.disconnect();
235
+ }, [layout]);
236
+ const stacked = layout === "stacked" || (layout === "auto" && narrow);
237
+ const editable = onChange !== undefined;
238
+ /* A card that opens the consumer's form is operable even with nothing to edit in
239
+ place: it has a tab stop, takes Enter and answers a press. */
240
+ const opens = stacked && onOpen !== undefined;
241
+ const operable = editable || opens;
242
+ const cell = byDensity(d, "px-3 py-1.5", "px-2 py-1");
243
+ const add = addLabel ?? copy.editable.add;
244
+ const emptyText = empty ?? copy.editable.empty;
245
+ /* The rows in the order they are drawn: what the arrows walk. */
246
+ const order = drawn.flatMap((entry) => entry.row === undefined ? [] : [getRowId(entry.row)]);
247
+ const stop = active !== null && order.includes(active) ? active : (order[0] ?? null);
248
+ useEffect(() => {
249
+ const id = focusOnOpen.current;
250
+ if (id === null || id !== editing)
251
+ return;
252
+ focusOnOpen.current = null;
253
+ const node = rowNodes.current.get(id);
254
+ if (node !== undefined)
255
+ (firstControl(node) ?? node).focus();
256
+ }, [editing]);
257
+ const start = (id, row, viaKeyboard) => {
258
+ if (opens) {
259
+ setActive(id);
260
+ onOpen?.(id);
261
+ return;
262
+ }
263
+ before.current = { id, row, touched: false };
264
+ focusOnOpen.current = viaKeyboard ? id : null;
265
+ setActive(id);
266
+ setEditing(id);
267
+ };
268
+ /* Keyed by the row: a commit that arrives late (the blur of the row you clicked
269
+ away from) must not close the row the click just opened. */
270
+ const close = (id) => setEditing((current) => (current === id ? null : current));
271
+ const commit = (id, refocus) => {
272
+ if (refocus)
273
+ rowNodes.current.get(id)?.focus();
274
+ if (before.current?.id === id)
275
+ before.current = null;
276
+ close(id);
277
+ };
278
+ const cancel = (id) => {
279
+ /* The focus goes back to the row FIRST: a control that writes its value on blur
280
+ (a number field) writes it now, and the old row is put back after it, not
281
+ before. */
282
+ rowNodes.current.get(id)?.focus();
283
+ const was = before.current;
284
+ before.current = null;
285
+ if (was !== null && was.id === id && was.touched)
286
+ onChange?.(id, was.row);
287
+ close(id);
288
+ };
289
+ /* What a control writes, marking the row as touched for `Escape`. */
290
+ const setter = (id) => (next) => {
291
+ if (before.current?.id === id)
292
+ before.current.touched = true;
293
+ onChange?.(id, next);
294
+ };
295
+ /* The wiring a row needs, the same for the table's `<tr>` and the stacked `<li>`. */
296
+ const rowWiring = (id, row) => ({
297
+ rowId: id,
298
+ rowRef: (node) => {
299
+ if (node === null)
300
+ rowNodes.current.delete(id);
301
+ else
302
+ rowNodes.current.set(id, node);
303
+ },
304
+ editable: operable,
305
+ editing: editing === id,
306
+ tabbable: stop === id,
307
+ onStart: (viaKeyboard) => start(id, row, viaKeyboard),
308
+ onCommit: (refocus) => commit(id, refocus),
309
+ onCancel: () => cancel(id),
310
+ onMove: (key) => move(id, key),
311
+ onFocusRow: () => setActive(id),
312
+ });
313
+ const move = (from, key) => {
314
+ const at = order.indexOf(from);
315
+ const to = key === "Home"
316
+ ? 0
317
+ : key === "End"
318
+ ? order.length - 1
319
+ : key === "ArrowDown"
320
+ ? Math.min(at + 1, order.length - 1)
321
+ : Math.max(at - 1, 0);
322
+ const id = order[to];
323
+ if (id === undefined)
324
+ return;
325
+ setActive(id);
326
+ rowNodes.current.get(id)?.focus();
327
+ };
328
+ /* ── the stacked list: one card per row, below `--container-handheld` ── */
329
+ const places = stackPlaces(columns);
330
+ const placed = (place) => columns.filter((_, i) => places[i] === place);
331
+ const titles = placed("title");
332
+ const details = placed("detail");
333
+ const amounts = placed("amount");
334
+ const trails = placed("trail");
335
+ const pad = byDensity(d, "px-3 py-2.5", "px-2 py-1.5");
336
+ const card = (row) => {
337
+ const id = getRowId(row);
338
+ const isEditing = editing === id;
339
+ const set = setter(id);
340
+ const parts = details.flatMap((c) => {
341
+ const said = c.brief === undefined ? null : c.brief(row);
342
+ if (c.brief !== undefined)
343
+ return says(said) ? [{ id: c.id, node: said }] : [];
344
+ return [
345
+ {
346
+ id: c.id,
347
+ node: (_jsxs(_Fragment, { children: [c.header, " ", c.render(row)] })),
348
+ },
349
+ ];
350
+ });
351
+ const controls = editable && isEditing ? columns.filter((c) => c.edit !== undefined) : [];
352
+ return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `group/row flex min-w-0 flex-col gap-1 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : "hover:bg-ink/4"} ${operable ? RING : ""} ${operable && !isEditing ? (opens ? "cursor-pointer" : "cursor-text") : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.map((c) => (_jsx("div", { className: "shrink-0", children: c.render(row) }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), parts.length === 0 && amounts.length === 0 ? null : (_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: "min-w-0 flex-1 text-muted tabular-nums", children: parts.map((p, i) => (_jsxs(Fragment, { children: [i > 0 ? " · " : null, p.node] }, p.id))) }), amounts.map((c) => (
353
+ /* The amount is read by its figure on screen and by its name aloud: a
354
+ lone «Bs. 1.296,00» at the end of a card does not say what it is. */
355
+ _jsxs("span", { className: "shrink-0 text-right font-medium whitespace-nowrap text-ink tabular-nums", children: [_jsxs("span", { className: "sr-only", children: [c.header, " "] }), c.render(row)] }, c.id)))] })), controls.length === 0 ? null : (
356
+ /* Each control under the column's name: a cell had the header above it, a
357
+ card has nothing, and a number box that does not say what it holds is a
358
+ guess. The name is drawn for the eye; the control carries its own label. */
359
+ _jsx("div", { className: "grid grid-cols-2 gap-x-3 gap-y-2 pt-1.5", children: controls.map((c) => (_jsxs("div", { "data-edit": "", className: `flex min-w-0 flex-col gap-1 ${c.numeric === true ? "" : "col-span-2"}`, children: [_jsx("span", { "aria-hidden": "true", className: "text-micro text-muted", children: c.header }), c.edit?.(row, set)] }, c.id))) }))] }, id));
360
+ };
361
+ const list = (_jsxs(_Fragment, { children: [_jsx("ul", { "data-stacked": "", className: "flex min-w-0 flex-col px-1", children: rows.length === 0 ? (_jsx("li", { className: `text-meta text-muted italic ${pad}`, children: emptyText })) : (drawn.map((entry) => {
362
+ if (entry.pile !== undefined) {
363
+ const pile = entry.pile;
364
+ const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
365
+ return (
366
+ /* The band says the group and, at the right, what its rows add up to in
367
+ the column that is the card's amount: the figure under the figures. */
368
+ _jsxs("li", { className: `flex min-w-0 items-center gap-3 bg-ground text-meta ${pad}`, style: pile.depth > 0
369
+ ? {
370
+ paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
371
+ }
372
+ : undefined, children: [_jsx("span", { className: "min-w-0 flex-1", children: pileTag(pile, copy.locale) }), amounts.map((c) => (_jsx("span", { className: "shrink-0 text-ink tabular-nums", children: groupTotal?.(c, inside) ?? null }, c.id)))] }, `pile:${pathKey(pile.path)}`));
373
+ }
374
+ return card(entry.row);
375
+ })) }), totals === undefined ? null : (
376
+ /* The footer's aggregates, one per line with the column's name: under a card
377
+ there is no column to stand under. */
378
+ _jsx("dl", { className: `mx-1 flex flex-col gap-1 border-t border-line-strong ${pad}`, children: columns.map((c, i) => {
379
+ const total = totals[i];
380
+ if (total === null || total === undefined)
381
+ return null;
382
+ return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
383
+ }) }))] }));
384
+ return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, className: `min-w-0 ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [columns.map((c) => (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width }, className: `border-b border-line-strong text-meta font-medium whitespace-nowrap text-muted ${cell} ${c.numeric === true ? "text-right" : "text-left"}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("th", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length + (onRemove === undefined ? 0 : 1), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (drawn.map((entry) => {
385
+ if (entry.pile !== undefined) {
386
+ const pile = entry.pile;
387
+ const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
388
+ return (
389
+ /* La banda respeta la cuadrícula: el rótulo en la primera
390
+ columna y cada total bajo la suya. Un total en un rincón
391
+ obliga a buscar de qué columna es; alineado no hay nada que
392
+ buscar. */
393
+ _jsxs("tr", { children: [columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
394
+ ? {
395
+ paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
396
+ }
397
+ : undefined, children: ci === 0
398
+ ? pileTag(pile, copy.locale)
399
+ : (groupTotal?.(c, inside) ?? null) }, c.id))), onRemove === undefined ? null : _jsx("td", { className: "w-0 bg-ground" })] }, `pile:${pathKey(pile.path)}`));
400
+ }
401
+ const row = entry.row;
402
+ const id = getRowId(row);
403
+ const isEditing = editing === id;
404
+ /* The wash lives on the cells. A background on the row paints
405
+ edge to edge, and `border-collapse` would ignore a radius. */
406
+ const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
407
+ const mark = `border-b border-line first:rounded-l-control last:rounded-r-control ${wash}`;
408
+ const set = setter(id);
409
+ return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row),
410
+ /* The focus ring is drawn on the row, inset, so it is not cut by
411
+ the box and does not move the cells. */
412
+ className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c) => {
413
+ const control = isEditing && c.edit !== undefined && editable;
414
+ return (_jsx("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: control ? c.edit?.(row, set) : c.render(row) }, c.id));
415
+ }), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${mark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
416
+ })) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined ? null : (
417
+ /* The «add» goes under the grid, as text, not as a button away from it:
418
+ what is about to happen is that one more row shows up right here.
419
+ The hover is the same inset mark as a row: the shell lines the label
420
+ up with the cells, and the button itself is the rounded wash. It sits
421
+ outside the scroll box, so a table with its own height keeps it in view. */
422
+ _jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { type: "button", onClick: onAdd, className: `flex w-full items-center gap-1.5 rounded-control text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), add] }) }))] }));
423
+ }
@@ -0,0 +1,14 @@
1
+ import { type Pivot, type PivotKey } from "../../foundations/pivot.js";
2
+ export type PivotTableProps = {
3
+ pivot: Pivot;
4
+ /** Qué pasa al tocar un cruce. Sin esto los números no son botones: una celda
5
+ * que no lleva a ningún lado no tiene por qué prometer que sí. */
6
+ onCell?: (row: PivotKey, column: PivotKey) => void;
7
+ /** Con alto, la grilla scrollea sola y el encabezado se pega a su borde; sin
8
+ * alto scrollea la página. Es la misma regla que las otras tres tablas y vive
9
+ * entera en `useScrollBox`. */
10
+ height?: number | string;
11
+ /** Qué es esta grilla. Se lee en voz alta y se dibuja. */
12
+ caption?: string;
13
+ };
14
+ export declare function PivotTable({ pivot, onCell, height, caption }: PivotTableProps): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ export type ReportRowKind = "section" | "group" | "line" | "total";
2
+ export interface ReportRow {
3
+ id: string;
4
+ label: string;
5
+ /** The indent. It comes from the consumer and not from the nesting, because a
6
+ * second-level total lives next to its siblings and not inside them. */
7
+ level?: number;
8
+ kind?: ReportRowKind;
9
+ /** One value per column, in the same order. `null` is «not applicable» and is
10
+ * drawn empty — different from `0`, which is an amount. */
11
+ values: readonly (number | null)[];
12
+ /** What hangs underneath when it opens. Its presence is what puts the chevron
13
+ * there. */
14
+ children?: readonly ReportRow[];
15
+ /** The note on the line: what goes into this account, where the number comes
16
+ * from. */
17
+ note?: string;
18
+ onClick?: () => void;
19
+ }
20
+ export interface ReportColumn {
21
+ id: string;
22
+ header: string;
23
+ /** For a percentage or a variation column, which is not formatted the same
24
+ * way. */
25
+ format?: (value: number) => string;
26
+ }
27
+ export declare function ReportTable({ columns, rows,
28
+ /** Which groups start open. By default, none: a balance sheet with everything
29
+ * expanded is four hundred lines, and what gets looked at first are the
30
+ * totals. */
31
+ defaultOpen, caption, height, }: {
32
+ columns: readonly ReportColumn[];
33
+ rows: readonly ReportRow[];
34
+ defaultOpen?: readonly string[];
35
+ /** What says what this report is about. It is read out loud and it is drawn. */
36
+ caption?: string;
37
+ height?: number;
38
+ }): import("react").JSX.Element;
@@ -0,0 +1,106 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { currentCopyLocale, fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ import { byDensity, useDensity } from "../../foundations/density.js";
5
+ import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
6
+ import { ChevronRight, Info } from "lucide-react";
7
+ import { useState } from "react";
8
+ /* `useGrouping: "always"` and not the default: without it, four-digit numbers
9
+ lose the thousands separator —`2023.04` next to `258,015.31` in en-US, or
10
+ `2023,04` next to `258.015,31` in es-ES— and in a column of amounts that
11
+ inconsistency reads as a data error. */
12
+ function amountFormat(locale) {
13
+ return new Intl.NumberFormat(locale, {
14
+ minimumFractionDigits: 2,
15
+ maximumFractionDigits: 2,
16
+ /* The assertion is because of the type definitions, not because of the
17
+ engine: `"always"` is ES2023 and TypeScript's `lib` still declares
18
+ `useGrouping` as a boolean. The runtime understands it, and a boolean here
19
+ is not enough — `true` still leaves four-digit numbers without a
20
+ separator. */
21
+ useGrouping: "always",
22
+ });
23
+ }
24
+ const INDENT = 1.25;
25
+ /** The look of each kind of row. It is up here and not spread across the JSX
26
+ * because it is THE decision table of the component: it reads in one go and it
27
+ * changes in one place.
28
+ *
29
+ * `cell` goes on EVERY cell and not on the `<tr>`: the first column is `sticky`
30
+ * and needs a background of its own to cover what passes underneath, so a
31
+ * background on the row plus another one on the pinned cell gives two layers of
32
+ * the same alpha and the band comes out of two greys. The hover of a clickable
33
+ * row is the same: a layer inside each cell, rounded at the two ends, so the
34
+ * pinned name stays opaque and the mark does not run off the sheet. */
35
+ const SKIN = {
36
+ section: { row: "", cell: "bg-ink/8", text: "font-semibold text-ink" },
37
+ group: { row: "", cell: "bg-surface", text: "font-medium text-ink" },
38
+ line: { row: "", cell: "bg-surface", text: "text-ink" },
39
+ total: {
40
+ row: "border-t border-line-strong",
41
+ cell: "bg-surface",
42
+ text: "font-semibold text-ink",
43
+ },
44
+ };
45
+ function Amount({ value, format }) {
46
+ if (value === null)
47
+ return _jsx("span", { className: "text-line-strong", children: "\u00B7" });
48
+ const zero = value === 0;
49
+ const negative = value < 0;
50
+ return (_jsx("span", { className: `tabular-nums ${zero ? "text-muted" : negative ? "text-bad" : "text-ink"}`, children: format === undefined ? amountFormat(currentCopyLocale()).format(value) : format(value) }));
51
+ }
52
+ function ReportLine({ row, columns, open, onToggle, compact, }) {
53
+ const copy = useUiCopy();
54
+ const kind = row.kind ?? "line";
55
+ const skin = SKIN[kind];
56
+ const level = row.level ?? 0;
57
+ const collapsible = row.children !== undefined && row.children.length > 0;
58
+ const isOpen = open.has(row.id);
59
+ const clickable = row.onClick !== undefined;
60
+ return (_jsxs(_Fragment, { children: [_jsxs("tr", { className: `group/row ${skin.row} ${clickable ? "cursor-pointer" : ""}`, onClick: row.onClick, children: [_jsxs("th", { scope: "row",
61
+ /* `sticky left-0` so that the account name does not go away when you
62
+ scroll twelve comparison columns to the right. An amount without its
63
+ line is a loose number. */
64
+ className: `sticky left-0 z-10 text-left font-normal ${skin.cell} ${compact ? "py-1 pr-4 pl-2" : "py-1.5 pr-6 pl-3"}`, style: { paddingLeft: `${(compact ? 0.5 : 0.75) + level * INDENT}rem` }, children: [clickable ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 rounded-l-control group-hover/row:bg-primary/5" })) : null, _jsxs("span", { className: `relative flex min-w-0 items-center gap-1.5 text-meta ${skin.text}`, children: [collapsible ? (_jsx("button", { type: "button", onClick: (e) => {
65
+ e.stopPropagation();
66
+ onToggle(row.id);
67
+ }, "aria-expanded": isOpen, "aria-label": fillCopy(isOpen ? copy.report.close : copy.report.open, {
68
+ name: row.label,
69
+ }), className: "-ml-1 inline-flex size-4 shrink-0 items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/8 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-instant) ${isOpen ? "rotate-90" : ""}`, strokeWidth: 2 }) })) : (
70
+ /* The slot for the chevron is reserved anyway: without it, a leaf
71
+ account and a collapsible one would start on different
72
+ verticals and the hierarchy would stop reading. */
73
+ _jsx("span", { className: "-ml-1 size-4 shrink-0", "aria-hidden": true })), _jsx("span", { className: "truncate", children: row.label })] })] }), columns.map((c, i) => (_jsxs("td", { className: `text-right text-meta whitespace-nowrap ${clickable ? "relative" : ""} ${skin.cell} ${compact ? "px-2 py-1" : "px-3 py-1.5"}`, children: [clickable ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 group-hover/row:bg-primary/5" })) : null, kind === "section" ? null : (_jsx("span", { className: "relative", children: _jsx(Amount, { value: row.values[i] ?? null, ...(c.format === undefined ? {} : { format: c.format }) }) }))] }, c.id))), _jsxs("td", { className: `w-0 ${clickable ? "relative" : ""} ${skin.cell} ${compact ? "pr-2" : "pr-3"}`, children: [clickable ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 rounded-r-control group-hover/row:bg-primary/5" })) : null, row.note === undefined ? null : (_jsx("span", { title: row.note, className: "relative inline-flex text-muted transition-colors duration-(--duration-instant) hover:text-ink", children: _jsx(Info, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-label": row.note }) }))] })] }), !collapsible || !isOpen
74
+ ? null
75
+ : row.children?.map((child) => (_jsx(ReportLine, { row: child, columns: columns, open: open, onToggle: onToggle, compact: compact }, child.id)))] }));
76
+ }
77
+ export function ReportTable({ columns, rows,
78
+ /** Which groups start open. By default, none: a balance sheet with everything
79
+ * expanded is four hundred lines, and what gets looked at first are the
80
+ * totals. */
81
+ defaultOpen, caption, height, }) {
82
+ const copy = useUiCopy();
83
+ const d = useDensity();
84
+ const compact = d === "compact";
85
+ const [open, setOpen] = useState(() => new Set(defaultOpen ?? []));
86
+ const caja = useScrollBox(height === undefined ? {} : { height });
87
+ function toggle(id) {
88
+ setOpen((prev) => {
89
+ const n = new Set(prev);
90
+ if (n.has(id))
91
+ n.delete(id);
92
+ else
93
+ n.add(id);
94
+ return n;
95
+ });
96
+ }
97
+ return (
98
+ /* Misma decisión que las otras dos tablas, en el mismo lugar: con alto la
99
+ caja scrollea y la pila arranca de cero adentro; sin alto y entrando, no
100
+ hay caja y el encabezado se pega debajo de la barra de la lista. Antes
101
+ esto era `overflow-auto` siempre, así que el encabezado heredaba el
102
+ desplazamiento de la barra y quedaba empujado 68 px hacia adentro de su
103
+ propia caja — banda en blanco arriba y títulos de columna cayendo entre
104
+ las filas. */
105
+ _jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right text-meta font-medium whitespace-nowrap text-muted ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
106
+ }
@@ -0,0 +1,16 @@
1
+ import type { LucideIcon } from "lucide-react";
2
+ export interface RowAction {
3
+ id: string;
4
+ /** What is read out loud, in the `title` and in the menu. */
5
+ label: string;
6
+ icon: LucideIcon;
7
+ tone?: "normal" | "action" | "danger";
8
+ disabled?: boolean;
9
+ onSelect: () => void;
10
+ }
11
+ export declare function RowActions({ actions,
12
+ /** How many stay visible before the rest fall into the `⋯`. */
13
+ inline, }: {
14
+ actions: readonly RowAction[];
15
+ inline?: number;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import { IconButton } from "../button/Button.js";
5
+ import { Menu } from "../menu/Menu.js";
6
+ import { MoreHorizontal } from "lucide-react";
7
+ export function RowActions({ actions,
8
+ /** How many stay visible before the rest fall into the `⋯`. */
9
+ inline = 2, }) {
10
+ const copy = useUiCopy();
11
+ if (actions.length === 0)
12
+ return _jsx("td", { className: "w-0" });
13
+ /* With only one left over there is no `⋯`: a menu with a single item is one
14
+ extra click to reach the same button. */
15
+ const inlineActions = actions.length <= inline + 1 ? actions : actions.slice(0, inline);
16
+ const overflow = actions.slice(inlineActions.length);
17
+ const byId = new Map(actions.map((a) => [a.id, a]));
18
+ return (_jsx("td", { className: "w-0 py-1 pr-2 pl-1 align-middle whitespace-nowrap", children: _jsxs("div", { onClick: (e) => e.stopPropagation(), className: "flex items-center justify-end gap-0.5 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100", children: [inlineActions.map((a) => (_jsx(IconButton, { icon: a.icon, label: a.label, size: "sm", ...(a.tone === undefined ? {} : { tone: a.tone }), ...(a.disabled === undefined ? {} : { disabled: a.disabled }), onClick: a.onSelect }, a.id))), overflow.length === 0 ? null : (_jsx(Menu, { label: copy.selection.more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", groups: [
19
+ {
20
+ options: overflow.map((a) => ({
21
+ id: a.id,
22
+ label: a.label,
23
+ icon: a.icon,
24
+ ...(a.disabled === undefined ? {} : { disabled: a.disabled }),
25
+ })),
26
+ },
27
+ ], onSelect: (id) => byId.get(id)?.onSelect() }))] }) }));
28
+ }