remix 3.0.0-beta.0 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/fetch-router.d.ts +7 -0
  2. package/dist/fetch-router.d.ts.map +1 -1
  3. package/dist/node-tsx/load-module.d.ts +2 -0
  4. package/dist/node-tsx/load-module.d.ts.map +1 -0
  5. package/dist/node-tsx/load-module.js +2 -0
  6. package/dist/node-tsx.d.ts +3 -0
  7. package/dist/node-tsx.d.ts.map +1 -0
  8. package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
  9. package/dist/render-middleware.d.ts +2 -0
  10. package/dist/render-middleware.d.ts.map +1 -0
  11. package/dist/render-middleware.js +2 -0
  12. package/dist/route-pattern/href.d.ts +2 -0
  13. package/dist/route-pattern/href.d.ts.map +1 -0
  14. package/dist/route-pattern/href.js +2 -0
  15. package/dist/route-pattern/join.d.ts +2 -0
  16. package/dist/route-pattern/join.d.ts.map +1 -0
  17. package/dist/route-pattern/join.js +2 -0
  18. package/dist/route-pattern/match.d.ts +2 -0
  19. package/dist/route-pattern/match.d.ts.map +1 -0
  20. package/dist/route-pattern/match.js +2 -0
  21. package/package.json +158 -44
  22. package/src/assert/README.md +109 -0
  23. package/src/assets/README.md +539 -0
  24. package/src/async-context-middleware/README.md +100 -0
  25. package/src/auth/README.md +445 -0
  26. package/src/auth-middleware/README.md +246 -0
  27. package/src/cli/README.md +78 -0
  28. package/src/compression-middleware/README.md +176 -0
  29. package/src/cookie/README.md +106 -0
  30. package/src/cop-middleware/README.md +117 -0
  31. package/src/cors-middleware/README.md +174 -0
  32. package/src/csrf-middleware/README.md +99 -0
  33. package/src/data-schema/README.md +422 -0
  34. package/src/data-table/README.md +552 -0
  35. package/src/data-table-mysql/README.md +97 -0
  36. package/src/data-table-postgres/README.md +74 -0
  37. package/src/data-table-sqlite/README.md +84 -0
  38. package/src/fetch-proxy/README.md +46 -0
  39. package/src/fetch-router/README.md +902 -0
  40. package/src/fetch-router.ts +7 -0
  41. package/src/file-storage/README.md +57 -0
  42. package/src/file-storage-s3/README.md +47 -0
  43. package/src/form-data-middleware/README.md +109 -0
  44. package/src/form-data-parser/README.md +160 -0
  45. package/src/fs/README.md +60 -0
  46. package/src/headers/README.md +629 -0
  47. package/src/html-template/README.md +101 -0
  48. package/src/lazy-file/README.md +109 -0
  49. package/src/logger-middleware/README.md +132 -0
  50. package/src/method-override-middleware/README.md +71 -0
  51. package/src/mime/README.md +110 -0
  52. package/src/multipart-parser/README.md +241 -0
  53. package/src/node-fetch-server/README.md +352 -0
  54. package/src/node-tsx/README.md +79 -0
  55. package/src/node-tsx/load-module.ts +2 -0
  56. package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
  57. package/src/render-middleware/README.md +99 -0
  58. package/src/render-middleware.ts +2 -0
  59. package/src/route-pattern/README.md +291 -0
  60. package/src/route-pattern/href.ts +2 -0
  61. package/src/route-pattern/join.ts +2 -0
  62. package/src/route-pattern/match.ts +2 -0
  63. package/src/session/README.md +171 -0
  64. package/src/session-middleware/README.md +109 -0
  65. package/src/session-storage-memcache/README.md +37 -0
  66. package/src/session-storage-redis/README.md +37 -0
  67. package/src/static-middleware/README.md +89 -0
  68. package/src/tar-parser/README.md +74 -0
  69. package/src/terminal/README.md +92 -0
  70. package/src/test/README.md +430 -0
  71. package/src/ui/README.md +219 -0
  72. package/src/ui/accordion/README.md +166 -0
  73. package/src/ui/anchor/README.md +153 -0
  74. package/src/ui/animation/README.md +316 -0
  75. package/src/ui/breadcrumbs/README.md +55 -0
  76. package/src/ui/button/README.md +44 -0
  77. package/src/ui/combobox/README.md +145 -0
  78. package/src/ui/glyph/README.md +72 -0
  79. package/src/ui/listbox/README.md +115 -0
  80. package/src/ui/menu/README.md +96 -0
  81. package/src/ui/popover/README.md +122 -0
  82. package/src/ui/scroll-lock/README.md +33 -0
  83. package/src/ui/select/README.md +107 -0
  84. package/src/ui/server/README.md +90 -0
  85. package/src/ui/test/README.md +107 -0
  86. package/src/ui/theme/README.md +103 -0
  87. package/dist/node-serve.d.ts +0 -2
  88. package/dist/node-serve.d.ts.map +0 -1
@@ -0,0 +1,44 @@
1
+ # button
2
+
3
+ `button` is the shared button styling contract for `remix/ui`. Use `Button` for ordinary action buttons, or compose flat `button.*Style` exports directly when a higher-level control needs button structure without a wrapper.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Button } from 'remix/ui/button'
9
+ import * as button from 'remix/ui/button'
10
+ import { Glyph } from 'remix/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,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/combobox'
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.
@@ -0,0 +1,72 @@
1
+ # glyph
2
+
3
+ `Glyph` renders references into a shared SVG sprite sheet. Render a glyph sheet once, then render individual `Glyph` instances by name.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Glyph } from 'remix/ui/glyph'
9
+ import { RMX_01_GLYPHS } from 'remix/ui/theme'
10
+
11
+ function Layout() {
12
+ return (
13
+ <html>
14
+ <body>
15
+ <RMX_01_GLYPHS />
16
+ <button aria-label="Delete">
17
+ <Glyph name="trash" />
18
+ </button>
19
+ </body>
20
+ </html>
21
+ )
22
+ }
23
+ ```
24
+
25
+ Most glyphs are decorative because the surrounding control supplies the accessible name. `Glyph` sets `aria-hidden` by default in that case.
26
+
27
+ ```tsx
28
+ <button aria-label="Search">
29
+ <Glyph name="search" />
30
+ </button>
31
+ ```
32
+
33
+ Give the glyph its own label only when the SVG itself is the accessible element.
34
+
35
+ ```tsx
36
+ <Glyph aria-label="Search" name="search" viewBox="0 0 20 20" width="24" />
37
+ ```
38
+
39
+ Use `createGlyphSheet` when a theme or app provides its own complete glyph set. The generated sheet exposes the stable symbol ids and the original values for reuse.
40
+
41
+ ```tsx
42
+ import { createGlyphSheet, type GlyphValues } from 'remix/ui/glyph'
43
+
44
+ declare const glyphValues: GlyphValues
45
+
46
+ export const AppGlyphs = createGlyphSheet(glyphValues)
47
+
48
+ AppGlyphs.ids.trash
49
+ AppGlyphs.values.trash
50
+ ```
51
+
52
+ ## `glyph.*`
53
+
54
+ - `Glyph`: renders an `<svg>` with a `<use>` element that points at the package-owned symbol id for `name`.
55
+ - `createGlyphSheet(values)`: creates a hidden SVG sprite sheet component from a complete glyph value set.
56
+ - `GlyphName`: typed union of supported glyph names.
57
+ - `GlyphProps`: props accepted by `Glyph`.
58
+ - `GlyphSheetProps`: props accepted by generated glyph sheet components.
59
+ - `GlyphSymbol`: SVG symbol value accepted by glyph value maps.
60
+ - `GlyphValues`: object shape expected by `createGlyphSheet`.
61
+ - `GlyphSheetComponent`: generated sprite sheet component with `ids` and `values` attached.
62
+
63
+ The built-in glyph names are `add`, `alert`, `check`, `chevronDown`, `chevronVertical`, `chevronUp`, `chevronRight`, `close`, `copy`, `edit`, `expand`, `info`, `menu`, `open`, `search`, `spinner`, and `trash`.
64
+
65
+ ## Behavior Notes
66
+
67
+ - Render the glyph sheet once before rendering glyph instances that reference it.
68
+ - `createGlyphSheet` renders a hidden zero-size SVG and clones each provided `<symbol>` with the stable package id.
69
+ - `Glyph` is `aria-hidden` by default when no accessible label or labelled-by relationship is provided.
70
+ - Labeled glyphs keep their accessible label and do not force `aria-hidden`.
71
+ - Host SVG props such as `viewBox`, `width`, `mix`, and `aria-label` are preserved.
72
+ - `createGlyphSheet` throws if a provided glyph value is not a `<symbol>` element.
@@ -0,0 +1,115 @@
1
+ # listbox
2
+
3
+ `listbox` is a headless option-list primitive for controlled selection and highlighting. Use it under components like `select` and `combobox`, or directly when you need custom listbox markup.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import type { Handle } from 'remix/ui'
9
+ import { Glyph } from 'remix/ui/glyph'
10
+ import * as listbox from 'remix/ui/listbox'
11
+ import type { ListboxValue } from 'remix/ui/listbox'
12
+
13
+ function FrameworkListbox(handle: Handle) {
14
+ let value: ListboxValue = 'remix'
15
+ let activeValue: ListboxValue = 'remix'
16
+
17
+ return () => (
18
+ <listbox.Context
19
+ value={value}
20
+ activeValue={activeValue}
21
+ onSelect={(nextValue) => {
22
+ value = nextValue
23
+ void handle.update()
24
+ }}
25
+ onHighlight={(nextValue) => {
26
+ activeValue = nextValue
27
+ void handle.update()
28
+ }}
29
+ >
30
+ <div aria-label="Frameworks" tabIndex={0} mix={[listbox.listStyle, listbox.list()]}>
31
+ {frameworks.map((option) => (
32
+ <div key={option.value} mix={[listbox.optionStyle, listbox.option(option)]}>
33
+ <Glyph mix={listbox.glyphStyle} name="check" />
34
+ <span mix={listbox.labelStyle}>{option.label}</span>
35
+ </div>
36
+ ))}
37
+ </div>
38
+ </listbox.Context>
39
+ )
40
+ }
41
+
42
+ let frameworks = [
43
+ { label: 'Remix', value: 'remix' },
44
+ { disabled: true, label: 'React Router', value: 'react-router' },
45
+ { label: 'React', value: 'react' },
46
+ { label: 'Preact', value: 'preact' },
47
+ ]
48
+ ```
49
+
50
+ Use `textValue` when the visible label is not the best string for typeahead search.
51
+
52
+ ```tsx
53
+ <div
54
+ mix={[
55
+ listbox.option({
56
+ label: 'Staging',
57
+ textValue: 'beta',
58
+ value: 'staging',
59
+ }),
60
+ ]}
61
+ >
62
+ Staging
63
+ </div>
64
+ ```
65
+
66
+ Use `ref` when a parent component needs imperative coordination with the current option registry.
67
+
68
+ ```tsx
69
+ import type { ListboxRef } from 'remix/ui/listbox'
70
+
71
+ let listboxRef: ListboxRef | undefined
72
+
73
+ function selectLastOption() {
74
+ listboxRef?.navigateLast()
75
+ void listboxRef?.selectActive()
76
+ }
77
+
78
+ ;<listbox.Context
79
+ value={value}
80
+ activeValue={activeValue}
81
+ ref={(ref) => {
82
+ listboxRef = ref
83
+ }}
84
+ onSelect={(nextValue) => {
85
+ value = nextValue
86
+ }}
87
+ onHighlight={(nextActiveValue) => {
88
+ activeValue = nextActiveValue
89
+ }}
90
+ >
91
+ {/* listbox markup */}
92
+ </listbox.Context>
93
+ ```
94
+
95
+ ## `listbox.*`
96
+
97
+ - `listbox.Context`: provider for controlled `value` and `activeValue`, option registration, selection, highlighting, optional ref access, `flashSelection`, `selectionFlashAttribute`, and `onSelectSettled`.
98
+ - `listbox.list()`: mixin that wires `role="listbox"`, default `tabIndex={-1}`, keyboard navigation, focus scrolling, and typeahead highlighting.
99
+ - `listbox.option(options)`: mixin that registers an option with required `label` and `value`, optional `disabled` and `textValue`, and wires `role="option"`, id, selected, disabled, highlighted, mouse, and click behavior.
100
+ - `listStyle`, `optionStyle`, `glyphStyle`, and `labelStyle`: flat style mixins for standard listbox presentation.
101
+ - `ListboxValue`: selected or active value, represented as `string | null`.
102
+ - `ListboxOption`: option input shape with `label`, `value`, optional `disabled`, and optional `textValue`.
103
+ - `ListboxRegisteredOption`: registered option metadata passed to callbacks and refs.
104
+ - `ListboxRef`: live ref object exposing active/selected options, option navigation, search matching, scrolling, and selection helpers.
105
+
106
+ ## Behavior Notes
107
+
108
+ - Selection and highlighting are controlled. `onSelect` and `onHighlight` notify the parent, but DOM state updates after the parent rerenders with new values.
109
+ - Disabled options are skipped by keyboard navigation, typeahead, mouse movement, and click selection.
110
+ - Arrow keys wrap through enabled options. `Home` and `End` move to enabled boundaries. `Enter` and Space select the active option.
111
+ - Mouse movement highlights enabled options. `mouseleave` clears the highlight when leaving the active option.
112
+ - `Tab` is prevented and highlights the first enabled option.
113
+ - Typeahead highlights the next matching enabled option without selecting it and supports `textValue`.
114
+ - Focus and keyboard navigation scroll the active option into view with nearest-edge alignment.
115
+ - `flashSelection` applies `selectionFlashAttribute` for 60ms, delays `onSelectSettled`, and ignores new highlight/select interactions until the flash completes.
@@ -0,0 +1,96 @@
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
+ ## Usage
6
+
7
+ ```tsx
8
+ import type { Handle } from 'remix/ui'
9
+ import { Menu, MenuItem, Submenu, onMenuSelect } from 'remix/ui/menu'
10
+ import { separatorStyle } from 'remix/ui/separator'
11
+
12
+ export function ViewMenu(handle: Handle) {
13
+ let wordWrap = false
14
+ let density = 'comfortable'
15
+
16
+ return () => (
17
+ <Menu
18
+ label="View"
19
+ mix={onMenuSelect((event) => {
20
+ if (event.item.name === 'wordWrap') {
21
+ wordWrap = event.item.checked ?? false
22
+ } else if (event.item.name === 'density' && event.item.value) {
23
+ density = event.item.value
24
+ }
25
+
26
+ void handle.update()
27
+ })}
28
+ >
29
+ <MenuItem checked={wordWrap} name="wordWrap" type="checkbox">
30
+ Word wrap
31
+ </MenuItem>
32
+ <MenuItem disabled name="minimap">
33
+ Minimap
34
+ </MenuItem>
35
+ <hr mix={separatorStyle} />
36
+ <MenuItem checked={density === 'compact'} name="density" type="radio" value="compact">
37
+ Compact
38
+ </MenuItem>
39
+ <MenuItem checked={density === 'comfortable'} name="density" type="radio" value="comfortable">
40
+ Comfortable
41
+ </MenuItem>
42
+ <Submenu label="Zoom">
43
+ <MenuItem name="zoomIn" value="zoom-in">
44
+ Zoom in
45
+ </MenuItem>
46
+ <MenuItem name="zoomOut" value="zoom-out">
47
+ Zoom out
48
+ </MenuItem>
49
+ </Submenu>
50
+ </Menu>
51
+ )
52
+ }
53
+ ```
54
+
55
+ Use `label` or `searchValue` when the rendered item content is not the text that should be used for event labels or typeahead.
56
+
57
+ ```tsx
58
+ <MenuItem label="Open command palette" name="commandPalette" searchValue="palette">
59
+ Command palette
60
+ </MenuItem>
61
+ ```
62
+
63
+ Use `menuLabel` when the menu surface needs a different accessible label from the visible trigger.
64
+
65
+ ```tsx
66
+ <Menu label="..." menuLabel="Project actions">
67
+ <MenuItem name="rename">Rename project</MenuItem>
68
+ </Menu>
69
+ ```
70
+
71
+ ## `menu.*`
72
+
73
+ - `Menu`: composed trigger, popover, and list component for the common menu case.
74
+ - `MenuItem`: menu item wrapper. Supports regular, checkbox, and radio item roles through `type`, `checked`, `name`, `value`, `label`, `disabled`, and `searchValue`.
75
+ - `Submenu`: nested menu wrapper with its own trigger and child menu surface.
76
+ - `MenuList`: lower-level list wrapper for custom composition.
77
+ - `onMenuSelect(...)`: event mixin for the bubbling `MenuSelectEvent`.
78
+ - `MenuSelectEvent`: bubbling event class whose `item` describes the selected item.
79
+ - `MenuSelectItem`: selected item shape with `checked`, `id`, `label`, `name`, `type`, and `value`.
80
+ - `menu.Context`, `menu.trigger()`, `menu.popover()`, `menu.list()`, `menu.item(...)`, and `menu.submenuTrigger(...)`: lower-level composition primitives.
81
+ - `buttonStyle`, `popoverStyle`, `listStyle`, `itemStyle`, `itemSlotStyle`, `itemLabelStyle`, `itemGlyphStyle`, and `triggerGlyphStyle`: flat style mixins used by the wrappers.
82
+ - `MenuProps`, `MenuItemProps`, `MenuListProps`, `MenuProviderProps`, `MenuTriggerOptions`, `MenuItemOptions`, and `SubmenuProps`: public TypeScript props and option types.
83
+
84
+ ## Behavior Notes
85
+
86
+ - Click opens the root menu and focuses the list; clicking the trigger again closes it and restores focus.
87
+ - `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.
88
+ - Keyboard navigation skips disabled items and does not wrap past the first or last enabled item.
89
+ - `Home` and `End` move to the first and last enabled item. Enter and Space activate the highlighted item.
90
+ - Printable keys use typeahead. Typeahead matches `searchValue` when provided and otherwise uses the item label.
91
+ - Submenus open with `ArrowRight`, close with `ArrowLeft`, and are anchored to the submenu trigger with `right-start` placement.
92
+ - 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.
93
+ - Selecting a regular item flashes that item. Selecting a checkbox or radio item flashes the committed checked state.
94
+ - Selection dispatches one bubbled `MenuSelectEvent`, closes the full menu tree, and restores focus to the root trigger.
95
+ - The composed `Menu` re-dispatches selection from its trigger so handlers on the `Menu` button and shared ancestors see the event once.
96
+ - `menuLabel`, `Submenu.menuLabel`, and `Submenu.listProps` let composed menus label and customize menu surfaces separately from trigger content.
@@ -0,0 +1,122 @@
1
+ # Popover
2
+
3
+ `popover` is a low-level primitive for anchored, dismissible floating panels.
4
+
5
+ Use it for custom surfaces like filters, inspectors, and view options. Higher-level widgets like `menu`, `select`, and `combobox` should build on top of it instead of exposing raw `popover.*` mixins directly.
6
+
7
+ ## Usage
8
+
9
+ ```tsx
10
+ import { css, on, type Handle } from 'remix/ui'
11
+ import { Button } from 'remix/ui/button'
12
+ import { Glyph } from 'remix/ui/glyph'
13
+ import { popover } from 'remix/ui/popover'
14
+
15
+ export function ViewOptions(handle: Handle) {
16
+ let open = false
17
+
18
+ function openPopover() {
19
+ open = true
20
+ void handle.update()
21
+ }
22
+
23
+ function closePopover() {
24
+ open = false
25
+ void handle.update()
26
+ }
27
+
28
+ return () => (
29
+ <popover.Context>
30
+ <Button
31
+ endIcon={<Glyph name="chevronDown" />}
32
+ mix={[
33
+ popover.anchor({ placement: 'bottom-end' }),
34
+ popover.focusOnHide(),
35
+ on('click', openPopover),
36
+ ]}
37
+ tone="secondary"
38
+ >
39
+ View options
40
+ </Button>
41
+
42
+ <div
43
+ mix={[
44
+ popover.surfaceStyle,
45
+ popover.surface({
46
+ open,
47
+ onHide() {
48
+ closePopover()
49
+ },
50
+ }),
51
+ ]}
52
+ >
53
+ <div mix={popover.contentStyle}>
54
+ <Button mix={[popover.focusOnShow(), on('click', closePopover)]} tone="ghost">
55
+ Close
56
+ </Button>
57
+ <div mix={panelBody}>Panel content</div>
58
+ </div>
59
+ </div>
60
+ </popover.Context>
61
+ )
62
+ }
63
+
64
+ let panelBody = css({
65
+ padding: '12px',
66
+ })
67
+ ```
68
+
69
+ ## `popover.*`
70
+
71
+ ### `popover.Context`
72
+
73
+ Provides shared coordination for one popover instance. Render the anchor, any focus targets, and the surface inside the same context.
74
+
75
+ ### `popover.anchor(options)`
76
+
77
+ Registers the host element as the anchor for the current popover surface.
78
+
79
+ - Accepts standard `AnchorOptions`.
80
+ - The stored anchor controls where the surface is positioned when it opens.
81
+ - Apply it to the button or other element the surface should attach to.
82
+
83
+ ### `popover.surface({ open, onHide, ... })`
84
+
85
+ Turns the host into the controlled popover surface.
86
+
87
+ - Wires `popover="manual"` and native `showPopover()` / `hidePopover()` behavior.
88
+ - Calls `onHide` for `Escape` and outside clicks with a `PopoverHideRequest`.
89
+ - Restores focus to the registered hide target unless `restoreFocusOnHide: false`.
90
+ - Accepts `closeOnAnchorClick: false` when the anchor must stay interactive while open.
91
+
92
+ Apply this to the actual floating root, not a nested child.
93
+
94
+ ### `popover.focusOnShow()`
95
+
96
+ Registers the element that should receive focus when the popover opens.
97
+
98
+ ### `popover.focusOnHide()`
99
+
100
+ Registers the element that should receive focus again when the popover closes.
101
+
102
+ ### `popover.surfaceStyle`
103
+
104
+ Default floating-surface presentation for popovers.
105
+
106
+ ### `popover.contentStyle`
107
+
108
+ Default inner scroll container for popover content.
109
+
110
+ ## Behavior Notes
111
+
112
+ - Opening anchors the surface to the registered anchor and locks page scrolling until close.
113
+ - `onHide` receives `{ reason: 'escape-key' | 'outside-click', target? }`.
114
+ - `popover.focusOnShow()` wins on open when present.
115
+ - `popover.focusOnHide()` is used on close by default when focus restoration is enabled.
116
+ - `closeOnAnchorClick: false` keeps anchor clicks inside the current session, which is useful for input-driven popovers like comboboxes.
117
+
118
+ ## When To Use Something Else
119
+
120
+ - Use `menu` for command surfaces.
121
+ - Use `listbox` or `select` for committed value picking.
122
+ - Use `combobox` for input-first popup selection.
@@ -0,0 +1,33 @@
1
+ # scroll-lock
2
+
3
+ `scroll-lock` locks document scrolling while a floating or modal surface is open. Use `lockScroll` directly for imperative flows or `lockScrollOnToggle` for popover-style elements that emit `beforetoggle`.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { lockScroll, lockScrollOnToggle } from 'remix/ui/scroll-lock'
9
+
10
+ function DialogSurface() {
11
+ return <div popover="auto" mix={lockScrollOnToggle()} />
12
+ }
13
+
14
+ function openModal() {
15
+ let unlock = lockScroll()
16
+
17
+ // Later, when the modal closes:
18
+ unlock()
19
+ }
20
+ ```
21
+
22
+ ## API
23
+
24
+ - `lockScroll(document?)`: locks the target document and returns an idempotent unlock function.
25
+ - `lockScrollOnToggle()`: mixin that locks on `beforetoggle` open, unlocks on close, and releases the lock when the host unmounts.
26
+
27
+ ## Behavior Notes
28
+
29
+ - The lock stores and restores the document element's inline `overflow` and `scrollbarGutter`.
30
+ - Scroll position is restored when the last active lock is released.
31
+ - Multiple locks are reference-counted, so the document stays locked until every unlock function has run.
32
+ - When a scrollbar is present and computed `scrollbar-gutter` is `auto`, the document reserves a stable gutter while locked.
33
+ - `lockScrollOnToggle` uses the host element's owner document.