aeon-ui-engine 1.3.5 → 1.4.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 +25 -0
- package/package.json +1 -1
- package/packages/cli/README.md +13 -1
- package/packages/cli/dist/bin.js +23 -5
- package/packages/cli/dist/chunk-5L47SLNV.js +247 -0
- package/packages/cli/dist/index.d.ts +17 -1
- package/packages/cli/dist/index.js +5 -3
- package/packages/cli/src/bin.ts +21 -3
- package/packages/cli/src/index.ts +1 -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/index.d.ts +2 -0
- package/packages/primitives/dist/index.js +22 -6
- 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/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/react/dist/index.d.ts +117 -25
- package/packages/react/dist/index.js +1731 -1006
- 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/index.ts +3 -0
- package/packages/react/src/prompt/index.tsx +45 -0
- package/packages/react/src/range-slider/index.tsx +322 -0
- package/packages/react/src/thread/index.tsx +74 -3
- 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/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/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/packages/cli/dist/chunk-IW5NT2Z2.js +0 -103
|
@@ -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 }
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { partAttrs, scopeAttrs, threadAnatomy } from '@aeon-ui/core'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
forwardRef,
|
|
4
|
+
type ButtonHTMLAttributes,
|
|
5
|
+
type HTMLAttributes,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react'
|
|
3
8
|
import { mergeProps } from '../utils/merge-props.js'
|
|
4
9
|
|
|
10
|
+
/** Base alignment / delivery states. */
|
|
5
11
|
export type ThreadMessageState = 'mine' | 'theirs' | 'pending' | 'failed'
|
|
6
12
|
|
|
13
|
+
/** Structured in-thread faces (space-combinable with mine/theirs). */
|
|
14
|
+
export type ThreadItemFace = 'command-result' | 'payment-card' | 'request-card'
|
|
15
|
+
|
|
7
16
|
export interface ThreadRootProps extends HTMLAttributes<HTMLElement> {
|
|
8
17
|
children?: ReactNode
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
|
-
* Thread — message stream.
|
|
21
|
+
* Thread — message stream.
|
|
22
|
+
* Item `state` projects mine | theirs | pending | failed, optionally plus
|
|
23
|
+
* command-result | payment-card | request-card (e.g. `mine payment-card`).
|
|
13
24
|
*/
|
|
14
25
|
const Root = forwardRef<HTMLElement, ThreadRootProps>(function ThreadRoot(
|
|
15
26
|
{ children, ...rest },
|
|
@@ -40,7 +51,7 @@ const List = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function
|
|
|
40
51
|
})
|
|
41
52
|
|
|
42
53
|
export interface ThreadItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
43
|
-
state?: ThreadMessageState | `${ThreadMessageState} ${
|
|
54
|
+
state?: ThreadMessageState | ThreadItemFace | `${ThreadMessageState} ${ThreadItemFace}` | string
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
const Item = forwardRef<HTMLDivElement, ThreadItemProps>(function ThreadItem(
|
|
@@ -85,6 +96,61 @@ const Day = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function
|
|
|
85
96
|
)
|
|
86
97
|
})
|
|
87
98
|
|
|
99
|
+
/** Reply / message-binding control — must be touch-reachable (BRC-218 §4.10). */
|
|
100
|
+
const Bind = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
101
|
+
function ThreadBind(props, ref) {
|
|
102
|
+
return (
|
|
103
|
+
<button
|
|
104
|
+
ref={ref}
|
|
105
|
+
type="button"
|
|
106
|
+
{...mergeProps(partAttrs(threadAnatomy.scope, threadAnatomy.bind), props)}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function ThreadCard(
|
|
113
|
+
props,
|
|
114
|
+
ref,
|
|
115
|
+
) {
|
|
116
|
+
return (
|
|
117
|
+
<div ref={ref} {...mergeProps(partAttrs(threadAnatomy.scope, threadAnatomy.card), props)} />
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const CardTitle = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
122
|
+
function ThreadCardTitle(props, ref) {
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
ref={ref}
|
|
126
|
+
{...mergeProps(partAttrs(threadAnatomy.scope, threadAnatomy.cardTitle), props)}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const CardBody = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
133
|
+
function ThreadCardBody(props, ref) {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
ref={ref}
|
|
137
|
+
{...mergeProps(partAttrs(threadAnatomy.scope, threadAnatomy.cardBody), props)}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const CardActions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
144
|
+
function ThreadCardActions(props, ref) {
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
ref={ref}
|
|
148
|
+
{...mergeProps(partAttrs(threadAnatomy.scope, threadAnatomy.cardActions), props)}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
88
154
|
export const Thread = {
|
|
89
155
|
Root,
|
|
90
156
|
List,
|
|
@@ -92,4 +158,9 @@ export const Thread = {
|
|
|
92
158
|
Bubble,
|
|
93
159
|
Meta,
|
|
94
160
|
Day,
|
|
161
|
+
Bind,
|
|
162
|
+
Card,
|
|
163
|
+
CardTitle,
|
|
164
|
+
CardBody,
|
|
165
|
+
CardActions,
|
|
95
166
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
cloneElement,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
type HTMLAttributes,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type Ref,
|
|
10
|
+
} from 'react'
|
|
11
|
+
import { mergeProps } from './merge-props.js'
|
|
12
|
+
|
|
13
|
+
function mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {
|
|
14
|
+
return (node: T) => {
|
|
15
|
+
for (const ref of refs) {
|
|
16
|
+
if (!ref) continue
|
|
17
|
+
if (typeof ref === 'function') ref(node)
|
|
18
|
+
else (ref as { current: T }).current = node
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SlotProps = HTMLAttributes<HTMLElement> & {
|
|
24
|
+
children?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Radix-style Slot — merges Aeon attrs onto a single child element
|
|
29
|
+
* (e.g. Next.js Link) instead of rendering a wrapper.
|
|
30
|
+
*/
|
|
31
|
+
export const Slot = forwardRef<HTMLElement, SlotProps>(function Slot(
|
|
32
|
+
{ children, ...props },
|
|
33
|
+
forwardedRef,
|
|
34
|
+
) {
|
|
35
|
+
const child = Children.only(children)
|
|
36
|
+
if (!isValidElement(child)) {
|
|
37
|
+
throw new Error('Slot expects a single React element child')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const childElement = child as ReactElement<HTMLAttributes<HTMLElement> & { ref?: Ref<HTMLElement> }>
|
|
41
|
+
const childProps = childElement.props
|
|
42
|
+
|
|
43
|
+
return cloneElement(childElement, {
|
|
44
|
+
...mergeProps(props, childProps),
|
|
45
|
+
ref: mergeRefs(forwardedRef, childProps.ref),
|
|
46
|
+
} as HTMLAttributes<HTMLElement>)
|
|
47
|
+
})
|
|
@@ -37,6 +37,8 @@ declare const componentSchemas: {
|
|
|
37
37
|
readonly progress: JsonSchema;
|
|
38
38
|
readonly radioGroup: JsonSchema;
|
|
39
39
|
readonly slider: JsonSchema;
|
|
40
|
+
readonly rangeSlider: JsonSchema;
|
|
41
|
+
readonly fileUpload: JsonSchema;
|
|
40
42
|
readonly pinInput: JsonSchema;
|
|
41
43
|
readonly avatar: JsonSchema;
|
|
42
44
|
readonly content: JsonSchema;
|
|
@@ -87,6 +89,8 @@ declare const catalog: {
|
|
|
87
89
|
readonly progress: JsonSchema;
|
|
88
90
|
readonly radioGroup: JsonSchema;
|
|
89
91
|
readonly slider: JsonSchema;
|
|
92
|
+
readonly rangeSlider: JsonSchema;
|
|
93
|
+
readonly fileUpload: JsonSchema;
|
|
90
94
|
readonly pinInput: JsonSchema;
|
|
91
95
|
readonly avatar: JsonSchema;
|
|
92
96
|
readonly content: JsonSchema;
|
|
@@ -89,11 +89,16 @@ var componentSchemas = {
|
|
|
89
89
|
},
|
|
90
90
|
variant: { type: "string", enum: ["solid", "outline", "ghost", "link"] },
|
|
91
91
|
size: { type: "string", enum: ["sm", "md", "lg"] },
|
|
92
|
-
disabled: { type: "boolean" }
|
|
92
|
+
disabled: { type: "boolean" },
|
|
93
|
+
asChild: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
description: "Merge button attrs onto a single child element (Link, a)."
|
|
96
|
+
}
|
|
93
97
|
},
|
|
94
98
|
slots: { Root: "button.root", Group: "button.group", Label: "button.label", Icon: "button.icon" },
|
|
95
99
|
examples: [
|
|
96
100
|
'<Button.Root status="pending" aria-busy>Saving\u2026</Button.Root>',
|
|
101
|
+
'<Button.Root asChild variant="solid"><a href="/upload">Upload</a></Button.Root>',
|
|
97
102
|
"const [snap, send] = useAeonMachine(buttonLifecycleMachine)\n<Button.Root status={snap.value as ButtonStatus} onClick={() => send({ type: 'PRESS' })} />"
|
|
98
103
|
]
|
|
99
104
|
}),
|
|
@@ -378,6 +383,67 @@ var componentSchemas = {
|
|
|
378
383
|
ValueText: "slider.valueText"
|
|
379
384
|
}
|
|
380
385
|
}),
|
|
386
|
+
rangeSlider: componentSchema({
|
|
387
|
+
id: "https://aeon-ui.com/schemas/rangeSlider.json",
|
|
388
|
+
title: "RangeSlider",
|
|
389
|
+
description: "Two-thumb range via rangeSliderMachine \u2014 idle | dragging with values pair and minStepsBetween gap.",
|
|
390
|
+
scope: "rangeSlider",
|
|
391
|
+
parts: ["root", "track", "range", "thumb", "valueText"],
|
|
392
|
+
states: ["idle", "dragging"],
|
|
393
|
+
machine: "rangeSliderMachine",
|
|
394
|
+
runtime: "useAeonMachine",
|
|
395
|
+
events: [
|
|
396
|
+
"SET_VALUES",
|
|
397
|
+
"SET_BOUNDS",
|
|
398
|
+
"SET_DISABLED",
|
|
399
|
+
"POINTER_DOWN",
|
|
400
|
+
"POINTER_MOVE",
|
|
401
|
+
"POINTER_UP",
|
|
402
|
+
"STEP",
|
|
403
|
+
"HOME",
|
|
404
|
+
"END"
|
|
405
|
+
],
|
|
406
|
+
slots: {
|
|
407
|
+
Root: "rangeSlider.root",
|
|
408
|
+
Track: "rangeSlider.track",
|
|
409
|
+
Range: "rangeSlider.range",
|
|
410
|
+
Thumb: "rangeSlider.thumb",
|
|
411
|
+
ValueText: "rangeSlider.valueText"
|
|
412
|
+
},
|
|
413
|
+
examples: [
|
|
414
|
+
"<RangeSlider.Root defaultValue={[20, 80]} minStepsBetween={1}>\n <RangeSlider.Track /><RangeSlider.Range />\n <RangeSlider.Thumb index={0} /><RangeSlider.Thumb index={1} />\n</RangeSlider.Root>"
|
|
415
|
+
]
|
|
416
|
+
}),
|
|
417
|
+
fileUpload: componentSchema({
|
|
418
|
+
id: "https://aeon-ui.com/schemas/fileUpload.json",
|
|
419
|
+
title: "FileUpload",
|
|
420
|
+
description: "Drag-drop surface via fileUploadMachine \u2014 idle | dragging | rejected. File bytes stay in React callbacks.",
|
|
421
|
+
scope: "fileUpload",
|
|
422
|
+
parts: [
|
|
423
|
+
"root",
|
|
424
|
+
"dropzone",
|
|
425
|
+
"input",
|
|
426
|
+
"label",
|
|
427
|
+
"hint",
|
|
428
|
+
"preview",
|
|
429
|
+
"clearTrigger",
|
|
430
|
+
"rejectionMessage"
|
|
431
|
+
],
|
|
432
|
+
states: ["idle", "dragging", "rejected"],
|
|
433
|
+
machine: "fileUploadMachine",
|
|
434
|
+
runtime: "useAeonMachine",
|
|
435
|
+
events: ["DRAG_OVER", "DRAG_LEAVE", "DROP_ACCEPT", "DROP_REJECT", "RESET"],
|
|
436
|
+
slots: {
|
|
437
|
+
Root: "fileUpload.root",
|
|
438
|
+
Dropzone: "fileUpload.dropzone",
|
|
439
|
+
Input: "fileUpload.input",
|
|
440
|
+
Label: "fileUpload.label",
|
|
441
|
+
Hint: "fileUpload.hint",
|
|
442
|
+
Preview: "fileUpload.preview",
|
|
443
|
+
ClearTrigger: "fileUpload.clearTrigger",
|
|
444
|
+
RejectionMessage: "fileUpload.rejectionMessage"
|
|
445
|
+
}
|
|
446
|
+
}),
|
|
381
447
|
pinInput: componentSchema({
|
|
382
448
|
id: "https://aeon-ui.com/schemas/pinInput.json",
|
|
383
449
|
title: "PinInput",
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
},
|
|
80
80
|
"disabled": {
|
|
81
81
|
"type": "boolean"
|
|
82
|
+
},
|
|
83
|
+
"asChild": {
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"description": "Merge button attrs onto a single child element (Link, a)."
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
},
|
|
@@ -134,6 +138,7 @@
|
|
|
134
138
|
},
|
|
135
139
|
"default": [
|
|
136
140
|
"<Button.Root status=\"pending\" aria-busy>Saving…</Button.Root>",
|
|
141
|
+
"<Button.Root asChild variant=\"solid\"><a href=\"/upload\">Upload</a></Button.Root>",
|
|
137
142
|
"const [snap, send] = useAeonMachine(buttonLifecycleMachine)\n<Button.Root status={snap.value as ButtonStatus} onClick={() => send({ type: 'PRESS' })} />"
|
|
138
143
|
]
|
|
139
144
|
}
|
|
@@ -164,6 +169,7 @@
|
|
|
164
169
|
},
|
|
165
170
|
"examples": [
|
|
166
171
|
"<Button.Root status=\"pending\" aria-busy>Saving…</Button.Root>",
|
|
172
|
+
"<Button.Root asChild variant=\"solid\"><a href=\"/upload\">Upload</a></Button.Root>",
|
|
167
173
|
"const [snap, send] = useAeonMachine(buttonLifecycleMachine)\n<Button.Root status={snap.value as ButtonStatus} onClick={() => send({ type: 'PRESS' })} />"
|
|
168
174
|
]
|
|
169
175
|
}
|