aeon-ui-engine 1.5.1 → 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 (43) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/package.json +2 -2
  3. package/packages/cli/node_modules/.bin/vitest +2 -2
  4. package/packages/core/node_modules/.bin/vitest +2 -2
  5. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  6. package/packages/panda/styled-system/styles.css +0 -4
  7. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  8. package/packages/react/dist/index.d.ts +29 -4
  9. package/packages/react/dist/index.js +85 -28
  10. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  11. package/packages/react/src/dialog/dialog.test.tsx +27 -0
  12. package/packages/react/src/dialog/index.tsx +11 -3
  13. package/packages/react/src/floating/FloatingPositioner.tsx +11 -1
  14. package/packages/react/src/menu/index.tsx +8 -3
  15. package/packages/react/src/popover/index.tsx +25 -4
  16. package/packages/react/src/slider/index.tsx +13 -6
  17. package/packages/schemas/dist/index.js +3 -1
  18. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/packages/schemas/prompts/statecharts.md +1 -1
  20. package/packages/schemas/registry/machines/slider.json +18 -1
  21. package/packages/tree/README.md +29 -0
  22. package/packages/tree/dist/index.d.ts +97 -0
  23. package/packages/tree/dist/index.js +107 -0
  24. package/packages/tree/node_modules/.bin/esbuild +21 -0
  25. package/packages/tree/node_modules/.bin/jiti +21 -0
  26. package/packages/tree/node_modules/.bin/tsc +21 -0
  27. package/packages/tree/node_modules/.bin/tsserver +21 -0
  28. package/packages/tree/node_modules/.bin/tsup +21 -0
  29. package/packages/tree/node_modules/.bin/tsup-node +21 -0
  30. package/packages/tree/node_modules/.bin/tsx +21 -0
  31. package/packages/tree/node_modules/.bin/vite +21 -0
  32. package/packages/tree/node_modules/.bin/vitest +21 -0
  33. package/packages/tree/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  34. package/packages/tree/package.json +42 -0
  35. package/packages/tree/src/exclusive-action.test.ts +54 -0
  36. package/packages/tree/src/exclusive-action.ts +104 -0
  37. package/packages/tree/src/index.ts +7 -0
  38. package/packages/tree/src/interpret.tsx +45 -0
  39. package/packages/tree/src/totality.ts +20 -0
  40. package/packages/tree/src/widget.ts +31 -0
  41. package/packages/tree/tsconfig.json +8 -0
  42. package/packages/tree/vitest.config.ts +8 -0
  43. package/scripts/vite-aliases.mjs +3 -0
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/vitest@4.1.8_@types+node@22.19.19_jsdom@29.1.1_vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3_/node_modules/vitest/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/vitest@4.1.8_@types+node@22.19.19_jsdom@29.1.1_vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3_/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/vitest@4.1.8_@types+node@22.19.19_jsdom@29.1.1_vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3_/node_modules/vitest/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/vitest@4.1.8_@types+node@22.19.19_jsdom@29.1.1_vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3_/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
21
+ fi
@@ -0,0 +1 @@
1
+ {"version":"4.1.8","results":[[":src/exclusive-action.test.ts",{"duration":2.708500000000001,"failed":false}]]}
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@aeon-ui/tree",
3
+ "version": "0.1.0",
4
+ "description": "Widget trees: UI as data projected from a statechart snapshot. Flutter's Widget layer, Aeon's anatomy, no local state.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/GenericCPU/aeon-ui.git",
9
+ "directory": "packages/tree"
10
+ },
11
+ "publishConfig": { "access": "public" },
12
+ "files": ["dist", "README.md"],
13
+ "keywords": ["aeon-ui", "statechart", "widget", "xstate"],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsup src/index.ts --format esm --dts --clean --external react",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run"
28
+ },
29
+ "dependencies": {
30
+ "@aeon-ui/core": "workspace:*"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^18 || ^19"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "^19.1.2",
37
+ "react": "^19.1.0",
38
+ "tsup": "^8.4.0",
39
+ "typescript": "^5.8.3",
40
+ "vitest": "^4.1.8"
41
+ }
42
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { exclusiveActionWidget, EXCLUSIVE_ACTION_STATES } from './exclusive-action.js'
3
+ import { uncoveredStates, widgetStates } from './totality.js'
4
+ import { host } from './widget.js'
5
+
6
+ const project = (state: string) =>
7
+ exclusiveActionWidget({
8
+ scope: 'example',
9
+ part: 'form',
10
+ state,
11
+ busy: state === 'busy' || state === 'confirming',
12
+ error: state === 'failure' ? 'nope' : null,
13
+ body: host('body'),
14
+ idleLabel: 'Save',
15
+ pendingLabel: 'Saving…',
16
+ onSubmit: () => {},
17
+ confirm: state === 'confirming',
18
+ })
19
+
20
+ describe('exclusiveActionWidget', () => {
21
+ it('covers every state of the chart', () => {
22
+ expect(uncoveredStates(EXCLUSIVE_ACTION_STATES, project)).toEqual([])
23
+ })
24
+
25
+ it('shows the failure copy only on failure', () => {
26
+ expect(widgetStates(project('idle'))).not.toContain('failure')
27
+ const failure = project('failure')
28
+ const error = failure.type === 'el' && failure.children.find((c) => c.type === 'el' && c.part === 'error')
29
+ expect(error && error.type === 'el' && error.children[0]).toEqual({ type: 'text', text: 'nope' })
30
+ })
31
+
32
+ it('mounts the confirm host only while confirming', () => {
33
+ const confirming = project('confirming')
34
+ const idle = project('idle')
35
+ const hasConfirm = (tree: ReturnType<typeof project>) =>
36
+ tree.type === 'el' && tree.children.some((c) => c.type === 'host' && c.id === 'confirm')
37
+ expect(hasConfirm(confirming)).toBe(true)
38
+ expect(hasConfirm(idle)).toBe(false)
39
+ })
40
+
41
+ it('labels the primary from busy, and refuses a second click', () => {
42
+ const busy = project('busy')
43
+ const primary =
44
+ busy.type === 'el'
45
+ ? busy.children
46
+ .flatMap((c) => (c.type === 'el' ? c.children : []))
47
+ .find((c) => c.type === 'el' && c.part === 'primary')
48
+ : undefined
49
+ expect(primary && primary.type === 'el' && primary.disabled).toBe(true)
50
+ expect(primary && primary.type === 'el' && primary.children).toEqual([
51
+ { type: 'text', text: 'Saving…' },
52
+ ])
53
+ })
54
+ })
@@ -0,0 +1,104 @@
1
+ import { host, text, type Widget } from './widget.js'
2
+
3
+ /**
4
+ * The chart every "click, maybe confirm, await, succeed or fail" region
5
+ * shares. One region, four faces — not a `busy` boolean beside the markup.
6
+ */
7
+ export const EXCLUSIVE_ACTION_STATES = ['idle', 'confirming', 'busy', 'failure'] as const
8
+
9
+ export type ExclusiveActionState = (typeof EXCLUSIVE_ACTION_STATES)[number]
10
+
11
+ export type ExclusiveActionInput = {
12
+ scope: string
13
+ part: string
14
+ /** Active snapshot token, usually `stateToAttr(snapshot.value)`. */
15
+ state: string
16
+ busy: boolean
17
+ error: string | null
18
+ className?: string
19
+ tag?: 'form' | 'div'
20
+ /** Panel-specific fields. A host, so the tree stays generic. */
21
+ body: Widget
22
+ idleLabel: string
23
+ pendingLabel: string
24
+ primaryClassName?: string
25
+ primaryDisabled?: boolean
26
+ onSubmit?: (event: { preventDefault(): void }) => void
27
+ secondary?: {
28
+ label: string
29
+ className?: string
30
+ disabled?: boolean
31
+ onClick: () => void
32
+ }
33
+ actionsClassName?: string
34
+ /** When set, the `confirm` host is mounted. The app renders its Prompt there. */
35
+ confirm?: boolean
36
+ }
37
+
38
+ /**
39
+ * One widget for an exclusive mutation. The root carries the chart state;
40
+ * the primary label and the disabled set are functions of `busy`, and the
41
+ * failure copy is a child that exists only when `error` is set.
42
+ */
43
+ export function exclusiveActionWidget(input: ExclusiveActionInput): Widget {
44
+ const children: Widget[] = [input.body]
45
+
46
+ if (input.error) {
47
+ children.push({
48
+ type: 'el',
49
+ tag: 'p',
50
+ part: 'error',
51
+ className: 'error',
52
+ role: 'alert',
53
+ children: [text(input.error)],
54
+ })
55
+ }
56
+
57
+ const actions: Widget[] = [
58
+ {
59
+ type: 'el',
60
+ tag: 'button',
61
+ part: 'primary',
62
+ buttonType: input.tag === 'div' ? 'button' : 'submit',
63
+ disabled: input.busy || input.primaryDisabled,
64
+ className: input.primaryClassName,
65
+ onClick: input.tag === 'div' ? () => input.onSubmit?.({ preventDefault() {} }) : undefined,
66
+ children: [text(input.busy ? input.pendingLabel : input.idleLabel)],
67
+ },
68
+ ]
69
+
70
+ if (input.secondary) {
71
+ actions.push({
72
+ type: 'el',
73
+ tag: 'button',
74
+ part: 'secondary',
75
+ buttonType: 'button',
76
+ disabled: input.busy || input.secondary.disabled,
77
+ className: input.secondary.className,
78
+ onClick: input.secondary.onClick,
79
+ children: [text(input.secondary.label)],
80
+ })
81
+ }
82
+
83
+ children.push({
84
+ type: 'el',
85
+ tag: 'div',
86
+ part: 'actions',
87
+ className: input.actionsClassName,
88
+ state: input.state,
89
+ children: actions,
90
+ })
91
+
92
+ if (input.confirm) children.push(host('confirm'))
93
+
94
+ return {
95
+ type: 'el',
96
+ tag: input.tag ?? 'form',
97
+ scope: input.scope,
98
+ part: input.part,
99
+ state: input.state,
100
+ className: input.className,
101
+ onSubmit: input.tag === 'div' ? undefined : input.onSubmit,
102
+ children,
103
+ }
104
+ }
@@ -0,0 +1,7 @@
1
+ export { exclusiveActionWidget, EXCLUSIVE_ACTION_STATES } from './exclusive-action.js'
2
+ export type { ExclusiveActionInput, ExclusiveActionState } from './exclusive-action.js'
3
+ export { interpret } from './interpret.js'
4
+ export type { WidgetHosts } from './interpret.js'
5
+ export { uncoveredStates, widgetStates } from './totality.js'
6
+ export { host, text } from './widget.js'
7
+ export type { Widget } from './widget.js'
@@ -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
+ })
@@ -99,6 +99,7 @@ export function aeonUiViteAliases(opts = {}) {
99
99
  { find: '@aeon-ui/react', replacement: path.join(packages, 'react/src/index.ts') },
100
100
  { find: '@aeon-ui/ui', replacement: path.join(packages, 'ui/src/index.ts') },
101
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') },
102
103
  { find: '@aeon-ui/panda', replacement: path.join(packages, 'panda') },
103
104
  ]
104
105
  }
@@ -120,6 +121,7 @@ export function aeonUiOptimizeDeps(opts = {}) {
120
121
  '@aeon-ui/primitives',
121
122
  '@aeon-ui/react',
122
123
  '@aeon-ui/ui',
124
+ '@aeon-ui/tree',
123
125
  '@aeon-ui/panda/styled-system/css',
124
126
  '@aeon-ui/panda/styled-system/recipes',
125
127
  ...(opts.exclude ?? []),
@@ -140,6 +142,7 @@ export function aeonUiTsconfigPaths(opts = {}) {
140
142
  '@aeon-ui/react': [`${prefix}/react/src/index.ts`],
141
143
  '@aeon-ui/ui': [`${prefix}/ui/src/index.ts`],
142
144
  '@aeon-ui/surface': [`${prefix}/surface/src/index.ts`],
145
+ '@aeon-ui/tree': [`${prefix}/tree/src/index.ts`],
143
146
  '@aeon-ui/panda/styled-system/css': [`${prefix}/panda/styled-system/css/index.d.ts`],
144
147
  '@aeon-ui/panda/styled-system/recipes': [`${prefix}/panda/styled-system/recipes/index.d.ts`],
145
148
  }