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.
- package/CHANGELOG.md +61 -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/combobox/index.tsx +13 -8
- 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/tooltip/index.tsx +48 -22
- package/packages/react/src/utils/slot.tsx +47 -0
- package/packages/schemas/dist/index.d.ts +73 -24
- package/packages/schemas/dist/index.js +514 -50
- package/packages/schemas/json/appNav.json +146 -0
- package/packages/schemas/json/appShell.json +151 -0
- package/packages/schemas/json/avatar.json +130 -0
- package/packages/schemas/json/bar.json +126 -0
- package/packages/schemas/json/button.json +6 -0
- package/packages/schemas/json/catalog.json +3496 -15
- package/packages/schemas/json/combobox.json +19 -7
- package/packages/schemas/json/composer.json +125 -0
- package/packages/schemas/json/content.json +167 -0
- package/packages/schemas/json/conversation.json +166 -0
- package/packages/schemas/json/entry.json +182 -0
- package/packages/schemas/json/fileUpload.json +168 -0
- package/packages/schemas/json/identity.json +147 -0
- package/packages/schemas/json/listRow.json +131 -0
- package/packages/schemas/json/metricStrip.json +132 -0
- package/packages/schemas/json/nav.json +136 -0
- package/packages/schemas/json/panel.json +143 -0
- package/packages/schemas/json/pinInput.json +112 -0
- package/packages/schemas/json/profileHeader.json +138 -0
- package/packages/schemas/json/prompt.json +155 -0
- package/packages/schemas/json/radioGroup.json +130 -0
- package/packages/schemas/json/rangeSlider.json +157 -0
- package/packages/schemas/json/separator.json +105 -0
- package/packages/schemas/json/slider.json +147 -0
- package/packages/schemas/json/statusBanner.json +149 -0
- package/packages/schemas/json/stickyBar.json +153 -0
- package/packages/schemas/json/thread.json +129 -0
- package/packages/schemas/json/tooltip.json +21 -7
- package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/schemas/prompts/cursorrules.md +6 -4
- package/packages/schemas/prompts/statecharts.md +23 -2
- package/packages/schemas/prompts/system.md +2 -2
- package/packages/schemas/registry/catalog.json +313 -4
- package/packages/schemas/registry/components/appNav.json +146 -0
- package/packages/schemas/registry/components/appShell.json +151 -0
- package/packages/schemas/registry/components/avatar.json +130 -0
- package/packages/schemas/registry/components/bar.json +126 -0
- package/packages/schemas/registry/components/button.json +6 -0
- package/packages/schemas/registry/components/combobox.json +19 -7
- package/packages/schemas/registry/components/composer.json +125 -0
- package/packages/schemas/registry/components/content.json +167 -0
- package/packages/schemas/registry/components/conversation.json +166 -0
- package/packages/schemas/registry/components/entry.json +182 -0
- package/packages/schemas/registry/components/fileUpload.json +168 -0
- package/packages/schemas/registry/components/identity.json +147 -0
- package/packages/schemas/registry/components/listRow.json +131 -0
- package/packages/schemas/registry/components/metricStrip.json +132 -0
- package/packages/schemas/registry/components/nav.json +136 -0
- package/packages/schemas/registry/components/panel.json +143 -0
- package/packages/schemas/registry/components/pinInput.json +112 -0
- package/packages/schemas/registry/components/profileHeader.json +138 -0
- package/packages/schemas/registry/components/prompt.json +155 -0
- package/packages/schemas/registry/components/radioGroup.json +130 -0
- package/packages/schemas/registry/components/rangeSlider.json +157 -0
- package/packages/schemas/registry/components/separator.json +105 -0
- package/packages/schemas/registry/components/slider.json +147 -0
- package/packages/schemas/registry/components/statusBanner.json +149 -0
- package/packages/schemas/registry/components/stickyBar.json +153 -0
- package/packages/schemas/registry/components/thread.json +129 -0
- package/packages/schemas/registry/components/tooltip.json +21 -7
- package/packages/schemas/registry/machines/accordion.json +58 -0
- package/packages/schemas/registry/machines/appNav.json +108 -0
- package/packages/schemas/registry/machines/appShell.json +128 -0
- package/packages/schemas/registry/machines/contentRegion.json +314 -0
- package/packages/schemas/registry/machines/fileUpload.json +182 -0
- package/packages/schemas/registry/machines/panel.json +131 -0
- package/packages/schemas/registry/machines/popover.json +143 -0
- package/packages/schemas/registry/machines/rangeSlider.json +201 -0
- package/packages/schemas/registry/machines/slider.json +151 -0
- package/packages/schemas/registry/machines/stickyBar.json +261 -0
- package/packages/schemas/registry/machines/tabs.json +58 -0
- package/packages/schemas/registry/machines/toast.json +89 -0
- package/packages/schemas/registry/machines/toggle.json +134 -0
- package/packages/schemas/scripts/emit.ts +27 -0
- package/packages/schemas/src/components.ts +499 -6
- package/packages/schemas/src/index.test.ts +83 -0
- package/packages/schemas/src/index.ts +1 -0
- package/packages/schemas/src/machine-id.ts +7 -0
- package/packages/schemas/src/prompts.ts +25 -4
- package/packages/schemas/src/registry.ts +28 -39
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"3.2.4","results":[[":src/select/select.test.tsx",{"duration":
|
|
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 {
|
|
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
|
-
{
|
|
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=
|
|
29
|
-
disabled={
|
|
30
|
-
|
|
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 [
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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,
|