aeon-ui-engine 1.3.5 → 1.5.1
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 +52 -0
- package/CONSUMER.md +67 -27
- package/INTEGRATION.md +1 -1
- package/README.md +17 -8
- package/package.json +4 -1
- package/packages/cli/README.md +13 -1
- package/packages/cli/dist/bin.js +50 -6
- package/packages/cli/dist/chunk-JMNRJCWF.js +301 -0
- package/packages/cli/dist/index.d.ts +31 -1
- package/packages/cli/dist/index.js +7 -3
- package/packages/cli/src/bin.ts +50 -4
- package/packages/cli/src/index.ts +2 -0
- package/packages/cli/src/init.ts +10 -4
- package/packages/cli/src/link.ts +61 -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/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
- package/packages/primitives/dist/index.d.ts +2 -0
- package/packages/primitives/dist/index.js +23 -7
- 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/dist/machines/slider.d.ts +22 -0
- package/packages/primitives/dist/machines/slider.js +1 -1
- package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/primitives/src/__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/primitives/src/machines/slider.ts +17 -0
- package/packages/react/dist/index.d.ts +132 -26
- package/packages/react/dist/index.js +1822 -1029
- package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/react/src/button/index.tsx +50 -15
- package/packages/react/src/composer/index.tsx +58 -3
- package/packages/react/src/file-upload/index.tsx +322 -0
- package/packages/react/src/floating/FloatingPositioner.tsx +8 -0
- package/packages/react/src/hooks/use-anchor-position.ts +88 -17
- package/packages/react/src/index.ts +3 -0
- package/packages/react/src/menu/index.tsx +37 -3
- package/packages/react/src/popover/index.tsx +7 -1
- package/packages/react/src/prompt/index.tsx +45 -0
- package/packages/react/src/range-slider/index.tsx +322 -0
- package/packages/react/src/slider/index.tsx +54 -14
- package/packages/react/src/thread/index.tsx +74 -3
- package/packages/react/src/tooltip/index.tsx +7 -1
- package/packages/react/src/utils/slot.tsx +47 -0
- package/packages/schemas/dist/index.d.ts +4 -0
- package/packages/schemas/dist/index.js +67 -1
- package/packages/schemas/json/button.json +6 -0
- package/packages/schemas/json/catalog.json +333 -0
- package/packages/schemas/json/fileUpload.json +168 -0
- package/packages/schemas/json/rangeSlider.json +157 -0
- package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/schemas/prompts/cursorrules.md +3 -1
- package/packages/schemas/prompts/statecharts.md +1 -1
- package/packages/schemas/registry/catalog.json +18 -0
- package/packages/schemas/registry/components/button.json +6 -0
- package/packages/schemas/registry/components/fileUpload.json +168 -0
- package/packages/schemas/registry/components/rangeSlider.json +157 -0
- package/packages/schemas/registry/machines/fileUpload.json +182 -0
- package/packages/schemas/registry/machines/rangeSlider.json +201 -0
- package/packages/schemas/scripts/emit.ts +4 -0
- package/packages/schemas/src/components.ts +70 -0
- package/packages/schemas/src/prompts.ts +3 -1
- 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/scripts/link-scoped-packages.mjs +206 -0
- package/scripts/vite-aliases.mjs +6 -8
- 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":374.59737499999983,"failed":false}],[":src/dialog/dialog.test.tsx",{"duration":302.22662500000024,"failed":false}],[":src/avatar/avatar.test.tsx",{"duration":222.467666,"failed":false}],[":src/tooltip/tooltip.test.tsx",{"duration":394.93274999999994,"failed":false}],[":src/toast/toast.test.tsx",{"duration":290.9687079999999,"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,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 }
|
|
@@ -13,6 +13,10 @@ export interface FloatingPositionerProps extends HTMLAttributes<HTMLDivElement>
|
|
|
13
13
|
portalled?: boolean
|
|
14
14
|
placement?: AnchorPlacement
|
|
15
15
|
matchAnchorWidth?: boolean
|
|
16
|
+
/** Gap between trigger and surface. */
|
|
17
|
+
gutter?: number
|
|
18
|
+
/** Stack level for the portalled surface. */
|
|
19
|
+
zIndex?: number
|
|
16
20
|
children?: ReactNode
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -27,6 +31,8 @@ export const FloatingPositioner = forwardRef<HTMLDivElement, FloatingPositionerP
|
|
|
27
31
|
portalled = true,
|
|
28
32
|
placement = 'bottom-start',
|
|
29
33
|
matchAnchorWidth = false,
|
|
34
|
+
gutter,
|
|
35
|
+
zIndex,
|
|
30
36
|
children,
|
|
31
37
|
className,
|
|
32
38
|
...rest
|
|
@@ -36,6 +42,8 @@ export const FloatingPositioner = forwardRef<HTMLDivElement, FloatingPositionerP
|
|
|
36
42
|
const mounted = usePortalledMount()
|
|
37
43
|
useAnchorPosition(triggerRef, positionerRef, open && portalled, placement, {
|
|
38
44
|
matchWidth: matchAnchorWidth,
|
|
45
|
+
gutter,
|
|
46
|
+
zIndex,
|
|
39
47
|
})
|
|
40
48
|
|
|
41
49
|
if (!open) return null
|
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { useEffect, type RefObject } from 'react'
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type AnchorSide = 'top' | 'bottom'
|
|
4
|
+
export type AnchorAlign = 'start' | 'end' | 'center'
|
|
5
|
+
export type AnchorPlacement = `${AnchorSide}-${AnchorAlign}`
|
|
4
6
|
|
|
5
7
|
export interface AnchorPositionOptions {
|
|
6
8
|
/** Match floating width to anchor (select listboxes). */
|
|
7
9
|
matchWidth?: boolean
|
|
10
|
+
/** Gap between anchor and floating surface. */
|
|
11
|
+
gutter?: number
|
|
12
|
+
/** Stack level written to the floating surface. */
|
|
13
|
+
zIndex?: number
|
|
14
|
+
/** Viewport padding kept clear when flipping and clamping. */
|
|
15
|
+
padding?: number
|
|
8
16
|
}
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
const DEFAULT_GUTTER = 4
|
|
19
|
+
const DEFAULT_Z_INDEX = 50
|
|
20
|
+
const DEFAULT_PADDING = 8
|
|
21
|
+
|
|
22
|
+
function splitPlacement(placement: AnchorPlacement): [AnchorSide, AnchorAlign] {
|
|
23
|
+
const [side, align] = placement.split('-') as [AnchorSide, AnchorAlign]
|
|
24
|
+
return [side, align]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pin floating surface to trigger rect (for portalled overlays).
|
|
29
|
+
*
|
|
30
|
+
* Collision aware: the preferred side flips when it has no room, and the
|
|
31
|
+
* surface is clamped inside the viewport so an end-aligned trigger near an edge
|
|
32
|
+
* never renders offscreen. The resolved geometry is published as
|
|
33
|
+
* `data-side` / `data-align` plus `--aeon-available-height` and
|
|
34
|
+
* `--aeon-anchor-width` so recipes can cap their own scroll area.
|
|
35
|
+
*/
|
|
11
36
|
export function useAnchorPosition(
|
|
12
37
|
anchorRef: RefObject<HTMLElement | null>,
|
|
13
38
|
floatingRef: RefObject<HTMLElement | null>,
|
|
@@ -15,22 +40,32 @@ export function useAnchorPosition(
|
|
|
15
40
|
placement: AnchorPlacement = 'bottom-start',
|
|
16
41
|
options: AnchorPositionOptions = {},
|
|
17
42
|
) {
|
|
18
|
-
const {
|
|
43
|
+
const {
|
|
44
|
+
matchWidth = false,
|
|
45
|
+
gutter = DEFAULT_GUTTER,
|
|
46
|
+
zIndex = DEFAULT_Z_INDEX,
|
|
47
|
+
padding = DEFAULT_PADDING,
|
|
48
|
+
} = options
|
|
19
49
|
|
|
20
50
|
useEffect(() => {
|
|
21
51
|
if (!active || !anchorRef.current || !floatingRef.current) return
|
|
22
52
|
|
|
53
|
+
let frame = 0
|
|
54
|
+
|
|
23
55
|
const update = () => {
|
|
24
56
|
const anchor = anchorRef.current
|
|
25
57
|
const floating = floatingRef.current
|
|
26
58
|
if (!anchor || !floating) return
|
|
27
59
|
|
|
60
|
+
const [preferredSide, align] = splitPlacement(placement)
|
|
28
61
|
const rect = anchor.getBoundingClientRect()
|
|
62
|
+
const viewportWidth = window.innerWidth
|
|
63
|
+
const viewportHeight = window.innerHeight
|
|
64
|
+
|
|
29
65
|
floating.style.position = 'fixed'
|
|
30
|
-
floating.style.left = `${rect.left}px`
|
|
31
66
|
floating.style.bottom = ''
|
|
32
67
|
floating.style.right = ''
|
|
33
|
-
floating.style.zIndex =
|
|
68
|
+
floating.style.zIndex = String(zIndex)
|
|
34
69
|
|
|
35
70
|
if (matchWidth) {
|
|
36
71
|
floating.style.width = `${rect.width}px`
|
|
@@ -40,22 +75,58 @@ export function useAnchorPosition(
|
|
|
40
75
|
floating.style.minWidth = ''
|
|
41
76
|
}
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
const width = floating.offsetWidth || floating.getBoundingClientRect().width
|
|
79
|
+
const height = floating.offsetHeight || floating.getBoundingClientRect().height
|
|
80
|
+
|
|
81
|
+
const roomBelow = viewportHeight - rect.bottom - gutter - padding
|
|
82
|
+
const roomAbove = rect.top - gutter - padding
|
|
83
|
+
const room = preferredSide === 'top' ? roomAbove : roomBelow
|
|
84
|
+
const side =
|
|
85
|
+
room >= height
|
|
86
|
+
? preferredSide
|
|
87
|
+
: roomAbove > roomBelow
|
|
88
|
+
? 'top'
|
|
89
|
+
: 'bottom'
|
|
90
|
+
const available = side === 'top' ? roomAbove : roomBelow
|
|
91
|
+
|
|
92
|
+
let left = rect.left
|
|
93
|
+
if (align === 'end') left = rect.right - width
|
|
94
|
+
else if (align === 'center') left = rect.left + rect.width / 2 - width / 2
|
|
95
|
+
|
|
96
|
+
const maxLeft = Math.max(padding, viewportWidth - width - padding)
|
|
97
|
+
left = Math.min(Math.max(padding, left), maxLeft)
|
|
98
|
+
|
|
99
|
+
const top =
|
|
100
|
+
side === 'top'
|
|
101
|
+
? Math.max(padding, rect.top - gutter - Math.min(height, Math.max(available, 0)))
|
|
102
|
+
: Math.min(
|
|
103
|
+
rect.bottom + gutter,
|
|
104
|
+
Math.max(padding, viewportHeight - height - padding),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
floating.style.left = `${Math.round(left)}px`
|
|
108
|
+
floating.style.top = `${Math.round(top)}px`
|
|
109
|
+
floating.style.setProperty('--aeon-available-height', `${Math.round(Math.max(available, 0))}px`)
|
|
110
|
+
floating.style.setProperty('--aeon-anchor-width', `${Math.round(rect.width)}px`)
|
|
111
|
+
floating.dataset.side = side
|
|
112
|
+
floating.dataset.align = align
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const schedule = () => {
|
|
116
|
+
cancelAnimationFrame(frame)
|
|
117
|
+
frame = requestAnimationFrame(update)
|
|
49
118
|
}
|
|
50
119
|
|
|
51
120
|
update()
|
|
52
|
-
|
|
121
|
+
// Measure again once the surface has laid out (flip/clamp need real height).
|
|
122
|
+
schedule()
|
|
53
123
|
|
|
54
|
-
window.addEventListener('resize',
|
|
55
|
-
window.addEventListener('scroll',
|
|
124
|
+
window.addEventListener('resize', schedule)
|
|
125
|
+
window.addEventListener('scroll', schedule, true)
|
|
56
126
|
return () => {
|
|
57
|
-
|
|
58
|
-
window.removeEventListener('
|
|
127
|
+
cancelAnimationFrame(frame)
|
|
128
|
+
window.removeEventListener('resize', schedule)
|
|
129
|
+
window.removeEventListener('scroll', schedule, true)
|
|
59
130
|
}
|
|
60
|
-
}, [active, anchorRef, floatingRef, placement, matchWidth])
|
|
131
|
+
}, [active, anchorRef, floatingRef, placement, matchWidth, gutter, zIndex, padding])
|
|
61
132
|
}
|
|
@@ -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'
|