PanelChrome no longer writes the panels' collapse buttons, the command dock tray toggles' names or the dock pins' pressed state. It presents each panel's open and pinned state to the new gev-panel-chrome store, and the buttons, now views, call it there: PanelCollapseButton for the panel headers, bound toggles and pins in the command dock. Clicks take effect once the chrome is initialized; a new owner starts from its own states. bindPanelDisclosure binds only a panel's Escape and refuses the retired buttons and onChange arguments. UiButton joins the component kit.
9.7 KiB
Panel surface contract
src/ui/styles/panel-surfaces.css supplies an opt-in visual surface for new
panels. It uses the application's default color, font, border and radius tokens;
Cyber supplies its existing red/slate palette, monospace typography and shaped
frame. This is CSS shared by the existing panel system, not a panel registry or
a controller. A panel ID does not need a new theme selector.
Markup
A rail panel uses an outer owner with its existing stable id, matching
data-panel-id, and panel-collapsible class. Its first non-glow child is the
surface. Keep the header and any outward popup outside the scrolling body:
<section
id="example-panel"
data-panel-id="example-panel"
class="panel-collapsible"
>
<div data-panel-surface>
<header data-panel-header>
<h2 class="panel-title panel-surface-title">Example</h2>
<PanelCollapseButton
class="panel-surface-control"
target="example-panel"
:name="t('Example')"
initial="−"
/>
</header>
<div data-panel-body data-rail-scroller>
<label>Filter <input class="panel-surface-control" type="text" /></label>
<button class="panel-surface-control" type="button">Apply</button>
<p class="panel-surface-error" role="status" hidden></p>
</div>
<aside data-panel-popup hidden aria-label="Example help">
<!-- The panel owns this popup's placement and interactions. -->
</aside>
</div>
</section>
Use native controls and accessible names. .panel-surface-control is an
optional skin for buttons, text fields, selects and textareas; it provides
hover, focus, disabled and invalid-state styling. Native disabled owns actual
disablement; aria-disabled alone only describes a state and must be enforced
by the controller. .panel-surface-error supplies error color, not announcement
or validation behavior. Keep checkbox/radio semantics and labels intact.
data-panel-header stays outside data-panel-body, so it remains reachable
when content scrolls. Give the body a bounded height through its containing
surface. The rail's allocation supplies that bound; a dock or dialog owner must
supply its own max-height or height. Avoid a flex body whose contents shrink to
fit when those contents should instead scroll.
Placement and ownership
| Mount | Shared contract | Owner responsibilities |
|---|---|---|
Direct child of #left-panel-stack |
An owner with a direct [data-panel-surface] child receives relative positioning and allocated height; inner surface/body skin |
Add the wrapper shown above, use the existing layout/disclosure owners, and retain left-rail focus/auto-collapse rules. |
Direct child of #right-context-rail |
The same hooks with right-rail allocation and intrinsic measurement | Keep data-rail-scroller on the body so measurement preserves its scroll position. Respect Cyber's one-expanded-panel policy. |
Surface inside #command-dock |
Surface, controls, popup decoration and inherited dock concealment | Supply geometry, stacking, disclosure, pin behavior if wanted, safe viewport bounds and dismissal. This CSS does not extend the existing Location/Visual Presets tray stack manager to arbitrary panels. |
dialog[data-panel-surface] |
The same surface/body skin; a closed dialog remains hidden | Set width/max-height, use showModal() or show() and close(), and own labels, initial/return focus and dismissal. Do not put panel-collapsible on a dialog. |
For rails, --panel-expanded-width and --panel-collapsed-width customize
the desired widths within the rail; otherwise Cyber uses its existing left,
right and collapsed panel widths. --left-panel-allocated-height and
--right-panel-allocated-height belong
to the existing layout owner, not to individual panels. The surface must be
the wrapper's first non-glow child because left-rail intrinsic measurement
inspects that child. Do not put an absolutely positioned popup before the
surface or inside its body. Position a popup beside or above its surface using
component CSS; the shared rules deliberately do not choose a direction that
could run beyond the viewport.
Rail measurement still considers visible child extents. A popup extending below the surface can therefore increase its measured height. Prefer a popup beside or above the surface, or integrate an external overlay host with the feature's layout owner; this CSS does not change measurement algorithms.
At viewport widths of 720px or less, both rails are intentional scroll containers. Their overflow can clip outward popups even when the panel's own surface is unclipped. Use a popup contained within the rail's visible bounds, or an explicitly owned overlay host outside the rail; the surface contract does not provide automatic popup escape from these small-screen scroll regions.
For dock disclosure, use a semantic button with .panel-surface-control and
the feature's own accessible name, aria-expanded state and event binding.
Do not copy the rail example's .panel-collapse-btn class into the dock:
existing dock rules intentionally hide that class. The generic surface skin
does not replace the dock's disclosure policy.
PanelCollapseButton (src/ui/rail/) renders a panel's collapse button
(.panel-collapse-btn) from the open state PanelChrome presents to the
gev-panel-chrome store, and its click asks PanelChrome to toggle the
panel through setPanelCollapsed. PanelChrome collects the buttons'
targets at initialization and binds each panel's Escape. For additions
present at startup, keep those hooks and the existing chrome lifecycle. A
dynamically mounted panel must own its disclosure: its own state, button and
Escape, bound and disposed with bindPanelDisclosure /
collapsePanelOnEscape from panelDisclosure.js (which bind Escape only). Do not rerun the whole application chrome
initializer just to add one panel. layoutLeftPanelRail and
layoutRightPanelRail accept arbitrary direct data-panel-id children; their
host owns layout scheduling, observations and collapse callbacks. Dynamic
content that changes size may require an owner-scheduled layout pass or an
owned ResizeObserver; adding CSS attributes does not register new observers.
Persisted/share-link state, pinning, provider loading, keyboard shortcuts and Cockpit-specific mounting still require deliberate integration with their current owners. This contract does not add entries to named persistence, dock, Cockpit or share-state policies. Dispose listeners, observers and timers, dismiss open popups/dialogs and restore focus before removing a dynamic panel.
Decoration, popups and visibility
The shared surface reserves ::before for its non-interactive background. The
Cyber frame clips this pseudo-element only; the surface stays unclipped with
visible overflow. This follows the existing Display and Global Context pattern.
Putting the shape on an interactive ancestor clips all descendant popups even
if their z-index is high. A scrolling body also clips descendants by design,
so outward popups must be siblings of the body, or use an explicitly owned
overlay host outside every clipping ancestor.
data-panel-popup gives a positioned popup the same decorative and control
tokens. It is not a popover API, focus trap, collision solver or top-layer
manager. The owner must keep it inside the viewport, above relevant siblings,
and reachable by pointer and keyboard. A native modal dialog participates in
the browser top layer; a surface in a rail does not. Transform, containment,
mask, clipping or stacking rules on an additional ancestor can still obstruct
either an ordinary panel or its popup. Arbitrary future markup is not covered.
hidden, a closed native dialog and collapsed bodies/popups remain hidden.
Clean UI and recording conceal conforming surfaces, including outward popups;
owners must close modal dialogs when entering these modes so an invisible
modal cannot leave the application inert. Existing Cockpit rail and dock
exclusions remain in force. New left-rail/Cockpit functionality needs an
explicit product integration; the surface attribute grants no mode exception.
Never use theme styling to alter stored preferences or force hidden content
visible.
Check an addition
Use the real application with the default and Cyber themes. Check open, collapsed, hidden, disabled/error, theme round trips, clean UI/recording and the intended Cockpit behavior. Exercise keyboard focus, Escape, pointer input, the last scrolling item, resize without reload and any pin/unpin controls. For an outward popup, test visible pixels and pointer hit-testing outside the surface; a positive bounding box alone does not prove it escaped clipping. Check each mount the addition actually supports. The CSS contract cannot establish provider correctness, disclosure lifecycle or responsive placement without those feature-specific checks.