@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
package/src/views.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A VIEW'S VERBS — the one product door into a panel that is not the centre
|
|
3
|
+
* document (ARCHITECTURE-CORE §Our tier over the Code-OSS frame, "a panel →
|
|
4
|
+
* view"; WORK.md §The core is Code-OSS U8, ruling (1), 2026-09-19).
|
|
5
|
+
*
|
|
6
|
+
* ## The problem this exists for, and the ruling that shaped it
|
|
7
|
+
*
|
|
8
|
+
* `editor.document.*` reaches only the ACTIVE CENTRE DOCUMENT, by its own
|
|
9
|
+
* contract. Every drawer utility — Console, Profiler, Network, I5's node
|
|
10
|
+
* editor — is therefore undrivable by the product, and each one that needed
|
|
11
|
+
* driving was shipping its OWN session verb to compensate (I5's
|
|
12
|
+
* `blender-node-view` is the worked case, and UV Editing, Animation and
|
|
13
|
+
* Texture Paint were each about to pay the same price).
|
|
14
|
+
*
|
|
15
|
+
* The ruling refused both a utility scope on the document door and a verb per
|
|
16
|
+
* view: **under the Code-OSS frame every one of our views is a VS Code view
|
|
17
|
+
* in a view container, and its state and verbs are reached through
|
|
18
|
+
* `vgai.<view>.<verb>` commands the bridge dispatches into the view** — the
|
|
19
|
+
* same one-name door as every other verb, so `vgai eval` reaches it through
|
|
20
|
+
* the frame's command service.
|
|
21
|
+
*
|
|
22
|
+
* ## Why the registry is HERE and not in the editor
|
|
23
|
+
*
|
|
24
|
+
* Both sides of it are outside `packages/editor/src`: a view's verbs are
|
|
25
|
+
* registered by the package that OWNS the view (`@vgai/blender`'s node
|
|
26
|
+
* editor), and they are read by the fork's bridge. The editor host mediates
|
|
27
|
+
* nothing, so a registry inside it would be a closure file with no reader of
|
|
28
|
+
* its own — the same reasoning `registerEditorHost` follows one module over.
|
|
29
|
+
*
|
|
30
|
+
* ## The two doors, one table
|
|
31
|
+
*
|
|
32
|
+
* Standalone `vgai edit` has no VS Code command service, so the SESSION's own
|
|
33
|
+
* verb is how a view is driven there. That is not a second implementation:
|
|
34
|
+
* the session verb calls {@link invokeViewVerb} on the same table the frame's
|
|
35
|
+
* commands call, exactly as `key-actions.ts`'s action table is one
|
|
36
|
+
* table behind two keyboards. A NEW view registers verbs here and adds no
|
|
37
|
+
* session verb at all.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export interface ViewVerb {
|
|
41
|
+
/** The verb, bare: `state`, `view-all`, `zoom`. The frame publishes it as
|
|
42
|
+
* `vgai.<view>.<verb>`; the session spells it its own way. */
|
|
43
|
+
readonly id: string;
|
|
44
|
+
/** What the palette calls it, when the verb is one a PERSON would run. A
|
|
45
|
+
* verb with no title is reachable by command id and by the session, and is
|
|
46
|
+
* not listed — a read (`state`) or a refusal probe is not a menu item. */
|
|
47
|
+
readonly title?: string;
|
|
48
|
+
/** Run it. `args` is the command's own argument object, unvalidated: a view
|
|
49
|
+
* validates its own arguments and REFUSES BY NAME, because what an argument
|
|
50
|
+
* means is the view's and no registry can know it. */
|
|
51
|
+
readonly run: (args?: Record<string, unknown>) => unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ViewVerbContribution {
|
|
55
|
+
/** The view's id, and the middle segment of every command it publishes —
|
|
56
|
+
* `blender-node-view` → `vgai.blender-node-view.view-all`. It is the same
|
|
57
|
+
* id the view registers under when it becomes a VS Code view. */
|
|
58
|
+
readonly view: string;
|
|
59
|
+
/** What the view is called, for the palette entry's category. */
|
|
60
|
+
readonly title: string;
|
|
61
|
+
readonly verbs: readonly ViewVerb[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const REGISTRY_KEY = Symbol.for('vgai.editor.viewVerbs');
|
|
65
|
+
interface RegistryState {
|
|
66
|
+
contributions: readonly ViewVerbContribution[];
|
|
67
|
+
listeners: Set<() => void>;
|
|
68
|
+
version: number;
|
|
69
|
+
}
|
|
70
|
+
const globals = globalThis as typeof globalThis & { [REGISTRY_KEY]?: RegistryState };
|
|
71
|
+
// One registry across module copies, for the reason `host.ts` has one: the
|
|
72
|
+
// fork's bridge and a package's contribution can be different instances of
|
|
73
|
+
// this module, and a view registered into a second table is a view the frame
|
|
74
|
+
// never sees.
|
|
75
|
+
globals[REGISTRY_KEY] ??= { contributions: [], listeners: new Set(), version: 0 };
|
|
76
|
+
const state: RegistryState = globals[REGISTRY_KEY];
|
|
77
|
+
|
|
78
|
+
function emit(): void {
|
|
79
|
+
state.version++;
|
|
80
|
+
for (const listener of state.listeners) listener();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a view's verbs. Returns the removal, which only fires while this
|
|
85
|
+
* contribution is still the live one for its id.
|
|
86
|
+
*
|
|
87
|
+
* THE SAME VIEW REGISTERING AGAIN REPLACES ITSELF. A contribution module is
|
|
88
|
+
* evaluated more than once per session by design: the tool loader imports it
|
|
89
|
+
* as `/@fs/<path>?t=<version>` (packages/editor/src/tool-loader.ts) so a
|
|
90
|
+
* project's save re-evaluates it, and a BUNDLED package's contribution is
|
|
91
|
+
* reached by its bare specifier as well — while this registry is ONE table on
|
|
92
|
+
* `globalThis` across
|
|
93
|
+
* every module copy. Module-level `registerViewVerbs(...)` in a contribution
|
|
94
|
+
* therefore runs once per evaluation, and on 2026-09-19 the second evaluation
|
|
95
|
+
* threw and the editor did not boot. The re-evaluation is recognised by the
|
|
96
|
+
* view id AND title agreeing — the same source, loaded again — and the newer
|
|
97
|
+
* table wins, its predecessor's disposer becoming a no-op.
|
|
98
|
+
*
|
|
99
|
+
* A DIFFERENT view claiming the id still THROWS: the id keys the whole
|
|
100
|
+
* `vgai.<view>.<verb>` command namespace, and a second view under it would
|
|
101
|
+
* publish commands that shadow the first with no sign of it.
|
|
102
|
+
*/
|
|
103
|
+
export function registerViewVerbs(contribution: ViewVerbContribution): () => void {
|
|
104
|
+
const existing = state.contributions.find((entry) => entry.view === contribution.view);
|
|
105
|
+
if (existing && existing.title !== contribution.title) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`registerViewVerbs: the view "${contribution.view}" already publishes verbs as "${existing.title}"; "${contribution.title}" is a different view under the same id. A view id keys its whole \`vgai.<view>.<verb>\` command namespace, so a second view under it would shadow the first with no sign of it.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
state.contributions = [
|
|
111
|
+
...state.contributions.filter((entry) => entry !== existing),
|
|
112
|
+
contribution,
|
|
113
|
+
];
|
|
114
|
+
emit();
|
|
115
|
+
return () => {
|
|
116
|
+
if (!state.contributions.includes(contribution)) return;
|
|
117
|
+
state.contributions = state.contributions.filter((entry) => entry !== contribution);
|
|
118
|
+
emit();
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Every view publishing verbs right now. */
|
|
123
|
+
export function viewVerbContributions(): readonly ViewVerbContribution[] {
|
|
124
|
+
return state.contributions;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function viewVerbsVersion(): number {
|
|
128
|
+
return state.version;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function subscribeViewVerbs(listener: () => void): () => void {
|
|
132
|
+
state.listeners.add(listener);
|
|
133
|
+
return () => state.listeners.delete(listener);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run one view's verb. The refusal is a THROW naming the whole vocabulary,
|
|
138
|
+
* because both callers — the frame's command and the session's verb — report
|
|
139
|
+
* a thrown message and a silent `undefined` is how a typo becomes a mystery.
|
|
140
|
+
*/
|
|
141
|
+
export function invokeViewVerb(
|
|
142
|
+
view: string,
|
|
143
|
+
verb: string,
|
|
144
|
+
args?: Record<string, unknown>,
|
|
145
|
+
): unknown {
|
|
146
|
+
const contribution = state.contributions.find((entry) => entry.view === view);
|
|
147
|
+
if (!contribution) {
|
|
148
|
+
const known = state.contributions.map((entry) => entry.view).join(', ') || 'none';
|
|
149
|
+
throw new Error(`No view "${view}" publishes verbs right now — the views that do: ${known}.`);
|
|
150
|
+
}
|
|
151
|
+
const entry = contribution.verbs.find((candidate) => candidate.id === verb);
|
|
152
|
+
if (!entry) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`The view "${view}" has no verb "${verb}" — it publishes: ${contribution.verbs.map((candidate) => candidate.id).join(', ')}.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return entry.run(args);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function __resetViewVerbsForTest(): void {
|
|
161
|
+
state.contributions = [];
|
|
162
|
+
state.listeners.clear();
|
|
163
|
+
state.version = 0;
|
|
164
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor design system — patterns, primitives and the theme members its
|
|
3
|
+
* own surfaces read, as ONE module. Theme definitions, CSS recipes,
|
|
4
|
+
* primitives and recurring patterns stay implementation details below it.
|
|
5
|
+
*
|
|
6
|
+
* It is not a published subpath: `./index.ts` re-exports it, and the face is
|
|
7
|
+
* `@volter/editor-sdk/widgets` (ARCHITECTURE-CORE §The target shape, rule 5 —
|
|
8
|
+
* one API, four faces). Everything in and below this file must stay
|
|
9
|
+
* react-only, or the Apache package starts reaching AGPL host source.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
composeEditorAppearance,
|
|
14
|
+
EDITOR_MATERIAL_IDS,
|
|
15
|
+
EDITOR_MATERIALS,
|
|
16
|
+
type EditorMaterialChoice,
|
|
17
|
+
type EditorMaterialId,
|
|
18
|
+
type EditorPalette,
|
|
19
|
+
isEditorMaterialId,
|
|
20
|
+
} from './editor-appearance';
|
|
21
|
+
export * from './patterns/Dialog';
|
|
22
|
+
export * from './patterns/Fields';
|
|
23
|
+
export * from './patterns/List';
|
|
24
|
+
export * from './patterns/StateSurface';
|
|
25
|
+
export * from './patterns/Surfaces';
|
|
26
|
+
export * from './patterns/Tabs';
|
|
27
|
+
export * from './patterns/Toolbar';
|
|
28
|
+
export * from './patterns/Tree';
|
|
29
|
+
export * from './primitives/AnchoredMenu';
|
|
30
|
+
export * from './primitives/Button';
|
|
31
|
+
export * from './primitives/banner-tones';
|
|
32
|
+
export * from './primitives/ColorInput';
|
|
33
|
+
export * from './primitives/clamp-to-viewport';
|
|
34
|
+
export * from './primitives/EditorIcon';
|
|
35
|
+
export { editorIcons } from './primitives/editor-icons';
|
|
36
|
+
export * from './primitives/FormControls';
|
|
37
|
+
export * from './primitives/HoverPreview';
|
|
38
|
+
export * from './primitives/JsonInput';
|
|
39
|
+
export * from './primitives/Layout';
|
|
40
|
+
export * from './primitives/Menu';
|
|
41
|
+
export * from './primitives/NumberInput';
|
|
42
|
+
export * from './primitives/Panel';
|
|
43
|
+
export * from './primitives/SectionHeader';
|
|
44
|
+
export * from './primitives/Text';
|
|
45
|
+
export * from './primitives/ThemeRootPortal';
|
|
46
|
+
export * from './primitives/Tooltip';
|
|
47
|
+
export * from './primitives/Vec3Input';
|
|
48
|
+
// Product surfaces consume runtime theme references through this boundary as
|
|
49
|
+
// they migrate away from direct `theme.ts` imports. Keeping the references
|
|
50
|
+
// here preserves custom-theme behavior without exposing design-system internals.
|
|
51
|
+
export {
|
|
52
|
+
accent,
|
|
53
|
+
accentMuted,
|
|
54
|
+
applyEditorTheme,
|
|
55
|
+
bg,
|
|
56
|
+
border,
|
|
57
|
+
chromeSize,
|
|
58
|
+
controlSize,
|
|
59
|
+
danger,
|
|
60
|
+
dangerFaint,
|
|
61
|
+
dangerMuted,
|
|
62
|
+
EDITOR_THEME_CLASS,
|
|
63
|
+
type EditorTheme,
|
|
64
|
+
type EditorThemeId,
|
|
65
|
+
type EditorThemeVariable,
|
|
66
|
+
editorThemes,
|
|
67
|
+
editorThemeVariables,
|
|
68
|
+
fontMono,
|
|
69
|
+
fontSans,
|
|
70
|
+
fontSize,
|
|
71
|
+
fontSizeVar,
|
|
72
|
+
fontWeight,
|
|
73
|
+
graphiteDarkEditorTheme,
|
|
74
|
+
lineHeight,
|
|
75
|
+
lineHeightVar,
|
|
76
|
+
midnightHighContrastEditorTheme,
|
|
77
|
+
motion,
|
|
78
|
+
motionVar,
|
|
79
|
+
radius,
|
|
80
|
+
scrim,
|
|
81
|
+
selection,
|
|
82
|
+
shadow,
|
|
83
|
+
space,
|
|
84
|
+
spaceVar,
|
|
85
|
+
strokeWidth,
|
|
86
|
+
success,
|
|
87
|
+
successMuted,
|
|
88
|
+
text,
|
|
89
|
+
themeVars,
|
|
90
|
+
warn,
|
|
91
|
+
warnMuted,
|
|
92
|
+
} from './theme';
|
|
93
|
+
export { zIndex } from './z-index';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { CLASSIC_MATERIAL, GLASS_MATERIAL } from './editor-material';
|
|
2
|
+
import type { EditorTheme } from './theme';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Editor appearance is deliberately composed from independent axes:
|
|
6
|
+
*
|
|
7
|
+
* palette (a theme-library document) × material
|
|
8
|
+
*
|
|
9
|
+
* Palette documents own chroma and typography. Materials own surface physics,
|
|
10
|
+
* boundaries, neutral interaction overlays, shape, elevation, and treatment.
|
|
11
|
+
* Components only receive the resolved EditorTheme token set and therefore
|
|
12
|
+
* remain blind to both axes.
|
|
13
|
+
*/
|
|
14
|
+
export const EDITOR_MATERIAL_IDS = ['classic', 'glass'] as const;
|
|
15
|
+
/** One of the editor's own materials, or one a package's style bundle
|
|
16
|
+
* carries ({@link registerContributedMaterial}). */
|
|
17
|
+
export type EditorMaterialId = string;
|
|
18
|
+
|
|
19
|
+
export interface EditorMaterialChoice {
|
|
20
|
+
readonly id: EditorMaterialId;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An OPAQUE material a package's style bundle carries (`@volter/editor-sdk/looks`
|
|
27
|
+
* `MaterialContribution`): Classic's physics under its own shape and
|
|
28
|
+
* elevation. Glass is the editor's own and is not contributable.
|
|
29
|
+
*/
|
|
30
|
+
export interface ContributedMaterialDefinition extends EditorMaterialChoice {
|
|
31
|
+
readonly shape: EditorTheme['shape'];
|
|
32
|
+
readonly elevation: EditorTheme['elevation'];
|
|
33
|
+
readonly density?: EditorTheme['density'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The only fields a color-theme document may contribute to composition. */
|
|
37
|
+
export type EditorPalette = Pick<EditorTheme, 'id' | 'color' | 'typography'>;
|
|
38
|
+
|
|
39
|
+
export const EDITOR_MATERIALS: readonly EditorMaterialChoice[] = Object.freeze([
|
|
40
|
+
{
|
|
41
|
+
id: 'classic',
|
|
42
|
+
title: 'Classic',
|
|
43
|
+
description: 'Opaque editor surfaces with conventional bars and panels.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'glass',
|
|
47
|
+
title: 'Glass',
|
|
48
|
+
description: 'Adaptive clear material with refraction, frost, and floating chrome.',
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const contributedMaterials = new Map<string, ContributedMaterialDefinition>();
|
|
53
|
+
let cachedMaterials: readonly EditorMaterialChoice[] | null = null;
|
|
54
|
+
const materialListeners = new Set<() => void>();
|
|
55
|
+
|
|
56
|
+
/** Every material the switch surfaces offer — the editor's own, then the
|
|
57
|
+
* contributed in registration order. Stable between registrations. */
|
|
58
|
+
export function editorMaterials(): readonly EditorMaterialChoice[] {
|
|
59
|
+
cachedMaterials ??= [
|
|
60
|
+
...EDITOR_MATERIALS,
|
|
61
|
+
...[...contributedMaterials.values()].map(({ id, title, description }) => ({
|
|
62
|
+
id,
|
|
63
|
+
title,
|
|
64
|
+
description,
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
67
|
+
return cachedMaterials;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function subscribeEditorMaterials(listener: () => void): () => void {
|
|
71
|
+
materialListeners.add(listener);
|
|
72
|
+
return () => materialListeners.delete(listener);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Register the material a style bundle carries. Returns the unregister. A
|
|
76
|
+
* duplicate id throws — ids key the persisted appearance setting. */
|
|
77
|
+
export function registerContributedMaterial(definition: ContributedMaterialDefinition): () => void {
|
|
78
|
+
if (isEditorMaterialId(definition.id))
|
|
79
|
+
throw new Error(
|
|
80
|
+
`registerContributedMaterial: material "${definition.id}" is already registered.`,
|
|
81
|
+
);
|
|
82
|
+
contributedMaterials.set(definition.id, definition);
|
|
83
|
+
cachedMaterials = null;
|
|
84
|
+
for (const listener of materialListeners) listener();
|
|
85
|
+
return () => {
|
|
86
|
+
if (contributedMaterials.get(definition.id) !== definition) return;
|
|
87
|
+
contributedMaterials.delete(definition.id);
|
|
88
|
+
cachedMaterials = null;
|
|
89
|
+
for (const listener of materialListeners) listener();
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isEditorMaterialId(value: unknown): value is EditorMaterialId {
|
|
94
|
+
return (
|
|
95
|
+
typeof value === 'string' &&
|
|
96
|
+
(EDITOR_MATERIAL_IDS.includes(value as (typeof EDITOR_MATERIAL_IDS)[number]) ||
|
|
97
|
+
contributedMaterials.has(value))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Compose one palette source with the selected material. */
|
|
102
|
+
export function composeEditorAppearance(
|
|
103
|
+
palette: EditorPalette,
|
|
104
|
+
materialId: EditorMaterialId,
|
|
105
|
+
reducedTransparency = false,
|
|
106
|
+
): EditorTheme {
|
|
107
|
+
if (materialId !== 'glass') {
|
|
108
|
+
// Classic, or a contributed opaque material: Classic's physics under the
|
|
109
|
+
// material's own shape and elevation.
|
|
110
|
+
const material = contributedMaterials.get(materialId);
|
|
111
|
+
return {
|
|
112
|
+
id: material ? `${palette.id}--${material.id}` : palette.id,
|
|
113
|
+
color: palette.color,
|
|
114
|
+
typography: palette.typography,
|
|
115
|
+
shape: material?.shape ?? CLASSIC_MATERIAL.shape,
|
|
116
|
+
elevation: material?.elevation ?? CLASSIC_MATERIAL.elevation,
|
|
117
|
+
...(material?.density ? { density: material.density } : {}),
|
|
118
|
+
appearance: { material: 'classic', transparency: 'standard' },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const resolvedId = `${palette.id}--${reducedTransparency ? 'glass-reduced' : 'glass'}`;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id: resolvedId,
|
|
126
|
+
color: {
|
|
127
|
+
// Material owns the physical surface; palette owns every semantic hue.
|
|
128
|
+
surface: reducedTransparency
|
|
129
|
+
? GLASS_MATERIAL.reducedTransparency.surface
|
|
130
|
+
: GLASS_MATERIAL.color.surface,
|
|
131
|
+
boundary: GLASS_MATERIAL.color.boundary,
|
|
132
|
+
content: palette.color.content,
|
|
133
|
+
accent: palette.color.accent,
|
|
134
|
+
semantic: palette.color.semantic,
|
|
135
|
+
neutralOverlay: GLASS_MATERIAL.color.neutralOverlay,
|
|
136
|
+
scrim: reducedTransparency
|
|
137
|
+
? GLASS_MATERIAL.reducedTransparency.scrim
|
|
138
|
+
: GLASS_MATERIAL.color.scrim,
|
|
139
|
+
},
|
|
140
|
+
typography: palette.typography,
|
|
141
|
+
shape: GLASS_MATERIAL.shape,
|
|
142
|
+
elevation: GLASS_MATERIAL.elevation,
|
|
143
|
+
appearance: {
|
|
144
|
+
material: 'glass',
|
|
145
|
+
transparency: reducedTransparency ? 'reduced' : 'standard',
|
|
146
|
+
},
|
|
147
|
+
...(reducedTransparency ? {} : { treatment: GLASS_MATERIAL.treatment }),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { EditorTheme } from './theme';
|
|
2
|
+
|
|
3
|
+
type MaterialColor = Pick<
|
|
4
|
+
EditorTheme['color'],
|
|
5
|
+
'surface' | 'boundary' | 'neutralOverlay' | 'scrim'
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export interface EditorMaterialDefinition {
|
|
9
|
+
readonly id: 'glass';
|
|
10
|
+
readonly color: MaterialColor;
|
|
11
|
+
readonly shape: EditorTheme['shape'];
|
|
12
|
+
readonly elevation: EditorTheme['elevation'];
|
|
13
|
+
readonly treatment: NonNullable<EditorTheme['treatment']>;
|
|
14
|
+
readonly reducedTransparency: {
|
|
15
|
+
readonly surface: EditorTheme['color']['surface'];
|
|
16
|
+
readonly scrim: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CLASSIC_MATERIAL = Object.freeze({
|
|
21
|
+
id: 'classic' as const,
|
|
22
|
+
shape: { small: '3px', medium: '6px', large: '8px', full: '9999px' },
|
|
23
|
+
elevation: {
|
|
24
|
+
small: '0 2px 8px rgba(0,0,0,0.6)',
|
|
25
|
+
medium: '0 4px 12px rgba(0,0,0,0.5)',
|
|
26
|
+
large: '0 8px 32px rgba(0,0,0,0.6)',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Canonical Glass material. It cannot contain accent or semantic colors. */
|
|
31
|
+
export const GLASS_MATERIAL: EditorMaterialDefinition = Object.freeze({
|
|
32
|
+
id: 'glass',
|
|
33
|
+
color: {
|
|
34
|
+
surface: {
|
|
35
|
+
shell: '#12161f',
|
|
36
|
+
panel: 'rgba(255,255,255,0.06)',
|
|
37
|
+
chrome: 'rgba(255,255,255,0.10)',
|
|
38
|
+
raised: 'rgba(255,255,255,0.16)',
|
|
39
|
+
inset: 'rgba(10,14,22,0.38)',
|
|
40
|
+
overlay: 'rgba(255,255,255,0.08)',
|
|
41
|
+
},
|
|
42
|
+
boundary: {
|
|
43
|
+
default: 'rgba(255,255,255,0.22)',
|
|
44
|
+
strong: 'rgba(255,255,255,0.38)',
|
|
45
|
+
},
|
|
46
|
+
neutralOverlay: {
|
|
47
|
+
hover: 'color-mix(in srgb, currentColor 10%, transparent)',
|
|
48
|
+
active: 'color-mix(in srgb, currentColor 16%, transparent)',
|
|
49
|
+
},
|
|
50
|
+
scrim: 'rgba(8,12,20,0.35)',
|
|
51
|
+
},
|
|
52
|
+
shape: { small: '4px', medium: '8px', large: '14px', full: '9999px' },
|
|
53
|
+
elevation: {
|
|
54
|
+
small: '0 4px 14px -6px rgba(0,0,0,0.45)',
|
|
55
|
+
medium: '0 8px 24px -10px rgba(0,0,0,0.5)',
|
|
56
|
+
large: '0 20px 46px -16px rgba(0,0,0,0.55), 0 4px 14px -6px rgba(0,0,0,0.35)',
|
|
57
|
+
},
|
|
58
|
+
treatment: {
|
|
59
|
+
backdropBlurPx: 1,
|
|
60
|
+
backdropSaturation: 1.15,
|
|
61
|
+
edgeSpecular: 0.7,
|
|
62
|
+
specularAngleDeg: 120,
|
|
63
|
+
refractionBezelPx: 56,
|
|
64
|
+
refractionThickness: 1.25,
|
|
65
|
+
textShadowOpacity: 0.72,
|
|
66
|
+
contentFrostBlurPx: 24,
|
|
67
|
+
ambientLiftOpacity: 0.07,
|
|
68
|
+
adaptiveContent: true,
|
|
69
|
+
brightTextShadowOpacity: 0,
|
|
70
|
+
brightFrostBg: 'rgba(255,255,255,0.3)',
|
|
71
|
+
},
|
|
72
|
+
reducedTransparency: {
|
|
73
|
+
surface: {
|
|
74
|
+
shell: '#12161f',
|
|
75
|
+
panel: '#33373e',
|
|
76
|
+
chrome: '#3d4047',
|
|
77
|
+
raised: '#4b4e55',
|
|
78
|
+
inset: '#23272f',
|
|
79
|
+
overlay: '#383b43',
|
|
80
|
+
},
|
|
81
|
+
scrim: 'rgba(8,12,20,0.5)',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ICON SET AXIS — the fourth appearance axis beside palette, material and
|
|
3
|
+
* composition. Every glyph the editor draws renders through `EditorIcon`
|
|
4
|
+
* (`primitives/EditorIcon.tsx`, the one consumer of Font Awesome),
|
|
5
|
+
* so a style bundle can carry a whole icon set as DATA keyed by the Font
|
|
6
|
+
* Awesome icon name each site names (`plus`, `xmark`, `trash`, …): a glyph
|
|
7
|
+
* present in the active set paints instead of the editor's own; a glyph
|
|
8
|
+
* absent falls through, so a set may be partial while it is drawn.
|
|
9
|
+
*
|
|
10
|
+
* This module is REGISTRY AND ACTIVE STATE ONLY — no settings, because the
|
|
11
|
+
* icon primitive sits in every core closure and must stay a leaf. The stored
|
|
12
|
+
* preference (`appearance.icons`) is read and written by `theme-preference.ts`,
|
|
13
|
+
* which applies it here, exactly as it owns the palette and material choices.
|
|
14
|
+
* Registered by `workspace-style.ts` with the bundle that carries it.
|
|
15
|
+
*/
|
|
16
|
+
import type { IconCategoryTone, IconSetContribution } from '../looks';
|
|
17
|
+
|
|
18
|
+
export type { IconCategoryTone };
|
|
19
|
+
|
|
20
|
+
export interface EditorIconGlyph {
|
|
21
|
+
/** Defaults to a 16-unit square. */
|
|
22
|
+
readonly viewBox?: string;
|
|
23
|
+
/** One `path` `d`, painted with `currentColor`. */
|
|
24
|
+
readonly path: string;
|
|
25
|
+
/** THE COLOUR CHANNEL — the glyph's own category (`IconCategoryTone`),
|
|
26
|
+
* painted `var(--vgai-category-<tone>, currentColor)`. With `tonedPath`
|
|
27
|
+
* it tints only that second path; alone it tints the whole glyph. An
|
|
28
|
+
* explicit `tone` prop at the site wins over both. */
|
|
29
|
+
readonly tone?: IconCategoryTone;
|
|
30
|
+
/** A second `path` `d` drawn OVER `path` and carrying `tone`: the operated
|
|
31
|
+
* element of an operator mark, where the cube itself stays neutral. */
|
|
32
|
+
readonly tonedPath?: string;
|
|
33
|
+
}
|
|
34
|
+
export type EditorIconSet = IconSetContribution;
|
|
35
|
+
export interface EditorIconSetChoice {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const DEFAULT_ICON_SET_ID = 'default';
|
|
41
|
+
const DEFAULT_CHOICE: EditorIconSetChoice = { id: DEFAULT_ICON_SET_ID, title: 'Editor' };
|
|
42
|
+
|
|
43
|
+
const contributedSets = new Map<string, EditorIconSet>();
|
|
44
|
+
let cachedChoices: readonly EditorIconSetChoice[] | null = null;
|
|
45
|
+
let activeId: string = DEFAULT_ICON_SET_ID;
|
|
46
|
+
const listeners = new Set<() => void>();
|
|
47
|
+
|
|
48
|
+
function emit(): void {
|
|
49
|
+
for (const listener of listeners) listener();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every set the switch surfaces offer — the editor's own, then the contributed. */
|
|
53
|
+
export function editorIconSets(): readonly EditorIconSetChoice[] {
|
|
54
|
+
cachedChoices ??= [
|
|
55
|
+
DEFAULT_CHOICE,
|
|
56
|
+
...[...contributedSets.values()].map(({ id, title }) => ({ id, title })),
|
|
57
|
+
];
|
|
58
|
+
return cachedChoices;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isEditorIconSetId(value: unknown): value is string {
|
|
62
|
+
return typeof value === 'string' && (value === DEFAULT_ICON_SET_ID || contributedSets.has(value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Register the set a style bundle carries. Returns the unregister. A
|
|
66
|
+
* duplicate id throws — ids key the persisted appearance setting. */
|
|
67
|
+
export function registerContributedIconSet(set: EditorIconSet): () => void {
|
|
68
|
+
if (isEditorIconSetId(set.id))
|
|
69
|
+
throw new Error(`registerContributedIconSet: icon set "${set.id}" is already registered.`);
|
|
70
|
+
contributedSets.set(set.id, set);
|
|
71
|
+
cachedChoices = null;
|
|
72
|
+
emit();
|
|
73
|
+
return () => {
|
|
74
|
+
if (contributedSets.get(set.id) !== set) return;
|
|
75
|
+
contributedSets.delete(set.id);
|
|
76
|
+
cachedChoices = null;
|
|
77
|
+
emit();
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The set painting now. */
|
|
82
|
+
export function activeIconSetSnapshot(): string {
|
|
83
|
+
return activeId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Applied by `theme-preference.ts` from the stored choice; an unregistered
|
|
87
|
+
* id paints the editor's own until its package registers. */
|
|
88
|
+
export function setActiveIconSet(id: string): void {
|
|
89
|
+
const next = isEditorIconSetId(id) ? id : DEFAULT_ICON_SET_ID;
|
|
90
|
+
if (next === activeId) return;
|
|
91
|
+
activeId = next;
|
|
92
|
+
emit();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Fires when the registry or the active set changes. */
|
|
96
|
+
export function subscribeIconSets(listener: () => void): () => void {
|
|
97
|
+
listeners.add(listener);
|
|
98
|
+
return () => listeners.delete(listener);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The active set's glyph for a Font Awesome icon name, or `null` to paint the editor's own. */
|
|
102
|
+
export function activeIconGlyph(iconName: string): EditorIconGlyph | null {
|
|
103
|
+
if (activeId === DEFAULT_ICON_SET_ID) return null;
|
|
104
|
+
return contributedSets.get(activeId)?.glyphs[iconName] ?? null;
|
|
105
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@volter/editor-sdk/widgets` — THE WIDGET KIT, the API's fourth face beside
|
|
3
|
+
* `protocol`, the host doors + `layouts`, and `session`
|
|
4
|
+
* (ARCHITECTURE-CORE §The target shape, rule 5: "One API, four faces,
|
|
5
|
+
* Apache"). These are the editor's OWN widgets — the exact components its
|
|
6
|
+
* inspector renders — so a panel built from them looks native and re-skins
|
|
7
|
+
* with the editor. Apache-2.0 like the rest of the SDK: a package or a
|
|
8
|
+
* project may draw a panel without taking the host's AGPL.
|
|
9
|
+
*
|
|
10
|
+
* Import it from a package contribution or a project's own
|
|
11
|
+
* `src/contributions/my-tool.document.tsx`:
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { Inline, NumberInput, SectionHeader, themeVars } from '@volter/editor-sdk/widgets';
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* What's here:
|
|
18
|
+
* - **Tokens**: typed semantic `themeVars`; legacy `THEME`/font aliases remain
|
|
19
|
+
* compatibility exports for existing inspector widgets only.
|
|
20
|
+
* - **Structure**: layout, typography, surfaces, dialogs, toolbars, tabs,
|
|
21
|
+
* trees, fields, feedback, and menus.
|
|
22
|
+
* - **Inputs**: `NumberInput` (scrub-or-type), `Vec3Input`, `ColorInput`,
|
|
23
|
+
* `DraftTextInput`, `ScrubbableInput`, `ColorPicker`/`ColorSwatch`,
|
|
24
|
+
* `GradientEditor`, `FontPicker`, `AlignmentGrid`, `BorderEditor`,
|
|
25
|
+
* `ShadowEditor`, `FilterEditor`, `CurveEditor`, `AssetSlotPicker`,
|
|
26
|
+
* `ToggleButton`.
|
|
27
|
+
* - **Appearance**: the theme table, the editor materials, the icon-set
|
|
28
|
+
* registry, the glyph table and the stacking scale — the values the
|
|
29
|
+
* components above paint with. `design-system.ts` re-exports the members
|
|
30
|
+
* its own surfaces use; this face is the UNION, which is why each of those
|
|
31
|
+
* modules also has its own line below. Where a name arrives by two stars
|
|
32
|
+
* both resolve to the SAME declaration, so nothing is ambiguous.
|
|
33
|
+
*
|
|
34
|
+
* Contract notes:
|
|
35
|
+
* - The rich widgets share one change contract, `ChangeHandlers<T>`
|
|
36
|
+
* (`value` + `onChange` per interaction tick + optional `onChangeEnd`
|
|
37
|
+
* commit — see its doc comment).
|
|
38
|
+
* - Layout and state chrome are API so built-in and project-authored tools
|
|
39
|
+
* cannot drift into parallel UI kits. Domain charts remain project-owned.
|
|
40
|
+
* - ONE SUBPATH: there is no `./design-system` and no `./theme` door, and the
|
|
41
|
+
* deep paths the host used to take (`./editor-material`,
|
|
42
|
+
* `./icon-set-registry`, `./interactive-edit-scope`, `./z-index`,
|
|
43
|
+
* `components/primitives/editor-icons`) all arrive through this module.
|
|
44
|
+
* The kit's dependency closure is deliberately shallow — react, react-dom,
|
|
45
|
+
* Font Awesome, and the SDK's own `looks` types — so a scaffolded
|
|
46
|
+
* project's `tsc` never drags the editor's internals along, and the
|
|
47
|
+
* Apache face never reaches AGPL host source (`interactive-edit-scope.ts`
|
|
48
|
+
* exists precisely to keep `NumberInput`/`ColorInput` off `EditorContext`).
|
|
49
|
+
* Don't add re-exports here that import the editor store, adapters, or
|
|
50
|
+
* server code.
|
|
51
|
+
* - The CLASSES these components paint with (`vgai-btn`, `vgai-input`,
|
|
52
|
+
* `vgai-menu`, …) are declared by the host's own `theme.css`, which stays
|
|
53
|
+
* in `packages/editor/src` because it is the editor's chrome sheet — the
|
|
54
|
+
* TOKENS those rules read are installed at runtime from `theme.ts` here
|
|
55
|
+
* (`editorThemeVariables`).
|
|
56
|
+
*
|
|
57
|
+
* The export lines are ONE alphabetical block because Biome's
|
|
58
|
+
* `organizeImports` sorts them; each line's subject is named above rather
|
|
59
|
+
* than beside it, so a re-sort cannot separate a comment from its target.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
export * from './design-system';
|
|
63
|
+
export * from './editor-appearance';
|
|
64
|
+
export * from './editor-material';
|
|
65
|
+
export * from './icon-set-registry';
|
|
66
|
+
export * from './inspector-widgets';
|
|
67
|
+
export * from './interactive-edit-scope';
|
|
68
|
+
export * from './primitives/DraftTextInput';
|
|
69
|
+
export * from './primitives/editor-icons';
|
|
70
|
+
export * from './theme';
|
|
71
|
+
export * from './z-index';
|