# 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).