@zyncat/ui 0.8.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 (334) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/alert.css +132 -0
  4. package/dist/alert.d.ts +46 -0
  5. package/dist/alert.js +80 -0
  6. package/dist/alert.js.map +1 -0
  7. package/dist/avatar-group.d.ts +24 -0
  8. package/dist/avatar-group.js +23 -0
  9. package/dist/avatar-group.js.map +1 -0
  10. package/dist/avatar.css +200 -0
  11. package/dist/avatar.d.ts +35 -0
  12. package/dist/avatar.js +67 -0
  13. package/dist/avatar.js.map +1 -0
  14. package/dist/badge.css +182 -0
  15. package/dist/badge.d.ts +34 -0
  16. package/dist/badge.js +4 -0
  17. package/dist/badge.js.map +1 -0
  18. package/dist/button.css +269 -0
  19. package/dist/button.d.ts +44 -0
  20. package/dist/button.js +4 -0
  21. package/dist/button.js.map +1 -0
  22. package/dist/checkbox.css +177 -0
  23. package/dist/checkbox.d.ts +35 -0
  24. package/dist/checkbox.js +5 -0
  25. package/dist/checkbox.js.map +1 -0
  26. package/dist/chunk-2ERBBFZS.js +12 -0
  27. package/dist/chunk-2ERBBFZS.js.map +1 -0
  28. package/dist/chunk-2HORSLXJ.js +191 -0
  29. package/dist/chunk-2HORSLXJ.js.map +1 -0
  30. package/dist/chunk-36WVAQKL.js +40 -0
  31. package/dist/chunk-36WVAQKL.js.map +1 -0
  32. package/dist/chunk-3SBCBYB7.js +575 -0
  33. package/dist/chunk-3SBCBYB7.js.map +1 -0
  34. package/dist/chunk-4R2GQO43.js +125 -0
  35. package/dist/chunk-4R2GQO43.js.map +1 -0
  36. package/dist/chunk-64UU4LGU.js +258 -0
  37. package/dist/chunk-64UU4LGU.js.map +1 -0
  38. package/dist/chunk-7SZCJ7WI.js +35 -0
  39. package/dist/chunk-7SZCJ7WI.js.map +1 -0
  40. package/dist/chunk-BIDVNLJO.js +38 -0
  41. package/dist/chunk-BIDVNLJO.js.map +1 -0
  42. package/dist/chunk-CONSWNIV.js +17 -0
  43. package/dist/chunk-CONSWNIV.js.map +1 -0
  44. package/dist/chunk-EQ32F2QJ.js +75 -0
  45. package/dist/chunk-EQ32F2QJ.js.map +1 -0
  46. package/dist/chunk-GY7JOPUQ.js +111 -0
  47. package/dist/chunk-GY7JOPUQ.js.map +1 -0
  48. package/dist/chunk-I4XQ4PV2.js +30 -0
  49. package/dist/chunk-I4XQ4PV2.js.map +1 -0
  50. package/dist/chunk-JSKXZMLH.js +67 -0
  51. package/dist/chunk-JSKXZMLH.js.map +1 -0
  52. package/dist/chunk-L3Q3VDIW.js +295 -0
  53. package/dist/chunk-L3Q3VDIW.js.map +1 -0
  54. package/dist/chunk-LDC3RIYA.js +62 -0
  55. package/dist/chunk-LDC3RIYA.js.map +1 -0
  56. package/dist/chunk-MKIQFK7S.js +43 -0
  57. package/dist/chunk-MKIQFK7S.js.map +1 -0
  58. package/dist/chunk-MMRIQNEN.js +20 -0
  59. package/dist/chunk-MMRIQNEN.js.map +1 -0
  60. package/dist/chunk-MXOPCFWR.js +59 -0
  61. package/dist/chunk-MXOPCFWR.js.map +1 -0
  62. package/dist/chunk-NEF5XAHG.js +17 -0
  63. package/dist/chunk-NEF5XAHG.js.map +1 -0
  64. package/dist/chunk-NNXJCLTV.js +22 -0
  65. package/dist/chunk-NNXJCLTV.js.map +1 -0
  66. package/dist/chunk-O4N2OBTR.js +94 -0
  67. package/dist/chunk-O4N2OBTR.js.map +1 -0
  68. package/dist/chunk-PMECHR3A.js +38 -0
  69. package/dist/chunk-PMECHR3A.js.map +1 -0
  70. package/dist/chunk-QDK66S4G.js +44 -0
  71. package/dist/chunk-QDK66S4G.js.map +1 -0
  72. package/dist/chunk-QGNC7LH5.js +8 -0
  73. package/dist/chunk-QGNC7LH5.js.map +1 -0
  74. package/dist/chunk-QMSJZCK3.js +398 -0
  75. package/dist/chunk-QMSJZCK3.js.map +1 -0
  76. package/dist/chunk-S5ZTZANS.js +57 -0
  77. package/dist/chunk-S5ZTZANS.js.map +1 -0
  78. package/dist/chunk-S7B34VGN.js +14 -0
  79. package/dist/chunk-S7B34VGN.js.map +1 -0
  80. package/dist/chunk-TCUCYFT7.js +25 -0
  81. package/dist/chunk-TCUCYFT7.js.map +1 -0
  82. package/dist/chunk-TYHR4VNM.js +193 -0
  83. package/dist/chunk-TYHR4VNM.js.map +1 -0
  84. package/dist/chunk-U5QQZC3U.js +56 -0
  85. package/dist/chunk-U5QQZC3U.js.map +1 -0
  86. package/dist/chunk-UE63OOVG.js +13 -0
  87. package/dist/chunk-UE63OOVG.js.map +1 -0
  88. package/dist/chunk-VTJTGVZZ.js +22 -0
  89. package/dist/chunk-VTJTGVZZ.js.map +1 -0
  90. package/dist/chunk-W3SHPKSB.js +49 -0
  91. package/dist/chunk-W3SHPKSB.js.map +1 -0
  92. package/dist/chunk-XZAEIQDR.js +188 -0
  93. package/dist/chunk-XZAEIQDR.js.map +1 -0
  94. package/dist/chunk-YGGCQNNU.js +92 -0
  95. package/dist/chunk-YGGCQNNU.js.map +1 -0
  96. package/dist/collapse.css +88 -0
  97. package/dist/collapse.d.ts +46 -0
  98. package/dist/collapse.js +5 -0
  99. package/dist/collapse.js.map +1 -0
  100. package/dist/count-badge.d.ts +13 -0
  101. package/dist/count-badge.js +13 -0
  102. package/dist/count-badge.js.map +1 -0
  103. package/dist/date-field.d.ts +25 -0
  104. package/dist/date-field.js +87 -0
  105. package/dist/date-field.js.map +1 -0
  106. package/dist/date-picker.css +595 -0
  107. package/dist/date-range-field.d.ts +33 -0
  108. package/dist/date-range-field.js +404 -0
  109. package/dist/date-range-field.js.map +1 -0
  110. package/dist/datetime-field.d.ts +29 -0
  111. package/dist/datetime-field.js +155 -0
  112. package/dist/datetime-field.js.map +1 -0
  113. package/dist/dialog.css +180 -0
  114. package/dist/dialog.d.ts +42 -0
  115. package/dist/dialog.js +128 -0
  116. package/dist/dialog.js.map +1 -0
  117. package/dist/dom-props-C0jrllBw.d.ts +5 -0
  118. package/dist/field-chrome-CMPOQSLs.d.ts +28 -0
  119. package/dist/field-shell-Cuda-feL.d.ts +24 -0
  120. package/dist/glide.d.ts +15 -0
  121. package/dist/glide.js +5 -0
  122. package/dist/glide.js.map +1 -0
  123. package/dist/icon.css +33 -0
  124. package/dist/input.css +333 -0
  125. package/dist/mcp.js +359 -0
  126. package/dist/motion-devtools.css +265 -0
  127. package/dist/motion-devtools.d.ts +51 -0
  128. package/dist/motion-devtools.js +354 -0
  129. package/dist/motion-devtools.js.map +1 -0
  130. package/dist/motion-scale-Canq4stZ.d.ts +18 -0
  131. package/dist/motion-tokens.d.ts +28 -0
  132. package/dist/motion-tokens.js +3 -0
  133. package/dist/motion-tokens.js.map +1 -0
  134. package/dist/multi-select.d.ts +44 -0
  135. package/dist/multi-select.js +109 -0
  136. package/dist/multi-select.js.map +1 -0
  137. package/dist/number-field.d.ts +36 -0
  138. package/dist/number-field.js +112 -0
  139. package/dist/number-field.js.map +1 -0
  140. package/dist/odometer.css +62 -0
  141. package/dist/otp-field.d.ts +31 -0
  142. package/dist/otp-field.js +98 -0
  143. package/dist/otp-field.js.map +1 -0
  144. package/dist/overlay.css +23 -0
  145. package/dist/pagination.css +43 -0
  146. package/dist/pagination.d.ts +29 -0
  147. package/dist/pagination.js +107 -0
  148. package/dist/pagination.js.map +1 -0
  149. package/dist/popover.css +69 -0
  150. package/dist/popover.d.ts +35 -0
  151. package/dist/popover.js +15 -0
  152. package/dist/popover.js.map +1 -0
  153. package/dist/radio-group.css +300 -0
  154. package/dist/radio-group.d.ts +52 -0
  155. package/dist/radio-group.js +115 -0
  156. package/dist/radio-group.js.map +1 -0
  157. package/dist/select.css +416 -0
  158. package/dist/select.d.ts +49 -0
  159. package/dist/select.js +99 -0
  160. package/dist/select.js.map +1 -0
  161. package/dist/sheet.css +26 -0
  162. package/dist/sheet.d.ts +31 -0
  163. package/dist/sheet.js +14 -0
  164. package/dist/sheet.js.map +1 -0
  165. package/dist/status-badge.d.ts +19 -0
  166. package/dist/status-badge.js +72 -0
  167. package/dist/status-badge.js.map +1 -0
  168. package/dist/table.css +348 -0
  169. package/dist/table.d.ts +75 -0
  170. package/dist/table.js +262 -0
  171. package/dist/table.js.map +1 -0
  172. package/dist/tabs.css +208 -0
  173. package/dist/tabs.d.ts +71 -0
  174. package/dist/tabs.js +181 -0
  175. package/dist/tabs.js.map +1 -0
  176. package/dist/tag.css +248 -0
  177. package/dist/tag.d.ts +52 -0
  178. package/dist/tag.js +72 -0
  179. package/dist/tag.js.map +1 -0
  180. package/dist/text-field.d.ts +28 -0
  181. package/dist/text-field.js +69 -0
  182. package/dist/text-field.js.map +1 -0
  183. package/dist/textarea.css +219 -0
  184. package/dist/textarea.d.ts +38 -0
  185. package/dist/textarea.js +144 -0
  186. package/dist/textarea.js.map +1 -0
  187. package/dist/time-field.d.ts +28 -0
  188. package/dist/time-field.js +66 -0
  189. package/dist/time-field.js.map +1 -0
  190. package/dist/timing-7JA_zQHP.d.ts +27 -0
  191. package/dist/toast-store.d.ts +86 -0
  192. package/dist/toast-store.js +3 -0
  193. package/dist/toast-store.js.map +1 -0
  194. package/dist/toast.css +257 -0
  195. package/dist/toast.d.ts +12 -0
  196. package/dist/toast.js +310 -0
  197. package/dist/toast.js.map +1 -0
  198. package/dist/toggle-tag.d.ts +34 -0
  199. package/dist/toggle-tag.js +57 -0
  200. package/dist/toggle-tag.js.map +1 -0
  201. package/dist/toggle.css +152 -0
  202. package/dist/toggle.d.ts +31 -0
  203. package/dist/toggle.js +75 -0
  204. package/dist/toggle.js.map +1 -0
  205. package/dist/tooltip.css +86 -0
  206. package/dist/tooltip.d.ts +30 -0
  207. package/dist/tooltip.js +268 -0
  208. package/dist/tooltip.js.map +1 -0
  209. package/dist/types-SGMShK7R.d.ts +22 -0
  210. package/llms.txt +196 -0
  211. package/next.d.mts +7 -0
  212. package/next.mjs +59 -0
  213. package/package.json +257 -0
  214. package/src/components/composites/alert/Alert.tsx +141 -0
  215. package/src/components/composites/alert/alert.css +132 -0
  216. package/src/components/composites/date-picker/DateField.tsx +80 -0
  217. package/src/components/composites/date-picker/DateRangeField.tsx +110 -0
  218. package/src/components/composites/date-picker/DateTimeField.tsx +162 -0
  219. package/src/components/composites/date-picker/TimeField.tsx +78 -0
  220. package/src/components/composites/date-picker/calendar-panel.tsx +216 -0
  221. package/src/components/composites/date-picker/date-picker.css +595 -0
  222. package/src/components/composites/date-picker/date-utils.ts +56 -0
  223. package/src/components/composites/date-picker/field-shell.tsx +85 -0
  224. package/src/components/composites/date-picker/range-panel.tsx +363 -0
  225. package/src/components/composites/date-picker/time-core.tsx +290 -0
  226. package/src/components/composites/date-picker/use-day-focus.ts +35 -0
  227. package/src/components/composites/dialog/Dialog.tsx +198 -0
  228. package/src/components/composites/dialog/dialog.css +180 -0
  229. package/src/components/composites/pagination/Pagination.tsx +131 -0
  230. package/src/components/composites/pagination/pagination.css +43 -0
  231. package/src/components/composites/popover/Popover.tsx +141 -0
  232. package/src/components/composites/popover/popover.css +69 -0
  233. package/src/components/composites/select/MultiSelect.tsx +133 -0
  234. package/src/components/composites/select/Select.tsx +122 -0
  235. package/src/components/composites/select/core/index.ts +4 -0
  236. package/src/components/composites/select/core/menu.tsx +100 -0
  237. package/src/components/composites/select/core/panel.tsx +154 -0
  238. package/src/components/composites/select/core/trigger.tsx +74 -0
  239. package/src/components/composites/select/core/types.ts +39 -0
  240. package/src/components/composites/select/core/use-listbox.ts +182 -0
  241. package/src/components/composites/select/select.css +416 -0
  242. package/src/components/composites/sheet/Sheet.tsx +135 -0
  243. package/src/components/composites/sheet/sheet.css +26 -0
  244. package/src/components/composites/sheet/use-sheet-drag.ts +126 -0
  245. package/src/components/composites/table/Table.tsx +377 -0
  246. package/src/components/composites/table/table.css +348 -0
  247. package/src/components/composites/tabs/Tabs.tsx +278 -0
  248. package/src/components/composites/tabs/tabs.css +208 -0
  249. package/src/components/composites/toast/Toast.tsx +380 -0
  250. package/src/components/composites/toast/toast-store.ts +295 -0
  251. package/src/components/composites/toast/toast.css +257 -0
  252. package/src/components/composites/tooltip/Tooltip.tsx +110 -0
  253. package/src/components/composites/tooltip/tooltip-host.tsx +184 -0
  254. package/src/components/composites/tooltip/tooltip-store.ts +81 -0
  255. package/src/components/composites/tooltip/tooltip.css +86 -0
  256. package/src/components/dev/MotionDevtools.tsx +200 -0
  257. package/src/components/dev/motion-devtools.css +265 -0
  258. package/src/components/dev/slowmo-engine.ts +230 -0
  259. package/src/components/internal/glass/glass.css +100 -0
  260. package/src/components/internal/glass/glint.ts +9 -0
  261. package/src/components/internal/hooks/use-controllable.ts +21 -0
  262. package/src/components/internal/hooks/use-scroll-edges.ts +46 -0
  263. package/src/components/internal/icon/Icon.tsx +66 -0
  264. package/src/components/internal/icon/IconSlot.tsx +10 -0
  265. package/src/components/internal/icon/icon.css +33 -0
  266. package/src/components/internal/overlay/focus.ts +68 -0
  267. package/src/components/internal/overlay/layer.tsx +149 -0
  268. package/src/components/internal/overlay/modal.tsx +141 -0
  269. package/src/components/internal/overlay/overlay.css +23 -0
  270. package/src/components/internal/overlay/position.ts +103 -0
  271. package/src/components/internal/utils/cx.ts +3 -0
  272. package/src/components/internal/utils/token-px.ts +8 -0
  273. package/src/components/primitives/avatar/Avatar.tsx +120 -0
  274. package/src/components/primitives/avatar/AvatarGroup.tsx +56 -0
  275. package/src/components/primitives/avatar/avatar.css +200 -0
  276. package/src/components/primitives/badge/Badge.tsx +75 -0
  277. package/src/components/primitives/badge/CountBadge.tsx +20 -0
  278. package/src/components/primitives/badge/StatusBadge.tsx +111 -0
  279. package/src/components/primitives/badge/badge.css +182 -0
  280. package/src/components/primitives/badge/odometer.css +62 -0
  281. package/src/components/primitives/badge/odometer.tsx +36 -0
  282. package/src/components/primitives/button/Button.tsx +102 -0
  283. package/src/components/primitives/button/button.css +269 -0
  284. package/src/components/primitives/checkbox/Checkbox.tsx +76 -0
  285. package/src/components/primitives/checkbox/check-glyph.tsx +28 -0
  286. package/src/components/primitives/checkbox/checkbox.css +177 -0
  287. package/src/components/primitives/collapse/Collapse.tsx +86 -0
  288. package/src/components/primitives/collapse/collapse.css +88 -0
  289. package/src/components/primitives/collapse/collapse.typecheck.tsx +29 -0
  290. package/src/components/primitives/input/NumberField.tsx +139 -0
  291. package/src/components/primitives/input/OtpField.tsx +134 -0
  292. package/src/components/primitives/input/TextField.tsx +110 -0
  293. package/src/components/primitives/input/field-chrome.tsx +83 -0
  294. package/src/components/primitives/input/input.css +333 -0
  295. package/src/components/primitives/radio-group/RadioGroup.tsx +183 -0
  296. package/src/components/primitives/radio-group/radio-group.css +300 -0
  297. package/src/components/primitives/tag/Tag.tsx +143 -0
  298. package/src/components/primitives/tag/ToggleTag.tsx +97 -0
  299. package/src/components/primitives/tag/tag.css +248 -0
  300. package/src/components/primitives/textarea/Textarea.tsx +190 -0
  301. package/src/components/primitives/textarea/textarea.css +219 -0
  302. package/src/components/primitives/toggle/Toggle.tsx +105 -0
  303. package/src/components/primitives/toggle/toggle.css +152 -0
  304. package/src/css.d.ts +1 -0
  305. package/src/dom-props.ts +1 -0
  306. package/src/engine/animate.ts +141 -0
  307. package/src/engine/drag.ts +53 -0
  308. package/src/engine/ease.ts +155 -0
  309. package/src/engine/flip.ts +59 -0
  310. package/src/engine/index.ts +13 -0
  311. package/src/mcp/server.ts +420 -0
  312. package/src/motion/element.tsx +53 -0
  313. package/src/motion/flip.tsx +87 -0
  314. package/src/motion/glide.tsx +47 -0
  315. package/src/motion/motion-timing.ts +33 -0
  316. package/src/motion/presence-context.ts +18 -0
  317. package/src/motion/presence.tsx +91 -0
  318. package/src/motion/timing.ts +49 -0
  319. package/src/motion/use-motion.ts +64 -0
  320. package/src/styles.css +26 -0
  321. package/src/tokens/avatar.css +20 -0
  322. package/src/tokens/color.css +58 -0
  323. package/src/tokens/elevation.css +22 -0
  324. package/src/tokens/fonts.css +3 -0
  325. package/src/tokens/glass.css +20 -0
  326. package/src/tokens/icons.css +7 -0
  327. package/src/tokens/layers.css +6 -0
  328. package/src/tokens/motion-scale.ts +17 -0
  329. package/src/tokens/motion-tokens.ts +92 -0
  330. package/src/tokens/motion.css +49 -0
  331. package/src/tokens/radius.css +11 -0
  332. package/src/tokens/semantic.css +65 -0
  333. package/src/tokens/spacing.css +26 -0
  334. package/src/tokens/typography.css +52 -0
@@ -0,0 +1,348 @@
1
+ /* Table - geometry + paint: sticky header, sortable columns, row selection + bulk bar, pinned identity column. */
2
+
3
+ .tbl,
4
+ .tbl * {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ .tbl {
9
+ --tbl-pad-y: var(--space-3);
10
+ --tbl-head-py: var(--space-3);
11
+ --tbl-head-h: calc(var(--leading-caption) + 2 * var(--tbl-head-py));
12
+ /* checkbox column: pad 8 + (8 + 18 + 8 hit area) + pad 8 - all tokens */
13
+ --tbl-check-w: calc(var(--control-box) + 4 * var(--space-2));
14
+
15
+ position: relative;
16
+ container-type: inline-size;
17
+ display: flex;
18
+ flex-direction: column;
19
+ max-height: 100%;
20
+ background: var(--bg-surface);
21
+ border: var(--border-hairline) solid var(--border-default);
22
+ border-radius: var(--radius-lg);
23
+ box-shadow: var(--shadow-xs);
24
+ overflow: hidden;
25
+ }
26
+
27
+ .tbl--compact {
28
+ --tbl-pad-y: var(--space-2);
29
+ --tbl-head-py: var(--space-2);
30
+ }
31
+
32
+ .tbl__viewport {
33
+ position: relative;
34
+ display: flex; /* flex sizing, not % height - % inside a max-height parent resolves to auto, scroller would grow to content */
35
+ flex-direction: column;
36
+ flex: 1 1 auto;
37
+ min-height: 0;
38
+ }
39
+ .tbl__scroll {
40
+ flex: 1 1 auto;
41
+ min-height: 0;
42
+ overflow: auto;
43
+ overscroll-behavior: contain;
44
+ }
45
+
46
+ .tbl__table {
47
+ width: 100%;
48
+ border-collapse: separate; /* collapse breaks sticky borders */
49
+ border-spacing: 0;
50
+ }
51
+
52
+ .tbl__table thead th {
53
+ position: sticky;
54
+ top: 0;
55
+ z-index: 2;
56
+ height: var(--tbl-head-h);
57
+ padding: var(--tbl-head-py) var(--space-3);
58
+ background: var(--bg-surface);
59
+ border-bottom: var(--border-hairline) solid var(--border-default);
60
+ font: var(--type-caption);
61
+ font-weight: var(--weight-medium);
62
+ color: var(--text-muted);
63
+ text-align: left;
64
+ vertical-align: middle;
65
+ white-space: nowrap;
66
+ transition: box-shadow var(--duration-base) var(--ease-standard);
67
+ }
68
+ .tbl[data-scrolled] .tbl__table thead th {
69
+ box-shadow: var(--shadow-sm);
70
+ }
71
+
72
+ .tbl__table td {
73
+ padding: var(--tbl-pad-y) var(--space-3);
74
+ border-bottom: var(--border-hairline) solid var(--border-subtle);
75
+ background: var(--row-bg, var(--bg-surface));
76
+ font: var(--type-body);
77
+ color: var(--text-body);
78
+ vertical-align: middle;
79
+ white-space: nowrap; /* data tables don't wrap - grow column absorbs slack; overflow scrolls + pins */
80
+ transition: background-color var(--duration-fast) var(--ease-standard);
81
+ }
82
+ .tbl__table tbody tr:last-child td {
83
+ border-bottom: 0;
84
+ }
85
+
86
+ /* outermost cells breathe a step more (16) - sortables do it via the button */
87
+ .tbl__table th:first-child:not(.tbl__cell--check):not(.tbl__cell--sortable),
88
+ .tbl__table td:first-child:not(.tbl__cell--check) {
89
+ padding-inline-start: var(--space-4);
90
+ }
91
+ .tbl__table th:last-child:not(.tbl__cell--sortable),
92
+ .tbl__table td:last-child {
93
+ padding-inline-end: var(--space-4);
94
+ }
95
+
96
+ /* one opaque --row-bg per row, read by every cell - stays opaque so sticky cells cover scrolled content */
97
+ .tbl__row {
98
+ --row-bg: var(--bg-surface);
99
+ }
100
+ .tbl__row:hover {
101
+ --row-bg: var(--bg-subtle);
102
+ }
103
+ .tbl__row[data-selected] {
104
+ --row-bg: color-mix(in oklab, var(--accent) 5%, var(--bg-surface));
105
+ }
106
+ .tbl__row[data-selected]:hover {
107
+ --row-bg: color-mix(in oklab, var(--accent) 5%, var(--bg-subtle));
108
+ }
109
+ .tbl--clickable tbody .tbl__row {
110
+ cursor: pointer;
111
+ }
112
+
113
+ .tbl__table td.tbl__cell--mono {
114
+ font: var(--type-mono);
115
+ font-variant-numeric: tabular-nums;
116
+ }
117
+ .tbl__table td.tbl__cell--strong {
118
+ color: var(--text-strong);
119
+ font-weight: var(--weight-medium);
120
+ }
121
+ .tbl__cell--end {
122
+ text-align: right;
123
+ }
124
+ .tbl__cell--center {
125
+ text-align: center;
126
+ }
127
+ .tbl__cell--grow {
128
+ width: 100%;
129
+ } /* greedy column; auto table layout sizes the others to content */
130
+
131
+ /* sort: full-cell button, inset by a margin so its focus ring isn't clipped */
132
+ .tbl__table th.tbl__cell--sortable {
133
+ padding: 0;
134
+ }
135
+ .tbl__sort {
136
+ appearance: none;
137
+ border: 0;
138
+ background: transparent;
139
+ cursor: pointer;
140
+ display: flex;
141
+ align-items: center;
142
+ gap: var(--space-1);
143
+ width: calc(100% - 2 * var(--space-1));
144
+ height: calc(var(--tbl-head-h) - 2 * var(--space-1));
145
+ margin: var(--space-1);
146
+ padding-inline: var(--space-2);
147
+ border-radius: var(--radius-sm);
148
+ font: inherit;
149
+ font-weight: inherit;
150
+ color: inherit;
151
+ white-space: nowrap;
152
+ transition: var(--transition-colors);
153
+ }
154
+ .tbl__table th.tbl__cell--sortable:first-child .tbl__sort {
155
+ padding-inline-start: var(--space-3);
156
+ }
157
+ .tbl__table th.tbl__cell--sortable:last-child .tbl__sort {
158
+ padding-inline-end: var(--space-3);
159
+ }
160
+ .tbl__cell--end .tbl__sort {
161
+ justify-content: flex-end;
162
+ }
163
+ .tbl__cell--center .tbl__sort {
164
+ justify-content: center;
165
+ }
166
+ .tbl__sort:hover {
167
+ color: var(--text-strong);
168
+ background: var(--neutral-wash);
169
+ }
170
+ .tbl__sort:focus-visible {
171
+ outline: none;
172
+ box-shadow: var(--focus-ring);
173
+ }
174
+ .tbl__table th[aria-sort] .tbl__sort {
175
+ color: var(--text-strong);
176
+ }
177
+
178
+ .tbl__sortIcon {
179
+ display: inline-flex;
180
+ flex: none;
181
+ opacity: 0;
182
+ transition:
183
+ opacity var(--duration-fast) var(--ease-standard),
184
+ transform var(--duration-base) var(--ease-glide);
185
+ }
186
+ .tbl__sort:hover .tbl__sortIcon,
187
+ .tbl__sort:focus-visible .tbl__sortIcon {
188
+ opacity: 0.5;
189
+ }
190
+ .tbl__sortIcon.is-on {
191
+ opacity: 1;
192
+ }
193
+ .tbl__sortIcon.is-desc {
194
+ transform: rotate(180deg);
195
+ }
196
+
197
+ .tbl__table .tbl__cell--check {
198
+ width: var(--tbl-check-w);
199
+ padding: 0 var(--space-2);
200
+ }
201
+ .tbl__cbx {
202
+ display: inline-flex;
203
+ }
204
+ .tbl__cbx .cbx__box {
205
+ margin-top: 0;
206
+ } /* no label - no baseline offset */
207
+
208
+ /* bulk bar: absolute overlay that morphs from the header; the delayed visibility flip removes it from paint + tab order only after the exit fade, while pointer-events dies on close */
209
+ .tbl__bulk {
210
+ position: absolute;
211
+ top: 0;
212
+ left: 0;
213
+ right: 0;
214
+ z-index: 4;
215
+ height: calc(var(--tbl-head-h) + var(--border-hairline));
216
+ display: flex;
217
+ align-items: center;
218
+ gap: var(--space-2);
219
+ padding-inline: var(--space-2);
220
+ background: var(--accent-subtle);
221
+ border-bottom: var(--border-hairline) solid var(--accent-border);
222
+ opacity: 0;
223
+ transform: translateY(calc(-1 * var(--space-2)));
224
+ visibility: hidden;
225
+ pointer-events: none;
226
+ transition:
227
+ opacity var(--duration-fast) var(--ease-exit),
228
+ transform var(--duration-fast) var(--ease-exit),
229
+ visibility 0s linear var(--duration-fast);
230
+ }
231
+ .tbl__bulk[data-open] {
232
+ opacity: 1;
233
+ transform: none;
234
+ visibility: visible;
235
+ pointer-events: auto;
236
+ transition:
237
+ opacity var(--duration-base) var(--ease-standard),
238
+ transform var(--duration-base) var(--ease-entrance),
239
+ visibility 0s;
240
+ }
241
+ .tbl__bulk .cbx:not(.cbx--disabled):hover {
242
+ background: var(--accent-wash);
243
+ }
244
+ .tbl__bulkCount {
245
+ display: inline-flex;
246
+ align-items: baseline;
247
+ font-family: var(--font-mono);
248
+ font-size: var(--size-caption);
249
+ line-height: var(--leading-caption);
250
+ font-weight: var(--weight-medium);
251
+ font-variant-numeric: tabular-nums;
252
+ color: var(--text-accent);
253
+ white-space: pre; /* keep the space before "selected" */
254
+ }
255
+ .tbl__bulkSpacer {
256
+ flex: 1 1 auto;
257
+ }
258
+
259
+ .tbl--pin .tbl__cell--pin {
260
+ position: sticky;
261
+ left: 0;
262
+ z-index: 1;
263
+ }
264
+ .tbl--pin thead .tbl__cell--pin {
265
+ z-index: 3;
266
+ }
267
+ .tbl--pin .tbl__cell--pinOff {
268
+ left: var(--tbl-check-w);
269
+ }
270
+
271
+ /* soft cast off the pinned edge once content scrolls behind it (gradient exception, section B3) */
272
+ .tbl--pin .tbl__cell--pinEdge::after {
273
+ content: '';
274
+ position: absolute;
275
+ top: 0;
276
+ bottom: calc(-1 * var(--border-hairline));
277
+ left: 100%;
278
+ width: var(--space-3);
279
+ background: linear-gradient(to right, var(--gray-wash), transparent);
280
+ opacity: 0;
281
+ transition: var(--transition-opacity);
282
+ pointer-events: none;
283
+ }
284
+ .tbl[data-x-back] .tbl__cell--pinEdge::after {
285
+ opacity: 1;
286
+ }
287
+
288
+ .tbl__edge {
289
+ position: absolute;
290
+ top: 0;
291
+ bottom: 0;
292
+ width: var(--space-6);
293
+ z-index: 3;
294
+ opacity: 0;
295
+ transition: var(--transition-opacity);
296
+ pointer-events: none;
297
+ }
298
+ .tbl__edge--l {
299
+ left: 0;
300
+ background: linear-gradient(to right, var(--bg-surface), transparent);
301
+ }
302
+ .tbl__edge--r {
303
+ right: 0;
304
+ background: linear-gradient(to left, var(--bg-surface), transparent);
305
+ }
306
+ .tbl[data-x-more] .tbl__edge--r {
307
+ opacity: 1;
308
+ }
309
+ .tbl[data-x-back]:not(.tbl--pin) .tbl__edge--l {
310
+ opacity: 1;
311
+ } /* when pinned, the pinned column itself is the left-edge signal */
312
+
313
+ .tbl__emptyCell {
314
+ padding: var(--space-7) var(--space-4);
315
+ text-align: center;
316
+ font: var(--type-body);
317
+ color: var(--text-muted);
318
+ }
319
+
320
+ .tbl__table tbody {
321
+ transition: var(--transition-opacity);
322
+ }
323
+ .tbl--loading .tbl__table tbody {
324
+ opacity: 0.45;
325
+ pointer-events: none;
326
+ }
327
+
328
+ .tbl__foot {
329
+ display: flex;
330
+ align-items: center;
331
+ justify-content: flex-end;
332
+ gap: var(--space-3);
333
+ padding: var(--space-2) var(--space-3);
334
+ background: var(--bg-subtle);
335
+ border-top: var(--border-hairline) solid var(--border-subtle);
336
+ }
337
+
338
+ /* column collapse at container breakpoints - 30rem / 42rem are intentional constants (no breakpoint tokens yet) */
339
+ @container (max-width: 42rem) {
340
+ .tbl__cell--hide-md {
341
+ display: none;
342
+ }
343
+ }
344
+ @container (max-width: 30rem) {
345
+ .tbl__cell--hide-sm {
346
+ display: none;
347
+ }
348
+ }
@@ -0,0 +1,278 @@
1
+ 'use client';
2
+
3
+ import './tabs.css';
4
+ import {
5
+ useId,
6
+ useLayoutEffect,
7
+ useRef,
8
+ type CSSProperties,
9
+ type HTMLAttributes,
10
+ type KeyboardEvent,
11
+ type ReactNode,
12
+ } from 'react';
13
+ import { animate } from '../../../engine';
14
+ import { UIMotion } from '../../../tokens/motion-tokens';
15
+ import { IconSlot } from '../../internal/icon/IconSlot';
16
+ import { GlidePill, useGlide } from '../../../motion/glide';
17
+ import { useScrollEdges } from '../../internal/hooks/use-scroll-edges';
18
+ import { type DisableableAnimation } from '../../../motion/timing';
19
+ import { resolveMotionTiming } from '../../../motion/motion-timing';
20
+ import type { DataAttributes } from '../../../dom-props';
21
+ import { cx } from '../../internal/utils/cx';
22
+
23
+ const SM = UIMotion;
24
+
25
+ /** One tab in the row. */
26
+ export interface TabItem {
27
+ /** Stable identity - also used in the tab/panel id pair. */
28
+ value: string;
29
+ /** Visible tab text. */
30
+ label: ReactNode;
31
+ /** Leading icon - your own node. */
32
+ icon?: ReactNode;
33
+ /** Rendered mono + tabular. Pass a number or a preformatted string. */
34
+ count?: number | string;
35
+ /** Dims the tab, blocks selection, and skips it during arrow-key travel. */
36
+ disabled?: boolean;
37
+ }
38
+
39
+ interface TabsOwnProps {
40
+ /** The tab row, in order. @default [] */
41
+ items: TabItem[];
42
+ /** Controlled - the value of the active tab (null/undefined hides the ink). */
43
+ value: string | null | undefined;
44
+ /**
45
+ * Fires on click and on arrow-key travel (automatic activation).
46
+ * `dir` is the direction of travel (+1 right / -1 left) - hand it to
47
+ * TabPanel so content enters from the side the user moved toward.
48
+ */
49
+ onChange?: (value: string, dir: 1 | -1) => void;
50
+ /**
51
+ * Shared id prefix wiring tab and panel aria. Give Tabs and its TabPanel the
52
+ * same `name`; omit it when the tabs have no managed panel.
53
+ */
54
+ name?: string;
55
+ /** Accessible name for the tablist (e.g. "Section views"). */
56
+ ariaLabel?: string;
57
+ /** Extra class(es) merged onto the root. */
58
+ className?: string;
59
+ /** Inline styles merged onto the root. */
60
+ style?: CSSProperties;
61
+ }
62
+
63
+ export interface TabsProps extends TabsOwnProps {
64
+ /** Standard <div> attributes (data-*, aria-*, ...) forwarded to the root. */
65
+ htmlProps?: Omit<HTMLAttributes<HTMLDivElement>, keyof TabsOwnProps> & DataAttributes;
66
+ }
67
+
68
+ const TABS_EDGE_PAD = 24;
69
+ const TABS_ENTER_X = 20;
70
+
71
+ export function Tabs({ items = [], value, onChange, name, ariaLabel, className = '', style, htmlProps }: TabsProps) {
72
+ const autoId = useId();
73
+ const base = name || autoId;
74
+
75
+ const listRef = useRef<HTMLDivElement>(null);
76
+ const inkRef = useRef<HTMLSpanElement>(null);
77
+ const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
78
+ const placedRef = useRef(false);
79
+ const valueRef = useRef(value);
80
+ valueRef.current = value;
81
+
82
+ const glide = useGlide(listRef);
83
+
84
+ const place = (animated: boolean) => {
85
+ const list = listRef.current,
86
+ ink = inkRef.current;
87
+ const el = valueRef.current != null ? tabRefs.current[valueRef.current] : null;
88
+ if (!list || !ink) return;
89
+ if (!el) {
90
+ ink.style.opacity = '0';
91
+ placedRef.current = false;
92
+ return;
93
+ }
94
+ const next = { x: el.offsetLeft, w: el.offsetWidth };
95
+ ink.style.opacity = '1';
96
+
97
+ if (!animated || !placedRef.current || SM.reduced) {
98
+ animate(ink, { x: [next.x], width: [next.w], timing: { duration: 0 } });
99
+ } else {
100
+ const lr = list.getBoundingClientRect();
101
+ const ir = ink.getBoundingClientRect();
102
+ const cur = { x: ir.left - lr.left + list.scrollLeft, w: ir.width };
103
+ if (Math.abs(cur.x - next.x) > 0.5 || Math.abs(cur.w - next.w) > 0.5) {
104
+ const dir = next.x + next.w / 2 >= cur.x + cur.w / 2 ? 1 : -1;
105
+ const span = dir === 1 ? next.x + next.w - cur.x : cur.x + cur.w - next.x;
106
+ const lead = dir === 1 ? cur.x : next.x;
107
+ animate(ink, {
108
+ x: [cur.x, lead, next.x],
109
+ width: [cur.w, span, next.w],
110
+ timing: { duration: SM.dur.slow, times: [0, 0.55, 1], ease: [SM.ease.standard, SM.ease.entrance] },
111
+ });
112
+ }
113
+ }
114
+ placedRef.current = true;
115
+ };
116
+
117
+ const syncEdges = useScrollEdges(listRef, (edges, el) => {
118
+ el.dataset.fade = ((edges.left ? 'start ' : '') + (edges.right ? 'end' : '')).trim();
119
+ });
120
+
121
+ useLayoutEffect(() => {
122
+ place(true);
123
+ const l = listRef.current,
124
+ el = value != null ? tabRefs.current[value] : null;
125
+ if (l && el && l.scrollWidth > l.clientWidth) {
126
+ const behavior: ScrollBehavior = SM.reduced ? 'auto' : 'smooth';
127
+ if (el.offsetLeft < l.scrollLeft + TABS_EDGE_PAD) {
128
+ l.scrollTo({ left: el.offsetLeft - TABS_EDGE_PAD, behavior });
129
+ } else if (el.offsetLeft + el.offsetWidth > l.scrollLeft + l.clientWidth - TABS_EDGE_PAD) {
130
+ l.scrollTo({ left: el.offsetLeft + el.offsetWidth - l.clientWidth + TABS_EDGE_PAD, behavior });
131
+ }
132
+ }
133
+ }, [value]);
134
+
135
+ useLayoutEffect(() => {
136
+ const ro = new ResizeObserver(() => {
137
+ place(false);
138
+ syncEdges();
139
+ });
140
+ if (listRef.current) ro.observe(listRef.current);
141
+ Object.values(tabRefs.current).forEach((el) => el && ro.observe(el));
142
+ syncEdges();
143
+ return () => ro.disconnect();
144
+ }, [items.length]); // eslint-disable-line react-hooks/exhaustive-deps
145
+
146
+ const select = (v: string) => {
147
+ if (v === value || !onChange) return;
148
+ const idx = (x: string | null | undefined) => items.findIndex((i) => i.value === x);
149
+ onChange(v, idx(v) >= idx(value) ? 1 : -1);
150
+ };
151
+
152
+ const onKeyDown = (e: KeyboardEvent) => {
153
+ const enabled = items.filter((i) => !i.disabled);
154
+ if (!enabled.length) return;
155
+ let next: TabItem | null = null;
156
+ if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
157
+ const step = e.key === 'ArrowRight' ? 1 : -1;
158
+ const i = enabled.findIndex((it) => it.value === value);
159
+ next = enabled[(i + step + enabled.length) % enabled.length];
160
+ } else if (e.key === 'Home') {
161
+ next = enabled[0];
162
+ } else if (e.key === 'End') {
163
+ next = enabled[enabled.length - 1];
164
+ }
165
+ if (!next) return;
166
+ e.preventDefault();
167
+ select(next.value);
168
+ const el = tabRefs.current[next.value];
169
+ if (el) el.focus();
170
+ };
171
+
172
+ const focusValue = items.some((i) => i.value === value && !i.disabled)
173
+ ? value
174
+ : (items.find((i) => !i.disabled) || ({} as Partial<TabItem>)).value;
175
+
176
+ return (
177
+ <div className={cx('tabs', className)} style={style} {...htmlProps}>
178
+ <div
179
+ className="tabs__list"
180
+ role="tablist"
181
+ aria-label={ariaLabel}
182
+ ref={listRef}
183
+ onKeyDown={onKeyDown}
184
+ onPointerLeave={glide.leave}
185
+ >
186
+ <GlidePill className="tab__hover" glide={glide} />
187
+ {items.map((it) => {
188
+ const selected = it.value === value;
189
+ return (
190
+ <button
191
+ key={it.value}
192
+ type="button"
193
+ role="tab"
194
+ id={base + '-tab-' + it.value}
195
+ aria-selected={selected}
196
+ aria-controls={name ? name + '-panel-' + it.value : undefined}
197
+ tabIndex={it.value === focusValue ? 0 : -1}
198
+ disabled={it.disabled}
199
+ className={'tab' + (selected ? ' is-selected' : '')}
200
+ ref={(el) => {
201
+ tabRefs.current[it.value] = el;
202
+ }}
203
+ onClick={() => select(it.value)}
204
+ onPointerEnter={
205
+ it.disabled ? undefined : (e) => glide.enter(e.currentTarget.firstElementChild as HTMLElement)
206
+ }
207
+ >
208
+ <span className="tab__pad">
209
+ {it.icon && <IconSlot size="sm">{it.icon}</IconSlot>}
210
+ <span className="tab__label">{it.label}</span>
211
+ {it.count != null && <span className="tab__count">{it.count}</span>}
212
+ </span>
213
+ </button>
214
+ );
215
+ })}
216
+ <span className="tabs__ink" key="ink" ref={inkRef} aria-hidden="true"></span>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ interface TabPanelOwnProps {
223
+ /** The active tab's value - changing it cuts to the new content. */
224
+ tab: string;
225
+ /** Same `name` as the paired Tabs - wires role/id/aria-labelledby. */
226
+ name?: string;
227
+ /** Direction of travel from Tabs' onChange; 0 = plain fade. */
228
+ dir?: -1 | 0 | 1;
229
+ /** Panel content - only this inner node animates in on `tab` change; the root chrome stays static. */
230
+ children?: ReactNode;
231
+ /** Entrance timing for the inner content - motion tokens only, or `null` to disable.
232
+ * @default duration 'base' + ease 'entrance' */
233
+ animation?: DisableableAnimation;
234
+ /** Extra class(es) merged onto the root. */
235
+ className?: string;
236
+ /** Inline styles merged onto the root. */
237
+ style?: CSSProperties;
238
+ }
239
+
240
+ export interface TabPanelProps extends TabPanelOwnProps {
241
+ /** Standard <div> attributes (data-*, aria-*, ...) forwarded to the root. */
242
+ htmlProps?: Omit<HTMLAttributes<HTMLDivElement>, keyof TabPanelOwnProps> & DataAttributes;
243
+ }
244
+
245
+ const TABPANEL_TIMING = {
246
+ open: { duration: 'base', ease: 'entrance' },
247
+ close: { duration: 'base', ease: 'entrance' },
248
+ } as const;
249
+
250
+ export function TabPanel({ tab, name, dir = 0, className = '', style, children, animation, htmlProps }: TabPanelProps) {
251
+ const enter = resolveMotionTiming(animation, TABPANEL_TIMING).open;
252
+ const innerRef = useRef<HTMLDivElement | null>(null);
253
+ const enterRef = useRef<ReturnType<typeof animate> | null>(null);
254
+
255
+ useLayoutEffect(() => {
256
+ const el = innerRef.current;
257
+ if (!el) return;
258
+ enterRef.current?.stop();
259
+ enterRef.current = animate(el, { x: [dir * TABS_ENTER_X, 0], y: [4, 0], timing: enter });
260
+ return () => enterRef.current?.stop();
261
+ }, [tab]); // eslint-disable-line react-hooks/exhaustive-deps
262
+
263
+ return (
264
+ <div
265
+ role="tabpanel"
266
+ tabIndex={0}
267
+ id={name ? `${name}-panel-${tab}` : undefined}
268
+ aria-labelledby={name ? `${name}-tab-${tab}` : undefined}
269
+ className={cx('tab-panel', className)}
270
+ style={style}
271
+ {...htmlProps}
272
+ >
273
+ <div ref={innerRef} className="tab-panel__inner">
274
+ {children}
275
+ </div>
276
+ </div>
277
+ );
278
+ }