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,226 @@
1
+ // ============================================================================
2
+ // @kayforms/devtools — Floating Debug Panel
3
+ // ============================================================================
4
+ // Zero-dependency, framework-agnostic floating panel that provides:
5
+ // - Timeline of all form mutations
6
+ // - State inspector (tree view)
7
+ // - Time-travel scrubber with undo/redo
8
+ // - Minimizable to a floating orb
9
+ // - Draggable positioning
10
+ // ============================================================================
11
+
12
+ import { createEffect, type DevToolsBridge, type FormStore } from "@kayforms/core";
13
+ import { DEVTOOLS_STYLES } from "./styles";
14
+ import { createTimeline, createInspector } from "./timeline";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface PanelOptions {
21
+ /** Initial position (default: bottom-right) */
22
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
23
+ /** Start minimized (default: false) */
24
+ minimized?: boolean;
25
+ /** Initial active tab */
26
+ activeTab?: "timeline" | "state";
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Panel
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export function createPanel(
34
+ bridge: DevToolsBridge,
35
+ forms: FormStore[],
36
+ options: PanelOptions = {}
37
+ ): { destroy: () => void; toggle: () => void } {
38
+ const { minimized = false, activeTab = "timeline" } = options;
39
+
40
+ // --- Inject styles ---
41
+ const styleEl = document.createElement("style");
42
+ styleEl.textContent = DEVTOOLS_STYLES;
43
+ document.head.appendChild(styleEl);
44
+
45
+ // --- Root container ---
46
+ const root = document.createElement("div");
47
+ root.className = `kayform-devtools${minimized ? " kf-minimized" : ""}`;
48
+ root.setAttribute("data-kayform-devtools", "");
49
+
50
+ // --- Minimized state ---
51
+ let isMinimized = minimized;
52
+ let currentTab = activeTab;
53
+
54
+ // --- Header ---
55
+ const header = document.createElement("div");
56
+ header.className = "kf-header";
57
+
58
+ const title = document.createElement("div");
59
+ title.className = "kf-header-title";
60
+
61
+ const logo = document.createElement("div");
62
+ logo.className = "kf-header-logo";
63
+ logo.textContent = "K";
64
+
65
+ const titleText = document.createElement("span");
66
+ titleText.textContent = "Kayforms DevTools";
67
+
68
+ title.append(logo, titleText);
69
+
70
+ const actions = document.createElement("div");
71
+ actions.className = "kf-header-actions";
72
+
73
+ const minimizeBtn = document.createElement("button");
74
+ minimizeBtn.textContent = "−";
75
+ minimizeBtn.title = "Minimize";
76
+ minimizeBtn.onclick = () => toggle();
77
+
78
+ const closeBtn = document.createElement("button");
79
+ closeBtn.textContent = "×";
80
+ closeBtn.title = "Close";
81
+ closeBtn.onclick = () => destroy();
82
+
83
+ actions.append(minimizeBtn, closeBtn);
84
+ header.append(title, actions);
85
+ root.appendChild(header);
86
+
87
+ // --- Body ---
88
+ const body = document.createElement("div");
89
+ body.className = "kf-body";
90
+
91
+ // Tabs
92
+ const tabs = document.createElement("div");
93
+ tabs.className = "kf-tabs";
94
+
95
+ const timelineTab = document.createElement("button");
96
+ timelineTab.className = `kf-tab${currentTab === "timeline" ? " kf-active" : ""}`;
97
+ timelineTab.textContent = "Timeline";
98
+
99
+ const stateTab = document.createElement("button");
100
+ stateTab.className = `kf-tab${currentTab === "state" ? " kf-active" : ""}`;
101
+ stateTab.textContent = "State";
102
+
103
+ tabs.append(timelineTab, stateTab);
104
+ body.appendChild(tabs);
105
+
106
+ // Tab content containers
107
+ const timelineContainer = document.createElement("div");
108
+ timelineContainer.style.cssText = `flex:1;overflow:hidden;display:flex;flex-direction:column;${
109
+ currentTab !== "timeline" ? "display:none;" : ""
110
+ }`;
111
+
112
+ const stateContainer = document.createElement("div");
113
+ stateContainer.style.cssText = `flex:1;overflow:hidden;display:flex;flex-direction:column;${
114
+ currentTab !== "state" ? "display:none;" : ""
115
+ }`;
116
+
117
+ body.append(timelineContainer, stateContainer);
118
+
119
+ // Wire up tabs
120
+ timelineTab.onclick = () => switchTab("timeline");
121
+ stateTab.onclick = () => switchTab("state");
122
+
123
+ function switchTab(tab: "timeline" | "state"): void {
124
+ currentTab = tab;
125
+ timelineTab.className = `kf-tab${tab === "timeline" ? " kf-active" : ""}`;
126
+ stateTab.className = `kf-tab${tab === "state" ? " kf-active" : ""}`;
127
+ timelineContainer.style.display = tab === "timeline" ? "flex" : "none";
128
+ stateContainer.style.display = tab === "state" ? "flex" : "none";
129
+ }
130
+
131
+ // --- Status bar ---
132
+ const statusBar = document.createElement("div");
133
+ statusBar.className = "kf-status";
134
+ body.appendChild(statusBar);
135
+
136
+ root.appendChild(body);
137
+
138
+ // --- Mount timeline + inspector ---
139
+ const cleanupTimeline = createTimeline(timelineContainer, bridge);
140
+ const cleanupInspector = createInspector(stateContainer, bridge);
141
+
142
+ // --- Status bar reactive updates ---
143
+ const cleanups: (() => void)[] = [cleanupTimeline, cleanupInspector];
144
+
145
+ if (forms.length > 0) {
146
+ const disposeStatus = createEffect(() => {
147
+ const statusParts: string[] = [];
148
+
149
+ for (const form of forms) {
150
+ const id = form.id ?? "unnamed";
151
+ const isValid = form.valid.value;
152
+ const isDirty = form.dirty.value;
153
+
154
+ const dot = document.createElement("span");
155
+ dot.className = `kf-status-dot ${isValid ? "kf-valid" : "kf-invalid"}`;
156
+
157
+ statusParts.push(
158
+ `<span class="kf-status-dot ${isValid ? "kf-valid" : "kf-invalid"}"></span>${id}` +
159
+ `${isDirty ? ' <span class="kf-status-dot kf-dirty"></span>dirty' : ""}`
160
+ );
161
+ }
162
+
163
+ statusBar.innerHTML = statusParts.join(" · ");
164
+ });
165
+ cleanups.push(disposeStatus);
166
+ }
167
+
168
+ // --- Dragging ---
169
+ let isDragging = false;
170
+ let dragStartX = 0;
171
+ let dragStartY = 0;
172
+ let panelStartX = 0;
173
+ let panelStartY = 0;
174
+
175
+ header.addEventListener("mousedown", (e: MouseEvent) => {
176
+ if (isMinimized) return;
177
+ isDragging = true;
178
+ dragStartX = e.clientX;
179
+ dragStartY = e.clientY;
180
+ const rect = root.getBoundingClientRect();
181
+ panelStartX = rect.left;
182
+ panelStartY = rect.top;
183
+ e.preventDefault();
184
+ });
185
+
186
+ document.addEventListener("mousemove", (e: MouseEvent) => {
187
+ if (!isDragging) return;
188
+ const dx = e.clientX - dragStartX;
189
+ const dy = e.clientY - dragStartY;
190
+ root.style.left = `${panelStartX + dx}px`;
191
+ root.style.top = `${panelStartY + dy}px`;
192
+ root.style.right = "auto";
193
+ root.style.bottom = "auto";
194
+ });
195
+
196
+ document.addEventListener("mouseup", () => {
197
+ isDragging = false;
198
+ });
199
+
200
+ // --- Toggle minimize ---
201
+ function toggle(): void {
202
+ isMinimized = !isMinimized;
203
+ root.className = `kayform-devtools${isMinimized ? " kf-minimized" : ""}`;
204
+ }
205
+
206
+ // Click to expand when minimized
207
+ root.addEventListener("click", (e: Event) => {
208
+ if (isMinimized && e.target === root) {
209
+ toggle();
210
+ }
211
+ });
212
+
213
+ // --- Mount to DOM ---
214
+ document.body.appendChild(root);
215
+
216
+ // --- Destroy ---
217
+ function destroy(): void {
218
+ for (const cleanup of cleanups) {
219
+ cleanup();
220
+ }
221
+ root.remove();
222
+ styleEl.remove();
223
+ }
224
+
225
+ return { destroy, toggle };
226
+ }
@@ -0,0 +1,422 @@
1
+ // ============================================================================
2
+ // @kayforms/devtools — Injected CSS Styles
3
+ // ============================================================================
4
+ // Zero-dependency styles for the floating debug panel. Injected as a
5
+ // <style> tag so there are no external CSS dependencies.
6
+ // ============================================================================
7
+
8
+ export const DEVTOOLS_STYLES = `
9
+ .kayform-devtools {
10
+ --kf-bg: #1a1a2e;
11
+ --kf-bg-secondary: #16213e;
12
+ --kf-bg-hover: #0f3460;
13
+ --kf-text: #e6e6e6;
14
+ --kf-text-muted: #8899aa;
15
+ --kf-accent: #00d2ff;
16
+ --kf-accent-gradient: linear-gradient(135deg, #00d2ff 0%, #7b2ff7 100%);
17
+ --kf-error: #ff6b6b;
18
+ --kf-success: #51cf66;
19
+ --kf-warning: #ffd43b;
20
+ --kf-border: rgba(255, 255, 255, 0.08);
21
+ --kf-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
22
+ --kf-radius: 12px;
23
+ --kf-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24
+ --kf-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
25
+
26
+ position: fixed;
27
+ bottom: 16px;
28
+ right: 16px;
29
+ width: 420px;
30
+ max-height: 560px;
31
+ background: var(--kf-bg);
32
+ border: 1px solid var(--kf-border);
33
+ border-radius: var(--kf-radius);
34
+ box-shadow: var(--kf-shadow);
35
+ font-family: var(--kf-font);
36
+ font-size: 12px;
37
+ color: var(--kf-text);
38
+ z-index: 99999;
39
+ display: flex;
40
+ flex-direction: column;
41
+ overflow: hidden;
42
+ backdrop-filter: blur(20px);
43
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
44
+ animation: kf-slide-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
45
+ }
46
+
47
+ @keyframes kf-slide-in {
48
+ from {
49
+ opacity: 0;
50
+ transform: translateY(20px) scale(0.95);
51
+ }
52
+ to {
53
+ opacity: 1;
54
+ transform: translateY(0) scale(1);
55
+ }
56
+ }
57
+
58
+ .kayform-devtools.kf-minimized {
59
+ width: 48px;
60
+ height: 48px;
61
+ max-height: 48px;
62
+ border-radius: 50%;
63
+ cursor: pointer;
64
+ background: var(--kf-accent-gradient);
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ box-sizing: border-box;
69
+ padding: 0;
70
+ }
71
+
72
+ .kayform-devtools.kf-minimized:hover {
73
+ transform: scale(1.1);
74
+ box-shadow: 0 0 20px rgba(0, 210, 255, 0.4);
75
+ }
76
+
77
+ .kayform-devtools.kf-minimized .kf-header {
78
+ background: none;
79
+ border: none;
80
+ padding: 0;
81
+ width: 100%;
82
+ height: 100%;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ }
87
+
88
+ .kayform-devtools.kf-minimized .kf-header-title {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ }
93
+
94
+ .kayform-devtools.kf-minimized .kf-header-logo {
95
+ background: none;
96
+ color: white;
97
+ font-size: 16px;
98
+ font-weight: bold;
99
+ width: auto;
100
+ height: auto;
101
+ -webkit-text-fill-color: white;
102
+ }
103
+
104
+ .kayform-devtools.kf-minimized .kf-header-title span,
105
+ .kayform-devtools.kf-minimized .kf-body,
106
+ .kayform-devtools.kf-minimized .kf-header-actions {
107
+ display: none;
108
+ }
109
+
110
+ /* --- Header --- */
111
+ .kf-header {
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: space-between;
115
+ padding: 10px 14px;
116
+ background: var(--kf-bg-secondary);
117
+ border-bottom: 1px solid var(--kf-border);
118
+ user-select: none;
119
+ cursor: grab;
120
+ }
121
+
122
+ .kf-header:active {
123
+ cursor: grabbing;
124
+ }
125
+
126
+ .kf-header-title {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 8px;
130
+ font-weight: 600;
131
+ font-size: 13px;
132
+ background: var(--kf-accent-gradient);
133
+ -webkit-background-clip: text;
134
+ -webkit-text-fill-color: transparent;
135
+ background-clip: text;
136
+ }
137
+
138
+ .kf-header-logo {
139
+ width: 18px;
140
+ height: 18px;
141
+ background: var(--kf-accent-gradient);
142
+ border-radius: 4px;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ font-size: 10px;
147
+ -webkit-text-fill-color: white;
148
+ }
149
+
150
+ .kf-header-actions {
151
+ display: flex;
152
+ gap: 4px;
153
+ }
154
+
155
+ .kf-header-actions button {
156
+ background: none;
157
+ border: none;
158
+ color: var(--kf-text-muted);
159
+ cursor: pointer;
160
+ padding: 4px 6px;
161
+ border-radius: 4px;
162
+ font-size: 14px;
163
+ transition: all 0.15s;
164
+ }
165
+
166
+ .kf-header-actions button:hover {
167
+ background: var(--kf-bg-hover);
168
+ color: var(--kf-text);
169
+ }
170
+
171
+ /* --- Body --- */
172
+ .kf-body {
173
+ flex: 1;
174
+ overflow: hidden;
175
+ display: flex;
176
+ flex-direction: column;
177
+ }
178
+
179
+ /* --- Tabs --- */
180
+ .kf-tabs {
181
+ display: flex;
182
+ border-bottom: 1px solid var(--kf-border);
183
+ padding: 0 8px;
184
+ }
185
+
186
+ .kf-tab {
187
+ padding: 8px 12px;
188
+ background: none;
189
+ border: none;
190
+ color: var(--kf-text-muted);
191
+ cursor: pointer;
192
+ font-size: 11px;
193
+ font-weight: 500;
194
+ text-transform: uppercase;
195
+ letter-spacing: 0.5px;
196
+ border-bottom: 2px solid transparent;
197
+ transition: all 0.15s;
198
+ }
199
+
200
+ .kf-tab:hover {
201
+ color: var(--kf-text);
202
+ }
203
+
204
+ .kf-tab.kf-active {
205
+ color: var(--kf-accent);
206
+ border-bottom-color: var(--kf-accent);
207
+ }
208
+
209
+ /* --- Timeline Panel --- */
210
+ .kf-timeline {
211
+ flex: 1;
212
+ overflow-y: auto;
213
+ padding: 8px;
214
+ }
215
+
216
+ .kf-timeline::-webkit-scrollbar {
217
+ width: 4px;
218
+ }
219
+
220
+ .kf-timeline::-webkit-scrollbar-thumb {
221
+ background: var(--kf-border);
222
+ border-radius: 2px;
223
+ }
224
+
225
+ .kf-timeline-entry {
226
+ display: flex;
227
+ align-items: flex-start;
228
+ gap: 8px;
229
+ padding: 6px 8px;
230
+ border-radius: 6px;
231
+ cursor: pointer;
232
+ transition: background 0.15s;
233
+ position: relative;
234
+ }
235
+
236
+ .kf-timeline-entry:hover {
237
+ background: var(--kf-bg-hover);
238
+ }
239
+
240
+ .kf-timeline-entry.kf-current {
241
+ background: rgba(0, 210, 255, 0.1);
242
+ border-left: 2px solid var(--kf-accent);
243
+ }
244
+
245
+ .kf-entry-dot {
246
+ width: 8px;
247
+ height: 8px;
248
+ border-radius: 50%;
249
+ background: var(--kf-accent);
250
+ margin-top: 4px;
251
+ flex-shrink: 0;
252
+ }
253
+
254
+ .kf-entry-dot.kf-action-SET_VALUE { background: var(--kf-accent); }
255
+ .kf-entry-dot.kf-action-SET_TOUCHED { background: var(--kf-warning); }
256
+ .kf-entry-dot.kf-action-VALIDATE { background: #9775fa; }
257
+ .kf-entry-dot.kf-action-SUBMIT { background: var(--kf-success); }
258
+ .kf-entry-dot.kf-action-RESET { background: var(--kf-error); }
259
+ .kf-entry-dot.kf-action-INIT { background: #868e96; }
260
+
261
+ .kf-entry-content {
262
+ flex: 1;
263
+ min-width: 0;
264
+ }
265
+
266
+ .kf-entry-action {
267
+ font-weight: 600;
268
+ font-family: var(--kf-mono);
269
+ font-size: 11px;
270
+ }
271
+
272
+ .kf-entry-path {
273
+ color: var(--kf-accent);
274
+ font-family: var(--kf-mono);
275
+ font-size: 10px;
276
+ margin-left: 6px;
277
+ }
278
+
279
+ .kf-entry-values {
280
+ display: flex;
281
+ gap: 6px;
282
+ margin-top: 2px;
283
+ font-family: var(--kf-mono);
284
+ font-size: 10px;
285
+ }
286
+
287
+ .kf-entry-prev {
288
+ color: var(--kf-error);
289
+ text-decoration: line-through;
290
+ opacity: 0.6;
291
+ }
292
+
293
+ .kf-entry-next {
294
+ color: var(--kf-success);
295
+ }
296
+
297
+ .kf-entry-time {
298
+ color: var(--kf-text-muted);
299
+ font-size: 9px;
300
+ font-family: var(--kf-mono);
301
+ margin-top: 2px;
302
+ }
303
+
304
+ /* --- Scrubber --- */
305
+ .kf-scrubber {
306
+ padding: 8px 14px;
307
+ border-top: 1px solid var(--kf-border);
308
+ display: flex;
309
+ align-items: center;
310
+ gap: 8px;
311
+ }
312
+
313
+ .kf-scrubber input[type="range"] {
314
+ flex: 1;
315
+ -webkit-appearance: none;
316
+ height: 4px;
317
+ background: var(--kf-bg-hover);
318
+ border-radius: 2px;
319
+ outline: none;
320
+ }
321
+
322
+ .kf-scrubber input[type="range"]::-webkit-slider-thumb {
323
+ -webkit-appearance: none;
324
+ width: 14px;
325
+ height: 14px;
326
+ border-radius: 50%;
327
+ background: var(--kf-accent-gradient);
328
+ cursor: pointer;
329
+ box-shadow: 0 0 8px rgba(0, 210, 255, 0.4);
330
+ transition: transform 0.15s;
331
+ }
332
+
333
+ .kf-scrubber input[type="range"]::-webkit-slider-thumb:hover {
334
+ transform: scale(1.2);
335
+ }
336
+
337
+ .kf-scrubber-controls {
338
+ display: flex;
339
+ gap: 2px;
340
+ }
341
+
342
+ .kf-scrubber-controls button {
343
+ background: none;
344
+ border: none;
345
+ color: var(--kf-text-muted);
346
+ cursor: pointer;
347
+ padding: 4px;
348
+ border-radius: 4px;
349
+ font-size: 12px;
350
+ transition: all 0.15s;
351
+ }
352
+
353
+ .kf-scrubber-controls button:hover {
354
+ background: var(--kf-bg-hover);
355
+ color: var(--kf-accent);
356
+ }
357
+
358
+ .kf-scrubber-count {
359
+ font-family: var(--kf-mono);
360
+ font-size: 10px;
361
+ color: var(--kf-text-muted);
362
+ min-width: 40px;
363
+ text-align: center;
364
+ }
365
+
366
+ /* --- State Inspector --- */
367
+ .kf-inspector {
368
+ flex: 1;
369
+ overflow-y: auto;
370
+ padding: 8px;
371
+ }
372
+
373
+ .kf-tree-node {
374
+ padding: 2px 0 2px 16px;
375
+ position: relative;
376
+ }
377
+
378
+ .kf-tree-key {
379
+ color: #9775fa;
380
+ font-family: var(--kf-mono);
381
+ font-size: 11px;
382
+ }
383
+
384
+ .kf-tree-value {
385
+ color: var(--kf-success);
386
+ font-family: var(--kf-mono);
387
+ font-size: 11px;
388
+ margin-left: 4px;
389
+ }
390
+
391
+ .kf-tree-value.kf-error-value {
392
+ color: var(--kf-error);
393
+ }
394
+
395
+ .kf-tree-bracket {
396
+ color: var(--kf-text-muted);
397
+ font-family: var(--kf-mono);
398
+ }
399
+
400
+ /* --- Status Bar --- */
401
+ .kf-status {
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: space-between;
405
+ padding: 6px 14px;
406
+ border-top: 1px solid var(--kf-border);
407
+ font-size: 10px;
408
+ color: var(--kf-text-muted);
409
+ }
410
+
411
+ .kf-status-dot {
412
+ width: 6px;
413
+ height: 6px;
414
+ border-radius: 50%;
415
+ display: inline-block;
416
+ margin-right: 4px;
417
+ }
418
+
419
+ .kf-status-dot.kf-valid { background: var(--kf-success); }
420
+ .kf-status-dot.kf-invalid { background: var(--kf-error); }
421
+ .kf-status-dot.kf-dirty { background: var(--kf-warning); }
422
+ `;