Unify preset/object GUI as collapsible cards; add NumberField for reliable numeric editing

This commit is contained in:
2026-10-17 20:38:37 -04:00
parent e314623c10
commit 5b2bc25ef6
7 changed files with 353 additions and 209 deletions
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>furniture-app</title> <title>furniture-app</title>
<script type="module" crossorigin src="/assets/index-DCx-KkvH.js"></script> <script type="module" crossorigin src="/assets/index-xKTFqHjg.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-YNouK8Wq.css"> <link rel="stylesheet" crossorigin href="/assets/index-DfR9GACM.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+42 -57
View File
@@ -1,44 +1,53 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { RoomCanvas } from './components/RoomCanvas'; import { RoomCanvas } from './components/RoomCanvas';
import { ControlPanel } from './components/ControlPanel'; import { ControlPanel } from './components/ControlPanel';
import type { FurnitureConfig, LayoutState } from './types'; import type { FurnitureConfig, FurniturePreset, LayoutState } from './types';
import { runRandomSearch } from './optimization/randomSearch'; import { runRandomSearch } from './optimization/randomSearch';
import { optimizeWithNN } from './ai/nn'; import { optimizeWithNN } from './ai/nn';
import { DEFAULT_PRESETS } from './furniture/presets';
const initialState: LayoutState = { const initialState: LayoutState = {
room: { width: 600, height: 400, gridSize: 10 }, room: { width: 600, height: 400, gridSize: 10 },
entrance: { x: 30, y: 30 }, entrance: { x: 30, y: 30, width: 20, height: 20 },
furniture: [], furniture: [],
objectCount: 0, presets: DEFAULT_PRESETS,
}; };
function makePreset(type: string, index: number): FurnitureConfig { function syncFurniture(state: LayoutState): LayoutState {
if (type === 'table') { const presets = state.presets || [];
return { let furniture = [...state.furniture];
id: `${type}_${Date.now()}_${index}`, for (const preset of presets) {
name: `Table ${index + 1}`, const desired = preset.amount ?? 0;
type: 'table', const existing = furniture.filter((f) => f.type === preset.id);
shape: { type: 'rectangle', x: 80 + index * 20, y: 80, width: 80, height: 60 }, if (existing.length > desired) {
conditions: [ const removeIds = new Set(
{ kind: 'walk_to_and_sit', weight: 1 }, [...existing]
{ kind: 'accessible', weight: 1 }, .sort((a, b) => Number(a.placed) - Number(b.placed))
], .slice(0, existing.length - desired)
color: '#a1887f', .map((f) => f.id),
lockedForOptimization: false, );
placed: false, furniture = furniture.filter((f) => !removeIds.has(f.id));
}; } else if (existing.length < desired) {
for (let i = existing.length; i < desired; i++) {
furniture.push(makeFurniture(preset, i));
} }
}
}
return { ...state, furniture };
}
function makeFurniture(preset: FurniturePreset, index: number): FurnitureConfig {
return { return {
id: `${type}_${Date.now()}_${index}`, id: `${preset.id}_${Date.now()}_${index}`,
name: `Chair ${index + 1}`, name: `${preset.name} ${index + 1}`,
type: 'chair', type: preset.id,
shape: { type: 'rectangle', x: 120 + index * 20, y: 160, width: 40, height: 40 }, shape: {
conditions: [ ...preset.shape,
{ kind: 'walk_to_and_sit', weight: 1 }, x: 80 + (index % 5) * 60,
{ kind: 'near_object', weight: 1, params: { targetId: '' } }, y: 80 + Math.floor(index / 5) * 60,
{ kind: 'accessible', weight: 1 }, },
], conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })),
color: '#d7ccc5', color: preset.color,
lockedForOptimization: false, lockedForOptimization: false,
placed: false, placed: false,
}; };
@@ -51,32 +60,9 @@ function App() {
const [optimizing, setOptimizing] = useState(false); const [optimizing, setOptimizing] = useState(false);
const [timeLeft, setTimeLeft] = useState(15); const [timeLeft, setTimeLeft] = useState(15);
useEffect(() => { useEffect(() => {
const amounts = state.objectAmounts || { table: 0, chair: 0 }; setState((s) => syncFurniture(s));
const types = ['table', 'chair']; }, [state.presets]);
let furniture = [...state.furniture];
for (const type of types) {
const desired = amounts[type] ?? 0;
let existing = furniture.filter((f) => f.type === type);
if (existing.length > desired) {
// remove extras (unplaced first)
const toRemove = new Set<string>();
const sorted = [...existing].sort((a, b) => Number(a.placed) - Number(b.placed));
for (let i = 0; i < existing.length - desired; i++) {
toRemove.add(sorted[i].id);
}
furniture = furniture.filter((f) => !toRemove.has(f.id));
} else if (existing.length < desired) {
for (let i = existing.length; i < desired; i++) {
furniture.push(makePreset(type, i));
}
}
}
if (furniture.length !== state.furniture.length) {
setState({ ...state, furniture });
}
}, [state.objectAmounts]);
useEffect(() => { useEffect(() => {
if (!optimizing) return; if (!optimizing) return;
@@ -99,11 +85,10 @@ function App() {
}, [optimizing, timeLeft]); }, [optimizing, timeLeft]);
const handleOptimize = () => { const handleOptimize = () => {
const existing = state.furniture.map((f) => ({ setState((s) => ({
...f, ...s,
lockedForOptimization: f.placed ? true : false, furniture: s.furniture.map((f) => ({ ...f, lockedForOptimization: f.placed ? true : false })),
})); }));
setState({ ...state, furniture: existing });
setOptimizing(true); setOptimizing(true);
setTimeLeft(15); setTimeLeft(15);
}; };
+185 -146
View File
@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import type { ConditionKind, Entrance, FurnitureConfig, LayoutState, RoomConfig } from '../types'; import type { ConditionKind, Entrance, FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types';
import { NumberField } from './NumberField';
interface ControlPanelProps { interface ControlPanelProps {
state: LayoutState; state: LayoutState;
@@ -79,16 +80,14 @@ export function ControlPanel({
setOpenId(id); setOpenId(id);
}; };
const updateObject = (id: string, patch: Partial<FurnitureConfig>) => { const updatePreset = (id: string, patch: Partial<FurniturePreset>) => {
const updated = state.furniture.map((f) => (f.id === id ? { ...f, ...patch } : f)); const presets = (state.presets || []).map((p) => (p.id === id ? { ...p, ...patch } : p));
onStateChange({ ...state, furniture: updated }); onStateChange({ ...state, presets });
}; };
const updateSize = (id: string, width: number, height: number) => { const setPresetAmount = (id: string, amount: number) => {
const updated = state.furniture.map((f) => const presets = (state.presets || []).map((p) => (p.id === id ? { ...p, amount: Math.max(0, amount) } : p));
f.id === id ? { ...f, shape: { ...f.shape, width: Math.max(10, width), height: Math.max(10, height) } } : f, onStateChange({ ...state, presets });
);
onStateChange({ ...state, furniture: updated });
}; };
const toggleLock = (id: string) => { const toggleLock = (id: string) => {
@@ -111,19 +110,35 @@ export function ControlPanel({
onStateChange({ ...state, furniture: updated }); onStateChange({ ...state, furniture: updated });
}; };
const adjustAmount = (type: string, delta: number) => { const toggleFurnitureCondition = (id: string, k: ConditionKind, enabled: boolean) => {
const current = state.objectAmounts?.[type] ?? 0; const updated = state.furniture.map((x) => {
onStateChange({ if (x.id !== id) return x;
...state, const has = x.conditions.some((c) => c.kind === k);
objectAmounts: { ...(state.objectAmounts || {}), [type]: Math.max(0, current + delta) }, if (enabled && !has) {
const params = k === 'near_object' ? { targetId: '' } : {};
return { ...x, conditions: [...x.conditions, { kind: k, weight: 1, params }] };
}
if (!enabled && has) {
return { ...x, conditions: x.conditions.filter((c) => c.kind !== k) };
}
return x;
}); });
onStateChange({ ...state, furniture: updated });
}; };
const setAmount = (type: string, value: number) => { const togglePresetCondition = (id: string, k: ConditionKind, enabled: boolean) => {
onStateChange({ const presets = (state.presets || []).map((p) => {
...state, if (p.id !== id) return p;
objectAmounts: { ...(state.objectAmounts || {}), [type]: Math.max(0, value) }, const has = p.conditions.some((c) => c.kind === k);
if (enabled && !has) {
return { ...p, conditions: [...p.conditions, { kind: k, weight: 1, params: {} }] };
}
if (!enabled && has) {
return { ...p, conditions: p.conditions.filter((c) => c.kind !== k) };
}
return p;
}); });
onStateChange({ ...state, presets });
}; };
const toggleOpen = (id: string) => { const toggleOpen = (id: string) => {
@@ -131,46 +146,144 @@ export function ControlPanel({
setOpenId((cur) => (cur === id ? null : id)); setOpenId((cur) => (cur === id ? null : id));
}; };
const conditionsEditor = (
conditions: FurnitureConfig['conditions'],
onToggle: (k: ConditionKind, enabled: boolean) => void,
nearSelect?: { value: string; onChange: (v: string) => void; options: { id: string; name: string }[] },
) => (
<div className="cond-list">
{CONDITION_KINDS.map((k) => {
const active = conditions.some((c) => c.kind === k);
return (
<div className="cond" key={k}>
<label>
<input type="checkbox" checked={active} onChange={(e) => onToggle(k, e.target.checked)} />
{CONDITION_LABELS[k]}
</label>
{k === 'near_object' && active && nearSelect && (
<select
value={nearSelect.value}
onClick={(e) => e.stopPropagation()}
onChange={(e) => nearSelect.onChange(e.target.value)}
>
<option value="">Select object…</option>
{nearSelect.options.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
)}
</div>
);
})}
</div>
);
const renderPresetCard = (p: FurniturePreset) => {
const open = openId === 'preset_' + p.id;
return (
<div className={`obj-card ${open ? 'open' : ''}`} key={p.id}>
<div className="obj-head" onClick={() => toggleOpen('preset_' + p.id)}>
<span className={`obj-caret ${open ? 'open' : ''}`}>▶</span>
<span className="obj-swatch" style={{ background: p.color }} />
<span className="obj-name">{p.name}</span>
<span className="obj-count" onClick={(e) => e.stopPropagation()}>
<NumberField
value={p.amount ?? 0}
min={0}
width={44}
onChange={(v) => setPresetAmount(p.id, v)}
/>
</span>
</div>
{open && (
<div className="obj-body">
<div className="grid-2">
<label className="field">
<span>Width</span>
<NumberField
value={p.shape.width}
min={10}
width="100%"
onChange={(v) => updatePreset(p.id, { shape: { ...p.shape, width: v } })}
/>
</label>
<label className="field">
<span>Height</span>
<NumberField
value={p.shape.height}
min={10}
width="100%"
onChange={(v) => updatePreset(p.id, { shape: { ...p.shape, height: v } })}
/>
</label>
</div>
<div className="section-title" style={{ marginTop: 12 }}>
Default Conditions
</div>
{conditionsEditor(p.conditions, (k, enabled) => togglePresetCondition(p.id, k, enabled))}
</div>
)}
</div>
);
};
const renderObjectCard = (f: FurnitureConfig) => { const renderObjectCard = (f: FurnitureConfig) => {
const open = openId === f.id; const open = openId === f.id;
const selected = selectedId === f.id; const selected = selectedId === f.id;
return ( return (
<div key={f.id} className={`obj-card${selected ? ' selected' : ''}`}> <div key={f.id} className={`obj-card ${selected ? 'selected' : ''}`}>
<div className="obj-head" onClick={() => toggleOpen(f.id)}> <div className="obj-head" onClick={() => toggleOpen(f.id)}>
<span className={`obj-caret ${open ? 'open' : ''}`}>{'\u25B6'}</span> <span className={`obj-caret ${open ? 'open' : ''}`}>▶</span>
<span className="obj-swatch" style={{ background: f.color || '#9aa0a6' }} /> <span className="obj-swatch" style={{ background: f.color || '#9aa0a6' }} />
<span className="obj-name">{f.name}</span> <span className="obj-name">{f.name}</span>
{f.lockedForOptimization && <span className="obj-badge locked">locked</span>} {f.lockedForOptimization && <span className="obj-badge locked">locked</span>}
</div> </div>
{open && ( {open && (
<div className="obj-body"> <div className="obj-body">
<div className="field" style={{ marginBottom: 8 }}> <label className="field">
<span>Name</span> <span>Name</span>
<input value={f.name} onChange={(e) => updateObject(f.id, { name: e.target.value })} /> <input
</div> value={f.name}
onClick={(e) => e.stopPropagation()}
<div className="grid-2"> onChange={(e) => {
const updated = state.furniture.map((x) => (x.id === f.id ? { ...x, name: e.target.value } : x));
onStateChange({ ...state, furniture: updated });
}}
/>
</label>
<div className="grid-2" style={{ marginTop: 8 }}>
<label className="field"> <label className="field">
<span>Width</span> <span>Width</span>
<input <NumberField
type="number"
min={10}
value={f.shape.width} value={f.shape.width}
onChange={(e) => updateSize(f.id, Number(e.target.value), f.shape.height)} min={10}
width="100%"
onChange={(v) => {
const updated = state.furniture.map((x) =>
x.id === f.id ? { ...x, shape: { ...x.shape, width: v } } : x,
);
onStateChange({ ...state, furniture: updated });
}}
/> />
</label> </label>
<label className="field"> <label className="field">
<span>Height</span> <span>Height</span>
<input <NumberField
type="number"
min={10}
value={f.shape.height} value={f.shape.height}
onChange={(e) => updateSize(f.id, f.shape.width, Number(e.target.value))} min={10}
width="100%"
onChange={(v) => {
const updated = state.furniture.map((x) =>
x.id === f.id ? { ...x, shape: { ...x.shape, height: v } } : x,
);
onStateChange({ ...state, furniture: updated });
}}
/> />
</label> </label>
</div> </div>
<div className="row" style={{ marginTop: 10 }}>
<div className="row" style={{ marginTop: 10, justifyContent: 'space-between' }}>
<label> <label>
<input <input
type="checkbox" type="checkbox"
@@ -180,56 +293,21 @@ export function ControlPanel({
Lock position Lock position
</label> </label>
</div> </div>
<div className="section-title" style={{ marginTop: 12 }}> <div className="section-title" style={{ marginTop: 12 }}>
Conditions Conditions
</div> </div>
<div className="cond-list"> {conditionsEditor(
{CONDITION_KINDS.map((k) => { f.conditions,
const active = f.conditions.some((c) => c.kind === k); (k, enabled) => toggleFurnitureCondition(f.id, k, enabled),
const targetId = (f.conditions.find((c) => c.kind === k)?.params as { targetId?: string } | undefined) {
?.targetId; value:
return ( (f.conditions.find((c) => c.kind === 'near_object')?.params as { targetId?: string } | undefined)
<div className="cond" key={k}> ?.targetId || '',
<label> onChange: (v) => setNearObjectTarget(f.id, v),
<input options: state.furniture.filter((o) => o.id !== f.id).map((o) => ({ id: o.id, name: o.name })),
type="checkbox" },
checked={active}
onChange={(e) => {
const updated = state.furniture.map((x) => {
if (x.id !== f.id) return x;
const has = x.conditions.some((c) => c.kind === k);
if (e.target.checked && !has) {
const params = k === 'near_object' ? { targetId: '' } : {};
return { ...x, conditions: [...x.conditions, { kind: k, weight: 1, params }] };
}
if (!e.target.checked && has) {
return { ...x, conditions: x.conditions.filter((c) => c.kind !== k) };
}
return x;
});
onStateChange({ ...state, furniture: updated });
}}
/>
{CONDITION_LABELS[k]}
</label>
{k === 'near_object' && active && (
<select value={targetId || ''} onChange={(e) => setNearObjectTarget(f.id, e.target.value)}>
<option value="">Select object…</option>
{state.furniture
.filter((other) => other.id !== f.id)
.map((other) => (
<option key={other.id} value={other.id}>
{other.name}
</option>
))}
</select>
)} )}
</div> </div>
);
})}
</div>
</div>
)} )}
</div> </div>
); );
@@ -243,16 +321,10 @@ export function ControlPanel({
Venue Planner Venue Planner
</div> </div>
<div className="segmented" style={{ marginTop: 12 }}> <div className="segmented" style={{ marginTop: 12 }}>
<button <button className={mode === 'layout' ? 'active' : ''} onClick={() => onModeChange('layout')}>
className={mode === 'layout' ? 'active' : ''}
onClick={() => onModeChange('layout')}
>
Layout Layout
</button> </button>
<button <button className={mode === 'optimize' ? 'active' : ''} onClick={() => onModeChange('optimize')}>
className={mode === 'optimize' ? 'active' : ''}
onClick={() => onModeChange('optimize')}
>
Optimize Optimize
</button> </button>
</div> </div>
@@ -266,18 +338,20 @@ export function ControlPanel({
<div className="grid-2"> <div className="grid-2">
<label className="field"> <label className="field">
<span>Width</span> <span>Width</span>
<input <NumberField
type="number"
value={state.room.width} value={state.room.width}
onChange={(e) => setRoom({ width: Number(e.target.value) })} min={100}
width="100%"
onChange={(v) => setRoom({ width: v })}
/> />
</label> </label>
<label className="field"> <label className="field">
<span>Height</span> <span>Height</span>
<input <NumberField
type="number"
value={state.room.height} value={state.room.height}
onChange={(e) => setRoom({ height: Number(e.target.value) })} min={100}
width="100%"
onChange={(v) => setRoom({ height: v })}
/> />
</label> </label>
</div> </div>
@@ -296,41 +370,8 @@ export function ControlPanel({
</div> </div>
<div className="section"> <div className="section">
<div className="section-title">Furniture Presets</div> <div className="section-title">Presets</div>
<div className="obj-list"> <div className="obj-list">{(state.presets || []).map(renderPresetCard)}</div>
<div className="counter">
<span className="counter-label">
<span className="swatch" style={{ background: '#a1887f' }} />
Tables
</span>
<span className="stepper">
<button onClick={() => adjustAmount('table', -1)}>−</button>
<input
type="number"
min={0}
value={state.objectAmounts?.table ?? 0}
onChange={(e) => setAmount('table', Number(e.target.value))}
/>
<button onClick={() => adjustAmount('table', 1)}>+</button>
</span>
</div>
<div className="counter">
<span className="counter-label">
<span className="swatch" style={{ background: '#d7ccc5' }} />
Chairs
</span>
<span className="stepper">
<button onClick={() => adjustAmount('chair', -1)}>−</button>
<input
type="number"
min={0}
value={state.objectAmounts?.chair ?? 0}
onChange={(e) => setAmount('chair', Number(e.target.value))}
/>
<button onClick={() => adjustAmount('chair', 1)}>+</button>
</span>
</div>
</div>
</div> </div>
<div className="section"> <div className="section">
@@ -350,36 +391,36 @@ export function ControlPanel({
<div className="grid-2"> <div className="grid-2">
<label className="field"> <label className="field">
<span>X</span> <span>X</span>
<input <NumberField
type="number"
value={state.entrance.x} value={state.entrance.x}
onChange={(e) => setEntrance({ x: Number(e.target.value) })} width="100%"
onChange={(v) => setEntrance({ x: v })}
/> />
</label> </label>
<label className="field"> <label className="field">
<span>Y</span> <span>Y</span>
<input <NumberField
type="number"
value={state.entrance.y} value={state.entrance.y}
onChange={(e) => setEntrance({ y: Number(e.target.value) })} width="100%"
onChange={(v) => setEntrance({ y: v })}
/> />
</label> </label>
<label className="field"> <label className="field">
<span>Width</span> <span>Width</span>
<input <NumberField
type="number"
min={10}
value={state.entrance.width ?? 20} value={state.entrance.width ?? 20}
onChange={(e) => setEntrance({ width: Math.max(10, Number(e.target.value)) })} min={10}
width="100%"
onChange={(v) => setEntrance({ width: v })}
/> />
</label> </label>
<label className="field"> <label className="field">
<span>Height</span> <span>Height</span>
<input <NumberField
type="number"
min={10}
value={state.entrance.height ?? 20} value={state.entrance.height ?? 20}
onChange={(e) => setEntrance({ height: Math.max(10, Number(e.target.value)) })} min={10}
width="100%"
onChange={(v) => setEntrance({ height: v })}
/> />
</label> </label>
</div> </div>
@@ -387,7 +428,7 @@ export function ControlPanel({
)} )}
</div> </div>
)} )}
{state.furniture.length === 0 && !state.entrance ? ( {state.furniture.length === 0 ? (
<div className="empty">No objects yet — add a preset or create one.</div> <div className="empty">No objects yet — add a preset or create one.</div>
) : ( ) : (
<div className="obj-list">{state.furniture.map(renderObjectCard)}</div> <div className="obj-list">{state.furniture.map(renderObjectCard)}</div>
@@ -407,11 +448,9 @@ export function ControlPanel({
{optimizing ? `Optimizing… ${timeLeft}s` : 'Optimize Layout'} {optimizing ? `Optimizing… ${timeLeft}s` : 'Optimize Layout'}
</button> </button>
{optimizing && ( {optimizing && (
<>
<div className="progress"> <div className="progress">
<div style={{ width: `${((15 - timeLeft) / 15) * 100}%` }} /> <div style={{ width: `${((15 - timeLeft) / 15) * 100}%` }} />
</div> </div>
</>
)} )}
</div> </div>
)} )}
+73
View File
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
interface NumberFieldProps {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
width?: number | string;
className?: string;
title?: string;
}
export function NumberField({ value, onChange, min, max, step, width, className, title }: NumberFieldProps) {
const [text, setText] = useState<string>(String(value));
useEffect(() => {
setText(String(value));
}, [value]);
const clamp = (n: number) => {
if (min !== undefined) n = Math.max(min, n);
if (max !== undefined) n = Math.min(max, n);
return n;
};
return (
<input
type="text"
inputMode="numeric"
className={className}
title={title}
style={width ? { width } : undefined}
value={text}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => {
const raw = e.target.value;
if (!/^-?\d*$/.test(raw)) return;
setText(raw);
if (raw === '' || raw === '-') return;
const parsed = Number(raw);
if (!Number.isNaN(parsed)) {
const next = clamp(parsed);
if (next !== value) onChange(next);
}
}}
onBlur={() => {
if (text === '' || text === '-') {
const fallback = clamp(value);
onChange(fallback);
setText(String(fallback));
} else {
setText(String(value));
}
}}
onKeyDown={(e) => {
if (e.key === 'ArrowUp' && step) {
e.preventDefault();
const next = clamp(value + step);
onChange(next);
setText(String(next));
}
if (e.key === 'ArrowDown' && step) {
e.preventDefault();
const next = clamp(value - step);
onChange(next);
setText(String(next));
}
}}
/>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { FurniturePreset } from '../types';
export const DEFAULT_PRESETS: FurniturePreset[] = [
{
id: 'table',
name: 'Table',
color: '#a1887f',
amount: 0,
shape: { type: 'rectangle', width: 80, height: 60 },
conditions: [
{ kind: 'walk_to_and_sit', weight: 1 },
{ kind: 'accessible', weight: 1 },
],
},
{
id: 'chair',
name: 'Chair',
color: '#d7ccc5',
amount: 0,
shape: { type: 'rectangle', width: 40, height: 40 },
conditions: [
{ kind: 'walk_to_and_sit', weight: 1 },
{ kind: 'near_object', weight: 1, params: { targetId: '' } },
{ kind: 'accessible', weight: 1 },
],
},
];
+19
View File
@@ -378,6 +378,25 @@ label {
background: rgba(224, 168, 62, 0.14); background: rgba(224, 168, 62, 0.14);
} }
.obj-count {
display: flex;
align-items: center;
}
.obj-count input {
text-align: center;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 6px;
font-weight: 600;
}
.obj-count input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.obj-body { .obj-body {
padding: 4px 10px 12px; padding: 4px 10px 12px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
+1
View File
@@ -95,6 +95,7 @@ export interface FurniturePreset {
shape: Omit<ShapeData, 'x' | 'y'>; shape: Omit<ShapeData, 'x' | 'y'>;
conditions: ConditionSpec[]; conditions: ConditionSpec[];
color?: string; color?: string;
amount?: number;
} }
export interface LayoutState { export interface LayoutState {