feat(skills): ship ue-design-skills bundle, licensing and delivery gate

Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 23:48:55 +07:00
parent 80ea7b03a9
commit ecd87ac96d
67 changed files with 21630 additions and 0 deletions
@@ -0,0 +1,380 @@
---
name: ue-ui-architecture
description: >-
Design or review scalable Unreal Engine UI: a root layout per local player,
tagged layer stacks, extension points for slot-based widget injection from
feature plugins, input and focus policy per screen, and centrally arbitrated
loading screens. Use when building HUD, menu or modal architecture, adding
mode or plugin UI without base-class dependencies, supporting gamepad or local
multiplayer, or debugging focus, ordering, duplicate-widget and teardown
issues.
---
# UE UI architecture
The invariant:
> UI ownership, placement and content are separate. The shell owns slots and
> layers; features contribute content through handles and contracts.
Measured patterns from a reference product: [patterns](references/patterns.md).
Detection recipes for silent UI failures: [failure modes](references/failure-modes.md).
Related skills: `ue-modular-gameplay`, `ue-input-architecture`,
`ue-gameplay-messaging`, `ue-gameplay-tag-governance`.
Method, not architecture — how to check any claim in this bundle before repeating it: `ue-evidence-discipline`.
---
## 1. One root layout per local player
```text
UI manager subsystem
└─ UI policy
└─ primary layout, one per local player
├─ UI.Layer.Game
├─ UI.Layer.GameMenu
├─ UI.Layer.Menu
└─ UI.Layer.Modal
```
Responsibilities: the **subsystem** is the process-level entry point and owns the
active policy; the **policy** decides which layout class to create, where to
attach it, and how local players behave; the **layout** is a named layer registry
with a push/pop API; **widgets** are content only.
Do not let gameplay systems add widgets to the viewport with a guessed draw
order. A widget that bypasses the layout is invisible to every mechanism the
layout provides — visibility toggles, input suspension, focus restoration.
### Let the base subsystem stand aside
A base UI subsystem should decline to be created when a project subclass exists,
so the two never coexist. The idiom — *do not create me if I have derived
classes* — is worth copying anywhere a plugin offers a replaceable subsystem.
### Local players own their UI
Each local player needs an independent layout, focus state and input routing. One
global HUD root breaks split-screen and player-specific modal stacks, and the
breakage appears only when a second player joins.
---
## 2. Tagged layer stacks
Layers express **modality**, not individual screens:
| Layer | Use |
|---|---|
| Game | HUD and persistent gameplay presentation |
| GameMenu | pause and in-game menus |
| Menu | front-end navigation |
| Modal | confirmations, errors, blockers |
Each layer is an activatable widget stack: screens are pushed by soft class,
activated and deactivated, and popped by lifecycle.
### Rules
- register each layer tag exactly once per layout;
- no screen hard-codes a sibling widget path;
- the modal layer carries the strongest input and focus policy;
- popping restores the previous screen and its focus;
- async load failure reports a reason and clears any loading state;
- **pushing to an unregistered layer tag must not fail silently.** A typo in a
layer tag that returns null with no log is a screen that never appears and
nothing to search for. Recipe: UI-04.
### Layers are not draw-order buckets
If two widgets belong to one HUD composition, use extension slots inside the HUD
layout. Inventing `UI.Layer.HUDLeft` and `UI.Layer.HUDRight` turns a modality
concept into a layout concept and loses both.
---
## 3. Extension points decouple slot owners from contributors
A subsystem holds two maps keyed by tag:
```text
extension point tag → registered points
extension point tag → registered extensions
```
**A point** is owned by a shell widget: its tag, an optional context object, the
data or widget classes it accepts, its tag match mode, and a callback.
**An extension** is owned by a feature: the target tag, an optional context, a
data object or widget class, and a registration handle.
### Registration must be order-independent
Both sides catch up on registration — a new point receives already-registered
matching extensions, and a new extension notifies already-registered matching
points. This is what removes initialization-order coupling between the HUD and
feature activation, and it is the single most valuable property of the pattern.
### Both sides copy before iterating
Callbacks can register or unregister anything, including themselves. Copy the
list before notifying, and define what happens to an entry removed mid-notify.
---
## 4. Contract matching
An extension matches a point when all applicable rules pass:
1. **tag** — exact, or ancestor-ward for partial matching;
2. **context** — same object, or both explicitly global;
3. **class contract** — the data derives from, or implements, an allowed class;
4. **ownership** — local player and world are compatible.
### Context tiers
One slot widget can register points for the global context, for its local player,
and for that player's player state. The same slot tag then serves global HUD
elements and player-specific elements with no cross-player leakage — and
split-screen works without a special case.
### Know which direction partial matching runs
Ascending from the extension's tag to its ancestors means a point tagged `A.B`
with partial matching receives an extension tagged `A.B.C`. **The reverse does
not happen.** Prefer exact tags for production HUD placement, and give every
parent tag used for partial matching a coherent data contract.
### A dead context object is not a global context
If context matching compares pointers and the context is held weakly, an
extension whose context died matches nothing at all — it does not fall back to
global, and it does not clean itself up. It sits in the map until someone
unregisters it. Recipe: UI-06.
---
## 5. The slot widget owns rendering
The slot widget registers and unregisters points, converts extension data into
entry widgets, discovers its context, enforces the class whitelist, and defines
empty-state behaviour.
The feature does not find the slot and does not add children. It registers an
extension and retains the handle. That is dependency inversion:
```text
feature knows: tag + widget class
shell knows: tag + layout container
neither knows the other's concrete type
```
Register in the widget's build step rather than its constructor, and unregister
when Slate resources are released — otherwise a rebuilt widget leaves a
registration behind.
---
## 6. Feature-driven UI has two distinct paths
**Layout requests** push an activatable screen into a layer.
**Element extensions** register a widget into a named slot.
They are different mechanisms with different removal semantics, and putting both
in one feature action is convenient and dangerous. Retain separately: handles to
pushed layouts, an extension handle per slot registration, actor extension
request handles, and per-actor ownership records.
### The two teardowns are not equivalent
Unregistering an extension handle removes the entry widget **synchronously**.
Deactivating a layout **asks** the stack to release it. Do not assume the second
is removal — verify the stack no longer retains or reactivates it. Recipe: UI-05.
Removal must also be keyed to the specific actor that was registered: more than
one HUD actor can be alive on a client at once.
---
## 7. Ordering must be a real contract
If the API exposes a priority, the implementation must sort or insert by it.
**Copying a priority into a request does not create ordering.**
Define: which direction wins; the tie-break for equal values; whether priority is
per point, per tag, or per context; what happens when it changes; and who owns
the sort — the subsystem or the slot widget.
Where ordering is unimplemented, the effective order is registration order, which
in a plugin-based project is feature activation order — non-deterministic from the
designer's point of view, and made worse by any removal that swaps elements.
Recipe: UI-01.
---
## 8. The loading screen is arbitration, not a layer
A loading screen sits above the entire UI, including the root layout. A manager
aggregates reasons from objects implementing a loading-process interface:
```text
ShouldShowLoadingScreen(out Reason) → bool
```
Blockers typically include: experience or mode still loading, map travel, pending
network game, world not begun, seamless travel, missing player controllers, and
explicitly registered async tasks.
Manager behaviour: inspect all providers, show while any blocker exists, retain a
human-readable reason, enforce an optional minimum display time, hide when clear,
and survive world teardown.
### Rules
- providers report need and reason; the manager owns presentation;
- no subsystem toggles the loading widget directly;
- **require a non-empty reason.** The reason string is the only diagnostic that
answers "why is this screen still up";
- stale tasks have a timeout, a cancel path and an owner;
- editor and packaged behaviour are both tested — an input blocker that is
disabled in the editor means the editor is not testing the shipped behaviour.
Recipe: UI-08;
- **measure before copying performance side effects.** Forcing garbage collection
when the screen hides puts a hitch exactly at the moment control returns to the
player. Recipe: UI-07.
---
## 9. Input and focus policy per screen
Each activatable screen declares its input configuration: game-only, menu-only or
both; mouse capture and cursor behaviour; a desired focus target; back-action
handling; whether lower layers receive input.
The framework owns the activation stack, focus restoration, gamepad navigation,
the bound-action bar, and input suspension during transitions.
### Validate what the compiler cannot
- **every activatable screen has a desired focus target.** A screen without one
opens and leaves the gamepad with nowhere to go. If your framework only warns
about this, treat the warning as an error — a screen with no focus is not
shippable. Recipe: UI-09;
- the input configuration matches the layer's role;
- a required back action exists;
- no raw key handling bypasses the UI input system;
- extension point contracts are non-empty where required.
### Transitions and focus interact
A non-zero stack transition duration can break focus hand-off to the incoming
screen on a gamepad. If you disable transitions to fix focus, write down that
this is why — otherwise someone will re-enable them.
---
## 10. The communication boundary
UI may listen to local messages for **transient facts**: an elimination for the
feed, an accolade for a toast, an inventory delta for a notification.
Persistent display reads **observable state**: health, score, inventory,
settings. Do not rebuild state from a stream of messages — a widget created
mid-match will have missed them. See `ue-gameplay-messaging`.
UI sends commands through accountable controllers and services, never as a
broadcast with no owner.
---
## 11. Test matrix
### Extension ordering
Point first then extension; extension first then point; several points with
distinct contexts; global plus local-player plus player-state points;
unregistering an extension while its point lives; destroying a point before the
extension's owner.
### Modular lifecycle
```text
baseline HUD
→ activate feature → exactly one layout, one entry per extension
→ deactivate → back to baseline, verified not assumed
→ reactivate → no duplicates
```
### Local players
One player; split-screen; adding and removing a player at runtime; confirming
player-specific context does not leak; independent modal and focus stacks.
### Input and focus
Keyboard and mouse; gamepad navigation; device hot-swap; back through nested
stacks; modal blocking lower layers; the loading screen capturing input **in a
packaged build**; focus restored after a pop.
---
## 12. Review checklist
- [ ] One root layout per local player.
- [ ] Every screen enters through a named layer.
- [ ] HUD contributions use extension slots, not direct widget references.
- [ ] Registration order is irrelevant in both directions.
- [ ] Context and class contract are explicit at every point.
- [ ] Features retain extension, layout and request handles.
- [ ] Deactivation removes layouts and entries completely, verified.
- [ ] Ordering is deterministic, or documented as unspecified.
- [ ] Pushing to an unknown layer tag is loud.
- [ ] The loading screen is centrally arbitrated and every reason is non-empty.
- [ ] Every activatable widget has a desired focus target.
- [ ] Device presentation stays in the input/UI layer.
- [ ] Persistent display reads state; only transient facts come from messages.
- [ ] Split-screen behaviour is tested, not assumed.
---
## 13. When to simplify
For a small single-player game with one fixed HUD and no modular modes, a root
widget with a few direct child references is enough and this indirection is
overhead.
Introduce layers and extension points when at least one applies: plugins or modes
add UI independently; local multiplayer is required; modal, menu and game input
policies conflict; screens need async loading and a stack; contributors must not
depend on the shell widget class; gamepad focus restoration must be systematic.
The goal is not maximum indirection. It is explicit ownership and replaceable
placement contracts — and the cost of that is a debugging path that crosses
several assets, which you pay every time something does not appear.
---
## Provenance
The findings behind the failure modes come from a line-by-line source audit of
the UI layer of Epic's Lyra Starter Game on Unreal Engine 5.6: the extension
subsystem, the layer and policy plugins, the loading-screen plugin, and the
project's own widgets and feature action.
The framework the project sits on — the activatable widget stack, focus
management, the input subsystem — was **not** in the audited tree, so every
statement about it is inferred from call sites and is marked as such rather than
presented as measured. Widget blueprints are binary and were not read.
Source addresses stay in the research archive that produced this skill; what
ships is the detection recipe. Each entry carries a stable identifier (`UI-01`
and up) that resolves back to the audited location.
## Evidence boundary
One project, one engine version, one workspace. Layer-to-container binding lives
in binary assets, so which layers actually exist in that project is out of scope
rather than concluded. Re-run the recipes against your own tree before acting on
any specific claim.