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,166 @@
1
+ # accordion
2
+
3
+ `Accordion` renders a disclosure set with one or more expandable items. Use it for grouped settings, FAQ sections, and dense panels where each item owns a trigger and content region.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'remix/ui/accordion'
9
+
10
+ export function SettingsAccordion() {
11
+ return (
12
+ <Accordion defaultValue="account">
13
+ <AccordionItem value="account">
14
+ <AccordionTrigger>Account</AccordionTrigger>
15
+ <AccordionContent>Manage account preferences.</AccordionContent>
16
+ </AccordionItem>
17
+
18
+ <AccordionItem value="billing">
19
+ <AccordionTrigger>Billing</AccordionTrigger>
20
+ <AccordionContent>Review billing details.</AccordionContent>
21
+ </AccordionItem>
22
+ </Accordion>
23
+ )
24
+ }
25
+ ```
26
+
27
+ Use `type="multiple"` when more than one panel may stay open. `defaultValue` and `value` are arrays in multiple mode.
28
+
29
+ ```tsx
30
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'remix/ui/accordion'
31
+
32
+ export function StatusAccordion() {
33
+ return (
34
+ <Accordion defaultValue={['api', 'alerts']} type="multiple">
35
+ <AccordionItem value="api">
36
+ <AccordionTrigger>API status checks</AccordionTrigger>
37
+ <AccordionContent>Review uptime checks and response time alerts.</AccordionContent>
38
+ </AccordionItem>
39
+
40
+ <AccordionItem disabled value="access">
41
+ <AccordionTrigger>Access control sync</AccordionTrigger>
42
+ <AccordionContent>This disabled item cannot be opened or focused.</AccordionContent>
43
+ </AccordionItem>
44
+
45
+ <AccordionItem value="alerts">
46
+ <AccordionTrigger>Alert routing</AccordionTrigger>
47
+ <AccordionContent>Confirm escalation rules and notification channels.</AccordionContent>
48
+ </AccordionItem>
49
+ </Accordion>
50
+ )
51
+ }
52
+ ```
53
+
54
+ Control the open value when state should live in the owning component. Single mode uses `string | null`; multiple mode uses `string[]`.
55
+
56
+ ```tsx
57
+ import type { Handle } from 'remix/ui'
58
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'remix/ui/accordion'
59
+
60
+ export function ControlledAccordion(handle: Handle) {
61
+ let value: string | null = 'account'
62
+
63
+ return () => (
64
+ <Accordion
65
+ value={value}
66
+ onValueChange={(nextValue) => {
67
+ value = nextValue
68
+ void handle.update()
69
+ }}
70
+ >
71
+ <AccordionItem value="account">
72
+ <AccordionTrigger>Account</AccordionTrigger>
73
+ <AccordionContent>Manage account preferences.</AccordionContent>
74
+ </AccordionItem>
75
+
76
+ <AccordionItem value="billing">
77
+ <AccordionTrigger>Billing</AccordionTrigger>
78
+ <AccordionContent>Review billing details.</AccordionContent>
79
+ </AccordionItem>
80
+ </Accordion>
81
+ )
82
+ }
83
+ ```
84
+
85
+ Listen for bubbling `AccordionChangeEvent` events with `onAccordionChange`.
86
+
87
+ ```tsx
88
+ import {
89
+ Accordion,
90
+ AccordionContent,
91
+ AccordionItem,
92
+ AccordionTrigger,
93
+ onAccordionChange,
94
+ } from 'remix/ui/accordion'
95
+
96
+ export function TrackedAccordion() {
97
+ return (
98
+ <div
99
+ mix={[
100
+ onAccordionChange((event) => {
101
+ console.log(event.accordionType, event.itemValue, event.value)
102
+ }),
103
+ ]}
104
+ >
105
+ <Accordion>
106
+ <AccordionItem value="account">
107
+ <AccordionTrigger>Account</AccordionTrigger>
108
+ <AccordionContent>Manage account preferences.</AccordionContent>
109
+ </AccordionItem>
110
+ </Accordion>
111
+ </div>
112
+ )
113
+ }
114
+ ```
115
+
116
+ Set `collapsible={false}` in single mode when the open item must stay open. The locked-open trigger receives `aria-disabled`.
117
+
118
+ ```tsx
119
+ <Accordion collapsible={false} defaultValue="account">
120
+ <AccordionItem value="account">
121
+ <AccordionTrigger>Account</AccordionTrigger>
122
+ <AccordionContent>Manage account preferences.</AccordionContent>
123
+ </AccordionItem>
124
+ </Accordion>
125
+ ```
126
+
127
+ Use `headingLevel` to choose the heading wrapper rendered around each trigger. The default level is `3`.
128
+
129
+ ```tsx
130
+ <Accordion defaultValue="shipping" headingLevel={2}>
131
+ <AccordionItem value="shipping">
132
+ <AccordionTrigger>Shipping</AccordionTrigger>
133
+ <AccordionContent>Review shipping preferences.</AccordionContent>
134
+ </AccordionItem>
135
+ </Accordion>
136
+ ```
137
+
138
+ Pass `indicator={null}` to remove the default chevron, or pass a custom node to replace it.
139
+
140
+ ```tsx
141
+ <AccordionTrigger indicator={null}>No indicator</AccordionTrigger>
142
+ <AccordionTrigger indicator={<span aria-hidden>+</span>}>Custom indicator</AccordionTrigger>
143
+ ```
144
+
145
+ ## `accordion.*`
146
+
147
+ - `Accordion`: root component. Defaults to single-item mode and supports controlled `value`, uncontrolled `defaultValue`, `onValueChange`, `disabled`, `headingLevel`, `collapsible`, and `type="multiple"`.
148
+ - `AccordionItem`: registers one accordion item by `value`. Pass `disabled` to prevent that item from opening or receiving keyboard focus.
149
+ - `AccordionTrigger`: heading-wrapped button for an item. It wires `aria-expanded`, `aria-controls`, keyboard navigation, and the default chevron indicator.
150
+ - `AccordionContent`: panel for an item. It wires the panel id, `aria-labelledby`, `aria-hidden`, inert state, and open/closed state attributes.
151
+ - `onAccordionChange(...)`: event mixin for the bubbling `AccordionChangeEvent`.
152
+ - `AccordionChangeEvent`: bubbling event class with `value`, `itemValue`, and `accordionType`.
153
+ - `AccordionProps`, `AccordionSingleProps`, `AccordionMultipleProps`, `AccordionItemProps`, `AccordionTriggerProps`, and `AccordionContentProps`: public TypeScript props.
154
+ - `rootStyle`, `itemStyle`, `triggerStyle`, `indicatorStyle`, `panelStyle`, and `bodyStyle`: flat style mixins used by the component wrappers.
155
+
156
+ ## Behavior Notes
157
+
158
+ - Single mode stores one open value or `null`; multiple mode stores an array of open values.
159
+ - Single accordions are collapsible by default. Set `collapsible={false}` to keep the open item locked open.
160
+ - Root `disabled` disables every item. Item `disabled` only disables that item.
161
+ - Arrow keys move between enabled triggers. `Home` and `End` move to the first and last enabled triggers.
162
+ - Disabled items are skipped by keyboard navigation.
163
+ - Trigger and panel ids are generated and linked with `aria-controls`, `aria-labelledby`, and `aria-expanded`; closed panels receive `aria-hidden` and `inert`.
164
+ - `AccordionTrigger` renders inside an `h1`-`h6` element based on `headingLevel`.
165
+ - Each item and trigger receives `data-state="open"` or `data-state="closed"` for styling.
166
+ - `AccordionChangeEvent` bubbles from the root and includes `value`, `itemValue`, and `accordionType`.
@@ -0,0 +1,153 @@
1
+ # anchor
2
+
3
+ `anchor` positions a floating element against an anchor element and keeps it constrained to the viewport. Use it for custom floating surfaces that need placement, flipping, offsets, and optional relative alignment.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { anchor } from 'remix/ui/anchor'
9
+
10
+ let trigger = document.querySelector<HTMLButtonElement>('[data-trigger]')
11
+ let panel = document.querySelector<HTMLElement>('[data-panel]')
12
+
13
+ if (trigger && panel) {
14
+ let cleanup = anchor(panel, trigger, {
15
+ placement: 'bottom-end',
16
+ offset: 8,
17
+ })
18
+
19
+ // Later, when the surface closes or unmounts:
20
+ cleanup()
21
+ }
22
+ ```
23
+
24
+ Use the returned cleanup function with the lifecycle that owns the floating element. For native popovers, position on open and clean up on close.
25
+
26
+ ```tsx
27
+ import { anchor } from 'remix/ui/anchor'
28
+
29
+ let cleanupAnchor = () => {}
30
+
31
+ button.addEventListener('click', () => {
32
+ popover.showPopover()
33
+ })
34
+
35
+ popover.addEventListener('beforetoggle', (event) => {
36
+ let toggleEvent = event as ToggleEvent
37
+
38
+ if (toggleEvent.newState === 'open') {
39
+ cleanupAnchor()
40
+ cleanupAnchor = anchor(popover, button, {
41
+ placement: 'bottom-start',
42
+ offset: 4,
43
+ })
44
+ return
45
+ }
46
+
47
+ cleanupAnchor()
48
+ cleanupAnchor = () => {}
49
+ })
50
+ ```
51
+
52
+ ## `anchor.*`
53
+
54
+ - `anchor(floatingElement, anchorElement, options)`: positions `floatingElement` against `anchorElement`, starts animation-frame polling for geometry changes, and returns a cleanup function.
55
+ - `AnchorOptions`: placement, inset, relative alignment, and offset options.
56
+ - `AnchorPlacement`: exported placement names for the main sides and top/bottom start/end alignment.
57
+
58
+ ## Placements
59
+
60
+ Default placement is `bottom`. Use start/end variants to align an edge instead of centering the floating element on the anchor.
61
+
62
+ ```tsx
63
+ anchor(panel, trigger, { placement: 'bottom' })
64
+ anchor(panel, trigger, { placement: 'bottom-start' })
65
+ anchor(panel, trigger, { placement: 'bottom-end' })
66
+ anchor(panel, trigger, { placement: 'top' })
67
+ anchor(panel, trigger, { placement: 'left' })
68
+ anchor(panel, trigger, { placement: 'right' })
69
+ ```
70
+
71
+ The positioning logic can also handle left/right start/end placements through `AnchorOptions['placement']`.
72
+
73
+ ```tsx
74
+ anchor(panel, trigger, { placement: 'right-start' })
75
+ anchor(panel, trigger, { placement: 'left-end' })
76
+ ```
77
+
78
+ When the requested placement would overflow the viewport, `anchor` flips to the opposite side and writes the final placement to `data-anchor-placement`.
79
+
80
+ ```tsx
81
+ let cleanup = anchor(panel, trigger, {
82
+ placement: 'bottom-start',
83
+ })
84
+
85
+ panel.dataset.anchorPlacement
86
+ ```
87
+
88
+ ## Offsets
89
+
90
+ Use `offset` for distance along the placement axis. Use `offsetX` and `offsetY` for independent adjustment after placement is resolved.
91
+
92
+ ```tsx
93
+ anchor(panel, trigger, {
94
+ placement: 'bottom-start',
95
+ offset: 8,
96
+ offsetX: 4,
97
+ offsetY: -2,
98
+ })
99
+ ```
100
+
101
+ Offsets may be numbers or functions that receive the floating element.
102
+
103
+ ```tsx
104
+ anchor(panel, trigger, {
105
+ placement: 'bottom-start',
106
+ offset: (floating) => floating.offsetHeight / 10,
107
+ offsetX: (floating) => floating.offsetWidth / 20,
108
+ })
109
+ ```
110
+
111
+ ## Inset Positioning
112
+
113
+ Pass `inset: true` to align the floating element inside the anchor edge instead of outside it. This is useful for surfaces that should visually cover or line up with the trigger.
114
+
115
+ ```tsx
116
+ anchor(panel, trigger, {
117
+ placement: 'bottom-start',
118
+ inset: true,
119
+ })
120
+ ```
121
+
122
+ ## Relative Alignment
123
+
124
+ Use `relativeTo` when a child inside the floating element should align to the anchor instead of the floating element's outer box. The value is a selector scoped to the floating element.
125
+
126
+ ```tsx
127
+ anchor(listbox, trigger, {
128
+ placement: 'bottom-start',
129
+ relativeTo: '[role="option"][aria-selected="true"]',
130
+ })
131
+ ```
132
+
133
+ Combine `relativeTo` with `inset` for selected-option popovers where the selected option should sit over the trigger.
134
+
135
+ ```tsx
136
+ anchor(listbox, trigger, {
137
+ placement: 'left',
138
+ inset: true,
139
+ relativeTo: '[aria-selected="true"]',
140
+ })
141
+ ```
142
+
143
+ ## Behavior Notes
144
+
145
+ - Default placement is below the anchor.
146
+ - Supported placements include top, bottom, left, right, top/bottom start/end variants, and left/right start/end placements through `AnchorOptions['placement']`.
147
+ - The floating element flips when the requested placement would overflow the viewport and records the final placement in `data-anchor-placement`.
148
+ - Oversized floating elements are constrained with max dimensions and remain inside the viewport padding.
149
+ - Oversized inset surfaces with `relativeTo` preserve alignment by scrolling the nearest scrollable descendant when possible.
150
+ - `offset`, `offsetX`, and `offsetY` may be numbers or functions that receive the floating element.
151
+ - `relativeTo` lets a surface align to an inner element, which is useful for selected options inside popovers.
152
+ - `anchor` polls on animation frames for anchor or floating geometry changes and repositions when either changes.
153
+ - The returned cleanup function cancels animation-frame polling.
@@ -0,0 +1,316 @@
1
+ # animation
2
+
3
+ `animation` provides small primitives for entrance, exit, layout, spring, and tween animation. Use these helpers with Remix UI mixins, CSS transitions, the Web Animations API, and imperative `requestAnimationFrame` loops.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { animateEntrance, animateExit, animateLayout, spring } from 'remix/ui/animation'
9
+
10
+ let panelTransition = spring.transition(['opacity', 'transform'], 'snappy')
11
+
12
+ function Panel() {
13
+ return () => (
14
+ <div
15
+ style={{ transition: panelTransition }}
16
+ mix={[
17
+ animateEntrance({ opacity: 0, duration: 120 }),
18
+ animateExit({ opacity: 0, duration: 120 }),
19
+ animateLayout(),
20
+ ]}
21
+ >
22
+ Saved filters
23
+ </div>
24
+ )
25
+ }
26
+ ```
27
+
28
+ ## Entrance And Exit
29
+
30
+ `animateEntrance` animates an element from the provided keyframe into its natural styles when the element is inserted.
31
+
32
+ ```tsx
33
+ import { animateEntrance, spring } from 'remix/ui/animation'
34
+
35
+ function Toast() {
36
+ return () => (
37
+ <div
38
+ mix={[
39
+ animateEntrance({
40
+ opacity: 0,
41
+ transform: 'translateY(8px)',
42
+ ...spring('snappy'),
43
+ }),
44
+ ]}
45
+ >
46
+ Saved
47
+ </div>
48
+ )
49
+ }
50
+ ```
51
+
52
+ `animateExit` keeps a removed keyed element in the DOM long enough to animate from its natural styles to the provided keyframe.
53
+
54
+ ```tsx
55
+ import type { Handle } from 'remix/ui'
56
+ import { animateExit } from 'remix/ui/animation'
57
+
58
+ function Item(handle: Handle<{ id: string; label: string }>) {
59
+ return () => (
60
+ <li
61
+ key={handle.props.id}
62
+ mix={[
63
+ animateExit({
64
+ opacity: 0,
65
+ transform: 'scale(0.96)',
66
+ duration: 120,
67
+ easing: 'ease-in',
68
+ }),
69
+ ]}
70
+ >
71
+ {handle.props.label}
72
+ </li>
73
+ )
74
+ }
75
+ ```
76
+
77
+ Passing `true` uses the default opacity animation. Passing `false`, `null`, or `undefined` disables the animation.
78
+
79
+ ```tsx
80
+ <div mix={[animateEntrance(true), animateExit(false)]} />
81
+ ```
82
+
83
+ Animation configs combine WAAPI timing options with style properties for the animated keyframe.
84
+
85
+ ```ts
86
+ type AnimateMixinConfig = {
87
+ duration: number
88
+ easing?: string
89
+ delay?: number
90
+ composite?: CompositeOperation
91
+ initial?: boolean
92
+ [property: string]: unknown
93
+ }
94
+ ```
95
+
96
+ Pass `initial: false` to skip only the first keyed entrance for an element within a parent. Later insertions for the same key can still animate.
97
+
98
+ ```tsx
99
+ <div key={id} mix={[animateEntrance({ opacity: 0, duration: 150, initial: false })]} />
100
+ ```
101
+
102
+ Exit animations can reclaim a removed keyed node if the same keyed element is rendered again before the exit finishes. The reclaimed node retargets toward its natural styles instead of simply reversing the exit animation.
103
+
104
+ ## Layout Animation
105
+
106
+ `animateLayout` animates layout changes with a FLIP-style transform projection. Use it on elements whose position or size can change between renders.
107
+
108
+ ```tsx
109
+ import type { Handle } from 'remix/ui'
110
+ import { animateLayout, spring } from 'remix/ui/animation'
111
+
112
+ function Card(handle: Handle<{ expanded: boolean }>) {
113
+ return () => (
114
+ <section
115
+ class={handle.props.expanded ? 'card expanded' : 'card'}
116
+ mix={[
117
+ animateLayout({
118
+ ...spring('smooth'),
119
+ }),
120
+ ]}
121
+ >
122
+ Details
123
+ </section>
124
+ )
125
+ }
126
+ ```
127
+
128
+ Pass `size: false` when the element should animate position only and avoid scale projection.
129
+
130
+ ```tsx
131
+ <div mix={[animateLayout({ duration: 180, easing: 'ease-out', size: false })]} />
132
+ ```
133
+
134
+ Passing `true` or no argument enables the default layout animation. Passing `false`, `null`, or `undefined` disables it. Layout animation skips work when geometry does not change, keeps in-flight animations running when their target geometry has not changed, and continues from the current visual transform when a new layout change interrupts an active animation.
135
+
136
+ ## Spring
137
+
138
+ `spring` returns a decorated iterator. It can be iterated for JavaScript animation, spread into Web Animations API options, or stringified for CSS transition syntax.
139
+
140
+ ```tsx
141
+ import { spring } from 'remix/ui/animation'
142
+
143
+ spring('bouncy')
144
+ spring('snappy')
145
+ spring('smooth')
146
+ spring({ duration: 400, bounce: 0.3 })
147
+ ```
148
+
149
+ ```ts
150
+ interface SpringIterator extends IterableIterator<number> {
151
+ duration: number
152
+ easing: string
153
+ toString(): string
154
+ }
155
+ ```
156
+
157
+ Use `spring.transition` to build CSS transition entries.
158
+
159
+ ```tsx
160
+ let transition = spring.transition(['opacity', 'transform'], 'bouncy')
161
+
162
+ function Button() {
163
+ return () => <button style={{ transition }}>Save</button>
164
+ }
165
+ ```
166
+
167
+ Spread a spring into animation mixin or WAAPI options.
168
+
169
+ ```tsx
170
+ <div
171
+ mix={[
172
+ animateEntrance({
173
+ opacity: 0,
174
+ transform: 'scale(0.92)',
175
+ ...spring('bouncy'),
176
+ }),
177
+ ]}
178
+ />
179
+ ```
180
+
181
+ ```ts
182
+ element.animate(
183
+ [
184
+ { opacity: 0, transform: 'scale(0.92)' },
185
+ { opacity: 1, transform: 'scale(1)' },
186
+ ],
187
+ { ...spring('snappy') },
188
+ )
189
+ ```
190
+
191
+ Use the iterator values as progress from `0` to `1` for imperative animation.
192
+
193
+ ```ts
194
+ let from = 0
195
+ let to = 200
196
+
197
+ for (let progress of spring('bouncy')) {
198
+ let x = from + (to - from) * progress
199
+ element.style.transform = `translateX(${x}px)`
200
+ await nextFrame()
201
+ }
202
+ ```
203
+
204
+ The built-in presets are:
205
+
206
+ | Preset | Bounce | Duration | Character |
207
+ | -------- | ------ | -------- | ------------------------ |
208
+ | `smooth` | -0.3 | 400ms | Overdamped, no overshoot |
209
+ | `snappy` | 0 | 200ms | Quick, no overshoot |
210
+ | `bouncy` | 0.3 | 400ms | Underdamped bounce |
211
+
212
+ Override preset duration or velocity with the second argument.
213
+
214
+ ```ts
215
+ spring('bouncy', { duration: 300 })
216
+ spring('snappy', { velocity: 2 })
217
+ ```
218
+
219
+ Use explicit options when you need full control.
220
+
221
+ ```ts
222
+ spring({
223
+ duration: 500,
224
+ bounce: 0.35,
225
+ velocity: 0,
226
+ })
227
+ ```
228
+
229
+ ## Tween
230
+
231
+ `tween` creates a generator that interpolates a numeric value over time with a cubic-bezier curve. Call `next()` once to initialize the generator, then pass `requestAnimationFrame` timestamps into `next(timestamp)`.
232
+
233
+ ```ts
234
+ import { easings, tween } from 'remix/ui/animation'
235
+
236
+ let animation = tween({
237
+ from: 0,
238
+ to: 100,
239
+ duration: 300,
240
+ curve: easings.easeOut,
241
+ })
242
+
243
+ animation.next()
244
+
245
+ function tick(timestamp: number) {
246
+ let { value, done } = animation.next(timestamp)
247
+ element.style.transform = `translateX(${value}px)`
248
+ if (!done) requestAnimationFrame(tick)
249
+ }
250
+
251
+ requestAnimationFrame(tick)
252
+ ```
253
+
254
+ Animate multiple values with separate tweens.
255
+
256
+ ```ts
257
+ let xAnimation = tween({ from: 0, to: 100, duration: 500, curve: easings.easeOut })
258
+ let scaleAnimation = tween({ from: 1, to: 1.2, duration: 500, curve: easings.easeOut })
259
+
260
+ xAnimation.next()
261
+ scaleAnimation.next()
262
+
263
+ function tick(timestamp: number) {
264
+ let x = xAnimation.next(timestamp)
265
+ let scale = scaleAnimation.next(timestamp)
266
+
267
+ element.style.transform = `translateX(${x.value}px) scale(${scale.value})`
268
+
269
+ if (!x.done || !scale.done) {
270
+ requestAnimationFrame(tick)
271
+ }
272
+ }
273
+ ```
274
+
275
+ The built-in easing presets are cubic-bezier control points matching common CSS timing functions.
276
+
277
+ ```ts
278
+ easings.linear
279
+ easings.ease
280
+ easings.easeIn
281
+ easings.easeOut
282
+ easings.easeInOut
283
+ ```
284
+
285
+ Custom curves use the same control points as CSS `cubic-bezier(x1, y1, x2, y2)`.
286
+
287
+ ```ts
288
+ let animation = tween({
289
+ from: 0,
290
+ to: 100,
291
+ duration: 500,
292
+ curve: { x1: 0.68, y1: -0.55, x2: 0.265, y2: 1.55 },
293
+ })
294
+ ```
295
+
296
+ ## API
297
+
298
+ - `animateEntrance(config?)`: mixin that animates an element when it enters the DOM.
299
+ - `animateExit(config?)`: mixin that persists a removed keyed element long enough to run its exit animation.
300
+ - `animateLayout(config?)`: mixin that animates layout changes by comparing geometry between renders.
301
+ - `spring(preset?, overrides?)`: creates a `SpringIterator` from a named preset.
302
+ - `spring(options?)`: creates a `SpringIterator` from explicit spring options.
303
+ - `spring.transition(property, presetOrOptions?, overrides?)`: builds one or more CSS transition entries from a spring.
304
+ - `spring.presets`: named `smooth`, `snappy`, and `bouncy` spring defaults.
305
+ - `tween(options)`: generator that interpolates numeric values over time with a cubic-bezier curve.
306
+ - `easings`: common cubic-bezier presets for `tween`.
307
+ - `SpringIterator`, `SpringPreset`, `SpringOptions`, `TweenOptions`, and `BezierCurve`: public TypeScript types for spring and tween configuration.
308
+
309
+ ## Behavior Notes
310
+
311
+ - Animation mixin style properties are copied into WAAPI keyframes; `duration`, `easing`, `delay`, `composite`, and `initial` are treated as options.
312
+ - `animateEntrance({ initial: false })` only skips the first keyed entrance tracked for the parent node.
313
+ - `animateExit` needs keyed elements when removed nodes may be reclaimed or persisted across list updates.
314
+ - `animateLayout({ size: false })` animates translation without scale projection.
315
+ - `spring()` yields progress values from `0` to `1`; its `duration` and `easing` properties are enumerable so `{ ...spring() }` works with WAAPI options.
316
+ - `tween(...)` yields the initial value first; advance the generator with frame timestamps via `next(timestamp)` and read `done` to detect completion.
@@ -0,0 +1,55 @@
1
+ # breadcrumbs
2
+
3
+ `Breadcrumbs` renders semantic breadcrumb navigation from a list of items. Use it when the page needs a compact path back through parent sections.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { Breadcrumbs } from 'remix/ui/breadcrumbs'
9
+
10
+ export function ProjectBreadcrumbs() {
11
+ return (
12
+ <Breadcrumbs
13
+ items={[
14
+ { href: '/', label: 'Home' },
15
+ { href: '/projects', label: 'Projects' },
16
+ { label: 'Roadmap' },
17
+ ]}
18
+ />
19
+ )
20
+ }
21
+ ```
22
+
23
+ Mark an earlier item as current when the page belongs to a parent section but the final crumb is still useful context. The current item always renders as text with `aria-current="page"`.
24
+
25
+ ```tsx
26
+ <Breadcrumbs
27
+ items={[
28
+ { href: '/', label: 'Home' },
29
+ { current: true, href: '/components', label: 'Components' },
30
+ { label: 'Breadcrumbs' },
31
+ ]}
32
+ />
33
+ ```
34
+
35
+ Pass `separator` to replace the default chevron glyph.
36
+
37
+ ```tsx
38
+ <Breadcrumbs items={[{ href: '/', label: 'Home' }, { label: 'Breadcrumbs' }]} separator="/" />
39
+ ```
40
+
41
+ ## `breadcrumbs.*`
42
+
43
+ - `Breadcrumbs`: component that renders a `<nav>` with an ordered list of breadcrumb items.
44
+ - `BreadcrumbItem`: item shape with `label`, optional `href`, and optional `current`.
45
+ - `BreadcrumbsProps`: nav props plus required `items` and optional `separator`.
46
+
47
+ ## Behavior Notes
48
+
49
+ - The default `aria-label` is `"Breadcrumb"` unless an `aria-label` prop is provided.
50
+ - Breadcrumb items render inside an ordered list.
51
+ - The last item is treated as current when no item has `current: true`.
52
+ - An explicit current item wins over the last-item default.
53
+ - Current items render as text with `aria-current="page"`, even when they include `href`.
54
+ - Non-current items with `href` render as links; non-current items without `href` render as text.
55
+ - The default separator is the `chevronRight` glyph. Separators render between items and are hidden from assistive technology.