dynim-core 1.0.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.
Files changed (86) hide show
  1. package/README.md +290 -0
  2. package/dist/builder/ai-prompt-popover.d.ts +26 -0
  3. package/dist/builder/ai-prompt-popover.d.ts.map +1 -0
  4. package/dist/builder/ai-prompt-popover.js +180 -0
  5. package/dist/builder/builder-client.d.ts +48 -0
  6. package/dist/builder/builder-client.d.ts.map +1 -0
  7. package/dist/builder/builder-client.js +157 -0
  8. package/dist/builder/builder.d.ts +41 -0
  9. package/dist/builder/builder.d.ts.map +1 -0
  10. package/dist/builder/builder.js +537 -0
  11. package/dist/builder/bundle-manager.d.ts +60 -0
  12. package/dist/builder/bundle-manager.d.ts.map +1 -0
  13. package/dist/builder/bundle-manager.js +357 -0
  14. package/dist/builder/classifier/classname-analyzer.d.ts +6 -0
  15. package/dist/builder/classifier/classname-analyzer.d.ts.map +1 -0
  16. package/dist/builder/classifier/classname-analyzer.js +107 -0
  17. package/dist/builder/classifier/index.d.ts +20 -0
  18. package/dist/builder/classifier/index.d.ts.map +1 -0
  19. package/dist/builder/classifier/index.js +181 -0
  20. package/dist/builder/classifier/semantic-analyzer.d.ts +24 -0
  21. package/dist/builder/classifier/semantic-analyzer.d.ts.map +1 -0
  22. package/dist/builder/classifier/semantic-analyzer.js +94 -0
  23. package/dist/builder/classifier/size-analyzer.d.ts +7 -0
  24. package/dist/builder/classifier/size-analyzer.d.ts.map +1 -0
  25. package/dist/builder/classifier/size-analyzer.js +120 -0
  26. package/dist/builder/classifier/visual-analyzer.d.ts +6 -0
  27. package/dist/builder/classifier/visual-analyzer.d.ts.map +1 -0
  28. package/dist/builder/classifier/visual-analyzer.js +158 -0
  29. package/dist/builder/client.d.ts +22 -0
  30. package/dist/builder/client.d.ts.map +1 -0
  31. package/dist/builder/client.js +54 -0
  32. package/dist/builder/code-client.d.ts +101 -0
  33. package/dist/builder/code-client.d.ts.map +1 -0
  34. package/dist/builder/code-client.js +418 -0
  35. package/dist/builder/diff-state.d.ts +24 -0
  36. package/dist/builder/diff-state.d.ts.map +1 -0
  37. package/dist/builder/diff-state.js +134 -0
  38. package/dist/builder/dom-scanner.d.ts +20 -0
  39. package/dist/builder/dom-scanner.d.ts.map +1 -0
  40. package/dist/builder/dom-scanner.js +102 -0
  41. package/dist/builder/drag-engine.d.ts +41 -0
  42. package/dist/builder/drag-engine.d.ts.map +1 -0
  43. package/dist/builder/drag-engine.js +686 -0
  44. package/dist/builder/editor-overlays.d.ts +31 -0
  45. package/dist/builder/editor-overlays.d.ts.map +1 -0
  46. package/dist/builder/editor-overlays.js +202 -0
  47. package/dist/builder/editor-state.d.ts +50 -0
  48. package/dist/builder/editor-state.d.ts.map +1 -0
  49. package/dist/builder/editor-state.js +132 -0
  50. package/dist/builder/element-utils.d.ts +43 -0
  51. package/dist/builder/element-utils.d.ts.map +1 -0
  52. package/dist/builder/element-utils.js +227 -0
  53. package/dist/builder/fiber-capture.d.ts +28 -0
  54. package/dist/builder/fiber-capture.d.ts.map +1 -0
  55. package/dist/builder/fiber-capture.js +264 -0
  56. package/dist/builder/freeze-overlay.d.ts +26 -0
  57. package/dist/builder/freeze-overlay.d.ts.map +1 -0
  58. package/dist/builder/freeze-overlay.js +213 -0
  59. package/dist/builder/history-state.d.ts +41 -0
  60. package/dist/builder/history-state.d.ts.map +1 -0
  61. package/dist/builder/history-state.js +76 -0
  62. package/dist/builder/index.d.ts +62 -0
  63. package/dist/builder/index.d.ts.map +1 -0
  64. package/dist/builder/index.js +92 -0
  65. package/dist/builder/state.d.ts +27 -0
  66. package/dist/builder/state.d.ts.map +1 -0
  67. package/dist/builder/state.js +50 -0
  68. package/dist/builder/style-applier.d.ts +61 -0
  69. package/dist/builder/style-applier.d.ts.map +1 -0
  70. package/dist/builder/style-applier.js +311 -0
  71. package/dist/builder/tree-state.d.ts +71 -0
  72. package/dist/builder/tree-state.d.ts.map +1 -0
  73. package/dist/builder/tree-state.js +168 -0
  74. package/dist/builder/widget.d.ts +29 -0
  75. package/dist/builder/widget.d.ts.map +1 -0
  76. package/dist/builder/widget.js +181 -0
  77. package/dist/index.d.ts +11 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +12 -0
  80. package/package.json +25 -0
  81. package/src/styles/base.css +378 -0
  82. package/src/styles/builder.css +422 -0
  83. package/src/styles/editor.css +131 -0
  84. package/src/styles/themes/dark.css +24 -0
  85. package/src/styles/themes/light.css +21 -0
  86. package/src/styles/variables.css +63 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Code Client - Handles communication with the flexcode endpoints via build server
3
+ *
4
+ * Uses:
5
+ * - HTTP POST + SSE for code generation (streaming responses)
6
+ * - HTTP POST for save/abandon operations
7
+ * - HTTP GET for retrieving edits
8
+ * - JWT session tokens for authentication
9
+ *
10
+ * The SDK only talks to the build server (:8080), which forwards
11
+ * code requests to the flexcode service (:8765).
12
+ */
13
+ const DEFAULT_API_BASE = 'http://localhost:8080';
14
+ /** Create initial empty message state */
15
+ function createEmptyMessage() {
16
+ return {
17
+ thinking: '',
18
+ text: '',
19
+ edits: [],
20
+ status: 'idle',
21
+ bundleReady: false,
22
+ bundleError: undefined,
23
+ };
24
+ }
25
+ export function createCodeClient(config = {}) {
26
+ const { apiBase = DEFAULT_API_BASE, sessionToken: initialSessionToken, refreshToken: initialRefreshToken, getSession, onMessageUpdate, onRawEvent, onMessage, // deprecated but still supported
27
+ onEdit, onError, onStreamStart, onStreamEnd, onSessionRefresh, onAuthError } = config;
28
+ let currentSessionToken = initialSessionToken;
29
+ let currentRefreshToken = initialRefreshToken;
30
+ let isRefreshing = false;
31
+ // Track if cache has been warmed (projectId comes from JWT, so just need one flag)
32
+ let hasWarmedCache = false;
33
+ // Current message state (accumulated)
34
+ let currentMessage = createEmptyMessage();
35
+ // Track if we need to add a line break before next text (after tool use)
36
+ let needsTextBreak = false;
37
+ /** Update message state and notify listeners */
38
+ function updateMessage(updates) {
39
+ currentMessage = { ...currentMessage, ...updates };
40
+ onMessageUpdate?.(currentMessage);
41
+ }
42
+ /** Append to thinking content */
43
+ function appendThinking(content) {
44
+ // If we already have text output and now Claude is thinking again,
45
+ // that means a new response segment is starting - mark for line break
46
+ if (currentMessage.text.length > 0) {
47
+ needsTextBreak = true;
48
+ }
49
+ currentMessage = {
50
+ ...currentMessage,
51
+ thinking: currentMessage.thinking + content
52
+ };
53
+ onMessageUpdate?.(currentMessage);
54
+ }
55
+ /** Append to text content */
56
+ function appendText(content) {
57
+ // Add line break if this is new text after a tool use
58
+ let prefix = '';
59
+ if (needsTextBreak && currentMessage.text.length > 0) {
60
+ prefix = '\n\n';
61
+ needsTextBreak = false;
62
+ }
63
+ currentMessage = {
64
+ ...currentMessage,
65
+ text: currentMessage.text + prefix + content
66
+ };
67
+ onMessageUpdate?.(currentMessage);
68
+ }
69
+ /** Mark that next text should have a line break (called after tool use) */
70
+ function markTextBreak() {
71
+ needsTextBreak = true;
72
+ }
73
+ /** Add an edit */
74
+ function addEdit(edit) {
75
+ currentMessage = {
76
+ ...currentMessage,
77
+ edits: [...currentMessage.edits, edit]
78
+ };
79
+ onMessageUpdate?.(currentMessage);
80
+ }
81
+ /**
82
+ * Get a valid session token, refreshing if necessary
83
+ */
84
+ async function getValidToken() {
85
+ if (currentSessionToken) {
86
+ // Check if token is expired (JWT decode)
87
+ try {
88
+ const payload = JSON.parse(atob(currentSessionToken.split('.')[1]));
89
+ const exp = payload.exp * 1000; // Convert to ms
90
+ if (Date.now() < exp - 60000) { // 1 minute buffer
91
+ return currentSessionToken;
92
+ }
93
+ }
94
+ catch {
95
+ // Token parse failed, try to refresh
96
+ }
97
+ }
98
+ // Try to refresh
99
+ if (currentRefreshToken && !isRefreshing) {
100
+ isRefreshing = true;
101
+ try {
102
+ const response = await fetch(`${apiBase}/api/sessions/refresh`, {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify({ refresh_token: currentRefreshToken })
106
+ });
107
+ if (response.ok) {
108
+ const data = await response.json();
109
+ currentSessionToken = data.token;
110
+ onSessionRefresh?.(data.token);
111
+ isRefreshing = false;
112
+ return currentSessionToken ?? null;
113
+ }
114
+ }
115
+ catch (err) {
116
+ console.error('[CodeClient] Failed to refresh session:', err);
117
+ }
118
+ isRefreshing = false;
119
+ }
120
+ // Try to get new session from callback
121
+ if (getSession) {
122
+ try {
123
+ const session = await getSession();
124
+ currentSessionToken = session.token;
125
+ if (session.refreshToken) {
126
+ currentRefreshToken = session.refreshToken;
127
+ }
128
+ return currentSessionToken;
129
+ }
130
+ catch (err) {
131
+ console.error('[CodeClient] Failed to get session:', err);
132
+ onAuthError?.(err);
133
+ }
134
+ }
135
+ onAuthError?.(new Error('No valid session token available'));
136
+ return null;
137
+ }
138
+ /**
139
+ * Make an authenticated request
140
+ */
141
+ async function authenticatedFetch(url, options = {}) {
142
+ const token = await getValidToken();
143
+ if (!token) {
144
+ throw new Error('Not authenticated');
145
+ }
146
+ const headers = new Headers(options.headers);
147
+ headers.set('Authorization', `Bearer ${token}`);
148
+ return fetch(url, { ...options, headers });
149
+ }
150
+ /**
151
+ * Send a code generation request and receive SSE streaming response
152
+ * ProjectId comes from the JWT token (set when session was created)
153
+ */
154
+ async function sendCode(query) {
155
+ const token = await getValidToken();
156
+ if (!token) {
157
+ onAuthError?.(new Error('Not authenticated'));
158
+ return;
159
+ }
160
+ // Reset message state for new request
161
+ currentMessage = createEmptyMessage();
162
+ needsTextBreak = false;
163
+ updateMessage({ status: 'streaming' });
164
+ // Auto-warm cache (fire and forget)
165
+ if (!hasWarmedCache) {
166
+ hasWarmedCache = true;
167
+ warmCache().catch(() => { });
168
+ }
169
+ const body = JSON.stringify({ query });
170
+ console.log('[CodeClient] Sending code request:', { query: query.slice(0, 50) });
171
+ try {
172
+ const response = await fetch(`${apiBase}/api/code`, {
173
+ method: 'POST',
174
+ headers: {
175
+ 'Authorization': `Bearer ${token}`,
176
+ 'Content-Type': 'application/json',
177
+ 'Accept': 'text/event-stream'
178
+ },
179
+ body
180
+ });
181
+ if (!response.ok) {
182
+ if (response.status === 401 || response.status === 403) {
183
+ onAuthError?.(new Error('Authentication failed'));
184
+ }
185
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
186
+ }
187
+ onStreamStart?.();
188
+ try {
189
+ // Read SSE stream
190
+ const reader = response.body?.getReader();
191
+ if (!reader) {
192
+ throw new Error('No response body');
193
+ }
194
+ const decoder = new TextDecoder();
195
+ let buffer = '';
196
+ while (true) {
197
+ const { done, value } = await reader.read();
198
+ if (done)
199
+ break;
200
+ buffer += decoder.decode(value, { stream: true });
201
+ // Process complete SSE events
202
+ const lines = buffer.split('\n');
203
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
204
+ for (const line of lines) {
205
+ if (line.startsWith('data: ')) {
206
+ try {
207
+ const data = JSON.parse(line.slice(6));
208
+ // Skip keepalive events (just for connection health)
209
+ if (data.type === 'keepalive') {
210
+ continue;
211
+ }
212
+ // Notify raw event listeners
213
+ onRawEvent?.(data);
214
+ onMessage?.(data); // deprecated callback
215
+ // Process event and update structured message
216
+ switch (data.type) {
217
+ case 'thinking':
218
+ if (data.content) {
219
+ appendThinking(data.content);
220
+ }
221
+ break;
222
+ case 'text':
223
+ if (data.content) {
224
+ appendText(data.content);
225
+ }
226
+ break;
227
+ case 'edit':
228
+ if (data.edit) {
229
+ addEdit(data.edit);
230
+ onEdit?.(data.edit);
231
+ // Next text after an edit should be on a new line
232
+ markTextBreak();
233
+ }
234
+ break;
235
+ case 'question':
236
+ updateMessage({
237
+ status: 'waiting',
238
+ question: data.content
239
+ });
240
+ break;
241
+ case 'error':
242
+ updateMessage({
243
+ status: 'error',
244
+ error: data.content
245
+ });
246
+ break;
247
+ case 'session_started':
248
+ updateMessage({ sessionId: data.session_id });
249
+ break;
250
+ case 'done':
251
+ if (currentMessage.status !== 'waiting' && currentMessage.status !== 'error') {
252
+ updateMessage({ status: 'done' });
253
+ }
254
+ break;
255
+ case 'bundle_ready':
256
+ console.log('[CodeClient] Bundle ready:', { projectId: data.project_id });
257
+ updateMessage({ bundleReady: true, projectId: data.project_id });
258
+ break;
259
+ case 'bundle_error':
260
+ console.log('[CodeClient] Bundle error:', { error: data.content });
261
+ updateMessage({ bundleError: data.content });
262
+ break;
263
+ }
264
+ }
265
+ catch {
266
+ // Ignore parse errors for malformed events
267
+ }
268
+ }
269
+ }
270
+ }
271
+ // Ensure we mark as done if stream ends without done event
272
+ if (currentMessage.status === 'streaming') {
273
+ updateMessage({ status: 'done' });
274
+ }
275
+ }
276
+ finally {
277
+ onStreamEnd?.();
278
+ }
279
+ }
280
+ catch (error) {
281
+ console.error('[CodeClient] Code request error:', error);
282
+ updateMessage({ status: 'error', error: error.message });
283
+ onError?.(error);
284
+ }
285
+ }
286
+ /**
287
+ * Save pending edits (commit temp changes)
288
+ * ProjectId comes from the JWT token
289
+ */
290
+ async function saveCode() {
291
+ console.log('[CodeClient] Saving code');
292
+ try {
293
+ const response = await authenticatedFetch(`${apiBase}/api/code/save`, {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: '{}'
297
+ });
298
+ if (!response.ok) {
299
+ if (response.status === 401 || response.status === 403) {
300
+ onAuthError?.(new Error('Authentication failed'));
301
+ }
302
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
303
+ }
304
+ // Reset message state after save
305
+ currentMessage = createEmptyMessage();
306
+ onMessageUpdate?.(currentMessage);
307
+ console.log('[CodeClient] Code saved successfully');
308
+ }
309
+ catch (error) {
310
+ console.error('[CodeClient] Save error:', error);
311
+ onError?.(error);
312
+ throw error;
313
+ }
314
+ }
315
+ /**
316
+ * Abandon pending edits (discard temp changes)
317
+ * ProjectId comes from the JWT token
318
+ */
319
+ async function abandonCode() {
320
+ console.log('[CodeClient] Abandoning code');
321
+ try {
322
+ const response = await authenticatedFetch(`${apiBase}/api/code/abandon`, {
323
+ method: 'POST',
324
+ headers: { 'Content-Type': 'application/json' },
325
+ body: '{}'
326
+ });
327
+ if (!response.ok) {
328
+ if (response.status === 401 || response.status === 403) {
329
+ onAuthError?.(new Error('Authentication failed'));
330
+ }
331
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
332
+ }
333
+ // Reset message state after abandon
334
+ currentMessage = createEmptyMessage();
335
+ onMessageUpdate?.(currentMessage);
336
+ console.log('[CodeClient] Code abandoned successfully');
337
+ }
338
+ catch (error) {
339
+ console.error('[CodeClient] Abandon error:', error);
340
+ onError?.(error);
341
+ throw error;
342
+ }
343
+ }
344
+ /**
345
+ * Get list of current edits
346
+ * ProjectId comes from the JWT token
347
+ */
348
+ async function getEdits() {
349
+ console.log('[CodeClient] Getting edits');
350
+ // Proactively warm cache when fetching edits (common init pattern)
351
+ if (!hasWarmedCache) {
352
+ hasWarmedCache = true;
353
+ warmCache().catch(() => { });
354
+ }
355
+ try {
356
+ const response = await authenticatedFetch(`${apiBase}/api/code/edits`);
357
+ if (!response.ok) {
358
+ if (response.status === 401 || response.status === 403) {
359
+ onAuthError?.(new Error('Authentication failed'));
360
+ }
361
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
362
+ }
363
+ const data = await response.json();
364
+ console.log('[CodeClient] Got edits:', data);
365
+ return data.edits || [];
366
+ }
367
+ catch (error) {
368
+ console.error('[CodeClient] Get edits error:', error);
369
+ onError?.(error);
370
+ throw error;
371
+ }
372
+ }
373
+ /**
374
+ * Pre-warm the file cache for faster code requests
375
+ * ProjectId comes from the JWT token
376
+ */
377
+ async function warmCache() {
378
+ try {
379
+ const response = await authenticatedFetch(`${apiBase}/api/code/warm-cache`, {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json' },
382
+ body: '{}'
383
+ });
384
+ if (!response.ok) {
385
+ console.warn('[CodeClient] Warm cache request failed:', response.status);
386
+ return;
387
+ }
388
+ console.log('[CodeClient] Cache warming started');
389
+ }
390
+ catch (error) {
391
+ console.warn('[CodeClient] Warm cache error:', error);
392
+ }
393
+ }
394
+ /**
395
+ * Set/update the session token
396
+ */
397
+ function setSessionToken(token, refreshToken) {
398
+ currentSessionToken = token;
399
+ if (refreshToken) {
400
+ currentRefreshToken = refreshToken;
401
+ }
402
+ }
403
+ return {
404
+ sendCode,
405
+ saveCode,
406
+ abandonCode,
407
+ getEdits,
408
+ warmCache,
409
+ setSessionToken,
410
+ isAuthenticated: () => !!currentSessionToken,
411
+ getMessage: () => currentMessage,
412
+ resetMessage: () => {
413
+ currentMessage = createEmptyMessage();
414
+ needsTextBreak = false;
415
+ onMessageUpdate?.(currentMessage);
416
+ }
417
+ };
418
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Diff State - Tracks original element positions for computing serializable diffs
3
+ *
4
+ * This module records the FIRST position of each moved element.
5
+ * When getDiff() is called, it compares original vs current DOM state
6
+ * to produce serializable diff objects (not DOM references).
7
+ */
8
+ import { type ElementIdentifier, type PlacementTarget } from './element-utils';
9
+ export interface DiffEntry {
10
+ element: ElementIdentifier;
11
+ type: 'moved' | 'removed';
12
+ from: PlacementTarget;
13
+ to: PlacementTarget;
14
+ }
15
+ export interface DiffState {
16
+ snapshotInitialState: (rootElement: HTMLElement) => void;
17
+ trackElement: (domElement: HTMLElement, fallbackPlacement?: PlacementTarget) => void;
18
+ getDiff: () => DiffEntry[];
19
+ hasChanges: () => boolean;
20
+ clearAll: () => void;
21
+ getTrackedCount: () => number;
22
+ }
23
+ export declare function createDiffState(): DiffState;
24
+ //# sourceMappingURL=diff-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-state.d.ts","sourceRoot":"","sources":["../../src/builder/diff-state.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,EAAE,EAAE,eAAe,CAAC;CACrB;AA6CD,MAAM,WAAW,SAAS;IACxB,oBAAoB,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,IAAI,CAAC;IACzD,YAAY,EAAE,CAAC,UAAU,EAAE,WAAW,EAAE,iBAAiB,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IACrF,OAAO,EAAE,MAAM,SAAS,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED,wBAAgB,eAAe,IAAI,SAAS,CAyG3C"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Diff State - Tracks original element positions for computing serializable diffs
3
+ *
4
+ * This module records the FIRST position of each moved element.
5
+ * When getDiff() is called, it compares original vs current DOM state
6
+ * to produce serializable diff objects (not DOM references).
7
+ */
8
+ import { getStableId, findByStableId, buildElementIdentifier, buildPlacementTarget } from './element-utils';
9
+ function serializeIdentifier(el) {
10
+ if (!el)
11
+ return '';
12
+ if (el.id)
13
+ return `#${el.id}`;
14
+ const parts = [
15
+ el.tagName,
16
+ el.classes.sort().join('.'),
17
+ `nth:${el.nthOfType}`,
18
+ el.textContentPreview?.slice(0, 20) || '',
19
+ ];
20
+ if (el.parentIdentifier) {
21
+ parts.push('>' + serializeIdentifier(el.parentIdentifier));
22
+ }
23
+ return parts.join('|');
24
+ }
25
+ function placementsEqual(a, b) {
26
+ if (!a || !b)
27
+ return a === b;
28
+ if (a.position !== b.position)
29
+ return false;
30
+ if (a.parentStableId && b.parentStableId) {
31
+ if (a.parentStableId !== b.parentStableId)
32
+ return false;
33
+ }
34
+ else {
35
+ if (serializeIdentifier(a.parent) !== serializeIdentifier(b.parent))
36
+ return false;
37
+ }
38
+ if (a.referenceStableId && b.referenceStableId) {
39
+ return a.referenceStableId === b.referenceStableId;
40
+ }
41
+ if (a.referenceElement && b.referenceElement) {
42
+ return serializeIdentifier(a.referenceElement) === serializeIdentifier(b.referenceElement);
43
+ }
44
+ return a.referenceElement === b.referenceElement;
45
+ }
46
+ export function createDiffState() {
47
+ const initialSnapshot = new Map();
48
+ const movedElements = new Map();
49
+ function snapshotInitialState(rootElement) {
50
+ if (!rootElement)
51
+ return;
52
+ function walkAndSnapshot(element) {
53
+ if (element.nodeType !== Node.ELEMENT_NODE)
54
+ return;
55
+ const htmlElement = element;
56
+ const stableId = getStableId(htmlElement);
57
+ const placement = buildPlacementTarget(htmlElement);
58
+ if (placement) {
59
+ initialSnapshot.set(stableId, placement);
60
+ }
61
+ for (const child of Array.from(element.children)) {
62
+ walkAndSnapshot(child);
63
+ }
64
+ }
65
+ for (const child of Array.from(rootElement.children)) {
66
+ walkAndSnapshot(child);
67
+ }
68
+ }
69
+ function trackElement(domElement, _fallbackPlacement) {
70
+ if (!domElement)
71
+ return;
72
+ const stableId = getStableId(domElement);
73
+ if (movedElements.has(stableId)) {
74
+ return;
75
+ }
76
+ const elementIdentifier = buildElementIdentifier(domElement);
77
+ movedElements.set(stableId, {
78
+ stableId,
79
+ element: elementIdentifier,
80
+ });
81
+ }
82
+ function getDiff() {
83
+ const diffs = [];
84
+ for (const [stableId, tracked] of movedElements) {
85
+ const element = findByStableId(stableId);
86
+ const originalPlacement = initialSnapshot.get(stableId);
87
+ if (!element) {
88
+ if (originalPlacement) {
89
+ diffs.push({
90
+ element: tracked.element,
91
+ type: 'removed',
92
+ from: originalPlacement,
93
+ to: originalPlacement,
94
+ });
95
+ }
96
+ continue;
97
+ }
98
+ if (!originalPlacement)
99
+ continue;
100
+ const currentPlacement = buildPlacementTarget(element);
101
+ if (!currentPlacement)
102
+ continue;
103
+ if (!placementsEqual(originalPlacement, currentPlacement)) {
104
+ diffs.push({
105
+ element: tracked.element,
106
+ type: 'moved',
107
+ from: originalPlacement,
108
+ to: currentPlacement,
109
+ });
110
+ }
111
+ }
112
+ return diffs;
113
+ }
114
+ function hasChanges() {
115
+ return movedElements.size > 0;
116
+ }
117
+ function clearAll() {
118
+ movedElements.clear();
119
+ }
120
+ function getTrackedCount() {
121
+ return movedElements.size;
122
+ }
123
+ if (typeof window !== 'undefined') {
124
+ window.__diffState = { getDiff, hasChanges, getTrackedCount, movedElements, initialSnapshot };
125
+ }
126
+ return {
127
+ snapshotInitialState,
128
+ trackElement,
129
+ getDiff,
130
+ hasChanges,
131
+ clearAll,
132
+ getTrackedCount,
133
+ };
134
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * DOM scanner - serializes DOM tree for the component tree
3
+ */
4
+ import { STABLE_ID_ATTR } from './element-utils';
5
+ import type { SerializedDOMNode } from './tree-state';
6
+ export interface ScanOptions {
7
+ maxDepth?: number;
8
+ skipIds?: string[];
9
+ }
10
+ export declare function scanDOM(root: HTMLElement, options?: ScanOptions): SerializedDOMNode | null;
11
+ export interface DebouncedScanner {
12
+ scan: () => void;
13
+ cancel: () => void;
14
+ }
15
+ export declare function createDebouncedScanner(root: HTMLElement, callback: (tree: SerializedDOMNode | null) => void, delay?: number): DebouncedScanner;
16
+ export declare function getElementAtPath(path: number[], root: HTMLElement): HTMLElement | null;
17
+ export declare function queryElements(selector: string, root?: HTMLElement): HTMLElement[];
18
+ export declare function generateSelector(element: HTMLElement): string;
19
+ export { STABLE_ID_ATTR };
20
+ //# sourceMappingURL=dom-scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-scanner.d.ts","sourceRoot":"","sources":["../../src/builder/dom-scanner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAgD,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,cAAc,CAAC;AAEtE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,WAAgB,GAAG,iBAAiB,GAAG,IAAI,CAiD9F;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,EAClD,KAAK,SAAM,GACV,gBAAgB,CAqBlB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAStF;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,WAA2B,GAAG,WAAW,EAAE,CAMhG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAiB7D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}