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,354 @@
1
+ // ============================================================================
2
+ // KayForms DevTools Panel Controller
3
+ // ============================================================================
4
+
5
+ let selectedFormId = "";
6
+ let playInterval = null;
7
+ let currentHistory = [];
8
+ let currentCursor = -1;
9
+
10
+ // --- DOM References ---
11
+ const formSelect = document.getElementById("form-select");
12
+ const btnExport = document.getElementById("btn-export");
13
+ const btnImport = document.getElementById("btn-import");
14
+ const importFile = document.getElementById("import-file");
15
+ const btnRewind = document.getElementById("btn-rewind");
16
+ const btnPlayPause = document.getElementById("btn-play-pause");
17
+ const btnClear = document.getElementById("btn-clear");
18
+ const timeSlider = document.getElementById("time-slider");
19
+ const sliderTicks = document.getElementById("slider-ticks");
20
+ const currentStepLabel = document.getElementById("current-step-label");
21
+ const totalStepsLabel = document.getElementById("total-steps-label");
22
+ const timelineList = document.getElementById("timeline-list");
23
+ const jsonValues = document.getElementById("json-values");
24
+ const jsonErrors = document.getElementById("json-errors");
25
+ const validBadge = document.getElementById("valid-badge");
26
+ const entriesCountBadge = document.getElementById("entries-count-badge");
27
+
28
+ // --- Extension Port Connection ---
29
+ const tabId = chrome.devtools.inspectedWindow.tabId;
30
+ const port = chrome.runtime.connect({ name: "kayforms-devtools" });
31
+ port.postMessage({ type: "init", tabId });
32
+
33
+ // Listen for updates from content script
34
+ port.onMessage.addListener((message) => {
35
+ if (message.type === "history-change") {
36
+ loadForms();
37
+ }
38
+ });
39
+
40
+ // --- Initialization ---
41
+ loadForms();
42
+ // Periodically check for active forms in case events missed
43
+ setInterval(loadForms, 2000);
44
+
45
+ // --- Form Selector Change ---
46
+ formSelect.addEventListener("change", (e) => {
47
+ selectedFormId = e.target.value;
48
+ stopPlayback();
49
+ if (selectedFormId) {
50
+ fetchHistory(selectedFormId);
51
+ } else {
52
+ renderEmptyState();
53
+ }
54
+ });
55
+
56
+ // --- Fetch & Update UI ---
57
+ function loadForms() {
58
+ chrome.devtools.inspectedWindow.eval(
59
+ "window.__KAYFORMS_DEVTOOLS__ ? Object.keys(window.__KAYFORMS_DEVTOOLS__.forms) : []",
60
+ (formIds, isException) => {
61
+ if (isException || !formIds) return;
62
+
63
+ const previousSelection = selectedFormId;
64
+
65
+ // Update dropdown
66
+ formSelect.innerHTML = "";
67
+ if (formIds.length === 0) {
68
+ formSelect.innerHTML = '<option value="">-- No Active Forms --</option>';
69
+ selectedFormId = "";
70
+ renderEmptyState();
71
+ return;
72
+ }
73
+
74
+ formIds.forEach((id) => {
75
+ const option = document.createElement("option");
76
+ option.value = id;
77
+ option.textContent = id;
78
+ formSelect.appendChild(option);
79
+ });
80
+
81
+ // Restore selection if valid
82
+ if (formIds.includes(previousSelection)) {
83
+ selectedFormId = previousSelection;
84
+ formSelect.value = selectedFormId;
85
+ } else {
86
+ selectedFormId = formIds[0];
87
+ formSelect.value = selectedFormId;
88
+ }
89
+
90
+ fetchHistory(selectedFormId);
91
+ }
92
+ );
93
+ }
94
+
95
+ function fetchHistory(formId) {
96
+ if (!formId) return;
97
+ chrome.devtools.inspectedWindow.eval(
98
+ `(() => {
99
+ const devtools = window.__KAYFORMS_DEVTOOLS__;
100
+ if (!devtools || !devtools.forms['${formId}']) return null;
101
+ const form = devtools.forms['${formId}'];
102
+ return {
103
+ history: form.getHistory(),
104
+ cursor: form.getCursor()
105
+ };
106
+ })()`,
107
+ (data, isException) => {
108
+ if (isException || !data) return;
109
+ currentHistory = data.history || [];
110
+ currentCursor = data.cursor ?? -1;
111
+ renderUI();
112
+ }
113
+ );
114
+ }
115
+
116
+ function renderEmptyState() {
117
+ timelineList.innerHTML = '<div class="empty-state">No form state changes recorded yet. Interacted fields will show up here.</div>';
118
+ jsonValues.textContent = "{}";
119
+ jsonErrors.textContent = "{}";
120
+ entriesCountBadge.textContent = "0 entries";
121
+ validBadge.className = "badge badge-success";
122
+ validBadge.textContent = "Valid";
123
+ timeSlider.disabled = true;
124
+ timeSlider.max = 0;
125
+ timeSlider.value = 0;
126
+ currentStepLabel.textContent = "0";
127
+ totalStepsLabel.textContent = "0";
128
+ }
129
+
130
+ function renderUI() {
131
+ const history = currentHistory;
132
+ const cursor = currentCursor;
133
+
134
+ entriesCountBadge.textContent = `${history.length} entries`;
135
+
136
+ if (history.length === 0) {
137
+ renderEmptyState();
138
+ return;
139
+ }
140
+
141
+ // Update slider bounds
142
+ timeSlider.disabled = history.length <= 1;
143
+ timeSlider.max = history.length - 1;
144
+ timeSlider.value = cursor;
145
+
146
+ currentStepLabel.textContent = cursor + 1;
147
+ totalStepsLabel.textContent = history.length;
148
+
149
+ // Render ticks
150
+ sliderTicks.innerHTML = "";
151
+ for (let i = 0; i < history.length; i++) {
152
+ const tick = document.createElement("div");
153
+ tick.className = `slider-tick ${i === cursor ? "active" : ""}`;
154
+ sliderTicks.appendChild(tick);
155
+ }
156
+
157
+ // Render Timeline list
158
+ timelineList.innerHTML = "";
159
+ for (let i = history.length - 1; i >= 0; i--) {
160
+ const entry = history[i];
161
+ const isCurrent = i === cursor;
162
+ const isFuture = i > cursor;
163
+
164
+ const card = document.createElement("div");
165
+ card.className = `entry-card ${isCurrent ? "active" : ""} ${isFuture ? "future" : ""}`;
166
+ card.onclick = () => jumpTo(i);
167
+
168
+ // Number indicator
169
+ const num = document.createElement("span");
170
+ num.className = "entry-index";
171
+ num.textContent = `#${i + 1}`;
172
+ card.appendChild(num);
173
+
174
+ // Indicator Dot (color matches status)
175
+ const dot = document.createElement("div");
176
+ dot.className = "entry-dot";
177
+ const hasErrors = entry.errors && Object.keys(entry.errors).length > 0;
178
+ if (hasErrors) {
179
+ dot.classList.add("validation-error");
180
+ } else if (entry.changedField) {
181
+ dot.classList.add("changed-field");
182
+ }
183
+ card.appendChild(dot);
184
+
185
+ // Details column
186
+ const details = document.createElement("div");
187
+ details.className = "entry-details";
188
+
189
+ const row1 = document.createElement("div");
190
+ row1.className = "entry-row";
191
+
192
+ const action = document.createElement("span");
193
+ action.className = "entry-action";
194
+ action.textContent = entry.changedField ? "Field Update" : "Form Action";
195
+ row1.appendChild(action);
196
+
197
+ const time = document.createElement("span");
198
+ time.className = "entry-time";
199
+ const d = new Date(entry.timestamp);
200
+ time.textContent = `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}.${d.getMilliseconds().toString().padStart(3, "0")}`;
201
+ row1.appendChild(time);
202
+
203
+ details.appendChild(row1);
204
+
205
+ if (entry.changedField) {
206
+ const row2 = document.createElement("div");
207
+ row2.className = "entry-row";
208
+ const path = document.createElement("span");
209
+ path.className = "entry-path";
210
+ path.textContent = entry.changedField;
211
+ row2.appendChild(path);
212
+ details.appendChild(row2);
213
+ }
214
+
215
+ card.appendChild(details);
216
+ timelineList.appendChild(card);
217
+ }
218
+
219
+ // Populate JSON inspector for selected entry
220
+ const selectedEntry = history[cursor] || history[history.length - 1];
221
+ if (selectedEntry) {
222
+ jsonValues.textContent = JSON.stringify(selectedEntry.values, null, 2);
223
+ jsonErrors.textContent = JSON.stringify(selectedEntry.errors, null, 2);
224
+
225
+ const hasErrors = selectedEntry.errors && Object.keys(selectedEntry.errors).length > 0;
226
+ if (hasErrors) {
227
+ validBadge.className = "badge badge-danger";
228
+ validBadge.textContent = "Invalid";
229
+ } else {
230
+ validBadge.className = "badge badge-success";
231
+ validBadge.textContent = "Valid";
232
+ }
233
+ }
234
+ }
235
+
236
+ // --- Time-Travel Actions ---
237
+ function jumpTo(index) {
238
+ if (!selectedFormId) return;
239
+ chrome.devtools.inspectedWindow.eval(
240
+ `window.__KAYFORMS_DEVTOOLS__.forms['${selectedFormId}'].jumpTo(${index})`,
241
+ () => {
242
+ fetchHistory(selectedFormId);
243
+ }
244
+ );
245
+ }
246
+
247
+ timeSlider.addEventListener("input", (e) => {
248
+ const index = parseInt(e.target.value, 10);
249
+ jumpTo(index);
250
+ });
251
+
252
+ btnRewind.addEventListener("click", () => {
253
+ stopPlayback();
254
+ jumpTo(0);
255
+ });
256
+
257
+ btnClear.addEventListener("click", () => {
258
+ if (!selectedFormId) return;
259
+ stopPlayback();
260
+ chrome.devtools.inspectedWindow.eval(
261
+ `window.__KAYFORMS_DEVTOOLS__.forms['${selectedFormId}'].clearHistory()`,
262
+ () => {
263
+ fetchHistory(selectedFormId);
264
+ }
265
+ );
266
+ });
267
+
268
+ // --- Playback Engine ---
269
+ function startPlayback() {
270
+ if (playInterval) return;
271
+
272
+ btnPlayPause.textContent = "⏸";
273
+ btnPlayPause.title = "Pause history";
274
+ btnPlayPause.classList.add("playing");
275
+
276
+ playInterval = setInterval(() => {
277
+ if (currentCursor < currentHistory.length - 1) {
278
+ chrome.devtools.inspectedWindow.eval(
279
+ `window.__KAYFORMS_DEVTOOLS__.forms['${selectedFormId}'].redo()`,
280
+ () => {
281
+ fetchHistory(selectedFormId);
282
+ }
283
+ );
284
+ } else {
285
+ stopPlayback();
286
+ }
287
+ }, 600);
288
+ }
289
+
290
+ function stopPlayback() {
291
+ if (!playInterval) return;
292
+ clearInterval(playInterval);
293
+ playInterval = null;
294
+ btnPlayPause.textContent = "▶";
295
+ btnPlayPause.title = "Play history";
296
+ btnPlayPause.classList.remove("playing");
297
+ }
298
+
299
+ btnPlayPause.addEventListener("click", () => {
300
+ if (playInterval) {
301
+ stopPlayback();
302
+ } else {
303
+ startPlayback();
304
+ }
305
+ });
306
+
307
+ // --- Export & Import ---
308
+ btnExport.addEventListener("click", () => {
309
+ if (!selectedFormId || currentHistory.length === 0) return;
310
+
311
+ const dataStr = JSON.stringify(currentHistory, null, 2);
312
+ const blob = new Blob([dataStr], { type: "application/json" });
313
+ const url = URL.createObjectURL(blob);
314
+
315
+ // Chrome extensions can download files via DevTools page inside helper scripts
316
+ // or a simple element injection:
317
+ const a = document.createElement("a");
318
+ a.href = url;
319
+ a.download = `kayforms-${selectedFormId}-history.json`;
320
+ document.body.appendChild(a);
321
+ a.click();
322
+ document.body.removeChild(a);
323
+ URL.revokeObjectURL(url);
324
+ });
325
+
326
+ btnImport.addEventListener("click", () => {
327
+ importFile.click();
328
+ });
329
+
330
+ importFile.addEventListener("change", (e) => {
331
+ const file = e.target.files[0];
332
+ if (!file) return;
333
+
334
+ const reader = new FileReader();
335
+ reader.onload = (event) => {
336
+ try {
337
+ const importedData = JSON.parse(event.target.result);
338
+ if (!Array.isArray(importedData)) {
339
+ alert("Invalid history format: must be an array of states.");
340
+ return;
341
+ }
342
+
343
+ chrome.devtools.inspectedWindow.eval(
344
+ `window.__KAYFORMS_DEVTOOLS__.forms['${selectedFormId}'].importHistory(${JSON.stringify(importedData)})`,
345
+ () => {
346
+ fetchHistory(selectedFormId);
347
+ }
348
+ );
349
+ } catch (err) {
350
+ alert("Error parsing JSON file: " + err.message);
351
+ }
352
+ };
353
+ reader.readAsText(file);
354
+ });
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@kayforms/devtools",
3
+ "version": "0.1.0",
4
+ "description": "In-page time-travel debug panel for Kayforms",
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
+ "devDependencies": {
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.5.0",
34
+ "rimraf": "^5.0.0"
35
+ },
36
+ "sideEffects": true,
37
+ "license": "MIT"
38
+ }
@@ -0,0 +1,95 @@
1
+ // ============================================================================
2
+ // @kayforms/devtools — Public API
3
+ // ============================================================================
4
+ // Simple one-line setup: import and call connectDevTools(form) to get a
5
+ // floating debug panel with time-travel.
6
+ // ============================================================================
7
+
8
+ import { createDevTools, type DevToolsBridge, type FormStore, type DevToolsConfig } from "@kayforms/core";
9
+ import { createPanel, type PanelOptions } from "./panel";
10
+
11
+ export { createPanel, type PanelOptions } from "./panel";
12
+ export { DEVTOOLS_STYLES } from "./styles";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // connectDevTools — One-line setup
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface ConnectOptions extends DevToolsConfig, PanelOptions {}
19
+
20
+ /**
21
+ * Connect one or more forms to the Kayforms DevTools panel.
22
+ * Opens a floating debug panel with time-travel debugging.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { createForm } from '@kayforms/core';
27
+ * import { connectDevTools } from '@kayforms/devtools';
28
+ *
29
+ * const form = createForm({
30
+ * id: 'login',
31
+ * initialValues: { email: '', password: '' },
32
+ * });
33
+ *
34
+ * // One line to enable devtools!
35
+ * const devtools = connectDevTools(form);
36
+ *
37
+ * // Multiple forms:
38
+ * connectDevTools(form1, form2, form3);
39
+ *
40
+ * // With options:
41
+ * connectDevTools(form, { minimized: true, maxEntries: 1000 });
42
+ * ```
43
+ *
44
+ * @returns Object with `destroy()` to remove the panel and `bridge` for programmatic access
45
+ */
46
+ export function connectDevTools(
47
+ ...args: (FormStore | ConnectOptions)[]
48
+ ): { destroy: () => void; bridge: DevToolsBridge; toggle: () => void } {
49
+ // Parse arguments: forms + optional options at the end
50
+ let options: ConnectOptions = {};
51
+ let forms: FormStore[];
52
+
53
+ const lastArg = args[args.length - 1];
54
+ if (lastArg && typeof lastArg === "object" && !("values" in lastArg)) {
55
+ options = lastArg as ConnectOptions;
56
+ forms = args.slice(0, -1) as FormStore[];
57
+ } else {
58
+ forms = args as FormStore[];
59
+ }
60
+
61
+ // Create DevTools bridge
62
+ const bridge = createDevTools({
63
+ snapshotInterval: options.snapshotInterval,
64
+ maxEntries: options.maxEntries,
65
+ enableInProduction: options.enableInProduction,
66
+ });
67
+
68
+ // Attach all forms
69
+ const detachFns: (() => void)[] = [];
70
+ for (const form of forms) {
71
+ const detach = bridge.attach(form);
72
+ detachFns.push(detach);
73
+ }
74
+
75
+ // Create floating panel
76
+ const panel = createPanel(bridge, forms, {
77
+ position: options.position,
78
+ minimized: options.minimized,
79
+ activeTab: options.activeTab,
80
+ });
81
+
82
+ function destroy(): void {
83
+ panel.destroy();
84
+ for (const detach of detachFns) {
85
+ detach();
86
+ }
87
+ bridge.detach();
88
+ }
89
+
90
+ return {
91
+ destroy,
92
+ bridge,
93
+ toggle: panel.toggle,
94
+ };
95
+ }