@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.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure curve math for `CurveEditor` (W1b) — piecewise cubic-bezier
|
|
3
|
+
* value-over-normalized-time curves, structurally identical to the engine's
|
|
4
|
+
* `PiecewiseBezier` schema shape (`schema/particles.ts`) and to three.quarks'
|
|
5
|
+
* own `PiecewiseBezier` JSON: segment `i` spans `[start_i, start_{i+1}]`
|
|
6
|
+
* (the last segment ends at 1), and `p0..p3` are VALUE control points over
|
|
7
|
+
* the segment's normalized time — i.e. the plotted curve is an exact SVG
|
|
8
|
+
* cubic with x-controls at 1/3 and 2/3 of the segment width.
|
|
9
|
+
*
|
|
10
|
+
* Kept editor-generic (no particle imports): any future consumer uses the
|
|
11
|
+
* same helpers.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface CurveBezier {
|
|
15
|
+
p0: number;
|
|
16
|
+
p1: number;
|
|
17
|
+
p2: number;
|
|
18
|
+
p3: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CurveSegment {
|
|
22
|
+
function: CurveBezier;
|
|
23
|
+
start: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CurveKey {
|
|
27
|
+
time: number;
|
|
28
|
+
value: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** End time of segment `i` — the next segment's start, or 1 for the last. */
|
|
32
|
+
export function segmentEnd(segments: CurveSegment[], i: number): number {
|
|
33
|
+
return i + 1 < segments.length ? segments[i + 1]!.start : 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The curve's keys (n segments → n+1 keys): key `i` sits at segment `i`'s
|
|
38
|
+
* start with value `p0`; the final key sits at t=1 with the last `p3`.
|
|
39
|
+
*/
|
|
40
|
+
export function curveKeys(segments: CurveSegment[]): CurveKey[] {
|
|
41
|
+
const keys: CurveKey[] = segments.map((s) => ({ time: s.start, value: s.function.p0 }));
|
|
42
|
+
const last = segments[segments.length - 1];
|
|
43
|
+
if (last) keys.push({ time: 1, value: last.function.p3 });
|
|
44
|
+
return keys;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cloneSegments(segments: CurveSegment[]): CurveSegment[] {
|
|
48
|
+
return segments.map((s) => ({ start: s.start, function: { ...s.function } }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MIN_SEGMENT_WIDTH = 0.01;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Move key `keyIndex` to (time, value). Endpoint keys are time-locked (first
|
|
55
|
+
* to its own start, last to 1); interior key times clamp between neighbors.
|
|
56
|
+
* Tangent control values (p1/p2) shift WITH their endpoint so the curve's
|
|
57
|
+
* local shape is preserved.
|
|
58
|
+
*/
|
|
59
|
+
export function moveKey(
|
|
60
|
+
segments: CurveSegment[],
|
|
61
|
+
keyIndex: number,
|
|
62
|
+
time: number,
|
|
63
|
+
value: number,
|
|
64
|
+
): CurveSegment[] {
|
|
65
|
+
const next = cloneSegments(segments);
|
|
66
|
+
const n = next.length;
|
|
67
|
+
if (keyIndex < 0 || keyIndex > n) return next;
|
|
68
|
+
|
|
69
|
+
if (keyIndex < n) {
|
|
70
|
+
// Key owns segment keyIndex's start/p0 (and the previous segment's p3).
|
|
71
|
+
const seg = next[keyIndex]!;
|
|
72
|
+
const dv = value - seg.function.p0;
|
|
73
|
+
seg.function.p0 = value;
|
|
74
|
+
seg.function.p1 += dv;
|
|
75
|
+
if (keyIndex > 0) {
|
|
76
|
+
const prev = next[keyIndex - 1]!;
|
|
77
|
+
const dvPrev = value - prev.function.p3;
|
|
78
|
+
prev.function.p3 = value;
|
|
79
|
+
prev.function.p2 += dvPrev;
|
|
80
|
+
// Interior keys may move in time, clamped between neighbors.
|
|
81
|
+
const lo = prev.start + MIN_SEGMENT_WIDTH;
|
|
82
|
+
const hi = segmentEnd(next, keyIndex) - MIN_SEGMENT_WIDTH;
|
|
83
|
+
seg.start = Math.min(hi, Math.max(lo, time));
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// Final key: last segment's p3, time locked at 1.
|
|
87
|
+
const seg = next[n - 1]!;
|
|
88
|
+
const dv = value - seg.function.p3;
|
|
89
|
+
seg.function.p3 = value;
|
|
90
|
+
seg.function.p2 += dv;
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Move a tangent control value of key `keyIndex`: `out` = its segment's p1
|
|
97
|
+
* (exists for every key but the last), `in` = the previous segment's p2
|
|
98
|
+
* (exists for every key but the first).
|
|
99
|
+
*/
|
|
100
|
+
export function moveTangent(
|
|
101
|
+
segments: CurveSegment[],
|
|
102
|
+
keyIndex: number,
|
|
103
|
+
which: 'in' | 'out',
|
|
104
|
+
value: number,
|
|
105
|
+
): CurveSegment[] {
|
|
106
|
+
const next = cloneSegments(segments);
|
|
107
|
+
if (which === 'out' && keyIndex < next.length) {
|
|
108
|
+
next[keyIndex]!.function.p1 = value;
|
|
109
|
+
} else if (which === 'in' && keyIndex > 0 && keyIndex <= next.length) {
|
|
110
|
+
next[keyIndex - 1]!.function.p2 = value;
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function lerp(a: number, b: number, u: number): number {
|
|
116
|
+
return a + (b - a) * u;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Evaluate the curve at normalized time t (matches quarks' genValue). */
|
|
120
|
+
export function sampleCurve(segments: CurveSegment[], t: number): number {
|
|
121
|
+
for (let i = 0; i < segments.length; i++) {
|
|
122
|
+
const start = segments[i]!.start;
|
|
123
|
+
const end = segmentEnd(segments, i);
|
|
124
|
+
if (t >= start && t <= end) {
|
|
125
|
+
const u = end > start ? (t - start) / (end - start) : 0;
|
|
126
|
+
const { p0, p1, p2, p3 } = segments[i]!.function;
|
|
127
|
+
const b01 = lerp(p0, p1, u);
|
|
128
|
+
const b12 = lerp(p1, p2, u);
|
|
129
|
+
const b23 = lerp(p2, p3, u);
|
|
130
|
+
const b012 = lerp(b01, b12, u);
|
|
131
|
+
const b123 = lerp(b12, b23, u);
|
|
132
|
+
return lerp(b012, b123, u);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Insert a key at time `t` by de Casteljau subdivision of the containing
|
|
140
|
+
* segment — the curve's shape is EXACTLY preserved. Returns the input
|
|
141
|
+
* unchanged when `t` falls outside the curve or too close to an existing key.
|
|
142
|
+
*/
|
|
143
|
+
export function splitAt(segments: CurveSegment[], t: number): CurveSegment[] {
|
|
144
|
+
for (let i = 0; i < segments.length; i++) {
|
|
145
|
+
const start = segments[i]!.start;
|
|
146
|
+
const end = segmentEnd(segments, i);
|
|
147
|
+
if (t <= start + MIN_SEGMENT_WIDTH || t >= end - MIN_SEGMENT_WIDTH) continue;
|
|
148
|
+
if (t < start || t > end) continue;
|
|
149
|
+
const u = (t - start) / (end - start);
|
|
150
|
+
const { p0, p1, p2, p3 } = segments[i]!.function;
|
|
151
|
+
const b01 = lerp(p0, p1, u);
|
|
152
|
+
const b12 = lerp(p1, p2, u);
|
|
153
|
+
const b23 = lerp(p2, p3, u);
|
|
154
|
+
const b012 = lerp(b01, b12, u);
|
|
155
|
+
const b123 = lerp(b12, b23, u);
|
|
156
|
+
const mid = lerp(b012, b123, u);
|
|
157
|
+
const next = cloneSegments(segments);
|
|
158
|
+
next.splice(
|
|
159
|
+
i,
|
|
160
|
+
1,
|
|
161
|
+
{ start, function: { p0, p1: b01, p2: b012, p3: mid } },
|
|
162
|
+
{ start: t, function: { p0: mid, p1: b123, p2: b23, p3 } },
|
|
163
|
+
);
|
|
164
|
+
return next;
|
|
165
|
+
}
|
|
166
|
+
return segments;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Delete interior key `keyIndex`, merging its two adjacent segments. The
|
|
171
|
+
* merged bezier keeps both outer endpoints and rescales the outer tangent
|
|
172
|
+
* controls so endpoint SLOPES are preserved (the interior wiggle is
|
|
173
|
+
* re-fitted — same approximation Unity's curve editor makes). Endpoint keys
|
|
174
|
+
* are not deletable; returns the input unchanged for them.
|
|
175
|
+
*/
|
|
176
|
+
export function deleteKey(segments: CurveSegment[], keyIndex: number): CurveSegment[] {
|
|
177
|
+
if (keyIndex <= 0 || keyIndex >= segments.length) return segments;
|
|
178
|
+
const left = segments[keyIndex - 1]!;
|
|
179
|
+
const right = segments[keyIndex]!;
|
|
180
|
+
const leftDur = right.start - left.start;
|
|
181
|
+
const rightDur = segmentEnd(segments, keyIndex) - right.start;
|
|
182
|
+
const total = leftDur + rightDur;
|
|
183
|
+
if (total <= 0) return segments;
|
|
184
|
+
const lf = left.function;
|
|
185
|
+
const rf = right.function;
|
|
186
|
+
const merged: CurveSegment = {
|
|
187
|
+
start: left.start,
|
|
188
|
+
function: {
|
|
189
|
+
p0: lf.p0,
|
|
190
|
+
p1: lf.p0 + (lf.p1 - lf.p0) * (leftDur > 0 ? total / leftDur : 1),
|
|
191
|
+
p2: rf.p3 + (rf.p2 - rf.p3) * (rightDur > 0 ? total / rightDur : 1),
|
|
192
|
+
p3: rf.p3,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const next = cloneSegments(segments);
|
|
196
|
+
next.splice(keyIndex - 1, 2, merged);
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Min/max over every control value (bezier hull bounds the curve). */
|
|
201
|
+
export function curveValueRange(segments: CurveSegment[]): { min: number; max: number } {
|
|
202
|
+
let min = Number.POSITIVE_INFINITY;
|
|
203
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
204
|
+
for (const s of segments) {
|
|
205
|
+
for (const v of [s.function.p0, s.function.p1, s.function.p2, s.function.p3]) {
|
|
206
|
+
if (v < min) min = v;
|
|
207
|
+
if (v > max) max = v;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return { min: 0, max: 1 };
|
|
211
|
+
return { min, max };
|
|
212
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The figma-grade inspector widget kit (spec 27 §5 C1). PURE,
|
|
3
|
+
* contract-driven components — `value` + `onChange` (+ optional
|
|
4
|
+
* `onChangeEnd` for the commit tick, see `shared.tsx`'s `ChangeHandlers<T>`
|
|
5
|
+
* doc comment). The kit stays contract-pure: widgets never import editor
|
|
6
|
+
* state, which is what lets the whole directory sit in the Apache face.
|
|
7
|
+
*
|
|
8
|
+
* This is an INTERNAL barrel — `../index.ts` is the published door
|
|
9
|
+
* (`@volter/editor-sdk/widgets`), and it re-exports this whole surface.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from './AlignmentGrid';
|
|
13
|
+
export * from './AssetSlotPicker';
|
|
14
|
+
export * from './BorderEditor';
|
|
15
|
+
export * from './ColorPicker';
|
|
16
|
+
export * from './CurveEditor';
|
|
17
|
+
export type { ColorFormat } from './color-utils';
|
|
18
|
+
export * from './curve-utils';
|
|
19
|
+
export * from './FilterEditor';
|
|
20
|
+
export * from './FontPicker';
|
|
21
|
+
export * from './GradientEditor';
|
|
22
|
+
export * from './ScrubbableInput';
|
|
23
|
+
export * from './ShadowEditor';
|
|
24
|
+
export * from './shared';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Button } from '../design-system';
|
|
2
|
+
/**
|
|
3
|
+
* Shared theme + tiny primitives for the inspector widget kit (spec 27 §5
|
|
4
|
+
* C1). Values are now sourced from `../../theme.ts` (editor-style-polish
|
|
5
|
+
* U0) — the editor's single canonical token module — rather than
|
|
6
|
+
* independently retyped here, which is how this kit's accent had drifted
|
|
7
|
+
* from the app's semantic accent (C-2).
|
|
8
|
+
* `THEME.accent` now follows whichever palette is composed with the active
|
|
9
|
+
* material. `FONT` also drops
|
|
10
|
+
* `'Inter'` (never loaded anywhere — 0 `@font-face` hits — so it only ever
|
|
11
|
+
* silently fell back to its own fallback chain, per finding I-29); `FONT`/
|
|
12
|
+
* `FONT_MONO` now resolve through the editor theme variables. Interactive
|
|
13
|
+
* controls themselves come from the canonical design-system primitives so
|
|
14
|
+
* this helper module cannot create a competing inline paint recipe.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type React from 'react';
|
|
18
|
+
import { themeVars } from '../design-system';
|
|
19
|
+
|
|
20
|
+
export const THEME = {
|
|
21
|
+
bg: themeVars.surface.panel,
|
|
22
|
+
surface: themeVars.surface.chrome,
|
|
23
|
+
surfaceHover: themeVars.surface.raised,
|
|
24
|
+
border: themeVars.boundary.default,
|
|
25
|
+
text: themeVars.content.primary,
|
|
26
|
+
textMuted: themeVars.content.muted,
|
|
27
|
+
onAccent: themeVars.content.onAccent,
|
|
28
|
+
accent: themeVars.accent.default,
|
|
29
|
+
neutralHover: themeVars.neutralOverlay.hover,
|
|
30
|
+
radiusSmall: themeVars.shape.small,
|
|
31
|
+
radiusMedium: themeVars.shape.medium,
|
|
32
|
+
inputBg: themeVars.surface.inset,
|
|
33
|
+
dynamic: themeVars.semantic.dynamic,
|
|
34
|
+
dynamicBg: themeVars.semantic.dynamicMuted,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export const FONT = themeVars.typography.sans;
|
|
38
|
+
export const FONT_MONO = themeVars.typography.mono;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every widget's change contract (report §"prop shape" for the C2 wiring
|
|
42
|
+
* step): `onChange` fires on every interaction tick (drag/scrub/keystroke —
|
|
43
|
+
* the LIVE PREVIEW a caller can apply immediately, mirroring `boxEdit.apply`/
|
|
44
|
+
* `inspector.set`'s optimistic echo, T0 §2/A4); `onChangeEnd` fires once per
|
|
45
|
+
* gesture (pointerup/blur/Enter — the COMMIT a caller should push through
|
|
46
|
+
* `writeStyleAuto`/`inspector.set` as a single undo step, mirroring
|
|
47
|
+
* `BoxEditProvider.end`). A caller that only cares about single-shot edits
|
|
48
|
+
* (a `<Select>`, a click) may omit `onChangeEnd` — widgets fall back to
|
|
49
|
+
* treating `onChange` as the commit in that case.
|
|
50
|
+
*/
|
|
51
|
+
export interface ChangeHandlers<T> {
|
|
52
|
+
value: T;
|
|
53
|
+
onChange: (value: T) => void;
|
|
54
|
+
onChangeEnd?: ((value: T) => void) | undefined;
|
|
55
|
+
disabled?: boolean | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function fireChange<T>(handlers: Pick<ChangeHandlers<T>, 'onChange'>, value: T): void {
|
|
59
|
+
handlers.onChange(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function fireEnd<T>(
|
|
63
|
+
handlers: Pick<ChangeHandlers<T>, 'onChange' | 'onChangeEnd'>,
|
|
64
|
+
value: T,
|
|
65
|
+
): void {
|
|
66
|
+
(handlers.onChangeEnd ?? handlers.onChange)(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const rowStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 4 };
|
|
70
|
+
|
|
71
|
+
/* ------------------------------------------------------------------------ */
|
|
72
|
+
/* Inspector-section style kit (editor-style-polish U3, I-8/I-10/I-12/I-13/ */
|
|
73
|
+
/* I-17/I-22) — the ONE home for the style objects the first-party sections */
|
|
74
|
+
/* used to hand-duplicate ~15 times each with tiny unintentional drift. */
|
|
75
|
+
/* ------------------------------------------------------------------------ */
|
|
76
|
+
|
|
77
|
+
/** Stacked field label ("label on its own line, field below" grammar) —
|
|
78
|
+
* replaces the `{fontSize:10, color:'#8c8c8c', marginBottom:2}` literal
|
|
79
|
+
* re-authored in nearly every `components/inspectors/*` section (I-8). */
|
|
80
|
+
export const fieldLabelStyle: React.CSSProperties = {
|
|
81
|
+
fontSize: 'var(--vgai-font-sm)',
|
|
82
|
+
color: themeVars.content.muted,
|
|
83
|
+
marginBottom: 2,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Sub-header / group label ("label over a related cluster of fields") — the
|
|
87
|
+
* ONE treatment for what used to be four (I-12): `Inspector.tsx`'s generic
|
|
88
|
+
* group header (11/600/uppercase/ls 0.4/#9aa0a6 — the incumbent this
|
|
89
|
+
* standardizes on), `ParticleSection.subHeaderStyle`,
|
|
90
|
+
* `PostProcessingSection.effectLabel`, and `MaterialSection`'s Textures. */
|
|
91
|
+
export const groupLabelStyle: React.CSSProperties = {
|
|
92
|
+
fontSize: 'var(--vgai-font-base)',
|
|
93
|
+
fontWeight: 600,
|
|
94
|
+
color: themeVars.content.muted,
|
|
95
|
+
textTransform: 'uppercase',
|
|
96
|
+
letterSpacing: 0.4,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** One label-column width for every label-left/field-right row (I-13 —
|
|
100
|
+
* replaces the 56/64/70/76/80 ladder across Inspector.tsx/`Field`/
|
|
101
|
+
* `ComponentsSection`). */
|
|
102
|
+
export const LABEL_COL_WIDTH = 72;
|
|
103
|
+
|
|
104
|
+
export function ToggleButton({
|
|
105
|
+
label,
|
|
106
|
+
active,
|
|
107
|
+
disabled,
|
|
108
|
+
onClick,
|
|
109
|
+
testId,
|
|
110
|
+
}: {
|
|
111
|
+
label: React.ReactNode;
|
|
112
|
+
active: boolean;
|
|
113
|
+
disabled?: boolean | undefined;
|
|
114
|
+
onClick: () => void;
|
|
115
|
+
testId?: string | undefined;
|
|
116
|
+
}): React.ReactElement {
|
|
117
|
+
return (
|
|
118
|
+
<Button
|
|
119
|
+
variant={active ? 'primary' : 'secondary'}
|
|
120
|
+
size="compact"
|
|
121
|
+
aria-pressed={active}
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
data-testid={testId}
|
|
124
|
+
onClick={() => {
|
|
125
|
+
if (!disabled) onClick();
|
|
126
|
+
}}
|
|
127
|
+
style={{ flex: 1 }}
|
|
128
|
+
>
|
|
129
|
+
{label}
|
|
130
|
+
</Button>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `parseNumericValue('12.5px') -> {num: 12.5, unit: 'px'}` (visual-edit :98). */
|
|
135
|
+
export function parseNumericValue(value: string): { num: number; unit: string } {
|
|
136
|
+
const match = value.match(/^(-?\d+\.?\d*)\s*(px|em|rem|%|vh|vw|deg|s|ms)?$/);
|
|
137
|
+
if (match) return { num: Number.parseFloat(match[1] ?? '0'), unit: match[2] || '' };
|
|
138
|
+
const num = Number.parseFloat(value);
|
|
139
|
+
return { num: Number.isNaN(num) ? 0 : num, unit: '' };
|
|
140
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive-edit (undo-gesture) scope — the ONE thing the shared input
|
|
3
|
+
* primitives (`primitives/NumberInput.tsx` / `primitives/ColorInput.tsx`)
|
|
4
|
+
* need from the editor: "a continuous gesture (scrub / color drag) starts /
|
|
5
|
+
* ends now, coalesce it into a single undo step".
|
|
6
|
+
*
|
|
7
|
+
* Split out of the editor's `EditorContext.tsx`: the widget kit is a public
|
|
8
|
+
* export (`@volter/editor-sdk/widgets`) that scaffolded PROJECTS typecheck — importing
|
|
9
|
+
* `EditorContext` (and through it `EditorShellStore` and most of the editor)
|
|
10
|
+
* from a widget would drag the whole editor source graph into every project's
|
|
11
|
+
* `tsc`. This module's closure is react-only; `EditorProvider` bridges it to
|
|
12
|
+
* the real store (`beginInteractiveEdit`/`endInteractiveEdit`).
|
|
13
|
+
*
|
|
14
|
+
* Widgets consume it optionally (`useInteractiveEditScope()` returns `null`
|
|
15
|
+
* outside a provider — e.g. the widget gallery, or a project tool rendered
|
|
16
|
+
* in some future host): no scope simply means no undo coalescing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createContext, useContext } from 'react';
|
|
20
|
+
|
|
21
|
+
/** Begin/end a coalesced interactive edit gesture (one undo step). Both are
|
|
22
|
+
* idempotent in the editor's implementation — safe to call defensively. */
|
|
23
|
+
export interface InteractiveEditScope {
|
|
24
|
+
begin(): void;
|
|
25
|
+
end(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const InteractiveEditScopeContext = createContext<InteractiveEditScope | null>(null);
|
|
29
|
+
|
|
30
|
+
/** The ambient gesture scope, or `null` when no host provides one. */
|
|
31
|
+
export function useInteractiveEditScope(): InteractiveEditScope | null {
|
|
32
|
+
return useContext(InteractiveEditScopeContext);
|
|
33
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type HTMLAttributes, type LabelHTMLAttributes, type ReactNode, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { Actions, Inline, Stack } from '../primitives/Layout';
|
|
4
|
+
import { Text } from '../primitives/Text';
|
|
5
|
+
|
|
6
|
+
function classes(base: string, className?: string): string {
|
|
7
|
+
return className ? `${base} ${className}` : base;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DialogProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
labelledBy: string;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
size?: 'compact' | 'default' | 'wide';
|
|
14
|
+
variant?: 'default' | 'command';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Modal editor surface. The scrim, sizing, elevation, and spacing are one recipe. */
|
|
18
|
+
export function Dialog({
|
|
19
|
+
labelledBy,
|
|
20
|
+
onDismiss,
|
|
21
|
+
size = 'default',
|
|
22
|
+
variant = 'default',
|
|
23
|
+
className,
|
|
24
|
+
...props
|
|
25
|
+
}: DialogProps) {
|
|
26
|
+
// F10/F11 (U6.5): the `position: fixed` scrim must anchor to the window, not
|
|
27
|
+
// to whatever ancestor happens to be its containing block. Rendered inline it
|
|
28
|
+
// sat inside `#editor-root` (z-index 10), so the islands-mode footer/session
|
|
29
|
+
// tray (`#editor-bottom`, z-index 100) painted OVER the scrim (F10); and under
|
|
30
|
+
// a glass theme a card's `backdrop-filter` became the fixed scrim's containing
|
|
31
|
+
// block, collapsing `inset: 0` to a ~40px box and shoving the dialog off the
|
|
32
|
+
// top edge (F11). Portaling to the filter-free theme root — a sibling of
|
|
33
|
+
// `#editor-bottom` — makes `inset: 0` resolve to the viewport (full-cover
|
|
34
|
+
// scrim, correctly centered dialog) and lets the scrim's z-index win. Falls
|
|
35
|
+
// back to inline rendering when no theme root is present (isolated unit tests).
|
|
36
|
+
//
|
|
37
|
+
// R2 / 42:608 (P6-U7): resolve the portal root SYNCHRONOUSLY on the first
|
|
38
|
+
// render (lazy `useState` init) rather than in a post-mount layout effect.
|
|
39
|
+
// The former effect-based null→portal flip re-committed the scrim ONE render
|
|
40
|
+
// after mount, which — because switching an element between inline and a
|
|
41
|
+
// `createPortal` boundary remounts its DOM subtree — destroyed and recreated
|
|
42
|
+
// any child input right after a consumer's mount-time autofocus ran. That is
|
|
43
|
+
// exactly why Ctrl+K opened the Command Palette with its search box unfocused
|
|
44
|
+
// (focus fell to <body>), so typed text and Enter went nowhere and no
|
|
45
|
+
// document opened. The editor mounts a single `.vgai-editor-theme` root at
|
|
46
|
+
// app init, long before any Dialog opens, so a plain `document.querySelector`
|
|
47
|
+
// finds it on render 1 — the scrim portals from the start, the input mounts
|
|
48
|
+
// once, and focus is preserved.
|
|
49
|
+
const [portalRoot] = useState<Element | null>(() =>
|
|
50
|
+
typeof document === 'undefined' ? null : document.querySelector('.vgai-editor-theme'),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const scrim = (
|
|
54
|
+
<div
|
|
55
|
+
className="vgai-dialog-scrim"
|
|
56
|
+
data-variant={variant}
|
|
57
|
+
onPointerDown={(event) => {
|
|
58
|
+
if (event.target === event.currentTarget) onDismiss?.();
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
{...props}
|
|
63
|
+
role="dialog"
|
|
64
|
+
aria-modal="true"
|
|
65
|
+
aria-labelledby={labelledBy}
|
|
66
|
+
data-size={size}
|
|
67
|
+
data-variant={variant}
|
|
68
|
+
className={classes('vgai-dialog', className)}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return portalRoot ? createPortal(scrim, portalRoot) : scrim;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function DialogHeader({
|
|
77
|
+
titleId,
|
|
78
|
+
title,
|
|
79
|
+
description,
|
|
80
|
+
}: {
|
|
81
|
+
titleId: string;
|
|
82
|
+
title: ReactNode;
|
|
83
|
+
description?: ReactNode;
|
|
84
|
+
}) {
|
|
85
|
+
return (
|
|
86
|
+
<Stack className="vgai-dialog-header" gap={2}>
|
|
87
|
+
<Text id={titleId} as="h2" variant="heading">
|
|
88
|
+
{title}
|
|
89
|
+
</Text>
|
|
90
|
+
{description && (
|
|
91
|
+
<Text as="p" tone="muted">
|
|
92
|
+
{description}
|
|
93
|
+
</Text>
|
|
94
|
+
)}
|
|
95
|
+
</Stack>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function DialogBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
100
|
+
return <Stack {...props} className={classes('vgai-dialog-body', className)} />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function DialogFooter({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
104
|
+
return (
|
|
105
|
+
<Actions {...props} className={classes('vgai-dialog-footer', className)}>
|
|
106
|
+
{children}
|
|
107
|
+
</Actions>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function DialogField({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>) {
|
|
112
|
+
return <label {...props} className={classes('vgai-dialog-field', className)} />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function DialogFieldLabel({ children }: { children: ReactNode }) {
|
|
116
|
+
return (
|
|
117
|
+
<Text variant="label" tone="muted">
|
|
118
|
+
{children}
|
|
119
|
+
</Text>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function JoinedField({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
124
|
+
return <Inline {...props} className={classes('vgai-joined-field', className)} />;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function JoinedFieldSuffix({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
128
|
+
return <span {...props} className={classes('vgai-joined-field-suffix', className)} />;
|
|
129
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HTMLAttributes, LabelHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
function classes(base: string, className?: string): string {
|
|
4
|
+
return className ? `${base} ${className}` : base;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function FieldGroup({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
8
|
+
return <div {...props} className={classes('vgai-field-group', className)} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FieldRowProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
htmlFor?: string;
|
|
14
|
+
hint?: ReactNode;
|
|
15
|
+
compact?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FieldRow({
|
|
19
|
+
label,
|
|
20
|
+
htmlFor,
|
|
21
|
+
hint,
|
|
22
|
+
compact = false,
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
}: FieldRowProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
{...props}
|
|
30
|
+
data-compact={compact || undefined}
|
|
31
|
+
className={classes('vgai-field-row', className)}
|
|
32
|
+
>
|
|
33
|
+
<label htmlFor={htmlFor} className="vgai-field-label">
|
|
34
|
+
{label}
|
|
35
|
+
</label>
|
|
36
|
+
<div className="vgai-field-control">{children}</div>
|
|
37
|
+
{hint && <div className="vgai-field-hint">{hint}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function FieldLabel({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>) {
|
|
43
|
+
return <label {...props} className={classes('vgai-field-label', className)} />;
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface EditorListButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
selected?: boolean;
|
|
5
|
+
accent?: 'bar' | 'none';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Selectable list row with canonical hover, focus, disabled, and selection paint. */
|
|
9
|
+
export const EditorListButton = forwardRef<HTMLButtonElement, EditorListButtonProps>(
|
|
10
|
+
function EditorListButton(
|
|
11
|
+
{ selected = false, accent = 'bar', className, type = 'button', ...props },
|
|
12
|
+
ref,
|
|
13
|
+
) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
{...props}
|
|
17
|
+
ref={ref}
|
|
18
|
+
type={type}
|
|
19
|
+
data-selected={selected || undefined}
|
|
20
|
+
data-accent={accent}
|
|
21
|
+
className={className ? `vgai-list-button ${className}` : 'vgai-list-button'}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type StateSurfaceTone = 'neutral' | 'loading' | 'error' | 'success';
|
|
4
|
+
|
|
5
|
+
export interface StateSurfaceProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
6
|
+
tone?: StateSurfaceTone;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
action?: ReactNode;
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StateSurface({
|
|
15
|
+
tone = 'neutral',
|
|
16
|
+
icon,
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
action,
|
|
20
|
+
compact = false,
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: StateSurfaceProps) {
|
|
24
|
+
const defaultRole =
|
|
25
|
+
tone === 'error' ? 'alert' : tone === 'loading' || tone === 'success' ? 'status' : undefined;
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
{...props}
|
|
29
|
+
role={props.role ?? defaultRole}
|
|
30
|
+
data-tone={tone}
|
|
31
|
+
data-compact={compact || undefined}
|
|
32
|
+
className={className ? `vgai-state-surface ${className}` : 'vgai-state-surface'}
|
|
33
|
+
>
|
|
34
|
+
{icon && <div className="vgai-state-surface-icon">{icon}</div>}
|
|
35
|
+
<strong className="vgai-state-surface-title">{title}</strong>
|
|
36
|
+
{description && <div className="vgai-state-surface-description">{description}</div>}
|
|
37
|
+
{action && <div className="vgai-state-surface-action">{action}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|