aeon-ui-engine 1.3.1 → 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.
Files changed (153) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/package.json +1 -1
  3. package/packages/cli/README.md +13 -1
  4. package/packages/cli/dist/bin.js +23 -5
  5. package/packages/cli/dist/chunk-5L47SLNV.js +247 -0
  6. package/packages/cli/dist/index.d.ts +17 -1
  7. package/packages/cli/dist/index.js +5 -3
  8. package/packages/cli/src/bin.ts +21 -3
  9. package/packages/cli/src/index.ts +1 -0
  10. package/packages/cli/src/migrate.ts +170 -0
  11. package/packages/core/dist/index.d.ts +35 -7
  12. package/packages/core/dist/index.js +102 -2
  13. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  14. package/packages/core/src/anatomy.ts +39 -3
  15. package/packages/core/src/identicon.test.ts +39 -0
  16. package/packages/core/src/identicon.ts +79 -0
  17. package/packages/core/src/index.ts +1 -0
  18. package/packages/panda/panda.config.ts +4 -0
  19. package/packages/panda/src/recipes/composer.ts +58 -4
  20. package/packages/panda/src/recipes/file-upload.ts +100 -0
  21. package/packages/panda/src/recipes/index.ts +2 -0
  22. package/packages/panda/src/recipes/range-slider.ts +59 -0
  23. package/packages/panda/src/recipes/thread.ts +77 -2
  24. package/packages/panda/styled-system/recipes/aeon-composer.d.ts +1 -1
  25. package/packages/panda/styled-system/recipes/aeon-composer.mjs +12 -0
  26. package/packages/panda/styled-system/recipes/aeon-file-upload.d.ts +31 -0
  27. package/packages/panda/styled-system/recipes/aeon-file-upload.mjs +61 -0
  28. package/packages/panda/styled-system/recipes/aeon-range-slider.d.ts +31 -0
  29. package/packages/panda/styled-system/recipes/aeon-range-slider.mjs +49 -0
  30. package/packages/panda/styled-system/recipes/aeon-thread.d.ts +1 -1
  31. package/packages/panda/styled-system/recipes/aeon-thread.mjs +20 -0
  32. package/packages/panda/styled-system/recipes/index.d.ts +2 -0
  33. package/packages/panda/styled-system/recipes/index.mjs +2 -0
  34. package/packages/panda/styled-system/styles.css +292 -0
  35. package/packages/primitives/dist/chunk-CV4UXXZF.js +44 -0
  36. package/packages/primitives/dist/chunk-IZKQSCYU.js +164 -0
  37. package/packages/primitives/dist/index.d.ts +2 -0
  38. package/packages/primitives/dist/index.js +22 -6
  39. package/packages/primitives/dist/machines/file-upload.d.ts +39 -0
  40. package/packages/primitives/dist/machines/file-upload.js +8 -0
  41. package/packages/primitives/dist/machines/range-slider.d.ts +129 -0
  42. package/packages/primitives/dist/machines/range-slider.js +12 -0
  43. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  44. package/packages/primitives/src/__tests__/file-upload.test.ts +25 -0
  45. package/packages/primitives/src/__tests__/range-slider.test.ts +39 -0
  46. package/packages/primitives/src/index.ts +2 -0
  47. package/packages/primitives/src/machines/file-upload.ts +55 -0
  48. package/packages/primitives/src/machines/range-slider.ts +209 -0
  49. package/packages/react/dist/index.d.ts +117 -25
  50. package/packages/react/dist/index.js +1731 -1006
  51. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  52. package/packages/react/src/button/index.tsx +50 -15
  53. package/packages/react/src/combobox/index.tsx +13 -8
  54. package/packages/react/src/composer/index.tsx +58 -3
  55. package/packages/react/src/file-upload/index.tsx +322 -0
  56. package/packages/react/src/index.ts +3 -0
  57. package/packages/react/src/prompt/index.tsx +45 -0
  58. package/packages/react/src/range-slider/index.tsx +322 -0
  59. package/packages/react/src/thread/index.tsx +74 -3
  60. package/packages/react/src/tooltip/index.tsx +48 -22
  61. package/packages/react/src/utils/slot.tsx +47 -0
  62. package/packages/schemas/dist/index.d.ts +73 -24
  63. package/packages/schemas/dist/index.js +514 -50
  64. package/packages/schemas/json/appNav.json +146 -0
  65. package/packages/schemas/json/appShell.json +151 -0
  66. package/packages/schemas/json/avatar.json +130 -0
  67. package/packages/schemas/json/bar.json +126 -0
  68. package/packages/schemas/json/button.json +6 -0
  69. package/packages/schemas/json/catalog.json +3496 -15
  70. package/packages/schemas/json/combobox.json +19 -7
  71. package/packages/schemas/json/composer.json +125 -0
  72. package/packages/schemas/json/content.json +167 -0
  73. package/packages/schemas/json/conversation.json +166 -0
  74. package/packages/schemas/json/entry.json +182 -0
  75. package/packages/schemas/json/fileUpload.json +168 -0
  76. package/packages/schemas/json/identity.json +147 -0
  77. package/packages/schemas/json/listRow.json +131 -0
  78. package/packages/schemas/json/metricStrip.json +132 -0
  79. package/packages/schemas/json/nav.json +136 -0
  80. package/packages/schemas/json/panel.json +143 -0
  81. package/packages/schemas/json/pinInput.json +112 -0
  82. package/packages/schemas/json/profileHeader.json +138 -0
  83. package/packages/schemas/json/prompt.json +155 -0
  84. package/packages/schemas/json/radioGroup.json +130 -0
  85. package/packages/schemas/json/rangeSlider.json +157 -0
  86. package/packages/schemas/json/separator.json +105 -0
  87. package/packages/schemas/json/slider.json +147 -0
  88. package/packages/schemas/json/statusBanner.json +149 -0
  89. package/packages/schemas/json/stickyBar.json +153 -0
  90. package/packages/schemas/json/thread.json +129 -0
  91. package/packages/schemas/json/tooltip.json +21 -7
  92. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  93. package/packages/schemas/prompts/cursorrules.md +6 -4
  94. package/packages/schemas/prompts/statecharts.md +23 -2
  95. package/packages/schemas/prompts/system.md +2 -2
  96. package/packages/schemas/registry/catalog.json +313 -4
  97. package/packages/schemas/registry/components/appNav.json +146 -0
  98. package/packages/schemas/registry/components/appShell.json +151 -0
  99. package/packages/schemas/registry/components/avatar.json +130 -0
  100. package/packages/schemas/registry/components/bar.json +126 -0
  101. package/packages/schemas/registry/components/button.json +6 -0
  102. package/packages/schemas/registry/components/combobox.json +19 -7
  103. package/packages/schemas/registry/components/composer.json +125 -0
  104. package/packages/schemas/registry/components/content.json +167 -0
  105. package/packages/schemas/registry/components/conversation.json +166 -0
  106. package/packages/schemas/registry/components/entry.json +182 -0
  107. package/packages/schemas/registry/components/fileUpload.json +168 -0
  108. package/packages/schemas/registry/components/identity.json +147 -0
  109. package/packages/schemas/registry/components/listRow.json +131 -0
  110. package/packages/schemas/registry/components/metricStrip.json +132 -0
  111. package/packages/schemas/registry/components/nav.json +136 -0
  112. package/packages/schemas/registry/components/panel.json +143 -0
  113. package/packages/schemas/registry/components/pinInput.json +112 -0
  114. package/packages/schemas/registry/components/profileHeader.json +138 -0
  115. package/packages/schemas/registry/components/prompt.json +155 -0
  116. package/packages/schemas/registry/components/radioGroup.json +130 -0
  117. package/packages/schemas/registry/components/rangeSlider.json +157 -0
  118. package/packages/schemas/registry/components/separator.json +105 -0
  119. package/packages/schemas/registry/components/slider.json +147 -0
  120. package/packages/schemas/registry/components/statusBanner.json +149 -0
  121. package/packages/schemas/registry/components/stickyBar.json +153 -0
  122. package/packages/schemas/registry/components/thread.json +129 -0
  123. package/packages/schemas/registry/components/tooltip.json +21 -7
  124. package/packages/schemas/registry/machines/accordion.json +58 -0
  125. package/packages/schemas/registry/machines/appNav.json +108 -0
  126. package/packages/schemas/registry/machines/appShell.json +128 -0
  127. package/packages/schemas/registry/machines/contentRegion.json +314 -0
  128. package/packages/schemas/registry/machines/fileUpload.json +182 -0
  129. package/packages/schemas/registry/machines/panel.json +131 -0
  130. package/packages/schemas/registry/machines/popover.json +143 -0
  131. package/packages/schemas/registry/machines/rangeSlider.json +201 -0
  132. package/packages/schemas/registry/machines/slider.json +151 -0
  133. package/packages/schemas/registry/machines/stickyBar.json +261 -0
  134. package/packages/schemas/registry/machines/tabs.json +58 -0
  135. package/packages/schemas/registry/machines/toast.json +89 -0
  136. package/packages/schemas/registry/machines/toggle.json +134 -0
  137. package/packages/schemas/scripts/emit.ts +27 -0
  138. package/packages/schemas/src/components.ts +499 -6
  139. package/packages/schemas/src/index.test.ts +83 -0
  140. package/packages/schemas/src/index.ts +1 -0
  141. package/packages/schemas/src/machine-id.ts +7 -0
  142. package/packages/schemas/src/prompts.ts +25 -4
  143. package/packages/schemas/src/registry.ts +28 -39
  144. package/packages/schemas/tsconfig.json +1 -1
  145. package/packages/ui/dist/index.d.ts +100 -46
  146. package/packages/ui/dist/index.js +463 -341
  147. package/packages/ui/src/composer.tsx +37 -11
  148. package/packages/ui/src/file-upload.tsx +36 -0
  149. package/packages/ui/src/index.ts +2 -0
  150. package/packages/ui/src/prompt.tsx +3 -0
  151. package/packages/ui/src/range-slider.tsx +24 -0
  152. package/packages/ui/src/thread.tsx +40 -0
  153. 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 { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
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. Item `state` projects mine | theirs | pending | failed.
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} ${string}`
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
  }
@@ -1,7 +1,9 @@
1
1
  import { tooltipAnatomy, partAttrs } from '@aeon-ui/core'
2
+ import { popoverMachine } from '@aeon-ui/primitives'
2
3
  import {
3
4
  createContext,
4
5
  forwardRef,
6
+ useCallback,
5
7
  useContext,
6
8
  useEffect,
7
9
  useId,
@@ -15,6 +17,7 @@ import {
15
17
  type ReactNode,
16
18
  } from 'react'
17
19
  import { FloatingPositioner } from '../floating/FloatingPositioner.js'
20
+ import { useAeonMachine } from '../hooks/use-aeon-machine.js'
18
21
  import { useOutsideClick } from '../hooks/use-outside-click.js'
19
22
  import { usePrefersHover } from '../hooks/use-prefers-hover.js'
20
23
  import { mergeProps } from '../utils/merge-props.js'
@@ -76,8 +79,10 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
76
79
  },
77
80
  ref,
78
81
  ) {
79
- const [uncontrolled, setUncontrolled] = useState(defaultOpen)
80
- const resolvedOpen = open ?? uncontrolled
82
+ const [snapshot, send] = useAeonMachine(popoverMachine, {
83
+ input: { open: defaultOpen },
84
+ })
85
+ const resolvedOpen = open ?? snapshot.matches('open')
81
86
  const showTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
82
87
  const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
83
88
  const triggerRef = useRef<HTMLSpanElement>(null)
@@ -87,6 +92,10 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
87
92
  const prefersHover = usePrefersHover()
88
93
  const [outsideClickEnabled, setOutsideClickEnabled] = useState(false)
89
94
 
95
+ useEffect(() => {
96
+ if (open !== undefined) send({ type: 'SET_OPEN', open })
97
+ }, [open, send])
98
+
90
99
  useEffect(() => {
91
100
  if (!resolvedOpen || prefersHover) {
92
101
  setOutsideClickEnabled(false)
@@ -97,52 +106,57 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
97
106
  return () => window.clearTimeout(id)
98
107
  }, [resolvedOpen, prefersHover, touchOutsideGraceMs])
99
108
 
100
- const setOpen = (next: boolean) => {
101
- if (open === undefined) setUncontrolled(next)
102
- onOpenChange?.(next)
103
- }
109
+ const setOpen = useCallback(
110
+ (next: boolean) => {
111
+ if (open === undefined) send({ type: next ? 'OPEN' : 'CLOSE' })
112
+ onOpenChange?.(next)
113
+ },
114
+ [open, send, onOpenChange],
115
+ )
104
116
 
105
117
  useEffect(() => {
106
118
  onOpenChange?.(resolvedOpen)
107
119
  }, [resolvedOpen, onOpenChange])
108
120
 
109
- const hideNow = () => {
121
+ const hideNow = useCallback(() => {
110
122
  if (showTimer.current !== undefined) clearTimeout(showTimer.current)
111
123
  if (hideTimer.current !== undefined) clearTimeout(hideTimer.current)
112
124
  setOpen(false)
113
- }
125
+ }, [setOpen])
114
126
 
115
- const showImmediately = () => {
127
+ const showImmediately = useCallback(() => {
116
128
  if (showTimer.current !== undefined) clearTimeout(showTimer.current)
117
129
  if (hideTimer.current !== undefined) clearTimeout(hideTimer.current)
118
130
  setOpen(true)
119
- if (!prefersHover) {
120
- if (touchDurationMs > 0) {
121
- hideTimer.current = setTimeout(() => hideNow(), touchDurationMs)
122
- }
131
+ if (!prefersHover && touchDurationMs > 0) {
132
+ hideTimer.current = setTimeout(() => hideNow(), touchDurationMs)
123
133
  }
124
- }
134
+ }, [setOpen, prefersHover, touchDurationMs, hideNow])
125
135
 
126
- const scheduleShow = () => {
136
+ const scheduleShow = useCallback(() => {
127
137
  if (hideTimer.current !== undefined) clearTimeout(hideTimer.current)
128
138
  showTimer.current = setTimeout(() => setOpen(true), openDelay)
129
- }
139
+ }, [setOpen, openDelay])
130
140
 
131
- const scheduleHide = () => {
141
+ const scheduleHide = useCallback(() => {
132
142
  if (showTimer.current !== undefined) clearTimeout(showTimer.current)
133
143
  if (closeDelay <= 0) {
134
144
  setOpen(false)
135
145
  return
136
146
  }
137
147
  hideTimer.current = setTimeout(() => setOpen(false), closeDelay)
138
- }
148
+ }, [setOpen, closeDelay])
139
149
 
140
- const toggleTouch = () => {
150
+ const toggleTouch = useCallback(() => {
141
151
  if (resolvedOpen) hideNow()
142
152
  else showImmediately()
143
- }
153
+ }, [resolvedOpen, hideNow, showImmediately])
144
154
 
145
- useOutsideClick([triggerRef, positionerRef], resolvedOpen && !prefersHover && outsideClickEnabled, hideNow)
155
+ useOutsideClick(
156
+ [triggerRef, positionerRef],
157
+ resolvedOpen && !prefersHover && outsideClickEnabled,
158
+ hideNow,
159
+ )
146
160
 
147
161
  const value = useMemo(
148
162
  () => ({
@@ -160,7 +174,19 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
160
174
  showOnFocus,
161
175
  prefersHover,
162
176
  }),
163
- [resolvedOpen, triggerId, contentId, openDelay, closeDelay, touchDurationMs, touchOutsideGraceMs, showOnFocus, prefersHover],
177
+ [
178
+ resolvedOpen,
179
+ setOpen,
180
+ triggerId,
181
+ contentId,
182
+ scheduleShow,
183
+ scheduleHide,
184
+ showImmediately,
185
+ hideNow,
186
+ toggleTouch,
187
+ showOnFocus,
188
+ prefersHover,
189
+ ],
164
190
  )
165
191
 
166
192
  return (
@@ -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
+ })