@volter/editor-sdk 0.5.57

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 (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +19 -0
  4. package/package.json +90 -0
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +83 -0
  7. package/src/client.ts +1547 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +985 -0
  10. package/src/document-probe.ts +237 -0
  11. package/src/editor-view.ts +220 -0
  12. package/src/extension.ts +40 -0
  13. package/src/generations.ts +178 -0
  14. package/src/host.ts +1167 -0
  15. package/src/http-transport.browser.ts +14 -0
  16. package/src/http-transport.node.ts +19 -0
  17. package/src/index.ts +128 -0
  18. package/src/layout-arrangements.ts +5 -0
  19. package/src/layouts.tsx +108 -0
  20. package/src/looks.ts +14 -0
  21. package/src/project/output-roots.ts +73 -0
  22. package/src/project/tab-census.ts +149 -0
  23. package/src/project-tool-catalog.ts +96 -0
  24. package/src/selection.tsx +108 -0
  25. package/src/services.ts +18 -0
  26. package/src/session/build-report.ts +19 -0
  27. package/src/session/collaboration-types.ts +262 -0
  28. package/src/session/command-table.ts +333 -0
  29. package/src/session/discovery.ts +90 -0
  30. package/src/session/editor-brand.ts +73 -0
  31. package/src/session/editor-compatibility.ts +248 -0
  32. package/src/session/editor-control-lifecycle.ts +68 -0
  33. package/src/session/editor-control-protocol.ts +5 -0
  34. package/src/session/entrypoint-selection-readers.ts +66 -0
  35. package/src/session/entrypoint-selection-source.ts +120 -0
  36. package/src/session/game-css-scope.ts +30 -0
  37. package/src/session/product-create.ts +24 -0
  38. package/src/session/product-locator.ts +389 -0
  39. package/src/session/project-module-url.ts +245 -0
  40. package/src/session/registry-format.ts +203 -0
  41. package/src/session/relative-path-guard.ts +56 -0
  42. package/src/session/source-glob.ts +15 -0
  43. package/src/session/tool-contribution-convention.ts +116 -0
  44. package/src/session/workbench-locator.ts +650 -0
  45. package/src/session.ts +41 -0
  46. package/src/share.ts +160 -0
  47. package/src/tools/errors.ts +91 -0
  48. package/src/tools/provider-execution.ts +70 -0
  49. package/src/tools/registry.ts +341 -0
  50. package/src/tools/types.ts +159 -0
  51. package/src/transport.ts +97 -0
  52. package/src/types.ts +1581 -0
  53. package/src/views.ts +164 -0
  54. package/src/widgets/design-system.ts +93 -0
  55. package/src/widgets/editor-appearance.ts +149 -0
  56. package/src/widgets/editor-material.ts +83 -0
  57. package/src/widgets/icon-set-registry.ts +105 -0
  58. package/src/widgets/index.ts +71 -0
  59. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  60. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  61. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  62. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  63. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  64. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  65. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  66. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  67. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  68. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  69. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  70. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  71. package/src/widgets/inspector-widgets/index.ts +24 -0
  72. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  73. package/src/widgets/interactive-edit-scope.ts +33 -0
  74. package/src/widgets/patterns/Dialog.tsx +129 -0
  75. package/src/widgets/patterns/Fields.tsx +44 -0
  76. package/src/widgets/patterns/List.tsx +25 -0
  77. package/src/widgets/patterns/StateSurface.tsx +40 -0
  78. package/src/widgets/patterns/Surfaces.tsx +122 -0
  79. package/src/widgets/patterns/Tabs.tsx +80 -0
  80. package/src/widgets/patterns/Toolbar.tsx +72 -0
  81. package/src/widgets/patterns/Tree.tsx +72 -0
  82. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  83. package/src/widgets/primitives/Button.tsx +62 -0
  84. package/src/widgets/primitives/ColorInput.tsx +78 -0
  85. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  86. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  87. package/src/widgets/primitives/FormControls.tsx +88 -0
  88. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  89. package/src/widgets/primitives/JsonInput.tsx +113 -0
  90. package/src/widgets/primitives/Layout.tsx +100 -0
  91. package/src/widgets/primitives/Menu.tsx +140 -0
  92. package/src/widgets/primitives/NumberInput.tsx +169 -0
  93. package/src/widgets/primitives/Panel.tsx +80 -0
  94. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  95. package/src/widgets/primitives/Text.tsx +54 -0
  96. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  97. package/src/widgets/primitives/Tooltip.tsx +204 -0
  98. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  99. package/src/widgets/primitives/banner-tones.ts +32 -0
  100. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  101. package/src/widgets/primitives/editor-icons.ts +245 -0
  102. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  103. package/src/widgets/theme.ts +2633 -0
  104. package/src/widgets/z-index.ts +25 -0
@@ -0,0 +1,204 @@
1
+ import {
2
+ cloneElement,
3
+ isValidElement,
4
+ type ReactElement,
5
+ useEffect,
6
+ useId,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import { createPortal } from 'react-dom';
12
+
13
+ type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
14
+
15
+ interface TooltipProps {
16
+ text: string;
17
+ /** Explicitly `| undefined`: a hint resolved from the active keymap
18
+ * (`keymap-presets.ts`) is absent when that keymap leaves the action
19
+ * unbound, and the tooltip simply omits the `<kbd>` rather than lie. */
20
+ hotkey?: string | undefined;
21
+ children: ReactElement;
22
+ position?: TooltipPosition;
23
+ delay?: number;
24
+ }
25
+
26
+ interface TooltipCoordinates {
27
+ left: number;
28
+ top: number;
29
+ ready: boolean;
30
+ }
31
+
32
+ const VIEWPORT_GUTTER = 8;
33
+ const TOOLTIP_GAP = 6;
34
+
35
+ function opposite(position: TooltipPosition): TooltipPosition {
36
+ if (position === 'top') return 'bottom';
37
+ if (position === 'bottom') return 'top';
38
+ if (position === 'left') return 'right';
39
+ return 'left';
40
+ }
41
+
42
+ function placementFits(position: TooltipPosition, trigger: DOMRect, tooltip: DOMRect): boolean {
43
+ if (position === 'top') return trigger.top - tooltip.height - TOOLTIP_GAP >= VIEWPORT_GUTTER;
44
+ if (position === 'bottom')
45
+ return trigger.bottom + tooltip.height + TOOLTIP_GAP <= window.innerHeight - VIEWPORT_GUTTER;
46
+ if (position === 'left') return trigger.left - tooltip.width - TOOLTIP_GAP >= VIEWPORT_GUTTER;
47
+ return trigger.right + tooltip.width + TOOLTIP_GAP <= window.innerWidth - VIEWPORT_GUTTER;
48
+ }
49
+
50
+ function coordinatesFor(
51
+ position: TooltipPosition,
52
+ trigger: DOMRect,
53
+ tooltip: DOMRect,
54
+ ): TooltipCoordinates {
55
+ let left = trigger.left + (trigger.width - tooltip.width) / 2;
56
+ let top = trigger.top + (trigger.height - tooltip.height) / 2;
57
+ if (position === 'top') top = trigger.top - tooltip.height - TOOLTIP_GAP;
58
+ else if (position === 'bottom') top = trigger.bottom + TOOLTIP_GAP;
59
+ else if (position === 'left') left = trigger.left - tooltip.width - TOOLTIP_GAP;
60
+ else left = trigger.right + TOOLTIP_GAP;
61
+
62
+ return {
63
+ left: Math.max(
64
+ VIEWPORT_GUTTER,
65
+ Math.min(left, window.innerWidth - tooltip.width - VIEWPORT_GUTTER),
66
+ ),
67
+ top: Math.max(
68
+ VIEWPORT_GUTTER,
69
+ Math.min(top, window.innerHeight - tooltip.height - VIEWPORT_GUTTER),
70
+ ),
71
+ ready: true,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Canonical editor tooltip.
77
+ *
78
+ * The popup is portaled to the editor theme root and positioned against the
79
+ * viewport. That lets it escape dock/panel overflow clipping without leaking
80
+ * outside the theme boundary. Pointer and keyboard focus both expose the same
81
+ * accessible description.
82
+ */
83
+ export function Tooltip({
84
+ text,
85
+ hotkey,
86
+ children,
87
+ position = 'bottom',
88
+ delay = 400,
89
+ }: TooltipProps) {
90
+ const id = useId();
91
+ const wrapperRef = useRef<HTMLSpanElement>(null);
92
+ const tooltipRef = useRef<HTMLDivElement>(null);
93
+ const timer = useRef<ReturnType<typeof setTimeout>>(null);
94
+ const hovered = useRef(false);
95
+ const focused = useRef(false);
96
+ const [visible, setVisible] = useState(false);
97
+ const [portalRoot, setPortalRoot] = useState<Element | null>(null);
98
+ const [coordinates, setCoordinates] = useState<TooltipCoordinates>({
99
+ left: 0,
100
+ top: 0,
101
+ ready: false,
102
+ });
103
+
104
+ useLayoutEffect(() => {
105
+ setPortalRoot(wrapperRef.current?.closest('.vgai-editor-theme') ?? null);
106
+ }, []);
107
+
108
+ useEffect(
109
+ () => () => {
110
+ if (timer.current) clearTimeout(timer.current);
111
+ },
112
+ [],
113
+ );
114
+
115
+ const hide = () => {
116
+ if (timer.current) clearTimeout(timer.current);
117
+ timer.current = null;
118
+ setVisible(false);
119
+ };
120
+
121
+ const hideIfInactive = () => {
122
+ if (!hovered.current && !focused.current) hide();
123
+ };
124
+
125
+ const show = (wait: number) => {
126
+ if (timer.current) clearTimeout(timer.current);
127
+ timer.current = setTimeout(() => {
128
+ setCoordinates((current) => ({ ...current, ready: false }));
129
+ setVisible(true);
130
+ }, wait);
131
+ };
132
+
133
+ useLayoutEffect(() => {
134
+ if (!visible) return;
135
+ const update = () => {
136
+ const trigger = wrapperRef.current?.getBoundingClientRect();
137
+ const tooltip = tooltipRef.current?.getBoundingClientRect();
138
+ if (!trigger || !tooltip) return;
139
+ const resolved = placementFits(position, trigger, tooltip) ? position : opposite(position);
140
+ setCoordinates(coordinatesFor(resolved, trigger, tooltip));
141
+ };
142
+ update();
143
+ window.addEventListener('resize', update);
144
+ window.addEventListener('scroll', update, true);
145
+ return () => {
146
+ window.removeEventListener('resize', update);
147
+ window.removeEventListener('scroll', update, true);
148
+ };
149
+ }, [hotkey, position, text, visible]);
150
+
151
+ let child = children;
152
+ if (isValidElement<{ 'aria-describedby'?: string }>(children)) {
153
+ const describedBy = [children.props['aria-describedby'], visible ? id : null]
154
+ .filter(Boolean)
155
+ .join(' ');
156
+ child = cloneElement(children, describedBy ? { 'aria-describedby': describedBy } : {});
157
+ }
158
+
159
+ return (
160
+ <span
161
+ ref={wrapperRef}
162
+ className="vgai-tooltip-anchor"
163
+ onMouseEnter={() => {
164
+ hovered.current = true;
165
+ show(delay);
166
+ }}
167
+ onMouseLeave={() => {
168
+ hovered.current = false;
169
+ hideIfInactive();
170
+ }}
171
+ onFocusCapture={() => {
172
+ focused.current = true;
173
+ show(0);
174
+ }}
175
+ onBlurCapture={(event) => {
176
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
177
+ focused.current = false;
178
+ hideIfInactive();
179
+ }
180
+ }}
181
+ >
182
+ {child}
183
+ {visible &&
184
+ portalRoot &&
185
+ createPortal(
186
+ <div
187
+ ref={tooltipRef}
188
+ id={id}
189
+ role="tooltip"
190
+ className="vgai-tooltip"
191
+ style={{
192
+ left: coordinates.left,
193
+ top: coordinates.top,
194
+ visibility: coordinates.ready ? 'visible' : 'hidden',
195
+ }}
196
+ >
197
+ <span>{text}</span>
198
+ {hotkey && <kbd>{hotkey}</kbd>}
199
+ </div>,
200
+ portalRoot,
201
+ )}
202
+ </span>
203
+ );
204
+ }
@@ -0,0 +1,70 @@
1
+ import { NumberInput } from './NumberInput';
2
+
3
+ interface Vec3InputProps {
4
+ value: [number | null, number | null, number | null];
5
+ onChange: (value: [number, number, number]) => void;
6
+ /** Per-axis callback for multi-edit. When provided, takes priority over onChange. */
7
+ onAxisChange?: (axis: 0 | 1 | 2, value: number) => void;
8
+ step?: number;
9
+ disabled?: boolean;
10
+ /** When set, each axis field gets `data-testid="{prefix}-x|y|z"` for e2e. */
11
+ testIdPrefix?: string | undefined;
12
+ }
13
+
14
+ export function Vec3Input({
15
+ value,
16
+ onChange,
17
+ onAxisChange,
18
+ step = 0.1,
19
+ disabled = false,
20
+ testIdPrefix,
21
+ }: Vec3InputProps) {
22
+ const tid = (axis: 'x' | 'y' | 'z') => (testIdPrefix ? `${testIdPrefix}-${axis}` : undefined);
23
+ const handleAxis = (axis: 0 | 1 | 2) => (v: number) => {
24
+ if (onAxisChange) {
25
+ onAxisChange(axis, v);
26
+ } else {
27
+ const tuple: [number, number, number] = [value[0] ?? 0, value[1] ?? 0, value[2] ?? 0];
28
+ tuple[axis] = v;
29
+ onChange(tuple);
30
+ }
31
+ };
32
+
33
+ // Axis tint literals are deliberate (not tokenized): they mirror the
34
+ // viewport gizmo's R/G/B axis colors, a cross-tool convention (Unity/
35
+ // Blender) distinct from the semantic danger/success/accent tones.
36
+ return (
37
+ <div style={{ display: 'flex', gap: 4 }}>
38
+ <NumberInput
39
+ label="X"
40
+ labelColor="#E06178"
41
+ value={value[0]}
42
+ onChange={handleAxis(0)}
43
+ step={step}
44
+ disabled={disabled}
45
+ style={{ flex: 1 }}
46
+ testId={tid('x')}
47
+ />
48
+ <NumberInput
49
+ label="Y"
50
+ labelColor="#C2ED66"
51
+ value={value[1]}
52
+ onChange={handleAxis(1)}
53
+ step={step}
54
+ disabled={disabled}
55
+ style={{ flex: 1 }}
56
+ testId={tid('y')}
57
+ />
58
+ <NumberInput
59
+ label="Z"
60
+ labelColor="#6BABF5"
61
+ value={value[2]}
62
+ onChange={handleAxis(2)}
63
+ step={step}
64
+ disabled={disabled}
65
+ style={{ flex: 1 }}
66
+ testId={tid('z')}
67
+ />
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,32 @@
1
+ import { themeVars } from '../theme';
2
+
3
+ /**
4
+ * Shared semantic-tone triples for dismissible chrome banners
5
+ * (V-16): two sibling banners each hand-rolled an identical-shaped
6
+ * `{bg, border, fg}` triple as its own inline magic values — built from the
7
+ * same lightness formula, but two independent literals with no shared
8
+ * constant. Hoisted here so a future retouch of one can't silently drift
9
+ * from the other (the same class of bug V-4 already demonstrated happened
10
+ * to `PlayBar`'s button styles).
11
+ *
12
+ * These are tinted-panel triples, not `theme.ts`'s flat `danger`/`warn`
13
+ * scalars (`theme.ts` has no "dark tinted banner background" slot) — kept
14
+ * as their own small module rather than added to the shared token file.
15
+ */
16
+
17
+ export interface BannerTone {
18
+ bg: string;
19
+ border: string;
20
+ fg: string;
21
+ }
22
+
23
+ export const TONE_ERROR: BannerTone = {
24
+ bg: themeVars.semantic.dangerFaint,
25
+ border: themeVars.semantic.danger,
26
+ fg: themeVars.semantic.danger,
27
+ };
28
+ export const TONE_WARNING: BannerTone = {
29
+ bg: themeVars.semantic.warningMuted,
30
+ border: themeVars.semantic.warning,
31
+ fg: themeVars.semantic.warning,
32
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared clamp-to-viewport positioning (P6 glass-native chrome, U6.5 F5/F11).
3
+ *
4
+ * A popup/panel that anchors to a pointer position (a right-click context menu)
5
+ * or otherwise sets its own absolute/fixed `top`/`left` can open partly — or
6
+ * entirely — off the visible window when the anchor sits near an edge. This
7
+ * utility takes the *desired* rect (position + measured size) and returns a
8
+ * shifted `{ top, left }` that keeps the whole rect inside the window: it never
9
+ * resizes, only slides the popup so
10
+ *
11
+ * top ≥ margin, bottom ≤ innerHeight − margin,
12
+ * left ≥ margin, right ≤ innerWidth − margin.
13
+ *
14
+ * If the rect is larger than the viewport on an axis the leading edge wins
15
+ * (the popup pins to `margin` and lets the far edge overflow) so the anchor /
16
+ * first items stay reachable rather than the popup vanishing upward or left.
17
+ *
18
+ * Keep this the single home for the clamp — do not re-derive the arithmetic at
19
+ * each call site.
20
+ */
21
+ export interface ClampDesiredRect {
22
+ readonly top: number;
23
+ readonly left: number;
24
+ readonly width: number;
25
+ readonly height: number;
26
+ }
27
+
28
+ export interface ClampedPoint {
29
+ readonly top: number;
30
+ readonly left: number;
31
+ }
32
+
33
+ export function clampRectToViewport(desired: ClampDesiredRect, margin = 8): ClampedPoint {
34
+ const viewportWidth = window.innerWidth;
35
+ const viewportHeight = window.innerHeight;
36
+ // Math.max(margin, …) guards the case where the popup is wider/taller than the
37
+ // window: the upper bound collapses to `margin`, pinning the leading edge.
38
+ const maxLeft = Math.max(margin, viewportWidth - desired.width - margin);
39
+ const maxTop = Math.max(margin, viewportHeight - desired.height - margin);
40
+ return {
41
+ left: Math.min(Math.max(desired.left, margin), maxLeft),
42
+ top: Math.min(Math.max(desired.top, margin), maxTop),
43
+ };
44
+ }
@@ -0,0 +1,245 @@
1
+ import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
2
+ import {
3
+ faArrowPointer,
4
+ faArrowsRotate,
5
+ faArrowsToDot,
6
+ faArrowUpFromBracket,
7
+ faBookOpen,
8
+ faBorderAll,
9
+ faBorderTopLeft,
10
+ faCamera,
11
+ faChartSimple,
12
+ faCheck,
13
+ faCircleExclamation,
14
+ faCircleHalfStroke,
15
+ faCircleInfo,
16
+ faCode,
17
+ faCompress,
18
+ faCrosshairs,
19
+ faCube,
20
+ faCubesStacked,
21
+ faDiagramProject,
22
+ faExpand,
23
+ faEye,
24
+ faEyeSlash,
25
+ faFile,
26
+ faFileCode,
27
+ faFileExport,
28
+ faFolder,
29
+ faFolderOpen,
30
+ faFolderTree,
31
+ faForwardStep,
32
+ faGripLines,
33
+ faHammer,
34
+ faHand,
35
+ faImage,
36
+ faLightbulb,
37
+ faList,
38
+ faLock,
39
+ faLockOpen,
40
+ faMagnet,
41
+ faMagnifyingGlass,
42
+ faMagnifyingGlassMinus,
43
+ faMagnifyingGlassPlus,
44
+ faMaximize,
45
+ faMusic,
46
+ faPause,
47
+ faPlay,
48
+ faPlus,
49
+ faPuzzlePiece,
50
+ faRotate,
51
+ faRotateLeft,
52
+ faRotateRight,
53
+ faScissors,
54
+ faSliders,
55
+ faStop,
56
+ faTable,
57
+ faTableCells,
58
+ faTrash,
59
+ faTriangleExclamation,
60
+ faUpDownLeftRight,
61
+ faUpRightAndDownLeftFromCenter,
62
+ faUsers,
63
+ faXmark,
64
+ } from '@fortawesome/free-solid-svg-icons';
65
+
66
+ /**
67
+ * A definition under a name of OURS, carrying a Font Awesome drawing until an
68
+ * icon set supplies its own (see `editorIcons.modeling`). The name is the only
69
+ * thing that changes: `icon` — the width/height/path tuple every renderer
70
+ * actually paints — is the source definition's, so nothing moves on screen
71
+ * until a set keys the new name.
72
+ */
73
+ function meshGlyph(iconName: string, source: IconDefinition): IconDefinition {
74
+ return { ...source, iconName: iconName as IconDefinition['iconName'] };
75
+ }
76
+
77
+ /**
78
+ * Semantic icon registry for editor chrome. Components may still import a
79
+ * genuinely domain-specific Font Awesome definition, but recurring actions,
80
+ * content kinds, status, and viewport tools use these stable meanings.
81
+ */
82
+ export const editorIcons = {
83
+ action: {
84
+ add: faPlus,
85
+ close: faXmark,
86
+ delete: faTrash,
87
+ search: faMagnifyingGlass,
88
+ refresh: faArrowsRotate,
89
+ undo: faRotateLeft,
90
+ redo: faRotateRight,
91
+ build: faHammer,
92
+ export: faFileExport,
93
+ select: faArrowPointer,
94
+ },
95
+ transport: {
96
+ play: faPlay,
97
+ pause: faPause,
98
+ restart: faRotateRight,
99
+ stop: faStop,
100
+ step: faForwardStep,
101
+ },
102
+ content: {
103
+ project: faFolderOpen,
104
+ folder: faFolder,
105
+ file: faFile,
106
+ source: faFileCode,
107
+ scene: faBookOpen,
108
+ entity: faCube,
109
+ component: faPuzzlePiece,
110
+ camera: faCamera,
111
+ light: faLightbulb,
112
+ image: faImage,
113
+ audio: faMusic,
114
+ data: faTable,
115
+ code: faCode,
116
+ stateMachine: faDiagramProject,
117
+ },
118
+ status: {
119
+ success: faCheck,
120
+ info: faCircleInfo,
121
+ warning: faTriangleExclamation,
122
+ error: faCircleExclamation,
123
+ visible: faEye,
124
+ hidden: faEyeSlash,
125
+ locked: faLock,
126
+ unlocked: faLockOpen,
127
+ },
128
+ /** The 3D viewport's navigation cluster (`ViewportFurniture`). */
129
+ viewport: {
130
+ zoomIn: faMagnifyingGlassPlus,
131
+ zoomOut: faMagnifyingGlassMinus,
132
+ pan: faHand,
133
+ frame: faExpand,
134
+ /**
135
+ * IT OWNS ITS NAME, for the reason `modeling` below states in full:
136
+ * Blender's mark for this toggle is a grid IN PERSPECTIVE — a trapezoid,
137
+ * measured 32x26 device on `modeling-edit-none.png`'s navigation capsule —
138
+ * while three other sites ask for `border-all` and all three mean a FLAT
139
+ * grid (the canvas viewport's grid toggle, the 3D overlay's, and the CSS
140
+ * border editor's "all borders"). Redrawing the shared name would have
141
+ * put a receding floor on a CSS border button. Font Awesome's `border-all`
142
+ * stays as the drawing, so a set without this key paints what it painted.
143
+ */
144
+ projection: meshGlyph('viewport-projection', faBorderAll),
145
+ },
146
+ /**
147
+ * Mesh-modeling operators — the shelf glyphs a modeling document shows
148
+ * (Blender's toolbar column). One per operator the `mesh` capability ships;
149
+ * a contribution reaches them through `@volter/editor-sdk/widgets`.
150
+ *
151
+ * EACH OWNS ITS NAME. An icon set keys a glyph by `icon.iconName`
152
+ * (`EditorIcon` → `activeIconGlyph`), so while these pointed at
153
+ * `scissors`/`compress`/`grip-lines`/`table-cells` the operator mark and
154
+ * every OTHER site using that name were one drawing: redrawing Knife as a
155
+ * cut cube would have recut the editor's scissors everywhere. Blender draws
156
+ * an operator AS THE OPERATION ON A CUBE, which is meaningless as a generic
157
+ * mark, so the two must be separable — hence the `mesh-*` names below, which
158
+ * no other site asks for.
159
+ *
160
+ * Font Awesome's `IconPrefix`/`IconName` are closed string unions of ITS
161
+ * catalog, so a name of ours cannot typecheck as one: the prefix stays the
162
+ * source definition's (nothing looks a glyph up by prefix) and the name is
163
+ * asserted. The `icon` tuple stays the source's too, so a set that does not
164
+ * carry the `mesh-*` name still paints exactly what it painted before.
165
+ */
166
+ modeling: {
167
+ extrude: meshGlyph('mesh-extrude', faArrowUpFromBracket),
168
+ inset: meshGlyph('mesh-inset', faCompress),
169
+ bevel: meshGlyph('mesh-bevel', faBorderTopLeft),
170
+ loopCut: meshGlyph('mesh-loop-cut', faGripLines),
171
+ knife: meshGlyph('mesh-knife', faScissors),
172
+ smooth: meshGlyph('mesh-smooth', faCircleHalfStroke),
173
+ shrinkFatten: meshGlyph('mesh-shrink-fatten', faUpRightAndDownLeftFromCenter),
174
+ subdivide: meshGlyph('mesh-subdivide', faTableCells),
175
+ },
176
+ tool: {
177
+ extension: faPuzzlePiece,
178
+ /**
179
+ * THE TRANSFORM TOOLS OF THE SHELF (`ToolStrip`) — Blender's Edit Mode
180
+ * toolbar group 2, in Blender's own order: Move, Rotate, Scale,
181
+ * Transform.
182
+ *
183
+ * Move and Rotate keep the shared Font Awesome names because the Blender
184
+ * set's drawings for `up-down-left-right` and `rotate` say the same thing
185
+ * at every other site that asks for them. Scale and Transform do not:
186
+ * they asked for `maximize` (Font Awesome's window-expand cross) and
187
+ * `arrows-to-dot` (a crosshair), which are generic meanings other
188
+ * surfaces can reasonably want, and Blender's marks for the two tools —
189
+ * a small square growing into a larger outlined one, and a square inside
190
+ * a broken ring of arrows — are not those meanings. Same rule as
191
+ * `modeling` above: a glyph that IS an operation owns its own name.
192
+ */
193
+ /** Blender's group 1, Select Box — the tool that arms no gizmo. It shares
194
+ * `arrow-pointer` with the action set's `select` above, because the two
195
+ * mean the same thing and the Blender set already carries one drawing of
196
+ * it in Blender's idiom (`blender-icons.source.mjs`, under `-- tools --`).
197
+ * Blender's own mark is that pointer inside a dashed marquee; the marquee
198
+ * is what this glyph does not carry, and that is the one difference
199
+ * between the two shelves' first button. */
200
+ select: faArrowPointer,
201
+ move: faUpDownLeftRight,
202
+ rotate: faRotate,
203
+ scale: meshGlyph('tool-scale', faMaximize),
204
+ transform: meshGlyph('tool-transform', faArrowsToDot),
205
+ snap: faMagnet,
206
+ frame: faCrosshairs,
207
+ grid: faBorderAll,
208
+ profiler: faChartSimple,
209
+ },
210
+ /**
211
+ * A STATIC PANEL'S OWN GLYPH — what Blender puts in an area's EDITOR-TYPE
212
+ * WELL, and the only place this editor draws one (`workspace-regions.ts`'s
213
+ * `groupTabs`). `workspace-static-panels.ts` declares WHICH glyph per panel
214
+ * by name and takes {@link WorkspacePanelGlyphName} as a type-only import,
215
+ * so the inventory stays a dependency-free table and no panel can name a
216
+ * glyph this one does not resolve.
217
+ *
218
+ * TWO OF THESE ARE MEASURED AND FIVE ARE OURS, and the split is a fact
219
+ * about the reference frames rather than about effort. `outliner` and
220
+ * `properties` are Blender's own marks, read at native 2x off
221
+ * `outliner.png` and `modeling-edit-none.png`'s Properties area and drawn
222
+ * in `@vgai/blender`'s `blender-icons.source.mjs`, which carries the pixel
223
+ * edges. The other five have NO Blender reading at all: the reference set
224
+ * photographs the Layout, Modeling, Sculpting and Texture Paint workspaces
225
+ * — 3D View, Outliner, Properties, Timeline, Image Editor and the asset
226
+ * shelf — with every editor-type dropdown CLOSED, so it contains no File
227
+ * Browser, no Asset Browser, and no picture of the editors our Content,
228
+ * Library, Conversations, Project work and History panels correspond to.
229
+ * They reuse names the Blender set already draws, chosen here, so a well
230
+ * stays in that set's idiom instead of falling through to a Font Awesome
231
+ * silhouette beside two measured marks.
232
+ */
233
+ panel: {
234
+ outliner: meshGlyph('outliner', faFolderTree),
235
+ properties: meshGlyph('properties', faSliders),
236
+ 'folder-open': faFolderOpen,
237
+ 'cubes-stacked': faCubesStacked,
238
+ users: faUsers,
239
+ list: faList,
240
+ 'rotate-left': faRotateLeft,
241
+ },
242
+ } as const;
243
+
244
+ /** The glyph names a static panel may declare (`workspace-static-panels.ts`). */
245
+ export type WorkspacePanelGlyphName = keyof (typeof editorIcons)['panel'];
@@ -0,0 +1,42 @@
1
+ import type React from 'react';
2
+ import { fontWeight, space, themeVars } from '../theme';
3
+
4
+ /**
5
+ * Shared typography and metrics for panel titles and disclosure sections.
6
+ *
7
+ * The heights are the VARS, not `chromeSize`'s constants: a material may
8
+ * retune the chrome's density, and the constant is only the editor's own
9
+ * default. Read at module load it cannot learn the active material — the
10
+ * Blender material declares a 26px panel header and every Panel still drew
11
+ * the default 30, because `--vgai-panel-header-height` (which does carry the
12
+ * material's value) was not what this style read.
13
+ */
14
+ export const panelHeaderLabelStyle: React.CSSProperties = {
15
+ minHeight: 'var(--vgai-panel-header-height)',
16
+ padding: `0 ${space[4]}px`,
17
+ color: themeVars.content.muted,
18
+ fontSize: 'var(--vgai-font-base)',
19
+ fontWeight: fontWeight.semibold,
20
+ textTransform: 'uppercase',
21
+ display: 'flex',
22
+ alignItems: 'center',
23
+ gap: space[2],
24
+ userSelect: 'none',
25
+ };
26
+
27
+ /** Disclosure rows are one density step below full panel headers. */
28
+ export const sectionHeaderLabelStyle: React.CSSProperties = {
29
+ ...panelHeaderLabelStyle,
30
+ minHeight: 'var(--vgai-local-toolbar-height)',
31
+ };
32
+
33
+ /** Complete header rail treatment for a standalone panel. Glass treatments
34
+ * emit a transparent structural surface; opaque/reduced-transparency themes
35
+ * retain their occluding stack through the same variable. Headers never
36
+ * own the content-frost filter. */
37
+ export const panelHeaderStyle: React.CSSProperties = {
38
+ ...panelHeaderLabelStyle,
39
+ background: 'var(--vgai-surface-sticky)',
40
+ borderBottom: '1px solid var(--vgai-structural-divider)',
41
+ flexShrink: 0,
42
+ };