@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,56 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useUiCopy } from "../../foundations/copy.js";
4
+ import { Tab, TabList, Tabs as AriaTabs } from "react-aria-components";
5
+ /* El color de la notificación. Estático y en una tabla porque Tailwind no puede
6
+ componer una clase con un pedazo variable: `bg-${tone}/12` no existe en la
7
+ hoja. */
8
+ const NOTICE_TONE = {
9
+ neutral: "bg-ink/8 text-muted",
10
+ info: "bg-info/12 text-info",
11
+ good: "bg-good/12 text-good",
12
+ warn: "bg-warn/14 text-warn",
13
+ bad: "bg-bad/12 text-bad",
14
+ accent: "bg-accent/14 text-accent",
15
+ };
16
+ const NOTICE_DOT = {
17
+ neutral: "bg-muted",
18
+ info: "bg-info",
19
+ good: "bg-good",
20
+ warn: "bg-warn",
21
+ bad: "bg-bad",
22
+ accent: "bg-accent",
23
+ };
24
+ /**
25
+ * The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Chatter`).
26
+ *
27
+ * Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
28
+ * arrows move between tabs and select them, Home/End jump to the ends, and only the open
29
+ * tab is in the Tab order. The hand-written version had the roles and none of the keys.
30
+ */
31
+ export function Tabs({ tabs, current, onSelect, label, }) {
32
+ const copy = useUiCopy();
33
+ return (_jsx(AriaTabs, { selectedKey: current, onSelectionChange: (key) => onSelect?.(String(key)), children: _jsx(TabList, { "aria-label": label ?? copy.record.faces, className: "flex min-w-0 flex-wrap items-end gap-1", children: tabs.map((a) => {
34
+ const Icon = a.icon;
35
+ const empty = a.count === 0;
36
+ /* Un cero no pide nada, así que no se dibuja. Es la diferencia con el
37
+ contador, donde «0» es una respuesta. */
38
+ const notice = a.notice?.count === 0 ? undefined : a.notice;
39
+ return (_jsx(Tab, { id: a.id, ...(notice === undefined
40
+ ? {}
41
+ : { "aria-label": `${a.label} · ${notice.label}` }), className: ({ isSelected }) => `inline-flex shrink-0 cursor-default items-center px-3 py-2 text-meta outline-0 transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${isSelected
42
+ ? "-mb-px rounded-t-control border border-b-0 border-line-strong bg-surface font-medium text-ink"
43
+ : "border border-transparent text-muted hover:text-ink"}`, children: ({ isSelected }) => (
44
+ /* The `title` goes on the content and not on the tab: React Aria does not
45
+ forward it, and on the content the native tooltip still covers the
46
+ whole tab. */
47
+ _jsxs("span", { className: "inline-flex items-center gap-1.5", ...(notice === undefined ? {} : { title: notice.label }), children: [Icon === undefined ? null : (_jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${isSelected ? "text-ink" : "text-muted"}`, strokeWidth: 2, "aria-hidden": true })), a.label, a.count === undefined ? null : (_jsx("span", { className: `rounded-pill px-1.5 text-micro tabular-nums ${empty
48
+ ? "bg-ink/5 text-muted/70"
49
+ : isSelected
50
+ ? "bg-ink/8 text-ink"
51
+ : "bg-ink/8 text-muted"}`, children: a.count.toLocaleString(copy.locale) })), notice === undefined ? null : notice.count === undefined ? (
52
+ /* Sin número, un punto. La palabra no se dibuja pero está: en el
53
+ `title` y en el nombre accesible de la pestaña. */
54
+ _jsx("span", { className: `size-1.5 shrink-0 rounded-pill ${NOTICE_DOT[notice.tone ?? "bad"]}`, "aria-hidden": true })) : (_jsx("span", { className: `rounded-pill px-1.5 text-micro font-medium tabular-nums ${NOTICE_TONE[notice.tone ?? "bad"]}`, "aria-hidden": true, children: notice.count.toLocaleString(copy.locale) }))] })) }, a.id));
55
+ }) }) }));
56
+ }
@@ -0,0 +1 @@
1
+ export { type TabItem, type TabNotice, Tabs } from "./Tabs.js";
@@ -0,0 +1 @@
1
+ export { Tabs } from "./Tabs.js";
@@ -0,0 +1,36 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ import type { ReactNode } from "react";
3
+ export type { Tone };
4
+ /** The classes of a tone on a surface — the capsule's colors, shared with the chips of
5
+ * `Tags` so a classification reads the same as a value and as a removable chip. */
6
+ export declare function tagTone(tone: Tone, surface: "canvas" | "chrome"): string;
7
+ export declare function Tag({ children, tone, surface,
8
+ /** Context: a one-word classification sometimes needs a sentence that does not
9
+ * fit. The sentence goes into the `title` and not underneath, because the
10
+ * line below is taken up by the row's metadata. */
11
+ title, }: {
12
+ children: ReactNode;
13
+ tone?: Tone;
14
+ surface?: "canvas" | "chrome";
15
+ title?: string;
16
+ }): import("react").JSX.Element;
17
+ /** The status dot.
18
+ *
19
+ * **It never goes alone.** A colored dot without its word next to it forces
20
+ * you to learn a legend that is written down nowhere, and it says nothing to
21
+ * someone who cannot tell red from green. The dot speeds up the scanning of a
22
+ * column; the word is what informs. That is why `label` is mandatory and gets
23
+ * drawn — if it needs to be hidden, it is hidden with `aria` and not by
24
+ * deleting it.
25
+ *
26
+ * `pulse` for what is ALIVE: a bank statement still importing. It is the only animation in
27
+ * the system, and it exists because «importando» and «importado» differ by one
28
+ * letter and that is far too little for a state that changes on its own. */
29
+ export declare function StatusDot({ tone, label, pulse, surface, }: {
30
+ tone?: Tone;
31
+ /** ReactNode and not `string`: the label is visible text, and sometimes it
32
+ * carries a figure or a name already formatted by whoever composes. */
33
+ label: ReactNode;
34
+ pulse?: boolean;
35
+ surface?: "canvas" | "chrome";
36
+ }): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Each tone on its two surfaces. The state does not change meaning when
3
+ * crossing from the canvas to the chrome, but its value does: `good` #2F6B3C
4
+ * gives 1.7:1 on #1C1A17 and the lightened one has to be used. */
5
+ const TAG_TONE = {
6
+ neutral: {
7
+ canvas: "border border-line-strong bg-ink/5 text-ink",
8
+ chrome: "border border-chrome-muted/30 bg-chrome-ink/10 text-chrome-ink",
9
+ },
10
+ info: {
11
+ canvas: "border border-info/25 bg-info/10 text-info",
12
+ chrome: "border border-chrome-link/30 bg-chrome-link/15 text-chrome-link",
13
+ },
14
+ good: {
15
+ canvas: "border border-good/25 bg-good/10 text-good",
16
+ chrome: "border border-chrome-good/30 bg-chrome-good/15 text-chrome-good",
17
+ },
18
+ warn: {
19
+ canvas: "border border-warn/30 bg-warn/10 text-warn",
20
+ chrome: "border border-chrome-warn/35 bg-chrome-warn/15 text-chrome-warn",
21
+ },
22
+ bad: {
23
+ canvas: "border border-bad/25 bg-bad/10 text-bad",
24
+ chrome: "border border-chrome-bad/35 bg-chrome-bad/15 text-chrome-bad",
25
+ },
26
+ accent: {
27
+ // The gold as TEXT only exists on the chrome (7.39:1). On the canvas it
28
+ // gives 2.43:1, so there this tag falls back to ink on a gold wash: the
29
+ // brand is visible and the word is readable.
30
+ canvas: "border border-accent/40 bg-accent/18 text-ink",
31
+ chrome: "border border-chrome-accent/40 bg-chrome-accent/15 text-chrome-accent",
32
+ },
33
+ };
34
+ /** The classes of a tone on a surface — the capsule's colors, shared with the chips of
35
+ * `Tags` so a classification reads the same as a value and as a removable chip. */
36
+ export function tagTone(tone, surface) {
37
+ return TAG_TONE[tone][surface];
38
+ }
39
+ export function Tag({ children, tone = "neutral", surface = "canvas",
40
+ /** Context: a one-word classification sometimes needs a sentence that does not
41
+ * fit. The sentence goes into the `title` and not underneath, because the
42
+ * line below is taken up by the row's metadata. */
43
+ title, }) {
44
+ return (_jsx("span", { title: title, className: `inline-flex shrink-0 items-center rounded-pill px-2 py-0.5 text-micro font-medium whitespace-nowrap ${TAG_TONE[tone][surface]}`, children: children }));
45
+ }
46
+ const DOT_TONE = {
47
+ neutral: { canvas: "bg-muted", chrome: "bg-chrome-muted" },
48
+ info: { canvas: "bg-info", chrome: "bg-chrome-link" },
49
+ good: { canvas: "bg-good", chrome: "bg-chrome-good" },
50
+ warn: { canvas: "bg-warn", chrome: "bg-chrome-warn" },
51
+ bad: { canvas: "bg-bad", chrome: "bg-chrome-bad" },
52
+ accent: { canvas: "bg-accent", chrome: "bg-chrome-accent" },
53
+ };
54
+ /** The status dot.
55
+ *
56
+ * **It never goes alone.** A colored dot without its word next to it forces
57
+ * you to learn a legend that is written down nowhere, and it says nothing to
58
+ * someone who cannot tell red from green. The dot speeds up the scanning of a
59
+ * column; the word is what informs. That is why `label` is mandatory and gets
60
+ * drawn — if it needs to be hidden, it is hidden with `aria` and not by
61
+ * deleting it.
62
+ *
63
+ * `pulse` for what is ALIVE: a bank statement still importing. It is the only animation in
64
+ * the system, and it exists because «importando» and «importado» differ by one
65
+ * letter and that is far too little for a state that changes on its own. */
66
+ export function StatusDot({ tone = "neutral", label, pulse = false, surface = "canvas", }) {
67
+ return (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap", children: [_jsxs("span", { className: "relative flex size-1.5 shrink-0", children: [pulse ? (_jsx("span", { className: `absolute inline-flex size-full animate-ping rounded-pill opacity-70 ${DOT_TONE[tone][surface]}` })) : null, _jsx("span", { className: `relative inline-flex size-1.5 rounded-pill ${DOT_TONE[tone][surface]}` })] }), _jsx("span", { className: `text-micro ${surface === "chrome" ? "text-chrome-muted" : "text-muted"}`, children: label })] }));
68
+ }
@@ -0,0 +1,57 @@
1
+ import type { Surface, Tone } from "../../foundations/types.js";
2
+ import type { LucideIcon } from "lucide-react";
3
+ export type TagItem = {
4
+ id: string;
5
+ label: string;
6
+ /** What clarifies it without having to read it whole: which table a field
7
+ * comes from, which symbol a comparison is. It goes muted and inside. */
8
+ note?: string;
9
+ icon?: LucideIcon;
10
+ /** Choose it. Without this the chip is a value and is not pressed. */
11
+ onSelect?: () => void;
12
+ /** Remove it, when removing it means something. It is a separate button
13
+ * inside the chip — and Backspace/Delete on the focused chip — so removing
14
+ * is not the same as choosing. */
15
+ onRemove?: () => void;
16
+ /** The accessible name of the × . React Aria already names it «Remove» +
17
+ * the chip; this replaces the verb when the chip needs a more exact one
18
+ * («quitar el filtro»). */
19
+ removeLabel?: string;
20
+ /** It is chosen right now. */
21
+ selected?: boolean;
22
+ /** It is MARKED by the keyboard of someone else — the search input that
23
+ * drives a list of suggestions — which is different from chosen: marked is
24
+ * «Enter takes this one», chosen is «this one is already set». */
25
+ active?: boolean;
26
+ /** What the chip MEANS, the same tones as `Tag`: a customer category «crédito vencido»
27
+ * in `bad`. Neutral by default — most tags classify, they do not warn. Chosen and
28
+ * marked chips keep their own paint: that state is what the strip is saying then. */
29
+ tone?: Tone;
30
+ title?: string;
31
+ };
32
+ export declare function Tags({ items, surface, label, labelledBy, stack, multiple, }: {
33
+ items: readonly TagItem[];
34
+ /** By default it inherits the surface that wraps it. */
35
+ surface?: Surface;
36
+ /** The accessible name of the strip. A strip of values is read as text and
37
+ * does not need one; a strip that can be pressed or removed is a control,
38
+ * and a control without a name is announced as «grid» and nothing else. */
39
+ label?: string;
40
+ /** The id of a visible label that names the strip — the label of the `Field` it lives
41
+ * in. It wins over `label`. */
42
+ labelledBy?: string;
43
+ /**
44
+ * One below the other, and not on a line that wraps.
45
+ *
46
+ * The difference is not aesthetic: a strip ACCOMPANIES —they are the parts of
47
+ * something being written, and are read together— and a list IS CHOSEN FROM,
48
+ * and there the eye goes down a column and so does the keyboard. Twelve
49
+ * attributes wrapped in three lines are twelve things to sweep in a zigzag to
50
+ * find one.
51
+ */
52
+ stack?: boolean;
53
+ /** When chips carry `selected`, more than one can be chosen at a time. By
54
+ * default the choice is ONE — a join, a grain, a period — and the strip is
55
+ * announced as single-select. */
56
+ multiple?: boolean;
57
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,104 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AriaLocale } from "../../foundations/aria.js";
4
+ import { bySurface, useSurface } from "../../foundations/surface.js";
5
+ import { cn, focusRing } from "../../foundations/variants.js";
6
+ import { tagTone } from "./Tag.js";
7
+ import { X } from "lucide-react";
8
+ import { Button as AriaButton, Tag as AriaTag, TagGroup, TagList, } from "react-aria-components";
9
+ export function Tags({ items, surface, label, labelledBy, stack = false, multiple = false, }) {
10
+ const sup = useSurface(surface);
11
+ if (items.length === 0)
12
+ return null;
13
+ const interactive = items.some((i) => i.onSelect !== undefined || i.onRemove !== undefined);
14
+ if (!interactive)
15
+ return _jsx(StillStrip, { items: items, surface: sup, label: label, stack: stack });
16
+ return (_jsx(AriaLocale, { children: _jsx(ChipGroup, { items: items, surface: sup, label: label, labelledBy: labelledBy, stack: stack, multiple: multiple }) }));
17
+ }
18
+ const STRIP = "flex min-w-0 flex-wrap items-center gap-1";
19
+ const STACK = "flex min-w-0 flex-col items-stretch gap-0.5";
20
+ /** A strip nobody presses: text in a row, a list only when it is named. */
21
+ function StillStrip({ items, surface, label, stack, }) {
22
+ const listed = label !== undefined;
23
+ return (_jsx("span", { className: stack ? STACK : STRIP, ...(listed ? { role: "list", "aria-label": label } : {}), children: items.map((item) => (_jsx("span", { ...(listed ? { role: "listitem" } : {}), ...(item.title === undefined ? {} : { title: item.title }), className: shell({ item, surface, stack, removable: false, pressable: false }), children: _jsx(Inside, { item: item }) }, item.id))) }));
24
+ }
25
+ function ChipGroup({ items, surface, label, labelledBy, stack, multiple, }) {
26
+ const byId = new Map(items.map((item) => [item.id, item]));
27
+ const removable = items.some((i) => i.onRemove !== undefined);
28
+ /* Selection is React Aria's only when the chips SAY whether they are chosen: then the
29
+ strip is announced as a selectable list and each chip as selected or not. Chips that
30
+ only act (`onSelect` without `selected`) are actions, and pressing one does not leave
31
+ anything «selected» behind. */
32
+ const selecting = items.some((i) => i.selected !== undefined && i.onSelect !== undefined);
33
+ const chosen = new Set(items.filter((i) => i.selected === true).map((i) => i.id));
34
+ /* The choice is the consumer's: the strip reports which chip was pressed and waits for the
35
+ next `selected`. In a single choice the chip that ARRIVES is the one pressed; pressing
36
+ the chosen one again reports that one, the way a radio reports the click on itself. */
37
+ const onSelectionChange = (next) => {
38
+ if (next === "all")
39
+ return;
40
+ const keys = new Set([...next].map(String));
41
+ const added = [...keys].filter((k) => !chosen.has(k));
42
+ const dropped = [...chosen].filter((k) => !keys.has(k));
43
+ const pressed = multiple ? [...added, ...dropped] : added.length > 0 ? added : dropped;
44
+ for (const key of pressed)
45
+ byId.get(key)?.onSelect?.();
46
+ };
47
+ /* Backspace on a chip removes it — and only the chips that can be removed: a chip without
48
+ `onRemove` in a strip that has some is still a value. */
49
+ const onRemove = (keys) => {
50
+ for (const key of keys)
51
+ byId.get(String(key))?.onRemove?.();
52
+ };
53
+ return (_jsx(TagGroup
54
+ /* The name falls back to the chips themselves: «cliente: Andina, estado: vencida» says
55
+ what the strip holds, which is better than an anonymous grid. */
56
+ , { ...(labelledBy === undefined
57
+ ? { "aria-label": label ?? items.map((i) => i.label).join(", ") }
58
+ : { "aria-labelledby": labelledBy }), ...(removable ? { onRemove } : {}), ...(selecting
59
+ ? {
60
+ selectionMode: multiple ? "multiple" : "single",
61
+ selectedKeys: chosen,
62
+ onSelectionChange,
63
+ }
64
+ : {}),
65
+ /* Escape belongs to whatever holds the strip — the popover it lives in closes with it —
66
+ and not to «clear the selection», which would erase the choice without a word. */
67
+ escapeKeyBehavior: "none", className: "min-w-0", children: _jsx(TagList, { items: items, className: stack ? STACK : STRIP, children: (item) => (_jsxs(AriaTag, { id: item.id, textValue: item.note === undefined ? item.label : `${item.label} ${item.note}`, ...(!selecting && item.onSelect !== undefined
68
+ ? { onAction: item.onSelect }
69
+ : {}), ...(item.title === undefined ? {} : { title: item.title }), className: ({ isFocusVisible, isHovered }) => cn(shell({
70
+ item,
71
+ surface,
72
+ stack,
73
+ removable: item.onRemove !== undefined,
74
+ pressable: item.onSelect !== undefined,
75
+ }), item.onSelect !== undefined && isHovered && item.active !== true
76
+ ? bySurface(surface, "border-ink/40", "border-chrome-ink/40")
77
+ : "", "outline-offset-1", focusRing({ isFocusVisible })), children: [_jsx(Inside, { item: item }), item.onRemove === undefined ? null : _jsx(Remove, { item: item, surface: surface })] })) }) }));
78
+ }
79
+ function paint(item, surface) {
80
+ if (item.active === true) {
81
+ return bySurface(surface, "border border-primary bg-primary text-surface", "border border-chrome-link bg-chrome-link text-chrome");
82
+ }
83
+ if (item.selected === true) {
84
+ return bySurface(surface, "border border-accent/40 bg-accent/18 text-ink", "border border-chrome-link/40 bg-chrome-link/25 text-chrome-ink");
85
+ }
86
+ if (item.tone !== undefined && item.tone !== "neutral")
87
+ return tagTone(item.tone, surface);
88
+ return bySurface(surface, "border border-line-strong bg-ink/5 text-ink", "border border-chrome-muted/30 bg-chrome-ink/12 text-chrome-ink");
89
+ }
90
+ /* Stacked, each chip takes its whole line: a column of chips of different widths does not
91
+ read as a list, it reads as a mess — and the blank on the right is what lets it be
92
+ pressed without aiming. */
93
+ function shell({ item, surface, stack, removable, pressable, }) {
94
+ return cn(stack ? "flex w-full justify-start" : "inline-flex shrink-0", "min-w-0 items-center gap-1 rounded-pill py-0.5 text-micro transition-colors duration-(--duration-instant)", paint(item, surface), removable ? "pr-0.5 pl-2" : "px-2", pressable ? "cursor-pointer" : "cursor-default");
95
+ }
96
+ function Inside({ item }) {
97
+ const Icon = item.icon;
98
+ return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
99
+ }
100
+ /* The × is React Aria's `remove` slot: it removes THIS chip and never presses it, so
101
+ removing a pressable chip does not also choose it. */
102
+ function Remove({ item, surface }) {
103
+ return (_jsx(AriaButton, { slot: "remove", ...(item.removeLabel === undefined ? {} : { "aria-label": item.removeLabel }), className: ({ isFocusVisible }) => cn("inline-flex size-4 shrink-0 cursor-default items-center justify-center rounded-pill transition-colors duration-(--duration-instant)", bySurface(surface, "hover:bg-ink/15 pressed:bg-ink/20", "hover:bg-chrome-ink/25 pressed:bg-chrome-ink/30"), focusRing({ isFocusVisible })), children: _jsx(X, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }) }));
104
+ }
@@ -0,0 +1,32 @@
1
+ import { type FormMode } from "../field/mode.js";
2
+ import { type TagOption } from "./choices.js";
3
+ export type TagsFieldProps = {
4
+ /** The ids of the chosen tags, in the order they were added. */
5
+ value: readonly string[];
6
+ onChange?: (next: string[]) => void;
7
+ /** Every tag that can be put on the record — chosen ones included, for their labels. */
8
+ options: readonly TagOption[];
9
+ /**
10
+ * Create a tag from the typed text and return its new id (or a promise of it); the field
11
+ * then adds it. Without it there is no «create» row — the catalog is closed.
12
+ *
13
+ * Context: the id is returned and not invented here because the record is created by the
14
+ * backend (Odoo's `name_create`); the consumer also appends the new option to `options`.
15
+ */
16
+ onCreate?: (label: string) => string | Promise<string>;
17
+ /** The input's placeholder while no tag is chosen. Defaults to the copy table's «Add…». */
18
+ placeholder?: string;
19
+ disabled?: boolean;
20
+ id?: string;
21
+ describedBy?: string;
22
+ invalid?: boolean;
23
+ /** Forces the mode. By default it inherits the form's: in read mode it draws `Tags`. */
24
+ mode?: FormMode;
25
+ };
26
+ /**
27
+ * A many2many as removable chips plus an inline combobox that adds one more.
28
+ *
29
+ * Works inside `<Field label=…>`: the field's label names both the input and the strip of
30
+ * chips, and its hint and error describe the input. In read mode it is `Tags`.
31
+ */
32
+ export declare function TagsField({ mode, ...props }: TagsFieldProps): import("react").JSX.Element;
@@ -0,0 +1,182 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AriaLocale } from "../../foundations/aria.js";
4
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
5
+ import { cn } from "../../foundations/variants.js";
6
+ import { fieldGroupStyles, listOptionStyles } from "../field/aria.js";
7
+ import { useFieldIds } from "../field/context.js";
8
+ import { ReadValue } from "../field/Field.js";
9
+ import { useFormMode } from "../field/mode.js";
10
+ import { Popover } from "../overlay/Popover.js";
11
+ import { tagChoices, withoutTag, withTag, } from "./choices.js";
12
+ import { Tags } from "./Tags.js";
13
+ import { Plus } from "lucide-react";
14
+ import { useContext, useEffect, useMemo, useRef, useState, } from "react";
15
+ import { ComboBox as AriaComboBox, ComboBoxStateContext, Group, Input, ListBox, ListBoxItem, useFilter, useLocale, } from "react-aria-components";
16
+ /* ==================================================================== *
17
+ * TAGSFIELD — a many2many written as chips: the categories of a customer, the labels of an
18
+ * invoice.
19
+ *
20
+ * Context: the read form of this value is `Tags` — a strip of chips. Editing it is the same
21
+ * strip plus ONE way in: the chips stay where they are read (removable with their × or
22
+ * Backspace), and an input right after the last one adds the next. A separate «add tag»
23
+ * button that opens a list would be two controls for one value, and a multi-select with
24
+ * checkboxes hides what is chosen behind a closed field — the opposite of a many2many,
25
+ * whose whole point is that the categories are seen.
26
+ *
27
+ * ── React Aria's tag group and React Aria's combobox, side by side ──
28
+ *
29
+ * The chips are `Tags` (React Aria's `TagGroup`): one tab stop, arrows between chips,
30
+ * Backspace or Delete removes the focused one. The input is React Aria's `ComboBox`: typing
31
+ * filters in the copy table's locale («exportacion» finds «exportación»), arrows walk the
32
+ * list, Enter adds. Two additions are ours, both what a person who types tags expects:
33
+ * while typing, the FIRST match is marked, so Enter adds it (typing «may» and Enter is the
34
+ * whole gesture, and the row it takes is the tinted one); and Backspace in the empty input
35
+ * removes the last chip.
36
+ *
37
+ * The chosen tags never come back as options — choosing «mayorista» twice is a duplicate,
38
+ * not a choice. «Create «…»» is offered only when `onCreate` is given and the text names no
39
+ * existing tag (see `tagChoices`).
40
+ *
41
+ * The value is a plain `string[]` of ids, in the order they were added — what the backend
42
+ * stores in a many2many, never React Aria's `Selection`.
43
+ * ==================================================================== */
44
+ /** The key of the «create» row. Not an id: it cannot collide with one. */
45
+ const CREATE = "\u0000create";
46
+ /** The chosen tags as chips, in the order of `value`. An id missing from `options` is still
47
+ * drawn (as itself): hiding it would hide data the record has. */
48
+ function chosenOptions(options, value) {
49
+ const byId = new Map(options.map((option) => [option.value, option]));
50
+ return value.map((id) => byId.get(id) ?? { value: id, label: id });
51
+ }
52
+ /**
53
+ * A many2many as removable chips plus an inline combobox that adds one more.
54
+ *
55
+ * Works inside `<Field label=…>`: the field's label names both the input and the strip of
56
+ * chips, and its hint and error describe the input. In read mode it is `Tags`.
57
+ */
58
+ export function TagsField({ mode, ...props }) {
59
+ if (useFormMode(mode) === "read") {
60
+ const chosen = chosenOptions(props.options, props.value);
61
+ if (chosen.length === 0)
62
+ return _jsx(ReadValue, { value: null });
63
+ return (_jsx("span", { className: "flex min-h-(--size-control-md) items-center", children: _jsx(Tags, { items: chosen.map((option) => ({
64
+ id: option.value,
65
+ label: option.label,
66
+ ...(option.tone === undefined ? {} : { tone: option.tone }),
67
+ })) }) }));
68
+ }
69
+ /* The filter reads the locale from React Aria's context, so the box that provides it has
70
+ to be ABOVE the component that asks for it. */
71
+ return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
72
+ }
73
+ function EditableTagsField({ value, onChange, options, onCreate, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
74
+ const { id, labelledBy, describedBy, invalid } = useFieldIds({
75
+ id: ownId,
76
+ describedBy: ownDescribedBy,
77
+ invalid: ownInvalid,
78
+ });
79
+ const copy = useUiCopy();
80
+ const { contains } = useFilter({ sensitivity: "base" });
81
+ const { locale } = useLocale();
82
+ const collator = useMemo(() => new Intl.Collator(locale, { sensitivity: "base", usage: "search" }), [locale]);
83
+ const [text, setText] = useState("");
84
+ const boxRef = useRef(null);
85
+ const [width, setWidth] = useState(null);
86
+ const inputRef = useRef(null);
87
+ /* The value after an `await` is the one of THEN, not the one of the render that started
88
+ creating: a tag removed meanwhile must not come back. */
89
+ const latest = useRef(value);
90
+ latest.current = value;
91
+ const { available, create } = tagChoices(options, value, text, { contains, same: (a, b) => collator.compare(a.trim(), b) === 0 }, onCreate !== undefined && !disabled);
92
+ const listed = create === null
93
+ ? available
94
+ : [
95
+ ...available,
96
+ { value: CREATE, label: fillCopy(copy.inlineEdit.createTag, { name: create }) },
97
+ ];
98
+ const commit = (key) => {
99
+ if (key === null)
100
+ return;
101
+ const chosen = String(key);
102
+ setText("");
103
+ if (chosen !== CREATE) {
104
+ onChange?.(withTag(latest.current, chosen));
105
+ return;
106
+ }
107
+ if (create === null || onCreate === undefined)
108
+ return;
109
+ void Promise.resolve(onCreate(create)).then((created) => onChange?.(withTag(latest.current, created)));
110
+ };
111
+ const remove = (tagId) => {
112
+ const next = withoutTag(value, tagId);
113
+ onChange?.(next);
114
+ /* The last chip leaves and takes its strip with it: the focus goes to the input instead
115
+ of falling to the page. */
116
+ if (next.length === 0)
117
+ inputRef.current?.focus();
118
+ };
119
+ const chips = chosenOptions(options, value).map((option) => ({
120
+ id: option.value,
121
+ label: option.label,
122
+ ...(option.tone === undefined ? {} : { tone: option.tone }),
123
+ ...(disabled ? {} : { onRemove: () => remove(option.value) }),
124
+ }));
125
+ return (
126
+ /* The chips sit beside the combobox, not inside it: React Aria builds the combobox's
127
+ list from what it holds, and a tag group in there is read as part of that list. The
128
+ popover is anchored to the whole box and as wide as it — the list continues the field,
129
+ and the input alone is a sliver after the last chip. */
130
+ _jsxs(Group, { ref: boxRef, isDisabled: disabled, isInvalid: invalid, className: (state) => fieldGroupStyles({
131
+ ...state,
132
+ /* The box grows with the chips: five categories wrap to a second line instead of
133
+ scrolling out of sight. */
134
+ className: "h-auto min-h-(--size-control-md) flex-wrap gap-1 py-1 pl-1.5",
135
+ }), children: [chips.length === 0 ? null : (_jsx(Tags, { items: chips, ...(labelledBy === undefined ? {} : { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
136
+ /* Arriving at the field shows what can still be added, the way a many2one opens its
137
+ list on focus. With every tag chosen and no text there is nothing to show, and the
138
+ list stays closed. */
139
+ menuTrigger: "focus", onOpenChange: (isOpen) => {
140
+ if (isOpen)
141
+ setWidth(boxRef.current?.getBoundingClientRect().width ?? null);
142
+ }, isInvalid: invalid, isDisabled: disabled, className: "flex min-w-24 flex-1", children: [_jsx(TagInput, { inputRef: inputRef, placeholder: value.length === 0 ? (placeholder ?? copy.inlineEdit.addTag) : undefined, firstKey: listed[0]?.value ?? null, onTake: commit, onBackspace: () => {
143
+ const last = value.at(-1);
144
+ if (last !== undefined)
145
+ onChange?.(withoutTag(value, last));
146
+ }, empty: text === "" }), _jsx(Popover, { triggerRef: boxRef, ...(width === null
147
+ ? {}
148
+ : { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : (_jsx("span", { className: "truncate", children: item.label })) })) }) })] })] }));
149
+ }
150
+ /** The input after the chips. It reads the combobox's state to know whether the keyboard
151
+ * marked an option — Enter with nothing marked takes the first match. */
152
+ function TagInput({ inputRef, placeholder, firstKey, onTake, onBackspace, empty, }) {
153
+ const state = useContext(ComboBoxStateContext);
154
+ /* While typing, the first match is MARKED — so the row Enter is going to take is the row
155
+ the eye sees tinted, not a guess. Only when the keyboard has not marked another one. */
156
+ const open = state?.isOpen === true;
157
+ useEffect(() => {
158
+ if (state === null || !open || empty || firstKey === null)
159
+ return;
160
+ const focused = state.selectionManager.focusedKey;
161
+ if (focused === null || state.collection.getItem(focused) === null) {
162
+ state.selectionManager.setFocusedKey(firstKey);
163
+ }
164
+ }, [state, open, empty, firstKey]);
165
+ const onKeyDown = (event) => {
166
+ if (event.key === "Backspace" && empty) {
167
+ onBackspace();
168
+ return;
169
+ }
170
+ if (event.key !== "Enter" || empty || firstKey === null)
171
+ return;
172
+ if (state?.selectionManager.focusedKey != null)
173
+ return;
174
+ /* Stopped in the capture phase, so React Aria's own Enter — «commit nothing, reset the
175
+ text» — does not run after ours. */
176
+ event.preventDefault();
177
+ event.stopPropagation();
178
+ state?.close();
179
+ onTake(firstKey);
180
+ };
181
+ return (_jsx(Input, { ref: inputRef, placeholder: placeholder, onKeyDownCapture: onKeyDown, className: "h-(--size-control-sm) min-w-0 flex-1 bg-transparent px-1 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }));
182
+ }
@@ -0,0 +1,38 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ /**
3
+ * One tag that can be put on a record: what is stored (`value`, the id) and what is read
4
+ * (`label`), plus the tone it MEANS when it means something.
5
+ *
6
+ * Context: the same plain shape as `ChoiceOption` — the categories of a customer and the
7
+ * labels of an invoice come from data, and the ids go back to the backend as they came.
8
+ */
9
+ export interface TagOption {
10
+ value: string;
11
+ label: string;
12
+ /** Neutral by default: «mayorista» classifies, it does not warn. */
13
+ tone?: Tone;
14
+ }
15
+ /** How the typed text is compared — the locale's, handed in by the component. */
16
+ export type TagMatch = {
17
+ /** `label` contains `query`, ignoring case and accents. */
18
+ contains: (label: string, query: string) => boolean;
19
+ /** `a` and `b` are the same word, ignoring case and accents. */
20
+ same: (a: string, b: string) => boolean;
21
+ };
22
+ /**
23
+ * What the inline input offers for a given text.
24
+ *
25
+ * Context: pure and outside the component because it is the contract of the field — the
26
+ * chosen ones never come back as options (choosing «mayorista» twice is a duplicate, not a
27
+ * choice), the typed text filters, and «create» is offered only when the text names NO
28
+ * existing tag, chosen or not: creating «Mayorista» next to «mayorista» is how a catalog
29
+ * ends up with both.
30
+ */
31
+ export declare function tagChoices(options: readonly TagOption[], value: readonly string[], text: string, match: TagMatch, creatable: boolean): {
32
+ available: TagOption[];
33
+ create: string | null;
34
+ };
35
+ /** The value with one more id at the end; the same value when it is already there. */
36
+ export declare function withTag(value: readonly string[], id: string): string[];
37
+ /** The value without that id. */
38
+ export declare function withoutTag(value: readonly string[], id: string): string[];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What the inline input offers for a given text.
3
+ *
4
+ * Context: pure and outside the component because it is the contract of the field — the
5
+ * chosen ones never come back as options (choosing «mayorista» twice is a duplicate, not a
6
+ * choice), the typed text filters, and «create» is offered only when the text names NO
7
+ * existing tag, chosen or not: creating «Mayorista» next to «mayorista» is how a catalog
8
+ * ends up with both.
9
+ */
10
+ export function tagChoices(options, value, text, match, creatable) {
11
+ const chosen = new Set(value);
12
+ const query = text.trim();
13
+ const available = options.filter((option) => !chosen.has(option.value) && (query === "" || match.contains(option.label, query)));
14
+ const create = creatable && query !== "" && !options.some((option) => match.same(option.label, query))
15
+ ? query
16
+ : null;
17
+ return { available, create };
18
+ }
19
+ /** The value with one more id at the end; the same value when it is already there. */
20
+ export function withTag(value, id) {
21
+ return value.includes(id) ? [...value] : [...value, id];
22
+ }
23
+ /** The value without that id. */
24
+ export function withoutTag(value, id) {
25
+ return value.filter((one) => one !== id);
26
+ }
@@ -0,0 +1,4 @@
1
+ export { StatusDot, Tag } from "./Tag.js";
2
+ export { type TagItem, Tags } from "./Tags.js";
3
+ export { type TagOption } from "./choices.js";
4
+ export { TagsField, type TagsFieldProps } from "./TagsField.js";
@@ -0,0 +1,8 @@
1
+ /* Saying a state without writing a sentence: the capsule and the dot. */
2
+ export { StatusDot, Tag } from "./Tag.js";
3
+ /* La tira de fichas: la misma en la barra, en el desplegable y en cualquier
4
+ lugar donde se dibuje una lista de palabras cortas. */
5
+ export { Tags } from "./Tags.js";
6
+ /* El many2many: las fichas elegidas más una entrada que agrega la siguiente. */
7
+ export {} from "./choices.js";
8
+ export { TagsField } from "./TagsField.js";
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ export declare function Toolbar({ label, children, orientation, }: {
3
+ /** What the group is, read aloud on entering it: «Acciones de las líneas». A screen with
4
+ * two toolbars is two unnamed groups otherwise. */
5
+ label: string;
6
+ children: ReactNode;
7
+ orientation?: "horizontal" | "vertical";
8
+ }): import("react").JSX.Element;
9
+ /** A hairline between two groups of a `Toolbar`, across its direction. */
10
+ export declare function ToolbarSeparator({ orientation, }: {
11
+ /** `vertical` (the default) splits a horizontal toolbar; `horizontal`, a vertical one. */
12
+ orientation?: "horizontal" | "vertical";
13
+ }): import("react").JSX.Element;