attaform 0.17.2 → 0.18.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 (115) hide show
  1. package/README.md +77 -36
  2. package/dist/chunks/devtools.cjs +10 -37
  3. package/dist/chunks/devtools.cjs.map +1 -1
  4. package/dist/chunks/devtools.mjs +10 -37
  5. package/dist/chunks/devtools.mjs.map +1 -1
  6. package/dist/chunks/indexeddb.cjs +4 -4
  7. package/dist/chunks/indexeddb.cjs.map +1 -1
  8. package/dist/chunks/indexeddb.mjs +1 -1
  9. package/dist/chunks/local-storage.cjs +2 -2
  10. package/dist/chunks/local-storage.cjs.map +1 -1
  11. package/dist/chunks/local-storage.mjs +1 -1
  12. package/dist/chunks/session-storage.cjs +2 -2
  13. package/dist/chunks/session-storage.cjs.map +1 -1
  14. package/dist/chunks/session-storage.mjs +1 -1
  15. package/dist/index.cjs +42 -37
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +159 -196
  18. package/dist/index.d.mts +159 -196
  19. package/dist/index.d.ts +159 -196
  20. package/dist/index.mjs +5 -7
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/nuxt.cjs +31 -40
  23. package/dist/nuxt.cjs.map +1 -1
  24. package/dist/nuxt.d.cts +8 -1
  25. package/dist/nuxt.d.mts +8 -1
  26. package/dist/nuxt.d.ts +8 -1
  27. package/dist/nuxt.mjs +32 -41
  28. package/dist/nuxt.mjs.map +1 -1
  29. package/dist/runtime/components/AttaformDevtoolsPanel.d.vue.ts +7 -0
  30. package/dist/runtime/components/AttaformDevtoolsPanel.vue +453 -0
  31. package/dist/runtime/components/AttaformDevtoolsPanel.vue.d.ts +7 -0
  32. package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +37 -0
  33. package/dist/runtime/components/DevtoolsValueTree.vue +192 -0
  34. package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +37 -0
  35. package/dist/runtime/plugins/attaform.cjs +17 -6
  36. package/dist/runtime/plugins/attaform.cjs.map +1 -1
  37. package/dist/runtime/plugins/attaform.mjs +15 -4
  38. package/dist/runtime/plugins/attaform.mjs.map +1 -1
  39. package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.2b7M2mww.d.mts} +57 -23
  40. package/dist/shared/attaform.5UhpSVFI.cjs +63 -0
  41. package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
  42. package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
  43. package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
  44. package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
  45. package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
  46. package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
  47. package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
  48. package/dist/shared/{attaform.Drt6fivF.mjs → attaform.CtNUB9nf.mjs} +74 -76
  49. package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
  50. package/dist/shared/{attaform.C5MH4lNh.d.mts → attaform.DF8wo-ry.d.ts} +4 -4
  51. package/dist/shared/attaform.DK9aj0N8.d.ts +1651 -0
  52. package/dist/shared/{attaform.BPRHR3Zs.cjs → attaform.DUHru0OF.cjs} +83 -85
  53. package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
  54. package/dist/shared/{attaform.C6lbmMUe.d.ts → attaform.DVLB6CAn.d.mts} +4 -4
  55. package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.Dj9mwbaV.d.cts} +756 -148
  56. package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.Dj9mwbaV.d.mts} +756 -148
  57. package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.Dj9mwbaV.d.ts} +756 -148
  58. package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
  59. package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
  60. package/dist/shared/attaform.DoSuaKMd.d.cts +1651 -0
  61. package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
  62. package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
  63. package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
  64. package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
  65. package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.M33WKVV4.d.cts} +57 -23
  66. package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
  67. package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
  68. package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
  69. package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
  70. package/dist/shared/attaform.iTqxvl-P.d.mts +1651 -0
  71. package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.tsNFcEW7.d.ts} +57 -23
  72. package/dist/shared/{attaform.DtMN-MAm.d.cts → attaform.tts_OM7j.d.cts} +4 -4
  73. package/dist/vite.cjs +288 -2
  74. package/dist/vite.cjs.map +1 -1
  75. package/dist/vite.mjs +288 -2
  76. package/dist/vite.mjs.map +1 -1
  77. package/dist/zod-v3.cjs +11 -8
  78. package/dist/zod-v3.cjs.map +1 -1
  79. package/dist/zod-v3.d.cts +6 -6
  80. package/dist/zod-v3.d.mts +6 -6
  81. package/dist/zod-v3.d.ts +6 -6
  82. package/dist/zod-v3.mjs +3 -3
  83. package/dist/zod-v4.cjs +11 -8
  84. package/dist/zod-v4.cjs.map +1 -1
  85. package/dist/zod-v4.d.cts +5 -5
  86. package/dist/zod-v4.d.mts +5 -5
  87. package/dist/zod-v4.d.ts +5 -5
  88. package/dist/zod-v4.mjs +3 -3
  89. package/dist/zod.cjs +15 -16
  90. package/dist/zod.cjs.map +1 -1
  91. package/dist/zod.d.cts +127 -40
  92. package/dist/zod.d.mts +127 -40
  93. package/dist/zod.d.ts +127 -40
  94. package/dist/zod.mjs +7 -11
  95. package/dist/zod.mjs.map +1 -1
  96. package/package.json +18 -7
  97. package/dist/shared/attaform.B1jvxsOF.d.mts +0 -156
  98. package/dist/shared/attaform.B3ZaPIzS.mjs.map +0 -1
  99. package/dist/shared/attaform.BBM2muQ9.cjs +0 -101
  100. package/dist/shared/attaform.BBM2muQ9.cjs.map +0 -1
  101. package/dist/shared/attaform.BPRHR3Zs.cjs.map +0 -1
  102. package/dist/shared/attaform.BV40t5y2.cjs.map +0 -1
  103. package/dist/shared/attaform.C6qzEdIM.d.cts +0 -156
  104. package/dist/shared/attaform.C8LVFVVe.cjs +0 -32
  105. package/dist/shared/attaform.C8LVFVVe.cjs.map +0 -1
  106. package/dist/shared/attaform.CIEQgJnM.mjs.map +0 -1
  107. package/dist/shared/attaform.CTwNcpLE.d.ts +0 -156
  108. package/dist/shared/attaform.Cer8JO_P.cjs.map +0 -1
  109. package/dist/shared/attaform.CpERWz3u.mjs.map +0 -1
  110. package/dist/shared/attaform.Dee2rU1P.cjs.map +0 -1
  111. package/dist/shared/attaform.Drt6fivF.mjs.map +0 -1
  112. package/dist/shared/attaform.Vo-Kft0t.mjs +0 -29
  113. package/dist/shared/attaform.Vo-Kft0t.mjs.map +0 -1
  114. package/dist/shared/attaform.h1sq3BFu.mjs +0 -92
  115. 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;