@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
@@ -0,0 +1,199 @@
1
+ import { css, type Handle } from '@remix-run/ui'
2
+ import { Combobox, ComboboxOption, onComboboxChange } from '@remix-run/ui/combobox'
3
+ import { theme } from '@remix-run/ui/theme'
4
+
5
+ /**
6
+ * @name Combobox Overview
7
+ * @description A searchable combobox with keyboard navigation. Try typing airport names or codes like ord, lax, or jfk.
8
+ */
9
+ export default function Example(handle: Handle) {
10
+ let value: string | null = null
11
+
12
+ return () => (
13
+ <div mix={stackCss}>
14
+ <div mix={fieldCss}>
15
+ <label for="airport-combobox" mix={labelCss}>
16
+ Airport
17
+ </label>
18
+
19
+ <Combobox
20
+ inputId="airport-combobox"
21
+ mix={[
22
+ comboboxCss,
23
+ onComboboxChange((event) => {
24
+ value = event.value
25
+ void handle.update()
26
+ }),
27
+ ]}
28
+ name="airport"
29
+ placeholder="Search airports or codes"
30
+ >
31
+ {airportOptions.map((airport) => (
32
+ <ComboboxOption
33
+ key={airport.value}
34
+ disabled={airport.disabled}
35
+ label={airport.label}
36
+ searchValue={airport.searchValue}
37
+ value={airport.value}
38
+ />
39
+ ))}
40
+ </Combobox>
41
+
42
+ <div mix={helpCss}>
43
+ Try typing `san`, `wash`, or airport codes like `ord`, `lax`, or `jfk`, then use ArrowDown
44
+ and Enter.
45
+ </div>
46
+ </div>
47
+
48
+ <p mix={valueCss}>{`value=${value ?? 'null'}`}</p>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ type AirportOption = {
54
+ disabled?: boolean
55
+ label: string
56
+ searchValue: string[]
57
+ value: string
58
+ }
59
+
60
+ const airportOptions: AirportOption[] = [
61
+ {
62
+ label: 'Hartsfield-Jackson Atlanta International',
63
+ searchValue: ['atl', 'atlanta', 'hartsfield-jackson atlanta international'],
64
+ value: 'ATL',
65
+ },
66
+ {
67
+ label: 'Austin-Bergstrom International',
68
+ searchValue: ['aus', 'austin', 'austin-bergstrom international'],
69
+ value: 'AUS',
70
+ },
71
+ {
72
+ label: 'Boston Logan International',
73
+ searchValue: ['bos', 'boston', 'boston logan international'],
74
+ value: 'BOS',
75
+ },
76
+ {
77
+ label: 'Baltimore/Washington International',
78
+ searchValue: ['bwi', 'baltimore', 'washington', 'baltimore/washington international'],
79
+ value: 'BWI',
80
+ },
81
+ {
82
+ label: 'Ronald Reagan Washington National',
83
+ searchValue: ['dca', 'reagan', 'washington', 'ronald reagan washington national'],
84
+ value: 'DCA',
85
+ },
86
+ {
87
+ label: 'Denver International',
88
+ searchValue: ['den', 'denver', 'denver international'],
89
+ value: 'DEN',
90
+ },
91
+ {
92
+ label: 'Dallas/Fort Worth International',
93
+ searchValue: ['dfw', 'dallas', 'fort worth', 'dallas/fort worth international'],
94
+ value: 'DFW',
95
+ },
96
+ {
97
+ label: 'Newark Liberty International',
98
+ searchValue: ['ewr', 'newark', 'newark liberty international'],
99
+ value: 'EWR',
100
+ },
101
+ {
102
+ label: 'Daniel K. Inouye International',
103
+ searchValue: ['hnl', 'honolulu', 'daniel k. inouye international'],
104
+ value: 'HNL',
105
+ },
106
+ {
107
+ label: 'Washington Dulles International',
108
+ searchValue: ['iad', 'dulles', 'washington', 'washington dulles international'],
109
+ value: 'IAD',
110
+ },
111
+ {
112
+ label: 'John F. Kennedy International',
113
+ searchValue: ['jfk', 'kennedy', 'new york', 'john f. kennedy international'],
114
+ value: 'JFK',
115
+ },
116
+ {
117
+ label: 'Harry Reid International',
118
+ searchValue: ['las', 'las vegas', 'harry reid international'],
119
+ value: 'LAS',
120
+ },
121
+ {
122
+ label: 'Los Angeles International',
123
+ searchValue: ['lax', 'los angeles', 'los angeles international'],
124
+ value: 'LAX',
125
+ },
126
+ {
127
+ label: 'Orlando International',
128
+ searchValue: ['mco', 'orlando', 'orlando international'],
129
+ value: 'MCO',
130
+ },
131
+ {
132
+ label: 'Miami International',
133
+ searchValue: ['mia', 'miami', 'miami international'],
134
+ value: 'MIA',
135
+ },
136
+ {
137
+ label: "Chicago O'Hare International",
138
+ searchValue: ['ord', 'ohare', 'chicago', "chicago o'hare international"],
139
+ value: 'ORD',
140
+ },
141
+ {
142
+ label: 'Portland International',
143
+ searchValue: ['pdx', 'portland', 'portland international'],
144
+ value: 'PDX',
145
+ },
146
+ {
147
+ label: 'San Diego International',
148
+ searchValue: ['san', 'san diego', 'san diego international'],
149
+ value: 'SAN',
150
+ },
151
+ {
152
+ label: 'San Francisco International',
153
+ searchValue: ['sfo', 'san francisco', 'san francisco international'],
154
+ value: 'SFO',
155
+ },
156
+ {
157
+ disabled: true,
158
+ label: 'San Jose Mineta International',
159
+ searchValue: ['sjc', 'san jose', 'san jose mineta international'],
160
+ value: 'SJC',
161
+ },
162
+ ]
163
+
164
+ const comboboxCss = css({
165
+ width: '16rem',
166
+ })
167
+
168
+ const stackCss = css({
169
+ display: 'flex',
170
+ flexDirection: 'column',
171
+ gap: theme.space.sm,
172
+ width: '100%',
173
+ })
174
+
175
+ const fieldCss = css({
176
+ display: 'flex',
177
+ flexDirection: 'column',
178
+ gap: theme.space.xs,
179
+ })
180
+
181
+ const labelCss = css({
182
+ margin: 0,
183
+ fontSize: theme.fontSize.xs,
184
+ fontWeight: theme.fontWeight.semibold,
185
+ color: theme.colors.text.primary,
186
+ })
187
+
188
+ const helpCss = css({
189
+ fontSize: theme.fontSize.xs,
190
+ lineHeight: theme.lineHeight.relaxed,
191
+ color: theme.colors.text.secondary,
192
+ })
193
+
194
+ const valueCss = css({
195
+ margin: 0,
196
+ fontFamily: theme.fontFamily.mono,
197
+ fontSize: theme.fontSize.xs,
198
+ color: theme.colors.text.secondary,
199
+ })
@@ -0,0 +1,72 @@
1
+ # glyph
2
+
3
+ `Glyph` renders references into a shared SVG sprite sheet. Render a glyph sheet once, then render individual `Glyph` instances by name.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Glyph } from 'remix/ui/glyph'
9
+ import { RMX_01_GLYPHS } from 'remix/ui/theme'
10
+
11
+ function Layout() {
12
+ return (
13
+ <html>
14
+ <body>
15
+ <RMX_01_GLYPHS />
16
+ <button aria-label="Delete">
17
+ <Glyph name="trash" />
18
+ </button>
19
+ </body>
20
+ </html>
21
+ )
22
+ }
23
+ ```
24
+
25
+ Most glyphs are decorative because the surrounding control supplies the accessible name. `Glyph` sets `aria-hidden` by default in that case.
26
+
27
+ ```tsx
28
+ <button aria-label="Search">
29
+ <Glyph name="search" />
30
+ </button>
31
+ ```
32
+
33
+ Give the glyph its own label only when the SVG itself is the accessible element.
34
+
35
+ ```tsx
36
+ <Glyph aria-label="Search" name="search" viewBox="0 0 20 20" width="24" />
37
+ ```
38
+
39
+ Use `createGlyphSheet` when a theme or app provides its own complete glyph set. The generated sheet exposes the stable symbol ids and the original values for reuse.
40
+
41
+ ```tsx
42
+ import { createGlyphSheet, type GlyphValues } from 'remix/ui/glyph'
43
+
44
+ declare const glyphValues: GlyphValues
45
+
46
+ export const AppGlyphs = createGlyphSheet(glyphValues)
47
+
48
+ AppGlyphs.ids.trash
49
+ AppGlyphs.values.trash
50
+ ```
51
+
52
+ ## `glyph.*`
53
+
54
+ - `Glyph`: renders an `<svg>` with a `<use>` element that points at the package-owned symbol id for `name`.
55
+ - `createGlyphSheet(values)`: creates a hidden SVG sprite sheet component from a complete glyph value set.
56
+ - `GlyphName`: typed union of supported glyph names.
57
+ - `GlyphProps`: props accepted by `Glyph`.
58
+ - `GlyphSheetProps`: props accepted by generated glyph sheet components.
59
+ - `GlyphSymbol`: SVG symbol value accepted by glyph value maps.
60
+ - `GlyphValues`: object shape expected by `createGlyphSheet`.
61
+ - `GlyphSheetComponent`: generated sprite sheet component with `ids` and `values` attached.
62
+
63
+ The built-in glyph names are `add`, `alert`, `check`, `chevronDown`, `chevronVertical`, `chevronUp`, `chevronRight`, `close`, `copy`, `edit`, `expand`, `info`, `menu`, `open`, `search`, `spinner`, and `trash`.
64
+
65
+ ## Behavior Notes
66
+
67
+ - Render the glyph sheet once before rendering glyph instances that reference it.
68
+ - `createGlyphSheet` renders a hidden zero-size SVG and clones each provided `<symbol>` with the stable package id.
69
+ - `Glyph` is `aria-hidden` by default when no accessible label or labelled-by relationship is provided.
70
+ - Labeled glyphs keep their accessible label and do not force `aria-hidden`.
71
+ - Host SVG props such as `viewBox`, `width`, `mix`, and `aria-label` are preserved.
72
+ - `createGlyphSheet` throws if a provided glyph value is not a `<symbol>` element.
@@ -31,7 +31,7 @@ export function createGlyphSheet(values: GlyphValues): GlyphSheetComponent {
31
31
  >,
32
32
  )
33
33
 
34
- function GlyphSheet(handle: Handle<GlyphSheetProps>) {
34
+ function GlyphSheet(handle: Handle<GlyphSheetProps>): () => RemixElement {
35
35
  return () => {
36
36
  let { style, ...svgProps } = handle.props
37
37
  let hiddenStyle = {
@@ -70,7 +70,7 @@ export function createGlyphSheet(values: GlyphValues): GlyphSheetComponent {
70
70
  })
71
71
  }
72
72
 
73
- export function Glyph(handle: Handle<GlyphProps>) {
73
+ export function Glyph(handle: Handle<GlyphProps>): () => RemixElement {
74
74
  return () => {
75
75
  let { fill, name, ...svgProps } = handle.props
76
76
  let glyphId = glyphContract[name].id
@@ -0,0 +1,115 @@
1
+ # listbox
2
+
3
+ `listbox` is a headless option-list primitive for controlled selection and highlighting. Use it under components like `select` and `combobox`, or directly when you need custom listbox markup.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import type { Handle } from 'remix/ui'
9
+ import { Glyph } from 'remix/ui/glyph'
10
+ import * as listbox from 'remix/ui/listbox'
11
+ import type { ListboxValue } from 'remix/ui/listbox'
12
+
13
+ function FrameworkListbox(handle: Handle) {
14
+ let value: ListboxValue = 'remix'
15
+ let activeValue: ListboxValue = 'remix'
16
+
17
+ return () => (
18
+ <listbox.Context
19
+ value={value}
20
+ activeValue={activeValue}
21
+ onSelect={(nextValue) => {
22
+ value = nextValue
23
+ void handle.update()
24
+ }}
25
+ onHighlight={(nextValue) => {
26
+ activeValue = nextValue
27
+ void handle.update()
28
+ }}
29
+ >
30
+ <div aria-label="Frameworks" tabIndex={0} mix={[listbox.listStyle, listbox.list()]}>
31
+ {frameworks.map((option) => (
32
+ <div key={option.value} mix={[listbox.optionStyle, listbox.option(option)]}>
33
+ <Glyph mix={listbox.glyphStyle} name="check" />
34
+ <span mix={listbox.labelStyle}>{option.label}</span>
35
+ </div>
36
+ ))}
37
+ </div>
38
+ </listbox.Context>
39
+ )
40
+ }
41
+
42
+ let frameworks = [
43
+ { label: 'Remix', value: 'remix' },
44
+ { disabled: true, label: 'React Router', value: 'react-router' },
45
+ { label: 'React', value: 'react' },
46
+ { label: 'Preact', value: 'preact' },
47
+ ]
48
+ ```
49
+
50
+ Use `textValue` when the visible label is not the best string for typeahead search.
51
+
52
+ ```tsx
53
+ <div
54
+ mix={[
55
+ listbox.option({
56
+ label: 'Staging',
57
+ textValue: 'beta',
58
+ value: 'staging',
59
+ }),
60
+ ]}
61
+ >
62
+ Staging
63
+ </div>
64
+ ```
65
+
66
+ Use `ref` when a parent component needs imperative coordination with the current option registry.
67
+
68
+ ```tsx
69
+ import type { ListboxRef } from 'remix/ui/listbox'
70
+
71
+ let listboxRef: ListboxRef | undefined
72
+
73
+ function selectLastOption() {
74
+ listboxRef?.navigateLast()
75
+ void listboxRef?.selectActive()
76
+ }
77
+
78
+ ;<listbox.Context
79
+ value={value}
80
+ activeValue={activeValue}
81
+ ref={(ref) => {
82
+ listboxRef = ref
83
+ }}
84
+ onSelect={(nextValue) => {
85
+ value = nextValue
86
+ }}
87
+ onHighlight={(nextActiveValue) => {
88
+ activeValue = nextActiveValue
89
+ }}
90
+ >
91
+ {/* listbox markup */}
92
+ </listbox.Context>
93
+ ```
94
+
95
+ ## `listbox.*`
96
+
97
+ - `listbox.Context`: provider for controlled `value` and `activeValue`, option registration, selection, highlighting, optional ref access, `flashSelection`, `selectionFlashAttribute`, and `onSelectSettled`.
98
+ - `listbox.list()`: mixin that wires `role="listbox"`, default `tabIndex={-1}`, keyboard navigation, focus scrolling, and typeahead highlighting.
99
+ - `listbox.option(options)`: mixin that registers an option with required `label` and `value`, optional `disabled` and `textValue`, and wires `role="option"`, id, selected, disabled, highlighted, mouse, and click behavior.
100
+ - `listStyle`, `optionStyle`, `glyphStyle`, and `labelStyle`: flat style mixins for standard listbox presentation.
101
+ - `ListboxValue`: selected or active value, represented as `string | null`.
102
+ - `ListboxOption`: option input shape with `label`, `value`, optional `disabled`, and optional `textValue`.
103
+ - `ListboxRegisteredOption`: registered option metadata passed to callbacks and refs.
104
+ - `ListboxRef`: live ref object exposing active/selected options, option navigation, search matching, scrolling, and selection helpers.
105
+
106
+ ## Behavior Notes
107
+
108
+ - Selection and highlighting are controlled. `onSelect` and `onHighlight` notify the parent, but DOM state updates after the parent rerenders with new values.
109
+ - Disabled options are skipped by keyboard navigation, typeahead, mouse movement, and click selection.
110
+ - Arrow keys wrap through enabled options. `Home` and `End` move to enabled boundaries. `Enter` and Space select the active option.
111
+ - Mouse movement highlights enabled options. `mouseleave` clears the highlight when leaving the active option.
112
+ - `Tab` is prevented and highlights the first enabled option.
113
+ - Typeahead highlights the next matching enabled option without selecting it and supports `textValue`.
114
+ - Focus and keyboard navigation scroll the active option into view with nearest-edge alignment.
115
+ - `flashSelection` applies `selectionFlashAttribute` for 60ms, delays `onSelectSettled`, and ignores new highlight/select interactions until the flash completes.
@@ -0,0 +1,101 @@
1
+ import { css, on, type Handle } from '@remix-run/ui'
2
+ import { Glyph } from '@remix-run/ui/glyph'
3
+ import * as listbox from '@remix-run/ui/listbox'
4
+ import type { ListboxValue } from '@remix-run/ui/listbox'
5
+ import { theme } from '@remix-run/ui/theme'
6
+
7
+ /**
8
+ * @name Listbox Overview
9
+ * @description A keyboard-navigable listbox with selection, highlighting, and an optional flash-selection animation.
10
+ */
11
+ export default function Example(handle: Handle) {
12
+ let value: ListboxValue = options[0]!.value
13
+ let activeValue: ListboxValue = options[0]!.value
14
+ let flashSelection = false
15
+
16
+ return () => {
17
+ return (
18
+ <div mix={stackCss}>
19
+ <listbox.Context
20
+ value={value}
21
+ activeValue={activeValue}
22
+ flashSelection={flashSelection}
23
+ onSelect={(nextValue) => {
24
+ value = nextValue
25
+ handle.update()
26
+ }}
27
+ onHighlight={(nextActiveValue) => {
28
+ activeValue = nextActiveValue
29
+ handle.update()
30
+ }}
31
+ >
32
+ <div tabIndex={0} mix={[listbox.listStyle, listbox.list(), containerCss]}>
33
+ {options.map((option) => (
34
+ <div key={option.value} mix={[listbox.optionStyle, listbox.option(option)]}>
35
+ <Glyph mix={listbox.glyphStyle} name="check" />
36
+ <span mix={listbox.labelStyle}>{option.label}</span>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ </listbox.Context>
41
+ <div mix={controlsCss}>
42
+ <label mix={checkboxLabelCss}>
43
+ <input
44
+ type="checkbox"
45
+ defaultChecked={flashSelection}
46
+ mix={on('change', (event) => {
47
+ flashSelection = event.currentTarget.checked
48
+ handle.update()
49
+ })}
50
+ />{' '}
51
+ Flash selection
52
+ </label>
53
+ <p mix={valueCss}>{`value=${value ?? 'null'}`}</p>
54
+ </div>
55
+ </div>
56
+ )
57
+ }
58
+ }
59
+
60
+ const options = [
61
+ { label: 'Apple', value: 'apple' },
62
+ { label: 'Banana', value: 'banana' },
63
+ { label: 'Cherry', value: 'cherry' },
64
+ { label: 'Date', value: 'date' },
65
+ { label: 'Elderberry', value: 'elderberry' },
66
+ { label: 'Fig', value: 'fig' },
67
+ { label: 'Grape', value: 'grape' },
68
+ ] as const
69
+
70
+ const containerCss = css({
71
+ borderColor: theme.colors.border.subtle,
72
+ padding: theme.space.xs,
73
+ borderRadius: theme.radius.lg,
74
+ borderStyle: 'solid',
75
+ })
76
+
77
+ const stackCss = css({
78
+ display: 'flex',
79
+ flexDirection: 'column',
80
+ gap: theme.space.md,
81
+ width: '100%',
82
+ })
83
+
84
+ const controlsCss = css({
85
+ display: 'flex',
86
+ flexDirection: 'column',
87
+ gap: theme.space.xs,
88
+ })
89
+
90
+ const checkboxLabelCss = css({
91
+ fontSize: theme.fontSize.sm,
92
+ lineHeight: theme.lineHeight.normal,
93
+ color: theme.colors.text.secondary,
94
+ })
95
+
96
+ const valueCss = css({
97
+ margin: 0,
98
+ fontFamily: theme.fontFamily.mono,
99
+ fontSize: theme.fontSize.xs,
100
+ color: theme.colors.text.secondary,
101
+ })