aeon-ui-engine 1.0.0 → 1.0.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/COMPONENTS.md +7 -2
- package/INTEGRATION.md +14 -0
- package/README.md +8 -4
- package/apps/demo/og.config.mjs +8 -3
- package/apps/demo/public/og-aeon-ui-engine.png +0 -0
- package/apps/demo/public/og.svg +2 -2
- package/apps/demo/scripts/generate-og-png.mjs +4 -2
- package/apps/demo/src/App.tsx +2 -2
- package/apps/demo/src/aeonDiagrams.ts +17 -75
- package/apps/demo/src/components/DiagramsSection.tsx +8 -9
- package/apps/demo/src/components/HeroInstall.tsx +7 -4
- package/apps/demo/src/components/LandingFaq.tsx +1 -1
- package/apps/demo/src/components/LandingFooter.tsx +8 -0
- package/apps/demo/src/components/LandingHeader.tsx +4 -2
- package/apps/demo/src/components/LayersSection.tsx +6 -10
- package/apps/demo/src/components/MermaidDiagram.tsx +5 -3
- package/apps/demo/src/components/PhilosophySection.tsx +0 -2
- package/apps/demo/src/components/PlaygroundBlock.tsx +3 -8
- package/apps/demo/src/components/PlaygroundControlsDemo.tsx +63 -0
- package/apps/demo/src/components/PlaygroundSection.tsx +7 -91
- package/apps/demo/src/components/ThemeSwitcher.tsx +12 -9
- package/apps/demo/src/demo-layout.css +29 -0
- package/apps/demo/src/landing.css +136 -79
- package/apps/demo/src/nav.ts +3 -3
- package/apps/demo/src/site.ts +99 -228
- package/docs/MACHINES.md +6 -1
- package/package.json +1 -1
- package/packages/core/src/anatomy.ts +23 -0
- package/packages/panda/panda.config.ts +19 -1
- package/packages/panda/src/recipes/index.ts +5 -0
- package/packages/panda/src/recipes/pin-input.ts +29 -0
- package/packages/panda/src/recipes/progress.ts +44 -0
- package/packages/panda/src/recipes/radio-group.ts +51 -0
- package/packages/panda/src/recipes/select.ts +2 -0
- package/packages/panda/src/recipes/separator.ts +35 -0
- package/packages/panda/src/recipes/slider.ts +57 -0
- package/packages/panda/styled-system/recipes/aeon-pin-input.d.ts +28 -0
- package/packages/panda/styled-system/recipes/aeon-pin-input.mjs +36 -0
- package/packages/panda/styled-system/recipes/aeon-progress.d.ts +28 -0
- package/packages/panda/styled-system/recipes/aeon-progress.mjs +44 -0
- package/packages/panda/styled-system/recipes/aeon-radio-group.d.ts +28 -0
- package/packages/panda/styled-system/recipes/aeon-radio-group.mjs +48 -0
- package/packages/panda/styled-system/recipes/aeon-separator.d.ts +31 -0
- package/packages/panda/styled-system/recipes/aeon-separator.mjs +41 -0
- package/packages/panda/styled-system/recipes/aeon-slider.d.ts +28 -0
- package/packages/panda/styled-system/recipes/aeon-slider.mjs +48 -0
- package/packages/panda/styled-system/recipes/index.d.ts +5 -0
- package/packages/panda/styled-system/recipes/index.mjs +5 -0
- package/packages/panda/styled-system/styles.css +223 -46
- package/packages/panda/styled-system/types/conditions.d.ts +1 -1
- package/packages/primitives/src/index.ts +1 -0
- package/packages/primitives/src/machines/slider.ts +93 -0
- package/packages/react/src/checkbox/index.tsx +14 -0
- package/packages/react/src/floating/FloatingPositioner.tsx +59 -0
- package/packages/react/src/hooks/use-anchor-position.ts +33 -5
- package/packages/react/src/hooks/use-prefers-hover.ts +16 -0
- package/packages/react/src/index.ts +5 -0
- package/packages/react/src/menu/index.tsx +38 -11
- package/packages/react/src/pin-input/index.tsx +223 -0
- package/packages/react/src/popover/index.tsx +38 -11
- package/packages/react/src/progress/index.tsx +110 -0
- package/packages/react/src/radio-group/index.tsx +199 -0
- package/packages/react/src/select/index.tsx +51 -28
- package/packages/react/src/select/select.test.tsx +21 -0
- package/packages/react/src/separator/index.tsx +23 -0
- package/packages/react/src/slider/index.tsx +264 -0
- package/packages/react/src/toast/primitives.tsx +8 -1
- package/packages/react/src/toast/toast.test.tsx +40 -0
- package/packages/react/src/tooltip/index.tsx +124 -20
- package/packages/react/src/tooltip/tooltip.test.tsx +114 -0
- package/packages/site/README.md +1 -1
- package/packages/ui/src/index.ts +5 -0
- package/packages/ui/src/pin-input.tsx +16 -0
- package/packages/ui/src/progress.tsx +21 -0
- package/packages/ui/src/radio-group.tsx +24 -0
- package/packages/ui/src/separator.tsx +15 -0
- package/packages/ui/src/slider.tsx +24 -0
- package/apps/demo/public/og.png +0 -0
- package/apps/demo/src/components/SceneSection.tsx +0 -157
|
@@ -8,20 +8,32 @@ import {
|
|
|
8
8
|
useMemo,
|
|
9
9
|
useRef,
|
|
10
10
|
useState,
|
|
11
|
+
type FocusEvent,
|
|
11
12
|
type HTMLAttributes,
|
|
13
|
+
type MouseEvent,
|
|
14
|
+
type PointerEvent,
|
|
12
15
|
type ReactNode,
|
|
13
16
|
} from 'react'
|
|
17
|
+
import { FloatingPositioner } from '../floating/FloatingPositioner.js'
|
|
18
|
+
import { useOutsideClick } from '../hooks/use-outside-click.js'
|
|
19
|
+
import { usePrefersHover } from '../hooks/use-prefers-hover.js'
|
|
14
20
|
import { mergeProps } from '../utils/merge-props.js'
|
|
21
|
+
import type { AnchorPlacement } from '../hooks/use-anchor-position.js'
|
|
15
22
|
|
|
16
23
|
interface TooltipContextValue {
|
|
17
24
|
open: boolean
|
|
18
25
|
setOpen: (open: boolean) => void
|
|
19
26
|
triggerId: string
|
|
20
27
|
contentId: string
|
|
28
|
+
triggerRef: React.RefObject<HTMLSpanElement | null>
|
|
29
|
+
positionerRef: React.RefObject<HTMLDivElement | null>
|
|
21
30
|
scheduleShow: () => void
|
|
22
31
|
scheduleHide: () => void
|
|
32
|
+
showImmediately: () => void
|
|
23
33
|
hideNow: () => void
|
|
34
|
+
toggleTouch: () => void
|
|
24
35
|
showOnFocus: boolean
|
|
36
|
+
prefersHover: boolean
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
const TooltipCtx = createContext<TooltipContextValue | null>(null)
|
|
@@ -38,6 +50,10 @@ export interface TooltipRootProps {
|
|
|
38
50
|
onOpenChange?: (open: boolean) => void
|
|
39
51
|
openDelay?: number
|
|
40
52
|
closeDelay?: number
|
|
53
|
+
/** Auto-hide on touch devices after open (0 = until outside tap). Default 5000. */
|
|
54
|
+
touchDurationMs?: number
|
|
55
|
+
/** Ignore outside-dismiss briefly after touch open (ghost-click guard). */
|
|
56
|
+
touchOutsideGraceMs?: number
|
|
41
57
|
/** Focus after click/tap can leave tooltips open; disable for dense click targets. */
|
|
42
58
|
showOnFocus?: boolean
|
|
43
59
|
children?: ReactNode
|
|
@@ -51,6 +67,8 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
|
|
|
51
67
|
onOpenChange,
|
|
52
68
|
openDelay = 400,
|
|
53
69
|
closeDelay = 100,
|
|
70
|
+
touchDurationMs = 5000,
|
|
71
|
+
touchOutsideGraceMs = 400,
|
|
54
72
|
showOnFocus = true,
|
|
55
73
|
children,
|
|
56
74
|
className,
|
|
@@ -62,8 +80,22 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
|
|
|
62
80
|
const resolvedOpen = open ?? uncontrolled
|
|
63
81
|
const showTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
64
82
|
const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
83
|
+
const triggerRef = useRef<HTMLSpanElement>(null)
|
|
84
|
+
const positionerRef = useRef<HTMLDivElement>(null)
|
|
65
85
|
const triggerId = useId()
|
|
66
86
|
const contentId = useId()
|
|
87
|
+
const prefersHover = usePrefersHover()
|
|
88
|
+
const [outsideClickEnabled, setOutsideClickEnabled] = useState(false)
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!resolvedOpen || prefersHover) {
|
|
92
|
+
setOutsideClickEnabled(false)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
setOutsideClickEnabled(false)
|
|
96
|
+
const id = window.setTimeout(() => setOutsideClickEnabled(true), touchOutsideGraceMs)
|
|
97
|
+
return () => window.clearTimeout(id)
|
|
98
|
+
}, [resolvedOpen, prefersHover, touchOutsideGraceMs])
|
|
67
99
|
|
|
68
100
|
const setOpen = (next: boolean) => {
|
|
69
101
|
if (open === undefined) setUncontrolled(next)
|
|
@@ -80,6 +112,17 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
|
|
|
80
112
|
setOpen(false)
|
|
81
113
|
}
|
|
82
114
|
|
|
115
|
+
const showImmediately = () => {
|
|
116
|
+
if (showTimer.current !== undefined) clearTimeout(showTimer.current)
|
|
117
|
+
if (hideTimer.current !== undefined) clearTimeout(hideTimer.current)
|
|
118
|
+
setOpen(true)
|
|
119
|
+
if (!prefersHover) {
|
|
120
|
+
if (touchDurationMs > 0) {
|
|
121
|
+
hideTimer.current = setTimeout(() => hideNow(), touchDurationMs)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
const scheduleShow = () => {
|
|
84
127
|
if (hideTimer.current !== undefined) clearTimeout(hideTimer.current)
|
|
85
128
|
showTimer.current = setTimeout(() => setOpen(true), openDelay)
|
|
@@ -94,18 +137,30 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
|
|
|
94
137
|
hideTimer.current = setTimeout(() => setOpen(false), closeDelay)
|
|
95
138
|
}
|
|
96
139
|
|
|
140
|
+
const toggleTouch = () => {
|
|
141
|
+
if (resolvedOpen) hideNow()
|
|
142
|
+
else showImmediately()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
useOutsideClick([triggerRef, positionerRef], resolvedOpen && !prefersHover && outsideClickEnabled, hideNow)
|
|
146
|
+
|
|
97
147
|
const value = useMemo(
|
|
98
148
|
() => ({
|
|
99
149
|
open: resolvedOpen,
|
|
100
150
|
setOpen,
|
|
101
151
|
triggerId,
|
|
102
152
|
contentId,
|
|
153
|
+
triggerRef,
|
|
154
|
+
positionerRef,
|
|
103
155
|
scheduleShow,
|
|
104
156
|
scheduleHide,
|
|
157
|
+
showImmediately,
|
|
105
158
|
hideNow,
|
|
159
|
+
toggleTouch,
|
|
106
160
|
showOnFocus,
|
|
161
|
+
prefersHover,
|
|
107
162
|
}),
|
|
108
|
-
[resolvedOpen, triggerId, contentId, openDelay, closeDelay, showOnFocus],
|
|
163
|
+
[resolvedOpen, triggerId, contentId, openDelay, closeDelay, touchDurationMs, touchOutsideGraceMs, showOnFocus, prefersHover],
|
|
109
164
|
)
|
|
110
165
|
|
|
111
166
|
return (
|
|
@@ -127,45 +182,94 @@ const Root = forwardRef<HTMLDivElement, TooltipRootProps>(function TooltipRoot(
|
|
|
127
182
|
})
|
|
128
183
|
|
|
129
184
|
const Trigger = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(function TooltipTrigger(
|
|
130
|
-
|
|
185
|
+
{ onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, onPointerDown, ...rest },
|
|
131
186
|
ref,
|
|
132
187
|
) {
|
|
133
|
-
const {
|
|
134
|
-
|
|
188
|
+
const {
|
|
189
|
+
open,
|
|
190
|
+
triggerId,
|
|
191
|
+
contentId,
|
|
192
|
+
triggerRef,
|
|
193
|
+
scheduleShow,
|
|
194
|
+
scheduleHide,
|
|
195
|
+
hideNow,
|
|
196
|
+
toggleTouch,
|
|
197
|
+
showOnFocus,
|
|
198
|
+
prefersHover,
|
|
199
|
+
} = useTooltipCtx()
|
|
200
|
+
|
|
135
201
|
return (
|
|
136
202
|
<span
|
|
137
|
-
ref={
|
|
203
|
+
ref={(node) => {
|
|
204
|
+
triggerRef.current = node
|
|
205
|
+
if (typeof ref === 'function') ref(node)
|
|
206
|
+
else if (ref) ref.current = node
|
|
207
|
+
}}
|
|
138
208
|
id={triggerId}
|
|
139
209
|
tabIndex={0}
|
|
140
210
|
aria-describedby={open ? contentId : undefined}
|
|
141
211
|
{...mergeProps(
|
|
142
212
|
partAttrs(tooltipAnatomy.scope, tooltipAnatomy.trigger, { state: open ? 'open' : 'closed' }),
|
|
143
213
|
{
|
|
144
|
-
onMouseEnter:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
214
|
+
onMouseEnter: (e: MouseEvent<HTMLSpanElement>) => {
|
|
215
|
+
onMouseEnter?.(e)
|
|
216
|
+
if (prefersHover) scheduleShow()
|
|
217
|
+
},
|
|
218
|
+
onMouseLeave: (e: MouseEvent<HTMLSpanElement>) => {
|
|
219
|
+
onMouseLeave?.(e)
|
|
220
|
+
if (prefersHover) scheduleHide()
|
|
221
|
+
},
|
|
222
|
+
onFocus: (e: FocusEvent<HTMLSpanElement>) => {
|
|
223
|
+
onFocus?.(e)
|
|
224
|
+
if (showOnFocus && prefersHover) scheduleShow()
|
|
225
|
+
},
|
|
226
|
+
onBlur: (e: FocusEvent<HTMLSpanElement>) => {
|
|
227
|
+
onBlur?.(e)
|
|
228
|
+
if (showOnFocus && prefersHover) scheduleHide()
|
|
229
|
+
},
|
|
230
|
+
onClick: (e: MouseEvent<HTMLSpanElement>) => {
|
|
231
|
+
onClick?.(e)
|
|
232
|
+
if (!e.defaultPrevented && !prefersHover) toggleTouch()
|
|
233
|
+
},
|
|
234
|
+
onPointerDown: (e: PointerEvent<HTMLSpanElement>) => {
|
|
235
|
+
onPointerDown?.(e)
|
|
236
|
+
if (showOnFocus && prefersHover && e.pointerType === 'mouse') hideNow()
|
|
237
|
+
},
|
|
151
238
|
},
|
|
152
|
-
|
|
239
|
+
rest,
|
|
153
240
|
)}
|
|
154
241
|
/>
|
|
155
242
|
)
|
|
156
243
|
})
|
|
157
244
|
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
export interface TooltipPositionerProps extends HTMLAttributes<HTMLDivElement> {
|
|
246
|
+
/** Mount tooltip in document.body (default true). */
|
|
247
|
+
portalled?: boolean
|
|
248
|
+
placement?: AnchorPlacement
|
|
249
|
+
children?: ReactNode
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const Positioner = forwardRef<HTMLDivElement, TooltipPositionerProps>(function TooltipPositioner(
|
|
253
|
+
{ portalled = true, placement = 'top-start', children, className, ...rest },
|
|
160
254
|
ref,
|
|
161
255
|
) {
|
|
162
|
-
const { open } = useTooltipCtx()
|
|
163
|
-
|
|
256
|
+
const { open, triggerRef, positionerRef } = useTooltipCtx()
|
|
257
|
+
|
|
164
258
|
return (
|
|
165
|
-
<
|
|
259
|
+
<FloatingPositioner
|
|
166
260
|
ref={ref}
|
|
167
|
-
{
|
|
168
|
-
|
|
261
|
+
open={open}
|
|
262
|
+
scope={tooltipAnatomy.scope}
|
|
263
|
+
part={tooltipAnatomy.positioner}
|
|
264
|
+
triggerRef={triggerRef}
|
|
265
|
+
positionerRef={positionerRef}
|
|
266
|
+
portalled={portalled}
|
|
267
|
+
placement={placement}
|
|
268
|
+
className={className}
|
|
269
|
+
{...rest}
|
|
270
|
+
>
|
|
271
|
+
{children}
|
|
272
|
+
</FloatingPositioner>
|
|
169
273
|
)
|
|
170
274
|
})
|
|
171
275
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { Tooltip } from './index.js'
|
|
5
|
+
|
|
6
|
+
function mockPrefersHover(matches: boolean) {
|
|
7
|
+
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
|
8
|
+
matches: query.includes('hover: hover') ? matches : false,
|
|
9
|
+
media: query,
|
|
10
|
+
onchange: null,
|
|
11
|
+
addEventListener: vi.fn(),
|
|
12
|
+
removeEventListener: vi.fn(),
|
|
13
|
+
dispatchEvent: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Tooltip', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.restoreAllMocks()
|
|
20
|
+
vi.useRealTimers()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.useRealTimers()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('opens on hover when the device supports hover', async () => {
|
|
28
|
+
mockPrefersHover(true)
|
|
29
|
+
const user = userEvent.setup()
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
<Tooltip.Root openDelay={0}>
|
|
33
|
+
<Tooltip.Trigger>Anchor</Tooltip.Trigger>
|
|
34
|
+
<Tooltip.Positioner portalled={false}>
|
|
35
|
+
<Tooltip.Content>Hint</Tooltip.Content>
|
|
36
|
+
</Tooltip.Positioner>
|
|
37
|
+
</Tooltip.Root>,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
await user.hover(screen.getByText('Anchor'))
|
|
41
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent('Hint')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('toggles on tap when hover is unavailable', async () => {
|
|
45
|
+
mockPrefersHover(false)
|
|
46
|
+
const user = userEvent.setup()
|
|
47
|
+
|
|
48
|
+
render(
|
|
49
|
+
<Tooltip.Root>
|
|
50
|
+
<Tooltip.Trigger>Anchor</Tooltip.Trigger>
|
|
51
|
+
<Tooltip.Positioner portalled={false}>
|
|
52
|
+
<Tooltip.Content>Hint</Tooltip.Content>
|
|
53
|
+
</Tooltip.Positioner>
|
|
54
|
+
</Tooltip.Root>,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const trigger = screen.getByText('Anchor')
|
|
58
|
+
await user.click(trigger)
|
|
59
|
+
expect(screen.getByRole('tooltip')).toHaveTextContent('Hint')
|
|
60
|
+
|
|
61
|
+
await user.click(trigger)
|
|
62
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('stays open for touchDurationMs on touch devices', () => {
|
|
66
|
+
vi.useFakeTimers()
|
|
67
|
+
mockPrefersHover(false)
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<Tooltip.Root touchDurationMs={5000}>
|
|
71
|
+
<Tooltip.Trigger>Anchor</Tooltip.Trigger>
|
|
72
|
+
<Tooltip.Positioner portalled={false}>
|
|
73
|
+
<Tooltip.Content>Hint</Tooltip.Content>
|
|
74
|
+
</Tooltip.Positioner>
|
|
75
|
+
</Tooltip.Root>,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
fireEvent.click(screen.getByText('Anchor'))
|
|
79
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
vi.advanceTimersByTime(4999)
|
|
83
|
+
})
|
|
84
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
vi.advanceTimersByTime(1)
|
|
88
|
+
})
|
|
89
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('closes on outside tap when hover is unavailable', async () => {
|
|
93
|
+
mockPrefersHover(false)
|
|
94
|
+
const user = userEvent.setup()
|
|
95
|
+
|
|
96
|
+
render(
|
|
97
|
+
<div>
|
|
98
|
+
<Tooltip.Root touchOutsideGraceMs={0}>
|
|
99
|
+
<Tooltip.Trigger>Anchor</Tooltip.Trigger>
|
|
100
|
+
<Tooltip.Positioner portalled={false}>
|
|
101
|
+
<Tooltip.Content>Hint</Tooltip.Content>
|
|
102
|
+
</Tooltip.Positioner>
|
|
103
|
+
</Tooltip.Root>
|
|
104
|
+
<button type="button">Elsewhere</button>
|
|
105
|
+
</div>,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
await user.click(screen.getByText('Anchor'))
|
|
109
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
|
110
|
+
|
|
111
|
+
await user.click(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
112
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
})
|
package/packages/site/README.md
CHANGED
|
@@ -9,7 +9,7 @@ const html = openGraphHeadHtml({
|
|
|
9
9
|
title: 'Aeon UI — Future-proof UI infrastructure',
|
|
10
10
|
description: 'Statechart-driven primitives and a styled layer your team controls.',
|
|
11
11
|
url: absoluteUrl('https://aeon-ui.com', '/'),
|
|
12
|
-
image: absoluteUrl('https://aeon-ui.com', '/og.png'),
|
|
12
|
+
image: absoluteUrl('https://aeon-ui.com', '/og-aeon-ui-engine.png'),
|
|
13
13
|
siteName: 'Aeon UI',
|
|
14
14
|
})
|
|
15
15
|
```
|
package/packages/ui/src/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ export { Switch } from './switch.js'
|
|
|
5
5
|
export { Checkbox } from './checkbox.js'
|
|
6
6
|
export { Dialog } from './dialog.js'
|
|
7
7
|
export { Tabs } from './tabs.js'
|
|
8
|
+
export { Separator } from './separator.js'
|
|
9
|
+
export { Progress } from './progress.js'
|
|
10
|
+
export { RadioGroup } from './radio-group.js'
|
|
11
|
+
export { Slider } from './slider.js'
|
|
12
|
+
export { PinInput } from './pin-input.js'
|
|
8
13
|
export { Accordion } from './accordion.js'
|
|
9
14
|
export { Badge } from './badge.js'
|
|
10
15
|
export { Scroll } from './scroll.js'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PinInput as Headless } from '@aeon-ui/react'
|
|
2
|
+
import { aeonPinInput } from '@aeon-ui/panda/styled-system/recipes'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import { cn } from './cn.js'
|
|
5
|
+
|
|
6
|
+
const pinInput = aeonPinInput()
|
|
7
|
+
|
|
8
|
+
export const PinInput = {
|
|
9
|
+
Root: ({ className, ...props }: ComponentProps<typeof Headless.Root>) => (
|
|
10
|
+
<Headless.Root className={cn(pinInput.root, className)} {...props} />
|
|
11
|
+
),
|
|
12
|
+
Input: ({ className, ...props }: ComponentProps<typeof Headless.Input>) => (
|
|
13
|
+
<Headless.Input className={cn(pinInput.input, className)} {...props} />
|
|
14
|
+
),
|
|
15
|
+
Group: Headless.Group,
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Progress as Headless } from '@aeon-ui/react'
|
|
2
|
+
import { aeonProgress } from '@aeon-ui/panda/styled-system/recipes'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import { cn } from './cn.js'
|
|
5
|
+
|
|
6
|
+
const progress = aeonProgress()
|
|
7
|
+
|
|
8
|
+
export const Progress = {
|
|
9
|
+
Root: ({ className, ...props }: ComponentProps<typeof Headless.Root>) => (
|
|
10
|
+
<Headless.Root className={cn(progress.root, className)} {...props} />
|
|
11
|
+
),
|
|
12
|
+
Track: ({ className, ...props }: ComponentProps<typeof Headless.Track>) => (
|
|
13
|
+
<Headless.Track className={cn(progress.track, className)} {...props} />
|
|
14
|
+
),
|
|
15
|
+
Range: ({ className, ...props }: ComponentProps<typeof Headless.Range>) => (
|
|
16
|
+
<Headless.Range className={cn(progress.range, className)} {...props} />
|
|
17
|
+
),
|
|
18
|
+
Label: ({ className, ...props }: ComponentProps<typeof Headless.Label>) => (
|
|
19
|
+
<Headless.Label className={cn(progress.label, className)} {...props} />
|
|
20
|
+
),
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RadioGroup as Headless } from '@aeon-ui/react'
|
|
2
|
+
import { aeonRadioGroup } from '@aeon-ui/panda/styled-system/recipes'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import { cn } from './cn.js'
|
|
5
|
+
|
|
6
|
+
const radioGroup = aeonRadioGroup()
|
|
7
|
+
|
|
8
|
+
export const RadioGroup = {
|
|
9
|
+
Root: ({ className, ...props }: ComponentProps<typeof Headless.Root>) => (
|
|
10
|
+
<Headless.Root className={cn(radioGroup.root, className)} {...props} />
|
|
11
|
+
),
|
|
12
|
+
Item: ({ className, ...props }: ComponentProps<typeof Headless.Item>) => (
|
|
13
|
+
<Headless.Item className={cn(radioGroup.item, className)} {...props} />
|
|
14
|
+
),
|
|
15
|
+
ItemControl: ({ className, ...props }: ComponentProps<typeof Headless.ItemControl>) => (
|
|
16
|
+
<Headless.ItemControl className={cn(radioGroup.itemControl, className)} {...props} />
|
|
17
|
+
),
|
|
18
|
+
ItemIndicator: ({ className, ...props }: ComponentProps<typeof Headless.ItemIndicator>) => (
|
|
19
|
+
<Headless.ItemIndicator className={cn(radioGroup.itemIndicator, className)} {...props} />
|
|
20
|
+
),
|
|
21
|
+
ItemLabel: ({ className, ...props }: ComponentProps<typeof Headless.ItemLabel>) => (
|
|
22
|
+
<Headless.ItemLabel className={cn(radioGroup.itemLabel, className)} {...props} />
|
|
23
|
+
),
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Separator as Headless } from '@aeon-ui/react'
|
|
2
|
+
import { aeonSeparator } from '@aeon-ui/panda/styled-system/recipes'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import { cn } from './cn.js'
|
|
5
|
+
|
|
6
|
+
type RootProps = ComponentProps<typeof Headless.Root> & {
|
|
7
|
+
orientation?: 'horizontal' | 'vertical'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Separator = {
|
|
11
|
+
Root: ({ className, orientation = 'horizontal', ...props }: RootProps) => {
|
|
12
|
+
const styles = aeonSeparator({ orientation })
|
|
13
|
+
return <Headless.Root className={cn(styles.root, className)} orientation={orientation} {...props} />
|
|
14
|
+
},
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Slider as Headless } from '@aeon-ui/react'
|
|
2
|
+
import { aeonSlider } from '@aeon-ui/panda/styled-system/recipes'
|
|
3
|
+
import type { ComponentProps } from 'react'
|
|
4
|
+
import { cn } from './cn.js'
|
|
5
|
+
|
|
6
|
+
const slider = aeonSlider()
|
|
7
|
+
|
|
8
|
+
export const Slider = {
|
|
9
|
+
Root: ({ className, ...props }: ComponentProps<typeof Headless.Root>) => (
|
|
10
|
+
<Headless.Root className={cn(slider.root, className)} {...props} />
|
|
11
|
+
),
|
|
12
|
+
Track: ({ className, ...props }: ComponentProps<typeof Headless.Track>) => (
|
|
13
|
+
<Headless.Track className={cn(slider.track, className)} {...props} />
|
|
14
|
+
),
|
|
15
|
+
Range: ({ className, ...props }: ComponentProps<typeof Headless.Range>) => (
|
|
16
|
+
<Headless.Range className={cn(slider.range, className)} {...props} />
|
|
17
|
+
),
|
|
18
|
+
Thumb: ({ className, ...props }: ComponentProps<typeof Headless.Thumb>) => (
|
|
19
|
+
<Headless.Thumb className={cn(slider.thumb, className)} {...props} />
|
|
20
|
+
),
|
|
21
|
+
ValueText: ({ className, ...props }: ComponentProps<typeof Headless.ValueText>) => (
|
|
22
|
+
<Headless.ValueText className={cn(slider.valueText, className)} {...props} />
|
|
23
|
+
),
|
|
24
|
+
}
|
package/apps/demo/public/og.png
DELETED
|
Binary file
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { Button } from '@aeon-ui/ui'
|
|
3
|
-
import { createDemoSignal, type DemoSignal } from '@aeon-ui/signal'
|
|
4
|
-
import { surfaceMaterials } from '@aeon-ui/surface'
|
|
5
|
-
import {
|
|
6
|
-
ProceduralGrid,
|
|
7
|
-
SceneLayer,
|
|
8
|
-
ScenePlane,
|
|
9
|
-
SceneRoot,
|
|
10
|
-
SceneSlot,
|
|
11
|
-
SceneStage,
|
|
12
|
-
SceneSurface,
|
|
13
|
-
SignalBars,
|
|
14
|
-
} from '@aeon-ui/react-scene'
|
|
15
|
-
import { site } from '../site.js'
|
|
16
|
-
|
|
17
|
-
export function SceneSection() {
|
|
18
|
-
const [demo, setDemo] = useState<DemoSignal | null>(null)
|
|
19
|
-
const playing = demo !== null
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
return () => {
|
|
23
|
-
demo?.stop()
|
|
24
|
-
}
|
|
25
|
-
}, [demo])
|
|
26
|
-
|
|
27
|
-
const toggleDemo = () => {
|
|
28
|
-
if (demo) {
|
|
29
|
-
demo.stop()
|
|
30
|
-
setDemo(null)
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
const next = createDemoSignal({ frequency: 92 })
|
|
34
|
-
next.start()
|
|
35
|
-
setDemo(next)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<section id="scene" className="section">
|
|
40
|
-
<div className="container">
|
|
41
|
-
<header className="section-header">
|
|
42
|
-
<p className="section-label">{site.sceneLabel}</p>
|
|
43
|
-
<h2 className="section-title">{site.sceneTitle}</h2>
|
|
44
|
-
<p className="section-lead">{site.sceneLead}</p>
|
|
45
|
-
</header>
|
|
46
|
-
|
|
47
|
-
<div className="scene-demo panel">
|
|
48
|
-
<SceneRoot className="scene-demo__stage" state={playing ? 'active' : 'idle'}>
|
|
49
|
-
<ProceduralGrid cell={28} lineColor="rgba(94, 234, 212, 0.08)" />
|
|
50
|
-
<div className="scene-demo__swatches" role="list" aria-label="Surface materials">
|
|
51
|
-
{surfaceMaterials.map((material) => (
|
|
52
|
-
<SceneSurface
|
|
53
|
-
key={material}
|
|
54
|
-
material={material}
|
|
55
|
-
role="listitem"
|
|
56
|
-
className="scene-demo__swatch"
|
|
57
|
-
title={material}
|
|
58
|
-
/>
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
<div className="scene-demo__viz">
|
|
62
|
-
<SignalBars
|
|
63
|
-
barCount={7}
|
|
64
|
-
active={playing}
|
|
65
|
-
analyser={demo?.analyser ?? null}
|
|
66
|
-
className="scene-demo__bars"
|
|
67
|
-
/>
|
|
68
|
-
<Button.Root size="sm" variant={playing ? 'outline' : 'solid'} onClick={toggleDemo}>
|
|
69
|
-
{playing ? 'Stop demo signal' : 'Start demo signal'}
|
|
70
|
-
</Button.Root>
|
|
71
|
-
</div>
|
|
72
|
-
</SceneRoot>
|
|
73
|
-
|
|
74
|
-
<div className="scene-demo__code">
|
|
75
|
-
<pre>
|
|
76
|
-
<code>{`import '@aeon-ui/surface/surface.css'
|
|
77
|
-
import { SignalBars, SceneSurface } from '@aeon-ui/react-scene'
|
|
78
|
-
import { attachMediaAnalyser } from '@aeon-ui/signal'`}</code>
|
|
79
|
-
</pre>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<div className="scene-demo panel scene-demo--3d">
|
|
84
|
-
<header className="scene-demo__3d-header">
|
|
85
|
-
<h3 className="scene-demo__3d-title">{site.scene3dTitle}</h3>
|
|
86
|
-
<p className="scene-demo__3d-lead">{site.scene3dLead}</p>
|
|
87
|
-
</header>
|
|
88
|
-
|
|
89
|
-
<SceneStage className="scene-3d-demo" minHeight={340} perspective={{ distance: 980, origin: '50% 36%' }}>
|
|
90
|
-
<SceneLayer part="sky" transform={{ z: -130 }}>
|
|
91
|
-
<ScenePlane
|
|
92
|
-
part="sky"
|
|
93
|
-
style={{
|
|
94
|
-
background: 'linear-gradient(180deg, #243044 0%, #121820 55%, #0a0e14 100%)',
|
|
95
|
-
}}
|
|
96
|
-
/>
|
|
97
|
-
</SceneLayer>
|
|
98
|
-
|
|
99
|
-
<SceneLayer part="back-wall" transform={{ z: -85 }}>
|
|
100
|
-
<ScenePlane part="back-wall" material="stone" style={{ opacity: 0.85 }} />
|
|
101
|
-
</SceneLayer>
|
|
102
|
-
|
|
103
|
-
<SceneLayer part="wall-left" transform={{ z: -55, rotateY: 42 }} origin="0% 100%">
|
|
104
|
-
<ScenePlane
|
|
105
|
-
part="wall-left"
|
|
106
|
-
wall="left"
|
|
107
|
-
material="brick"
|
|
108
|
-
fill={false}
|
|
109
|
-
style={{ width: '54%', height: '78%', bottom: 0, left: 0 }}
|
|
110
|
-
/>
|
|
111
|
-
</SceneLayer>
|
|
112
|
-
|
|
113
|
-
<SceneLayer part="wall-right" transform={{ z: -55, rotateY: -42 }} origin="100% 100%">
|
|
114
|
-
<ScenePlane
|
|
115
|
-
part="wall-right"
|
|
116
|
-
wall="right"
|
|
117
|
-
material="brick"
|
|
118
|
-
fill={false}
|
|
119
|
-
style={{ width: '54%', height: '78%', bottom: 0, right: 0 }}
|
|
120
|
-
/>
|
|
121
|
-
</SceneLayer>
|
|
122
|
-
|
|
123
|
-
<SceneLayer part="ground" transform={{ z: 0, rotateX: 56 }} origin="50% 0%">
|
|
124
|
-
<ScenePlane part="ground" material="gravel">
|
|
125
|
-
<SceneSlot
|
|
126
|
-
part="board"
|
|
127
|
-
transform={{ z: 28 }}
|
|
128
|
-
style={{
|
|
129
|
-
left: '14%',
|
|
130
|
-
right: '14%',
|
|
131
|
-
top: '22%',
|
|
132
|
-
padding: '1rem 1.25rem',
|
|
133
|
-
borderRadius: 8,
|
|
134
|
-
background: 'rgba(18, 22, 28, 0.72)',
|
|
135
|
-
border: '1px dashed rgba(94, 234, 212, 0.35)',
|
|
136
|
-
color: 'var(--text-muted)',
|
|
137
|
-
fontSize: '0.8rem',
|
|
138
|
-
textAlign: 'center',
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
Your UI mounts here — tables, HUD, controls
|
|
142
|
-
</SceneSlot>
|
|
143
|
-
</ScenePlane>
|
|
144
|
-
</SceneLayer>
|
|
145
|
-
</SceneStage>
|
|
146
|
-
|
|
147
|
-
<div className="scene-demo__code">
|
|
148
|
-
<pre>
|
|
149
|
-
<code>{`import '@aeon-ui/react-scene/scene-3d.css'
|
|
150
|
-
import { SceneStage, SceneLayer, ScenePlane, SceneSlot } from '@aeon-ui/react-scene'`}</code>
|
|
151
|
-
</pre>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
</section>
|
|
156
|
-
)
|
|
157
|
-
}
|