Skip to content

Commit ae230bb

Browse files
committed
feat(editor): visual flex-layout inspector + editable padding
Replace FlexContainer's five raw enum dropdowns with a compact auto-layout widget — an icon direction switch, a 3x3 alignment grid whose axes swap with the flow direction, a Distribute control and a Stretch toggle — injected through the same extra/hideFields door as the UINode anchor block. Add a first-class 'sides' inspector field ({left,top,right,bottom}) so FlexContainer padding, previously dropped by inferField and uneditable, is authorable through the normal reflected field door: inference -> per-edge merge write -> automation coercion -> an L/T/R/B SidesControl. Reusable for any four-edge box (e.g. TextInput padding). Also honor ESTELLA_WIN_W/H for screenshot automation (default size unchanged).
1 parent 0dc30a9 commit ae230bb

8 files changed

Lines changed: 265 additions & 7 deletions

File tree

desktop/electron/main.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,10 @@ function createWindow() {
359359
if (!isMac) Menu.setApplicationMenu(null);
360360

361361
win = new BrowserWindow({
362-
width: 1480,
363-
height: 920,
362+
// Automation (screenshot capture of tall panels) can enlarge the window via env;
363+
// unset in normal use, so the default size is unchanged. Mirrors ESTELLA_MCP_W/H.
364+
width: Number(process.env.ESTELLA_WIN_W) || 1480,
365+
height: Number(process.env.ESTELLA_WIN_H) || 920,
364366
minWidth: 1080,
365367
minHeight: 680,
366368
title: 'Estella Editor',

desktop/src/engine/EditorControlSurface.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ export function coerceFieldValue(
135135
}
136136
return fail('a "#rrggbbaa" hex string or { r, g, b, a } with 0..1 channels');
137137
}
138+
case 'sides': {
139+
// A four-edge box — accept [left, top, right, bottom] or { left, top, right, bottom }.
140+
let v: unknown = value;
141+
if (typeof v === 'string') v = parseJsonText(v);
142+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
143+
const o = v as Record<string, unknown>;
144+
v = [o.left, o.top, o.right, o.bottom];
145+
}
146+
if (!Array.isArray(v) || v.length < 4 || v.slice(0, 4).some((c) => typeof c !== 'number' || !Number.isFinite(c))) {
147+
return fail('4 numbers (e.g. [8, 8, 8, 8]) or { left, top, right, bottom }');
148+
}
149+
return v.slice(0, 4) as InspectorFieldValue;
150+
}
138151
case 'gradient':
139152
case 'curve':
140153
case 'dimension': {

desktop/src/engine/SceneCommands.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ export function toModelValue(
128128
...(Number.isFinite(w) ? { w } : {}),
129129
};
130130
}
131+
case 'sides': {
132+
// A four-edge box ({ left, top, right, bottom }). Merged onto the base like a vec —
133+
// a NaN edge means "keep this entity's own value" so a single-edge edit fanned
134+
// across a multi-selection never clobbers the untouched edges.
135+
const [l, t, r, b] = value as [number, number, number, number];
136+
const base = cur[key] as { left: number; top: number; right: number; bottom: number };
137+
return {
138+
...base,
139+
...(Number.isFinite(l) ? { left: l } : {}),
140+
...(Number.isFinite(t) ? { top: t } : {}),
141+
...(Number.isFinite(r) ? { right: r } : {}),
142+
...(Number.isFinite(b) ? { bottom: b } : {}),
143+
};
144+
}
131145
case 'vec2list':
132146
// A whole Vec2[] replacement (polygon vertices / chain points) — normalized to
133147
// plain {x,y} so a dragged vertex writes clean data; coalesced to one undo step.

desktop/src/engine/schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ export function inferField(key: string, v: unknown, isColor: boolean): Inspector
216216
if ('x' in o && 'y' in o) return { key, label, type: 'vec2', value: [o.x, o.y] };
217217
// CSS-length shape ({ value, unit }) — the UINode Dimension (width/height/inset/margin).
218218
if ('value' in o && 'unit' in o) return { key, label, type: 'dimension', value: { value: o.value, unit: o.unit } };
219+
// Four-edge box ({ left, top, right, bottom }) — FlexContainer / TextInput padding.
220+
if ('left' in o && 'top' in o && 'right' in o && 'bottom' in o) {
221+
return { key, label, type: 'sides', value: [o.left, o.top, o.right, o.bottom] };
222+
}
219223
}
220224
return null; // unknown shape — not editable here
221225
}

desktop/src/i18n/messages/details.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,33 @@ export const detailsMessages = defineMessages({
7070
zh: '由父级布局决定位置 · 自身对齐覆盖交叉轴。',
7171
},
7272

73+
// — FlexContainer layout widget (direction / alignment grid / distribute / wrap) —
74+
'det.flexDirection': { en: 'Direction', zh: '方向' },
75+
'det.flexDirectionAria': { en: 'Flex direction', zh: '排列方向' },
76+
'det.flexRow': { en: 'Row →', zh: '横向 →' },
77+
'det.flexColumn': { en: 'Column ↓', zh: '纵向 ↓' },
78+
'det.flexRowReverse': { en: 'Row reversed ←', zh: '横向反转 ←' },
79+
'det.flexColumnReverse': { en: 'Column reversed ↑', zh: '纵向反转 ↑' },
80+
'det.flexAlign': { en: 'Align', zh: '对齐' },
81+
'det.flexAlignGridAria': { en: 'Alignment grid', zh: '对齐网格' },
82+
'det.flexAlignCell': { en: 'Align {main} / {cross}', zh: '主轴 {main} · 交叉轴 {cross}' },
83+
'det.flexMainStart': { en: 'main start', zh: '主轴起点' },
84+
'det.flexMainCenter': { en: 'main center', zh: '主轴居中' },
85+
'det.flexMainEnd': { en: 'main end', zh: '主轴终点' },
86+
'det.flexCrossStart': { en: 'cross start', zh: '交叉轴起点' },
87+
'det.flexCrossCenter': { en: 'cross center', zh: '交叉轴居中' },
88+
'det.flexCrossEnd': { en: 'cross end', zh: '交叉轴终点' },
89+
'det.flexDistribute': { en: 'Distribute', zh: '分布' },
90+
'det.flexDistributeAria': { en: 'Main-axis distribution', zh: '主轴分布' },
91+
'det.flexPacked': { en: 'Packed', zh: '紧凑' },
92+
'det.flexBetween': { en: 'Between', zh: '两端' },
93+
'det.flexAround': { en: 'Around', zh: '环绕' },
94+
'det.flexEvenly': { en: 'Evenly', zh: '均匀' },
95+
'det.flexWrap': { en: 'Wrap', zh: '换行' },
96+
'det.flexWrapAria': { en: 'Wrap children onto multiple lines', zh: '子项换到多行' },
97+
'det.flexStretch': { en: 'Stretch', zh: '填满' },
98+
'det.flexStretchAria': { en: 'Stretch children across the cross axis', zh: '子项填满交叉轴' },
99+
73100
// — Component section chrome —
74101
'det.enableComponent': { en: 'Enable component', zh: '启用组件' },
75102
'det.disableComponent': { en: 'Disable component', zh: '禁用组件' },

desktop/src/panels/Details.tsx

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'reac
44
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
55
import {
66
AlertTriangle,
7+
ArrowDown,
8+
ArrowLeft,
9+
ArrowRight,
10+
ArrowUp,
711
Box,
812
Camera,
913
Check,
@@ -55,7 +59,7 @@ import { InspectorClipboard } from '@/engine/inspectorClipboard';
5559
import { SceneCommands, toModelValue } from '@/engine/SceneCommands';
5660
import { ENTITY_SOURCES, createFromSource } from '@/engine/entitySources';
5761
import { PlayInspect } from '@/engine/PlayInspect';
58-
import { DimensionUnit, AnchorAxis, detectAnchorAxes, UIPositionType, parseLocaleTable, EasingType, INVALID_ENTITY } from 'esengine';
62+
import { DimensionUnit, AnchorAxis, detectAnchorAxes, UIPositionType, FlexDirection, FlexWrap, JustifyContent, AlignItems, parseLocaleTable, EasingType, INVALID_ENTITY } from 'esengine';
5963
import type { SceneData, InputMapAsset, ActionType, Binding, LocaleTableAsset, PluralCategory, GearValue, GearTween } from 'esengine';
6064
import { modelAddableComponentEntries, subscribeSchemas, getSchemaRevision, prettyLabel, hexToRgba, dynamicEnumOptions, boxGroupsFor, isRequiredEmpty, type BoxGroupDef } from '@/engine/schema';
6165
import * as imap from '@/project/inputMapDoc';
@@ -248,6 +252,41 @@ export function VecControl({
248252
);
249253
}
250254

255+
// A four-edge box ({ left, top, right, bottom }) as four labelled wells — the padding
256+
// analog of VecControl. Edges are L·T·R·B; each edit writes ONLY its edge (NaN on the
257+
// others) so a single-edge change fanned across a multi-selection keeps the untouched
258+
// edges of the non-primary entities (see toModelValue 'sides').
259+
const SIDE_EDGES = ['l', 't', 'r', 'b'] as const;
260+
export function SidesControl({
261+
value,
262+
mixed,
263+
onBegin,
264+
onEnd,
265+
onCancel,
266+
onChange,
267+
}: ControlGesture & { value: number[]; mixed?: boolean; onChange: (v: number[]) => void }) {
268+
return (
269+
<div className="vec sides">
270+
{SIDE_EDGES.map((edge, i) => (
271+
<VecField
272+
key={edge}
273+
axis={edge}
274+
value={value[i] ?? 0}
275+
mixed={mixed}
276+
onBegin={onBegin}
277+
onEnd={onEnd}
278+
onCancel={onCancel}
279+
onCommit={(v) => {
280+
const next = [NaN, NaN, NaN, NaN];
281+
next[i] = v;
282+
onChange(next);
283+
}}
284+
/>
285+
))}
286+
</div>
287+
);
288+
}
289+
251290
// A named-int dropdown (e.g. Camera projection, body type) — a themed popover, not
252291
// a native <select>, so the list matches the editor and searches when long. The
253292
// stored value is the option's int; an unknown value shows a "(n)" placeholder.
@@ -1287,6 +1326,9 @@ function FieldRow({ entities, comp, field, write }: { entities: EntityId[]; comp
12871326
case 'dimension':
12881327
control = <DimControl value={field.value as DimensionValue} mixed={mixed} onBegin={begin} onEnd={end} onChange={apply} />;
12891328
break;
1329+
case 'sides':
1330+
control = <SidesControl value={field.value as number[]} mixed={mixed} onBegin={begin} onEnd={end} onCancel={cancel} onChange={apply} />;
1331+
break;
12901332
case 'bool':
12911333
control = <BoolControl value={field.value as boolean} mixed={mixed} onBegin={begin} onEnd={end} onChange={apply} />;
12921334
break;
@@ -1710,6 +1752,134 @@ function UILayoutControl({ entities, comp }: { entities: EntityId[]; comp: Inspe
17101752
);
17111753
}
17121754

1755+
// The FlexContainer "auto-layout" widget — direction, a 3×3 alignment grid, main-axis
1756+
// distribution and a cross-axis stretch toggle, in place of five stacked enum dropdowns.
1757+
// The grid's axes swap with the flow direction so the highlighted cell always reads as
1758+
// where the children actually pack (the Figma model). Gap + padding stay as normal
1759+
// fields below (padding via the reflected 'sides' control).
1760+
const FLEX_DIR_OPTS: SegmentedOption<string>[] = [
1761+
{ value: String(FlexDirection.Row), icon: <ArrowRight size={12} strokeWidth={2.2} />, title: t('det.flexRow') },
1762+
{ value: String(FlexDirection.Column), icon: <ArrowDown size={12} strokeWidth={2.2} />, title: t('det.flexColumn') },
1763+
{ value: String(FlexDirection.RowReverse), icon: <ArrowLeft size={12} strokeWidth={2.2} />, title: t('det.flexRowReverse') },
1764+
{ value: String(FlexDirection.ColumnReverse), icon: <ArrowUp size={12} strokeWidth={2.2} />, title: t('det.flexColumnReverse') },
1765+
];
1766+
const FLEX_DISTRIBUTE_OPTS: SegmentedOption<string>[] = [
1767+
{ value: 'packed', label: t('det.flexPacked') },
1768+
{ value: String(JustifyContent.SpaceBetween), label: t('det.flexBetween') },
1769+
{ value: String(JustifyContent.SpaceAround), label: t('det.flexAround') },
1770+
{ value: String(JustifyContent.SpaceEvenly), label: t('det.flexEvenly') },
1771+
];
1772+
const FLEX_MAIN_LABEL = [t('det.flexMainStart'), t('det.flexMainCenter'), t('det.flexMainEnd')];
1773+
const FLEX_CROSS_LABEL = [t('det.flexCrossStart'), t('det.flexCrossCenter'), t('det.flexCrossEnd')];
1774+
// Fields the widget owns, so the generic field flow skips them — gap, padding and the
1775+
// wrap-only alignContent stay as normal rows below (padding via the 'sides' control).
1776+
const FLEX_WIDGET_OWNED_FIELDS: ReadonlySet<string> = new Set(['direction', 'justifyContent', 'alignItems', 'wrap']);
1777+
// Place the cell's dot at its spatial position (col → left/centre/right, row →
1778+
// top/middle/bottom) so the grid previews where children land, Figma-style.
1779+
const FLEX_JUSTIFY_CSS = ['flex-start', 'center', 'flex-end'];
1780+
1781+
function FlexLayoutControl({ entities, comp }: { entities: EntityId[]; comp: InspectorComponent }) {
1782+
const field = (key: string) => comp.fields.find((f) => f.key === key);
1783+
const fieldNum = (key: string, dflt: number) => {
1784+
const f = field(key);
1785+
return f && f.mixed !== true ? Number(f.value) : dflt;
1786+
};
1787+
const dir = fieldNum('direction', FlexDirection.Row);
1788+
const justify = fieldNum('justifyContent', JustifyContent.Start);
1789+
const align = fieldNum('alignItems', AlignItems.Stretch);
1790+
const wrap = fieldNum('wrap', FlexWrap.NoWrap);
1791+
const horizontal = dir === FlexDirection.Row || dir === FlexDirection.RowReverse;
1792+
1793+
const write = (label: string, edits: Array<[string, number]>) => {
1794+
SceneCommands.beginGesture(label);
1795+
for (const id of entities) for (const [k, v] of edits) SceneCommands.setField(id, 'FlexContainer', k, 'enum', v);
1796+
SceneCommands.endGesture();
1797+
};
1798+
1799+
// 3×3 grid cell (col c, row r), c/r ∈ {0 Start, 1 Centre, 2 End}. The MAIN axis is
1800+
// columns when the flow is horizontal, rows when vertical — so a click packs the
1801+
// children where the cell sits. Space-mode justify has no single active cell.
1802+
const packed = justify <= JustifyContent.End;
1803+
const activeMain = packed ? justify : null;
1804+
const activeCross = align <= AlignItems.End ? align : null; // Stretch → no single lane
1805+
const cell = (c: number, r: number) => {
1806+
const main = horizontal ? c : r;
1807+
const cross = horizontal ? r : c;
1808+
return { main, cross, on: activeMain === main && activeCross === cross };
1809+
};
1810+
const distributeValue = justify >= JustifyContent.SpaceBetween ? String(justify) : 'packed';
1811+
const stretched = align === AlignItems.Stretch;
1812+
1813+
return (
1814+
<div className="flex-block">
1815+
<div className="flex-row">
1816+
<span className="flex-lbl">{t('det.flexDirection')}</span>
1817+
<Segmented
1818+
grow
1819+
ariaLabel={t('det.flexDirectionAria')}
1820+
value={field('direction')?.mixed ? '' : String(dir)}
1821+
options={FLEX_DIR_OPTS}
1822+
onChange={(v) => write('Flex Direction', [['direction', Number(v)]])}
1823+
/>
1824+
</div>
1825+
<div className="flex-row flex-align-row">
1826+
<span className="flex-lbl">{t('det.flexAlign')}</span>
1827+
<div className="flex-grid" role="group" aria-label={t('det.flexAlignGridAria')}>
1828+
{[0, 1, 2].map((r) =>
1829+
[0, 1, 2].map((c) => {
1830+
const st = cell(c, r);
1831+
return (
1832+
<button
1833+
key={`${c}-${r}`}
1834+
type="button"
1835+
className={`flex-cell${st.on ? ' on' : ''}${packed ? '' : ' dim'}`}
1836+
style={{ justifyContent: FLEX_JUSTIFY_CSS[c], alignItems: FLEX_JUSTIFY_CSS[r] }}
1837+
title={t('det.flexAlignCell', { main: FLEX_MAIN_LABEL[st.main], cross: FLEX_CROSS_LABEL[st.cross] })}
1838+
aria-pressed={st.on}
1839+
onClick={() => write('Flex Align', [['justifyContent', st.main], ['alignItems', st.cross]])}
1840+
>
1841+
<i />
1842+
</button>
1843+
);
1844+
}),
1845+
)}
1846+
</div>
1847+
<button
1848+
type="button"
1849+
className={`flex-toggle${stretched ? ' on' : ''}`}
1850+
title={t('det.flexStretchAria')}
1851+
aria-pressed={stretched}
1852+
onClick={() => write('Flex Stretch', [['alignItems', stretched ? AlignItems.Center : AlignItems.Stretch]])}
1853+
>
1854+
{t('det.flexStretch')}
1855+
</button>
1856+
</div>
1857+
<div className="flex-row">
1858+
<span className="flex-lbl">{t('det.flexDistribute')}</span>
1859+
<Segmented
1860+
grow
1861+
ariaLabel={t('det.flexDistributeAria')}
1862+
value={distributeValue}
1863+
options={FLEX_DISTRIBUTE_OPTS}
1864+
onChange={(val) => write('Flex Distribute', [['justifyContent', val === 'packed' ? JustifyContent.Start : Number(val)]])}
1865+
/>
1866+
</div>
1867+
<div className="flex-row">
1868+
<span className="flex-lbl">{t('det.flexWrap')}</span>
1869+
<button
1870+
type="button"
1871+
className={`flex-toggle${wrap === FlexWrap.Wrap ? ' on' : ''}`}
1872+
title={t('det.flexWrapAria')}
1873+
aria-pressed={wrap === FlexWrap.Wrap}
1874+
onClick={() => write('Flex Wrap', [['wrap', wrap === FlexWrap.Wrap ? FlexWrap.NoWrap : FlexWrap.Wrap]])}
1875+
>
1876+
{t('det.flexWrap')}
1877+
</button>
1878+
</div>
1879+
</div>
1880+
);
1881+
}
1882+
17131883
// One side of a box-model group: a lettered edge (L/R/T/B) + its Dimension well.
17141884
// It commits through `fieldWriter` — the same door as FieldRow — so undo, mixed,
17151885
// and reset behave identically; right-click keeps the per-field Copy/Paste/Reset.
@@ -3205,16 +3375,20 @@ function EditorDetails() {
32053375
extra={
32063376
comp.name === 'UINode'
32073377
? <UILayoutControl entities={ids} comp={comp} />
3208-
: COMP_COLLIDER_SHAPE[comp.name]
3209-
? <ColliderShapeControl entities={ids} current={comp.name} />
3210-
: undefined
3378+
: comp.name === 'FlexContainer'
3379+
? <FlexLayoutControl entities={ids} comp={comp} />
3380+
: COMP_COLLIDER_SHAPE[comp.name]
3381+
? <ColliderShapeControl entities={ids} current={comp.name} />
3382+
: undefined
32113383
}
32123384
hideFields={
32133385
comp.name === 'UINode'
32143386
? uiLayoutOwnedFields(
32153387
Number(comp.fields.find((f) => f.key === 'position')?.value ?? 0) === UIPositionType.Absolute,
32163388
)
3217-
: undefined
3389+
: comp.name === 'FlexContainer'
3390+
? FLEX_WIDGET_OWNED_FIELDS
3391+
: undefined
32183392
}
32193393
/>
32203394
))}

desktop/src/theme/inspector.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,27 @@
126126
.box-side.modified .box-side-k { color: var(--warn); }
127127
.box-side.mixed .box-side-k { color: var(--text-mute); }
128128

129+
/* FlexContainer auto-layout widget: a labelled direction switch, a 3×3 alignment grid
130+
whose dot sits where children pack, a main-axis Distribute control and a cross-axis
131+
Stretch toggle — a compact visual stand-in for five stacked enum dropdowns. */
132+
.flex-block { padding: 7px 8px 9px; display: flex; flex-direction: column; gap: 9px; }
133+
.flex-row { display: flex; align-items: center; gap: 9px; }
134+
.flex-lbl { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .5px; color: var(--text-mute); flex-shrink: 0; width: 56px; }
135+
.flex-row .seg { flex: 1; min-width: 0; }
136+
.flex-align-row { align-items: stretch; }
137+
.flex-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 2px; width: 66px; height: 66px; flex-shrink: 0; padding: 3px; background: var(--inset); border: 1px solid var(--border); border-radius: var(--r-sm); }
138+
.flex-cell { display: flex; padding: 3px; border-radius: 2px; background: none; border: none; cursor: pointer; transition: background var(--t-fast); }
139+
.flex-cell i { width: 4px; height: 4px; border-radius: 1px; background: var(--text-mute); font-style: normal; transition: background var(--t-fast), transform var(--t-fast); }
140+
.flex-cell:hover { background: var(--srf-4); }
141+
.flex-cell:hover i { background: var(--text-dim); }
142+
.flex-cell.on { background: var(--acc-soft); }
143+
.flex-cell.on i { background: var(--acc); transform: scale(1.6); }
144+
.flex-cell.dim i { opacity: .4; }
145+
.flex-toggle { flex: 1; min-height: 24px; display: flex; align-items: center; justify-content: center; padding: 0 10px; font-size: var(--fs-2xs); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--inset); color: var(--text-dim); cursor: pointer; transition: border-color var(--t-fast), background var(--t-fast), color var(--t-fast); }
146+
.flex-align-row .flex-toggle { flex: 0 0 44px; }
147+
.flex-toggle:hover { border-color: var(--acc); color: var(--text); }
148+
.flex-toggle.on { background: var(--acc); border-color: var(--acc); color: var(--on-accent); }
149+
129150
/* property rows (label col + value col, reset on right) */
130151
.prop { display: grid; grid-template-columns: 38% 1fr 18px; align-items: center; gap: 4px; min-height: var(--h-prop); padding: 1px 0; position: relative; }
131152
.prop-label { font-size: var(--fs-sm); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-left: 2px; }
@@ -166,6 +187,8 @@
166187
.ax.y { background: var(--ax-y); }
167188
.ax.z { background: var(--ax-z); }
168189
.ax.w, .ax.h { background: var(--srf-5); color: var(--text-dim); }
190+
/* Edge-labelled wells (SidesControl: padding L·T·R·B) — neutral, like w/h. */
191+
.ax.l, .ax.t, .ax.r, .ax.b { background: var(--srf-5); color: var(--text-dim); }
169192
.vfield input { width: 100%; min-width: 0; background: none; border: none; outline: none; padding: 0 5px; font-size: var(--fs-xs); color: var(--text); font-family: var(--mono); }
170193

171194
.sw { position: relative; width: 28px; height: 18px; border-radius: var(--r-xs); border: 1px solid var(--border-line); flex-shrink: 0; cursor: pointer; overflow: hidden; padding: 0;

desktop/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type InspectorFieldType =
4343
| 'gradient' // a color-over-life gradient ({ stops: [...] })
4444
| 'curve' // a scalar over-life curve ({ keys: [...] })
4545
| 'dimension' // a CSS-style length ({ value, unit }) — UINode box/inset fields
46+
| 'sides' // a four-edge box ({ left, top, right, bottom }) — FlexContainer/TextInput padding
4647
| 'asset' // a texture/material/font/... ref (@uuid: string, or 0 for none)
4748
| 'entity'; // a reference to another scene entity (source id, or 0/INVALID for none)
4849

0 commit comments

Comments
 (0)