@remix-run/ui 0.1.1 → 0.1.2

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 (195) hide show
  1. package/README.md +24 -3
  2. package/dist/animation/animate-layout-mixin.js +66 -66
  3. package/dist/animation/animate-layout-mixin.js.map +1 -1
  4. package/dist/components/accordion/accordion.d.ts +3 -3
  5. package/dist/components/accordion/accordion.js +0 -2
  6. package/dist/components/accordion/accordion.js.map +1 -1
  7. package/dist/components/accordion/demos/card.demo.d.ts +6 -0
  8. package/dist/components/accordion/demos/card.demo.js +58 -0
  9. package/dist/components/accordion/demos/card.demo.js.map +1 -0
  10. package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
  11. package/dist/components/accordion/demos/multiple.demo.js +19 -0
  12. package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
  13. package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
  14. package/dist/components/accordion/demos/overview.demo.js +19 -0
  15. package/dist/components/accordion/demos/overview.demo.js.map +1 -0
  16. package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
  17. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
  18. package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
  19. package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
  20. package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
  21. package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
  22. package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
  23. package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
  24. package/dist/components/button/button.d.ts +82 -0
  25. package/dist/components/button/button.js +60 -2
  26. package/dist/components/button/button.js.map +1 -1
  27. package/dist/components/button/demos/aliases.demo.d.ts +6 -0
  28. package/dist/components/button/demos/aliases.demo.js +18 -0
  29. package/dist/components/button/demos/aliases.demo.js.map +1 -0
  30. package/dist/components/button/demos/basic.demo.d.ts +6 -0
  31. package/dist/components/button/demos/basic.demo.js +19 -0
  32. package/dist/components/button/demos/basic.demo.js.map +1 -0
  33. package/dist/components/button/demos/states.demo.d.ts +6 -0
  34. package/dist/components/button/demos/states.demo.js +29 -0
  35. package/dist/components/button/demos/states.demo.js.map +1 -0
  36. package/dist/components/combobox/combobox.d.ts +3 -3
  37. package/dist/components/combobox/combobox.js +0 -2
  38. package/dist/components/combobox/combobox.js.map +1 -1
  39. package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
  40. package/dist/components/combobox/demos/overview.demo.js +153 -0
  41. package/dist/components/combobox/demos/overview.demo.js.map +1 -0
  42. package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
  43. package/dist/components/listbox/demos/overview.demo.js +64 -0
  44. package/dist/components/listbox/demos/overview.demo.js.map +1 -0
  45. package/dist/components/listbox/listbox.d.ts +1 -6
  46. package/dist/components/listbox/listbox.js +21 -33
  47. package/dist/components/listbox/listbox.js.map +1 -1
  48. package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
  49. package/dist/components/menu/demos/bubbling.demo.js +15 -0
  50. package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
  51. package/dist/components/menu/demos/overview.demo.d.ts +6 -0
  52. package/dist/components/menu/demos/overview.demo.js +32 -0
  53. package/dist/components/menu/demos/overview.demo.js.map +1 -0
  54. package/dist/components/menu/menu.d.ts +5 -14
  55. package/dist/components/menu/menu.js +23 -38
  56. package/dist/components/menu/menu.js.map +1 -1
  57. package/dist/components/popover/demos/overview.demo.d.ts +6 -0
  58. package/dist/components/popover/demos/overview.demo.js +64 -0
  59. package/dist/components/popover/demos/overview.demo.js.map +1 -0
  60. package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
  61. package/dist/components/select/demos/deconstructed.demo.js +53 -0
  62. package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
  63. package/dist/components/select/demos/overview.demo.d.ts +5 -0
  64. package/dist/components/select/demos/overview.demo.js +27 -0
  65. package/dist/components/select/demos/overview.demo.js.map +1 -0
  66. package/dist/components/select/select.d.ts +3 -3
  67. package/dist/components/select/select.js +17 -26
  68. package/dist/components/select/select.js.map +1 -1
  69. package/dist/components/tabs/tabs.d.ts +3 -3
  70. package/dist/components/tabs/tabs.js +0 -2
  71. package/dist/components/tabs/tabs.js.map +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/runtime/component.d.ts +8 -5
  75. package/dist/runtime/component.js +84 -80
  76. package/dist/runtime/component.js.map +1 -1
  77. package/dist/runtime/core/attributes.d.ts +12 -0
  78. package/dist/runtime/core/attributes.js +85 -0
  79. package/dist/runtime/core/attributes.js.map +1 -0
  80. package/dist/runtime/core/children.d.ts +7 -0
  81. package/dist/runtime/core/children.js +30 -0
  82. package/dist/runtime/core/children.js.map +1 -0
  83. package/dist/runtime/core/props.d.ts +9 -0
  84. package/dist/runtime/core/props.js +169 -0
  85. package/dist/runtime/core/props.js.map +1 -0
  86. package/dist/runtime/core/vnode.d.ts +3 -0
  87. package/dist/runtime/core/vnode.js +39 -0
  88. package/dist/runtime/core/vnode.js.map +1 -0
  89. package/dist/runtime/create-element.js +2 -1
  90. package/dist/runtime/create-element.js.map +1 -1
  91. package/dist/runtime/diff-dom.js +47 -14
  92. package/dist/runtime/diff-dom.js.map +1 -1
  93. package/dist/runtime/diff-props.d.ts +1 -2
  94. package/dist/runtime/diff-props.js +2 -181
  95. package/dist/runtime/diff-props.js.map +1 -1
  96. package/dist/runtime/dom.d.ts +4 -0
  97. package/dist/runtime/frame.d.ts +23 -2
  98. package/dist/runtime/frame.js +41 -47
  99. package/dist/runtime/frame.js.map +1 -1
  100. package/dist/runtime/jsx.js +2 -28
  101. package/dist/runtime/jsx.js.map +1 -1
  102. package/dist/runtime/mixins/mixin.js +9 -4
  103. package/dist/runtime/mixins/mixin.js.map +1 -1
  104. package/dist/runtime/mixins/on-mixin.d.ts +15 -4
  105. package/dist/runtime/mixins/on-mixin.js +9 -2
  106. package/dist/runtime/mixins/on-mixin.js.map +1 -1
  107. package/dist/runtime/navigation.js +9 -1
  108. package/dist/runtime/navigation.js.map +1 -1
  109. package/dist/runtime/reconcile.js +433 -192
  110. package/dist/runtime/reconcile.js.map +1 -1
  111. package/dist/runtime/run.d.ts +17 -2
  112. package/dist/runtime/run.js.map +1 -1
  113. package/dist/runtime/stream-protocol.d.ts +8 -0
  114. package/dist/runtime/stream-protocol.js +20 -0
  115. package/dist/runtime/stream-protocol.js.map +1 -0
  116. package/dist/runtime/to-vnode.js +5 -9
  117. package/dist/runtime/to-vnode.js.map +1 -1
  118. package/dist/runtime/vnode.d.ts +5 -4
  119. package/dist/runtime/vnode.js +2 -3
  120. package/dist/runtime/vnode.js.map +1 -1
  121. package/dist/server/stream.js +76 -106
  122. package/dist/server/stream.js.map +1 -1
  123. package/dist/style/layers.d.ts +2 -0
  124. package/dist/style/layers.js +3 -0
  125. package/dist/style/layers.js.map +1 -0
  126. package/dist/style/stylesheet.d.ts +3 -1
  127. package/dist/style/stylesheet.js +69 -8
  128. package/dist/style/stylesheet.js.map +1 -1
  129. package/dist/theme/glyph-contract.d.ts +1 -1
  130. package/dist/theme/runtime.js +4 -1
  131. package/dist/theme/runtime.js.map +1 -1
  132. package/package.json +3 -3
  133. package/src/animation/README.md +314 -0
  134. package/src/animation/animate-layout-mixin.ts +79 -71
  135. package/src/components/accordion/README.md +166 -0
  136. package/src/components/accordion/accordion.tsx +8 -10
  137. package/src/components/accordion/demos/card.demo.tsx +113 -0
  138. package/src/components/accordion/demos/multiple.demo.tsx +56 -0
  139. package/src/components/accordion/demos/overview.demo.tsx +56 -0
  140. package/src/components/anchor/README.md +153 -0
  141. package/src/components/breadcrumbs/README.md +55 -0
  142. package/src/components/breadcrumbs/breadcrumbs.tsx +0 -2
  143. package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
  144. package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
  145. package/src/components/button/README.md +4 -4
  146. package/src/components/button/button.tsx +88 -2
  147. package/src/components/button/demos/aliases.demo.tsx +27 -0
  148. package/src/components/button/demos/basic.demo.tsx +30 -0
  149. package/src/components/button/demos/states.demo.tsx +52 -0
  150. package/src/components/combobox/combobox.tsx +8 -10
  151. package/src/components/combobox/demos/overview.demo.tsx +199 -0
  152. package/src/components/glyph/README.md +72 -0
  153. package/src/components/listbox/README.md +115 -0
  154. package/src/components/listbox/demos/overview.demo.tsx +101 -0
  155. package/src/components/listbox/listbox.ts +23 -32
  156. package/src/components/menu/README.md +96 -0
  157. package/src/components/menu/demos/bubbling.demo.tsx +34 -0
  158. package/src/components/menu/demos/overview.demo.tsx +76 -0
  159. package/src/components/menu/menu.tsx +35 -41
  160. package/src/components/popover/README.md +3 -3
  161. package/src/components/popover/demos/overview.demo.tsx +143 -0
  162. package/src/components/select/README.md +107 -0
  163. package/src/components/select/demos/deconstructed.demo.tsx +95 -0
  164. package/src/components/select/demos/overview.demo.tsx +51 -0
  165. package/src/components/select/select.tsx +28 -28
  166. package/src/components/tabs/tabs.tsx +10 -5
  167. package/src/index.ts +1 -1
  168. package/src/runtime/component.ts +94 -82
  169. package/src/runtime/core/attributes.ts +97 -0
  170. package/src/runtime/core/children.ts +42 -0
  171. package/src/runtime/core/props.ts +185 -0
  172. package/src/runtime/core/vnode.ts +48 -0
  173. package/src/runtime/create-element.ts +2 -1
  174. package/src/runtime/diff-dom.ts +59 -12
  175. package/src/runtime/diff-props.ts +2 -187
  176. package/src/runtime/dom.ts +4 -0
  177. package/src/runtime/frame.ts +71 -55
  178. package/src/runtime/jsx.ts +2 -29
  179. package/src/runtime/mixins/mixin.ts +9 -4
  180. package/src/runtime/mixins/on-mixin.ts +20 -12
  181. package/src/runtime/navigation.ts +10 -1
  182. package/src/runtime/reconcile.ts +540 -209
  183. package/src/runtime/run.ts +17 -1
  184. package/src/runtime/stream-protocol.ts +27 -0
  185. package/src/runtime/to-vnode.ts +5 -8
  186. package/src/runtime/vnode.ts +6 -9
  187. package/src/server/README.md +88 -0
  188. package/src/server/stream.ts +99 -116
  189. package/src/style/layers.ts +2 -0
  190. package/src/style/stylesheet.ts +75 -10
  191. package/src/test/README.md +107 -0
  192. package/src/test/utils.ts +3 -1
  193. package/src/theme/README.md +103 -0
  194. package/src/theme/runtime.ts +4 -1
  195. package/src/utils/scroll-lock/README.md +33 -0
@@ -1,13 +1,13 @@
1
1
  # button
2
2
 
3
- `button` is the shared button styling contract for `@remix-run/ui`. Use `Button` for ordinary action buttons, or compose flat `button.*Style` exports directly when a higher-level control needs button structure without a wrapper.
3
+ `button` is the shared button styling contract for `remix/ui`. Use `Button` for ordinary action buttons, or compose flat `button.*Style` exports directly when a higher-level control needs button structure without a wrapper.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  ```tsx
8
- import { Button } from '@remix-run/ui/button'
9
- import * as button from '@remix-run/ui/button'
10
- import { Glyph } from '@remix-run/ui/glyph'
8
+ import { Button } from 'remix/ui/button'
9
+ import * as button from 'remix/ui/button'
10
+ import { Glyph } from 'remix/ui/glyph'
11
11
 
12
12
  function Actions() {
13
13
  return (
@@ -1,5 +1,3 @@
1
- // @jsxRuntime classic
2
- // @jsx createElement
3
1
  import { attrs, createElement, createMixin, css } from '@remix-run/ui'
4
2
  import type { CSSMixinDescriptor, ElementProps, Handle, Props, RemixNode } from '@remix-run/ui'
5
3
 
@@ -93,12 +91,61 @@ const buttonDefaultsMixin = createMixin<Element, [], ElementProps>(
93
91
 
94
92
  const buttonDefaultsCss = buttonDefaultsMixin()
95
93
 
94
+ /**
95
+ * Base button styling with the default `type="button"` behavior for `<button>`
96
+ * hosts. Compose with a tone style (e.g. {@link primaryStyle}) when applying
97
+ * button styling without using the {@link Button} component.
98
+ *
99
+ * @category mixin
100
+ */
96
101
  export const baseStyle = [buttonDefaultsCss, buttonBaseStyleCss] as const
102
+
103
+ /**
104
+ * Icon slot sizing and `aria-hidden` defaults for decorative icons rendered
105
+ * inside a button.
106
+ *
107
+ * @category mixin
108
+ */
97
109
  export const iconStyle = [buttonIconAttrsCss, buttonIconCss] as const
110
+
111
+ /**
112
+ * Inline label slot with the standard button label spacing.
113
+ *
114
+ * @category mixin
115
+ */
98
116
  export const labelStyle = buttonLabelCss
117
+
118
+ /**
119
+ * Primary visual treatment for buttons. Combine with {@link baseStyle} when
120
+ * styling a non-`Button` host element.
121
+ *
122
+ * @category mixin
123
+ */
99
124
  export const primaryStyle = createButtonCss(theme.colors.action.primary)
125
+
126
+ /**
127
+ * Secondary visual treatment for buttons. Combine with {@link baseStyle} when
128
+ * styling a non-`Button` host element.
129
+ *
130
+ * @category mixin
131
+ */
100
132
  export const secondaryStyle = createButtonCss(theme.colors.action.secondary)
133
+
134
+ /**
135
+ * Ghost visual treatment for buttons — transparent background with a hover
136
+ * surface. Combine with {@link baseStyle} when styling a non-`Button` host
137
+ * element.
138
+ *
139
+ * @category mixin
140
+ */
101
141
  export const ghostStyle = ghostButtonToneCss
142
+
143
+ /**
144
+ * Danger visual treatment for destructive actions. Combine with
145
+ * {@link baseStyle} when styling a non-`Button` host element.
146
+ *
147
+ * @category mixin
148
+ */
102
149
  export const dangerStyle = createButtonCss(theme.colors.action.danger)
103
150
 
104
151
  const toneStyleByTone = {
@@ -108,15 +155,54 @@ const toneStyleByTone = {
108
155
  danger: dangerStyle,
109
156
  } as const
110
157
 
158
+ /**
159
+ * Visual treatment supported by {@link Button} — `'primary'`, `'secondary'`,
160
+ * `'ghost'`, or `'danger'`.
161
+ */
111
162
  export type ButtonTone = keyof typeof toneStyleByTone
112
163
 
164
+ /**
165
+ * Props accepted by the {@link Button} component.
166
+ *
167
+ * Extends the native `<button>` element props with optional icon slots and a
168
+ * tone variant
169
+ */
113
170
  export type ButtonProps = Omit<Props<'button'>, 'children'> & {
171
+ /**
172
+ * Content rendered inside the button's label slot.
173
+ */
114
174
  readonly children?: RemixNode
175
+ /**
176
+ * Decorative icon rendered after the label, inside the icon slot.
177
+ */
115
178
  readonly endIcon?: RemixNode
179
+ /**
180
+ * Decorative icon rendered before the label, inside the icon slot.
181
+ */
116
182
  readonly startIcon?: RemixNode
183
+ /**
184
+ * Visual treatment to apply to the button (default `'secondary'`).
185
+ */
117
186
  readonly tone?: ButtonTone
118
187
  }
119
188
 
189
+ /**
190
+ * Renders a `<button>` with `baseStyle` and the resolved tone style, along with
191
+ * optional start and end icons.
192
+ *
193
+ * @param handle Component handle providing the runtime API and the resolved {@link ButtonProps}.
194
+ * @returns A render function for the button element.
195
+ *
196
+ * @example
197
+ * ```tsx
198
+ * import { Button } from '@remix-run/ui/button'
199
+ * import { Glyph } from '@remix-run/ui/glyph'
200
+ *
201
+ * <Button startIcon={<Glyph name="add" />} tone="primary">
202
+ * Create project
203
+ * </Button>
204
+ * ```
205
+ */
120
206
  export function Button(handle: Handle<ButtonProps>) {
121
207
  return () => {
122
208
  let { children, endIcon, mix, startIcon, tone = 'secondary', ...buttonProps } = handle.props
@@ -0,0 +1,27 @@
1
+ import { Button } from '@remix-run/ui/button'
2
+ import { css } from '@remix-run/ui'
3
+ import { theme } from '@remix-run/ui/theme'
4
+
5
+ /**
6
+ * @name Button Component
7
+ * @description The Button component wraps the low-level style primitives and accepts a tone prop for quick theming.
8
+ * @order 2
9
+ */
10
+ export default function Example() {
11
+ return () => (
12
+ <div mix={buttonRowCss}>
13
+ <Button tone="primary" type="submit">
14
+ Save
15
+ </Button>
16
+ <Button tone="secondary">Secondary</Button>
17
+ <Button tone="ghost">Ghost</Button>
18
+ <Button tone="danger">Delete</Button>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ const buttonRowCss = css({
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ gap: theme.space.sm,
27
+ })
@@ -0,0 +1,30 @@
1
+ import { css } from '@remix-run/ui'
2
+ import * as button from '@remix-run/ui/button'
3
+ import { Glyph } from '@remix-run/ui/glyph'
4
+ import { theme } from '@remix-run/ui/theme'
5
+
6
+ /**
7
+ * @name Basic Button
8
+ * @description The default button contract supports both ordinary actions and link-shaped navigation.
9
+ * @order 1
10
+ */
11
+ export default function Example() {
12
+ return () => (
13
+ <div mix={buttonRowCss}>
14
+ <button type="submit" mix={[button.baseStyle, button.primaryStyle]}>
15
+ <Glyph mix={button.iconStyle} name="add" />
16
+ <span mix={button.labelStyle}>Publish</span>
17
+ </button>
18
+ <a href="/api/remix/ui/button/overview/" mix={[button.baseStyle, button.secondaryStyle]}>
19
+ <span mix={button.labelStyle}>View button docs</span>
20
+ <Glyph mix={button.iconStyle} name="chevronRight" />
21
+ </a>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ const buttonRowCss = css({
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ gap: theme.space.sm,
30
+ })
@@ -0,0 +1,52 @@
1
+ import * as button from '@remix-run/ui/button'
2
+ import { css } from '@remix-run/ui'
3
+ import { Glyph } from '@remix-run/ui/glyph'
4
+ import { theme } from '@remix-run/ui/theme'
5
+
6
+ /**
7
+ * @name Button States
8
+ * @description Buttons support normal, disabled, and busy (loading) states using aria attributes.
9
+ * @order 3
10
+ */
11
+ export default function Example() {
12
+ return () => (
13
+ <div mix={buttonRowCss}>
14
+ <button mix={[button.baseStyle, button.primaryStyle]}>
15
+ <Glyph mix={button.iconStyle} name="add" />
16
+ <span mix={button.labelStyle}>New issue</span>
17
+ </button>
18
+
19
+ <button mix={[button.baseStyle, button.ghostStyle]}>
20
+ <span mix={button.labelStyle}>Open</span>
21
+ <Glyph mix={button.iconStyle} name="chevronRight" />
22
+ </button>
23
+
24
+ <button disabled mix={[button.baseStyle, button.secondaryStyle]}>
25
+ <span mix={button.labelStyle}>Disabled</span>
26
+ </button>
27
+
28
+ <button aria-busy="true" mix={[button.baseStyle, button.secondaryStyle]}>
29
+ <Glyph mix={[button.iconStyle, spinnerGlyphCss, spinCss]} name="spinner" />
30
+ <span mix={button.labelStyle}>Saving</span>
31
+ </button>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ const buttonRowCss = css({
37
+ display: 'flex',
38
+ alignItems: 'center',
39
+ gap: theme.space.sm,
40
+ })
41
+
42
+ const spinnerGlyphCss = css({
43
+ opacity: 0.72,
44
+ })
45
+
46
+ const spinCss = css({
47
+ '@keyframes demo-button-spin': {
48
+ from: { transform: 'rotate(0deg)' },
49
+ to: { transform: 'rotate(360deg)' },
50
+ },
51
+ animation: 'demo-button-spin 1s linear infinite',
52
+ })
@@ -1,5 +1,3 @@
1
- // @jsxRuntime classic
2
- // @jsx createElement
3
1
  import {
4
2
  attrs,
5
3
  css,
@@ -8,6 +6,7 @@ import {
8
6
  on,
9
7
  ref,
10
8
  type CSSMixinDescriptor,
9
+ type Dispatched,
11
10
  type ElementProps,
12
11
  type Handle,
13
12
  type MixinHandle,
@@ -26,8 +25,8 @@ import { wait } from '../../utils/wait.ts'
26
25
  const COMBOBOX_CHANGE_EVENT = 'rmx:combobox-change' as const
27
26
  const INPUT_COMMIT_DELAY_MS = 50
28
27
 
29
- type ComboboxChangeHandler = (
30
- event: ComboboxChangeEvent,
28
+ type ComboboxChangeHandler<target extends HTMLElement> = (
29
+ event: Dispatched<ComboboxChangeEvent, target>,
31
30
  signal: AbortSignal,
32
31
  ) => void | Promise<void>
33
32
 
@@ -966,12 +965,11 @@ const combobox = {
966
965
  popover: popoverMixin,
967
966
  } as const
968
967
 
969
- export function onComboboxChange(handler: ComboboxChangeHandler, captureBoolean?: boolean) {
970
- return on<HTMLElement, typeof COMBOBOX_CHANGE_EVENT>(
971
- COMBOBOX_CHANGE_EVENT,
972
- handler,
973
- captureBoolean,
974
- )
968
+ export function onComboboxChange<target extends HTMLElement>(
969
+ handler: ComboboxChangeHandler<target>,
970
+ captureBoolean?: boolean,
971
+ ) {
972
+ return on(COMBOBOX_CHANGE_EVENT, handler, captureBoolean)
975
973
  }
976
974
 
977
975
  export function Combobox(handle: Handle<ComboboxProps>) {
@@ -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.
@@ -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.