aeon-ui-engine 1.4.0 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/CONSUMER.md +67 -27
  3. package/INTEGRATION.md +1 -1
  4. package/README.md +17 -8
  5. package/package.json +4 -1
  6. package/packages/cli/dist/bin.js +29 -3
  7. package/packages/cli/dist/{chunk-5L47SLNV.js → chunk-JMNRJCWF.js} +63 -9
  8. package/packages/cli/dist/index.d.ts +15 -1
  9. package/packages/cli/dist/index.js +3 -1
  10. package/packages/cli/src/bin.ts +30 -2
  11. package/packages/cli/src/index.ts +1 -0
  12. package/packages/cli/src/init.ts +10 -4
  13. package/packages/cli/src/link.ts +61 -0
  14. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  15. package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
  16. package/packages/primitives/dist/index.js +1 -1
  17. package/packages/primitives/dist/machines/range-slider.d.ts +3 -3
  18. package/packages/primitives/dist/machines/slider.d.ts +22 -0
  19. package/packages/primitives/dist/machines/slider.js +1 -1
  20. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  21. package/packages/primitives/src/machines/slider.ts +17 -0
  22. package/packages/react/dist/index.d.ts +15 -1
  23. package/packages/react/dist/index.js +89 -21
  24. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  25. package/packages/react/src/floating/FloatingPositioner.tsx +8 -0
  26. package/packages/react/src/hooks/use-anchor-position.ts +88 -17
  27. package/packages/react/src/menu/index.tsx +37 -3
  28. package/packages/react/src/popover/index.tsx +7 -1
  29. package/packages/react/src/slider/index.tsx +54 -14
  30. package/packages/react/src/tooltip/index.tsx +7 -1
  31. package/packages/schemas/prompts/cursorrules.md +3 -1
  32. package/packages/schemas/prompts/statecharts.md +1 -1
  33. package/packages/schemas/src/prompts.ts +3 -1
  34. package/scripts/link-scoped-packages.mjs +206 -0
  35. package/scripts/vite-aliases.mjs +6 -8
@@ -25,6 +25,8 @@ interface SliderContextValue {
25
25
  dragging: boolean
26
26
  send: ReturnType<typeof useAeonMachine<typeof sliderMachine>>[1]
27
27
  onValueChange?: (value: number) => void
28
+ /** Snap, store and report a user-intended value. */
29
+ commit: (value: number) => void
28
30
  setValueFromClientX: (clientX: number) => void
29
31
  trackRef: React.RefObject<HTMLDivElement | null>
30
32
  }
@@ -83,14 +85,33 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
83
85
  const resolvedValue = value ?? snapshot.context.value
84
86
  const dragging = snapshot.matches({ interaction: 'dragging' })
85
87
 
86
- useEffect(() => {
87
- onValueChange?.(resolvedValue)
88
- }, [resolvedValue, onValueChange])
89
-
90
88
  useEffect(() => {
91
89
  if (value !== undefined) send({ type: 'SET_VALUE', value })
92
90
  }, [value, send])
93
91
 
92
+ useEffect(() => {
93
+ send({ type: 'SET_BOUNDS', min, max, step })
94
+ }, [min, max, step, send])
95
+
96
+ useEffect(() => {
97
+ send({ type: 'SET_DISABLED', disabled })
98
+ }, [disabled, send])
99
+
100
+ /**
101
+ * Interaction is emitted here, never echoed from the resolved value. A
102
+ * controlled slider therefore reports what the user did (Aeon ≤1.4.0 stayed
103
+ * silent while `value` was set) and never fires on mount.
104
+ */
105
+ const commit = useCallback(
106
+ (next: number) => {
107
+ if (disabled) return
108
+ const snapped = snapSliderValue(next, min, max, step)
109
+ send({ type: 'SET_VALUE', value: snapped })
110
+ onValueChange?.(snapped)
111
+ },
112
+ [disabled, min, max, step, send, onValueChange],
113
+ )
114
+
94
115
  const setValueFromClientX = useCallback(
95
116
  (clientX: number) => {
96
117
  const track = trackRef.current
@@ -98,11 +119,9 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
98
119
  const rect = track.getBoundingClientRect()
99
120
  if (rect.width <= 0) return
100
121
  const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width))
101
- const next = snapSliderValue(min + ratio * (max - min), min, max, step)
102
- send({ type: 'SET_VALUE', value: next })
103
- if (value === undefined) onValueChange?.(next)
122
+ commit(min + ratio * (max - min))
104
123
  },
105
- [disabled, min, max, step, send, value, onValueChange],
124
+ [commit, disabled, min, max],
106
125
  )
107
126
 
108
127
  useEffect(() => {
@@ -127,10 +146,22 @@ const Root = forwardRef<HTMLDivElement, SliderRootProps>(function SliderRoot(
127
146
  dragging,
128
147
  send,
129
148
  onValueChange,
149
+ commit,
130
150
  setValueFromClientX,
131
151
  trackRef,
132
152
  }),
133
- [resolvedValue, min, max, step, disabled, dragging, send, onValueChange, setValueFromClientX],
153
+ [
154
+ resolvedValue,
155
+ min,
156
+ max,
157
+ step,
158
+ disabled,
159
+ dragging,
160
+ send,
161
+ onValueChange,
162
+ commit,
163
+ setValueFromClientX,
164
+ ],
134
165
  )
135
166
 
136
167
  return (
@@ -201,7 +232,8 @@ const Thumb = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(funct
201
232
  { onKeyDown, onPointerDown, style, ...rest },
202
233
  ref,
203
234
  ) {
204
- const { value, min, max, step, disabled, dragging, send, setValueFromClientX } = useSliderCtx()
235
+ const { value, min, max, step, disabled, dragging, send, commit, setValueFromClientX } =
236
+ useSliderCtx()
205
237
 
206
238
  return (
207
239
  <span
@@ -229,19 +261,27 @@ const Thumb = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(funct
229
261
  if (disabled || e.defaultPrevented) return
230
262
  if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
231
263
  e.preventDefault()
232
- send({ type: 'STEP', delta: step })
264
+ commit(value + step)
233
265
  }
234
266
  if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
235
267
  e.preventDefault()
236
- send({ type: 'STEP', delta: -step })
268
+ commit(value - step)
269
+ }
270
+ if (e.key === 'PageUp') {
271
+ e.preventDefault()
272
+ commit(value + (max - min) / 10)
273
+ }
274
+ if (e.key === 'PageDown') {
275
+ e.preventDefault()
276
+ commit(value - (max - min) / 10)
237
277
  }
238
278
  if (e.key === 'Home') {
239
279
  e.preventDefault()
240
- send({ type: 'HOME' })
280
+ commit(min)
241
281
  }
242
282
  if (e.key === 'End') {
243
283
  e.preventDefault()
244
- send({ type: 'END' })
284
+ commit(max)
245
285
  }
246
286
  },
247
287
  },
@@ -272,11 +272,15 @@ export interface TooltipPositionerProps extends HTMLAttributes<HTMLDivElement> {
272
272
  /** Mount tooltip in document.body (default true). */
273
273
  portalled?: boolean
274
274
  placement?: AnchorPlacement
275
+ /** Gap between trigger and tip (default 4). */
276
+ gutter?: number
277
+ /** Stack level for the portalled surface (default 50). */
278
+ zIndex?: number
275
279
  children?: ReactNode
276
280
  }
277
281
 
278
282
  const Positioner = forwardRef<HTMLDivElement, TooltipPositionerProps>(function TooltipPositioner(
279
- { portalled = true, placement = 'top-start', children, className, ...rest },
283
+ { portalled = true, placement = 'top-start', gutter, zIndex, children, className, ...rest },
280
284
  ref,
281
285
  ) {
282
286
  const { open, triggerRef, positionerRef } = useTooltipCtx()
@@ -291,6 +295,8 @@ const Positioner = forwardRef<HTMLDivElement, TooltipPositionerProps>(function T
291
295
  positionerRef={positionerRef}
292
296
  portalled={portalled}
293
297
  placement={placement}
298
+ gutter={gutter}
299
+ zIndex={zIndex}
294
300
  className={className}
295
301
  {...rest}
296
302
  >
@@ -35,7 +35,9 @@ When generating Aeon UI, constrain props/parts/states to @aeon-ui/schemas catalo
35
35
  Import headless from @aeon-ui/react or styled from @aeon-ui/ui.
36
36
  Install: npm install aeon-ui-engine react react-dom xstate @xstate/react
37
37
  CSS: import 'aeon-ui-engine/aeon.css'
38
- Vite: aeonUiVitePlugin() from aeon-ui-engine/vite
38
+ Link: postinstall + npx aeon-ui link (node_modules/@aeon-ui/*)
39
+ Next.js: transpilePackages for @aeon-ui/* + "use client"; Vite plugin optional
40
+ Vue/Solid/Svelte: thin adapters only — React is the full catalog
39
41
 
40
42
  ## Async region statechart
41
43
  States: idle → loading → (success | empty | failure); REFRESH from success/empty/failure; STALE flag while success.
@@ -34,7 +34,7 @@ Rule: Hidden input stays in sync with snapshot for forms.
34
34
 
35
35
  ## Slider statechart
36
36
  Machine: sliderMachine — idle | dragging; value/min/max/step in context.
37
- Events: SET_VALUE, STEP, POINTER_DOWN, POINTER_UP, HOME, END.
37
+ Events: SET_VALUE, STEP, POINTER_DOWN, POINTER_UP, HOME, END, SET_BOUNDS, SET_DISABLED.
38
38
  Rule: Disabled blocks SET_VALUE / drag; project idle vs dragging on data-aeon-state.
39
39
 
40
40
  ## Content region statechart
@@ -128,7 +128,9 @@ When generating Aeon UI, constrain props/parts/states to @aeon-ui/schemas catalo
128
128
  Import headless from @aeon-ui/react or styled from @aeon-ui/ui.
129
129
  Install: npm install aeon-ui-engine react react-dom xstate @xstate/react
130
130
  CSS: import 'aeon-ui-engine/aeon.css'
131
- Vite: aeonUiVitePlugin() from aeon-ui-engine/vite
131
+ Link: postinstall + npx aeon-ui link (node_modules/@aeon-ui/*)
132
+ Next.js: transpilePackages for @aeon-ui/* + "use client"; Vite plugin optional
133
+ Vue/Solid/Svelte: thin adapters only — React is the full catalog
132
134
 
133
135
  ${STATECHART_PROMPTS.async}
134
136
 
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Materialize `node_modules/@aeon-ui/*` → `aeon-ui-engine/packages/*`
3
+ * so Next.js / Vite / Node can resolve scoped imports without bundler aliases.
4
+ *
5
+ * Runs as `aeon-ui-engine` postinstall (uses INIT_CWD) or via `npx aeon-ui link`.
6
+ *
7
+ * @example
8
+ * node ./scripts/link-scoped-packages.mjs
9
+ * node ./scripts/link-scoped-packages.mjs --cwd /path/to/app
10
+ */
11
+
12
+ import { createRequire } from 'node:module'
13
+ import {
14
+ existsSync,
15
+ lstatSync,
16
+ mkdirSync,
17
+ readlinkSync,
18
+ realpathSync,
19
+ rmSync,
20
+ symlinkSync,
21
+ } from 'node:fs'
22
+ import path from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+
25
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
26
+
27
+ /** Packages exposed under `@aeon-ui/<name>` for consumers. */
28
+ export const SCOPED_PACKAGES = [
29
+ 'core',
30
+ 'primitives',
31
+ 'react',
32
+ 'ui',
33
+ 'panda',
34
+ 'schemas',
35
+ 'surface',
36
+ 'vue',
37
+ 'solid',
38
+ 'svelte',
39
+ ]
40
+
41
+ /**
42
+ * Absolute path to the `aeon-ui-engine` package root (npm install or monorepo).
43
+ */
44
+ export function resolveEngineRoot(opts = {}) {
45
+ if (opts.engineRoot) return path.resolve(opts.engineRoot)
46
+ if (process.env.AEON_UI_ROOT) return path.resolve(process.env.AEON_UI_ROOT)
47
+
48
+ try {
49
+ const require = createRequire(import.meta.url)
50
+ return path.dirname(require.resolve('aeon-ui-engine/package.json'))
51
+ } catch {
52
+ // Running from the monorepo checkout (this file lives in scripts/).
53
+ return path.resolve(scriptDir, '..')
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Consumer `node_modules` where `@aeon-ui/*` should appear.
59
+ * Prefer INIT_CWD (npm/pnpm/yarn set this to the install project).
60
+ */
61
+ export function resolveTargetNodeModules(opts = {}) {
62
+ if (opts.nodeModules) return path.resolve(opts.nodeModules)
63
+ if (opts.cwd) return path.join(path.resolve(opts.cwd), 'node_modules')
64
+
65
+ // Prefer INIT_CWD (npm/pnpm/yarn set this to the install project).
66
+ // Skip when postinstall runs inside the engine package itself during a
67
+ // monorepo install — workspace already provides @aeon-ui/*.
68
+ if (process.env.INIT_CWD) {
69
+ const initCwd = path.resolve(process.env.INIT_CWD)
70
+ const engineRoot = resolveEngineRoot(opts)
71
+ if (initCwd === engineRoot) {
72
+ return path.join(engineRoot, 'node_modules')
73
+ }
74
+ return path.join(initCwd, 'node_modules')
75
+ }
76
+
77
+ const engineRoot = resolveEngineRoot(opts)
78
+ // Installed as dependency: …/node_modules/aeon-ui-engine
79
+ const parent = path.dirname(engineRoot)
80
+ if (path.basename(parent) === 'node_modules') {
81
+ return parent
82
+ }
83
+
84
+ // Monorepo / direct run: link into engine's own node_modules
85
+ return path.join(engineRoot, 'node_modules')
86
+ }
87
+
88
+ function isSymlink(p) {
89
+ try {
90
+ return lstatSync(p).isSymbolicLink()
91
+ } catch {
92
+ return false
93
+ }
94
+ }
95
+
96
+ function sameTarget(linkPath, absTarget) {
97
+ try {
98
+ const current = readlinkSync(linkPath)
99
+ const resolved = path.isAbsolute(current)
100
+ ? path.normalize(current)
101
+ : path.resolve(path.dirname(linkPath), current)
102
+ if (resolved === absTarget) return true
103
+ } catch {
104
+ /* not a symlink or unreadable */
105
+ }
106
+ try {
107
+ return realpathSync(linkPath) === realpathSync(absTarget)
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+
113
+ /**
114
+ * @param {{ cwd?: string, engineRoot?: string, nodeModules?: string, quiet?: boolean, force?: boolean }} [opts]
115
+ * @returns {{ linked: string[], skipped: string[], missing: string[], targetNodeModules: string, engineRoot: string }}
116
+ */
117
+ export function linkScopedPackages(opts = {}) {
118
+ const quiet = opts.quiet ?? false
119
+ const force = opts.force ?? false
120
+ const engineRoot = resolveEngineRoot(opts)
121
+ const targetNodeModules = resolveTargetNodeModules({ ...opts, engineRoot })
122
+ const scopeDir = path.join(targetNodeModules, '@aeon-ui')
123
+ const packagesRoot = path.join(engineRoot, 'packages')
124
+
125
+ const linked = []
126
+ const skipped = []
127
+ const missing = []
128
+
129
+ mkdirSync(scopeDir, { recursive: true })
130
+
131
+ for (const name of SCOPED_PACKAGES) {
132
+ const absTarget = path.join(packagesRoot, name)
133
+ if (!existsSync(absTarget)) {
134
+ missing.push(name)
135
+ continue
136
+ }
137
+
138
+ const linkPath = path.join(scopeDir, name)
139
+
140
+ if (existsSync(linkPath) || isSymlink(linkPath)) {
141
+ if (sameTarget(linkPath, absTarget)) {
142
+ skipped.push(name)
143
+ continue
144
+ }
145
+ // Do not clobber pnpm/npm workspace links unless --force
146
+ if (!force) {
147
+ skipped.push(name)
148
+ continue
149
+ }
150
+ rmSync(linkPath, { recursive: true, force: true })
151
+ }
152
+
153
+ const type = process.platform === 'win32' ? 'junction' : 'dir'
154
+ symlinkSync(absTarget, linkPath, type)
155
+ linked.push(name)
156
+ }
157
+
158
+ if (!quiet) {
159
+ const rel = (n) => `@aeon-ui/${n}`
160
+ for (const n of linked) console.log(` link ${rel(n)} → packages/${n}`)
161
+ for (const n of skipped) console.log(` skip ${rel(n)} (already present)`)
162
+ for (const n of missing) console.log(` miss ${rel(n)} (not in engine)`)
163
+ if (linked.length === 0 && skipped.length > 0) {
164
+ console.log(`aeon-ui link: ${skipped.length} package(s) already present in ${targetNodeModules}`)
165
+ } else if (linked.length > 0) {
166
+ console.log(`aeon-ui link: linked ${linked.length} package(s) into ${targetNodeModules}`)
167
+ }
168
+ }
169
+
170
+ return { linked, skipped, missing, targetNodeModules, engineRoot }
171
+ }
172
+
173
+ function parseArgs(argv) {
174
+ const opts = {}
175
+ for (let i = 0; i < argv.length; i++) {
176
+ const a = argv[i]
177
+ if (a === '--cwd' && argv[i + 1]) {
178
+ opts.cwd = argv[++i]
179
+ } else if (a === '--engine-root' && argv[i + 1]) {
180
+ opts.engineRoot = argv[++i]
181
+ } else if (a === '--quiet' || a === '-q') {
182
+ opts.quiet = true
183
+ } else if (a === '--force' || a === '-f') {
184
+ opts.force = true
185
+ }
186
+ }
187
+ return opts
188
+ }
189
+
190
+ const isMain = (() => {
191
+ if (!process.argv[1]) return false
192
+ try {
193
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])
194
+ } catch {
195
+ return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1])
196
+ }
197
+ })()
198
+
199
+ if (isMain) {
200
+ try {
201
+ linkScopedPackages(parseArgs(process.argv.slice(2)))
202
+ } catch (err) {
203
+ console.error('aeon-ui link failed:', err instanceof Error ? err.message : err)
204
+ process.exitCode = 1
205
+ }
206
+ }
@@ -1,19 +1,17 @@
1
1
  /**
2
- * Vite + TypeScript wiring for apps that install `aeon-ui-engine` from npm.
2
+ * Vite helpers for apps that install `aeon-ui-engine` from npm.
3
+ *
4
+ * Scoped `@aeon-ui/*` imports resolve via postinstall linking
5
+ * (`aeon-ui-engine/link` / `npx aeon-ui link`). This plugin is optional —
6
+ * it still helps with optimizeDeps and monorepo `packagesRoot`.
3
7
  *
4
8
  * @example
5
- * // vite.config.ts — one call, done
9
+ * // vite.config.ts — optional
6
10
  * import { aeonUiVitePlugin } from 'aeon-ui-engine/vite'
7
11
  *
8
12
  * export default defineConfig({
9
13
  * plugins: [react(), aeonUiVitePlugin()],
10
14
  * })
11
- *
12
- * // tsconfig.json — spread the base config
13
- * {
14
- * "extends": "aeon-ui-engine/tsconfig",
15
- * "compilerOptions": { "jsx": "react-jsx" }
16
- * }
17
15
  */
18
16
  import { createRequire } from 'node:module'
19
17
  import path from 'node:path'