@symbo.ls/connect 3.4.0 → 3.4.2

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/static/panel.js CHANGED
@@ -21,6 +21,7 @@
21
21
  let redoStack = []
22
22
  let platformProjectData = null // project data from platform API
23
23
  let platformProjectId = null
24
+ let cachedDesignSystem = null // cached DS for autocomplete suggestions
24
25
 
25
26
  // ============================================================
26
27
  // IndexedDB
@@ -1264,13 +1265,70 @@
1264
1265
 
1265
1266
  function updateSyncButton () {
1266
1267
  const btn = document.getElementById('btn-sync')
1268
+ const dropdown = document.getElementById('btn-sync-dropdown')
1267
1269
  if (!btn) return
1268
1270
  const count = pendingSyncOps.length
1269
1271
  if (count > 0) {
1270
1272
  btn.style.display = ''
1271
1273
  btn.textContent = 'Sync (' + count + ')'
1274
+ if (dropdown) dropdown.style.display = ''
1272
1275
  } else {
1273
1276
  btn.style.display = 'none'
1277
+ if (dropdown) dropdown.style.display = 'none'
1278
+ const panel = document.getElementById('sync-changes-panel')
1279
+ if (panel) panel.style.display = 'none'
1280
+ }
1281
+ }
1282
+
1283
+ function toggleSyncChangesPanel () {
1284
+ const panel = document.getElementById('sync-changes-panel')
1285
+ if (!panel) return
1286
+ const visible = panel.style.display !== 'none'
1287
+ if (visible) {
1288
+ panel.style.display = 'none'
1289
+ return
1290
+ }
1291
+ panel.style.display = ''
1292
+ renderSyncChangesList()
1293
+ }
1294
+
1295
+ function renderSyncChangesList () {
1296
+ const list = document.getElementById('sync-changes-list')
1297
+ if (!list) return
1298
+ list.innerHTML = ''
1299
+
1300
+ if (pendingSyncOps.length === 0) {
1301
+ list.innerHTML = '<div class="empty-message">No pending changes</div>'
1302
+ return
1303
+ }
1304
+
1305
+ for (let i = 0; i < pendingSyncOps.length; i++) {
1306
+ const op = pendingSyncOps[i]
1307
+ const row = document.createElement('div')
1308
+ row.className = 'sync-change-row'
1309
+
1310
+ const path = document.createElement('span')
1311
+ path.className = 'sync-change-path'
1312
+ path.textContent = (op.elementPath || '') + '.' + op.key
1313
+
1314
+ const val = document.createElement('span')
1315
+ val.className = 'sync-change-val'
1316
+ val.textContent = typeof op.value === 'string' ? op.value : JSON.stringify(op.value)
1317
+
1318
+ const removeBtn = document.createElement('button')
1319
+ removeBtn.className = 'sync-change-remove'
1320
+ removeBtn.textContent = '\u00d7'
1321
+ removeBtn.title = 'Remove this change'
1322
+ removeBtn.addEventListener('click', () => {
1323
+ pendingSyncOps.splice(i, 1)
1324
+ updateSyncButton()
1325
+ renderSyncChangesList()
1326
+ })
1327
+
1328
+ row.appendChild(path)
1329
+ row.appendChild(val)
1330
+ row.appendChild(removeBtn)
1331
+ list.appendChild(row)
1274
1332
  }
1275
1333
  }
1276
1334
 
@@ -1701,6 +1759,12 @@
1701
1759
  }
1702
1760
 
1703
1761
  setStatus('Tree loaded')
1762
+ // Pre-cache design system for autocomplete
1763
+ if (!cachedDesignSystem) {
1764
+ fetchRuntimeDesignSystem().then(ds => {
1765
+ if (ds && Object.keys(ds).length > 0) cachedDesignSystem = ds
1766
+ })
1767
+ }
1704
1768
  } else {
1705
1769
  setStatus('No DOMQL root found')
1706
1770
  }
@@ -2124,7 +2188,7 @@
2124
2188
 
2125
2189
  // --- Original view ---
2126
2190
  if (!hasOriginal) {
2127
- originalPanel.innerHTML = '<div class="empty-message">No original definition props found</div>'
2191
+ originalPanel.innerHTML = '<div class="empty-message">No original definition props found. Connect a local folder or platform to recognize original props.</div>'
2128
2192
  } else {
2129
2193
  for (const [key, val] of Object.entries(selectedInfo.originalProps)) {
2130
2194
  const isFunc = key in funcProps
@@ -2205,6 +2269,36 @@
2205
2269
  row.dataset.propKey = key
2206
2270
  row.dataset.propType = type
2207
2271
 
2272
+ // Checkbox to disable/enable prop (like CSS DevTools)
2273
+ const checkbox = document.createElement('input')
2274
+ checkbox.type = 'checkbox'
2275
+ checkbox.checked = true
2276
+ checkbox.className = 'prop-checkbox'
2277
+ checkbox.title = 'Toggle property'
2278
+ checkbox.addEventListener('change', async () => {
2279
+ row.classList.toggle('prop-disabled', !checkbox.checked)
2280
+ if (checkbox.checked) {
2281
+ // Re-enable: set value back
2282
+ const expr = type === 'state'
2283
+ ? 'JSON.stringify(window.__DOMQL_INSPECTOR__.updateState(' +
2284
+ JSON.stringify(selectedPath) + ',' + JSON.stringify(key) + ',' + JSON.stringify(val) + '))'
2285
+ : 'JSON.stringify(window.__DOMQL_INSPECTOR__.updateProp(' +
2286
+ JSON.stringify(selectedPath) + ',' + JSON.stringify(key) + ',' + JSON.stringify(val) + '))'
2287
+ await pageEval(expr).catch(() => {})
2288
+ setStatus('Enabled ' + key)
2289
+ } else {
2290
+ // Disable: set to undefined/remove
2291
+ const expr = type === 'state'
2292
+ ? 'JSON.stringify(window.__DOMQL_INSPECTOR__.updateState(' +
2293
+ JSON.stringify(selectedPath) + ',' + JSON.stringify(key) + ',undefined))'
2294
+ : 'JSON.stringify(window.__DOMQL_INSPECTOR__.updateProp(' +
2295
+ JSON.stringify(selectedPath) + ',' + JSON.stringify(key) + ',undefined))'
2296
+ await pageEval(expr).catch(() => {})
2297
+ setStatus('Disabled ' + key)
2298
+ }
2299
+ })
2300
+ row.appendChild(checkbox)
2301
+
2208
2302
  const keyEl = document.createElement('span')
2209
2303
  keyEl.className = 'prop-key'
2210
2304
  keyEl.textContent = key
@@ -2240,6 +2334,56 @@
2240
2334
  valEl.addEventListener('click', () => startEditing(valEl, key, editableVal, type, componentName))
2241
2335
  }
2242
2336
 
2337
+ // Number arrows (like CSS DevTools)
2338
+ const isNum = typeof val === 'number' || (typeof val === 'string' && /^-?\d+(\.\d+)?(px|em|rem|%|vh|vw|ms|s)?$/.test(val))
2339
+ if (isNum && !isComplex(val)) {
2340
+ const arrows = document.createElement('span')
2341
+ arrows.className = 'prop-num-arrows'
2342
+ const upBtn = document.createElement('button')
2343
+ upBtn.className = 'prop-num-arrow up'
2344
+ upBtn.textContent = '\u25B2'
2345
+ upBtn.title = 'Increment'
2346
+ const downBtn = document.createElement('button')
2347
+ downBtn.className = 'prop-num-arrow down'
2348
+ downBtn.textContent = '\u25BC'
2349
+ downBtn.title = 'Decrement'
2350
+
2351
+ const nudge = async (delta) => {
2352
+ const current = typeof val === 'number' ? val : parseFloat(val)
2353
+ const unit = typeof val === 'string' ? val.replace(/^-?\d+(\.\d+)?/, '') : ''
2354
+ const newNum = current + delta
2355
+ const newVal = unit ? String(newNum) + unit : newNum
2356
+ val = newVal
2357
+ valEl.innerHTML = ''
2358
+ valEl.appendChild(renderValue(newVal))
2359
+ // Reattach click
2360
+ if (!isComplex(newVal)) {
2361
+ valEl.addEventListener('click', () => startEditing(valEl, key, newVal, type, componentName))
2362
+ }
2363
+ const expr = type === 'state'
2364
+ ? 'JSON.stringify(window.__DOMQL_INSPECTOR__.updateState(' +
2365
+ JSON.stringify(selectedPath) + ',' + JSON.stringify(key) + ',' + JSON.stringify(newVal) + '))'
2366
+ : 'JSON.stringify(window.__DOMQL_INSPECTOR__.updateProp(' +
2367
+ JSON.stringify(selectedPath) + ',' + JSON.stringify(key) + ',' + JSON.stringify(newVal) + '))'
2368
+ await pageEval(expr).catch(() => {})
2369
+ }
2370
+ upBtn.addEventListener('click', (e) => { e.stopPropagation(); nudge(1) })
2371
+ downBtn.addEventListener('click', (e) => { e.stopPropagation(); nudge(-1) })
2372
+ arrows.appendChild(upBtn)
2373
+ arrows.appendChild(downBtn)
2374
+
2375
+ const semiEl = document.createElement('span')
2376
+ semiEl.className = 'prop-semi'
2377
+ semiEl.textContent = ';'
2378
+
2379
+ row.appendChild(keyEl)
2380
+ row.appendChild(colonEl)
2381
+ row.appendChild(valEl)
2382
+ row.appendChild(arrows)
2383
+ row.appendChild(semiEl)
2384
+ return row
2385
+ }
2386
+
2243
2387
  const semiEl = document.createElement('span')
2244
2388
  semiEl.className = 'prop-semi'
2245
2389
  semiEl.textContent = ';'
@@ -2251,6 +2395,49 @@
2251
2395
  return row
2252
2396
  }
2253
2397
 
2398
+ // All known prop keys for autocomplete
2399
+ const KNOWN_PROP_KEYS = [
2400
+ // Layout
2401
+ 'display', 'position', 'overflow', 'overflowX', 'overflowY', 'visibility',
2402
+ 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink', 'flexBasis',
2403
+ 'alignItems', 'alignContent', 'alignSelf', 'justifyContent', 'justifyItems', 'justifySelf',
2404
+ 'flow', 'wrap', 'order',
2405
+ // Spacing
2406
+ 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
2407
+ 'paddingInline', 'paddingBlock',
2408
+ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
2409
+ 'marginInline', 'marginBlock',
2410
+ 'gap', 'rowGap', 'columnGap',
2411
+ // Size
2412
+ 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'boxSize',
2413
+ // Position
2414
+ 'top', 'right', 'bottom', 'left', 'inset', 'zIndex',
2415
+ // Color
2416
+ 'color', 'background', 'backgroundColor', 'borderColor', 'fill', 'stroke',
2417
+ 'opacity',
2418
+ // Typography
2419
+ 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'lineHeight', 'letterSpacing',
2420
+ 'textAlign', 'textDecoration', 'textTransform', 'textOverflow', 'whiteSpace', 'wordBreak',
2421
+ // Border
2422
+ 'border', 'borderWidth', 'borderStyle', 'borderRadius', 'round',
2423
+ 'borderTop', 'borderRight', 'borderBottom', 'borderLeft',
2424
+ // Effects
2425
+ 'boxShadow', 'transition', 'transform', 'animation', 'cursor', 'pointerEvents',
2426
+ 'userSelect', 'outline', 'outlineColor', 'outlineWidth', 'outlineOffset',
2427
+ // Background
2428
+ 'backgroundImage', 'backgroundSize', 'backgroundRepeat', 'backgroundPosition', 'backgroundClip',
2429
+ // Grid
2430
+ 'gridTemplate', 'gridTemplateColumns', 'gridTemplateRows', 'gridColumn', 'gridRow',
2431
+ 'gridArea', 'gridGap',
2432
+ // DOMQL specific
2433
+ 'tag', 'text', 'html', 'theme', 'scope', 'if', 'content',
2434
+ 'src', 'href', 'placeholder', 'type', 'value', 'title', 'alt',
2435
+ 'childExtend', 'childProps', 'extend', 'props',
2436
+ // Other
2437
+ 'objectFit', 'objectPosition', 'resize', 'appearance', 'boxSizing',
2438
+ 'willChange', 'verticalAlign', 'mixBlendMode', 'backdropFilter', 'filter'
2439
+ ]
2440
+
2254
2441
  function createAddButton (type) {
2255
2442
  const wrap = document.createElement('div')
2256
2443
  wrap.className = 'prop-add-row'
@@ -2261,7 +2448,7 @@
2261
2448
  // Replace button with inline key:value editor
2262
2449
  wrap.innerHTML = ''
2263
2450
  const row = document.createElement('div')
2264
- row.className = 'prop-row'
2451
+ row.className = 'prop-row prop-add-inline'
2265
2452
 
2266
2453
  const keyInput = document.createElement('input')
2267
2454
  keyInput.className = 'prop-edit-input'
@@ -2287,7 +2474,112 @@
2287
2474
  row.appendChild(semiEl)
2288
2475
  wrap.appendChild(row)
2289
2476
 
2477
+ // Key autocomplete dropdown
2478
+ const keyDropdown = document.createElement('div')
2479
+ keyDropdown.className = 'prop-dropdown'
2480
+ keyDropdown.style.display = 'none'
2481
+ row.appendChild(keyDropdown)
2482
+
2483
+ // Value autocomplete dropdown
2484
+ const valDropdown = document.createElement('div')
2485
+ valDropdown.className = 'prop-dropdown'
2486
+ valDropdown.style.display = 'none'
2487
+ row.appendChild(valDropdown)
2488
+
2489
+ let keyFiltered = []
2490
+ let keyActiveIdx = -1
2491
+ let valFiltered = []
2492
+ let valActiveIdx = -1
2493
+ let activeDropdown = null
2494
+
2495
+ function showKeyDropdown () {
2496
+ activeDropdown = 'key'
2497
+ valDropdown.style.display = 'none'
2498
+ const text = keyInput.value.toLowerCase()
2499
+ // Exclude props that already exist
2500
+ const existing = selectedInfo ? Object.keys(selectedInfo.props || {}) : []
2501
+ keyFiltered = KNOWN_PROP_KEYS
2502
+ .filter(k => !existing.includes(k) && (text ? k.toLowerCase().includes(text) : true))
2503
+ .slice(0, 20)
2504
+ .map(k => ({ label: k }))
2505
+ keyActiveIdx = -1
2506
+ renderKeyDD()
2507
+ }
2508
+
2509
+ function renderKeyDD () {
2510
+ keyDropdown.innerHTML = ''
2511
+ if (keyFiltered.length === 0) { keyDropdown.style.display = 'none'; return }
2512
+ keyDropdown.style.display = ''
2513
+ for (let i = 0; i < keyFiltered.length; i++) {
2514
+ const item = document.createElement('div')
2515
+ item.className = 'prop-dropdown-item' + (i === keyActiveIdx ? ' active' : '')
2516
+ item.textContent = keyFiltered[i].label
2517
+ item.addEventListener('mousedown', (e) => {
2518
+ e.preventDefault()
2519
+ keyInput.value = keyFiltered[i].label
2520
+ keyDropdown.style.display = 'none'
2521
+ valInput.focus()
2522
+ showValDropdown()
2523
+ })
2524
+ keyDropdown.appendChild(item)
2525
+ }
2526
+ }
2527
+
2528
+ function showValDropdown () {
2529
+ activeDropdown = 'val'
2530
+ keyDropdown.style.display = 'none'
2531
+ const propName = keyInput.value.trim()
2532
+ const allSuggestions = getSuggestionsForProp(propName) || []
2533
+ const text = valInput.value.toLowerCase()
2534
+ valFiltered = text
2535
+ ? allSuggestions.filter(s => s.label.toLowerCase().includes(text)).slice(0, 20)
2536
+ : allSuggestions.slice(0, 20)
2537
+ valActiveIdx = -1
2538
+ renderValDD()
2539
+ }
2540
+
2541
+ function renderValDD () {
2542
+ valDropdown.innerHTML = ''
2543
+ if (valFiltered.length === 0) { valDropdown.style.display = 'none'; return }
2544
+ valDropdown.style.display = ''
2545
+ for (let i = 0; i < valFiltered.length; i++) {
2546
+ const s = valFiltered[i]
2547
+ const item = document.createElement('div')
2548
+ item.className = 'prop-dropdown-item' + (i === valActiveIdx ? ' active' : '')
2549
+ if (s.hex !== undefined) {
2550
+ const sw = document.createElement('span')
2551
+ sw.className = 'dd-swatch'
2552
+ sw.style.background = s.hex || '#555'
2553
+ item.appendChild(sw)
2554
+ }
2555
+ const label = document.createElement('span')
2556
+ label.textContent = s.label
2557
+ item.appendChild(label)
2558
+ if (s.hint) {
2559
+ const hint = document.createElement('span')
2560
+ hint.className = 'dd-hint'
2561
+ hint.textContent = s.hint
2562
+ item.appendChild(hint)
2563
+ }
2564
+ item.addEventListener('mousedown', (e) => {
2565
+ e.preventDefault()
2566
+ valInput.value = s.label
2567
+ valDropdown.style.display = 'none'
2568
+ })
2569
+ valDropdown.appendChild(item)
2570
+ }
2571
+ }
2572
+
2573
+ keyInput.addEventListener('input', showKeyDropdown)
2574
+ keyInput.addEventListener('focus', showKeyDropdown)
2575
+ keyInput.addEventListener('blur', () => setTimeout(() => { keyDropdown.style.display = 'none' }, 150))
2576
+
2577
+ valInput.addEventListener('input', showValDropdown)
2578
+ valInput.addEventListener('focus', showValDropdown)
2579
+ valInput.addEventListener('blur', () => setTimeout(() => { valDropdown.style.display = 'none' }, 150))
2580
+
2290
2581
  keyInput.focus()
2582
+ showKeyDropdown()
2291
2583
 
2292
2584
  const commit = async () => {
2293
2585
  const key = keyInput.value.trim()
@@ -2316,13 +2608,31 @@
2316
2608
  }
2317
2609
 
2318
2610
  valInput.addEventListener('keydown', (e) => {
2611
+ if (activeDropdown === 'val' && valFiltered.length > 0) {
2612
+ if (e.key === 'ArrowDown') { e.preventDefault(); valActiveIdx = Math.min(valActiveIdx + 1, valFiltered.length - 1); renderValDD(); return }
2613
+ if (e.key === 'ArrowUp') { e.preventDefault(); valActiveIdx = Math.max(valActiveIdx - 1, 0); renderValDD(); return }
2614
+ if (e.key === 'Enter' && valActiveIdx >= 0) { e.preventDefault(); valInput.value = valFiltered[valActiveIdx].label; valDropdown.style.display = 'none'; commit(); return }
2615
+ }
2319
2616
  if (e.key === 'Enter') commit()
2320
2617
  if (e.key === 'Escape') renderDetail()
2321
2618
  })
2322
2619
  keyInput.addEventListener('keydown', (e) => {
2620
+ if (activeDropdown === 'key' && keyFiltered.length > 0) {
2621
+ if (e.key === 'ArrowDown') { e.preventDefault(); keyActiveIdx = Math.min(keyActiveIdx + 1, keyFiltered.length - 1); renderKeyDD(); return }
2622
+ if (e.key === 'ArrowUp') { e.preventDefault(); keyActiveIdx = Math.max(keyActiveIdx - 1, 0); renderKeyDD(); return }
2623
+ if ((e.key === 'Enter' || e.key === 'Tab') && keyActiveIdx >= 0) {
2624
+ e.preventDefault()
2625
+ keyInput.value = keyFiltered[keyActiveIdx].label
2626
+ keyDropdown.style.display = 'none'
2627
+ valInput.focus()
2628
+ showValDropdown()
2629
+ return
2630
+ }
2631
+ }
2323
2632
  if (e.key === 'Enter' || e.key === 'Tab') {
2324
2633
  e.preventDefault()
2325
2634
  valInput.focus()
2635
+ showValDropdown()
2326
2636
  }
2327
2637
  if (e.key === 'Escape') renderDetail()
2328
2638
  })
@@ -2647,23 +2957,15 @@
2647
2957
  // ============================================================
2648
2958
  // Design System tab
2649
2959
  // ============================================================
2650
- async function renderDesignSystem () {
2651
- const container = document.getElementById('design-system-container')
2652
- container.innerHTML = ''
2653
-
2654
- // Try runtime — check multiple paths for designSystem
2655
- let ds = null
2960
+ async function fetchRuntimeDesignSystem () {
2656
2961
  try {
2657
2962
  const raw = await pageEval(`(function(){
2658
2963
  var I = window.__DOMQL_INSPECTOR__;
2659
2964
  if (!I) return 'null';
2660
- // Try from root context
2661
2965
  var ds = I.getDesignSystem();
2662
2966
  if (ds) return JSON.stringify(ds);
2663
- // Try from any element that has context
2664
2967
  var root = I.findRoot();
2665
2968
  if (root) {
2666
- // Walk to find any child with context.designSystem
2667
2969
  function findDS(el, depth) {
2668
2970
  if (depth > 4 || !el) return null;
2669
2971
  if (el.context && el.context.designSystem) return el;
@@ -2684,39 +2986,96 @@
2684
2986
  }
2685
2987
  return 'null';
2686
2988
  })()`)
2687
- if (raw && raw !== 'null') ds = JSON.parse(raw)
2989
+ if (raw && raw !== 'null') return JSON.parse(raw)
2688
2990
  } catch (e) { /* ignore */ }
2991
+ return null
2992
+ }
2993
+
2994
+ async function renderDesignSystem () {
2995
+ const container = document.getElementById('design-system-container')
2996
+ container.innerHTML = ''
2997
+
2998
+ // Build sub-tabs: Original / Computed
2999
+ const subtabs = document.createElement('div')
3000
+ subtabs.id = 'ds-subtabs'
3001
+ subtabs.innerHTML = '<button class="props-subtab active" data-dsview="original">Original</button>' +
3002
+ '<button class="props-subtab" data-dsview="computed">Computed</button>'
3003
+
3004
+ const originalPanel = document.createElement('div')
3005
+ originalPanel.id = 'ds-original'
3006
+ originalPanel.className = 'props-subpanel active'
3007
+
3008
+ const computedPanel = document.createElement('div')
3009
+ computedPanel.id = 'ds-computed'
3010
+ computedPanel.className = 'props-subpanel'
3011
+
3012
+ container.appendChild(subtabs)
3013
+ container.appendChild(originalPanel)
3014
+ container.appendChild(computedPanel)
3015
+
3016
+ // Wire sub-tab switching
3017
+ subtabs.querySelectorAll('.props-subtab').forEach(btn => {
3018
+ btn.addEventListener('click', () => {
3019
+ subtabs.querySelectorAll('.props-subtab').forEach(b => b.classList.remove('active'))
3020
+ btn.classList.add('active')
3021
+ originalPanel.classList.toggle('active', btn.dataset.dsview === 'original')
3022
+ computedPanel.classList.toggle('active', btn.dataset.dsview === 'computed')
3023
+ })
3024
+ })
2689
3025
 
2690
- // Fallback: look for designSystem files in fileCache
2691
- if (!ds && Object.keys(fileCache).length) {
2692
- ds = buildDesignSystemFromFiles()
3026
+ // --- Original (from source files) ---
3027
+ const originalDS = buildDesignSystemFromFiles()
3028
+ if (originalDS && Object.keys(originalDS).length > 0) {
3029
+ cachedDesignSystem = originalDS
3030
+ renderDesignSystemContent(originalPanel, originalDS, true)
3031
+ } else {
3032
+ originalPanel.innerHTML = '<div class="empty-message">No source files found. Connect a local folder or platform to view original design system.</div>'
2693
3033
  }
2694
3034
 
2695
- if (!ds || Object.keys(ds).length === 0) {
3035
+ // --- Computed (from runtime) ---
3036
+ const runtimeDS = await fetchRuntimeDesignSystem()
3037
+ if (runtimeDS && Object.keys(runtimeDS).length > 0) {
3038
+ if (!cachedDesignSystem) cachedDesignSystem = runtimeDS
3039
+ renderDesignSystemContent(computedPanel, runtimeDS, false)
3040
+ } else {
2696
3041
  if (!renderDesignSystem._retried) {
2697
3042
  renderDesignSystem._retried = true
2698
- container.innerHTML = '<div class="empty-message">Loading design system...</div>'
2699
- setTimeout(() => renderDesignSystem(), 2000)
3043
+ computedPanel.innerHTML = '<div class="empty-message">Loading...</div>'
3044
+ setTimeout(async () => {
3045
+ const ds = await fetchRuntimeDesignSystem()
3046
+ renderDesignSystem._retried = false
3047
+ if (ds && Object.keys(ds).length > 0) {
3048
+ if (!cachedDesignSystem) cachedDesignSystem = ds
3049
+ computedPanel.innerHTML = ''
3050
+ renderDesignSystemContent(computedPanel, ds, false)
3051
+ } else {
3052
+ computedPanel.innerHTML = '<div class="empty-message">Design system is empty at runtime</div>'
3053
+ }
3054
+ }, 2000)
2700
3055
  } else {
2701
3056
  renderDesignSystem._retried = false
2702
- container.innerHTML = '<div class="empty-message">Design system is empty</div>'
3057
+ computedPanel.innerHTML = '<div class="empty-message">Design system is empty at runtime</div>'
2703
3058
  }
2704
- return
2705
3059
  }
2706
- renderDesignSystem._retried = false
2707
3060
 
2708
- renderDesignSystemContent(container, ds)
3061
+ // If no original but we have computed, default to computed tab
3062
+ if ((!originalDS || Object.keys(originalDS).length === 0) && runtimeDS && Object.keys(runtimeDS).length > 0) {
3063
+ subtabs.querySelectorAll('.props-subtab').forEach(b => b.classList.remove('active'))
3064
+ subtabs.querySelector('[data-dsview="computed"]').classList.add('active')
3065
+ originalPanel.classList.remove('active')
3066
+ computedPanel.classList.add('active')
3067
+ }
2709
3068
  }
2710
3069
 
2711
- function renderDesignSystemContent (container, ds) {
3070
+ function renderDesignSystemContent (container, ds, isOriginal) {
2712
3071
 
2713
3072
  // Render known categories in order, then the rest
2714
- const categoryOrder = ['color', 'colors', 'theme', 'themes', 'spacing', 'space', 'typography', 'font', 'fontSize', 'fontFamily', 'fontWeight', 'lineHeight', 'letterSpacing', 'borderRadius', 'shadow', 'media', 'breakpoints', 'opacity', 'transition', 'gradient']
3073
+ const categoryOrder = ['color', 'COLOR', 'colors', 'gradient', 'GRADIENT', 'theme', 'THEME', 'themes', 'font', 'FONT', 'font_family', 'FONT_FAMILY', 'typography', 'TYPOGRAPHY', 'spacing', 'SPACING', 'space', 'timing', 'TIMING', 'class', 'CLASS', 'grid', 'GRID', 'icons', 'ICONS', 'shape', 'SHAPE', 'reset', 'RESET', 'animation', 'ANIMATION', 'media', 'MEDIA', 'cases', 'CASES', 'fontSize', 'fontFamily', 'fontWeight', 'lineHeight', 'letterSpacing', 'borderRadius', 'shadow', 'breakpoints', 'opacity', 'transition']
2715
3074
  const rendered = {}
2716
3075
 
2717
3076
  for (const cat of categoryOrder) {
2718
3077
  if (ds[cat] !== undefined) {
2719
- renderDSCategory(container, cat, ds[cat], [cat])
3078
+ renderDSCategory(container, cat, ds[cat], [cat], isOriginal)
2720
3079
  rendered[cat] = true
2721
3080
  }
2722
3081
  }
@@ -2726,36 +3085,59 @@
2726
3085
  if (rendered[key]) continue
2727
3086
  if (key.startsWith('__') || typeof ds[key] === 'function') continue
2728
3087
  if (ds[key] && ds[key].__type === 'function') continue
2729
- renderDSCategory(container, key, ds[key], [key])
3088
+ renderDSCategory(container, key, ds[key], [key], isOriginal)
2730
3089
  }
2731
3090
  }
2732
3091
 
2733
3092
  function buildDesignSystemFromFiles () {
2734
3093
  const ds = {}
3094
+ const dsFiles = []
3095
+
3096
+ // Collect all DS-related files
2735
3097
  for (const [path, content] of Object.entries(fileCache)) {
2736
3098
  if (!content) continue
2737
- // Match files in designSystem folder or files named designSystem
2738
- const isDS = /designSystem/i.test(path) || /design.system/i.test(path)
3099
+ const isDS = /designSystem/i.test(path) || /design.system/i.test(path) || /design-system/i.test(path)
2739
3100
  if (!isDS) continue
3101
+ dsFiles.push({ path, content })
3102
+ }
3103
+
3104
+ if (dsFiles.length === 0) return ds
2740
3105
 
2741
- // Try to extract exported values (basic heuristic)
3106
+ // Sort: index files last (they aggregate), category files first
3107
+ dsFiles.sort((a, b) => {
3108
+ const aName = a.path.split('/').pop().replace(/\.(js|jsx|ts|tsx|json)$/i, '').toLowerCase()
3109
+ const bName = b.path.split('/').pop().replace(/\.(js|jsx|ts|tsx|json)$/i, '').toLowerCase()
3110
+ const aIsIndex = aName === 'index' || aName === 'designsystem'
3111
+ const bIsIndex = bName === 'index' || bName === 'designsystem'
3112
+ if (aIsIndex && !bIsIndex) return 1
3113
+ if (!aIsIndex && bIsIndex) return -1
3114
+ return 0
3115
+ })
3116
+
3117
+ for (const { path, content } of dsFiles) {
2742
3118
  const name = path.split('/').pop().replace(/\.(js|jsx|ts|tsx|json)$/i, '')
3119
+ const nameLower = name.toLowerCase()
2743
3120
  try {
2744
3121
  if (path.endsWith('.json')) {
2745
3122
  const parsed = JSON.parse(content)
2746
- if (name.toLowerCase() === 'index' || name.toLowerCase() === 'designsystem') {
2747
- Object.assign(ds, parsed)
3123
+ if (nameLower === 'index' || nameLower === 'designsystem') {
3124
+ // Index only adds keys not already present from individual files
3125
+ for (const [k, v] of Object.entries(parsed)) {
3126
+ if (!ds[k] && !ds[k.toLowerCase()]) ds[k] = v
3127
+ }
2748
3128
  } else {
2749
3129
  ds[name] = parsed
2750
3130
  }
2751
3131
  } else {
2752
3132
  // Try to extract object literals from export default or module.exports
2753
- const match = content.match(/(?:export\s+default|module\.exports\s*=)\s*(\{[\s\S]*\})\s*$/m)
3133
+ const match = content.match(/(?:export\s+default|module\.exports\s*=)\s*(\{[\s\S]*?\})\s*;?\s*$/m)
2754
3134
  if (match) {
2755
3135
  try {
2756
3136
  const obj = (new Function('return ' + match[1]))()
2757
- if (name.toLowerCase() === 'index' || name.toLowerCase() === 'designsystem') {
2758
- Object.assign(ds, obj)
3137
+ if (nameLower === 'index' || nameLower === 'designsystem') {
3138
+ for (const [k, v] of Object.entries(obj)) {
3139
+ if (!ds[k] && !ds[k.toLowerCase()]) ds[k] = v
3140
+ }
2759
3141
  } else {
2760
3142
  ds[name] = obj
2761
3143
  }
@@ -2767,7 +3149,7 @@
2767
3149
  return ds
2768
3150
  }
2769
3151
 
2770
- function renderDSCategory (container, name, value, path) {
3152
+ function renderDSCategory (container, name, value, path, isOriginal) {
2771
3153
  const section = document.createElement('div')
2772
3154
  section.className = 'ds-category'
2773
3155
 
@@ -2797,7 +3179,7 @@
2797
3179
  body.className = 'ds-category-body'
2798
3180
  body.style.display = 'none'
2799
3181
 
2800
- renderDSValue(body, value, path, 0)
3182
+ renderDSValue(body, value, path, 0, isOriginal)
2801
3183
 
2802
3184
  section.appendChild(body)
2803
3185
  container.appendChild(section)
@@ -2811,7 +3193,7 @@
2811
3193
  })
2812
3194
  }
2813
3195
 
2814
- function renderDSValue (container, value, path, depth) {
3196
+ function renderDSValue (container, value, path, depth, isOriginal) {
2815
3197
  if (depth > 6) return
2816
3198
  if (value === null || value === undefined) return
2817
3199
  if (value && value.__type === 'function') {
@@ -2860,7 +3242,7 @@
2860
3242
 
2861
3243
  const subBody = document.createElement('div')
2862
3244
  subBody.style.display = 'none'
2863
- renderDSValue(subBody, val, [...path, key], depth + 1)
3245
+ renderDSValue(subBody, val, [...path, key], depth + 1, isOriginal)
2864
3246
  container.appendChild(subBody)
2865
3247
 
2866
3248
  let subExpanded = false
@@ -2898,19 +3280,23 @@
2898
3280
  row.appendChild(colon)
2899
3281
  row.appendChild(valSpan)
2900
3282
 
2901
- // Make editable on click
2902
- row.style.cursor = 'pointer'
2903
- row.addEventListener('click', (e) => {
2904
- e.stopPropagation()
2905
- editDSValue(row, path, key, val, valSpan)
2906
- })
3283
+ // Make editable on click — only for original, computed is read-only
3284
+ if (isOriginal) {
3285
+ row.style.cursor = 'pointer'
3286
+ row.addEventListener('click', (e) => {
3287
+ e.stopPropagation()
3288
+ editDSValue(row, path, key, val, valSpan, isOriginal)
3289
+ })
3290
+ } else {
3291
+ row.classList.add('ds-readonly')
3292
+ }
2907
3293
 
2908
3294
  container.appendChild(row)
2909
3295
  }
2910
3296
  }
2911
3297
  }
2912
3298
 
2913
- function editDSValue (row, path, key, currentVal, valSpan) {
3299
+ function editDSValue (row, path, key, currentVal, valSpan, isOriginal) {
2914
3300
  if (row.querySelector('input')) return // already editing
2915
3301
 
2916
3302
  const input = document.createElement('input')
@@ -2919,10 +3305,85 @@
2919
3305
  input.value = typeof currentVal === 'string' ? currentVal : JSON.stringify(currentVal)
2920
3306
  input.style.width = '120px'
2921
3307
 
3308
+ // Build autocomplete for DS values
3309
+ const dsSuggestions = getDSSuggestionsForPath(path, key)
3310
+ let dropdown = null
3311
+ let filtered = []
3312
+ let activeIndex = -1
3313
+
2922
3314
  valSpan.style.display = 'none'
2923
3315
  row.appendChild(input)
3316
+
3317
+ if (dsSuggestions.length > 0) {
3318
+ dropdown = document.createElement('div')
3319
+ dropdown.className = 'prop-dropdown'
3320
+ row.appendChild(dropdown)
3321
+ }
3322
+
3323
+ function filterSuggestions (text) {
3324
+ const lower = (text || '').toLowerCase()
3325
+ filtered = lower
3326
+ ? dsSuggestions.filter(s => s.label.toLowerCase().includes(lower))
3327
+ : dsSuggestions.slice()
3328
+ if (filtered.length > 30) filtered = filtered.slice(0, 30)
3329
+ activeIndex = -1
3330
+ renderDDItems()
3331
+ }
3332
+
3333
+ function renderDDItems () {
3334
+ if (!dropdown) return
3335
+ dropdown.innerHTML = ''
3336
+ if (filtered.length === 0) { dropdown.style.display = 'none'; return }
3337
+ dropdown.style.display = ''
3338
+ for (let i = 0; i < filtered.length; i++) {
3339
+ const s = filtered[i]
3340
+ const item = document.createElement('div')
3341
+ item.className = 'prop-dropdown-item' + (i === activeIndex ? ' active' : '')
3342
+ if (s.hex !== undefined) {
3343
+ const sw = document.createElement('span')
3344
+ sw.className = 'dd-swatch'
3345
+ sw.style.background = s.hex || 'repeating-conic-gradient(#555 0% 25%, transparent 0% 50%) 50%/8px 8px'
3346
+ item.appendChild(sw)
3347
+ }
3348
+ const label = document.createElement('span')
3349
+ label.className = 'dd-label'
3350
+ label.textContent = s.label
3351
+ item.appendChild(label)
3352
+ if (s.hint) {
3353
+ const hint = document.createElement('span')
3354
+ hint.className = 'dd-hint'
3355
+ hint.textContent = s.hint
3356
+ item.appendChild(hint)
3357
+ }
3358
+ item.addEventListener('mousedown', (e) => {
3359
+ e.preventDefault()
3360
+ input.value = s.label
3361
+ commit()
3362
+ })
3363
+ dropdown.appendChild(item)
3364
+ }
3365
+ }
3366
+
3367
+ input.addEventListener('input', () => filterSuggestions(input.value))
3368
+ input.addEventListener('keydown', (e) => {
3369
+ if (dropdown && filtered.length > 0) {
3370
+ if (e.key === 'ArrowDown') { e.preventDefault(); activeIndex = Math.min(activeIndex + 1, filtered.length - 1); renderDDItems() }
3371
+ else if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex = Math.max(activeIndex - 1, 0); renderDDItems() }
3372
+ else if (e.key === 'Enter' && activeIndex >= 0) { e.preventDefault(); input.value = filtered[activeIndex].label; commit(); return }
3373
+ }
3374
+ if (e.key === 'Enter') { e.preventDefault(); commit() }
3375
+ if (e.key === 'Escape') { cleanup() }
3376
+ })
3377
+
2924
3378
  input.focus()
2925
3379
  input.select()
3380
+ if (dsSuggestions.length > 0) filterSuggestions(input.value)
3381
+
3382
+ function cleanup () {
3383
+ if (dropdown) dropdown.remove()
3384
+ input.remove()
3385
+ valSpan.style.display = ''
3386
+ }
2926
3387
 
2927
3388
  const commit = async () => {
2928
3389
  let newVal = input.value.trim()
@@ -2932,32 +3393,126 @@
2932
3393
  else if (newVal === 'null') newVal = null
2933
3394
  else if (!isNaN(Number(newVal)) && newVal !== '') newVal = Number(newVal)
2934
3395
 
2935
- // Update via pageEval on root.context.designSystem
2936
3396
  const fullPath = [...path, key]
3397
+
3398
+ if (isOriginal) {
3399
+ // Update source file via updateDesignSystem pattern
3400
+ // Path: e.g. ['color', 'primary'] -> 'COLOR.primary'
3401
+ const dsKey = fullPath.length > 1
3402
+ ? fullPath[0].toUpperCase() + '.' + fullPath.slice(1).join('.')
3403
+ : fullPath[0]
3404
+ if (connectionMode === 'local') {
3405
+ // Find and update the source file
3406
+ const category = fullPath[0]
3407
+ const dsFilePath = findDSSourceFile(category)
3408
+ if (dsFilePath) {
3409
+ await updateDSSourceFile(dsFilePath, fullPath.slice(1), newVal)
3410
+ setStatus('Updated ' + dsKey + ' in source')
3411
+ } else {
3412
+ setStatus('Source file not found for ' + category)
3413
+ }
3414
+ } else {
3415
+ setStatus('Source editing requires local folder connection')
3416
+ }
3417
+ }
3418
+
3419
+ // Always update runtime too
2937
3420
  const pathStr = fullPath.map(p => '["' + p.replace(/"/g, '\\"') + '"]').join('')
2938
3421
  const valExpr = typeof newVal === 'string' ? '"' + newVal.replace(/"/g, '\\"') + '"' : String(newVal)
2939
-
2940
3422
  try {
2941
3423
  await pageEval(`(function(){
2942
3424
  var root = window.__DOMQL_INSPECTOR__.findRoot();
2943
3425
  if (!root || !root.context || !root.context.designSystem) return;
2944
3426
  root.context.designSystem${pathStr} = ${valExpr};
2945
3427
  })()`)
2946
- setStatus('Updated ' + key)
3428
+ if (!isOriginal) setStatus('Updated ' + key + ' (runtime)')
2947
3429
  } catch (e) {
2948
- setStatus('Error: ' + e.message)
3430
+ if (!isOriginal) setStatus('Error: ' + e.message)
2949
3431
  }
2950
3432
 
2951
- input.remove()
3433
+ // Update cached DS
3434
+ if (cachedDesignSystem) {
3435
+ let obj = cachedDesignSystem
3436
+ for (let i = 0; i < fullPath.length - 1; i++) {
3437
+ if (!obj[fullPath[i]]) obj[fullPath[i]] = {}
3438
+ obj = obj[fullPath[i]]
3439
+ }
3440
+ obj[fullPath[fullPath.length - 1]] = newVal
3441
+ }
3442
+
3443
+ cleanup()
2952
3444
  valSpan.textContent = String(newVal ?? '')
2953
- valSpan.style.display = ''
2954
3445
  }
2955
3446
 
2956
- input.addEventListener('keydown', (e) => {
2957
- if (e.key === 'Enter') { e.preventDefault(); commit() }
2958
- if (e.key === 'Escape') { input.remove(); valSpan.style.display = '' }
3447
+ input.addEventListener('blur', () => {
3448
+ setTimeout(() => { if (document.activeElement !== input) commit() }, 150)
2959
3449
  })
2960
- input.addEventListener('blur', commit)
3450
+ }
3451
+
3452
+ // Find the source file in fileCache for a DS category
3453
+ function findDSSourceFile (category) {
3454
+ const lc = category.toLowerCase()
3455
+ for (const path of Object.keys(fileCache)) {
3456
+ if (!/designSystem/i.test(path)) continue
3457
+ const filename = path.split('/').pop().replace(/\.(js|jsx|ts|tsx|json)$/i, '').toLowerCase()
3458
+ if (filename === lc || filename === category) return path
3459
+ }
3460
+ return null
3461
+ }
3462
+
3463
+ // Update a value in a DS source file (basic string replacement)
3464
+ async function updateDSSourceFile (filePath, keyPath, newVal) {
3465
+ const content = fileCache[filePath]
3466
+ if (!content) return
3467
+
3468
+ const key = keyPath[keyPath.length - 1]
3469
+ const valStr = typeof newVal === 'string' ? "'" + newVal.replace(/'/g, "\\'") + "'" : String(newVal)
3470
+
3471
+ // Try to find and replace the key-value pair
3472
+ const patterns = [
3473
+ new RegExp("(\\b" + escapeRegExp(key) + "\\s*:\\s*)(['\"][^'\"]*['\"]|[\\d.]+|true|false|null)", 'm'),
3474
+ new RegExp("(['\"]" + escapeRegExp(key) + "['\"]\\s*:\\s*)(['\"][^'\"]*['\"]|[\\d.]+|true|false|null)", 'm')
3475
+ ]
3476
+
3477
+ let updated = content
3478
+ let replaced = false
3479
+ for (const pat of patterns) {
3480
+ if (pat.test(updated)) {
3481
+ updated = updated.replace(pat, '$1' + valStr)
3482
+ replaced = true
3483
+ break
3484
+ }
3485
+ }
3486
+
3487
+ if (replaced) {
3488
+ fileCache[filePath] = updated
3489
+ // Save to IndexedDB
3490
+ try {
3491
+ const db = await openDB()
3492
+ const tx = db.transaction(FILES_STORE, 'readwrite')
3493
+ tx.objectStore(FILES_STORE).put(fileCache, folderName)
3494
+ } catch (e) { /* ignore */ }
3495
+ }
3496
+ }
3497
+
3498
+ function escapeRegExp (s) {
3499
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
3500
+ }
3501
+
3502
+ // Get autocomplete suggestions based on DS path context
3503
+ function getDSSuggestionsForPath (path, key) {
3504
+ if (!cachedDesignSystem) return []
3505
+ const category = (path[0] || '').toLowerCase()
3506
+
3507
+ // For color categories, suggest existing color values
3508
+ if (category === 'color' || category === 'colors') {
3509
+ return extractDSColorSuggestions()
3510
+ }
3511
+ // For theme categories, suggest theme names
3512
+ if (category === 'theme' || category === 'themes') {
3513
+ return extractDSKeys('theme').concat(extractDSKeys('THEME')).map(k => ({ label: k }))
3514
+ }
3515
+ return []
2961
3516
  }
2962
3517
 
2963
3518
  function renderChildrenTab () {
@@ -3034,13 +3589,44 @@
3034
3589
  const item = document.createElement('div')
3035
3590
  item.className = 'method-item'
3036
3591
 
3592
+ const nameRow = document.createElement('div')
3593
+ nameRow.className = 'method-name-row'
3594
+
3037
3595
  const name = document.createElement('span')
3038
3596
  name.className = 'method-name'
3039
3597
  name.textContent = method + '()'
3040
3598
 
3041
- const argsInput = document.createElement('input')
3042
- argsInput.className = 'method-args'
3043
- argsInput.placeholder = 'args (JSON)'
3599
+ const toggleBtn = document.createElement('button')
3600
+ toggleBtn.className = 'method-toggle'
3601
+ toggleBtn.textContent = '\u25B6'
3602
+ toggleBtn.title = 'Show input'
3603
+
3604
+ nameRow.appendChild(name)
3605
+ nameRow.appendChild(toggleBtn)
3606
+ item.appendChild(nameRow)
3607
+
3608
+ // Collapsible input area
3609
+ const inputArea = document.createElement('div')
3610
+ inputArea.className = 'method-input-area'
3611
+ inputArea.style.display = 'none'
3612
+
3613
+ const argsTextarea = document.createElement('textarea')
3614
+ argsTextarea.className = 'method-args-textarea'
3615
+ argsTextarea.placeholder = '// JSON or object literal\n{\n "key": "value"\n}'
3616
+ argsTextarea.rows = 6
3617
+ argsTextarea.spellcheck = false
3618
+ argsTextarea.addEventListener('keydown', (e) => {
3619
+ if (e.key === 'Tab') {
3620
+ e.preventDefault()
3621
+ const start = argsTextarea.selectionStart
3622
+ const end = argsTextarea.selectionEnd
3623
+ argsTextarea.value = argsTextarea.value.substring(0, start) + ' ' + argsTextarea.value.substring(end)
3624
+ argsTextarea.selectionStart = argsTextarea.selectionEnd = start + 2
3625
+ }
3626
+ })
3627
+
3628
+ const btnRow = document.createElement('div')
3629
+ btnRow.className = 'method-btn-row'
3044
3630
 
3045
3631
  const btn = document.createElement('button')
3046
3632
  btn.className = 'method-btn'
@@ -3049,15 +3635,43 @@
3049
3635
  const result = document.createElement('span')
3050
3636
  result.className = 'method-result'
3051
3637
 
3638
+ btnRow.appendChild(btn)
3639
+ btnRow.appendChild(result)
3640
+ inputArea.appendChild(argsTextarea)
3641
+ inputArea.appendChild(btnRow)
3642
+ item.appendChild(inputArea)
3643
+
3644
+ let expanded = false
3645
+ toggleBtn.addEventListener('click', () => {
3646
+ expanded = !expanded
3647
+ toggleBtn.textContent = expanded ? '\u25BC' : '\u25B6'
3648
+ inputArea.style.display = expanded ? 'block' : 'none'
3649
+ })
3650
+
3651
+ // Also toggle on name click
3652
+ name.style.cursor = 'pointer'
3653
+ name.addEventListener('click', () => {
3654
+ expanded = !expanded
3655
+ toggleBtn.textContent = expanded ? '\u25BC' : '\u25B6'
3656
+ inputArea.style.display = expanded ? 'block' : 'none'
3657
+ })
3658
+
3052
3659
  btn.addEventListener('click', async () => {
3053
3660
  let args = []
3054
- if (argsInput.value.trim()) {
3661
+ const raw = argsTextarea.value.trim()
3662
+ if (raw) {
3055
3663
  try {
3056
- args = JSON.parse('[' + argsInput.value + ']')
3664
+ // Try parsing as JSON array first, then as single arg
3665
+ if (raw.startsWith('[')) args = JSON.parse(raw)
3666
+ else args = [JSON.parse(raw)]
3057
3667
  } catch (e) {
3058
- result.textContent = 'Invalid JSON args'
3059
- result.style.color = 'var(--error-color)'
3060
- return
3668
+ // Try wrapping in array
3669
+ try { args = JSON.parse('[' + raw + ']') }
3670
+ catch (e2) {
3671
+ result.textContent = 'Invalid JSON/object'
3672
+ result.style.color = 'var(--error-color)'
3673
+ return
3674
+ }
3061
3675
  }
3062
3676
  }
3063
3677
 
@@ -3075,14 +3689,14 @@
3075
3689
  JSON.stringify(args) + '))'
3076
3690
  }
3077
3691
 
3078
- const raw = await pageEval(expr)
3079
- const res = JSON.parse(raw)
3692
+ const rawRes = await pageEval(expr)
3693
+ const res = JSON.parse(rawRes)
3080
3694
  if (res.error) {
3081
3695
  result.textContent = 'Error: ' + res.error
3082
3696
  result.style.color = 'var(--error-color)'
3083
3697
  } else {
3084
3698
  result.textContent = res.result !== undefined ? JSON.stringify(res.result).slice(0, 100) : 'OK'
3085
- result.style.color = 'var(--text-dim)'
3699
+ result.style.color = 'var(--type-color)'
3086
3700
  setTimeout(() => selectElement(selectedPath), 200)
3087
3701
  }
3088
3702
  } catch (e) {
@@ -3091,10 +3705,6 @@
3091
3705
  }
3092
3706
  })
3093
3707
 
3094
- item.appendChild(name)
3095
- item.appendChild(argsInput)
3096
- item.appendChild(btn)
3097
- item.appendChild(result)
3098
3708
  return item
3099
3709
  }
3100
3710
 
@@ -3163,89 +3773,135 @@
3163
3773
  const editor = document.createElement('div')
3164
3774
  editor.className = 'obj-editor'
3165
3775
 
3166
- function rebuild () {
3167
- editor.innerHTML = ''
3168
- const entries = isArray
3169
- ? workingCopy.map((v, i) => [String(i), v])
3170
- : Object.entries(workingCopy)
3776
+ // Recursively build editor for an object/array at any nesting depth
3777
+ function buildLevel (container, obj, depth, setParentValue) {
3778
+ container.innerHTML = ''
3779
+ const isArr = Array.isArray(obj)
3780
+ const entries = isArr
3781
+ ? obj.map((v, i) => [String(i), v])
3782
+ : Object.entries(obj)
3171
3783
 
3172
3784
  for (const [k, v] of entries) {
3173
3785
  const row = document.createElement('div')
3174
3786
  row.className = 'obj-editor-row'
3787
+ if (depth > 0) row.style.paddingLeft = (depth * 12) + 'px'
3175
3788
 
3176
- if (!isArray) {
3789
+ // Key label
3790
+ if (!isArr) {
3177
3791
  const keyInput = document.createElement('input')
3178
3792
  keyInput.className = 'prop-edit-input obj-key-input'
3179
3793
  keyInput.value = k
3180
3794
  keyInput.readOnly = true
3181
3795
  keyInput.title = k
3182
3796
  row.appendChild(keyInput)
3183
-
3184
- const colon = document.createElement('span')
3185
- colon.className = 'prop-colon'
3186
- colon.textContent = ': '
3187
- row.appendChild(colon)
3188
3797
  } else {
3189
3798
  const idx = document.createElement('span')
3190
3799
  idx.className = 'prop-key'
3191
3800
  idx.textContent = k
3192
3801
  idx.style.minWidth = '20px'
3193
3802
  row.appendChild(idx)
3194
-
3195
- const colon = document.createElement('span')
3196
- colon.className = 'prop-colon'
3197
- colon.textContent = ': '
3198
- row.appendChild(colon)
3199
3803
  }
3200
3804
 
3201
- const valInput = document.createElement('input')
3202
- valInput.className = 'prop-edit-input'
3203
- valInput.value = typeof v === 'string' ? v
3204
- : v === null ? 'null'
3205
- : typeof v === 'object' ? JSON.stringify(v)
3206
- : String(v)
3207
- valInput.dataset.origType = typeof v === 'string' ? 'string'
3208
- : v === null ? 'null'
3209
- : typeof v === 'object' ? 'json'
3210
- : typeof v
3211
- valInput.addEventListener('change', () => {
3212
- const parsed = parsePreservingType(valInput.value, valInput.dataset.origType)
3213
- if (isArray) workingCopy[parseInt(k)] = parsed
3214
- else workingCopy[k] = parsed
3215
- })
3216
- row.appendChild(valInput)
3217
-
3218
- // Delete button
3219
- const delBtn = document.createElement('button')
3220
- delBtn.className = 'obj-editor-del'
3221
- delBtn.textContent = '\u00d7'
3222
- delBtn.title = 'Remove'
3223
- delBtn.addEventListener('click', (e) => {
3224
- e.stopPropagation()
3225
- if (isArray) workingCopy.splice(parseInt(k), 1)
3226
- else delete workingCopy[k]
3227
- rebuild()
3228
- })
3229
- row.appendChild(delBtn)
3805
+ const colon = document.createElement('span')
3806
+ colon.className = 'prop-colon'
3807
+ colon.textContent = ': '
3808
+ row.appendChild(colon)
3230
3809
 
3231
- editor.appendChild(row)
3810
+ const isNested = v !== null && typeof v === 'object'
3811
+
3812
+ if (isNested) {
3813
+ // Nested object/array — show toggle + nested editor
3814
+ const nestedIsArr = Array.isArray(v)
3815
+ const preview = document.createElement('span')
3816
+ preview.className = 'obj-nested-preview'
3817
+ preview.textContent = nestedIsArr ? '[' + v.length + ']' : '{' + Object.keys(v).length + '}'
3818
+ preview.style.cursor = 'pointer'
3819
+ preview.style.color = 'var(--text-dim)'
3820
+ preview.style.fontSize = '10px'
3821
+ row.appendChild(preview)
3822
+
3823
+ // Delete button
3824
+ const delBtn = document.createElement('button')
3825
+ delBtn.className = 'obj-editor-del'
3826
+ delBtn.textContent = '\u00d7'
3827
+ delBtn.title = 'Remove'
3828
+ delBtn.addEventListener('click', (e) => {
3829
+ e.stopPropagation()
3830
+ if (isArr) obj.splice(parseInt(k), 1)
3831
+ else delete obj[k]
3832
+ if (setParentValue) setParentValue(obj)
3833
+ buildLevel(container, obj, depth, setParentValue)
3834
+ })
3835
+ row.appendChild(delBtn)
3836
+ container.appendChild(row)
3837
+
3838
+ // Nested container (collapsed by default for depth > 0)
3839
+ const nestedContainer = document.createElement('div')
3840
+ nestedContainer.className = 'obj-editor-nested'
3841
+ let nestedExpanded = depth === 0
3842
+ if (!nestedExpanded) nestedContainer.style.display = 'none'
3843
+ else buildLevel(nestedContainer, v, depth + 1, (newVal) => {
3844
+ if (isArr) obj[parseInt(k)] = newVal
3845
+ else obj[k] = newVal
3846
+ })
3847
+
3848
+ preview.addEventListener('click', () => {
3849
+ nestedExpanded = !nestedExpanded
3850
+ nestedContainer.style.display = nestedExpanded ? 'block' : 'none'
3851
+ if (nestedExpanded && nestedContainer.innerHTML === '') {
3852
+ buildLevel(nestedContainer, v, depth + 1, (newVal) => {
3853
+ if (isArr) obj[parseInt(k)] = newVal
3854
+ else obj[k] = newVal
3855
+ })
3856
+ }
3857
+ })
3858
+ container.appendChild(nestedContainer)
3859
+ } else {
3860
+ // Primitive value — editable input
3861
+ const valInput = document.createElement('input')
3862
+ valInput.className = 'prop-edit-input'
3863
+ valInput.value = v === null ? 'null' : String(v)
3864
+ valInput.dataset.origType = v === null ? 'null' : typeof v
3865
+ valInput.addEventListener('change', () => {
3866
+ const parsed = parsePreservingType(valInput.value, valInput.dataset.origType)
3867
+ if (isArr) obj[parseInt(k)] = parsed
3868
+ else obj[k] = parsed
3869
+ })
3870
+ row.appendChild(valInput)
3871
+
3872
+ // Delete button
3873
+ const delBtn = document.createElement('button')
3874
+ delBtn.className = 'obj-editor-del'
3875
+ delBtn.textContent = '\u00d7'
3876
+ delBtn.title = 'Remove'
3877
+ delBtn.addEventListener('click', (e) => {
3878
+ e.stopPropagation()
3879
+ if (isArr) obj.splice(parseInt(k), 1)
3880
+ else delete obj[k]
3881
+ if (setParentValue) setParentValue(obj)
3882
+ buildLevel(container, obj, depth, setParentValue)
3883
+ })
3884
+ row.appendChild(delBtn)
3885
+ container.appendChild(row)
3886
+ }
3232
3887
  }
3233
3888
 
3234
3889
  // Add entry button
3235
3890
  const addRow = document.createElement('div')
3236
3891
  addRow.className = 'obj-editor-row obj-editor-add'
3892
+ if (depth > 0) addRow.style.paddingLeft = (depth * 12) + 'px'
3237
3893
  const addBtn = document.createElement('button')
3238
3894
  addBtn.className = 'prop-add-btn'
3239
- addBtn.textContent = isArray ? '+ Add item' : '+ Add key'
3895
+ addBtn.textContent = isArr ? '+ Add item' : '+ Add key'
3240
3896
  addBtn.style.fontSize = '10px'
3241
3897
  addBtn.style.padding = '2px 6px'
3242
3898
  addBtn.addEventListener('click', (e) => {
3243
3899
  e.stopPropagation()
3244
- if (isArray) { workingCopy.push(''); rebuild() }
3900
+ if (isArr) { obj.push(''); buildLevel(container, obj, depth, setParentValue) }
3245
3901
  else {
3246
- // Inline key input instead of prompt()
3247
3902
  const keyRow = document.createElement('div')
3248
3903
  keyRow.className = 'obj-editor-row'
3904
+ if (depth > 0) keyRow.style.paddingLeft = (depth * 12) + 'px'
3249
3905
  const keyInput = document.createElement('input')
3250
3906
  keyInput.className = 'prop-edit-input'
3251
3907
  keyInput.placeholder = 'key name'
@@ -3253,25 +3909,27 @@
3253
3909
  keyRow.appendChild(keyInput)
3254
3910
  addRow.before(keyRow)
3255
3911
  keyInput.focus()
3912
+ const finishAdd = () => {
3913
+ const newKey = keyInput.value.trim()
3914
+ if (newKey && !(newKey in obj)) { obj[newKey] = ''; buildLevel(container, obj, depth, setParentValue) }
3915
+ else keyRow.remove()
3916
+ }
3256
3917
  keyInput.addEventListener('keydown', (ev) => {
3257
- if (ev.key === 'Enter') {
3258
- const k = keyInput.value.trim()
3259
- if (k && !(k in workingCopy)) { workingCopy[k] = ''; rebuild() }
3260
- else keyRow.remove()
3261
- }
3918
+ if (ev.key === 'Enter') finishAdd()
3262
3919
  if (ev.key === 'Escape') keyRow.remove()
3263
3920
  })
3264
- keyInput.addEventListener('blur', () => {
3265
- const k = keyInput.value.trim()
3266
- if (k && !(k in workingCopy)) { workingCopy[k] = ''; rebuild() }
3267
- else keyRow.remove()
3268
- })
3921
+ keyInput.addEventListener('blur', finishAdd)
3269
3922
  }
3270
3923
  })
3271
3924
  addRow.appendChild(addBtn)
3272
- editor.appendChild(addRow)
3925
+ container.appendChild(addRow)
3926
+ }
3273
3927
 
3274
- // Save / Cancel buttons
3928
+ function rebuild () {
3929
+ editor.innerHTML = ''
3930
+ buildLevel(editor, workingCopy, 0, null)
3931
+
3932
+ // Save / Cancel buttons (always at bottom of top-level editor)
3275
3933
  const actions = document.createElement('div')
3276
3934
  actions.className = 'obj-editor-actions'
3277
3935
  const saveBtn = document.createElement('button')
@@ -3306,24 +3964,7 @@
3306
3964
  }
3307
3965
 
3308
3966
  async function commitObj () {
3309
- // Read latest values from inputs
3310
- const inputs = editor.querySelectorAll('.obj-editor-row:not(.obj-editor-add)')
3311
- if (isArray) {
3312
- workingCopy = []
3313
- inputs.forEach(row => {
3314
- const inp = row.querySelector('.prop-edit-input')
3315
- if (inp) workingCopy.push(parsePreservingType(inp.value, inp.dataset.origType))
3316
- })
3317
- } else {
3318
- workingCopy = {}
3319
- inputs.forEach(row => {
3320
- const keyInp = row.querySelector('.obj-key-input')
3321
- const valInp = row.querySelectorAll('.prop-edit-input')[row.querySelector('.obj-key-input') ? 1 : 0]
3322
- if (keyInp && valInp) {
3323
- workingCopy[keyInp.value] = parsePreservingType(valInp.value, valInp.dataset.origType)
3324
- }
3325
- })
3326
- }
3967
+ // workingCopy is already updated in-place by input change handlers and recursive buildLevel
3327
3968
 
3328
3969
  el.classList.remove('editing')
3329
3970
  el.innerHTML = ''
@@ -3476,6 +4117,25 @@
3476
4117
  let newValue = input.value
3477
4118
  try { newValue = JSON.parse(newValue) } catch (e) {}
3478
4119
 
4120
+ // Skip update if value hasn't changed
4121
+ const unchanged = newValue === currentValue ||
4122
+ (typeof newValue === 'string' && typeof currentValue === 'string' && newValue === currentValue) ||
4123
+ JSON.stringify(newValue) === JSON.stringify(currentValue)
4124
+
4125
+ if (unchanged) {
4126
+ el.innerHTML = ''
4127
+ el.appendChild(renderValue(currentValue))
4128
+ if (opts && opts.tabNext) {
4129
+ setTimeout(() => {
4130
+ const allVals = Array.from(document.querySelectorAll('.prop-value.editable'))
4131
+ const idx = allVals.indexOf(el)
4132
+ const next = allVals[idx + (opts.tabBack ? -1 : 1)]
4133
+ if (next) next.click()
4134
+ }, 0)
4135
+ }
4136
+ return
4137
+ }
4138
+
3479
4139
  el.innerHTML = ''
3480
4140
  el.appendChild(renderValue(newValue))
3481
4141
 
@@ -4492,11 +5152,91 @@ Do NOT include any explanation, only valid JSON.`
4492
5152
  'hr', 'br', 'strong', 'em', 'b', 'i', 'u', 'small', 'mark'
4493
5153
  ]
4494
5154
 
5155
+ // Extract DS token suggestions from cached design system
5156
+ function extractDSColorSuggestions () {
5157
+ if (!cachedDesignSystem) return []
5158
+ const colors = cachedDesignSystem.color || cachedDesignSystem.COLOR || cachedDesignSystem.colors || {}
5159
+ const items = []
5160
+ for (const [k, v] of Object.entries(colors)) {
5161
+ if (k.startsWith('__')) continue
5162
+ if (typeof v === 'string') {
5163
+ items.push({ label: k, hex: /^(#|rgb|hsl)/.test(v) ? v : '', hint: v })
5164
+ }
5165
+ }
5166
+ return items
5167
+ }
5168
+
5169
+ function extractDSThemeSuggestions () {
5170
+ if (!cachedDesignSystem) return []
5171
+ const themes = cachedDesignSystem.theme || cachedDesignSystem.THEME || cachedDesignSystem.themes || {}
5172
+ const items = []
5173
+ for (const k of Object.keys(themes)) {
5174
+ if (k.startsWith('__')) continue
5175
+ items.push({ label: k, hint: 'theme' })
5176
+ }
5177
+ return items
5178
+ }
5179
+
5180
+ function extractDSKeys (category) {
5181
+ if (!cachedDesignSystem) return []
5182
+ const obj = cachedDesignSystem[category]
5183
+ if (!obj || typeof obj !== 'object') return []
5184
+ return Object.keys(obj).filter(k => !k.startsWith('__'))
5185
+ }
5186
+
5187
+ function extractDSSpacingSuggestions () {
5188
+ if (!cachedDesignSystem) return []
5189
+ const sp = cachedDesignSystem.spacing || cachedDesignSystem.SPACING || {}
5190
+ if (sp.base && sp.ratio) {
5191
+ // Generate tokens from base/ratio
5192
+ return generateTokens(sp.base, sp.ratio, 'px', -4, 7)
5193
+ }
5194
+ // Otherwise extract raw keys
5195
+ const items = []
5196
+ for (const [k, v] of Object.entries(sp)) {
5197
+ if (k.startsWith('__')) continue
5198
+ items.push({ label: k, hint: typeof v === 'number' ? v + 'px' : String(v) })
5199
+ }
5200
+ return items
5201
+ }
5202
+
5203
+ function extractDSFontSuggestions () {
5204
+ if (!cachedDesignSystem) return []
5205
+ const typo = cachedDesignSystem.typography || cachedDesignSystem.TYPOGRAPHY || {}
5206
+ if (typo.base && typo.ratio) {
5207
+ return generateTokens(typo.base, typo.ratio, 'px', -3, 7)
5208
+ }
5209
+ const font = cachedDesignSystem.font || cachedDesignSystem.FONT || {}
5210
+ const items = []
5211
+ for (const [k, v] of Object.entries(font)) {
5212
+ if (k.startsWith('__')) continue
5213
+ items.push({ label: k, hint: String(v) })
5214
+ }
5215
+ return items
5216
+ }
5217
+
5218
+ function extractDSGradientSuggestions () {
5219
+ if (!cachedDesignSystem) return []
5220
+ const gr = cachedDesignSystem.gradient || cachedDesignSystem.GRADIENT || {}
5221
+ return Object.keys(gr).filter(k => !k.startsWith('__')).map(k => ({ label: k, hint: 'gradient' }))
5222
+ }
5223
+
4495
5224
  function getSuggestionsForProp (propName) {
4496
5225
  // CSS enum values (display, position, flexDirection, etc.)
4497
5226
  if (CSS_ENUMS[propName]) return CSS_ENUMS[propName].map(v => ({ label: v }))
4498
- // Theme tokens with modifiers
5227
+
5228
+ // Theme tokens — prefer DS tokens, fall back to defaults
4499
5229
  if (propName === 'theme') {
5230
+ const dsThemes = extractDSThemeSuggestions()
5231
+ if (dsThemes.length > 0) {
5232
+ // Add modifiers to DS themes
5233
+ for (const t of dsThemes.slice()) {
5234
+ for (const mod of THEME_MODIFIERS) {
5235
+ dsThemes.push({ label: t.label + ' ' + mod, hint: 'modifier' })
5236
+ }
5237
+ }
5238
+ return dsThemes
5239
+ }
4500
5240
  const items = THEME_TOKENS.map(v => ({ label: v, hint: 'theme' }))
4501
5241
  for (const t of ['primary', 'secondary', 'card', 'dialog', 'label']) {
4502
5242
  for (const mod of THEME_MODIFIERS) {
@@ -4505,18 +5245,57 @@ Do NOT include any explanation, only valid JSON.`
4505
5245
  }
4506
5246
  return items
4507
5247
  }
4508
- // Color properties
5248
+
5249
+ // Color properties — prefer DS color tokens
4509
5250
  if (COLOR_PROPS.has(propName)) {
5251
+ const dsColors = extractDSColorSuggestions()
5252
+ if (dsColors.length > 0) {
5253
+ // Merge DS colors with gradients
5254
+ const dsGradients = extractDSGradientSuggestions()
5255
+ return [...dsColors, ...dsGradients, ...COLOR_TOKENS]
5256
+ }
4510
5257
  const items = COLOR_TOKENS.slice()
4511
5258
  for (const g of GRADIENT_TOKENS) items.push({ label: g, hint: 'gradient' })
4512
5259
  return items
4513
5260
  }
4514
- // Spacing / size tokens
4515
- if (SPACING_PROPS.has(propName)) return SPACING_TOKENS
4516
- // Font size tokens
4517
- if (FONT_SIZE_PROPS.has(propName)) return FONT_SIZE_TOKENS
5261
+
5262
+ // Spacing prefer DS spacing tokens
5263
+ if (SPACING_PROPS.has(propName)) {
5264
+ const dsSpacing = extractDSSpacingSuggestions()
5265
+ return dsSpacing.length > 0 ? dsSpacing : SPACING_TOKENS
5266
+ }
5267
+
5268
+ // Font size — prefer DS typography tokens
5269
+ if (FONT_SIZE_PROPS.has(propName)) {
5270
+ const dsFonts = extractDSFontSuggestions()
5271
+ return dsFonts.length > 0 ? dsFonts : FONT_SIZE_TOKENS
5272
+ }
5273
+
4518
5274
  // Timing tokens
4519
- if (propName === 'transition' || propName === 'transitionDuration' || propName === 'animationDuration') return TIMING_TOKENS
5275
+ if (propName === 'transition' || propName === 'transitionDuration' || propName === 'animationDuration') {
5276
+ const dsTiming = cachedDesignSystem && (cachedDesignSystem.timing || cachedDesignSystem.TIMING)
5277
+ if (dsTiming && typeof dsTiming === 'object') {
5278
+ const items = []
5279
+ for (const [k, v] of Object.entries(dsTiming)) {
5280
+ if (!k.startsWith('__')) items.push({ label: k, hint: String(v) })
5281
+ }
5282
+ if (items.length > 0) return items
5283
+ }
5284
+ return TIMING_TOKENS
5285
+ }
5286
+
5287
+ // Font family — from DS
5288
+ if (propName === 'fontFamily') {
5289
+ const ff = cachedDesignSystem && (cachedDesignSystem.font_family || cachedDesignSystem.FONT_FAMILY)
5290
+ if (ff && typeof ff === 'object') {
5291
+ const items = []
5292
+ for (const [k, v] of Object.entries(ff)) {
5293
+ if (!k.startsWith('__')) items.push({ label: k, hint: typeof v === 'string' ? v : '' })
5294
+ }
5295
+ if (items.length > 0) return items
5296
+ }
5297
+ }
5298
+
4520
5299
  // HTML tags
4521
5300
  if (propName === 'tag') return HTML_TAGS.map(v => ({ label: v }))
4522
5301
  return null
@@ -4541,6 +5320,8 @@ Do NOT include any explanation, only valid JSON.`
4541
5320
  p.classList.toggle('active', p.id === 'mode-' + mode)
4542
5321
  })
4543
5322
  if (mode === 'chat') updateChatContextLabel()
5323
+ if (mode === 'gallery') renderGallery()
5324
+ if (mode === 'content') renderContent()
4544
5325
  }
4545
5326
 
4546
5327
  // ============================================================
@@ -4738,10 +5519,64 @@ Do NOT include any explanation, only valid JSON.`
4738
5519
  initElementsSync()
4739
5520
  initAutoRefresh()
4740
5521
 
4741
- // Mode tabs (Editor / Chat)
5522
+ // Mode tabs (Editor / Chat / Gallery / Content)
4742
5523
  document.querySelectorAll('.mode-tab').forEach(tab => {
4743
5524
  tab.addEventListener('click', () => switchMode(tab.dataset.mode))
4744
5525
  })
5526
+
5527
+ // Gallery controls
5528
+ const gallerySearch = document.getElementById('gallery-search')
5529
+ if (gallerySearch) {
5530
+ gallerySearch.addEventListener('input', () => {
5531
+ const q = gallerySearch.value.toLowerCase()
5532
+ document.querySelectorAll('.gallery-card').forEach(card => {
5533
+ const name = card.querySelector('.gallery-card-name')?.textContent.toLowerCase() || ''
5534
+ card.style.display = name.includes(q) ? '' : 'none'
5535
+ })
5536
+ })
5537
+ }
5538
+ // Content controls
5539
+ const contentLang = document.getElementById('content-language')
5540
+ if (contentLang) {
5541
+ contentLang.addEventListener('change', () => {
5542
+ activeContentLang = contentLang.value
5543
+ renderContent()
5544
+ })
5545
+ }
5546
+ const addLangBtn = document.getElementById('content-add-lang')
5547
+ if (addLangBtn) {
5548
+ addLangBtn.addEventListener('click', () => {
5549
+ const code = window.prompt ? null : '' // prompt doesn't work in DevTools
5550
+ // Use inline input instead
5551
+ const toolbar = document.getElementById('content-toolbar')
5552
+ const inp = document.createElement('input')
5553
+ inp.className = 'content-lang-input'
5554
+ inp.placeholder = 'lang code (e.g. fr)'
5555
+ inp.style.width = '80px'
5556
+ toolbar.appendChild(inp)
5557
+ inp.focus()
5558
+ const add = () => {
5559
+ const code = inp.value.trim().toLowerCase()
5560
+ if (code && !contentLanguages.includes(code)) {
5561
+ contentLanguages.push(code)
5562
+ contentData[code] = {}
5563
+ const opt = document.createElement('option')
5564
+ opt.value = code
5565
+ opt.textContent = code.toUpperCase()
5566
+ contentLang.appendChild(opt)
5567
+ contentLang.value = code
5568
+ activeContentLang = code
5569
+ renderContent()
5570
+ }
5571
+ inp.remove()
5572
+ }
5573
+ inp.addEventListener('keydown', (e) => {
5574
+ if (e.key === 'Enter') add()
5575
+ if (e.key === 'Escape') inp.remove()
5576
+ })
5577
+ inp.addEventListener('blur', add)
5578
+ })
5579
+ }
4745
5580
  document.getElementById('btn-app-disconnect').addEventListener('click', disconnect)
4746
5581
 
4747
5582
  // Tree pane tabs (Active Nodes / State Tree / Design System)
@@ -4809,6 +5644,10 @@ Do NOT include any explanation, only valid JSON.`
4809
5644
  document.getElementById('btn-refresh').addEventListener('click', loadTree)
4810
5645
  document.getElementById('btn-inspect').addEventListener('click', inspectSelected)
4811
5646
  document.getElementById('btn-sync').addEventListener('click', syncChanges)
5647
+ document.getElementById('btn-sync-dropdown').addEventListener('click', toggleSyncChangesPanel)
5648
+ document.getElementById('sync-changes-close').addEventListener('click', () => {
5649
+ document.getElementById('sync-changes-panel').style.display = 'none'
5650
+ })
4812
5651
  document.getElementById('btn-undo').addEventListener('click', undo)
4813
5652
  document.getElementById('btn-redo').addEventListener('click', redo)
4814
5653
 
@@ -4923,6 +5762,474 @@ Do NOT include any explanation, only valid JSON.`
4923
5762
  if (connectionMode) loadTree()
4924
5763
  }
4925
5764
 
5765
+ // ============================================================
5766
+ // Gallery mode — display all components
5767
+ // ============================================================
5768
+ async function renderGallery () {
5769
+ const container = document.getElementById('gallery-container')
5770
+ const pagesRow = document.getElementById('gallery-pages')
5771
+ if (!container) return
5772
+ container.innerHTML = '<div class="empty-message">Loading...</div>'
5773
+ if (pagesRow) pagesRow.innerHTML = ''
5774
+
5775
+ // Get components from element.context.components and pages from element.context.pages
5776
+ let result = { components: [], pages: [], debug: '' }
5777
+ try {
5778
+ const raw = await pageEval(`(function(){
5779
+ var I = window.__DOMQL_INSPECTOR__;
5780
+ if (!I) return JSON.stringify({components:[],pages:[],debug:'no inspector'});
5781
+ var el = I.findRoot();
5782
+ if (!el) return JSON.stringify({components:[],pages:[],debug:'no root'});
5783
+
5784
+ // Find context - may be on root or on a child element
5785
+ var ctx = el.context;
5786
+ if (!ctx) {
5787
+ // Walk children to find an element with context
5788
+ function findCtx(e, d) {
5789
+ if (d > 4 || !e) return null;
5790
+ if (e.context) return e.context;
5791
+ for (var k in e) {
5792
+ if (k === 'parent' || k === 'node' || k === 'state' || k.startsWith('__')) continue;
5793
+ if (e[k] && typeof e[k] === 'object' && e[k].node) {
5794
+ var found = findCtx(e[k], d + 1);
5795
+ if (found) return found;
5796
+ }
5797
+ }
5798
+ return null;
5799
+ }
5800
+ ctx = findCtx(el, 0);
5801
+ }
5802
+ if (!ctx) return JSON.stringify({components:[],pages:[],debug:'no context on root or children. root key=' + (el.key||'?') + ' has keys: ' + Object.keys(el).slice(0,15).join(',')});
5803
+
5804
+ var comps = [];
5805
+ var pages = [];
5806
+
5807
+ // Count component usage in the live tree
5808
+ var usage = {};
5809
+ function countUsage(e, d) {
5810
+ if (d > 20 || !e || !e.node) return;
5811
+ var cname = (e.__ref && e.__ref.__name) || e.component || '';
5812
+ if (cname) usage[cname] = (usage[cname] || 0) + 1;
5813
+ // Also count by key if it's CapitalCase
5814
+ if (e.key && /^[A-Z]/.test(e.key) && e.key !== cname) {
5815
+ usage[e.key] = (usage[e.key] || 0) + 1;
5816
+ }
5817
+ for (var ck in e) {
5818
+ if (ck === 'parent' || ck === 'node' || ck === 'context' || ck === 'state' || ck.startsWith('__')) continue;
5819
+ if (e[ck] && typeof e[ck] === 'object' && e[ck].node) countUsage(e[ck], d + 1);
5820
+ }
5821
+ }
5822
+ countUsage(el, 0);
5823
+
5824
+ // Extract context.components (CapitalCase keys)
5825
+ var src = ctx.components || {};
5826
+ for (var k in src) {
5827
+ if (k[0] === k[0].toUpperCase() && k[0] !== '_' && /^[A-Z]/.test(k)) {
5828
+ comps.push({ name: k, count: usage[k] || 0 });
5829
+ }
5830
+ }
5831
+
5832
+ // Extract context.pages
5833
+ var pgs = ctx.pages || {};
5834
+ for (var k in pgs) {
5835
+ if (k.startsWith('__')) continue;
5836
+ try {
5837
+ pages.push({ name: k, path: pgs[k] && pgs[k].path ? pgs[k].path : '/' + k });
5838
+ } catch(e) {}
5839
+ }
5840
+
5841
+ var debug = 'ctx keys: ' + Object.keys(ctx).slice(0,20).join(',') + '; components keys: ' + Object.keys(src).slice(0,10).join(',');
5842
+ return JSON.stringify({ components: comps, pages: pages, debug: debug });
5843
+ })()`)
5844
+ if (raw) result = JSON.parse(raw)
5845
+ } catch (e) { result.debug = 'parse error: ' + e.message }
5846
+
5847
+ // Render pages row
5848
+ if (pagesRow && result.pages.length > 0) {
5849
+ const label = document.createElement('span')
5850
+ label.className = 'gallery-section-label'
5851
+ label.textContent = 'Pages'
5852
+ pagesRow.appendChild(label)
5853
+
5854
+ for (const page of result.pages) {
5855
+ const chip = document.createElement('button')
5856
+ chip.className = 'gallery-page-chip'
5857
+ chip.textContent = page.name
5858
+ chip.title = page.path || ''
5859
+ chip.addEventListener('click', () => {
5860
+ // Navigate to page in the inspected tab
5861
+ const path = page.path.startsWith('/') ? page.path : '/' + page.path
5862
+ pageEval('window.location.pathname = ' + JSON.stringify(path)).catch(() => {})
5863
+ })
5864
+ pagesRow.appendChild(chip)
5865
+ }
5866
+ }
5867
+
5868
+ // Render components grid
5869
+ container.innerHTML = ''
5870
+
5871
+ if (result.components.length === 0) {
5872
+ container.innerHTML = '<div class="empty-message">No components found' + (result.debug ? '<br><small style="opacity:0.5">' + result.debug + '</small>' : '') + '</div>'
5873
+ return
5874
+ }
5875
+
5876
+ for (const comp of result.components) {
5877
+ const card = document.createElement('div')
5878
+ card.className = 'gallery-card'
5879
+
5880
+ const name = document.createElement('div')
5881
+ name.className = 'gallery-card-name'
5882
+ name.textContent = comp.name
5883
+
5884
+ if (comp.count > 0) {
5885
+ const count = document.createElement('span')
5886
+ count.className = 'gallery-card-count'
5887
+ count.textContent = comp.count
5888
+ count.title = 'Used ' + comp.count + ' time' + (comp.count === 1 ? '' : 's')
5889
+ name.appendChild(count)
5890
+ }
5891
+
5892
+ card.appendChild(name)
5893
+
5894
+ card.addEventListener('click', () => {
5895
+ // Try to find this component in the tree and select it
5896
+ switchMode('editor')
5897
+ if (treeData) {
5898
+ const path = findComponentPath(comp.name)
5899
+ if (path) {
5900
+ selectElement(path)
5901
+ highlightElement(path)
5902
+ expandAndSelectTreePath(path)
5903
+ }
5904
+ }
5905
+ })
5906
+
5907
+ container.appendChild(card)
5908
+ }
5909
+ }
5910
+
5911
+ // Find the tree path of a component by its name (walks tree looking for matching __ref.__name or key)
5912
+ function findComponentPath (name) {
5913
+ if (!treeData) return null
5914
+ function walk (node, path) {
5915
+ if (!node || !node.children) return null
5916
+ for (const child of node.children) {
5917
+ const childPath = path ? path + '.' + child.key : child.key
5918
+ if (child.key === name || child.component === name) return childPath
5919
+ const found = walk(child, childPath)
5920
+ if (found) return found
5921
+ }
5922
+ return null
5923
+ }
5924
+ return walk(treeData, '')
5925
+ }
5926
+
5927
+ // ============================================================
5928
+ // Content mode — CMS-like environment with languages
5929
+ // ============================================================
5930
+ let contentData = {} // { en: { key: value }, fr: { key: value } }
5931
+ let contentLanguages = ['en']
5932
+ let activeContentLang = 'en'
5933
+
5934
+ let contentSelectedKey = null // currently selected sidebar key
5935
+
5936
+ async function renderContent () {
5937
+ const sidebar = document.getElementById('content-sidebar')
5938
+ const main = document.getElementById('content-main')
5939
+ if (!sidebar || !main) return
5940
+ sidebar.innerHTML = '<div class="empty-message">Loading...</div>'
5941
+ main.innerHTML = ''
5942
+
5943
+ // Extract root state (same data source as the State Tree tab)
5944
+ let rootState = {}
5945
+ let contentDebug = ''
5946
+ try {
5947
+ const raw = await pageEval(`(function(){
5948
+ var I = window.__DOMQL_INSPECTOR__;
5949
+ if (!I) return JSON.stringify({data:{},debug:'no inspector'});
5950
+ var el = I.findRoot();
5951
+ if (!el) return JSON.stringify({data:{},debug:'no root'});
5952
+
5953
+ // Find state - may be on root or on a child element
5954
+ var st = el.state;
5955
+ if (!st || typeof st !== 'object' || !Object.keys(st).some(function(k){ return k !== 'parent' && k !== 'update' && !k.startsWith('__') && typeof st[k] !== 'function'; })) {
5956
+ // Walk children to find element with meaningful state
5957
+ function findState(e, d) {
5958
+ if (d > 4 || !e) return null;
5959
+ for (var k in e) {
5960
+ if (k === 'parent' || k === 'node' || k === 'context' || k.startsWith('__')) continue;
5961
+ if (e[k] && typeof e[k] === 'object' && e[k].node) {
5962
+ if (e[k].state && typeof e[k].state === 'object') {
5963
+ var hasData = Object.keys(e[k].state).some(function(sk){ return sk !== 'parent' && sk !== 'update' && !sk.startsWith('__') && typeof e[k].state[sk] !== 'function'; });
5964
+ if (hasData) return e[k].state;
5965
+ }
5966
+ var found = findState(e[k], d + 1);
5967
+ if (found) return found;
5968
+ }
5969
+ }
5970
+ return null;
5971
+ }
5972
+ var childState = findState(el, 0);
5973
+ if (childState) st = childState;
5974
+ }
5975
+ if (!st) return JSON.stringify({data:{},debug:'no state found on root or children'});
5976
+ var SKIP = {parent:1,root:1,update:1,set:1,reset:1,replace:1,toggle:1,
5977
+ remove:1,add:1,apply:1,setByPath:1,parse:1,clean:1,destroy:1,
5978
+ create:1,quietUpdate:1,__element:1,__depends:1,__ref:1};
5979
+ var result = {};
5980
+ var stKeys = [];
5981
+ var skipped = [];
5982
+ for (var k in st) {
5983
+ if (SKIP[k] || k.startsWith('__') || typeof st[k] === 'function') continue;
5984
+ stKeys.push(k);
5985
+ try {
5986
+ var v = st[k];
5987
+ var t = typeof v;
5988
+ if (v === null || t === 'string' || t === 'number' || t === 'boolean') {
5989
+ result[k] = { type: 'primitive', value: v };
5990
+ } else if (Array.isArray(v)) {
5991
+ result[k] = { type: 'array', length: v.length, value: JSON.parse(JSON.stringify(v.slice(0, 50))) };
5992
+ } else if (t === 'object') {
5993
+ if (v.node) { skipped.push(k + '(node)'); continue; }
5994
+ if (v.parent === st) { skipped.push(k + '(childState)'); continue; }
5995
+ var keys = Object.keys(v).filter(function(kk){ return !kk.startsWith('__') && typeof v[kk] !== 'function'; });
5996
+ var obj = {};
5997
+ keys.slice(0, 100).forEach(function(kk){ try { obj[kk] = JSON.parse(JSON.stringify(v[kk])); } catch(e){} });
5998
+ result[k] = { type: 'object', keys: keys.length, value: obj };
5999
+ }
6000
+ } catch(e) { skipped.push(k + '(err:' + e.message + ')'); }
6001
+ }
6002
+ return JSON.stringify({data:result, debug:'keys: ' + stKeys.join(',') + '; skipped: ' + skipped.join(',') + '; resultKeys: ' + Object.keys(result).join(',')});
6003
+ })()`)
6004
+ if (raw) {
6005
+ const parsed = JSON.parse(raw)
6006
+ rootState = parsed.data || {}
6007
+ contentDebug = parsed.debug || ''
6008
+ }
6009
+ } catch (e) { contentDebug = 'parse error: ' + e.message }
6010
+
6011
+ sidebar.innerHTML = ''
6012
+
6013
+ // Sidebar: show keys that have objects or arrays as values
6014
+ const sidebarKeys = []
6015
+ const primitiveKeys = []
6016
+ for (const [key, info] of Object.entries(rootState)) {
6017
+ if (info.type === 'object' || info.type === 'array') {
6018
+ sidebarKeys.push(key)
6019
+ } else {
6020
+ primitiveKeys.push(key)
6021
+ }
6022
+ }
6023
+
6024
+ if (sidebarKeys.length === 0 && primitiveKeys.length === 0) {
6025
+ sidebar.innerHTML = '<div class="empty-message">No root state data' + (contentDebug ? '<br><small style="opacity:0.5">' + contentDebug + '</small>' : '') + '</div>'
6026
+ main.innerHTML = ''
6027
+ return
6028
+ }
6029
+
6030
+ // Temporary debug
6031
+ if (contentDebug) {
6032
+ const dbg = document.createElement('div')
6033
+ dbg.style.cssText = 'font-size:9px;color:var(--text-dim);padding:4px 8px;opacity:0.5'
6034
+ dbg.textContent = contentDebug
6035
+ sidebar.appendChild(dbg)
6036
+ }
6037
+
6038
+ // Render sidebar items
6039
+ for (const key of sidebarKeys) {
6040
+ const info = rootState[key]
6041
+ const item = document.createElement('div')
6042
+ item.className = 'content-sidebar-item'
6043
+ if (contentSelectedKey === key) item.classList.add('active')
6044
+
6045
+ const label = document.createElement('span')
6046
+ label.className = 'content-sidebar-label'
6047
+ label.textContent = key
6048
+
6049
+ const badge = document.createElement('span')
6050
+ badge.className = 'content-sidebar-badge'
6051
+ badge.textContent = info.type === 'array' ? '[' + info.length + ']' : '{' + info.keys + '}'
6052
+
6053
+ item.appendChild(label)
6054
+ item.appendChild(badge)
6055
+ item.addEventListener('click', () => {
6056
+ contentSelectedKey = key
6057
+ renderContentMain(main, key, rootState[key])
6058
+ // Update active
6059
+ sidebar.querySelectorAll('.content-sidebar-item').forEach(i => i.classList.remove('active'))
6060
+ item.classList.add('active')
6061
+ })
6062
+ sidebar.appendChild(item)
6063
+ }
6064
+
6065
+ // Auto-select first sidebar item, or show primitives
6066
+ if (contentSelectedKey && rootState[contentSelectedKey]) {
6067
+ renderContentMain(main, contentSelectedKey, rootState[contentSelectedKey])
6068
+ } else if (sidebarKeys.length > 0) {
6069
+ contentSelectedKey = sidebarKeys[0]
6070
+ sidebar.querySelector('.content-sidebar-item')?.classList.add('active')
6071
+ renderContentMain(main, sidebarKeys[0], rootState[sidebarKeys[0]])
6072
+ } else {
6073
+ renderContentPrimitives(main, primitiveKeys, rootState)
6074
+ }
6075
+
6076
+ // Show primitives section at bottom of main if there are any
6077
+ if (primitiveKeys.length > 0 && sidebarKeys.length > 0) {
6078
+ const primSection = document.createElement('div')
6079
+ primSection.className = 'content-primitives-section'
6080
+ const primHeader = document.createElement('div')
6081
+ primHeader.className = 'section-header'
6082
+ primHeader.textContent = 'Root Values'
6083
+ primSection.appendChild(primHeader)
6084
+ renderContentPrimitives(primSection, primitiveKeys, rootState)
6085
+ main.appendChild(primSection)
6086
+ }
6087
+ }
6088
+
6089
+ function renderContentMain (container, key, info) {
6090
+ // Clear previous content (keep primitives section if any)
6091
+ const primSection = container.querySelector('.content-primitives-section')
6092
+ container.innerHTML = ''
6093
+
6094
+ const header = document.createElement('div')
6095
+ header.className = 'content-main-header'
6096
+ header.textContent = key + (info.type === 'array' ? ' [' + info.length + ' items]' : '')
6097
+ container.appendChild(header)
6098
+
6099
+ const data = info.value
6100
+ if (info.type === 'array') {
6101
+ for (let i = 0; i < data.length; i++) {
6102
+ const item = data[i]
6103
+ if (item && typeof item === 'object') {
6104
+ renderContentObject(container, key, i, item)
6105
+ } else {
6106
+ renderContentField(container, key + '[' + i + ']', String(i), item, key, i)
6107
+ }
6108
+ }
6109
+ } else if (info.type === 'object') {
6110
+ for (const [subKey, val] of Object.entries(data)) {
6111
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
6112
+ renderContentObject(container, key, subKey, val)
6113
+ } else {
6114
+ renderContentField(container, key + '.' + subKey, subKey, val, key, subKey)
6115
+ }
6116
+ }
6117
+ }
6118
+
6119
+ if (primSection) container.appendChild(primSection)
6120
+ }
6121
+
6122
+ function renderContentObject (container, stateKey, index, obj) {
6123
+ const card = document.createElement('div')
6124
+ card.className = 'content-object-card'
6125
+
6126
+ const cardHeader = document.createElement('div')
6127
+ cardHeader.className = 'content-object-header'
6128
+ cardHeader.textContent = typeof index === 'number' ? '#' + index : index
6129
+ card.appendChild(cardHeader)
6130
+
6131
+ for (const [k, v] of Object.entries(obj)) {
6132
+ if (k.startsWith('__') || typeof v === 'function') continue
6133
+ const row = document.createElement('div')
6134
+ row.className = 'content-field'
6135
+
6136
+ const label = document.createElement('label')
6137
+ label.className = 'content-field-label'
6138
+ label.textContent = k
6139
+
6140
+ const input = document.createElement(typeof v === 'string' && v.length > 60 ? 'textarea' : 'input')
6141
+ input.className = 'content-input'
6142
+ input.value = v === null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v)
6143
+ if (input.tagName === 'TEXTAREA') { input.rows = 2 }
6144
+
6145
+ input.addEventListener('change', async () => {
6146
+ let newVal = input.value
6147
+ if (newVal === 'true') newVal = true
6148
+ else if (newVal === 'false') newVal = false
6149
+ else if (newVal === 'null') newVal = null
6150
+ else if (!isNaN(Number(newVal)) && newVal !== '') newVal = Number(newVal)
6151
+ try {
6152
+ await pageEval('(function(){ var root = window.__DOMQL_INSPECTOR__.findRoot(); if(!root || !root.state) return; var st = root.state; ' +
6153
+ 'var target = st[' + JSON.stringify(stateKey) + ']; if(!target) return; ' +
6154
+ (typeof index === 'number'
6155
+ ? 'target = target[' + index + ']; '
6156
+ : 'target = target[' + JSON.stringify(index) + ']; ') +
6157
+ 'if(target) target[' + JSON.stringify(k) + '] = ' + JSON.stringify(newVal) + '; if(st.update) st.update(); })()')
6158
+ setStatus('Updated ' + k)
6159
+ } catch (e) { setStatus('Error: ' + (e.message || e)) }
6160
+ })
6161
+
6162
+ row.appendChild(label)
6163
+ row.appendChild(input)
6164
+ card.appendChild(row)
6165
+ }
6166
+
6167
+ container.appendChild(card)
6168
+ }
6169
+
6170
+ function renderContentField (container, fullPath, label, value, stateKey, subKey) {
6171
+ const row = document.createElement('div')
6172
+ row.className = 'content-field'
6173
+
6174
+ const labelEl = document.createElement('label')
6175
+ labelEl.className = 'content-field-label'
6176
+ labelEl.textContent = label
6177
+
6178
+ const input = document.createElement('input')
6179
+ input.className = 'content-input'
6180
+ input.value = value === null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value)
6181
+
6182
+ input.addEventListener('change', async () => {
6183
+ let newVal = input.value
6184
+ if (newVal === 'true') newVal = true
6185
+ else if (newVal === 'false') newVal = false
6186
+ else if (newVal === 'null') newVal = null
6187
+ else if (!isNaN(Number(newVal)) && newVal !== '') newVal = Number(newVal)
6188
+ try {
6189
+ await pageEval('(function(){ var root = window.__DOMQL_INSPECTOR__.findRoot(); if(root && root.state) { root.state.' + stateKey +
6190
+ (typeof subKey === 'number' ? '[' + subKey + ']' : '["' + subKey + '"]') +
6191
+ ' = ' + JSON.stringify(newVal) + '; if(root.state.update) root.state.update(); } })()')
6192
+ setStatus('Updated ' + label)
6193
+ } catch (e) { setStatus('Error: ' + (e.message || e)) }
6194
+ })
6195
+
6196
+ row.appendChild(labelEl)
6197
+ row.appendChild(input)
6198
+ container.appendChild(row)
6199
+ }
6200
+
6201
+ function renderContentPrimitives (container, keys, rootState) {
6202
+ for (const key of keys) {
6203
+ const info = rootState[key]
6204
+ const row = document.createElement('div')
6205
+ row.className = 'content-field'
6206
+
6207
+ const label = document.createElement('label')
6208
+ label.className = 'content-field-label'
6209
+ label.textContent = key
6210
+
6211
+ const input = document.createElement('input')
6212
+ input.className = 'content-input'
6213
+ input.value = info.value === null ? '' : String(info.value)
6214
+
6215
+ input.addEventListener('change', async () => {
6216
+ let newVal = input.value
6217
+ if (newVal === 'true') newVal = true
6218
+ else if (newVal === 'false') newVal = false
6219
+ else if (newVal === 'null') newVal = null
6220
+ else if (!isNaN(Number(newVal)) && newVal !== '') newVal = Number(newVal)
6221
+ try {
6222
+ await pageEval('(function(){ var root = window.__DOMQL_INSPECTOR__.findRoot(); if(root && root.state) { root.state["' + key + '"] = ' + JSON.stringify(newVal) + '; if(root.state.update) root.state.update(); } })()')
6223
+ setStatus('Updated ' + key)
6224
+ } catch (e) { setStatus('Error: ' + (e.message || e)) }
6225
+ })
6226
+
6227
+ row.appendChild(label)
6228
+ row.appendChild(input)
6229
+ container.appendChild(row)
6230
+ }
6231
+ }
6232
+
4926
6233
  // Auto-refresh: listen for page navigation and poll for DOM changes
4927
6234
  function initAutoRefresh () {
4928
6235
  // Refresh tree when user navigates to a new page
@@ -4942,6 +6249,8 @@ Do NOT include any explanation, only valid JSON.`
4942
6249
  let lastTreeHash = ''
4943
6250
  setInterval(async () => {
4944
6251
  if (!connectionMode) return
6252
+ // Skip polling while an editor is active to avoid re-rendering mid-edit
6253
+ if (document.querySelector('.prop-value.editing') || document.querySelector('.obj-editor') || document.querySelector('.prop-add-inline')) return
4945
6254
  try {
4946
6255
  const ready = await pageEval('typeof window.__DOMQL_INSPECTOR__ !== "undefined"')
4947
6256
  if (!ready) return