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.
- package/LICENSE +21 -0
- package/README.md +337 -0
- package/examples/react-demo/README.md +337 -0
- package/examples/react-demo/eslint.config.js +22 -0
- package/examples/react-demo/index.html +13 -0
- package/examples/react-demo/package.json +33 -0
- package/examples/react-demo/public/apple-touch-icon.png +0 -0
- package/examples/react-demo/public/favicon-96x96.png +0 -0
- package/examples/react-demo/public/favicon.ico +0 -0
- package/examples/react-demo/public/favicon.svg +17 -0
- package/examples/react-demo/public/icons.svg +24 -0
- package/examples/react-demo/public/site.webmanifest +21 -0
- package/examples/react-demo/public/web-app-manifest-192x192.png +0 -0
- package/examples/react-demo/public/web-app-manifest-512x512.png +0 -0
- package/examples/react-demo/src/App.css +184 -0
- package/examples/react-demo/src/App.tsx +825 -0
- package/examples/react-demo/src/assets/hero.png +0 -0
- package/examples/react-demo/src/assets/react.svg +1 -0
- package/examples/react-demo/src/assets/vite.svg +1 -0
- package/examples/react-demo/src/index.css +627 -0
- package/examples/react-demo/src/main.tsx +10 -0
- package/examples/react-demo/tsconfig.app.json +25 -0
- package/examples/react-demo/tsconfig.json +7 -0
- package/examples/react-demo/tsconfig.node.json +24 -0
- package/examples/react-demo/vite.config.ts +7 -0
- package/kayforms.jpg +0 -0
- package/package.json +26 -0
- package/packages/angular/package.json +43 -0
- package/packages/angular/src/index.ts +198 -0
- package/packages/angular/tsconfig.json +8 -0
- package/packages/angular/tsup.config.ts +17 -0
- package/packages/core/README.md +337 -0
- package/packages/core/package.json +37 -0
- package/packages/core/src/batch.ts +106 -0
- package/packages/core/src/devtools.ts +329 -0
- package/packages/core/src/field.ts +167 -0
- package/packages/core/src/form.ts +448 -0
- package/packages/core/src/index.ts +71 -0
- package/packages/core/src/registry.ts +126 -0
- package/packages/core/src/signal.ts +399 -0
- package/packages/core/src/time-travel.ts +275 -0
- package/packages/core/src/validation.ts +243 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/core/tsup.config.ts +16 -0
- package/packages/devtools/extension/background.js +35 -0
- package/packages/devtools/extension/content-script.js +10 -0
- package/packages/devtools/extension/devtools.html +9 -0
- package/packages/devtools/extension/devtools.js +8 -0
- package/packages/devtools/extension/manifest.json +19 -0
- package/packages/devtools/extension/panel.css +505 -0
- package/packages/devtools/extension/panel.html +108 -0
- package/packages/devtools/extension/panel.js +354 -0
- package/packages/devtools/package.json +38 -0
- package/packages/devtools/src/index.ts +95 -0
- package/packages/devtools/src/panel.ts +226 -0
- package/packages/devtools/src/styles.ts +422 -0
- package/packages/devtools/src/timeline.ts +283 -0
- package/packages/devtools/tsconfig.json +8 -0
- package/packages/devtools/tsup.config.ts +17 -0
- package/packages/react/package.json +46 -0
- package/packages/react/src/index.ts +279 -0
- package/packages/react/tsconfig.json +8 -0
- package/packages/react/tsup.config.ts +17 -0
- package/packages/solid/package.json +42 -0
- package/packages/solid/src/index.ts +206 -0
- package/packages/solid/tsconfig.json +8 -0
- package/packages/solid/tsup.config.ts +17 -0
- package/packages/svelte/package.json +42 -0
- package/packages/svelte/src/index.ts +199 -0
- package/packages/svelte/tsconfig.json +8 -0
- package/packages/svelte/tsup.config.ts +17 -0
- package/packages/vanilla/package.json +38 -0
- package/packages/vanilla/src/index.ts +254 -0
- package/packages/vanilla/tsconfig.json +8 -0
- package/packages/vanilla/tsup.config.ts +17 -0
- package/packages/vue/package.json +42 -0
- package/packages/vue/src/index.ts +217 -0
- package/packages/vue/tsconfig.json +8 -0
- package/packages/vue/tsup.config.ts +17 -0
- package/pnpm-workspace.yaml +3 -0
- 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
|
+
}
|