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 (
|
return (
|
||||||
<div className="w-full space-y-2">
|
// Fill the host's scroll slot and own the scroll internally: the eraser /
|
||||||
<div className="flex items-center gap-2">
|
// 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
|
<Button
|
||||||
aria-pressed={paintEraser}
|
aria-pressed={paintEraser}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -79,6 +82,7 @@ export function MaterialPaintPanel() {
|
|||||||
Reset all
|
Reset all
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="subtle-scrollbar min-h-0 flex-1 space-y-2 overflow-y-auto">
|
||||||
<MaterialPicker
|
<MaterialPicker
|
||||||
onChange={(material) => {
|
onChange={(material) => {
|
||||||
// Custom-create: pre-create a scene material and select it as the
|
// Custom-create: pre-create a scene material and select it as the
|
||||||
@@ -106,5 +110,6 @@ export function MaterialPaintPanel() {
|
|||||||
</PanelSection>
|
</PanelSection>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function MaterialPicker({
|
|||||||
<div className={`min-w-0 space-y-3 ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
|
<div className={`min-w-0 space-y-3 ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
|
||||||
{(catalogItems.length > 0 || onChange) && (
|
{(catalogItems.length > 0 || onChange) && (
|
||||||
<div className="min-w-0 space-y-1">
|
<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) => (
|
{availableCategories.map((category) => (
|
||||||
<button
|
<button
|
||||||
className={`rounded-full px-3 py-1 font-medium text-xs transition-colors ${
|
className={`rounded-full px-3 py-1 font-medium text-xs transition-colors ${
|
||||||
@@ -107,9 +107,11 @@ export function MaterialPicker({
|
|||||||
key={category}
|
key={category}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedCategory(category)
|
setSelectedCategory(category)
|
||||||
if (showCustom) {
|
// Auto-select the first material in the category so the brush
|
||||||
setShowCustom(false)
|
// 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"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -126,7 +128,7 @@ export function MaterialPicker({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent ${
|
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}
|
key={item.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -159,7 +161,7 @@ export function MaterialPicker({
|
|||||||
{selectedCategory === 'colors' && onChange ? (
|
{selectedCategory === 'colors' && onChange ? (
|
||||||
<button
|
<button
|
||||||
className={`group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent ${
|
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={() => {
|
onClick={() => {
|
||||||
triggerSFX('sfx:menu-click')
|
triggerSFX('sfx:menu-click')
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
|||||||
const updateSceneMaterial = useScene((state) => state.updateSceneMaterial)
|
const updateSceneMaterial = useScene((state) => state.updateSceneMaterial)
|
||||||
const removeSceneMaterial = useScene((state) => state.removeSceneMaterial)
|
const removeSceneMaterial = useScene((state) => state.removeSceneMaterial)
|
||||||
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
||||||
|
const activePaintRef = useEditor((state) => state.activePaintMaterial?.materialPreset)
|
||||||
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
||||||
|
|
||||||
const materialEntries = useMemo(
|
const materialEntries = useMemo(
|
||||||
@@ -71,6 +72,7 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
|||||||
activePaintTarget={activePaintTarget}
|
activePaintTarget={activePaintTarget}
|
||||||
autoEdit={autoEditId === id}
|
autoEdit={autoEditId === id}
|
||||||
id={id}
|
id={id}
|
||||||
|
isActive={activePaintRef === toSceneMaterialRef(id)}
|
||||||
key={id}
|
key={id}
|
||||||
removeSceneMaterial={removeSceneMaterial}
|
removeSceneMaterial={removeSceneMaterial}
|
||||||
sceneMaterial={sceneMaterial}
|
sceneMaterial={sceneMaterial}
|
||||||
@@ -89,6 +91,7 @@ function SceneMaterialRow({
|
|||||||
usageCount,
|
usageCount,
|
||||||
activePaintTarget,
|
activePaintTarget,
|
||||||
autoEdit,
|
autoEdit,
|
||||||
|
isActive,
|
||||||
addSceneMaterial,
|
addSceneMaterial,
|
||||||
updateSceneMaterial,
|
updateSceneMaterial,
|
||||||
removeSceneMaterial,
|
removeSceneMaterial,
|
||||||
@@ -99,6 +102,7 @@ function SceneMaterialRow({
|
|||||||
usageCount: number
|
usageCount: number
|
||||||
activePaintTarget: ReturnType<typeof useEditor.getState>['activePaintTarget']
|
activePaintTarget: ReturnType<typeof useEditor.getState>['activePaintTarget']
|
||||||
autoEdit: boolean
|
autoEdit: boolean
|
||||||
|
isActive: boolean
|
||||||
addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
|
addSceneMaterial: ReturnType<typeof useScene.getState>['addSceneMaterial']
|
||||||
updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
|
updateSceneMaterial: ReturnType<typeof useScene.getState>['updateSceneMaterial']
|
||||||
removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
|
removeSceneMaterial: ReturnType<typeof useScene.getState>['removeSceneMaterial']
|
||||||
@@ -133,7 +137,11 @@ function SceneMaterialRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="h-8 w-8 shrink-0 rounded-md border border-border/70"
|
className="h-8 w-8 shrink-0 rounded-md border border-border/70"
|
||||||
|
|||||||
Reference in New Issue
Block a user