feat(paint-slots): paint panel polish — sticky controls, selection outlines, auto-select
- MaterialPaintPanel owns its scroll: the eraser/reset row stays pinned and the category tabs stick to the top, so only the material list scrolls. - Selected catalog swatch + active scene-material card use the same `ring-1 ring-primary ring-inset` outline as item/preset tiles. - Choosing a material category auto-selects its first material. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ff2a24795c
commit
8badd415b9
@@ -56,8 +56,11 @@ export function MaterialPaintPanel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
// Fill the host's scroll slot and own the scroll internally: the eraser /
|
||||
// reset row stays pinned (shrink-0) while only the material list below
|
||||
// scrolls. The category tabs pin too (sticky, inside the scroll region).
|
||||
<div className="flex h-full min-h-0 w-full flex-col">
|
||||
<div className="flex shrink-0 items-center gap-2 pb-2">
|
||||
<Button
|
||||
aria-pressed={paintEraser}
|
||||
className="flex-1"
|
||||
@@ -79,32 +82,34 @@ export function MaterialPaintPanel() {
|
||||
Reset all
|
||||
</Button>
|
||||
</div>
|
||||
<MaterialPicker
|
||||
onChange={(material) => {
|
||||
// Custom-create: pre-create a scene material and select it as the
|
||||
// brush via a `scene:` ref so painting stores the ref and edits to
|
||||
// it propagate everywhere. The user edits it inline in the scene-
|
||||
// material list below (auto-opened) — no separate right-side pane.
|
||||
const id = generateSceneMaterialId()
|
||||
const count = Object.keys(useScene.getState().materials).length
|
||||
useScene.getState().addSceneMaterial({ id, name: `Material ${count + 1}`, material })
|
||||
setActivePaintMaterial({
|
||||
materialPreset: toSceneMaterialRef(id),
|
||||
sourceTarget: activePaintTarget,
|
||||
})
|
||||
setAutoEditMaterialId(id)
|
||||
}}
|
||||
onSelectMaterialPreset={(materialPreset) => {
|
||||
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
||||
}}
|
||||
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
||||
value={activePaintMaterial?.material}
|
||||
/>
|
||||
{materialCount > 0 ? (
|
||||
<PanelSection title="Scene materials">
|
||||
<SceneMaterialList autoEditId={autoEditMaterialId} />
|
||||
</PanelSection>
|
||||
) : null}
|
||||
<div className="subtle-scrollbar min-h-0 flex-1 space-y-2 overflow-y-auto">
|
||||
<MaterialPicker
|
||||
onChange={(material) => {
|
||||
// Custom-create: pre-create a scene material and select it as the
|
||||
// brush via a `scene:` ref so painting stores the ref and edits to
|
||||
// it propagate everywhere. The user edits it inline in the scene-
|
||||
// material list below (auto-opened) — no separate right-side pane.
|
||||
const id = generateSceneMaterialId()
|
||||
const count = Object.keys(useScene.getState().materials).length
|
||||
useScene.getState().addSceneMaterial({ id, name: `Material ${count + 1}`, material })
|
||||
setActivePaintMaterial({
|
||||
materialPreset: toSceneMaterialRef(id),
|
||||
sourceTarget: activePaintTarget,
|
||||
})
|
||||
setAutoEditMaterialId(id)
|
||||
}}
|
||||
onSelectMaterialPreset={(materialPreset) => {
|
||||
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
||||
}}
|
||||
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
||||
value={activePaintMaterial?.material}
|
||||
/>
|
||||
{materialCount > 0 ? (
|
||||
<PanelSection title="Scene materials">
|
||||
<SceneMaterialList autoEditId={autoEditMaterialId} />
|
||||
</PanelSection>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function MaterialPicker({
|
||||
<div className={`min-w-0 space-y-3 ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
|
||||
{(catalogItems.length > 0 || onChange) && (
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex flex-wrap gap-1 pb-1">
|
||||
<div className="sticky top-0 z-10 flex flex-wrap gap-1 bg-sidebar pb-1">
|
||||
{availableCategories.map((category) => (
|
||||
<button
|
||||
className={`rounded-full px-3 py-1 font-medium text-xs transition-colors ${
|
||||
@@ -107,9 +107,11 @@ export function MaterialPicker({
|
||||
key={category}
|
||||
onClick={() => {
|
||||
setSelectedCategory(category)
|
||||
if (showCustom) {
|
||||
setShowCustom(false)
|
||||
}
|
||||
// Auto-select the first material in the category so the brush
|
||||
// is immediately ready (and the row shows as selected).
|
||||
const first = getMaterialsForCategory(category)[0]
|
||||
if (first) handleCatalogSelect(first.id)
|
||||
else if (showCustom) setShowCustom(false)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
@@ -126,7 +128,7 @@ export function MaterialPicker({
|
||||
return (
|
||||
<button
|
||||
className={`group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent ${
|
||||
isSelected ? 'bg-sidebar-accent ring-2 ring-primary-foreground' : ''
|
||||
isSelected ? 'bg-sidebar-accent ring-1 ring-primary ring-inset' : ''
|
||||
}`}
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
@@ -159,7 +161,7 @@ export function MaterialPicker({
|
||||
{selectedCategory === 'colors' && onChange ? (
|
||||
<button
|
||||
className={`group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent ${
|
||||
showCustom ? 'bg-sidebar-accent ring-2 ring-primary-foreground' : ''
|
||||
showCustom ? 'bg-sidebar-accent ring-1 ring-primary ring-inset' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
triggerSFX('sfx:menu-click')
|
||||
|
||||
@@ -32,6 +32,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
||||
const updateSceneMaterial = useScene((state) => state.updateSceneMaterial)
|
||||
const removeSceneMaterial = useScene((state) => state.removeSceneMaterial)
|
||||
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
||||
const activePaintRef = useEditor((state) => state.activePaintMaterial?.materialPreset)
|
||||
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
||||
|
||||
const materialEntries = useMemo(
|
||||
@@ -71,6 +72,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
||||
activePaintTarget={activePaintTarget}
|
||||
autoEdit={autoEditId === id}
|
||||
id={id}
|
||||
isActive={activePaintRef === toSceneMaterialRef(id)}
|
||||
key={id}
|
||||
removeSceneMaterial={removeSceneMaterial}
|
||||
sceneMaterial={sceneMaterial}
|
||||
@@ -89,6 +91,7 @@ function SceneMaterialRow({
|
||||
usageCount,
|
||||
activePaintTarget,
|
||||
autoEdit,
|
||||
isActive,
|
||||
addSceneMaterial,
|
||||
updateSceneMaterial,
|
||||
removeSceneMaterial,
|
||||
@@ -99,6 +102,7 @@ function SceneMaterialRow({
|
||||
usageCount: number
|
||||
activePaintTarget: ReturnType<typeof useEditor.getState>['activePaintTarget']
|
||||
autoEdit: boolean
|
||||
isActive: boolean
|
||||
addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
|
||||
updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
|
||||
removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
|
||||
@@ -133,7 +137,11 @@ function SceneMaterialRow({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border/60 bg-background/40 p-2">
|
||||
<div
|
||||
className={`rounded-md border border-border/60 bg-background/40 p-2 ${
|
||||
isActive ? 'ring-1 ring-primary ring-inset' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-8 w-8 shrink-0 rounded-md border border-border/70"
|
||||
|
||||
Reference in New Issue
Block a user