aeon-ui-engine 1.4.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/CONSUMER.md +67 -27
- package/INTEGRATION.md +1 -1
- package/README.md +17 -8
- package/package.json +5 -2
- package/packages/cli/dist/bin.js +29 -3
- package/packages/cli/dist/{chunk-5L47SLNV.js → chunk-JMNRJCWF.js} +63 -9
- package/packages/cli/dist/index.d.ts +15 -1
- package/packages/cli/dist/index.js +3 -1
- package/packages/cli/node_modules/.bin/vitest +2 -2
- package/packages/cli/src/bin.ts +30 -2
- package/packages/cli/src/index.ts +1 -0
- package/packages/cli/src/init.ts +10 -4
- package/packages/cli/src/link.ts +61 -0
- package/packages/core/node_modules/.bin/vitest +2 -2
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/panda/styled-system/styles.css +0 -4
- package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
- package/packages/primitives/dist/index.js +1 -1
- package/packages/primitives/dist/machines/range-slider.d.ts +3 -3
- 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/machines/slider.ts +17 -0
- package/packages/react/dist/index.d.ts +44 -5
- package/packages/react/dist/index.js +173 -48
- package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/react/src/dialog/dialog.test.tsx +27 -0
- package/packages/react/src/dialog/index.tsx +11 -3
- package/packages/react/src/floating/FloatingPositioner.tsx +19 -1
- package/packages/react/src/hooks/use-anchor-position.ts +88 -17
- package/packages/react/src/menu/index.tsx +45 -6
- package/packages/react/src/popover/index.tsx +31 -4
- package/packages/react/src/slider/index.tsx +66 -19
- package/packages/react/src/tooltip/index.tsx +7 -1
- package/packages/schemas/dist/index.js +3 -1
- package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/schemas/prompts/cursorrules.md +3 -1
- package/packages/schemas/registry/machines/slider.json +18 -1
- package/packages/schemas/src/prompts.ts +3 -1
- package/packages/tree/README.md +29 -0
- package/packages/tree/dist/index.d.ts +97 -0
- package/packages/tree/dist/index.js +107 -0
- package/packages/tree/node_modules/.bin/esbuild +21 -0
- package/packages/tree/node_modules/.bin/jiti +21 -0
- package/packages/tree/node_modules/.bin/tsc +21 -0
- package/packages/tree/node_modules/.bin/tsserver +21 -0
- package/packages/tree/node_modules/.bin/tsup +21 -0
- package/packages/tree/node_modules/.bin/tsup-node +21 -0
- package/packages/tree/node_modules/.bin/tsx +21 -0
- package/packages/tree/node_modules/.bin/vite +21 -0
- package/packages/tree/node_modules/.bin/vitest +21 -0
- package/packages/tree/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/tree/package.json +42 -0
- package/packages/tree/src/exclusive-action.test.ts +54 -0
- package/packages/tree/src/exclusive-action.ts +104 -0
- package/packages/tree/src/index.ts +7 -0
- package/packages/tree/src/interpret.tsx +45 -0
- package/packages/tree/src/totality.ts +20 -0
- package/packages/tree/src/widget.ts +31 -0
- package/packages/tree/tsconfig.json +8 -0
- package/packages/tree/vitest.config.ts +8 -0
- package/scripts/link-scoped-packages.mjs +206 -0
- package/scripts/vite-aliases.mjs +9 -8
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { useEffect, type RefObject } from 'react'
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type AnchorSide = 'top' | 'bottom'
|
|
4
|
+
export type AnchorAlign = 'start' | 'end' | 'center'
|
|
5
|
+
export type AnchorPlacement = `${AnchorSide}-${AnchorAlign}`
|
|
4
6
|
|
|
5
7
|
export interface AnchorPositionOptions {
|
|
6
8
|
/** Match floating width to anchor (select listboxes). */
|
|
7
9
|
matchWidth?: boolean
|
|
10
|
+
/** Gap between anchor and floating surface. */
|
|
11
|
+
gutter?: number
|
|
12
|
+
/** Stack level written to the floating surface. */
|
|
13
|
+
zIndex?: number
|
|
14
|
+
/** Viewport padding kept clear when flipping and clamping. */
|
|
15
|
+
padding?: number
|
|
8
16
|
}
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
const DEFAULT_GUTTER = 4
|
|
19
|
+
const DEFAULT_Z_INDEX = 50
|
|
20
|
+
const DEFAULT_PADDING = 8
|
|
21
|
+
|
|
22
|
+
function splitPlacement(placement: AnchorPlacement): [AnchorSide, AnchorAlign] {
|
|
23
|
+
const [side, align] = placement.split('-') as [AnchorSide, AnchorAlign]
|
|
24
|
+
return [side, align]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pin floating surface to trigger rect (for portalled overlays).
|
|
29
|
+
*
|
|
30
|
+
* Collision aware: the preferred side flips when it has no room, and the
|
|
31
|
+
* surface is clamped inside the viewport so an end-aligned trigger near an edge
|
|
32
|
+
* never renders offscreen. The resolved geometry is published as
|
|
33
|
+
* `data-side` / `data-align` plus `--aeon-available-height` and
|
|
34
|
+
* `--aeon-anchor-width` so recipes can cap their own scroll area.
|
|
35
|
+
*/
|
|
11
36
|
export function useAnchorPosition(
|
|
12
37
|
anchorRef: RefObject<HTMLElement | null>,
|
|
13
38
|
floatingRef: RefObject<HTMLElement | null>,
|
|
@@ -15,22 +40,32 @@ export function useAnchorPosition(
|
|
|
15
40
|
placement: AnchorPlacement = 'bottom-start',
|
|
16
41
|
options: AnchorPositionOptions = {},
|
|
17
42
|
) {
|
|
18
|
-
const {
|
|
43
|
+
const {
|
|
44
|
+
matchWidth = false,
|
|
45
|
+
gutter = DEFAULT_GUTTER,
|
|
46
|
+
zIndex = DEFAULT_Z_INDEX,
|
|
47
|
+
padding = DEFAULT_PADDING,
|
|
48
|
+
} = options
|
|
19
49
|
|
|
20
50
|
useEffect(() => {
|
|
21
51
|
if (!active || !anchorRef.current || !floatingRef.current) return
|
|
22
52
|
|
|
53
|
+
let frame = 0
|
|
54
|
+
|
|
23
55
|
const update = () => {
|
|
24
56
|
const anchor = anchorRef.current
|
|
25
57
|
const floating = floatingRef.current
|
|
26
58
|
if (!anchor || !floating) return
|
|
27
59
|
|
|
60
|
+
const [preferredSide, align] = splitPlacement(placement)
|
|
28
61
|
const rect = anchor.getBoundingClientRect()
|
|
62
|
+
const viewportWidth = window.innerWidth
|
|
63
|
+
const viewportHeight = window.innerHeight
|
|
64
|
+
|
|
29
65
|
floating.style.position = 'fixed'
|
|
30
|
-
floating.style.left = `${rect.left}px`
|
|
31
66
|
floating.style.bottom = ''
|
|
32
67
|
floating.style.right = ''
|
|
33
|
-
floating.style.zIndex =
|
|
68
|
+
floating.style.zIndex = String(zIndex)
|
|
34
69
|
|
|
35
70
|
if (matchWidth) {
|
|
36
71
|
floating.style.width = `${rect.width}px`
|
|
@@ -40,22 +75,58 @@ export function useAnchorPosition(
|
|
|
40
75
|
floating.style.minWidth = ''
|
|
41
76
|
}
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
const width = floating.offsetWidth || floating.getBoundingClientRect().width
|
|
79
|
+
const height = floating.offsetHeight || floating.getBoundingClientRect().height
|
|
80
|
+
|
|
81
|
+
const roomBelow = viewportHeight - rect.bottom - gutter - padding
|
|
82
|
+
const roomAbove = rect.top - gutter - padding
|
|
83
|
+
const room = preferredSide === 'top' ? roomAbove : roomBelow
|
|
84
|
+
const side =
|
|
85
|
+
room >= height
|
|
86
|
+
? preferredSide
|
|
87
|
+
: roomAbove > roomBelow
|
|
88
|
+
? 'top'
|
|
89
|
+
: 'bottom'
|
|
90
|
+
const available = side === 'top' ? roomAbove : roomBelow
|
|
91
|
+
|
|
92
|
+
let left = rect.left
|
|
93
|
+
if (align === 'end') left = rect.right - width
|
|
94
|
+
else if (align === 'center') left = rect.left + rect.width / 2 - width / 2
|
|
95
|
+
|
|
96
|
+
const maxLeft = Math.max(padding, viewportWidth - width - padding)
|
|
97
|
+
left = Math.min(Math.max(padding, left), maxLeft)
|
|
98
|
+
|
|
99
|
+
const top =
|
|
100
|
+
side === 'top'
|
|
101
|
+
? Math.max(padding, rect.top - gutter - Math.min(height, Math.max(available, 0)))
|
|
102
|
+
: Math.min(
|
|
103
|
+
rect.bottom + gutter,
|
|
104
|
+
Math.max(padding, viewportHeight - height - padding),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
floating.style.left = `${Math.round(left)}px`
|
|
108
|
+
floating.style.top = `${Math.round(top)}px`
|
|
109
|
+
floating.style.setProperty('--aeon-available-height', `${Math.round(Math.max(available, 0))}px`)
|
|
110
|
+
floating.style.setProperty('--aeon-anchor-width', `${Math.round(rect.width)}px`)
|
|
111
|
+
floating.dataset.side = side
|
|
112
|
+
floating.dataset.align = align
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const schedule = () => {
|
|
116
|
+
cancelAnimationFrame(frame)
|
|
117
|
+
frame = requestAnimationFrame(update)
|
|
49
118
|
}
|
|
50
119
|
|
|
51
120
|
update()
|
|
52
|
-
|
|
121
|
+
// Measure again once the surface has laid out (flip/clamp need real height).
|
|
122
|
+
schedule()
|
|
53
123
|
|
|
54
|
-
window.addEventListener('resize',
|
|
55
|
-
window.addEventListener('scroll',
|
|
124
|
+
window.addEventListener('resize', schedule)
|
|
125
|
+
window.addEventListener('scroll', schedule, true)
|
|
56
126
|
return () => {
|
|
57
|
-
|
|
58
|
-
window.removeEventListener('
|
|
127
|
+
cancelAnimationFrame(frame)
|
|
128
|
+
window.removeEventListener('resize', schedule)
|
|
129
|
+
window.removeEventListener('scroll', schedule, true)
|
|
59
130
|
}
|
|
60
|
-
}, [active, anchorRef, floatingRef, placement, matchWidth])
|
|
131
|
+
}, [active, anchorRef, floatingRef, placement, matchWidth, gutter, zIndex, padding])
|
|
61
132
|
}
|
|
@@ -51,16 +51,21 @@ const Root = forwardRef<HTMLDivElement, MenuRootProps>(function MenuRoot(
|
|
|
51
51
|
ref,
|
|
52
52
|
) {
|
|
53
53
|
const [snapshot, send] = useAeonMachine(popoverMachine, { input: { open: defaultOpen } })
|
|
54
|
-
const
|
|
54
|
+
const machineOpen = snapshot.context.open
|
|
55
|
+
const resolvedOpen = open ?? machineOpen
|
|
55
56
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
56
57
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
57
58
|
const positionerRef = useRef<HTMLDivElement>(null)
|
|
58
59
|
const triggerId = useId()
|
|
59
60
|
const menuId = useId()
|
|
60
61
|
|
|
62
|
+
/** Machine intent, so a controlled owner still hears its own trigger. */
|
|
63
|
+
const reportedOpen = useRef(machineOpen)
|
|
61
64
|
useEffect(() => {
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
if (reportedOpen.current === machineOpen) return
|
|
66
|
+
reportedOpen.current = machineOpen
|
|
67
|
+
onOpenChange?.(machineOpen)
|
|
68
|
+
}, [machineOpen, onOpenChange])
|
|
64
69
|
|
|
65
70
|
useEffect(() => {
|
|
66
71
|
if (open !== undefined) send({ type: 'SET_OPEN', open })
|
|
@@ -129,11 +134,15 @@ export interface MenuPositionerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
129
134
|
/** Mount menu in document.body (default true). */
|
|
130
135
|
portalled?: boolean
|
|
131
136
|
placement?: AnchorPlacement
|
|
137
|
+
/** Gap between trigger and menu (default 4). */
|
|
138
|
+
gutter?: number
|
|
139
|
+
/** Stack level for the portalled surface (default 50). */
|
|
140
|
+
zIndex?: number
|
|
132
141
|
children?: ReactNode
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
const Positioner = forwardRef<HTMLDivElement, MenuPositionerProps>(function MenuPositioner(
|
|
136
|
-
{ portalled = true, placement = 'bottom-start', children, className, ...rest },
|
|
145
|
+
{ portalled = true, placement = 'bottom-start', gutter, zIndex, children, className, ...rest },
|
|
137
146
|
ref,
|
|
138
147
|
) {
|
|
139
148
|
const { open, triggerRef, positionerRef } = useMenuCtx()
|
|
@@ -148,6 +157,8 @@ const Positioner = forwardRef<HTMLDivElement, MenuPositionerProps>(function Menu
|
|
|
148
157
|
positionerRef={positionerRef}
|
|
149
158
|
portalled={portalled}
|
|
150
159
|
placement={placement}
|
|
160
|
+
gutter={gutter}
|
|
161
|
+
zIndex={zIndex}
|
|
151
162
|
className={className}
|
|
152
163
|
{...rest}
|
|
153
164
|
>
|
|
@@ -160,13 +171,41 @@ const Content = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(funct
|
|
|
160
171
|
{ onKeyDown, ...rest },
|
|
161
172
|
ref,
|
|
162
173
|
) {
|
|
163
|
-
const { open, send, menuId } = useMenuCtx()
|
|
174
|
+
const { open, send, menuId, triggerRef } = useMenuCtx()
|
|
175
|
+
const contentRef = useRef<HTMLDivElement | null>(null)
|
|
176
|
+
|
|
177
|
+
// Focus enters the surface while it is open and returns to the trigger on
|
|
178
|
+
// dismiss, so keyboard users stay inside the menu they opened.
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!open) return
|
|
181
|
+
const trigger = triggerRef.current
|
|
182
|
+
contentRef.current?.focus({ preventScroll: true })
|
|
183
|
+
return () => {
|
|
184
|
+
if (trigger?.isConnected) trigger.focus({ preventScroll: true })
|
|
185
|
+
}
|
|
186
|
+
}, [open, triggerRef])
|
|
187
|
+
|
|
188
|
+
// Escape closes wherever focus sits — trigger, item or elsewhere on the page.
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!open) return
|
|
191
|
+
const onDocumentKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
192
|
+
if (event.key === 'Escape') send({ type: 'ESCAPE' })
|
|
193
|
+
}
|
|
194
|
+
document.addEventListener('keydown', onDocumentKeyDown)
|
|
195
|
+
return () => document.removeEventListener('keydown', onDocumentKeyDown)
|
|
196
|
+
}, [open, send])
|
|
197
|
+
|
|
164
198
|
if (!open) return null
|
|
165
199
|
return (
|
|
166
200
|
<div
|
|
167
|
-
ref={
|
|
201
|
+
ref={(node) => {
|
|
202
|
+
contentRef.current = node
|
|
203
|
+
if (typeof ref === 'function') ref(node)
|
|
204
|
+
else if (ref) ref.current = node
|
|
205
|
+
}}
|
|
168
206
|
id={menuId}
|
|
169
207
|
role="menu"
|
|
208
|
+
tabIndex={-1}
|
|
170
209
|
{...mergeProps(
|
|
171
210
|
partAttrs(menuAnatomy.scope, menuAnatomy.content, { state: 'open' }),
|
|
172
211
|
{
|
|
@@ -53,16 +53,21 @@ const Root = forwardRef<HTMLDivElement, PopoverRootProps>(function PopoverRoot(
|
|
|
53
53
|
const [snapshot, send] = useAeonMachine(popoverMachine, {
|
|
54
54
|
input: { open: defaultOpen },
|
|
55
55
|
})
|
|
56
|
-
const
|
|
56
|
+
const machineOpen = snapshot.context.open
|
|
57
|
+
const resolvedOpen = open ?? machineOpen
|
|
57
58
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
58
59
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
59
60
|
const positionerRef = useRef<HTMLDivElement>(null)
|
|
60
61
|
const triggerId = useId()
|
|
61
62
|
const contentId = useId()
|
|
62
63
|
|
|
64
|
+
/** Machine intent, so a controlled owner still hears its own trigger. */
|
|
65
|
+
const reportedOpen = useRef(machineOpen)
|
|
63
66
|
useEffect(() => {
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
if (reportedOpen.current === machineOpen) return
|
|
68
|
+
reportedOpen.current = machineOpen
|
|
69
|
+
onOpenChange?.(machineOpen)
|
|
70
|
+
}, [machineOpen, onOpenChange])
|
|
66
71
|
|
|
67
72
|
useEffect(() => {
|
|
68
73
|
if (open !== undefined) send({ type: 'SET_OPEN', open })
|
|
@@ -132,11 +137,29 @@ export interface PopoverPositionerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
132
137
|
/** Mount panel in document.body (default true). */
|
|
133
138
|
portalled?: boolean
|
|
134
139
|
placement?: AnchorPlacement
|
|
140
|
+
/** Align to this region rather than the trigger. See `FloatingPositioner`. */
|
|
141
|
+
anchorRef?: React.RefObject<HTMLElement | null>
|
|
142
|
+
/** Size the panel to the anchor, so it spans the region it belongs to. */
|
|
143
|
+
matchAnchorWidth?: boolean
|
|
144
|
+
/** Gap between trigger and panel (default 4). */
|
|
145
|
+
gutter?: number
|
|
146
|
+
/** Stack level for the portalled surface (default 50). */
|
|
147
|
+
zIndex?: number
|
|
135
148
|
children?: ReactNode
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
const Positioner = forwardRef<HTMLDivElement, PopoverPositionerProps>(function PopoverPositioner(
|
|
139
|
-
{
|
|
152
|
+
{
|
|
153
|
+
portalled = true,
|
|
154
|
+
placement = 'bottom-start',
|
|
155
|
+
anchorRef,
|
|
156
|
+
matchAnchorWidth = false,
|
|
157
|
+
gutter,
|
|
158
|
+
zIndex,
|
|
159
|
+
children,
|
|
160
|
+
className,
|
|
161
|
+
...rest
|
|
162
|
+
},
|
|
140
163
|
ref,
|
|
141
164
|
) {
|
|
142
165
|
const { open, triggerRef, positionerRef } = usePopoverCtx()
|
|
@@ -149,8 +172,12 @@ const Positioner = forwardRef<HTMLDivElement, PopoverPositionerProps>(function P
|
|
|
149
172
|
part={popoverAnatomy.positioner}
|
|
150
173
|
triggerRef={triggerRef}
|
|
151
174
|
positionerRef={positionerRef}
|
|
175
|
+
anchorRef={anchorRef}
|
|
176
|
+
matchAnchorWidth={matchAnchorWidth}
|
|
152
177
|
portalled={portalled}
|
|
153
178
|
placement={placement}
|
|
179
|
+
gutter={gutter}
|
|
180
|
+
zIndex={zIndex}
|
|
154
181
|
className={className}
|
|
155
182
|
{...rest}
|
|
156
183
|
>
|
|
@@ -16,22 +16,30 @@ import {
|
|
|
16
16
|
import { useAeonMachine } from '../hooks/use-aeon-machine.js'
|
|
17
17
|
import { mergeProps } from '../utils/merge-props.js'
|
|
18
18
|
|
|
19
|
-
interface SliderContextValue {
|
|
19
|
+
export interface SliderContextValue {
|
|
20
20
|
value: number
|
|
21
21
|
min: number
|
|
22
22
|
max: number
|
|
23
23
|
step: number
|
|
24
24
|
disabled: boolean
|
|
25
25
|
dragging: boolean
|
|
26
|
+
/** Nested region value from the machine: `interaction.idle` | `interaction.dragging`. */
|
|
27
|
+
interaction: 'idle' | 'dragging'
|
|
26
28
|
send: ReturnType<typeof useAeonMachine<typeof sliderMachine>>[1]
|
|
27
29
|
onValueChange?: (value: number) => void
|
|
30
|
+
/** Snap, store and report a user-intended value. */
|
|
31
|
+
commit: (value: number) => void
|
|
28
32
|
setValueFromClientX: (clientX: number) => void
|
|
29
33
|
trackRef: React.RefObject<HTMLDivElement | null>
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const SliderCtx = createContext<SliderContextValue | null>(null)
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Read the slider's live machine state from inside `Slider.Root`.
|
|
40
|
+
* `interaction` is the nested region value — the same thing the snapshot holds.
|
|
41
|
+
*/
|
|
42
|
+
export function useSliderContext() {
|
|
35
43
|
const ctx = useContext(SliderCtx)
|
|
36
44
|
if (!ctx) throw new Error('Slider parts must be used within Slider.Root')
|
|
37
45
|
return ctx
|
|
@@ -83,14 +91,33 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
|
|
|
83
91
|
const resolvedValue = value ?? snapshot.context.value
|
|
84
92
|
const dragging = snapshot.matches({ interaction: 'dragging' })
|
|
85
93
|
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
onValueChange?.(resolvedValue)
|
|
88
|
-
}, [resolvedValue, onValueChange])
|
|
89
|
-
|
|
90
94
|
useEffect(() => {
|
|
91
95
|
if (value !== undefined) send({ type: 'SET_VALUE', value })
|
|
92
96
|
}, [value, send])
|
|
93
97
|
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
send({ type: 'SET_BOUNDS', min, max, step })
|
|
100
|
+
}, [min, max, step, send])
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
send({ type: 'SET_DISABLED', disabled })
|
|
104
|
+
}, [disabled, send])
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Interaction is emitted here, never echoed from the resolved value. A
|
|
108
|
+
* controlled slider therefore reports what the user did (Aeon ≤1.4.0 stayed
|
|
109
|
+
* silent while `value` was set) and never fires on mount.
|
|
110
|
+
*/
|
|
111
|
+
const commit = useCallback(
|
|
112
|
+
(next: number) => {
|
|
113
|
+
if (disabled) return
|
|
114
|
+
const snapped = snapSliderValue(next, min, max, step)
|
|
115
|
+
send({ type: 'SET_VALUE', value: snapped })
|
|
116
|
+
onValueChange?.(snapped)
|
|
117
|
+
},
|
|
118
|
+
[disabled, min, max, step, send, onValueChange],
|
|
119
|
+
)
|
|
120
|
+
|
|
94
121
|
const setValueFromClientX = useCallback(
|
|
95
122
|
(clientX: number) => {
|
|
96
123
|
const track = trackRef.current
|
|
@@ -98,11 +125,9 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
|
|
|
98
125
|
const rect = track.getBoundingClientRect()
|
|
99
126
|
if (rect.width <= 0) return
|
|
100
127
|
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)
|
|
128
|
+
commit(min + ratio * (max - min))
|
|
104
129
|
},
|
|
105
|
-
[disabled, min, max
|
|
130
|
+
[commit, disabled, min, max],
|
|
106
131
|
)
|
|
107
132
|
|
|
108
133
|
useEffect(() => {
|
|
@@ -125,12 +150,25 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
|
|
|
125
150
|
step,
|
|
126
151
|
disabled,
|
|
127
152
|
dragging,
|
|
153
|
+
interaction: (dragging ? 'dragging' : 'idle') as 'idle' | 'dragging',
|
|
128
154
|
send,
|
|
129
155
|
onValueChange,
|
|
156
|
+
commit,
|
|
130
157
|
setValueFromClientX,
|
|
131
158
|
trackRef,
|
|
132
159
|
}),
|
|
133
|
-
[
|
|
160
|
+
[
|
|
161
|
+
resolvedValue,
|
|
162
|
+
min,
|
|
163
|
+
max,
|
|
164
|
+
step,
|
|
165
|
+
disabled,
|
|
166
|
+
dragging,
|
|
167
|
+
send,
|
|
168
|
+
onValueChange,
|
|
169
|
+
commit,
|
|
170
|
+
setValueFromClientX,
|
|
171
|
+
],
|
|
134
172
|
)
|
|
135
173
|
|
|
136
174
|
return (
|
|
@@ -156,7 +194,7 @@ const Track = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(functio
|
|
|
156
194
|
{ onPointerDown, ...rest },
|
|
157
195
|
ref,
|
|
158
196
|
) {
|
|
159
|
-
const { disabled, send, setValueFromClientX, trackRef } =
|
|
197
|
+
const { disabled, send, setValueFromClientX, trackRef } = useSliderContext()
|
|
160
198
|
|
|
161
199
|
return (
|
|
162
200
|
<div
|
|
@@ -186,7 +224,7 @@ const Range = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(functio
|
|
|
186
224
|
{ style, ...rest },
|
|
187
225
|
ref,
|
|
188
226
|
) {
|
|
189
|
-
const { value, min, max } =
|
|
227
|
+
const { value, min, max } = useSliderContext()
|
|
190
228
|
return (
|
|
191
229
|
<div
|
|
192
230
|
ref={ref}
|
|
@@ -201,7 +239,8 @@ const Thumb = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(funct
|
|
|
201
239
|
{ onKeyDown, onPointerDown, style, ...rest },
|
|
202
240
|
ref,
|
|
203
241
|
) {
|
|
204
|
-
const { value, min, max, step, disabled, dragging, send, setValueFromClientX } =
|
|
242
|
+
const { value, min, max, step, disabled, dragging, send, commit, setValueFromClientX } =
|
|
243
|
+
useSliderContext()
|
|
205
244
|
|
|
206
245
|
return (
|
|
207
246
|
<span
|
|
@@ -229,19 +268,27 @@ const Thumb = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(funct
|
|
|
229
268
|
if (disabled || e.defaultPrevented) return
|
|
230
269
|
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
231
270
|
e.preventDefault()
|
|
232
|
-
|
|
271
|
+
commit(value + step)
|
|
233
272
|
}
|
|
234
273
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
235
274
|
e.preventDefault()
|
|
236
|
-
|
|
275
|
+
commit(value - step)
|
|
276
|
+
}
|
|
277
|
+
if (e.key === 'PageUp') {
|
|
278
|
+
e.preventDefault()
|
|
279
|
+
commit(value + (max - min) / 10)
|
|
280
|
+
}
|
|
281
|
+
if (e.key === 'PageDown') {
|
|
282
|
+
e.preventDefault()
|
|
283
|
+
commit(value - (max - min) / 10)
|
|
237
284
|
}
|
|
238
285
|
if (e.key === 'Home') {
|
|
239
286
|
e.preventDefault()
|
|
240
|
-
|
|
287
|
+
commit(min)
|
|
241
288
|
}
|
|
242
289
|
if (e.key === 'End') {
|
|
243
290
|
e.preventDefault()
|
|
244
|
-
|
|
291
|
+
commit(max)
|
|
245
292
|
}
|
|
246
293
|
},
|
|
247
294
|
},
|
|
@@ -255,7 +302,7 @@ const ValueText = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(f
|
|
|
255
302
|
props,
|
|
256
303
|
ref,
|
|
257
304
|
) {
|
|
258
|
-
const { value } =
|
|
305
|
+
const { value } = useSliderContext()
|
|
259
306
|
return (
|
|
260
307
|
<span ref={ref} {...mergeProps(partOnlyAttrs(sliderAnatomy.valueText), props)}>
|
|
261
308
|
{props.children ?? value}
|
|
@@ -272,11 +272,15 @@ export interface TooltipPositionerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
272
272
|
/** Mount tooltip in document.body (default true). */
|
|
273
273
|
portalled?: boolean
|
|
274
274
|
placement?: AnchorPlacement
|
|
275
|
+
/** Gap between trigger and tip (default 4). */
|
|
276
|
+
gutter?: number
|
|
277
|
+
/** Stack level for the portalled surface (default 50). */
|
|
278
|
+
zIndex?: number
|
|
275
279
|
children?: ReactNode
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
const Positioner = forwardRef<HTMLDivElement, TooltipPositionerProps>(function TooltipPositioner(
|
|
279
|
-
{ portalled = true, placement = 'top-start', children, className, ...rest },
|
|
283
|
+
{ portalled = true, placement = 'top-start', gutter, zIndex, children, className, ...rest },
|
|
280
284
|
ref,
|
|
281
285
|
) {
|
|
282
286
|
const { open, triggerRef, positionerRef } = useTooltipCtx()
|
|
@@ -291,6 +295,8 @@ const Positioner = forwardRef<HTMLDivElement, TooltipPositionerProps>(function T
|
|
|
291
295
|
positionerRef={positionerRef}
|
|
292
296
|
portalled={portalled}
|
|
293
297
|
placement={placement}
|
|
298
|
+
gutter={gutter}
|
|
299
|
+
zIndex={zIndex}
|
|
294
300
|
className={className}
|
|
295
301
|
{...rest}
|
|
296
302
|
>
|
|
@@ -926,7 +926,9 @@ When generating Aeon UI, constrain props/parts/states to @aeon-ui/schemas catalo
|
|
|
926
926
|
Import headless from @aeon-ui/react or styled from @aeon-ui/ui.
|
|
927
927
|
Install: npm install aeon-ui-engine react react-dom xstate @xstate/react
|
|
928
928
|
CSS: import 'aeon-ui-engine/aeon.css'
|
|
929
|
-
|
|
929
|
+
Link: postinstall + npx aeon-ui link (node_modules/@aeon-ui/*)
|
|
930
|
+
Next.js: transpilePackages for @aeon-ui/* + "use client"; Vite plugin optional
|
|
931
|
+
Vue/Solid/Svelte: thin adapters only \u2014 React is the full catalog
|
|
930
932
|
|
|
931
933
|
${STATECHART_PROMPTS.async}
|
|
932
934
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.1.8","results":[[":src/index.test.ts",{"duration":
|
|
1
|
+
{"version":"4.1.8","results":[[":src/index.test.ts",{"duration":42.299125000000004,"failed":false}],[":src/aeon-to-schema.test.ts",{"duration":7.255999999999972,"failed":false}]]}
|
|
@@ -35,7 +35,9 @@ When generating Aeon UI, constrain props/parts/states to @aeon-ui/schemas catalo
|
|
|
35
35
|
Import headless from @aeon-ui/react or styled from @aeon-ui/ui.
|
|
36
36
|
Install: npm install aeon-ui-engine react react-dom xstate @xstate/react
|
|
37
37
|
CSS: import 'aeon-ui-engine/aeon.css'
|
|
38
|
-
|
|
38
|
+
Link: postinstall + npx aeon-ui link (node_modules/@aeon-ui/*)
|
|
39
|
+
Next.js: transpilePackages for @aeon-ui/* + "use client"; Vite plugin optional
|
|
40
|
+
Vue/Solid/Svelte: thin adapters only — React is the full catalog
|
|
39
41
|
|
|
40
42
|
## Async region statechart
|
|
41
43
|
States: idle → loading → (success | empty | failure); REFRESH from success/empty/failure; STALE flag while success.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"id": "slider",
|
|
3
3
|
"initial": "interaction",
|
|
4
4
|
"events": [
|
|
5
|
+
"SET_BOUNDS",
|
|
6
|
+
"SET_DISABLED",
|
|
5
7
|
"SET_VALUE",
|
|
6
8
|
"STEP",
|
|
7
9
|
"HOME",
|
|
@@ -14,6 +16,8 @@
|
|
|
14
16
|
"id": "slider",
|
|
15
17
|
"type": "compound",
|
|
16
18
|
"events": [
|
|
19
|
+
"SET_BOUNDS",
|
|
20
|
+
"SET_DISABLED",
|
|
17
21
|
"SET_VALUE",
|
|
18
22
|
"STEP",
|
|
19
23
|
"HOME",
|
|
@@ -21,7 +25,18 @@
|
|
|
21
25
|
"POINTER_DOWN",
|
|
22
26
|
"POINTER_UP"
|
|
23
27
|
],
|
|
24
|
-
"transitions": [
|
|
28
|
+
"transitions": [
|
|
29
|
+
{
|
|
30
|
+
"event": "SET_BOUNDS",
|
|
31
|
+
"target": [],
|
|
32
|
+
"guarded": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"event": "SET_DISABLED",
|
|
36
|
+
"target": [],
|
|
37
|
+
"guarded": false
|
|
38
|
+
}
|
|
39
|
+
],
|
|
25
40
|
"children": [
|
|
26
41
|
{
|
|
27
42
|
"key": "interaction",
|
|
@@ -110,6 +125,8 @@
|
|
|
110
125
|
"states": {
|
|
111
126
|
"slider": {
|
|
112
127
|
"events": [
|
|
128
|
+
"SET_BOUNDS",
|
|
129
|
+
"SET_DISABLED",
|
|
113
130
|
"SET_VALUE",
|
|
114
131
|
"STEP",
|
|
115
132
|
"HOME",
|
|
@@ -128,7 +128,9 @@ When generating Aeon UI, constrain props/parts/states to @aeon-ui/schemas catalo
|
|
|
128
128
|
Import headless from @aeon-ui/react or styled from @aeon-ui/ui.
|
|
129
129
|
Install: npm install aeon-ui-engine react react-dom xstate @xstate/react
|
|
130
130
|
CSS: import 'aeon-ui-engine/aeon.css'
|
|
131
|
-
|
|
131
|
+
Link: postinstall + npx aeon-ui link (node_modules/@aeon-ui/*)
|
|
132
|
+
Next.js: transpilePackages for @aeon-ui/* + "use client"; Vite plugin optional
|
|
133
|
+
Vue/Solid/Svelte: thin adapters only — React is the full catalog
|
|
132
134
|
|
|
133
135
|
${STATECHART_PROMPTS.async}
|
|
134
136
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @aeon-ui/tree
|
|
2
|
+
|
|
3
|
+
Widget trees for Aeon. A widget is configuration — immutable data describing one face of a chart — interpreted into DOM that carries `data-aeon-scope`, `data-aeon-part`, and `data-aeon-state`.
|
|
4
|
+
|
|
5
|
+
This is Flutter's Widget and Element layers only. There is no RenderObject (layout stays CSS) and no `StatefulWidget` (the chart is the only state). A tree that stored `busy` itself would be the boolean twin the philosophy rules out.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { exclusiveActionWidget, interpret, uncoveredStates, EXCLUSIVE_ACTION_STATES } from '@aeon-ui/tree'
|
|
9
|
+
|
|
10
|
+
const tree = exclusiveActionWidget({
|
|
11
|
+
scope: 'settings-change-password',
|
|
12
|
+
part: 'change-password-form',
|
|
13
|
+
state, // snapshot token
|
|
14
|
+
busy,
|
|
15
|
+
error,
|
|
16
|
+
body: { type: 'host', id: 'body' },
|
|
17
|
+
idleLabel: 'Update password',
|
|
18
|
+
pendingLabel: 'Updating…',
|
|
19
|
+
onSubmit,
|
|
20
|
+
confirm: confirm !== null,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
uncoveredStates(EXCLUSIVE_ACTION_STATES, (s) => exclusiveActionWidget({ ...args, state: s }))
|
|
24
|
+
// [] — every state of the chart has a face
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`interpret(tree, hosts)` is the element layer. Hosts are where an existing compound (a field, a `Prompt`) fills a slot the tree names. Product CSS restyles the anatomy attributes; class names are passed in by the app, not invented here.
|
|
28
|
+
|
|
29
|
+
`exclusiveActionWidget` is the one face shared by every "confirm, then await" region: `idle → confirming → busy → idle | failure`.
|