@fps-games/editor 0.1.1-beta.6 → 0.1.1-beta.7

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 (93) hide show
  1. package/dist/local-editor-harness.d.ts +14 -3
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +205 -35
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts +33 -0
  6. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.d.ts.map +1 -0
  7. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js +231 -0
  8. package/node_modules/@fps-games/editor-babylon/dist/editor-sky.js.map +1 -0
  9. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts +3 -0
  10. package/node_modules/@fps-games/editor-babylon/dist/editor-world.d.ts.map +1 -1
  11. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js +8 -0
  12. package/node_modules/@fps-games/editor-babylon/dist/editor-world.js.map +1 -1
  13. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts +2 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/index.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/dist/index.js +2 -0
  16. package/node_modules/@fps-games/editor-babylon/dist/index.js.map +1 -1
  17. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts +40 -0
  18. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.d.ts.map +1 -0
  19. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js +237 -0
  20. package/node_modules/@fps-games/editor-babylon/dist/infinite-grid.js.map +1 -0
  21. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts +1 -1
  22. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.d.ts.map +1 -1
  23. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js +15 -50
  24. package/node_modules/@fps-games/editor-babylon/dist/projection-selection-controller.js.map +1 -1
  25. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +5 -0
  26. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-babylon/dist/projection.js +61 -8
  28. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  29. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +7 -0
  30. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  31. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  32. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js +97 -46
  33. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-context-menu.js.map +1 -1
  34. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts +5 -0
  35. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.d.ts.map +1 -1
  36. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js +57 -7
  37. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-actions.js.map +1 -1
  38. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.d.ts.map +1 -1
  39. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js +61 -5
  40. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-controller.js.map +1 -1
  41. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js +0 -2
  42. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-tree.js.map +1 -1
  43. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.d.ts.map +1 -1
  44. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js +6 -2
  45. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-hierarchy-view.js.map +1 -1
  46. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +1 -1
  47. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -1
  48. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +16 -0
  49. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -1
  50. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +8 -1
  51. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  52. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +358 -53
  53. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  54. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +5 -0
  55. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
  56. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +28 -8
  57. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  58. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +4 -1
  60. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.d.ts.map +1 -1
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js +14 -0
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-theme.js.map +1 -1
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts +11 -0
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.d.ts.map +1 -0
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js +148 -0
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-tooltip.js.map +1 -0
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +42 -1
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +5 -4
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +606 -199
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  74. package/node_modules/@fps-games/editor-browser/package.json +2 -2
  75. package/node_modules/@fps-games/editor-core/dist/index.d.ts +1 -0
  76. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
  77. package/node_modules/@fps-games/editor-core/dist/index.js +1 -0
  78. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
  79. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts +6 -1
  80. package/node_modules/@fps-games/editor-core/dist/inspector.d.ts.map +1 -1
  81. package/node_modules/@fps-games/editor-core/dist/inspector.js +6 -1
  82. package/node_modules/@fps-games/editor-core/dist/inspector.js.map +1 -1
  83. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts +7 -0
  84. package/node_modules/@fps-games/editor-core/dist/scene-graph.d.ts.map +1 -1
  85. package/node_modules/@fps-games/editor-core/dist/scene-graph.js.map +1 -1
  86. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts +12 -0
  87. package/node_modules/@fps-games/editor-core/dist/selection-strategy.d.ts.map +1 -0
  88. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js +52 -0
  89. package/node_modules/@fps-games/editor-core/dist/selection-strategy.js.map +1 -0
  90. package/node_modules/@fps-games/editor-core/package.json +2 -2
  91. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  92. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  93. package/package.json +6 -6
@@ -7,11 +7,11 @@ const DOCK_TAB_ICONS = {
7
7
  };
8
8
  export function renderHierarchyPanel(doc, panel, state, rename, drop) {
9
9
  clearElement(panel);
10
- const createGroupButton = createToolbarButton(doc, '+ Group', 'group');
10
+ const createGroupButton = createToolbarButton(doc, '+ Empty', 'object');
11
11
  createGroupButton.dataset.editorHierarchyCreateGroup = 'true';
12
12
  createGroupButton.style.padding = '3px 7px';
13
13
  createGroupButton.style.fontSize = '11px';
14
- panel.appendChild(createPanelHeader(doc, 'Graph', [createGroupButton], 'hierarchy'));
14
+ panel.appendChild(createPanelHeader(doc, 'Hierarchy', [createGroupButton], 'hierarchy'));
15
15
  const list = createTreeView(doc);
16
16
  for (const item of state.hierarchy) {
17
17
  const isRenaming = rename?.id === item.id;
@@ -256,6 +256,7 @@ function formatHistoryTimestamp(createdAt) {
256
256
  ].join('');
257
257
  }
258
258
  export function renderInspectorPanel(doc, panel, state, filter = '', options = {}) {
259
+ const renderSnapshot = captureInspectorPanelRenderSnapshot(doc, panel);
259
260
  clearElement(panel);
260
261
  const title = doc.createElement('h2');
261
262
  title.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:13px;margin:-8px -8px 8px;padding:7px 8px;border-bottom:1px solid var(--fps-editor-divider);background:var(--fps-editor-chrome-dark);font-weight:800;color:var(--fps-editor-text-strong)';
@@ -274,23 +275,45 @@ export function renderInspectorPanel(doc, panel, state, filter = '', options = {
274
275
  empty.textContent = '请从层级树或 Scene View 中选择一个 GameObject。';
275
276
  empty.style.cssText = 'color:var(--fps-editor-muted);line-height:1.45';
276
277
  panel.appendChild(empty);
278
+ restoreInspectorPanelRenderSnapshot(panel, renderSnapshot);
277
279
  return;
278
280
  }
279
281
  const controlRegistry = createLocalEditorBrowserInspectorControlRegistry(options.controls, options.controlConflict);
280
282
  appendInspectorSummary(doc, panel, inspectorObject, selectionCount);
281
283
  const visibleSections = filterInspectorSections(inspectorObject.sections.filter(section => section.placement !== 'summary'), filter);
282
284
  if (visibleSections.length === 0) {
283
- if (!filter.trim() && inspectorObject.sections.length === 0)
285
+ if (!filter.trim() && inspectorObject.sections.length === 0) {
286
+ restoreInspectorPanelRenderSnapshot(panel, renderSnapshot);
284
287
  return;
288
+ }
285
289
  const empty = doc.createElement('div');
286
290
  empty.textContent = '没有匹配的 Inspector 字段。';
287
291
  empty.style.cssText = 'color:var(--fps-editor-muted);line-height:1.45';
288
292
  panel.appendChild(empty);
293
+ restoreInspectorPanelRenderSnapshot(panel, renderSnapshot);
289
294
  return;
290
295
  }
291
296
  for (const section of visibleSections) {
292
297
  panel.appendChild(createInspectorSectionBlock(doc, inspectorObject, section, controlRegistry));
293
298
  }
299
+ restoreInspectorPanelRenderSnapshot(panel, renderSnapshot);
300
+ }
301
+ function captureInspectorPanelRenderSnapshot(doc, panel) {
302
+ const activeElement = doc.activeElement;
303
+ const elementConstructor = doc.defaultView?.HTMLElement;
304
+ return {
305
+ preserveScroll: !!activeElement
306
+ && (elementConstructor ? activeElement instanceof elementConstructor : activeElement instanceof HTMLElement)
307
+ && panel.contains(activeElement),
308
+ scrollTop: panel.scrollTop,
309
+ scrollLeft: panel.scrollLeft,
310
+ };
311
+ }
312
+ function restoreInspectorPanelRenderSnapshot(panel, snapshot) {
313
+ if (!snapshot.preserveScroll)
314
+ return;
315
+ panel.scrollTop = snapshot.scrollTop;
316
+ panel.scrollLeft = snapshot.scrollLeft;
294
317
  }
295
318
  function createInspectorSearchInput(doc, value) {
296
319
  const input = doc.createElement('input');
@@ -308,10 +331,14 @@ function createInspectorSearchInput(doc, value) {
308
331
  }
309
332
  function appendInspectorSummary(doc, panel, inspectorObject, selectionCount) {
310
333
  const block = createInspectorComponentBlock(doc);
334
+ const header = doc.createElement('div');
335
+ header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 8px';
311
336
  const title = doc.createElement('h3');
312
337
  title.textContent = selectionCount > 1 ? 'Selection' : 'GameObject';
313
- title.style.cssText = 'font-size:12px;margin:0 0 8px;font-weight:900;color:var(--fps-editor-text-strong)';
314
- block.appendChild(title);
338
+ title.style.cssText = 'font-size:12px;margin:0;font-weight:900;color:var(--fps-editor-text-strong)';
339
+ header.appendChild(title);
340
+ header.appendChild(createInspectorAccessBadge(doc, 'readonly'));
341
+ block.appendChild(header);
315
342
  appendReadOnlyRow(doc, block, selectionCount > 1 ? 'Selected' : 'Name', inspectorObject.label ?? inspectorObject.activeId ?? inspectorObject.targetIds[0] ?? '无');
316
343
  appendReadOnlyRow(doc, block, 'Active ID', inspectorObject.activeId ?? '无');
317
344
  if (selectionCount > 1)
@@ -342,7 +369,11 @@ function createInspectorSectionBlock(doc, inspectorObject, section, controlRegis
342
369
  const block = doc.createElement('details');
343
370
  block.dataset.editorInspectorSection = section.id;
344
371
  block.open = section.collapsedByDefault !== true;
345
- block.style.cssText = createInspectorBlockStyle();
372
+ const status = resolveLocalEditorBrowserInspectorSectionStatus(section);
373
+ const { access, effect } = status;
374
+ block.dataset.editorInspectorAccess = access;
375
+ block.dataset.editorInspectorEffect = effect;
376
+ block.style.cssText = createInspectorBlockStyle(access, effect);
346
377
  const sectionTitle = doc.createElement('summary');
347
378
  sectionTitle.style.cssText = [
348
379
  'font-size:12px',
@@ -367,13 +398,13 @@ function createInspectorSectionBlock(doc, inspectorObject, section, controlRegis
367
398
  summary.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:none;font-size:11px';
368
399
  meta.appendChild(summary);
369
400
  }
370
- if (section.persistence === 'runtime' || section.runtimeOnly || section.persistence === 'readonly') {
371
- const badge = createBadge(doc, section.persistence === 'runtime' || section.runtimeOnly ? 'Runtime' : 'Readonly', {
372
- compact: true,
373
- tone: section.persistence === 'readonly' ? 'default' : 'warning',
374
- });
375
- badge.style.flex = '0 0 auto';
376
- meta.appendChild(badge);
401
+ const badge = createInspectorAccessBadge(doc, access);
402
+ badge.style.flex = '0 0 auto';
403
+ meta.appendChild(badge);
404
+ const effectBadge = createInspectorEffectBadge(doc, effect, section.disabledReason);
405
+ if (effectBadge && status.showEffectBadge) {
406
+ effectBadge.style.flex = '0 0 auto';
407
+ meta.appendChild(effectBadge);
377
408
  }
378
409
  sectionTitle.appendChild(meta);
379
410
  block.appendChild(sectionTitle);
@@ -390,18 +421,133 @@ function createInspectorSectionBlock(doc, inspectorObject, section, controlRegis
390
421
  }
391
422
  function createInspectorComponentBlock(doc) {
392
423
  const block = doc.createElement('div');
393
- block.style.cssText = createInspectorBlockStyle();
424
+ block.dataset.editorInspectorAccess = 'readonly';
425
+ block.dataset.editorInspectorEffect = 'active';
426
+ block.style.cssText = createInspectorBlockStyle('readonly', 'active');
394
427
  return block;
395
428
  }
396
- function createInspectorBlockStyle() {
429
+ export function resolveLocalEditorBrowserInspectorSectionStatus(section) {
430
+ const access = getInspectorSectionAccess(section);
431
+ const effect = getInspectorSectionEffect(section);
432
+ return {
433
+ access,
434
+ effect,
435
+ showEffectBadge: effect !== 'active'
436
+ && !(access === 'runtime' && effect === 'runtime')
437
+ && !(effect === 'default' && inspectorSummaryLooksDefault(section.summary)),
438
+ };
439
+ }
440
+ function createInspectorBlockStyle(_access, _effect = 'active') {
397
441
  return [
398
442
  'margin:0 0 8px',
399
443
  'padding:9px',
400
- 'border:1px solid var(--fps-editor-border)',
444
+ 'border:1px solid var(--fps-editor-inspector-section-border, var(--fps-editor-border))',
401
445
  'border-radius:3px',
402
446
  'background:var(--fps-editor-panel-soft)',
403
447
  ].join(';');
404
448
  }
449
+ function isInspectorPropertyEditable(property) {
450
+ return property.readOnly !== true
451
+ && property.persistence === 'document'
452
+ && getInspectorPropertyEffect(property) === 'active';
453
+ }
454
+ function getInspectorPropertyAccess(property) {
455
+ if (property.persistence === 'runtime')
456
+ return 'runtime';
457
+ if (getInspectorPropertyEffect(property) === 'runtime')
458
+ return 'runtime';
459
+ return isInspectorPropertyEditable(property) ? 'editable' : 'readonly';
460
+ }
461
+ function getInspectorSectionAccess(section) {
462
+ if (section.runtimeOnly || section.persistence === 'runtime' || getInspectorSectionEffect(section) === 'runtime')
463
+ return 'runtime';
464
+ const editableCount = section.properties.filter(isInspectorPropertyEditable).length;
465
+ if (editableCount === 0)
466
+ return 'readonly';
467
+ if (editableCount === section.properties.length)
468
+ return 'editable';
469
+ return 'mixed';
470
+ }
471
+ function getInspectorPropertyEffect(property) {
472
+ if (property.effect)
473
+ return property.effect;
474
+ if (property.persistence === 'runtime')
475
+ return 'runtime';
476
+ return 'active';
477
+ }
478
+ function getInspectorSectionEffect(section) {
479
+ if (section.effect)
480
+ return section.effect;
481
+ if (section.runtimeOnly || section.persistence === 'runtime')
482
+ return 'runtime';
483
+ if (section.properties.some(isInspectorPropertyEditable))
484
+ return 'active';
485
+ const effects = new Set(section.properties.map(getInspectorPropertyEffect));
486
+ if (effects.size === 1)
487
+ return [...effects][0] ?? 'active';
488
+ if (effects.has('unsupported'))
489
+ return 'unsupported';
490
+ if (effects.has('default'))
491
+ return 'default';
492
+ if (effects.has('derived'))
493
+ return 'derived';
494
+ if (effects.has('runtime'))
495
+ return 'runtime';
496
+ return 'active';
497
+ }
498
+ function createInspectorAccessBadge(doc, access) {
499
+ const labels = {
500
+ editable: 'EDITABLE',
501
+ mixed: 'MIXED',
502
+ readonly: 'READONLY',
503
+ runtime: 'RUNTIME',
504
+ };
505
+ const badge = createBadge(doc, labels[access], {
506
+ compact: true,
507
+ tone: access === 'editable' ? 'success' : access === 'mixed' || access === 'runtime' ? 'warning' : 'default',
508
+ });
509
+ badge.title = access === 'editable'
510
+ ? 'This section writes to the document.'
511
+ : access === 'mixed'
512
+ ? 'This section contains both editable and read-only fields.'
513
+ : access === 'runtime'
514
+ ? 'Runtime-only context, not saved to the document.'
515
+ : 'Read-only information.';
516
+ return badge;
517
+ }
518
+ function createInspectorEffectBadge(doc, effect, disabledReason) {
519
+ if (effect === 'active')
520
+ return null;
521
+ const labels = {
522
+ default: 'DEFAULTS',
523
+ derived: 'DERIVED',
524
+ runtime: 'RUNTIME',
525
+ unsupported: 'NO EFFECT',
526
+ };
527
+ const badge = createBadge(doc, labels[effect], {
528
+ compact: true,
529
+ tone: effect === 'unsupported' ? 'danger' : 'warning',
530
+ });
531
+ badge.title = disabledReason ?? createInspectorEffectTitle(effect);
532
+ return badge;
533
+ }
534
+ function createInspectorEffectTitle(effect) {
535
+ switch (effect) {
536
+ case 'default':
537
+ return 'Default values are being shown; edits do not affect this object until an override is configured.';
538
+ case 'derived':
539
+ return 'Derived from document or runtime state.';
540
+ case 'runtime':
541
+ return 'Runtime-only context, not saved to the document.';
542
+ case 'unsupported':
543
+ return 'This field is visible but editing is not supported for the current object.';
544
+ default:
545
+ return 'Editable field.';
546
+ }
547
+ }
548
+ function inspectorSummaryLooksDefault(summary) {
549
+ return summary?.trim().toLowerCase() === 'defaults';
550
+ }
405
551
  function groupInspectorProperties(properties) {
406
552
  const groups = [];
407
553
  const consumed = new Set();
@@ -430,7 +576,7 @@ function groupInspectorProperties(properties) {
430
576
  for (const entry of vector)
431
577
  consumed.add(entry.path);
432
578
  const parts = basePath.split('.');
433
- groups.push({ kind: 'vector', label: toTitle(parts[parts.length - 1] ?? basePath), properties: vector });
579
+ groups.push({ kind: 'vector', label: resolveInspectorVectorGroupLabel(vector, parts[parts.length - 1] ?? basePath), properties: vector });
434
580
  }
435
581
  else {
436
582
  groups.push({ kind: 'property', property });
@@ -439,6 +585,16 @@ function groupInspectorProperties(properties) {
439
585
  }
440
586
  return groups;
441
587
  }
588
+ function resolveInspectorVectorGroupLabel(properties, fallback) {
589
+ const labels = properties.map(property => {
590
+ const match = property.label.match(/^(.*)\.(x|y|z|r|g|b)$/);
591
+ return match?.[1]?.trim() ?? '';
592
+ });
593
+ const first = labels[0];
594
+ if (first && labels.every(label => label === first))
595
+ return first;
596
+ return toTitle(fallback);
597
+ }
442
598
  const builtinInspectorControlRegistrations = [
443
599
  {
444
600
  id: 'builtin.readonly',
@@ -509,7 +665,16 @@ const builtinInspectorControlRegistrations = [
509
665
  ];
510
666
  function appendInspectorPropertyRow(doc, parent, inspectorObject, property, controlRegistry) {
511
667
  const control = renderInspectorControl(doc, controlRegistry, inspectorObject, property);
512
- parent.appendChild(createPropertyRow(doc, property.label, control));
668
+ const access = getInspectorPropertyAccess(property);
669
+ const effect = getInspectorPropertyEffect(property);
670
+ const row = createPropertyRow(doc, property.label, control);
671
+ row.dataset.editorInspectorAccess = access;
672
+ row.dataset.editorInspectorEffect = effect;
673
+ row.title = createInspectorStatusTitle(access, effect, property.tooltip ?? property.disabledReason);
674
+ const label = row.firstElementChild;
675
+ if (label)
676
+ label.style.cssText = createInspectorPropertyLabelStyle(access, effect);
677
+ parent.appendChild(row);
513
678
  }
514
679
  export function createLocalEditorBrowserInspectorControlRegistry(controls = [], conflict = 'error') {
515
680
  const registrations = new Map();
@@ -538,7 +703,7 @@ function compareInspectorControlRegistrations(left, right) {
538
703
  return (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id);
539
704
  }
540
705
  function renderInspectorControl(doc, registrations, target, property) {
541
- if (property.readOnly || property.persistence !== 'document')
706
+ if (!isInspectorPropertyEditable(property))
542
707
  return createInspectorReadonlyControl(doc, target, property);
543
708
  const context = {
544
709
  doc,
@@ -570,18 +735,145 @@ function localEditorBrowserInspectorControlSupports(registration, context) {
570
735
  function appendInspectorVectorInputs(doc, parent, inspectorObject, label, properties) {
571
736
  const wrapper = doc.createElement('div');
572
737
  wrapper.style.cssText = 'margin:8px 0';
738
+ const access = getInspectorPropertyGroupAccess(properties);
739
+ const effect = getInspectorPropertyGroupEffect(properties);
740
+ wrapper.dataset.editorInspectorAccess = access;
741
+ wrapper.dataset.editorInspectorEffect = effect;
573
742
  const labelElement = doc.createElement('div');
574
- labelElement.textContent = label;
575
- labelElement.style.cssText = 'color:var(--fps-editor-muted);font-weight:800;margin-bottom:5px';
743
+ labelElement.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:5px';
744
+ const labelText = doc.createElement('span');
745
+ labelText.textContent = label;
746
+ labelText.style.cssText = createInspectorPropertyLabelStyle(access, effect);
747
+ labelElement.appendChild(labelText);
748
+ if (access !== 'editable') {
749
+ const badge = createInspectorAccessBadge(doc, access);
750
+ badge.style.flex = '0 0 auto';
751
+ labelElement.appendChild(badge);
752
+ }
753
+ const effectBadge = createInspectorEffectBadge(doc, effect, getInspectorPropertyGroupDisabledReason(properties));
754
+ if (effectBadge && !(access === 'runtime' && effect === 'runtime')) {
755
+ effectBadge.style.flex = '0 0 auto';
756
+ labelElement.appendChild(effectBadge);
757
+ }
576
758
  wrapper.appendChild(labelElement);
577
759
  const fields = doc.createElement('div');
578
760
  fields.style.cssText = 'display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px';
579
761
  for (const property of properties) {
580
- fields.appendChild(createInspectorNumberControl(doc, inspectorObject, property));
762
+ fields.appendChild(isInspectorPropertyEditable(property)
763
+ ? createInspectorNumberControl(doc, inspectorObject, property)
764
+ : createInspectorReadonlyControl(doc, inspectorObject, property));
581
765
  }
582
766
  wrapper.appendChild(fields);
583
767
  parent.appendChild(wrapper);
584
768
  }
769
+ function getInspectorPropertyGroupAccess(properties) {
770
+ if (properties.some(property => property.persistence === 'runtime'))
771
+ return 'runtime';
772
+ const editableCount = properties.filter(isInspectorPropertyEditable).length;
773
+ if (editableCount === 0)
774
+ return 'readonly';
775
+ if (editableCount === properties.length)
776
+ return 'editable';
777
+ return 'mixed';
778
+ }
779
+ function getInspectorPropertyGroupEffect(properties) {
780
+ const effects = new Set(properties.map(getInspectorPropertyEffect));
781
+ if (effects.size === 1)
782
+ return [...effects][0] ?? 'active';
783
+ if (effects.has('unsupported'))
784
+ return 'unsupported';
785
+ if (effects.has('default'))
786
+ return 'default';
787
+ if (effects.has('derived'))
788
+ return 'derived';
789
+ if (effects.has('runtime'))
790
+ return 'runtime';
791
+ return 'active';
792
+ }
793
+ function getInspectorPropertyGroupDisabledReason(properties) {
794
+ return properties.find(property => property.disabledReason)?.disabledReason;
795
+ }
796
+ function createInspectorPropertyLabelStyle(access, effect = 'active') {
797
+ return [
798
+ `color:${effect === 'unsupported' ? 'var(--fps-editor-danger-text)' : effect === 'default' || effect === 'derived' || access === 'runtime' ? 'var(--fps-editor-warn)' : access === 'editable' ? 'var(--fps-editor-muted-strong)' : 'var(--fps-editor-muted)'}`,
799
+ 'font-weight:800',
800
+ access === 'readonly' ? 'opacity:0.82' : '',
801
+ ].filter(Boolean).join(';');
802
+ }
803
+ function createInspectorAccessTitle(access, tooltip) {
804
+ const accessLabel = access === 'editable'
805
+ ? 'Editable'
806
+ : access === 'mixed'
807
+ ? 'Mixed editable and read-only'
808
+ : access === 'runtime'
809
+ ? 'Runtime-only'
810
+ : 'Read-only';
811
+ return tooltip ? `${accessLabel}: ${tooltip}` : accessLabel;
812
+ }
813
+ function createInspectorStatusTitle(access, effect, detail) {
814
+ const parts = [createInspectorAccessTitle(access)];
815
+ if (effect !== 'active')
816
+ parts.push(createInspectorEffectTitle(effect));
817
+ if (detail)
818
+ parts.push(detail);
819
+ return [...new Set(parts)].join(' ');
820
+ }
821
+ function createInspectorReadonlyValueShell(doc, access, effect = 'active', title) {
822
+ const valueElement = doc.createElement('div');
823
+ valueElement.dataset.editorInspectorAccess = access;
824
+ valueElement.dataset.editorInspectorEffect = effect;
825
+ valueElement.title = title ?? createInspectorStatusTitle(access, effect);
826
+ const warningLike = effect === 'default' || effect === 'derived' || access === 'runtime' || effect === 'runtime';
827
+ const unsupported = effect === 'unsupported';
828
+ valueElement.style.cssText = [
829
+ 'min-width:0',
830
+ 'min-height:24px',
831
+ 'display:flex',
832
+ 'align-items:center',
833
+ 'gap:5px',
834
+ 'padding:3px 6px',
835
+ `border:1px solid ${unsupported ? 'var(--fps-editor-danger-border)' : warningLike ? 'var(--fps-editor-warn-border)' : 'var(--fps-editor-readonly-border)'}`,
836
+ 'border-radius:3px',
837
+ `background:${unsupported ? 'var(--fps-editor-danger-soft)' : warningLike ? 'var(--fps-editor-warn-soft)' : 'var(--fps-editor-readonly-bg)'}`,
838
+ `color:${unsupported ? 'var(--fps-editor-danger-text)' : warningLike ? 'var(--fps-editor-warn)' : 'var(--fps-editor-readonly-text)'}`,
839
+ warningLike ? 'font-style:italic' : '',
840
+ ].filter(Boolean).join(';');
841
+ const icon = createLocalEditorIcon(doc, 'lock', { size: 11, strokeWidth: 2.3 });
842
+ icon.style.opacity = '0.78';
843
+ valueElement.appendChild(icon);
844
+ return valueElement;
845
+ }
846
+ function appendInspectorReadonlyTextValue(doc, parent, value, access, effect = 'active', title) {
847
+ const valueText = doc.createElement('span');
848
+ valueText.textContent = value;
849
+ valueText.style.cssText = 'min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
850
+ parent.appendChild(valueText);
851
+ parent.title = title ?? createInspectorStatusTitle(access, effect);
852
+ }
853
+ function createInspectorReadonlyScalarControl(doc, value, access, effect = 'active', title) {
854
+ const valueElement = createInspectorReadonlyValueShell(doc, access, effect, title);
855
+ appendInspectorReadonlyTextValue(doc, valueElement, value, access, effect, title);
856
+ return valueElement;
857
+ }
858
+ function createInspectorReadonlyDetailsShell(doc, property, access) {
859
+ const effect = getInspectorPropertyEffect(property);
860
+ const warningLike = effect === 'default' || effect === 'derived' || access === 'runtime' || effect === 'runtime';
861
+ const unsupported = effect === 'unsupported';
862
+ const details = doc.createElement('details');
863
+ details.dataset.editorInspectorAccess = access;
864
+ details.dataset.editorInspectorEffect = effect;
865
+ details.title = createInspectorStatusTitle(access, effect, property.tooltip ?? property.disabledReason);
866
+ details.style.cssText = [
867
+ 'min-width:0',
868
+ 'max-width:100%',
869
+ 'padding:3px 6px',
870
+ `border:1px solid ${unsupported ? 'var(--fps-editor-danger-border)' : warningLike ? 'var(--fps-editor-warn-border)' : 'var(--fps-editor-readonly-border)'}`,
871
+ 'border-radius:3px',
872
+ `background:${unsupported ? 'var(--fps-editor-danger-soft)' : warningLike ? 'var(--fps-editor-warn-soft)' : 'var(--fps-editor-readonly-bg)'}`,
873
+ `color:${unsupported ? 'var(--fps-editor-danger-text)' : warningLike ? 'var(--fps-editor-warn)' : 'var(--fps-editor-readonly-text)'}`,
874
+ ].join(';');
875
+ return details;
876
+ }
585
877
  function createInspectorInputBase(doc, target, property) {
586
878
  const input = doc.createElement('input');
587
879
  input.value = property.mixed ? '' : String(property.value);
@@ -609,7 +901,14 @@ function createInspectorBooleanControl(doc, target, property) {
609
901
  const input = createInspectorInputBase(doc, target, property);
610
902
  input.type = 'checkbox';
611
903
  input.checked = property.mixed ? false : property.value === true;
612
- input.style.cssText = 'width:16px;height:16px;accent-color:var(--fps-editor-accent);justify-self:start';
904
+ input.style.cssText = [
905
+ 'width:16px',
906
+ 'height:16px',
907
+ 'accent-color:var(--fps-editor-accent)',
908
+ 'justify-self:start',
909
+ 'cursor:pointer',
910
+ 'box-shadow:0 0 0 1px var(--fps-editor-editable-border)',
911
+ ].join(';');
613
912
  return input;
614
913
  }
615
914
  function createInspectorEnumControl(doc, target, property) {
@@ -658,36 +957,30 @@ function createInspectorColorControl(doc, target, property) {
658
957
  const input = createInspectorInputBase(doc, target, property);
659
958
  input.type = 'color';
660
959
  input.value = colorValueToHex(property.value);
661
- input.style.cssText = 'width:34px;height:26px;border:1px solid var(--fps-editor-border);border-radius:3px;background:transparent;padding:0';
960
+ input.style.cssText = 'width:34px;height:26px;border:1px solid var(--fps-editor-editable-border);border-radius:3px;background:var(--fps-editor-editable-bg);padding:0;box-shadow:var(--fps-editor-editable-shadow)';
662
961
  return input;
663
962
  }
664
963
  function createInspectorReadonlyControl(doc, _target, property) {
665
964
  if (!property.mixed && isExpandableInspectorValue(property.value)) {
666
965
  return createInspectorObjectReadonlyControl(doc, property);
667
966
  }
668
- const valueElement = doc.createElement('div');
669
- valueElement.textContent = property.mixed ? '--' : formatLocalEditorBrowserInspectorValue(property.value);
670
- valueElement.style.cssText = [
671
- `color:${property.persistence === 'runtime' ? 'var(--fps-editor-muted)' : 'var(--fps-editor-text)'}`,
672
- 'overflow:hidden',
673
- 'text-overflow:ellipsis',
674
- 'white-space:nowrap',
675
- property.persistence === 'runtime' ? 'font-style:italic' : '',
676
- ].filter(Boolean).join(';');
677
- if (property.persistence === 'runtime')
678
- valueElement.title = 'Runtime-only context';
679
- return valueElement;
967
+ const access = getInspectorPropertyAccess(property);
968
+ const effect = getInspectorPropertyEffect(property);
969
+ return createInspectorReadonlyScalarControl(doc, property.mixed ? '--' : formatLocalEditorBrowserInspectorValue(property.value), access, effect, createInspectorStatusTitle(access, effect, property.tooltip ?? property.disabledReason));
680
970
  }
681
971
  function createInspectorObjectReadonlyControl(doc, property) {
682
- const details = doc.createElement('details');
683
- details.style.cssText = [
684
- 'min-width:0',
685
- 'max-width:100%',
686
- `color:${property.persistence === 'runtime' ? 'var(--fps-editor-muted)' : 'var(--fps-editor-text)'}`,
687
- ].join(';');
972
+ const access = getInspectorPropertyAccess(property);
973
+ const effect = getInspectorPropertyEffect(property);
974
+ const details = createInspectorReadonlyDetailsShell(doc, property, access);
688
975
  const summary = doc.createElement('summary');
689
- summary.textContent = formatInspectorObjectSummary(property.value);
690
- summary.style.cssText = 'cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
976
+ summary.style.cssText = 'cursor:pointer;display:flex;align-items:center;gap:5px;overflow:hidden;white-space:nowrap';
977
+ const icon = createLocalEditorIcon(doc, 'lock', { size: 11, strokeWidth: 2.3 });
978
+ icon.style.opacity = '0.78';
979
+ summary.appendChild(icon);
980
+ const summaryText = doc.createElement('span');
981
+ summaryText.textContent = formatInspectorObjectSummary(property.value);
982
+ summaryText.style.cssText = 'min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
983
+ summary.appendChild(summaryText);
691
984
  details.appendChild(summary);
692
985
  const pre = doc.createElement('pre');
693
986
  pre.textContent = formatLocalEditorBrowserInspectorValue(property.value);
@@ -698,15 +991,13 @@ function createInspectorObjectReadonlyControl(doc, property) {
698
991
  'word-break:break-word',
699
992
  'margin:6px 0 0',
700
993
  'padding:7px',
701
- 'border:1px solid var(--fps-editor-border)',
994
+ `border:1px solid ${effect === 'unsupported' ? 'var(--fps-editor-danger-border)' : effect === 'default' || effect === 'derived' || effect === 'runtime' || access === 'runtime' ? 'var(--fps-editor-warn-border)' : 'var(--fps-editor-readonly-border)'}`,
702
995
  'border-radius:3px',
703
996
  'background:var(--fps-editor-field)',
704
997
  'font-size:11px',
705
998
  'line-height:1.45',
706
999
  ].join(';');
707
1000
  details.appendChild(pre);
708
- if (property.persistence === 'runtime')
709
- details.title = 'Runtime-only context';
710
1001
  return details;
711
1002
  }
712
1003
  export function applyLocalEditorBrowserInspectorControlBinding(element, target, property, options) {
@@ -718,24 +1009,38 @@ export function applyLocalEditorBrowserInspectorControlBinding(element, target,
718
1009
  element.dataset.serializedValueType = property.valueType;
719
1010
  element.dataset.serializedCommitMode = property.commitMode;
720
1011
  element.dataset.serializedPersistence = property.persistence;
1012
+ element.dataset.serializedEffect = getInspectorPropertyEffect(property);
1013
+ if (property.disabledReason)
1014
+ element.dataset.serializedDisabledReason = property.disabledReason;
721
1015
  if (options?.source)
722
1016
  element.dataset.serializedEditSource = options.source;
723
- element.title = property.tooltip ?? property.label;
1017
+ const access = getInspectorPropertyAccess(property);
1018
+ const effect = getInspectorPropertyEffect(property);
1019
+ element.title = access === 'editable' && effect === 'active'
1020
+ ? property.tooltip ?? property.label
1021
+ : createInspectorStatusTitle(access, effect, property.tooltip ?? property.disabledReason ?? property.label);
724
1022
  if ('disabled' in element) {
725
- element.disabled = property.readOnly === true || property.persistence !== 'document';
1023
+ element.disabled = !isInspectorPropertyEditable(property);
726
1024
  }
727
1025
  }
728
1026
  function createInspectorInputStyle() {
729
1027
  return [
730
1028
  createEditorInputStyle(),
731
1029
  'padding:0 6px',
1030
+ 'background:var(--fps-editor-editable-bg)',
1031
+ 'border-color:var(--fps-editor-editable-border)',
1032
+ 'color:var(--fps-editor-text-strong)',
1033
+ 'box-shadow:var(--fps-editor-editable-shadow)',
732
1034
  ].join(';');
733
1035
  }
734
1036
  function appendReadOnlyRow(doc, parent, label, value) {
735
- const valueElement = doc.createElement('div');
736
- valueElement.textContent = value;
737
- valueElement.style.cssText = 'color:var(--fps-editor-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
738
- parent.appendChild(createPropertyRow(doc, label, valueElement));
1037
+ const valueElement = createInspectorReadonlyScalarControl(doc, value, 'readonly', 'active', 'Read-only summary');
1038
+ const row = createPropertyRow(doc, label, valueElement);
1039
+ row.dataset.editorInspectorAccess = 'readonly';
1040
+ const labelElement = row.firstElementChild;
1041
+ if (labelElement)
1042
+ labelElement.style.cssText = createInspectorPropertyLabelStyle('readonly', 'active');
1043
+ parent.appendChild(row);
739
1044
  }
740
1045
  function createLegacyInspectorObject(serializedObject) {
741
1046
  const targetIds = 'targetIds' in serializedObject ? serializedObject.targetIds : [serializedObject.targetId];