@remix-run/ui 0.3.0 → 0.4.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 (308) hide show
  1. package/README.md +50 -164
  2. package/dist/accordion/index.d.ts +46 -0
  3. package/dist/accordion/index.js +142 -0
  4. package/dist/accordion/index.js.map +1 -0
  5. package/dist/accordion/primitives.d.ts +104 -0
  6. package/dist/accordion/primitives.js +312 -0
  7. package/dist/accordion/primitives.js.map +1 -0
  8. package/dist/{components/anchor/anchor.js → anchor/index.js} +1 -1
  9. package/dist/anchor/index.js.map +1 -0
  10. package/dist/animation/demos/reordering.js +9 -6
  11. package/dist/animation/demos/reordering.js.map +1 -1
  12. package/dist/{components/breadcrumbs/breadcrumbs.js → breadcrumbs/index.js} +19 -19
  13. package/dist/breadcrumbs/index.js.map +1 -0
  14. package/dist/button/index.d.ts +15 -0
  15. package/dist/button/index.js +128 -0
  16. package/dist/button/index.js.map +1 -0
  17. package/dist/checkbox/index.d.ts +14 -0
  18. package/dist/checkbox/index.js +125 -0
  19. package/dist/checkbox/index.js.map +1 -0
  20. package/dist/combobox/index.d.ts +21 -0
  21. package/dist/combobox/index.js +58 -0
  22. package/dist/combobox/index.js.map +1 -0
  23. package/dist/{components/combobox/combobox.d.ts → combobox/primitives.d.ts} +3 -7
  24. package/dist/{components/combobox/combobox.js → combobox/primitives.js} +8 -61
  25. package/dist/combobox/primitives.js.map +1 -0
  26. package/dist/input/index.d.ts +13 -0
  27. package/dist/input/index.js +121 -0
  28. package/dist/input/index.js.map +1 -0
  29. package/dist/listbox/flash-attribute.js.map +1 -0
  30. package/dist/{components/listbox/listbox.d.ts → listbox/index.d.ts} +2 -6
  31. package/dist/{components/listbox/listbox.js → listbox/index.js} +4 -82
  32. package/dist/listbox/index.js.map +1 -0
  33. package/dist/menu/hover-aim.js.map +1 -0
  34. package/dist/menu/index.d.ts +42 -0
  35. package/dist/menu/index.js +250 -0
  36. package/dist/menu/index.js.map +1 -0
  37. package/dist/{components/menu/menu.d.ts → menu/primitives.d.ts} +3 -32
  38. package/dist/{components/menu/menu.js → menu/primitives.js} +97 -314
  39. package/dist/menu/primitives.js.map +1 -0
  40. package/dist/{components/popover/popover.d.ts → popover/index.d.ts} +2 -4
  41. package/dist/{components/popover/popover.js → popover/index.js} +5 -48
  42. package/dist/popover/index.js.map +1 -0
  43. package/dist/{interactions/outside-click/outside-click-mixin.js → popover/outside-click.js} +1 -1
  44. package/dist/popover/outside-click.js.map +1 -0
  45. package/dist/popover/scroll-lock.d.ts +1 -0
  46. package/dist/{utils → popover}/scroll-lock.js +0 -18
  47. package/dist/popover/scroll-lock.js.map +1 -0
  48. package/dist/radio/index.d.ts +12 -0
  49. package/dist/radio/index.js +77 -0
  50. package/dist/radio/index.js.map +1 -0
  51. package/dist/runtime/reconcile.js +11 -5
  52. package/dist/runtime/reconcile.js.map +1 -1
  53. package/dist/runtime/vdom.js +3 -0
  54. package/dist/runtime/vdom.js.map +1 -1
  55. package/dist/select/index.d.ts +19 -0
  56. package/dist/select/index.js +95 -0
  57. package/dist/select/index.js.map +1 -0
  58. package/dist/select/keydown.js.map +1 -0
  59. package/dist/{components/select/select.d.ts → select/primitives.d.ts} +4 -6
  60. package/dist/{components/select/select.js → select/primitives.js} +11 -63
  61. package/dist/select/primitives.js.map +1 -0
  62. package/dist/server/stream.js +2 -2
  63. package/dist/server/stream.js.map +1 -1
  64. package/dist/shared/focus-styles.d.ts +1 -0
  65. package/dist/shared/focus-styles.js +2 -0
  66. package/dist/shared/focus-styles.js.map +1 -0
  67. package/dist/shared/icons.d.ts +9 -0
  68. package/dist/shared/icons.js +42 -0
  69. package/dist/shared/icons.js.map +1 -0
  70. package/dist/shared/listbox-popover-styles.d.ts +6 -0
  71. package/dist/shared/listbox-popover-styles.js +137 -0
  72. package/dist/shared/listbox-popover-styles.js.map +1 -0
  73. package/dist/shared/style-values.d.ts +78 -0
  74. package/dist/{theme/presets/rmx-01/index.js → shared/style-values.js} +3 -34
  75. package/dist/shared/style-values.js.map +1 -0
  76. package/dist/{interactions/typeahead/typeahead-mixin.js → shared/typeahead.js} +1 -1
  77. package/dist/shared/typeahead.js.map +1 -0
  78. package/dist/shared/wait-for-css-transition.js.map +1 -0
  79. package/dist/shared/wait.js.map +1 -0
  80. package/dist/tabs/index.d.ts +32 -0
  81. package/dist/tabs/index.js +157 -0
  82. package/dist/tabs/index.js.map +1 -0
  83. package/dist/tabs/primitives.d.ts +57 -0
  84. package/dist/tabs/primitives.js +238 -0
  85. package/dist/tabs/primitives.js.map +1 -0
  86. package/dist/toggle/index.d.ts +9 -0
  87. package/dist/toggle/index.js +108 -0
  88. package/dist/toggle/index.js.map +1 -0
  89. package/dist/toggle/primitives.d.ts +29 -0
  90. package/dist/toggle/primitives.js +127 -0
  91. package/dist/toggle/primitives.js.map +1 -0
  92. package/package.json +73 -44
  93. package/src/{components/accordion → accordion}/README.md +50 -14
  94. package/src/accordion/index.tsx +285 -0
  95. package/src/accordion/primitives.tsx +512 -0
  96. package/src/{components/anchor → anchor}/README.md +37 -2
  97. package/src/animation/demos/reordering.tsx +9 -6
  98. package/src/{components/breadcrumbs → breadcrumbs}/README.md +4 -4
  99. package/src/{components/breadcrumbs/breadcrumbs.tsx → breadcrumbs/index.tsx} +18 -18
  100. package/src/button/README.md +44 -0
  101. package/src/button/index.ts +168 -0
  102. package/src/checkbox/README.md +59 -0
  103. package/src/checkbox/index.ts +162 -0
  104. package/src/{components/combobox → combobox}/README.md +58 -9
  105. package/src/combobox/index.tsx +112 -0
  106. package/src/{components/combobox/combobox.tsx → combobox/primitives.tsx} +5 -91
  107. package/src/input/README.md +52 -0
  108. package/src/input/index.ts +147 -0
  109. package/src/{components/listbox → listbox}/README.md +9 -41
  110. package/src/{components/listbox/listbox.ts → listbox/index.ts} +2 -87
  111. package/src/{components/menu → menu}/README.md +55 -14
  112. package/src/menu/index.tsx +354 -0
  113. package/src/{components/menu/menu.tsx → menu/primitives.tsx} +99 -413
  114. package/src/{components/popover → popover}/README.md +20 -39
  115. package/src/{components/popover/popover.ts → popover/index.ts} +3 -57
  116. package/src/{utils → popover}/scroll-lock.ts +0 -28
  117. package/src/radio/README.md +53 -0
  118. package/src/radio/index.ts +100 -0
  119. package/src/runtime/demos/readme.demo.tsx +16 -14
  120. package/src/runtime/reconcile.ts +11 -5
  121. package/src/runtime/vdom.ts +3 -0
  122. package/src/{components/select → select}/README.md +29 -19
  123. package/src/select/index.tsx +159 -0
  124. package/src/{components/select/select.tsx → select/primitives.tsx} +10 -98
  125. package/src/server/stream.ts +3 -3
  126. package/src/shared/focus-styles.ts +2 -0
  127. package/src/shared/icons.tsx +62 -0
  128. package/src/shared/listbox-popover-styles.ts +146 -0
  129. package/src/shared/style-values.ts +165 -0
  130. package/src/tabs/README.md +141 -0
  131. package/src/tabs/index.tsx +244 -0
  132. package/src/tabs/primitives.ts +365 -0
  133. package/src/toggle/README.md +56 -0
  134. package/src/toggle/index.tsx +134 -0
  135. package/src/toggle/primitives.ts +192 -0
  136. package/dist/animation/demos/animation.demo.d.ts +0 -5
  137. package/dist/animation/demos/animation.demo.js +0 -92
  138. package/dist/animation/demos/animation.demo.js.map +0 -1
  139. package/dist/animation/demos/spring.demo.d.ts +0 -6
  140. package/dist/animation/demos/spring.demo.js +0 -304
  141. package/dist/animation/demos/spring.demo.js.map +0 -1
  142. package/dist/components/accordion/accordion.d.ts +0 -92
  143. package/dist/components/accordion/accordion.js +0 -335
  144. package/dist/components/accordion/accordion.js.map +0 -1
  145. package/dist/components/accordion/demos/card.demo.d.ts +0 -7
  146. package/dist/components/accordion/demos/card.demo.js +0 -59
  147. package/dist/components/accordion/demos/card.demo.js.map +0 -1
  148. package/dist/components/accordion/demos/multiple.demo.d.ts +0 -7
  149. package/dist/components/accordion/demos/multiple.demo.js +0 -20
  150. package/dist/components/accordion/demos/multiple.demo.js.map +0 -1
  151. package/dist/components/accordion/demos/overview.demo.d.ts +0 -7
  152. package/dist/components/accordion/demos/overview.demo.js +0 -20
  153. package/dist/components/accordion/demos/overview.demo.js.map +0 -1
  154. package/dist/components/anchor/anchor.js.map +0 -1
  155. package/dist/components/breadcrumbs/basic.demo.d.ts +0 -6
  156. package/dist/components/breadcrumbs/basic.demo.js +0 -15
  157. package/dist/components/breadcrumbs/basic.demo.js.map +0 -1
  158. package/dist/components/breadcrumbs/breadcrumbs.js.map +0 -1
  159. package/dist/components/breadcrumbs/separator.demo.d.ts +0 -6
  160. package/dist/components/breadcrumbs/separator.demo.js +0 -15
  161. package/dist/components/breadcrumbs/separator.demo.js.map +0 -1
  162. package/dist/components/button/button.d.ts +0 -105
  163. package/dist/components/button/button.js +0 -207
  164. package/dist/components/button/button.js.map +0 -1
  165. package/dist/components/button/demos/aliases.demo.d.ts +0 -7
  166. package/dist/components/button/demos/aliases.demo.js +0 -19
  167. package/dist/components/button/demos/aliases.demo.js.map +0 -1
  168. package/dist/components/button/demos/basic.demo.d.ts +0 -7
  169. package/dist/components/button/demos/basic.demo.js +0 -20
  170. package/dist/components/button/demos/basic.demo.js.map +0 -1
  171. package/dist/components/button/demos/states.demo.d.ts +0 -7
  172. package/dist/components/button/demos/states.demo.js +0 -30
  173. package/dist/components/button/demos/states.demo.js.map +0 -1
  174. package/dist/components/combobox/combobox.js.map +0 -1
  175. package/dist/components/combobox/overview.demo.d.ts +0 -7
  176. package/dist/components/combobox/overview.demo.js +0 -154
  177. package/dist/components/combobox/overview.demo.js.map +0 -1
  178. package/dist/components/glyph/glyph.d.ts +0 -14
  179. package/dist/components/glyph/glyph.js +0 -65
  180. package/dist/components/glyph/glyph.js.map +0 -1
  181. package/dist/components/listbox/listbox.js.map +0 -1
  182. package/dist/components/listbox/overview.demo.d.ts +0 -7
  183. package/dist/components/listbox/overview.demo.js +0 -65
  184. package/dist/components/listbox/overview.demo.js.map +0 -1
  185. package/dist/components/menu/demos/bubbling.demo.d.ts +0 -6
  186. package/dist/components/menu/demos/bubbling.demo.js +0 -16
  187. package/dist/components/menu/demos/bubbling.demo.js.map +0 -1
  188. package/dist/components/menu/demos/context-trigger.demo.d.ts +0 -7
  189. package/dist/components/menu/demos/context-trigger.demo.js +0 -87
  190. package/dist/components/menu/demos/context-trigger.demo.js.map +0 -1
  191. package/dist/components/menu/demos/overview.demo.d.ts +0 -7
  192. package/dist/components/menu/demos/overview.demo.js +0 -33
  193. package/dist/components/menu/demos/overview.demo.js.map +0 -1
  194. package/dist/components/menu/hover-aim.js.map +0 -1
  195. package/dist/components/menu/menu.js.map +0 -1
  196. package/dist/components/popover/overview.demo.d.ts +0 -7
  197. package/dist/components/popover/overview.demo.js +0 -65
  198. package/dist/components/popover/overview.demo.js.map +0 -1
  199. package/dist/components/popover/popover.js.map +0 -1
  200. package/dist/components/select/deconstructed.demo.d.ts +0 -7
  201. package/dist/components/select/deconstructed.demo.js +0 -54
  202. package/dist/components/select/deconstructed.demo.js.map +0 -1
  203. package/dist/components/select/overview.demo.d.ts +0 -6
  204. package/dist/components/select/overview.demo.js +0 -28
  205. package/dist/components/select/overview.demo.js.map +0 -1
  206. package/dist/components/select/select.js.map +0 -1
  207. package/dist/components/separator/separator.d.ts +0 -2
  208. package/dist/components/separator/separator.js +0 -15
  209. package/dist/components/separator/separator.js.map +0 -1
  210. package/dist/components/tabs/tabs.d.ts +0 -78
  211. package/dist/components/tabs/tabs.js +0 -339
  212. package/dist/components/tabs/tabs.js.map +0 -1
  213. package/dist/interactions/keydown/keydown.js.map +0 -1
  214. package/dist/interactions/outside-click/outside-click-mixin.js.map +0 -1
  215. package/dist/interactions/typeahead/typeahead-mixin.js.map +0 -1
  216. package/dist/runtime/demos/basic.demo.d.ts +0 -6
  217. package/dist/runtime/demos/basic.demo.js +0 -16
  218. package/dist/runtime/demos/basic.demo.js.map +0 -1
  219. package/dist/runtime/demos/controlled-uncontrolled-values.demo.d.ts +0 -6
  220. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js +0 -77
  221. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js.map +0 -1
  222. package/dist/runtime/demos/keyed-list.demo.d.ts +0 -6
  223. package/dist/runtime/demos/keyed-list.demo.js +0 -62
  224. package/dist/runtime/demos/keyed-list.demo.js.map +0 -1
  225. package/dist/runtime/demos/readme.demo.d.ts +0 -5
  226. package/dist/runtime/demos/readme.demo.js +0 -396
  227. package/dist/runtime/demos/readme.demo.js.map +0 -1
  228. package/dist/theme/contract.d.ts +0 -61
  229. package/dist/theme/contract.js +0 -131
  230. package/dist/theme/contract.js.map +0 -1
  231. package/dist/theme/glyph-contract.d.ts +0 -11
  232. package/dist/theme/glyph-contract.js +0 -34
  233. package/dist/theme/glyph-contract.js.map +0 -1
  234. package/dist/theme/presets/rmx-01/glyphs.d.ts +0 -2
  235. package/dist/theme/presets/rmx-01/glyphs.js +0 -252
  236. package/dist/theme/presets/rmx-01/glyphs.js.map +0 -1
  237. package/dist/theme/presets/rmx-01/index.d.ts +0 -4
  238. package/dist/theme/presets/rmx-01/index.js.map +0 -1
  239. package/dist/theme/runtime.d.ts +0 -2
  240. package/dist/theme/runtime.js +0 -84
  241. package/dist/theme/runtime.js.map +0 -1
  242. package/dist/theme/theme.d.ts +0 -6
  243. package/dist/theme/theme.js +0 -5
  244. package/dist/theme/theme.js.map +0 -1
  245. package/dist/utils/flash-attribute.js.map +0 -1
  246. package/dist/utils/scroll-lock.d.ts +0 -3
  247. package/dist/utils/scroll-lock.js.map +0 -1
  248. package/dist/utils/wait-for-css-transition.js.map +0 -1
  249. package/dist/utils/wait.js.map +0 -1
  250. package/src/animation/demos/animation.demo.tsx +0 -194
  251. package/src/animation/demos/spring.demo.tsx +0 -385
  252. package/src/components/accordion/accordion.tsx +0 -565
  253. package/src/components/accordion/demos/card.demo.tsx +0 -114
  254. package/src/components/accordion/demos/multiple.demo.tsx +0 -57
  255. package/src/components/accordion/demos/overview.demo.tsx +0 -57
  256. package/src/components/breadcrumbs/basic.demo.tsx +0 -18
  257. package/src/components/breadcrumbs/separator.demo.tsx +0 -19
  258. package/src/components/button/README.md +0 -44
  259. package/src/components/button/button.tsx +0 -286
  260. package/src/components/button/demos/aliases.demo.tsx +0 -28
  261. package/src/components/button/demos/basic.demo.tsx +0 -31
  262. package/src/components/button/demos/states.demo.tsx +0 -53
  263. package/src/components/combobox/overview.demo.tsx +0 -200
  264. package/src/components/glyph/README.md +0 -72
  265. package/src/components/glyph/glyph.tsx +0 -110
  266. package/src/components/listbox/overview.demo.tsx +0 -102
  267. package/src/components/menu/demos/bubbling.demo.tsx +0 -35
  268. package/src/components/menu/demos/context-trigger.demo.tsx +0 -133
  269. package/src/components/menu/demos/overview.demo.tsx +0 -77
  270. package/src/components/popover/overview.demo.tsx +0 -144
  271. package/src/components/select/deconstructed.demo.tsx +0 -96
  272. package/src/components/select/overview.demo.tsx +0 -52
  273. package/src/components/separator/separator.ts +0 -19
  274. package/src/components/tabs/README.md +0 -105
  275. package/src/components/tabs/tabs.tsx +0 -504
  276. package/src/runtime/demos/basic.demo.tsx +0 -21
  277. package/src/runtime/demos/controlled-uncontrolled-values.demo.tsx +0 -177
  278. package/src/runtime/demos/keyed-list.demo.tsx +0 -94
  279. package/src/theme/README.md +0 -103
  280. package/src/theme/contract.ts +0 -215
  281. package/src/theme/glyph-contract.ts +0 -59
  282. package/src/theme/presets/rmx-01/glyphs.tsx +0 -308
  283. package/src/theme/presets/rmx-01/index.ts +0 -123
  284. package/src/theme/runtime.ts +0 -119
  285. package/src/theme/theme.ts +0 -15
  286. package/src/utils/scroll-lock/README.md +0 -33
  287. /package/dist/{components/anchor/anchor.d.ts → anchor/index.d.ts} +0 -0
  288. /package/dist/{components/breadcrumbs/breadcrumbs.d.ts → breadcrumbs/index.d.ts} +0 -0
  289. /package/dist/{utils → listbox}/flash-attribute.d.ts +0 -0
  290. /package/dist/{utils → listbox}/flash-attribute.js +0 -0
  291. /package/dist/{components/menu → menu}/hover-aim.d.ts +0 -0
  292. /package/dist/{components/menu → menu}/hover-aim.js +0 -0
  293. /package/dist/{interactions/outside-click/outside-click-mixin.d.ts → popover/outside-click.d.ts} +0 -0
  294. /package/dist/{interactions/keydown → select}/keydown.d.ts +0 -0
  295. /package/dist/{interactions/keydown → select}/keydown.js +0 -0
  296. /package/dist/{interactions/typeahead/typeahead-mixin.d.ts → shared/typeahead.d.ts} +0 -0
  297. /package/dist/{utils → shared}/wait-for-css-transition.d.ts +0 -0
  298. /package/dist/{utils → shared}/wait-for-css-transition.js +0 -0
  299. /package/dist/{utils → shared}/wait.d.ts +0 -0
  300. /package/dist/{utils → shared}/wait.js +0 -0
  301. /package/src/{components/anchor/anchor.ts → anchor/index.ts} +0 -0
  302. /package/src/{utils → listbox}/flash-attribute.ts +0 -0
  303. /package/src/{components/menu → menu}/hover-aim.ts +0 -0
  304. /package/src/{interactions/outside-click/outside-click-mixin.ts → popover/outside-click.ts} +0 -0
  305. /package/src/{interactions/keydown → select}/keydown.ts +0 -0
  306. /package/src/{interactions/typeahead/typeahead-mixin.ts → shared/typeahead.ts} +0 -0
  307. /package/src/{utils → shared}/wait-for-css-transition.ts +0 -0
  308. /package/src/{utils → shared}/wait.ts +0 -0
@@ -1,22 +1,15 @@
1
1
  import {
2
2
  attrs,
3
3
  createMixin,
4
- css,
5
4
  on,
6
- type CSSMixinDescriptor,
7
5
  type ElementProps,
8
6
  type Handle,
9
7
  type MixinFactory,
10
8
  type RemixNode,
11
9
  } from '@remix-run/ui'
12
- import {
13
- anchor as positionAnchor,
14
- type AnchorOptions,
15
- type AnchorTarget,
16
- } from '../anchor/anchor.ts'
17
- import { onOutsideClick } from '../../interactions/outside-click/outside-click-mixin.ts'
18
- import { theme } from '../../theme/theme.ts'
19
- import { lockScroll } from '../../utils/scroll-lock.ts'
10
+ import { anchor as positionAnchor, type AnchorOptions, type AnchorTarget } from '../anchor/index.ts'
11
+ import { onOutsideClick } from './outside-click.ts'
12
+ import { lockScroll } from './scroll-lock.ts'
20
13
 
21
14
  export interface PopoverContext {
22
15
  hideFocusTarget: HTMLElement | null
@@ -34,53 +27,6 @@ interface AnchorRef {
34
27
  options: AnchorOptions
35
28
  }
36
29
 
37
- const popupViewportClampMaxHeight = '50dvh'
38
-
39
- const popoverSurfaceTransitionCss: CSSMixinDescriptor = css({
40
- opacity: 0,
41
- '&:popover-open': {
42
- opacity: 1,
43
- },
44
- '&:not(:popover-open)': {
45
- pointerEvents: 'none',
46
- transition: 'opacity 180ms ease-in, overlay 180ms ease-in, display 180ms ease-in',
47
- transitionBehavior: 'allow-discrete',
48
- },
49
- })
50
-
51
- const popoverContentCss: CSSMixinDescriptor = css({
52
- flex: '1 1 auto',
53
- minHeight: 0,
54
- padding: theme.space.xs,
55
- overflow: 'auto',
56
- overscrollBehavior: 'contain',
57
- })
58
-
59
- const popoverSurfaceCss: CSSMixinDescriptor = css({
60
- position: 'fixed',
61
- inset: 'auto',
62
- margin: 0,
63
- padding: theme.space.none,
64
- display: 'flex',
65
- flexDirection: 'column',
66
- minHeight: 0,
67
- minWidth: '12rem',
68
- maxWidth: `min(24rem, calc(100vw - (${theme.space.lg} * 2)))`,
69
- maxHeight: popupViewportClampMaxHeight,
70
- border: `1px solid ${theme.colors.border.subtle}`,
71
- borderRadius: theme.radius.lg,
72
- backgroundColor: theme.surface.lvl0,
73
- color: theme.colors.text.primary,
74
- overflow: 'hidden',
75
- boxShadow: `${theme.shadow.xs}, ${theme.shadow.md}`,
76
- '&::backdrop': {
77
- background: 'transparent',
78
- },
79
- })
80
-
81
- export const contentStyle = popoverContentCss
82
- export const surfaceStyle = [popoverSurfaceCss, popoverSurfaceTransitionCss] as const
83
-
84
30
  function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>): () => RemixNode {
85
31
  handle.context.set({
86
32
  hideFocusTarget: null,
@@ -1,5 +1,3 @@
1
- import { createMixin, on, type ElementProps, type MixinFactory } from '@remix-run/ui'
2
-
3
1
  type ScrollLockState = {
4
2
  count: number
5
3
  documentOverflow: string
@@ -73,29 +71,3 @@ export function lockScroll(targetDocument: Document | undefined = globalThis.doc
73
71
  view.scrollTo(currentState.scrollX, currentState.scrollY)
74
72
  }
75
73
  }
76
-
77
- export const lockScrollOnToggle: MixinFactory<HTMLElement, [], ElementProps> = createMixin<
78
- HTMLElement,
79
- [],
80
- ElementProps
81
- >((handle) => {
82
- let unlockScroll = () => {}
83
-
84
- handle.signal.addEventListener('abort', () => {
85
- unlockScroll()
86
- unlockScroll = () => {}
87
- })
88
-
89
- return () => [
90
- on('beforetoggle', (event) => {
91
- unlockScroll()
92
- unlockScroll = () => {}
93
-
94
- if (event.newState !== 'open') {
95
- return
96
- }
97
-
98
- unlockScroll = lockScroll((event.currentTarget as HTMLElement).ownerDocument)
99
- }),
100
- ]
101
- })
@@ -0,0 +1,53 @@
1
+ # radio
2
+
3
+ `radio` is a style mixin for native radio inputs. It only owns radio visuals and the default `type="radio"` behavior for native `<input>` hosts.
4
+
5
+ ## Primitive Usage
6
+
7
+ ```tsx
8
+ import radio from 'remix/ui/radio'
9
+
10
+ function ShippingSpeed() {
11
+ return () => (
12
+ <fieldset>
13
+ <label>
14
+ <input defaultChecked mix={radio()} name="shipping-speed" value="standard" />
15
+ Standard
16
+ </label>
17
+ <label>
18
+ <input mix={radio({ size: 'lg' })} name="shipping-speed" value="express" />
19
+ Express
20
+ </label>
21
+ </fieldset>
22
+ )
23
+ }
24
+ ```
25
+
26
+ Compose app-owned styles around the primitive when a radio option needs local layout:
27
+
28
+ ```tsx
29
+ import radio from 'remix/ui/radio'
30
+ import { optionStyle } from './shipping.styles'
31
+
32
+ function ShippingOption() {
33
+ return () => (
34
+ <label mix={optionStyle}>
35
+ <input mix={radio()} name="shipping-speed" value="overnight" />
36
+ Overnight
37
+ </label>
38
+ )
39
+ }
40
+ ```
41
+
42
+ ## `remix/ui/radio`
43
+
44
+ - `radio(options)`: style mixin for native radio inputs or radio-like hosts.
45
+ - `RadioOptions`: accepts `size`.
46
+ - `RadioSize`: `'md'` or `'lg'`. Defaults to `'md'`.
47
+
48
+ ## Behavior Notes
49
+
50
+ - `radio()` returns a mixin descriptor, so it composes with other mixins in the host element's `mix` prop.
51
+ - Native `<input>` hosts receive `type="radio"` unless an explicit `type` is provided.
52
+ - Radio state is native browser behavior. The mixin also styles `aria-checked="true"` and `data-state="checked"` for custom hosts.
53
+ - Disabled hosts use the shared disabled treatment through `disabled` or `aria-disabled="true"`.
@@ -0,0 +1,100 @@
1
+ import { createMixin, css } from '@remix-run/ui'
2
+ import type { CSSMixinDescriptor, ElementProps, MixinDescriptor } from '@remix-run/ui'
3
+ import { renderMixinElement } from '../runtime/mixins/mixin.ts'
4
+ import { controlFocusShadow } from '../shared/focus-styles.ts'
5
+
6
+ export type RadioSize = 'md' | 'lg'
7
+
8
+ export interface RadioOptions {
9
+ size?: RadioSize
10
+ }
11
+
12
+ type RadioMixin = readonly [
13
+ MixinDescriptor<Element, [], ElementProps>,
14
+ CSSMixinDescriptor,
15
+ CSSMixinDescriptor,
16
+ ]
17
+
18
+ const checkedSelector = '&:checked, &[aria-checked="true"], &[data-state="checked"]'
19
+ const checkedMarkSelector =
20
+ '&:checked::before, &[aria-checked="true"]::before, &[data-state="checked"]::before'
21
+
22
+ const radioDefaultAttrs = createMixin<Element, [], ElementProps>((handle, hostType) => (props) => {
23
+ if (hostType !== 'input' || props.type !== undefined) {
24
+ return handle.element
25
+ }
26
+
27
+ return renderMixinElement(handle.element, {
28
+ ...props,
29
+ type: 'radio',
30
+ })
31
+ })()
32
+
33
+ const baseStyle: CSSMixinDescriptor = css({
34
+ appearance: 'none',
35
+ WebkitAppearance: 'none',
36
+ margin: 0,
37
+ boxSizing: 'border-box',
38
+ position: 'relative',
39
+ display: 'inline-grid',
40
+ placeItems: 'center',
41
+ width: 'var(--rmx-radio-size)',
42
+ height: 'var(--rmx-radio-size)',
43
+ minWidth: 'var(--rmx-radio-size)',
44
+ minHeight: 'var(--rmx-radio-size)',
45
+ padding: 0,
46
+ border: '1px solid rgba(0, 0, 0, 0.12)',
47
+ borderRadius: '9999px',
48
+ background: '#FFFFFF',
49
+ boxShadow: 'inset 0 1px 1px rgba(0, 0, 0, 0.06), inset 0 2px 2px rgba(0, 0, 0, 0.06)',
50
+ verticalAlign: 'middle',
51
+ flex: 'none',
52
+ '&::before': {
53
+ content: '""',
54
+ width: 'var(--rmx-radio-mark-size)',
55
+ height: 'var(--rmx-radio-mark-size)',
56
+ borderRadius: '99px',
57
+ background:
58
+ 'linear-gradient(180deg, rgba(53, 115, 246, 0) 25%, rgba(53, 115, 246, 0.15) 100%), #FFFFFF',
59
+ boxShadow:
60
+ '0 1px 1px -0.5px #0944BE, 0 2px 2px -1px #0944BE, 0 4px 4px -2px #0944BE, 0 0 1px 1px rgba(0, 0, 0, 0.25), inset 0 0 2px 1px #FFFFFF',
61
+ opacity: 0,
62
+ pointerEvents: 'none',
63
+ },
64
+ '&:disabled, &[aria-disabled="true"]': {
65
+ opacity: 0.55,
66
+ },
67
+ [checkedSelector]: {
68
+ border: 0,
69
+ borderRadius: '99px',
70
+ background: 'linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0) 75.48%), #3573F6',
71
+ backgroundBlendMode: 'overlay, normal',
72
+ boxShadow:
73
+ '0 1px 0 rgba(255, 255, 255, 0.6), 0 0 16px rgba(53, 115, 246, 0.25), inset 0 0 3px 1px rgba(0, 0, 0, 0.1)',
74
+ },
75
+ [checkedMarkSelector]: {
76
+ opacity: 1,
77
+ },
78
+ '&:focus-visible': {
79
+ outline: 0,
80
+ boxShadow: controlFocusShadow,
81
+ },
82
+ })
83
+
84
+ const sizeStyles = {
85
+ md: css({
86
+ '--rmx-radio-size': '16px',
87
+ '--rmx-radio-mark-size': '8px',
88
+ }),
89
+ lg: css({
90
+ '--rmx-radio-size': '20px',
91
+ '--rmx-radio-mark-size': '10px',
92
+ }),
93
+ } as const satisfies Record<RadioSize, CSSMixinDescriptor>
94
+
95
+ export function radio(options: RadioOptions = {}): RadioMixin {
96
+ let { size = 'md' } = options
97
+ return [radioDefaultAttrs, baseStyle, sizeStyles[size]]
98
+ }
99
+
100
+ export default radio
@@ -196,12 +196,14 @@ function SlugForm(handle: Handle) {
196
196
  function KeyboardTracker(handle: Handle) {
197
197
  let keys: string[] = []
198
198
 
199
- addEventListeners(document, handle.signal, {
200
- keydown: (event) => {
201
- keys.push(event.key)
202
- if (keys.length > 10) keys.shift()
203
- handle.update()
204
- },
199
+ handle.queueTask(() => {
200
+ addEventListeners(document, handle.signal, {
201
+ keydown: (event) => {
202
+ keys.push(event.key)
203
+ if (keys.length > 10) keys.shift()
204
+ handle.update()
205
+ },
206
+ })
205
207
  })
206
208
 
207
209
  return () => <div>Keys: {keys.join(', ') || '(press some keys)'}</div>
@@ -457,14 +459,14 @@ function FormWithScroll(handle: Handle) {
457
459
  // handle.signal - Clock
458
460
  // ============================================================================
459
461
  function Clock(handle: Handle) {
460
- let interval = setInterval(() => {
461
- // clear the interval when the component is disconnected
462
- if (handle.signal.aborted) {
463
- clearInterval(interval)
464
- return
465
- }
466
- handle.update()
467
- }, 1000)
462
+ handle.queueTask(() => {
463
+ let interval = setInterval(() => {
464
+ handle.update()
465
+ }, 1000)
466
+
467
+ handle.signal.addEventListener('abort', () => clearInterval(interval), { once: true })
468
+ })
469
+
468
470
  return () => <span>{new Date().toLocaleTimeString()}</span>
469
471
  }
470
472
 
@@ -340,6 +340,10 @@ function enqueueMixinBindingUpdate(
340
340
  let prevProps = getHostProps(node)
341
341
  let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)
342
342
  patchHostProps(prevProps, nextProps, this.node)
343
+ if (node._controlledState || shouldTrackControlledReflection(nextProps)) {
344
+ ensureControlledReflection(node, this.scheduler)
345
+ syncControlledReflection(node, nextProps)
346
+ }
343
347
 
344
348
  dispatchMixinCommit(state)
345
349
  done(state ? getMixinRuntimeSignal(state) : AbortSignal.abort())
@@ -700,10 +704,12 @@ function insert(
700
704
  cursor = null
701
705
  }
702
706
 
703
- cursor =
704
- node.type === Frame
705
- ? skipCommentsExceptFrameStart(cursor ?? null)
706
- : skipComments(cursor ?? null)
707
+ // Preserve frame-start markers for non-Frame nodes too, so a following <Frame>
708
+ // (e.g. the first child of a bare Fragment at a clientEntry boundary) can still
709
+ // claim its rmx:f marker during hydration instead of being re-inserted fresh.
710
+ // A rmx:f marker always belongs to a <Frame>, so no non-Frame node should
711
+ // consume one.
712
+ cursor = skipCommentsExceptFrameStart(cursor ?? null)
707
713
 
708
714
  // Also check after skipComments in case we skipped past the anchor
709
715
  if (cursor && anchor && cursor === anchor) {
@@ -1097,7 +1103,7 @@ function resolveClientFrame(node: VNode, runtime: FrameRuntime): void {
1097
1103
  let resolveController = new AbortController()
1098
1104
  node._frameResolveController = resolveController
1099
1105
 
1100
- Promise.resolve(runtime.resolveFrame(frameSrc, resolveController.signal))
1106
+ Promise.resolve(runtime.resolveFrame(frameSrc, resolveController.signal, getFrameName(node)))
1101
1107
  .then(async (content) => {
1102
1108
  if (node._frameResolveToken !== token || resolveController.signal.aborted) return
1103
1109
  node._frameFallbackRoot?.dispose()
@@ -172,6 +172,9 @@ export function createRangeRoot(
172
172
  export function createRoot(container: HTMLElement, options: VirtualRootOptions = {}): VirtualRoot {
173
173
  let vroot: VNode | null = null
174
174
  let styles = options.styleManager ?? defaultStyleManager
175
+ if (container.innerHTML.trim() !== '') {
176
+ styles.replaceServerStyles(container)
177
+ }
175
178
  let hydrationCursor = container.innerHTML.trim() !== '' ? container.firstChild : undefined
176
179
 
177
180
  let eventTarget = new TypedEventTarget<VirtualRootEventMap>()
@@ -2,10 +2,11 @@
2
2
 
3
3
  `Select` is a button-triggered popup value picker backed by `listbox` and `popover`. Use it when the user should choose one stable string value from a finite set.
4
4
 
5
- ## Usage
5
+ ## Component Usage
6
6
 
7
7
  ```tsx
8
- import { Option, Select, onSelectChange } from 'remix/ui/select'
8
+ import { Option, Select } from 'remix/ui/select'
9
+ import { onSelectChange } from 'remix/ui/select/primitives'
9
10
 
10
11
  export function FrameworkSelect() {
11
12
  return (
@@ -44,34 +45,34 @@ Use `textValue` when closed-trigger typeahead should match a different string fr
44
45
  </Select>
45
46
  ```
46
47
 
48
+ ## Primitive Usage
49
+
47
50
  Use the lower-level primitives when the trigger or popup structure needs to be owned by another component. Keep the same provider, trigger, popover, list, option, and hidden-input relationship.
48
51
 
49
52
  ```tsx
50
53
  import type { Handle } from 'remix/ui'
51
- import * as button from 'remix/ui/button'
52
- import * as listbox from 'remix/ui/listbox'
53
54
  import * as popover from 'remix/ui/popover'
54
- import * as select from 'remix/ui/select'
55
+ import { triggerStyle } from 'remix/ui/select'
56
+ import * as select from 'remix/ui/select/primitives'
57
+ import { listStyle, optionStyle, surfaceStyle } from './select.styles'
55
58
 
56
59
  function SelectValue(handle: Handle) {
57
60
  let context = handle.context.get(select.Context)
58
61
 
59
- return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
62
+ return () => <span>{context.displayedLabel}</span>
60
63
  }
61
64
 
62
65
  function IssueTypeSelect() {
63
66
  return () => (
64
- <select.Context defaultLabel="Select a type" name="issueType">
65
- <button type="button" mix={[button.baseStyle, select.triggerStyle, select.trigger()]}>
67
+ <select.Context defaultLabel="Select a type" labelSwapDelayMs={100} name="issueType">
68
+ <button type="button" mix={[triggerStyle, select.trigger()]}>
66
69
  <SelectValue />
67
70
  </button>
68
71
  <popover.Context>
69
- <div mix={[popover.surfaceStyle, select.popover()]}>
70
- <div mix={[popover.contentStyle, listbox.listStyle, select.list()]}>
71
- <div mix={[listbox.optionStyle, select.option({ label: 'Bug', value: 'bug' })]}>
72
- Bug
73
- </div>
74
- <div mix={[listbox.optionStyle, select.option({ label: 'Feature', value: 'feature' })]}>
72
+ <div mix={[surfaceStyle, select.popover()]}>
73
+ <div mix={[listStyle, select.list()]}>
74
+ <div mix={[optionStyle, select.option({ label: 'Bug', value: 'bug' })]}>Bug</div>
75
+ <div mix={[optionStyle, select.option({ label: 'Feature', value: 'feature' })]}>
75
76
  Feature
76
77
  </div>
77
78
  </div>
@@ -83,15 +84,24 @@ function IssueTypeSelect() {
83
84
  }
84
85
  ```
85
86
 
86
- ## `select.*`
87
+ ## `remix/ui/select`
87
88
 
88
89
  - `Select`: composed trigger, popover, listbox, option list, and optional hidden input for form participation. Accepts `defaultLabel`, `defaultValue`, `disabled`, `name`, and button props.
89
- - `Option`: option wrapper that renders the standard check glyph and label slot. Accepts `label`, `value`, optional `disabled`, and optional `textValue`.
90
+ - `Option`: option component that renders the standard check indicator and label slot. Accepts `label`, `value`, optional `disabled`, and optional `textValue`.
91
+ - `triggerStyle`: standard select trigger style for custom trigger composition.
92
+ - `SelectProps` and `SelectOptionProps`: public TypeScript props for the composed APIs.
93
+
94
+ ## `remix/ui/select/primitives`
95
+
96
+ - `Context`: lower-level provider for custom composition. Accepts `defaultLabel`, `defaultValue`, `disabled`, `name`, and primitive-only `labelSwapDelayMs`; the label-swap delay defaults to `75` milliseconds.
97
+ - `trigger()`: wires the trigger button, open behavior, closed-trigger typeahead, and trigger ARIA attributes.
98
+ - `popover()`: wires the popover surface and keeps its min-width synced to the trigger before open.
99
+ - `list()`: wires the listbox root used inside the popover.
100
+ - `option(...)`: registers one selectable option. Accepts `label`, `value`, optional `disabled`, and optional `textValue`.
101
+ - `hiddenInput()`: mirrors the selected value into a hidden input for form participation.
90
102
  - `onSelectChange(...)`: event mixin for the bubbling `SelectChangeEvent`.
91
- - `Context`, `trigger()`, `popover()`, `list()`, `option(...)`, and `hiddenInput()`: lower-level composition primitives.
92
- - `triggerStyle`: standard select trigger style.
93
103
  - `SelectChangeEvent`: event with `value`, `label`, and `optionId`.
94
- - `SelectProps`, `SelectContextProps`, and `SelectOptionProps`: public TypeScript props for the composed and lower-level APIs.
104
+ - `SelectContextProps`, `SelectProps`, and `SelectOptionProps`: primitive prop types for custom composition.
95
105
 
96
106
  ## Behavior Notes
97
107
 
@@ -0,0 +1,159 @@
1
+ import { createElement, css } from '@remix-run/ui'
2
+ import type { CSSMixinDescriptor, Handle, Props, RemixNode } from '@remix-run/ui'
3
+
4
+ import * as popover from '@remix-run/ui/popover'
5
+ import * as select from '@remix-run/ui/select/primitives'
6
+ import type { SearchValue } from '../shared/typeahead.ts'
7
+ import { CheckIcon, ChevronVerticalIcon } from '../shared/icons.tsx'
8
+ import {
9
+ listboxIndicatorStyle,
10
+ listboxLabelStyle,
11
+ listboxListStyle,
12
+ listboxOptionStyle,
13
+ popoverSurfaceStyle,
14
+ } from '../shared/listbox-popover-styles.ts'
15
+
16
+ export interface SelectProps extends Omit<Props<'button'>, 'children' | 'name'> {
17
+ children?: RemixNode
18
+ defaultLabel: string
19
+ defaultValue?: string | null
20
+ disabled?: boolean
21
+ name?: string
22
+ }
23
+
24
+ export type SelectOptionProps = Props<'div'> & {
25
+ children?: RemixNode
26
+ disabled?: boolean
27
+ label: string
28
+ textValue?: SearchValue
29
+ value: string
30
+ }
31
+
32
+ const selectTriggerShadow =
33
+ '0 2px 3px -1px rgba(0, 0, 0, 0.04), 0 3px 4px -1.5px rgba(0, 0, 0, 0.04), 0 4px 5px -2px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.12)'
34
+
35
+ const selectTriggerFocusShadow =
36
+ '0 2px 3px -1px rgba(0, 0, 0, 0.04), 0 3px 4px -1.5px rgba(0, 0, 0, 0.04), 0 4px 5px -2px rgba(0, 0, 0, 0.04), 0 0 0 1px #3573F6, 0 0 0 4px rgba(53, 115, 246, 0.1), 0 6px 32px 4px rgba(53, 115, 246, 0.08), inset 0 0 8px 1px rgba(53, 115, 246, 0.05)'
37
+
38
+ const selectTriggerCss: CSSMixinDescriptor = css({
39
+ appearance: 'none',
40
+ margin: 0,
41
+ boxSizing: 'border-box',
42
+ width: '100%',
43
+ minWidth: 0,
44
+ height: '32px',
45
+ paddingBlock: '6px',
46
+ paddingInlineStart: '12px',
47
+ paddingInlineEnd: '8px',
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'flex-start',
51
+ gap: '6px',
52
+ border: 0,
53
+ borderRadius: '8px',
54
+ background: '#FFFFFF',
55
+ boxShadow: selectTriggerShadow,
56
+ color: '#101010',
57
+ fontFamily: '"Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
58
+ fontStyle: 'normal',
59
+ fontWeight: 400,
60
+ fontSize: '13px',
61
+ lineHeight: '20px',
62
+ fontFeatureSettings: '"ss01" on, "cv01" on',
63
+ letterSpacing: 0,
64
+ textAlign: 'left',
65
+ textShadow: '0 1px 0 #FFFFFF',
66
+ whiteSpace: 'nowrap',
67
+ '&:hover, &:focus-visible, &[aria-expanded="true"], &[aria-expanded="true"]:hover, &[aria-expanded="true"]:focus-visible':
68
+ {
69
+ background: '#FFFFFF',
70
+ color: '#101010',
71
+ },
72
+ '&:active': {
73
+ background: '#FFFFFF',
74
+ },
75
+ '&:focus-visible': {
76
+ outline: 0,
77
+ boxShadow: selectTriggerFocusShadow,
78
+ },
79
+ '&[aria-expanded="true"]': {
80
+ boxShadow: selectTriggerFocusShadow,
81
+ },
82
+ '&:disabled': {
83
+ opacity: 0.55,
84
+ },
85
+ })
86
+
87
+ const triggerLabelCss: CSSMixinDescriptor = css({
88
+ display: 'inline-flex',
89
+ alignItems: 'center',
90
+ flex: '1 1 auto',
91
+ minWidth: 0,
92
+ overflow: 'hidden',
93
+ textOverflow: 'ellipsis',
94
+ })
95
+
96
+ const triggerIconCss: CSSMixinDescriptor = css({
97
+ display: 'inline-flex',
98
+ alignItems: 'center',
99
+ justifyContent: 'center',
100
+ width: '16px',
101
+ height: '16px',
102
+ color: '#707070',
103
+ flex: 'none',
104
+ '& > svg': {
105
+ display: 'block',
106
+ width: '100%',
107
+ height: '100%',
108
+ },
109
+ })
110
+
111
+ export const triggerStyle = selectTriggerCss
112
+
113
+ function SelectLabel(handle: Handle): () => RemixNode {
114
+ let context = handle.context.get(select.Context)
115
+
116
+ return () => <span mix={triggerLabelCss}>{context.displayedLabel}</span>
117
+ }
118
+
119
+ export function Select(handle: Handle<SelectProps>): () => RemixNode {
120
+ return () => {
121
+ let { children, defaultLabel, defaultValue, disabled, name, mix, ...buttonProps } = handle.props
122
+
123
+ return (
124
+ <select.Context
125
+ defaultLabel={defaultLabel}
126
+ defaultValue={defaultValue}
127
+ disabled={disabled}
128
+ name={name}
129
+ >
130
+ <button type="button" {...buttonProps} mix={[triggerStyle, select.trigger(), mix]}>
131
+ <SelectLabel />
132
+ <ChevronVerticalIcon mix={triggerIconCss} />
133
+ </button>
134
+ <popover.Context>
135
+ <div mix={[popoverSurfaceStyle, select.popover()]}>
136
+ <div mix={[listboxListStyle, select.list()]}>{children}</div>
137
+ </div>
138
+ </popover.Context>
139
+ {name && <input mix={select.hiddenInput()} />}
140
+ </select.Context>
141
+ )
142
+ }
143
+ }
144
+
145
+ export function Option(handle: Handle<SelectOptionProps>): () => RemixNode {
146
+ return () => {
147
+ let { label, value, disabled, textValue, children, mix, ...divProps } = handle.props
148
+
149
+ return (
150
+ <div
151
+ {...divProps}
152
+ mix={[listboxOptionStyle, select.option({ value, label, disabled, textValue }), mix]}
153
+ >
154
+ <CheckIcon mix={listboxIndicatorStyle} />
155
+ <span mix={listboxLabelStyle}>{children ?? label}</span>
156
+ </div>
157
+ )
158
+ }
159
+ }