@timeax/form-palette 0.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 (109) hide show
  1. package/.scaffold-cache.json +537 -0
  2. package/package.json +42 -0
  3. package/src/.scaffold-cache.json +544 -0
  4. package/src/adapters/axios.ts +117 -0
  5. package/src/adapters/index.ts +91 -0
  6. package/src/adapters/inertia.ts +187 -0
  7. package/src/core/adapter-registry.ts +87 -0
  8. package/src/core/bound/bind-host.ts +14 -0
  9. package/src/core/bound/observe-bound-field.ts +172 -0
  10. package/src/core/bound/wait-for-bound-field.ts +57 -0
  11. package/src/core/context.ts +23 -0
  12. package/src/core/core-provider.tsx +818 -0
  13. package/src/core/core-root.tsx +72 -0
  14. package/src/core/core-shell.tsx +44 -0
  15. package/src/core/errors/error-strip.tsx +71 -0
  16. package/src/core/errors/index.ts +2 -0
  17. package/src/core/errors/map-error-bag.ts +51 -0
  18. package/src/core/errors/map-zod.ts +39 -0
  19. package/src/core/hooks/use-button.ts +220 -0
  20. package/src/core/hooks/use-core-context.ts +20 -0
  21. package/src/core/hooks/use-core-utility.ts +0 -0
  22. package/src/core/hooks/use-core.ts +13 -0
  23. package/src/core/hooks/use-field.ts +497 -0
  24. package/src/core/hooks/use-optional-field.ts +28 -0
  25. package/src/core/index.ts +0 -0
  26. package/src/core/registry/binder-registry.ts +82 -0
  27. package/src/core/registry/field-registry.ts +187 -0
  28. package/src/core/test.tsx +17 -0
  29. package/src/global.d.ts +14 -0
  30. package/src/index.ts +68 -0
  31. package/src/input/index.ts +4 -0
  32. package/src/input/input-field.tsx +854 -0
  33. package/src/input/input-layout-graph.ts +230 -0
  34. package/src/input/input-props.ts +190 -0
  35. package/src/lib/get-global-countries.ts +87 -0
  36. package/src/lib/utils.ts +6 -0
  37. package/src/presets/index.ts +0 -0
  38. package/src/presets/shadcn-preset.ts +0 -0
  39. package/src/presets/shadcn-variants/checkbox.tsx +849 -0
  40. package/src/presets/shadcn-variants/chips.tsx +756 -0
  41. package/src/presets/shadcn-variants/color.tsx +284 -0
  42. package/src/presets/shadcn-variants/custom.tsx +227 -0
  43. package/src/presets/shadcn-variants/date.tsx +796 -0
  44. package/src/presets/shadcn-variants/file.tsx +764 -0
  45. package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
  46. package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
  47. package/src/presets/shadcn-variants/number.tsx +176 -0
  48. package/src/presets/shadcn-variants/password.tsx +737 -0
  49. package/src/presets/shadcn-variants/phone.tsx +628 -0
  50. package/src/presets/shadcn-variants/radio.tsx +578 -0
  51. package/src/presets/shadcn-variants/select.tsx +956 -0
  52. package/src/presets/shadcn-variants/slider.tsx +622 -0
  53. package/src/presets/shadcn-variants/text.tsx +343 -0
  54. package/src/presets/shadcn-variants/textarea.tsx +66 -0
  55. package/src/presets/shadcn-variants/toggle.tsx +218 -0
  56. package/src/presets/shadcn-variants/treeselect.tsx +784 -0
  57. package/src/presets/ui/badge.tsx +46 -0
  58. package/src/presets/ui/button.tsx +60 -0
  59. package/src/presets/ui/calendar.tsx +214 -0
  60. package/src/presets/ui/checkbox.tsx +115 -0
  61. package/src/presets/ui/custom.tsx +0 -0
  62. package/src/presets/ui/dialog.tsx +141 -0
  63. package/src/presets/ui/field.tsx +246 -0
  64. package/src/presets/ui/input-mask.tsx +739 -0
  65. package/src/presets/ui/input-otp.tsx +77 -0
  66. package/src/presets/ui/input.tsx +1011 -0
  67. package/src/presets/ui/label.tsx +22 -0
  68. package/src/presets/ui/number.tsx +1370 -0
  69. package/src/presets/ui/popover.tsx +46 -0
  70. package/src/presets/ui/radio-group.tsx +43 -0
  71. package/src/presets/ui/scroll-area.tsx +56 -0
  72. package/src/presets/ui/select.tsx +190 -0
  73. package/src/presets/ui/separator.tsx +28 -0
  74. package/src/presets/ui/slider.tsx +61 -0
  75. package/src/presets/ui/switch.tsx +32 -0
  76. package/src/presets/ui/textarea.tsx +634 -0
  77. package/src/presets/ui/time-dropdowns.tsx +350 -0
  78. package/src/schema/adapter.ts +217 -0
  79. package/src/schema/core.ts +429 -0
  80. package/src/schema/field-map.ts +0 -0
  81. package/src/schema/field.ts +224 -0
  82. package/src/schema/index.ts +0 -0
  83. package/src/schema/input-field.ts +260 -0
  84. package/src/schema/presets.ts +0 -0
  85. package/src/schema/variant.ts +216 -0
  86. package/src/variants/core/checkbox.tsx +54 -0
  87. package/src/variants/core/chips.tsx +22 -0
  88. package/src/variants/core/color.tsx +16 -0
  89. package/src/variants/core/custom.tsx +18 -0
  90. package/src/variants/core/date.tsx +25 -0
  91. package/src/variants/core/file.tsx +9 -0
  92. package/src/variants/core/keyvalue.tsx +12 -0
  93. package/src/variants/core/multiselect.tsx +28 -0
  94. package/src/variants/core/number.tsx +115 -0
  95. package/src/variants/core/password.tsx +35 -0
  96. package/src/variants/core/phone.tsx +16 -0
  97. package/src/variants/core/radio.tsx +38 -0
  98. package/src/variants/core/select.tsx +15 -0
  99. package/src/variants/core/slider.tsx +55 -0
  100. package/src/variants/core/text.tsx +114 -0
  101. package/src/variants/core/textarea.tsx +22 -0
  102. package/src/variants/core/toggle.tsx +50 -0
  103. package/src/variants/core/treeselect.tsx +11 -0
  104. package/src/variants/helpers/selection-summary.tsx +236 -0
  105. package/src/variants/index.ts +75 -0
  106. package/src/variants/registry.ts +38 -0
  107. package/src/variants/select-shared.ts +0 -0
  108. package/src/variants/shared.ts +126 -0
  109. package/tsconfig.json +14 -0
@@ -0,0 +1,91 @@
1
+ // src/adapters/index.ts
2
+
3
+ import axios from "axios";
4
+ import { registerAdapter } from "@/core/adapter-registry";
5
+ import type { AdapterKey } from "@/schema/adapter";
6
+
7
+ import { createAxiosAdapter } from "./axios";
8
+ import { createInertiaAdapter } from "./inertia";
9
+
10
+ // Re-export core adapter types + helpers so hosts can import from a single place.
11
+ export * from "@/schema/adapter";
12
+ export * from "@/core/adapter-registry";
13
+
14
+ // Re-export the concrete factories for hosts that want manual wiring.
15
+ export { createAxiosAdapter, createInertiaAdapter };
16
+
17
+ /**
18
+ * Register the Axios adapter under the "axios" key.
19
+ *
20
+ * This performs a basic runtime check to make sure Axios is present.
21
+ * If Axios isn't available or doesn't look like a proper Axios instance,
22
+ * an error is thrown.
23
+ */
24
+ export function registerAxiosAdapter(): void {
25
+ // Basic sanity check – if this fails, something is wrong with the axios import.
26
+ if (!axios || typeof axios.request !== "function") {
27
+ throw new Error(
28
+ "[form-palette] Axios does not appear to be available. " +
29
+ "Make sure 'axios' is installed and resolvable before calling registerAxiosAdapter()."
30
+ );
31
+ }
32
+
33
+ registerAdapter<"axios">("axios", createAxiosAdapter);
34
+ }
35
+
36
+ /**
37
+ * Register the Inertia adapter under the "inertia" key.
38
+ *
39
+ * This explicitly tests that '@inertiajs/react' can be imported and that
40
+ * it exposes a router with a .visit() method. If not, an error is thrown.
41
+ *
42
+ * Note:
43
+ * - This function is async because it uses dynamic import.
44
+ * - Call it at bootstrap time and await it:
45
+ *
46
+ * await registerInertiaAdapter();
47
+ */
48
+ export async function registerInertiaAdapter(): Promise<void> {
49
+ try {
50
+ const mod: any = await import("@inertiajs/react");
51
+ const router = mod?.router ?? mod?.Inertia;
52
+
53
+ if (!router || typeof router.visit !== "function") {
54
+ throw new Error(
55
+ "[form-palette] '@inertiajs/react' was imported, " +
56
+ "but no router with a .visit() method was found."
57
+ );
58
+ }
59
+ } catch (error) {
60
+ throw new Error(
61
+ "[form-palette] Failed to import '@inertiajs/react'. " +
62
+ "Cannot register the 'inertia' adapter. " +
63
+ "Make sure '@inertiajs/react' is installed and resolvable."
64
+ );
65
+ }
66
+
67
+ registerAdapter<"inertia">("inertia", createInertiaAdapter);
68
+ }
69
+
70
+ /**
71
+ * Optional helper: convenience registration for known adapter keys.
72
+ *
73
+ * This is purely ergonomic; you can also call registerAxiosAdapter /
74
+ * registerInertiaAdapter directly.
75
+ */
76
+ export async function registerKnownAdapter(key: AdapterKey): Promise<void> {
77
+ switch (key) {
78
+ case "axios":
79
+ registerAxiosAdapter();
80
+ return;
81
+ case "inertia":
82
+ await registerInertiaAdapter();
83
+ return;
84
+ default:
85
+ // For now, we only special-case axios/inertia here.
86
+ // Other adapters can be registered by calling registerAdapter() directly.
87
+ throw new Error(
88
+ `[form-palette] registerKnownAdapter: adapter "${key}" is not handled here.`
89
+ );
90
+ }
91
+ }
@@ -0,0 +1,187 @@
1
+ import { VisitOptions, Page } from './../../../../node_modules/@inertiajs/core/types/types.d';
2
+ // src/adapters/inertia.ts
3
+ import type {
4
+ NamedAdapterFactory,
5
+ NamedAdapterConfig,
6
+ AdapterResult,
7
+ AdapterOk,
8
+ AdapterError,
9
+ } from "@/schema/adapter";
10
+
11
+ // (Adapters augmentation is above in the same file)
12
+
13
+ /**
14
+ * Lazy-load the Inertia router from '@inertiajs/react'.
15
+ *
16
+ * This keeps '@inertiajs/react' out of the main bundle until an
17
+ * Inertia adapter is actually used.
18
+ */
19
+ async function loadInertiaRouter() {
20
+ const mod: any = await import("@inertiajs/react");
21
+ const router = mod?.router ?? mod?.Inertia;
22
+
23
+ if (!router || typeof router.visit !== "function") {
24
+ throw new Error(
25
+ "[form-palette] Inertia router not found in @inertiajs/react"
26
+ );
27
+ }
28
+
29
+ return router as {
30
+ visit: (url: string, options?: VisitOptions) => void;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Shape raw Inertia errors into something with `.errors`
36
+ * so Form Palette's autoErr branch can pick them up.
37
+ */
38
+ function normalizeInertiaError(
39
+ raw: unknown
40
+ ): { errors: Record<string, string | string[]> } | unknown {
41
+ if (
42
+ raw &&
43
+ typeof raw === "object" &&
44
+ "errors" in (raw as any) &&
45
+ typeof (raw as any).errors === "object"
46
+ ) {
47
+ // Already in { errors: {...} } shape
48
+ return raw as any;
49
+ }
50
+
51
+ if (
52
+ raw &&
53
+ typeof raw === "object" &&
54
+ !("errors" in (raw as any))
55
+ ) {
56
+ // Inertia usually passes the error bag directly to onError.
57
+ return { errors: raw as Record<string, string | string[]> };
58
+ }
59
+
60
+ return raw;
61
+ }
62
+
63
+ export const createInertiaAdapter: NamedAdapterFactory<"inertia"> = (
64
+ config: NamedAdapterConfig<"inertia">
65
+ ): AdapterResult<AdapterOk<"inertia">> => {
66
+ const { method, url, data, callbacks } = config;
67
+
68
+ const upperMethod = method.toUpperCase() as VisitOptions["method"];
69
+
70
+ /**
71
+ * Build VisitOptions with callbacks wired to AdapterCallbacks
72
+ * + optional Promise resolve/reject.
73
+ */
74
+ function buildOptions(
75
+ resolve?: (value: AdapterOk<"inertia">) => void,
76
+ reject?: (reason: AdapterError<"inertia">) => void,
77
+ extraOptions?: unknown
78
+ ): VisitOptions {
79
+ const merged: VisitOptions = {
80
+ method: upperMethod,
81
+ //@ts-ignore
82
+ data,
83
+ onSuccess: (page: Page) => {
84
+ callbacks?.onSuccess?.(page as AdapterOk<"inertia">);
85
+ resolve?.(page as AdapterOk<"inertia">);
86
+ },
87
+ onError: (rawErrors: any) => {
88
+ const payload = normalizeInertiaError(rawErrors);
89
+ callbacks?.onError?.(payload as AdapterError<"inertia">);
90
+ reject?.(payload as AdapterError<"inertia">);
91
+ },
92
+ onFinish: () => {
93
+ callbacks?.onFinish?.();
94
+ },
95
+ ...(extraOptions as VisitOptions | undefined),
96
+ };
97
+
98
+ return merged;
99
+ }
100
+
101
+ function submit(options?: unknown): void {
102
+ // Fire-and-forget; we still propagate callbacks and finish.
103
+ (async () => {
104
+ let finished = false;
105
+ const finish = () => {
106
+ if (finished) return;
107
+ finished = true;
108
+ callbacks?.onFinish?.();
109
+ };
110
+
111
+ try {
112
+ const router = await loadInertiaRouter();
113
+ const visitOptions = buildOptions(undefined, undefined, options);
114
+ // NOTE: buildOptions already wires onFinish, so we
115
+ // call finish() only if the lazy import itself fails.
116
+ router.visit(url, visitOptions);
117
+ } catch (error) {
118
+ const payload = normalizeInertiaError(error);
119
+ callbacks?.onError?.(payload as AdapterError<"inertia">);
120
+ finish();
121
+ }
122
+ })();
123
+ }
124
+
125
+ function send(options?: unknown): Promise<AdapterOk<"inertia">> {
126
+ return new Promise(async (resolve, reject) => {
127
+ let finished = false;
128
+ const finish = () => {
129
+ if (finished) return;
130
+ finished = true;
131
+ callbacks?.onFinish?.();
132
+ };
133
+
134
+ try {
135
+ const router = await loadInertiaRouter();
136
+ const visitOptions = buildOptions(
137
+ (page) => {
138
+ // buildOptions' onFinish will call onFinish();
139
+ resolve(page);
140
+ },
141
+ (err) => {
142
+ reject(err);
143
+ },
144
+ options
145
+ );
146
+ router.visit(url, visitOptions);
147
+ } catch (error) {
148
+ const payload = normalizeInertiaError(error);
149
+ callbacks?.onError?.(payload as AdapterError<"inertia">);
150
+ finish();
151
+ reject(payload as AdapterError<"inertia">);
152
+ }
153
+ });
154
+ }
155
+
156
+ function run(options?: unknown): Promise<AdapterOk<"inertia">> {
157
+ // Same as send(), so the core can safely `await adapter.run()`
158
+ // if it wants, or ignore the promise if it doesn't care.
159
+ return send(options);
160
+ }
161
+
162
+ return {
163
+ submit,
164
+ send,
165
+ run,
166
+ };
167
+ };
168
+
169
+ declare module "@/schema/adapter" {
170
+ interface Adapters {
171
+ inertia: {
172
+ /**
173
+ * What adapter.send() resolves with for Inertia.
174
+ * This is the Page object passed to onSuccess.
175
+ */
176
+ ok: Page<any>;
177
+
178
+ /**
179
+ * What callbacks.onError receives for Inertia.
180
+ *
181
+ * We shape this as `{ errors: ErrorBag }` so Form Palette's
182
+ * autoErr branch can see `.errors`.
183
+ */
184
+ err: { errors: Record<string, string | string[]> } | unknown;
185
+ };
186
+ }
187
+ }
@@ -0,0 +1,87 @@
1
+ // src/core/adapter-registry.ts
2
+
3
+ import { AdapterKey, AdapterOk, NamedAdapterFactory } from "@/schema/adapter";
4
+
5
+ /**
6
+ * Internal registry of adapter factories.
7
+ *
8
+ * We keep it simple: a plain JS object keyed by AdapterKey.
9
+ */
10
+ const registry: Partial<
11
+ Record<AdapterKey, NamedAdapterFactory<AdapterKey, any>>
12
+ > = {};
13
+
14
+ /**
15
+ * Built-in 'local' adapter.
16
+ *
17
+ * Semantics:
18
+ * - send(options?) resolves to `{ data: Body }`
19
+ * - submit/run do nothing by default (no side effects)
20
+ *
21
+ * The core will typically call onSubmitted with the result of send().
22
+ */
23
+ export const localAdapter: NamedAdapterFactory<"local", any> = (config) => {
24
+ return {
25
+ submit() {
26
+ // no-op; core is responsible for calling onSubmitted
27
+ // using send() if it chooses to.
28
+ },
29
+ async send() {
30
+ const result: AdapterOk<"local"> = { data: config.data };
31
+
32
+ if (config.callbacks?.onSuccess) {
33
+ config.callbacks.onSuccess(result);
34
+ }
35
+
36
+ if (config.callbacks?.onFinish) {
37
+ config.callbacks.onFinish();
38
+ }
39
+
40
+ return result;
41
+ },
42
+ run() {
43
+ // By default, run behaves like submit (no-op),
44
+ // but hosts can choose to always call send() instead.
45
+ this.submit();
46
+ },
47
+ };
48
+ };
49
+
50
+ /**
51
+ * Initialise registry with the built-in 'local' adapter.
52
+ */
53
+ registry.local = localAdapter as NamedAdapterFactory<AdapterKey, any>;
54
+
55
+ /**
56
+ * Register or override an adapter factory for a given key.
57
+ *
58
+ * Hosts can call this at bootstrap time, e.g.:
59
+ *
60
+ * registerAdapter<'axios'>('axios', axiosAdapter);
61
+ */
62
+ export function registerAdapter<K extends AdapterKey, Body = any>(
63
+ key: K,
64
+ factory: NamedAdapterFactory<K, Body>
65
+ ): void {
66
+ registry[key] = factory as NamedAdapterFactory<AdapterKey, any>;
67
+ }
68
+
69
+ /**
70
+ * Lookup an adapter factory by key.
71
+ *
72
+ * If no adapter is found for the given key, this returns undefined.
73
+ */
74
+ export function getAdapter<K extends AdapterKey>(
75
+ key: K
76
+ ): NamedAdapterFactory<K, any> | undefined {
77
+ const factory = registry[key];
78
+
79
+ return factory as NamedAdapterFactory<K, any> | undefined;
80
+ }
81
+
82
+ /**
83
+ * Check whether an adapter is registered for the given key.
84
+ */
85
+ export function hasAdapter(key: AdapterKey): boolean {
86
+ return typeof registry[key] === "function";
87
+ }
@@ -0,0 +1,14 @@
1
+ // src/core/bound/bind-host.ts (or inline in binder-registry.ts)
2
+ import type { Dict, CoreContext } from "@/schema/core";
3
+ import type { Field } from "@/schema/field";
4
+
5
+ /**
6
+ * Minimal surface needed for bound helpers.
7
+ *
8
+ * CoreContext already satisfies this, and FieldRegistry can be made to
9
+ * satisfy it as well (via getBind).
10
+ */
11
+ export interface BindHost<V extends Dict = Dict> {
12
+ getBind(id: string): Field | undefined;
13
+ controlButton?(): void;
14
+ }
@@ -0,0 +1,172 @@
1
+ // src/core/bound/observe-bound-field.ts
2
+
3
+ import type { Dict } from "@/schema/core";
4
+ import type { Field } from "@/schema/field";
5
+ import type { BindHost } from "@/core/bound/bind-host";
6
+
7
+ /** Get the live bound field (if mounted and present). */
8
+ export function getBoundField<V extends Dict>(
9
+ host: BindHost<V>,
10
+ bindId: string
11
+ ): Field | undefined {
12
+ return host.getBind(bindId);
13
+ }
14
+
15
+ export function hasBoundField<V extends Dict>(
16
+ host: BindHost<V>,
17
+ bindId: string
18
+ ): boolean {
19
+ return !!getBoundField(host, bindId);
20
+ }
21
+
22
+ export function readBoundValue<T = unknown, V extends Dict = Dict>(
23
+ host: BindHost<V>,
24
+ bindId: string
25
+ ): T | undefined {
26
+ return getBoundField(host, bindId)?.value as T | undefined;
27
+ }
28
+
29
+ export function setBoundValue<T = unknown, V extends Dict = Dict>(
30
+ host: BindHost<V>,
31
+ bindId: string,
32
+ value: T,
33
+ variant: string = "util"
34
+ ): boolean {
35
+ const f = getBoundField(host, bindId);
36
+ if (!f) return false;
37
+
38
+ (f as any).value = value as unknown;
39
+
40
+ // optional: dirty/enable logic if host supports it
41
+ try {
42
+ host.controlButton?.();
43
+ } catch {
44
+ // ignore
45
+ }
46
+
47
+ (f as any).onChange?.(value, undefined, variant);
48
+ return true;
49
+ }
50
+
51
+ export function setBoundError<V extends Dict>(
52
+ _host: BindHost<V>, // host not strictly needed here
53
+ bindId: string,
54
+ msg: string
55
+ ): boolean {
56
+ const f = _host.getBind(bindId);
57
+ if (!f) return false;
58
+ (f as any).error = msg ?? "";
59
+ return true;
60
+ }
61
+
62
+ export function validateBoundField<V extends Dict>(
63
+ host: BindHost<V>,
64
+ bindId: string,
65
+ report = true
66
+ ): boolean {
67
+ const f = getBoundField(host, bindId);
68
+ if (!f) return false;
69
+ return !!(f as any).validate?.(report);
70
+ }
71
+
72
+ /**
73
+ * Observe a bound field for value/error + liveness.
74
+ */
75
+ export function observeBoundField<T = unknown, V extends Dict = Dict>(
76
+ host: BindHost<V>,
77
+ bindId: string,
78
+ handler: (evt: {
79
+ exists: boolean;
80
+ field?: Field;
81
+ value?: T;
82
+ error?: string;
83
+ }) => void,
84
+ pollMs = 300
85
+ ): () => void {
86
+ let current: Field | undefined = getBoundField(host, bindId);
87
+ let restoreOnChange: Field["onChange"] | undefined;
88
+
89
+ const fire = () => {
90
+ if (!current) {
91
+ handler({ exists: false });
92
+ return;
93
+ }
94
+
95
+ handler({
96
+ exists: true,
97
+ field: current,
98
+ value: (current as any).value as T,
99
+ error: (current as any).error,
100
+ });
101
+ };
102
+
103
+ const wire = () => {
104
+ const f = getBoundField(host, bindId);
105
+
106
+ if (f === current) return;
107
+
108
+ if (current && restoreOnChange) {
109
+ (current as any).onChange = restoreOnChange;
110
+ restoreOnChange = undefined;
111
+ }
112
+
113
+ current = f;
114
+
115
+ if (current) {
116
+ restoreOnChange = (current as any).onChange;
117
+ (current as any).onChange = (
118
+ next: unknown,
119
+ prev: unknown,
120
+ variant: string
121
+ ) => {
122
+ restoreOnChange?.(next, prev, variant);
123
+ handler({
124
+ exists: true,
125
+ field: current,
126
+ value: next as T,
127
+ error: (current as any).error,
128
+ });
129
+ };
130
+ }
131
+
132
+ fire();
133
+ };
134
+
135
+ // initial
136
+ wire();
137
+
138
+ let intervalId: number | undefined;
139
+ if (typeof window !== "undefined") {
140
+ intervalId = window.setInterval(wire, pollMs);
141
+ }
142
+
143
+ let mo: MutationObserver | undefined;
144
+ if (
145
+ typeof MutationObserver !== "undefined" &&
146
+ typeof document !== "undefined"
147
+ ) {
148
+ try {
149
+ mo = new MutationObserver(wire);
150
+ mo.observe(document.body, {
151
+ childList: true,
152
+ subtree: true,
153
+ });
154
+ } catch {
155
+ // ignore
156
+ }
157
+ }
158
+
159
+ return () => {
160
+ if (typeof window !== "undefined" && typeof intervalId === "number") {
161
+ window.clearInterval(intervalId);
162
+ }
163
+ if (mo) {
164
+ mo.disconnect();
165
+ mo = undefined;
166
+ }
167
+ if (current && restoreOnChange) {
168
+ (current as any).onChange = restoreOnChange;
169
+ restoreOnChange = undefined;
170
+ }
171
+ };
172
+ }
@@ -0,0 +1,57 @@
1
+ // src/core/bound/wait-for-bound-field.ts
2
+
3
+ import type { Dict } from "@/schema/core";
4
+ import type { Field } from "@/schema/field";
5
+ import type { BindHost } from "@/core/bound/bind-host";
6
+ import {
7
+ getBoundField,
8
+ observeBoundField,
9
+ } from "@/core/bound/observe-bound-field";
10
+
11
+ export function waitForBoundField<V extends Dict>(
12
+ host: BindHost<V>,
13
+ bindId: string,
14
+ timeoutMs = 5000
15
+ ): Promise<Field> {
16
+ const existing = getBoundField(host, bindId);
17
+ if (existing) return Promise.resolve(existing);
18
+
19
+ return new Promise<Field>((resolve, reject) => {
20
+ let settled = false;
21
+
22
+ const settleResolve = (field: Field) => {
23
+ if (settled) return;
24
+ settled = true;
25
+ stop();
26
+ clearTimeout(to);
27
+ resolve(field);
28
+ };
29
+
30
+ const settleReject = (error: Error) => {
31
+ if (settled) return;
32
+ settled = true;
33
+ stop();
34
+ clearTimeout(to);
35
+ reject(error);
36
+ };
37
+
38
+ const stop = observeBoundField(
39
+ host,
40
+ bindId,
41
+ (e) => {
42
+ if (e.exists && e.field) {
43
+ settleResolve(e.field);
44
+ }
45
+ },
46
+ 150
47
+ );
48
+
49
+ const to = setTimeout(() => {
50
+ settleReject(
51
+ new Error(
52
+ `waitForBoundField('${bindId}') timed out after ${timeoutMs}ms`
53
+ )
54
+ );
55
+ }, timeoutMs);
56
+ });
57
+ }
@@ -0,0 +1,23 @@
1
+ // src/core/context.ts
2
+ import React from "react";
3
+ import type { CoreContext, Dict } from "@/schema/core";
4
+
5
+ /**
6
+ * Non-generic alias for the core context type used at runtime.
7
+ *
8
+ * We store CoreContext<Dict> in React context and let
9
+ * caller-side hooks (useCore, useCoreContext, etc.) cast
10
+ * to a more specific generic shape when needed.
11
+ */
12
+ export type AnyCoreContext = CoreContext<Dict>;
13
+
14
+ /**
15
+ * React context carrying the current form/core instance.
16
+ *
17
+ * - Provider is set up in core-provider.tsx.
18
+ * - Consumers should generally use the typed hook in
19
+ * hooks/use-core-context.ts instead of reading this directly.
20
+ */
21
+ export const CoreContextReact = React.createContext<AnyCoreContext | null>(
22
+ null
23
+ );