aeon-ui-engine 1.3.5 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/CONSUMER.md +67 -27
- package/INTEGRATION.md +1 -1
- package/README.md +17 -8
- package/package.json +4 -1
- package/packages/cli/README.md +13 -1
- package/packages/cli/dist/bin.js +50 -6
- package/packages/cli/dist/chunk-JMNRJCWF.js +301 -0
- package/packages/cli/dist/index.d.ts +31 -1
- package/packages/cli/dist/index.js +7 -3
- package/packages/cli/src/bin.ts +50 -4
- package/packages/cli/src/index.ts +2 -0
- package/packages/cli/src/init.ts +10 -4
- package/packages/cli/src/link.ts +61 -0
- package/packages/cli/src/migrate.ts +170 -0
- package/packages/core/dist/index.d.ts +35 -7
- package/packages/core/dist/index.js +102 -2
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/core/src/anatomy.ts +39 -3
- package/packages/core/src/identicon.test.ts +39 -0
- package/packages/core/src/identicon.ts +79 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/panda/panda.config.ts +4 -0
- package/packages/panda/src/recipes/composer.ts +58 -4
- package/packages/panda/src/recipes/file-upload.ts +100 -0
- package/packages/panda/src/recipes/index.ts +2 -0
- package/packages/panda/src/recipes/range-slider.ts +59 -0
- package/packages/panda/src/recipes/thread.ts +77 -2
- package/packages/panda/styled-system/recipes/aeon-composer.d.ts +1 -1
- package/packages/panda/styled-system/recipes/aeon-composer.mjs +12 -0
- package/packages/panda/styled-system/recipes/aeon-file-upload.d.ts +31 -0
- package/packages/panda/styled-system/recipes/aeon-file-upload.mjs +61 -0
- package/packages/panda/styled-system/recipes/aeon-range-slider.d.ts +31 -0
- package/packages/panda/styled-system/recipes/aeon-range-slider.mjs +49 -0
- package/packages/panda/styled-system/recipes/aeon-thread.d.ts +1 -1
- package/packages/panda/styled-system/recipes/aeon-thread.mjs +20 -0
- package/packages/panda/styled-system/recipes/index.d.ts +2 -0
- package/packages/panda/styled-system/recipes/index.mjs +2 -0
- package/packages/panda/styled-system/styles.css +292 -0
- package/packages/primitives/dist/chunk-CV4UXXZF.js +44 -0
- package/packages/primitives/dist/chunk-IZKQSCYU.js +164 -0
- package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
- package/packages/primitives/dist/index.d.ts +2 -0
- package/packages/primitives/dist/index.js +23 -7
- package/packages/primitives/dist/machines/file-upload.d.ts +39 -0
- package/packages/primitives/dist/machines/file-upload.js +8 -0
- package/packages/primitives/dist/machines/range-slider.d.ts +129 -0
- package/packages/primitives/dist/machines/range-slider.js +12 -0
- package/packages/primitives/dist/machines/slider.d.ts +22 -0
- package/packages/primitives/dist/machines/slider.js +1 -1
- package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/primitives/src/__tests__/file-upload.test.ts +25 -0
- package/packages/primitives/src/__tests__/range-slider.test.ts +39 -0
- package/packages/primitives/src/index.ts +2 -0
- package/packages/primitives/src/machines/file-upload.ts +55 -0
- package/packages/primitives/src/machines/range-slider.ts +209 -0
- package/packages/primitives/src/machines/slider.ts +17 -0
- package/packages/react/dist/index.d.ts +132 -26
- package/packages/react/dist/index.js +1822 -1029
- package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/react/src/button/index.tsx +50 -15
- package/packages/react/src/composer/index.tsx +58 -3
- package/packages/react/src/file-upload/index.tsx +322 -0
- package/packages/react/src/floating/FloatingPositioner.tsx +8 -0
- package/packages/react/src/hooks/use-anchor-position.ts +88 -17
- package/packages/react/src/index.ts +3 -0
- package/packages/react/src/menu/index.tsx +37 -3
- package/packages/react/src/popover/index.tsx +7 -1
- package/packages/react/src/prompt/index.tsx +45 -0
- package/packages/react/src/range-slider/index.tsx +322 -0
- package/packages/react/src/slider/index.tsx +54 -14
- package/packages/react/src/thread/index.tsx +74 -3
- package/packages/react/src/tooltip/index.tsx +7 -1
- package/packages/react/src/utils/slot.tsx +47 -0
- package/packages/schemas/dist/index.d.ts +4 -0
- package/packages/schemas/dist/index.js +67 -1
- package/packages/schemas/json/button.json +6 -0
- package/packages/schemas/json/catalog.json +333 -0
- package/packages/schemas/json/fileUpload.json +168 -0
- package/packages/schemas/json/rangeSlider.json +157 -0
- package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/schemas/prompts/cursorrules.md +3 -1
- package/packages/schemas/prompts/statecharts.md +1 -1
- package/packages/schemas/registry/catalog.json +18 -0
- package/packages/schemas/registry/components/button.json +6 -0
- package/packages/schemas/registry/components/fileUpload.json +168 -0
- package/packages/schemas/registry/components/rangeSlider.json +157 -0
- package/packages/schemas/registry/machines/fileUpload.json +182 -0
- package/packages/schemas/registry/machines/rangeSlider.json +201 -0
- package/packages/schemas/scripts/emit.ts +4 -0
- package/packages/schemas/src/components.ts +70 -0
- package/packages/schemas/src/prompts.ts +3 -1
- package/packages/schemas/tsconfig.json +1 -1
- package/packages/ui/dist/index.d.ts +100 -46
- package/packages/ui/dist/index.js +463 -341
- package/packages/ui/src/composer.tsx +37 -11
- package/packages/ui/src/file-upload.tsx +36 -0
- package/packages/ui/src/index.ts +2 -0
- package/packages/ui/src/prompt.tsx +3 -0
- package/packages/ui/src/range-slider.tsx +24 -0
- package/packages/ui/src/thread.tsx +40 -0
- package/scripts/link-scoped-packages.mjs +206 -0
- package/scripts/vite-aliases.mjs +6 -8
- package/packages/cli/dist/chunk-IW5NT2Z2.js +0 -103
|
@@ -129,11 +129,15 @@ export interface MenuPositionerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
129
129
|
/** Mount menu in document.body (default true). */
|
|
130
130
|
portalled?: boolean
|
|
131
131
|
placement?: AnchorPlacement
|
|
132
|
+
/** Gap between trigger and menu (default 4). */
|
|
133
|
+
gutter?: number
|
|
134
|
+
/** Stack level for the portalled surface (default 50). */
|
|
135
|
+
zIndex?: number
|
|
132
136
|
children?: ReactNode
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
const Positioner = forwardRef<HTMLDivElement, MenuPositionerProps>(function MenuPositioner(
|
|
136
|
-
{ portalled = true, placement = 'bottom-start', children, className, ...rest },
|
|
140
|
+
{ portalled = true, placement = 'bottom-start', gutter, zIndex, children, className, ...rest },
|
|
137
141
|
ref,
|
|
138
142
|
) {
|
|
139
143
|
const { open, triggerRef, positionerRef } = useMenuCtx()
|
|
@@ -148,6 +152,8 @@ const Positioner = forwardRef<HTMLDivElement, MenuPositionerProps>(function Menu
|
|
|
148
152
|
positionerRef={positionerRef}
|
|
149
153
|
portalled={portalled}
|
|
150
154
|
placement={placement}
|
|
155
|
+
gutter={gutter}
|
|
156
|
+
zIndex={zIndex}
|
|
151
157
|
className={className}
|
|
152
158
|
{...rest}
|
|
153
159
|
>
|
|
@@ -160,13 +166,41 @@ const Content = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(funct
|
|
|
160
166
|
{ onKeyDown, ...rest },
|
|
161
167
|
ref,
|
|
162
168
|
) {
|
|
163
|
-
const { open, send, menuId } = useMenuCtx()
|
|
169
|
+
const { open, send, menuId, triggerRef } = useMenuCtx()
|
|
170
|
+
const contentRef = useRef<HTMLDivElement | null>(null)
|
|
171
|
+
|
|
172
|
+
// Focus enters the surface while it is open and returns to the trigger on
|
|
173
|
+
// dismiss, so keyboard users stay inside the menu they opened.
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!open) return
|
|
176
|
+
const trigger = triggerRef.current
|
|
177
|
+
contentRef.current?.focus({ preventScroll: true })
|
|
178
|
+
return () => {
|
|
179
|
+
if (trigger?.isConnected) trigger.focus({ preventScroll: true })
|
|
180
|
+
}
|
|
181
|
+
}, [open, triggerRef])
|
|
182
|
+
|
|
183
|
+
// Escape closes wherever focus sits — trigger, item or elsewhere on the page.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!open) return
|
|
186
|
+
const onDocumentKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
187
|
+
if (event.key === 'Escape') send({ type: 'ESCAPE' })
|
|
188
|
+
}
|
|
189
|
+
document.addEventListener('keydown', onDocumentKeyDown)
|
|
190
|
+
return () => document.removeEventListener('keydown', onDocumentKeyDown)
|
|
191
|
+
}, [open, send])
|
|
192
|
+
|
|
164
193
|
if (!open) return null
|
|
165
194
|
return (
|
|
166
195
|
<div
|
|
167
|
-
ref={
|
|
196
|
+
ref={(node) => {
|
|
197
|
+
contentRef.current = node
|
|
198
|
+
if (typeof ref === 'function') ref(node)
|
|
199
|
+
else if (ref) ref.current = node
|
|
200
|
+
}}
|
|
168
201
|
id={menuId}
|
|
169
202
|
role="menu"
|
|
203
|
+
tabIndex={-1}
|
|
170
204
|
{...mergeProps(
|
|
171
205
|
partAttrs(menuAnatomy.scope, menuAnatomy.content, { state: 'open' }),
|
|
172
206
|
{
|
|
@@ -132,11 +132,15 @@ export interface PopoverPositionerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
132
132
|
/** Mount panel in document.body (default true). */
|
|
133
133
|
portalled?: boolean
|
|
134
134
|
placement?: AnchorPlacement
|
|
135
|
+
/** Gap between trigger and panel (default 4). */
|
|
136
|
+
gutter?: number
|
|
137
|
+
/** Stack level for the portalled surface (default 50). */
|
|
138
|
+
zIndex?: number
|
|
135
139
|
children?: ReactNode
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
const Positioner = forwardRef<HTMLDivElement, PopoverPositionerProps>(function PopoverPositioner(
|
|
139
|
-
{ portalled = true, placement = 'bottom-start', children, className, ...rest },
|
|
143
|
+
{ portalled = true, placement = 'bottom-start', gutter, zIndex, children, className, ...rest },
|
|
140
144
|
ref,
|
|
141
145
|
) {
|
|
142
146
|
const { open, triggerRef, positionerRef } = usePopoverCtx()
|
|
@@ -151,6 +155,8 @@ const Positioner = forwardRef<HTMLDivElement, PopoverPositionerProps>(function P
|
|
|
151
155
|
positionerRef={positionerRef}
|
|
152
156
|
portalled={portalled}
|
|
153
157
|
placement={placement}
|
|
158
|
+
gutter={gutter}
|
|
159
|
+
zIndex={zIndex}
|
|
154
160
|
className={className}
|
|
155
161
|
{...rest}
|
|
156
162
|
>
|
|
@@ -97,6 +97,48 @@ const Amount = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(functi
|
|
|
97
97
|
)
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
+
/** Command verb face — e.g. `/pay` (BRC-218 §4 confirmation). */
|
|
101
|
+
const Verb = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
102
|
+
function PromptVerb(props, ref) {
|
|
103
|
+
const { status } = usePromptCtx()
|
|
104
|
+
return (
|
|
105
|
+
<p
|
|
106
|
+
ref={ref}
|
|
107
|
+
{...mergeProps(partAttrs(promptAnatomy.scope, promptAnatomy.verb, { state: status }), props)}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
/** Fully-qualified recipient `@handle@domain` — never alias alone. */
|
|
114
|
+
const Recipient = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
115
|
+
function PromptRecipient(props, ref) {
|
|
116
|
+
const { status } = usePromptCtx()
|
|
117
|
+
return (
|
|
118
|
+
<p
|
|
119
|
+
ref={ref}
|
|
120
|
+
{...mergeProps(
|
|
121
|
+
partAttrs(promptAnatomy.scope, promptAnatomy.recipient, { state: status }),
|
|
122
|
+
props,
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
/** Plain-language effect statement for the confirmation sheet. */
|
|
130
|
+
const Effect = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
131
|
+
function PromptEffect(props, ref) {
|
|
132
|
+
const { status } = usePromptCtx()
|
|
133
|
+
return (
|
|
134
|
+
<p
|
|
135
|
+
ref={ref}
|
|
136
|
+
{...mergeProps(partAttrs(promptAnatomy.scope, promptAnatomy.effect, { state: status }), props)}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
100
142
|
const Actions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function PromptActions(
|
|
101
143
|
props,
|
|
102
144
|
ref,
|
|
@@ -155,6 +197,9 @@ export const Prompt = {
|
|
|
155
197
|
Eyebrow,
|
|
156
198
|
Meta,
|
|
157
199
|
Amount,
|
|
200
|
+
Verb,
|
|
201
|
+
Recipient,
|
|
202
|
+
Effect,
|
|
158
203
|
Actions,
|
|
159
204
|
Primary,
|
|
160
205
|
Secondary,
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { partAttrs, partOnlyAttrs, rangeSliderAnatomy } from '@aeon-ui/core'
|
|
2
|
+
import {
|
|
3
|
+
rangeSliderMachine,
|
|
4
|
+
snapRangeSliderValue,
|
|
5
|
+
} from '@aeon-ui/primitives'
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
forwardRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
type HTMLAttributes,
|
|
15
|
+
type KeyboardEvent,
|
|
16
|
+
type PointerEvent,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
} from 'react'
|
|
19
|
+
import { useAeonMachine } from '../hooks/use-aeon-machine.js'
|
|
20
|
+
import { mergeProps } from '../utils/merge-props.js'
|
|
21
|
+
|
|
22
|
+
type ThumbIndex = 0 | 1
|
|
23
|
+
|
|
24
|
+
interface RangeSliderContextValue {
|
|
25
|
+
values: [number, number]
|
|
26
|
+
min: number
|
|
27
|
+
max: number
|
|
28
|
+
step: number
|
|
29
|
+
disabled: boolean
|
|
30
|
+
dragging: boolean
|
|
31
|
+
activeThumb: ThumbIndex | null
|
|
32
|
+
send: ReturnType<typeof useAeonMachine<typeof rangeSliderMachine>>[1]
|
|
33
|
+
setThumbFromClientX: (thumb: ThumbIndex, clientX: number) => void
|
|
34
|
+
trackRef: React.RefObject<HTMLDivElement | null>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const RangeSliderCtx = createContext<RangeSliderContextValue | null>(null)
|
|
38
|
+
|
|
39
|
+
function useRangeSliderCtx() {
|
|
40
|
+
const ctx = useContext(RangeSliderCtx)
|
|
41
|
+
if (!ctx) throw new Error('RangeSlider parts must be used within RangeSlider.Root')
|
|
42
|
+
return ctx
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function percent(value: number, min: number, max: number) {
|
|
46
|
+
if (max <= min) return 0
|
|
47
|
+
return ((value - min) / (max - min)) * 100
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RangeSliderRootProps {
|
|
51
|
+
value?: [number, number]
|
|
52
|
+
defaultValue?: [number, number]
|
|
53
|
+
min?: number
|
|
54
|
+
max?: number
|
|
55
|
+
step?: number
|
|
56
|
+
minStepsBetween?: number
|
|
57
|
+
disabled?: boolean
|
|
58
|
+
onValueChange?: (value: [number, number]) => void
|
|
59
|
+
children?: ReactNode
|
|
60
|
+
className?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const Root = forwardRef<HTMLDivElement, RangeSliderRootProps>(function RangeSliderRoot(
|
|
64
|
+
{
|
|
65
|
+
value,
|
|
66
|
+
defaultValue,
|
|
67
|
+
min = 0,
|
|
68
|
+
max = 100,
|
|
69
|
+
step = 1,
|
|
70
|
+
minStepsBetween = 0,
|
|
71
|
+
disabled = false,
|
|
72
|
+
onValueChange,
|
|
73
|
+
children,
|
|
74
|
+
className,
|
|
75
|
+
...rest
|
|
76
|
+
},
|
|
77
|
+
ref,
|
|
78
|
+
) {
|
|
79
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
80
|
+
const [snapshot, send] = useAeonMachine(rangeSliderMachine, {
|
|
81
|
+
input: {
|
|
82
|
+
values: defaultValue ?? [min, max],
|
|
83
|
+
min,
|
|
84
|
+
max,
|
|
85
|
+
step,
|
|
86
|
+
minStepsBetween,
|
|
87
|
+
disabled,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const resolvedValues = value ?? snapshot.context.values
|
|
92
|
+
const dragging = snapshot.matches({ interaction: 'dragging' })
|
|
93
|
+
const activeThumb = snapshot.context.activeThumb
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (value !== undefined) send({ type: 'SET_VALUES', values: value })
|
|
97
|
+
}, [value, send])
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
send({ type: 'SET_BOUNDS', min, max, step, minStepsBetween })
|
|
101
|
+
}, [min, max, step, minStepsBetween, send])
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
send({ type: 'SET_DISABLED', disabled })
|
|
105
|
+
}, [disabled, send])
|
|
106
|
+
|
|
107
|
+
const setThumbFromClientX = useCallback(
|
|
108
|
+
(thumb: ThumbIndex, clientX: number) => {
|
|
109
|
+
const track = trackRef.current
|
|
110
|
+
if (!track || disabled) return
|
|
111
|
+
const rect = track.getBoundingClientRect()
|
|
112
|
+
if (rect.width <= 0) return
|
|
113
|
+
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width))
|
|
114
|
+
const next = snapRangeSliderValue(min + ratio * (max - min), min, max, step)
|
|
115
|
+
send({ type: 'POINTER_MOVE', value: next })
|
|
116
|
+
if (value === undefined) {
|
|
117
|
+
// Machine updates async via snapshot; notify after POINTER_MOVE by reading next tick
|
|
118
|
+
// Consumers in uncontrolled mode get onValueChange from effect below.
|
|
119
|
+
}
|
|
120
|
+
void thumb
|
|
121
|
+
},
|
|
122
|
+
[disabled, min, max, step, send, value],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const lastNotified = useRef<string>('')
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const key = resolvedValues.join(',')
|
|
128
|
+
if (key === lastNotified.current) return
|
|
129
|
+
lastNotified.current = key
|
|
130
|
+
onValueChange?.(resolvedValues)
|
|
131
|
+
}, [resolvedValues, onValueChange])
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!dragging) return
|
|
135
|
+
const onMove = (e: globalThis.PointerEvent) => {
|
|
136
|
+
if (activeThumb == null) return
|
|
137
|
+
setThumbFromClientX(activeThumb, e.clientX)
|
|
138
|
+
}
|
|
139
|
+
const onUp = () => send({ type: 'POINTER_UP' })
|
|
140
|
+
window.addEventListener('pointermove', onMove)
|
|
141
|
+
window.addEventListener('pointerup', onUp)
|
|
142
|
+
return () => {
|
|
143
|
+
window.removeEventListener('pointermove', onMove)
|
|
144
|
+
window.removeEventListener('pointerup', onUp)
|
|
145
|
+
}
|
|
146
|
+
}, [dragging, activeThumb, setThumbFromClientX, send])
|
|
147
|
+
|
|
148
|
+
const ctx = useMemo(
|
|
149
|
+
() => ({
|
|
150
|
+
values: resolvedValues,
|
|
151
|
+
min,
|
|
152
|
+
max,
|
|
153
|
+
step,
|
|
154
|
+
disabled,
|
|
155
|
+
dragging,
|
|
156
|
+
activeThumb,
|
|
157
|
+
send,
|
|
158
|
+
setThumbFromClientX,
|
|
159
|
+
trackRef,
|
|
160
|
+
}),
|
|
161
|
+
[
|
|
162
|
+
resolvedValues,
|
|
163
|
+
min,
|
|
164
|
+
max,
|
|
165
|
+
step,
|
|
166
|
+
disabled,
|
|
167
|
+
dragging,
|
|
168
|
+
activeThumb,
|
|
169
|
+
send,
|
|
170
|
+
setThumbFromClientX,
|
|
171
|
+
],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<RangeSliderCtx.Provider value={ctx}>
|
|
176
|
+
<div
|
|
177
|
+
ref={ref}
|
|
178
|
+
className={className}
|
|
179
|
+
{...mergeProps(
|
|
180
|
+
partAttrs(rangeSliderAnatomy.scope, rangeSliderAnatomy.root, {
|
|
181
|
+
state: dragging ? 'dragging' : 'idle',
|
|
182
|
+
disabled,
|
|
183
|
+
}),
|
|
184
|
+
rest as HTMLAttributes<HTMLDivElement>,
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
{children}
|
|
188
|
+
</div>
|
|
189
|
+
</RangeSliderCtx.Provider>
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const Track = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function RangeSliderTrack(
|
|
194
|
+
{ onPointerDown, ...rest },
|
|
195
|
+
ref,
|
|
196
|
+
) {
|
|
197
|
+
const { disabled, values, send, setThumbFromClientX, trackRef, min, max } = useRangeSliderCtx()
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div
|
|
201
|
+
ref={(node) => {
|
|
202
|
+
trackRef.current = node
|
|
203
|
+
if (typeof ref === 'function') ref(node)
|
|
204
|
+
else if (ref) ref.current = node
|
|
205
|
+
}}
|
|
206
|
+
{...mergeProps(
|
|
207
|
+
partOnlyAttrs(rangeSliderAnatomy.track),
|
|
208
|
+
{
|
|
209
|
+
onPointerDown: (e: PointerEvent<HTMLDivElement>) => {
|
|
210
|
+
onPointerDown?.(e)
|
|
211
|
+
if (disabled || e.button !== 0) return
|
|
212
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
213
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
214
|
+
const ratio = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width))
|
|
215
|
+
const pointerValue = min + ratio * (max - min)
|
|
216
|
+
const dist0 = Math.abs(pointerValue - values[0])
|
|
217
|
+
const dist1 = Math.abs(pointerValue - values[1])
|
|
218
|
+
const thumb: ThumbIndex = dist0 <= dist1 ? 0 : 1
|
|
219
|
+
send({ type: 'POINTER_DOWN', thumb })
|
|
220
|
+
setThumbFromClientX(thumb, e.clientX)
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
rest,
|
|
224
|
+
)}
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const Range = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function RangeSliderRange(
|
|
230
|
+
{ style, ...rest },
|
|
231
|
+
ref,
|
|
232
|
+
) {
|
|
233
|
+
const { values, min, max } = useRangeSliderCtx()
|
|
234
|
+
const left = percent(values[0], min, max)
|
|
235
|
+
const right = percent(values[1], min, max)
|
|
236
|
+
return (
|
|
237
|
+
<div
|
|
238
|
+
ref={ref}
|
|
239
|
+
aria-hidden
|
|
240
|
+
{...mergeProps(partOnlyAttrs(rangeSliderAnatomy.range), rest, {
|
|
241
|
+
style: { ...style, left: `${left}%`, width: `${Math.max(0, right - left)}%` },
|
|
242
|
+
})}
|
|
243
|
+
/>
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
export interface RangeSliderThumbProps extends HTMLAttributes<HTMLSpanElement> {
|
|
248
|
+
index: ThumbIndex
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const Thumb = forwardRef<HTMLSpanElement, RangeSliderThumbProps>(function RangeSliderThumb(
|
|
252
|
+
{ index, onKeyDown, onPointerDown, style, ...rest },
|
|
253
|
+
ref,
|
|
254
|
+
) {
|
|
255
|
+
const { values, min, max, step, disabled, dragging, activeThumb, send, setThumbFromClientX } =
|
|
256
|
+
useRangeSliderCtx()
|
|
257
|
+
const value = values[index]
|
|
258
|
+
const isActive = dragging && activeThumb === index
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<span
|
|
262
|
+
ref={ref}
|
|
263
|
+
role="slider"
|
|
264
|
+
tabIndex={disabled ? -1 : 0}
|
|
265
|
+
aria-valuemin={min}
|
|
266
|
+
aria-valuemax={max}
|
|
267
|
+
aria-valuenow={value}
|
|
268
|
+
aria-disabled={disabled || undefined}
|
|
269
|
+
data-aeon-index={index}
|
|
270
|
+
{...mergeProps(
|
|
271
|
+
partOnlyAttrs(rangeSliderAnatomy.thumb, {
|
|
272
|
+
state: isActive ? 'dragging' : disabled ? 'disabled' : 'idle',
|
|
273
|
+
}),
|
|
274
|
+
{
|
|
275
|
+
style: { ...style, left: `${percent(value, min, max)}%` },
|
|
276
|
+
onPointerDown: (e: PointerEvent<HTMLSpanElement>) => {
|
|
277
|
+
onPointerDown?.(e)
|
|
278
|
+
if (disabled || e.button !== 0) return
|
|
279
|
+
e.stopPropagation()
|
|
280
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
281
|
+
send({ type: 'POINTER_DOWN', thumb: index })
|
|
282
|
+
setThumbFromClientX(index, e.clientX)
|
|
283
|
+
},
|
|
284
|
+
onKeyDown: (e: KeyboardEvent<HTMLSpanElement>) => {
|
|
285
|
+
onKeyDown?.(e)
|
|
286
|
+
if (disabled || e.defaultPrevented) return
|
|
287
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
288
|
+
e.preventDefault()
|
|
289
|
+
send({ type: 'STEP', thumb: index, delta: step })
|
|
290
|
+
}
|
|
291
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
292
|
+
e.preventDefault()
|
|
293
|
+
send({ type: 'STEP', thumb: index, delta: -step })
|
|
294
|
+
}
|
|
295
|
+
if (e.key === 'Home') {
|
|
296
|
+
e.preventDefault()
|
|
297
|
+
send({ type: 'HOME', thumb: index })
|
|
298
|
+
}
|
|
299
|
+
if (e.key === 'End') {
|
|
300
|
+
e.preventDefault()
|
|
301
|
+
send({ type: 'END', thumb: index })
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
rest,
|
|
306
|
+
)}
|
|
307
|
+
/>
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const ValueText = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
|
312
|
+
function RangeSliderValueText(props, ref) {
|
|
313
|
+
const { values } = useRangeSliderCtx()
|
|
314
|
+
return (
|
|
315
|
+
<span ref={ref} {...mergeProps(partOnlyAttrs(rangeSliderAnatomy.valueText), props)}>
|
|
316
|
+
{props.children ?? `${values[0]} – ${values[1]}`}
|
|
317
|
+
</span>
|
|
318
|
+
)
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
export const RangeSlider = { Root, Track, Range, Thumb, ValueText }
|
|
@@ -25,6 +25,8 @@ interface SliderContextValue {
|
|
|
25
25
|
dragging: boolean
|
|
26
26
|
send: ReturnType<typeof useAeonMachine<typeof sliderMachine>>[1]
|
|
27
27
|
onValueChange?: (value: number) => void
|
|
28
|
+
/** Snap, store and report a user-intended value. */
|
|
29
|
+
commit: (value: number) => void
|
|
28
30
|
setValueFromClientX: (clientX: number) => void
|
|
29
31
|
trackRef: React.RefObject<HTMLDivElement | null>
|
|
30
32
|
}
|
|
@@ -83,14 +85,33 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
|
|
|
83
85
|
const resolvedValue = value ?? snapshot.context.value
|
|
84
86
|
const dragging = snapshot.matches({ interaction: 'dragging' })
|
|
85
87
|
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
onValueChange?.(resolvedValue)
|
|
88
|
-
}, [resolvedValue, onValueChange])
|
|
89
|
-
|
|
90
88
|
useEffect(() => {
|
|
91
89
|
if (value !== undefined) send({ type: 'SET_VALUE', value })
|
|
92
90
|
}, [value, send])
|
|
93
91
|
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
send({ type: 'SET_BOUNDS', min, max, step })
|
|
94
|
+
}, [min, max, step, send])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
send({ type: 'SET_DISABLED', disabled })
|
|
98
|
+
}, [disabled, send])
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Interaction is emitted here, never echoed from the resolved value. A
|
|
102
|
+
* controlled slider therefore reports what the user did (Aeon ≤1.4.0 stayed
|
|
103
|
+
* silent while `value` was set) and never fires on mount.
|
|
104
|
+
*/
|
|
105
|
+
const commit = useCallback(
|
|
106
|
+
(next: number) => {
|
|
107
|
+
if (disabled) return
|
|
108
|
+
const snapped = snapSliderValue(next, min, max, step)
|
|
109
|
+
send({ type: 'SET_VALUE', value: snapped })
|
|
110
|
+
onValueChange?.(snapped)
|
|
111
|
+
},
|
|
112
|
+
[disabled, min, max, step, send, onValueChange],
|
|
113
|
+
)
|
|
114
|
+
|
|
94
115
|
const setValueFromClientX = useCallback(
|
|
95
116
|
(clientX: number) => {
|
|
96
117
|
const track = trackRef.current
|
|
@@ -98,11 +119,9 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
|
|
|
98
119
|
const rect = track.getBoundingClientRect()
|
|
99
120
|
if (rect.width <= 0) return
|
|
100
121
|
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width))
|
|
101
|
-
|
|
102
|
-
send({ type: 'SET_VALUE', value: next })
|
|
103
|
-
if (value === undefined) onValueChange?.(next)
|
|
122
|
+
commit(min + ratio * (max - min))
|
|
104
123
|
},
|
|
105
|
-
[disabled, min, max
|
|
124
|
+
[commit, disabled, min, max],
|
|
106
125
|
)
|
|
107
126
|
|
|
108
127
|
useEffect(() => {
|
|
@@ -127,10 +146,22 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
|
|
|
127
146
|
dragging,
|
|
128
147
|
send,
|
|
129
148
|
onValueChange,
|
|
149
|
+
commit,
|
|
130
150
|
setValueFromClientX,
|
|
131
151
|
trackRef,
|
|
132
152
|
}),
|
|
133
|
-
[
|
|
153
|
+
[
|
|
154
|
+
resolvedValue,
|
|
155
|
+
min,
|
|
156
|
+
max,
|
|
157
|
+
step,
|
|
158
|
+
disabled,
|
|
159
|
+
dragging,
|
|
160
|
+
send,
|
|
161
|
+
onValueChange,
|
|
162
|
+
commit,
|
|
163
|
+
setValueFromClientX,
|
|
164
|
+
],
|
|
134
165
|
)
|
|
135
166
|
|
|
136
167
|
return (
|
|
@@ -201,7 +232,8 @@ const Thumb = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(funct
|
|
|
201
232
|
{ onKeyDown, onPointerDown, style, ...rest },
|
|
202
233
|
ref,
|
|
203
234
|
) {
|
|
204
|
-
const { value, min, max, step, disabled, dragging, send, setValueFromClientX } =
|
|
235
|
+
const { value, min, max, step, disabled, dragging, send, commit, setValueFromClientX } =
|
|
236
|
+
useSliderCtx()
|
|
205
237
|
|
|
206
238
|
return (
|
|
207
239
|
<span
|
|
@@ -229,19 +261,27 @@ const Thumb = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(funct
|
|
|
229
261
|
if (disabled || e.defaultPrevented) return
|
|
230
262
|
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
231
263
|
e.preventDefault()
|
|
232
|
-
|
|
264
|
+
commit(value + step)
|
|
233
265
|
}
|
|
234
266
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
235
267
|
e.preventDefault()
|
|
236
|
-
|
|
268
|
+
commit(value - step)
|
|
269
|
+
}
|
|
270
|
+
if (e.key === 'PageUp') {
|
|
271
|
+
e.preventDefault()
|
|
272
|
+
commit(value + (max - min) / 10)
|
|
273
|
+
}
|
|
274
|
+
if (e.key === 'PageDown') {
|
|
275
|
+
e.preventDefault()
|
|
276
|
+
commit(value - (max - min) / 10)
|
|
237
277
|
}
|
|
238
278
|
if (e.key === 'Home') {
|
|
239
279
|
e.preventDefault()
|
|
240
|
-
|
|
280
|
+
commit(min)
|
|
241
281
|
}
|
|
242
282
|
if (e.key === 'End') {
|
|
243
283
|
e.preventDefault()
|
|
244
|
-
|
|
284
|
+
commit(max)
|
|
245
285
|
}
|
|
246
286
|
},
|
|
247
287
|
},
|