# @poetry/controllers API

The JavaScript surface: the 53 Stimulus controllers and the DOM helper modules, one source over two channels (the engine's importmap pins and the npm package) - generated from the source JSDoc and the controllers manifest.

## @poetry/controllers

@poetry/controllers - poetry's Stimulus controllers + DOM helpers, one
source shipped over two channels: importmap-first (the engine
pins this tree; zero build) and this same tree as the npm package for
esbuild / Vite / jsbundling hosts. Never requires a bundler.

### .registerPoetryControllers(application)

The bundler-host one-liner: registers every poetry controller on the
host's Stimulus application (importmap hosts get the same
registrations via the engine's pins + this same call in their
controllers/index.js), arms the portal event bridge, and installs the
unregistered-controller warning.

## poetry--core--state

The controllable-state controller: seeds the data-open/
data-closed pair from a Value default when no other layer owns it, and
exposes toggle/open/close actions. "Controlled vs uncontrolled" is just which layer wrote the
attribute - a server re-render, the URL, an Outlet, or this default -
the controller code is identical either way.

Optional reflection targets (the Collapsible contract): a trigger
target mirrors aria-expanded; a content target rides the presence
helper (the pair flip deferred through animationend, hidden applied
only after the exit animation finishes).

**Targets**: `content`, `trigger`

**Values**: `state` (String, default: "closed")

### #connect()

Seeds the pair from the Value default when no other layer wrote it,
then reflects the current state onto the optional targets.

### #disconnect()

Abandons any exit animation still holding the content.

### #toggle()

The toggle action: open when closed, close when open.

### #open()

Opens: flips the pair, unhides the content target (when present) and
runs its entry presence, mirrors aria-expanded onto the trigger.

### #close()

Closes: flips the pair and holds the content target through its exit
animation before hiding it (the presence contract).

## poetry--core--dialog

The native-dialog primitive: borrow the PLATFORM overlay -
showModal() gives the focus trap, Esc handling, top-layer stacking, and
focus return for free; this controller adds what the platform doesn't:
the data-open/data-closed pair for CSS variants, backdrop-click
dismissal, a body scroll-lock, and the presence-hold close - exit flips
the pair to data-closed and HOLDS the dialog through its CSS exit
animation before the native close() (synchronous when no exit animation
applies, so reduced-motion and unthemed hosts close instantly).
Consumed by Dialog (and AlertDialog / CommandDialog / Sheet).

**Targets**: `dialog`

**Values**: `dismissible` (Boolean, default: true); `hotkey` (String, default: "")

### #connect()

Heals a restored zombie snapshot, subscribes the before-cache close,
and arms the opt-in global hotkey.

### #disconnect()

Balances the scroll lock and unwires the hotkey and before-cache
subscriptions.

### #toggle()

The toggle action (and the hotkey's landing): open <-> close.

### #open()

Opens modally: showModal() (the platform trap, top-layer and focus
return), the pair flip, and the body scroll lock. Returns the
resulting state (what an agent tool reports back).

### #close(event)

Closes with the presence hold: flips the pair to data-closed and
holds the dialog through its CSS exit animation before the native
close() and the unlock (synchronous when no exit animation applies).
The native cancel event (Esc) routes through here so state stays in
sync.

### #backdropClose(event)

The click action discriminating backdrop presses: a backdrop click
targets the <dialog> element itself AND lands outside its bounding
rect (the backdrop is rendered by the dialog). The target check alone
is not enough: clicks on the dialog's own padding / grid gaps also
target the element (found in a live browser pass - they were
incorrectly dismissing).

### #lockScroll()

Takes the shared refcounted body scroll lock (scrollbar-width
compensated) - subclasses (sheet/drawer/sidebar) inherit these entry
points unchanged; the instance flag keeps double-unlocks (disconnect
after close) balanced.

### #unlockScroll()

Balances `lockScroll`; safe when already unlocked.

## poetry--core--popper

The anchored-positioning primitive: one
controller over the VENDORED @floating-ui/dom. A fixed middleware
pipeline - offset -> shift -> flip (both gated on avoidCollisions)
-> size -> arrow (when an arrow target exists) -> hide - and a fixed
output contract: data-side / data-align mirror the RESOLVED placement
(after flip), and the content gets the sizing CSS vars
(--transform-origin / -available-width / -available-height /
-anchor-width / -anchor-height) so ported Tailwind classes like
data-[side=top]:slide-in-from-bottom-2 and
max-h-[var(--available-height)] work unchanged.

The Arrow stays a markup primitive: any [data-slot=popper-arrow]
marked as the arrow target is positioned + given data-side here; the SVG
itself is poetry-ui's business.

VIRTUAL-ANCHOR mode (the ContextMenu contract): a non-empty anchorPoint
value - canonically the data-poetry--core--popper-anchor-point="x,y"
attribute on the controller root (the DOM is the store) - floats against a
floating-ui VirtualElement: a zero-size rect at (x,y) client coords.
The resolved ELEMENT anchor rides along as
contextElement so autoUpdate still tracks ancestor scroll/resize.
setAnchor(x, y) is the ergonomics wrapper ContextMenu calls from its
contextmenu / long-press handlers; clearing the value returns to plain
element anchoring.

Consumers: Popover, Dropdown/Context Menu, Tooltip, HoverCard, Select,
Combobox, Menubar.

**Targets**: `anchor`, `arrow`, `content`

**Values**: `anchor` (String, default: ""); `anchorPoint` (String, default: ""); `side` (String, default: "bottom"); `align` (String, default: "center"); `sideOffset` (Number, default: 0); `alignOffset` (Number, default: 0); `avoidCollisions` (Boolean, default: true); `strategy` (String, default: "fixed")

### #connect()

Arms autoUpdate (which fires one immediate position pass).

### #disconnect()

Disarms, stranding any in-flight update (the generation bump).

### #anchorPointValueChanged()

Stimulus value callback - reactive re-anchor: switching between
element and virtual anchoring (or moving the point) re-arms
autoUpdate against the new reference. Fires during initialization
too, before connect - the connected guard skips that first call.

### #strategyValueChanged()

Stimulus value callback - reactive strategy (the portal seam): a
consumer flips fixed <-> absolute when it portals/restores content;
re-arming repositions in the new coordinate space immediately.

### #reposition()

Manual re-run for consumers whose geometry changed outside
autoUpdate's sensors (e.g. content swapped by a Turbo Stream).

### #setAnchor(x, y)

The ContextMenu entry point: stores the pointer coords (the ATTRIBUTE
is canonical - inspectable, server-settable) and repositions
immediately; the value-changed callback then re-arms autoUpdate's
tracking.

### #setAnchorElement(element)

The NavigationMenu viewport entry point: floats against a
caller-supplied element (the active trigger) instead of a
target/selector. Re-arms autoUpdate so the new reference's ancestors
are tracked, then repositions - the positioner's inset transition
turns that write into the position morph.

## poetry--core--dismissable

The dismissal layer: Escape + pointerdown-outside for every
overlay. This controller NEVER removes DOM - it dispatches "dismiss" and
the consumer closes itself (removes the node, collapses a disclosure,
navigates a frame). The class-level stack makes Esc topmost-only, so
stacked overlays peel one at a time.

**Values**: `disableOutsidePointerEvents` (Boolean, default: false)

**Events**: `poetry--core--dismissable:dismiss`, `poetry--core--dismissable:interact-outside`

### #connect()

Registers on the layer stack, wires the capture-phase Escape and
pointerdown listeners, subscribes the before-cache dismissal, and
takes the body scrim when configured.

### #disconnect()

Leaves the stack and unwires everything, releasing the scrim when
held.

## poetry--core--focus-scope

The overlay focus scope: traps Tab/Shift+Tab within the
subtree, loops at the edges, and - the part to get exact - snapshots
document.activeElement on connect and RESTORES it on disconnect (focus
return). Backs Dialog-family overlays, Popover, Menus, Select, Command.

Listeners are wired here, not as data-actions: pause/resume must attach
and detach them dynamically as scopes stack, which data-action cannot do.

**Values**: `trapped` (Boolean, default: true); `loop` (Boolean, default: true)

**Events**: `poetry--core--focus-scope:mount-auto-focus`, `poetry--core--focus-scope:unmount-auto-focus`

### #connect()

Snapshots the focus-return element BEFORE any focus moves, pauses the
previous scope and takes the top of the stack, takes the guard
refcount when trapped, then runs mount auto-focus (cancelable - a
consumer owns initial focus by vetoing it).

### #disconnect()

Leaves the stack (resuming the scope below when this was the top),
releases the guards, and restores focus to the snapshot (cancelable -
a consumer sends focus elsewhere by vetoing it).

## poetry--core--roving-focus

The roving-tabindex engine: the group is ONE Tab stop -
exactly one item holds tabindex=0, the rest -1, and arrow keys move focus
among the collection items in DOM order. Orientation gates which arrows
are live (horizontal flips Left/Right under RTL via the direction
helper); Home/End jump to the edges; loop wraps. Backs Tabs, Toolbar,
RadioGroup, Menus, ToggleGroup, and the Select/Command list.

Dynamic items: NAVIGATION recomputes the collection on every keydown (the
DOM is the registry - always fresh, zero bookkeeping), but that alone is
not correct: an item appended between keystrokes would sit at its natural
tabindex and grow the group a SECOND Tab stop before any arrow is
pressed. The MutationObserver exists for that one job - re-stamping the
roving tabindex the moment items enter or leave.

**Values**: `orientation` (String, default: "vertical"); `loop` (Boolean, default: true); `manageTabindex` (Boolean, default: true)

**Events**: `poetry--core--roving-focus:entry`

### #connect()

Stamps the initial tab stops and starts the item-mutation
re-stamping (the header's second-Tab-stop hazard).

### #disconnect()

Stops the mutation observer.

### #keydown(event)

The group root's keydown action: arrows/Home/End move the roving
focus - orientation-gated, RTL-aware, loop-wrapping. The caret guard
leaves text-entry controls their keys until the caret reaches the
travel-direction boundary.

## @poetry/controllers/helpers/state

The controllable-state convention (the DOM is the store): runtime
state is a set of presence-boolean data attributes - written here,
styled by CSS variants (data-open:..., via the vendored bridge
variants that also match the older data-state="open|closed" value
form), owned by whichever layer set it (a Stimulus Value default, the
DOM, the URL, or a server re-render).

Keys are PAIRS (or triples): setting one member writes its attribute
and removes its counterparts. The negative popup/panel/pressed/selected
keys only remove, deliberately: there is no data-popup-closed -
absence IS the state.

### .stateOf(element)

The vocabulary key `element` currently wears, per the derivation order
above. Attribute-only negative states (the popup/panel/pressed/selected
negatives) have no positive attribute to find and derive as undefined -
callers that care test the attribute directly.

### .setState(element, key)

Writes vocabulary key `key` onto `element`: sets its add-attribute (when
the key has one), removes its counterparts, and announces the flip as a
bubbling `poetry:state-change` CustomEvent carrying `{ state: key }`.

Raises `Error` - on a key outside the vocabulary

## @poetry/controllers/helpers/presence

Presence: the mount/unmount animation convention - a helper, not a
controller (no element ownership here). Exit flips the pair to
data-closed and HOLDS the node in the DOM until its CSS exit animation /
transition finishes, then hands removal back to the caller via onRemove
(this module never removes DOM); enter just flips the pair to data-open
so the data-open: animation runs. State writes go through setState so
poetry:state-change fires like every other state flip.

### .flushPendingExits()

Settles every exit still waiting on its CSS animation, synchronously
(each runs its onRemove). Wired to turbo:before-cache at module load;
the dismissable layer also calls it directly (see above).

### .measurePresence(element, { property = "--poetry-presence-height" } = {})

The measured-entry/exit hook (the Accordion contract's height
mechanism): height keyframes cannot animate to auto, so the keyframe
chain reads a CSS var instead. This measures the settled box -
temporarily unhiding the element and suppressing its animations so
scrollHeight reports the real content height - writes "<n>px" to the
custom property, then restores exactly what it changed.

### .enterPresence(element, { measure = false, property } = {})

Runs the entry: flips the pair to data-open (through setState, so the
state-change event fires) wearing data-starting-style for exactly one
painted frame after the flip (the two-frame trick below), so CSS
transitions can animate FROM the starting declarations. No poetry class
consumes the attribute yet - it ships so a future theme layer can adopt
the transition idiom without touching JS.

### .exitPresence(element, { onRemove, measure = false, property } = {})

Runs the exit: flips the pair to data-closed and HOLDS the node in the
DOM until its CSS exit animation/transition finishes, then hands
removal back to the caller (this module never removes DOM). onRemove
runs at most once, on the first of animationend / transitionend (on the
element itself, not a child) or the safety timeout - or synchronously
when no exit animation exists. data-ending-style rides the whole exit.

## @poetry/controllers/helpers/portal

The portal-on-open mechanism: move popper
content to a stable container (body by default) while open so it can
position `absolute` - static under compositor scroll, immune to
transformed ancestors - and return it HOME on close, exactly where a
placeholder comment marks the origin.

THE EVENT BRIDGE (the logical-tree trap): a component-tree portal
re-bubbles events from its logical position; a DOM portal does not. A
host's data-action on the component ROOT would go deaf to events
rising out of portaled content. The bridge restores logical-tree
bubbling for poetry's OWN

### .registerBridgeEvents(names)

Adds event names to the bridge list. index.js registers the union of
every controller's declared `static events` at boot - the bridge list
stays honest against the manifest surface without portal.js importing
the controllers (no cycle).

### .resolvePortalContainer(root)

The portal-container seam, attribute-shaped: a host scoping themes to
a subtree points its overlays at a container inside that scope via
data-poetry-portal-container="<element id>".

### .isPortaled(content)

Whether `content` is currently portaled out (has a live placeholder).

### .logicallyContains(container, node)

Containment that follows portals HOME: a node inside portaled content
counts as inside `container` when the content's home position does.
Focus-scope's trap keys on this - a portaled sub level is outside the
root content's subtree but logically inside its tree, and the trap
must follow the logical tree or every portaled level reads as outside.

### .portalContent(content, { container = document.body } = {})

Moves `content` to `container`, leaving a placeholder comment at home
and wiring the event bridge for the registered list. No-op (false) when
already portaled or parentless. The home-effective `dir` is stamped
onto undeclared content for the trip, so direction-dependent behavior
survives the move (un-stamped on restore).

### .restoreContent(content)

Returns portaled `content` to its placeholder and unwires the bridge.
When the origin is gone (a morph replaced it) the content is DROPPED,
never stranded at the container.

## @poetry/controllers/helpers/announce

The announce SINGLETON: shared screen-reader live regions - a plain
module, not a controller. Injecting a node that IS a live region is
unreliably announced across SR/browser pairs (the region must exist
BEFORE its text changes), so consumers keep their own nodes
aria-live=off and route announcements
through here: TWO lazily-created sr-only regions on body (polite +
assertive), refcounted via acquire()/release() (regions removed when the
last consumer releases; consumers: Toast, async form status, Combobox
result counts - API changes after Toast ships are breaking, so the
surface stays exactly this).

Announcement mechanics: clear-then-set on a microtask (identical
consecutive messages re-announce), a per-region message queue with a
small gap between messages, textContent ONLY (live-region injection is a
real sink - never innerHTML).

Tab-visibility muting: while the tab is hidden both regions flip
aria-live=off (muted); announcements made while hidden keep at most the
LAST message, flushed on return (no backlog flood).

### .acquire()

Takes a refcount on the shared live regions (created on the first
acquire): a consumer acquires while it is connected and releases on
teardown.

### .release()

Releases one refcount; the last release removes the regions and their
visibility listener.

### .announce(message, politeness = "polite")

The announcement surface: queues `message` on the shared region for
`politeness`. While the tab is hidden only the LAST message is kept,
flushed on return.

## @poetry/controllers/helpers/breakpoint

The mobile breakpoint: matchMedia below Tailwind's md (768px), with a
change listener. The
first poetry consumer is the Sidebar's mobile-Sheet mode. Environments
without matchMedia (the dommy QuickJS engine, bare jsdom) report
DESKTOP - the server-rendered desktop shell is the safe default.

### .watchMobile(onChange)

Watches the mobile breakpoint: calls `onChange(isMobile)` immediately
with the current state and again on every crossing. Environments
without matchMedia report desktop once and never call again.

## @poetry/controllers/helpers/collection

The DOM is the registry: collection items are read from the document
in DOM order - no client-side bookkeeping, no registration step; a
membership question is always a fresh query.

### .collectionItems(root, selector = COLLECTION_ITEM_SELECTOR)

The collection items under `root`, in DOM order.

## @poetry/controllers/helpers/direction

Reading direction: the platform mechanism - the closest [dir]
ancestor - consumed by roving-focus (Left/Right flip) and popper (side flip).

### .directionOf(element)

The reading direction in effect at `element`: the closest `[dir]`
ancestor's value. Only an explicit rtl flips - dir="auto", dir="ltr" and
no [dir] ancestor at all resolve "ltr".

## @poetry/controllers/helpers/escape

Capture-phase Escape handling: the primitive under dismissable's
topmost-only Esc behavior.

### .isImeKeydown(event)

True when a keydown belongs to an IME composition. An Escape that
cancels IME composition must never reach dismissal: CJK users press
Escape to drop an in-progress composition, and closing the overlay
under them destroys the field they were typing into. Chromium reports
isComposing on the cancel keydown; some engines only mark it with the
legacy 229 keyCode - check both. Every Escape consumer (dismissable via
onEscapeKeydown, plus controllers with their own Escape branches) gates
through this predicate.

### .onEscapeKeydown(callback, { capture = true, target = window } = {})

Subscribes `callback` to Escape keydowns, with IME-cancel presses
filtered out via `isImeKeydown`.

## @poetry/controllers/helpers/filter_rank

The Command filter spec: deterministic
substring + a 5-band rank - deliberately NOT a fuzzy command-score. Pure string
functions so the CI spec table pins the contract: any scoring change is
a reviewed table change, never a silent reorder of every palette. The
score's ONLY job is picking the auto-highlighted first match - the
controller hides score-0 items and NEVER reorders the DOM (DOM order is
the ranking authority within a band).

Bands: prefix 4 > word-boundary 3 > substring 2 > keyword 1 > hidden 0;
an empty query scores 1 (everything visible). Matching is
diacritic-folded ("creme" matches "Crème") and case-insensitive.

### .normalize(value)

trim + lowercase + NFKD-fold combining marks: diacritic-insensitive
matching for free in every locale that marks are decorative in (folding
is always-on - the contract's documented call).

### .scoreText(label, query, keywords = [])

The pure scorer. label/query arrive RAW (normalization is this
function's business); keywords is an array of raw strings.

### .filterLabel(item)

The label an item filters against: data-filter-value overrides
(icon-rich content), else the item-text part, else the item's own text.

### .filterKeywords(item)

data-keywords: whitespace-separated extra filter terms an item may
carry beyond its visible label.

### .scoreItem(item, query)

Scores one collection item against the query via its filter label and
keywords.

## @poetry/controllers/helpers/focus_guards

Focus guards: two visually-hidden tabindex=0 sentinels at the
body edges so focusin/focusout fire predictably at the document boundary
while any trapped overlay is open - a Tab out of the last real element
lands on a guard (which focus-scope yanks back), never on nothing.
Refcounted module state: one pair per page no matter how many overlays.

### .ensureFocusGuards()

Takes a refcount on the page-edge guard pair, creating the two
sentinels on the first acquire. Pair every call with
`removeFocusGuards` on teardown.

### .removeFocusGuards()

Releases one refcount on the guard pair; the last release removes both
sentinels from the document.

## @poetry/controllers/helpers/hotkey

The hotkey descriptor grammar, extracted from the dialog controller so
any surface can speak it (the generic hotkey controller, future
data-hotkey affordances). "meta+k" / "ctrl+shift+p": '+'-separated
modifiers plus one final key token, matched exactly - unlisted modifiers
must be UP, so plain typing never triggers. "meta" matches metaKey OR
ctrlKey (⌘K on mac, ^K elsewhere - the command-palette convention).

### .matchesHotkey(event, descriptor)

Whether a keydown satisfies a hotkey descriptor (the grammar above).

### .isEditingTarget(event)

True when the event originates in a text-editing context - unmodified
single-key shortcuts ("/", "?") must stay inert while the user types
(the standard hotkey ignore list: form fields and contenteditable).

## @poetry/controllers/helpers/id_integrity

The composed-DOM duplicate-id tripwire: the ONLY
check that sees the page as actually composed - static lints can't see
across templates, frames, streams, or cached fragments, so this scans
the live document for duplicate [id] values after every composition
event and reports what it finds. Development tooling: install via
poetry_id_integrity_script (dev layouts); never wired in production.

Duplicate ids are always a bug regardless of source (ARIA IDREFs
resolve to the first match only), and under StableId they are the
signature of the two documented hazards: the same key rendered twice,
or sequence-mode collisions across frames/cached fragments.

### .scanForDuplicateIds(root = document)

Scans the live (composed) document for duplicate [id] values.

### .installPoetryIdIntegrityCheck({ report } = {})

Installs the scanner on the page's composition events (initial load,
Turbo loads, frame loads, morphs, stream insertions - each checked one
frame after the event so the DOM has settled).

## @poetry/controllers/helpers/incomplete_date

The nullable-segment date/time value (the IncompleteDate model, adapted
from an Apache-2.0-licensed source - source and license in
THIRD_PARTY_NOTICES.md): segments
are stored RAW so a user can edit day before
month - the object can hold February 31st and only commit constrains.
Hour is stored in the LOCALE'S HOUR CYCLE with a separate dayPeriod bit
(0 = AM, 1 = PM), so am/pm edits are independent and "12 means 0" lives
in exactly one place. Gregorian-only by design (the calendar seam is
clean if that ever changes - limits() is the only calendar knowledge).

### .daysInMonth(year, month)

Days in a Gregorian month, leap Februaries included.

### .resolveHourCycle(locale, override = null)

The resolved hour cycle for a locale, with two known Intl bug
detections built in: Chrome resolves `hour12: false` to the buggy h24
per the ECMA-402 spec bug, and WebKit misreports
resolvedOptions().hourCycle in some locales - so the cycle is INFERRED
by formatting hour 0 and hour 23 and reading what comes out.

## @poetry/controllers/helpers/mask

The mask engine, adapted from an MIT-licensed source (source and
license in THIRD_PARTY_NOTICES.md).
Pure functions over a parsed slot list, zero DOM. A mask is a sequence of
slots: token slots
validate ONE character against a pattern, literal slots are fixed chrome
("/", "-", " ") the engine inserts and the user never types. Every value
decision lives here so it stays exhaustively unit-testable; the caret
math and events live in mask_controller.js.

### .parseMask(mask, tokens = {})

Parses a mask into a slot list. String grammar: token chars from the
(custom-over-default merged) token map, "\" escapes the next char to a
literal, "?" is consumed and makes every LATER slot optional - the flag
is STICKY, it never resets, so "(999) 999-9999? x9999" is complete
without the extension. Array grammar: RegExp item = token slot, string
item = literal.

### .applyMaskToRaw(raw, slots, transform)

Raw chars -> masked string. Literals append EAGERLY (raw "12" under
"99/99" is "12/" - the separator paints the moment it is reachable); a
token slot consumes the next raw char when it matches, else silently
DROPS it and retries the SAME slot with the following char; transform
(poetry's upcase knob) runs before validation.

### .processInput(text, slots)

Re-parses arbitrary display text (autofill, a server-rendered value,
IME output): literals self-match or are inserted, token slots scan
forward discarding non-matching chars, and the walk stops at the first
slot the remaining text cannot fill.

### .extractRaw(masked, slots)

Chars at token positions - the value the form actually means.

### .buildDisplayValue(value, slots, slotChar = "_", showSlots = true)

Pads the masked value with the mask skeleton. slotChar "_" by default;
a multi-char slotChar indexes per position ("dd/mm/yyyy" under a date
mask) falling back to "_" past its end; null/"" disables padding (the
display stops at the first empty token slot).

### .checkComplete(masked, slots)

Complete = every non-optional token position is filled and
pattern-valid.

### .generatePattern(slots, kind = "full")

Regex source for the HTML pattern attribute: "full" wraps each token in
a capture group, "full-inexact" doesn't; optional tokens get a trailing
"?"; literals are regex-escaped.

### .findNextEditablePosition(pos, slots, filledLength)

Skips a literal run rightward from `pos`, bounded by the filled region
- the caret never lands inside chrome or out in the skeleton.

### .nextTokenPosition(slots, from = 0)

First token position at or after `from`.

### .prevTokenPosition(slots, from)

Last token position at or before `from`.

## @poetry/controllers/helpers/registration_guard

The registration guard: Stimulus never errors on a data-controller
identifier nothing registered - the element simply stays inert - and one
failed import in the host's controllers graph silently takes every poetry
controller down with it. After the page is ready (and again on every
Turbo navigation) this compares the poetry-prefixed identifiers on the
page with the application's registry and warns ONCE per identifier. Host
controllers are never inspected (they may lazy-load); poetry's cannot.

### .unregisteredPoetryControllers(application, root = document)

The poetry identifiers on the page that `application` has not
registered.

### .checkPoetryRegistration(application, root = document)

Warns once per newly-seen unregistered poetry identifier.

### .guardPoetryRegistration(application)

Runs the check once the DOM is parsed (registration usually happens
while the document is still loading) and after every Turbo navigation.
Idempotent: the first registrar to call it wires the listeners.

### .resetPoetryRegistrationGuard()

Test seam: forget what has been warned about and re-arm the scheduler.

## @poetry/controllers/helpers/scroll_lock

Body scroll-lock with scrollbar-width compensation: bare
`overflow: hidden` shifts the whole layout by the scrollbar width the
moment an overlay opens on a scrollable page.
The gap is measured BEFORE locking and paid back as body padding-right.
Refcounted so stacked overlays (a dialog opened from a sheet) lock once
and restore only when the LAST one closes - per-instance saved values
break on out-of-order closes.

Why not scrollbar-gutter: stable (this helper's original primary)?
Measured live with classic
scrollbars: Chrome drops the viewport's rail AND its reserved gutter
the moment the viewport's used overflow computes to hidden - whether
the pair sits on the root, propagates from body, or the gutter was set
permanently - so the page shifted by the scrollbar width anyway (the
exact wiggle the strategy existed to stop). The body-padding payback
is the only compensation the viewport honors; its known cost is that
position:fixed elements aren't compensated - accepted deliberately,
not an oversight.

### .lockScroll()

Locks body scrolling, paying the measured scrollbar gap back as body
padding-right so the layout never shifts. Refcounted: stacked overlays
lock once; only the first call writes styles.

### .unlockScroll()

Releases one scroll lock; the LAST release restores the body's saved
overflow and padding-right.

### .resetScrollLock()

Test seam: vitest suites run many overlays in one document.

## @poetry/controllers/helpers/scroller_geometry

Scroll geometry for the message-scroller, adapted from an MIT-licensed
source (source and license in THIRD_PARTY_NOTICES.md): pure functions
of {viewport, content, spacer, rects} - no Stimulus, no state. This is
the jsdom-testable half; the controller owns the policy that decides
when to call these. The constants encode deliberate fixes - treat them
as pinned values, not tunables.

### .getMessageScrollerScrollable({ content, scrollEdgeThreshold, spacer, viewport })

Whether the scroller has scroll room past each edge, beyond the
configured threshold.

### .getMessageScrollerVisibilityState({ content, scrollMargin, scrollPreviousItemPeek, spacer, viewport, visibleMessageIds })

The visible message ids (top-to-bottom) and the current anchor - the
last anchor row to have reached the reading line (the comments inside
hold the line rules).

### .getMessageScrollerItems(content, spacer)

The collection is DOM order: rows are content's element children minus
the tail spacer - membership is a filter over live children, never a
registration step.

### .getNewScrollAnchor(items, previousItemCount)

The first anchor row appended after `previousItemCount` items existed.

### .getUnanchoredScrollAnchor(items, handledAnchors)

The first anchor row not yet in `handledAnchors`.

### .hasMultipleNewScrollAnchors(items, previousItemCount)

Whether more than one anchor row arrived after `previousItemCount`
items existed (a multi-turn batch).

### .getLastScrollAnchor(items)

The last anchor row in the collection.

### .getFirstVisibleMessageItem({ content, spacer, viewport })

The topmost message row intersecting the viewport.

### .getElementScrollTop({ align, element, scrollMargin, spacer, viewport })

Target scrollTop that aligns `element` to the viewport inset (content
block padding respected).

### .getElementTop(element, viewport)

`element`'s top in the viewport's content space (independent of the
current scroll position).

### .getElementViewportTop(element, viewport)

`element`'s top relative to the viewport's current visual top.

### .getTailSpacerHeight({ content, scrollTop, spacer, viewport })

Scroll room the tail spacer must fake below the last row so the
requested scrollTop is reachable. Caller clamps/ceils (see the
controller).

### .getContentBottom({ content, spacer, viewport })

Lowest row bottom in content space plus block padding, EXCLUDING the
tail spacer - the spacer must never make the jump button appear.

### .getMaxScrollTop(viewport)

The viewport's maximum scrollTop (never negative).

### .getContentBlockPadding(spacer)

Block padding of the spacer's parent (the content element); zeros
without a spacer.

### .getFlexGap(element)

The element's effective row gap (0 for "normal" or no element).

### .areScrollStatesEqual(current, next)

Value equality for two scrollable snapshots.

### .areVisibilityStatesEqual(current, next)

Value equality for two visibility snapshots (anchor + ordered ids).

## @poetry/controllers/helpers/tabbable

Tabbable-candidate walk: the shared filter behind focus-scope and
the Dialog trap. Candidates in DOM order, minus disabled / hidden /
tabindex=-1 / inert-subtree elements.

### .tabbableWithin(container)

The tabbable elements under `container`, in DOM order: candidate
tags/tabindexes minus disabled / hidden / tabindex=-1 / inert-subtree /
type=hidden elements, with each radio group collapsed to its single
real tab stop (see below).

## @poetry/controllers/helpers/turbo_cache

Turbo snapshots the page BEFORE caching it (turbo:before-cache), and an
overlay still open at that moment serializes INTO the snapshot: the
restoration visit then renders a de-modalized zombie dialog (the open
attribute survives HTML serialization, top-layer/modal state does not)
over a body whose inline scroll-lock / pointer-events-scrim styles came
back frozen - a page that can never scroll, or never be clicked, again
(both reproduced live against the docs app). Overlay controllers
subscribe their synchronous teardown here; no-op when Turbo is absent.

### .onBeforeCache(callback)

Subscribes `callback` to Turbo's before-cache moment - the last
synchronous chance to tear an overlay down before the snapshot is
taken. A page without Turbo simply never fires it.

## @poetry/controllers/helpers/typeahead

The APG typeahead buffer, shared: printable keys accumulate into a
search buffer that resets after a timeout (1s default), matching wraps
from the current item, and a repeated same-letter buffer cycles matches.
Extracted VERBATIM from menu_controller.js so the menu family and the
Select listbox run the identical algorithm - the buffer/timer state
lives in the instance this factory returns, one per consuming
controller.

### .typeaheadLabel(item)

The label an item types against: data-text-value overrides textContent
(icon-rich content declares its typed label explicitly).

### .createTypeahead()

One typeahead instance for a consuming controller - the buffer/timer
state lives in the returned object (the module header holds the
algorithm).

## IncompleteDate

A date/time value whose segments may each be null (the module header
holds the storage rules). One instance backs one date-field.

Exported from `@poetry/controllers/helpers/incomplete_date`.

### #constructor(hourCycle = "h23")

### #twelveHour()

### #limits(type)

Segment limits in DISPLAY terms. Day deliberately allows the calendar
maximum (31) regardless of the month segment - editing order must not
trap the user; constrain() clamps at commit.

### #cycle(type, amount, placeholderValue, { round = false } = {})

Arrow/page steps a segment. The first press on an EMPTY segment lands
on the placeholder value, the second one moves it (the cycle
contract); values wrap within the segment's limits (the spinbutton
contract).

### #set(type, value)

Sets a segment, clamped to its limits.

### #clear(type)

Empties a segment.

### #isComplete(types)

Whether every listed segment holds a value.

### #isValidDate()

A complete date can still be invalid (February 31st): valid means the
day exists in the month.

### #constrain(types)

Commit-time clamp (blur): the raw day is pulled into the real month.

### #hourInH23()

The stored hour converted to h23. Under h11/h12 an unset dayPeriod
reads as AM.

### #setFromH23(h23)

Sets hour (and dayPeriod under h11/h12) from an h23 hour.

### #toISODate()

### #toISOTime(withSeconds = false)

### #setFromISODate(iso)

Fills year/month/day from "YYYY-MM-DD".

### #setFromISOTime(iso)

Fills hour/minute(/second) from "HH:MM(:SS)" (h23 on the wire).

### #setFromISODateTime(iso)

Fills every field from "YYYY-MM-DDTHH:MM(:SS)" - the datetime-local
wire format (no zone, local wall time).

### #toISODateTime(withSeconds = false)

## poetry--core--accordion

The accordion open-set machine: single (optionally collapsible)
or multiple. Composes with poetry--core--roving-focus (manageTabindex:
false - APG keeps every trigger tabbable) attached separately on the
same root. Panels ride the presence helper; the measured
--accordion-panel-height var feeds the vendored accordion-down/up
keyframes.

**Values**: `type` (String, default: "single"); `collapsible` (Boolean, default: false)

**Events**: `poetry:accordion:change`

### #toggle(event)

Each trigger's click action: toggles its item - single mode closes
the others first; single non-collapsible keeps the open item open.
The change event reports the open values.

### #connect()

Reflects the locked-open trigger (aria-disabled) and adopts
data-panel-open onto server-open triggers.

### #disconnect()

Flushes any in-flight transition window so its timer/listener doesn't
outlive the controller (Turbo teardown, or a test between cases).

## poetry--core--action-bar

The floating bulk-actions bar (the ActionBar contract): shows
while its table's selection is non-empty, and holds the contract rules -
focus NEVER moves in on show; if focus was inside when the bar hides,
it returns to where it was before entering (the FocusScope restoreFocus
equivalent, scoped small); "Actions available." is announced ONCE per
appearance; the visible count RETAINS its last non-zero value while the
bar animates out (never a "None selected" flash); Escape anywhere
inside clears the selection (the table's engine listens for

**Targets**: `count`

**Values**: `label` (String, default: "%{count} selected")

**Events**: `poetry:data-table:clear-selection`

### #connect()

Subscribes to the table's selection-change on the shared wrapper and
starts the focus bookkeeping the hide-restore needs.

### #disconnect()

Unwires both listeners.

### #keydown(event)

The keydown action: Escape anywhere inside clears the selection.

### #clear()

The clear affordance (and Escape): asks the table's engine - which
owns the state - to clear the selection.

## poetry--core--autocomplete

The Autocomplete: a REAL text
input that IS the form value, suggesting from a server-rendered list
that filters as you type. This is the input-is-the-value semantic -
the sibling of Combobox, whose value is a SELECTED ITEM behind a
native <select>. Selecting a suggestion writes the input and closes;
the text always submits as ordinary params.

The contract's fixed behaviors: locale-aware filtering
(toLocaleLowerCase), list scroll reset on every filter pass, and
change-reason details on the commit event (item-press | enter-key).
Positioning rides the shared popper controller (input = anchor).

**Targets**: `content`, `empty`, `input`, `list`

**Values**: `open` (Boolean, default: false); `openOnFocus` (Boolean, default: true)

**Events**: `poetry:autocomplete:closed`, `poetry:autocomplete:commit`, `poetry:autocomplete:open`

### #disconnect()

Closes an open popup so its state never outlives the controller.

### #input()

The input action: re-filters the list and opens.

### #focus()

The focus action: opens pre-typing when openOnFocus allows.

### #blurred(event)

The focusout action: closes, unless focus moved into the popup (an
item click commits first - the body comment).

### #keydown(event)

The input's keydown action: arrows open / move the highlight, Enter
commits it, Escape closes (consumed - the next press reaches the
layer above), Tab closes and passes through. IME keydowns are
ignored.

### #itemPress(event)

Each item's pointerdown action (not click): commits BEFORE the
input's blur closes the popup.

### #itemEnter(event)

Each item's pointerenter action: moves the highlight under the
pointer.

## poetry--core--calendar

The Calendar engine, decided OWN-THE-ENGINE: no date-picker library
(a heavy JS dep). The month grid is SERVER-RENDERED (Ruby Date math) so a
no-JS page shows a valid calendar; this controller adds month navigation
(regenerating the 42-cell grid from plain Date math - reusing the day
button DOM, no innerHTML churn), single-date selection (writing the
hidden input + the data-selected/aria vocabulary), and roving arrow-key
focus over the days. Selection is a real form value; the DatePicker
composes this inside a Popover.

Range mode: mode="range" swaps the click path for a
transcription of an MIT-licensed range-selection algorithm (source and
license in THIRD_PARTY_NOTICES.md) and the reflection
for the range vocabulary (data-range-start/middle/end - dictionary
classes that shipped inert until range mode). The form value is TWO
hidden inputs (name[start]/name[end] - the wire shape is poetry's).
The transcribed semantics hold exactly: an incomplete
start-only pick renders as a plain selected single day; the range
vocabulary appears only once the range completes.

Dropdown caption (caption_layout: :dropdown), the invisible-select overlay
pattern: each [data-calendar-unit=month|year] wrapper holds a visible
text label (calendar-dropdown-value) with the real select stretched
invisibly over it - #jump reads the selects, #render reflects
navigation back into both select values AND label text (no caption
target in that mode). Week numbers: one role=rowheader per week; each
row's Thursday decides the ISO number (matches Ruby Date#cweek under
any week_start).

Still deferred: multiple months.

**Targets**: `caption`, `day`, `endInput`, `grid`, `input`, `startInput`

**Values**: `month` (String); `selected` (String); `mode` (String, default: "single"); `rangeStart` (String); `rangeEnd` (String); `weekStart` (Number, default: 0); `min` (String); `max` (String); `monthNames` (Array, default: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"])

**Events**: `poetry:calendar:change`

### #connect()

Syncs the selected/today vocabulary over the server-rendered grid.

### #previousMonth()

The previous-month button's click action.

### #nextMonth()

The next-month button's click action.

### #jump()

The dropdown caption's change action (the month/year select
wrappers): navigates to the selects' month.

### #select(event)

Each day button's click action: single mode writes the selection;
range mode runs the addToRange transcription. The change event
carries value (single) or start/end (range) as ISO strings.

### #keydown(event)

The grid's keydown action: arrows move focus by day/week, PageUp/Down
by month, Home/End to the week edges.

### #selectedValueChanged()

Stimulus value callback. The DOM is the store (the anchor-point
precedent): external writes to selected/month - the DatePicker input
variant syncs typed dates this way - re-render on change. The booted
gate skips the init echo (the server's own paint - Stimulus fires
initial value callbacks BEFORE connect, and a connect-era render
would restamp data-today with the client clock over the server's
pinned one). Internal writes re-render once more; the in-place cell
diff makes that free.

### #monthValueChanged()

Stimulus value callback: re-renders on external month writes (see above).

## poetry--core--carousel

The Carousel engine, decided NATIVE: no carousel library - the platform's
scroll-snap owns the physics (touch, momentum, snapping, overscroll),
and this controller adds only what CSS can't: prev/next paging, button
state, and arrow keys. Navigation scrolls the VIEWPORT by a bounding-
rect delta (RTL- and transform-safe: never scrollLeft sign
conventions) - not scrollIntoView, whose alignment bubbles to
scrollable ancestors and whose "nearest" no-ops when several slides
fit the viewport at once (the vertical stack). Deferred with the
library-grade machinery: loop (it clones slides), autoplay, and a plugin API.

**Targets**: `next`, `previous`, `viewport`

**Values**: `orientation` (String, default: "horizontal")

**Events**: `poetry:carousel:select`

### #connect()

Syncs button state to the initial scroll position.

### #disconnect()

Cancels any pending sync frame.

### #scrolled()

The viewport's scroll action - button state follows the SCROLL
(finger, wheel, keyboard PageDown), not just our own paging.
rAF-coalesced: scroll fires in bursts.

### #previous()

The previous button's click action: pages one slide back.

### #next()

The next button's click action: pages one slide forward.

### #keydown(event)

The region root's keydown action:
arrows page the carousel, orientation-aware.

### #scrollTo(index)

The programmatic surface: scrolls to a slide by index (clamped).

## poetry--core--checkbox-group

The select-all recipe (a parent checkbox running the APG
mixed-state pattern) over the checked family. Wrap the parent and its
rows, route the bubbling per-checkbox observe event at #changed, and
mark the parent (target: all) plus each row (target: item):

  <div data-controller="poetry--core--checkbox-group"
       data-action="poetry:checkbox:change->poetry--core--checkbox-group#changed">

A parent toggle fans its new state out to every enabled row; a row
toggle re-derives the parent (all -> checked, none -> unchecked, some ->
indeterminate, re-entered via the checked controller's set() - the only
path back to mixed). Every state write goes THROUGH each box's checked
controller (input first, real change event, aria-checked + the checked
triple together) - this controller never touches attributes itself.
Disabled rows are skipped by fan-out and excluded from the derivation
(the DataTable disabledBehavior 'selection' doctrine;
table_selection_controller is this same recipe for NATIVE checkboxes).

**Targets**: `all`, `item`

**Events**: `poetry:checkbox-group:change`

### #changed(event)

The wrapper's data-action route for every bubbling checkbox change: a
parent toggle fans out, a row toggle re-derives the parent, and the
group's own change event reports the enabled count/total. The
applying flag mutes the echoes our own set() calls dispatch (a
fan-out must not re-derive per row; a parent reflection must not fan
out).

## poetry--core--checked

The toggle family's checked-state owner (Checkbox introduces it; Switch
reuses it VERBATIM - zero fork, CI-asserted). The architecture is the
STORE INVERSION: the hidden native <input type=checkbox> is the form
participant AND the store - the visual button[role=checkbox|switch] only
REFLECTS it (aria-checked incl. "mixed" + the data-checked/data-unchecked/
data-indeterminate attributes on the control and every part that carries
them: the checkbox indicator, the switch thumb). Every transition writes the input FIRST, dispatches a REAL
bubbling change event (no synthetic prototype-setter dance - the
input LEADS and the visual follows), then reflects attributes.

The three states: checked / unchecked / indeterminate. Indeterminate is
server/programmatic only (aria-checked=mixed, input.indeterminate - a
JS-only property re-derived from the checked attributes on connect); the first user
toggle resolves it to CHECKED - contractual, do not loosen. A Switch
never renders it (the Ruby component raises), so that branch is
simply dormant there.

Enter is suppressed on role=checkbox ONLY (WAI-ARIA: checkboxes activate
on Space alone); role=switch has no keydown
handler, so Enter toggles via the native button click (a deliberate
asymmetry, keyed off the role - no controller fork).

No inputId value -> pure visual mode: state lives on the button's
checked attributes alone (controlled-UI cases like DataTable row selection).
The component-flavored prefixes the dynamic dispatch below emits under
(data-component on the host: checkbox / switch, with the bare fallback) -
the events declaration, the portal bridge, and the manifest enumerate
these REAL names; the identifier-default name never fires here.

**Values**: `inputId` (String, default: "")

**Events**: `poetry:checkbox:change`, `poetry:checked:change`, `poetry:switch:change`

### #connect()

Reconcile-on-connect: derives input.checked/indeterminate from the
checked attributes (the server truth), arms the form-reset restore,
and installs the role-keyed Enter suppression.

### #disconnect()

Unwires the keydown and form-reset listeners.

### #toggle()

Control activation (click / Space / label-for): indeterminate
resolves to checked (contractual), else flip.

### #check()

Programmatically checks (see set).

### #uncheck()

Programmatically unchecks (see set).

### #set(state)

The programmatic controllable-state surface: set(true | false |
"indeterminate") reaches all three states (the select-all recipe -
checkbox_group_controller - re-enters indeterminate this way).

## poetry--core--clipboard-text

ClipboardText, adapted from an MIT-licensed source (source and license
in THIRD_PARTY_NOTICES.md): a read-only value with one
copy affordance. navigator.clipboard is the primary path; the
execCommand fallback SAVES AND RESTORES the user's own selection and
focus (copying a field must never eat a selection made elsewhere on the
page). Success stamps data-copied on
the root for a beat (CSS swaps the copy/check glyphs off it), announces
through the live-region singleton, and dispatches the copied event.

**Targets**: `input`, `source`

**Values**: `text` (String); `message` (String)

**Events**: `poetry:clipboard-text:copied`

### #disconnect()

Clears the copied-beat timer.

### #copy()

The copy action: writes the text (navigator.clipboard first, then the
selection-preserving execCommand fallback), stamps data-copied for a
beat, announces, and dispatches the copied event with the text in the
detail. A failed write changes nothing.

## poetry--core--combobox

The Combobox ORCHESTRATOR: Select's shell
x Command's engine, composed VIA THE EVENT CONTRACT ONLY - this thin
controller owns open/close + the commit pipeline + autofill adoption and
listens for the embedded engine's poetry:command:select; it contains NO
filter/highlight/scoring code (Command owns those) and Command gained no
combobox code (engine purity, fenced both directions by the conformance
greps). Zero code is shared with SelectController - the PATTERNS are
(the layer mechanism, the native-first 5-step pipeline, the adoption
path), re-instantiated here because a facade extraction was explicitly
declined (the Select contract's open question, answered by this build).

THE THREE DELIBERATE DELTAS vs Select, each pinned by tests so neither
sibling's rules leak:
- open focuses the COMMAND INPUT for every reason (a combobox session is
  a TYPING session - APG editable combobox); the selected option gets the
  HIGHLIGHT (activedescendant, via the Command controller) + scroll, not
  DOM focus;
- Tab while open CLOSES WITHOUT COMMIT and lets focus proceed (Popover
  semantics, modal:false default; modal:true restores the trap) - Select
  is Tab-inert;
- a printable key on the CLOSED trigger OPENS and SEEDS the filter
  (typing filters, never blind-commits) - Select's closed-trigger
  typeahead-commit does NOT port.

TWO MEANINGS, TWO ATTRIBUTES, ONE LIST: data-highlighted +
aria-activedescendant = position (Command's, never aria-selected);
aria-selected + data-selected + the indicator = the COMMITTED value
(Select's twin-write, written only here, in the pipeline).

THE SYNC INVARIANT (Select's, inherited): native_select.value, the value
Value, the twin-write, and the display never diverge - every write path
funnels through #apply, NATIVE FIRST (serialization truth is never
behind the facade), with real bubbling change/input on the native so
Turbo auto-submit and form listeners work unmodified.

MULTIPLE (the input-inside layout): the value is a
LIST (the value Value carries a JSON array over the same String seam),
the native is a <select multiple> posting name[], and the trigger is
replaced by the chips FIELD - one chip per committed value IN VALUE
ORDER with the filter input inline after them (data-slot=combobox-chip-input,
which the engine resolves too; the engine itself rides the ROOT).
Selection TOGGLES and the popup STAYS OPEN; chips take REAL DOM focus
(:focus-visible styles it - never data-highlighted) and focusing a chip
closes the popup; Backspace on the empty input removes the last chip;
Escape on the CLOSED popup clears the query and wipes the selection.
The input NEVER mirrors selection text; single-mode paths are
behavior-identical.

**Values**: `open` (Boolean, default: false); `value` (String, default: ""); `modal` (Boolean, default: false); `multiple` (Boolean, default: false)

**Events**: `poetry:combobox:change`, `poetry:combobox:closed`, `poetry:combobox:open`, `poetry:combobox:select`

### #connect()

Wires the content (and chips-field) listeners, captures the
placeholder, reconciles silently from the native select / the Value
(multiple parses the JSON-array seam), and catches a server-open
popup up (layers + the late portal).

### #disconnect()

Restores portaled content (drop-never-strand) and unwires everything.

### #openValueChanged(value)

Stimulus value callback - controllable open state.

### #valueValueChanged(value)

Stimulus value callback - controllable value (multiple parses the
JSON-array seam; both modes no-op on the applied echo).

### #toggle()

The trigger's click action: open <-> close.

### #triggerKeydown(event)

The trigger's keydown action: Enter / Space / ArrowDown / ArrowUp
open (reason: list-navigation). A PRINTABLE key opens AND seeds the
filter input with the char (reason: keyboard + the char as
data-open-seed, a poetry extension - the char is never lost, the
filter pass runs immediately). There is deliberately NO
closed-trigger typeahead-commit here.

### #chipsPointerdown(event)

The chips frame's pointerdown action (multiple): a press anywhere in
the FRAME focuses the input and opens the popup (the whole frame IS
the field) - except a chip-remove press, which is a removal,
never a chips-area press.

### #inputKeydown(event)

The inline input's OWN keyboard map (multiple; the engine's map rides
the same keydown): Backspace on an empty input removes the LAST chip
(focus stays here), ArrowLeft at caret 0 walks into the chips,
ArrowDown/Up reopen the popup, Enter with no highlight closes, and
Escape on the CLOSED popup clears the query AND wipes the selection
to [] (contractual; readOnly blocks every mutation).

### #chipKeydown(event)

A focused chip's keyboard map: Left/Right walk the chips
(off either end -> back to the input), Backspace/Delete remove (next
highlight: same index, step back at the tail, the input once
emptied), Enter/Space are no-ops returning to the input, ArrowDown/Up
reopen the popup, and a printable char resumes the typing session.

### #removeChip(event)

The chip-remove button's click action: removes the chip's value;
focus returns to the input WITHOUT opening (not a chips-area press).

### #clear()

The show_clear X's click action - the trigger-side deselection
surface: commits the blank value through the
pipeline, then hands focus to the trigger (the X hides itself once
the value empties, and a focused hidden button would drop focus to
body). Single mode only (the component raises on multiple; the guard
here is belt and braces).

### #open(reason = "trigger-press", { seed = "" } = {})

Programmatically opens.

### #close(reason = "none")

Programmatically closes.

### #setValue(value)

The programmatic controllable-state surface (multiple accepts an
array). Returns the resulting state - what an agent tool reports
back - so an unknown value (not committed) reads as changed: false.

### #nativeChanged()

The hidden native's change action: browser autofill (or any
programmatic write) adopts - multiple compares MEMBERSHIP, not order
(the body comment holds the reorder rule).

## poetry--core--command

The Command palette engine - the suite's
first ACTIVEDESCENDANT component (the APG editable-combobox pattern, the
deliberate delta vs the menus/Select family's roving focus): real DOM
focus stays pinned to the input for the whole session; the highlighted
option carries data-highlighted + its server-stable id in the input's
aria-activedescendant (the twin-write, activedescendant-flavored).
Options never get tabindex; roving-focus is NOT attached; aria-selected
is NEVER written here (reserved for committed values - Combobox's
twin-write; a bare palette has no committed value).

THE FILTER is the deterministic helpers/filter_rank spec (prefix 4 >
word-boundary 3 > substring 2 > keyword 1 > hidden 0, diacritic-folded)
and it is HIDE-ONLY: score-0 items get hidden + data-hidden, matches get
both removed, and children are NEVER reordered - DOM order is the
ranking authority within a band; the score's only job is seating the
auto-highlight. filter:false is the server-driven mode: steps 1-2
(hiding) are skipped entirely and only
highlight/activation/announcement run over whatever the server rendered
- the Turbo-frame async seam Combobox's recipe plugs into.

ACTIVATION IS AN EVENT, NOT AN ACTION: Enter/click dispatches cancelable
poetry:command:select and this controller does nothing further - no
navigation, no close, no value write. The listener (host data-action,
Combobox's commit pipeline) owns the consequences - the engine-purity
Combobox's composition depends on (no popper/commit/native-select code
lives here, fenced by the conformance greps).

The DOM is the store, filtering included: query in the input, visibility
as hidden + data-hidden, highlight as data-highlighted +
aria-activedescendant, group visibility derived - a Turbo Stream can
append items mid-session and the next keystroke ranks them.
Each part matches both families' vocabularies: Command's own names and the
combobox's (two names for the same parts; the engine rides both).

**Values**: `filter` (Boolean, default: true); `loop` (Boolean, default: false); `debounce` (Number, default: 0)

**Events**: `poetry:command:filter`, `poetry:command:highlight`, `poetry:command:select`

### #connect()

Reconciles (a non-empty input re-derives visibility + highlight
silently; an empty one just seats the highlight) and wires the portal
delegation on the list node (the body comments hold the rules).

### #disconnect()

Clears the timers and unwires the list delegation.

### #filterInput()

The input's input action: runs the filter pass (debounced when
configured).

### #keydown(event)

The input's keydown action: ArrowUp/Down move the highlight
(Meta/Ctrl jumps first/last), Enter activates it.
Home/End/ArrowLeft/ArrowRight fall through to the input (CARET
movement - APG-correct for an editable field; a Home/End list-jump
is deliberately not offered). Space TYPES a space, never
activates (a text field - the family delta vs Select/menus). Esc/Tab
are NOT handled - the hosting layer owns them (Dialog dismiss /
Combobox close / natural tab-out).

### #activate(event)

Each in-scope item's click action: dispatches the cancelable select
event - and nothing further (the engine-purity contract).

### #pointerHighlight(event)

Each in-scope item's pointermove action: hovering highlights (no
scroll chasing); pointerleave does NOT clear - the keyboard position
survives mouse exit (contractual).

### #highlightItem(item, { scroll = true } = {})

The composition surface (Combobox calls it): moves the highlight to a
given option (visible + enabled required).

### #reset()

Clears the query and silently re-derives visibility + highlight (the
clean-reopen reset an overlay host runs on close).

## poetry--core--context-menu

The ContextMenu DELTA layer: trigger
acquisition + pointer-point capture ONLY. Everything menu-shaped (open
state machine, items, typeahead, submenus, dismissal, the layer stack)
stays with poetry--core--menu on the same root; positioning stays with
poetry--core--popper, driven here through its VIRTUAL-ANCHOR attribute
(data-poetry--core--popper-anchor-point-value="x,y" - the DOM is the
store, so the write works whether or not popper has connected yet).

Input paths (all contractual):
- contextmenu (mouse right-click, or Shift+F10 / the ContextMenu key on a
  focused surface): preventDefault, capture the point, open. A
  keyboard-synthesized event carries no usable point (0,0) - the anchor
  attribute is CLEARED so popper falls back to the trigger surface rect
  (deliberately better than a 0,0 fallback).
- long-press (touch/pen ONLY - mouse never long-presses): pointerdown
  starts a longPressDelay timer (700ms default, exposed as a
  Value); ANY pointermove/pointerup/pointercancel cancels (no slop radius
  - the platform's own touch slop absorbs jitter); a second pointerdown
  restarts (multi-touch guard); the contextmenu handler itself clears the
  timer (Android synthesizes contextmenu from long-press - clearing there
  prevents double-open). data-pressing on the surface is the styleable
  long-press feedback window (poetry addition).
- disabled STANDS THE HANDLERS DOWN (no preventDefault): the
  browser-native context menu returns - never a dead right-click.

**Values**: `longPressDelay` (Number, default: 700); `disabled` (Boolean, default: false)

**Events**: `poetry:context-menu:open`

### #disconnect()

Clears any armed long-press and stops the native-menu suppression.

### #disabledValueChanged(disabled)

Stimulus value callback: disabling mid-press clears the armed
long-press.

### #open(event)

The trigger surface's contextmenu action. Re-invoking on an
already-open menu re-captures the point and repositions (popper's
anchorPoint value is reactive; autoUpdate re-reads the stored point).

### #pressStart(event)

The surface's pointerdown action - touch/pen long-press. While open,
a press on the surface closes first (press-again-to-dismiss), then
the timer arms for a fresh open.

### #pressCancel()

pointermove / pointerup / pointercancel - any of them cancels the
armed press.

## poetry--core--date-field

The segmented date/time editor. The component renders a real native
<input type=date|time> that IS the form value (no JS = native pickers;
its ISO value format is exactly the wire contract), plus an empty group.
This controller builds per-segment role=spinbutton spans from
Intl.DateTimeFormat.formatToParts - so the LOCALE decides segment order
and literals - and syncs segments -> native input eagerly whenever the
value is complete and valid, constraining on blur (February 31st is
representable mid-edit; commit clamps).

Hour is stored in the locale's hour cycle with a separate dayPeriod
segment; the cycle itself is INFERRED by formatting (two Intl bugs make
resolvedOptions() untrustworthy - helpers/incomplete_date.js).

Latin-digit typing only (the NumberField documented divergence); the
DISPLAY uses Intl.NumberFormat, so locales that render non-latin digits
still see their own numerals.

**Targets**: `group`, `input`

**Values**: `locale` (String, default: ""); `hourCycle` (String, default: ""); `seconds` (Boolean, default: false); `placeholder` (String, default: ""); `labels` (Object, default: {}); `placeholders` (Object, default: {})

**Events**: `poetry:date-field:change`

### #connect()

Builds the locale-driven segment row from formatToParts, marks the
field enhanced, retires the native input from the tab order (it stays
the form value and the constraint-validation carrier), and routes
native invalid to the first empty segment.

### #disconnect()

Restores the native input's tab order / AT visibility and unwires the
document listeners.

### #focusGap(event)

The group's click action: clicking blank space lands on the earliest
unfilled segment (the focusLast heuristic, simplified - segments
themselves stop propagation by matching first).

### #settle(event)

The group's focusout action: leaving the whole group constrains
(Feb 31 -> Feb 29) and commits.

## poetry--core--date-picker

The DatePicker glue: the thin coordinator between a Popover and
the Calendar it wraps. The Calendar owns selection + the form value (its
name: hidden input); this controller only reacts to the calendar's change
event to (a) write the human-formatted date into the trigger label and
(b) close the popover so the pick feels complete. Everything hard is
already done by poetry--core--calendar and poetry--core--popover.

**Targets**: `input`, `label`

**Values**: `placeholder` (String, default: "Pick a date"); `mode` (String, default: "single"); `locale` (String, default: "en-US")

### #picked(event)

The calendar-change action on the wrapper: writes the formatted date
into the trigger label (and input, when present) and closes the
popover so the pick feels complete. Range mode: the label joins the
pair, a start-only pick shows one date, and the popover closes only
once the range COMPLETES - never mid-range.

### #inputChanged()

The input's change action: a parseable date re-selects and re-months
the calendar through its reactive values (the DOM is the store), and
the calendar's own render refreshes the hidden ISO form value.
Unparseable text changes nothing.

### #inputKeydown(event)

The input's keydown action: ArrowDown opens the calendar.

## poetry--core--deferred

Error isolation for deferred turbo-frames. Turbo owns the
loading physics (loading="lazy" fetches on visibility, "eager" after
paint) but leaves failure INVISIBLE: a missing frame logs "Content
missing" or - Turbo 8 - promotes the error response to a full-page
visit, and a network error leaves the placeholder spinning forever.
This controller makes failure a state: data-error reflected on the
frame (a bespoke boolean, deliberately not the shared open/closed
vocabulary), the placeholder hidden, and the slotted <template> error
content stamped so the region shows a real, retryable message.

THE BOOT CONTRACT: the frame renders with NO src - the URL rides
srcValue and connect() arms it. A visible lazy frame with src in
markup can complete its fetch before the controllers module graph
finishes loading (caught live: a local 404 beat Stimulus and Turbo 8
navigated the whole page to the error response); with src armed at
connect, no fetch - and so no failure - can predate the instance.

retry() prefers Turbo's FrameElement#reload(); environments without
the Turbo runtime (jsdom, dommy) fall back to re-setting src, the same
signal Turbo reacts to.

**Targets**: `error`, `placeholder`

**Values**: `src` (String)

### #connect()

Wires the three failure listeners, subscribes the before-cache reset,
and arms src LAST (the boot contract above: no fetch can predate the
instance).

### #disconnect()

Unwires the listeners and the before-cache subscription.

### #retry()

The retry action: back to the placeholder posture, then reload -
Turbo's FrameElement#reload when the runtime is present, else the
re-set-src fallback (the same signal Turbo reacts to).

## poetry--core--drawer

The Drawer: the dialog machinery + the swipe-to-dismiss gesture.
Everything hard about the OVERLAY is inherited (native <dialog> platform
trap, backdrop-click discrimination, scroll lock, hotkey); this subclass
adds the two things a drawer is:

  * ANIMATED presence - the first consumer of the presence helpers:
    enter rides the data-starting-style two-frame trick (a transition
    from --closed-transform), exit HOLDS the dialog through the
    data-ending-style transition before the native close() (the
    presence-hold close the sheet styling depends on).
  * The SWIPE - pointer-captured drag along the dismiss direction,
    writing the swipe CSS-var contract onto the <dialog> (::backdrop
    inherits from its originating element, so the overlay fade rides the
    same vars): --drawer-swipe-movement-x/y (px toward dismissal),
    --drawer-swipe-progress (0..1), data-swiping while tracking
    (duration-0 - the drawer follows the finger), and on release either
    a snap-back or a dismissal whose exit duration scales with
    --drawer-swipe-strength (mostly-swiped closes fast).

Deferred with the rest of the stack machinery (see the Drawer style):
snap points, nested-drawer stacking, and bleed.

**Targets**: `dialog`

**Values**: `dismissible` (Boolean, default: true); `hotkey` (String, default: ""); `direction` (String, default: "down"); `modal` (Boolean, default: true); `snapPoints` (Array, default: [])

### #open()

Opens per the modal value: showModal() plus the scroll lock, or the
non-modal show() (no top layer - the component pins the panel; the
body comment). The first snap offset lands BEFORE the enter
transition starts, then the animated entry runs.

### #escapeClose(event)

The non-modal Escape exit (the component wires this keydown action
only when modal is false): a non-modal dialog never fires cancel, so
Esc needs its own path while focus is inside; modal drawers ride the
native cancel.

### #close(event)

Closes through the animated exit (data-ending-style), then the
native close(), the unlock, and the swipe-var reset. The native
cancel event (Esc) routes through here so state stays in sync.

### #swipeStart(event)

The <dialog>'s pointerdown action: arms the swipe - always from the
handle; elsewhere only when the press is not on a control and no
scrollable child still scrolls toward the gesture.

### #swipeMove(event)

The captured pointermove action: past the slop, tracks the drag -
writing the swipe CSS vars (the finger-following, duration-0 window)
and the release velocity.

### #swipeEnd(event)

The pointerup action - release physics: dismiss past half the travel
or on a flick (exit duration scaled by the remaining travel), else
snap back; snap-pointed sheets settle between their points instead.

### #swipeCancel(event)

The pointercancel action: abandons the drag and snaps home.

## poetry--core--file-input

The FileInput dropzone engine: the
component renders a <label> wrapping a visually-hidden native
<input type=file>, so click-to-browse and keyboard access are the
PLATFORM's - this controller adds only what HTML cannot: drag-and-drop
onto the label, the selected-file list, and the clear affordance. The
native input stays the single source of truth (its FileList is the
form value; ActiveStorage direct upload rides it untouched).

Drag state uses an enter/leave COUNTER: dragenter/dragleave fire per
descendant crossed, so a boolean flickers over child elements - the
counter nets out to zero only when the pointer truly leaves the zone.

**Targets**: `clear`, `input`, `list`

**Values**: `multiple` (Boolean, default: false)

**Events**: `poetry:file-input:change`

### #connect()

Reflects the input's current FileList (list, clear, data-populated).

### #dragenter(event)

File drags flip the zone into its accepting state - depth-counted
(the header explains the counter).

### #dragover(event)

The required preventDefault - without it the browser navigates to
the dropped file.

### #dragleave()

Nets the depth counter down; at zero the drag styling clears.

### #drop(event)

Assigns the dropped files to the native input (single-file zones take
the FIRST - the native picker's rule) and dispatches a real change so
every listener sees a picker-identical selection.

### #changed()

The native input's change action (picker AND drop land here):
reflects the selection and reports the file names.

### #clear(event)

The clear button's click action: empties the input and dispatches a
real change.

## poetry--core--hotkey

Declarative global shortcut:
put the controller on any clickable element and declare the descriptor -

  <a href="/inbox" data-controller="poetry--core--hotkey"
     data-poetry--core--hotkey-keys-value="g+i">

On match the controller dispatches a cancelable poetry--core--hotkey:pressed
event, then clicks the host element (buttons, links, summary - anything
click-activatable). Unmodified single-key descriptors stay inert while
the user is typing in an input/textarea/select/contentEditable; combos
carrying meta/ctrl/alt fire everywhere (the ⌘K convention).

**Values**: `keys` (String, default: "")

**Events**: `poetry--core--hotkey:pressed`

### #connect()

Arms the window keydown listener when a descriptor is declared (an
empty keys value stays inert).

### #disconnect()

Disarms the listener.

## poetry--core--hover-card

The HoverCard controller (the popper-consumer trio's thinnest machine):
pointer-only enrichment behind a LINK. Two timers (open 600 / close
300, over the trigger+content
pair, re-enter cancels - the grace window, no polygon), the touch double-guard
(pointerType 'touch' no-ops AND touchstart preventDefaults so a tap can
never synthesize a focus-open - a tap just navigates the link), the focus
mirror (trigger focus opens immediately / blur closes - a keyboard user
SEES the card), the per-open TABINDEX STRIP (every tabbable inside is
forced tabindex=-1: keyboard and touch users never reach inside,
intentional and contractual - the reachable-elsewhere rule), and the
SELECTION HOLD (text selection started in the card keeps it open and
suppresses body user-select while dragging, so previews are copyable).

NO focus-scope anywhere in the lifecycle: focus never moves in, so there
is nothing to trap or restore - the trio's simplest teardown. The
token-activated dismissable delivers Esc (topmost-only) + outside press
while open. No aria surface is added (no haspopup/expanded/describedby):
advertising a keyboard-unreachable surface to AT is worse than silence.

**Values**: `open` (Boolean, default: false); `openDelay` (Number, default: 600); `closeDelay` (Number, default: 300)

**Events**: `poetry:hover-card:closed`, `poetry:hover-card:open`

### #connect()

Wires the content listeners (portal-safe) and reconciles a
server-pinned card (layer + tabindex strip catch up; the DOM wins).

### #disconnect()

Clears every timer, restores suppressed selection and portaled
content (drop-never-strand), and unwires the listeners.

### #openValueChanged(value)

Stimulus value callback - controllable state: pinned previews (Turbo
Stream / Outlet ownership).

### #pointerEnter(event)

The trigger's pointerenter action: arms the open timer; touch
pointerType is EXCLUDED - a tap navigates the
link instead.

### #pointerLeave(event)

The trigger's pointerleave action: cancels a pending open; leaving
toward the content keeps the card, anywhere else arms the close
grace.

### #focusOpen()

The trigger's focus action: opens IMMEDIATELY, skipping the timers -
a keyboard user sees the preview even though they
cannot enter it.

### #blurClose()

The trigger's blur action: closes immediately.

### #touchGuard(event)

The trigger's touchstart action - the touch guard: preventDefault so
a tap can never synthesize a focus event (a focus-opened card on
touch would be unreachable). The tap still navigates the
link.

## poetry--core--mask

The input-mask machine (the engine is adapted from an MIT-licensed
source - source and license in THIRD_PARTY_NOTICES.md) on a bare native
<input> - this.element IS the input. Three layers:

1. THE PRIMARY PATH is keydown with preventDefault: a printable char
   skips forward over literals from the caret and pattern-tests (a
   failing char is a NO-OP, it never reaches the field), a selection is
   replaced via a raw rebuild, and the caret lands past the next literal
   run. Backspace scans BACKWARD over literals (the caret lands at the
   deleted token), Delete scans FORWARD (the caret stays),
   Cmd/Ctrl+Backspace kills to start, arrows hop literal runs. Because
   preventDefault kills the native undo stack, a custom
   {raw, selectionStart} history (max 100, deduped) backs
   Cmd/Ctrl+Z / Shift+Cmd/Ctrl+Z / Ctrl+Y.

2. THE FALLBACK is the input-event diff (IME, mobile keyboards,
   autofill, password managers - paths that never emit clean keydowns):
   the longest common prefix+suffix against the previous display
   isolates the inserted text and the removed span, the value is rebuilt
   from raw pieces, re-masked, and the caret lands after the insertion.

3. THE DISPLAY: skeleton padding ("__/__") shows on focus
   (showMaskOnFocus, default) or permanently (alwaysShowMask); blur
   strips it back to the filled region - or clears the field entirely
   when autoClear is set and the mask is incomplete. A COLLAPSED caret
   is clamped into [first token, end of filled region] on focus (rAF),
   mousedown (rAF) and mouseup; selections are never touched.

A deliberate divergence from the adapted source: every programmatic value write dispatches a
native bubbling `input` event (#painting guards the controller against
its own echo) so Rails/Turbo listeners stay live. The raw value mirrors
to data-raw after every change; `pattern` is set on connect from
generatePattern("full-inexact") unless the input already carries one.

**Values**: `mask` (String); `slotChar` (String, default: "_"); `alwaysShowMask` (Boolean); `showMaskOnFocus` (Boolean, default: true); `autoClear` (Boolean); `upcase` (Boolean)

**Events**: `poetry:mask:change`, `poetry:mask:complete`

### #connect()

Parses the mask, stamps the pattern attribute (unless the input
carries one), wires the listener set, and processes a server-rendered
value silently.

### #disconnect()

Unwires the listener set.

## poetry--core--menu

The menus-family controller (the DropdownMenu contract's ANCHOR - ONE
menu engine behind DropdownMenu / ContextMenu / Menubar).
This owns ONLY what is menu-specific: open/close with the data-open-reason
initial-focus contract, item activation (the cancelable poetry:menu:select),
checkbox/radio state (poetry:menu:change), the APG typeahead buffer,
submenu open/close with hover intent + sibling exclusivity, and the
cancelable poetry:menu:edge-navigate seam a Menubar coordinator consumes.
Everything else is composed BY REFERENCE: focus-scope (trap + focus return
to the trigger), dismissable (topmost-only Esc + outside press, arriving
here as its "dismiss" event), roving-focus (arrows/Home/End per menu
level), popper (positioning - markup-owned, untouched here), presence
(data-open/data-closed flip -> animationend -> hidden).

STRUCTURAL RESOLUTION, no targets: the content is found via the trigger's
aria-controls id (portal-safe - a Stimulus target cannot cross a portal
move), items via the collection helper over the family's data-slot suffix
selectors ([data-slot$=menu-item] etc., so dropdown-menu-*, context-menu-*
and menubar-* anatomies all resolve), subs via their own aria-controls
pairs. Content-level listeners are wired programmatically in connect for
the same portal-safety reason.

The layer stack is ACTIVATED on open: the focus-scope / dismissable /
roving-focus identifiers are appended to the content's data-controller (a
statically-connected trap or dismiss layer on a hidden menu would steal
focus at page load and swallow topmost-Esc). Close reverses: presence exit
-> hidden -> tokens removed -> focus-scope's disconnect restores focus to
the trigger. Each open sub level adds its own dismissable layer (the
close-one-level-at-a-time Esc chain for free) + its own roving group; subs
join the ROOT focus scope (no nested traps, contractual).
Both family spellings resolve: dropdown-menu-* / context-menu-* share the
"menu-" suffix; menubar-* is its own word (menubar-item does NOT end with
"menu-item"), so every part selector carries the pair.

**Values**: `open` (Boolean, default: false); `modal` (Boolean, default: true); `loop` (Boolean, default: false); `typeaheadTimeout` (Number, default: 1000); `closeOnSelect` (Boolean, default: true)

**Events**: `poetry:menu:change`, `poetry:menu:closed`, `poetry:menu:edge-navigate`, `poetry:menu:open`, `poetry:menu:select`

### #connect()

Wires the content listeners (portal-safe) and reconciles: server-open
content adopts the layer stack and re-portals one frame late.

### #disconnect()

Restores portaled subs then the content (drop-never-strand), unwires
everything, and clears the sub hover-intent timers.

### #openValueChanged(value)

Stimulus value callback - controllable state: a host (outlet / Turbo
Stream / URL param) may own the open value; flipping the attribute
drives the same machine.

### #toggle()

The trigger's click action: open <-> close.

### #triggerKeydown(event)

The trigger's keydown action: Enter / Space / ArrowDown open + focus
the FIRST enabled item; ArrowUp opens + focuses the LAST (the APG
menu-button map). preventDefault also suppresses the button's
synthetic click, so toggle cannot double-fire.

### #open(reason = "trigger-press", { focus = true, seed = null } = {})

Programmatically opens (the family surface Menubar's coordinator
calls).

### #close(reason = "none", { restoreFocus = true } = {})

Programmatically closes.

### #activate(event)

Item activation - click/Enter/Space unified. Kept as a public action
for markup-declared data-action; the delegated content listener
claims the event first, so both paths never double-activate.

### #keydown(event)

The content's keydown route (also wired programmatically): Tab closes
and lets focus move on, Enter/Space activate (Space defers to a live
typeahead; native link/submit items act through the element itself),
the direction-aware horizontal pair opens/closes submenus or fires
the Menubar edge seam, and printable keys run typeahead per level.

### #subEnter(event)

Each sub-trigger's pointerenter action: hover intent toward opening
its sub level.

### #subLeave(event)

Each sub-trigger's pointerleave action: hover intent toward closing
(entering the sub pair within the grace cancels it).

### #openSub(event)

The sub-trigger's click action: opens its sub level (the delegated
click claims first).

### #closeSub(event)

Closes a sub-trigger's whole subtree (markup-declared affordances).

## poetry--core--menubar

The Menubar cross-menu COORDINATOR - kept
deliberately anorexic: ONE piece of state (`value`, which menu is open)
plus the three behaviors no other layer can own. The ownership split:
- roving-focus (horizontal, manageTabindex TRUE) owns which trigger is
  tabbable/focused on the bar - it knows nothing about menus.
- poetry--core--menu (one instance per menu, modal: false) owns
  everything inside an open menu - it knows nothing about siblings.
- this controller owns:
  1. TOGGLE - pointerdown opens (closing any sibling) / closes the open
     menu. Keyboard open (ArrowDown/Enter/Space -> first item; ArrowUp ->
     last item) rides the family's open-reason contract; pointer-open
     leaves focus on the trigger (pointer users keep their context).
  2. HOVER-SLIDE - pointerenter on a sibling trigger is a no-op from cold;
     once ANY menu is open it swaps to the hovered menu (the gated-hover
     rule) and focus moves to the new trigger.
  3. EDGE-NAVIGATE - the family menu controller fires a cancelable
     poetry:menu:edge-navigate when ArrowLeft/Right has no submenu
     meaning; this coordinator consumes it (loop-aware, RTL-aware,
     disabled-skipping) and opens the adjacent menu with its FIRST item
     focused (both directions - APG menubar). Standalone DropdownMenu
     leaves the event unconsumed.
Dismiss (Esc/outside) and select arrive as the family's poetry:menu:closed
- the coordinator nulls value; focus return to the trigger is the family
focus-scope's job (its connect snapshot IS the trigger, because every
open path here puts focus there first).

A press on a sibling trigger must be TOGGLE's, not the open menu's
dismissable layer's: the coordinator vetoes interact-outside for presses
landing on this bar's triggers (otherwise dismiss-then-toggle would
close-and-reopen in the same pointerdown).

**Values**: `value` (String, default: ""); `loop` (Boolean, default: false)

**Events**: `poetry:menubar:value-changed`

### #connect()

Wires the sibling-trigger interact-outside veto and reconciles one
microtask behind (the per-menu controllers connect after the bar).

### #disconnect()

Unwires the veto listener.

### #valueValueChanged(value, previous)

Stimulus value callback - controllable state: a host (outlet / Turbo
Stream / URL param) may own the value; flipping the attribute drives
the same machine.

### #toggle(event)

Each trigger's pointerdown action: opens (closing any sibling) or
closes the open menu - left button only, ctrl-click passes (the macOS
context menu); pointer-open leaves focus on the trigger.

### #hoverSlide(event)

Each trigger's pointerenter action - the gated hover: a no-op from
cold; once ANY menu is open it swaps to the hovered menu.

### #triggerKeydown(event)

Each trigger's keydown action: Enter/Space/ArrowDown open with the
FIRST item focused, ArrowUp with the last (the family's open-reason
contract).

### #slideAdjacent(event)

The edge-navigate action (cancelable, bubbling from the open menu's
ROOT content). detail.direction is the PHYSICAL arrow; RTL maps it
here. ALWAYS consumed once the menu belongs to this bar - an
unconsumed edge would fall through to the bar's roving-focus and move
trigger focus while the menu stays open (bar arrows are inert while
open, by rule). At a no-loop boundary that means: consumed, no
move.

### #onMenuClosed(event)

The family's closed event, from any of the bar's menus: nulls the
value unless the close is one half of an in-flight swap.

## poetry--core--message-scroller

How long (ms) data-autoscrolling stays set during a programmatic scroll
before clearing. While set, the follow-bottom RELEASE is suppressed so the
auto-scroll animation cannot release itself.

**Targets**: `button`, `content`, `spacer`, `viewport`

**Values**: `autoScroll` (Boolean, default: false); `defaultScrollPosition` (String, default: "end"); `preserveScrollOnPrepend` (Boolean, default: true); `trackVisibility` (Boolean, default: false); `scrollEdgeThreshold` (Number, default: 8); `scrollPreviousItemPeek` (Number, default: 64); `scrollMargin` (Number, default: 0)

**Events**: `poetry:message-scroller:mode`, `poetry:message-scroller:pinned`, `poetry:message-scroller:scrollable`, `poetry:message-scroller:unpinned`, `poetry:message-scroller:visibility`

### #connect()

Builds the ref bag, wires the viewport listeners and the three
observers, runs the mount pass (rows counted, defaultScrollPosition
applied once), and arms visibility tracking when opted in.

### #disconnect()

Cancels every frame/timer, disconnects the observers, and unwires the
viewport listeners (the body comment holds the stale-id rule).

### #defaultScrollPositionValueChanged()

Stimulus value callback: a defaultScrollPosition change re-arms the
one-shot apply.

### #autoScrollValueChanged()

Stimulus value callback: re-pin if we were following.

### #syncAfterScroll()

The viewport's scroll handler: commits scrollable state, schedules a
visibility sync, and re-captures the prepend anchor.

### #userScrollIntent()

The wheel/touchmove handler: a deliberate gesture releases
auto-follow, turn-anchoring, AND an in-flight programmatic jump so
re-pinning never fights the reader.

### #keydownIntent(event)

The viewport's keydown handler: scroll keys count as deliberate
intent.

### #jump(event)

The jump button's click action. No-op while inactive; blurs so focus
is not stranded on a control about to inert itself.

### #scrollToEnd(eventOrOptions = {})

Scrolls to the end - callable as a Stimulus action (options via
params) or directly with an options object (outlet callers).

### #scrollToStart(eventOrOptions = {})

Scrolls to the start (scrollToEnd's calling conventions).

### #scrollToMessage(eventOrId, options)

scrollToMessage("id", options) programmatically, or as an action with
data-...-message-id-param (align/behavior/scrollMargin params pass
through).

## poetry--core--navigation-menu

The NavigationMenu coordinator (the viewport=false mode): a
DISCLOSURE BAR, not a menu - Tab moves through triggers and links
normally, arrows are convenience navigation, nothing traps. Each item's
panel is its own popup positioned under the item; this controller owns
what per-item popovers can't: ONE panel open at a time, hover intent
(open/close delays so diagonal travel into a panel doesn't flicker),
Esc + focus-out + outside-press closing, and the vocabulary writes
(data-popup-open on the trigger - the chevron's rotation hook - and the
presence-driven open/closed pair on the panel).

THE VIEWPORT MODE: when the markup ships the shared
positioner > popup > viewport shell (viewport: true), panels are
lazily ADOPTED into the viewport on first activation (the Rails
stand-in for a portal - server-rendered content stays in
place until JS activates) and the composite MORPHS: the popper
re-anchors to the active trigger (full floating-ui) while CSS
transitions the positioner's insets, the popup's --popup-width/height
pin old -> new across two frames so width/height transition, panels
slide by data-activation-direction (new trigger vs old - travel
direction drives the slide), and data-instant suppresses transitions
on cold opens. Vars reset to auto after the animations finish (the
auto-size reset via getAnimations().finished).

**Values**: `openDelay` (Number, default: 50); `closeDelay` (Number, default: 150)

### #connect()

Subscribes the before-cache close (the body comment holds the zombie
rules).

### #disconnect()

Clears the timer and unwires the outside-press and before-cache
listeners.

### #toggle(event)

Each trigger's click action - immediate, cancels hover intent.

### #scheduleOpen(event)

Each ITEM's pointerenter action (the panel lives inside its item, so
moving into the panel never schedules a close): arms hover intent -
instant switch while the bar is already open, the open delay on cold
entry. Touch is click's job.

### #scheduleClose(event)

Each ITEM's pointerleave action: arms the close grace.

### #cancelClose()

The shared positioner's pointerenter action - in viewport mode the
panel no longer lives inside its item, so entering the popup must
cancel a pending close.

### #keydown(event)

The root's keydown action: Escape closes and refocuses the trigger;
ArrowLeft/Right move between the bar's stops (triggers and top-level
links).

### #focusLeft(event)

The root's focusout action - a disclosure closes when focus leaves it
entirely (never traps).

## poetry--core--number-field

NumberField: a formatted
visible <input type=text> over a hidden <input type=number> that is the
form/validation truth - the spinbutton ARIA pattern is deliberately
not used. The controller owns stepping (arrows, steppers with
press-and-hold, opt-in wheel), parse/clamp/format, and the two-input
sync. Number | null value model: empty is null, never NaN.

Deliberate v1 boundaries (documented in the component):
no scrub area, Latin-digit parsing only (locale separators and
currency/percent symbols ARE handled via Intl.formatToParts), and the
server renders the raw number - the display formats on connect.

**Targets**: `decrement`, `hidden`, `increment`, `input`

**Values**: `min` (Number); `max` (Number); `step` (Number, default: 1); `largeStep` (Number, default: 10); `smallStep` (Number, default: 0.1); `snap` (Boolean); `wheel` (Boolean); `format` (Object); `locale` (String)

**Events**: `poetry:number-field:change`, `poetry:number-field:commit`

### #connect()

Adopts the hidden value, paints the formatted display, reflects the
boundaries, and wires the opt-in wheel plus the window-level release
listeners.

### #disconnect()

Unwires the wheel/release listeners and stops any repeat.

### #keydown(event)

The visible input's keydown action: ArrowUp/Down step (Shift = large
step, Alt = small), Home/End jump to the bounds, and printable keys
pass the character gate.

### #input()

The input action: text updates freely; the value goes live only
while parseable. The display is NEVER rewritten mid-typing
(dirty-text authority).

### #focus()

The focus action: the first focus parks the caret at the end.

### #blur()

The blur action - the text commit point: empty clears, unparseable
text is left as typed with no commit, parseable text clamps and
normalizes to the canonical formatted display (never snapped to
step - blur is not a correction gesture).

### #press(event)

Each stepper's pointerdown action: steps once, then repeats after the
hold delay (mouse focuses the input; touch would pop the software
keyboard).

### #tap(event)

The stepper's click action: quick touch taps synthesize click
without a held pointer - step once, but never double-step after a
handled pointerdown.

### #leave()

The stepper's pointerleave action: stops the repeat.

### #hiddenChanged()

The hidden input's change action: browser autofill lands there -
adopt, clamp, commit.

## poetry--core--optimistic-form

Optimistic UI for Turbo forms, adapted from an MIT-licensed source
(source and license in THIRD_PARTY_NOTICES.md).
The optimistic update is AUTHORED AS A TURBO STREAM inside a <template>
target - the same vocabulary the server answers in, so prediction and
truth share one mental model and there is no bespoke DOM patching. On
turbo:submit-start the template contents are cloned into the document
and Turbo paints the predicted state immediately; on turbo:submit-end
the controller reconciles ONLY when the submission failed, by
appending <turbo-stream action="refresh"> - which must morph (the
helper documents the turbo-refresh-method=morph meta) so the
correction is seamless.

THE SERVER CONTRACT (enforced nowhere, so stated everywhere): success
answers 204 (or a targeted stream for authoritative correction under
contention) - NEVER a redirect, because a redirect under morph
refreshes is itself a full reload and defeats the optimism. Failure
answers 4xx so event.detail.success is false and the refresh restores
authoritative truth. Full story: the Optimistic Forms guide on the poetry docs site.

**Targets**: `template`

### #apply()

Applies every template's stream(s) - the turbo:submit-start action.
Rapid resubmits inside the window cannot stack duplicate clones; the
first paint is still immediate.

### #reconcile(event)

The turbo:submit-end action: appends the authoritative refresh stream
ONLY when the submission failed (the body comment holds the rule).

## poetry--core--otp

The InputOTP projection (the single-input architecture, poetry's own
build - no npm dependency): ONE real native <input> holds the whole
value, stretched invisibly over the slot row, so paste, SMS autofill,
IME, constraint validation and form serialization are all native and AT
sees ONE text field. The n slot cells are an aria-hidden MIRROR painted
here - this controller contains ZERO editing logic:

- #sync filters input.value per-character through the pattern + truncates
  to length (writing back only when filtering changed it - that one line
  IS paste splitting: "123-456" under digits becomes "123456"), paints
  slot[i] with value[i], and projects the caret: the active cell is
  min(selectionStart, length - 1) while focused (data-active), with the
  fake blinking caret element visible only on the active EMPTY cell.
- Auto-advance and backspace-retreat are not features: typing moves the
  native caret forward, Backspace moves it back - the active cell is a
  PROJECTION of selectionStart. Arrows/Home/End/IME: native, re-projected
  via the document-level selectionchange listener (bound on focus,
  unbound on blur - n OTP fields must not all re-project on every caret
  move anywhere).
- poetry:otp:change fires per accepted mutation; poetry:otp:complete
  fires once when the value reaches length and re-arms below it (the
  enable-the-submit-button hook - it never submits).

**Targets**: `input`, `slot`

**Values**: `length` (Number, default: 6); `pattern` (String, default: "\\d")

**Events**: `poetry:otp:change`, `poetry:otp:complete`

### #connect()

Compiles the pattern, adopts the input's value, and paints the
initial projection (no events dispatched).

### #disconnect()

Unbinds the document-level selectionchange listener.

### #sync(event)

The native input's input / focus / blur action (one handler - the
projection is idempotent; blur clears the active cell; focus binds
the selectionchange re-projection).

### #paste(event)

The paste action. Paste is the one native path maxlength breaks: the
browser truncates the RAW clipboard text to maxlength before any
input event, so "123-456" loses its tail before the sync pass could
filter the dashes (real Chrome; jsdom doesn't enforce maxlength,
which is why the unit tier never saw it). Intercept, filter FIRST,
splice at the selection.

### #focusInput()

The container's click action (gaps, separators - the input already
covers the cells at z-20): focuses the real control.

## poetry--core--popover

The Popover controller (the popper-consumer trio's click-open member):
the menu controller's #show/#hide + token-activated layer skeleton with
ALL item machinery deleted - no typeahead, no roving, no subs, no
collection. What remains is exactly the APG dialog-pattern-lite: the
trigger toggles a role=dialog panel, focus MOVES INTO the content on open
(focus-scope's mount default - deliberately NOT vetoed, the contrast with
the menu family's data-open-reason contract) and RETURNS to the trigger on
close; the trap is enforced only when modal (the default is
modal: FALSE - the deliberate contrast with the menu family's true).

STRUCTURAL RESOLUTION, no targets: the content is found via the trigger's
aria-controls id (portal-safe - the menu-controller pattern verbatim);
content-level listeners are wired programmatically in connect for the same
reason.

The layer stack is ACTIVATED on open: focus-scope + dismissable are
appended to the content's data-controller (a statically-connected trap on
hidden content would steal focus at page load; a static dismissable would
swallow topmost-Esc), with trapped / disable-outside-pointer-events both
set from modal. Close reverses: presence exit -> hidden -> tokens removed
-> focus-scope's disconnect restores focus to the trigger (suppressed for
outside-press when non-modal: focus follows the click).

**Values**: `open` (Boolean, default: false); `modal` (Boolean, default: false)

**Events**: `poetry:popover:closed`, `poetry:popover:open`

### #connect()

Wires the content listeners (programmatic - portal-safe) and
reconciles: server-open content adopts the layer stack and re-portals
one frame late (the body comments hold the rules).

### #disconnect()

Restores portaled content (drop-never-strand) and unwires the
content listeners.

### #openValueChanged(value)

Stimulus value callback - controllable state: a host (outlet / Turbo
Stream / URL param) may own the open value; flipping the attribute
drives the same machine.

### #toggle()

The trigger's click action (native button Enter/Space arrive as
click - no custom keydown map, the deliberate contrast with the menu
trigger).

### #open()

Programmatically opens.

### #close(reason = "none")

Programmatically closes.

## poetry--core--pressed

The Toggle micro-machine (the smallest controller in the suite): flip
aria-pressed and mirror the bare data-pressed presence boolean -
WRITTEN TOGETHER, never separately. The DOM is the store: no Values, no internal state. This is
deliberately NOT poetry--core--checked (different ARIA vocabulary, no
input to sync) and not poetry--core--state (that owns aria-expanded
disclosure) - three micro-controllers, three vocabularies.

No keydown code: Space AND Enter both activate a native button
(click) - the platform already implements the keyboard.

poetry:toggle:change is CANCELABLE and fires BEFORE the flip renders:
preventDefault vetoes it (hosts that must confirm). The pressed value in
the detail is the state the toggle is ABOUT to enter.

**Events**: `poetry:toggle:change`

### #toggle()

The click action: flips to the opposite pressed state. No-op while
disabled; a listener that vetoes the (cancelable) change event stops
the flip.

### #press()

Programmatically presses the toggle (see set).

### #unpress()

Programmatically releases the toggle (see set).

### #set(pressed)

Programmatically writes a pressed state through the same veto path
the click takes. No-op while disabled.

## poetry--core--questionnaire

The Questionnaire machine, adapted from an MIT-licensed source (source
and license in THIRD_PARTY_NOTICES.md): a native <form> of fieldset
items shown ONE at a
time. The server renders the complete initial state (active item,
statuses, shortcuts, button visibility); this controller owns the
runtime transitions - navigation (validate-gated Next, Skip for
optional items, submit validates every item and jumps to the first
invalid), answer tracking (choice change / text input -> status), the
keyboard map (Cmd/Ctrl+Enter confirm, ArrowUp/Down answer focus,
ArrowLeft/Right item navigation, Enter-on-filled-answer confirm,
letter/number shortcuts), and the data-attribute stamps the styling
contract reads. Answers are native radio/checkbox/text inputs - the
form serializes with zero JS.

**Targets**: `next`, `previous`, `progress`, `skip`, `submit`

**Values**: `shortcuts` (String, default: "")

**Events**: `poetry:questionnaire:item-change`, `poetry:questionnaire:status-change`

### #connect()

Derives every stamp (checked/filled/status/progress/buttons) from the
server-rendered DOM.

### #previous()

The previous button's click action: back one item (no validation).

### #next()

The next button's click action: validate-gated advance - an invalid
active item takes focus instead.

### #skip()

The skip button's click action (optional items only): stamps skipped,
then advances - or submits from the last item.

### #submit(event)

The form's submit action: every enabled item must validate; the first
invalid one becomes active with its error focused.

### #reset()

The form's reset action: native reset restores the inputs; re-derive
every stamp from the restored DOM and return to the first item.

### #change(event)

The choice inputs' change action: re-stamps the item's choices,
status, and validity.

### #input(event)

The text inputs' input action: re-stamps filled/status/validity.

### #keydown(event)

The form's keydown action (the primitive's full map): Cmd/Ctrl+Enter
confirms, ArrowUp/Down move answer focus, ArrowLeft/Right navigate
items (outside text entry and radios), Enter on a filled answer
confirms, and letter/number shortcuts pick choices.

## poetry--core--radio-group

The RadioGroup checked-value machine (the Accordion composition shape):
this controller owns ONLY the value + attribute/input writes - ZERO
keyboard code. The shared poetry--core--roving-focus runs on the same
root in its DEFAULT tabindex-managing mode (one Tab stop) with the
orientation: "both" extension (all four arrows, APG radio), and this
controller consumes its cancelable entry event to implement SELECTION
FOLLOWS FOCUS: entry fires only on arrow/Home/End navigation - never on
Tab or click-focus - so checking on entry is exactly the APG contract
(Tab into the group never changes the value; contractual).

The form story is the hidden-native-input rule: one <input type=radio>
per item, shared name (aria-hidden, tabindex=-1) - native radio
serialization, byte-identical to collection_radio_buttons. check() writes
every item's aria-checked/checked pair/indicator, sets the hidden input's
.checked (the native group unchecks siblings; written explicitly anyway -
belt and braces), moves the roving tab stop to the checked item, and
dispatches poetry:radio-group:change + native input/change on the newly
checked hidden input (Rails autosave/change-tracking listeners fire).

**Targets**: `input`

**Values**: `value` (String, default: "")

**Events**: `poetry:radio-group:change`

### #connect()

Reconcile-on-connect: adopts the server-checked item into the Value
when none was given, else normalizes the DOM to the Value (the body
comment holds the rules).

### #check(event)

Each item's click action (Space arrives here too, via native button
activation): checks the pressed item.

### #entryCheck(event)

The roving-focus entry action - the entry event fires ONLY on
arrow/Home/End navigation (never on Tab), so this IS
selection-follows-focus (the APG contract).

### #setValue(value)

The programmatic controllable-state surface: checks the item carrying
`value`. Unknown values log a debug line and change nothing.

### #valueValueChanged(value, previous)

Stimulus value callback: re-writes the DOM when the Value changes
after connect (an Outlet or a host writing the attribute directly).

## poetry--core--resizable

The Resizable engine, decided NATIVE: no panel library -
panels are flex children whose flex-grow IS the percentage, and this
controller implements the APG window-splitter on the handles: pointer
drag redistributes the two adjacent panels (clamped to each panel's
min/max), arrows step, Home/End jump the range, and every move writes
aria-valuenow (the preceding panel's size).
Deferred with the library's machinery: persistence (autoSaveId),
collapsible panels, and the imperative API.

**Values**: `orientation` (String, default: "horizontal")

**Events**: `poetry:resizable:resize`

### #connect()

Writes the initial aria-value* reflection onto every handle.

### #dragStart(event)

Each handle's pointerdown action: begins a drag between the two
adjacent panels (pointer capture holds the gesture).

### #dragMove(event)

The captured pointermove action: redistributes the two panels by the
drag delta, clamped to both panels' min/max.

### #dragEnd(event)

The pointerup / pointercancel action: ends the drag.

### #keydown(event)

Each handle's keydown action (the APG window splitter): arrows step
by 5%, Home/End jump the range.

## poetry--core--scroll-spy

Scroll-spy: marks the
nav link whose section is currently active while the page scrolls - the
docs-TOC pattern. Put the controller on the nav; every link target's
href="#id" names its section:

  <nav data-controller="poetry--core--scroll-spy">
    <a href="#usage" data-poetry--core--scroll-spy-target="link">Usage</a>

The active section is the LAST one whose top sits above the offset line
(a closest-heading reduce); its link gains data-active and a
poetry--core--scroll-spy:changed event carries the id. rAF-coalesced
passive scroll + resize listeners; call refresh() after content changes.

### #connect()

Resolves the sections and wires the rAF-coalesced passive
scroll/resize listeners.

### #disconnect()

Unwires the listeners and cancels any pending frame.

### #refresh()

Re-resolves sections from the links' hashes (content changed, Turbo
morph, tab switch) and recomputes immediately.

## poetry--core--search-field

The SearchField seams: Escape CLEARS a
non-empty field and is consumed - the NEXT press reaches the dismissal
layer and closes a parent overlay; an already-empty field lets Escape
propagate untouched. Emptiness is checked against the RAW input value
(autofill and scripts poke the DOM directly). The clear button keeps
focus in the input by preventing the press's focus steal at
pointerdown - on mobile that is what keeps the virtual keyboard up.
Enter is never intercepted: native form submission is the Rails path.

**Targets**: `clear`, `input`

**Events**: `poetry:search-field:clear`

### #connect()

Reflects the initial emptiness (data-empty + the clear affordance).

### #changed()

The input action: keeps data-empty and the clear affordance honest.

### #keydown(event)

The input's keydown action: Escape CLEARS a non-empty field and is
consumed; an already-empty field lets it propagate to the dismissal
layer (the seam the header documents).

### #holdFocus(event)

The clear button's pointerdown action: keeps focus (and the mobile
keyboard) in the input - the later click acts.

### #clear(event)

The clear button's click action: empties the field (dispatching a
REAL input event so live-search listeners react) and refocuses it.
No-op while disabled or readonly.

## poetry--core--select

The Select listbox controller - the APG select-only combobox on the menus
MACHINERY (popper markup-owned + focus-scope/dismissable/roving-focus
activated as layers + the shared typeahead helper) but deliberately NOT a
mode of poetry--core--menu: everything menu-specific (submenus,
close_on_select, checkbox/radio aria-checked, edge-navigate, Tab-closes,
pointer-open-no-focus) is wrong for a listbox, and everything here is
dead weight for menus.

THE SYNC INVARIANT (the component): native_select.value, the value Value,
aria-selected/data-selected on options, and the display text never diverge -
every write path (commit, closed-trigger typeahead, autofill adoption,
programmatic setValue) funnels through #apply, which writes the NATIVE
SELECT FIRST (serialization truth is never behind the facade), dispatches
real bubbling change/input on it (Turbo auto-submit and friends work
unmodified), flips aria-selected + data-selected TOGETHER on every option,
syncs the value display from the option's item-text (data-text-value
override), toggles trigger[data-placeholder], then fires
poetry:select:change.

Three deltas vs the menu family, all deliberate and contractual:
- Tab while open is INERT (a value picker resolves by commit or Esc);
- open focuses the SELECTED option for EVERY reason, pointer included;
- typeahead on the CLOSED trigger COMMITS the match without opening
  (native <select> behavior), while open typeahead only moves focus.

**Values**: `open` (Boolean, default: false); `value` (String, default: ""); `modal` (Boolean, default: true); `loop` (Boolean, default: false); `typeaheadTimeout` (Number, default: 1000); `alignItemWithTrigger` (Boolean, default: false)

**Events**: `poetry:select:change`, `poetry:select:closed`, `poetry:select:open`, `poetry:select:select`

### #connect()

Captures the placeholder, reconciles silently from the native select
(the serialization truth) or the given Value, wires the content
listeners, and catches a server-open listbox up (layers + the late
portal).

### #disconnect()

Restores portaled content (drop-never-strand), unwires the content
listeners, resets typeahead and the scroll hold, and abandons any
exit.

### #openValueChanged(value)

Stimulus value callback - controllable open state.

### #valueValueChanged(value)

Stimulus value callback - controllable value (no-ops on the applied
echo).

### #toggle()

The trigger's click action: open <-> close.

### #triggerKeydown(event)

The trigger's keydown action: Enter / Space / ArrowDown / ArrowUp
open and focus the SELECTED option (closed arrows never step
the value). A printable key on the CLOSED trigger commits the
typeahead match WITHOUT opening (native <select> behavior - the full
commit pipeline minus open/close).

### #open(reason = "trigger-press", { seed = null } = {})

Programmatically opens.

### #close(reason = "none")

Programmatically closes.

### #setValue(value)

The programmatic controllable-state surface: applies a value through
the full sync pipeline.

### #commit(event)

Each option's click action (markup-declared); the delegated content
click claims first, so both paths never double-commit.

### #keydown(event)

The content's keydown route (also wired programmatically): Enter and
Space commit the focused option (Space defers to a live typeahead),
Tab is INERT while open (a value picker resolves by commit or Esc -
contractual), Left/Right no-op (flat listbox), and printable
keys move focus via typeahead - selection never follows focus.

### #nativeChanged()

The hidden native select's change action: browser autofill (or any
programmatic write) fires change there - the UI adopts it without
re-writing the native (no loop).

### #syncScrollButtons()

The viewport's scroll action (also run on open): shows each scroll
button only while scroll room remains on its side.

### #scrollHoldStart(event)

The scroll buttons' pointerenter action: scrolls the viewport a few
px per frame while hovered (hold-to-scroll).

### #scrollHoldStop()

The scroll buttons' pointerleave action: stops the held scroll.

## poetry--core--sensitive-input

The SensitiveInput machine, adapted from an MIT-licensed source
(source and license in THIRD_PARTY_NOTICES.md): a secret field in
three states - masked | revealed | empty - where data-state on the root
carries the truth and CSS renders it. Masked-with-value turns the MASK
OVERLAY into the reveal affordance (role=button + label + sr-hint; only
text spans inside - a role on the surrounding group would trip axe
nested-interactive around the inert input)
while the real input stays rendered for layout but goes inert
(aria-hidden, tabindex -1, readonly, transparent). Reveal: click
anywhere on the group (mask clicks bubble) or Enter/Space on the mask,
focus moves into the input. Re-mask: Escape (focus returns to the mask
- the input just lost its tab stop), leaving the component, or the eye.
Typing into an empty field auto-reveals so composition happens in
type=text. The no-JS story is a plain password input.

**Targets**: `hint`, `input`, `mask`, `toggle`

**Values**: `maskedLabel` (String); `hiddenMessage` (String); `readOnly` (Boolean)

**Events**: `poetry:sensitive-input:mask`, `poetry:sensitive-input:reveal`

### #connect()

Adopts the server-rendered data-state and re-derives the reflection.

### #reveal(event)

The group's click action: click anywhere on the bordered group
reveals (the mask button's own clicks bubble here too). Addon-cell
clicks and synthetic label clicks are filtered (the body comments).

### #maskKeydown(event)

The mask overlay's keydown action: Enter/Space reveals (the overlay
is the reveal affordance while masked).

### #inputKeydown(event)

The input's keydown action: Escape re-masks and is consumed - the
NEXT press reaches the dismissal layer.

### #blurred(event)

The root's focusout action: leaving the component with a value
re-masks.

### #changed()

The input action: emptiness drives the state; the first character
typed into an empty field reveals (composition belongs in type=text).

### #toggle(event)

The eye's click action: re-masks and hands focus to the mask button -
the eye is about to hide (it only exists while revealed).

## poetry--core--sheet

The Sheet: the dialog machinery, animated end to end. Everything hard
about the overlay is inherited (native <dialog> platform trap,
backdrop-click discrimination, scroll lock, hotkey) - including the
presence-hold close, which the base controller owns (exit flips the
pair to data-closed and holds the dialog through the slide-out before
the native close()). This subclass only upgrades the ENTER: open rides
enterPresence so the data-starting-style hook fires like every other
presence consumer.

**Targets**: `dialog`

**Values**: `dismissible` (Boolean, default: true); `hotkey` (String, default: "")

### #open()

Opens with the animated entry: showModal(), then the presence enter
so the data-starting-style hook fires like every other presence
consumer (the exit half is inherited - the base close() already holds
through the slide-out).

## poetry--core--sidebar

The Sidebar state machine (desktop plus the mobile mode): expand/collapse
coordination for the app shell. The COLLAPSE itself is pure CSS - the
peer sidebar carries data-state=expanded|collapsed and the dictionary's
group-data-[state=collapsed] classes do all the width/transform work;
this controller only flips that attribute (plus data-collapsible, which
the source sets to the mode WHILE collapsed and "" while expanded),
persists the choice to a cookie (so the SERVER can read it and render
the right initial state - poetry's server-first angle), and binds the
Cmd/Ctrl+B shortcut.

MOBILE (DOM-move): below md the trigger routes to a separate
never-persisted openMobile state (only desktop
toggles write the cookie). Opening ADOPTS the server-rendered nav
children from the desktop inner into the mobile <dialog> (one render,
no duplicate ids - the render-twice rejection) and shows it through the
sheet presence path; closing holds through the slide-out, then moves
the children back. Crossing to desktop while open restores INSTANTLY.
The component-facing event namespace (the poetry:<component> rule).

**Targets**: `inner`, `mobileDialog`, `mobileInner`, `sidebar`

**Values**: `open` (Boolean, default: true); `collapsible` (String, default: "offcanvas"); `cookieName` (String, default: "sidebar_state"); `cookieMaxAge` (Number, default: 604800); `shortcut` (String, default: "b")

**Events**: `poetry:sidebar:mobile-toggle`, `poetry:sidebar:toggle`

### #connect()

Heals a restored zombie snapshot, reflects the server value once (the
body comment explains why not openValueChanged), starts the
breakpoint watcher, subscribes the before-cache close, and binds the
shortcut.

### #disconnect()

Unwires the shortcut / watcher / before-cache subscriptions and
balances the scroll lock.

### #toggle()

The trigger's (and rail's) click action - also the shortcut's
landing. On mobile the SAME trigger routes to the sheet; on desktop
it flips the collapse (inert when collapsible is "none").

### #closeMobile(event)

The mobile dialog's cancel action (and the close affordances): closes
through the sheet exit, then moves the nav children home.

### #mobileBackdropClose(event)

The mobile dialog's click action - the dialog's coordinate
discrimination (a backdrop click targets the <dialog> itself AND
lands outside its bounding rect).

### #open()

Programmatically expands the desktop sidebar (persisted).

### #close()

Programmatically collapses the desktop sidebar (persisted).

## poetry--core--slider

The Slider machine - the only form control with real math. Three concerns,
nothing else:

1. THE VALUE MATH (the pure core, unit-tested exhaustively): snap to the
   step grid with decimal-precision rounding (a 0.1 grid lands on
   0.3, never 0.30000000000000004), clamp to
   [min, max], and clamp against neighbor thumbs +- the min gap
   (minStepsBetweenThumbs * step) so range thumbs can never cross.

2. TWO INPUT PATHS feeding it: the APG keyboard map per thumb (arrows
   +-step, Shift+Arrow / PageUp / PageDown +-step*10, Home/End to the
   thumb's EFFECTIVE min/max) with the orientation x RTL x inverted
   resolution - horizontal RTL swaps Left/Right only, inverted flips the
   whole axis, both compose (rtl + inverted = ltr math); and pointer
   capture on the root - pointerdown jumps the NEAREST thumb (ties to
   the later index, so stacked thumbs stay separable), the track
   box is read ONCE at pointerdown (no per-frame layout), pointermove
   projects the value ABSOLUTELY from the pointer position (overshoot
   past a neighbor clamps, never swaps), pointerup commits.

3. THE DOM PROJECTION: aria-valuemin/max/now per role=slider thumb - the
   range bounds are DYNAMIC (neighbor-clamped, rewritten on every
   neighbor move: APG multithumb), the --slider-start/--slider-end
   geometry vars consumed by the calc() rules, and one hidden native
   input per thumb. poetry:slider:change fires per mutation; commit
   (pointerup / each keydown) syncs the
   hidden inputs + dispatches native input/change so Rails listeners get
   one event per gesture, not per frame.

**Targets**: `input`, `range`, `thumb`, `track`

**Values**: `min` (Number, default: 0); `max` (Number, default: 100); `step` (Number, default: 1); `value` (Array, default: []); `minStepsBetweenThumbs` (Number, default: 0); `orientation` (String, default: "horizontal"); `inverted` (Boolean, default: false)

**Events**: `poetry:slider:change`, `poetry:slider:commit`

### #connect()

Adopts the value (the Value, else the server-rendered aria-valuenow),
projects, and primes the hidden inputs silently.

### #disconnect()

Ends any in-flight gesture without committing.

### #valueValueChanged(value)

Stimulus value callback - controllable state: a host (outlet / Turbo
Stream) may own the value.

### #keydown(event)

Each thumb's keydown action: the APG map (arrows step, Shift/Page =
large step, Home/End to the thumb's EFFECTIVE bounds) under the
orientation x RTL x inverted resolution; every keyboard change
commits.

### #pointerdown(event)

The root's pointerdown action: jumps the NEAREST thumb to the pointer
(ties to the later index), reads the track box once, and starts the
window-level drag; pointerup commits.

### #setValue(value)

The programmatic controllable-state surface: must keep the thumb
count; changes commit.

## poetry--core--table-selection

The table row-selection engine (the SelectionManager
contract, checkbox-flavored): per-row checkboxes are the form value
(selected_ids[] - no JS means plain checkboxes in a form, the honest
fallback), this controller adds what HTML cannot:

- select-all with a real INDETERMINATE middle state (a JS property,
  never an attribute), computed over ENABLED rows only (disabled rows
  are skipped by select-all and ranges - disabledBehavior 'selection').
- Shift-click range selection off the ANCHOR model: the
  anchor is the last plainly-toggled row; Shift sets every row in
  anchor..target to the ANCHOR row's state (the checkbox idiom).
- aria-selected + data-selected mirrored onto rows, count announcements
  through the announce singleton, and a bubbling selection-change event
  the ActionBar block feeds on.
- poetry:data-table:clear-selection (dispatched by the ActionBar's
  Escape) clears everything from anywhere inside the wrapper.

**Targets**: `all`

**Values**: `label` (String, default: "%{count} selected")

**Events**: `poetry:data-table:selection-change`

### #connect()

Wires the ActionBar's clear-selection listener and reflects silently.

### #disconnect()

Unwires the clear-selection listener.

### #toggled(event)

Each row checkbox's change action. Shift ranges ride the click that
produced the change (change events carry no modifiers - press
captures the gesture): a shift-toggle sets anchor..target to the
ANCHOR row's state; a plain toggle re-anchors.

### #press(event)

The pointerdown/keydown seam: remembers whether the NEXT change was
a shift-gesture (change events themselves carry no modifiers).

### #toggleAll(event)

The select-all checkbox's change action: fans its new state out to
every ENABLED row and clears the anchor.

## poetry--core--tabs

The Tabs activation machine: this controller owns ONLY the
active-value state + attribute writes; the shared poetry--core--roving-focus
on the tablist owns the keyboard (default tabindex-managing mode - one Tab
stop). Triggers are DUMB buttons (click -> tabs#activate; with automatic
activation - the APG default for tabs - focusin activates too, so arrow
keys both move focus AND switch panels).

The state vocabulary: the active trigger carries data-active
(the styled token) + aria-selected; inactive panels carry the hidden
property + data-hidden. data-activation-direction is deliberately NOT
emitted - no shipped class consumes it (add it with the animated
indicator, when something does).

Panels are scoped to THIS root (a nested Tabs inside a panel owns its own
triggers/panels - the DOM is the registry, same rule as roving-focus).

**Values**: `activateOnFocus` (Boolean, default: true)

**Events**: `poetry:tabs:change`

### #connect()

Reconcile-on-connect: derives the full vocabulary from the
server-rendered data-active truth (the body comment holds the rules).

### #activate(event)

Each trigger's click action: activates the pressed trigger's value.

### #focusActivate(event)

The tablist's roving-focus entry action - automatic activation
follows the roving focus via the roving controller's entry event
(deterministic: never depends on the platform firing focusin for a
programmatic .focus()). A raw focusin routes here too, so hand-wired
hosts get the same behavior.

### #setValue(value)

The programmatic controllable-state surface: setValue("account").
Unknown values are ignored (the contract's guard). Returns the
resulting state - what an agent tool reports back - so a no-op is
visible as changed: false.

## poetry--core--tag-group

The TagGroup removal engine (the tag-group contract):
navigation itself rides the roving-focus controller (the toolbar
precedent) - this controller owns what tags add on top:

- Delete/Backspace on a focused tag removes it (row-origin keys only;
  keys from a tag's inner remove button must not drive the grid).
- The remove button removes exactly its own tag.
- Focus recovery after removal is the reference walk: FORWARD through
  the pre-removal order to the first surviving enabled tag, then
  backward; when the last tag goes, the CONTAINER takes focus, flips
  role grid->group, and becomes the tab stop.
- The container is a live region ONLY while focus is within (polite,
  additions) - SRs hear tags added while working in the group without
  spam from elsewhere.

Removal is CANCELABLE (poetry:tag-group:remove): a Turbo-driven host
preventDefault()s and re-renders; otherwise this controller removes the
row (and the hidden input riding it - form mode serializes name[] per
tag).

**Events**: `poetry:tag-group:remove`

### #connect()

Wires the focus-scoped live-region toggling and reflects emptiness
(role, tab stop, data-empty).

### #disconnect()

Unwires the focus listeners.

### #keydown(event)

The container's keydown action: Delete/Backspace on a focused tag ROW
removes it - row-origin keys only (keys from a tag's inner remove
button must not drive the grid).

### #remove(event)

The remove button's click action: removes exactly its own tag.

## poetry--core--toast

One toast item (poetry's own Toast - the stacked-toaster genre with
strict a11y semantics). The item is role=status aria-live=off: it
never announces itself - on connect it speaks ONCE through the
announce singleton at its politeness (destructive -> assertive,
wired server-side via the
politeness value). The auto-dismiss timer follows APG/WCAG 2.2.1 timing:
it PAUSES on hover, focus-within, window blur and tab-hidden (reasons are
refcounted so overlapping pauses cannot resume early), and duration <= 0
means persistent (required for undo/action toasts). Dismiss flips
data-open -> data-closed, dispatches poetry:toast:dismiss {id, reason} (the
toaster's reflow + focus-return seam), then presence holds the node until
its exit animation finishes before removal.

Swipe-to-dismiss is a browser-verification-GATED enhancement (contract) -
it does not ship in this pass; the swipe reason is reserved.

**Targets**: `action`, `close`

**Values**: `duration` (Number, default: 5000); `politeness` (String, default: "polite")

**Events**: `poetry:toast:dismiss`, `poetry:toast:show`

### #connect()

Assigns an id when missing, adopts the open pair, announces ONCE
through the singleton, wires the pause sources and the toast-local
Escape, and starts the auto-dismiss timer (queued/hidden toasts hold
theirs).

### #disconnect()

Stops the timer and unwires every listener.

### #pause(eventOrReason)

Holds the auto-dismiss timer under a reason (hover/focus derive from
events; the toaster and window paths pass strings). Reasons pool in a
set, so overlapping pauses cannot resume early.

### #resume(eventOrReason)

Releases one pause reason; the timer restarts when none remain.

### #dismiss(eventOrReason)

Dismisses the toast. Reasons: timeout | close-press | action |
swipe(reserved) | manual - the family reason vocabulary plus
poetry's own timeout/action/queued/manual extensions. A click on the
action slot reports "action"; the close
button "close-press". The dismiss event goes out BEFORE removal (the
toaster's reflow + focus-return seam); presence then holds the node
through its exit animation.

## poetry--core--toast-trigger

The client-side toast delivery trigger (poetry's no-round-trip path -
what a toast() JS factory does elsewhere, done with server-rendered
markup): press -> dispatch poetry:toaster:stamp, and the toaster clones
the addressed <template>'s toast into its region. The toast inside the
template is byte-for-byte what a Turbo Stream would deliver.

**Values**: `template` (String); `toaster` (String)

**Events**: `poetry:toaster:stamp`

### #fire()

The click action: stamps the addressed template's toast into the
toaster region (the no-round-trip delivery the header describes).

## poetry--core--toaster

The toast viewport (one per page): the labeled role=region <ol> that is
the Turbo Stream append target (id=poetry-toaster, data-turbo-permanent).
It ACQUIRES the announce singleton for its lifetime (items announce
through it on connect), owns the F8 hotkey (focus moves to the most
recent toast; the prior focus is remembered so dismissal returns it),
enforces the visible LIMIT (default 3 - the oldest overflow toasts queue
hidden with their timers held, promoted as newer ones dismiss), and owns
the stack reflow (--poetry-toast-index, newest = 0, for the offset/scale
stack styling).

Items arrive by server render, Turbo Stream append, or a stamp (the
poetry:toaster:stamp window event - poetry_toast_trigger's no-round-trip
path, cloning a <template> toast into the region);
a childList MutationObserver reconciles limit + reflow on every change,
so no append path needs to know about the toaster.

**Targets**: `item`

**Values**: `hotkey` (String, default: "F8"); `limit` (Number, default: 3); `position` (String)

### #connect()

Acquires the announce singleton, wires the hotkey / stamp / dismiss
listeners, and starts the childList reconciler.

### #disconnect()

Unwires everything and releases the announce singleton.

### #focusRegion(event)

The hotkey's landing (F8 default, configurable): moves focus into the
region - onto the most recent visible toast, else the region itself.
The prior focus is remembered for the dismiss return.

### #reflow()

Stack reflow: newest toast = index 0 (the front of the stack),
written as the offset/scale custom property.

## poetry--core--toggle-group

The ToggleGroup value-set machine (the Accordion composition, second
consumer): this controller owns ONLY the pressed-values set + attribute
writes; the shared poetry--core--roving-focus on the same root owns the
keyboard (default tabindex-managing mode - one Tab stop). Items are DUMB
buttons (data-action -> group#toggle; poetry--core--pressed is NOT
attached in group context - one owner, no event soup).

The role/vocabulary split, enforced here: type=single is
RADIO semantics - items carry aria-checked (aria-pressed stripped) and
re-pressing the sole pressed item deselects to EMPTY;
type=multiple is toolbar semantics - independent aria-pressed items. The
controller reads type once and never mixes vocabularies; the bare
data-pressed presence boolean styles both types identically (Toggle's
classes just work).

After every transition the PRESSED item becomes the roving tab stop
(re-entering the group lands on the selection) -
written directly as the tabindex stamp roving-focus adopts.

**Values**: `type` (String, default: "single")

**Events**: `poetry:toggle-group:change`

### #connect()

Reconcile-on-connect: re-derives the type-correct aria vocabulary
from the server-rendered data-pressed truth, then prefers the pressed
tab stop.

### #toggle(event)

Each item's click action (Space/Enter arrive via native button
activation): single mode is radio semantics with deselect-to-empty;
multiple XORs the value in and out.

### #setValue(value)

The programmatic controllable-state surface: setValue("b") in single
mode ("" / null clears); setValue(["a", "b"]) in multiple mode.
Validated against type; unknown values are ignored (logged in dev -
the contract's guard).

## poetry--core--tooltip

The Tooltip controller (the popper-consumer trio's timing machine): open
DELAYS (provider delay_duration, default 0), the provider-scoped
WARM grace (one tooltip open - or closed less than skip_delay_duration ms
ago - lets siblings in the same provider scope open instantly), ONE OPEN
GLOBALLY (the document-level will-open event),
close-on-scroll, and the pointer-vs-focus open paths with their latches.

A11y is the strictest of the trio: the content is role=tooltip and the
trigger's aria-describedby is set on open and REMOVED on close
(describedby must never reference hidden content); focus-scope is NOT
composed at all - focus never enters a tooltip; touch NEVER opens one
(no long-press path, deliberately). Esc rides a token-activated
dismissable layer while open, so a tooltip above a Dialog peels first.

THE WARM REGISTRY (the provider mechanism): a module-level WeakMap keyed
by the [data-slot=tooltip-provider] ancestor (document fallback) holding
{openCount, warmUntil}. The DOM ancestor IS the provider scope; the
WeakMap lets morphed/replaced providers garbage-collect (no Turbo leaks).

**Values**: `open` (Boolean, default: false); `delayDuration` (Number, default: -1); `disableHoverableContent` (Boolean, default: false)

**Events**: `poetry:tooltip:closed`, `poetry:tooltip:open`

### #connect()

Wires the content listeners plus the document-level will-open
listener, and reconciles a server-pinned tooltip (describedby, layer,
scope count catch up; the DOM wins).

### #disconnect()

Clears the timers, balances the warm-scope count for an open tooltip,
drops the scroll listener, restores portaled content
(drop-never-strand), and unwires everything.

### #openValueChanged(value)

Stimulus value callback - controllable state: pinned tooltips (Turbo
Stream / Outlet ownership).

### #pointerMove(event)

The trigger's pointermove action (not pointerenter, deliberately):
opens
once per hover via the latch - instantly when the provider scope is
warm, else after the delay; touch pointerType is EXCLUDED entirely.

### #pointerLeave(event)

The trigger's pointerleave action: cancels a pending open; with
hoverable content the close waits its grace, else it closes now.

### #pointerDown()

The trigger's pointerdown action: activating the control dismisses
its hint; the latch also suppresses the focus-open until pointerup
(pointer users never get a focus-opened tooltip - contractual).

### #clickClose()

The trigger's click action: an activation dismisses its hint.

### #focusOpen()

The trigger's focus action: opens INSTANTLY (skipping all delays) -
unless the focus was caused by a pointerdown (the latch).

### #blurClose()

The trigger's blur action: closes (the focus interaction's exit).

## poetry--core--tree

The Tree engine (the flat-treegrid contract): the
server renders a FLAT list of role=row siblings - hierarchy lives
entirely in aria-level/posinset/setsize (static per render) - so this
controller owns only what HTML cannot: roving focus over VISIBLE rows,
the four-branch ArrowLeft/Right expansion logic (including
focus-to-parent), Enter-toggles-expandable (the no-action default),
typeahead, and subtree show/hide that preserves nested collapsed state
(a row is visible iff every ancestor is expanded).

Expansion state IS the DOM (aria-expanded + hidden); the host persists
it by listening for poetry:tree:toggle. Selection modes are deferred
(the TagGroup reasoning).

**Events**: `poetry:tree:toggle`

### #connect()

Applies ancestor-expansion visibility and settles the single tab
stop.

### #keydown(event)

The treegrid container's keydown action (row-origin keys only):
arrows rove the visible rows, the direction-aware expand/collapse
pair runs the four-branch logic (collapse on a leaf focuses the
parent), Home/End jump the edges, Enter toggles expandables, and
printable keys run typeahead.

### #press(event)

Each row's click action: toggles when expandable (the default press
behavior with no action/link/selection); clicks on inner controls
(links, the chevron) stay their own.

### #toggle(event)

The chevron's click action (the chevron rides tabindex -1; its
pointerdown is preventDefault'd via pressStart so focus never leaves
the row): toggles, then refocuses the row.

### #pressStart(event)

The chevron's pointerdown action: never steal focus from the row.