Blacksite/docs/editor/design-system.md

230 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Editor Design System
Blacksite's editor uses the Penpot **Inspector Material Slot** as its visual source of truth. This
guide defines the reusable visual and interaction language; domain behavior remains in its owning
editor subsystem. See [ADR 0049](../adr/0049-penpot-led-editor-visual-system.md).
## Semantic palette
The default theme mirrors Blacksite's established palette. These values are published under
Penpot Assets / Colors and represented by `EditorVisualPalette`; domain widgets consume semantic
roles rather than literals. Theme selection and persistence are intentionally deferred to Editor
Settings, but the renderer-facing UI seam already accepts a different palette.
| Role | Default |
|------|-------|
| Canvas / panel / recessed | `#07080A` / `#0D0F12` / `#090B0D` |
| Control / elevated | `#15181C` / `#1F2328` |
| Brand accent / hover | `#E5A433` / `#F9C14F` |
| Selection and valid drop | `#41C6CF` |
| Border / strong border | `#262B31` / `#40474F` |
| Primary / secondary / muted text | `#DCE1E5` / `#9BA3AB` / `#606972` |
| Healthy / warning / error | `#54BE7F` / `#EBB448` / `#E25E5E` |
Source Sans Pro Regular and Bold are bundled in `crates/editor/assets/fonts/source-sans/`. Type
roles use the Penpot 916 px scale; weight is selected through separate egui font families rather
than simulated styling. Spacing follows 4/8/12 px steps. Common controls are 2231 px tall with
4/5/6/7/8 px radii.
Use semantic tokens from `ui/design_system`; do not copy literal colors into a domain card. The
selection color is reserved for focus, valid drag targets, selection, and active values. Health,
unsaved, processing, failed, read-only, inherited, and built-in states keep the same geometry.
## Composition contract
Reusable controls render a supplied view model and return actions. They do not query unrelated
Bevy resources, save files, schedule processing, or directly mutate scene/world state. The owning
domain resolves actions after rendering. This keeps the visual system reusable in the Inspector,
Content Browser, Material Library, and future editor panels.
The material implementation is split as follows:
- `ui/design_system` owns tokens, typography, control chrome, property-grid geometry, and the color
popup.
- `ui/materials/panel.rs` owns the material-slot view model and action contract, while
`ui/materials/pickers.rs` owns the compact current/recent Material menu and the richer Texture
asset-picker presentation.
- `ui/materials/inputs.rs` renders schema-declared material controls.
- Inspector and asset editors provide state and apply returned actions through authored documents.
The Content Browser follows the same boundary. `editor_ui::content_browser` owns the standalone or
docked chrome mode plus the shared Sources rows, content heading, bounded folder/asset cards,
Details identity/rows, status presentation, and returned actions. The production
`ui/asset_browser` modules supply project state and own navigation history, selection, transactions,
thumbnail requests, and action handling; they do not carry a second body renderer. The UI Gallery
uses the same presentation functions with fixture view models, so a Gallery correction reaches the
production panel. The dock tab supplies the 36 px title row represented in the standalone Penpot
board. Production therefore renders the remaining 55 px single-row toolbar at wide widths or the
88 px two-row compact toolbar below 1000 px. An expanded composition at 1400 px and wider adds
foundation Combo Box and Slider instances for sort and thumbnail scale without changing the
standard-wide anchors; those controls return the same actions as their overflow counterparts.
### Asset category colors
Asset classification is a theme role, independent from thumbnail health, Git state, processing,
selection, or hover. Every `EditorAssetKind` maps exhaustively to the shared `AssetTypePalette` and
renders a 3 px keyline inside the bottom edge of its grid card. Primitive, Light, Model, Texture,
Material, Audio, Level, Prefab, Post Process, Rendering Profile, and Shader each have a distinct
default color; folders have no category keyline. The keyline remains stable for placeholder,
pending, ready, and failed thumbnails, so a rendered image never hides the asset type and a failure
never replaces it. The UI Gallery `theme.json` exposes the same roles for live iteration.
At the 1240 px Penpot reference, Sources is 216 px, Details is 290 px, and the grid owns the flexible
middle lane. At the 620 px reference, Sources and Details are absent by default and are opened from
dedicated toolbar actions. The compact toolbar preserves Back/Forward/Up, a 292 px reference search
field, Grid/List, Sources, Details, Import, New, and overflow. Status geometry is invariant: catalog
health is left, active processing is shown when space permits, and selection is right-aligned.
The content heading is 56 px, and grid cards form one continuous row-major stream across folder and
asset kinds rather than separate folder and file rows. Each body lane uses the semantic panel fill,
5 px radius, and strong inside border from the Penpot board. The grid's recessed surface begins 16
px inside that lane and cards begin another 6 px inside the surface; the resulting 22 px card
origin is shared by production and Gallery. The Details heading stays outside its scrolling content
body, and its identity stack owns the same fixed vertical geometry in both consumers. Ordinary
Details values use one clipped line; project-relative paths and dependency identities use the
Penpot 32 px two-line value region with ellipsis and a full-value tooltip, so a resized Details pane
never paints text across its boundary.
At the 1600 px expanded reference, only the flexible toolbar gap and grid lane grow. Sort and
thumbnail scale appear between the type filter and view controls. Grid cards scale from 68×88 to
132×152 around a fixed 40 px identity footer, and ready 256 px thumbnails remain square and
aspect-fitted at every size.
## Reference layout
At the current 620 px wide reference, 12 px outer padding produces a 596 px slot and a 569 px parameter
body. A 32 px **Materials** heading precedes repeated slots. Expanded headers are 82 px with a
64 px preview; collapsed headers are 52 px with a 32 px preview. Surface is 46 px, Inputs is
216 px (24 px header plus six 32 px rows), UV is 54 px, and Advanced is 30 px collapsed or 178 px
expanded. An expanded wide row keeps its label/value/channel geometry fixed while only the texture
field grows above the reference width.
| Element | Exported geometry |
|---------|-------------------|
| Parameter label | x12, 82 px |
| Scalar group | x103.35, 138.8 px visible ownership |
| Scalar internals | 6 px leading inset, 93.8 px track, 6 px gap, 33 px number |
| Channel | x242.5, 48 px |
| Texture group | x298.5; 198.5 px field at the reference width |
| Locate / Clear | 24 px each, separated from the field by 6 px |
At the 420 px Inspector floor, the slot is 396 px and parameter sections are 369 px. Surface remains
46 px and Inputs is 372 px: each of the six rows uses a 58 px two-line layout with parameter,
value, and channel above a 345 px asset picker. UV reflows from 54 px to 94 px. The layout must not introduce nested or horizontal
scrolling, clipped buttons, or height-dependent width changes.
These reference dimensions are implemented as owning rectangles, not nested `horizontal` flow.
At 569 px, a scalar row begins its value group at x103.35, channel at x242.5, and texture group at
x298.5. At 369 px, the scalar group remains at x103.35 and expands to 205.3 px (a 160.3 px track),
the channel begins at x309, and the texture line is `x12/y31/w345`. Texture names are clipped and
ellipsized inside the field, so content cannot move
the fixed Locate/Clear anchors. A transient width below 372 px retains finite clipped rectangles
and replaces the two low-frequency texture actions with one overflow control until the Inspector
minimum is restored.
## Material controls
Primitive, static-mesh, and skinned-mesh surfaces use the same ordered `MaterialsSection` and
slot-keyed `MaterialSlotPanel` actions. Static/skinned geometry cards do not embed duplicate
material presentations. The slot header and
asset zone include the material sphere, bounded name and project-relative path, shader, health,
assignment interaction, and responsive actions. The entire zone is the drop target; there is no
second “Drop material asset” box. Preview/name/path and Browse open the picker, while Shader,
Locate, Clear, and overflow remain isolated. Direct assignments do not show an “Explicit” badge.
Inherited identities are limited to Model Source, Model Default, Project Default, and Default Grid.
Expanded content shows supported controls only:
- Surface: Opaque/Cutout, cutoff when Cutout, Separate/ORM, and Double Sided.
- Inputs: six Standard Lit rows. Emissive color owns the independently stored intensity companion
and edits/restores both through the Emissive popup.
- UV: two equal Offset/Tiling groups with fixed X/Y controls and independent resets; Material
Instances reset to inheritance.
- Advanced Inputs: only evaluator-supported schema declarations.
Imported source content is read-only with Extract Editable in overflow. DefaultGrid is immutable.
Broken explicit references keep their failed identity and diagnostics rather than exposing a weaker
layer. Stable IDs, fingerprints, orphan state, and provenance live in overflow diagnostics, not in
the parameter grid.
## Pickers and direct manipulation
The compact Material menu shows the current assignment and editor-local recent choices, then offers
Browse Asset Library and Clear. The richer Texture picker uses bounded thumbnail rows for current,
recent, and project textures. A popup dismissal or invalid choice does not assign anything. Material and
Instance drops use the active theme's selection highlight; Texture drops show an explicit invalid state.
Handoff v2.3.1 has one Shader selector in the asset identity block. Surface contains the blend mode
and Double Sided without a visible `Blend` label; it does not mirror Shader as a second "Shading
Model" control. UV is followed by a
collapsed Advanced row. Its culling, render queue, depth-write, and receive-shadows mock controls are
explicitly painted muted previews until those renderer contracts exist, and therefore never return
actions. Standard egui disabled opacity is not compounded over these controls.
The Inspector actor identity remains fixed. The remaining dock-tab body is one actor-salted
component scroll region whose clip is intersected with the dock body, so scrolled Transform or
component cards cannot paint or receive input beneath the actor header. Component-local children
must preserve that inherited clip rather than replacing it with their own maximum rectangle. The
vertical scrollbar floats over a stable four-pixel interaction rail, so crossing the height overflow
threshold cannot change the content width or trigger a responsive-layout flip.
Texture fields open their picker when the field itself is clicked. They show a thumbnail or typed
fallback icon plus Locate and Clear actions. A ready typed thumbnail replaces the generic Texture
glyph rather than appearing beside a redundant icon; a redundant folder button is not used.
The color popup is a fixed 420 x 350 px modal centered over the owning Inspector region, with a
dimmed Inspector-local scrim. Its exported geometry includes a 38 px header, 250 x 26 mode tabs,
178 x 202 wheel editor, 198 x 186 values region, byte RGBA fields, degree/percentage HSV fields,
functional HEX copy, a 156 x 14 checkerboard alpha control, 396 x 28 recents row, and 396 x 26
footer. Wheel, Sliders, and Presets share that fixed frame. The exported eyedropper lane is a muted,
non-interactive preview labelled `Planned — screen sampling pending`.
Every preview updates the live material document. Apply keeps the dirty in-memory value; Cancel,
Escape, Close, and outside dismissal restore the exact value captured on open. No picker action
writes source or queues derived processing. At the 620 px reference, Material scalar inputs use a
138.8 x 22 visible group: 6 px leading inset, 93.8 x 10 interaction track with a 3 px painted rail
and 4 px thumb radius, 6 px separation, and a 33 x 22 numeric field. At the 420 px reference only
the track grows, to 160.3 px. Expanding
Advanced scrolls its complete 178 px disabled-preview body into view rather than revealing a clipped
partial section.
## Focused UI gallery workflow
Use the lightweight production-component gallery for routine design iteration:
```bash
python scripts/editor/ui_gallery.py watch
```
The gallery links `editor_ui` and `material_schema` directly and does not open a project, scene,
renderer, or gameplay runtime. F1F7 select Content Browser 1240/620, Materials 620/420,
Renderer 760/480, and Content Browser 1600 expanded; Shift+F1F4 select Live, Side by side,
Overlay, and Difference; Page Up/Page Down cycle the
ownership and health fixtures. `crates/ui_gallery/fixtures/theme.json` and `material.json` reload in
the running process. Rust changes rebuild only the focused dependency closure, report a bounded
`resolved/rebuilt` counter, launch the replacement, and then stop the last-good gallery; a failed
build leaves the last-good process available.
Renderer presets are not screenshots of a gallery-only approximation. `editor_ui::renderer` owns
the shared renderer-array header, 84/60 px renderer identity headers, 38 px property heading, 50 px
indexed material heading, and slot rail used by both the gallery and the full Static Mesh Renderer
Inspector. Renderer material slots reuse `MaterialSlotPanel`; the first slot may retain its full
82 px identity/action header while only its parameter body is collapsed, secondary slots use the
compact state, and an actually empty slot has an explicit drop/Choose Material presentation.
Difference statistics are diagnostic, not a standalone acceptance threshold: truthful runtime
semantics and renderer rasterization can intentionally differ from an authored mock. Every such
difference is recorded in the [M2 UI gallery delta matrix](evaluations/content-workspace-m2/ui-gallery-delta-matrix.md).
Final acceptance still requires a paired full-editor capture and interaction scenario. If the
build-storage hard limit blocks a rebuild, keep the current last-good binary and evidence, record
the gate as pending, and resolve storage through the managed lane workflow rather than deleting
individual Cargo files.
## Review checklist
- Verify the 620 px geometry numerically and in a native capture.
- Verify the narrow reflow, long and Unicode names, invalid drops, read-only/imported/built-in
states, and popup cancellation.
- Verify handle identity and dirty-document behavior during live interaction.
- Run `python scripts/codex/architecture_audit.py check`; new design-system work does not earn an
exception from the module-size ratchet.