The SeatLayer buyer picker, described so a Swift, Kotlin or React Native engineer can build it without reading Dart. Names here are the Dart API's names, deliberately: the four SDKs should agree on what things are called.
Every token reference (size.dockBarHeight, color.dark.surface,
motion.duration.sheet) resolves in tokens.json.
This file is the catalogue: what each component is called, what it reads, and
which slot restyles it. picker-spec.md is the full
specification — every state, every animation, every string and every capability,
in buyer order. Where the two disagree, the spec is newer.
Decision, 2026-08-28, restated after the web-parity round. Actions carry
radius.button, which is what the web picker's own buttons measure. Material's
default stadium button is therefore wrong for this design, and every action that
is not listed as a pill below overrides it.
radius.button is its own role, not a fraction of radius.base: a brand that
rounds its cards to 20 pt must not thereby grow pill buttons, and the
organizer's branding radius is never inherited by it. radius.control is the
same value under its own name, for the form controls that are not actions —
the best-seats selects and stepper.
True pills (radius.pill / radius.chip, 999):
- the hold countdown pill and the sales-closed pill in the header, and the header's close ring
- the price-legend chips, including
All prices - the Map | 3D segmented control, track and segments
- the test-mode chip
- the floor rail's track and its floor chips
- the dock's
‹ ›steps and its‹ Venueway out - the seat card's photo-strip pills —
View from hereand3D— and the flight chip that leaves the card - every piece of 3D chrome: the back pill, the deck's nav chips, the caption
- the seat-view caption strip
- a toast's action, the access panel's action, and the booked overlay's seat
list and
Back to map
radius.button: the seat card's Cancel and ✓ Add seat, its
See it in 3D action and its decision-row 3D square, its 3D inspection
chips and its tier rows; the sheet's Hold seats & checkout; the
accessibility sheet's rows; Try again; the prompts'
action pairs.
radius.peekButton (12): nothing on the phone sheet any more — the
collapsed bar's two doors are gone, and the sheet's one button takes
radius.button with every other primary action. The token stays for hosts and
for the wide checkout bar.
radius.control: the best-seats selects, its stepper and its action.
Round icon controls — the map buttons, the 3D seat stepper — are circles and
are unaffected. A cart card rounds to size.cartCardRadius; the
seat card to radius.confirmCard; the sheet to radius.sheet.
Dart: SeatLayerPickerThemeData(buttonRadius:) moves every radius.button
action at once, and the per-element slots — primaryButtonStyle,
secondaryButtonStyle, continueButtonStyle, iconButtonStyle, chipShape —
and each widget's style: parameter still win over it.
Every component reads one picker snapshot and calls back into a controller. No component fetches anything itself.
Snapshot fields the catalogue refers to:
| Field | Meaning |
|---|---|
event |
name, venue, currency, branding, test mode |
map.rung |
venue (overview) or seats (a section is focused) |
map.focusedSectionId |
the focused section, or null |
map.isVenue3D |
whether the immersive scene is up |
map.categoryFilter |
the active price-chip filter |
sections[] |
id, label, displayLabel, color, seatsLeft, priceMin, priceMax, zoneId |
categories[] |
key, label, color, priceMin |
selection[] |
SelectedSeat: label, sectionLabel, rowLabel, seatNumber, price, currency, objectType, tiers, screenPoint |
cart |
lines[], ticketCount, cartTotal |
hold |
holdId, expiresAt, owner |
access |
why access is limited, where it is — paused, revoked, expired, unverified |
capabilities |
which optional features are available — venue 3D, seat view, best available |
branding.attributionRequired |
whether the attribution line must render |
Two rules bind every component:
- The venue map owns the venue; native owns the chrome. Everything in this catalogue is drawn natively, on top of the map.
- A row name may already contain its section. Print
rowLabelwith the section prefix removed (Dart:pickerRowLabel), or the card readsStalls D · Row Stalls D C.
Measured off the picker's own container, never the device: phone below
size.phoneBreakpoint, wide at or above size.wideBreakpoint. The phone
composition is a column:
Header size.headerHeight
PriceLegend band size.topRailHeight
┌ map surface (WebView) ──────────────────────────────────┐
│ TestModeBadge (top-left) ViewModeControl (top-right) │
│ FloorStrip (left rail) │
│ corner controls: accessibility ◦ zoom column │
│ toast (bottom centre) · extend prompt: opt-in only │
│ ConfirmCard / Venue3D chrome / status overlay │
│ (no DockBar on any width by default — host opt-in) │
└─────────────────────────────────────────────────────────┘
CartSheet (collapsed) handle + foot + safe inset
The prices keep a band of their own between the header and the map: floated on the map's top edge, the last chip was clipped under the Map/3D control and seat numbers read through the gaps on a busy chart. The Map/3D control keeps the map's top-right corner on the line below, where the test badge sits at the other end.
The header, the rail and the sheet are rows of the same column, so the map
surface begins and ends where they do. Everything else in the diagram stands on
the map and is reported to the runtime as viewport insets — see
picker-spec.md §2.3.
Name SeatLayerPickerHeader · Style slot headerStyle · Instance
override style:
- Inputs
event.name,event.venue,branding.logo,hold. - States compact (phone, one line) / full (wide, two lines); with and without a hold.
- Anatomy
size.headerHeighttall, groundcolor.*.surface, elevationelevation.header. Left: brand tilesize.headerLogoSize. Centre: event name,type.headerTitle, ellipsized. Right: the HoldPill, then the dismiss control. - Callbacks
onClose. - Commands none.
Name SeatLayerPriceLegend · Style slots legendChipStyle,
chipShape · Instance override style:
- Inputs
categories[],map.categoryFilter,event.currency. - States chip idle / selected / sold out; empty (the band is not drawn); not drawn at all while the immersive scene is up. The band takes the map chrome's palette, so it darkens with the scene.
- Anatomy a band of
size.topRailHeightoncolor.*.surfacewith a hairline beneath, holding one horizontally scrolling row. Each chip:size.legendChipHeightof ink inside asize.minimumHitTargetreach, a dot ofsize.legendChipDotSize, then the amount,type.legendChip. Idle ground iscolor.*.backgroundwith a hairline; selected is the accent withcolor.*.onAccentand a ring on the dot.All pricesis pinned first and never scrolls away. On the light theme the dot is the category colour mixed into the surface under a full-strength ring of it. The row CLOSES with one grey swatch andstrings.notAvailable— the single inert disc the map paints for a seat nobody can take, whatever the reason. It is not a chip: no pill, no hairline, no press target, not a toggle to assistive technology. (It replaces a held/sold pair with a padlock and a diagonal.) - Callbacks none.
- Commands
picker.setCategoryFilter { keys, focus }— the first tap filters and frames that band; the second clears the filter and frames the whole venue.focusis sent on both: the unframed path leaves the camera inside the buyer's drill-in and the map returns washed out.
Name SeatLayerSeatNotes (rows from seatLayerSeatNoteRows) · Style
slots none — the bands take the surface's own tone tokens · Spec §3.8.9
- Inputs a seat's
accessibility[],wheelchairSpaceTypeandcommercial(restrictedView,obstructedView,premium,note). - Model first.
seatLayerSeatNoteRowsdecides which rows a seat earns and in what order — accommodation types, the wheelchair provision, restricted view, obstructed view, premium, the organizer's note — and every surface draws that one list: the seat card as full-bleed bands under the category band, the cart card as the same rows in words (seatLayerCartNoteLines). Restricted and obstructed are SEPARATE rows; a wheelchair accommodation with a provision reported yields the provision row only; the organizer's sentence hangs under the first selling mark it explains, and is its own row when there is none. - Anatomy full-bleed bands, no radius and no border,
size.notePadY×size.notePadX, asize.noteIconSizeglyph withsize.noteIconGapbeside it, titletype.noteTitleand organizer linetype.noteBody, a hairline ofcolor.*.divideratopacity.noteHairlineon every join but the first.compact(the wide layout's tap card) uses thenoteCompact*sizes. - Tones neutral
color.*.textatopacity.noteNeutralWash; cautioncolor.*.warningatopacity.noteToneWashwithcolor.*.warnText; premiumcolor.*.premiumwithcolor.*.premiumText. Each ink is measured against the tinted band, not the surface it is mixed from. - Callbacks none. It is a statement, not a control.
Name SeatLayerSeatIcon / seatLayerSeatGlyphs · Spec §3.8.9
Sixteen drawings — the twelve accommodations, restrictedView,
obstructedView, premium and note — plus the accessibility sheet's own
contrast disc. Authored in a 20-unit box, stroked at 1.45 units with round
caps and joins in the caller's ink, hidden from assistive technology. The path
data is transcribed verbatim from the runtime's shared set and is the contract:
a port transcribes the same strings rather than redrawing them, and never
substitutes an emoji or a platform icon. An unknown key draws nothing.
Name SeatLayerPickerMapControls · Style slot iconButtonStyle
- Inputs
map.isVenue3D,map.focusedSectionId,map.canZoomIn,map.canStepBack(frommap.atVenueFit, falling back tomap.canZoomOut),capabilities. Instance overrideaccessibilityControl:— the widget drawn at the head of the column; null drawsSeatLayerPickerAccessibilityFilters. The drop-in layout passes whateverbuilders.accessibilityFiltersreturns, so replacing the control does not mean rebuilding the column. - States phone corners / wide rail; the map-only controls stand down while the immersive scene is up.
- Anatomy round controls
size.mapControlSize, the accessibility controlsize.accessibilityControlSize. ONE bottom-right column on both compositions — the ♿ control, then the zoom discs,size.zoomColumnGapapart — atsize.mapAnchorInsetfrom the map's edges. It lifts bysize.dockBarHeightonly where a host opted into a dock; by default no width mounts one. The accessible-section stepper sits beside the accessibility control,size.accessStepGapfrom it, on its inner side. - The ♿ control heads the column (2026-09-06). It stood alone in the
bottom-left region, opposite the stack of discs, and the wide composition
drew it a second time in its side panel. Who can sit where is an earlier
question than how close the camera is, so it is the disc above
+, once, on both widths. - Phone column exactly three,
size.zoomColumnGapapart, top to bottom: the ♿ control,+(picker.zoomIn) and a framed dot,strings.fitWholeVenue, that shows the whole venue from any depth viapicker.overview. There is no−on the phone (2026-09-06): its only rung of its own was among the seats, everywhere else the one step back is the whole venue, and two discs for one move read as a puzzle. Pinch steps out.SeatLayerPickerZoomOutButtonstays public for a host's own composition. +retires but keeps its slot — inert oncemap.rungisseatsormap.canZoomInis false, drawn with its space maintained, because the column is anchored at its foot and a disc that left the tree pulled the ♿ disc down under the thumb reaching for it.- The whole-venue disc is never dimmed. Snapshot camera facts go stale after a pinch, which changes no state; dimming the escape hatch on a stale reading stranded buyers. At the venue already the press is a harmless no-op.
- Disabled discs dim in place rather than appearing and disappearing
under the thumb: the glyph steps back to
color.*.mutedTextat 55 per cent, the ring to the chrome hairline at 60 per cent, and the shadow is dropped. The ground never changes and there is no opacity wash — one read as a grey blot on the light map and vanished on the dark one. - While a seat card is up on the phone the whole column fades to opacity 0
over
motion.duration.crossfadeand goes inert. It fades rather than unmounting, so nothing else on the map moves. - Ground
color.*.chromewith acolor.*.chromeLinehairline, from the map chrome's side, never the panel'ssurface/divider— those vanish into a dark map at 1.14:1. Lightchromeis#d6dce6with the darker edge; darkchromeis#556278with the pale one. Dark separates by the fill (2.96:1 against the map), light by the edge (3.72:1 against the disc). Applies to every floating control, the accessibility disc and the Map/3D track included. - Wide
+and−under the same ♿ disc, and no fit control of its own:SeatLayerPickerZoomToFitButtonleft the rail on 2026-09-06 and stays a public component for a host that mounts it itself. The immersive scene keeps its Fit chip, which saysstrings.fitWholeVenuelike the phone's disc. - Commands
picker.zoomIn,picker.overview(the phone's whole-venue disc),picker.zoomOut(the wide column's−),picker.setAccessibilityFilters,picker.setColorblindSafe,picker.setBuyerView. - Note
SeatLayerPickerViewModeControl(the Map/3D segmented control) is a member of this stack on wide layouts only; on a phone the top rail owns it.
Name SeatLayerDockBar · Style slot dockBarStyle · Instance
override style:
Off by default on every width. dockBarFor resolves showDockBar ?? false,
so no composition mounts a bar unless the host asks: the prev/next arrows bought
a two-tap version of a gesture the finger does better, and the bar's height plus
the home-indicator inset pushed every bottom-corner control up the screen. The
phone's way back is pinch-out past the melt point, the back-to-overview control
top-left, or the whole-venue disc at the foot of the map's column; the wide
layout keeps − as well. A host that wants the bar sets showDockBar: true;
the widget also mounts standalone. Everything below describes the bar where it
IS drawn.
- Inputs
sections[](withaccessibleFree),map.focusedSectionId,map.rung,map.accessibilityFilter. - States not mounted unless the host asked; hidden at rung
venue; visible at rungseats; step controls disabled at the ends ofsections[](never wrapping around). - Anatomy edge-to-edge,
size.dockBarHeightplus the bottom safe area, elevationelevation.dockBar. Left: a 10 pt dot in the section's colour, the section name (type.dockSection, ellipsizes) andN left(type.dockCount, never ellipsizes; omitted whenseatsLeftis unknown), followed by· ♿ Nunder an active filter onsection-access-counts-v1where this section was counted — absent counts stay silent, never♿ 0. Right:‹ ›section steps, then‹ Venue. - Motion slides in over
motion.duration.dock; the name cross-fades overmotion.duration.crossfadewhen the focus changes. - Callbacks
onSectionChanged(id),onOverview. - Commands
picker.focusSection { id },picker.overview.
Name SeatLayerConfirmCard · Style slots confirmCardStyle,
primaryButtonStyle, secondaryButtonStyle, pillStyle · Instance
override style:
- Inputs the newest unconfirmed
SelectedSeat,capabilities(seatView,venue3d), and — onseat-view-thumbnail-v1— the seat'sseatViewThumb,sightlineMetresandseatViewConfidence. Plusmode:(SeatLayerConfirmCardMode.add|.remove), which chooses the question. - States with a photo (loading, arrived, never arrived), without one (no
strip at all, and the 3D square in the decision row), 3D-only, with a sight
line, with tiers, with seat notes; the confidence teaser or its passport chip
inside 3D; committing; removing —
the same card over a seat already in the cart, raised by a second tap on it
(bridge event
seat.retap, payload{ seat }, the seat still selected). The primary readsstrings.removeSeatincolor.*.errorwith a cross rather than a tick; Cancel, the drag, the tap outside and Back all KEEP the seat; and the seat stays counted in the cart the whole time it is asked about. - Commands
picker.openSeatView,picker.showSeatIn3D,picker.deselect,picker.removeCartLine(the remove answer), and — where the bundle advertises it —picker.setSelectionFocuswith{ seatId }on open and{ seatId: null }on close, which is what makes the runtime ring the seat the card is asking about and pale its neighbours. Paint only: no busy state, safe read-only, and never raised as an error. - Backdrop the map goes behind glass: a black veil at
opacity.confirmScrimunder asize.confirmScrimBlurblur, masked clear tosize.confirmScrimClearRadiusaround the tapped seat and reaching full strength atsize.confirmScrimFeatherRadius. A spotlight, not a curtain. Only drawn when there is a seat point to centre on, never takes a pointer event, and under reduced transparency drops the blur foropacity.confirmScrimFlat. - Anatomy
size.confirmCardMaxWidth, capped at the map width less2 × size.confirmCardGutter, radiusradius.confirmCard, elevationelevation.confirmCard.- Identity grid,
size.confirmIdentityHeight: section, row and seat as three EQUAL centred cells, hairline-divided, keys atsize.confirmIdentityKeyFontSizeand values atsize.confirmIdentityValueFontSize. Only a section longer thansize.confirmSectionShortMaxdrops tosize.confirmIdentityLongSectionFontSizeand wraps to two lines. - Category band,
size.confirmBandHeight: the category colour itself, full bleed, no dot and no rail, with its ink chosen per colour (white where white clears 3:1, otherwise#0B0F19). The name atsize.confirmBandNameFontSizeand the price atsize.confirmBandPriceFontSize— and no remaining count; the legend keeps that. Paddingsize.confirmBandPadTop/…PadTrailing/…PadBottom/…PadLeading, which is what keeps the price off the card's edge. In 3D,size.confirmImmersiveBandPadY/…PadXandsize.confirmImmersiveBandPriceFontSize. - Photo strip,
size.confirmPhotoHeight, only where the seat names an authored photograph, carrying theView from hereand3Dpills and, in its trailing top corner, the sight line (strings.sightline,size.confirmSightFont/confirmSightPadX/confirmSightPadY). With no photograph the slot is not drawn at all — no rail, no caption — and a photograph that never arrives collapses it away overmotion.duration.thumbOut. 3a. Confidence teaser, 3D card only and only where the seat carries one:size.confidenceTeaser*, headline overmodeledTarget ?? reality, withstrings.passportin the readable accent at the trailing edge. Where a host can open the passport it is instead a chip on the inspection row. 3b. Inspection row, 3D card only: one line ofsize.confirmInspectChipHeightchips —strings.passport(with an accent dot) andstrings.viewFromHere(spoken asstrings.viewFromThisSeat) — atsize.confirmInspectChipFontSize. - Seat notes, where the seat carries any, as full-bleed bands directly
under the category band and ABOVE the tier chooser — see SeatNotes below.
Then tiers (
size.confirmTierHeight). The body owns no padding of its own; the tier chooser carries the gutter so the bands can reach both edges. - Actions,
size.confirmActionHeight: with no photo strip a 44 × 44 ghost square carrying a cube andstrings.venue3Datsize.confirm3dSquareFontSizeopens the row, thenCancelat 34 % of the whole row and✓ Add seatat the rest, each in its own box atradius.buttoninside the card's gutter.
- Identity grid,
- Placement one home, and the map moves instead: a fixed bottom sheet
size.confirmCardRestInsetabove the map's foot, for every seat. Nothing about the tap is read. While it is up, the picker reports the sheet's band (size.confirmCardSeatGapabove the card's top edge) as the bottom viewport inset, so the runtime frames the venue into what is left clear above it. Full rule and constants:picker-spec.md§3.8.2. - Motion the map dims to ink behind it (
SeatLayerPickerStyles.scrimColor); the card springs up from the foot of the map withmotion.curve.springovermotion.duration.cardEnter.Add seatinvites once, breathes until touched, and then the add runs as ONE ordered chain: the confirm sweep fills the button under a drawn tick overmotion.duration.pressSweepand the label turns toAdded; a chip in the category's colour carrying the seat label flies from the card to the foot overmotion.durationOutsideBudget.confirmFlightonmotion.curve.spring; on landing the controller'scartLandingends; the footer's count and total swell 1 → 1.3 → 1 overmotion.duration.bumpwith the ink lerping to the accent; and only THEN does the map's lift pull back. A map moving under a chip still in flight read as two motions fighting. Under reduced motion no chip is created, so nothing waits on a landing. - While the card is up the sheet's handle disc is not drawn, the map's
control column fades to opacity 0 and goes inert, the footer's count and
total EXCLUDE the candidate (
confirmedCartLines), and the footer's call to action stays a disabled button with the label it already had — not a sentence about the card. - Callbacks
onConfirm,onCancel,onViewFromSeat,onShow3D,onSeatConfidence(the teaser is a button only where a host takes it). - Commands
picker.openSeatView,picker.showSeatIn3D,picker.deselecton cancel. Inremovemode:picker.removeCartLineon the primary — the same path the cart row's ✕ takes — and NOTHING on any of the ways out.
Web only. There is no native surface and nothing here is to be ported.
No Dart name, no style slot, no token. Runtime source: hoverCard.ts (the DOM
element) and hoverCardContent.ts (the pure model). Spec: picker-spec.md
§3.8a.
- Mouse only, by design every update is gated on
pointerType === 'mouse', a touch or pen pointer hides the card, and a pointer-leave takes it down. A tap synthesizesmousemove/mouseover, neverpointermove, so a touch sequence produces no card at all. It is decoration over a canvas that already announces every seat on the keyboard path:aria-hidden, no pointer events, no focus. - Modes one reused card, switched on the renderer's own
seatsPickable()— the same test that decides whether a click picks a seat or zooms. Above the line it describes the seat under the cursor, below it the section block. - Head the labelled SECTION / ROW / SEAT cells shared with ConfirmCard, hairline-divided, so a hover is the first frame of the card a click opens rather than a differently shaped restatement of it. A section card's head is its name, centred over the bands. Rows carry category name and price only — availability counts were refused (2026-09-04): a per-category "12 left" on every hover is a pressure device, and the number most likely to be stale.
- Category band full bleed, edge to edge like a legend chip, no dot and no
rail — bands inside a margin read as buttons on a panel rather than as one
card about one place. Ink from
categoryBandInk(), and the direction is chosen by the fill, not by strongest contrast: a light fill takes dark ink, a dark or saturated fill takes light ink, and either is kept when it clears the small-text bar on its own. Strongest contrast put black on the arena red (6.3 : 1 against white's 3.3 : 1), and the band read as a warning stripe rather than as a price. - Host stand-down while the host has its own card up about that seat — the
pinned "cannot be taken" explanation, or the confirm card —
hostCardOpen()answers true, the model resolves to nothing and the map's card hides. Without it, clicking a sold or held seat produced two cards saying the same thing. - Placement below-right of the cursor at 16 px; near a viewport edge it flips to the other side at the same offset rather than sliding under the pointer, and clamps at an 8 px margin only when it fits on neither side.
One web shape, one native user. The web picker uses a single interrupt
shape (.sl-interrupt) for the two moments where something has ended and there
is exactly one way on: hold expiry (pickerHoldExpiredDialog.ts) and session
recovery (accessPanelElement / accessCopy in pickerFragments.ts). Styles
in pickerStyles.ts. Values below are the web's own, in px — no token is
minted for a shape three of the four SDKs do not draw.
- Veil the scrim ink at 35 %, filling the widget root with 16 px of padding and the card centred in it. The map keeps painting behind it, so the buyer sees the venue they are coming back to rather than a blurred-out widget.
- Card
min(420px, 100%), 24 px padding (20 px on a narrow layout), 15 px radius, on the surface with a hairline and a deep shadow. Left-aligned — these carry a sentence and a fact, and centred body copy makes both harder to scan — and it scrolls rather than clipping its button when it cannot fit. - Title 20 px, weight 700 (18 px narrow), with an inline glyph at text size rather than an accent disc, which gave a recoverable pause the weight of an error.
- Body 14 px in the muted colour. The one line that is fact rather than explanation — which seats were released — takes the text colour at weight 700.
- Action, and there is exactly one. A 44 px-minimum pill on the accent, trailing on a wide layout and full width on narrow, where it is the only thing to do and a right-aligned pill wastes the row that matters. Every interrupt has exactly one primary action: two access reasons once built none, which left the buyer behind a veil with nothing to press. On the hold dialog a press on the veil and Escape run that same action rather than dismissing into the state underneath, which is gone.
What Flutter takes. The session-recovery panel, and only that:
SeatLayerPickerAccessPanel (picker-spec.md §3.13.3) is this shape and keeps
the one-action rule, including for the two reasons that used to be dead ends.
Hold expiry does not use it — Flutter announces a lapse without blocking the
map; see picker-spec.md §3.13.7, "Divergence from the web picker".
Name SeatLayerCartSheet · Style slots sheetStyle,
continueButtonStyle · Instance overrides style:,
continueButtonStyle:
- Inputs
cart,hold,capabilities.bestAvailable,event.currency. - States collapsed empty, collapsed with tickets, expanded empty (the best-seats form), expanded with tickets.
- Anatomy ONE surface: ground
color.*.background, a hairline on its top edge and a shadow of0 -8 26 -20. Radiusradius.sheet;elevation.sheetis available throughsheetStyleand is 0 by default, because the shadow is drawn.- Handle a
size.sheetHandleWidth×size.sheetHandleHeight(44 × 44) disc, not a pill: filled withcolor.*.background, lifted by its own shadow (black at 20 per cent, blur 10, offset 0 / 2), carrying a 16-point chevron incolor.*.mutedText, straddling the sheet's own top edge —size.sheetHandleOverhang(22) above it,size.sheetHeadHeight(16) inside. No hairline and no band under it. The chevron points up when collapsed and down when open, rotating overmotion.duration.chevron. The whole band is the tap target and the sheet's drag runs under it. Not drawn while a confirm card is up. It is the cart's ONE named toggle in both states:strings.expandCart/strings.collapseCart, with the expanded state and its own tap action. - Collapsed the FOOTER BLOCK ONLY — head, foot, safe inset. No cards rest in the collapsed sheet: a list that unrolled itself on every add read as a panel the buyer had not opened. Never a fixed peek either: a surface clipped to a different height than the content it holds cuts the bottom off its own buttons.
- Cart region the CartList in
size.cartTrayPadXgutters, height zero while collapsed; opened, it shows up tosize.cartPeekMaxHeight(189) — three cards and a sliver of the fourth — scrolling inside its own box, and also carries the closed-sales statement and, on an empty cart, the best-seats form. The extras stay laid out offstage while shut, so the sheet opens straight to its height. - Foot the lapse notice, the hold-ending cue, the inline action error,
the total line,
size.footTotalGap(8), the BookButton and the centred attribution, insize.footPadX(16) /size.footPadTop(4) /size.footPadBottom(2). A hairline above it only when the sheet is open with tickets — collapsed, the panel's own top edge is the only edge there is. Not restyled on a phone at all — one block on both widths. The attribution is absent entirely on a white-labelled account. - Total line
strings.noSeatsSelectedon an empty cart, elsestrings.ticketCountattype.footTotalLabel(13 / w600) with the total attype.footTotalAmount(17 / w700) in tabular figures on the trailing edge. Collapsed and non-empty it grows a second, muted line naming the seats — each line label with-replaced by·, joined by a comma and two spaces (204 · Q · 7, 204 · U · 13), 12 / w600 incolor.*.mutedText, one line, ellipsized, with an unfold glyph at 16 — and tapping that line opens the sheet. A live region; the count and total swell 1 → 1.3 → 1 overmotion.duration.bumpwith the ink lerping to the accent, fired when the flying chip lands, never on the press. There is noFrom <min>anywhere: it stated a price and offered nothing to do about it.
- Handle a
- Rules the sheet never opens itself; any map tap while expanded collapses it, and so does a tap on a cart card. Count and total exclude a seat a confirm card is still asking about.
- Motion
motion.duration.sheet; springs, not tweens (seepicker-spec.md§3.9). - Commands
picker.checkoutfrom the foot's own button.
Name SeatLayerCartList · Card SeatLayerCartCard
- Inputs
cart.lines[]joined toselection[]. - Anatomy one card per ticket — the SAME card on every width — separated by
size.cartCardGap(6). Each is at leastsize.cartCardMinHeight(52) oncolor.*.surface, hairline border,size.cartCardRadius(12), padded12 / 4 / 4 / 4(leading / top / trailing / bottom; the trailing side is tight because the action boxes carry their own touch floor): a category dot (9 pt, with a hairline so a pale category is still findable), a 10 pt gap, the name at 15 / w700 — the only part that ellipsizes — the position line under it at 13 / w600 incolor.*.mutedTextwith tabular figures, joining row, seat and ticket type with·(R · 6 · Lower Bowl), then the amount at 15 / w800 incolor.*.text, then the two actions. Both actions are 18-point glyphs incolor.*.textinside a TIGHTsize.minimumHitTargetbox: Material pads an icon button to 48 of its own accord, and four points per card is what puts the fourth card past the open cap. - Actions the eye opens the view from that seat — drawn only where the host
allows it, the runtime advertises
seatViewand the seat carries an authored photograph — then the ×. Cards may also be swiped toward the leading edge to remove. - Card tap a press on the card's own face — not the × and not the eye —
sends
picker.frameSeat { seatId, fraction: 0.5 }; the sheet stays where the buyer put it, and the seat lands in the room the open sheet leaves. A line with no selected seat behind it is not pressable. - Notes what the organizer said about the seat, said ONCE and in WORDS,
under a hairline inside the card: accommodations, then the wheelchair
provision (
strings.accessiblePhysicalSeat/strings.emptyWheelchairSpace), then Restricted view, Obstructed view and Premium seat as SEPARATE lines, then the organizer's sentence — attached to the first selling mark, or its ownstrings.organizerNoteline where there is none. Title attype.cartNoteTitle, the sentence attype.cartNoteText. No plate and no ground: the card is already a bordered ticket, and a tinted band inside one reads as a card inside a card. The icon rows belong to the seat card, not here. - Held cards a wash of the accent at 7 per cent over the surface, a border blended 45 per cent toward the accent, and the dot becomes a lock — a lock is not a colour. Held cards are never swipeable; the × stays.
- Rules no folding and no
+N more. Consecutive seats used to collapse into a run, which was a second rendering of one cart to keep in step; the open sheet caps and scrolls instead. Long text truncates with an ellipsis — there is no shorten-by-fact ladder (a known gap, spec §4.9). - States a card the buyer has asked to remove is drawn at
opacity.removingwith its × inert and its swipe disabled, from the press until the snapshot that drops it (or the failure that puts it back). Any cell whose words change between snapshots cross-fades overmotion.duration.crossfade; the rest of the card does not move. Seepicker-spec.md§3.13. - Commands
picker.removeCartLine { label }, sent silently — no toast and no undo, because the card leaving and the total moving are the whole answer. It carries its own busy action (removingCartLine) because it is the one inventory mutation that does not put the checkout call to action down.
Name SeatLayerBookButton · Style slot primaryButtonStyle (the
sheet merges continueButtonStyle over it) · Instance override style:
- Inputs
cart,hold, busy state, and whether this width may offer the best-seats form (onFindBestSeats). - Anatomy full width,
size.checkoutButtonHeight, radiusradius.button,type.bookButton. Carries its own label only — the total is on the line above it. - States idle, busy (spinner), disabled with a reason, and — on a phone
with an empty cart — an ENABLED
strings.findBestSeatsCtathat opens the sheet on the best-seats form (the form lives only in the open sheet). While a confirm card is up the button keeps the label it already had and is simply disabled — never a sentence about the card — and offers no finder. Disabled is a designed state — a surface-toned ground, muted ink, an inset hairline — not Material's own greys, which vanish on the dark scene sheet. The label ladder is inpicker-spec.md§3.10.3. - Commands
picker.checkout, thenpicker.rejectHandoffif the host refuses the handoff, so a rejected hold is never stranded.
Name SeatLayerVenue3D · Style slot pillStyle
- Inputs the focused
SelectedSeat,map.isVenue3D,capabilities. - States live seat view / venue 360°; stepper disabled in venue mode.
- Anatomy one dark glass whatever the resolved mode is —
color.dark.immersiveGlass, its border and ink, blurred bysize.immersiveGlassBlur; captions use the deeper caption glass. Every piece is a pill. Top-left‹ Back to venue(size.immersiveBackPillHeight), drawn only while the buyer is sitting in an exact seat. Bottom: a caption chip naming the seat, then‹previous seat,Open venue 360°,›next seat, and recentre, as chips ofsize.immersiveNavChipHeight. - Motion
motion.duration.immersive. - Commands
picker.showSeatIn3D,picker.openVenue360,picker.setBuyerView,picker.recentre3D.
Name rendered by SeatLayerPickerHeader (showHoldPill) · Style slot
pillStyle
- Inputs
hold.expiresAt. - States counting down; absent when there is no picker-owned hold — a hold handed to the host is the host's to display.
- Anatomy a true pill (
radius.pill) with a timer glyph andmm:ss,type.pill. - Haptics
holdCreatedon creation,holdExpiredon expiry.
Name SeatLayerPickerTestModeIndicator
- Inputs
event.mode. - Rules required chrome: it has no host switch, and exactly one may render. It steps below the immersive scene's own back control rather than under it.
- Anatomy a pill of
size.testChipHeightatradius.pill, one recipe in both themes: warning ink on the warning colour atopacity.warnPillWashover the surface, a leading dot ofsize.testChipDotSize. Copystrings.testMode; the accessible name keepsstrings.testModeLong. Amber, never the accent — an environment flag must not wear "buy" gold. - Contrast the ink is measured against the wash, never the bare surface,
and must clear 4.5:1 on it: the hue if it already reads, else the hue walked
toward
color.*.textuntil it clears, else a neutral chosen by contrast. Never a fixed blend — one produced a 2.3:1 pill on a mixed theme.
These carry no separate entry here because their whole description is their specification. Names, slots and files:
| Component | Slot | Spec |
|---|---|---|
SeatLayerFloorStrip |
floorStripStyle |
§3.7 |
SeatLayerBestSeatsForm |
— | §3.11 — lives ONLY in the open sheet (the empty cart's way in is the footer's own full-width strings.findBestSeatsCta). A one-line ✦ strings.findSeatsTogether title, truncating rather than wrapping, with an ⓘ immediately after the title that toggles a one-line strings.closestGroupChosenInstantly under it (animated size, muted 12 pt, named strings.aboutBestSeats). No premium chip — the snapshot carries no chart-level premium fact; see §4.9 |
SeatLayerPickerToast / …ToastQueue / …ToastLayer |
— | §3.12 |
SeatLayerPickerLoadingView / …ErrorView / …EmptyView |
— | §3.13.1–2 |
SeatLayerPickerAccessPanel |
— | §3.13.3 |
SeatLayerPickerSalesClosedStatement / …SalesClosedPill |
— | §3.13.4 |
SeatLayerPickerSoldOutOverlay |
— | §3.13.5 |
SeatLayerPickerExtendHoldPrompt |
one named +5 min step, once per hold, dismissable; phone: off by default, host opt-in via SeatLayerPickerChromeOptions(showExtendHoldPrompt: true); wide keeps it |
§3.13.8 |
SeatLayerPickerBookedOverlay |
— | §3.13.10 |
SeatLayerPickerGeneralAdmissionPrompt / …TablePrompt |
— | §3.13.11–12 |
SeatLayerPickerAccessibilityFilters |
— | §3.5, §4.10 — the head of the map's control column on both widths; switches apply on change, no apply step, the sheet stays open; opening it clears a pending seat card; the sheet is bounded to size.accessSheetMaxHeightFraction (floor size.accessSheetMinHeight) and scrolls inside it. Rows are a FIXED 50 high, closed by a hairline of color.*.divider at 35 per cent, and read icon cell (size.accessRowIconCell) · size.accessRowGap · label (flexible, ellipsized) · ⓘ in a 36 column where the row has a note · count in a 68 column · size.accessRowSwitchGap · drawn switch. The whole line toggles. The count is N free on a quiet ground of color.*.text at 6 per cent, tappable to jump where the runtime supports it; a sold-out provision stays, named and disabled, reading 0. The two map switches (hide limited view, colourblind-friendly) sit under a strings.viewGroupTitle heading |
SeatLayerPickerAccessibleStepper |
— | §3.4.1 |
SeatLayerSeatViewChrome |
seatViewChromeStyle |
§3.15 |
SeatLayerPickerAttribution |
— | §3.10.3 |
SeatLayerCheckoutCta (the one label resolver) |
— | §3.9, §3.10.3 |
SeatLayerTypeScale / seatLayerReadingOrder / seatLayerBoldWeight |
— | §4.10 |