@remix-run/ui 0.0.0 → 0.1.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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,124 @@
1
+ // @jsxRuntime classic
2
+ // @jsx createElement
3
+ import { css, createElement } from '@remix-run/ui'
4
+ import type { Handle, Props, RemixNode } from '@remix-run/ui'
5
+
6
+ import { Glyph } from '../glyph/glyph.tsx'
7
+ import { theme } from '../../theme/theme.ts'
8
+
9
+ export type BreadcrumbItem = {
10
+ current?: boolean
11
+ href?: string
12
+ label: RemixNode
13
+ }
14
+
15
+ export type BreadcrumbsProps = Omit<Props<'nav'>, 'children'> & {
16
+ items: BreadcrumbItem[]
17
+ separator?: RemixNode
18
+ }
19
+
20
+ const rootCss = css({
21
+ minWidth: 0,
22
+ })
23
+
24
+ const listCss = css({
25
+ display: 'flex',
26
+ flexWrap: 'wrap',
27
+ alignItems: 'center',
28
+ gap: `${theme.space.xs} ${theme.space.sm}`,
29
+ minWidth: 0,
30
+ margin: 0,
31
+ padding: 0,
32
+ listStyle: 'none',
33
+ })
34
+
35
+ const itemCss = css({
36
+ display: 'inline-flex',
37
+ alignItems: 'center',
38
+ minWidth: 0,
39
+ })
40
+
41
+ const separatorCss = css({
42
+ display: 'inline-flex',
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ flexShrink: 0,
46
+ width: theme.fontSize.sm,
47
+ height: theme.fontSize.sm,
48
+ color: theme.colors.text.muted,
49
+ })
50
+
51
+ const linkCss = css({
52
+ color: theme.colors.text.secondary,
53
+ fontSize: theme.fontSize.sm,
54
+ lineHeight: theme.lineHeight.normal,
55
+ textDecoration: 'none',
56
+ whiteSpace: 'nowrap',
57
+ '&:hover': {
58
+ color: theme.colors.text.primary,
59
+ },
60
+ })
61
+
62
+ const currentCss = css({
63
+ color: theme.colors.text.primary,
64
+ fontSize: theme.fontSize.sm,
65
+ lineHeight: theme.lineHeight.normal,
66
+ fontWeight: theme.fontWeight.medium,
67
+ whiteSpace: 'nowrap',
68
+ })
69
+
70
+ const textCss = css({
71
+ color: theme.colors.text.secondary,
72
+ fontSize: theme.fontSize.sm,
73
+ lineHeight: theme.lineHeight.normal,
74
+ whiteSpace: 'nowrap',
75
+ })
76
+
77
+ export function Breadcrumbs(handle: Handle<BreadcrumbsProps>) {
78
+ return () => {
79
+ let { 'aria-label': ariaLabel, items, separator, mix, ...navProps } = handle.props
80
+ let currentIndex = items.findIndex((item) => item.current)
81
+ if (currentIndex === -1) {
82
+ currentIndex = Math.max(0, items.length - 1)
83
+ }
84
+
85
+ let separatorContent = separator ?? <Glyph name="chevronRight" />
86
+
87
+ return (
88
+ <nav aria-label={ariaLabel ?? 'Breadcrumb'} {...navProps} mix={[rootCss, mix]}>
89
+ <ol mix={listCss}>
90
+ {items.flatMap((item, index) => {
91
+ let isCurrent = index === currentIndex
92
+ let content = isCurrent ? (
93
+ <span aria-current="page" mix={currentCss}>
94
+ {item.label}
95
+ </span>
96
+ ) : item.href ? (
97
+ <a href={item.href} mix={linkCss}>
98
+ {item.label}
99
+ </a>
100
+ ) : (
101
+ <span mix={textCss}>{item.label}</span>
102
+ )
103
+
104
+ let nodes: RemixNode[] = [
105
+ <li key={`item-${index}`} mix={itemCss}>
106
+ {content}
107
+ </li>,
108
+ ]
109
+
110
+ if (index < items.length - 1) {
111
+ nodes.push(
112
+ <li key={`separator-${index}`} aria-hidden="true" mix={separatorCss}>
113
+ {separatorContent}
114
+ </li>,
115
+ )
116
+ }
117
+
118
+ return nodes
119
+ })}
120
+ </ol>
121
+ </nav>
122
+ )
123
+ }
124
+ }
@@ -0,0 +1,44 @@
1
+ # button
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.
4
+
5
+ ## Usage
6
+
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'
11
+
12
+ function Actions() {
13
+ return (
14
+ <div>
15
+ <Button startIcon={<Glyph name="add" />} tone="primary">
16
+ Create project
17
+ </Button>
18
+
19
+ <a href="/projects" mix={[button.baseStyle, button.secondaryStyle]}>
20
+ <span mix={button.labelStyle}>View projects</span>
21
+ <Glyph mix={button.iconStyle} name="chevronRight" />
22
+ </a>
23
+ </div>
24
+ )
25
+ }
26
+ ```
27
+
28
+ ## `button.*`
29
+
30
+ - `Button`: thin button wrapper for the common `base + tone + label/icon slots` case. Pass `tone`, `startIcon`, and `endIcon` when the default authored structure is enough.
31
+ - `button.baseStyle`: base host styling plus the default `type="button"` behavior for button elements.
32
+ - `button.primaryStyle`, `button.secondaryStyle`, `button.ghostStyle`, and `button.dangerStyle`: visual button treatments.
33
+ - `button.labelStyle`: inline label slot with the standard button spacing.
34
+ - `button.iconStyle`: icon slot sizing and `aria-hidden` defaults for decorative icons.
35
+
36
+ ## Behavior Notes
37
+
38
+ - `button.baseStyle` only adds `type="button"` when the host element is a `<button>` and no explicit `type` was provided.
39
+ - `Button` renders `children` inside `button.labelStyle` and renders `startIcon` and `endIcon` inside `button.iconStyle`.
40
+ - Use an explicit accessible name when you render an icon-only button.
41
+
42
+ ## When To Use Something Else
43
+
44
+ Use `button.*Style` exports directly when a control needs button structure plus extra behavior or layout, like `select`, `menu`, or `tabs`. Those controls own their own interaction mixins and should not hide that behavior behind `Button`.
@@ -0,0 +1,188 @@
1
+ // @jsxRuntime classic
2
+ // @jsx createElement
3
+ import { attrs, createElement, createMixin, css } from '@remix-run/ui'
4
+ import type { CSSMixinDescriptor, ElementProps, Handle, Props, RemixNode } from '@remix-run/ui'
5
+
6
+ import { theme } from '../../theme/theme.ts'
7
+
8
+ const ghostButtonToneCss: CSSMixinDescriptor = css({
9
+ backgroundColor: 'transparent',
10
+ backgroundImage: 'none',
11
+ color: theme.colors.text.secondary,
12
+ border: '0.5px solid transparent',
13
+ boxShadow: 'none',
14
+ '&:hover': {
15
+ backgroundColor: theme.surface.lvl3,
16
+ color: theme.colors.text.primary,
17
+ },
18
+ '&:active': {
19
+ backgroundColor: `color-mix(in oklab, ${theme.surface.lvl3} 94%, black)`,
20
+ color: `color-mix(in oklab, ${theme.colors.text.primary} 94%, black)`,
21
+ },
22
+ '&:focus-visible': {
23
+ outline: `2px solid ${theme.colors.focus.ring}`,
24
+ outlineOffset: '2px',
25
+ backgroundColor: theme.surface.lvl3,
26
+ color: theme.colors.text.primary,
27
+ },
28
+ '&:disabled': {
29
+ opacity: 0.6,
30
+ },
31
+ })
32
+
33
+ const buttonBaseStyleCss: CSSMixinDescriptor = css({
34
+ '--rmx-button-label-padding-inline': theme.space.xs,
35
+ all: 'unset',
36
+ boxSizing: 'border-box',
37
+ cursor: 'revert',
38
+ position: 'relative',
39
+ isolation: 'isolate',
40
+ display: 'inline-flex',
41
+ alignItems: 'center',
42
+ justifyContent: 'center',
43
+ overflow: 'hidden',
44
+ borderRadius: theme.radius.full,
45
+ fontFamily: theme.fontFamily.sans,
46
+ minHeight: theme.control.height.sm,
47
+ paddingInline: theme.space.md,
48
+ fontSize: theme.fontSize.xs,
49
+ lineHeight: '1',
50
+ fontWeight: theme.fontWeight.medium,
51
+ whiteSpace: 'nowrap',
52
+ textDecoration: 'none',
53
+ userSelect: 'none',
54
+ WebkitUserSelect: 'none',
55
+ verticalAlign: 'top',
56
+ })
57
+
58
+ const buttonLabelCss: CSSMixinDescriptor = css({
59
+ display: 'inline-flex',
60
+ alignItems: 'center',
61
+ minWidth: 0,
62
+ paddingInline: 'var(--rmx-button-label-padding-inline)',
63
+ })
64
+
65
+ const buttonIconCss: CSSMixinDescriptor = css({
66
+ display: 'inline-flex',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ width: '1em',
70
+ height: '1em',
71
+ flexShrink: 0,
72
+ '& > svg': {
73
+ display: 'block',
74
+ width: '100%',
75
+ height: '100%',
76
+ },
77
+ })
78
+
79
+ const buttonIconAttrsCss = attrs({ 'aria-hidden': true })
80
+
81
+ const buttonDefaultsMixin = createMixin<Element, [], ElementProps>(
82
+ (handle, hostType) => (props) => {
83
+ if (hostType !== 'button' || props.type !== undefined) {
84
+ return handle.element
85
+ }
86
+
87
+ return createElement(handle.element as unknown as string, {
88
+ ...props,
89
+ type: 'button',
90
+ })
91
+ },
92
+ )
93
+
94
+ const buttonDefaultsCss = buttonDefaultsMixin()
95
+
96
+ export const baseStyle = [buttonDefaultsCss, buttonBaseStyleCss] as const
97
+ export const iconStyle = [buttonIconAttrsCss, buttonIconCss] as const
98
+ export const labelStyle = buttonLabelCss
99
+ export const primaryStyle = createButtonCss(theme.colors.action.primary)
100
+ export const secondaryStyle = createButtonCss(theme.colors.action.secondary)
101
+ export const ghostStyle = ghostButtonToneCss
102
+ export const dangerStyle = createButtonCss(theme.colors.action.danger)
103
+
104
+ const toneStyleByTone = {
105
+ primary: primaryStyle,
106
+ secondary: secondaryStyle,
107
+ ghost: ghostStyle,
108
+ danger: dangerStyle,
109
+ } as const
110
+
111
+ export type ButtonTone = keyof typeof toneStyleByTone
112
+
113
+ export type ButtonProps = Omit<Props<'button'>, 'children'> & {
114
+ readonly children?: RemixNode
115
+ readonly endIcon?: RemixNode
116
+ readonly startIcon?: RemixNode
117
+ readonly tone?: ButtonTone
118
+ }
119
+
120
+ export function Button(handle: Handle<ButtonProps>) {
121
+ return () => {
122
+ let { children, endIcon, mix, startIcon, tone = 'secondary', ...buttonProps } = handle.props
123
+
124
+ return (
125
+ <button {...buttonProps} mix={[baseStyle, toneStyleByTone[tone], mix]}>
126
+ {startIcon ? <span mix={iconStyle}>{startIcon}</span> : null}
127
+ {children !== undefined ? <span mix={labelStyle}>{children}</span> : null}
128
+ {endIcon ? <span mix={iconStyle}>{endIcon}</span> : null}
129
+ </button>
130
+ )
131
+ }
132
+ }
133
+
134
+ function createButtonCss(buttonTheme: {
135
+ background: string
136
+ backgroundHover: string
137
+ backgroundActive: string
138
+ foreground: string
139
+ border: string
140
+ }): CSSMixinDescriptor {
141
+ let borderColor = createButtonBorderColor(buttonTheme.border)
142
+ let hoverBorderColor = createButtonBorderColor(buttonTheme.backgroundHover)
143
+ let activeBorderColor = createButtonBorderColor(buttonTheme.backgroundActive)
144
+
145
+ return css({
146
+ backgroundColor: buttonTheme.background,
147
+ backgroundImage: createButtonBackgroundImage(buttonTheme.background),
148
+ color: buttonTheme.foreground,
149
+ border: `0.5px solid ${borderColor}`,
150
+ boxShadow: `${createButtonHighlight(buttonTheme.background)}, ${theme.shadow.xs}, ${theme.shadow.sm}`,
151
+ '&:hover': {
152
+ backgroundColor: buttonTheme.backgroundHover,
153
+ backgroundImage: createButtonBackgroundImage(buttonTheme.backgroundHover),
154
+ borderColor: hoverBorderColor,
155
+ },
156
+ '&:active': {
157
+ backgroundColor: buttonTheme.backgroundActive,
158
+ backgroundImage: createButtonBackgroundImage(buttonTheme.backgroundActive),
159
+ borderColor: activeBorderColor,
160
+ boxShadow: `${createButtonHighlight(buttonTheme.backgroundActive, 0.12)}, ${theme.shadow.xs}`,
161
+ },
162
+ '&:focus-visible': {
163
+ outline: `2px solid ${theme.colors.focus.ring}`,
164
+ outlineOffset: '2px',
165
+ },
166
+ '&:disabled': {
167
+ opacity: 0.6,
168
+ cursor: 'not-allowed',
169
+ },
170
+ })
171
+ }
172
+
173
+ function createButtonBackgroundImage(background: string) {
174
+ return `linear-gradient(to bottom, ${createButtonHighlightColor(background, 0.18)} 0%, ${background} 100%)`
175
+ }
176
+
177
+ function createButtonBorderColor(color: string) {
178
+ return `color-mix(in oklab, ${color} 74%, black)`
179
+ }
180
+
181
+ function createButtonHighlight(background: string, amount = 0.16) {
182
+ return `inset 0 1px 0 ${createButtonHighlightColor(background, amount)}`
183
+ }
184
+
185
+ function createButtonHighlightColor(background: string, amount: number) {
186
+ let alpha = Math.round(amount * 100)
187
+ return `color-mix(in oklab, white ${alpha}%, ${background})`
188
+ }
@@ -0,0 +1,145 @@
1
+ # Combobox
2
+
3
+ `Combobox` is the input-first popup value picker for `remix/ui`.
4
+
5
+ Use it when the user should type draft text, filter a popup list, and still commit one stable form value. If you just need a button-triggered picker, use `Select` instead.
6
+
7
+ ## Usage
8
+
9
+ ```tsx
10
+ import { css, type Handle } from 'remix/ui'
11
+ import { Combobox, ComboboxOption, onComboboxChange } from 'remix/ui'
12
+
13
+ let airports = [
14
+ {
15
+ label: 'Los Angeles International',
16
+ searchValue: ['lax', 'los angeles', 'los angeles international'],
17
+ value: 'LAX',
18
+ },
19
+ {
20
+ label: 'John F. Kennedy International',
21
+ searchValue: ['jfk', 'new york', 'john f. kennedy international'],
22
+ value: 'JFK',
23
+ },
24
+ ] as const
25
+
26
+ export default function AirportField(handle: Handle) {
27
+ let value: string | null = null
28
+
29
+ return () => (
30
+ <div mix={root}>
31
+ <Combobox
32
+ inputId="airport"
33
+ mix={onComboboxChange((event) => {
34
+ value = event.value
35
+ void handle.update()
36
+ })}
37
+ name="airport"
38
+ placeholder="Search airports or codes"
39
+ >
40
+ {airports.map((airport) => (
41
+ <ComboboxOption
42
+ key={airport.value}
43
+ label={airport.label}
44
+ searchValue={airport.searchValue}
45
+ value={airport.value}
46
+ />
47
+ ))}
48
+ </Combobox>
49
+
50
+ <p>{`value=${value ?? 'null'}`}</p>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ let root = css({
56
+ display: 'grid',
57
+ gap: '8px',
58
+ })
59
+ ```
60
+
61
+ ## Public API
62
+
63
+ ### `Combobox`
64
+
65
+ The convenience component.
66
+
67
+ - Renders the text input, popover surface, listbox root, and hidden form input.
68
+ - Dispatches a bubbled custom event that `onComboboxChange(...)` listens for when the committed value changes.
69
+ - Accepts `defaultValue`, `disabled`, `inputId`, `name`, and `placeholder`.
70
+
71
+ ### `ComboboxOption`
72
+
73
+ The default option row for `Combobox`.
74
+
75
+ - Uses the shared listbox option visuals.
76
+ - Accepts `label`, `value`, optional `searchValue`, and optional `disabled`.
77
+ - `searchValue` can be a string or string array for aliases like airport codes, abbreviations, or alternate labels.
78
+
79
+ ### `onComboboxChange(...)`
80
+
81
+ The listener mixin for bubbled committed-value changes.
82
+
83
+ The event object includes:
84
+
85
+ - `event.value`: the committed value or `null`
86
+ - `event.label`: the committed option label or `null`
87
+ - `event.optionId`: the generated option id or `null`
88
+
89
+ ### `combobox.Context`
90
+
91
+ The lower-level coordinator for custom combobox composition.
92
+
93
+ It wraps the shared `popover` and `listbox` contexts and owns the draft text, committed value, popup state, and selection timing.
94
+
95
+ ### `combobox.input()`
96
+
97
+ Turns the host input into the combobox input.
98
+
99
+ - Keeps focus on the input during list navigation and pointer selection.
100
+ - Wires `role="combobox"`, `aria-expanded`, `aria-controls`, and `aria-activedescendant`.
101
+ - Opens from typing, click, and arrow-key navigation.
102
+
103
+ ### `combobox.popover()`
104
+
105
+ Turns the host into the combobox popover surface.
106
+
107
+ - Uses the shared popover primitive.
108
+ - Keeps anchor clicks inside the session so the input stays interactive while open.
109
+ - Applies the combobox open/close reason contract used by `combobox.popoverStyle`.
110
+
111
+ ### `combobox.list()`
112
+
113
+ Turns the host into the popup listbox root and applies the generated list id.
114
+
115
+ ### `combobox.option(options)`
116
+
117
+ Registers one option with the combobox and listbox layers.
118
+
119
+ - Accepts `label`, `value`, optional `searchValue`, and optional `disabled`.
120
+ - Hides non-matching options from the current draft filter.
121
+ - Prevents pointer selection from blurring the input before the click commits.
122
+
123
+ ### `combobox.hiddenInput()`
124
+
125
+ Mirrors the committed value into a hidden input for forms.
126
+
127
+ Apply it to an `<input type="hidden" />` inside the same `combobox.Context`.
128
+
129
+ ## Behavior Notes
130
+
131
+ - Typing opens the popup in hint mode when there are matches.
132
+ - If typing leaves no matches, the popup closes immediately without the navigation fade-out.
133
+ - Selecting from the list flashes the option, then closes the popup and finally commits the visible input label.
134
+ - Typing clears the committed value immediately; the hidden form value becomes empty until the user commits again.
135
+ - Blur commits an exact `label` or `searchValue` match. A non-matching blur clears the draft text and committed value.
136
+ - `Escape` keeps exact-match draft text but clears non-matching draft text and selection.
137
+ - Disabled options can stay visible in filtered results, but they are skipped by keyboard navigation and selection.
138
+
139
+ ## When To Use Something Else
140
+
141
+ Use `Select` when you want the ordinary button-triggered single-select control.
142
+
143
+ Use `listbox` when you need listbox semantics without an editable text input.
144
+
145
+ Use `popover` directly for custom floating panels that are not value-picking controls.