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.
Files changed (79) hide show
  1. package/COMPONENTS.md +7 -2
  2. package/INTEGRATION.md +14 -0
  3. package/README.md +8 -4
  4. package/apps/demo/og.config.mjs +8 -3
  5. package/apps/demo/public/og-aeon-ui-engine.png +0 -0
  6. package/apps/demo/public/og.svg +2 -2
  7. package/apps/demo/scripts/generate-og-png.mjs +4 -2
  8. package/apps/demo/src/App.tsx +2 -2
  9. package/apps/demo/src/aeonDiagrams.ts +17 -75
  10. package/apps/demo/src/components/DiagramsSection.tsx +8 -9
  11. package/apps/demo/src/components/HeroInstall.tsx +7 -4
  12. package/apps/demo/src/components/LandingFaq.tsx +1 -1
  13. package/apps/demo/src/components/LandingFooter.tsx +8 -0
  14. package/apps/demo/src/components/LandingHeader.tsx +4 -2
  15. package/apps/demo/src/components/LayersSection.tsx +6 -10
  16. package/apps/demo/src/components/MermaidDiagram.tsx +5 -3
  17. package/apps/demo/src/components/PhilosophySection.tsx +0 -2
  18. package/apps/demo/src/components/PlaygroundBlock.tsx +3 -8
  19. package/apps/demo/src/components/PlaygroundControlsDemo.tsx +63 -0
  20. package/apps/demo/src/components/PlaygroundSection.tsx +7 -91
  21. package/apps/demo/src/components/ThemeSwitcher.tsx +12 -9
  22. package/apps/demo/src/demo-layout.css +29 -0
  23. package/apps/demo/src/landing.css +136 -79
  24. package/apps/demo/src/nav.ts +3 -3
  25. package/apps/demo/src/site.ts +99 -228
  26. package/docs/MACHINES.md +6 -1
  27. package/package.json +1 -1
  28. package/packages/core/src/anatomy.ts +23 -0
  29. package/packages/panda/panda.config.ts +19 -1
  30. package/packages/panda/src/recipes/index.ts +5 -0
  31. package/packages/panda/src/recipes/pin-input.ts +29 -0
  32. package/packages/panda/src/recipes/progress.ts +44 -0
  33. package/packages/panda/src/recipes/radio-group.ts +51 -0
  34. package/packages/panda/src/recipes/select.ts +2 -0
  35. package/packages/panda/src/recipes/separator.ts +35 -0
  36. package/packages/panda/src/recipes/slider.ts +57 -0
  37. package/packages/panda/styled-system/recipes/aeon-pin-input.d.ts +28 -0
  38. package/packages/panda/styled-system/recipes/aeon-pin-input.mjs +36 -0
  39. package/packages/panda/styled-system/recipes/aeon-progress.d.ts +28 -0
  40. package/packages/panda/styled-system/recipes/aeon-progress.mjs +44 -0
  41. package/packages/panda/styled-system/recipes/aeon-radio-group.d.ts +28 -0
  42. package/packages/panda/styled-system/recipes/aeon-radio-group.mjs +48 -0
  43. package/packages/panda/styled-system/recipes/aeon-separator.d.ts +31 -0
  44. package/packages/panda/styled-system/recipes/aeon-separator.mjs +41 -0
  45. package/packages/panda/styled-system/recipes/aeon-slider.d.ts +28 -0
  46. package/packages/panda/styled-system/recipes/aeon-slider.mjs +48 -0
  47. package/packages/panda/styled-system/recipes/index.d.ts +5 -0
  48. package/packages/panda/styled-system/recipes/index.mjs +5 -0
  49. package/packages/panda/styled-system/styles.css +223 -46
  50. package/packages/panda/styled-system/types/conditions.d.ts +1 -1
  51. package/packages/primitives/src/index.ts +1 -0
  52. package/packages/primitives/src/machines/slider.ts +93 -0
  53. package/packages/react/src/checkbox/index.tsx +14 -0
  54. package/packages/react/src/floating/FloatingPositioner.tsx +59 -0
  55. package/packages/react/src/hooks/use-anchor-position.ts +33 -5
  56. package/packages/react/src/hooks/use-prefers-hover.ts +16 -0
  57. package/packages/react/src/index.ts +5 -0
  58. package/packages/react/src/menu/index.tsx +38 -11
  59. package/packages/react/src/pin-input/index.tsx +223 -0
  60. package/packages/react/src/popover/index.tsx +38 -11
  61. package/packages/react/src/progress/index.tsx +110 -0
  62. package/packages/react/src/radio-group/index.tsx +199 -0
  63. package/packages/react/src/select/index.tsx +51 -28
  64. package/packages/react/src/select/select.test.tsx +21 -0
  65. package/packages/react/src/separator/index.tsx +23 -0
  66. package/packages/react/src/slider/index.tsx +264 -0
  67. package/packages/react/src/toast/primitives.tsx +8 -1
  68. package/packages/react/src/toast/toast.test.tsx +40 -0
  69. package/packages/react/src/tooltip/index.tsx +124 -20
  70. package/packages/react/src/tooltip/tooltip.test.tsx +114 -0
  71. package/packages/site/README.md +1 -1
  72. package/packages/ui/src/index.ts +5 -0
  73. package/packages/ui/src/pin-input.tsx +16 -0
  74. package/packages/ui/src/progress.tsx +21 -0
  75. package/packages/ui/src/radio-group.tsx +24 -0
  76. package/packages/ui/src/separator.tsx +15 -0
  77. package/packages/ui/src/slider.tsx +24 -0
  78. package/apps/demo/public/og.png +0 -0
  79. 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
- props,
185
+ { onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, onPointerDown, ...rest },
131
186
  ref,
132
187
  ) {
133
- const { open, triggerId, contentId, scheduleShow, scheduleHide, hideNow, showOnFocus } =
134
- useTooltipCtx()
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={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: scheduleShow,
145
- onMouseLeave: scheduleHide,
146
- onFocus: showOnFocus ? scheduleShow : undefined,
147
- onBlur: showOnFocus ? scheduleHide : undefined,
148
- /* Dismiss on press only for focus-driven tooltips; on dense click targets
149
- pointerdown-dismiss races the child button click and drops the event. */
150
- onPointerDown: showOnFocus ? hideNow : undefined,
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
- props,
239
+ rest,
153
240
  )}
154
241
  />
155
242
  )
156
243
  })
157
244
 
158
- const Positioner = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function TooltipPositioner(
159
- props,
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
- if (!open) return null
256
+ const { open, triggerRef, positionerRef } = useTooltipCtx()
257
+
164
258
  return (
165
- <div
259
+ <FloatingPositioner
166
260
  ref={ref}
167
- {...mergeProps(partAttrs(tooltipAnatomy.scope, tooltipAnatomy.positioner, { state: 'open' }), props)}
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
+ })
@@ -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
  ```
@@ -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
+ }
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
- }