@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,2633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical design-token module for the vgai editor (editor-style-polish U0).
|
|
3
|
+
* Every color/spacing/radius/type/shadow/z-index value the editor's ~98 React
|
|
4
|
+
* components currently hand-roll as inline hex literals gets one home here. This module installs the variables consumed by `theme.css` and
|
|
5
|
+
* supplies plain TS/TSX call sites that still build inline `style`
|
|
6
|
+
* objects (e.g. `inspector-widgets/shared.tsx`, `primitives/Panel.tsx`) — so
|
|
7
|
+
* it must stay import-safe from any context: no DOM access, no side effects,
|
|
8
|
+
* just typed `const` data.
|
|
9
|
+
*
|
|
10
|
+
* The original Classic Graphite palette remains byte-for-byte compatible;
|
|
11
|
+
* the neutral Glass default is a separate palette rather than a mutation of
|
|
12
|
+
* Classic's historical action/focus color.
|
|
13
|
+
* selection material is derived separately so glass never turns every
|
|
14
|
+
* selected row, tab, and segment blue. The mono stack is the
|
|
15
|
+
* `ui-monospace` spelling with
|
|
16
|
+
* 2 independent votes, not `FONT_MONO`'s 1; the sans stack drops `'Inter'`
|
|
17
|
+
* (never loaded — zero `@font-face` hits repo-wide) in favor of the
|
|
18
|
+
* `-apple-system` chain already live in root `index.html`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { GLASS_MATERIAL } from './editor-material';
|
|
22
|
+
import { zIndex } from './z-index';
|
|
23
|
+
|
|
24
|
+
const GRAPHITE_BG = {
|
|
25
|
+
0: '#1a1a1a',
|
|
26
|
+
1: '#242424',
|
|
27
|
+
2: '#2c2c2c',
|
|
28
|
+
3: '#333333',
|
|
29
|
+
inset: '#1e1e1e',
|
|
30
|
+
} as const;
|
|
31
|
+
const GRAPHITE_BORDER = { 1: '#333333', 2: '#444444' } as const;
|
|
32
|
+
// Compatibility contract: these are the original Classic values.
|
|
33
|
+
const GRAPHITE_TEXT = { 1: '#c5c8ce', 2: '#9aa0a6', 3: '#90959c' } as const;
|
|
34
|
+
const GRAPHITE_ACCENT = '#579EFF';
|
|
35
|
+
const GRAPHITE_ACCENT_MUTED = 'rgba(87,158,255,0.15)';
|
|
36
|
+
// Glass defaults to this separate palette. A material never introduces hue.
|
|
37
|
+
const GRAPHITE_NEUTRAL_TEXT = { 1: '#f2f6fb', 2: '#d3d9e0', 3: '#aab0b7' } as const;
|
|
38
|
+
const GRAPHITE_NEUTRAL_ACCENT = '#d8dee9';
|
|
39
|
+
const GRAPHITE_NEUTRAL_ACCENT_MUTED = 'rgba(216,222,233,0.18)';
|
|
40
|
+
const GRAPHITE_DANGER = '#FF6B6B';
|
|
41
|
+
const GRAPHITE_WARN = '#e0a030';
|
|
42
|
+
const GRAPHITE_SUCCESS = '#4caf50';
|
|
43
|
+
const GRAPHITE_SUCCESS_MUTED = 'rgba(76,175,80,0.15)';
|
|
44
|
+
const GRAPHITE_DANGER_MUTED = 'rgba(255,107,107,0.15)';
|
|
45
|
+
const GRAPHITE_WARN_MUTED = 'rgba(224,160,48,0.12)';
|
|
46
|
+
const GRAPHITE_DANGER_FAINT = 'rgba(255,107,107,0.06)';
|
|
47
|
+
const GRAPHITE_SCRIM = 'rgba(0,0,0,0.5)';
|
|
48
|
+
const GRAPHITE_DYNAMIC = '#ff79c6';
|
|
49
|
+
const GRAPHITE_DYNAMIC_BG = 'rgba(255,121,198,0.08)';
|
|
50
|
+
// Component-instance identity (H1) — our translation of Unity's blue-prefab
|
|
51
|
+
// label. Cyan, NOT the accent role: `accent` means "action/focus" and
|
|
52
|
+
// several palettes deliberately keep it neutral (`GRAPHITE_NEUTRAL_ACCENT`
|
|
53
|
+
// is near-white), which made an instance row indistinguishable from a plain
|
|
54
|
+
// one. Cyan is also the widest free hue gap in this palette — clear of
|
|
55
|
+
// danger (red), warning (amber), success (green ~122°), dynamic (pink
|
|
56
|
+
// ~326°) and the graphite accent (blue ~215°). Depth is tuned so the tint
|
|
57
|
+
// also survives a BRIGHT backdrop under Glass (2.36:1 on white — the same
|
|
58
|
+
// band as dynamic/warning) while still reading at 6.65:1 on the opaque
|
|
59
|
+
// graphite row.
|
|
60
|
+
const GRAPHITE_INSTANCE = '#22b8d6';
|
|
61
|
+
const GRAPHITE_FONT_SANS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
62
|
+
const GRAPHITE_FONT_MONO = 'ui-monospace, "SF Mono", Monaco, "Cascadia Code", monospace';
|
|
63
|
+
const GRAPHITE_SHADOW = {
|
|
64
|
+
sm: '0 2px 8px rgba(0,0,0,0.6)',
|
|
65
|
+
md: '0 4px 12px rgba(0,0,0,0.5)',
|
|
66
|
+
lg: '0 8px 32px rgba(0,0,0,0.6)',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compatibility references used by existing DOM call sites. They resolve at
|
|
71
|
+
* paint time, so the whole existing editor participates in runtime themes.
|
|
72
|
+
* New code should prefer the semantic {@link themeVars} vocabulary.
|
|
73
|
+
*/
|
|
74
|
+
export const bg = {
|
|
75
|
+
/** App shell / loading screen (`AppRoot.tsx`, root `index.html` body). */
|
|
76
|
+
0: 'var(--vgai-surface-shell)',
|
|
77
|
+
/** Panel body (`Panel.tsx`'s content area, THEME.bg). */
|
|
78
|
+
1: 'var(--vgai-surface-panel)',
|
|
79
|
+
/** Surface: header / menu / dropdown / popover (THEME.surface). */
|
|
80
|
+
2: 'var(--vgai-surface-chrome)',
|
|
81
|
+
/** Raised / hover surface (THEME.surfaceHover). */
|
|
82
|
+
3: 'var(--vgai-surface-raised)',
|
|
83
|
+
/** Input well / recessed field background (THEME.inputBg). */
|
|
84
|
+
inset: 'var(--vgai-surface-inset)',
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
/** Border ramp — same literal as `bg[3]` for `border[1]` by design (distinguished by CSS property, not value). */
|
|
88
|
+
export const border = {
|
|
89
|
+
/** Default border. */
|
|
90
|
+
1: 'var(--vgai-boundary-default)',
|
|
91
|
+
/** Hover / emphasis border. */
|
|
92
|
+
2: 'var(--vgai-boundary-strong)',
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/** Text ramp — primary → tertiary/dim. */
|
|
96
|
+
export const text = {
|
|
97
|
+
/** Primary text (THEME.text). */
|
|
98
|
+
1: 'var(--vgai-content-primary)',
|
|
99
|
+
/** Secondary / muted text (THEME.textMuted). */
|
|
100
|
+
2: 'var(--vgai-content-muted)',
|
|
101
|
+
/** Tertiary / dim text. */
|
|
102
|
+
3: 'var(--vgai-content-dim)',
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
/** Canonical action/focus accent. Selection chrome uses `selection` below. */
|
|
106
|
+
export const accent = 'var(--vgai-accent)';
|
|
107
|
+
/** Semantic tint behind accent-colored actions, status, and information. */
|
|
108
|
+
export const accentMuted = 'var(--vgai-accent-muted)';
|
|
109
|
+
/** Material selection roles: accent in Classic, adaptive neutral in Glass. */
|
|
110
|
+
export const selection = {
|
|
111
|
+
background: 'var(--vgai-selection-bg)',
|
|
112
|
+
border: 'var(--vgai-selection-border)',
|
|
113
|
+
indicator: 'var(--vgai-selection-indicator)',
|
|
114
|
+
} as const;
|
|
115
|
+
|
|
116
|
+
/** Semantic tones. */
|
|
117
|
+
export const danger = 'var(--vgai-danger)';
|
|
118
|
+
export const warn = 'var(--vgai-warn)';
|
|
119
|
+
export const success = 'var(--vgai-success)';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Semantic background tints (U6, punch-list #1) — one family alpha (0.15,
|
|
123
|
+
* matching `accentMuted`) for chip/band/state backgrounds behind
|
|
124
|
+
* semantic-colored content. Converges the drifted hand-rolled alphas
|
|
125
|
+
* (0.12/0.15) that U3-U5 left behind in EnvironmentSection (stale-bake),
|
|
126
|
+
* ConsolePanel (entity chip), AudioSection
|
|
127
|
+
* (stop state), and the unsaved-document treatment.
|
|
128
|
+
*/
|
|
129
|
+
export const successMuted = 'var(--vgai-success-muted)';
|
|
130
|
+
export const dangerMuted = 'var(--vgai-danger-muted)';
|
|
131
|
+
/** Resting warning surface used for unsaved/attention states. */
|
|
132
|
+
export const warnMuted = 'var(--vgai-warn-muted)';
|
|
133
|
+
/**
|
|
134
|
+
* Deliberately fainter danger tint for RESTING full-row backgrounds
|
|
135
|
+
* (ConsolePanel error rows): a run of consecutive error
|
|
136
|
+
* rows must read as a list, not a solid red wall, so rows sit at 0.06 while
|
|
137
|
+
* point-emphasis chips/bands use `dangerMuted`. Two files independently
|
|
138
|
+
* converged on this exact value — tokenized so they can't drift apart.
|
|
139
|
+
*/
|
|
140
|
+
export const dangerFaint = 'var(--vgai-danger-faint)';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The one modal/backdrop dimming scrim. The canonical Dialog pattern and
|
|
144
|
+
* non-dialog modal surfaces share this semantic value.
|
|
145
|
+
*/
|
|
146
|
+
export const scrim = 'var(--vgai-scrim)';
|
|
147
|
+
|
|
148
|
+
/** Kit-specific "reactive/bound value" indicator (inspector-widgets only — not otherwise contested). */
|
|
149
|
+
export const dynamic = 'var(--vgai-dynamic)';
|
|
150
|
+
export const dynamicBg = 'var(--vgai-dynamic-muted)';
|
|
151
|
+
|
|
152
|
+
/** 4/8-based spacing scale — compose padding/margin/gap from these instead of ad hoc shorthand strings. */
|
|
153
|
+
export const space = {
|
|
154
|
+
1: 2,
|
|
155
|
+
2: 4,
|
|
156
|
+
3: 6,
|
|
157
|
+
4: 8,
|
|
158
|
+
5: 10,
|
|
159
|
+
6: 12,
|
|
160
|
+
8: 16,
|
|
161
|
+
10: 20,
|
|
162
|
+
12: 24,
|
|
163
|
+
} as const;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Semantic editor-chrome dimensions. Different roles intentionally use
|
|
167
|
+
* different heights, but every instance of a role must consume the same
|
|
168
|
+
* token instead of retyping a nearby number.
|
|
169
|
+
*/
|
|
170
|
+
export const chromeSize = {
|
|
171
|
+
commandBar: 36,
|
|
172
|
+
panelHeader: 30,
|
|
173
|
+
localToolbar: 28,
|
|
174
|
+
treeRow: 24,
|
|
175
|
+
// How far one level of a tree steps right. The editor's own is 14, which is
|
|
176
|
+
// what `GameHierarchy`'s `INDENT` always was. Blender's Outliner steps by
|
|
177
|
+
// its ROW HEIGHT — a square grid, the shape every DCC tree uses: measured on
|
|
178
|
+
// `outliner.png` (2x), the type glyphs of Scene Collection, Collection and
|
|
179
|
+
// Camera centre at x 30.25, 50.25 and 70.25 CSS and their chevrons one cell
|
|
180
|
+
// left of each, so the step is exactly 20.
|
|
181
|
+
treeIndent: 14,
|
|
182
|
+
// 22 was always too cramped for the bottom row (owner design review,
|
|
183
|
+
// 2026-07-19 — "the wrong height even during the regular page"; 28 was
|
|
184
|
+
// still too tight on second look). 36 mirrors the command bar, so the
|
|
185
|
+
// top and bottom chrome rows are symmetric.
|
|
186
|
+
statusBar: 36,
|
|
187
|
+
// The tool rail (Blender's toolbar): a tool's box, its width and the gap
|
|
188
|
+
// between groups — the EDITOR'S OWN, which is what Classic paints. These
|
|
189
|
+
// are not Blender's (a comment here used to claim they were): the Blender
|
|
190
|
+
// style declares its own `chrome.toolSize/toolWidth/toolGap`
|
|
191
|
+
// (`blender.style.ts`), those are the measured ones, and they are what the
|
|
192
|
+
// rail actually renders at under that look. Changing a number here moves
|
|
193
|
+
// Classic and nothing else.
|
|
194
|
+
toolSize: 34,
|
|
195
|
+
toolWidth: 38,
|
|
196
|
+
toolGap: 7,
|
|
197
|
+
// How wide the seam between two docked AREAS is cut. The editor's own is
|
|
198
|
+
// the resting stroke (spelled out rather than read from `strokeWidth`,
|
|
199
|
+
// which is declared below this object), so a skin that declares nothing
|
|
200
|
+
// keeps today's hairline; Blender's groove is 3.5 — see
|
|
201
|
+
// `color.boundary.area` for the frame and coordinates it was read at.
|
|
202
|
+
areaSeam: 1,
|
|
203
|
+
// The EMBOSS beside that groove, in percent of white mixed into the area's
|
|
204
|
+
// own fill. Zero paints nothing at all (`areaEmbossValue` returns
|
|
205
|
+
// `transparent`), which is the editor's own look and every skin but
|
|
206
|
+
// Blender's; Blender's is 8.2. See `areaEmbossValue`.
|
|
207
|
+
areaEmboss: 0,
|
|
208
|
+
} as const;
|
|
209
|
+
|
|
210
|
+
/** What a material may retune of the chrome's density, all in px. */
|
|
211
|
+
export interface EditorDensity {
|
|
212
|
+
readonly control?: Partial<Record<keyof typeof controlSize, number>>;
|
|
213
|
+
readonly font?: Partial<Record<keyof typeof fontSize, number>>;
|
|
214
|
+
/** Glyph sizes, independent of the type scale ({@link iconSize}). */
|
|
215
|
+
readonly icon?: Partial<Record<keyof typeof iconSize, number>>;
|
|
216
|
+
readonly chrome?: Partial<Record<keyof typeof chromeSize, number>>;
|
|
217
|
+
/** WHAT THE LOOK STATES ABOUT THE 3D STAGE, as opposed to about the chrome.
|
|
218
|
+
* Every member is independently optional and there is no default table: an
|
|
219
|
+
* absent member emits an EMPTY token and its reader keeps the editor's own
|
|
220
|
+
* behaviour (the `color.viewport` group's convention, for the same reason
|
|
221
|
+
* — the editor's gizmo is not expressed in px at all, and its shelf's boot
|
|
222
|
+
* tool is its own). See `DensityContribution.viewport` in `looks.ts` for
|
|
223
|
+
* both derivations and for why a non-length member sits under `density`. */
|
|
224
|
+
readonly viewport?: {
|
|
225
|
+
readonly gizmoSize?: number;
|
|
226
|
+
readonly shelfTool?: 'select' | 'transform';
|
|
227
|
+
readonly upAxis?: 'y' | 'z';
|
|
228
|
+
readonly boxSelect?: 'contain' | 'touch';
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function density(theme: Pick<EditorTheme, 'density'>) {
|
|
232
|
+
return {
|
|
233
|
+
control: { ...controlSize, ...theme.density?.control },
|
|
234
|
+
font: { ...fontSize, ...theme.density?.font },
|
|
235
|
+
icon: { ...iconSize, ...theme.density?.icon },
|
|
236
|
+
chrome: { ...chromeSize, ...theme.density?.chrome },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Reusable interactive-control heights. */
|
|
241
|
+
export const controlSize = {
|
|
242
|
+
compact: 20,
|
|
243
|
+
default: 24,
|
|
244
|
+
comfortable: 28,
|
|
245
|
+
} as const;
|
|
246
|
+
|
|
247
|
+
/** Resting boundaries stay quiet; interaction affordances may strengthen. */
|
|
248
|
+
export const strokeWidth = {
|
|
249
|
+
resting: 1,
|
|
250
|
+
active: 2,
|
|
251
|
+
} as const;
|
|
252
|
+
|
|
253
|
+
/** Named type scale (px). Covers the overwhelming majority of real fontSize call sites. */
|
|
254
|
+
/**
|
|
255
|
+
* ICON SIZE — its OWN axis, not a rung of the type scale.
|
|
256
|
+
*
|
|
257
|
+
* Every glyph used to take its size from `fontSize`, so a skin that tightened
|
|
258
|
+
* its text tightened its icons with it: under Blender's density the chrome's
|
|
259
|
+
* glyphs rendered at 10–11 px where Blender draws 14 (measured against
|
|
260
|
+
* `properties-data-edit.png` and `outliner.png` at matched scale), which is
|
|
261
|
+
* what "the sizes are totally wrong" names. Blender sizes icons
|
|
262
|
+
* independently of text and so does every application whose icons are a
|
|
263
|
+
* pictorial language rather than a typographic one.
|
|
264
|
+
*
|
|
265
|
+
* The defaults below are the type scale's values, so a skin that declares no
|
|
266
|
+
* `density.icon` looks exactly as it did.
|
|
267
|
+
*/
|
|
268
|
+
export const iconSize = {
|
|
269
|
+
xs: 9,
|
|
270
|
+
sm: 10,
|
|
271
|
+
md: 12,
|
|
272
|
+
lg: 13,
|
|
273
|
+
xl: 14,
|
|
274
|
+
'2xl': 16,
|
|
275
|
+
} as const;
|
|
276
|
+
|
|
277
|
+
export const fontSize = {
|
|
278
|
+
xs: 9,
|
|
279
|
+
sm: 10,
|
|
280
|
+
base: 11,
|
|
281
|
+
md: 12,
|
|
282
|
+
lg: 13,
|
|
283
|
+
xl: 14,
|
|
284
|
+
'2xl': 16,
|
|
285
|
+
heading: 24,
|
|
286
|
+
} as const;
|
|
287
|
+
|
|
288
|
+
export const fontWeight = {
|
|
289
|
+
regular: 400,
|
|
290
|
+
semibold: 600,
|
|
291
|
+
bold: 700,
|
|
292
|
+
} as const;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Line-height scale (unitless, so it scales with each element's own size).
|
|
296
|
+
* Minted from the values the estate already used by hand: 1 for single-line
|
|
297
|
+
* chrome, 1.2 for headings and labels, 1.4 for control copy, 1.5 for prose.
|
|
298
|
+
*/
|
|
299
|
+
export const lineHeight = {
|
|
300
|
+
tight: 1,
|
|
301
|
+
snug: 1.2,
|
|
302
|
+
normal: 1.4,
|
|
303
|
+
relaxed: 1.5,
|
|
304
|
+
} as const;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Motion scale. Durations in ms; one standard easing. Minted from the values
|
|
308
|
+
* the estate already used by hand (140/180/300ms and one cubic-bezier).
|
|
309
|
+
* `prefers-reduced-motion` handling stays with each animation's own rule.
|
|
310
|
+
*/
|
|
311
|
+
export const motion = {
|
|
312
|
+
duration: { fast: 140, base: 180, slow: 300 },
|
|
313
|
+
easing: {
|
|
314
|
+
standard: 'cubic-bezier(0.32, 0.72, 0.28, 1)',
|
|
315
|
+
out: 'ease-out',
|
|
316
|
+
},
|
|
317
|
+
} as const;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Sans stack — the fallback chain already live everywhere else (originally
|
|
321
|
+
* declared in root `index.html`). `'Inter'` is intentionally NOT included:
|
|
322
|
+
* no `@font-face`/`<link>` for it exists anywhere in `packages/editor`, so it
|
|
323
|
+
* only ever silently fell back to this same chain.
|
|
324
|
+
*/
|
|
325
|
+
export const fontSans = 'var(--vgai-font-sans)';
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Canonical mono stack — replaces `FONT_MONO`/`MONO`/18 bare `'monospace'`
|
|
329
|
+
* sites. This spelling had 2 independent votes in the codebase vs. 1 each
|
|
330
|
+
* for the others.
|
|
331
|
+
*/
|
|
332
|
+
export const fontMono = 'var(--vgai-font-mono)';
|
|
333
|
+
|
|
334
|
+
const DEFAULT_RADIUS = {
|
|
335
|
+
small: '3px',
|
|
336
|
+
medium: '6px',
|
|
337
|
+
large: '8px',
|
|
338
|
+
full: '9999px',
|
|
339
|
+
} as const;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Runtime border-radius references. Like the color compatibility exports,
|
|
343
|
+
* these resolve at paint time so existing product surfaces participate in a
|
|
344
|
+
* custom theme instead of freezing the Graphite defaults into inline styles.
|
|
345
|
+
*/
|
|
346
|
+
export const radius = {
|
|
347
|
+
sm: 'var(--vgai-radius-sm)',
|
|
348
|
+
md: 'var(--vgai-radius-md)',
|
|
349
|
+
lg: 'var(--vgai-radius-lg)',
|
|
350
|
+
full: 'var(--vgai-radius-full)',
|
|
351
|
+
} as const;
|
|
352
|
+
|
|
353
|
+
/** Drop-shadow tiers. */
|
|
354
|
+
export const shadow = {
|
|
355
|
+
/** Tooltip tier. */
|
|
356
|
+
sm: 'var(--vgai-shadow-sm)',
|
|
357
|
+
/** Dropdown / menu tier — already the dominant value pre-token. */
|
|
358
|
+
md: 'var(--vgai-shadow-md)',
|
|
359
|
+
/** Modal tier. */
|
|
360
|
+
lg: 'var(--vgai-shadow-lg)',
|
|
361
|
+
} as const;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Typed `var()` handles for the numeric scales, for components that style
|
|
365
|
+
* inline (`style={{ fontSize: fontSizeVar.sm }}`). An inline literal
|
|
366
|
+
* (`fontSize: 11`) sits above every stylesheet in the cascade and is the one
|
|
367
|
+
* place a theme cannot reach; these keep inline styles on the token.
|
|
368
|
+
*/
|
|
369
|
+
export const fontSizeVar = {
|
|
370
|
+
xs: 'var(--vgai-font-xs)',
|
|
371
|
+
sm: 'var(--vgai-font-sm)',
|
|
372
|
+
base: 'var(--vgai-font-base)',
|
|
373
|
+
md: 'var(--vgai-font-md)',
|
|
374
|
+
lg: 'var(--vgai-font-lg)',
|
|
375
|
+
xl: 'var(--vgai-font-xl)',
|
|
376
|
+
'2xl': 'var(--vgai-font-2xl)',
|
|
377
|
+
heading: 'var(--vgai-font-heading)',
|
|
378
|
+
} as const;
|
|
379
|
+
|
|
380
|
+
export const spaceVar = {
|
|
381
|
+
1: 'var(--vgai-space-1)',
|
|
382
|
+
2: 'var(--vgai-space-2)',
|
|
383
|
+
3: 'var(--vgai-space-3)',
|
|
384
|
+
4: 'var(--vgai-space-4)',
|
|
385
|
+
5: 'var(--vgai-space-5)',
|
|
386
|
+
6: 'var(--vgai-space-6)',
|
|
387
|
+
8: 'var(--vgai-space-8)',
|
|
388
|
+
10: 'var(--vgai-space-10)',
|
|
389
|
+
12: 'var(--vgai-space-12)',
|
|
390
|
+
} as const;
|
|
391
|
+
|
|
392
|
+
export const lineHeightVar = {
|
|
393
|
+
tight: 'var(--vgai-leading-tight)',
|
|
394
|
+
snug: 'var(--vgai-leading-snug)',
|
|
395
|
+
normal: 'var(--vgai-leading-normal)',
|
|
396
|
+
relaxed: 'var(--vgai-leading-relaxed)',
|
|
397
|
+
} as const;
|
|
398
|
+
|
|
399
|
+
export const motionVar = {
|
|
400
|
+
duration: {
|
|
401
|
+
fast: 'var(--vgai-duration-fast)',
|
|
402
|
+
base: 'var(--vgai-duration-base)',
|
|
403
|
+
slow: 'var(--vgai-duration-slow)',
|
|
404
|
+
},
|
|
405
|
+
easing: {
|
|
406
|
+
standard: 'var(--vgai-ease-standard)',
|
|
407
|
+
out: 'var(--vgai-ease-out)',
|
|
408
|
+
},
|
|
409
|
+
} as const;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* `<active stroke>px solid <accent>` — apply via a global `:focus-visible` rule for
|
|
413
|
+
* `className`-based controls (see `theme.css`), and via a shared
|
|
414
|
+
* `onFocus`/`onBlur` handler for inline-`style` components that can't
|
|
415
|
+
* express the pseudo-class directly (out of scope for U0 — later units).
|
|
416
|
+
*/
|
|
417
|
+
export const focusRing = 'var(--vgai-focus-ring)';
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The EDITOR AREAS a palette may paint separately (`EditorTheme.color.region`,
|
|
421
|
+
* whose docblock carries the measurement). The vocabulary is deliberately tiny
|
|
422
|
+
* and named after the reference's own editors, not after our panels: a name
|
|
423
|
+
* earns its place when a frame shows that area painted differently from
|
|
424
|
+
* `surface.panel`. `workspace-static-panels.ts` is where a panel claims one.
|
|
425
|
+
*/
|
|
426
|
+
export const EDITOR_REGION_NAMES = ['outliner', 'properties'] as const;
|
|
427
|
+
export type EditorRegionName = (typeof EDITOR_REGION_NAMES)[number];
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Runtime editor-theme contract. The compatibility exports above are CSS
|
|
431
|
+
* references so existing DOM chrome themes at paint time; concrete defaults
|
|
432
|
+
* live in {@link graphiteDarkEditorTheme}. New DOM code should consume
|
|
433
|
+
* {@link themeVars}' semantic names.
|
|
434
|
+
*/
|
|
435
|
+
export interface EditorTheme {
|
|
436
|
+
readonly id: string;
|
|
437
|
+
readonly color: {
|
|
438
|
+
readonly surface: {
|
|
439
|
+
readonly shell: string;
|
|
440
|
+
readonly panel: string;
|
|
441
|
+
readonly chrome: string;
|
|
442
|
+
readonly raised: string;
|
|
443
|
+
readonly inset: string;
|
|
444
|
+
/** Translucent editor HUD chrome drawn over a game/scene viewport. */
|
|
445
|
+
readonly overlay: string;
|
|
446
|
+
};
|
|
447
|
+
readonly boundary: {
|
|
448
|
+
readonly default: string;
|
|
449
|
+
readonly strong: string;
|
|
450
|
+
/**
|
|
451
|
+
* THE AREA SEAM — the line between two docked AREAS, which is a
|
|
452
|
+
* different subject from `default`, the outline around a WIDGET.
|
|
453
|
+
* Optional, like `widget` and `category`: a palette that names none
|
|
454
|
+
* paints every seam `boundary.default`, exactly as before this member
|
|
455
|
+
* existed, so no other skin moves by a pixel.
|
|
456
|
+
*
|
|
457
|
+
* Blender 5.2 factory startup, measured on the native 2x frames in
|
|
458
|
+
* `/Volumes/PeakSSD/volter-work/blender-reference` (halved here):
|
|
459
|
+
*
|
|
460
|
+
* - `modeling.png`, the seam between the Outliner and the Properties
|
|
461
|
+
* editor, median over x 2900..3400: rows 419..425 are (21,21,21) —
|
|
462
|
+
* 3.5 CSS px of #151515 — with a one-pixel lighter emboss on each
|
|
463
|
+
* side (417..418 = 59/60, 426..427 = 54/64).
|
|
464
|
+
* - the same frame's viewport↔Properties seam, median over rows
|
|
465
|
+
* 600..1900: x 2836..2842 is the identical (21,21,21) run, same
|
|
466
|
+
* 3.5 px.
|
|
467
|
+
*
|
|
468
|
+
* So the seam is DARKER than either neighbour, where our
|
|
469
|
+
* `boundary.default` #3c3c3c is lighter than both — that inversion is
|
|
470
|
+
* what made our window read as tiles with bright grout.
|
|
471
|
+
*
|
|
472
|
+
* The two SHELL seams measure narrower (topbar↔header y 48..52 = 2.5
|
|
473
|
+
* px, viewport↔status bar y 2057..2060 = 2 px) for a reason that is
|
|
474
|
+
* not a second width: the top and status bars are #171717 themselves,
|
|
475
|
+
* two levels off the groove, so there is nothing there to see. They
|
|
476
|
+
* take the colour and keep their hairline width; only an interior
|
|
477
|
+
* area↔area seam is cut to `chrome.areaSeam` px.
|
|
478
|
+
*/
|
|
479
|
+
readonly area?: string;
|
|
480
|
+
/**
|
|
481
|
+
* THE TREE'S INDENT GUIDE — the vertical rule an OPEN parent draws down
|
|
482
|
+
* its own disclosure column, through the rows of its subtree. Optional,
|
|
483
|
+
* per member, exactly like `area`, `widget` and `category`: a palette
|
|
484
|
+
* that names none paints NO GUIDE AT ALL, which is what every skin but
|
|
485
|
+
* Blender's did before this member existed, so nothing else moves.
|
|
486
|
+
*
|
|
487
|
+
* Measured on `modeling-object-none.png` and
|
|
488
|
+
* `modeling-object-selected.png` (Blender 5.2, native 2x, OBJECT mode;
|
|
489
|
+
* the Outliner is `modeling.png[y 53..419, x 2843..3452]`, which is
|
|
490
|
+
* `outliner.png`'s own box, so the coordinates below are that crop's):
|
|
491
|
+
*
|
|
492
|
+
* - the rule is ONE device pixel at x 59 — 0.5 CSS px, at CSS 29.5..30.0
|
|
493
|
+
* against the Collection row's chevron centre of 30.25;
|
|
494
|
+
* - it runs y 143..242 CONTINUOUSLY, i.e. from 5 CSS px into the first
|
|
495
|
+
* child's row to 5 CSS px above the last child's row bottom (rows are
|
|
496
|
+
* 40 device px: Camera 133..172, Cube 173..212, Light 213..252);
|
|
497
|
+
* - every one of those 100 pixels is exactly (101,101,101) — over the
|
|
498
|
+
* panel's #272727/#2a2a2a stripe, over the #1c304c selected bar AND
|
|
499
|
+
* over the #324c7f active bar — so the ink is OPAQUE #656565 and is
|
|
500
|
+
* painted OVER the row's fill, never mixed into it.
|
|
501
|
+
*
|
|
502
|
+
* Where the evidence stops: the frame nests exactly ONE level
|
|
503
|
+
* (Scene Collection → Collection → three objects), and the root row
|
|
504
|
+
* carries no chevron and no guide beneath it — measured, x 0..48 is
|
|
505
|
+
* clear for the whole Collection row. What a row THREE levels deep
|
|
506
|
+
* carries is therefore not photographed; `theme.css`'s
|
|
507
|
+
* `.vgai-tree-indent-guide` records the reading taken and why.
|
|
508
|
+
*/
|
|
509
|
+
readonly indent?: string;
|
|
510
|
+
/**
|
|
511
|
+
* A DIVIDER'S RULE — the line a chrome bar draws between two GROUPS of
|
|
512
|
+
* its own controls, as against `default`, which outlines a widget.
|
|
513
|
+
* Blender keeps them apart and we had one value for both.
|
|
514
|
+
*
|
|
515
|
+
* Optional, transcribing {@link indent}'s shape rather than inventing
|
|
516
|
+
* one: a palette naming none is bit-identical to before this member
|
|
517
|
+
* existed. The fallback differs only because the situations do — no
|
|
518
|
+
* skin ever drew an indent guide, so absent there means `transparent`
|
|
519
|
+
* and no guide; every skin draws this rule, so absent here means
|
|
520
|
+
* `boundary.default`, which is what it drew.
|
|
521
|
+
*
|
|
522
|
+
* MEASURED, `topbar.png` at native 2x: the rule closing the menu words
|
|
523
|
+
* before the workspace tabs is at x 488..489, y 6..45 — 2 device px
|
|
524
|
+
* wide, 40 tall, a FLAT #2e2e2e (46) with no AA ramp on either side,
|
|
525
|
+
* between the bar's own 23 and the tab strip's 28. Ours painted
|
|
526
|
+
* `boundary.default` #3c3c3c (60), lighter than the strip it borders.
|
|
527
|
+
*
|
|
528
|
+
* ONE SITE, and the scope is the measurement's: `.vgai-project-menu-rule`
|
|
529
|
+
* is the only divider in either frame I measured. The token is read
|
|
530
|
+
* there and nowhere else; widening it to every `.vgai-divider` is a
|
|
531
|
+
* later unit's, with its own frames.
|
|
532
|
+
*/
|
|
533
|
+
readonly divider?: string;
|
|
534
|
+
};
|
|
535
|
+
readonly content: {
|
|
536
|
+
readonly primary: string;
|
|
537
|
+
readonly muted: string;
|
|
538
|
+
readonly dim: string;
|
|
539
|
+
readonly onAccent: string;
|
|
540
|
+
/**
|
|
541
|
+
* A MENU WORD — the ink of an unenclosed clickable word in a menu bar or
|
|
542
|
+
* an area header. Its own role because a bar of words is neither a
|
|
543
|
+
* widget's content nor a tree row's name: it is the loudest thing a
|
|
544
|
+
* chrome band says, and Blender inks it one step under its panel text.
|
|
545
|
+
*
|
|
546
|
+
* Optional, and falls back to {@link primary} in the emitter, which is
|
|
547
|
+
* what the top bar already paints (`ProjectHeader.css`, PR #7317) — a
|
|
548
|
+
* palette naming no menu ink keeps that bar bit-identical.
|
|
549
|
+
*
|
|
550
|
+
* MEASURED, Blender 5.2 at native 2x, and the reason this is ONE member
|
|
551
|
+
* and not two: the same #d8d8d8 (216) inks `File`/`Edit`/`Render` in the
|
|
552
|
+
* top bar over its #171717 (23) band (`topbar.png`, eroded stroke
|
|
553
|
+
* interiors peak at 216) AND `View`/`Select`/`Add`/`Mesh`/`Vertex`/
|
|
554
|
+
* `Edge`/`Face`/`UV` in the 3D View's area header over its #343434 (52)
|
|
555
|
+
* band (`modeling-edit-none.png`, x 420..1100, y 56..104 — the histogram
|
|
556
|
+
* there tops out at exactly 216 with nothing above it). Two backdrops
|
|
557
|
+
* 29 levels apart reading one value is what says the ink is OPAQUE
|
|
558
|
+
* rather than an alpha at the site.
|
|
559
|
+
*
|
|
560
|
+
* IT REMOVES A SPLIT. Ours drew the two sites at two different members —
|
|
561
|
+
* 229 `content.primary` in the top bar and 194 `content.muted` in the
|
|
562
|
+
* area header (measured live before this member existed) — because the
|
|
563
|
+
* area header's words inherit `.vgai-menu-trigger`'s base ink and only
|
|
564
|
+
* the top bar carried an override. One role, one member, one rule.
|
|
565
|
+
*
|
|
566
|
+
* WHERE THE EVIDENCE STOPS: no reference frame photographs a HOVERED or
|
|
567
|
+
* a DISABLED menu word, so the hover state keeps reading `content.primary`
|
|
568
|
+
* as it did.
|
|
569
|
+
*/
|
|
570
|
+
readonly menu?: string;
|
|
571
|
+
/**
|
|
572
|
+
* THE STATUS BAND'S OWN INK — the quietest text in the window, and its
|
|
573
|
+
* own role because the band is one voice: every glyph, rule and digit in
|
|
574
|
+
* it reads at one level regardless of what put them there.
|
|
575
|
+
*
|
|
576
|
+
* Optional, falling back to {@link dim} in the emitter, which is what
|
|
577
|
+
* `.vgai-editor-bottom-bar` already inherits — a palette naming none
|
|
578
|
+
* paints exactly what it painted before.
|
|
579
|
+
*
|
|
580
|
+
* MEASURED, Blender 5.2 at native 2x: the whole status band of
|
|
581
|
+
* `modeling-edit-none.png` (the bottom 46 device px, full width)
|
|
582
|
+
* histograms to a ceiling of #878787 (135) — 135 and its AA neighbour
|
|
583
|
+
* 134 and nothing above either, against the 216 its menu words ink and
|
|
584
|
+
* the 229 its panels do. `EditorBottomBar.css` recorded the 15 levels
|
|
585
|
+
* between it and `content.dim` as residue rather than hardcode the hex;
|
|
586
|
+
* this is the member that closes it.
|
|
587
|
+
*/
|
|
588
|
+
readonly status?: string;
|
|
589
|
+
/**
|
|
590
|
+
* A FIELD'S PLACEHOLDER. Its own role because nothing else in the editor
|
|
591
|
+
* is text that is not content — and because until this member existed a
|
|
592
|
+
* BROWSER DEFAULT decided an ink in our product: no `::placeholder` rule
|
|
593
|
+
* existed anywhere in the editor's CSS.
|
|
594
|
+
*
|
|
595
|
+
* MEASURED LIVE, and it corrects the reading that named this gap: Chrome
|
|
596
|
+
* resolves its UA `::placeholder` to a FIXED rgb(117,117,117) here — the
|
|
597
|
+
* same value for a field inked 229, 100, rgb(200,0,0) or black, four
|
|
598
|
+
* samples in one page — so it is not composited from the field's own ink
|
|
599
|
+
* at all. A fixed browser constant is exactly the thing a palette cannot
|
|
600
|
+
* re-skin, which is the defect.
|
|
601
|
+
*
|
|
602
|
+
* MEASURED, Blender 5.2 at native 2x: the Outliner's `Search`
|
|
603
|
+
* placeholder in `modeling-object-none.png` (x 3100..3260, y 60..96)
|
|
604
|
+
* peaks at #5e5e5e (94) and stops there — below its own `content.dim`,
|
|
605
|
+
* which is what a placeholder should be.
|
|
606
|
+
*
|
|
607
|
+
* Optional, falling back to {@link dim} — the palette's own quiet ink
|
|
608
|
+
* and the only measured member on the right side of the field's text. A
|
|
609
|
+
* palette naming none therefore moves off Chrome's constant; that is the
|
|
610
|
+
* point, and it is stated rather than hidden, because reproducing a UA
|
|
611
|
+
* constant in our own stylesheet would nail one browser's opinion into
|
|
612
|
+
* the product under a different name.
|
|
613
|
+
*/
|
|
614
|
+
readonly placeholder?: string;
|
|
615
|
+
/**
|
|
616
|
+
* THE ACTIVE SUBJECT'S OWN INK — the name of the one thing edits target,
|
|
617
|
+
* where a palette wants that said in COLOUR rather than in fill.
|
|
618
|
+
*
|
|
619
|
+
* Optional, and emitted EMPTY when a palette names none (the `viewport`
|
|
620
|
+
* group's answer, not `category`'s): the one reader is the hierarchy
|
|
621
|
+
* row's label, which falls back to inheriting the row's ink, so a
|
|
622
|
+
* palette without it paints exactly what it painted before.
|
|
623
|
+
*
|
|
624
|
+
* Blender is why it exists. Measured on `outliner.png` (native 2x): the
|
|
625
|
+
* Cube — the active object — inks its NAME at #ffae28, an 82-pixel
|
|
626
|
+
* plateau, while Camera and Light ink at the row's ordinary #c2c2c2.
|
|
627
|
+
*
|
|
628
|
+
* CORRECTED 2026-09-18, and the correction matters because this note was
|
|
629
|
+
* the evidence behind "Blender marks the active row with the name and
|
|
630
|
+
* nothing else": it used to add that the Cube's row FILL is the plain 42
|
|
631
|
+
* stripe. That is true of `outliner.png` and says nothing about the state,
|
|
632
|
+
* because `outliner.png` is cut from `modeling.png`, whose Outliner is
|
|
633
|
+
* BYTE-IDENTICAL to `modeling-edit-none.png` — the Modeling workspace
|
|
634
|
+
* opens in EDIT mode, where no object-mode selection exists to paint. In
|
|
635
|
+
* `modeling-object-selected.png` the same Cube row carries a #324c7f band
|
|
636
|
+
* inside a #5a74a7 hairline. The name is one of three marks, not the only
|
|
637
|
+
* one; the other two are on `--vgai-tree-row-selected-bg` below.
|
|
638
|
+
*
|
|
639
|
+
* It is NOT `viewport.active`, and that is Blender's own distinction
|
|
640
|
+
* rather than ours: its 3D View paints the active object's outline
|
|
641
|
+
* #ffa028 where its Outliner inks the name #ffae28 — two theme entries,
|
|
642
|
+
* fourteen levels of green apart, and this palette already carries the
|
|
643
|
+
* first.
|
|
644
|
+
*/
|
|
645
|
+
readonly active?: string;
|
|
646
|
+
/**
|
|
647
|
+
* THE SELECTED-BUT-NOT-ACTIVE SUBJECT'S INK — {@link active}'s other
|
|
648
|
+
* half, for a palette whose reference distinguishes the two.
|
|
649
|
+
*
|
|
650
|
+
* Optional and emitted EMPTY the same way, and its one reader (the
|
|
651
|
+
* hierarchy row's label) falls back to {@link active}, so a palette that
|
|
652
|
+
* names only an active ink paints every selected row exactly what it
|
|
653
|
+
* painted before.
|
|
654
|
+
*
|
|
655
|
+
* Measured on `modeling-object-selected.png` (object mode, every object
|
|
656
|
+
* selected): Camera and Light — selected, not active — ink their names
|
|
657
|
+
* #e86900 as solid plateaus, while the Cube inks #ffae28. It is not any
|
|
658
|
+
* scaling or mix of the active ink (232/255, 105/174 and 0/40 are three
|
|
659
|
+
* different ratios), which is why it is a second entry rather than a
|
|
660
|
+
* derivation.
|
|
661
|
+
*/
|
|
662
|
+
readonly selected?: string;
|
|
663
|
+
/** Palette-owned ink ramp used only when Glass measures a bright backdrop. */
|
|
664
|
+
readonly onBright: {
|
|
665
|
+
readonly primary: string;
|
|
666
|
+
readonly muted: string;
|
|
667
|
+
readonly dim: string;
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
readonly accent: { readonly default: string; readonly muted: string };
|
|
671
|
+
readonly semantic: {
|
|
672
|
+
readonly danger: string;
|
|
673
|
+
readonly dangerMuted: string;
|
|
674
|
+
readonly dangerFaint: string;
|
|
675
|
+
readonly warning: string;
|
|
676
|
+
readonly warningMuted: string;
|
|
677
|
+
readonly success: string;
|
|
678
|
+
readonly successMuted: string;
|
|
679
|
+
readonly dynamic: string;
|
|
680
|
+
readonly dynamicMuted: string;
|
|
681
|
+
/**
|
|
682
|
+
* Component-instance identity (H1) — since 2026-07-31 the color of the
|
|
683
|
+
* dotted RULE under an instance row's name, not the name's own color
|
|
684
|
+
* (owner: "perhaps underline instead of color"). Still MUST be
|
|
685
|
+
* hue-bearing in every palette: a near-neutral value makes the rule
|
|
686
|
+
* read as an artifact rather than a mark, which is the same defect this
|
|
687
|
+
* role was split out of `accent` to fix. It carries less weight than it
|
|
688
|
+
* did as text color, though — the underline is the signal and the hue
|
|
689
|
+
* is the hint, so contrast here buys legibility, not the whole
|
|
690
|
+
* distinction.
|
|
691
|
+
*/
|
|
692
|
+
readonly instance: string;
|
|
693
|
+
};
|
|
694
|
+
readonly neutralOverlay: { readonly hover: string; readonly active: string };
|
|
695
|
+
readonly scrim: string;
|
|
696
|
+
/**
|
|
697
|
+
* The 3D VIEWPORT's own colours — Blender's "3D Viewport" theme section
|
|
698
|
+
* transcribed: window background, grid, the X and Y axis lines, the
|
|
699
|
+
* selected and active object. Optional: a palette without it keeps the
|
|
700
|
+
* editor's own (the accent selects, the dressing paints the background).
|
|
701
|
+
* Read by `native-selection-style.ts` off the emitted tokens.
|
|
702
|
+
*/
|
|
703
|
+
readonly viewport?: {
|
|
704
|
+
readonly background: string;
|
|
705
|
+
readonly grid: string;
|
|
706
|
+
readonly axisX: string;
|
|
707
|
+
readonly axisY: string;
|
|
708
|
+
readonly selection: string;
|
|
709
|
+
readonly active: string;
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* WIDGET COLOUR CLASSES — one fill per KIND of widget, the way Blender's
|
|
713
|
+
* theme carries `wcol_regular`/`wcol_menu`/`wcol_text` rather than one
|
|
714
|
+
* "raised surface". Optional, and every member is independently optional:
|
|
715
|
+
* a palette that names none is byte-identical to before this group
|
|
716
|
+
* existed, because each token falls back to the surface it used to read
|
|
717
|
+
* (`regular`/`menu` → `surface.raised`, `field` → `surface.inset`).
|
|
718
|
+
*
|
|
719
|
+
* MEASURED, Blender 5.2 factory startup, reference frames at 2x (halved
|
|
720
|
+
* here). The three classes really are three colours, which is the whole
|
|
721
|
+
* reason the group exists — our single `surface.raised` painted a
|
|
722
|
+
* dropdown trigger pushbutton-grey where Blender paints a dark well:
|
|
723
|
+
*
|
|
724
|
+
* - `regular` — a pushbutton. `Add Modifier`, `properties-modifier.png`
|
|
725
|
+
* (src x=200, y 142–177): inner #535353.
|
|
726
|
+
* - `menu` — a dropdown/menu BUTTON. `Object Mode v` and `Global v`,
|
|
727
|
+
* `modeling-object-none.png` (src y=78, x 97–… and x 1172–…), and the
|
|
728
|
+
* Scene/ViewLayer ID selectors' icon half in `topbar.png` (src y=26,
|
|
729
|
+
* x 2791–2850): inner #272727. NOT the menu POPUP's background
|
|
730
|
+
* (Blender's `wcol_pulldown`/`wcol_menu_back`) — the shot runner
|
|
731
|
+
* cannot photograph an open menu (`scripts/blender-reference/README.md`
|
|
732
|
+
* records the two measurements that proved it), so no popup value is
|
|
733
|
+
* declared anywhere and the popup keeps `surface.overlay`.
|
|
734
|
+
* - `field` — a text field. The Search fields in `properties-modifier.png`
|
|
735
|
+
* (src x=380, y 7–42) and `outliner.png` (src x=400, y 7–42), and the
|
|
736
|
+
* Scene name half in `topbar.png` (src y=26, x 2853–…): inner #1c1c1c.
|
|
737
|
+
*
|
|
738
|
+
* NO `edge` member, and that is a measurement, not an omission: all three
|
|
739
|
+
* classes outline in the SAME #3c3c3c, which the palette already carries
|
|
740
|
+
* as `boundary.default` — the colour every one of these call sites
|
|
741
|
+
* already borders with.
|
|
742
|
+
*
|
|
743
|
+
* Blender's `wcol_option` (a checkbox — `Add Rest Position`,
|
|
744
|
+
* `properties-data-edit.png`, src y=758, x 92–115) measures #535353,
|
|
745
|
+
* identical to `regular`, and our own checkbox is a native
|
|
746
|
+
* `<input type="checkbox">` whose unchecked box no colour token can reach
|
|
747
|
+
* without `appearance: none`. There is therefore nothing to declare and
|
|
748
|
+
* nothing to read; it belongs to whichever unit redraws that control.
|
|
749
|
+
*
|
|
750
|
+
* `emboss` is the fourth member and the odd one out: it is not a FILL but
|
|
751
|
+
* the 1 CSS px line Blender lays UNDER every widget, below that widget's
|
|
752
|
+
* own `#3c3c3c` outline (Blender's `ThemeUserInterface.widget_emboss`).
|
|
753
|
+
* Measured on five widget KINDS at native 2x, which is what says it is
|
|
754
|
+
* one rule and not five decorations:
|
|
755
|
+
*
|
|
756
|
+
* - pushbutton — `Add Modifier`, `properties-modifier.png` x=300:
|
|
757
|
+
* fill 142–177, outline 178–179, emboss 180–181.
|
|
758
|
+
* - text field — the datablock name, `properties-object.png` x=470:
|
|
759
|
+
* fill #1c1c1c 142–177, outline 178–179, emboss 180–181.
|
|
760
|
+
* - dropdown well — the datablock kind well, `properties-data-edit.png`
|
|
761
|
+
* x=103: fill #272727, outline 178–179, emboss 180–181.
|
|
762
|
+
* - checkbox — `Add Rest Position`, same frame x=103: fill #535353
|
|
763
|
+
* 746–769, outline 770–771, emboss 772–773.
|
|
764
|
+
* - list box — Vertex Groups, same frame x=103: interior #2c2c2c,
|
|
765
|
+
* outline 424–425, emboss 426–427.
|
|
766
|
+
*
|
|
767
|
+
* THE VALUE IS TRANSLUCENT BLACK, NOT A COLOUR — that is the whole reason
|
|
768
|
+
* it is declared as one token and not per surface. Read off four
|
|
769
|
+
* different backdrops (median over 160–500 px each, single-valued in
|
|
770
|
+
* every band):
|
|
771
|
+
*
|
|
772
|
+
* backdrop emboss
|
|
773
|
+
* #272727 39 Outliner header #212121 33
|
|
774
|
+
* #2f2f2f 47 Properties body #282828 40
|
|
775
|
+
* #343434 52 3D View header #2d2d2d 45
|
|
776
|
+
* #3c3c3c 60 a panel band #333333 51
|
|
777
|
+
*
|
|
778
|
+
* No 8-bit alpha reproduces all four, and the browser leaves no finer
|
|
779
|
+
* dial: Chrome QUANTIZES a `box-shadow` alpha to a byte (a declared
|
|
780
|
+
* 0.142 reads back as `rgba(0, 0, 0, 0.14)` and composites as 36/255).
|
|
781
|
+
* 36/255 renders 33/40/45/52 and 37/255 renders 33/40/44/51 — each is
|
|
782
|
+
* three of four, and the one they miss is the OTHER one. 0.142 is
|
|
783
|
+
* declared because the byte it lands on (36) is exact on the two
|
|
784
|
+
* surfaces this look actually paints widgets over, the Properties body
|
|
785
|
+
* (47 → 40, confirmed in our own capture) and the Outliner header
|
|
786
|
+
* (39 → 33); the residue is +1 under a widget sitting on a panel BAND.
|
|
787
|
+
* Only stacked widgets in one group escape the rule entirely: Blender's
|
|
788
|
+
* Location X/Y/Z share outlines and the emboss lands under the GROUP.
|
|
789
|
+
*
|
|
790
|
+
* A palette that names no `emboss` emits `--vgai-widget-emboss-shadow:
|
|
791
|
+
* none` and every widget paints exactly what it painted before this
|
|
792
|
+
* member existed.
|
|
793
|
+
*/
|
|
794
|
+
readonly widget?: {
|
|
795
|
+
readonly regular?: string;
|
|
796
|
+
readonly menu?: string;
|
|
797
|
+
readonly field?: string;
|
|
798
|
+
readonly emboss?: string;
|
|
799
|
+
};
|
|
800
|
+
/**
|
|
801
|
+
* CATEGORY INKS — colour as a GLYPH's own channel, the way Blender's
|
|
802
|
+
* Properties-tab rail carries it. That rail groups by HUE, and the hue is
|
|
803
|
+
* the discriminator: at 14 px the tab shapes in `properties-object.png`
|
|
804
|
+
* are near-indistinguishable, so what says "this is the Modifier group"
|
|
805
|
+
* is that it is blue. The outliner says the same thing about a row's type
|
|
806
|
+
* glyph, and an edit-mode operator mark says it about the element it
|
|
807
|
+
* operates on.
|
|
808
|
+
*
|
|
809
|
+
* Optional, and every member is independently optional: each token
|
|
810
|
+
* emits with a `currentColor` fallback, so a palette naming none paints
|
|
811
|
+
* every glyph monochrome exactly as before this group existed. A glyph
|
|
812
|
+
* takes a tone through the icon SET (`IconSetContribution`'s per-glyph
|
|
813
|
+
* `tone`/`tonedPath`); the palette only says what each tone's ink is.
|
|
814
|
+
*
|
|
815
|
+
* EVERY VALUE HERE IS INK — the glyph's own colour at FULL opacity, never
|
|
816
|
+
* the composite a particular chrome produces. The SITE applies Blender's
|
|
817
|
+
* alpha. Corrected 2026-09-18 after the group was found in two colour
|
|
818
|
+
* spaces: `object` had been sampled from the Object tab in
|
|
819
|
+
* `properties-object.png`, which is the ACTIVE tab and paints its ink
|
|
820
|
+
* FULL, while `modifier`/`material`/`tool`/`data` were sampled from
|
|
821
|
+
* INACTIVE tabs in the same frame, which paint at α — so the green
|
|
822
|
+
* rendered 31 levels low wherever the ink was wanted, and `modifier`
|
|
823
|
+
* looked impossible to α-undo (its blue solved to 257).
|
|
824
|
+
*
|
|
825
|
+
* THE ALPHA IS 0.80, EXACTLY, and it is now PROVED rather than fitted,
|
|
826
|
+
* because three inks are photographed at full in one frame each and the
|
|
827
|
+
* same ink is photographed dimmed in another:
|
|
828
|
+
*
|
|
829
|
+
* site α
|
|
830
|
+
* Properties rail, ACTIVE tab 1.00
|
|
831
|
+
* Properties rail, inactive tab 0.80
|
|
832
|
+
* Outliner row glyph (active object or not) 0.80
|
|
833
|
+
* Edit-mode tool shelf 1.00
|
|
834
|
+
*
|
|
835
|
+
* Object at 0.80 renders every channel of every sample exactly:
|
|
836
|
+
* #e09557 over #1c1c1c → 28+0.8·(224−28, 149−28, 87−28) = (185,125,75) =
|
|
837
|
+
* #b97d4b, which is the inactive Object tab in `properties-modifier.png`
|
|
838
|
+
* and `properties-data-edit.png` (src x 10-56, y 522-549); over the
|
|
839
|
+
* outliner's #272727 → (187,127,77) = #bb7f4d (`outliner.png`, the Camera
|
|
840
|
+
* and Light rows, src x 126-155 / 129-152); over the selected row's
|
|
841
|
+
* #525252 → (196,136,86) = #c48856 (the Cube row's own glyph plate,
|
|
842
|
+
* src x 121-160). The earlier 0.791 came from the two-background form
|
|
843
|
+
* (196−187)/(82−39), where 195.6 had rounded UP to 196 — one quantisation
|
|
844
|
+
* step in the numerator, 1% in the answer, and enough to put `modifier`
|
|
845
|
+
* out of gamut.
|
|
846
|
+
*
|
|
847
|
+
* - `object` — #e09557, PHOTOGRAPHED at full: `properties-object.png`,
|
|
848
|
+
* the ACTIVE Object tab (src x 10-56, y 522-549), over its #2f2f2f
|
|
849
|
+
* open-tab plate. Unchanged by the correction; it was always ink.
|
|
850
|
+
* - `modifier` — #73a1ff, PHOTOGRAPHED at full: `properties-modifier.png`,
|
|
851
|
+
* the ACTIVE Modifier tab (src y 578-605) = (115,161,255). Its dimmed
|
|
852
|
+
* form #6286d1 (this member's former value) is that ink at 0.80 over
|
|
853
|
+
* #1c1c1c, and Particles/Physics/Constraints (y 634-774) measure the
|
|
854
|
+
* identical blue, which is the grouping.
|
|
855
|
+
* - `data` — #00d3a2, PHOTOGRAPHED at full: `properties-data-edit.png`,
|
|
856
|
+
* the ACTIVE Object Data tab (src y 802-829) = (0,211,162). Its red is
|
|
857
|
+
* CLAMPED AT ZERO, which is why α-undoing it from a composite was
|
|
858
|
+
* unstable: the outliner's three data glyphs paint (7,176,137) on the
|
|
859
|
+
* row's 39, (15,184,145) on a #4f4f4f icon plate and (5,182,141) on the
|
|
860
|
+
* Cube's tinted plate (`outliner.png` src x 266-327, y 135-247), and
|
|
861
|
+
* all three resolve to this one ink at 0.80 only once the red is read
|
|
862
|
+
* as 0.2·backdrop rather than as ink.
|
|
863
|
+
* - `material` — #cb646e, DERIVED, not photographed: no reference frame
|
|
864
|
+
* opens the Material tab, so this is the inactive #a8565e
|
|
865
|
+
* (`properties-object.png` src y 858-885) α-undone at 0.80. It
|
|
866
|
+
* re-renders that composite byte-for-byte; if a frame ever shows the
|
|
867
|
+
* tab open, read it there instead.
|
|
868
|
+
* - `tool` — #cbcbcb, DERIVED the same way from the inactive #a8a8a8
|
|
869
|
+
* (`properties-object.png`, the Tool tab, src y 82-109; Render, Output,
|
|
870
|
+
* View Layer and Scene, y 155-350, measure the identical grey). The
|
|
871
|
+
* scene-level tabs are deliberately the quiet, hue-less group and read
|
|
872
|
+
* DIMMER than the row text (#c2c2c2) only because they are dimmed —
|
|
873
|
+
* the ink itself is lighter than the text.
|
|
874
|
+
* - `operator` — #95dab2. `modeling-edit-none.png`, the edit-mode tool
|
|
875
|
+
* column: Add Cube (src x 36-86, y 690-740), Extrude (y 990-1030),
|
|
876
|
+
* Loop Cut / Knife / Poly Build (y 1080-1280). Already ink and
|
|
877
|
+
* unchanged: that shelf is a 1.00 site (its neutral bodies measure
|
|
878
|
+
* #e4e4e4 over #272727, which no α below 1 can produce). Only the
|
|
879
|
+
* OPERATED element takes the tint; the cube stays #e3e3e3.
|
|
880
|
+
*
|
|
881
|
+
* MEASURED BUT NOT DECLARED. Every ink below was read off a frame; none
|
|
882
|
+
* is declared, because a member no glyph can paint is a name with no
|
|
883
|
+
* caller. The coordinates are here so the unit that earns one
|
|
884
|
+
* transcribes rather than re-measures:
|
|
885
|
+
*
|
|
886
|
+
* - `world` — #cb646e at ink (the World tab, `properties-object.png`
|
|
887
|
+
* src x 10-56, y 378-405, measures #a6555d inactive), within a level
|
|
888
|
+
* of `material` because Blender puts World and Material in one red
|
|
889
|
+
* group. Nothing in this editor draws a globe to MEAN a World
|
|
890
|
+
* datablock: `faGlobe`'s sites are the transform-SPACE toggle and a
|
|
891
|
+
* build profile, where red reads as an error state.
|
|
892
|
+
* - no `camera`/`light` — in `outliner.png` the camera and light OBJECT
|
|
893
|
+
* glyphs measure #bb7f4d, byte-identical to the mesh object's, and
|
|
894
|
+
* their DATA glyphs measure the same green as the mesh's. Blender's
|
|
895
|
+
* outliner discriminates object-vs-data, not camera-vs-light.
|
|
896
|
+
* - no `accent` — no glyph in any frame paints in the palette's accent.
|
|
897
|
+
* - `deform` — #d6c1e4. Blender's edit-mode column carries a SECOND
|
|
898
|
+
* operator tint, a lilac on the deforming tools (Smooth src x 36-88
|
|
899
|
+
* y 1330-1380, Shear y 1500-1550, Edge Slide y 1420-1470) against the
|
|
900
|
+
* creating tools' green. Our glyph set ships none of those operators.
|
|
901
|
+
*/
|
|
902
|
+
readonly category?: {
|
|
903
|
+
readonly object?: string;
|
|
904
|
+
readonly modifier?: string;
|
|
905
|
+
readonly material?: string;
|
|
906
|
+
readonly tool?: string;
|
|
907
|
+
readonly operator?: string;
|
|
908
|
+
/** Mesh DATA — Blender's green Object Data tab and its outliner data
|
|
909
|
+
* glyphs: `.tui.icon_object_data` #00d4a3
|
|
910
|
+
* (`userdef_default_theme.c:275`), which every `DEF_ICON_OBJECT_DATA`
|
|
911
|
+
* mark paints in. */
|
|
912
|
+
readonly data?: string;
|
|
913
|
+
/** Blender's SCENE group — the Render, Output, View Layer and Scene
|
|
914
|
+
* tabs, every one of them a `DEF_ICON_SCENE` in `UI_icons.hh`:
|
|
915
|
+
* `.tui.icon_scene` #cccccc (`userdef_default_theme.c:272`). Note the
|
|
916
|
+
* World tab is NOT in it — `UI_icons.hh:193` declares it
|
|
917
|
+
* `DEF_ICON_SHADING(WORLD)`, so it takes `material` with Material and
|
|
918
|
+
* Texture. That correction is what reading the source bought over
|
|
919
|
+
* reading the rail. */
|
|
920
|
+
readonly scene?: string;
|
|
921
|
+
/** Blender's COLLECTION group — one tab, `DEF_ICON_COLLECTION(GROUP)`
|
|
922
|
+
* (`UI_icons.hh:248`): `.tui.icon_collection` #ffffff
|
|
923
|
+
* (`userdef_default_theme.c:273`). White rather than the scene grey,
|
|
924
|
+
* which is why it is its own name. */
|
|
925
|
+
readonly collection?: string;
|
|
926
|
+
};
|
|
927
|
+
/**
|
|
928
|
+
* REGION FILLS — one colour per EDITOR AREA, the way Blender's theme
|
|
929
|
+
* carries `theme.outliner.back` and `theme.properties.back` rather than
|
|
930
|
+
* one "panel". Our dock paints every group from a single
|
|
931
|
+
* `--dv-group-view-background-color`, so before this member a panel could
|
|
932
|
+
* not have its own fill and the Outliner sat eight levels too light.
|
|
933
|
+
*
|
|
934
|
+
* A region names ONE colour and it paints the area's BODY AND ITS HEADER,
|
|
935
|
+
* because that is what the frames measure — see below. Optional, and every
|
|
936
|
+
* member is independently optional: an undeclared region falls back to
|
|
937
|
+
* what its call sites already read (`surface.panel` for the body,
|
|
938
|
+
* `surface.chrome` for the header), so a palette naming none is
|
|
939
|
+
* byte-identical to before this group existed.
|
|
940
|
+
*
|
|
941
|
+
* A panel claims a region by name in `workspace-static-panels.ts`
|
|
942
|
+
* ({@link EditorRegionName}); the workspace host applies the ACTIVE
|
|
943
|
+
* panel's claim to the region it sits in. A panel that claims none is
|
|
944
|
+
* unchanged.
|
|
945
|
+
*
|
|
946
|
+
* MEASURED, Blender 5.2 factory startup, `modeling.png` at 2x (source
|
|
947
|
+
* coordinates in the 2x frame):
|
|
948
|
+
*
|
|
949
|
+
* - `outliner` — #272727, BODY AND HEADER ALIKE. Body: row y=300 over
|
|
950
|
+
* x 2845..3448 is #272727 with the row alternation stepping to
|
|
951
|
+
* #2a2a2a. Header: row y=75 between the search field and the chevron
|
|
952
|
+
* (x 3260..3345) is the identical #272727. Our `surface.chrome`
|
|
953
|
+
* #1c1c1c painted that header eleven levels DARKER than its own body,
|
|
954
|
+
* where Blender's is the same colour — the header does not read as a
|
|
955
|
+
* slab there at all.
|
|
956
|
+
* - `properties` — #2f2f2f, body and header alike. Body: column x=3400,
|
|
957
|
+
* y 608..2054 is #2f2f2f. Header: row y=450 over x 2924..3030 and
|
|
958
|
+
* x 3261..3395 is #2f2f2f. The body already matched `surface.panel`;
|
|
959
|
+
* the member exists for the HEADER, which was #1c1c1c.
|
|
960
|
+
*
|
|
961
|
+
* NOT DECLARABLE HERE, and it is the one place Blender's header differs
|
|
962
|
+
* from its body: the 3D viewport's header is #343434 over a #3f3f3f back
|
|
963
|
+
* (`modeling.png` y=108 is 52, y=109 is 63). That area is not a dock
|
|
964
|
+
* panel — it is the document surface, whose fill is `viewport.background`
|
|
965
|
+
* and whose header is `--vgai-surface-header` — so it takes no region
|
|
966
|
+
* claim and the "one colour, header and body" rule above stands for
|
|
967
|
+
* everything this member can reach.
|
|
968
|
+
*/
|
|
969
|
+
readonly region?: { readonly [name in EditorRegionName]?: string };
|
|
970
|
+
};
|
|
971
|
+
readonly typography: { readonly sans: string; readonly mono: string };
|
|
972
|
+
readonly shape: {
|
|
973
|
+
readonly small: string;
|
|
974
|
+
readonly medium: string;
|
|
975
|
+
readonly large: string;
|
|
976
|
+
readonly full: string;
|
|
977
|
+
};
|
|
978
|
+
readonly elevation: { readonly small: string; readonly medium: string; readonly large: string };
|
|
979
|
+
/** A material's own chrome density (`editor-appearance.ts`); absent means
|
|
980
|
+
* the editor's own `controlSize`/`fontSize`/`chromeSize`. */
|
|
981
|
+
readonly density?: EditorDensity;
|
|
982
|
+
/**
|
|
983
|
+
* Surface-treatment vocabulary (Glass-UI spike, W3 — work item 3).
|
|
984
|
+
* Optional: absent means no backdrop treatment, matching every existing
|
|
985
|
+
* theme's opaque panels. Extensible record for future treatment axes;
|
|
986
|
+
* only backdrop blur/ saturation are defined for now. A "glass" look is
|
|
987
|
+
* entirely DATA here — no `[data-vgai-theme=…]` CSS special-casing
|
|
988
|
+
* exists or should be added; `editorThemeVariables` is the only place
|
|
989
|
+
* this is consumed.
|
|
990
|
+
*/
|
|
991
|
+
readonly treatment?: {
|
|
992
|
+
/** Backdrop blur radius in px (0–64). */
|
|
993
|
+
readonly backdropBlurPx?: number;
|
|
994
|
+
/** Backdrop saturation multiplier (0–3; 1 = unchanged). */
|
|
995
|
+
readonly backdropSaturation?: number;
|
|
996
|
+
/**
|
|
997
|
+
* Backdrop brightness multiplier (0–2; 1 = unchanged). The luminance
|
|
998
|
+
* clamp that keeps dark-glass text legible over arbitrarily bright scene
|
|
999
|
+
* content (glass-UI W7, report §2.22): a value below 1 darkens whatever
|
|
1000
|
+
* the card floats over BEFORE the translucent panel color composites, so
|
|
1001
|
+
* a pure-white backdrop can never wash out card chrome.
|
|
1002
|
+
*/
|
|
1003
|
+
readonly backdropBrightness?: number;
|
|
1004
|
+
/**
|
|
1005
|
+
* Liquid Glass Tier-1 edge-specular intensity (0–1; report §2.24). Drives
|
|
1006
|
+
* the alphas of the pure-CSS "light catching the edge" trio — the 1.5px
|
|
1007
|
+
* gradient border ring, the 1px inset rim catches, and the diagonal
|
|
1008
|
+
* sheen — all emitted as `--vgai-card-specular-*` variables and painted
|
|
1009
|
+
* by `workspace-dock.css`'s card-chrome pseudo-elements. 0/absent means
|
|
1010
|
+
* none of the three paint. Calibrated so 0.6 reproduces the committed
|
|
1011
|
+
* prototype's variant-B alphas exactly
|
|
1012
|
+
* (`docs/assets/glass-ui-feasibility/liquid-glass-proto/index.html`).
|
|
1013
|
+
*/
|
|
1014
|
+
readonly edgeSpecular?: number;
|
|
1015
|
+
/**
|
|
1016
|
+
* Direction the specular "light" arrives from, in degrees (0–360;
|
|
1017
|
+
* default 120 — upper-left key light, matching the prototype). Rotates
|
|
1018
|
+
* both the ring gradient and the sheen; only meaningful when
|
|
1019
|
+
* `edgeSpecular` > 0.
|
|
1020
|
+
*/
|
|
1021
|
+
readonly specularAngleDeg?: number;
|
|
1022
|
+
/**
|
|
1023
|
+
* Liquid Glass Tier-1 thickness cue (0–1; report §2.24): scales the
|
|
1024
|
+
* geometry of the inset-shadow depth pair (upper glass glow + lower
|
|
1025
|
+
* inner shadow) and the card's drop shadow. 0/absent keeps the theme's
|
|
1026
|
+
* ordinary `elevation.large` card shadow. Calibrated so 0.5 reproduces
|
|
1027
|
+
* the prototype's variant-B depths exactly.
|
|
1028
|
+
*/
|
|
1029
|
+
readonly thickness?: number;
|
|
1030
|
+
/**
|
|
1031
|
+
* Liquid Glass refraction: width of the refracting bezel band in px
|
|
1032
|
+
* (4–80; the owner-reopened visual default is 56). Only meaningful when
|
|
1033
|
+
* `refractionThickness` > 0. Displacement follows the convex-squircle
|
|
1034
|
+
* height profile from full bend at the rim to zero at the bezel's inner
|
|
1035
|
+
* boundary; the interior is strictly neutral — the backdrop passes
|
|
1036
|
+
* through untouched (`components/glass-refraction.ts`).
|
|
1037
|
+
*/
|
|
1038
|
+
readonly refractionBezelPx?: number;
|
|
1039
|
+
/**
|
|
1040
|
+
* Liquid Glass refraction: slab-thickness factor (0–2; ×40px of glass;
|
|
1041
|
+
* the owner-reopened visual default is 1.25). This is THE refraction knob — the
|
|
1042
|
+
* `feDisplacementMap` scale is physically DERIVED from the generated
|
|
1043
|
+
* field's max magnitude, never authored directly. 0/absent means no
|
|
1044
|
+
* refraction. Capability-gated at the surface host
|
|
1045
|
+
* (`components/glass-refraction.ts`): it only ever paints when Chromium
|
|
1046
|
+
* supports `backdrop-filter: url(#…)` AND the GPU is not a software
|
|
1047
|
+
* rasterizer; everywhere else the plain
|
|
1048
|
+
* `--vgai-surface-backdrop-filter` list paints instead, automatically.
|
|
1049
|
+
* Distinct from `thickness` (the Tier-1 inset-shadow depth cue), which
|
|
1050
|
+
* survives as an independent axis.
|
|
1051
|
+
*/
|
|
1052
|
+
readonly refractionThickness?: number;
|
|
1053
|
+
/**
|
|
1054
|
+
* Content-legibility text shadow opacity (0–1): emits
|
|
1055
|
+
* `--vgai-content-text-shadow: 0 1px 2px rgba(0,0,0,<v>)` inherited by
|
|
1056
|
+
* all editor chrome. The §2.32 legibility finding: near-white text +
|
|
1057
|
+
* concentrated text-shadow is the correct default over a predominantly dark
|
|
1058
|
+
* viewport when the surface itself is a ≤15% white whisper (a full
|
|
1059
|
+
* adaptive palette is a later unit). 0/absent emits `none` — every
|
|
1060
|
+
* pre-existing theme is byte-identical.
|
|
1061
|
+
*/
|
|
1062
|
+
readonly textShadowOpacity?: number;
|
|
1063
|
+
/**
|
|
1064
|
+
* Maximum body/content frost blur in px (0–24; §2.31 P2 owner
|
|
1065
|
+
* amendment). On the SVG path the lens blends from its clear rim toward
|
|
1066
|
+
* this blur with optical depth. `.vgai-content-frost` also applies the
|
|
1067
|
+
* same value locally behind text-dense zones, using a colorless/light
|
|
1068
|
+
* material lift rather than black paint. 0/absent emits
|
|
1069
|
+
* `none`/`transparent`, so treatment-less themes stay byte-identical.
|
|
1070
|
+
*/
|
|
1071
|
+
readonly contentFrostBlurPx?: number;
|
|
1072
|
+
/**
|
|
1073
|
+
* Faint ambient lift (P6-U6 owner taste decision 2, approved
|
|
1074
|
+
* 2026-07-19): peak white alpha (0–1, faint — ≤0.1 territory) of a
|
|
1075
|
+
* fixed top-lit luminance wash layered with card/island fills so
|
|
1076
|
+
* clear glass still reads as a surface over a pure-black void —
|
|
1077
|
+
* physically a glass sheet over black IS invisible, and the owner
|
|
1078
|
+
* chose a subtle ambient light response over accepting the physics.
|
|
1079
|
+
* Emitted as the `--vgai-ambient-lift` background-image layer
|
|
1080
|
+
* (gradient geometry is a design constant here; this axis is only the
|
|
1081
|
+
* intensity). 0/absent emits `none` — every pre-existing theme and
|
|
1082
|
+
* every lite/reduced-transparency fallback paints byte-identically.
|
|
1083
|
+
*/
|
|
1084
|
+
readonly ambientLiftOpacity?: number;
|
|
1085
|
+
/** Enable measured per-surface use of the palette's bright-backdrop ink ramp. */
|
|
1086
|
+
readonly adaptiveContent?: boolean;
|
|
1087
|
+
/** Bright-side text-shadow opacity; material physics, never palette chroma. */
|
|
1088
|
+
readonly brightTextShadowOpacity?: number;
|
|
1089
|
+
/** Bright-side content frost tint; material physics, never content ink. */
|
|
1090
|
+
readonly brightFrostBg?: string;
|
|
1091
|
+
};
|
|
1092
|
+
/** Resolved axis identity. Legacy standalone themes are inferred when absent. */
|
|
1093
|
+
readonly appearance?: {
|
|
1094
|
+
readonly material: 'classic' | 'glass';
|
|
1095
|
+
readonly transparency: 'standard' | 'reduced';
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export const graphiteDarkEditorTheme: EditorTheme = {
|
|
1100
|
+
id: 'graphite-dark',
|
|
1101
|
+
color: {
|
|
1102
|
+
surface: {
|
|
1103
|
+
shell: GRAPHITE_BG[0],
|
|
1104
|
+
panel: GRAPHITE_BG[1],
|
|
1105
|
+
chrome: GRAPHITE_BG[2],
|
|
1106
|
+
raised: GRAPHITE_BG[3],
|
|
1107
|
+
inset: GRAPHITE_BG.inset,
|
|
1108
|
+
overlay: 'rgba(36,36,36,0.94)',
|
|
1109
|
+
},
|
|
1110
|
+
boundary: { default: GRAPHITE_BORDER[1], strong: GRAPHITE_BORDER[2] },
|
|
1111
|
+
content: {
|
|
1112
|
+
primary: GRAPHITE_TEXT[1],
|
|
1113
|
+
muted: GRAPHITE_TEXT[2],
|
|
1114
|
+
dim: GRAPHITE_TEXT[3],
|
|
1115
|
+
onAccent: '#101820',
|
|
1116
|
+
onBright: {
|
|
1117
|
+
primary: '#1a2330',
|
|
1118
|
+
muted: 'rgba(26,35,48,0.82)',
|
|
1119
|
+
dim: 'rgba(26,35,48,0.74)',
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
accent: { default: GRAPHITE_ACCENT, muted: GRAPHITE_ACCENT_MUTED },
|
|
1123
|
+
semantic: {
|
|
1124
|
+
danger: GRAPHITE_DANGER,
|
|
1125
|
+
dangerMuted: GRAPHITE_DANGER_MUTED,
|
|
1126
|
+
dangerFaint: GRAPHITE_DANGER_FAINT,
|
|
1127
|
+
warning: GRAPHITE_WARN,
|
|
1128
|
+
warningMuted: GRAPHITE_WARN_MUTED,
|
|
1129
|
+
success: GRAPHITE_SUCCESS,
|
|
1130
|
+
successMuted: GRAPHITE_SUCCESS_MUTED,
|
|
1131
|
+
dynamic: GRAPHITE_DYNAMIC,
|
|
1132
|
+
dynamicMuted: GRAPHITE_DYNAMIC_BG,
|
|
1133
|
+
instance: GRAPHITE_INSTANCE,
|
|
1134
|
+
},
|
|
1135
|
+
neutralOverlay: {
|
|
1136
|
+
hover: 'rgba(255,255,255,0.08)',
|
|
1137
|
+
active: 'rgba(255,255,255,0.14)',
|
|
1138
|
+
},
|
|
1139
|
+
scrim: GRAPHITE_SCRIM,
|
|
1140
|
+
},
|
|
1141
|
+
typography: { sans: GRAPHITE_FONT_SANS, mono: GRAPHITE_FONT_MONO },
|
|
1142
|
+
shape: {
|
|
1143
|
+
small: DEFAULT_RADIUS.small,
|
|
1144
|
+
medium: DEFAULT_RADIUS.medium,
|
|
1145
|
+
large: DEFAULT_RADIUS.large,
|
|
1146
|
+
full: DEFAULT_RADIUS.full,
|
|
1147
|
+
},
|
|
1148
|
+
elevation: {
|
|
1149
|
+
small: GRAPHITE_SHADOW.sm,
|
|
1150
|
+
medium: GRAPHITE_SHADOW.md,
|
|
1151
|
+
large: GRAPHITE_SHADOW.lg,
|
|
1152
|
+
},
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
/** Neutral palette used by the default Glass style without rewriting Classic. */
|
|
1156
|
+
export const graphiteNeutralEditorTheme: EditorTheme = {
|
|
1157
|
+
...graphiteDarkEditorTheme,
|
|
1158
|
+
id: 'graphite-neutral',
|
|
1159
|
+
color: {
|
|
1160
|
+
...graphiteDarkEditorTheme.color,
|
|
1161
|
+
content: {
|
|
1162
|
+
primary: GRAPHITE_NEUTRAL_TEXT[1],
|
|
1163
|
+
muted: GRAPHITE_NEUTRAL_TEXT[2],
|
|
1164
|
+
dim: GRAPHITE_NEUTRAL_TEXT[3],
|
|
1165
|
+
onAccent: '#101820',
|
|
1166
|
+
onBright: {
|
|
1167
|
+
primary: '#1a2330',
|
|
1168
|
+
muted: 'rgba(26,35,48,0.82)',
|
|
1169
|
+
dim: 'rgba(26,35,48,0.74)',
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
accent: {
|
|
1173
|
+
default: GRAPHITE_NEUTRAL_ACCENT,
|
|
1174
|
+
muted: GRAPHITE_NEUTRAL_ACCENT_MUTED,
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Production high-contrast theme. It deliberately preserves the editor's
|
|
1181
|
+
* dark, low-glare character while increasing text/boundary separation and
|
|
1182
|
+
* shifting the accent away from the default graphite palette.
|
|
1183
|
+
*/
|
|
1184
|
+
export const midnightHighContrastEditorTheme: EditorTheme = {
|
|
1185
|
+
id: 'midnight-high-contrast',
|
|
1186
|
+
color: {
|
|
1187
|
+
surface: {
|
|
1188
|
+
shell: '#0b0f14',
|
|
1189
|
+
panel: '#111821',
|
|
1190
|
+
chrome: '#17212c',
|
|
1191
|
+
raised: '#223040',
|
|
1192
|
+
inset: '#080c11',
|
|
1193
|
+
overlay: 'rgba(17,24,33,0.96)',
|
|
1194
|
+
},
|
|
1195
|
+
boundary: { default: '#34465a', strong: '#71869d' },
|
|
1196
|
+
content: {
|
|
1197
|
+
primary: '#f2f5f8',
|
|
1198
|
+
muted: '#bdc8d5',
|
|
1199
|
+
dim: '#8493a5',
|
|
1200
|
+
onAccent: '#071019',
|
|
1201
|
+
onBright: {
|
|
1202
|
+
primary: '#071019',
|
|
1203
|
+
muted: 'rgba(7,16,25,0.82)',
|
|
1204
|
+
dim: 'rgba(7,16,25,0.72)',
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
accent: { default: '#65d1ff', muted: 'rgba(101,209,255,0.18)' },
|
|
1208
|
+
semantic: {
|
|
1209
|
+
danger: '#ff7b86',
|
|
1210
|
+
dangerMuted: 'rgba(255,123,134,0.18)',
|
|
1211
|
+
dangerFaint: 'rgba(255,123,134,0.08)',
|
|
1212
|
+
warning: '#f3c969',
|
|
1213
|
+
warningMuted: 'rgba(243,201,105,0.16)',
|
|
1214
|
+
success: '#62d394',
|
|
1215
|
+
successMuted: 'rgba(98,211,148,0.18)',
|
|
1216
|
+
dynamic: '#e889ff',
|
|
1217
|
+
dynamicMuted: 'rgba(232,137,255,0.12)',
|
|
1218
|
+
// This is the ONE palette whose accent already owns cyan (#65d1ff), so
|
|
1219
|
+
// instance identity takes the next-widest free gap instead — the
|
|
1220
|
+
// indigo/periwinkle band between the accent (~198°) and dynamic
|
|
1221
|
+
// (~288°), ~45° clear of both.
|
|
1222
|
+
instance: '#a49bff',
|
|
1223
|
+
},
|
|
1224
|
+
neutralOverlay: {
|
|
1225
|
+
hover: 'rgba(255,255,255,0.10)',
|
|
1226
|
+
active: 'rgba(255,255,255,0.18)',
|
|
1227
|
+
},
|
|
1228
|
+
scrim: 'rgba(0,0,0,0.68)',
|
|
1229
|
+
},
|
|
1230
|
+
typography: { sans: GRAPHITE_FONT_SANS, mono: GRAPHITE_FONT_MONO },
|
|
1231
|
+
shape: { small: '3px', medium: '6px', large: '8px', full: '9999px' },
|
|
1232
|
+
elevation: {
|
|
1233
|
+
small: '0 2px 8px rgba(0,0,0,0.72)',
|
|
1234
|
+
medium: '0 6px 18px rgba(0,0,0,0.68)',
|
|
1235
|
+
large: '0 12px 40px rgba(0,0,0,0.76)',
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Glass-UI spike (W3, work item 3): graphite-derived, translucent built-in
|
|
1241
|
+
* theme. Surface alphas target panel alpha >= 0.55
|
|
1242
|
+
* COMPOSITED (i.e. after `backdropSaturation`'s perceived contrast boost)
|
|
1243
|
+
* to keep text-on-surface contrast workable over arbitrary live 3D content
|
|
1244
|
+
* — the spike's proven `color-mix(... 62%, transparent)` card recipe,
|
|
1245
|
+
* expressed here as theme data instead of injected CSS. Elevation is
|
|
1246
|
+
* strengthened slightly over graphite-dark: a translucent card needs a more
|
|
1247
|
+
* assertive shadow to read as "above" the scene it is blurring.
|
|
1248
|
+
*/
|
|
1249
|
+
export const glassDarkEditorTheme: EditorTheme = {
|
|
1250
|
+
id: 'glass-dark',
|
|
1251
|
+
appearance: { material: 'glass', transparency: 'standard' },
|
|
1252
|
+
color: {
|
|
1253
|
+
surface: {
|
|
1254
|
+
shell: 'rgba(26,26,26,0.88)',
|
|
1255
|
+
panel: 'rgba(36,36,36,0.62)',
|
|
1256
|
+
chrome: 'rgba(44,44,44,0.68)',
|
|
1257
|
+
raised: 'rgba(51,51,51,0.74)',
|
|
1258
|
+
inset: 'rgba(30,30,30,0.7)',
|
|
1259
|
+
overlay: 'rgba(36,36,36,0.55)',
|
|
1260
|
+
},
|
|
1261
|
+
boundary: { default: 'rgba(255,255,255,0.14)', strong: 'rgba(255,255,255,0.24)' },
|
|
1262
|
+
content: {
|
|
1263
|
+
primary: GRAPHITE_TEXT[1],
|
|
1264
|
+
muted: GRAPHITE_TEXT[2],
|
|
1265
|
+
dim: GRAPHITE_TEXT[3],
|
|
1266
|
+
onAccent: '#101820',
|
|
1267
|
+
onBright: {
|
|
1268
|
+
primary: '#1a2330',
|
|
1269
|
+
muted: 'rgba(26,35,48,0.82)',
|
|
1270
|
+
dim: 'rgba(26,35,48,0.74)',
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
accent: {
|
|
1274
|
+
default: GRAPHITE_NEUTRAL_ACCENT,
|
|
1275
|
+
muted: GRAPHITE_NEUTRAL_ACCENT_MUTED,
|
|
1276
|
+
},
|
|
1277
|
+
semantic: {
|
|
1278
|
+
danger: GRAPHITE_DANGER,
|
|
1279
|
+
dangerMuted: GRAPHITE_DANGER_MUTED,
|
|
1280
|
+
dangerFaint: GRAPHITE_DANGER_FAINT,
|
|
1281
|
+
warning: GRAPHITE_WARN,
|
|
1282
|
+
warningMuted: GRAPHITE_WARN_MUTED,
|
|
1283
|
+
success: GRAPHITE_SUCCESS,
|
|
1284
|
+
successMuted: GRAPHITE_SUCCESS_MUTED,
|
|
1285
|
+
dynamic: GRAPHITE_DYNAMIC,
|
|
1286
|
+
dynamicMuted: GRAPHITE_DYNAMIC_BG,
|
|
1287
|
+
instance: GRAPHITE_INSTANCE,
|
|
1288
|
+
},
|
|
1289
|
+
neutralOverlay: {
|
|
1290
|
+
hover: 'color-mix(in srgb, currentColor 9%, transparent)',
|
|
1291
|
+
active: 'color-mix(in srgb, currentColor 14%, transparent)',
|
|
1292
|
+
},
|
|
1293
|
+
// Lighter dim than the opaque themes — the modal scrim's blur
|
|
1294
|
+
// (`--vgai-scrim-backdrop-filter`, emitted for every treatment theme)
|
|
1295
|
+
// does the separation work, the macOS read (P6-U6 owner taste
|
|
1296
|
+
// decision 3). The lite fallback pins the original heavy dim.
|
|
1297
|
+
scrim: 'rgba(0,0,0,0.35)',
|
|
1298
|
+
},
|
|
1299
|
+
typography: { sans: GRAPHITE_FONT_SANS, mono: GRAPHITE_FONT_MONO },
|
|
1300
|
+
shape: {
|
|
1301
|
+
small: DEFAULT_RADIUS.small,
|
|
1302
|
+
medium: DEFAULT_RADIUS.medium,
|
|
1303
|
+
large: DEFAULT_RADIUS.large,
|
|
1304
|
+
full: DEFAULT_RADIUS.full,
|
|
1305
|
+
},
|
|
1306
|
+
elevation: {
|
|
1307
|
+
small: '0 4px 16px rgba(0,0,0,0.55)',
|
|
1308
|
+
medium: '0 8px 28px rgba(0,0,0,0.5)',
|
|
1309
|
+
large: '0 16px 48px rgba(0,0,0,0.55)',
|
|
1310
|
+
},
|
|
1311
|
+
// brightness 0.5 tuned live over a pure-white scene (W7 contrast session,
|
|
1312
|
+
// report §2.22): 0.65 was invisible over white, 0.2 killed the backdrop
|
|
1313
|
+
// bleed entirely; 0.5 holds crisp text over white while the scene still
|
|
1314
|
+
// reads through every card.
|
|
1315
|
+
// Liquid Glass Tier-1 axes (report §2.24) tuned live over the dark
|
|
1316
|
+
// template scene AND a forced-white scene (0/0.4/0.55/0.6/0.8 specular ×
|
|
1317
|
+
// 0.3/0.5/0.7 thickness close-up matrix): edgeSpecular 0.6 — the exact
|
|
1318
|
+
// prototype-B reference the owner reviewed; 0.8 over-brightens the lit
|
|
1319
|
+
// corner until it merges into a white backdrop, 0.4 is invisible at
|
|
1320
|
+
// editor-card sizes, and the feared ring-plus-1px-boundary "doubled edge"
|
|
1321
|
+
// does not materialize at 0.6. Angle 120 (upper-left key light).
|
|
1322
|
+
// Thickness 0.5 — the prototype-B depth pair unchanged; 0.7 pushed the
|
|
1323
|
+
// lower inner shadow into a visible dark band across the card bottom.
|
|
1324
|
+
treatment: {
|
|
1325
|
+
backdropBlurPx: 14,
|
|
1326
|
+
backdropSaturation: 1.3,
|
|
1327
|
+
backdropBrightness: 0.5,
|
|
1328
|
+
edgeSpecular: 0.6,
|
|
1329
|
+
specularAngleDeg: 120,
|
|
1330
|
+
thickness: 0.5,
|
|
1331
|
+
},
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Measured-fallback built-in (Glass-UI feasibility report §2.5, "Measured
|
|
1336
|
+
* frame-throughput impact"): the report's conclusion is that translucency
|
|
1337
|
+
* itself is frame-free but the blur-bearing `backdrop-filter` is not, so a
|
|
1338
|
+
* no-filter treatment must ship as a first-class fallback — not just a
|
|
1339
|
+
* theoretical escape hatch — for hardware where the backdrop filter is
|
|
1340
|
+
* unaffordable. Derived from `glassDarkEditorTheme` with NO `treatment` key
|
|
1341
|
+
* (so `editorThemeVariables` emits `--vgai-surface-backdrop-filter: none`)
|
|
1342
|
+
* and surface alphas raised to compensate for the legibility that blur would
|
|
1343
|
+
* otherwise have provided: panel 0.62->0.78, chrome 0.68->0.82, raised
|
|
1344
|
+
* 0.74->0.86, inset 0.70->0.82; shell/overlay are unchanged since they were
|
|
1345
|
+
* already high-alpha. Values are decided by the report; do not retune here.
|
|
1346
|
+
*/
|
|
1347
|
+
export const glassDarkLiteEditorTheme: EditorTheme = (() => {
|
|
1348
|
+
// Destructure `treatment` out rather than setting it to `undefined` —
|
|
1349
|
+
// `exactOptionalPropertyTypes` treats those as different, and the whole
|
|
1350
|
+
// point of this theme is that the key is ABSENT (matching every other
|
|
1351
|
+
// no-blur theme), not present-with-undefined.
|
|
1352
|
+
const { treatment: _treatment, ...glassDarkWithoutTreatment } = glassDarkEditorTheme;
|
|
1353
|
+
return {
|
|
1354
|
+
...glassDarkWithoutTreatment,
|
|
1355
|
+
id: 'glass-dark-lite',
|
|
1356
|
+
appearance: { material: 'glass', transparency: 'reduced' },
|
|
1357
|
+
color: {
|
|
1358
|
+
...glassDarkEditorTheme.color,
|
|
1359
|
+
// U6.5 F8: the reduced-transparency tier is SOLID, not merely
|
|
1360
|
+
// higher-alpha — the audit (shot 25) caught the scene reading through
|
|
1361
|
+
// the former translucent fills, defeating the accessibility request
|
|
1362
|
+
// outright ("the ladder says lite → solid"). Values are the previous
|
|
1363
|
+
// translucent tints composited over the shell, so the hue family is
|
|
1364
|
+
// unchanged; only the see-through is gone.
|
|
1365
|
+
surface: {
|
|
1366
|
+
shell: '#1a1a1a',
|
|
1367
|
+
panel: '#222222',
|
|
1368
|
+
chrome: '#292929',
|
|
1369
|
+
raised: '#303030',
|
|
1370
|
+
inset: '#1d1d1d',
|
|
1371
|
+
overlay: '#202020',
|
|
1372
|
+
},
|
|
1373
|
+
// No treatment ⇒ no scrim blur; the dim must carry modal separation
|
|
1374
|
+
// alone, so this fallback keeps the pre-P6-U6 heavy scrim. (The scrim
|
|
1375
|
+
// stays translucent by function — dimming IS its job.)
|
|
1376
|
+
scrim: GRAPHITE_SCRIM,
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
})();
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* REAL liquid glass (owner direction reset, report §2.31; material proven in
|
|
1383
|
+
* §2.32's demo — `docs/assets/glass-ui-feasibility/liquid-glass-real/`).
|
|
1384
|
+
* The surface has NO background fill of its own: panel is a 6% white
|
|
1385
|
+
* "material presence" whisper (never a dark fill — the owner rejected the
|
|
1386
|
+
* tinted-frost model outright: "Real glass morphism has no background color
|
|
1387
|
+
* and instead morphs the colors behind it"). The material reads as glass
|
|
1388
|
+
* because the backdrop's colors BEND at the bezel and keep displacing as
|
|
1389
|
+
* optical depth increases through the body (`refractionBezelPx` /
|
|
1390
|
+
* `refractionThickness`, painted by `components/glass-refraction.ts`). Frost
|
|
1391
|
+
* 1px + saturate 1.15 are carried by `backdropBlurPx` /
|
|
1392
|
+
* `backdropSaturation` so the non-Chromium plain-list fallback and the SVG
|
|
1393
|
+
* chain cannot drift. Treatment numbers were strengthened after the owner
|
|
1394
|
+
* visual review: bezel 56, thickness 1.25, frost 1px, saturation 1.15, tint
|
|
1395
|
+
* 6%. Content is near-white with a subtle
|
|
1396
|
+
* text shadow (the §2.32 legibility finding for a predominantly dark
|
|
1397
|
+
* viewport; adaptive palette is a later unit). No `backdropBrightness`:
|
|
1398
|
+
* darkening the backdrop was part of the rejected frost model.
|
|
1399
|
+
*/
|
|
1400
|
+
export const liquidGlassEditorTheme: EditorTheme = {
|
|
1401
|
+
id: 'liquid-glass',
|
|
1402
|
+
appearance: { material: 'glass', transparency: 'standard' },
|
|
1403
|
+
color: {
|
|
1404
|
+
surface: {
|
|
1405
|
+
shell: GLASS_MATERIAL.color.surface.shell,
|
|
1406
|
+
// THE glass surface — 6% white, the §2.32 "material presence" whisper.
|
|
1407
|
+
panel: GLASS_MATERIAL.color.surface.panel,
|
|
1408
|
+
chrome: GLASS_MATERIAL.color.surface.chrome,
|
|
1409
|
+
raised: GLASS_MATERIAL.color.surface.raised,
|
|
1410
|
+
// Input wells recess by shading, not by an opaque slab: a control
|
|
1411
|
+
// fill, not a panel surface (the §2.31 ban is on surface fills).
|
|
1412
|
+
inset: GLASS_MATERIAL.color.surface.inset,
|
|
1413
|
+
overlay: GLASS_MATERIAL.color.surface.overlay,
|
|
1414
|
+
},
|
|
1415
|
+
boundary: GLASS_MATERIAL.color.boundary,
|
|
1416
|
+
content: {
|
|
1417
|
+
primary: '#f2f6fb',
|
|
1418
|
+
// Dark-side ramp polish (P3, owner directive #3): muted/dim tuned FOR
|
|
1419
|
+
// the clear glass surface rather than inherited from the opaque-panel
|
|
1420
|
+
// ramp — over live scene content the old 0.72/0.55 alphas dropped
|
|
1421
|
+
// secondary text below comfortable legibility the moment the backdrop
|
|
1422
|
+
// carried any detail. Raised presence, same near-white family
|
|
1423
|
+
// (measured on the rts mid-green worst case: muted 0.8 read 3.6:1;
|
|
1424
|
+
// 0.85 is the highest alpha that still reads as a distinct tier).
|
|
1425
|
+
muted: 'rgba(242,246,251,0.85)',
|
|
1426
|
+
dim: 'rgba(242,246,251,0.68)',
|
|
1427
|
+
onAccent: '#0b1526',
|
|
1428
|
+
onBright: {
|
|
1429
|
+
primary: '#1a2330',
|
|
1430
|
+
muted: 'rgba(26,35,48,0.82)',
|
|
1431
|
+
dim: 'rgba(26,35,48,0.74)',
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
// Legacy resolved-theme export kept neutral too. The canonical Glass
|
|
1435
|
+
// material itself cannot carry an accent; hue comes from a palette.
|
|
1436
|
+
accent: {
|
|
1437
|
+
default: GRAPHITE_NEUTRAL_ACCENT,
|
|
1438
|
+
muted: GRAPHITE_NEUTRAL_ACCENT_MUTED,
|
|
1439
|
+
},
|
|
1440
|
+
semantic: {
|
|
1441
|
+
danger: '#ff7b86',
|
|
1442
|
+
dangerMuted: 'rgba(255,123,134,0.22)',
|
|
1443
|
+
dangerFaint: 'rgba(255,123,134,0.10)',
|
|
1444
|
+
warning: '#ffd35f',
|
|
1445
|
+
warningMuted: 'rgba(255,211,95,0.18)',
|
|
1446
|
+
success: '#58e078',
|
|
1447
|
+
successMuted: 'rgba(88,224,120,0.20)',
|
|
1448
|
+
dynamic: '#ff8fd8',
|
|
1449
|
+
dynamicMuted: 'rgba(255,143,216,0.14)',
|
|
1450
|
+
// Brighter sibling of the graphite cyan, matching this theme's lifted
|
|
1451
|
+
// semantic family (its danger/warning/success are all brighter too).
|
|
1452
|
+
instance: '#3ad2ea',
|
|
1453
|
+
},
|
|
1454
|
+
neutralOverlay: GLASS_MATERIAL.color.neutralOverlay,
|
|
1455
|
+
// Lighter dim + scrim blur (`--vgai-scrim-backdrop-filter`) — the macOS
|
|
1456
|
+
// modal read, P6-U6 owner taste decision 3; lite pins the heavier dim.
|
|
1457
|
+
scrim: GLASS_MATERIAL.color.scrim,
|
|
1458
|
+
},
|
|
1459
|
+
typography: { sans: GRAPHITE_FONT_SANS, mono: GRAPHITE_FONT_MONO },
|
|
1460
|
+
// Rounder shapes than graphite: the lens corner IS the material — a
|
|
1461
|
+
// larger card radius gives the bezel band a visible curve to bend around
|
|
1462
|
+
// (the demo's card is 24px; 14px keeps editor density workable).
|
|
1463
|
+
// `full` must be a px value, never 50%: restyle2 U0 killed the 50% oval
|
|
1464
|
+
// token (50% renders an ellipse on any non-square element) and pill
|
|
1465
|
+
// consumers are specified against true half-circle end caps.
|
|
1466
|
+
shape: GLASS_MATERIAL.shape,
|
|
1467
|
+
// Soft, deep drop shadows (the demo's `.glass` stack): a clear surface
|
|
1468
|
+
// needs shadow — not fill — to read as "above" the scene.
|
|
1469
|
+
elevation: GLASS_MATERIAL.elevation,
|
|
1470
|
+
treatment: GLASS_MATERIAL.treatment,
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Liquid-glass degrade tier (reduced transparency / no-treatment
|
|
1475
|
+
* environments), following the `glass-dark-lite` precedent: the SAME
|
|
1476
|
+
* palette family with NO `treatment` key (so
|
|
1477
|
+
* `--vgai-surface-backdrop-filter` emits `none` and
|
|
1478
|
+
* `--vgai-card-refraction-thickness` emits `0`) and — U6.5 F8 — SOLID
|
|
1479
|
+
* fills: the audit (shot 25) caught the scene reading through the former
|
|
1480
|
+
* `rgba(255,255,255,0.14)` card fills with no backdrop-filter to earn the
|
|
1481
|
+
* translucency, defeating the reduced-transparency request outright ("the
|
|
1482
|
+
* ladder says lite → solid"). Each value is the previous light tint
|
|
1483
|
+
* composited over the #12161f shell (panel 0.14, chrome 0.18, raised 0.24,
|
|
1484
|
+
* overlay 0.16; inset's dark wash over the solid panel), so the lifted
|
|
1485
|
+
* blue-gray family reads the same — opaque.
|
|
1486
|
+
*/
|
|
1487
|
+
export const liquidGlassLiteEditorTheme: EditorTheme = (() => {
|
|
1488
|
+
// Destructure `treatment` out rather than setting it to `undefined` —
|
|
1489
|
+
// `exactOptionalPropertyTypes` treats those as different, and the whole
|
|
1490
|
+
// point of this theme is that the key is ABSENT (same idiom as
|
|
1491
|
+
// `glassDarkLiteEditorTheme`).
|
|
1492
|
+
const { treatment: _treatment, ...liquidGlassWithoutTreatment } = liquidGlassEditorTheme;
|
|
1493
|
+
return {
|
|
1494
|
+
...liquidGlassWithoutTreatment,
|
|
1495
|
+
id: 'liquid-glass-lite',
|
|
1496
|
+
appearance: { material: 'glass', transparency: 'reduced' },
|
|
1497
|
+
color: {
|
|
1498
|
+
...liquidGlassEditorTheme.color,
|
|
1499
|
+
surface: GLASS_MATERIAL.reducedTransparency.surface,
|
|
1500
|
+
// No treatment ⇒ no scrim blur; keep the pre-P6-U6 heavier dim so
|
|
1501
|
+
// modal separation survives the degrade. (Scrims stay translucent by
|
|
1502
|
+
// function — dimming IS their job.)
|
|
1503
|
+
scrim: GLASS_MATERIAL.reducedTransparency.scrim,
|
|
1504
|
+
},
|
|
1505
|
+
};
|
|
1506
|
+
})();
|
|
1507
|
+
|
|
1508
|
+
export const editorThemes = {
|
|
1509
|
+
'graphite-dark': graphiteDarkEditorTheme,
|
|
1510
|
+
'graphite-neutral': graphiteNeutralEditorTheme,
|
|
1511
|
+
'midnight-high-contrast': midnightHighContrastEditorTheme,
|
|
1512
|
+
'glass-dark': glassDarkEditorTheme,
|
|
1513
|
+
'glass-dark-lite': glassDarkLiteEditorTheme,
|
|
1514
|
+
'liquid-glass': liquidGlassEditorTheme,
|
|
1515
|
+
'liquid-glass-lite': liquidGlassLiteEditorTheme,
|
|
1516
|
+
} as const satisfies Record<string, EditorTheme>;
|
|
1517
|
+
|
|
1518
|
+
export type EditorThemeId = keyof typeof editorThemes;
|
|
1519
|
+
|
|
1520
|
+
export function isEditorThemeId(value: unknown): value is EditorThemeId {
|
|
1521
|
+
return typeof value === 'string' && Object.hasOwn(editorThemes, value);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/** Semantic CSS-variable references for rendered editor chrome. */
|
|
1525
|
+
export const themeVars = {
|
|
1526
|
+
surface: {
|
|
1527
|
+
shell: 'var(--vgai-surface-shell)',
|
|
1528
|
+
panel: 'var(--vgai-surface-panel)',
|
|
1529
|
+
chrome: 'var(--vgai-surface-chrome)',
|
|
1530
|
+
raised: 'var(--vgai-surface-raised)',
|
|
1531
|
+
inset: 'var(--vgai-surface-inset)',
|
|
1532
|
+
overlay: 'var(--vgai-surface-overlay)',
|
|
1533
|
+
},
|
|
1534
|
+
boundary: {
|
|
1535
|
+
default: 'var(--vgai-boundary-default)',
|
|
1536
|
+
strong: 'var(--vgai-boundary-strong)',
|
|
1537
|
+
},
|
|
1538
|
+
content: {
|
|
1539
|
+
primary: 'var(--vgai-content-primary)',
|
|
1540
|
+
muted: 'var(--vgai-content-muted)',
|
|
1541
|
+
dim: 'var(--vgai-content-dim)',
|
|
1542
|
+
onAccent: 'var(--vgai-content-on-accent)',
|
|
1543
|
+
/** The ACTIVE subject's own ink; EMPTY under a palette that names none,
|
|
1544
|
+
* so a site reading it must write its own fallback. */
|
|
1545
|
+
active: 'var(--vgai-content-active)',
|
|
1546
|
+
/** The SELECTED-but-not-active subject's ink; EMPTY the same way, and the
|
|
1547
|
+
* emitter already falls it back to {@link active}, so a palette naming
|
|
1548
|
+
* only an active ink resolves this to that. (The fallback is in the
|
|
1549
|
+
* EMITTER and not in a `var(…, …)` default, because every variable is
|
|
1550
|
+
* always emitted — as `''` when absent — so a CSS fallback would never
|
|
1551
|
+
* fire.) */
|
|
1552
|
+
selected: 'var(--vgai-content-selected)',
|
|
1553
|
+
},
|
|
1554
|
+
accent: {
|
|
1555
|
+
default: 'var(--vgai-accent)',
|
|
1556
|
+
muted: 'var(--vgai-accent-muted)',
|
|
1557
|
+
},
|
|
1558
|
+
selection: {
|
|
1559
|
+
background: 'var(--vgai-selection-bg)',
|
|
1560
|
+
border: 'var(--vgai-selection-border)',
|
|
1561
|
+
indicator: 'var(--vgai-selection-indicator)',
|
|
1562
|
+
},
|
|
1563
|
+
semantic: {
|
|
1564
|
+
danger: 'var(--vgai-danger)',
|
|
1565
|
+
dangerMuted: 'var(--vgai-danger-muted)',
|
|
1566
|
+
dangerFaint: 'var(--vgai-danger-faint)',
|
|
1567
|
+
warning: 'var(--vgai-warn)',
|
|
1568
|
+
warningMuted: 'var(--vgai-warn-muted)',
|
|
1569
|
+
success: 'var(--vgai-success)',
|
|
1570
|
+
successMuted: 'var(--vgai-success-muted)',
|
|
1571
|
+
dynamic: 'var(--vgai-dynamic)',
|
|
1572
|
+
dynamicMuted: 'var(--vgai-dynamic-muted)',
|
|
1573
|
+
instance: 'var(--vgai-instance)',
|
|
1574
|
+
},
|
|
1575
|
+
neutralOverlay: {
|
|
1576
|
+
hover: 'var(--vgai-neutral-hover)',
|
|
1577
|
+
active: 'var(--vgai-neutral-active)',
|
|
1578
|
+
},
|
|
1579
|
+
scrim: 'var(--vgai-scrim)',
|
|
1580
|
+
typography: {
|
|
1581
|
+
sans: 'var(--vgai-font-sans)',
|
|
1582
|
+
mono: 'var(--vgai-font-mono)',
|
|
1583
|
+
},
|
|
1584
|
+
shape: {
|
|
1585
|
+
small: 'var(--vgai-radius-sm)',
|
|
1586
|
+
medium: 'var(--vgai-radius-md)',
|
|
1587
|
+
large: 'var(--vgai-radius-lg)',
|
|
1588
|
+
full: 'var(--vgai-radius-full)',
|
|
1589
|
+
},
|
|
1590
|
+
elevation: {
|
|
1591
|
+
small: 'var(--vgai-shadow-sm)',
|
|
1592
|
+
medium: 'var(--vgai-shadow-md)',
|
|
1593
|
+
large: 'var(--vgai-shadow-lg)',
|
|
1594
|
+
},
|
|
1595
|
+
focusRing: 'var(--vgai-focus-ring)',
|
|
1596
|
+
} as const;
|
|
1597
|
+
|
|
1598
|
+
export type EditorThemeVariable = `--vgai-${string}`;
|
|
1599
|
+
|
|
1600
|
+
/** Theme-derived native-select artwork; data URIs cannot inherit CSS `color`. */
|
|
1601
|
+
function chevronDataUri(color: string): string {
|
|
1602
|
+
const svg =
|
|
1603
|
+
`<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'>` +
|
|
1604
|
+
`<path d='M1 1L5 5L9 1' stroke='${color}' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/>` +
|
|
1605
|
+
`</svg>`;
|
|
1606
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Compose the treatment vocabulary's `backdrop-filter` value: `blur(..px)
|
|
1611
|
+
* saturate(..)` when either axis is set, else `'none'`. Treatment-less
|
|
1612
|
+
* themes (graphite, classic, and the `-lite` reduced-transparency tiers)
|
|
1613
|
+
* emit `'none'` — no `[data-vgai-theme=…]` CSS special-casing anywhere;
|
|
1614
|
+
* this function is the single place a theme's treatment becomes a real
|
|
1615
|
+
* filter string.
|
|
1616
|
+
*/
|
|
1617
|
+
function backdropFilterValue(treatment: EditorTheme['treatment']): string {
|
|
1618
|
+
if (!treatment) return 'none';
|
|
1619
|
+
const parts: string[] = [];
|
|
1620
|
+
if (treatment.backdropBlurPx) parts.push(`blur(${treatment.backdropBlurPx}px)`);
|
|
1621
|
+
if (treatment.backdropSaturation !== undefined && treatment.backdropSaturation !== 1) {
|
|
1622
|
+
parts.push(`saturate(${treatment.backdropSaturation})`);
|
|
1623
|
+
}
|
|
1624
|
+
if (treatment.backdropBrightness !== undefined && treatment.backdropBrightness !== 1) {
|
|
1625
|
+
parts.push(`brightness(${treatment.backdropBrightness})`);
|
|
1626
|
+
}
|
|
1627
|
+
return parts.length > 0 ? parts.join(' ') : 'none';
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Structural header surfaces (`--vgai-surface-sticky`/`-panel`). Glass
|
|
1632
|
+
* themes leave title/table rails transparent: the owner-reopened acceptance
|
|
1633
|
+
* explicitly reserves frost for significant text floors, not headers.
|
|
1634
|
+
* Classic themes retain the old occluding layered stack. Reduced Glass keeps
|
|
1635
|
+
* Glass morphology even though it intentionally has no optical treatment.
|
|
1636
|
+
*/
|
|
1637
|
+
function stickySurfaceValue(
|
|
1638
|
+
token: '--vgai-surface-chrome' | '--vgai-surface-panel',
|
|
1639
|
+
glassMaterial: boolean,
|
|
1640
|
+
): string {
|
|
1641
|
+
if (glassMaterial) return 'transparent';
|
|
1642
|
+
return (
|
|
1643
|
+
`linear-gradient(var(${token}), var(${token})), ` +
|
|
1644
|
+
`linear-gradient(var(--vgai-surface-shell), var(--vgai-surface-shell))`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Content-zone frost (`--vgai-content-frost-filter`/`-bg`, §2.31 P2 owner
|
|
1650
|
+
* amendment): the local frosted-blur layer behind text-dense interior zones.
|
|
1651
|
+
* Filter reuses the theme's own saturation term so the frost and the plain
|
|
1652
|
+
* surface treatment cannot drift; bg is a fixed light tint — NEVER a dark
|
|
1653
|
+
* fill (the rejected tinted-frost model). `none`/`transparent` when the
|
|
1654
|
+
* axis is 0/absent, so non-frost themes paint byte-identically.
|
|
1655
|
+
*/
|
|
1656
|
+
function contentFrostFilterValue(treatment: EditorTheme['treatment']): string {
|
|
1657
|
+
const blurPx = treatment?.contentFrostBlurPx ?? 0;
|
|
1658
|
+
if (blurPx <= 0) return 'none';
|
|
1659
|
+
const parts = [`blur(${blurPx}px)`];
|
|
1660
|
+
if (treatment?.backdropSaturation !== undefined && treatment.backdropSaturation !== 1) {
|
|
1661
|
+
parts.push(`saturate(${treatment.backdropSaturation})`);
|
|
1662
|
+
}
|
|
1663
|
+
return parts.join(' ');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function contentFrostBgValue(treatment: EditorTheme['treatment']): string {
|
|
1667
|
+
const blurPx = treatment?.contentFrostBlurPx ?? 0;
|
|
1668
|
+
return blurPx > 0 ? 'rgba(255,255,255,0.10)' : 'transparent';
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/** Text-entry wells are thin overlays within a glass surface, never nested
|
|
1672
|
+
* lenses. `currentColor` makes the translucent wash and rim follow the
|
|
1673
|
+
* adaptive light/dark ink flip; Classic retains the established opaque inset
|
|
1674
|
+
* recipe. Reduced Glass is safe because its parent surfaces are solid. */
|
|
1675
|
+
function inputSurfaceValue(glassMaterial: boolean): string {
|
|
1676
|
+
// A text field is its own WIDGET CLASS (Blender's `wcol_text`, measured
|
|
1677
|
+
// #1c1c1c), which is why this reads `--vgai-widget-field` rather than
|
|
1678
|
+
// `--vgai-surface-inset` directly — that token resolves to `surface.inset`
|
|
1679
|
+
// for every palette that does not name the class, so this is the same value
|
|
1680
|
+
// it has always been.
|
|
1681
|
+
return glassMaterial
|
|
1682
|
+
? 'color-mix(in srgb, currentColor 7%, transparent)'
|
|
1683
|
+
: 'var(--vgai-widget-field)';
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function inputBorderValue(glassMaterial: boolean): string {
|
|
1687
|
+
return glassMaterial
|
|
1688
|
+
? 'color-mix(in srgb, currentColor 26%, transparent)'
|
|
1689
|
+
: 'var(--vgai-boundary-default)';
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function inputShadowValue(glassMaterial: boolean, embossShadow: string): string {
|
|
1693
|
+
// A text field is a WIDGET, so it takes the widget emboss
|
|
1694
|
+
// (`EditorTheme.color.widget.emboss`) like every other one. It composes
|
|
1695
|
+
// here rather than in the four stylesheet rules that read this token
|
|
1696
|
+
// because `none` is not a legal member of a `box-shadow` LIST: a rule
|
|
1697
|
+
// written `var(--vgai-widget-emboss-shadow), var(--vgai-input-shadow)`
|
|
1698
|
+
// would be invalid for every palette that names neither.
|
|
1699
|
+
const emboss = embossShadow === 'none' ? '' : embossShadow;
|
|
1700
|
+
const glass = glassMaterial
|
|
1701
|
+
? 'inset 0 1px 0 color-mix(in srgb, currentColor 14%, transparent), 0 5px 14px -12px color-mix(in srgb, currentColor 55%, transparent)'
|
|
1702
|
+
: '';
|
|
1703
|
+
const layers = [emboss, glass].filter(Boolean);
|
|
1704
|
+
return layers.length === 0 ? 'none' : layers.join(', ');
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Liquid Glass Tier-1 calibration anchors (report §2.24): the rgba/px
|
|
1709
|
+
* constants inside the two functions below ARE the committed prototype's
|
|
1710
|
+
* variant-B recipe (`docs/assets/glass-ui-feasibility/liquid-glass-proto/
|
|
1711
|
+
* index.html`, `.card.specular`), and the axes are normalized so
|
|
1712
|
+
* `edgeSpecular` 0.6 / `thickness` 0.5 reproduce that look exactly — the
|
|
1713
|
+
* values the owner reviewed. Everything here is inert data → CSS custom
|
|
1714
|
+
* properties; the pixels are painted by `workspace-dock.css`'s card-chrome
|
|
1715
|
+
* pseudo-elements, with NO added backdrop-filter and NO runtime JS.
|
|
1716
|
+
*/
|
|
1717
|
+
const SPECULAR_REFERENCE = 0.6;
|
|
1718
|
+
const THICKNESS_REFERENCE = 0.5;
|
|
1719
|
+
|
|
1720
|
+
/** White at a prototype-B-anchored alpha: `base` × (edgeSpecular ÷ 0.6), clamped to 1. */
|
|
1721
|
+
function specularWhite(base: number, edgeSpecular: number): string {
|
|
1722
|
+
const alpha = Math.min(1, Number(((base * edgeSpecular) / SPECULAR_REFERENCE).toFixed(3)));
|
|
1723
|
+
return `rgba(255,255,255,${alpha})`;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* The 1.5px edge ring's gradient (`--vgai-card-specular-ring`): brightest at
|
|
1728
|
+
* the light-facing corner, falling to a faint trace on the far side, so the
|
|
1729
|
+
* rim reads as light catching a polished edge rather than a drawn border.
|
|
1730
|
+
* `'none'` when the treatment carries no specular — the ring pseudo-element
|
|
1731
|
+
* then paints nothing, which is the whole degrade story.
|
|
1732
|
+
*/
|
|
1733
|
+
function specularRingValue(treatment: EditorTheme['treatment']): string {
|
|
1734
|
+
const intensity = treatment?.edgeSpecular ?? 0;
|
|
1735
|
+
if (intensity <= 0) return 'none';
|
|
1736
|
+
const angle = treatment?.specularAngleDeg ?? 120;
|
|
1737
|
+
const white = (base: number) => specularWhite(base, intensity);
|
|
1738
|
+
return (
|
|
1739
|
+
`linear-gradient(${angle}deg, ${white(0.95)} 0%, ${white(0.38)} 16%, ` +
|
|
1740
|
+
`${white(0.16)} 40%, ${white(0.14)} 60%, ${white(0.45)} 84%, ${white(0.85)} 100%)`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* The interior sheen wash (`--vgai-card-specular-sheen`): a soft diagonal
|
|
1746
|
+
* gradient from the lit corner across ~40% of the card, painted BELOW card
|
|
1747
|
+
* content (negative-z pseudo) so text never sits on a brightened band edge.
|
|
1748
|
+
*/
|
|
1749
|
+
function specularSheenValue(treatment: EditorTheme['treatment']): string {
|
|
1750
|
+
const intensity = treatment?.edgeSpecular ?? 0;
|
|
1751
|
+
if (intensity <= 0) return 'none';
|
|
1752
|
+
const angle = treatment?.specularAngleDeg ?? 120;
|
|
1753
|
+
const white = (base: number) => specularWhite(base, intensity);
|
|
1754
|
+
return `linear-gradient(${angle}deg, ${white(0.16)} 0%, ${white(0.05)} 24%, transparent 40%)`;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* The card's full box-shadow stack (`--vgai-card-specular-shadow`):
|
|
1759
|
+
* `edgeSpecular` contributes the 1px inset rim catches; `thickness` scales
|
|
1760
|
+
* the glass-depth pair (upper inner glow + lower inner shadow) plus the
|
|
1761
|
+
* matching drop shadow, replacing the theme's stock elevation. Either axis
|
|
1762
|
+
* absent → its terms drop out; both absent → exactly the pre-existing
|
|
1763
|
+
* `var(--vgai-shadow-lg)` card shadow, so treatment-less themes and the
|
|
1764
|
+
* reduced-transparency fallback paint byte-identical cards.
|
|
1765
|
+
*/
|
|
1766
|
+
function specularShadowValue(treatment: EditorTheme['treatment']): string {
|
|
1767
|
+
const intensity = treatment?.edgeSpecular ?? 0;
|
|
1768
|
+
const thickness = treatment?.thickness ?? 0;
|
|
1769
|
+
const parts: string[] = [];
|
|
1770
|
+
if (intensity > 0) {
|
|
1771
|
+
parts.push(`inset 0 1px 1px ${specularWhite(0.28, intensity)}`);
|
|
1772
|
+
parts.push(`inset 0 -1px 1px ${specularWhite(0.1, intensity)}`);
|
|
1773
|
+
}
|
|
1774
|
+
if (thickness > 0) {
|
|
1775
|
+
const depth = (px: number) =>
|
|
1776
|
+
`${Number(((px * thickness) / THICKNESS_REFERENCE).toFixed(1))}px`;
|
|
1777
|
+
parts.push(`inset 0 ${depth(12)} ${depth(24)} ${depth(-14)} rgba(255,255,255,0.35)`);
|
|
1778
|
+
parts.push(`inset 0 ${depth(-14)} ${depth(28)} ${depth(-18)} rgba(0,0,0,0.55)`);
|
|
1779
|
+
parts.push(`0 ${depth(18)} ${depth(40)} ${depth(-18)} rgba(0,0,0,0.55)`);
|
|
1780
|
+
} else {
|
|
1781
|
+
parts.push('var(--vgai-shadow-lg)');
|
|
1782
|
+
}
|
|
1783
|
+
return parts.join(', ');
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Floating glass-island chrome (`--vgai-island-*`, P6 glass-native chrome
|
|
1788
|
+
* U1/U2): free-standing chrome that floats over the canvas —
|
|
1789
|
+
* `.vgai-floating-toolbar` (the viewport toolstrip and its siblings), and
|
|
1790
|
+
* the header/footer clusters in later P6 units. A theme WITH a glass
|
|
1791
|
+
* treatment paints islands as REAL glass, the same material as cards: the
|
|
1792
|
+
* panel-whisper fill (never an opaque or color-mix slab), the theme's own
|
|
1793
|
+
* plain backdrop list (upgraded per-island to the url() refraction chain by
|
|
1794
|
+
* `components/glass-refraction.ts` where capable — the var here is the
|
|
1795
|
+
* degrade fallback that paints when refraction can't), the P6 demo
|
|
1796
|
+
* toolbar's 17px radius, the card border (the specular ring supplies the
|
|
1797
|
+
* rim), and the deep drop shadow a clear surface needs to read as "above"
|
|
1798
|
+
* the scene. A Classic theme emits EXACTLY the pre-P6
|
|
1799
|
+
* floating-toolbar recipe — 90% panel color-mix over transparent, blur(8px),
|
|
1800
|
+
* strong boundary, small shadow, the medium radius token — so
|
|
1801
|
+
* graphite/classic paint byte-identically to the pre-island editor. Reduced
|
|
1802
|
+
* Glass keeps the Glass shape/boundary/shadow while its filter becomes none.
|
|
1803
|
+
*/
|
|
1804
|
+
/** Resolve material identity without making treatment presence do two jobs. */
|
|
1805
|
+
export function usesGlassMaterial(theme: EditorTheme): boolean {
|
|
1806
|
+
if (theme.appearance) return theme.appearance.material === 'glass';
|
|
1807
|
+
// Compatibility for standalone pre-axis themes, which declare no
|
|
1808
|
+
// `appearance` axis at all.
|
|
1809
|
+
return theme.treatment !== undefined;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function islandSurfaceValue(theme: EditorTheme): string {
|
|
1813
|
+
return usesGlassMaterial(theme)
|
|
1814
|
+
? 'var(--vgai-surface-panel)'
|
|
1815
|
+
: 'color-mix(in srgb, var(--vgai-surface-panel) 90%, transparent)';
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function islandBackdropFilterValue(theme: EditorTheme): string {
|
|
1819
|
+
// Referencing the surface var (not re-deriving from treatment data) means
|
|
1820
|
+
// the island fallback and the card plain list cannot drift — and the
|
|
1821
|
+
// refraction manager parses that same var for its in-chain frost/sat.
|
|
1822
|
+
if (usesGlassMaterial(theme)) {
|
|
1823
|
+
return theme.treatment ? 'var(--vgai-surface-backdrop-filter)' : 'none';
|
|
1824
|
+
}
|
|
1825
|
+
return 'blur(8px)';
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function islandRadiusValue(glassMaterial: boolean): string {
|
|
1829
|
+
// 17px is the P6 reference demo toolbar's committed radius — a design
|
|
1830
|
+
// constant of the glass chrome, not a shape token.
|
|
1831
|
+
return glassMaterial ? '17px' : 'var(--vgai-radius-md)';
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function islandBorderColorValue(glassMaterial: boolean): string {
|
|
1835
|
+
return glassMaterial ? 'var(--vgai-boundary-default)' : 'var(--vgai-boundary-strong)';
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function islandShadowValue(glassMaterial: boolean): string {
|
|
1839
|
+
return glassMaterial ? 'var(--vgai-shadow-lg)' : 'var(--vgai-shadow-sm)';
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function accentAlphaValue(theme: EditorTheme, alpha: number): string | null {
|
|
1843
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(theme.color.accent.default);
|
|
1844
|
+
if (!m) return null;
|
|
1845
|
+
const [r, g, b] = [m[1], m[2], m[3]].map((c) => Number.parseInt(c as string, 16));
|
|
1846
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* Selection is material state, not brand/action state. Glass and its
|
|
1851
|
+
* reduced-transparency descendants use neutral currentColor mixes so the
|
|
1852
|
+
* selection adapts with Liquid Glass's bright/dark content ramp. Opaque
|
|
1853
|
+
* Classic themes retain their established accent selection treatment.
|
|
1854
|
+
*/
|
|
1855
|
+
function usesNeutralSelection(theme: EditorTheme): boolean {
|
|
1856
|
+
return usesGlassMaterial(theme);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function selectionBackgroundValue(theme: EditorTheme): string {
|
|
1860
|
+
return usesNeutralSelection(theme) ? 'var(--vgai-neutral-active)' : 'var(--vgai-accent-muted)';
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* Whether a palette colour PAINTS ON ITS OWN, or is a wash that lets the
|
|
1865
|
+
* surface beneath show through.
|
|
1866
|
+
*
|
|
1867
|
+
* The pressed ink depends on nothing else: "white on the accent" only reads
|
|
1868
|
+
* when the accent is actually there.
|
|
1869
|
+
*/
|
|
1870
|
+
function isOpaqueColor(value: string): boolean {
|
|
1871
|
+
const v = value.trim().toLowerCase();
|
|
1872
|
+
if (v.startsWith('#')) {
|
|
1873
|
+
// #rgba and #rrggbbaa carry the alpha in the last 1 or 2 digits.
|
|
1874
|
+
if (v.length === 5) return v.endsWith('f');
|
|
1875
|
+
if (v.length === 9) return v.endsWith('ff');
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
const fn = /^rgba?\(([^)]*)\)$/.exec(v) ?? /^hsla?\(([^)]*)\)$/.exec(v);
|
|
1879
|
+
if (fn) {
|
|
1880
|
+
const alpha = (fn[1] ?? '').split(/[,/]/)[3];
|
|
1881
|
+
return alpha === undefined || Number.parseFloat(alpha) >= 1;
|
|
1882
|
+
}
|
|
1883
|
+
// `transparent`, and anything that mixes toward it, is not a paint.
|
|
1884
|
+
return v !== 'transparent' && !v.includes('transparent');
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* The ink that reads ON `--vgai-selection-bg` — and it is DERIVED, because
|
|
1889
|
+
* the fill it has to read on is different per skin.
|
|
1890
|
+
*
|
|
1891
|
+
* Blender's pressed fill is the solid accent (`accent.muted` = `#4772b3`),
|
|
1892
|
+
* so its ink is the palette's on-accent white. Every other skin's pressed
|
|
1893
|
+
* fill is a WASH (`rgba(87,158,255,0.15)` and siblings) over the ordinary
|
|
1894
|
+
* panel, so the ordinary ink is what reads there — and the on-accent ink,
|
|
1895
|
+
* which is near-BLACK in those palettes because their accent is light, is
|
|
1896
|
+
* invisible on it. Measured live through `editor.document.query`'s resolved
|
|
1897
|
+
* styles: Classic painted `rgb(16,24,32)` on `rgba(87,158,255,0.15)`, which
|
|
1898
|
+
* is how a pressed eye toggle became an unreadable black smudge while the
|
|
1899
|
+
* same control read white in Blender.
|
|
1900
|
+
*/
|
|
1901
|
+
function selectionInkValue(theme: EditorTheme): string {
|
|
1902
|
+
if (usesNeutralSelection(theme)) return 'var(--vgai-content-primary)';
|
|
1903
|
+
return isOpaqueColor(theme.color.accent.muted)
|
|
1904
|
+
? 'var(--vgai-content-on-accent)'
|
|
1905
|
+
: 'var(--vgai-content-primary)';
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function selectionBorderValue(theme: EditorTheme): string {
|
|
1909
|
+
return usesNeutralSelection(theme)
|
|
1910
|
+
? 'color-mix(in srgb, currentColor 30%, transparent)'
|
|
1911
|
+
: 'var(--vgai-accent)';
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function selectionIndicatorValue(theme: EditorTheme): string {
|
|
1915
|
+
return usesNeutralSelection(theme)
|
|
1916
|
+
? 'color-mix(in srgb, currentColor 76%, transparent)'
|
|
1917
|
+
: 'var(--vgai-accent)';
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Controls INSIDE glass chrome are concentric capsules (the HIG
|
|
1922
|
+
* concentricity rule: nested radii follow the container's pill), and the
|
|
1923
|
+
* ONE prominent action per toolbar is a TINTED-GLASS capsule — accent as a
|
|
1924
|
+
* translucent tint in the material, never an opaque painted slab. Opaque
|
|
1925
|
+
* themes keep the flat design's small radius and solid accent.
|
|
1926
|
+
*/
|
|
1927
|
+
function islandControlRadiusValue(glassMaterial: boolean): string {
|
|
1928
|
+
return glassMaterial ? 'var(--vgai-radius-full)' : 'var(--vgai-radius-sm)';
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function islandPrimaryBgValue(theme: EditorTheme): string {
|
|
1932
|
+
if (!usesGlassMaterial(theme) || !theme.treatment) return 'var(--vgai-accent)';
|
|
1933
|
+
return accentAlphaValue(theme, 0.82) ?? 'var(--vgai-accent)';
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* Islands wear a DIMMER specular ring than cards (owner design review,
|
|
1938
|
+
* spec "Design-review round" item 1): chrome pills sit close to content
|
|
1939
|
+
* and at card intensity the rim reads as a drawn border, not caught light.
|
|
1940
|
+
* 0.4× the card's edgeSpecular, same gradient profile. 'none' without a
|
|
1941
|
+
* specular treatment — the ring pseudo then paints nothing (the degrade).
|
|
1942
|
+
*/
|
|
1943
|
+
function islandSpecularRingValue(treatment: EditorTheme['treatment']): string {
|
|
1944
|
+
const intensity = treatment?.edgeSpecular ?? 0;
|
|
1945
|
+
if (intensity <= 0) return 'none';
|
|
1946
|
+
return specularRingValue({
|
|
1947
|
+
...(treatment as NonNullable<EditorTheme['treatment']>),
|
|
1948
|
+
edgeSpecular: intensity * 0.4,
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Faint ambient lift (`--vgai-ambient-lift`, P6-U6 taste decision 2): a
|
|
1954
|
+
* top-lit white wash — full alpha at the top edge, ~a third at the
|
|
1955
|
+
* midline, gone by the bottom — layered over the translucent color fill
|
|
1956
|
+
* (an extra background-image layer) on cards and islands, so clear glass
|
|
1957
|
+
* reads as a lit surface over a black void. The
|
|
1958
|
+
* 165° angle matches the specular key-light family (upper-left-ish);
|
|
1959
|
+
* geometry is a design constant, the treatment axis is intensity only.
|
|
1960
|
+
* 'none' without the axis — the extra background layer then paints
|
|
1961
|
+
* nothing and treatment-less themes stay byte-identical.
|
|
1962
|
+
*/
|
|
1963
|
+
function ambientLiftValue(treatment: EditorTheme['treatment']): string {
|
|
1964
|
+
const alpha = treatment?.ambientLiftOpacity ?? 0;
|
|
1965
|
+
if (alpha <= 0) return 'none';
|
|
1966
|
+
const top = Number(alpha.toFixed(3));
|
|
1967
|
+
const mid = Number((alpha * 0.35).toFixed(3));
|
|
1968
|
+
return `linear-gradient(165deg, rgba(255,255,255,${top}) 0%, rgba(255,255,255,${mid}) 55%, rgba(255,255,255,0) 100%)`;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Content-legibility text shadow (`--vgai-content-text-shadow`, §2.32):
|
|
1973
|
+
* `none` for every theme without the axis, so pre-existing themes paint
|
|
1974
|
+
* byte-identically; the liquid-glass value is the demo's proven
|
|
1975
|
+
* `0 1px 2px rgba(0,0,0,0.72)`.
|
|
1976
|
+
*/
|
|
1977
|
+
function textShadowFromOpacity(opacity: number): string {
|
|
1978
|
+
if (opacity <= 0) return 'none';
|
|
1979
|
+
return `0 1px 2px rgba(0,0,0,${Number(opacity.toFixed(3))})`;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function contentTextShadowValue(treatment: EditorTheme['treatment']): string {
|
|
1983
|
+
return textShadowFromOpacity(treatment?.textShadowOpacity ?? 0);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
/**
|
|
1987
|
+
* Adaptive bright-backdrop content ramp (P3): the `--vgai-content-*-on-bright`
|
|
1988
|
+
* values `theme.css` flips to under a measured `data-vgai-backdrop="bright"`
|
|
1989
|
+
* classification. Themes without adaptive material physics emit their normal
|
|
1990
|
+
* content values here — the values must be LITERALS (mirroring, not
|
|
1991
|
+
* `var(--vgai-content-…)` references), or the CSS flip
|
|
1992
|
+
* `--vgai-content-primary: var(--vgai-content-primary-on-bright)` would be a
|
|
1993
|
+
* self-referential cycle; mirroring makes the flip inert instead. The
|
|
1994
|
+
* bright-side text shadow defaults to `none` only when the axis exists
|
|
1995
|
+
* (dark ink needs no shadow); without the axis it mirrors the normal shadow.
|
|
1996
|
+
*/
|
|
1997
|
+
function contentOnBrightValues(theme: EditorTheme): {
|
|
1998
|
+
primary: string;
|
|
1999
|
+
muted: string;
|
|
2000
|
+
dim: string;
|
|
2001
|
+
textShadow: string;
|
|
2002
|
+
frostBg: string;
|
|
2003
|
+
} {
|
|
2004
|
+
if (!theme.treatment?.adaptiveContent) {
|
|
2005
|
+
return {
|
|
2006
|
+
primary: theme.color.content.primary,
|
|
2007
|
+
muted: theme.color.content.muted,
|
|
2008
|
+
dim: theme.color.content.dim,
|
|
2009
|
+
textShadow: contentTextShadowValue(theme.treatment),
|
|
2010
|
+
frostBg: contentFrostBgValue(theme.treatment),
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
return {
|
|
2014
|
+
primary: theme.color.content.onBright.primary,
|
|
2015
|
+
muted: theme.color.content.onBright.muted,
|
|
2016
|
+
dim: theme.color.content.onBright.dim,
|
|
2017
|
+
textShadow: textShadowFromOpacity(theme.treatment.brightTextShadowOpacity ?? 0),
|
|
2018
|
+
frostBg: theme.treatment.brightFrostBg ?? contentFrostBgValue(theme.treatment),
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* REGULAR Liquid Glass (menus / popovers / dialogs / palette / tooltips —
|
|
2024
|
+
* `.vgai-menu` / `.vgai-popover` / `.vgai-dialog` in theme.css; P3 overlay
|
|
2025
|
+
* chrome formalized as `--vgai-glass-regular-*` in P6-U6 per the owner's
|
|
2026
|
+
* taste decision): Apple's REGULAR variant is the "more solid" material —
|
|
2027
|
+
* adaptive, self-legible, used for transient chrome that must obscure
|
|
2028
|
+
* whatever it covers; Clear is the refractive card/island family, and the
|
|
2029
|
+
* two are never mixed within an element class. Transient surfaces are NOT
|
|
2030
|
+
* backdrop-classified (sampling them is overkill — they live for a click),
|
|
2031
|
+
* so the material is SELF-SUFFICIENT on both luminance sides:
|
|
2032
|
+
* - adaptive themes (liquid-glass): a raised light tint over a
|
|
2033
|
+
* strong frost, statically paired (in theme.css) with the `-on-bright`
|
|
2034
|
+
* dark ink ramp — legible over ANY backdrop, Apple's own light-material
|
|
2035
|
+
* menu answer (the P3 recipe, unchanged).
|
|
2036
|
+
* - treatment themes WITHOUT the axis (glass-dark): the SAME solidity on
|
|
2037
|
+
* the dark side — the theme's own chrome tone raised to high opacity
|
|
2038
|
+
* over the same strong frost, not the previous thin translucency.
|
|
2039
|
+
* - treatment-less themes mirror the plain chrome surface / `none`, so the
|
|
2040
|
+
* theme.css consumption is inert and they paint byte-identically.
|
|
2041
|
+
*/
|
|
2042
|
+
function glassRegularBgValue(theme: EditorTheme): string {
|
|
2043
|
+
if (!theme.treatment) return theme.color.surface.chrome;
|
|
2044
|
+
// Raised LIGHT tint (never a dark fill — §2.31): strong enough that the
|
|
2045
|
+
// dark ink ramp holds ≥4.5:1 even over a black backdrop after compositing.
|
|
2046
|
+
// U6.5 F7 — floored 0.62 → 0.9 (the dark-side floor, symmetric): Regular
|
|
2047
|
+
// menus must OBSCURE what they cover, and the 0.62 tint let underlying
|
|
2048
|
+
// chrome read through at near-full contrast (audit shot 03 — hierarchy
|
|
2049
|
+
// rows through the View menu; menus opened inside a header island are
|
|
2050
|
+
// backdrop-root-captured, so their frost blur cannot reach the page
|
|
2051
|
+
// behind and the fill alone must carry the occlusion).
|
|
2052
|
+
if (theme.treatment.adaptiveContent) return 'rgba(255,255,255,0.9)';
|
|
2053
|
+
// Dark-side Regular: the theme's chrome tone, alpha floored at 0.9 so the
|
|
2054
|
+
// material reads solid (colors it covers may glow through the frost, never
|
|
2055
|
+
// read through the fill).
|
|
2056
|
+
return withMinimumAlpha(theme.color.surface.chrome, 0.9);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/**
|
|
2060
|
+
* REGULAR Liquid Glass for persistent, text-heavy PANELS. This is the same
|
|
2061
|
+
* functional material as transient Regular glass, but it must retain enough
|
|
2062
|
+
* scene continuity to work as a permanent inspector/hierarchy surface rather
|
|
2063
|
+
* than an opaque menu. The base is the theme's faint panel presence; sampled
|
|
2064
|
+
* backdrop luminance then supplies a complementary, low-alpha correction.
|
|
2065
|
+
* Dark scenes are lifted and bright scenes are dimmed without replacing
|
|
2066
|
+
* either with a white card. Strong body frost removes high-frequency detail.
|
|
2067
|
+
*
|
|
2068
|
+
* Themes without the adaptive bright-side ramp retain their authored panel
|
|
2069
|
+
* surface unchanged. Plain themes also receive no filter, so opting a panel
|
|
2070
|
+
* into the class is inert for Classic and reduced-transparency modes.
|
|
2071
|
+
*/
|
|
2072
|
+
function glassRegularPanelBgValue(theme: EditorTheme): string {
|
|
2073
|
+
return theme.color.surface.panel;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/** Raise an `rgba(r,g,b,a)` color's alpha to at least `minAlpha`; any other
|
|
2077
|
+
* color syntax is already effectively solid and passes through unchanged. */
|
|
2078
|
+
function withMinimumAlpha(color: string, minAlpha: number): string {
|
|
2079
|
+
const m = /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9.]+)\s*\)$/.exec(color);
|
|
2080
|
+
if (!m) return color;
|
|
2081
|
+
const alpha = Math.max(Number(m[4]), minAlpha);
|
|
2082
|
+
return `rgba(${m[1]},${m[2]},${m[3]},${alpha})`;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function glassRegularBackdropFilterValue(treatment: EditorTheme['treatment']): string {
|
|
2086
|
+
if (!treatment) return 'none';
|
|
2087
|
+
// Double the content frost (floor 16px): a menu floats over UNFROSTED
|
|
2088
|
+
// scene regions, so it needs more blur than an in-panel text zone to
|
|
2089
|
+
// average high-frequency backdrops into a stable field.
|
|
2090
|
+
// Regular overlays are text-heavy surfaces, so they use the same authored
|
|
2091
|
+
// frost tier rather than inventing a doubled second tier.
|
|
2092
|
+
const blurPx = Math.max(16, treatment.contentFrostBlurPx ?? 0);
|
|
2093
|
+
const parts = [`blur(${blurPx}px)`];
|
|
2094
|
+
if (treatment.backdropSaturation !== undefined && treatment.backdropSaturation !== 1) {
|
|
2095
|
+
parts.push(`saturate(${treatment.backdropSaturation})`);
|
|
2096
|
+
}
|
|
2097
|
+
return parts.join(' ');
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* The AREA seam's colour. A palette that names none paints the boundary its
|
|
2102
|
+
* call sites already read, so every skin but Blender is pixel-identical.
|
|
2103
|
+
*/
|
|
2104
|
+
function areaBoundaryValue(theme: EditorTheme): string {
|
|
2105
|
+
return theme.color.boundary.area ?? theme.color.boundary.default;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* A tree row's ALTERNATE band — a step off whatever AREA FILL the tree sits on,
|
|
2110
|
+
* stated once here because two places must produce it and a duplicated
|
|
2111
|
+
* percentage drifts. `editorThemeVariables` emits it against `surface.panel`
|
|
2112
|
+
* (the fill every unclaimed group has); the workspace host re-emits
|
|
2113
|
+
* it on a group whose panel CLAIMS a region, against that region's fill.
|
|
2114
|
+
*
|
|
2115
|
+
* It cannot simply read an inherited `--vgai-surface-region`: a custom
|
|
2116
|
+
* property's `var()` references are substituted at computed-value time ON THE
|
|
2117
|
+
* ELEMENT THAT DECLARES IT, so a root-declared expression would have baked in
|
|
2118
|
+
* the root's panel fill before any descendant could override it — measured, and
|
|
2119
|
+
* it is why the Outliner's rows stepped ELEVEN levels after `color.region`
|
|
2120
|
+
* landed instead of Blender's three (#323232 on #272727 where the frame shows
|
|
2121
|
+
* #2a2a2a).
|
|
2122
|
+
*
|
|
2123
|
+
* The solve, against `modeling.png` at 2x: A = the area fill, B = `content.primary`
|
|
2124
|
+
* 0xe6=230, target = A + 3. For the Outliner's 0x27=39 that is 42, and
|
|
2125
|
+
* p = (230-42)/(230-39) = 188/191 = 98.43% — the same 98.4% the panel-fill solve
|
|
2126
|
+
* gives, because the step is three levels either way.
|
|
2127
|
+
*/
|
|
2128
|
+
export function regionRowAltValue(areaFill: string, ink: string): string {
|
|
2129
|
+
return `color-mix(in srgb, ${areaFill} 98.4%, ${ink})`;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/**
|
|
2133
|
+
* THE AREA EMBOSS — the one CSS px of light Blender puts on each side of the
|
|
2134
|
+
* dark area groove. It is not a colour a palette names: it is the area's OWN
|
|
2135
|
+
* FILL lightened, which is why a bright viewport gets a bright line and a dark
|
|
2136
|
+
* Outliner a dim one, and why one number (`chrome.areaEmboss`, percent of
|
|
2137
|
+
* white) covers every area. `0` returns `transparent` — nothing painted, which
|
|
2138
|
+
* is byte-identical to having no rule at all.
|
|
2139
|
+
*
|
|
2140
|
+
* SOLVED, not chosen, against `modeling.png` at its native 2x. Each pair is
|
|
2141
|
+
* the area's flat fill and the light band beside the groove, read as whole
|
|
2142
|
+
* pixel runs (the emboss is 2 device px = 1 CSS px):
|
|
2143
|
+
*
|
|
2144
|
+
* fill emboss coordinates
|
|
2145
|
+
* Properties #2f2f2f = 47 → 64 x=3200, y 426..427 (below the
|
|
2146
|
+
* Outliner↔Properties groove)
|
|
2147
|
+
* Outliner #272727 = 39 → 57/56 y=300, x 2843..2844 (right of the
|
|
2148
|
+
* viewport↔right-column groove)
|
|
2149
|
+
* tab rail #171717 = 23 → 43/42 y=1500, x 2843..2844 (same groove,
|
|
2150
|
+
* lower down, where the Properties
|
|
2151
|
+
* tab rail is the neighbour)
|
|
2152
|
+
* row-alt #2a2a2a = 42 → 59/60 x=3200, y 417..418
|
|
2153
|
+
*
|
|
2154
|
+
* `p = (out - fill) / (255 - fill)` for those four is 0.0817, 0.0810/0.0841,
|
|
2155
|
+
* 0.0862/0.0822 — mean 8.14%, and 8.2% is the value that renders all four
|
|
2156
|
+
* measured integers (42.0, 56.7, 59.5, 64.1 → 42, 57, 59/60, 64).
|
|
2157
|
+
*
|
|
2158
|
+
* THE PIXELS CONTRADICTED THE BRIEF ON THE FIFTH PAIR, and this is the
|
|
2159
|
+
* correction: the viewport's band measures 63 → 93/94 (y 2055..2056 at the
|
|
2160
|
+
* status bar, x 4..5 at the window's left edge, x 2834 at the sash) and the
|
|
2161
|
+
* 3D View's own header 52 → 86 (y 53..54 under the top bar) — both nearly
|
|
2162
|
+
* DOUBLE the lift the other four share, and a single mix cannot produce that
|
|
2163
|
+
* (a fixed colour at a fixed alpha must compress the fills' spread, and these
|
|
2164
|
+
* expand it: 34 levels of output across 11 levels of input). Applying the SAME
|
|
2165
|
+
* 8.2% TWICE does: 63 → 78.7 → 93.2 (measured 93/94) and 52 → 68.6 → 83.9
|
|
2166
|
+
* (measured 86). Both doubled areas are the 3D View's — its main region
|
|
2167
|
+
* overlaps its area, so the region's emboss lands on the area's — and the
|
|
2168
|
+
* single-pass value shows through where the two passes do not align
|
|
2169
|
+
* (x=2835 reads 78 beside x=2834's 94, against a predicted 78.7). So this
|
|
2170
|
+
* is ONE number applied once by an ordinary area and twice by the stage,
|
|
2171
|
+
* which is Blender's own structure, not a second constant.
|
|
2172
|
+
*/
|
|
2173
|
+
export function areaEmbossValue(areaFill: string): string {
|
|
2174
|
+
return `color-mix(in srgb, var(--vgai-area-emboss-mix), ${areaFill})`;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* The light and its weight, as the one token every {@link areaEmbossValue}
|
|
2179
|
+
* expression mixes in. A skin that declares no `chrome.areaEmboss` gets
|
|
2180
|
+
* `transparent 100%`, so every emboss expression — root, group, nested —
|
|
2181
|
+
* computes to a fully transparent colour and paints nothing whatsoever.
|
|
2182
|
+
* Emitted as a PAIR because the whole point of the indirection is that a
|
|
2183
|
+
* group's own expression (the workspace host's) can be written
|
|
2184
|
+
* without reading the theme object.
|
|
2185
|
+
*/
|
|
2186
|
+
function areaEmbossMixValue(theme: Pick<EditorTheme, 'density'>): string {
|
|
2187
|
+
const amount = density(theme).chrome.areaEmboss;
|
|
2188
|
+
return amount > 0 ? `#ffffff ${amount}%` : 'transparent 100%';
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/** Map one typed theme to the semantic variables consumed by editor chrome. */
|
|
2192
|
+
export function editorThemeVariables(theme: EditorTheme): Record<EditorThemeVariable, string> {
|
|
2193
|
+
const onBright = contentOnBrightValues(theme);
|
|
2194
|
+
const glassMaterial = usesGlassMaterial(theme);
|
|
2195
|
+
// The WIDGET classes, resolved once: a palette that names none reads the
|
|
2196
|
+
// surface each call site read before the group existed, so every token below
|
|
2197
|
+
// is the same string it used to be and no skin moves. See
|
|
2198
|
+
// `EditorTheme.color.widget` for the measurements.
|
|
2199
|
+
const widgetRegular = theme.color.widget?.regular ?? theme.color.surface.raised;
|
|
2200
|
+
const widgetMenu = theme.color.widget?.menu ?? theme.color.surface.raised;
|
|
2201
|
+
const widgetField = theme.color.widget?.field ?? theme.color.surface.inset;
|
|
2202
|
+
// The emboss is stated once, as a whole `box-shadow` value rather than as a
|
|
2203
|
+
// colour, so the 1 px offset lives in ONE place and a palette that names no
|
|
2204
|
+
// emboss resolves to the literal `none` — which is what keeps every other
|
|
2205
|
+
// look byte-identical, and what lets a rule COMPOSE it into an existing
|
|
2206
|
+
// shadow list (`none` cannot appear inside one).
|
|
2207
|
+
const widgetEmbossShadow = theme.color.widget?.emboss
|
|
2208
|
+
? `0 1px 0 ${theme.color.widget.emboss}`
|
|
2209
|
+
: 'none';
|
|
2210
|
+
// Hover LIFTS a widget by a step from ITS OWN fill, recomputed per class so
|
|
2211
|
+
// a palette that moves a class's fill gets a hover that still belongs to
|
|
2212
|
+
// it. THIS IS THE ONLY STATEMENT OF THE HOVER RELATIONSHIP — keep it that
|
|
2213
|
+
// way: a hover surface stated twice is a number that gets corrected in one
|
|
2214
|
+
// place and left wrong in the one that paints.
|
|
2215
|
+
//
|
|
2216
|
+
// The percentage is SOLVED, not chosen. `color-mix(in srgb, A p%, B)` on
|
|
2217
|
+
// opaque colours is `A*p + B*(1-p)`, so `p = (B - target)/(B - A)`; with
|
|
2218
|
+
// Blender's `widget.regular` 0x53=83 as A, `content.primary` 0xe6=230 as B
|
|
2219
|
+
// and the hover target 0x65=101, p = 129/147 = 87.755%. At the 86% this
|
|
2220
|
+
// was, `--vgai-widget-regular-hover` rendered #686868.
|
|
2221
|
+
//
|
|
2222
|
+
// HONEST LABEL: #656565 is the only number here that is NOT measurable
|
|
2223
|
+
// from the reference frames — none of the eighteen captures in
|
|
2224
|
+
// /Volumes/PeakSSD/volter-work/blender-reference holds a widget under the
|
|
2225
|
+
// pointer, and a search of every frame for a #656565 REGION finds only
|
|
2226
|
+
// scattered viewport-gradient pixels. It is the target this file's own
|
|
2227
|
+
// docblock has carried; the solve against it is exact, the target is
|
|
2228
|
+
// inherited.
|
|
2229
|
+
const lift = (fill: string) =>
|
|
2230
|
+
`color-mix(in srgb, ${fill} 87.8%, ${theme.color.content.primary})`;
|
|
2231
|
+
// The AREA HEADER's fill, stated once here because two variables need it —
|
|
2232
|
+
// `--vgai-surface-header` paints it and `--vgai-area-emboss-stage-header`
|
|
2233
|
+
// lightens it. The solve is documented with the other derived surfaces below.
|
|
2234
|
+
const surfaceHeader = `color-mix(in srgb, ${theme.color.surface.panel} 86.5%, ${theme.color.surface.raised})`;
|
|
2235
|
+
return {
|
|
2236
|
+
'--vgai-surface-shell': theme.color.surface.shell,
|
|
2237
|
+
'--vgai-surface-panel': theme.color.surface.panel,
|
|
2238
|
+
'--vgai-surface-chrome': theme.color.surface.chrome,
|
|
2239
|
+
'--vgai-surface-raised': theme.color.surface.raised,
|
|
2240
|
+
'--vgai-surface-inset': theme.color.surface.inset,
|
|
2241
|
+
'--vgai-surface-overlay': theme.color.surface.overlay,
|
|
2242
|
+
'--vgai-surface-backdrop-filter': backdropFilterValue(theme.treatment),
|
|
2243
|
+
'--vgai-surface-sticky': stickySurfaceValue('--vgai-surface-chrome', glassMaterial),
|
|
2244
|
+
'--vgai-surface-sticky-panel': stickySurfaceValue('--vgai-surface-panel', glassMaterial),
|
|
2245
|
+
'--vgai-surface-sticky-backdrop-filter': 'none',
|
|
2246
|
+
// Optical body frost is independent from nested content-floor paint.
|
|
2247
|
+
// The refraction host reads this stable token even when a Regular panel
|
|
2248
|
+
// locally disables `--vgai-content-frost-filter` to enforce one filter.
|
|
2249
|
+
'--vgai-surface-body-frost-filter': contentFrostFilterValue(theme.treatment),
|
|
2250
|
+
'--vgai-content-frost-filter': contentFrostFilterValue(theme.treatment),
|
|
2251
|
+
'--vgai-content-frost-bg': contentFrostBgValue(theme.treatment),
|
|
2252
|
+
'--vgai-input-surface': inputSurfaceValue(glassMaterial),
|
|
2253
|
+
'--vgai-input-border': inputBorderValue(glassMaterial),
|
|
2254
|
+
'--vgai-input-shadow': inputShadowValue(glassMaterial, widgetEmbossShadow),
|
|
2255
|
+
'--vgai-structural-divider': glassMaterial ? 'transparent' : theme.color.boundary.default,
|
|
2256
|
+
'--vgai-card-specular-ring': specularRingValue(theme.treatment),
|
|
2257
|
+
'--vgai-card-specular-sheen': specularSheenValue(theme.treatment),
|
|
2258
|
+
'--vgai-card-specular-shadow': specularShadowValue(theme.treatment),
|
|
2259
|
+
// Liquid Glass refraction (report §2.31/§2.32): unitless numbers,
|
|
2260
|
+
// consumed by the surface host's refraction manager
|
|
2261
|
+
// (`components/glass-refraction.ts`) via getComputedStyle — never by
|
|
2262
|
+
// any CSS rule. Thickness '0' (no refraction) for treatment-less themes
|
|
2263
|
+
// keeps the degrade ladder data-driven: the reduced-transparency paint
|
|
2264
|
+
// path strips `treatment`, which zeroes this var, which removes every
|
|
2265
|
+
// per-surface url() filter automatically.
|
|
2266
|
+
'--vgai-card-refraction-thickness': String(theme.treatment?.refractionThickness ?? 0),
|
|
2267
|
+
'--vgai-card-refraction-bezel': String(theme.treatment?.refractionBezelPx ?? 28),
|
|
2268
|
+
'--vgai-card-refraction-angle': String(theme.treatment?.specularAngleDeg ?? 120),
|
|
2269
|
+
// Floating glass-island chrome (P6 U1/U2): real-glass material for
|
|
2270
|
+
// treatment themes, the exact pre-P6 floating-toolbar recipe otherwise
|
|
2271
|
+
// (see the island*Value functions above for the ladder rationale).
|
|
2272
|
+
'--vgai-ambient-lift': ambientLiftValue(theme.treatment),
|
|
2273
|
+
'--vgai-island-surface': islandSurfaceValue(theme),
|
|
2274
|
+
'--vgai-island-backdrop-filter': islandBackdropFilterValue(theme),
|
|
2275
|
+
'--vgai-island-radius': islandRadiusValue(glassMaterial),
|
|
2276
|
+
'--vgai-island-border-color': islandBorderColorValue(glassMaterial),
|
|
2277
|
+
'--vgai-island-shadow': islandShadowValue(glassMaterial),
|
|
2278
|
+
'--vgai-island-specular-ring': islandSpecularRingValue(theme.treatment),
|
|
2279
|
+
'--vgai-selection-bg': selectionBackgroundValue(theme),
|
|
2280
|
+
'--vgai-content-on-selection': selectionInkValue(theme),
|
|
2281
|
+
'--vgai-selection-border': selectionBorderValue(theme),
|
|
2282
|
+
'--vgai-selection-indicator': selectionIndicatorValue(theme),
|
|
2283
|
+
'--vgai-island-control-radius': islandControlRadiusValue(glassMaterial),
|
|
2284
|
+
'--vgai-island-primary-bg': islandPrimaryBgValue(theme),
|
|
2285
|
+
'--vgai-content-text-shadow': contentTextShadowValue(theme.treatment),
|
|
2286
|
+
// Adaptive bright-backdrop ramp (P3): consumed only by theme.css's
|
|
2287
|
+
// `[data-vgai-backdrop="bright"]` / Regular-glass scopes. Themes without
|
|
2288
|
+
// adaptive material physics mirror their normal values here, so those
|
|
2289
|
+
// scopes are inert for them. `--vgai-content-adaptive` is the unitless
|
|
2290
|
+
// classifier gate (components/backdrop-luminance.ts reads it via
|
|
2291
|
+
// getComputedStyle, same idiom as `--vgai-card-refraction-thickness`):
|
|
2292
|
+
// '0' means no surface is ever classified — today's dark default.
|
|
2293
|
+
'--vgai-content-primary-on-bright': onBright.primary,
|
|
2294
|
+
'--vgai-content-muted-on-bright': onBright.muted,
|
|
2295
|
+
'--vgai-content-dim-on-bright': onBright.dim,
|
|
2296
|
+
'--vgai-content-text-shadow-on-bright': onBright.textShadow,
|
|
2297
|
+
'--vgai-content-frost-bg-on-bright': onBright.frostBg,
|
|
2298
|
+
'--vgai-content-adaptive': theme.treatment?.adaptiveContent ? '1' : '0',
|
|
2299
|
+
'--vgai-glass-regular-bg': glassRegularBgValue(theme),
|
|
2300
|
+
'--vgai-glass-regular-backdrop-filter': glassRegularBackdropFilterValue(theme.treatment),
|
|
2301
|
+
'--vgai-glass-regular-panel-bg': glassRegularPanelBgValue(theme),
|
|
2302
|
+
'--vgai-glass-regular-panel-backdrop-filter': glassRegularBackdropFilterValue(theme.treatment),
|
|
2303
|
+
'--vgai-glass-dark-frost-panel-bg': 'rgba(5,8,13,0.74)',
|
|
2304
|
+
'--vgai-glass-dark-frost-content-bg': 'rgba(5,8,13,0.84)',
|
|
2305
|
+
// Modal scrim (P6-U6 owner taste decision 3): glass themes read like
|
|
2306
|
+
// macOS — a LIGHTER dim plus a blur doing the separation work; opaque
|
|
2307
|
+
// themes keep their heavy dim and `none`, byte-identically.
|
|
2308
|
+
'--vgai-scrim-backdrop-filter': theme.treatment ? 'blur(8px)' : 'none',
|
|
2309
|
+
'--vgai-boundary-default': theme.color.boundary.default,
|
|
2310
|
+
'--vgai-boundary-strong': theme.color.boundary.strong,
|
|
2311
|
+
// The AREA seam. Never emitted empty — a seam that stops being painted
|
|
2312
|
+
// fuses two areas into one field — so an absent member resolves to the
|
|
2313
|
+
// boundary its call sites already read (`EditorTheme.color.boundary.area`).
|
|
2314
|
+
'--vgai-boundary-area': areaBoundaryValue(theme),
|
|
2315
|
+
// THE TREE'S INDENT GUIDE (`color.boundary.indent`). Emitted EMPTY —
|
|
2316
|
+
// `transparent` — when the palette names none, because no skin but
|
|
2317
|
+
// Blender's drew one and a fallback to any other boundary would put a rule
|
|
2318
|
+
// into every tree in the editor. `.vgai-tree-indent-guide` paints this and
|
|
2319
|
+
// nothing else, so absent means the guide's spans are there and invisible.
|
|
2320
|
+
'--vgai-tree-indent-guide': theme.color.boundary.indent ?? 'transparent',
|
|
2321
|
+
// THE DIVIDER RULE (`color.boundary.divider`). Same optional shape, a
|
|
2322
|
+
// different fallback for a stated reason: no skin drew an indent guide, so
|
|
2323
|
+
// absent means `transparent` above; every skin draws this rule, so absent
|
|
2324
|
+
// means the boundary it already drew.
|
|
2325
|
+
'--vgai-boundary-divider': theme.color.boundary.divider ?? theme.color.boundary.default,
|
|
2326
|
+
// THE EMBOSS beside that groove (`areaEmbossValue`). Three values because
|
|
2327
|
+
// three fills meet a groove: an ordinary docked area (a group with no
|
|
2328
|
+
// region claim takes `surface.panel`; one that claims a region re-emits
|
|
2329
|
+
// this against its own fill in the workspace host, the same
|
|
2330
|
+
// way `--vgai-surface-row-alt` is re-emitted there and for the same
|
|
2331
|
+
// substitution reason), and the STAGE area, whose sides are the viewport's
|
|
2332
|
+
// fill and whose top edge is its own header band — both of them lifted
|
|
2333
|
+
// TWICE, which is what the 3D View's overlapping region does in Blender.
|
|
2334
|
+
'--vgai-area-emboss-mix': areaEmbossMixValue(theme),
|
|
2335
|
+
'--vgai-area-emboss': areaEmbossValue(theme.color.surface.panel),
|
|
2336
|
+
'--vgai-area-emboss-stage': areaEmbossValue(
|
|
2337
|
+
areaEmbossValue(theme.color.viewport?.background ?? theme.color.surface.panel),
|
|
2338
|
+
),
|
|
2339
|
+
'--vgai-area-emboss-stage-header': areaEmbossValue(areaEmbossValue(surfaceHeader)),
|
|
2340
|
+
'--vgai-content-primary': theme.color.content.primary,
|
|
2341
|
+
'--vgai-content-muted': theme.color.content.muted,
|
|
2342
|
+
'--vgai-content-dim': theme.color.content.dim,
|
|
2343
|
+
'--vgai-content-on-accent': theme.color.content.onAccent,
|
|
2344
|
+
// THE MENU WORD (`color.content.menu`). Falls back in the EMITTER — not in
|
|
2345
|
+
// a `var(…, …)` default, for the reason `content.selected` states: every
|
|
2346
|
+
// variable here is always emitted, so a CSS fallback would never fire.
|
|
2347
|
+
// The fallback is `content.primary` because that is what the top bar's
|
|
2348
|
+
// menu words already paint, so a palette naming no menu ink leaves that
|
|
2349
|
+
// bar bit-identical; the area header's words, which read `content.muted`
|
|
2350
|
+
// only because nothing had overridden the base rule, converge onto it.
|
|
2351
|
+
'--vgai-content-menu': theme.color.content.menu ?? theme.color.content.primary,
|
|
2352
|
+
// THE STATUS BAND'S INK and THE PLACEHOLDER'S, both falling back to
|
|
2353
|
+
// `content.dim` in the EMITTER for the reason above. The band already
|
|
2354
|
+
// inherited dim, so its fallback is bit-identical; the placeholder had no
|
|
2355
|
+
// rule at all and inherited CHROME's fixed rgb(117,117,117), so a palette
|
|
2356
|
+
// naming none moves onto the palette's own quiet ink — which is the whole
|
|
2357
|
+
// point of the member (see its docblock).
|
|
2358
|
+
'--vgai-content-status': theme.color.content.status ?? theme.color.content.dim,
|
|
2359
|
+
'--vgai-content-placeholder': theme.color.content.placeholder ?? theme.color.content.dim,
|
|
2360
|
+
// Emitted EMPTY when the palette names none — the `viewport` group's
|
|
2361
|
+
// answer. Its one reader inherits the row's ink through the fallback.
|
|
2362
|
+
'--vgai-content-active': theme.color.content.active ?? '',
|
|
2363
|
+
'--vgai-content-selected': theme.color.content.selected ?? theme.color.content.active ?? '',
|
|
2364
|
+
'--vgai-accent': theme.color.accent.default,
|
|
2365
|
+
'--vgai-accent-muted': theme.color.accent.muted,
|
|
2366
|
+
'--vgai-danger': theme.color.semantic.danger,
|
|
2367
|
+
'--vgai-danger-muted': theme.color.semantic.dangerMuted,
|
|
2368
|
+
'--vgai-danger-faint': theme.color.semantic.dangerFaint,
|
|
2369
|
+
'--vgai-warn': theme.color.semantic.warning,
|
|
2370
|
+
'--vgai-warn-muted': theme.color.semantic.warningMuted,
|
|
2371
|
+
'--vgai-success': theme.color.semantic.success,
|
|
2372
|
+
'--vgai-success-muted': theme.color.semantic.successMuted,
|
|
2373
|
+
'--vgai-dynamic': theme.color.semantic.dynamic,
|
|
2374
|
+
'--vgai-dynamic-muted': theme.color.semantic.dynamicMuted,
|
|
2375
|
+
'--vgai-instance': theme.color.semantic.instance,
|
|
2376
|
+
'--vgai-neutral-hover': theme.color.neutralOverlay.hover,
|
|
2377
|
+
'--vgai-neutral-active': theme.color.neutralOverlay.active,
|
|
2378
|
+
'--vgai-scrim': theme.color.scrim,
|
|
2379
|
+
// DERIVED surfaces — relationships every skin keeps, computed from its own
|
|
2380
|
+
// palette (Blender's measured steps: a panel header is a slab #3c3c3c
|
|
2381
|
+
// over #2f2f2f, an area header #343434, outliner rows alternate by three
|
|
2382
|
+
// levels). The hover step is stated ONCE, by `lift` above, because that
|
|
2383
|
+
// is what paints a hovered widget.
|
|
2384
|
+
//
|
|
2385
|
+
// Each percentage below is SOLVED from that measured target against the
|
|
2386
|
+
// two inputs the expression already mixes, never chosen for roundness:
|
|
2387
|
+
// `color-mix(in srgb, A p%, B)` on opaque colours is the plain sRGB
|
|
2388
|
+
// average `A*p + B*(1-p)`, so `p = (B - target) / (B - A)`. The three
|
|
2389
|
+
// solves, with the Blender palette's own channel values (grey, so one
|
|
2390
|
+
// channel states all three) and the coordinate the target was read at in
|
|
2391
|
+
// `/Volumes/PeakSSD/volter-work/blender-reference` (1728x1052 factory
|
|
2392
|
+
// startup captured at 2x):
|
|
2393
|
+
//
|
|
2394
|
+
// header A=panel 0x2f=47, B=raised 0x54=84, target 0x34=52
|
|
2395
|
+
// p = (84-52)/(84-47) = 32/37 = 86.49% → 86.5%
|
|
2396
|
+
// (modeling.png, the 3D viewport's area header, y 60..108 over
|
|
2397
|
+
// x 300..2700, median 52 — it was 78%, which renders 55.)
|
|
2398
|
+
// section A=panel 0x2f=47, B=raised 0x54=84, target 0x3c=60
|
|
2399
|
+
// p = (84-60)/(84-47) = 24/37 = 64.86% → 64.9%
|
|
2400
|
+
// (properties-object.png at 2x, the Transform panel's card:
|
|
2401
|
+
// row y=380 over x 76..277 and x 495..590 is 60, and the card
|
|
2402
|
+
// header at y=230 is the same 60 — one continuous fill, header
|
|
2403
|
+
// and body. It was 62%, which renders 61, one level off the
|
|
2404
|
+
// value THIS BLOCK'S OWN COMMENT already named.)
|
|
2405
|
+
// s-edge A=panel 0x2f=47, B=raised 0x54=84, target 0x49=73
|
|
2406
|
+
// p = (84-73)/(84-47) = 11/37 = 29.73% → 29.7%
|
|
2407
|
+
// (the same card's 1 CSS px outline: properties-object.png
|
|
2408
|
+
// x 74..75 and x 591..592 over its whole height, y 200..201 and
|
|
2409
|
+
// y 1030 at its ends, all 73. Blender outlines a panel card
|
|
2410
|
+
// LIGHTER than the card; `boundary.default` #3c3c3c is the
|
|
2411
|
+
// card's own fill there and would be invisible.)
|
|
2412
|
+
// rowAlt A=panel 0x2f=47, B=primary 0xe6=230, target = panel + 3 = 50
|
|
2413
|
+
// p = (230-50)/(230-47) = 180/183 = 98.36% → 98.4%
|
|
2414
|
+
// (modeling.png, Outliner rows over x 3300..3420: y 186..225 is
|
|
2415
|
+
// 39 = #272727 and y 226..265 is 42 = #2a2a2a — a THREE-level
|
|
2416
|
+
// alternation on a 40 px (20 CSS px) pitch. It was 96%, which
|
|
2417
|
+
// steps seven.)
|
|
2418
|
+
//
|
|
2419
|
+
// Each is carried to one decimal because the integer next to it misses:
|
|
2420
|
+
// 98% renders 0x33, and `lift`'s 88% renders 0x66.
|
|
2421
|
+
'--vgai-surface-section': `color-mix(in srgb, ${theme.color.surface.panel} 64.9%, ${theme.color.surface.raised})`,
|
|
2422
|
+
'--vgai-surface-section-edge': `color-mix(in srgb, ${theme.color.surface.panel} 29.7%, ${theme.color.surface.raised})`,
|
|
2423
|
+
'--vgai-surface-header': surfaceHeader,
|
|
2424
|
+
'--vgai-surface-row-alt': regionRowAltValue(
|
|
2425
|
+
theme.color.surface.panel,
|
|
2426
|
+
theme.color.content.primary,
|
|
2427
|
+
),
|
|
2428
|
+
// A SELECTED TREE ROW is not an active widget, and there are TWO of them.
|
|
2429
|
+
//
|
|
2430
|
+
// Measured in Blender 5.2, `modeling-object-selected.png` (object mode,
|
|
2431
|
+
// every object selected), Outliner rows at device y 133-252, sampled over a
|
|
2432
|
+
// clean x band (crop-local 440-500 against the `outliner.png` origin
|
|
2433
|
+
// 2843,53). Three row states, three fills:
|
|
2434
|
+
//
|
|
2435
|
+
// unselected #272727 / #2a2a2a — the ordinary stripe
|
|
2436
|
+
// selected, not active #1c304c — (28,48,76)
|
|
2437
|
+
// selected AND active #324c7f — (50,76,127), inside a
|
|
2438
|
+
// 1 px #5a74a7 border
|
|
2439
|
+
//
|
|
2440
|
+
// Both bands are the same 18-of-20 px box: device rows 135-170 and 177-208
|
|
2441
|
+
// inside a 40 px pitch, so ONE ordinary stripe row shows above and below
|
|
2442
|
+
// each. `theme.css`'s `.vgai-tree-row` rules paint that inset.
|
|
2443
|
+
//
|
|
2444
|
+
// The derivations, and their residual against the measurement. The palette
|
|
2445
|
+
// carries the accent (#4772b3, Blender's own widget blue) and both bands
|
|
2446
|
+
// are it sunk toward black — the same relation the single band already
|
|
2447
|
+
// used, so a palette that declares nothing new gets a coherent pair:
|
|
2448
|
+
// active 0.70 x #4772b3 = #32507d against #324c7f — green +4, blue -2
|
|
2449
|
+
// selected 0.42 x #4772b3 = #1e304b against #1c304c — red +2, blue -1
|
|
2450
|
+
// Neither band is EXACTLY a scaling of the accent (Blender's are their own
|
|
2451
|
+
// theme entries), and the residual is under five levels on one channel of
|
|
2452
|
+
// each, so no palette token is minted for four levels of green.
|
|
2453
|
+
//
|
|
2454
|
+
// The BORDER is exact and additive: #5a74a7 is the active fill plus 40 on
|
|
2455
|
+
// every channel (50+40, 76+40, 127+40). No `color-mix` expresses a uniform
|
|
2456
|
+
// lift — mixing toward white moves the channels by different amounts — so
|
|
2457
|
+
// this is the one place the stylesheet uses relative colour syntax, which
|
|
2458
|
+
// states the measurement literally and derives for every palette.
|
|
2459
|
+
'--vgai-tree-row-selected-bg': `color-mix(in srgb, ${theme.color.accent.default} 42%, #000)`,
|
|
2460
|
+
'--vgai-tree-row-active-bg': `color-mix(in srgb, ${theme.color.accent.default} 70%, #000)`,
|
|
2461
|
+
'--vgai-tree-row-active-border':
|
|
2462
|
+
'rgb(from var(--vgai-tree-row-active-bg) calc(r + 40) calc(g + 40) calc(b + 40))',
|
|
2463
|
+
// WHO GETS THE ROW'S ONE TEXT MARK when a palette declares an ACTIVE ink.
|
|
2464
|
+
// The instance rule (`semantic.instance`, owner 2026-07-31) and the active
|
|
2465
|
+
// object's orange name both want the name, and on the active row only one
|
|
2466
|
+
// can be read: a dotted rule in one hue under a name in another is two
|
|
2467
|
+
// marks arguing. So the rule YIELDS there, and only there — this is `none`
|
|
2468
|
+
// exactly when `content.active` exists and `underline` otherwise, so a
|
|
2469
|
+
// palette that names no active ink re-states what the row already drew and
|
|
2470
|
+
// its instance rows are untouched. Derived rather than declared: the
|
|
2471
|
+
// emitter is the one place that knows what the palette said.
|
|
2472
|
+
'--vgai-tree-active-name-underline': theme.color.content.active ? 'none' : 'underline',
|
|
2473
|
+
// The viewport group, empty when the palette carries none (readers treat
|
|
2474
|
+
// an empty token as "the editor's own").
|
|
2475
|
+
'--vgai-viewport-background': theme.color.viewport?.background ?? '',
|
|
2476
|
+
'--vgai-viewport-grid': theme.color.viewport?.grid ?? '',
|
|
2477
|
+
'--vgai-viewport-axis-x': theme.color.viewport?.axisX ?? '',
|
|
2478
|
+
'--vgai-viewport-axis-y': theme.color.viewport?.axisY ?? '',
|
|
2479
|
+
'--vgai-viewport-selection': theme.color.viewport?.selection ?? '',
|
|
2480
|
+
'--vgai-viewport-active': theme.color.viewport?.active ?? '',
|
|
2481
|
+
// THE TRANSFORM GIZMO'S SCREEN SIZE, in px per gizmo unit, emitted the
|
|
2482
|
+
// same way and read the same way (`native-selection-style.ts`): a look
|
|
2483
|
+
// that names none emits empty, and the viewport keeps three's own
|
|
2484
|
+
// viewport-relative handle. Blender's is `U.gizmo_size` — see
|
|
2485
|
+
// `DensityContribution.viewport.gizmoSize` for the derivation.
|
|
2486
|
+
'--vgai-viewport-gizmo-size':
|
|
2487
|
+
theme.density?.viewport?.gizmoSize === undefined ? '' : `${theme.density.viewport.gizmoSize}`,
|
|
2488
|
+
// WHICH TOOL THE STAGE'S SHELF OPENS ON, emitted the same way and read the
|
|
2489
|
+
// same way. It travels with the MATERIAL rather than through the settings
|
|
2490
|
+
// layers, and that is the point: a chrome REGION crosses the Code-OSS
|
|
2491
|
+
// frame's configuration service, which knows only the key list its
|
|
2492
|
+
// generated contribution carries and drops anything newer — measured
|
|
2493
|
+
// 2026-09-21, sixteen `not a vgai setting, so it was not applied` warnings
|
|
2494
|
+
// on the first boot after this value existed, with the look's tool never
|
|
2495
|
+
// reaching the stage. A theme token is installed on the theme root by the
|
|
2496
|
+
// page itself and crosses nothing.
|
|
2497
|
+
'--vgai-viewport-shelf-tool': theme.density?.viewport?.shelfTool ?? '',
|
|
2498
|
+
// WHICH AXIS IS UP IN THE WORLD THE STAGE PRESENTS, and what a box select
|
|
2499
|
+
// means there — emitted empty by every look that states neither, which is
|
|
2500
|
+
// the whole "an absent member keeps the editor's own" convention this
|
|
2501
|
+
// group runs on. See `DensityContribution.viewport` for both.
|
|
2502
|
+
'--vgai-viewport-up-axis': theme.density?.viewport?.upAxis ?? '',
|
|
2503
|
+
'--vgai-viewport-box-select': theme.density?.viewport?.boxSelect ?? '',
|
|
2504
|
+
// The widget classes. Unlike `viewport`, these are never emitted empty:
|
|
2505
|
+
// every one paints a control that must stay painted, so an absent group
|
|
2506
|
+
// resolves to the surface that call site already read.
|
|
2507
|
+
'--vgai-widget-regular': widgetRegular,
|
|
2508
|
+
'--vgai-widget-regular-hover': lift(widgetRegular),
|
|
2509
|
+
'--vgai-widget-menu': widgetMenu,
|
|
2510
|
+
'--vgai-widget-menu-hover': lift(widgetMenu),
|
|
2511
|
+
'--vgai-widget-field': widgetField,
|
|
2512
|
+
'--vgai-widget-emboss-shadow': widgetEmbossShadow,
|
|
2513
|
+
// The CATEGORY inks. Emitted as `currentColor` when the palette names
|
|
2514
|
+
// none, which is the whole compatibility story: a toned glyph paints
|
|
2515
|
+
// `var(--vgai-category-object, currentColor)`, so under a palette
|
|
2516
|
+
// without the group it paints exactly what a monochrome glyph paints.
|
|
2517
|
+
// The literal is used rather than an empty string (the `viewport` group's
|
|
2518
|
+
// answer) because these tokens are read by a `fill`, where empty is not
|
|
2519
|
+
// a colour and the fallback must therefore be a real one.
|
|
2520
|
+
'--vgai-category-object': theme.color.category?.object ?? 'currentColor',
|
|
2521
|
+
'--vgai-category-modifier': theme.color.category?.modifier ?? 'currentColor',
|
|
2522
|
+
'--vgai-category-material': theme.color.category?.material ?? 'currentColor',
|
|
2523
|
+
'--vgai-category-tool': theme.color.category?.tool ?? 'currentColor',
|
|
2524
|
+
'--vgai-category-operator': theme.color.category?.operator ?? 'currentColor',
|
|
2525
|
+
'--vgai-category-data': theme.color.category?.data ?? 'currentColor',
|
|
2526
|
+
'--vgai-category-scene': theme.color.category?.scene ?? 'currentColor',
|
|
2527
|
+
'--vgai-category-collection': theme.color.category?.collection ?? 'currentColor',
|
|
2528
|
+
// The REGION fills, a pair per area. Never emitted empty (the `widget`
|
|
2529
|
+
// group's answer, not the `viewport` group's): each falls back to the
|
|
2530
|
+
// surface its call site already reads, so the dock can point a group at
|
|
2531
|
+
// `var(--vgai-region-<name>)` unconditionally and a palette that names no
|
|
2532
|
+
// region paints exactly what it painted before.
|
|
2533
|
+
'--vgai-region-outliner': theme.color.region?.outliner ?? theme.color.surface.panel,
|
|
2534
|
+
'--vgai-region-outliner-header': theme.color.region?.outliner ?? theme.color.surface.chrome,
|
|
2535
|
+
'--vgai-region-properties': theme.color.region?.properties ?? theme.color.surface.panel,
|
|
2536
|
+
'--vgai-region-properties-header': theme.color.region?.properties ?? theme.color.surface.chrome,
|
|
2537
|
+
'--vgai-font-sans': theme.typography.sans,
|
|
2538
|
+
'--vgai-font-mono': theme.typography.mono,
|
|
2539
|
+
'--vgai-radius-sm': theme.shape.small,
|
|
2540
|
+
'--vgai-radius-md': theme.shape.medium,
|
|
2541
|
+
'--vgai-radius-lg': theme.shape.large,
|
|
2542
|
+
'--vgai-radius-full': theme.shape.full,
|
|
2543
|
+
'--vgai-shadow-sm': theme.elevation.small,
|
|
2544
|
+
'--vgai-shadow-md': theme.elevation.medium,
|
|
2545
|
+
'--vgai-shadow-lg': theme.elevation.large,
|
|
2546
|
+
'--vgai-focus-ring': `${strokeWidth.active}px solid ${theme.color.accent.default}`,
|
|
2547
|
+
'--vgai-select-chevron': chevronDataUri(theme.color.content.muted),
|
|
2548
|
+
'--vgai-space-1': `${space[1]}px`,
|
|
2549
|
+
'--vgai-space-2': `${space[2]}px`,
|
|
2550
|
+
'--vgai-space-3': `${space[3]}px`,
|
|
2551
|
+
'--vgai-space-4': `${space[4]}px`,
|
|
2552
|
+
'--vgai-space-5': `${space[5]}px`,
|
|
2553
|
+
'--vgai-space-6': `${space[6]}px`,
|
|
2554
|
+
'--vgai-space-8': `${space[8]}px`,
|
|
2555
|
+
'--vgai-space-10': `${space[10]}px`,
|
|
2556
|
+
'--vgai-space-12': `${space[12]}px`,
|
|
2557
|
+
'--vgai-command-bar-height': `${density(theme).chrome.commandBar}px`,
|
|
2558
|
+
'--vgai-panel-header-height': `${density(theme).chrome.panelHeader}px`,
|
|
2559
|
+
'--vgai-local-toolbar-height': `${density(theme).chrome.localToolbar}px`,
|
|
2560
|
+
'--vgai-tree-row-height': `${density(theme).chrome.treeRow}px`,
|
|
2561
|
+
'--vgai-tree-indent': `${density(theme).chrome.treeIndent}px`,
|
|
2562
|
+
'--vgai-status-bar-height': `${density(theme).chrome.statusBar}px`,
|
|
2563
|
+
'--vgai-tool-size': `${density(theme).chrome.toolSize}px`,
|
|
2564
|
+
'--vgai-tool-width': `${density(theme).chrome.toolWidth}px`,
|
|
2565
|
+
'--vgai-tool-gap': `${density(theme).chrome.toolGap}px`,
|
|
2566
|
+
'--vgai-area-seam-width': `${density(theme).chrome.areaSeam}px`,
|
|
2567
|
+
'--vgai-control-compact-height': `${density(theme).control.compact}px`,
|
|
2568
|
+
'--vgai-control-default-height': `${density(theme).control.default}px`,
|
|
2569
|
+
'--vgai-control-comfortable-height': `${density(theme).control.comfortable}px`,
|
|
2570
|
+
'--vgai-stroke-resting': `${strokeWidth.resting}px`,
|
|
2571
|
+
'--vgai-stroke-active': `${strokeWidth.active}px`,
|
|
2572
|
+
'--vgai-font-xs': `${density(theme).font.xs}px`,
|
|
2573
|
+
'--vgai-font-sm': `${density(theme).font.sm}px`,
|
|
2574
|
+
'--vgai-font-base': `${density(theme).font.base}px`,
|
|
2575
|
+
'--vgai-font-md': `${density(theme).font.md}px`,
|
|
2576
|
+
'--vgai-font-lg': `${density(theme).font.lg}px`,
|
|
2577
|
+
'--vgai-font-xl': `${density(theme).font.xl}px`,
|
|
2578
|
+
'--vgai-font-2xl': `${density(theme).font['2xl']}px`,
|
|
2579
|
+
'--vgai-icon-xs': `${density(theme).icon.xs}px`,
|
|
2580
|
+
'--vgai-icon-sm': `${density(theme).icon.sm}px`,
|
|
2581
|
+
'--vgai-icon-md': `${density(theme).icon.md}px`,
|
|
2582
|
+
'--vgai-icon-lg': `${density(theme).icon.lg}px`,
|
|
2583
|
+
'--vgai-icon-xl': `${density(theme).icon.xl}px`,
|
|
2584
|
+
'--vgai-icon-2xl': `${density(theme).icon['2xl']}px`,
|
|
2585
|
+
'--vgai-font-heading': `${density(theme).font.heading}px`,
|
|
2586
|
+
'--vgai-font-weight-regular': String(fontWeight.regular),
|
|
2587
|
+
'--vgai-font-weight-semibold': String(fontWeight.semibold),
|
|
2588
|
+
'--vgai-font-weight-bold': String(fontWeight.bold),
|
|
2589
|
+
'--vgai-leading-tight': String(lineHeight.tight),
|
|
2590
|
+
'--vgai-leading-snug': String(lineHeight.snug),
|
|
2591
|
+
'--vgai-leading-normal': String(lineHeight.normal),
|
|
2592
|
+
'--vgai-leading-relaxed': String(lineHeight.relaxed),
|
|
2593
|
+
'--vgai-duration-fast': `${motion.duration.fast}ms`,
|
|
2594
|
+
'--vgai-duration-base': `${motion.duration.base}ms`,
|
|
2595
|
+
'--vgai-duration-slow': `${motion.duration.slow}ms`,
|
|
2596
|
+
'--vgai-ease-standard': motion.easing.standard,
|
|
2597
|
+
'--vgai-ease-out': motion.easing.out,
|
|
2598
|
+
'--vgai-z-base': String(zIndex.base),
|
|
2599
|
+
'--vgai-z-overlay-low': String(zIndex.overlayLow),
|
|
2600
|
+
'--vgai-z-sticky': String(zIndex.sticky),
|
|
2601
|
+
'--vgai-z-dropdown': String(zIndex.dropdown),
|
|
2602
|
+
'--vgai-z-toast': String(zIndex.toast),
|
|
2603
|
+
'--vgai-z-modal': String(zIndex.modal),
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
export const EDITOR_THEME_CLASS = 'vgai-editor-theme';
|
|
2608
|
+
|
|
2609
|
+
/** Install or switch a theme on one editor-chrome root. */
|
|
2610
|
+
export function applyEditorTheme(
|
|
2611
|
+
root: HTMLElement,
|
|
2612
|
+
theme: EditorTheme = graphiteDarkEditorTheme,
|
|
2613
|
+
): void {
|
|
2614
|
+
root.classList.add(EDITOR_THEME_CLASS);
|
|
2615
|
+
root.dataset['vgaiTheme'] = theme.id;
|
|
2616
|
+
// Chrome mode (P6 glass-native chrome, U3/U4): under the Glass material
|
|
2617
|
+
// there are no header/footer BARS — those surfaces dissolve into floating
|
|
2618
|
+
// glass clusters ("islands", the macOS liquid-glass model). Classic themes
|
|
2619
|
+
// keep bars. Reduced-transparency Glass strips optical treatment but
|
|
2620
|
+
// remains islands. Derived from explicit material identity, never from
|
|
2621
|
+
// theme identity (the theme-id ban holds: CSS scopes on this attribute,
|
|
2622
|
+
// not on [data-vgai-theme]).
|
|
2623
|
+
root.dataset['vgaiChrome'] = usesGlassMaterial(theme) ? 'islands' : 'bars';
|
|
2624
|
+
for (const [name, value] of Object.entries(editorThemeVariables(theme))) {
|
|
2625
|
+
root.style.setProperty(name, value);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* Re-exported from `z-index.ts` so callers can import either module for the
|
|
2631
|
+
* full token set. See that file's own doc comment for the scale rationale.
|
|
2632
|
+
*/
|
|
2633
|
+
export { zIndex };
|