kayforms 0.1.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +337 -0
  3. package/examples/react-demo/README.md +337 -0
  4. package/examples/react-demo/eslint.config.js +22 -0
  5. package/examples/react-demo/index.html +13 -0
  6. package/examples/react-demo/package.json +33 -0
  7. package/examples/react-demo/public/apple-touch-icon.png +0 -0
  8. package/examples/react-demo/public/favicon-96x96.png +0 -0
  9. package/examples/react-demo/public/favicon.ico +0 -0
  10. package/examples/react-demo/public/favicon.svg +17 -0
  11. package/examples/react-demo/public/icons.svg +24 -0
  12. package/examples/react-demo/public/site.webmanifest +21 -0
  13. package/examples/react-demo/public/web-app-manifest-192x192.png +0 -0
  14. package/examples/react-demo/public/web-app-manifest-512x512.png +0 -0
  15. package/examples/react-demo/src/App.css +184 -0
  16. package/examples/react-demo/src/App.tsx +825 -0
  17. package/examples/react-demo/src/assets/hero.png +0 -0
  18. package/examples/react-demo/src/assets/react.svg +1 -0
  19. package/examples/react-demo/src/assets/vite.svg +1 -0
  20. package/examples/react-demo/src/index.css +627 -0
  21. package/examples/react-demo/src/main.tsx +10 -0
  22. package/examples/react-demo/tsconfig.app.json +25 -0
  23. package/examples/react-demo/tsconfig.json +7 -0
  24. package/examples/react-demo/tsconfig.node.json +24 -0
  25. package/examples/react-demo/vite.config.ts +7 -0
  26. package/kayforms.jpg +0 -0
  27. package/package.json +26 -0
  28. package/packages/angular/package.json +43 -0
  29. package/packages/angular/src/index.ts +198 -0
  30. package/packages/angular/tsconfig.json +8 -0
  31. package/packages/angular/tsup.config.ts +17 -0
  32. package/packages/core/README.md +337 -0
  33. package/packages/core/package.json +37 -0
  34. package/packages/core/src/batch.ts +106 -0
  35. package/packages/core/src/devtools.ts +329 -0
  36. package/packages/core/src/field.ts +167 -0
  37. package/packages/core/src/form.ts +448 -0
  38. package/packages/core/src/index.ts +71 -0
  39. package/packages/core/src/registry.ts +126 -0
  40. package/packages/core/src/signal.ts +399 -0
  41. package/packages/core/src/time-travel.ts +275 -0
  42. package/packages/core/src/validation.ts +243 -0
  43. package/packages/core/tsconfig.json +8 -0
  44. package/packages/core/tsup.config.ts +16 -0
  45. package/packages/devtools/extension/background.js +35 -0
  46. package/packages/devtools/extension/content-script.js +10 -0
  47. package/packages/devtools/extension/devtools.html +9 -0
  48. package/packages/devtools/extension/devtools.js +8 -0
  49. package/packages/devtools/extension/manifest.json +19 -0
  50. package/packages/devtools/extension/panel.css +505 -0
  51. package/packages/devtools/extension/panel.html +108 -0
  52. package/packages/devtools/extension/panel.js +354 -0
  53. package/packages/devtools/package.json +38 -0
  54. package/packages/devtools/src/index.ts +95 -0
  55. package/packages/devtools/src/panel.ts +226 -0
  56. package/packages/devtools/src/styles.ts +422 -0
  57. package/packages/devtools/src/timeline.ts +283 -0
  58. package/packages/devtools/tsconfig.json +8 -0
  59. package/packages/devtools/tsup.config.ts +17 -0
  60. package/packages/react/package.json +46 -0
  61. package/packages/react/src/index.ts +279 -0
  62. package/packages/react/tsconfig.json +8 -0
  63. package/packages/react/tsup.config.ts +17 -0
  64. package/packages/solid/package.json +42 -0
  65. package/packages/solid/src/index.ts +206 -0
  66. package/packages/solid/tsconfig.json +8 -0
  67. package/packages/solid/tsup.config.ts +17 -0
  68. package/packages/svelte/package.json +42 -0
  69. package/packages/svelte/src/index.ts +199 -0
  70. package/packages/svelte/tsconfig.json +8 -0
  71. package/packages/svelte/tsup.config.ts +17 -0
  72. package/packages/vanilla/package.json +38 -0
  73. package/packages/vanilla/src/index.ts +254 -0
  74. package/packages/vanilla/tsconfig.json +8 -0
  75. package/packages/vanilla/tsup.config.ts +17 -0
  76. package/packages/vue/package.json +42 -0
  77. package/packages/vue/src/index.ts +217 -0
  78. package/packages/vue/tsconfig.json +8 -0
  79. package/packages/vue/tsup.config.ts +17 -0
  80. package/pnpm-workspace.yaml +3 -0
  81. package/tsconfig.base.json +21 -0
@@ -0,0 +1,283 @@
1
+ // ============================================================================
2
+ // @kayforms/devtools — Timeline Component
3
+ // ============================================================================
4
+ // Renders the action log timeline with scrubber controls.
5
+ // Pure DOM manipulation — no framework dependencies.
6
+ // ============================================================================
7
+
8
+ import { createEffect, type DevToolsBridge, type HistoryEntry } from "@kayforms/core";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Timeline Renderer
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function createTimeline(
15
+ container: HTMLElement,
16
+ bridge: DevToolsBridge
17
+ ): () => void {
18
+ const cleanups: (() => void)[] = [];
19
+
20
+ // --- Timeline entries list ---
21
+ const listEl = document.createElement("div");
22
+ listEl.className = "kf-timeline";
23
+ container.appendChild(listEl);
24
+
25
+ // --- Scrubber bar ---
26
+ const scrubberEl = document.createElement("div");
27
+ scrubberEl.className = "kf-scrubber";
28
+
29
+ const controlsEl = document.createElement("div");
30
+ controlsEl.className = "kf-scrubber-controls";
31
+
32
+ const undoBtn = document.createElement("button");
33
+ undoBtn.textContent = "⏪";
34
+ undoBtn.title = "Undo";
35
+ undoBtn.onclick = () => bridge.undo();
36
+
37
+ const redoBtn = document.createElement("button");
38
+ redoBtn.textContent = "⏩";
39
+ redoBtn.title = "Redo";
40
+ redoBtn.onclick = () => bridge.redo();
41
+
42
+ const resumeBtn = document.createElement("button");
43
+ resumeBtn.textContent = "▶";
44
+ resumeBtn.title = "Resume (live)";
45
+ resumeBtn.onclick = () => bridge.resume();
46
+
47
+ const clearBtn = document.createElement("button");
48
+ clearBtn.textContent = "🗑";
49
+ clearBtn.title = "Clear history";
50
+ clearBtn.onclick = () => bridge.clear();
51
+
52
+ controlsEl.append(undoBtn, redoBtn, resumeBtn, clearBtn);
53
+
54
+ const slider = document.createElement("input");
55
+ slider.type = "range";
56
+ slider.min = "0";
57
+ slider.max = "0";
58
+ slider.value = "0";
59
+ slider.addEventListener("input", () => {
60
+ const index = parseInt(slider.value, 10);
61
+ bridge.jumpTo(index);
62
+ });
63
+
64
+ const countEl = document.createElement("span");
65
+ countEl.className = "kf-scrubber-count";
66
+ countEl.textContent = "0";
67
+
68
+ scrubberEl.append(controlsEl, slider, countEl);
69
+ container.appendChild(scrubberEl);
70
+
71
+ // --- Render entries ---
72
+ function renderEntry(entry: HistoryEntry, index: number, isCurrent: boolean): HTMLElement {
73
+ const el = document.createElement("div");
74
+ el.className = `kf-timeline-entry${isCurrent ? " kf-current" : ""}`;
75
+ el.onclick = () => bridge.jumpTo(index);
76
+
77
+ // Color dot
78
+ const dot = document.createElement("div");
79
+ dot.className = `kf-entry-dot kf-action-${entry.action}`;
80
+ el.appendChild(dot);
81
+
82
+ // Content
83
+ const content = document.createElement("div");
84
+ content.className = "kf-entry-content";
85
+
86
+ // Action + path
87
+ const actionLine = document.createElement("div");
88
+ const actionSpan = document.createElement("span");
89
+ actionSpan.className = "kf-entry-action";
90
+ actionSpan.textContent = entry.action;
91
+ actionLine.appendChild(actionSpan);
92
+
93
+ if (entry.path) {
94
+ const pathSpan = document.createElement("span");
95
+ pathSpan.className = "kf-entry-path";
96
+ pathSpan.textContent = entry.path;
97
+ actionLine.appendChild(pathSpan);
98
+ }
99
+ content.appendChild(actionLine);
100
+
101
+ // Value change
102
+ if (entry.prevValue !== undefined || entry.nextValue !== undefined) {
103
+ const valuesEl = document.createElement("div");
104
+ valuesEl.className = "kf-entry-values";
105
+
106
+ if (entry.prevValue !== undefined && entry.action !== "INIT") {
107
+ const prevEl = document.createElement("span");
108
+ prevEl.className = "kf-entry-prev";
109
+ prevEl.textContent = truncate(JSON.stringify(entry.prevValue), 20);
110
+ valuesEl.appendChild(prevEl);
111
+
112
+ const arrow = document.createElement("span");
113
+ arrow.textContent = "→";
114
+ arrow.style.color = "#8899aa";
115
+ valuesEl.appendChild(arrow);
116
+ }
117
+
118
+ const nextEl = document.createElement("span");
119
+ nextEl.className = "kf-entry-next";
120
+ nextEl.textContent = truncate(JSON.stringify(entry.nextValue), 30);
121
+ valuesEl.appendChild(nextEl);
122
+
123
+ content.appendChild(valuesEl);
124
+ }
125
+
126
+ // Timestamp
127
+ const timeEl = document.createElement("div");
128
+ timeEl.className = "kf-entry-time";
129
+ const date = new Date(entry.timestamp);
130
+ timeEl.textContent = `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}.${date.getMilliseconds().toString().padStart(3, "0")}`;
131
+ content.appendChild(timeEl);
132
+
133
+ el.appendChild(content);
134
+ return el;
135
+ }
136
+
137
+ // --- Reactive updates ---
138
+ const disposeEffect = createEffect(() => {
139
+ const entries = bridge.history.value;
140
+ const cursorVal = bridge.cursor.value;
141
+ const currentIndex = cursorVal === -1 ? entries.length - 1 : cursorVal;
142
+
143
+ // Clear and re-render (simple approach — could be optimized with diffing)
144
+ listEl.innerHTML = "";
145
+
146
+ // Render in reverse (newest first)
147
+ for (let i = entries.length - 1; i >= 0; i--) {
148
+ const entryEl = renderEntry(entries[i], i, i === currentIndex);
149
+ listEl.appendChild(entryEl);
150
+ }
151
+
152
+ // Update scrubber
153
+ slider.max = String(Math.max(0, entries.length - 1));
154
+ slider.value = String(currentIndex);
155
+ countEl.textContent = `${currentIndex + 1}/${entries.length}`;
156
+
157
+ // Auto-scroll to current entry
158
+ if (cursorVal === -1 && listEl.firstChild) {
159
+ (listEl.firstChild as HTMLElement).scrollIntoView({ behavior: "smooth", block: "nearest" });
160
+ }
161
+ });
162
+ cleanups.push(disposeEffect);
163
+
164
+ return () => {
165
+ for (const cleanup of cleanups) {
166
+ cleanup();
167
+ }
168
+ listEl.remove();
169
+ scrubberEl.remove();
170
+ };
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // State Inspector
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export function createInspector(
178
+ container: HTMLElement,
179
+ bridge: DevToolsBridge
180
+ ): () => void {
181
+ const inspectorEl = document.createElement("div");
182
+ inspectorEl.className = "kf-inspector";
183
+ container.appendChild(inspectorEl);
184
+
185
+ function renderTree(obj: unknown, depth = 0): HTMLElement {
186
+ const fragment = document.createElement("div");
187
+
188
+ if (obj === null || obj === undefined) {
189
+ const valueEl = document.createElement("span");
190
+ valueEl.className = "kf-tree-value";
191
+ valueEl.textContent = String(obj);
192
+ fragment.appendChild(valueEl);
193
+ return fragment;
194
+ }
195
+
196
+ if (typeof obj !== "object") {
197
+ const valueEl = document.createElement("span");
198
+ valueEl.className = "kf-tree-value";
199
+ valueEl.textContent = JSON.stringify(obj);
200
+ fragment.appendChild(valueEl);
201
+ return fragment;
202
+ }
203
+
204
+ const entries = Object.entries(obj as Record<string, unknown>);
205
+ for (const [key, value] of entries) {
206
+ const nodeEl = document.createElement("div");
207
+ nodeEl.className = "kf-tree-node";
208
+
209
+ const keyEl = document.createElement("span");
210
+ keyEl.className = "kf-tree-key";
211
+ keyEl.textContent = key;
212
+ nodeEl.appendChild(keyEl);
213
+
214
+ const colonEl = document.createElement("span");
215
+ colonEl.className = "kf-tree-bracket";
216
+ colonEl.textContent = ": ";
217
+ nodeEl.appendChild(colonEl);
218
+
219
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
220
+ const bracketOpen = document.createElement("span");
221
+ bracketOpen.className = "kf-tree-bracket";
222
+ bracketOpen.textContent = "{";
223
+ nodeEl.appendChild(bracketOpen);
224
+
225
+ if (depth < 3) {
226
+ nodeEl.appendChild(renderTree(value, depth + 1));
227
+ } else {
228
+ const ellipsis = document.createElement("span");
229
+ ellipsis.className = "kf-tree-value";
230
+ ellipsis.textContent = "...";
231
+ nodeEl.appendChild(ellipsis);
232
+ }
233
+
234
+ const bracketClose = document.createElement("span");
235
+ bracketClose.className = "kf-tree-bracket";
236
+ bracketClose.textContent = "}";
237
+ nodeEl.appendChild(bracketClose);
238
+ } else {
239
+ const valueEl = document.createElement("span");
240
+ valueEl.className = "kf-tree-value";
241
+ valueEl.textContent = JSON.stringify(value);
242
+ nodeEl.appendChild(valueEl);
243
+ }
244
+
245
+ fragment.appendChild(nodeEl);
246
+ }
247
+
248
+ return fragment;
249
+ }
250
+
251
+ const disposeEffect = createEffect(() => {
252
+ const entries = bridge.history.value;
253
+ const cursorVal = bridge.cursor.value;
254
+ const currentIndex = cursorVal === -1 ? entries.length - 1 : cursorVal;
255
+
256
+ inspectorEl.innerHTML = "";
257
+
258
+ const snapshot = bridge.getSnapshotAt(currentIndex);
259
+ if (snapshot) {
260
+ inspectorEl.appendChild(renderTree(snapshot));
261
+ } else if (entries.length === 0) {
262
+ const emptyEl = document.createElement("div");
263
+ emptyEl.style.cssText =
264
+ "padding: 20px; text-align: center; color: var(--kf-text-muted);";
265
+ emptyEl.textContent = "No form data recorded yet";
266
+ inspectorEl.appendChild(emptyEl);
267
+ }
268
+ });
269
+
270
+ return () => {
271
+ disposeEffect();
272
+ inspectorEl.remove();
273
+ };
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Helpers
278
+ // ---------------------------------------------------------------------------
279
+
280
+ function truncate(str: string, max: number): string {
281
+ if (str.length <= max) return str;
282
+ return str.slice(0, max - 1) + "…";
283
+ }
@@ -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,17 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ minify: false,
10
+ treeshake: true,
11
+ external: ["@kayforms/core"],
12
+ outExtension({ format }) {
13
+ return {
14
+ js: format === "cjs" ? ".cjs" : ".js",
15
+ };
16
+ },
17
+ });
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@kayforms/react",
3
+ "version": "0.1.0",
4
+ "description": "React adapter for Kayforms reactive form library",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "clean": "rimraf dist",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@kayforms/core": "workspace:*"
30
+ },
31
+ "peerDependencies": {
32
+ "react": ">=18.0.0",
33
+ "react-dom": ">=18.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "^18.0.0",
37
+ "@types/react-dom": "^18.0.0",
38
+ "react": "^18.0.0",
39
+ "react-dom": "^18.0.0",
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.5.0",
42
+ "rimraf": "^5.0.0"
43
+ },
44
+ "sideEffects": false,
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,279 @@
1
+ // ============================================================================
2
+ // @kayforms/react — React Adapter
3
+ // ============================================================================
4
+ // Bridges Kayforms signals to React re-renders using useSyncExternalStore.
5
+ // Each useField() subscribes only to its own FieldNode — parent components
6
+ // never re-render on individual field changes.
7
+ // ============================================================================
8
+
9
+ import {
10
+ useSyncExternalStore,
11
+ useCallback,
12
+ useRef,
13
+ useEffect,
14
+ createContext,
15
+ useContext,
16
+ createElement,
17
+ type ReactNode,
18
+ } from "react";
19
+ import {
20
+ createForm,
21
+ type FormConfig,
22
+ type FormStore,
23
+ type FormErrors,
24
+ type FieldNode,
25
+ type Signal,
26
+ type Computed,
27
+ } from "@kayforms/core";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Signal → React bridge
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Subscribe to a Kayforms signal inside a React component.
35
+ * Uses useSyncExternalStore for tear-free concurrent rendering.
36
+ */
37
+ export function useSignalValue<T>(signal: Signal<T> | Computed<T>): T {
38
+ const subscribe = useCallback(
39
+ (onStoreChange: () => void) => {
40
+ return signal.subscribe(onStoreChange);
41
+ },
42
+ [signal]
43
+ );
44
+
45
+ const getSnapshot = useCallback(() => signal.value, [signal]);
46
+
47
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // FormContext
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const FormContext = createContext<FormStore | null>(null);
55
+
56
+ function useFormContext(): FormStore {
57
+ const ctx = useContext(FormContext);
58
+ if (!ctx) {
59
+ throw new Error(
60
+ "[kayforms/react] useField must be used inside a <FormProvider>. " +
61
+ "Wrap your form with <FormProvider form={...}>."
62
+ );
63
+ }
64
+ return ctx;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // FormProvider
69
+ // ---------------------------------------------------------------------------
70
+
71
+ export interface FormProviderProps {
72
+ form: FormStore;
73
+ children: ReactNode;
74
+ }
75
+
76
+ /**
77
+ * Provides form context to nested useField() hooks.
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * const form = useForm({ initialValues: { name: '' } });
82
+ * return (
83
+ * <FormProvider form={form.store}>
84
+ * <NameField />
85
+ * </FormProvider>
86
+ * );
87
+ * ```
88
+ */
89
+ export function FormProvider({ form, children }: FormProviderProps) {
90
+ return createElement(FormContext.Provider, { value: form }, children);
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // useForm
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export interface UseFormReturn<T extends Record<string, unknown>> {
98
+ /** The reactive form store */
99
+ store: FormStore<T>;
100
+ /** Current values (reactive) */
101
+ values: T;
102
+ /** Current errors (reactive) */
103
+ errors: FormErrors<T>;
104
+ /** Whether the form is dirty */
105
+ dirty: boolean;
106
+ /** Whether the form is valid */
107
+ valid: boolean;
108
+ /** Whether the form is submitting */
109
+ submitting: boolean;
110
+ /** Handle form submission (pass to <form onSubmit>) */
111
+ handleSubmit: (e?: { preventDefault?: () => void }) => void;
112
+ /** Reset the form */
113
+ reset: (values?: Partial<T>) => void;
114
+ /** Get a field node by path */
115
+ getField: <V = unknown>(path: string) => FieldNode<V>;
116
+ }
117
+
118
+ /**
119
+ * Create and manage a Kayforms form in a React component.
120
+ *
121
+ * @example
122
+ * ```tsx
123
+ * function LoginForm() {
124
+ * const { handleSubmit, getField, valid, submitting } = useForm({
125
+ * initialValues: { email: '', password: '' },
126
+ * fieldValidators: {
127
+ * email: [validators.required(), validators.email()],
128
+ * password: [validators.required(), validators.minLength(8)],
129
+ * },
130
+ * onSubmit: async (values) => {
131
+ * await login(values);
132
+ * },
133
+ * });
134
+ *
135
+ * return (
136
+ * <form onSubmit={handleSubmit}>
137
+ * <Field field={getField('email')} />
138
+ * <Field field={getField('password')} type="password" />
139
+ * <button disabled={!valid || submitting}>Login</button>
140
+ * </form>
141
+ * );
142
+ * }
143
+ * ```
144
+ */
145
+ export function useForm<T extends Record<string, unknown>>(
146
+ config: FormConfig<T>
147
+ ): UseFormReturn<T> {
148
+ // Create form store once (stable reference)
149
+ const storeRef = useRef<FormStore<T> | null>(null);
150
+ if (!storeRef.current) {
151
+ storeRef.current = createForm(config);
152
+ }
153
+ const store = storeRef.current;
154
+
155
+ // Cleanup on unmount
156
+ useEffect(() => {
157
+ return () => {
158
+ store.dispose();
159
+ };
160
+ }, [store]);
161
+
162
+ // Subscribe to reactive values
163
+ const values = useSignalValue(store.values);
164
+ const errors = useSignalValue(store.errors);
165
+ const dirty = useSignalValue(store.dirty);
166
+ const valid = useSignalValue(store.valid);
167
+ const submitting = useSignalValue(store.submitting);
168
+
169
+ const handleSubmit = useCallback(
170
+ (e?: { preventDefault?: () => void }) => {
171
+ e?.preventDefault?.();
172
+ store.submit();
173
+ },
174
+ [store]
175
+ );
176
+
177
+ const reset = useCallback(
178
+ (newValues?: Partial<T>) => {
179
+ store.reset(newValues);
180
+ },
181
+ [store]
182
+ );
183
+
184
+ return {
185
+ store,
186
+ values,
187
+ errors,
188
+ dirty,
189
+ valid,
190
+ submitting,
191
+ handleSubmit,
192
+ reset,
193
+ getField: store.getField,
194
+ };
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // useField
199
+ // ---------------------------------------------------------------------------
200
+
201
+ export interface UseFieldReturn<V = unknown> {
202
+ /** Current field value */
203
+ value: V;
204
+ /** Current error message (undefined = valid) */
205
+ error: string | undefined;
206
+ /** Whether the field has been touched */
207
+ touched: boolean;
208
+ /** Whether the field value differs from initial */
209
+ dirty: boolean;
210
+ /** Whether async validation is in progress */
211
+ validating: boolean;
212
+ /** Update the field value */
213
+ onChange: (value: V) => void;
214
+ /** Mark as touched (call on blur) */
215
+ onBlur: () => void;
216
+ /** Props to spread on an input element */
217
+ inputProps: {
218
+ value: V;
219
+ onChange: (e: { target: { value: unknown } }) => void;
220
+ onBlur: () => void;
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Subscribe to a single field's reactive state.
226
+ * Only re-renders when THIS field changes — not other fields.
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * function EmailField() {
231
+ * const { inputProps, error, touched } = useField<string>('email');
232
+ * return (
233
+ * <div>
234
+ * <input {...inputProps} type="email" />
235
+ * {touched && error && <span className="error">{error}</span>}
236
+ * </div>
237
+ * );
238
+ * }
239
+ * ```
240
+ */
241
+ export function useField<V = unknown>(path: string): UseFieldReturn<V> {
242
+ const store = useFormContext();
243
+ const field = store.getField<V>(path);
244
+
245
+ const value = useSignalValue(field.value);
246
+ const error = useSignalValue(field.error);
247
+ const touched = useSignalValue(field.touched);
248
+ const dirty = useSignalValue(field.dirty);
249
+ const validating = useSignalValue(field.validating);
250
+
251
+ const onChange = useCallback(
252
+ (v: V) => field.onChange(v),
253
+ [field]
254
+ );
255
+
256
+ const onBlur = useCallback(() => field.onBlur(), [field]);
257
+
258
+ const handleInputChange = useCallback(
259
+ (e: { target: { value: unknown } }) => {
260
+ field.onChange(e.target.value as V);
261
+ },
262
+ [field]
263
+ );
264
+
265
+ return {
266
+ value,
267
+ error,
268
+ touched,
269
+ dirty,
270
+ validating,
271
+ onChange,
272
+ onBlur,
273
+ inputProps: {
274
+ value,
275
+ onChange: handleInputChange,
276
+ onBlur,
277
+ },
278
+ };
279
+ }
@@ -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,17 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ minify: false,
10
+ treeshake: true,
11
+ external: ["react", "react-dom", "@kayforms/core"],
12
+ outExtension({ format }) {
13
+ return {
14
+ js: format === "cjs" ? ".cjs" : ".js",
15
+ };
16
+ },
17
+ });
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@kayforms/solid",
3
+ "version": "0.1.0",
4
+ "description": "Solid JS adapter for Kayforms reactive form library",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "clean": "rimraf dist",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@kayforms/core": "workspace:*"
30
+ },
31
+ "peerDependencies": {
32
+ "solid-js": "^1.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "solid-js": "^1.8.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.5.0",
38
+ "rimraf": "^5.0.0"
39
+ },
40
+ "sideEffects": false,
41
+ "license": "MIT"
42
+ }