@remix-run/ui 0.1.1 → 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 (243) hide show
  1. package/README.md +33 -10
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/animation/spring.d.ts +6 -35
  5. package/dist/animation/spring.js +9 -8
  6. package/dist/animation/spring.js.map +1 -1
  7. package/dist/components/accordion/accordion.d.ts +7 -7
  8. package/dist/components/accordion/accordion.js +0 -2
  9. package/dist/components/accordion/accordion.js.map +1 -1
  10. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/card.demo.js +58 -0
  12. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  15. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  16. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  17. package/dist/components/accordion/demos/overview.demo.js +19 -0
  18. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  19. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  20. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  21. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  22. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  23. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  24. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  25. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  26. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  27. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  28. package/dist/components/button/button.d.ts +86 -4
  29. package/dist/components/button/button.js +63 -3
  30. package/dist/components/button/button.js.map +1 -1
  31. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  32. package/dist/components/button/demos/aliases.demo.js +18 -0
  33. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  34. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  35. package/dist/components/button/demos/basic.demo.js +19 -0
  36. package/dist/components/button/demos/basic.demo.js.map +1 -0
  37. package/dist/components/button/demos/states.demo.d.ts +6 -0
  38. package/dist/components/button/demos/states.demo.js +29 -0
  39. package/dist/components/button/demos/states.demo.js.map +1 -0
  40. package/dist/components/combobox/combobox.d.ts +11 -11
  41. package/dist/components/combobox/combobox.js +0 -2
  42. package/dist/components/combobox/combobox.js.map +1 -1
  43. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  44. package/dist/components/combobox/demos/overview.demo.js +153 -0
  45. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  46. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  47. package/dist/components/listbox/demos/overview.demo.js +64 -0
  48. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  49. package/dist/components/listbox/listbox.d.ts +4 -9
  50. package/dist/components/listbox/listbox.js +21 -33
  51. package/dist/components/listbox/listbox.js.map +1 -1
  52. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  53. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  54. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  55. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  56. package/dist/components/menu/demos/overview.demo.js +32 -0
  57. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  58. package/dist/components/menu/menu.d.ts +15 -24
  59. package/dist/components/menu/menu.js +23 -38
  60. package/dist/components/menu/menu.js.map +1 -1
  61. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  62. package/dist/components/popover/demos/overview.demo.js +64 -0
  63. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  64. package/dist/components/popover/popover.d.ts +5 -5
  65. package/dist/components/popover/popover.js.map +1 -1
  66. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  67. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  68. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  69. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  70. package/dist/components/select/demos/overview.demo.js +27 -0
  71. package/dist/components/select/demos/overview.demo.js.map +1 -0
  72. package/dist/components/select/select.d.ts +11 -11
  73. package/dist/components/select/select.js +17 -26
  74. package/dist/components/select/select.js.map +1 -1
  75. package/dist/components/tabs/tabs.d.ts +6 -6
  76. package/dist/components/tabs/tabs.js +0 -2
  77. package/dist/components/tabs/tabs.js.map +1 -1
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/interactions/keydown/keydown.d.ts +1 -1
  81. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  82. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  83. package/dist/runtime/component.d.ts +18 -12
  84. package/dist/runtime/component.js +93 -80
  85. package/dist/runtime/component.js.map +1 -1
  86. package/dist/runtime/core/attributes.d.ts +14 -0
  87. package/dist/runtime/core/attributes.js +103 -0
  88. package/dist/runtime/core/attributes.js.map +1 -0
  89. package/dist/runtime/core/children.d.ts +7 -0
  90. package/dist/runtime/core/children.js +30 -0
  91. package/dist/runtime/core/children.js.map +1 -0
  92. package/dist/runtime/core/props.d.ts +9 -0
  93. package/dist/runtime/core/props.js +170 -0
  94. package/dist/runtime/core/props.js.map +1 -0
  95. package/dist/runtime/core/vnode.d.ts +3 -0
  96. package/dist/runtime/core/vnode.js +39 -0
  97. package/dist/runtime/core/vnode.js.map +1 -0
  98. package/dist/runtime/create-element.js +2 -1
  99. package/dist/runtime/create-element.js.map +1 -1
  100. package/dist/runtime/diff-dom.js +47 -14
  101. package/dist/runtime/diff-dom.js.map +1 -1
  102. package/dist/runtime/diff-props.d.ts +1 -2
  103. package/dist/runtime/diff-props.js +2 -181
  104. package/dist/runtime/diff-props.js.map +1 -1
  105. package/dist/runtime/dom.d.ts +20 -9
  106. package/dist/runtime/frame.d.ts +23 -2
  107. package/dist/runtime/frame.js +41 -47
  108. package/dist/runtime/frame.js.map +1 -1
  109. package/dist/runtime/jsx.d.ts +1 -1
  110. package/dist/runtime/jsx.js +2 -28
  111. package/dist/runtime/jsx.js.map +1 -1
  112. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  114. package/dist/runtime/mixins/mixin.d.ts +2 -1
  115. package/dist/runtime/mixins/mixin.js +9 -4
  116. package/dist/runtime/mixins/mixin.js.map +1 -1
  117. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  118. package/dist/runtime/mixins/on-mixin.js +9 -2
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  120. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  121. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  122. package/dist/runtime/navigation.js +9 -1
  123. package/dist/runtime/navigation.js.map +1 -1
  124. package/dist/runtime/reconcile.d.ts +1 -1
  125. package/dist/runtime/reconcile.js +452 -197
  126. package/dist/runtime/reconcile.js.map +1 -1
  127. package/dist/runtime/run.d.ts +17 -2
  128. package/dist/runtime/run.js.map +1 -1
  129. package/dist/runtime/scheduler.d.ts +10 -10
  130. package/dist/runtime/scheduler.js +1 -1
  131. package/dist/runtime/scheduler.js.map +1 -1
  132. package/dist/runtime/stream-protocol.d.ts +8 -0
  133. package/dist/runtime/stream-protocol.js +20 -0
  134. package/dist/runtime/stream-protocol.js.map +1 -0
  135. package/dist/runtime/svg-attributes.js +1 -0
  136. package/dist/runtime/svg-attributes.js.map +1 -1
  137. package/dist/runtime/to-vnode.js +7 -11
  138. package/dist/runtime/to-vnode.js.map +1 -1
  139. package/dist/runtime/vnode.d.ts +11 -5
  140. package/dist/runtime/vnode.js +6 -3
  141. package/dist/runtime/vnode.js.map +1 -1
  142. package/dist/server/stream.d.ts +2 -0
  143. package/dist/server/stream.js +154 -114
  144. package/dist/server/stream.js.map +1 -1
  145. package/dist/style/css-mixin.d.ts +2 -1
  146. package/dist/style/css-mixin.js.map +1 -1
  147. package/dist/style/index.d.ts +1 -1
  148. package/dist/style/layers.d.ts +2 -0
  149. package/dist/style/layers.js +3 -0
  150. package/dist/style/layers.js.map +1 -0
  151. package/dist/style/stylesheet.d.ts +13 -11
  152. package/dist/style/stylesheet.js +69 -8
  153. package/dist/style/stylesheet.js.map +1 -1
  154. package/dist/theme/contract.d.ts +24 -222
  155. package/dist/theme/contract.js +2 -2
  156. package/dist/theme/contract.js.map +1 -1
  157. package/dist/theme/glyph-contract.d.ts +1 -3
  158. package/dist/theme/glyph-contract.js.map +1 -1
  159. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  160. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  161. package/dist/theme/runtime.js +4 -1
  162. package/dist/theme/runtime.js.map +1 -1
  163. package/dist/utils/scroll-lock.d.ts +3 -3
  164. package/dist/utils/scroll-lock.js.map +1 -1
  165. package/package.json +3 -3
  166. package/src/animation/README.md +316 -0
  167. package/src/animation/animate-layout-mixin.ts +79 -71
  168. package/src/animation/spring.ts +21 -9
  169. package/src/components/accordion/README.md +166 -0
  170. package/src/components/accordion/accordion.tsx +14 -14
  171. package/src/components/accordion/demos/card.demo.tsx +113 -0
  172. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  173. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  174. package/src/components/anchor/README.md +153 -0
  175. package/src/components/anchor/anchor.ts +1 -1
  176. package/src/components/breadcrumbs/README.md +55 -0
  177. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
  178. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  179. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  180. package/src/components/button/README.md +4 -4
  181. package/src/components/button/button.tsx +118 -20
  182. package/src/components/button/demos/aliases.demo.tsx +27 -0
  183. package/src/components/button/demos/basic.demo.tsx +30 -0
  184. package/src/components/button/demos/states.demo.tsx +52 -0
  185. package/src/components/combobox/README.md +1 -1
  186. package/src/components/combobox/combobox.tsx +37 -21
  187. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  188. package/src/components/glyph/README.md +72 -0
  189. package/src/components/glyph/glyph.tsx +2 -2
  190. package/src/components/listbox/README.md +115 -0
  191. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  192. package/src/components/listbox/listbox.ts +106 -110
  193. package/src/components/menu/README.md +96 -0
  194. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  195. package/src/components/menu/demos/overview.demo.tsx +76 -0
  196. package/src/components/menu/menu.tsx +61 -55
  197. package/src/components/popover/README.md +3 -3
  198. package/src/components/popover/demos/overview.demo.tsx +143 -0
  199. package/src/components/popover/popover.ts +90 -79
  200. package/src/components/select/README.md +107 -0
  201. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  202. package/src/components/select/demos/overview.demo.tsx +51 -0
  203. package/src/components/select/select.tsx +53 -36
  204. package/src/components/tabs/tabs.tsx +10 -5
  205. package/src/index.ts +2 -1
  206. package/src/runtime/component.ts +122 -88
  207. package/src/runtime/core/attributes.ts +118 -0
  208. package/src/runtime/core/children.ts +42 -0
  209. package/src/runtime/core/props.ts +187 -0
  210. package/src/runtime/core/vnode.ts +48 -0
  211. package/src/runtime/create-element.ts +2 -1
  212. package/src/runtime/diff-dom.ts +59 -12
  213. package/src/runtime/diff-props.ts +2 -187
  214. package/src/runtime/dom.ts +20 -9
  215. package/src/runtime/frame.ts +71 -55
  216. package/src/runtime/jsx.ts +3 -30
  217. package/src/runtime/mixins/link-mixin.ts +88 -86
  218. package/src/runtime/mixins/mixin.ts +18 -5
  219. package/src/runtime/mixins/on-mixin.ts +20 -12
  220. package/src/runtime/mixins/ref-mixin.ts +4 -5
  221. package/src/runtime/navigation.ts +10 -1
  222. package/src/runtime/reconcile.ts +563 -223
  223. package/src/runtime/run.ts +17 -1
  224. package/src/runtime/scheduler.ts +20 -20
  225. package/src/runtime/stream-protocol.ts +27 -0
  226. package/src/runtime/svg-attributes.ts +1 -0
  227. package/src/runtime/to-vnode.ts +7 -10
  228. package/src/runtime/vnode.ts +16 -9
  229. package/src/server/README.md +90 -0
  230. package/src/server/stream.ts +176 -124
  231. package/src/style/css-mixin.ts +6 -1
  232. package/src/style/index.ts +1 -2
  233. package/src/style/layers.ts +2 -0
  234. package/src/style/stylesheet.ts +88 -11
  235. package/src/test/README.md +107 -0
  236. package/src/test/utils.ts +3 -1
  237. package/src/theme/README.md +103 -0
  238. package/src/theme/contract.ts +37 -5
  239. package/src/theme/glyph-contract.ts +3 -1
  240. package/src/theme/presets/rmx-01/index.ts +4 -2
  241. package/src/theme/runtime.ts +4 -1
  242. package/src/utils/scroll-lock/README.md +33 -0
  243. package/src/utils/scroll-lock.ts +7 -3
@@ -4,7 +4,9 @@ import {
4
4
  css,
5
5
  on,
6
6
  type CSSMixinDescriptor,
7
+ type ElementProps,
7
8
  type Handle,
9
+ type MixinFactory,
8
10
  type RemixNode,
9
11
  } from '@remix-run/ui'
10
12
  import { anchor as positionAnchor, type AnchorOptions } from '../anchor/anchor.ts'
@@ -75,7 +77,7 @@ const popoverSurfaceCss: CSSMixinDescriptor = css({
75
77
  export const contentStyle = popoverContentCss
76
78
  export const surfaceStyle = [popoverSurfaceCss, popoverSurfaceTransitionCss] as const
77
79
 
78
- function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>) {
80
+ function PopoverProvider(handle: Handle<PopoverProps, PopoverContext>): () => RemixNode {
79
81
  handle.context.set({
80
82
  hideFocusTarget: null,
81
83
  showFocusTarget: null,
@@ -99,7 +101,10 @@ export interface PopoverHideRequest {
99
101
  target?: Node | null
100
102
  }
101
103
 
102
- const anchorMixin = createMixin<HTMLElement, [options: AnchorOptions]>((handle) => {
104
+ const anchorMixin: MixinFactory<HTMLElement, [options: AnchorOptions], ElementProps> = createMixin<
105
+ HTMLElement,
106
+ [options: AnchorOptions]
107
+ >((handle) => {
103
108
  let context = handle.context.get(PopoverProvider)
104
109
  return (options) => {
105
110
  handle.queueTask((node) => {
@@ -108,91 +113,97 @@ const anchorMixin = createMixin<HTMLElement, [options: AnchorOptions]>((handle)
108
113
  }
109
114
  })
110
115
 
111
- const surfaceMixin = createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
112
- let openProp = false
113
- let cleanupAnchor = () => {}
114
- let unlockScroll = () => {}
115
- let context = handle.context.get(PopoverProvider)
116
-
117
- handle.queueTask((node, signal) => {
118
- context.surface = node
119
-
120
- signal.addEventListener('abort', () => {
121
- if (context.surface === node) {
122
- context.surface = null
123
- }
124
- })
125
- })
116
+ const surfaceMixin: MixinFactory<HTMLElement, [options: PopoverSurfaceOptions], ElementProps> =
117
+ createMixin<HTMLElement, [options: PopoverSurfaceOptions]>((handle) => {
118
+ let openProp = false
119
+ let cleanupAnchor = () => {}
120
+ let unlockScroll = () => {}
121
+ let context = handle.context.get(PopoverProvider)
126
122
 
127
- return (options) => {
128
- let wasOpen = openProp
129
- openProp = options.open
130
-
131
- handle.queueTask(async (node) => {
132
- if (openProp && !wasOpen) {
133
- node.showPopover()
134
- } else if (!openProp && wasOpen) {
135
- node.hidePopover()
136
- }
137
- })
123
+ handle.queueTask((node, signal) => {
124
+ context.surface = node
138
125
 
139
- return [
140
- attrs({ popover: 'manual' }),
141
-
142
- on('beforetoggle', (event) => {
143
- if (event.newState === 'open') {
144
- cleanupAnchor = positionAnchor(
145
- event.currentTarget,
146
- context.anchor!.node,
147
- context.anchor!.options,
148
- )
149
- unlockScroll = lockScroll()
150
- } else if (event.newState === 'closed') {
151
- cleanupAnchor()
152
- unlockScroll()
126
+ signal.addEventListener('abort', () => {
127
+ if (context.surface === node) {
128
+ context.surface = null
153
129
  }
154
- }),
130
+ })
131
+ })
155
132
 
156
- on('toggle', async (event) => {
157
- if (event.newState === 'open') {
158
- context.showFocusTarget?.focus()
159
- } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
160
- context.hideFocusTarget?.focus()
161
- }
162
- }),
133
+ return (options) => {
134
+ let wasOpen = openProp
135
+ openProp = options.open
163
136
 
164
- on('keydown', (event) => {
165
- if (event.key === 'Escape') {
166
- event.preventDefault()
167
- options.onHide({ reason: 'escape-key' })
137
+ handle.queueTask(async (node) => {
138
+ if (openProp && !wasOpen) {
139
+ node.showPopover()
140
+ } else if (!openProp && wasOpen) {
141
+ node.hidePopover()
168
142
  }
169
- }),
170
-
171
- onOutsideClick(
172
- openProp,
173
- (target) => {
174
- options.onHide({ reason: 'outside-click', target })
175
- },
176
- (target) => options.closeOnAnchorClick === false && !!context.anchor?.node.contains(target),
177
- options.stopOutsideClickPropagation ?? true,
178
- ),
179
- ]
180
- }
181
- })
182
-
183
- const focusOnShowMixin = createMixin<HTMLElement, []>((handle) => {
184
- handle.addEventListener('insert', (event) => {
185
- let context = handle.context.get(PopoverProvider)
186
- context.showFocusTarget = event.node
143
+ })
144
+
145
+ return [
146
+ attrs({ popover: 'manual' }),
147
+
148
+ on('beforetoggle', (event) => {
149
+ if (event.newState === 'open') {
150
+ cleanupAnchor = positionAnchor(
151
+ event.currentTarget,
152
+ context.anchor!.node,
153
+ context.anchor!.options,
154
+ )
155
+ unlockScroll = lockScroll()
156
+ } else if (event.newState === 'closed') {
157
+ cleanupAnchor()
158
+ unlockScroll()
159
+ }
160
+ }),
161
+
162
+ on('toggle', async (event) => {
163
+ if (event.newState === 'open') {
164
+ context.showFocusTarget?.focus()
165
+ } else if (event.newState === 'closed' && options.restoreFocusOnHide !== false) {
166
+ context.hideFocusTarget?.focus()
167
+ }
168
+ }),
169
+
170
+ on('keydown', (event) => {
171
+ if (event.key === 'Escape') {
172
+ event.preventDefault()
173
+ options.onHide({ reason: 'escape-key' })
174
+ }
175
+ }),
176
+
177
+ onOutsideClick(
178
+ openProp,
179
+ (target) => {
180
+ options.onHide({ reason: 'outside-click', target })
181
+ },
182
+ (target) =>
183
+ options.closeOnAnchorClick === false && !!context.anchor?.node.contains(target),
184
+ options.stopOutsideClickPropagation ?? true,
185
+ ),
186
+ ]
187
+ }
187
188
  })
188
- })
189
189
 
190
- const focusOnHideMixin = createMixin<HTMLElement, []>((handle) => {
191
- handle.addEventListener('insert', (event) => {
192
- let context = handle.context.get(PopoverProvider)
193
- context.hideFocusTarget = event.node
194
- })
195
- })
190
+ const focusOnShowMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
191
+ (handle) => {
192
+ handle.addEventListener('insert', (event) => {
193
+ let context = handle.context.get(PopoverProvider)
194
+ context.showFocusTarget = event.node
195
+ })
196
+ },
197
+ )
198
+
199
+ const focusOnHideMixin: MixinFactory<HTMLElement, [], ElementProps> = createMixin<HTMLElement, []>(
200
+ (handle) => {
201
+ handle.addEventListener('insert', (event) => {
202
+ let context = handle.context.get(PopoverProvider)
203
+ context.hideFocusTarget = event.node
204
+ })
205
+ },
206
+ )
196
207
 
197
208
  export const Context = PopoverProvider
198
209
  export const anchor = anchorMixin
@@ -0,0 +1,107 @@
1
+ # select
2
+
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
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Option, Select, onSelectChange } from 'remix/ui/select'
9
+
10
+ export function FrameworkSelect() {
11
+ return (
12
+ <Select
13
+ defaultLabel="Select a framework"
14
+ defaultValue="remix"
15
+ name="framework"
16
+ mix={onSelectChange((event) => {
17
+ console.log(event.value, event.label, event.optionId)
18
+ })}
19
+ >
20
+ <Option label="Remix framework" value="remix">
21
+ Remix
22
+ </Option>
23
+ <Option disabled label="React Router framework" value="react-router">
24
+ React Router
25
+ </Option>
26
+ <Option label="React framework" value="react">
27
+ React
28
+ </Option>
29
+ </Select>
30
+ )
31
+ }
32
+ ```
33
+
34
+ Use `textValue` when closed-trigger typeahead should match a different string from the visible label.
35
+
36
+ ```tsx
37
+ <Select defaultLabel="Select an environment">
38
+ <Option label="Production environment" value="production">
39
+ Production
40
+ </Option>
41
+ <Option label="Staging environment" textValue="beta" value="staging">
42
+ Staging
43
+ </Option>
44
+ </Select>
45
+ ```
46
+
47
+ 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
+
49
+ ```tsx
50
+ import type { Handle } from 'remix/ui'
51
+ import * as button from 'remix/ui/button'
52
+ import * as listbox from 'remix/ui/listbox'
53
+ import * as popover from 'remix/ui/popover'
54
+ import * as select from 'remix/ui/select'
55
+
56
+ function SelectValue(handle: Handle) {
57
+ let context = handle.context.get(select.Context)
58
+
59
+ return () => <span mix={button.labelStyle}>{context.displayedLabel}</span>
60
+ }
61
+
62
+ function IssueTypeSelect() {
63
+ return () => (
64
+ <select.Context defaultLabel="Select a type" name="issueType">
65
+ <button type="button" mix={[button.baseStyle, select.triggerStyle, select.trigger()]}>
66
+ <SelectValue />
67
+ </button>
68
+ <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' })]}>
75
+ Feature
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </popover.Context>
80
+ <input mix={select.hiddenInput()} />
81
+ </select.Context>
82
+ )
83
+ }
84
+ ```
85
+
86
+ ## `select.*`
87
+
88
+ - `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
+ - `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
+ - `SelectChangeEvent`: event with `value`, `label`, and `optionId`.
94
+ - `SelectProps`, `SelectContextProps`, and `SelectOptionProps`: public TypeScript props for the composed and lower-level APIs.
95
+
96
+ ## Behavior Notes
97
+
98
+ - `defaultLabel` is displayed before selection settles. `defaultValue` selects the matching option without replacing the trigger label until a new selection commits.
99
+ - `Option.label` is the committed display label and event label. `children` are the rendered option contents.
100
+ - Click, `ArrowDown`, and `ArrowUp` open the popup. Focus moves into the list and Escape restores focus to the trigger through popover behavior.
101
+ - Reopening highlights the current selected value.
102
+ - The popup min-width syncs to the trigger width before opening.
103
+ - Closed-trigger typeahead selects a matching option immediately and supports option `textValue`.
104
+ - Selecting an option flashes it with `data-select-flash`, waits for the close transition and label delay, updates the displayed label, and dispatches `SelectChangeEvent`.
105
+ - Selecting the already-selected value updates state but does not dispatch a change event.
106
+ - `SelectChangeEvent` bubbles from the trigger when a trigger exists. It includes `value`, `label`, and `optionId`.
107
+ - Passing `name` renders a hidden input so the selected value participates in `FormData`; disabled selects disable the trigger and hidden input.
@@ -0,0 +1,95 @@
1
+ import { css, type Handle } from '@remix-run/ui'
2
+ import * as button from '@remix-run/ui/button'
3
+ import { Glyph } from '@remix-run/ui/glyph'
4
+ import * as listbox from '@remix-run/ui/listbox'
5
+ import * as popover from '@remix-run/ui/popover'
6
+ import * as select from '@remix-run/ui/select'
7
+ import { theme } from '@remix-run/ui/theme'
8
+
9
+ /**
10
+ * @name Select Deconstructed
11
+ * @description Build a fully custom select by composing the select, popover, and listbox primitives directly.
12
+ */
13
+ export default function Example(handle: Handle) {
14
+ let label = 'Local'
15
+ let value = 'local'
16
+ let triggerId = `${handle.id}-trigger`
17
+
18
+ return () => (
19
+ <div
20
+ mix={[
21
+ stackCss,
22
+ select.onSelectChange((event) => {
23
+ label = event.label ?? 'Select an environment'
24
+ value = event.value ?? 'null'
25
+ void handle.update()
26
+ }),
27
+ ]}
28
+ >
29
+ <p mix={labelCss}>Environment</p>
30
+
31
+ <select.Context defaultLabel="Local" defaultValue="local" name="environment">
32
+ <button
33
+ id={triggerId}
34
+ type="button"
35
+ mix={[button.baseStyle, select.triggerStyle, select.trigger(), selectCss]}
36
+ >
37
+ <span mix={button.labelStyle}>{label}</span>
38
+ <Glyph mix={button.iconStyle} name="chevronVertical" />
39
+ </button>
40
+
41
+ <popover.Context>
42
+ <div mix={[popover.surfaceStyle, select.popover()]}>
43
+ <div
44
+ aria-labelledby={triggerId}
45
+ mix={[popover.contentStyle, listbox.listStyle, select.list()]}
46
+ >
47
+ {environmentOptions.map((option) => (
48
+ <div key={option.value} mix={[listbox.optionStyle, select.option(option)]}>
49
+ <Glyph mix={listbox.glyphStyle} name="check" />
50
+ <span mix={listbox.labelStyle}>{option.label}</span>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ </popover.Context>
56
+
57
+ <input mix={select.hiddenInput()} />
58
+ </select.Context>
59
+
60
+ <p mix={valueCss}>{`value=${value}`}</p>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ const environmentOptions = [
66
+ { label: 'Local', value: 'local' },
67
+ { label: 'Staging', value: 'staging' },
68
+ { label: 'Production', value: 'production' },
69
+ { disabled: true, label: 'Archived', value: 'archived' },
70
+ ] as const
71
+
72
+ const selectCss = css({
73
+ width: '16rem',
74
+ })
75
+
76
+ const stackCss = css({
77
+ display: 'flex',
78
+ flexDirection: 'column',
79
+ gap: theme.space.sm,
80
+ width: '100%',
81
+ })
82
+
83
+ const labelCss = css({
84
+ margin: 0,
85
+ fontSize: theme.fontSize.xs,
86
+ fontWeight: theme.fontWeight.semibold,
87
+ color: theme.colors.text.primary,
88
+ })
89
+
90
+ const valueCss = css({
91
+ margin: 0,
92
+ fontFamily: theme.fontFamily.mono,
93
+ fontSize: theme.fontSize.xs,
94
+ color: theme.colors.text.secondary,
95
+ })
@@ -0,0 +1,51 @@
1
+ import { css } from '@remix-run/ui'
2
+ import { Option, Select } from '@remix-run/ui/select'
3
+ import { theme } from '@remix-run/ui/theme'
4
+
5
+ /**
6
+ * @name Select Overview
7
+ * @description A styled select control with a searchable dropdown and accessible label.
8
+ */
9
+ export default function Example() {
10
+ return () => (
11
+ <div mix={stackCss}>
12
+ <label for="fruit-select" mix={labelCss}>
13
+ Choose a fruit
14
+ </label>
15
+ <Select
16
+ id="fruit-select"
17
+ defaultLabel="Banana"
18
+ defaultValue="banana"
19
+ name="fruit"
20
+ mix={selectCss}
21
+ >
22
+ <Option label="Apple" value="apple" />
23
+ <Option label="Apricot" value="apricot" />
24
+ <Option label="Banana" value="banana" />
25
+ <Option label="Blackberry" value="blackberry" />
26
+ <Option label="Blackcurrant" value="blackcurrant" />
27
+ <Option label="Blueberry" value="blueberry" />
28
+ <Option label="Boysenberry" value="boysenberry" />
29
+ <Option label="Cantaloupe" value="cantaloupe" />
30
+ </Select>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ const selectCss = css({
36
+ width: '16rem',
37
+ })
38
+
39
+ const stackCss = css({
40
+ display: 'flex',
41
+ flexDirection: 'column',
42
+ gap: theme.space.sm,
43
+ width: '100%',
44
+ })
45
+
46
+ const labelCss = css({
47
+ margin: 0,
48
+ fontSize: theme.fontSize.xs,
49
+ fontWeight: theme.fontWeight.semibold,
50
+ color: theme.colors.text.primary,
51
+ })