attaform 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -36
- package/dist/chunks/devtools.cjs +10 -37
- package/dist/chunks/devtools.cjs.map +1 -1
- package/dist/chunks/devtools.mjs +10 -37
- package/dist/chunks/devtools.mjs.map +1 -1
- package/dist/chunks/indexeddb.cjs +4 -4
- package/dist/chunks/indexeddb.cjs.map +1 -1
- package/dist/chunks/indexeddb.mjs +1 -1
- package/dist/chunks/local-storage.cjs +2 -2
- package/dist/chunks/local-storage.cjs.map +1 -1
- package/dist/chunks/local-storage.mjs +1 -1
- package/dist/chunks/session-storage.cjs +2 -2
- package/dist/chunks/session-storage.cjs.map +1 -1
- package/dist/chunks/session-storage.mjs +1 -1
- package/dist/index.cjs +42 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -196
- package/dist/index.d.mts +159 -196
- package/dist/index.d.ts +159 -196
- package/dist/index.mjs +5 -7
- package/dist/index.mjs.map +1 -1
- package/dist/nuxt.cjs +31 -40
- package/dist/nuxt.cjs.map +1 -1
- package/dist/nuxt.d.cts +8 -1
- package/dist/nuxt.d.mts +8 -1
- package/dist/nuxt.d.ts +8 -1
- package/dist/nuxt.mjs +32 -41
- package/dist/nuxt.mjs.map +1 -1
- package/dist/runtime/components/AttaformDevtoolsPanel.d.vue.ts +7 -0
- package/dist/runtime/components/AttaformDevtoolsPanel.vue +453 -0
- package/dist/runtime/components/AttaformDevtoolsPanel.vue.d.ts +7 -0
- package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +37 -0
- package/dist/runtime/components/DevtoolsValueTree.vue +192 -0
- package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +37 -0
- package/dist/runtime/plugins/attaform.cjs +17 -6
- package/dist/runtime/plugins/attaform.cjs.map +1 -1
- package/dist/runtime/plugins/attaform.mjs +15 -4
- package/dist/runtime/plugins/attaform.mjs.map +1 -1
- package/dist/shared/attaform.5UhpSVFI.cjs +63 -0
- package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
- package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
- package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
- package/dist/shared/attaform.Bgu9l6OG.d.cts +1651 -0
- package/dist/shared/attaform.BmDBu4ql.d.ts +1651 -0
- package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
- package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
- package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.BsMdl-35.d.cts} +754 -146
- package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.BsMdl-35.d.mts} +754 -146
- package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.BsMdl-35.d.ts} +754 -146
- package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
- package/dist/shared/{attaform.C6lbmMUe.d.ts → attaform.C3x1hKJC.d.mts} +4 -4
- package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.CWs1Z3p7.d.ts} +57 -23
- package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
- package/dist/shared/{attaform.C5MH4lNh.d.mts → attaform.CjmJpfLH.d.ts} +4 -4
- package/dist/shared/{attaform.Drt6fivF.mjs → attaform.CtNUB9nf.mjs} +74 -76
- package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
- package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.D-hDvb98.d.cts} +57 -23
- package/dist/shared/attaform.DAH3kvav.d.mts +1651 -0
- package/dist/shared/{attaform.BPRHR3Zs.cjs → attaform.DUHru0OF.cjs} +83 -85
- package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
- package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
- package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
- package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
- package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
- package/dist/shared/{attaform.DtMN-MAm.d.cts → attaform.Dzi89x8N.d.cts} +4 -4
- package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
- package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
- package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
- package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
- package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
- package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
- package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.bH7WvNad.d.mts} +57 -23
- package/dist/vite.cjs +270 -2
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.mjs +266 -2
- package/dist/vite.mjs.map +1 -1
- package/dist/zod-v3.cjs +11 -8
- package/dist/zod-v3.cjs.map +1 -1
- package/dist/zod-v3.d.cts +6 -6
- package/dist/zod-v3.d.mts +6 -6
- package/dist/zod-v3.d.ts +6 -6
- package/dist/zod-v3.mjs +3 -3
- package/dist/zod-v4.cjs +11 -8
- package/dist/zod-v4.cjs.map +1 -1
- package/dist/zod-v4.d.cts +5 -5
- package/dist/zod-v4.d.mts +5 -5
- package/dist/zod-v4.d.ts +5 -5
- package/dist/zod-v4.mjs +3 -3
- package/dist/zod.cjs +15 -16
- package/dist/zod.cjs.map +1 -1
- package/dist/zod.d.cts +127 -40
- package/dist/zod.d.mts +127 -40
- package/dist/zod.d.ts +127 -40
- package/dist/zod.mjs +7 -11
- package/dist/zod.mjs.map +1 -1
- package/package.json +19 -5
- package/dist/shared/attaform.B1jvxsOF.d.mts +0 -156
- package/dist/shared/attaform.B3ZaPIzS.mjs.map +0 -1
- package/dist/shared/attaform.BBM2muQ9.cjs +0 -101
- package/dist/shared/attaform.BBM2muQ9.cjs.map +0 -1
- package/dist/shared/attaform.BPRHR3Zs.cjs.map +0 -1
- package/dist/shared/attaform.BV40t5y2.cjs.map +0 -1
- package/dist/shared/attaform.C6qzEdIM.d.cts +0 -156
- package/dist/shared/attaform.C8LVFVVe.cjs +0 -32
- package/dist/shared/attaform.C8LVFVVe.cjs.map +0 -1
- package/dist/shared/attaform.CIEQgJnM.mjs.map +0 -1
- package/dist/shared/attaform.CTwNcpLE.d.ts +0 -156
- package/dist/shared/attaform.Cer8JO_P.cjs.map +0 -1
- package/dist/shared/attaform.CpERWz3u.mjs.map +0 -1
- package/dist/shared/attaform.Dee2rU1P.cjs.map +0 -1
- package/dist/shared/attaform.Drt6fivF.mjs.map +0 -1
- package/dist/shared/attaform.Vo-Kft0t.mjs +0 -29
- package/dist/shared/attaform.Vo-Kft0t.mjs.map +0 -1
- package/dist/shared/attaform.h1sq3BFu.mjs +0 -92
- package/dist/shared/attaform.h1sq3BFu.mjs.map +0 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onUnmounted, ref, watch } from "vue";
|
|
3
|
+
import { canonicalizePath } from "attaform";
|
|
4
|
+
import DevtoolsValueTree from "./DevtoolsValueTree.vue";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
bridge: { type: Object, required: true }
|
|
7
|
+
});
|
|
8
|
+
const updateTick = ref(0);
|
|
9
|
+
const registry = computed(() => props.bridge.registry);
|
|
10
|
+
const formEntries = computed(() => {
|
|
11
|
+
void updateTick.value;
|
|
12
|
+
return Array.from(registry.value.forms.entries());
|
|
13
|
+
});
|
|
14
|
+
const selectedFormKey = ref(null);
|
|
15
|
+
const activeKey = computed(() => {
|
|
16
|
+
if (selectedFormKey.value !== null && registry.value.forms.has(selectedFormKey.value)) {
|
|
17
|
+
return selectedFormKey.value;
|
|
18
|
+
}
|
|
19
|
+
return formEntries.value[0]?.[0] ?? null;
|
|
20
|
+
});
|
|
21
|
+
const activeForm = computed(() => {
|
|
22
|
+
const key = activeKey.value;
|
|
23
|
+
return key !== null ? registry.value.forms.get(key) ?? null : null;
|
|
24
|
+
});
|
|
25
|
+
const formValueView = computed(() => {
|
|
26
|
+
void updateTick.value;
|
|
27
|
+
const form = activeForm.value;
|
|
28
|
+
if (form === null) return null;
|
|
29
|
+
return form.form.value;
|
|
30
|
+
});
|
|
31
|
+
const schemaErrorRows = computed(() => {
|
|
32
|
+
void updateTick.value;
|
|
33
|
+
const form = activeForm.value;
|
|
34
|
+
if (form === null) return [];
|
|
35
|
+
return Array.from(form.schemaErrors.entries());
|
|
36
|
+
});
|
|
37
|
+
const userErrorRows = computed(() => {
|
|
38
|
+
void updateTick.value;
|
|
39
|
+
const form = activeForm.value;
|
|
40
|
+
if (form === null) return [];
|
|
41
|
+
return Array.from(form.userErrors.entries());
|
|
42
|
+
});
|
|
43
|
+
const aggregates = computed(() => {
|
|
44
|
+
void updateTick.value;
|
|
45
|
+
const form = activeForm.value;
|
|
46
|
+
if (form === null) return null;
|
|
47
|
+
return {
|
|
48
|
+
submitting: form.submitting.value,
|
|
49
|
+
submissionAttempts: form.submissionAttempts.value,
|
|
50
|
+
submitError: form.submitError.value,
|
|
51
|
+
activeValidations: form.activeValidations.value
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
function selectForm(key) {
|
|
55
|
+
selectedFormKey.value = key;
|
|
56
|
+
}
|
|
57
|
+
function humanizePathKey(key) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(key);
|
|
60
|
+
if (Array.isArray(parsed)) {
|
|
61
|
+
return parsed.map((seg) => typeof seg === "number" ? String(seg) : String(seg)).join(".");
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
return key;
|
|
66
|
+
}
|
|
67
|
+
function fmt(v) {
|
|
68
|
+
if (v === null) return "null";
|
|
69
|
+
if (v === void 0) return "undefined";
|
|
70
|
+
if (typeof v === "string") return v;
|
|
71
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
72
|
+
return String(v);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return JSON.stringify(v);
|
|
76
|
+
} catch {
|
|
77
|
+
return String(v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const selectedPath = ref(null);
|
|
81
|
+
const selectedKey = computed(
|
|
82
|
+
() => selectedPath.value === null ? null : JSON.stringify(selectedPath.value)
|
|
83
|
+
);
|
|
84
|
+
function selectPath(path) {
|
|
85
|
+
const key = JSON.stringify(path);
|
|
86
|
+
if (selectedKey.value === key) {
|
|
87
|
+
selectedPath.value = null;
|
|
88
|
+
} else {
|
|
89
|
+
selectedPath.value = path;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const selectedFieldState = computed(() => {
|
|
93
|
+
void updateTick.value;
|
|
94
|
+
const form = activeForm.value;
|
|
95
|
+
const path = selectedPath.value;
|
|
96
|
+
if (form === null || path === null) return null;
|
|
97
|
+
try {
|
|
98
|
+
const { key: canonicalKey } = canonicalizePath(path);
|
|
99
|
+
const record = form.fields.get(canonicalKey) ?? null;
|
|
100
|
+
let value = form.form.value;
|
|
101
|
+
for (const seg of path) {
|
|
102
|
+
if (value === null || typeof value !== "object") {
|
|
103
|
+
value = void 0;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
value = value[seg];
|
|
107
|
+
}
|
|
108
|
+
const schemaEntries = form.schemaErrors.get(canonicalKey) ?? [];
|
|
109
|
+
const userEntries = form.userErrors.get(canonicalKey) ?? [];
|
|
110
|
+
return {
|
|
111
|
+
record,
|
|
112
|
+
value,
|
|
113
|
+
errors: [...schemaEntries, ...userEntries],
|
|
114
|
+
schemaErrorCount: schemaEntries.length,
|
|
115
|
+
userErrorCount: userEntries.length
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("[attaform devtools] field-state lookup failed", { path, err });
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
function humanizeSelectedPath() {
|
|
123
|
+
const path = selectedPath.value;
|
|
124
|
+
if (path === null || path.length === 0) return "(root)";
|
|
125
|
+
return path.map((seg) => String(seg)).join(".");
|
|
126
|
+
}
|
|
127
|
+
function selectedValueView() {
|
|
128
|
+
const fs = selectedFieldState.value;
|
|
129
|
+
if (fs === null) return null;
|
|
130
|
+
return fs.value;
|
|
131
|
+
}
|
|
132
|
+
function handleEdit(rawPath, next) {
|
|
133
|
+
const form = activeForm.value;
|
|
134
|
+
if (form === null) return;
|
|
135
|
+
try {
|
|
136
|
+
const { segments: canonicalPath, key: canonicalKey } = canonicalizePath(
|
|
137
|
+
rawPath
|
|
138
|
+
);
|
|
139
|
+
form.setValueAtPath(canonicalPath, next, {
|
|
140
|
+
persist: form.persistOptIns.hasAnyOptInForPath(canonicalKey)
|
|
141
|
+
});
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("[attaform devtools] edit failed", { rawPath, next, err });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const MAX_TIMELINE_EVENTS = 200;
|
|
147
|
+
const events = ref([]);
|
|
148
|
+
const expandedEventId = ref(null);
|
|
149
|
+
let eventIdCounter = 0;
|
|
150
|
+
function pushEvent(seed) {
|
|
151
|
+
const entry = { id: ++eventIdCounter, ...seed };
|
|
152
|
+
const next = [entry, ...events.value];
|
|
153
|
+
if (next.length > MAX_TIMELINE_EVENTS) next.length = MAX_TIMELINE_EVENTS;
|
|
154
|
+
events.value = next;
|
|
155
|
+
}
|
|
156
|
+
function clearTimeline() {
|
|
157
|
+
events.value = [];
|
|
158
|
+
expandedEventId.value = null;
|
|
159
|
+
}
|
|
160
|
+
function toggleEvent(id) {
|
|
161
|
+
expandedEventId.value = expandedEventId.value === id ? null : id;
|
|
162
|
+
}
|
|
163
|
+
function formatTime(time) {
|
|
164
|
+
const d = new Date(time);
|
|
165
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
166
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
167
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
168
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
169
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
170
|
+
}
|
|
171
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
172
|
+
function subscribeForm(key, form) {
|
|
173
|
+
if (subscribers.has(key)) return;
|
|
174
|
+
const captureValue = () => {
|
|
175
|
+
try {
|
|
176
|
+
return structuredClone(form.form.value);
|
|
177
|
+
} catch {
|
|
178
|
+
return form.form.value;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const unsubChange = form.onFormChange(() => {
|
|
182
|
+
pushEvent({ type: "form.change", formKey: key, time: Date.now(), value: captureValue() });
|
|
183
|
+
updateTick.value++;
|
|
184
|
+
});
|
|
185
|
+
const unsubSubmit = form.onSubmitSuccess(() => {
|
|
186
|
+
pushEvent({ type: "submit.success", formKey: key, time: Date.now(), value: captureValue() });
|
|
187
|
+
updateTick.value++;
|
|
188
|
+
});
|
|
189
|
+
const unsubReset = form.onReset(() => {
|
|
190
|
+
pushEvent({ type: "reset", formKey: key, time: Date.now(), value: captureValue() });
|
|
191
|
+
updateTick.value++;
|
|
192
|
+
});
|
|
193
|
+
subscribers.set(key, () => {
|
|
194
|
+
unsubChange();
|
|
195
|
+
unsubSubmit();
|
|
196
|
+
unsubReset();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
watch(
|
|
200
|
+
formEntries,
|
|
201
|
+
(entries) => {
|
|
202
|
+
const liveKeys = new Set(entries.map(([k]) => k));
|
|
203
|
+
for (const [key, form] of entries) {
|
|
204
|
+
if (!subscribers.has(key)) subscribeForm(key, form);
|
|
205
|
+
}
|
|
206
|
+
for (const [key, unsub] of subscribers) {
|
|
207
|
+
if (!liveKeys.has(key)) {
|
|
208
|
+
unsub();
|
|
209
|
+
subscribers.delete(key);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{ immediate: true }
|
|
214
|
+
);
|
|
215
|
+
const POLL_INTERVAL_MS = 120;
|
|
216
|
+
const pollHandle = window.setInterval(() => {
|
|
217
|
+
updateTick.value++;
|
|
218
|
+
}, POLL_INTERVAL_MS);
|
|
219
|
+
onUnmounted(() => {
|
|
220
|
+
window.clearInterval(pollHandle);
|
|
221
|
+
for (const unsub of subscribers.values()) unsub();
|
|
222
|
+
subscribers.clear();
|
|
223
|
+
});
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<template>
|
|
227
|
+
<div class="atf-panel">
|
|
228
|
+
<header class="atf-header">
|
|
229
|
+
<div class="atf-brand">
|
|
230
|
+
<svg
|
|
231
|
+
class="atf-logo"
|
|
232
|
+
viewBox="0 0 24 24"
|
|
233
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
234
|
+
aria-hidden="true"
|
|
235
|
+
>
|
|
236
|
+
<rect width="24" height="24" rx="5" fill="#6938ef" />
|
|
237
|
+
<g
|
|
238
|
+
fill="none"
|
|
239
|
+
stroke="#ffffff"
|
|
240
|
+
stroke-width="2.25"
|
|
241
|
+
stroke-linecap="round"
|
|
242
|
+
stroke-linejoin="round"
|
|
243
|
+
>
|
|
244
|
+
<path d="M8 16 L12 8 L16 16" />
|
|
245
|
+
<path d="M9.5 13 L14.5 13" />
|
|
246
|
+
</g>
|
|
247
|
+
</svg>
|
|
248
|
+
<span class="atf-title">Attaform</span>
|
|
249
|
+
<span class="atf-version">v{{ bridge.version }}</span>
|
|
250
|
+
</div>
|
|
251
|
+
</header>
|
|
252
|
+
|
|
253
|
+
<div class="atf-body">
|
|
254
|
+
<aside class="atf-sidebar">
|
|
255
|
+
<div class="atf-sidebar-title">
|
|
256
|
+
Forms <span class="atf-count">{{ formEntries.length }}</span>
|
|
257
|
+
</div>
|
|
258
|
+
<ul v-if="formEntries.length === 0" class="atf-empty">
|
|
259
|
+
<li>
|
|
260
|
+
No registered forms yet.
|
|
261
|
+
<small>Call <code>useForm()</code> on a page to see it here.</small>
|
|
262
|
+
</li>
|
|
263
|
+
</ul>
|
|
264
|
+
<ul v-else class="atf-form-list">
|
|
265
|
+
<li
|
|
266
|
+
v-for="[key] in formEntries"
|
|
267
|
+
:key="key"
|
|
268
|
+
class="atf-form-item"
|
|
269
|
+
:class="{ active: key === activeKey }"
|
|
270
|
+
@click="selectForm(key)"
|
|
271
|
+
>
|
|
272
|
+
{{ key }}
|
|
273
|
+
</li>
|
|
274
|
+
</ul>
|
|
275
|
+
</aside>
|
|
276
|
+
|
|
277
|
+
<main class="atf-detail">
|
|
278
|
+
<div v-if="activeForm === null" class="atf-empty-detail">Select a form on the left.</div>
|
|
279
|
+
<template v-else>
|
|
280
|
+
<section class="atf-section">
|
|
281
|
+
<h2 class="atf-section-title">
|
|
282
|
+
Form value
|
|
283
|
+
<span class="atf-section-hint">click a key to inspect field state</span>
|
|
284
|
+
</h2>
|
|
285
|
+
<div class="atf-section-body atf-tree">
|
|
286
|
+
<DevtoolsValueTree
|
|
287
|
+
:value="formValueView"
|
|
288
|
+
:editable="true"
|
|
289
|
+
:on-edit="handleEdit"
|
|
290
|
+
:selected-key="selectedKey"
|
|
291
|
+
:on-select-path="selectPath"
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
|
|
296
|
+
<section v-if="selectedFieldState !== null" class="atf-section">
|
|
297
|
+
<h2 class="atf-section-title">
|
|
298
|
+
Field state
|
|
299
|
+
<code class="atf-path">{{ humanizeSelectedPath() }}</code>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
class="atf-clear-btn"
|
|
303
|
+
title="Deselect"
|
|
304
|
+
@click="selectedPath = null"
|
|
305
|
+
>
|
|
306
|
+
×
|
|
307
|
+
</button>
|
|
308
|
+
</h2>
|
|
309
|
+
<div class="atf-section-body">
|
|
310
|
+
<dl class="atf-aggregates">
|
|
311
|
+
<dt>connected</dt>
|
|
312
|
+
<dd>{{ fmt(selectedFieldState.record?.connected) }}</dd>
|
|
313
|
+
<dt>touched</dt>
|
|
314
|
+
<dd>{{ fmt(selectedFieldState.record?.touched) }}</dd>
|
|
315
|
+
<dt>focused</dt>
|
|
316
|
+
<dd>{{ fmt(selectedFieldState.record?.focused) }}</dd>
|
|
317
|
+
<dt>blurred</dt>
|
|
318
|
+
<dd>{{ fmt(selectedFieldState.record?.blurred) }}</dd>
|
|
319
|
+
<dt>updatedAt</dt>
|
|
320
|
+
<dd>{{ fmt(selectedFieldState.record?.updatedAt) }}</dd>
|
|
321
|
+
<dt>schemaErrors</dt>
|
|
322
|
+
<dd>{{ fmt(selectedFieldState.schemaErrorCount) }}</dd>
|
|
323
|
+
<dt>userErrors</dt>
|
|
324
|
+
<dd>{{ fmt(selectedFieldState.userErrorCount) }}</dd>
|
|
325
|
+
<dt>errors</dt>
|
|
326
|
+
<dd>
|
|
327
|
+
<span v-if="selectedFieldState.errors.length === 0">{{ fmt([]) }}</span>
|
|
328
|
+
<ul v-else class="atf-error-messages">
|
|
329
|
+
<li v-for="(e, i) in selectedFieldState.errors" :key="i">
|
|
330
|
+
{{ e.message }} <small>({{ e.code }})</small>
|
|
331
|
+
</li>
|
|
332
|
+
</ul>
|
|
333
|
+
</dd>
|
|
334
|
+
<dt>value</dt>
|
|
335
|
+
<dd class="atf-tree">
|
|
336
|
+
<DevtoolsValueTree :value="selectedValueView()" />
|
|
337
|
+
</dd>
|
|
338
|
+
</dl>
|
|
339
|
+
</div>
|
|
340
|
+
</section>
|
|
341
|
+
|
|
342
|
+
<section class="atf-section">
|
|
343
|
+
<h2 class="atf-section-title">
|
|
344
|
+
Schema Errors
|
|
345
|
+
<span v-if="schemaErrorRows.length" class="atf-badge atf-badge-error">
|
|
346
|
+
{{ schemaErrorRows.length }}
|
|
347
|
+
</span>
|
|
348
|
+
</h2>
|
|
349
|
+
<div class="atf-section-body">
|
|
350
|
+
<p v-if="schemaErrorRows.length === 0" class="atf-empty-list"> No schema errors. </p>
|
|
351
|
+
<ul v-else class="atf-error-list">
|
|
352
|
+
<li v-for="[path, errs] in schemaErrorRows" :key="path">
|
|
353
|
+
<code class="atf-path">{{ humanizePathKey(path) }}</code>
|
|
354
|
+
<ul class="atf-error-messages">
|
|
355
|
+
<li v-for="(e, i) in errs" :key="i">{{ e.message }}</li>
|
|
356
|
+
</ul>
|
|
357
|
+
</li>
|
|
358
|
+
</ul>
|
|
359
|
+
</div>
|
|
360
|
+
</section>
|
|
361
|
+
|
|
362
|
+
<section class="atf-section">
|
|
363
|
+
<h2 class="atf-section-title">
|
|
364
|
+
User Errors
|
|
365
|
+
<span v-if="userErrorRows.length" class="atf-badge atf-badge-warn">
|
|
366
|
+
{{ userErrorRows.length }}
|
|
367
|
+
</span>
|
|
368
|
+
</h2>
|
|
369
|
+
<div class="atf-section-body">
|
|
370
|
+
<p v-if="userErrorRows.length === 0" class="atf-empty-list">
|
|
371
|
+
No user-injected errors.
|
|
372
|
+
</p>
|
|
373
|
+
<ul v-else class="atf-error-list">
|
|
374
|
+
<li v-for="[path, errs] in userErrorRows" :key="path">
|
|
375
|
+
<code class="atf-path">{{ humanizePathKey(path) }}</code>
|
|
376
|
+
<ul class="atf-error-messages">
|
|
377
|
+
<li v-for="(e, i) in errs" :key="i">{{ e.message }}</li>
|
|
378
|
+
</ul>
|
|
379
|
+
</li>
|
|
380
|
+
</ul>
|
|
381
|
+
</div>
|
|
382
|
+
</section>
|
|
383
|
+
|
|
384
|
+
<section v-if="aggregates" class="atf-section">
|
|
385
|
+
<h2 class="atf-section-title">Aggregates</h2>
|
|
386
|
+
<div class="atf-section-body">
|
|
387
|
+
<dl class="atf-aggregates">
|
|
388
|
+
<dt>submitting</dt>
|
|
389
|
+
<dd>{{ fmt(aggregates.submitting) }}</dd>
|
|
390
|
+
<dt>submissionAttempts</dt>
|
|
391
|
+
<dd>{{ fmt(aggregates.submissionAttempts) }}</dd>
|
|
392
|
+
<dt>submitError</dt>
|
|
393
|
+
<dd>{{ fmt(aggregates.submitError) }}</dd>
|
|
394
|
+
<dt>activeValidations</dt>
|
|
395
|
+
<dd>{{ fmt(aggregates.activeValidations) }}</dd>
|
|
396
|
+
</dl>
|
|
397
|
+
</div>
|
|
398
|
+
</section>
|
|
399
|
+
|
|
400
|
+
<section class="atf-section">
|
|
401
|
+
<h2 class="atf-section-title">
|
|
402
|
+
Timeline
|
|
403
|
+
<span v-if="events.length" class="atf-badge atf-badge-neutral">
|
|
404
|
+
{{ events.length }}{{ events.length === MAX_TIMELINE_EVENTS ? "+" : "" }}
|
|
405
|
+
</span>
|
|
406
|
+
<button
|
|
407
|
+
v-if="events.length"
|
|
408
|
+
type="button"
|
|
409
|
+
class="atf-clear-btn"
|
|
410
|
+
@click="clearTimeline"
|
|
411
|
+
>
|
|
412
|
+
clear
|
|
413
|
+
</button>
|
|
414
|
+
</h2>
|
|
415
|
+
<div class="atf-section-body">
|
|
416
|
+
<p v-if="events.length === 0" class="atf-empty-list">
|
|
417
|
+
No events yet. Type into an input, submit, or call <code>reset()</code> to see
|
|
418
|
+
entries appear here.
|
|
419
|
+
</p>
|
|
420
|
+
<ul v-else class="atf-timeline">
|
|
421
|
+
<li
|
|
422
|
+
v-for="event in events"
|
|
423
|
+
:key="event.id"
|
|
424
|
+
class="atf-timeline-entry"
|
|
425
|
+
:class="[
|
|
426
|
+
`atf-timeline-${event.type.split('.')[0]}`,
|
|
427
|
+
{ expanded: expandedEventId === event.id }
|
|
428
|
+
]"
|
|
429
|
+
>
|
|
430
|
+
<div class="atf-timeline-row" @click="toggleEvent(event.id)">
|
|
431
|
+
<span class="atf-timeline-time">{{ formatTime(event.time) }}</span>
|
|
432
|
+
<span class="atf-timeline-type">{{ event.type }}</span>
|
|
433
|
+
<span class="atf-timeline-form">{{ event.formKey }}</span>
|
|
434
|
+
<span class="atf-timeline-caret">
|
|
435
|
+
{{ expandedEventId === event.id ? "\u2212" : "+" }}
|
|
436
|
+
</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div v-if="expandedEventId === event.id" class="atf-timeline-detail">
|
|
439
|
+
<DevtoolsValueTree :value="event.value" />
|
|
440
|
+
</div>
|
|
441
|
+
</li>
|
|
442
|
+
</ul>
|
|
443
|
+
</div>
|
|
444
|
+
</section>
|
|
445
|
+
</template>
|
|
446
|
+
</main>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</template>
|
|
450
|
+
|
|
451
|
+
<style scoped>
|
|
452
|
+
.atf-panel{--atf-bg:#0f172a;--atf-bg-elev:#111c33;--atf-fg:#e2e8f0;--atf-fg-muted:#94a3b8;--atf-border:rgba(148,163,184,.12);--atf-border-strong:rgba(148,163,184,.2);--atf-accent:#5b8def;--atf-key:#93c5fd;--atf-string:#86efac;--atf-number:#fbbf24;--atf-boolean:#f472b6;--atf-redacted:#f87171;--atf-muted:#64748b;--atf-row-hover:hsla(0,0%,100%,.04);--atf-error-bg:rgba(248,113,113,.1);--atf-warn-bg:rgba(251,191,36,.1);background:var(--atf-bg);color:var(--atf-fg);display:flex;flex-direction:column;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;height:100vh;line-height:1.5}@media (prefers-color-scheme:light){.atf-panel{--atf-bg:#fff;--atf-bg-elev:#f8fafc;--atf-fg:#0f172a;--atf-fg-muted:#64748b;--atf-border:rgba(15,23,42,.08);--atf-border-strong:rgba(15,23,42,.16);--atf-key:#2563eb;--atf-string:#16a34a;--atf-number:#d97706;--atf-boolean:#db2777;--atf-redacted:#dc2626;--atf-muted:#94a3b8;--atf-row-hover:rgba(15,23,42,.04);--atf-error-bg:rgba(220,38,38,.08);--atf-warn-bg:rgba(217,119,6,.08)}}.atf-header{background:var(--atf-bg-elev);border-bottom:1px solid var(--atf-border);flex:0 0 auto;padding:.75rem 1rem}.atf-brand{align-items:center;display:flex;gap:.5rem}.atf-logo{display:block;height:22px;width:22px}.atf-title{font-size:14px;font-weight:600}.atf-version{color:var(--atf-fg-muted);font-family:ui-monospace,monospace;font-size:11px}.atf-body{display:grid;flex:1 1 auto;grid-template-columns:200px 1fr;min-height:0}.atf-sidebar{border-right:1px solid var(--atf-border);overflow-y:auto;padding:.75rem 0}.atf-sidebar-title{align-items:center;color:var(--atf-fg-muted);display:flex;font-size:11px;gap:.4em;letter-spacing:.05em;padding:0 1rem .5rem;text-transform:uppercase}.atf-count{background:var(--atf-border-strong);border-radius:999px;color:var(--atf-fg);font-size:10px;padding:0 .4em}.atf-empty{color:var(--atf-fg-muted);list-style:none;margin:0;padding:0 1rem}.atf-empty small{display:block;font-size:11px;margin-top:.4rem}.atf-empty code{background:var(--atf-border);border-radius:3px;font-size:11px;padding:.05em .35em}.atf-form-list{list-style:none;margin:0;padding:0}.atf-form-item{cursor:pointer;font-family:ui-monospace,monospace;font-size:12px;padding:.4rem 1rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.atf-form-item:hover{background:var(--atf-row-hover)}.atf-form-item.active{background:rgba(91,141,239,.12);border-left:2px solid var(--atf-accent);color:var(--atf-key);padding-left:calc(1rem - 2px)}.atf-detail{overflow-y:auto;padding:1rem 1.25rem}.atf-empty-detail{color:var(--atf-fg-muted);padding:3rem 0;text-align:center}.atf-section{margin-bottom:1.25rem}.atf-section-title{align-items:center;color:var(--atf-fg-muted);display:flex;font-size:11px;font-weight:600;gap:.5em;letter-spacing:.06em;margin:0 0 .5rem;text-transform:uppercase}.atf-section-body{background:var(--atf-bg-elev);border:1px solid var(--atf-border);border-radius:6px;padding:.6rem .8rem}.atf-badge{border-radius:999px;font-size:10px;font-weight:600;letter-spacing:.04em;padding:0 .45em}.atf-badge-error{background:var(--atf-error-bg);color:var(--atf-redacted)}.atf-badge-warn{background:var(--atf-warn-bg);color:var(--atf-number)}.atf-empty-list{color:var(--atf-fg-muted);font-size:12px;margin:0}.atf-error-list{list-style:none;margin:0;padding:0}.atf-error-list>li+li{border-top:1px solid var(--atf-border);margin-top:.6rem;padding-top:.6rem}.atf-path{color:var(--atf-key);display:block;font-family:ui-monospace,monospace;font-size:12px;margin-bottom:.25rem}.atf-error-messages{color:var(--atf-redacted);font-size:12px;list-style:none;margin:0;padding:0}.atf-error-messages>li+li{margin-top:.2rem}.atf-aggregates{display:grid;font-family:ui-monospace,monospace;font-size:12px;gap:.35rem .75rem;grid-template-columns:max-content 1fr;margin:0}.atf-aggregates dt{color:var(--atf-key)}.atf-aggregates dd{color:var(--atf-fg);margin:0}.atf-badge-neutral{background:var(--atf-border-strong);color:var(--atf-fg)}.atf-clear-btn{background:transparent;border:1px solid var(--atf-border);border-radius:4px;cursor:pointer;font:inherit;font-size:11px;padding:.1rem .5rem}.atf-clear-btn,.atf-section-hint{color:var(--atf-fg-muted);margin-left:auto}.atf-section-hint{font-size:10px;font-weight:400;letter-spacing:0;text-transform:none}.atf-fg-muted{color:var(--atf-fg-muted)}.atf-clear-btn:hover{border-color:var(--atf-border-strong);color:var(--atf-fg)}.atf-timeline{list-style:none;margin:0;max-height:18rem;overflow-y:auto;padding:0}.atf-timeline-entry{border-top:1px solid var(--atf-border)}.atf-timeline-entry:first-child{border-top:0}.atf-timeline-row{align-items:baseline;cursor:pointer;display:grid;font-family:ui-monospace,monospace;font-size:11px;gap:.6rem;grid-template-columns:7.5rem 8rem 1fr auto;padding:.4rem 0}.atf-timeline-row:hover{background:var(--atf-row-hover)}.atf-timeline-time{color:var(--atf-fg-muted)}.atf-timeline-type{color:var(--atf-key);font-weight:600}.atf-timeline-form{color:var(--atf-fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.atf-timeline-caret{color:var(--atf-fg-muted);text-align:center;width:1em}.atf-timeline-entry.atf-timeline-submit .atf-timeline-type{color:var(--atf-string)}.atf-timeline-entry.atf-timeline-reset .atf-timeline-type{color:var(--atf-redacted)}.atf-timeline-detail{border-top:1px dashed var(--atf-border);margin-top:-1px;padding:.4rem 0 .6rem 7.5rem}
|
|
453
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AttaformDevtoolsBridge } from 'attaform';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
bridge: AttaformDevtoolsBridge;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
value: unknown;
|
|
3
|
+
label?: string;
|
|
4
|
+
depth?: number;
|
|
5
|
+
/**
|
|
6
|
+
* Path-from-root for this node. Used by edit-aware mounts so the
|
|
7
|
+
* commit handler knows which leaf the user just touched. Default
|
|
8
|
+
* `[]` for non-editable mounts; the panel passes the explicit path
|
|
9
|
+
* when it wires up `onEdit`.
|
|
10
|
+
*/
|
|
11
|
+
path?: ReadonlyArray<string | number>;
|
|
12
|
+
/**
|
|
13
|
+
* Edit-mode toggle. When `true` and `onEdit` is wired, leaf cells
|
|
14
|
+
* become click-to-edit. Sensitive (redacted) leaves stay read-only
|
|
15
|
+
* regardless — overwriting with the literal `[redacted]` string
|
|
16
|
+
* would destroy the real value.
|
|
17
|
+
*/
|
|
18
|
+
editable?: boolean | undefined;
|
|
19
|
+
onEdit?: ((path: ReadonlyArray<string | number>, next: unknown) => void) | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Canonical JSON-array key of the currently-selected path (`null`
|
|
22
|
+
* for no selection). Compared against this node's own key so the
|
|
23
|
+
* key label can render in a highlighted state. Plumbed as a key
|
|
24
|
+
* rather than a path array because string equality is cheap and
|
|
25
|
+
* cross-renders consistently.
|
|
26
|
+
*/
|
|
27
|
+
selectedKey?: string | null | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Click handler for the key label. Called with the node's current
|
|
30
|
+
* path-from-root; the panel toggles selection on identical paths.
|
|
31
|
+
* When omitted, the key label is plain text (no selection UX).
|
|
32
|
+
*/
|
|
33
|
+
onSelectPath?: ((path: ReadonlyArray<string | number>) => void) | undefined;
|
|
34
|
+
};
|
|
35
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
36
|
+
declare const _default: typeof __VLS_export;
|
|
37
|
+
export default _default;
|