aeon-ui-engine 1.4.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/CONSUMER.md +67 -27
  3. package/INTEGRATION.md +1 -1
  4. package/README.md +17 -8
  5. package/package.json +5 -2
  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/node_modules/.bin/vitest +2 -2
  11. package/packages/cli/src/bin.ts +30 -2
  12. package/packages/cli/src/index.ts +1 -0
  13. package/packages/cli/src/init.ts +10 -4
  14. package/packages/cli/src/link.ts +61 -0
  15. package/packages/core/node_modules/.bin/vitest +2 -2
  16. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  17. package/packages/panda/styled-system/styles.css +0 -4
  18. package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
  19. package/packages/primitives/dist/index.js +1 -1
  20. package/packages/primitives/dist/machines/range-slider.d.ts +3 -3
  21. package/packages/primitives/dist/machines/slider.d.ts +22 -0
  22. package/packages/primitives/dist/machines/slider.js +1 -1
  23. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  24. package/packages/primitives/src/machines/slider.ts +17 -0
  25. package/packages/react/dist/index.d.ts +44 -5
  26. package/packages/react/dist/index.js +173 -48
  27. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  28. package/packages/react/src/dialog/dialog.test.tsx +27 -0
  29. package/packages/react/src/dialog/index.tsx +11 -3
  30. package/packages/react/src/floating/FloatingPositioner.tsx +19 -1
  31. package/packages/react/src/hooks/use-anchor-position.ts +88 -17
  32. package/packages/react/src/menu/index.tsx +45 -6
  33. package/packages/react/src/popover/index.tsx +31 -4
  34. package/packages/react/src/slider/index.tsx +66 -19
  35. package/packages/react/src/tooltip/index.tsx +7 -1
  36. package/packages/schemas/dist/index.js +3 -1
  37. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  38. package/packages/schemas/prompts/cursorrules.md +3 -1
  39. package/packages/schemas/registry/machines/slider.json +18 -1
  40. package/packages/schemas/src/prompts.ts +3 -1
  41. package/packages/tree/README.md +29 -0
  42. package/packages/tree/dist/index.d.ts +97 -0
  43. package/packages/tree/dist/index.js +107 -0
  44. package/packages/tree/node_modules/.bin/esbuild +21 -0
  45. package/packages/tree/node_modules/.bin/jiti +21 -0
  46. package/packages/tree/node_modules/.bin/tsc +21 -0
  47. package/packages/tree/node_modules/.bin/tsserver +21 -0
  48. package/packages/tree/node_modules/.bin/tsup +21 -0
  49. package/packages/tree/node_modules/.bin/tsup-node +21 -0
  50. package/packages/tree/node_modules/.bin/tsx +21 -0
  51. package/packages/tree/node_modules/.bin/vite +21 -0
  52. package/packages/tree/node_modules/.bin/vitest +21 -0
  53. package/packages/tree/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  54. package/packages/tree/package.json +42 -0
  55. package/packages/tree/src/exclusive-action.test.ts +54 -0
  56. package/packages/tree/src/exclusive-action.ts +104 -0
  57. package/packages/tree/src/index.ts +7 -0
  58. package/packages/tree/src/interpret.tsx +45 -0
  59. package/packages/tree/src/totality.ts +20 -0
  60. package/packages/tree/src/widget.ts +31 -0
  61. package/packages/tree/tsconfig.json +8 -0
  62. package/packages/tree/vitest.config.ts +8 -0
  63. package/scripts/link-scoped-packages.mjs +206 -0
  64. package/scripts/vite-aliases.mjs +9 -8
@@ -0,0 +1,45 @@
1
+ import { partOnlyAttrs, scopeAttrs } from '@aeon-ui/core'
2
+ import { createElement, Fragment, type ReactNode } from 'react'
3
+ import type { Widget } from './widget.js'
4
+
5
+ export type WidgetHosts = Record<string, () => ReactNode>
6
+
7
+ /**
8
+ * Element layer: turn a widget tree into DOM. Anatomy comes from
9
+ * `@aeon-ui/core`, so product CSS restyles `data-aeon-*` and nothing else
10
+ * is introduced. Hosts are the escape for existing compounds (Prompt, a
11
+ * password field) — the tree names the slot, the app supplies the compound.
12
+ */
13
+ export function interpret(widget: Widget, hosts: WidgetHosts): ReactNode {
14
+ if (widget.type === 'text') return widget.text
15
+ if (widget.type === 'host') return hosts[widget.id]?.() ?? null
16
+
17
+ const attrs = widget.scope
18
+ ? scopeAttrs(widget.scope, widget.part ?? 'root', { state: widget.state })
19
+ : widget.part
20
+ ? partOnlyAttrs(widget.part, { state: widget.state })
21
+ : widget.state
22
+ ? { 'data-aeon-state': widget.state }
23
+ : {}
24
+
25
+ const props: Record<string, unknown> = {
26
+ ...attrs,
27
+ className: widget.className,
28
+ role: widget.role,
29
+ disabled: widget.disabled || undefined,
30
+ type: widget.tag === 'button' ? widget.buttonType ?? 'button' : undefined,
31
+ onSubmit:
32
+ widget.tag === 'form' && widget.onSubmit
33
+ ? (event: { preventDefault(): void }) => widget.onSubmit?.(event)
34
+ : undefined,
35
+ onClick: widget.onClick,
36
+ }
37
+
38
+ return createElement(
39
+ widget.tag,
40
+ props,
41
+ ...widget.children.map((child, index) =>
42
+ createElement(Fragment, { key: index }, interpret(child, hosts)),
43
+ ),
44
+ )
45
+ }
@@ -0,0 +1,20 @@
1
+ import type { Widget } from './widget.js'
2
+
3
+ /** Every `data-aeon-state` token a tree would project. */
4
+ export function widgetStates(widget: Widget): string[] {
5
+ if (widget.type !== 'el') return []
6
+ const own = widget.state ? [widget.state] : []
7
+ return [...own, ...widget.children.flatMap(widgetStates)]
8
+ }
9
+
10
+ /**
11
+ * States the chart names that the projection never puts on the tree.
12
+ * Totality (PHILOSOPHY.md §2): if the machine has the state, the face for
13
+ * that state must carry it. A missing name is a face the UI forgot.
14
+ */
15
+ export function uncoveredStates(
16
+ states: readonly string[],
17
+ project: (state: string) => Widget,
18
+ ): string[] {
19
+ return states.filter((state) => !widgetStates(project(state)).includes(state))
20
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A widget is configuration, the way a Flutter `Widget` is: immutable data
3
+ * describing one face of a chart. It holds no lifecycle. The only state it
4
+ * may name is a snapshot already produced by a machine (`data-aeon-state`).
5
+ *
6
+ * Callbacks are event bindings (what to send), not state. There is no
7
+ * equivalent of `StatefulWidget` or `setState` — a second copy of the chart
8
+ * living in the tree would be the boolean twin this layer exists to remove.
9
+ */
10
+ export type Widget =
11
+ | { type: 'text'; text: string }
12
+ | { type: 'host'; id: string }
13
+ | {
14
+ type: 'el'
15
+ tag: 'div' | 'form' | 'button' | 'p' | 'span'
16
+ scope?: string
17
+ part?: string
18
+ /** Chart snapshot token. Projected onto `data-aeon-state`. */
19
+ state?: string
20
+ className?: string
21
+ buttonType?: 'button' | 'submit'
22
+ disabled?: boolean
23
+ role?: string
24
+ onSubmit?: (event: { preventDefault(): void }) => void
25
+ onClick?: () => void
26
+ children: Widget[]
27
+ }
28
+
29
+ export const text = (value: string): Widget => ({ type: 'text', text: value })
30
+
31
+ export const host = (id: string): Widget => ({ type: 'host', id })
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['src/**/*.test.ts'],
7
+ },
8
+ })
@@ -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'
@@ -101,6 +99,7 @@ export function aeonUiViteAliases(opts = {}) {
101
99
  { find: '@aeon-ui/react', replacement: path.join(packages, 'react/src/index.ts') },
102
100
  { find: '@aeon-ui/ui', replacement: path.join(packages, 'ui/src/index.ts') },
103
101
  { find: '@aeon-ui/schemas', replacement: path.join(packages, 'schemas/src/index.ts') },
102
+ { find: '@aeon-ui/tree', replacement: path.join(packages, 'tree/src/index.ts') },
104
103
  { find: '@aeon-ui/panda', replacement: path.join(packages, 'panda') },
105
104
  ]
106
105
  }
@@ -122,6 +121,7 @@ export function aeonUiOptimizeDeps(opts = {}) {
122
121
  '@aeon-ui/primitives',
123
122
  '@aeon-ui/react',
124
123
  '@aeon-ui/ui',
124
+ '@aeon-ui/tree',
125
125
  '@aeon-ui/panda/styled-system/css',
126
126
  '@aeon-ui/panda/styled-system/recipes',
127
127
  ...(opts.exclude ?? []),
@@ -142,6 +142,7 @@ export function aeonUiTsconfigPaths(opts = {}) {
142
142
  '@aeon-ui/react': [`${prefix}/react/src/index.ts`],
143
143
  '@aeon-ui/ui': [`${prefix}/ui/src/index.ts`],
144
144
  '@aeon-ui/surface': [`${prefix}/surface/src/index.ts`],
145
+ '@aeon-ui/tree': [`${prefix}/tree/src/index.ts`],
145
146
  '@aeon-ui/panda/styled-system/css': [`${prefix}/panda/styled-system/css/index.d.ts`],
146
147
  '@aeon-ui/panda/styled-system/recipes': [`${prefix}/panda/styled-system/recipes/index.d.ts`],
147
148
  }