parametric-inspector: render def.presentation.icon before the title
User pointed out the auto-derived panel was title-only; the legacy panels rendered a small icon in front. Legacy used a URL path (next Image). The registry-driven path has structured `IconRef` values (iconify / svg / lazy component) declared on `def.presentation.icon`. - PanelWrapper's `icon` prop now accepts `string | React.ReactNode`. String → next/image (legacy URL behavior). Node → rendered as-is. - ParametricInspector resolves `def.presentation.icon` to a node: iconify → `<Icon icon="lucide:fence" />`, svg → inline svg, component → Suspense + lazy. Kind-owned custom panels (slab/ceiling) keep their existing legacy URL icons since they pass `<PanelWrapper>` themselves — no change needed there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
282cb22585
commit
8c2b03f99b
@@ -7,7 +7,11 @@ import { cn } from '../../../lib/utils'
|
|||||||
|
|
||||||
interface PanelWrapperProps {
|
interface PanelWrapperProps {
|
||||||
title: string
|
title: string
|
||||||
icon?: string
|
/** Either a URL path (legacy panels pass `/icons/floor.png` etc.,
|
||||||
|
* rendered via next/image) OR a React node (registry-driven
|
||||||
|
* inspector renders `<Icon icon="lucide:fence" />` from
|
||||||
|
* `def.presentation.icon`). */
|
||||||
|
icon?: string | React.ReactNode
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
onReset?: () => void
|
onReset?: () => void
|
||||||
onBack?: () => void
|
onBack?: () => void
|
||||||
@@ -58,9 +62,18 @@ export function PanelWrapper({
|
|||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{icon && (
|
{icon &&
|
||||||
<Image alt="" className="shrink-0 object-contain" height={16} src={icon} width={16} />
|
(typeof icon === 'string' ? (
|
||||||
)}
|
<Image
|
||||||
|
alt=""
|
||||||
|
className="shrink-0 object-contain"
|
||||||
|
height={16}
|
||||||
|
src={icon}
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="flex shrink-0 items-center justify-center">{icon}</span>
|
||||||
|
))}
|
||||||
<h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
|
<h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
import {
|
import {
|
||||||
type AnyNode,
|
type AnyNode,
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
|
type IconRef,
|
||||||
nodeRegistry,
|
nodeRegistry,
|
||||||
type ParamField,
|
type ParamField,
|
||||||
useScene,
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { Icon } from '@iconify/react'
|
||||||
import { Move, Trash2 } from 'lucide-react'
|
import { Move, Trash2 } from 'lucide-react'
|
||||||
import { type ComponentType, lazy, Suspense, useCallback } from 'react'
|
import { type ComponentType, lazy, Suspense, useCallback } from 'react'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
@@ -94,11 +96,12 @@ export function ParametricInspector() {
|
|||||||
|
|
||||||
const presentation = def.presentation
|
const presentation = def.presentation
|
||||||
const title = presentation?.label ?? nodeType ?? ''
|
const title = presentation?.label ?? nodeType ?? ''
|
||||||
|
const iconNode = renderIcon(presentation?.icon)
|
||||||
const canMove = !!def.capabilities.movable
|
const canMove = !!def.capabilities.movable
|
||||||
const canDelete = def.capabilities.deletable !== false
|
const canDelete = def.capabilities.deletable !== false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelWrapper onClose={handleClose} title={title} width={320}>
|
<PanelWrapper icon={iconNode} onClose={handleClose} title={title} width={320}>
|
||||||
{parametrics.groups.map((group, gi) => (
|
{parametrics.groups.map((group, gi) => (
|
||||||
<PanelSection key={`group-${gi}`} title={group.label}>
|
<PanelSection key={`group-${gi}`} title={group.label}>
|
||||||
{group.fields.map((field, fi) => (
|
{group.fields.map((field, fi) => (
|
||||||
@@ -132,6 +135,27 @@ export function ParametricInspector() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
|
||||||
|
if (!ref) return undefined
|
||||||
|
if (ref.kind === 'iconify') {
|
||||||
|
return <Icon height={16} icon={ref.name} width={16} />
|
||||||
|
}
|
||||||
|
if (ref.kind === 'svg') {
|
||||||
|
return (
|
||||||
|
<svg height={16} viewBox={ref.viewBox} width={16}>
|
||||||
|
<path d={ref.path} fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// `component`: lazy-loaded custom icon component. Suspense-safe.
|
||||||
|
const LazyIcon = lazy(ref.module)
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyIcon />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Cache lazy custom panel components by their loader so React.lazy isn't
|
// Cache lazy custom panel components by their loader so React.lazy isn't
|
||||||
// re-invoked across renders.
|
// re-invoked across renders.
|
||||||
const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
||||||
|
|||||||
Reference in New Issue
Block a user