import { type MdyWidgetKind } from "./catalog.js"; /** What the user did. */ export type MdyTransitionTrigger = /** A pointer press on a part. */ { readonly type: "pointer"; readonly part: string; } | { readonly type: "key"; readonly key: string; } /** A pointer press anywhere outside the widget. */ | { readonly type: "outside"; }; /** Whether the widget's overlay is showing. The only lifecycle state a transition moves between. */ export type MdyOverlayPhase = "open" | "closed"; export interface MdyWidgetTransition { readonly from: MdyOverlayPhase; readonly trigger: MdyTransitionTrigger; readonly to: MdyOverlayPhase; /** * Whether closing returns focus to the opener. * * Only the *deliberate* dismissals restore it. A pointer landing outside moved the user's * attention somewhere themselves, and pulling focus back from wherever they clicked is the * behaviour that makes an overlay feel like a trap. */ readonly restoresFocus?: boolean; } /** * A disabled widget does not transition at all. * * Stated as data rather than left implicit: "nothing happens" is a behaviour a renderer has to * implement, and a disabled trigger that still opens its popup is a real defect that looks like no * transition being declared. */ export declare const MDY_DISABLED_BLOCKS_TRANSITIONS = true; /** Every transition each kind admits. A kind with no overlay declares none — yet. */ export declare const MDY_WIDGET_TRANSITIONS: Readonly>; /** The transitions a kind admits from a given phase. */ export declare function transitionsFrom(kind: MdyWidgetKind, phase: MdyOverlayPhase): readonly MdyWidgetTransition[]; /** * What a key does, per kind. * * `widgetKeyIntent` answered this for years without asking which widget it was looking at: every * kind but `number` was told ArrowDown means "move to the next option", so a text field navigated a * list it does not have, a textarea was told Enter opens something, and a slider — whose arrows must * change its value — was given list navigation instead of a step. * * What a key may do follows from what the kind *is*: a widget with options navigates them, one with * a range steps it, one with two states toggles, one with an overlay opens and closes it. Declaring * that per kind is what stops the answer from being the same everywhere. */ /** * A binding that answers *any* printable character rather than one named key. * * Type-ahead is the gesture every list has and no single key names: typing `r` moves to the first * option beginning with r. Declared as a key it would have to pick one letter and mean all of them — * a table that says one thing and intends another, which a tool reads literally and a person reads * charitably. This is the key field admitting it has no key. */ export declare const MDY_ANY_PRINTABLE_KEY = "*printable*"; export interface MdyKeyBinding { /** A named key, or {@link MDY_ANY_PRINTABLE_KEY} where the gesture is "type a character". */ readonly key: string; /** Only when the overlay is showing, only when it is not, or either way. */ readonly when?: MdyOverlayPhase; readonly intent: "move" | "step" | "toggle" | "open" | "commit" | "cancel" | "reorder" | "remove" | "grab" | "undo" | "typeahead"; /** * Which way a `reorder` goes: `-1` earlier in the value, `1` later. * * Declared rather than derived from the key, because "earlier" is not "left": the strip runs in * the writing direction, so in a right-to-left document `ArrowLeft` moves a chip *later*. A * renderer that read the key would have to know that; reading the direction, it does not. */ readonly by?: -1 | 1; /** * Move by a *page* rather than by one place: the next month of a calendar, not the next day. * * Separate from `by` rather than a third value in it, because the two compose — a page key carries * a direction like any other movement, and a reader asking "which way" gets the same answer from * both kinds of move. */ readonly page?: boolean; /** Whether a `move` goes as far as it can rather than one place — `Home` and `End`. */ readonly toEnd?: boolean; /** * Which part answers this key, where it is not the control. * * `ArrowDown` opens a closed multiselect and steps the quantity on a focused counter chip: one * key, two meanings, decided by where a person is rather than by which binding was declared first. * Without this the table could only say *this kind answers this key*, and the second declaration * was unreachable — a binding that exists and can never be resolved. */ readonly on?: string; /** * Whether dismissing on this key returns focus to the opener. Only a `cancel` dismisses, so only a * `cancel` answers it, and the default is to restore. * * The two dismissals differ, and the difference is the whole reason this is declared rather than * assumed. Escape means *put me back where I was*, so focus returns to the opener. Tab is already * carrying focus to the next control, and pulling it back would trap the user in the field they * just left — the same key, the same close, the opposite answer. */ readonly restoresFocus?: boolean; /** * A field-level capability this binding depends on, where the kind alone does not decide it. * * `on` says *which part answers a key*; this says *whether the key exists at all* for this field. * Reordering a chosen value is the case that forced it: every multiselect has chips, and only one * declared `reorderable` has an order a person may change — so the two `Alt`+arrow bindings belong * to the kind and not to every field of it. * * Without it the table said a kind answers four keys that a default field answers none of, and * anything reading it across kinds — a sweep, a help panel, a consumer's own handler — had to * carry its own list of which. A capability named here is one it can ask the field about. */ readonly requires?: string; /** * A transient state the field must already be in for this key to do anything. * * `requires` names a **capability** — something a document turned on, true for the life of the * field. This names something that has **happened**, and stops being true again: the way back acts * on the last destructive change, so before one there is nothing for it to put back and the key * correctly does nothing. * * Declared because from outside the two are indistinguishable. A key that does nothing because the * moment is wrong looks exactly like a key nobody implemented, and a sweep over a plainly declared * control finds every such key and reports it as unanswered — which is how a table this project * already fixed once came to say a kind answers four keys that a default field answers none of. * Named here, a check can arrange the state before pressing, or count the key as unreached rather * than unanswered, and a help panel can say *when* the key applies instead of promising it always. * * The value is the state as this kind's own reader names it, so the field can be asked. */ readonly awaits?: string; /** * The held key this binding needs, where the gesture is a shortcut rather than a bare press. * * `"primary"` is the platform's own accelerator — `Cmd` where the platform uses it, `Ctrl` * everywhere else — and it is named that way because the two are not interchangeable: `Ctrl`+Z on * a Mac is not the undo gesture, and a table spelling both would declare a key half its readers * must ignore. `matchesKeyGesture` resolves it against an event, so the platform test is made once * rather than in each renderer. * * Absent means *bare*, and that is a refusal as much as a requirement: a binding that opens a * panel or commits a value does not answer a press with the accelerator held, because both of * those **add** something the person did not ask for, and the press may have been aimed at the * platform — `Cmd`+Space switches the input source, `Cmd`+ArrowDown reaches the end of a document. * * `"any"` is for the gestures that **remove**. Answering one wrongly costs a reopen; refusing one * wrongly leaves somebody inside a panel with the way out not working, and that is a keyboard trap * — the one class of defect with no exception to argue about. `Escape` in particular is the key a * control does not get to reinterpret: its meaning is *stop*, no modifier changes that on any * platform, and where the system claims a modified `Escape` it takes it before the page sees it. */ readonly modifier?: "primary" | "any"; } /** Every key each kind answers to. A kind that answers to none declares none. */ export declare const MDY_WIDGET_KEYBOARD: Readonly>; /** What this kind does with this key in this phase, or `null` if it does not claim it. */ /** * A key, or the press of one. * * A renderer asking "does this kind declare anything for Tab" has a literal and no press. A renderer * deciding what somebody just did has the event, and the two are not the same question: a binding * declared without a modifier does not answer a press with one held, and a caller that flattens the * press to `event.key` throws away the only fact that distinguishes them. * * Both spellings are accepted so the call site says which question it is asking, and so the string * form keeps meaning what it always meant. */ export type MdyKeyOrPress = string | { readonly key: string; readonly ctrlKey?: boolean; readonly metaKey?: boolean; readonly altKey?: boolean; readonly shiftKey?: boolean; }; export declare function keyBindingFor(kind: MdyWidgetKind, key: MdyKeyOrPress, open: boolean, /** * Where the person is. Absent means the control, which is what answers a key by default. * * A binding declared `on` another part is invisible from the control and the only answer from * that part, so one key can mean two things without either declaration shadowing the other. */ on?: string): MdyKeyBinding | null; /** * Whether a keyboard event is the gesture a binding declares. * * The platform test lives here and not in a renderer, because "the primary modifier" is one fact * about the machine and three copies of it drift the moment one is written from memory. It is read * from the event rather than from a user-agent string: `metaKey` is the accelerator where the * platform uses it and `ctrlKey` where it does not, and an event carries both — so a gesture is * recognised by what was actually held rather than by what the platform was guessed to be. * * Both are accepted, and that is deliberate rather than lax. A person on a Mac with a keyboard * mapped for another platform, or a remote session, holds the one their muscle memory knows; the * cost of accepting the other is a shortcut firing on a combination nothing else on the page claims. * * A binding with no modifier is a bare press, and a bare press is not this gesture: `z` alone must * not undo, or typing into a field beside the control would. */ export declare function matchesKeyGesture(binding: MdyKeyBinding, event: { readonly key: string; readonly ctrlKey?: boolean; readonly metaKey?: boolean; readonly altKey?: boolean; readonly shiftKey?: boolean; }): boolean; /** * How a control is operated, in one sentence, derived from the table above. * * Nothing else on a page says the key map exists. It is discoverable by guessing — the outside view's * largest finding — and a person who does not guess has a control they can see and cannot operate. * * Derived rather than written beside the table, because a sentence naming keys *is* a copy of the key * map: it goes stale the moment a binding moves, and this project has now found that shape five times * in five places. Derived, "it holds for every consumer" is true by construction rather than by * discipline. * * What it leaves out is as deliberate as what it says: * * - **Keys that need a capability the field did not ask for.** `reorderable` is opt-in, and a legend * listing a key that does nothing on this control is worse than no legend — it was the likeliest * reading of "reordering does not work". * - **Keys answered on a part that is not there.** A chip's keys mean nothing to a field holding no * chips. * - **The keys every control shares.** Tab moves between fields everywhere; a sentence that says so * on each of them is noise a reader learns to skip, and the ones that matter are inside it. */ export declare function widgetKeyGuide(kind: MdyWidgetKind, messages: { readonly keyGuideOpen: string; readonly keyGuideMove: string; readonly keyGuideStep: string; readonly keyGuideToggle: string; readonly keyGuideCommit: string; readonly keyGuideCancel: string; readonly keyGuideRemove: string; readonly keyGuideGrab: string; readonly keyGuideOr: string; readonly keyGuideJoin: string; readonly keyGuideSpace: string; }, options?: { /** The capabilities this field asked for; a key that needs one it did not ask for is left out. */ readonly capabilities?: readonly string[]; /** * The parts this control drew, where the caller knows. * * A key answered on a part that is not there is left out — a chip's keys mean nothing to a field * holding no chips. The part a person opens this kind with is always counted: it is the control, * and a control that is not drawn is not a control. */ readonly parts?: readonly string[]; /** * Which state to describe. A closed control's keys and an open one's are different sets, and a * sentence holding both says `ArrowDown` opens the list and moves through it at once. */ readonly open?: boolean; }): string;