Panel
The non-modal surface docked beside a live map, holding one subject — a parcel's report, a design being built — while the map behind it stays usable.
The panel
Code
<GdSidePanel title="42 Jarrah Street" @close="selected = null">
<template #meta><GdCode>L14 RP80432</GdCode> · Millbrook</template>
<GdPanelSection label="The lot">…</GdPanelSection>
<GdPanelSection label="What is on the land">
<template #action><GdButton variant="ghost" size="sm">See all</GdButton></template>
…
</GdPanelSection>
<template #footer>
<GdButton variant="ghost">Save</GdButton>
<GdButton variant="primary">Design this site</GdButton>
</template>
</GdSidePanel>The parts
Header
42 Jarrah Street
Code
<GdPanelHeader title="42 Jarrah Street" :scroller="bodyEl">
<template #leading><GdButton icon variant="ghost" label="Back">…</GdButton></template>
<template #meta><GdCode>L14 RP80432</GdCode> · Millbrook</template>
<template #actions><GdButton icon variant="ghost" label="Save this parcel">…</GdButton></template>
</GdPanelHeader>Section
The lot
What is on the land
Code
<GdPanelSection label="What is on the land">
<template #action><GdButton variant="ghost" size="sm">See all</GdButton></template>
<GdListRow title="Creek flooding" sub="Rear of lot" />
</GdPanelSection>Footer
Code
<GdPanelFooter>
<GdButton variant="ghost">Save</GdButton>
<GdButton variant="primary">Design this site</GdButton>
</GdPanelFooter>
<GdPanelFooter layout="stretch">
<GdButton variant="primary">Design this site</GdButton>
</GdPanelFooter>API
GdSidePanel
Props
titlestring—The subject. Also names the panel's landmark, via `aria-labelledby`.
metastring—The fact line under the title. The `meta` slot takes markup instead — chips and links.
side"right" | "left""right"Which edge the panel docks against, and which edge it enters from.
widthstring—Any CSS length. No default here — the component's own stylesheet sets the resting width.
labelstring—The landmark's name when there is no title to borrow one from.
closablebooleantrueDraws the close button and answers Escape. Off, the panel has no dismissal of its own and `close` never fires.
closeLabelstring"Close panel"The close button's accessible name.
footerLayout"end" | "between" | "stretch""end"Passed through to the `GdPanelFooter` the panel wires for you.
Slots
defaultThe body — usually a stack of `GdPanelSection`s. It is the scrolling element.
leadingBefore the title in the header — a back button.
titleMarkup in place of the `title` prop.
metaMarkup in place of the `meta` prop.
actionsThe header's right-hand controls, beside the close button.
footerThe floor. Present it and the panel renders a `GdPanelFooter` around it.
Events
close[]The close button, or Escape with focus inside the panel. The panel never removes itself — the caller owns whether it is mounted.
GdPanelHeader
Props
titlestring—The subject.
titleIdstring—Id for the heading, so the panel can name itself by `aria-labelledby`.
titleAsstring"h2"The heading LEVEL, which is the caller's — a panel beside a page's h1 is an h2, one nested deeper is deeper. The SIZE is not a prop at all.
metastring—The fact line under the title. The `meta` slot takes markup instead.
scrollerHTMLElement | null—The element whose scroll decides the rule — usually the panel's body. Without it the header simply never earns its hairline.
Slots
leadingBefore the title — a back button.
titleMarkup in place of the `title` prop.
metaMarkup in place of the `meta` prop.
actionsThe right-hand controls.
GdPanelSection
Props
labelstring—The eyebrow. The `label` slot takes markup instead.
labelAsstring"h3"The label's heading level — `h3` under `GdPanelHeader`'s `h2`. Pass it when the panel sits somewhere else in the outline.
Slots
defaultThe block's content.
labelMarkup in place of the `label` prop.
actionOne control opposite the label — "See all".
GdPanelFooter
Props
layout"end" | "between" | "stretch""end"`end` is a row of buttons with the commit on the right; `between` puts a quiet action left and the commit right; `stretch` gives the children the full width.
Slots
defaultThe actions. Their arrangement is the layout prop.
Usage Guidelines
- Use
GdSidePanelwhen the thing behind it must stay usable — a report about a parcel while the user pans, clicks the next lot, toggles a layer. - Use GdDrawer when the thing behind it must NOT be touched — a settings sheet, a form that has to be finished or abandoned. It traps focus, scrims the background and answers Escape from anywhere.
- Use GdPageHeader when you are opening a DOCUMENT rather than describing a subject. It is a full-bleed band and it carries the page's one
<h1>. - Give the panel's container
position: relative. The panel is positioned against it, and the map shell already is one. - Pass the body as
scrollerwhenever you useGdPanelHeaderon its own.GdSidePanelalready does it for you. - Call
sync()after any scroll you set from code — a new selection, a tab swap.GdSidePanel.scrollToTop()already does. - Use
layout="stretch"when the panel's floor is one door. Usebetweenwhen a quiet action has to sit opposite the commit; leave it onendotherwise. - Label every section. The label is a real heading, and it is how a screen-reader user navigates a panel with a dozen blocks in it.
Behavior & Anatomy
Panel, header or drawer
- Not GdPageHeader — that is a full-bleed band opening a DOCUMENT, carrying the page's one
<h1>. A panel floats over a canvas and its title is a subject, not a page. - Not GdDrawer — the drawer is a native
<dialog>opened withshowModal(): focus trapped, everything behind it scrimmed and inert, Escape answered from anywhere. Every one of those is right for a settings sheet and is the opposite of what a panel needs. - So: a plain
<aside>— complementary to the map, which stays the main content. No trap, no scrim, noinert, nothing stacked above--gd-z-panel.
Non-modal is the whole point: the user reads the report AND pans, clicks the next lot, toggles a layer. A surface that makes the map unreachable while describing a piece of it has misunderstood the job.
The earned hairline
At rest GdPanelHeader has no bottom border. The rule and its scroll shadow arrive only once the body beneath has actually moved. A header that always carries a rule reads as detached from the panel it belongs to. The line is a claim that there is content above the fold, and at scrollTop: 0 that claim is false. GdPanelFooter's rule is unconditional for the same reason inverted: a footer always has content above it, so its line is a fact rather than a claim.
- The header watches the scrolling element itself, passed as
scroller— every panel that hand-rolled this wrote the same passive listener and then got the reset wrong. - A programmatic
scrollTop = 0— a new selection, a tab swap — fires no scroll event, which strands the rule on over a body that is back at the top. Call the exposedsync()after any scroll you set from code;GdSidePanel.scrollToTop()already does. - The threshold is 4px rather than 0: a trackpad flick back to the top leaves a pixel or two of scrollTop behind on the way, and a rule that flickers there is worse than no rule at all.
- The border fades, the shadow does not: animating a shadow repaints the full width of the body's top edge every frame, for a mark nobody watches arrive.
The heading level is the caller's, the size is not
A panel beside a page's h1 is an h2, one nested deeper is deeper — so titleAs and labelAs exist. The SIZE is not a prop: a panel title is a panel title wherever it lands, or the outline and the type start disagreeing about which is the hierarchy. The section's label is a real heading and not a styled span for the same reason — a panel with twelve unlabelled blocks is a panel a screen-reader user has to read linearly to navigate.
Why the section draws the rule above it
The section does not draw its own rule — it draws the rule BETWEEN itself and the section before it. A border on every section puts a line above the first one, a few pixels under the header's hairline, and two rules that close together read as a mistake rather than as structure.
Why the footer pins two ways
As the last flex child of a panel column it is simply inert and the body scrolls past it; dropped inside a scrolling body it sticks to the bottom edge. Both arrangements exist already, and no caller should have to know which one the footer would have preferred. The safe-area inset is not decoration — on a phone this bar is the bottom of a sheet resting on the home indicator, and without it the panel's primary action sits under the gesture bar. stretch is for the panel whose floor is ONE door: a right-aligned button in a 400px bar reads as an afterthought rather than as the panel's entire point.