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
@@ -1 +1 @@
1
- {"version":"3.2.4","results":[[":src/select/select.test.tsx",{"duration":226.72845800000005,"failed":false}],[":src/dialog/dialog.test.tsx",{"duration":181.9869580000002,"failed":false}],[":src/avatar/avatar.test.tsx",{"duration":144.93595800000003,"failed":false}],[":src/tooltip/tooltip.test.tsx",{"duration":226.42358300000024,"failed":false}],[":src/toast/toast.test.tsx",{"duration":126.48416699999984,"failed":false}]]}
1
+ {"version":"3.2.4","results":[[":src/select/select.test.tsx",{"duration":233.12287500000002,"failed":false}],[":src/dialog/dialog.test.tsx",{"duration":197.85458300000005,"failed":false}],[":src/avatar/avatar.test.tsx",{"duration":175.06837500000006,"failed":false}],[":src/tooltip/tooltip.test.tsx",{"duration":236.69991700000014,"failed":false}],[":src/toast/toast.test.tsx",{"duration":174.01087499999994,"failed":false}]]}
@@ -1,6 +1,12 @@
1
1
  import { buttonAnatomy, partAttrs } from '@aeon-ui/core'
2
- import { forwardRef, type ButtonHTMLAttributes, 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'
9
+ import { Slot } from '../utils/slot.js'
4
10
 
5
11
  /** Action button lifecycle — see STATES.md and `buttonLifecycleMachine`. */
6
12
  export type ButtonStatus = 'idle' | 'pending' | 'success' | 'failure' | 'disabled'
@@ -10,34 +16,63 @@ export interface ButtonRootProps extends ButtonHTMLAttributes<HTMLButtonElement>
10
16
  size?: 'xs' | 'sm' | 'md' | 'lg'
11
17
  /** Machine-aligned lifecycle; `disabled` prop overrides to `disabled`. */
12
18
  status?: ButtonStatus
19
+ /**
20
+ * Merge button attrs onto a single child (e.g. Next.js `<Link>` / `<a>`).
21
+ * Child must be one React element.
22
+ */
23
+ asChild?: boolean
13
24
  children?: ReactNode
14
25
  }
15
26
 
16
27
  const Root = forwardRef<HTMLButtonElement, ButtonRootProps>(function ButtonRoot(
17
- { variant = 'solid', size = 'md', disabled, status = 'idle', children, className, ...rest },
28
+ {
29
+ variant = 'solid',
30
+ size = 'md',
31
+ disabled,
32
+ status = 'idle',
33
+ asChild = false,
34
+ children,
35
+ className,
36
+ type,
37
+ ...rest
38
+ },
18
39
  ref,
19
40
  ) {
20
41
  const resolvedStatus: ButtonStatus = disabled ? 'disabled' : status
21
42
  const isBusy = resolvedStatus === 'pending'
22
43
  // Success is terminal until RESET — keep data-aeon-state="success" but block input.
23
44
  const isLocked = resolvedStatus === 'success'
45
+ const isDisabled = Boolean(disabled || isBusy || isLocked || resolvedStatus === 'disabled')
46
+
47
+ const aeonProps = {
48
+ className,
49
+ 'data-aeon-variant': variant,
50
+ 'data-aeon-size': size,
51
+ 'aria-busy': isBusy || undefined,
52
+ 'aria-disabled': asChild && isDisabled ? true : undefined,
53
+ ...mergeProps(
54
+ partAttrs(buttonAnatomy.scope, buttonAnatomy.root, {
55
+ state: resolvedStatus,
56
+ disabled: isDisabled,
57
+ }),
58
+ rest,
59
+ ),
60
+ }
61
+
62
+ if (asChild) {
63
+ return (
64
+ <Slot ref={ref as never} {...aeonProps}>
65
+ {children}
66
+ </Slot>
67
+ )
68
+ }
24
69
 
25
70
  return (
26
71
  <button
27
72
  ref={ref}
28
- type="button"
29
- disabled={disabled || isBusy || isLocked || resolvedStatus === 'disabled'}
30
- aria-busy={isBusy || undefined}
31
- className={className}
32
- data-aeon-variant={variant}
33
- data-aeon-size={size}
34
- {...mergeProps(
35
- partAttrs(buttonAnatomy.scope, buttonAnatomy.root, {
36
- state: resolvedStatus,
37
- disabled: Boolean(disabled || resolvedStatus === 'disabled' || isLocked),
38
- }),
39
- rest,
40
- )}
73
+ type={type ?? 'button'}
74
+ disabled={isDisabled}
75
+ {...aeonProps}
41
76
  >
42
77
  {children}
43
78
  </button>
@@ -1,4 +1,5 @@
1
1
  import { comboboxAnatomy, partAttrs, partOnlyAttrs } from '@aeon-ui/core'
2
+ import { popoverMachine } from '@aeon-ui/primitives'
2
3
  import {
3
4
  createContext,
4
5
  forwardRef,
@@ -18,6 +19,7 @@ import {
18
19
  type MouseEvent,
19
20
  type ReactNode,
20
21
  } from 'react'
22
+ import { useAeonMachine } from '../hooks/use-aeon-machine.js'
21
23
  import { mergeProps } from '../utils/merge-props.js'
22
24
 
23
25
  interface RegisteredItem {
@@ -73,23 +75,27 @@ const Root = forwardRef<HTMLDivElement, ComboboxRootProps>(function ComboboxRoot
73
75
  { value, onValueChange, children, className, disabled },
74
76
  ref,
75
77
  ) {
76
- const [open, setOpenState] = useState(false)
78
+ const [snapshot, send] = useAeonMachine(popoverMachine, { input: { open: false } })
77
79
  const [query, setQuery] = useState('')
78
80
  const [highlightedValue, setHighlightedValue] = useState<string | null>(null)
79
81
  const [itemsVersion, setItemsVersion] = useState(0)
80
82
  const itemsRef = useRef<RegisteredItem[]>([])
81
83
  const listboxId = useId()
82
84
 
85
+ const open = disabled ? false : snapshot.matches('open')
86
+
83
87
  const setOpen = useCallback(
84
88
  (next: boolean) => {
85
89
  if (disabled) return
86
- setOpenState(next)
87
- if (!next) {
88
- setQuery('')
89
- setHighlightedValue(null)
90
+ if (next) {
91
+ send({ type: 'OPEN' })
92
+ return
90
93
  }
94
+ send({ type: 'CLOSE' })
95
+ setQuery('')
96
+ setHighlightedValue(null)
91
97
  },
92
- [disabled],
98
+ [disabled, send],
93
99
  )
94
100
 
95
101
  const registerItem = useCallback((item: RegisteredItem) => {
@@ -107,7 +113,7 @@ const Root = forwardRef<HTMLDivElement, ComboboxRootProps>(function ComboboxRoot
107
113
  const ctx = useMemo(
108
114
  () => ({
109
115
  value,
110
- open: disabled ? false : open,
116
+ open,
111
117
  query,
112
118
  listboxId,
113
119
  highlightedValue,
@@ -132,7 +138,6 @@ const Root = forwardRef<HTMLDivElement, ComboboxRootProps>(function ComboboxRoot
132
138
  registerItem,
133
139
  unregisterItem,
134
140
  getItems,
135
- disabled,
136
141
  ],
137
142
  )
138
143
 
@@ -1,6 +1,7 @@
1
1
  import { composerAnatomy, partAttrs, scopeAttrs } from '@aeon-ui/core'
2
2
  import {
3
3
  forwardRef,
4
+ type ButtonHTMLAttributes,
4
5
  type FormHTMLAttributes,
5
6
  type HTMLAttributes,
6
7
  type ReactNode,
@@ -8,7 +9,21 @@ import {
8
9
  } from 'react'
9
10
  import { mergeProps } from '../utils/merge-props.js'
10
11
 
11
- export type ComposerState = 'idle' | 'sending' | 'disabled' | 'error'
12
+ /**
13
+ * Composer root states.
14
+ * - chat: plain message compose
15
+ * - command: local slash input (BRC-218) — never from received text
16
+ * - lookup: non-confirming verbs (/whois, /help)
17
+ * - idle | sending | disabled | error: transport chrome
18
+ */
19
+ export type ComposerState =
20
+ | 'idle'
21
+ | 'sending'
22
+ | 'disabled'
23
+ | 'error'
24
+ | 'chat'
25
+ | 'command'
26
+ | 'lookup'
12
27
 
13
28
  export interface ComposerRootProps extends FormHTMLAttributes<HTMLFormElement> {
14
29
  state?: ComposerState
@@ -16,8 +31,8 @@ export interface ComposerRootProps extends FormHTMLAttributes<HTMLFormElement> {
16
31
  }
17
32
 
18
33
  /**
19
- * Composer — bottom compose chrome for threads (input + send).
20
- * Root state: idle | sending | disabled | error.
34
+ * Composer — bottom compose chrome for threads (input + send + command palette).
35
+ * Only local user input is command-eligible (BRC-218 §2.4).
21
36
  */
22
37
  const Root = forwardRef<HTMLFormElement, ComposerRootProps>(function ComposerRoot(
23
38
  { state = 'idle', children, ...rest },
@@ -67,9 +82,49 @@ const Send = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
67
82
  },
68
83
  )
69
84
 
85
+ const Toolbar = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
86
+ function ComposerToolbar(props, ref) {
87
+ return (
88
+ <div
89
+ ref={ref}
90
+ role="toolbar"
91
+ {...mergeProps(partAttrs(composerAnatomy.scope, composerAnatomy.toolbar), props)}
92
+ />
93
+ )
94
+ },
95
+ )
96
+
97
+ const Suggestions = forwardRef<HTMLUListElement, HTMLAttributes<HTMLUListElement>>(
98
+ function ComposerSuggestions(props, ref) {
99
+ return (
100
+ <ul
101
+ ref={ref}
102
+ role="listbox"
103
+ {...mergeProps(partAttrs(composerAnatomy.scope, composerAnatomy.suggestions), props)}
104
+ />
105
+ )
106
+ },
107
+ )
108
+
109
+ const Suggestion = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
110
+ function ComposerSuggestion(props, ref) {
111
+ return (
112
+ <button
113
+ ref={ref}
114
+ type="button"
115
+ role="option"
116
+ {...mergeProps(partAttrs(composerAnatomy.scope, composerAnatomy.suggestion), props)}
117
+ />
118
+ )
119
+ },
120
+ )
121
+
70
122
  export const Composer = {
71
123
  Root,
72
124
  Input,
73
125
  Actions,
74
126
  Send,
127
+ Toolbar,
128
+ Suggestions,
129
+ Suggestion,
75
130
  }
@@ -0,0 +1,322 @@
1
+ import { fileUploadAnatomy, partAttrs, partOnlyAttrs } from '@aeon-ui/core'
2
+ import { fileUploadMachine, getFileUploadPhase } from '@aeon-ui/primitives'
3
+ import {
4
+ createContext,
5
+ forwardRef,
6
+ useCallback,
7
+ useContext,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ChangeEvent,
13
+ type DragEvent,
14
+ type HTMLAttributes,
15
+ type InputHTMLAttributes,
16
+ type MouseEvent,
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
+ function isFileTypeAccepted(file: File, acceptString: string) {
23
+ if (!acceptString || acceptString === '*') return true
24
+ const acceptedTypes = acceptString.split(',').map((type) => type.trim()).filter(Boolean)
25
+ if (acceptedTypes.length === 0) return true
26
+ return acceptedTypes.some((acceptedType) => {
27
+ if (acceptedType.startsWith('.')) {
28
+ return file.name.toLowerCase().endsWith(acceptedType.toLowerCase())
29
+ }
30
+ if (acceptedType.includes('*')) {
31
+ const baseType = acceptedType.split('/')[0]
32
+ return file.type.startsWith(`${baseType}/`)
33
+ }
34
+ return file.type === acceptedType
35
+ })
36
+ }
37
+
38
+ interface FileUploadContextValue {
39
+ phase: 'idle' | 'dragging' | 'rejected'
40
+ accept: string
41
+ disabled: boolean
42
+ file: File | null
43
+ inputId: string
44
+ inputRef: React.RefObject<HTMLInputElement | null>
45
+ send: ReturnType<typeof useAeonMachine<typeof fileUploadMachine>>[1]
46
+ selectFile: (file: File | null) => void
47
+ clear: () => void
48
+ openPicker: () => void
49
+ }
50
+
51
+ const FileUploadCtx = createContext<FileUploadContextValue | null>(null)
52
+
53
+ function useFileUploadCtx() {
54
+ const ctx = useContext(FileUploadCtx)
55
+ if (!ctx) throw new Error('FileUpload parts must be used within FileUpload.Root')
56
+ return ctx
57
+ }
58
+
59
+ export interface FileUploadRootProps {
60
+ accept?: string
61
+ disabled?: boolean
62
+ /** Controlled selected file (bytes stay outside the machine). */
63
+ file?: File | null
64
+ defaultFile?: File | null
65
+ onFileSelect?: (file: File | null) => void
66
+ children?: ReactNode
67
+ className?: string
68
+ }
69
+
70
+ const Root = forwardRef<HTMLDivElement, FileUploadRootProps>(function FileUploadRoot(
71
+ {
72
+ accept = '*',
73
+ disabled = false,
74
+ file: fileProp,
75
+ defaultFile = null,
76
+ onFileSelect,
77
+ children,
78
+ className,
79
+ ...rest
80
+ },
81
+ ref,
82
+ ) {
83
+ const inputId = useId()
84
+ const inputRef = useRef<HTMLInputElement>(null)
85
+ const [snapshot, send] = useAeonMachine(fileUploadMachine)
86
+ const [uncontrolledFile, setUncontrolledFile] = useState<File | null>(defaultFile)
87
+ const file = fileProp !== undefined ? fileProp : uncontrolledFile
88
+ const phase = getFileUploadPhase(snapshot.value)
89
+
90
+ const selectFile = useCallback(
91
+ (next: File | null) => {
92
+ if (fileProp === undefined) setUncontrolledFile(next)
93
+ onFileSelect?.(next)
94
+ },
95
+ [fileProp, onFileSelect],
96
+ )
97
+
98
+ const clear = useCallback(() => {
99
+ send({ type: 'RESET' })
100
+ selectFile(null)
101
+ if (inputRef.current) inputRef.current.value = ''
102
+ }, [send, selectFile])
103
+
104
+ const openPicker = useCallback(() => {
105
+ if (disabled) return
106
+ inputRef.current?.click()
107
+ }, [disabled])
108
+
109
+ const ctx = useMemo(
110
+ () => ({
111
+ phase,
112
+ accept,
113
+ disabled,
114
+ file,
115
+ inputId,
116
+ inputRef,
117
+ send,
118
+ selectFile,
119
+ clear,
120
+ openPicker,
121
+ }),
122
+ [phase, accept, disabled, file, inputId, send, selectFile, clear, openPicker],
123
+ )
124
+
125
+ return (
126
+ <FileUploadCtx.Provider value={ctx}>
127
+ <div
128
+ ref={ref}
129
+ className={className}
130
+ {...mergeProps(
131
+ partAttrs(fileUploadAnatomy.scope, fileUploadAnatomy.root, {
132
+ state: phase,
133
+ disabled,
134
+ }),
135
+ rest as HTMLAttributes<HTMLDivElement>,
136
+ )}
137
+ >
138
+ {children}
139
+ </div>
140
+ </FileUploadCtx.Provider>
141
+ )
142
+ })
143
+
144
+ const Label = forwardRef<HTMLLabelElement, HTMLAttributes<HTMLLabelElement>>(function FileUploadLabel(
145
+ props,
146
+ ref,
147
+ ) {
148
+ const { inputId } = useFileUploadCtx()
149
+ return (
150
+ <label
151
+ ref={ref}
152
+ htmlFor={inputId}
153
+ {...mergeProps(partOnlyAttrs(fileUploadAnatomy.label), props)}
154
+ />
155
+ )
156
+ })
157
+
158
+ const Dropzone = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function FileUploadDropzone(
159
+ { onClick, onDragOver, onDragLeave, onDrop, ...rest },
160
+ ref,
161
+ ) {
162
+ const { accept, disabled, send, selectFile, openPicker, phase } = useFileUploadCtx()
163
+
164
+ return (
165
+ <div
166
+ ref={ref}
167
+ role="button"
168
+ tabIndex={disabled ? -1 : 0}
169
+ aria-disabled={disabled || undefined}
170
+ onKeyDown={(e) => {
171
+ if (disabled) return
172
+ if (e.key === 'Enter' || e.key === ' ') {
173
+ e.preventDefault()
174
+ openPicker()
175
+ }
176
+ }}
177
+ {...mergeProps(
178
+ partOnlyAttrs(fileUploadAnatomy.dropzone, {
179
+ state: phase,
180
+ disabled,
181
+ }),
182
+ {
183
+ onClick: (e: MouseEvent<HTMLDivElement>) => {
184
+ onClick?.(e)
185
+ if (disabled || e.defaultPrevented) return
186
+ openPicker()
187
+ },
188
+ onDragOver: (e: DragEvent<HTMLDivElement>) => {
189
+ onDragOver?.(e)
190
+ e.preventDefault()
191
+ if (disabled) return
192
+ send({ type: 'DRAG_OVER' })
193
+ },
194
+ onDragLeave: (e: DragEvent<HTMLDivElement>) => {
195
+ onDragLeave?.(e)
196
+ e.preventDefault()
197
+ if (disabled) return
198
+ send({ type: 'DRAG_LEAVE' })
199
+ },
200
+ onDrop: (e: DragEvent<HTMLDivElement>) => {
201
+ onDrop?.(e)
202
+ e.preventDefault()
203
+ if (disabled) return
204
+ const next = Array.from(e.dataTransfer.files)[0]
205
+ if (next && isFileTypeAccepted(next, accept)) {
206
+ send({ type: 'DROP_ACCEPT' })
207
+ selectFile(next)
208
+ } else {
209
+ send({ type: 'DROP_REJECT' })
210
+ }
211
+ },
212
+ },
213
+ rest,
214
+ )}
215
+ />
216
+ )
217
+ })
218
+
219
+ const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
220
+ function FileUploadInput({ onChange, ...rest }, ref) {
221
+ const { accept, disabled, inputId, inputRef, selectFile, send } = useFileUploadCtx()
222
+
223
+ return (
224
+ <input
225
+ ref={(node) => {
226
+ inputRef.current = node
227
+ if (typeof ref === 'function') ref(node)
228
+ else if (ref) ref.current = node
229
+ }}
230
+ id={inputId}
231
+ type="file"
232
+ accept={accept}
233
+ disabled={disabled}
234
+ {...mergeProps(
235
+ partOnlyAttrs(fileUploadAnatomy.input),
236
+ {
237
+ onChange: (e: ChangeEvent<HTMLInputElement>) => {
238
+ onChange?.(e)
239
+ const next = e.target.files?.[0] ?? null
240
+ if (next) {
241
+ send({ type: 'RESET' })
242
+ selectFile(next)
243
+ }
244
+ },
245
+ },
246
+ rest,
247
+ )}
248
+ />
249
+ )
250
+ },
251
+ )
252
+
253
+ const Hint = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
254
+ function FileUploadHint(props, ref) {
255
+ return <p ref={ref} {...mergeProps(partOnlyAttrs(fileUploadAnatomy.hint), props)} />
256
+ },
257
+ )
258
+
259
+ const Preview = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function FileUploadPreview(
260
+ { children, ...rest },
261
+ ref,
262
+ ) {
263
+ const { file } = useFileUploadCtx()
264
+ if (!file && !children) return null
265
+ return (
266
+ <div ref={ref} {...mergeProps(partOnlyAttrs(fileUploadAnatomy.preview), rest)}>
267
+ {children ?? file?.name}
268
+ </div>
269
+ )
270
+ })
271
+
272
+ const ClearTrigger = forwardRef<HTMLButtonElement, HTMLAttributes<HTMLButtonElement>>(
273
+ function FileUploadClearTrigger({ onClick, ...rest }, ref) {
274
+ const { clear, disabled, file } = useFileUploadCtx()
275
+ if (!file) return null
276
+ return (
277
+ <button
278
+ ref={ref}
279
+ type="button"
280
+ disabled={disabled}
281
+ {...mergeProps(
282
+ partOnlyAttrs(fileUploadAnatomy.clearTrigger),
283
+ {
284
+ onClick: (e: MouseEvent<HTMLButtonElement>) => {
285
+ onClick?.(e)
286
+ if (e.defaultPrevented) return
287
+ clear()
288
+ },
289
+ },
290
+ rest,
291
+ )}
292
+ />
293
+ )
294
+ },
295
+ )
296
+
297
+ const RejectionMessage = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
298
+ function FileUploadRejectionMessage(props, ref) {
299
+ const { phase } = useFileUploadCtx()
300
+ if (phase !== 'rejected') return null
301
+ return (
302
+ <p
303
+ ref={ref}
304
+ role="alert"
305
+ {...mergeProps(partOnlyAttrs(fileUploadAnatomy.rejectionMessage), props)}
306
+ />
307
+ )
308
+ },
309
+ )
310
+
311
+ export const FileUpload = {
312
+ Root,
313
+ Label,
314
+ Dropzone,
315
+ Input,
316
+ Hint,
317
+ Preview,
318
+ ClearTrigger,
319
+ RejectionMessage,
320
+ }
321
+
322
+ export { useFileUploadCtx as useFileUpload }
@@ -9,6 +9,8 @@ export * from './separator/index.js'
9
9
  export * from './progress/index.js'
10
10
  export * from './radio-group/index.js'
11
11
  export * from './slider/index.js'
12
+ export * from './range-slider/index.js'
13
+ export * from './file-upload/index.js'
12
14
  export * from './pin-input/index.js'
13
15
  export * from './accordion/index.js'
14
16
  export * from './async/index.js'
@@ -40,3 +42,4 @@ export * from './sticky-bar/index.js'
40
42
  export { useAeonMachine } from './hooks/use-aeon-machine.js'
41
43
  export { useLayoutMode } from './hooks/use-layout-mode.js'
42
44
  export { useFitText } from './hooks/use-fit-text.js'
45
+ export { Slot } from './utils/slot.js'
@@ -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,