@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.
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/animation/animate-layout-mixin.d.ts +12 -0
- package/dist/animation/animate-layout-mixin.js +214 -0
- package/dist/animation/animate-layout-mixin.js.map +1 -0
- package/dist/animation/animate-mixins.d.ts +29 -0
- package/dist/animation/animate-mixins.js +235 -0
- package/dist/animation/animate-mixins.js.map +1 -0
- package/dist/animation/index.d.ts +6 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/spring.d.ts +75 -0
- package/dist/animation/spring.js +183 -0
- package/dist/animation/spring.js.map +1 -0
- package/dist/animation/tween.d.ts +70 -0
- package/dist/animation/tween.js +93 -0
- package/dist/animation/tween.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +92 -0
- package/dist/components/accordion/accordion.js +337 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/anchor/anchor.d.ts +13 -0
- package/dist/components/anchor/anchor.js +558 -0
- package/dist/components/anchor/anchor.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/button/button.d.ts +23 -0
- package/dist/components/button/button.js +147 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +101 -0
- package/dist/components/combobox/combobox.js +708 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/glyph/glyph.d.ts +14 -0
- package/dist/components/glyph/glyph.js +65 -0
- package/dist/components/glyph/glyph.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +67 -0
- package/dist/components/listbox/listbox.js +340 -0
- package/dist/components/listbox/listbox.js.map +1 -0
- package/dist/components/menu/hover-aim.d.ts +5 -0
- package/dist/components/menu/hover-aim.js +308 -0
- package/dist/components/menu/hover-aim.js.map +1 -0
- package/dist/components/menu/menu.d.ts +164 -0
- package/dist/components/menu/menu.js +1106 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/popover/popover.d.ts +35 -0
- package/dist/components/popover/popover.js +138 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.js +397 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/separator/separator.d.ts +2 -0
- package/dist/components/separator/separator.js +15 -0
- package/dist/components/separator/separator.js.map +1 -0
- package/dist/components/tabs/tabs.d.ts +78 -0
- package/dist/components/tabs/tabs.js +341 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/keydown/keydown.d.ts +1 -0
- package/dist/interactions/keydown/keydown.js +8 -0
- package/dist/interactions/keydown/keydown.js.map +1 -0
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
- package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
- package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
- package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
- package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +3 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/runtime/client-entries.d.ts +91 -0
- package/dist/runtime/client-entries.js +40 -0
- package/dist/runtime/client-entries.js.map +1 -0
- package/dist/runtime/component.d.ts +223 -0
- package/dist/runtime/component.js +152 -0
- package/dist/runtime/component.js.map +1 -0
- package/dist/runtime/create-element.d.ts +10 -0
- package/dist/runtime/create-element.js +29 -0
- package/dist/runtime/create-element.js.map +1 -0
- package/dist/runtime/diff-dom.d.ts +2 -0
- package/dist/runtime/diff-dom.js +364 -0
- package/dist/runtime/diff-dom.js.map +1 -0
- package/dist/runtime/diff-props.d.ts +9 -0
- package/dist/runtime/diff-props.js +195 -0
- package/dist/runtime/diff-props.js.map +1 -0
- package/dist/runtime/document-state.d.ts +11 -0
- package/dist/runtime/document-state.js +106 -0
- package/dist/runtime/document-state.js.map +1 -0
- package/dist/runtime/dom.d.ts +3189 -0
- package/dist/runtime/dom.js +2 -0
- package/dist/runtime/dom.js.map +1 -0
- package/dist/runtime/error-event.d.ts +20 -0
- package/dist/runtime/error-event.js +19 -0
- package/dist/runtime/error-event.js.map +1 -0
- package/dist/runtime/event-listeners.d.ts +50 -0
- package/dist/runtime/event-listeners.js +31 -0
- package/dist/runtime/event-listeners.js.map +1 -0
- package/dist/runtime/frame.d.ts +119 -0
- package/dist/runtime/frame.js +915 -0
- package/dist/runtime/frame.js.map +1 -0
- package/dist/runtime/invariant.d.ts +23 -0
- package/dist/runtime/invariant.js +33 -0
- package/dist/runtime/invariant.js.map +1 -0
- package/dist/runtime/jsx.d.ts +320 -0
- package/dist/runtime/jsx.js +31 -0
- package/dist/runtime/jsx.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
- package/dist/runtime/mixins/attrs-mixin.js +23 -0
- package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.d.ts +17 -0
- package/dist/runtime/mixins/link-mixin.js +101 -0
- package/dist/runtime/mixins/link-mixin.js.map +1 -0
- package/dist/runtime/mixins/mixin.d.ts +145 -0
- package/dist/runtime/mixins/mixin.js +570 -0
- package/dist/runtime/mixins/mixin.js.map +1 -0
- package/dist/runtime/mixins/on-mixin.d.ts +16 -0
- package/dist/runtime/mixins/on-mixin.js +49 -0
- package/dist/runtime/mixins/on-mixin.js.map +1 -0
- package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
- package/dist/runtime/mixins/ref-mixin.js +21 -0
- package/dist/runtime/mixins/ref-mixin.js.map +1 -0
- package/dist/runtime/navigation.d.ts +22 -0
- package/dist/runtime/navigation.js +102 -0
- package/dist/runtime/navigation.js.map +1 -0
- package/dist/runtime/reconcile.d.ts +11 -0
- package/dist/runtime/reconcile.js +1240 -0
- package/dist/runtime/reconcile.js.map +1 -0
- package/dist/runtime/render.d.ts +54 -0
- package/dist/runtime/render.js +50 -0
- package/dist/runtime/render.js.map +1 -0
- package/dist/runtime/run.d.ts +45 -0
- package/dist/runtime/run.js +68 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +30 -0
- package/dist/runtime/scheduler.js +185 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/svg-attributes.d.ts +5 -0
- package/dist/runtime/svg-attributes.js +124 -0
- package/dist/runtime/svg-attributes.js.map +1 -0
- package/dist/runtime/to-vnode.d.ts +3 -0
- package/dist/runtime/to-vnode.js +43 -0
- package/dist/runtime/to-vnode.js.map +1 -0
- package/dist/runtime/typed-event-target.d.ts +50 -0
- package/dist/runtime/typed-event-target.js +6 -0
- package/dist/runtime/typed-event-target.js.map +1 -0
- package/dist/runtime/vdom.d.ts +55 -0
- package/dist/runtime/vdom.js +198 -0
- package/dist/runtime/vdom.js.map +1 -0
- package/dist/runtime/vnode.d.ts +79 -0
- package/dist/runtime/vnode.js +38 -0
- package/dist/runtime/vnode.js.map +1 -0
- package/dist/server/stream.d.ts +57 -0
- package/dist/server/stream.js +1007 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/style/css-mixin.d.ts +8 -0
- package/dist/style/css-mixin.js +54 -0
- package/dist/style/css-mixin.js.map +1 -0
- package/dist/style/index.d.ts +5 -0
- package/dist/style/index.js +4 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/style.d.ts +24 -0
- package/dist/style/style.js +269 -0
- package/dist/style/style.js.map +1 -0
- package/dist/style/stylesheet.d.ts +11 -0
- package/dist/style/stylesheet.js +174 -0
- package/dist/style/stylesheet.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +2 -0
- package/dist/test.js.map +1 -0
- package/dist/theme/contract.d.ts +259 -0
- package/dist/theme/contract.js +131 -0
- package/dist/theme/contract.js.map +1 -0
- package/dist/theme/glyph-contract.d.ts +13 -0
- package/dist/theme/glyph-contract.js +34 -0
- package/dist/theme/glyph-contract.js.map +1 -0
- package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
- package/dist/theme/presets/rmx-01/glyphs.js +252 -0
- package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
- package/dist/theme/presets/rmx-01/index.d.ts +2 -0
- package/dist/theme/presets/rmx-01/index.js +119 -0
- package/dist/theme/presets/rmx-01/index.js.map +1 -0
- package/dist/theme/runtime.d.ts +2 -0
- package/dist/theme/runtime.js +81 -0
- package/dist/theme/runtime.js.map +1 -0
- package/dist/theme/theme.d.ts +6 -0
- package/dist/theme/theme.js +5 -0
- package/dist/theme/theme.js.map +1 -0
- package/dist/utils/flash-attribute.d.ts +1 -0
- package/dist/utils/flash-attribute.js +11 -0
- package/dist/utils/flash-attribute.js.map +1 -0
- package/dist/utils/scroll-lock.d.ts +3 -0
- package/dist/utils/scroll-lock.js +69 -0
- package/dist/utils/scroll-lock.js.map +1 -0
- package/dist/utils/wait-for-css-transition.d.ts +1 -0
- package/dist/utils/wait-for-css-transition.js +58 -0
- package/dist/utils/wait-for-css-transition.js.map +1 -0
- package/dist/utils/wait.d.ts +1 -0
- package/dist/utils/wait.js +6 -0
- package/dist/utils/wait.js.map +1 -0
- package/package.json +125 -5
- package/src/animation/animate-layout-mixin.ts +266 -0
- package/src/animation/animate-mixins.ts +292 -0
- package/src/animation/index.ts +6 -0
- package/src/animation/spring.ts +299 -0
- package/src/animation/tween.ts +134 -0
- package/src/components/accordion/accordion.tsx +565 -0
- package/src/components/anchor/anchor.ts +904 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
- package/src/components/button/README.md +44 -0
- package/src/components/button/button.tsx +188 -0
- package/src/components/combobox/README.md +145 -0
- package/src/components/combobox/combobox.tsx +1014 -0
- package/src/components/glyph/glyph.tsx +110 -0
- package/src/components/listbox/listbox.ts +447 -0
- package/src/components/menu/hover-aim.ts +428 -0
- package/src/components/menu/menu.tsx +1547 -0
- package/src/components/popover/README.md +122 -0
- package/src/components/popover/popover.ts +201 -0
- package/src/components/select/select.tsx +585 -0
- package/src/components/separator/separator.ts +19 -0
- package/src/components/tabs/README.md +105 -0
- package/src/components/tabs/tabs.tsx +499 -0
- package/src/index.ts +72 -0
- package/src/interactions/keydown/keydown.ts +14 -0
- package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
- package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/runtime/client-entries.ts +137 -0
- package/src/runtime/component.ts +402 -0
- package/src/runtime/create-element.ts +38 -0
- package/src/runtime/diff-dom.ts +404 -0
- package/src/runtime/diff-props.ts +209 -0
- package/src/runtime/document-state.ts +130 -0
- package/src/runtime/dom.ts +3971 -0
- package/src/runtime/error-event.ts +26 -0
- package/src/runtime/event-listeners.ts +171 -0
- package/src/runtime/frame.ts +1211 -0
- package/src/runtime/invariant.ts +31 -0
- package/src/runtime/jsx.ts +398 -0
- package/src/runtime/mixins/attrs-mixin.ts +35 -0
- package/src/runtime/mixins/link-mixin.ts +131 -0
- package/src/runtime/mixins/mixin.ts +908 -0
- package/src/runtime/mixins/on-mixin.ts +89 -0
- package/src/runtime/mixins/ref-mixin.ts +32 -0
- package/src/runtime/navigation.ts +136 -0
- package/src/runtime/reconcile.ts +1722 -0
- package/src/runtime/render.ts +101 -0
- package/src/runtime/run.ts +100 -0
- package/src/runtime/scheduler.ts +262 -0
- package/src/runtime/svg-attributes.ts +133 -0
- package/src/runtime/to-vnode.ts +50 -0
- package/src/runtime/typed-event-target.ts +67 -0
- package/src/runtime/vdom.ts +295 -0
- package/src/runtime/vnode.ts +137 -0
- package/src/server/stream.ts +1342 -0
- package/src/style/css-mixin.ts +82 -0
- package/src/style/index.ts +10 -0
- package/src/style/style.ts +312 -0
- package/src/style/stylesheet.ts +209 -0
- package/src/test/setup.ts +7 -0
- package/src/test/utils.ts +45 -0
- package/src/test.ts +2 -0
- package/src/theme/contract.ts +183 -0
- package/src/theme/glyph-contract.ts +57 -0
- package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
- package/src/theme/presets/rmx-01/index.ts +121 -0
- package/src/theme/runtime.ts +116 -0
- package/src/theme/theme.ts +15 -0
- package/src/utils/flash-attribute.ts +11 -0
- package/src/utils/scroll-lock.ts +97 -0
- package/src/utils/wait-for-css-transition.ts +66 -0
- 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.
|