@sincpro/web-ui 0.0.0-stage → 0.2.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 (376) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -2
  3. package/dist/components/activity/Activity.d.ts +69 -0
  4. package/dist/components/activity/Activity.js +279 -0
  5. package/dist/components/activity/Messages.d.ts +66 -0
  6. package/dist/components/activity/Messages.js +46 -0
  7. package/dist/components/activity/Timeline.d.ts +21 -0
  8. package/dist/components/activity/Timeline.js +30 -0
  9. package/dist/components/activity/index.d.ts +3 -0
  10. package/dist/components/activity/index.js +4 -0
  11. package/dist/components/assist/AssistMark.d.ts +64 -0
  12. package/dist/components/assist/AssistMark.js +108 -0
  13. package/dist/components/assist/AssistPending.d.ts +24 -0
  14. package/dist/components/assist/AssistPending.js +26 -0
  15. package/dist/components/assist/AssistSuggestion.d.ts +42 -0
  16. package/dist/components/assist/AssistSuggestion.js +55 -0
  17. package/dist/components/assist/ChangeReview.d.ts +56 -0
  18. package/dist/components/assist/ChangeReview.js +121 -0
  19. package/dist/components/assist/assist.d.ts +77 -0
  20. package/dist/components/assist/assist.js +87 -0
  21. package/dist/components/assist/index.d.ts +5 -0
  22. package/dist/components/assist/index.js +7 -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 +47 -0
  26. package/dist/components/form/Sheet.js +90 -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 +111 -0
  40. package/dist/components/lines/LineItems.js +317 -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 +112 -0
  52. package/dist/components/listing/ListingBar.js +144 -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 +110 -0
  60. package/dist/components/listing/Row.js +123 -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 +75 -0
  96. package/dist/components/record/RecordHeader.js +104 -0
  97. package/dist/components/record/RecordNotices.d.ts +37 -0
  98. package/dist/components/record/RecordNotices.js +75 -0
  99. package/dist/components/record/RecordSkeleton.d.ts +3 -0
  100. package/dist/components/record/RecordSkeleton.js +21 -0
  101. package/dist/components/record/SmartButtons.d.ts +35 -0
  102. package/dist/components/record/SmartButtons.js +38 -0
  103. package/dist/components/record/StatusBar.d.ts +51 -0
  104. package/dist/components/record/StatusBar.js +168 -0
  105. package/dist/components/record/ThumbBar.d.ts +36 -0
  106. package/dist/components/record/ThumbBar.js +86 -0
  107. package/dist/components/record/index.d.ts +6 -0
  108. package/dist/components/record/index.js +8 -0
  109. package/dist/components/search/CriteriaBuilder.d.ts +58 -0
  110. package/dist/components/search/CriteriaBuilder.js +278 -0
  111. package/dist/components/search/CriteriaChips.d.ts +22 -0
  112. package/dist/components/search/CriteriaChips.js +64 -0
  113. package/dist/components/search/FilterBuilder.d.ts +142 -0
  114. package/dist/components/search/FilterBuilder.js +421 -0
  115. package/dist/components/search/JoinWord.d.ts +8 -0
  116. package/dist/components/search/JoinWord.js +47 -0
  117. package/dist/components/search/JsonEditor.d.ts +19 -0
  118. package/dist/components/search/JsonEditor.js +78 -0
  119. package/dist/components/search/SearchBar.d.ts +78 -0
  120. package/dist/components/search/SearchBar.js +387 -0
  121. package/dist/components/search/criteria.d.ts +116 -0
  122. package/dist/components/search/criteria.js +159 -0
  123. package/dist/components/search/datePeriods.d.ts +122 -0
  124. package/dist/components/search/datePeriods.js +134 -0
  125. package/dist/components/search/dateRanges.d.ts +54 -0
  126. package/dist/components/search/dateRanges.js +89 -0
  127. package/dist/components/search/index.d.ts +9 -0
  128. package/dist/components/search/index.js +20 -0
  129. package/dist/components/search/model.d.ts +54 -0
  130. package/dist/components/search/model.js +37 -0
  131. package/dist/components/search/query.d.ts +166 -0
  132. package/dist/components/search/query.js +436 -0
  133. package/dist/components/shell/AppFrame.d.ts +14 -0
  134. package/dist/components/shell/AppFrame.js +80 -0
  135. package/dist/components/shell/TopBar.d.ts +73 -0
  136. package/dist/components/shell/TopBar.js +54 -0
  137. package/dist/components/shell/headerSlot.d.ts +17 -0
  138. package/dist/components/shell/headerSlot.js +21 -0
  139. package/dist/components/shell/index.d.ts +2 -0
  140. package/dist/components/shell/index.js +3 -0
  141. package/dist/foundations/aria.d.ts +14 -0
  142. package/dist/foundations/aria.js +18 -0
  143. package/dist/foundations/calendar.d.ts +28 -0
  144. package/dist/foundations/calendar.js +183 -0
  145. package/dist/foundations/copy.d.ts +703 -0
  146. package/dist/foundations/copy.es.d.ts +2 -0
  147. package/dist/foundations/copy.es.js +430 -0
  148. package/dist/foundations/copy.js +520 -0
  149. package/dist/foundations/density.d.ts +16 -0
  150. package/dist/foundations/density.js +21 -0
  151. package/dist/foundations/grouping.d.ts +281 -0
  152. package/dist/foundations/grouping.js +0 -0
  153. package/dist/foundations/index.d.ts +14 -0
  154. package/dist/foundations/index.js +30 -0
  155. package/dist/foundations/layout.d.ts +9 -0
  156. package/dist/foundations/layout.js +9 -0
  157. package/dist/foundations/pivot.d.ts +222 -0
  158. package/dist/foundations/pivot.js +198 -0
  159. package/dist/foundations/pivotSheet.d.ts +61 -0
  160. package/dist/foundations/pivotSheet.js +0 -0
  161. package/dist/foundations/slots.d.ts +67 -0
  162. package/dist/foundations/slots.js +63 -0
  163. package/dist/foundations/sticky.d.ts +53 -0
  164. package/dist/foundations/sticky.js +150 -0
  165. package/dist/foundations/surface.d.ts +9 -0
  166. package/dist/foundations/surface.js +26 -0
  167. package/dist/foundations/theme.d.ts +42 -0
  168. package/dist/foundations/theme.js +88 -0
  169. package/dist/foundations/types.d.ts +23 -0
  170. package/dist/foundations/types.js +50 -0
  171. package/dist/foundations/variants.d.ts +19 -0
  172. package/dist/foundations/variants.js +48 -0
  173. package/dist/foundations/xlsx.d.ts +86 -0
  174. package/dist/foundations/xlsx.js +0 -0
  175. package/dist/index.d.ts +19 -0
  176. package/dist/index.js +19 -0
  177. package/dist/primitives/block/Block.d.ts +66 -0
  178. package/dist/primitives/block/Block.js +34 -0
  179. package/dist/primitives/block/Line.d.ts +18 -0
  180. package/dist/primitives/block/Line.js +45 -0
  181. package/dist/primitives/block/SectionGrid.d.ts +30 -0
  182. package/dist/primitives/block/SectionGrid.js +29 -0
  183. package/dist/primitives/block/Steps.d.ts +76 -0
  184. package/dist/primitives/block/Steps.js +43 -0
  185. package/dist/primitives/block/index.d.ts +4 -0
  186. package/dist/primitives/block/index.js +7 -0
  187. package/dist/primitives/button/Button.d.ts +137 -0
  188. package/dist/primitives/button/Button.js +234 -0
  189. package/dist/primitives/button/index.d.ts +1 -0
  190. package/dist/primitives/button/index.js +2 -0
  191. package/dist/primitives/cell/CellStack.d.ts +47 -0
  192. package/dist/primitives/cell/CellStack.js +33 -0
  193. package/dist/primitives/cell/index.d.ts +1 -0
  194. package/dist/primitives/cell/index.js +3 -0
  195. package/dist/primitives/choice/ComboBox.d.ts +19 -0
  196. package/dist/primitives/choice/ComboBox.js +52 -0
  197. package/dist/primitives/choice/RadioGroup.d.ts +18 -0
  198. package/dist/primitives/choice/RadioGroup.js +50 -0
  199. package/dist/primitives/choice/SearchField.d.ts +33 -0
  200. package/dist/primitives/choice/SearchField.js +46 -0
  201. package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
  202. package/dist/primitives/choice/SegmentedControl.js +69 -0
  203. package/dist/primitives/choice/Slider.d.ts +16 -0
  204. package/dist/primitives/choice/Slider.js +46 -0
  205. package/dist/primitives/choice/Switch.d.ts +14 -0
  206. package/dist/primitives/choice/Switch.js +65 -0
  207. package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
  208. package/dist/primitives/choice/ThemeSwitcher.js +25 -0
  209. package/dist/primitives/choice/index.d.ts +8 -0
  210. package/dist/primitives/choice/index.js +11 -0
  211. package/dist/primitives/choice/options.d.ts +28 -0
  212. package/dist/primitives/choice/options.js +6 -0
  213. package/dist/primitives/date/Calendar.d.ts +31 -0
  214. package/dist/primitives/date/Calendar.js +134 -0
  215. package/dist/primitives/date/DatePicker.d.ts +42 -0
  216. package/dist/primitives/date/DatePicker.js +103 -0
  217. package/dist/primitives/date/DateTime.d.ts +35 -0
  218. package/dist/primitives/date/DateTime.js +100 -0
  219. package/dist/primitives/date/index.d.ts +3 -0
  220. package/dist/primitives/date/index.js +6 -0
  221. package/dist/primitives/dialog/Dialog.d.ts +118 -0
  222. package/dist/primitives/dialog/Dialog.js +121 -0
  223. package/dist/primitives/dialog/Drill.d.ts +90 -0
  224. package/dist/primitives/dialog/Drill.js +172 -0
  225. package/dist/primitives/dialog/index.d.ts +2 -0
  226. package/dist/primitives/dialog/index.js +4 -0
  227. package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
  228. package/dist/primitives/disclosure/Disclosure.js +52 -0
  229. package/dist/primitives/disclosure/index.d.ts +1 -0
  230. package/dist/primitives/disclosure/index.js +2 -0
  231. package/dist/primitives/field/Checks.d.ts +57 -0
  232. package/dist/primitives/field/Checks.js +72 -0
  233. package/dist/primitives/field/DropZone.d.ts +42 -0
  234. package/dist/primitives/field/DropZone.js +52 -0
  235. package/dist/primitives/field/Field.d.ts +178 -0
  236. package/dist/primitives/field/Field.js +251 -0
  237. package/dist/primitives/field/Suggest.d.ts +48 -0
  238. package/dist/primitives/field/Suggest.js +43 -0
  239. package/dist/primitives/field/aria.d.ts +61 -0
  240. package/dist/primitives/field/aria.js +70 -0
  241. package/dist/primitives/field/context.d.ts +30 -0
  242. package/dist/primitives/field/context.js +13 -0
  243. package/dist/primitives/field/copy.d.ts +16 -0
  244. package/dist/primitives/field/copy.js +69 -0
  245. package/dist/primitives/field/index.d.ts +9 -0
  246. package/dist/primitives/field/index.js +15 -0
  247. package/dist/primitives/field/layout.d.ts +24 -0
  248. package/dist/primitives/field/layout.js +10 -0
  249. package/dist/primitives/field/mode.d.ts +9 -0
  250. package/dist/primitives/field/mode.js +13 -0
  251. package/dist/primitives/field/note.d.ts +55 -0
  252. package/dist/primitives/field/note.js +117 -0
  253. package/dist/primitives/hint/Hint.d.ts +31 -0
  254. package/dist/primitives/hint/Hint.js +42 -0
  255. package/dist/primitives/hint/index.d.ts +1 -0
  256. package/dist/primitives/hint/index.js +1 -0
  257. package/dist/primitives/index.d.ts +20 -0
  258. package/dist/primitives/index.js +21 -0
  259. package/dist/primitives/menu/Menu.d.ts +128 -0
  260. package/dist/primitives/menu/Menu.js +89 -0
  261. package/dist/primitives/menu/index.d.ts +1 -0
  262. package/dist/primitives/menu/index.js +2 -0
  263. package/dist/primitives/notice/Callout.d.ts +49 -0
  264. package/dist/primitives/notice/Callout.js +28 -0
  265. package/dist/primitives/notice/Flag.d.ts +13 -0
  266. package/dist/primitives/notice/Flag.js +31 -0
  267. package/dist/primitives/notice/Notice.d.ts +45 -0
  268. package/dist/primitives/notice/Notice.js +61 -0
  269. package/dist/primitives/notice/Toast.d.ts +55 -0
  270. package/dist/primitives/notice/Toast.js +142 -0
  271. package/dist/primitives/notice/index.d.ts +4 -0
  272. package/dist/primitives/notice/index.js +6 -0
  273. package/dist/primitives/overlay/Popover.d.ts +6 -0
  274. package/dist/primitives/overlay/Popover.js +27 -0
  275. package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
  276. package/dist/primitives/overlay/PopoverTrigger.js +38 -0
  277. package/dist/primitives/overlay/Preview.d.ts +19 -0
  278. package/dist/primitives/overlay/Preview.js +60 -0
  279. package/dist/primitives/overlay/Tooltip.d.ts +38 -0
  280. package/dist/primitives/overlay/Tooltip.js +74 -0
  281. package/dist/primitives/overlay/index.d.ts +4 -0
  282. package/dist/primitives/overlay/index.js +7 -0
  283. package/dist/primitives/picker/Picker.d.ts +89 -0
  284. package/dist/primitives/picker/Picker.js +204 -0
  285. package/dist/primitives/picker/index.d.ts +1 -0
  286. package/dist/primitives/picker/index.js +2 -0
  287. package/dist/primitives/surface/Peek.d.ts +60 -0
  288. package/dist/primitives/surface/Peek.js +71 -0
  289. package/dist/primitives/surface/index.d.ts +1 -0
  290. package/dist/primitives/surface/index.js +2 -0
  291. package/dist/primitives/table/CellCopy.d.ts +20 -0
  292. package/dist/primitives/table/CellCopy.js +23 -0
  293. package/dist/primitives/table/CellEditor.d.ts +14 -0
  294. package/dist/primitives/table/CellEditor.js +83 -0
  295. package/dist/primitives/table/DataTable.d.ts +189 -0
  296. package/dist/primitives/table/DataTable.js +330 -0
  297. package/dist/primitives/table/EditableTable.d.ts +191 -0
  298. package/dist/primitives/table/EditableTable.js +617 -0
  299. package/dist/primitives/table/PivotTable.d.ts +14 -0
  300. package/dist/primitives/table/PivotTable.js +0 -0
  301. package/dist/primitives/table/ReportTable.d.ts +38 -0
  302. package/dist/primitives/table/ReportTable.js +106 -0
  303. package/dist/primitives/table/RowActions.d.ts +16 -0
  304. package/dist/primitives/table/RowActions.js +28 -0
  305. package/dist/primitives/table/SheetTable.d.ts +73 -0
  306. package/dist/primitives/table/SheetTable.js +230 -0
  307. package/dist/primitives/table/Table.d.ts +106 -0
  308. package/dist/primitives/table/Table.js +116 -0
  309. package/dist/primitives/table/index.d.ts +7 -0
  310. package/dist/primitives/table/index.js +9 -0
  311. package/dist/primitives/tabs/Tabs.d.ts +91 -0
  312. package/dist/primitives/tabs/Tabs.js +56 -0
  313. package/dist/primitives/tabs/index.d.ts +1 -0
  314. package/dist/primitives/tabs/index.js +1 -0
  315. package/dist/primitives/tag/Tag.d.ts +36 -0
  316. package/dist/primitives/tag/Tag.js +68 -0
  317. package/dist/primitives/tag/Tags.d.ts +57 -0
  318. package/dist/primitives/tag/Tags.js +104 -0
  319. package/dist/primitives/tag/TagsField.d.ts +35 -0
  320. package/dist/primitives/tag/TagsField.js +186 -0
  321. package/dist/primitives/tag/choices.d.ts +38 -0
  322. package/dist/primitives/tag/choices.js +26 -0
  323. package/dist/primitives/tag/index.d.ts +4 -0
  324. package/dist/primitives/tag/index.js +8 -0
  325. package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
  326. package/dist/primitives/toolbar/Toolbar.js +36 -0
  327. package/dist/primitives/toolbar/index.d.ts +1 -0
  328. package/dist/primitives/toolbar/index.js +2 -0
  329. package/dist/primitives/tree/Tree.d.ts +56 -0
  330. package/dist/primitives/tree/Tree.js +74 -0
  331. package/dist/primitives/tree/index.d.ts +1 -0
  332. package/dist/primitives/tree/index.js +3 -0
  333. package/dist/primitives/value/Copyable.d.ts +48 -0
  334. package/dist/primitives/value/Copyable.js +71 -0
  335. package/dist/primitives/value/Fecha.d.ts +12 -0
  336. package/dist/primitives/value/Fecha.js +89 -0
  337. package/dist/primitives/value/Meter.d.ts +29 -0
  338. package/dist/primitives/value/Meter.js +46 -0
  339. package/dist/primitives/value/Metric.d.ts +42 -0
  340. package/dist/primitives/value/Metric.js +38 -0
  341. package/dist/primitives/value/Person.d.ts +90 -0
  342. package/dist/primitives/value/Person.js +208 -0
  343. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  344. package/dist/primitives/value/ProgressBar.js +53 -0
  345. package/dist/primitives/value/Relation.d.ts +74 -0
  346. package/dist/primitives/value/Relation.js +74 -0
  347. package/dist/primitives/value/Totals.d.ts +121 -0
  348. package/dist/primitives/value/Totals.js +54 -0
  349. package/dist/primitives/value/Value.d.ts +40 -0
  350. package/dist/primitives/value/Value.js +80 -0
  351. package/dist/primitives/value/index.d.ts +9 -0
  352. package/dist/primitives/value/index.js +10 -0
  353. package/dist/tokens.css +1062 -0
  354. package/dist/views/index.d.ts +2 -0
  355. package/dist/views/index.js +3 -0
  356. package/dist/views/list/List.d.ts +119 -0
  357. package/dist/views/list/List.js +101 -0
  358. package/dist/views/list/ListView.d.ts +6 -0
  359. package/dist/views/list/ListView.js +295 -0
  360. package/dist/views/list/columns.d.ts +18 -0
  361. package/dist/views/list/columns.js +27 -0
  362. package/dist/views/list/derive.d.ts +87 -0
  363. package/dist/views/list/derive.js +207 -0
  364. package/dist/views/list/index.d.ts +2 -0
  365. package/dist/views/list/index.js +4 -0
  366. package/dist/views/list/presentations.d.ts +39 -0
  367. package/dist/views/list/presentations.js +105 -0
  368. package/dist/views/list/types.d.ts +284 -0
  369. package/dist/views/list/types.js +1 -0
  370. package/dist/views/list/useNarrowList.d.ts +17 -0
  371. package/dist/views/list/useNarrowList.js +32 -0
  372. package/dist/views/record/Record.d.ts +145 -0
  373. package/dist/views/record/Record.js +211 -0
  374. package/dist/views/record/index.d.ts +1 -0
  375. package/dist/views/record/index.js +2 -0
  376. package/package.json +113 -4
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { HeaderSlotContext } from "./headerSlot.js";
4
+ import { stickyTop, useStickyLayer } from "../../foundations/sticky.js";
5
+ import { useState } from "react";
6
+ /* ==================================================================== *
7
+ * THE APPLICATION FRAME — the three bands, and nothing between them.
8
+ *
9
+ * ┌───────────────────────────────────────────────────────┐
10
+ * │ menu │ ← sticky
11
+ * ├───────────────────────────────────────────────────────┤ ← no seam
12
+ * │ view │
13
+ * ├───────────────────────────────────────────────────────┤ ← no seam
14
+ * │ status │
15
+ * └───────────────────────────────────────────────────────┘
16
+ *
17
+ * It exists because the frame was handwritten in the application, with a
18
+ * `px-3 py-2.5` here and a `bg-surface` there, and every hole fix opened
19
+ * another one in another view. The decisions below are taken ONCE and hold
20
+ * for every view — list, record, form — which is what makes them look alike
21
+ * without anybody remembering to copy them.
22
+ *
23
+ * ── What this frame decides, and why ────────────────────────────────
24
+ *
25
+ * **The three bands touch.** No padding, no margin, no `gap` between the
26
+ * menu, the view and the status. Any gap here reads as a white border around
27
+ * the content: the dark band of a list stops reading as the continuation of
28
+ * the menu and starts looking like a loose card.
29
+ *
30
+ * **The view does not get padding from the frame.** Inner air is put by the
31
+ * view, which is the only one that knows whether it is drawing a table to
32
+ * both edges or a narrow form. A padding here would impose it on both.
33
+ *
34
+ * **Nothing forces the window height.** A short view ends where its content
35
+ * ends: without cascading `h-full` there is no leftover white under the
36
+ * status, and the page scrolls as a whole like any page.
37
+ *
38
+ * ── The sidebar ─────────────────────────────────────────────────────
39
+ *
40
+ * ┌───────────────────────────────────────────────────────┐
41
+ * │ menu │ ← sticky
42
+ * ├──────────┬────────────────────────────────────────────┤
43
+ * │ sidebar │ view │
44
+ * │ (sticky) │ …scrolls with the page… │
45
+ * ├──────────┴────────────────────────────────────────────┤
46
+ * │ status │
47
+ * └───────────────────────────────────────────────────────┘
48
+ *
49
+ * It sticks UNDER the menu, not at the top of the window: it reads the same
50
+ * measured `--sticky-top` the view's own sticky bands read, so a menu that
51
+ * wraps to two lines pushes the sidebar down with it. It is as tall as what
52
+ * is left of the window and scrolls on its own, so a long module never takes
53
+ * the page with it. The page still scrolls as a whole — only the view moves.
54
+ * Without a `sidebar` the frame is exactly what it was.
55
+ * ==================================================================== */
56
+ export function AppFrame({
57
+ /** The bar at the top. It stays in view while scrolling. */
58
+ menu,
59
+ /** The band at the bottom. It goes AFTER the content, not pinned to the
60
+ * floor of the window — pinning left a hole between it and a short view. */
61
+ status,
62
+ /** The module's navigation, on the left of the view: a `NavigationTree`. */
63
+ sidebar, children, }) {
64
+ /* La barra de arriba MIDE lo que ocupa y se lo pasa a lo de abajo: la barra
65
+ de una lista se pega debajo de ella, y el encabezado de una tabla debajo de
66
+ las dos. Ninguna de las tres tiene alto fijo — ver `system/sticky.tsx`. */
67
+ const bar = useStickyLayer();
68
+ /* The slot for a view's header (see `headerSlot.ts`): full width, stuck under the
69
+ application bar, and it pushes the stack down for the sidebar and the table header. */
70
+ const [slot, setSlot] = useState(null);
71
+ const head = useStickyLayer();
72
+ return (
73
+ /* The frame owns the page: its background and its full height. A screen built inside
74
+ it is only composition — before, every page had to wrap itself in a `div` with the
75
+ ground color and `min-h-dvh`, and the one that forgot ended where its content did. */
76
+ _jsxs("div", { className: "relative flex min-h-dvh min-w-0 flex-col bg-ground", children: [menu === undefined ? null : (_jsx("div", { ref: bar.ref, className: "sticky top-0 z-40", children: menu })), sidebar === undefined ? (_jsx("main", { className: "flex min-w-0 flex-1 flex-col", style: bar.below, children: children })) : (_jsx(HeaderSlotContext, { value: slot, children: _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", style: bar.below, children: [_jsx("div", { ref: (node) => {
77
+ head.ref.current = node;
78
+ setSlot(node);
79
+ }, className: "sticky z-30 min-w-0", style: head.self }), _jsxs("div", { className: "flex min-w-0 flex-1 items-start", style: head.below, children: [_jsx("aside", { className: "sticky w-(--container-sidebar) shrink-0 self-start overflow-y-auto overscroll-contain border-r border-line bg-ground", style: { ...stickyTop, height: "calc(100dvh - var(--sticky-top, 0px))" }, children: sidebar }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col self-stretch", children: children })] })] }) })), status] }));
80
+ }
@@ -0,0 +1,73 @@
1
+ import { type MenuGroup } from "../../primitives/menu/Menu.js";
2
+ import { type LucideIcon } from "lucide-react";
3
+ import { type ReactNode } from "react";
4
+ export interface AppEntry {
5
+ id: string;
6
+ label: string;
7
+ icon: LucideIcon;
8
+ /** One line: what this app is for. It shows in the switcher. */
9
+ hint?: string;
10
+ }
11
+ export interface TopMenu {
12
+ id: string;
13
+ label: string;
14
+ groups: readonly MenuGroup[];
15
+ }
16
+ export declare function TopBar({ app, apps, currentApp, onAppSelect, menus, onMenuSelect, systray, user, onUserSelect, userGroups, }: {
17
+ /** The product's name, when there is ONE product. It is drawn like the
18
+ * current app of the switcher —same icon, same weight— but without the
19
+ * switcher: nothing to switch to. */
20
+ app?: {
21
+ label: string;
22
+ icon?: LucideIcon;
23
+ };
24
+ /** Everything that exists. Without this there is no switcher — a product with
25
+ * a single app does not need one, and drawing it empty promises something
26
+ * that is not there. */
27
+ apps?: readonly AppEntry[];
28
+ currentApp?: string;
29
+ onAppSelect?: (id: string) => void;
30
+ /** The menus OF the current app. */
31
+ menus?: readonly TopMenu[];
32
+ onMenuSelect?: (menuId: string, optionId: string) => void;
33
+ /** What happened while I was not looking: activity, alerts. Icons with a count. */
34
+ systray?: ReactNode;
35
+ user?: {
36
+ name: string;
37
+ detail?: string;
38
+ };
39
+ userGroups?: readonly MenuGroup[];
40
+ onUserSelect?: (id: string) => void;
41
+ }): import("react").JSX.Element;
42
+ /** The app switcher: a grid, not a list.
43
+ *
44
+ * Context: there are six or eight, each with an icon, and they are picked by
45
+ * recognition rather than by reading. A vertical list forces you to read all
46
+ * six to find the one you already know; the grid shows them at once and the
47
+ * position gets learned. */
48
+ export declare function AppSwitcher({ apps, current, onSelect, }: {
49
+ apps: readonly AppEntry[];
50
+ current?: string;
51
+ onSelect?: (id: string) => void;
52
+ }): import("react").JSX.Element;
53
+ /** A systray icon with its count.
54
+ *
55
+ * Zero is NOT drawn: a bell with a «0» next to it forces you to read a number
56
+ * to discover there is nothing. With no count you can already see there is
57
+ * nothing. */
58
+ export declare function SystrayButton({ icon: Icon, label, count, tone, onClick, active, }: {
59
+ icon: LucideIcon;
60
+ label: string;
61
+ count?: number;
62
+ tone?: "info" | "warn" | "bad";
63
+ onClick?: () => void;
64
+ active?: boolean;
65
+ }): import("react").JSX.Element;
66
+ /** Who I am and how I leave. The name is drawn when there is width; when narrow
67
+ * only the avatar stays, which is what gets recognised without reading. */
68
+ export declare function UserMenu({ name, detail, groups, onSelect, }: {
69
+ name: string;
70
+ detail?: string;
71
+ groups: readonly MenuGroup[];
72
+ onSelect?: (id: string) => void;
73
+ }): import("react").JSX.Element;
@@ -0,0 +1,54 @@
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 { SurfaceProvider } from "../../foundations/surface.js";
5
+ import { Menu } from "../../primitives/menu/Menu.js";
6
+ import { Avatar } from "../../primitives/value/Person.js";
7
+ import { Grid3x3 } from "lucide-react";
8
+ import { useState } from "react";
9
+ export function TopBar({ app, apps, currentApp, onAppSelect, menus, onMenuSelect, systray, user, onUserSelect, userGroups, }) {
10
+ const copy = useUiCopy();
11
+ const actual = apps?.find((a) => a.id === currentApp);
12
+ const brand = app ?? actual;
13
+ return (_jsx(SurfaceProvider, { value: "chrome", children: _jsxs("header", { className: "flex h-(--size-appbar) min-w-0 items-center gap-1 border-b border-chrome-edge bg-chrome px-2", children: [apps === undefined || apps.length === 0 ? null : (_jsx(AppSwitcher, { apps: apps, current: currentApp, ...(onAppSelect === undefined ? {} : { onSelect: onAppSelect }) })), brand === undefined ? null : (_jsxs("span", { className: "flex shrink-0 items-center gap-2 pr-2 pl-1", children: [brand.icon === undefined ? null : (_jsx(brand.icon, { className: "size-(--size-icon-md) text-chrome-link", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: "text-lead font-medium text-chrome-ink", children: brand.label })] })), menus === undefined ? null : (_jsx("nav", { className: "flex min-w-0 items-center gap-0.5 overflow-hidden", "aria-label": copy.shell.menus, children: menus.map((m) => (_jsx(Menu, { label: m.label, groups: m.groups, surface: "chrome", variant: "text", chevron: false, onSelect: (id) => onMenuSelect?.(m.id, id) }, m.id))) })), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1 pl-2", children: [systray, user === undefined ? null : (_jsx(UserMenu, { name: user.name, ...(user.detail === undefined ? {} : { detail: user.detail }), groups: userGroups ?? [], ...(onUserSelect === undefined ? {} : { onSelect: onUserSelect }) }))] })] }) }));
14
+ }
15
+ /** The app switcher: a grid, not a list.
16
+ *
17
+ * Context: there are six or eight, each with an icon, and they are picked by
18
+ * recognition rather than by reading. A vertical list forces you to read all
19
+ * six to find the one you already know; the grid shows them at once and the
20
+ * position gets learned. */
21
+ export function AppSwitcher({ apps, current, onSelect, }) {
22
+ const [open, setOpen] = useState(false);
23
+ const copy = useUiCopy();
24
+ return (_jsxs("div", { className: "relative shrink-0", children: [_jsx("button", { type: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": copy.shell.modules, title: copy.shell.modules, className: `inline-flex size-(--size-control-md) items-center justify-center rounded-pill transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-chrome-link ${open
25
+ ? "bg-chrome-ink/12 text-chrome-ink"
26
+ : "text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink"}`, children: _jsx(Grid3x3, { className: "size-(--size-icon-md)", strokeWidth: 2 }) }), !open ? null : (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40", onClick: () => setOpen(false), role: "presentation" }), _jsx("div", { role: "menu", className: "absolute top-full left-0 z-50 mt-1 grid w-(--container-apps) grid-cols-3 gap-1 rounded-panel border border-line bg-surface p-2 shadow-float", children: apps.map((a) => {
27
+ const here = a.id === current;
28
+ return (_jsxs("button", { type: "button", role: "menuitem", title: a.hint, onClick: () => {
29
+ onSelect?.(a.id);
30
+ setOpen(false);
31
+ }, className: `flex flex-col items-center gap-1.5 rounded-box px-2 py-3 text-center transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${here ? "bg-primary/8 text-primary" : "text-ink hover:bg-ink/5"}`, children: [_jsx(a.icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75, "aria-hidden": true }), _jsx("span", { className: "text-micro leading-tight", children: a.label })] }, a.id));
32
+ }) })] }))] }));
33
+ }
34
+ /** A systray icon with its count.
35
+ *
36
+ * Zero is NOT drawn: a bell with a «0» next to it forces you to read a number
37
+ * to discover there is nothing. With no count you can already see there is
38
+ * nothing. */
39
+ export function SystrayButton({ icon: Icon, label, count = 0, tone = "info", onClick, active = false, }) {
40
+ const PUNTO = {
41
+ info: "bg-chrome-link text-chrome",
42
+ warn: "bg-chrome-warn text-chrome",
43
+ bad: "bg-chrome-bad text-chrome",
44
+ };
45
+ return (_jsxs("button", { type: "button", onClick: onClick, title: count > 0 ? `${label} (${count})` : label, "aria-label": count > 0 ? `${label}: ${count}` : label, className: `relative inline-flex size-(--size-control-md) items-center justify-center rounded-pill transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-chrome-link ${active
46
+ ? "bg-chrome-ink/12 text-chrome-ink"
47
+ : "text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink"}`, children: [_jsx(Icon, { className: "size-(--size-icon-md)", strokeWidth: 2, "aria-hidden": true }), count <= 0 ? null : (_jsx("span", { "aria-hidden": true, className: `absolute -top-0.5 -right-0.5 inline-flex min-w-4 items-center justify-center rounded-pill px-1 text-micro leading-none font-semibold tabular-nums ${PUNTO[tone]}`, children: count > 99 ? "99+" : count }))] }));
48
+ }
49
+ /** Who I am and how I leave. The name is drawn when there is width; when narrow
50
+ * only the avatar stays, which is what gets recognised without reading. */
51
+ export function UserMenu({ name, detail, groups, onSelect, }) {
52
+ const copy = useUiCopy();
53
+ return (_jsxs("span", { className: "flex shrink-0 items-center gap-2 pl-1", children: [_jsx(Avatar, { name: name, size: "md" }), _jsxs("span", { className: "hidden min-w-0 flex-col leading-tight sm:flex", children: [_jsx("span", { className: "truncate text-meta text-chrome-ink", children: name }), detail === undefined ? null : (_jsx("span", { className: "truncate text-micro text-chrome-muted", children: detail }))] }), _jsx(Menu, { label: copy.shell.account, groups: groups, surface: "chrome", variant: "text", align: "end", iconOnly: true, ...(onSelect === undefined ? {} : { onSelect }) })] }));
54
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Context: **where a view's header goes when the frame has a sidebar.**
3
+ *
4
+ * With a sidebar the screen is a grid: the application bar on top, the sidebar on the
5
+ * left, the view on the right. A list's bar (crumbs, search, pagination, actions, sort,
6
+ * presentation switcher) is the header of the SCREEN, not of the column to the right of
7
+ * the sidebar — drawn inside that column it started at the sidebar's edge and the
8
+ * sidebar, taller and darker, won the top-left of the page. The frame offers a full-width
9
+ * slot between the application bar and the sidebar row; a view that has a header renders
10
+ * it into the slot (a portal, so its context — density, surface, copy — comes along).
11
+ *
12
+ * `null` = there is no slot (no sidebar, or the slot has not mounted yet): the view draws
13
+ * its header where it always did. Not exported from the package: it is the contract
14
+ * between `AppFrame` and the views, not something an app writes.
15
+ */
16
+ export declare const HeaderSlotContext: import("react").Context<HTMLElement | null>;
17
+ export declare function useHeaderSlot(): HTMLElement | null;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ /**
4
+ * Context: **where a view's header goes when the frame has a sidebar.**
5
+ *
6
+ * With a sidebar the screen is a grid: the application bar on top, the sidebar on the
7
+ * left, the view on the right. A list's bar (crumbs, search, pagination, actions, sort,
8
+ * presentation switcher) is the header of the SCREEN, not of the column to the right of
9
+ * the sidebar — drawn inside that column it started at the sidebar's edge and the
10
+ * sidebar, taller and darker, won the top-left of the page. The frame offers a full-width
11
+ * slot between the application bar and the sidebar row; a view that has a header renders
12
+ * it into the slot (a portal, so its context — density, surface, copy — comes along).
13
+ *
14
+ * `null` = there is no slot (no sidebar, or the slot has not mounted yet): the view draws
15
+ * its header where it always did. Not exported from the package: it is the contract
16
+ * between `AppFrame` and the views, not something an app writes.
17
+ */
18
+ export const HeaderSlotContext = createContext(null);
19
+ export function useHeaderSlot() {
20
+ return useContext(HeaderSlotContext);
21
+ }
@@ -0,0 +1,2 @@
1
+ export { AppFrame } from "./AppFrame.js";
2
+ export { type AppEntry, SystrayButton, TopBar, type TopMenu, } from "./TopBar.js";
@@ -0,0 +1,3 @@
1
+ /* EL MARCO DE LA APLICACIÓN — lo que está arriba de toda vista. */
2
+ export { AppFrame } from "./AppFrame.js";
3
+ export { SystrayButton, TopBar, } from "./TopBar.js";
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * The locale React Aria formats with — month names, the order of day/month/year in a date
4
+ * field, the first day of the week — taken from the copy table, the one locale the whole
5
+ * system speaks.
6
+ *
7
+ * Context: React Aria reads the browser's language when nobody tells it. An app installed
8
+ * in Spanish on a browser in English would show a Spanish form with a date field in
9
+ * month/day/year — the one place in the screen that reads in another language, and the
10
+ * one where a wrong order changes the data.
11
+ */
12
+ export declare function AriaLocale({ children }: {
13
+ children: ReactNode;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useUiCopy } from "./copy.js";
4
+ import { I18nProvider } from "react-aria-components";
5
+ /**
6
+ * The locale React Aria formats with — month names, the order of day/month/year in a date
7
+ * field, the first day of the week — taken from the copy table, the one locale the whole
8
+ * system speaks.
9
+ *
10
+ * Context: React Aria reads the browser's language when nobody tells it. An app installed
11
+ * in Spanish on a browser in English would show a Spanish form with a date field in
12
+ * month/day/year — the one place in the screen that reads in another language, and the
13
+ * one where a wrong order changes the data.
14
+ */
15
+ export function AriaLocale({ children }) {
16
+ const copy = useUiCopy();
17
+ return _jsx(I18nProvider, { locale: copy.locale, children: children });
18
+ }
@@ -0,0 +1,28 @@
1
+ import type { GroupGrain } from "./grouping.js";
2
+ /**
3
+ * Una fecha recortada a la CLAVE de su grano.
4
+ *
5
+ * El texto es el que produce el motor —«2026», «2026-Q3», «2026-09», «2026-W38»,
6
+ * «2026-09-15»—, y que los dos lados lo escriban igual es lo que deja comparar
7
+ * un agrupado hecho en el navegador con uno contado a mil kilómetros. Sin fecha
8
+ * la clave es vacía: un montón que existe —el de los que no tienen— y que se
9
+ * dibuja con las palabras de la copia.
10
+ */
11
+ export declare function dateKey(when: Date | string | number | null | undefined, grain: GroupGrain): string;
12
+ /**
13
+ * El RÓTULO de una clave de fecha, en el idioma activo.
14
+ *
15
+ * `within` son los granos que los montones de arriba ya dijeron: con el año
16
+ * arriba, un mes es «Septiembre» y no «Septiembre de 2026». Una clave que no
17
+ * tiene la forma de su grano se dibuja TAL CUAL —es lo que hace que un montón
18
+ * contado afuera, con una clave que esta capa no escribió, siga siendo legible
19
+ * en vez de desaparecer detrás de una fecha inventada.
20
+ */
21
+ export declare function dateLabel(key: string, grain: GroupGrain, within?: readonly GroupGrain[]): string;
22
+ /** El nombre de un mes —1 a 12— en el idioma activo. Lo usa el que ofrece meses
23
+ * sin tener ninguna fecha en la mano, como el recorte por período. */
24
+ export declare function monthName(month: number): string;
25
+ /** Cómo se llama un GRANO —«día», «semana»—, que es distinto de cómo se llama un
26
+ * montón suyo: el grano nombra el corte y el rótulo nombra lo cortado. Sale de
27
+ * la copia, que es donde ya viven las unidades de tiempo en singular. */
28
+ export declare function grainName(grain: GroupGrain): string;
@@ -0,0 +1,183 @@
1
+ import { currentCopy, currentCopyLocale, fillCopy } from "./copy.js";
2
+ /**
3
+ * EL CALENDARIO: de una fecha a una CLAVE, y de una clave a un RÓTULO.
4
+ *
5
+ * Context: un montón de fecha tiene dos textos y no son el mismo. «2026-09» es
6
+ * lo que el motor escribe —lo que identifica al montón, lo que se compara con lo
7
+ * que contó el servidor, lo que se guarda plegado— y «Septiembre» es lo que se
8
+ * lee. Escribir uno solo obliga a elegir cuál se pierde: con la clave a la vista
9
+ * la interfaz habla en máquina, y con el rótulo por identidad dos septiembres de
10
+ * años distintos se funden en un montón que no existe.
11
+ *
12
+ * Por eso son dos funciones y no una:
13
+ *
14
+ * dateKey(when, "month") → «2026-09» la identidad, en un solo idioma
15
+ * dateLabel("2026-09", …) → «Septiembre» el rótulo, en el idioma activo
16
+ *
17
+ * ── El idioma no se traduce: se PIDE ────────────────────────────────
18
+ *
19
+ * Los meses y los días de la semana los dice `Intl` con el `locale` que la copia
20
+ * tiene puesto, así que no hay acá ninguna tabla de doce meses en español ni en
21
+ * inglés — ni la habría que escribir para un tercer idioma. Lo único que sí es
22
+ * copia son la semana y el trimestre, porque ningún calendario los nombra: «W38»
23
+ * no es una palabra y `Intl` no la tiene.
24
+ *
25
+ * ── Un rótulo dice lo que el de arriba NO dijo ──────────────────────
26
+ *
27
+ * Un árbol de fechas se lee de arriba hacia abajo, y repetir el año en cada mes
28
+ * y el mes en cada día llena la sangría de texto que ya está dos renglones más
29
+ * arriba:
30
+ *
31
+ * 2026 ← el año lo dice una vez
32
+ * Septiembre ← no «Septiembre de 2026»
33
+ * Semana 38 ← no «Semana 38 de 2026»
34
+ * Martes ← no «martes, 15 sept 2026»
35
+ *
36
+ * De eso se ocupa `within`: los granos que los montones de arriba —del mismo
37
+ * campo— ya dijeron. Sin nadie arriba, el rótulo se planta solo y dice la fecha
38
+ * entera, porque un «Martes» suelto no es ningún día.
39
+ */
40
+ /**
41
+ * La forma de la clave de cada grano. Es `GroupGrain` entero: un grano que el
42
+ * calendario no supiera cortar sería un nivel que no parte nada.
43
+ *
44
+ * Los rangos van en la expresión y no en un `if` porque `new Date(2026, 12, 1)`
45
+ * no falla: se corre a enero de 2027. Una clave «2026-13» dibujada como «Enero»
46
+ * es un mes que nadie contó, y eso es peor que un rótulo ilegible.
47
+ */
48
+ const KEY_SHAPES = {
49
+ year: /^(\d{4})$/,
50
+ quarter: /^(\d{4})-Q([1-4])$/,
51
+ month: /^(\d{4})-(0[1-9]|1[0-2])$/,
52
+ week: /^(\d{4})-W(0[1-9]|[1-4]\d|5[0-3])$/,
53
+ day: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
54
+ };
55
+ /**
56
+ * Una fecha recortada a la CLAVE de su grano.
57
+ *
58
+ * El texto es el que produce el motor —«2026», «2026-Q3», «2026-09», «2026-W38»,
59
+ * «2026-09-15»—, y que los dos lados lo escriban igual es lo que deja comparar
60
+ * un agrupado hecho en el navegador con uno contado a mil kilómetros. Sin fecha
61
+ * la clave es vacía: un montón que existe —el de los que no tienen— y que se
62
+ * dibuja con las palabras de la copia.
63
+ */
64
+ export function dateKey(when, grain) {
65
+ if (when === null || when === undefined || when === "")
66
+ return "";
67
+ const date = when instanceof Date ? when : new Date(when);
68
+ if (Number.isNaN(date.getTime()))
69
+ return "";
70
+ const year = date.getFullYear();
71
+ const month = date.getMonth() + 1;
72
+ if (grain === "year")
73
+ return String(year);
74
+ if (grain === "quarter")
75
+ return `${year}-Q${Math.floor(date.getMonth() / 3) + 1}`;
76
+ if (grain === "month")
77
+ return `${year}-${pad(month)}`;
78
+ if (grain === "week") {
79
+ const [weekYear, week] = isoWeek(date);
80
+ return `${weekYear}-W${pad(week)}`;
81
+ }
82
+ return `${year}-${pad(month)}-${pad(date.getDate())}`;
83
+ }
84
+ /**
85
+ * El RÓTULO de una clave de fecha, en el idioma activo.
86
+ *
87
+ * `within` son los granos que los montones de arriba ya dijeron: con el año
88
+ * arriba, un mes es «Septiembre» y no «Septiembre de 2026». Una clave que no
89
+ * tiene la forma de su grano se dibuja TAL CUAL —es lo que hace que un montón
90
+ * contado afuera, con una clave que esta capa no escribió, siga siendo legible
91
+ * en vez de desaparecer detrás de una fecha inventada.
92
+ */
93
+ export function dateLabel(key, grain, within = []) {
94
+ const copy = currentCopy();
95
+ if (key === "")
96
+ return copy.time.noDate;
97
+ const parts = KEY_SHAPES[grain].exec(key);
98
+ if (parts === null)
99
+ return key;
100
+ const locale = currentCopyLocale();
101
+ const year = Number(parts[1]);
102
+ const said = (one) => within.includes(one);
103
+ if (grain === "year")
104
+ return String(year);
105
+ if (grain === "quarter") {
106
+ const quarter = Number(parts[2]);
107
+ return said("year")
108
+ ? fillCopy(copy.calendar.quarter, { n: quarter })
109
+ : fillCopy(copy.calendar.quarterOfYear, { n: quarter, year });
110
+ }
111
+ if (grain === "week") {
112
+ const week = Number(parts[2]);
113
+ /* El mes también alcanza: una semana adentro de septiembre ya sabe de qué
114
+ año es, porque el mes lo dijo. */
115
+ return said("year") || said("quarter") || said("month")
116
+ ? fillCopy(copy.calendar.week, { n: week })
117
+ : fillCopy(copy.calendar.weekOfYear, { n: week, year });
118
+ }
119
+ if (grain === "month") {
120
+ const when = new Date(year, Number(parts[2]) - 1, 1);
121
+ return capitalized(said("year")
122
+ ? format(locale, { month: "long" }).format(when)
123
+ : format(locale, { month: "long", year: "numeric" }).format(when));
124
+ }
125
+ const day = Number(parts[3]);
126
+ const when = new Date(year, Number(parts[2]) - 1, day);
127
+ /* El 31 de febrero tiene la forma de un día y no es ninguno: `Date` lo corre
128
+ al 3 de marzo en vez de quejarse, así que se pregunta si volvió el mismo. */
129
+ if (when.getDate() !== day)
130
+ return key;
131
+ /* Adentro de una semana, el día es su NOMBRE: son siete y ninguno se repite,
132
+ así que «Martes» alcanza y el número de día sería ruido. Afuera no: un
133
+ «Martes» suelto no dice cuál de los cincuenta y dos. */
134
+ if (said("week"))
135
+ return capitalized(format(locale, { weekday: "long" }).format(when));
136
+ return capitalized(format(locale, {
137
+ weekday: "long",
138
+ day: "numeric",
139
+ month: "short",
140
+ ...(said("year") ? {} : { year: "numeric" }),
141
+ }).format(when));
142
+ }
143
+ /** El nombre de un mes —1 a 12— en el idioma activo. Lo usa el que ofrece meses
144
+ * sin tener ninguna fecha en la mano, como el recorte por período. */
145
+ export function monthName(month) {
146
+ return capitalized(format(currentCopyLocale(), { month: "long" }).format(new Date(2000, month - 1, 1)));
147
+ }
148
+ /** Cómo se llama un GRANO —«día», «semana»—, que es distinto de cómo se llama un
149
+ * montón suyo: el grano nombra el corte y el rótulo nombra lo cortado. Sale de
150
+ * la copia, que es donde ya viven las unidades de tiempo en singular. */
151
+ export function grainName(grain) {
152
+ return capitalized(currentCopy().dates[grain]);
153
+ }
154
+ /**
155
+ * El año y la semana ISO de una fecha.
156
+ *
157
+ * La semana 1 es la que tiene el primer jueves del año, así que se calcula con
158
+ * el jueves de la semana del dato: es él quien decide a qué año pertenece, y por
159
+ * eso una semana de fin de diciembre puede ser la 1 del año siguiente. El año
160
+ * que sale de acá es el de ESA semana y no el de la fecha.
161
+ */
162
+ function isoWeek(date) {
163
+ const thursday = new Date(date.getFullYear(), date.getMonth(), date.getDate());
164
+ thursday.setDate(thursday.getDate() + 3 - ((thursday.getDay() + 6) % 7));
165
+ const first = new Date(thursday.getFullYear(), 0, 4);
166
+ const week = 1 +
167
+ Math.round(((thursday.getTime() - first.getTime()) / 86_400_000 - 3 + ((first.getDay() + 6) % 7)) /
168
+ 7);
169
+ return [thursday.getFullYear(), week];
170
+ }
171
+ function format(locale, options) {
172
+ return new Intl.DateTimeFormat(locale, options);
173
+ }
174
+ /** Un rótulo encabeza un montón, y en español `Intl` devuelve «septiembre» con
175
+ * minúscula. Se sube la primera letra con el idioma activo, que es el que sabe
176
+ * cuál es la mayúscula de esa letra. */
177
+ function capitalized(text) {
178
+ const first = text.charAt(0);
179
+ return first === "" ? text : first.toLocaleUpperCase(currentCopyLocale()) + text.slice(1);
180
+ }
181
+ function pad(value) {
182
+ return String(value).padStart(2, "0");
183
+ }