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,97 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ /**
4
+ * A widget is configuration, the way a Flutter `Widget` is: immutable data
5
+ * describing one face of a chart. It holds no lifecycle. The only state it
6
+ * may name is a snapshot already produced by a machine (`data-aeon-state`).
7
+ *
8
+ * Callbacks are event bindings (what to send), not state. There is no
9
+ * equivalent of `StatefulWidget` or `setState` — a second copy of the chart
10
+ * living in the tree would be the boolean twin this layer exists to remove.
11
+ */
12
+ type Widget = {
13
+ type: 'text';
14
+ text: string;
15
+ } | {
16
+ type: 'host';
17
+ id: string;
18
+ } | {
19
+ type: 'el';
20
+ tag: 'div' | 'form' | 'button' | 'p' | 'span';
21
+ scope?: string;
22
+ part?: string;
23
+ /** Chart snapshot token. Projected onto `data-aeon-state`. */
24
+ state?: string;
25
+ className?: string;
26
+ buttonType?: 'button' | 'submit';
27
+ disabled?: boolean;
28
+ role?: string;
29
+ onSubmit?: (event: {
30
+ preventDefault(): void;
31
+ }) => void;
32
+ onClick?: () => void;
33
+ children: Widget[];
34
+ };
35
+ declare const text: (value: string) => Widget;
36
+ declare const host: (id: string) => Widget;
37
+
38
+ /**
39
+ * The chart every "click, maybe confirm, await, succeed or fail" region
40
+ * shares. One region, four faces — not a `busy` boolean beside the markup.
41
+ */
42
+ declare const EXCLUSIVE_ACTION_STATES: readonly ["idle", "confirming", "busy", "failure"];
43
+ type ExclusiveActionState = (typeof EXCLUSIVE_ACTION_STATES)[number];
44
+ type ExclusiveActionInput = {
45
+ scope: string;
46
+ part: string;
47
+ /** Active snapshot token, usually `stateToAttr(snapshot.value)`. */
48
+ state: string;
49
+ busy: boolean;
50
+ error: string | null;
51
+ className?: string;
52
+ tag?: 'form' | 'div';
53
+ /** Panel-specific fields. A host, so the tree stays generic. */
54
+ body: Widget;
55
+ idleLabel: string;
56
+ pendingLabel: string;
57
+ primaryClassName?: string;
58
+ primaryDisabled?: boolean;
59
+ onSubmit?: (event: {
60
+ preventDefault(): void;
61
+ }) => void;
62
+ secondary?: {
63
+ label: string;
64
+ className?: string;
65
+ disabled?: boolean;
66
+ onClick: () => void;
67
+ };
68
+ actionsClassName?: string;
69
+ /** When set, the `confirm` host is mounted. The app renders its Prompt there. */
70
+ confirm?: boolean;
71
+ };
72
+ /**
73
+ * One widget for an exclusive mutation. The root carries the chart state;
74
+ * the primary label and the disabled set are functions of `busy`, and the
75
+ * failure copy is a child that exists only when `error` is set.
76
+ */
77
+ declare function exclusiveActionWidget(input: ExclusiveActionInput): Widget;
78
+
79
+ type WidgetHosts = Record<string, () => ReactNode>;
80
+ /**
81
+ * Element layer: turn a widget tree into DOM. Anatomy comes from
82
+ * `@aeon-ui/core`, so product CSS restyles `data-aeon-*` and nothing else
83
+ * is introduced. Hosts are the escape for existing compounds (Prompt, a
84
+ * password field) — the tree names the slot, the app supplies the compound.
85
+ */
86
+ declare function interpret(widget: Widget, hosts: WidgetHosts): ReactNode;
87
+
88
+ /** Every `data-aeon-state` token a tree would project. */
89
+ declare function widgetStates(widget: Widget): string[];
90
+ /**
91
+ * States the chart names that the projection never puts on the tree.
92
+ * Totality (PHILOSOPHY.md §2): if the machine has the state, the face for
93
+ * that state must carry it. A missing name is a face the UI forgot.
94
+ */
95
+ declare function uncoveredStates(states: readonly string[], project: (state: string) => Widget): string[];
96
+
97
+ export { EXCLUSIVE_ACTION_STATES, type ExclusiveActionInput, type ExclusiveActionState, type Widget, type WidgetHosts, exclusiveActionWidget, host, interpret, text, uncoveredStates, widgetStates };
@@ -0,0 +1,107 @@
1
+ // src/widget.ts
2
+ var text = (value) => ({ type: "text", text: value });
3
+ var host = (id) => ({ type: "host", id });
4
+
5
+ // src/exclusive-action.ts
6
+ var EXCLUSIVE_ACTION_STATES = ["idle", "confirming", "busy", "failure"];
7
+ function exclusiveActionWidget(input) {
8
+ const children = [input.body];
9
+ if (input.error) {
10
+ children.push({
11
+ type: "el",
12
+ tag: "p",
13
+ part: "error",
14
+ className: "error",
15
+ role: "alert",
16
+ children: [text(input.error)]
17
+ });
18
+ }
19
+ const actions = [
20
+ {
21
+ type: "el",
22
+ tag: "button",
23
+ part: "primary",
24
+ buttonType: input.tag === "div" ? "button" : "submit",
25
+ disabled: input.busy || input.primaryDisabled,
26
+ className: input.primaryClassName,
27
+ onClick: input.tag === "div" ? () => input.onSubmit?.({ preventDefault() {
28
+ } }) : void 0,
29
+ children: [text(input.busy ? input.pendingLabel : input.idleLabel)]
30
+ }
31
+ ];
32
+ if (input.secondary) {
33
+ actions.push({
34
+ type: "el",
35
+ tag: "button",
36
+ part: "secondary",
37
+ buttonType: "button",
38
+ disabled: input.busy || input.secondary.disabled,
39
+ className: input.secondary.className,
40
+ onClick: input.secondary.onClick,
41
+ children: [text(input.secondary.label)]
42
+ });
43
+ }
44
+ children.push({
45
+ type: "el",
46
+ tag: "div",
47
+ part: "actions",
48
+ className: input.actionsClassName,
49
+ state: input.state,
50
+ children: actions
51
+ });
52
+ if (input.confirm) children.push(host("confirm"));
53
+ return {
54
+ type: "el",
55
+ tag: input.tag ?? "form",
56
+ scope: input.scope,
57
+ part: input.part,
58
+ state: input.state,
59
+ className: input.className,
60
+ onSubmit: input.tag === "div" ? void 0 : input.onSubmit,
61
+ children
62
+ };
63
+ }
64
+
65
+ // src/interpret.tsx
66
+ import { partOnlyAttrs, scopeAttrs } from "@aeon-ui/core";
67
+ import { createElement, Fragment } from "react";
68
+ function interpret(widget, hosts) {
69
+ if (widget.type === "text") return widget.text;
70
+ if (widget.type === "host") return hosts[widget.id]?.() ?? null;
71
+ const attrs = widget.scope ? scopeAttrs(widget.scope, widget.part ?? "root", { state: widget.state }) : widget.part ? partOnlyAttrs(widget.part, { state: widget.state }) : widget.state ? { "data-aeon-state": widget.state } : {};
72
+ const props = {
73
+ ...attrs,
74
+ className: widget.className,
75
+ role: widget.role,
76
+ disabled: widget.disabled || void 0,
77
+ type: widget.tag === "button" ? widget.buttonType ?? "button" : void 0,
78
+ onSubmit: widget.tag === "form" && widget.onSubmit ? (event) => widget.onSubmit?.(event) : void 0,
79
+ onClick: widget.onClick
80
+ };
81
+ return createElement(
82
+ widget.tag,
83
+ props,
84
+ ...widget.children.map(
85
+ (child, index) => createElement(Fragment, { key: index }, interpret(child, hosts))
86
+ )
87
+ );
88
+ }
89
+
90
+ // src/totality.ts
91
+ function widgetStates(widget) {
92
+ if (widget.type !== "el") return [];
93
+ const own = widget.state ? [widget.state] : [];
94
+ return [...own, ...widget.children.flatMap(widgetStates)];
95
+ }
96
+ function uncoveredStates(states, project) {
97
+ return states.filter((state) => !widgetStates(project(state)).includes(state));
98
+ }
99
+ export {
100
+ EXCLUSIVE_ACTION_STATES,
101
+ exclusiveActionWidget,
102
+ host,
103
+ interpret,
104
+ text,
105
+ uncoveredStates,
106
+ widgetStates
107
+ };
@@ -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/esbuild@0.28.0/node_modules/esbuild/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/esbuild@0.28.0/node_modules/esbuild/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/esbuild@0.28.0/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/esbuild@0.28.0/node_modules/esbuild/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/esbuild@0.28.0/node_modules/esbuild/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/esbuild@0.28.0/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/../../../../node_modules/.pnpm/esbuild@0.28.0/node_modules/esbuild/bin/esbuild" "$@"
19
+ else
20
+ exec node "$basedir/../../../../node_modules/.pnpm/esbuild@0.28.0/node_modules/esbuild/bin/esbuild" "$@"
21
+ fi
@@ -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/jiti@2.7.0/node_modules/jiti/lib/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/jiti@2.7.0/node_modules/jiti/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/jiti@2.7.0/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/jiti@2.7.0/node_modules/jiti/lib/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/jiti@2.7.0/node_modules/jiti/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/jiti@2.7.0/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/../../../../node_modules/.pnpm/jiti@2.7.0/node_modules/jiti/lib/jiti-cli.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../../../../node_modules/.pnpm/jiti@2.7.0/node_modules/jiti/lib/jiti-cli.mjs" "$@"
21
+ fi
@@ -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/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.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/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -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/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/typescript@5.9.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/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi
@@ -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/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.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/../tsup/dist/cli-default.js" "$@"
19
+ else
20
+ exec node "$basedir/../tsup/dist/cli-default.js" "$@"
21
+ fi
@@ -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/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/dist/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.3/node_modules/tsup/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.22.3_typescript@5.9.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/../tsup/dist/cli-node.js" "$@"
19
+ else
20
+ exec node "$basedir/../tsup/dist/cli-node.js" "$@"
21
+ fi
@@ -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/tsx@4.22.3/node_modules/tsx/dist/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsx@4.22.3/node_modules/tsx/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/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/tsx@4.22.3/node_modules/tsx/dist/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/tsx@4.22.3/node_modules/tsx/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/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/../../../../node_modules/.pnpm/tsx@4.22.3/node_modules/tsx/dist/cli.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../../../../node_modules/.pnpm/tsx@4.22.3/node_modules/tsx/dist/cli.mjs" "$@"
21
+ fi
@@ -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/vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3/node_modules/vite/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3/node_modules/vite/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/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/vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3/node_modules/vite/bin/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3/node_modules/vite/node_modules:/Users/brandoncryderman/AeonUI/node_modules/.pnpm/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/../../../../node_modules/.pnpm/vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3/node_modules/vite/bin/vite.js" "$@"
19
+ else
20
+ exec node "$basedir/../../../../node_modules/.pnpm/vite@8.0.16_@types+node@22.19.19_esbuild@0.28.0_jiti@2.7.0_tsx@4.22.3/node_modules/vite/bin/vite.js" "$@"
21
+ fi
@@ -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'