# ADR 0049: Penpot-Led Editor Visual System

## Status

Accepted

## Context

Blacksite's editor controls evolved inside individual panels. Even after the material data and
Inspector composition became shared, local egui styling still produced inconsistent spacing,
typography, interaction states, and narrow-panel behavior. The Penpot **Inspector Material Slot**
provides an authored reference for the visual language rather than another implementation-specific
mock-up.

## Decision

The Penpot **Inspector Material Slot** is the source of truth for geometry, type roles, control
heights, radii, spacing, property-column geometry, and interaction states. Color is semantic rather
than panel-owned: Penpot's Assets / Colors library and `editor::ui::theme::EditorVisualPalette`
name the same roles, with Blacksite's existing palette as the default. A later Editor Settings slice
may select or override that palette without changing widgets or material behavior. At the 620 px
reference width, implementations match the documented geometry. Below that width, controls deliberately
reflow instead of shrinking, clipping, scrolling horizontally, or hiding required state.

Fixed Penpot sections use deterministic geometry models rather than nested egui flow layouts.
Surface, parameter rows, texture fields, UV groups, and Advanced each receive an owning rectangle;
every child rectangle is derived from it and clipped to it. The 569 px and 369 px section widths
encode the exported wide and compact references. A bounded transient mode below 372 px keeps all
rectangles finite, stacks the row, and collapses low-frequency texture actions into overflow while
the Inspector restores its 420 px minimum.

The reusable implementation lives under `editor::ui::design_system`. It consumes the editor-owned
semantic palette and owns spacing tokens, Source Sans Pro Regular/Bold typography roles, section chrome, property grids,
asset and texture fields, segmented controls, switches, status presentation, and the Blacksite
color picker. Domain widgets consume these primitives instead of copying raw colors or absolute
Penpot coordinates.

Material authoring is the first migrated domain. One action-returning `MaterialsSection` is used by
primitive, static-mesh, and skinned-mesh inspectors. It owns an ordered list of collapsible
`MaterialSlotPanel` models, and every returned action carries its stable slot ID. The widgets do not
query or mutate arbitrary world state.
Supported material behavior only is shown. Asset identity, health, inheritance, dirty state, and
read-only state share one geometry, while stable IDs and fingerprints remain in secondary
diagnostics.

The final Penpot component supersedes the earlier v2.2/v2.3 geometry. A 32 px **Materials** heading
precedes repeated slots. Expanded slot headers are 82 px with a 64 px preview; collapsed headers
are 52 px with a 32 px preview. A cyan disclosure spine and **PARAMETERS** ownership label inset the
Surface, Inputs, UV, and Advanced body. The asset identity block owns the single Shader selector. Penpot's former duplicate Surface
"Shading Model" selector was removed from the component and every state reference in handoff
v2.3.1. Surface contains blend mode, Separate/ORM selection, and Double Sided. UV
precedes Advanced with
an explicit 8 px section gap. Advanced render controls
that do not yet have renderer contracts are shown only as a clearly disabled preview: they emit no
actions and create no authored or runtime state.

The Material slot header is one assignment interaction, not an identity block beside a second drop
box. Its whole 596 x 82 px wide reference area accepts valid Material/Instance drops. Preview and
identity clicks plus the first-class Browse action open the compact current/recent Penpot menu;
Shader, Locate, Clear,
and overflow actions are isolated and cannot accidentally open it. The picker returns a stable
asset reference to the owning Inspector instead of mutating scene or world state.

The custom 420 x 350 color popup is a fixed modal centered over its owning Inspector clip, with an
Inspector-local dim layer rather than application- or viewport-relative placement. Its header,
mode tabs, wheel and values regions, RGBA byte fields, HSV degree/percentage fields, HEX copy,
checkerboard alpha control, recents, and footer use the exported Penpot coordinates. The exported
eyedropper lane remains a tooltip-labelled disabled preview until screen sampling has an owned
runtime contract. Apply keeps the in-memory dirty value and closes; Cancel, Escape, Close, or
outside dismissal restores the exact pre-open value. Neither path saves source or schedules derived
processing.

Standard Lit declares seven independently stored inputs but six primary presentation rows.
`emissive_intensity` is a schema companion of `emissive_color`; the Emissive popup previews and
restores both values as one interaction without merging their authored/runtime identities. Custom
shader binding counts likewise count primary rows while rendering companions through their owner.

Bounded scalar material inputs use the exported visible-child geometry rather than the narrower
transparent Penpot parent frame. At the 620 px reference that is a 138.8 px group composed from a
6 px leading inset, 93.8 px track, 6 px gap, and 33 x 22 numeric field. At 420 px the track expands
to 160.3 px while the number remains fixed. Expanding the 178 px Advanced preview requests a bounded
Inspector scroll reveal so its note and lower boundary are visible. The same no-implicit-persistence
rule applies to sliders, texture assignments, and shader controls.

Focused visual iteration uses a standalone `ui_gallery` binary that links the same `editor_ui` and
`material_schema` production crates as the editor. It owns no project, scene, renderer, or gameplay
runtime. Exact reference presets, direct Penpot PNG comparison, runtime fixture/theme reload, and a
managed last-good rebuild/restart loop shorten the edit cycle without creating a second widget
implementation. Gallery evidence is diagnostic until the same component passes a named full-editor
interaction scenario.

Renderer comparison uses the same rule. The renderer array, renderer identity, property heading,
indexed material heading, and material slots are production `editor_ui` components. A material
slot's asset header and parameter disclosure are related but distinct presentation state: renderer
arrays may keep the complete asset/action header visible while the expensive parameter body is
collapsed. Empty assignments use a first-class empty presentation instead of reusing pending or
failed thumbnail state.

Inspector overflow is part of the shared visual contract. A floating scrollbar keeps component
width stable as content crosses the vertical overflow threshold, and every nested component clip is
an intersection with the owning Inspector body. Ready texture fields use the typed thumbnail as
their sole leading identity; generic image glyphs are reserved for empty states.

Standard Lit exposes one shared UV Offset/Tiling transform through the same schema-driven input
document. The renderer maps it to Bevy StandardMaterial UV transforms and to every Surface ABI
texture lane. Material Instances may override it sparsely; resetting an instance restores its base.

Penpot's `CODEGEN MAP` and handoff annotations are design documentation. Runtime egui layout remains
responsive and semantic; generated SVG or code is not copied into production. Reviewed numeric
geometry is represented by small testable layout models instead of ad hoc child flow or generated
widget trees.
The same release slice applies the active Inspector Header, Array Header, renderer-panel, asset
field, vector, selection, and overlay components across the Inspector. Built-in component cards
dispatch through the registry and share one clipped actor-body scroll owner; domain behavior stays
outside the visual primitives.

## Consequences

- New editor UI has one reviewed visual vocabulary rather than panel-local approximations.
- Theme colors have one runtime owner and one design-side asset vocabulary; the material panel does
  not carry a private blue/teal palette.
- The 620 px and 420 px geometries are deterministic and unit-testable. The Inspector has a 420 px
  floor and one actor-body scroll region; material slots never introduce nested scrolling.
- Unsupported prototype controls are omitted until their runtime contracts exist.
- The explicitly approved Advanced preview is the sole exception: it is disabled, muted, and
  labelled as planned work rather than functioning authoring state.
- Native acceptance compares rendered geometry, typography, states, and interaction behavior with
  the Penpot reference; compile-only evidence is insufficient.
- Source Sans Pro and its SIL Open Font License are bundled with the editor.
- ADR 0048 continues to own composition and module-size policy. This ADR owns visual tokens,
  responsive component geometry, and interaction presentation.

Related decisions: [ADR 0035](0035-shared-material-assets-and-renderer-slots.md),
[ADR 0047](0047-editor-authored-asset-documents.md), and
[ADR 0048](0048-modular-editor-composition-and-debt-ratchet.md).
