remix 3.0.0-beta.4 → 3.0.0-beta.6

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 (143) hide show
  1. package/README.md +4 -2
  2. package/dist/assets/types/hmr.d.ts +2 -0
  3. package/dist/cli-entry.js +1 -1
  4. package/dist/data-table/cli.d.ts +2 -0
  5. package/dist/data-table/cli.d.ts.map +1 -0
  6. package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
  7. package/dist/node-hmr/runtime.d.ts +2 -0
  8. package/dist/node-hmr/runtime.d.ts.map +1 -0
  9. package/dist/node-hmr/runtime.js +2 -0
  10. package/dist/node-hmr/types.d.ts +2 -0
  11. package/dist/node-hmr.d.ts +2 -0
  12. package/dist/node-hmr.d.ts.map +1 -0
  13. package/dist/{ui/glyph.js → node-hmr.js} +1 -1
  14. package/dist/ui/accordion/primitives.d.ts +2 -0
  15. package/dist/ui/accordion/primitives.d.ts.map +1 -0
  16. package/dist/ui/accordion/primitives.js +2 -0
  17. package/dist/ui/button.d.ts +1 -0
  18. package/dist/ui/button.d.ts.map +1 -1
  19. package/dist/ui/button.js +1 -0
  20. package/dist/ui/checkbox.d.ts +3 -0
  21. package/dist/ui/checkbox.d.ts.map +1 -0
  22. package/dist/ui/checkbox.js +3 -0
  23. package/dist/ui/combobox/primitives.d.ts +2 -0
  24. package/dist/ui/combobox/primitives.d.ts.map +1 -0
  25. package/dist/ui/combobox/primitives.js +2 -0
  26. package/dist/ui/dev/refresh.d.ts +2 -0
  27. package/dist/ui/dev/refresh.d.ts.map +1 -0
  28. package/dist/ui/dev/refresh.js +2 -0
  29. package/dist/ui/input.d.ts +3 -0
  30. package/dist/ui/input.d.ts.map +1 -0
  31. package/dist/ui/input.js +3 -0
  32. package/dist/ui/menu/primitives.d.ts +2 -0
  33. package/dist/ui/menu/primitives.d.ts.map +1 -0
  34. package/dist/ui/menu/primitives.js +2 -0
  35. package/dist/ui/radio.d.ts +3 -0
  36. package/dist/ui/radio.d.ts.map +1 -0
  37. package/dist/ui/radio.js +3 -0
  38. package/dist/ui/select/primitives.d.ts +2 -0
  39. package/dist/ui/select/primitives.d.ts.map +1 -0
  40. package/dist/ui/select/primitives.js +2 -0
  41. package/dist/ui/tabs/primitives.d.ts +2 -0
  42. package/dist/ui/tabs/primitives.d.ts.map +1 -0
  43. package/dist/ui/tabs/primitives.js +2 -0
  44. package/dist/ui/tabs.d.ts +2 -0
  45. package/dist/ui/tabs.d.ts.map +1 -0
  46. package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
  47. package/dist/ui/toggle/primitives.d.ts +2 -0
  48. package/dist/ui/toggle/primitives.d.ts.map +1 -0
  49. package/dist/ui/toggle/primitives.js +2 -0
  50. package/dist/ui/toggle.d.ts +3 -0
  51. package/dist/ui/toggle.d.ts.map +1 -0
  52. package/dist/ui/toggle.js +3 -0
  53. package/dist/ui-hmr/assets.d.ts +2 -0
  54. package/dist/ui-hmr/assets.d.ts.map +1 -0
  55. package/dist/ui-hmr/assets.js +2 -0
  56. package/dist/ui-hmr/node.d.ts +3 -0
  57. package/dist/ui-hmr/node.d.ts.map +1 -0
  58. package/dist/ui-hmr/node.js +3 -0
  59. package/dist/ui-hmr/runtime/browser.d.ts +2 -0
  60. package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
  61. package/dist/ui-hmr/runtime/browser.js +2 -0
  62. package/dist/ui-hmr/runtime/server.d.ts +2 -0
  63. package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
  64. package/dist/ui-hmr/runtime/server.js +2 -0
  65. package/dist/ui-hmr.d.ts +2 -0
  66. package/dist/ui-hmr.d.ts.map +1 -0
  67. package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
  68. package/package.json +122 -142
  69. package/src/assets/README.md +322 -56
  70. package/src/assets/types/hmr.d.ts +2 -0
  71. package/src/cli/README.md +105 -1
  72. package/src/cookie/README.md +4 -4
  73. package/src/data-table/README.md +202 -68
  74. package/src/data-table/cli.ts +2 -0
  75. package/src/data-table-mysql/README.md +46 -17
  76. package/src/data-table-postgres/README.md +39 -13
  77. package/src/data-table-sqlite/README.md +38 -20
  78. package/src/fetch-proxy/README.md +25 -0
  79. package/src/form-data-parser/README.md +4 -4
  80. package/src/mime/README.md +8 -1
  81. package/src/node-fetch-server/README.md +39 -13
  82. package/src/node-hmr/README.md +307 -0
  83. package/src/node-hmr/runtime.ts +2 -0
  84. package/src/node-hmr/types.d.ts +2 -0
  85. package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
  86. package/src/route-pattern/README.md +141 -13
  87. package/src/session/README.md +1 -1
  88. package/src/session-middleware/README.md +9 -7
  89. package/src/test/README.md +161 -115
  90. package/src/ui/README.md +116 -157
  91. package/src/ui/accordion/README.md +50 -14
  92. package/src/ui/accordion/primitives/README.md +202 -0
  93. package/src/ui/accordion/primitives.ts +2 -0
  94. package/src/ui/anchor/README.md +37 -2
  95. package/src/ui/breadcrumbs/README.md +4 -4
  96. package/src/ui/button/README.md +26 -26
  97. package/src/ui/button.ts +1 -0
  98. package/src/ui/checkbox/README.md +59 -0
  99. package/src/ui/checkbox.ts +3 -0
  100. package/src/ui/combobox/README.md +58 -9
  101. package/src/ui/combobox/primitives/README.md +194 -0
  102. package/src/ui/combobox/primitives.ts +2 -0
  103. package/src/ui/dev/refresh.ts +2 -0
  104. package/src/ui/input/README.md +52 -0
  105. package/src/ui/input.ts +3 -0
  106. package/src/ui/listbox/README.md +9 -41
  107. package/src/ui/menu/README.md +55 -14
  108. package/src/ui/menu/primitives/README.md +161 -0
  109. package/src/ui/menu/primitives.ts +2 -0
  110. package/src/ui/popover/README.md +20 -39
  111. package/src/ui/radio/README.md +53 -0
  112. package/src/ui/radio.ts +3 -0
  113. package/src/ui/select/README.md +29 -19
  114. package/src/ui/select/primitives/README.md +117 -0
  115. package/src/ui/select/primitives.ts +2 -0
  116. package/src/ui/tabs/README.md +141 -0
  117. package/src/ui/tabs/primitives/README.md +141 -0
  118. package/src/ui/tabs/primitives.ts +2 -0
  119. package/src/ui/tabs.ts +2 -0
  120. package/src/ui/test/README.md +151 -60
  121. package/src/ui/toggle/README.md +56 -0
  122. package/src/ui/toggle/primitives/README.md +56 -0
  123. package/src/ui/toggle/primitives.ts +2 -0
  124. package/src/ui/toggle.ts +3 -0
  125. package/src/ui-hmr/README.md +119 -0
  126. package/{dist/ui/separator.js → src/ui-hmr/assets.ts} +1 -1
  127. package/src/ui-hmr/node.ts +3 -0
  128. package/src/ui-hmr/runtime/browser.ts +2 -0
  129. package/src/ui-hmr/runtime/server.ts +2 -0
  130. package/src/ui-hmr.ts +2 -0
  131. package/dist/ui/glyph.d.ts +0 -2
  132. package/dist/ui/glyph.d.ts.map +0 -1
  133. package/dist/ui/scroll-lock.d.ts +0 -2
  134. package/dist/ui/scroll-lock.d.ts.map +0 -1
  135. package/dist/ui/separator.d.ts +0 -2
  136. package/dist/ui/separator.d.ts.map +0 -1
  137. package/dist/ui/theme.d.ts +0 -2
  138. package/dist/ui/theme.d.ts.map +0 -1
  139. package/src/ui/glyph/README.md +0 -72
  140. package/src/ui/scroll-lock/README.md +0 -33
  141. package/src/ui/scroll-lock.ts +0 -2
  142. package/src/ui/separator.ts +0 -2
  143. package/src/ui/theme/README.md +0 -103
@@ -0,0 +1,194 @@
1
+ # combobox
2
+
3
+ `Combobox` is the input-first popup value picker for `remix/ui/combobox`.
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
+ ## Component Usage
8
+
9
+ ```tsx
10
+ import { css, type Handle } from 'remix/ui'
11
+ import { Combobox, ComboboxOption } from 'remix/ui/combobox'
12
+ import { onComboboxChange } from 'remix/ui/combobox/primitives'
13
+
14
+ let airports = [
15
+ {
16
+ label: 'Los Angeles International',
17
+ searchValue: ['lax', 'los angeles', 'los angeles international'],
18
+ value: 'LAX',
19
+ },
20
+ {
21
+ label: 'John F. Kennedy International',
22
+ searchValue: ['jfk', 'new york', 'john f. kennedy international'],
23
+ value: 'JFK',
24
+ },
25
+ ] as const
26
+
27
+ export default function AirportField(handle: Handle) {
28
+ let value: string | null = null
29
+
30
+ return () => (
31
+ <div mix={root}>
32
+ <Combobox
33
+ inputId="airport"
34
+ mix={onComboboxChange((event) => {
35
+ value = event.value
36
+ void handle.update()
37
+ })}
38
+ name="airport"
39
+ placeholder="Search airports or codes"
40
+ >
41
+ {airports.map((airport) => (
42
+ <ComboboxOption
43
+ key={airport.value}
44
+ label={airport.label}
45
+ searchValue={airport.searchValue}
46
+ value={airport.value}
47
+ />
48
+ ))}
49
+ </Combobox>
50
+
51
+ <p>{`value=${value ?? 'null'}`}</p>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ let root = css({
57
+ display: 'grid',
58
+ gap: '8px',
59
+ })
60
+ ```
61
+
62
+ ## Primitive Usage
63
+
64
+ Use the lower-level primitives when app code owns the input, popover, list, and option markup:
65
+
66
+ ```tsx
67
+ import * as combobox from 'remix/ui/combobox/primitives'
68
+ import { inputStyle, listStyle, optionStyle, popoverStyle } from './combobox.styles'
69
+
70
+ let frameworks = [
71
+ { label: 'Remix', searchValue: ['remix', 'rmx'], value: 'remix' },
72
+ { label: 'React Router', value: 'react-router' },
73
+ ]
74
+
75
+ export function PrimitiveCombobox() {
76
+ return (
77
+ <combobox.Context name="framework">
78
+ <input mix={[inputStyle, combobox.input()]} placeholder="Search frameworks" />
79
+ <div mix={[popoverStyle, combobox.popover()]}>
80
+ <div mix={[listStyle, combobox.list()]}>
81
+ {frameworks.map((option) => (
82
+ <div key={option.value} mix={[optionStyle, combobox.option(option)]}>
83
+ {option.label}
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ <input mix={combobox.hiddenInput()} />
89
+ </combobox.Context>
90
+ )
91
+ }
92
+ ```
93
+
94
+ ## `remix/ui/combobox`
95
+
96
+ ### `Combobox`
97
+
98
+ The convenience component.
99
+
100
+ - Renders the text input, popover surface, listbox root, and hidden form input.
101
+ - Dispatches a bubbled custom event that `onComboboxChange(...)` listens for when the committed value changes.
102
+ - Accepts `children`, `defaultValue`, `disabled`, `inputId`, `name`, `placeholder`, and root `div` props.
103
+
104
+ ### `ComboboxOption`
105
+
106
+ The default option row for `Combobox`.
107
+
108
+ - Uses the shared listbox option visuals.
109
+ - Accepts `label`, `value`, optional `searchValue`, and optional `disabled`.
110
+ - Renders `children` when provided, otherwise renders `label`.
111
+ - `searchValue` can be a string or string array for aliases like airport codes, abbreviations, or alternate labels.
112
+
113
+ ### Style and Prop Exports
114
+
115
+ - `inputStyle`: default combobox input style.
116
+ - `popoverStyle`: default combobox popover behavior style.
117
+ - `ComboboxProps` and `ComboboxOptionProps`: public TypeScript props for the composed APIs.
118
+
119
+ ## `remix/ui/combobox/primitives`
120
+
121
+ ### `onComboboxChange(...)`
122
+
123
+ The listener mixin from `remix/ui/combobox/primitives` for bubbled committed-value changes.
124
+
125
+ The event object includes:
126
+
127
+ - `event.value`: the committed value or `null`
128
+ - `event.label`: the committed option label or `null`
129
+ - `event.optionId`: the generated option id or `null`
130
+ - `ComboboxChangeEvent`: the event class dispatched for committed value changes.
131
+
132
+ ### `combobox.Context`
133
+
134
+ The lower-level coordinator from `remix/ui/combobox/primitives` for custom combobox composition.
135
+
136
+ It wraps the shared `popover` and `listbox` contexts and owns the draft text, committed value, popup state, and selection timing.
137
+
138
+ ### `combobox.input()`
139
+
140
+ Turns the host input into the combobox input.
141
+
142
+ - Keeps focus on the input during list navigation and pointer selection.
143
+ - Wires `role="combobox"`, `aria-expanded`, `aria-controls`, and `aria-activedescendant`.
144
+ - Opens from typing, click, and arrow-key navigation.
145
+
146
+ ### `combobox.popover()`
147
+
148
+ Turns the host into the combobox popover surface.
149
+
150
+ - Uses the shared popover primitive.
151
+ - Keeps anchor clicks inside the session so the input stays interactive while open.
152
+ - Applies the combobox open/close reason contract used by `popoverStyle`.
153
+
154
+ ### `combobox.list()`
155
+
156
+ Turns the host into the popup listbox root and applies the generated list id.
157
+
158
+ ### `combobox.option(options)`
159
+
160
+ Registers one option with the combobox and listbox layers.
161
+
162
+ - Accepts `label`, `value`, optional `searchValue`, and optional `disabled`.
163
+ - Hides non-matching options from the current draft filter.
164
+ - Prevents pointer selection from blurring the input before the click commits.
165
+
166
+ ### `combobox.hiddenInput()`
167
+
168
+ Mirrors the committed value into a hidden input for forms.
169
+
170
+ Apply it to an `<input type="hidden" />` inside the same `combobox.Context`.
171
+
172
+ ### Primitive Types
173
+
174
+ - `ComboboxOpenStrategy`: initial active-option strategy when the popup opens.
175
+ - `ComboboxHandle`: imperative ref for reading or updating the combobox value and draft label.
176
+ - `ComboboxContextProps`, `ComboboxProps`, `ComboboxOptionOptions`, and `ComboboxOptionProps`: primitive prop and option types for custom composition.
177
+
178
+ ## Behavior Notes
179
+
180
+ - Typing opens the popup in hint mode when there are matches.
181
+ - If typing leaves no matches, the popup closes immediately without the navigation fade-out.
182
+ - Selecting from the list flashes the option, then closes the popup and finally commits the visible input label.
183
+ - Typing clears the committed value immediately; the hidden form value becomes empty until the user commits again.
184
+ - Blur commits an exact `label` or `searchValue` match. A non-matching blur clears the draft text and committed value.
185
+ - `Escape` keeps exact-match draft text but clears non-matching draft text and selection.
186
+ - Disabled options can stay visible in filtered results, but they are skipped by keyboard navigation and selection.
187
+
188
+ ## When To Use Something Else
189
+
190
+ Use `Select` when you want the ordinary button-triggered single-select control.
191
+
192
+ Use `listbox` when you need listbox semantics without an editable text input.
193
+
194
+ Use `popover` directly for custom floating panels that are not value-picking controls.
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/ui/combobox/primitives'
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/ui/dev/refresh'
@@ -0,0 +1,52 @@
1
+ # input
2
+
3
+ `input` is a style mixin for text inputs. Use `input()` directly on standalone native inputs, or compose `input.root()` with `input.field()` when the input needs inline icons or controls.
4
+
5
+ ## Primitive Usage
6
+
7
+ ```tsx
8
+ import input from 'remix/ui/input'
9
+
10
+ function ProductFilters() {
11
+ return () => (
12
+ <div>
13
+ <input mix={input()} placeholder="Limit" />
14
+
15
+ <div mix={input.root()}>
16
+ <SearchIcon />
17
+ <input mix={input.field()} placeholder="Search and filter products" />
18
+ </div>
19
+ </div>
20
+ )
21
+ }
22
+ ```
23
+
24
+ Compose app-owned styles when a field needs local layout or adornments:
25
+
26
+ ```tsx
27
+ import input from 'remix/ui/input'
28
+ import { filterFieldStyle, filterRootStyle } from './filters.styles'
29
+
30
+ function SearchFilter() {
31
+ return () => (
32
+ <div mix={[filterRootStyle, input.root()]}>
33
+ <SearchIcon />
34
+ <input mix={[filterFieldStyle, input.field()]} placeholder="Search" />
35
+ </div>
36
+ )
37
+ }
38
+ ```
39
+
40
+ ## `remix/ui/input`
41
+
42
+ - `input(options)`: styles a standalone native input. `size` may be `'md'` or `'lg'` and defaults to `'md'`.
43
+ - `input.root(options)`: styles a flex input frame for inline icons, buttons, and a child input.
44
+ - `input.field()`: styles the child native input inside `input.root()`.
45
+ - `InputOptions`: accepts `size`.
46
+ - `InputSize`: `'md'` or `'lg'`.
47
+
48
+ ## Behavior Notes
49
+
50
+ - `input.root()` uses child selectors to size direct SVG children as presentational icons.
51
+ - Put the icon before or after the field to control its visual position.
52
+ - Focus and disabled states work through the native input; the root mirrors them with `:focus-within` and `:has(input:disabled)`.
@@ -0,0 +1,3 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/ui/input'
3
+ export { default } from '@remix-run/ui/input'
@@ -1,14 +1,14 @@
1
1
  # listbox
2
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.
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
4
 
5
- ## Usage
5
+ ## Primitive Usage
6
6
 
7
7
  ```tsx
8
8
  import type { Handle } from 'remix/ui'
9
- import { Glyph } from 'remix/ui/glyph'
10
9
  import * as listbox from 'remix/ui/listbox'
11
10
  import type { ListboxValue } from 'remix/ui/listbox'
11
+ import { listStyle, optionStyle } from './listbox.styles'
12
12
 
13
13
  function FrameworkListbox(handle: Handle) {
14
14
  let value: ListboxValue = 'remix'
@@ -27,11 +27,10 @@ function FrameworkListbox(handle: Handle) {
27
27
  void handle.update()
28
28
  }}
29
29
  >
30
- <div aria-label="Frameworks" tabIndex={0} mix={[listbox.listStyle, listbox.list()]}>
30
+ <div aria-label="Frameworks" tabIndex={0} mix={[listStyle, listbox.list()]}>
31
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>
32
+ <div key={option.value} mix={[optionStyle, listbox.option(option)]}>
33
+ {option.label}
35
34
  </div>
36
35
  ))}
37
36
  </div>
@@ -52,6 +51,7 @@ Use `textValue` when the visible label is not the best string for typeahead sear
52
51
  ```tsx
53
52
  <div
54
53
  mix={[
54
+ optionStyle,
55
55
  listbox.option({
56
56
  label: 'Staging',
57
57
  textValue: 'beta',
@@ -63,42 +63,13 @@ Use `textValue` when the visible label is not the best string for typeahead sear
63
63
  </div>
64
64
  ```
65
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.*`
66
+ ## `remix/ui/listbox`
96
67
 
97
68
  - `listbox.Context`: provider for controlled `value` and `activeValue`, option registration, selection, highlighting, optional ref access, `flashSelection`, `selectionFlashAttribute`, and `onSelectSettled`.
98
69
  - `listbox.list()`: mixin that wires `role="listbox"`, default `tabIndex={-1}`, keyboard navigation, focus scrolling, and typeahead highlighting.
99
70
  - `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
71
  - `ListboxValue`: selected or active value, represented as `string | null`.
72
+ - `ListboxContext` and `ListboxProviderProps`: provider context and prop types for controlled listboxes.
102
73
  - `ListboxOption`: option input shape with `label`, `value`, optional `disabled`, and optional `textValue`.
103
74
  - `ListboxRegisteredOption`: registered option metadata passed to callbacks and refs.
104
75
  - `ListboxRef`: live ref object exposing active/selected options, option navigation, search matching, scrolling, and selection helpers.
@@ -108,8 +79,5 @@ function selectLastOption() {
108
79
  - Selection and highlighting are controlled. `onSelect` and `onHighlight` notify the parent, but DOM state updates after the parent rerenders with new values.
109
80
  - Disabled options are skipped by keyboard navigation, typeahead, mouse movement, and click selection.
110
81
  - 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
82
  - 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
83
  - `flashSelection` applies `selectionFlashAttribute` for 60ms, delays `onSelectSettled`, and ignores new highlight/select interactions until the flash completes.
@@ -2,12 +2,13 @@
2
2
 
3
3
  `Menu` renders a button-triggered menu with keyboard navigation, checked items, selection events, and nested submenus. Use it for action menus and command groups.
4
4
 
5
- ## Usage
5
+ ## Component Usage
6
6
 
7
7
  ```tsx
8
8
  import type { Handle } from 'remix/ui'
9
- import { Menu, MenuItem, Submenu, onMenuSelect } from 'remix/ui/menu'
10
- import { separatorStyle } from 'remix/ui/separator'
9
+ import { css } from 'remix/ui'
10
+ import { Menu, MenuItem, Submenu } from 'remix/ui/menu'
11
+ import { onMenuSelect } from 'remix/ui/menu/primitives'
11
12
 
12
13
  export function ViewMenu(handle: Handle) {
13
14
  let wordWrap = false
@@ -50,6 +51,12 @@ export function ViewMenu(handle: Handle) {
50
51
  </Menu>
51
52
  )
52
53
  }
54
+
55
+ let separatorStyle = css({
56
+ border: 0,
57
+ borderBlockStart: '1px solid #e5e7eb',
58
+ marginBlock: '4px',
59
+ })
53
60
  ```
54
61
 
55
62
  Use `label` or `searchValue` when the rendered item content is not the text that should be used for event labels or typeahead.
@@ -71,8 +78,8 @@ Use `menuLabel` when the menu surface needs a different accessible label from th
71
78
  Use `menu.contextTrigger()` with `menu.Context` and `MenuList` when a menu should open at the right-click location of an element.
72
79
 
73
80
  ```tsx
74
- import * as menu from 'remix/ui/menu'
75
81
  import { MenuItem, MenuList } from 'remix/ui/menu'
82
+ import * as menu from 'remix/ui/menu/primitives'
76
83
 
77
84
  export function FileContextMenu(handle: Handle) {
78
85
  return () => (
@@ -89,20 +96,54 @@ export function FileContextMenu(handle: Handle) {
89
96
  }
90
97
  ```
91
98
 
92
- Attach `onMenuSelect(...)` to `MenuList` or a shared ancestor when using lower-level context menu composition.
99
+ Attach `onMenuSelect(...)` from `remix/ui/menu/primitives` to `MenuList` or a shared ancestor when using lower-level context menu composition.
100
+
101
+ ## Primitive Usage
102
+
103
+ Use only the lower-level primitives when app code owns the trigger, surface, and item markup:
104
+
105
+ ```tsx
106
+ import * as menu from 'remix/ui/menu/primitives'
107
+ import { itemStyle, listStyle, popoverStyle, triggerStyle } from './menu.styles'
108
+
109
+ export function PrimitiveMenu() {
110
+ return (
111
+ <menu.Context label="Project actions">
112
+ <button mix={[triggerStyle, menu.trigger()]} type="button">
113
+ Actions
114
+ </button>
115
+ <div mix={[popoverStyle, menu.popover()]}>
116
+ <div mix={[listStyle, menu.list()]}>
117
+ <div mix={[itemStyle, menu.item({ name: 'rename' })]}>Rename</div>
118
+ <div mix={[itemStyle, menu.item({ disabled: true, name: 'archive' })]}>Archive</div>
119
+ </div>
120
+ </div>
121
+ </menu.Context>
122
+ )
123
+ }
124
+ ```
93
125
 
94
- ## `menu.*`
126
+ ## `remix/ui/menu`
95
127
 
96
128
  - `Menu`: composed trigger, popover, and list component for the common menu case.
97
- - `MenuItem`: menu item wrapper. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
98
- - `Submenu`: nested menu wrapper with its own trigger and child menu surface.
99
- - `MenuList`: lower-level list wrapper for custom composition.
129
+ - `MenuItem`: menu item component. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
130
+ - `Submenu`: nested menu component with its own trigger and child menu surface.
131
+ - `MenuList`: lower-level styled list component for custom composition inside `menu.Context`.
132
+ - `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemIndicatorStyle`, and `triggerIndicatorStyle`: flat style mixins used by the component markup.
133
+ - `MenuProps`, `MenuItemProps`, `MenuListProps`, and `SubmenuProps`: public TypeScript props for the composed APIs.
134
+
135
+ ## `remix/ui/menu/primitives`
136
+
137
+ - `Context`: lower-level provider for custom menu composition.
138
+ - `trigger()`: wires a button-style trigger to open the root menu.
139
+ - `contextTrigger()`: opens the root menu from a `contextmenu` event at pointer coordinates, or from keyboard context-menu shortcuts.
140
+ - `popover()`: wires the menu popover surface.
141
+ - `list()`: wires the menu list root, focus handling, keyboard navigation, and typeahead.
142
+ - `item(...)`: registers one menu item. Supports regular, checkbox, and radio roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
143
+ - `submenuTrigger(...)`: registers a menu item that opens a child menu.
100
144
  - `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
101
- - `MenuSelectEvent`: bubbling event class whose `item` describes the selected item.
102
- - `MenuSelectItem`: selected item shape with `checked`, `id`, `label`, `name`, `type`, and `value`.
103
- - `menu.Context`, `menu.trigger()`, `menu.contextTrigger()`, `menu.popover()`, `menu.list()`, `menu.item(...)`, and `menu.submenuTrigger(...)`: lower-level composition primitives.
104
- - `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemGlyphStyle`, and `triggerGlyphStyle`: flat style mixins used by the wrappers.
105
- - `MenuProps`, `MenuItemProps`, `MenuListProps`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuContextTriggerOptions`, `MenuItemOptions`, and `SubmenuProps`: public TypeScript props and option types.
145
+ - `MenuSelectEvent`: bubbling event whose `item` describes the selected item.
146
+ - `MenuSelectItem`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuContextTriggerOptions`, `MenuItemOptions`, and `SubmenuTriggerOptions`: public TypeScript event, prop, and option types for the primitives.
106
147
 
107
148
  ## Behavior Notes
108
149
 
@@ -0,0 +1,161 @@
1
+ # menu
2
+
3
+ `Menu` renders a button-triggered menu with keyboard navigation, checked items, selection events, and nested submenus. Use it for action menus and command groups.
4
+
5
+ ## Component Usage
6
+
7
+ ```tsx
8
+ import type { Handle } from 'remix/ui'
9
+ import { css } from 'remix/ui'
10
+ import { Menu, MenuItem, Submenu } from 'remix/ui/menu'
11
+ import { onMenuSelect } from 'remix/ui/menu/primitives'
12
+
13
+ export function ViewMenu(handle: Handle) {
14
+ let wordWrap = false
15
+ let density = 'comfortable'
16
+
17
+ return () => (
18
+ <Menu
19
+ label="View"
20
+ mix={onMenuSelect((event) => {
21
+ if (event.item.name === 'wordWrap') {
22
+ wordWrap = event.item.checked ?? false
23
+ } else if (event.item.name === 'density' && event.item.value) {
24
+ density = event.item.value
25
+ }
26
+
27
+ void handle.update()
28
+ })}
29
+ >
30
+ <MenuItem checked={wordWrap} name="wordWrap" type="checkbox">
31
+ Word wrap
32
+ </MenuItem>
33
+ <MenuItem disabled name="minimap">
34
+ Minimap
35
+ </MenuItem>
36
+ <hr mix={separatorStyle} />
37
+ <MenuItem checked={density === 'compact'} name="density" type="radio" value="compact">
38
+ Compact
39
+ </MenuItem>
40
+ <MenuItem checked={density === 'comfortable'} name="density" type="radio" value="comfortable">
41
+ Comfortable
42
+ </MenuItem>
43
+ <Submenu label="Zoom">
44
+ <MenuItem name="zoomIn" value="zoom-in">
45
+ Zoom in
46
+ </MenuItem>
47
+ <MenuItem name="zoomOut" value="zoom-out">
48
+ Zoom out
49
+ </MenuItem>
50
+ </Submenu>
51
+ </Menu>
52
+ )
53
+ }
54
+
55
+ let separatorStyle = css({
56
+ border: 0,
57
+ borderBlockStart: '1px solid #e5e7eb',
58
+ marginBlock: '4px',
59
+ })
60
+ ```
61
+
62
+ Use `label` or `searchValue` when the rendered item content is not the text that should be used for event labels or typeahead.
63
+
64
+ ```tsx
65
+ <MenuItem label="Open command palette" name="commandPalette" searchValue="palette">
66
+ Command palette
67
+ </MenuItem>
68
+ ```
69
+
70
+ Use `menuLabel` when the menu surface needs a different accessible label from the visible trigger.
71
+
72
+ ```tsx
73
+ <Menu label="..." menuLabel="Project actions">
74
+ <MenuItem name="rename">Rename project</MenuItem>
75
+ </Menu>
76
+ ```
77
+
78
+ Use `menu.contextTrigger()` with `menu.Context` and `MenuList` when a menu should open at the right-click location of an element.
79
+
80
+ ```tsx
81
+ import { MenuItem, MenuList } from 'remix/ui/menu'
82
+ import * as menu from 'remix/ui/menu/primitives'
83
+
84
+ export function FileContextMenu(handle: Handle) {
85
+ return () => (
86
+ <menu.Context label="File actions">
87
+ <div mix={menu.contextTrigger()} tabIndex={0}>
88
+ File.txt
89
+ </div>
90
+ <MenuList>
91
+ <MenuItem name="rename">Rename</MenuItem>
92
+ <MenuItem name="delete">Delete</MenuItem>
93
+ </MenuList>
94
+ </menu.Context>
95
+ )
96
+ }
97
+ ```
98
+
99
+ Attach `onMenuSelect(...)` from `remix/ui/menu/primitives` to `MenuList` or a shared ancestor when using lower-level context menu composition.
100
+
101
+ ## Primitive Usage
102
+
103
+ Use only the lower-level primitives when app code owns the trigger, surface, and item markup:
104
+
105
+ ```tsx
106
+ import * as menu from 'remix/ui/menu/primitives'
107
+ import { itemStyle, listStyle, popoverStyle, triggerStyle } from './menu.styles'
108
+
109
+ export function PrimitiveMenu() {
110
+ return (
111
+ <menu.Context label="Project actions">
112
+ <button mix={[triggerStyle, menu.trigger()]} type="button">
113
+ Actions
114
+ </button>
115
+ <div mix={[popoverStyle, menu.popover()]}>
116
+ <div mix={[listStyle, menu.list()]}>
117
+ <div mix={[itemStyle, menu.item({ name: 'rename' })]}>Rename</div>
118
+ <div mix={[itemStyle, menu.item({ disabled: true, name: 'archive' })]}>Archive</div>
119
+ </div>
120
+ </div>
121
+ </menu.Context>
122
+ )
123
+ }
124
+ ```
125
+
126
+ ## `remix/ui/menu`
127
+
128
+ - `Menu`: composed trigger, popover, and list component for the common menu case.
129
+ - `MenuItem`: menu item component. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
130
+ - `Submenu`: nested menu component with its own trigger and child menu surface.
131
+ - `MenuList`: lower-level styled list component for custom composition inside `menu.Context`.
132
+ - `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemIndicatorStyle`, and `triggerIndicatorStyle`: flat style mixins used by the component markup.
133
+ - `MenuProps`, `MenuItemProps`, `MenuListProps`, and `SubmenuProps`: public TypeScript props for the composed APIs.
134
+
135
+ ## `remix/ui/menu/primitives`
136
+
137
+ - `Context`: lower-level provider for custom menu composition.
138
+ - `trigger()`: wires a button-style trigger to open the root menu.
139
+ - `contextTrigger()`: opens the root menu from a `contextmenu` event at pointer coordinates, or from keyboard context-menu shortcuts.
140
+ - `popover()`: wires the menu popover surface.
141
+ - `list()`: wires the menu list root, focus handling, keyboard navigation, and typeahead.
142
+ - `item(...)`: registers one menu item. Supports regular, checkbox, and radio roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
143
+ - `submenuTrigger(...)`: registers a menu item that opens a child menu.
144
+ - `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
145
+ - `MenuSelectEvent`: bubbling event whose `item` describes the selected item.
146
+ - `MenuSelectItem`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuContextTriggerOptions`, `MenuItemOptions`, and `SubmenuTriggerOptions`: public TypeScript event, prop, and option types for the primitives.
147
+
148
+ ## Behavior Notes
149
+
150
+ - Click opens the root menu and focuses the list; clicking the trigger again closes it and restores focus.
151
+ - `menu.contextTrigger()` opens the root menu from a `contextmenu` event at the pointer coordinates and supports keyboard opening with the Context Menu key or Shift+F10.
152
+ - `ArrowDown` opens from the trigger at the first enabled item. `ArrowUp` opens at the last enabled item. Enter and Space open the menu with focus on the list.
153
+ - Keyboard navigation skips disabled items and does not wrap past the first or last enabled item.
154
+ - `Home` and `End` move to the first and last enabled item. Enter and Space activate the highlighted item.
155
+ - Printable keys use typeahead. Typeahead matches `searchValue` when provided and otherwise uses the item label.
156
+ - Submenus open with `ArrowRight`, close with `ArrowLeft`, and are anchored to the submenu trigger with `right-start` placement.
157
+ - Pointer movement highlights enabled items. Submenus open after a short focus/pointer delay and use hover aim so they stay open while moving toward the child surface.
158
+ - Selecting a regular item flashes that item. Selecting a checkbox or radio item flashes the committed checked state.
159
+ - Selection dispatches one bubbled `MenuSelectEvent`, closes the full menu tree, and restores focus to the root trigger.
160
+ - The composed `Menu` re-dispatches selection from its trigger so handlers on the `Menu` button and shared ancestors see the event once.
161
+ - `menuLabel`, `Submenu.menuLabel`, and `Submenu.listProps` let composed menus label and customize menu surfaces separately from trigger content.
@@ -0,0 +1,2 @@
1
+ // IMPORTANT: This file is auto-generated, please do not edit manually.
2
+ export * from '@remix-run/ui/menu/primitives'