@mythicalos/ui-core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -0
- package/dist/index.js +27 -0
- package/dist/logic/file-explorer.d.ts +374 -0
- package/dist/logic/file-explorer.js +624 -0
- package/dist/logic/git-chip.d.ts +79 -0
- package/dist/logic/git-chip.js +118 -0
- package/dist/logic/popover.d.ts +242 -0
- package/dist/logic/popover.js +303 -0
- package/dist/logic/queue.d.ts +142 -0
- package/dist/logic/queue.js +143 -0
- package/dist/logic/save-bar.d.ts +39 -0
- package/dist/logic/save-bar.js +54 -0
- package/dist/logic/sendbar.d.ts +105 -0
- package/dist/logic/sendbar.js +136 -0
- package/dist/logic/session-card.d.ts +263 -0
- package/dist/logic/session-card.js +443 -0
- package/dist/logic/stat-tiles.d.ts +49 -0
- package/dist/logic/stat-tiles.js +102 -0
- package/dist/logic/terminal.d.ts +222 -0
- package/dist/logic/terminal.js +264 -0
- package/package.json +1 -1
- package/src/select/mythical-select.js +5 -1
- package/styles.css +750 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Flag tone. Per the design card: warn = behind / uncommitted, error = unpushed ("a real error
|
|
2
|
+
* state"), ok = clean. */
|
|
3
|
+
export type GitFlagTone = "warn" | "error" | "ok";
|
|
4
|
+
export interface GitFlag {
|
|
5
|
+
label: string;
|
|
6
|
+
tone: GitFlagTone;
|
|
7
|
+
}
|
|
8
|
+
/** A reported git status. Every field is REQUIRED so a caller cannot half-report a tree and get a
|
|
9
|
+
* confident render — but each is also runtime-guarded, because a JS consumer can still pass
|
|
10
|
+
* `undefined`, and "not reported" must degrade to honest silence, never to a fabricated zero. */
|
|
11
|
+
export interface GitStatus {
|
|
12
|
+
/** Branch name, or `null` for a detached HEAD. */
|
|
13
|
+
branch: string | null;
|
|
14
|
+
/** Commits behind upstream. `null` ⇒ there is NO upstream — the flag is omitted, not zeroed. */
|
|
15
|
+
behind: number | null;
|
|
16
|
+
/** Uncommitted working-tree changes. */
|
|
17
|
+
uncommitted: number;
|
|
18
|
+
/** Commits not pushed to upstream. `null` ⇒ there is NO upstream — the flag is omitted. */
|
|
19
|
+
unpushed: number | null;
|
|
20
|
+
}
|
|
21
|
+
/** The branch glyph the card prefixes the name with. */
|
|
22
|
+
export declare const GIT_BRANCH_GLYPH = "\u2387";
|
|
23
|
+
/** Shown in place of a branch name when there is no status to show at all. */
|
|
24
|
+
export declare const GIT_BRANCH_UNKNOWN = "\u2014";
|
|
25
|
+
/** A `null` branch is a real, nameable state — never a blank. */
|
|
26
|
+
export declare const GIT_DETACHED_LABEL = "detached HEAD";
|
|
27
|
+
/** The all-clear flag (token rule #7: the ✓ glyph rides along with the ok-soft color). */
|
|
28
|
+
export declare const GIT_CLEAN_LABEL = "clean \u2713";
|
|
29
|
+
/** Default copy while the first status read is in flight. */
|
|
30
|
+
export declare const GIT_LOADING_NOTE = "Checking repository status\u2026";
|
|
31
|
+
/** Default copy when no status and no reason were supplied — deliberately says nothing about the
|
|
32
|
+
* tree's actual state. */
|
|
33
|
+
export declare const GIT_UNAVAILABLE_NOTE = "Repository status is unavailable.";
|
|
34
|
+
/** Marker for a RETAINED status shown while the live read is failing: what is on screen is the last
|
|
35
|
+
* received status, not the current one. */
|
|
36
|
+
export declare const GIT_STALE_LABEL = "\u00B7 stale";
|
|
37
|
+
export declare const GIT_STALE_TITLE = "the last read failed \u2014 showing the last received status";
|
|
38
|
+
/** True when a status object was actually supplied. A JS caller can hand `null` where the type says
|
|
39
|
+
* `GitStatus | undefined` — `null` means "no status", so it must take the honest unavailable arm
|
|
40
|
+
* rather than crash on a field read. Both bindings gate on this. */
|
|
41
|
+
export declare function hasGitStatus(status: GitStatus | null | undefined): status is GitStatus;
|
|
42
|
+
/** Branch label: the real name, or the honest "detached HEAD" for `null`. An unreported branch
|
|
43
|
+
* (`undefined`, or any non-string) falls back to the neutral `—` rather than inventing a checkout
|
|
44
|
+
* state. */
|
|
45
|
+
export declare function gitBranchLabel(branch: string | null | undefined): string;
|
|
46
|
+
/** Counter flags in design order (behind · uncommitted · unpushed), collapsing to a single
|
|
47
|
+
* `clean ✓` when the tree is genuinely all-clear.
|
|
48
|
+
*
|
|
49
|
+
* The clean claim is guarded: it is made ONLY when `uncommitted` was reported as 0 AND both
|
|
50
|
+
* upstream counters were reported at all — `null` counts as reported (it is the explicit "no
|
|
51
|
+
* upstream" answer), `undefined` does not. A status that never reported its counters therefore
|
|
52
|
+
* yields NO flags rather than a green "clean ✓" the caller never earned. */
|
|
53
|
+
export declare function gitFlags(status: GitStatus): GitFlag[];
|
|
54
|
+
export interface GitChipState {
|
|
55
|
+
/** No status to show — the chip renders its note instead of flags. */
|
|
56
|
+
unavailable?: boolean;
|
|
57
|
+
/** The shown status is retained, not current. */
|
|
58
|
+
stale?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Every class the chip renders, root and elements — the bindings import these rather than
|
|
61
|
+
* spelling the strings themselves, so the two frameworks cannot drift apart on a rename. */
|
|
62
|
+
export declare const GIT_CHIP_PARTS: {
|
|
63
|
+
readonly root: "my-gitchip";
|
|
64
|
+
readonly branch: "my-gitchip__branch";
|
|
65
|
+
readonly flags: "my-gitchip__flags";
|
|
66
|
+
readonly flag: "my-gitchip__flag";
|
|
67
|
+
readonly note: "my-gitchip__note";
|
|
68
|
+
readonly stale: "my-gitchip__stale";
|
|
69
|
+
};
|
|
70
|
+
/** Root class: base + the unavailable/stale modifiers. */
|
|
71
|
+
export declare function gitChipClass(state?: GitChipState): string;
|
|
72
|
+
/** Flag badge class: base + tone modifier (always present — the tone IS the flag's meaning). */
|
|
73
|
+
export declare function gitFlagClass(tone: GitFlagTone): string;
|
|
74
|
+
/** The unavailable arm's sentence: a caller-supplied reason wins; else the loading line while the
|
|
75
|
+
* first read is in flight; else the neutral default. Never describes the tree. */
|
|
76
|
+
export declare function gitChipNote(o?: {
|
|
77
|
+
unavailableNote?: string;
|
|
78
|
+
loading?: boolean;
|
|
79
|
+
}): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @mythicalos/ui-core — the git status chip's flag derivation, branch label, copy and class
|
|
2
|
+
// derivation (ds/components-git-chip.html: a bordered inline mono chip — `⎇ <branch>` plus honest
|
|
3
|
+
// counter flags; warn-soft for behind/uncommitted, error-soft for unpushed, ok-soft for a clean
|
|
4
|
+
// tree — "never red unless something is actually wrong").
|
|
5
|
+
//
|
|
6
|
+
// Extracted from the reference implementation in the reference product's session detail pane. Two
|
|
7
|
+
// things were product-shaped there and are lifted out:
|
|
8
|
+
// 1. the `available:false` REASON VOCABULARY (`no_worktree`, `uid_isolation_unsupported`,
|
|
9
|
+
// `session_terminal`, `slugless`, …) and its copy table — those are one daemon's wire enum.
|
|
10
|
+
// The atom keeps the STATE and takes the human sentence as `unavailableNote`, so each product
|
|
11
|
+
// maps its own reason codes to copy and the atom still renders the honest arm.
|
|
12
|
+
// 2. the session view-model envelope (`GitVm`) — replaced by the plain `GitStatus` below.
|
|
13
|
+
//
|
|
14
|
+
// What is DESIGN and therefore stays (the honesty contract — see gitFlags):
|
|
15
|
+
// - `branch: null` is a DETACHED HEAD, rendered as such, never blank,
|
|
16
|
+
// - `behind`/`unpushed` of `null` means NO UPSTREAM: that flag is OMITTED, never rendered "0
|
|
17
|
+
// behind" (a `0` would claim an upstream comparison that never happened),
|
|
18
|
+
// - an absent/unreported status is NEVER coerced into a zero-count "clean ✓" row — the chip
|
|
19
|
+
// renders its unavailable arm (or, for a partially reported status, simply no flags).
|
|
20
|
+
/** The branch glyph the card prefixes the name with. */
|
|
21
|
+
export const GIT_BRANCH_GLYPH = "⎇";
|
|
22
|
+
/** Shown in place of a branch name when there is no status to show at all. */
|
|
23
|
+
export const GIT_BRANCH_UNKNOWN = "—";
|
|
24
|
+
/** A `null` branch is a real, nameable state — never a blank. */
|
|
25
|
+
export const GIT_DETACHED_LABEL = "detached HEAD";
|
|
26
|
+
/** The all-clear flag (token rule #7: the ✓ glyph rides along with the ok-soft color). */
|
|
27
|
+
export const GIT_CLEAN_LABEL = "clean ✓";
|
|
28
|
+
/** Default copy while the first status read is in flight. */
|
|
29
|
+
export const GIT_LOADING_NOTE = "Checking repository status…";
|
|
30
|
+
/** Default copy when no status and no reason were supplied — deliberately says nothing about the
|
|
31
|
+
* tree's actual state. */
|
|
32
|
+
export const GIT_UNAVAILABLE_NOTE = "Repository status is unavailable.";
|
|
33
|
+
/** Marker for a RETAINED status shown while the live read is failing: what is on screen is the last
|
|
34
|
+
* received status, not the current one. */
|
|
35
|
+
export const GIT_STALE_LABEL = "· stale";
|
|
36
|
+
export const GIT_STALE_TITLE = "the last read failed — showing the last received status";
|
|
37
|
+
/** A real, non-negative INTEGER commit/file count. `null` (no upstream), `undefined` (not
|
|
38
|
+
* reported), `NaN`, negatives and fractions are all "not a count" — a fractional value cannot be a
|
|
39
|
+
* number of commits, so it is malformed data and must not be rendered as one ("1.5↓ behind"). */
|
|
40
|
+
function isCount(n) {
|
|
41
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0;
|
|
42
|
+
}
|
|
43
|
+
/** True when a status object was actually supplied. A JS caller can hand `null` where the type says
|
|
44
|
+
* `GitStatus | undefined` — `null` means "no status", so it must take the honest unavailable arm
|
|
45
|
+
* rather than crash on a field read. Both bindings gate on this. */
|
|
46
|
+
export function hasGitStatus(status) {
|
|
47
|
+
return typeof status === "object" && status !== null;
|
|
48
|
+
}
|
|
49
|
+
/** Branch label: the real name, or the honest "detached HEAD" for `null`. An unreported branch
|
|
50
|
+
* (`undefined`, or any non-string) falls back to the neutral `—` rather than inventing a checkout
|
|
51
|
+
* state. */
|
|
52
|
+
export function gitBranchLabel(branch) {
|
|
53
|
+
if (branch === null)
|
|
54
|
+
return GIT_DETACHED_LABEL;
|
|
55
|
+
if (typeof branch !== "string")
|
|
56
|
+
return GIT_BRANCH_UNKNOWN;
|
|
57
|
+
// A git ref name cannot contain whitespace, so blank-or-whitespace is the same malformed input as
|
|
58
|
+
// `""` — it must not leave the branch slot visibly empty next to a confident status.
|
|
59
|
+
return branch.trim().length > 0 ? branch : GIT_DETACHED_LABEL;
|
|
60
|
+
}
|
|
61
|
+
/** Counter flags in design order (behind · uncommitted · unpushed), collapsing to a single
|
|
62
|
+
* `clean ✓` when the tree is genuinely all-clear.
|
|
63
|
+
*
|
|
64
|
+
* The clean claim is guarded: it is made ONLY when `uncommitted` was reported as 0 AND both
|
|
65
|
+
* upstream counters were reported at all — `null` counts as reported (it is the explicit "no
|
|
66
|
+
* upstream" answer), `undefined` does not. A status that never reported its counters therefore
|
|
67
|
+
* yields NO flags rather than a green "clean ✓" the caller never earned. */
|
|
68
|
+
export function gitFlags(status) {
|
|
69
|
+
const behind = status?.behind;
|
|
70
|
+
const uncommitted = status?.uncommitted;
|
|
71
|
+
const unpushed = status?.unpushed;
|
|
72
|
+
const flags = [];
|
|
73
|
+
if (isCount(behind) && behind > 0)
|
|
74
|
+
flags.push({ label: `${behind}↓ behind`, tone: "warn" });
|
|
75
|
+
if (isCount(uncommitted) && uncommitted > 0) {
|
|
76
|
+
flags.push({ label: `${uncommitted} uncommitted`, tone: "warn" });
|
|
77
|
+
}
|
|
78
|
+
if (isCount(unpushed) && unpushed > 0)
|
|
79
|
+
flags.push({ label: `${unpushed} unpushed`, tone: "error" });
|
|
80
|
+
if (flags.length > 0)
|
|
81
|
+
return flags;
|
|
82
|
+
const reported = (n) => n === null || isCount(n);
|
|
83
|
+
const clean = isCount(uncommitted) && uncommitted === 0 && reported(behind) && reported(unpushed);
|
|
84
|
+
return clean ? [{ label: GIT_CLEAN_LABEL, tone: "ok" }] : [];
|
|
85
|
+
}
|
|
86
|
+
/** Every class the chip renders, root and elements — the bindings import these rather than
|
|
87
|
+
* spelling the strings themselves, so the two frameworks cannot drift apart on a rename. */
|
|
88
|
+
export const GIT_CHIP_PARTS = {
|
|
89
|
+
root: "my-gitchip",
|
|
90
|
+
branch: "my-gitchip__branch",
|
|
91
|
+
flags: "my-gitchip__flags",
|
|
92
|
+
flag: "my-gitchip__flag",
|
|
93
|
+
note: "my-gitchip__note",
|
|
94
|
+
stale: "my-gitchip__stale",
|
|
95
|
+
};
|
|
96
|
+
/** Root class: base + the unavailable/stale modifiers. */
|
|
97
|
+
export function gitChipClass(state = {}) {
|
|
98
|
+
const base = GIT_CHIP_PARTS.root;
|
|
99
|
+
let cls = base;
|
|
100
|
+
if (state.unavailable === true)
|
|
101
|
+
cls += ` ${base}--unavailable`;
|
|
102
|
+
if (state.stale === true)
|
|
103
|
+
cls += ` ${base}--stale`;
|
|
104
|
+
return cls;
|
|
105
|
+
}
|
|
106
|
+
/** Flag badge class: base + tone modifier (always present — the tone IS the flag's meaning). */
|
|
107
|
+
export function gitFlagClass(tone) {
|
|
108
|
+
const base = GIT_CHIP_PARTS.flag;
|
|
109
|
+
return `${base} ${base}--${tone}`;
|
|
110
|
+
}
|
|
111
|
+
/** The unavailable arm's sentence: a caller-supplied reason wins; else the loading line while the
|
|
112
|
+
* first read is in flight; else the neutral default. Never describes the tree. */
|
|
113
|
+
export function gitChipNote(o = {}) {
|
|
114
|
+
const note = o.unavailableNote;
|
|
115
|
+
if (typeof note === "string" && note.trim().length > 0)
|
|
116
|
+
return note;
|
|
117
|
+
return o.loading === true ? GIT_LOADING_NOTE : GIT_UNAVAILABLE_NOTE;
|
|
118
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/** Trigger caret (design card: `<span class="car">▾</span>`). */
|
|
2
|
+
export declare const POPOVER_CARET = "\u25BE";
|
|
3
|
+
/** Selected-item marker (design card: "✓ marks current"). */
|
|
4
|
+
export declare const POPOVER_CHECK = "\u2713";
|
|
5
|
+
/** Anchor gap in px — the design card's `top: calc(100% + 6px)`. Mirrored in styles.css. */
|
|
6
|
+
export declare const POPOVER_GAP_PX = 6;
|
|
7
|
+
/** Extra breathing room the flip test demands beyond the gap (mythical-select uses `height + 8`
|
|
8
|
+
* for a 6px gap — i.e. gap + 2). */
|
|
9
|
+
export declare const POPOVER_BREATHING_PX = 2;
|
|
10
|
+
/** Trigger value shown when nothing is selected (the em-dash placeholder, as in the product's
|
|
11
|
+
* hand-rolled selector) — never a fabricated item label. */
|
|
12
|
+
export declare const POPOVER_EMPTY_VALUE = "\u2014";
|
|
13
|
+
/** One row of the popover's single-select menu. */
|
|
14
|
+
export interface PopoverItem {
|
|
15
|
+
/** Stable identity handed back to `onSelect` — never the array index. */
|
|
16
|
+
key: string;
|
|
17
|
+
label: string;
|
|
18
|
+
/** Marks the current choice: gets the ✓ and `aria-checked="true"`. */
|
|
19
|
+
selected?: boolean;
|
|
20
|
+
/** Rendered, focusable-by-arrow? No — skipped by roving focus and inert on click. */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type PopoverPlacement = "below" | "above";
|
|
24
|
+
export type PopoverAlign = "start" | "end";
|
|
25
|
+
export interface PopoverPosition {
|
|
26
|
+
placement: PopoverPlacement;
|
|
27
|
+
align: PopoverAlign;
|
|
28
|
+
}
|
|
29
|
+
/** The pre-measurement position: the design card's own (anchored below, left-aligned). A binding
|
|
30
|
+
* renders this on the first open frame, then corrects it once the panel can be measured. */
|
|
31
|
+
export declare const POPOVER_DEFAULT_POSITION: PopoverPosition;
|
|
32
|
+
/** The subset of a DOMRect this module needs (so callers can pass a real rect or a plain object). */
|
|
33
|
+
export interface PopoverRect {
|
|
34
|
+
top: number;
|
|
35
|
+
bottom: number;
|
|
36
|
+
left: number;
|
|
37
|
+
right: number;
|
|
38
|
+
}
|
|
39
|
+
export interface PopoverSize {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
43
|
+
export interface PopoverViewport {
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Viewport-aware vertical placement. Flips UP only when the panel would clip off the bottom AND it
|
|
49
|
+
* genuinely fits above — a panel taller than both gaps stays `below` (scrolling down to it beats
|
|
50
|
+
* clipping it against the top edge, and it matches <mythical-select>'s behaviour exactly).
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolvePopoverPlacement(anchor: PopoverRect, panelHeight: number, viewport: PopoverViewport, gap?: number): PopoverPlacement;
|
|
53
|
+
/**
|
|
54
|
+
* Viewport-aware horizontal alignment. The panel is start-aligned (design card: `left: 0`) and
|
|
55
|
+
* switches to end-aligned only when start does NOT fit and end does.
|
|
56
|
+
*
|
|
57
|
+
* "Fits" means BOTH resulting edges are inside the viewport, on both branches — an alignment pins
|
|
58
|
+
* one panel edge to the matching anchor edge, so an anchor that is itself partly off-screen drags
|
|
59
|
+
* the panel off with it. Testing only the far edge is wrong in each direction symmetrically:
|
|
60
|
+
* start-aligning an anchor hanging off the LEFT clips the panel's left edge even though its right
|
|
61
|
+
* edge is comfortable, and end-aligning an anchor hanging off the RIGHT clips its right edge even
|
|
62
|
+
* though `right - panelWidth >= 0`. A panel that fits neither way stays `start` — flipping would
|
|
63
|
+
* only trade one overflow for the other.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolvePopoverAlign(anchor: PopoverRect, panelWidth: number, viewport: PopoverViewport): PopoverAlign;
|
|
66
|
+
/** Both axes at once — what a binding calls from its post-open measurement effect. */
|
|
67
|
+
export declare function resolvePopoverPosition(anchor: PopoverRect, panel: PopoverSize, viewport: PopoverViewport, gap?: number): PopoverPosition;
|
|
68
|
+
/** True when two positions describe the same placement — lets a binding skip a redundant
|
|
69
|
+
* re-render after re-measuring (and, more importantly, not loop measure→setState→measure). */
|
|
70
|
+
export declare function samePopoverPosition(a: PopoverPosition, b: PopoverPosition): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* EVERY class name this component family renders — the derived ones the functions below compose
|
|
73
|
+
* AND the purely structural ones a binding puts on a wrapper/span. One source, so a binding cannot
|
|
74
|
+
* invent, misspell, or quietly re-style a class, and the Preact and React siblings cannot drift on
|
|
75
|
+
* markup either. (The rest of this package's atoms inline their structural classes in both
|
|
76
|
+
* bindings; the popover has more of them than any other atom, so it earns the map.)
|
|
77
|
+
*/
|
|
78
|
+
export declare const POPOVER_CLASS: {
|
|
79
|
+
readonly anchor: "my-pop-anchor";
|
|
80
|
+
readonly trigger: "my-pop-trigger";
|
|
81
|
+
readonly triggerLabel: "my-pop-trigger__label";
|
|
82
|
+
readonly triggerValue: "my-pop-trigger__value";
|
|
83
|
+
readonly triggerCaret: "my-pop-trigger__caret";
|
|
84
|
+
readonly panel: "my-pop";
|
|
85
|
+
readonly panelAbove: "my-pop--above";
|
|
86
|
+
readonly panelEnd: "my-pop--end";
|
|
87
|
+
readonly head: "my-pop__head";
|
|
88
|
+
readonly title: "my-pop__title";
|
|
89
|
+
readonly caption: "my-pop__caption";
|
|
90
|
+
readonly menu: "my-pop__menu";
|
|
91
|
+
readonly item: "my-pop__item";
|
|
92
|
+
readonly itemLabel: "my-pop__label";
|
|
93
|
+
readonly itemCheck: "my-pop__check";
|
|
94
|
+
readonly divider: "my-pop__div";
|
|
95
|
+
readonly foot: "my-pop__foot";
|
|
96
|
+
readonly isOpen: "is-open";
|
|
97
|
+
readonly isSelected: "is-selected";
|
|
98
|
+
readonly isDisabled: "is-disabled";
|
|
99
|
+
};
|
|
100
|
+
export interface PopoverTriggerState {
|
|
101
|
+
open?: boolean;
|
|
102
|
+
disabled?: boolean;
|
|
103
|
+
}
|
|
104
|
+
/** Trigger class list. Shape rule 10: the trigger is clickable, so styles.css gives it
|
|
105
|
+
* `--my-r-control` — never the pill radius. */
|
|
106
|
+
export declare function popoverTriggerClass(s?: PopoverTriggerState): string;
|
|
107
|
+
/** Panel class list; the flip/align modifiers are the only thing position changes. */
|
|
108
|
+
export declare function popoverPanelClass(pos?: PopoverPosition): string;
|
|
109
|
+
/** Menu-row class list. */
|
|
110
|
+
export declare function popoverItemClass(item?: Pick<PopoverItem, "selected" | "disabled">): string;
|
|
111
|
+
export interface PopoverIds {
|
|
112
|
+
trigger: string;
|
|
113
|
+
/** The visual panel — the surface that carries the border/shadow and the optional head/footer. */
|
|
114
|
+
panel: string;
|
|
115
|
+
/** The `role="menu"` element NESTED in the panel. A menu may only own menuitem-family, group and
|
|
116
|
+
* separator children, so the heading and the caller's footer are siblings of this element, not
|
|
117
|
+
* children of it — putting them inside would make the menu's owned content invalid and let
|
|
118
|
+
* assistive tech drop or mis-announce them. */
|
|
119
|
+
menu: string;
|
|
120
|
+
/** The heading, when there is one — it names the menu in place of the trigger. */
|
|
121
|
+
title: string;
|
|
122
|
+
}
|
|
123
|
+
/** Stable, collision-free id set derived from one caller-supplied base id. */
|
|
124
|
+
export declare function popoverIds(baseId: string): PopoverIds;
|
|
125
|
+
export interface PopoverTriggerAria {
|
|
126
|
+
id: string;
|
|
127
|
+
"aria-haspopup": "menu";
|
|
128
|
+
"aria-expanded": "true" | "false";
|
|
129
|
+
/** The MENU (not the panel wrapper) it controls, and only while open — the element does not
|
|
130
|
+
* exist when closed, and pointing at a missing id is an authoring error assistive tech reports. */
|
|
131
|
+
"aria-controls": string | undefined;
|
|
132
|
+
}
|
|
133
|
+
export declare function popoverTriggerAria(open: boolean, ids: PopoverIds): PopoverTriggerAria;
|
|
134
|
+
export interface PopoverPanelAria {
|
|
135
|
+
id: string;
|
|
136
|
+
}
|
|
137
|
+
/** The visual panel: the surface, deliberately WITHOUT a role. It holds the optional heading and
|
|
138
|
+
* the caller's footer, which a `menu` may not own. */
|
|
139
|
+
export declare function popoverPanelAria(ids: PopoverIds): PopoverPanelAria;
|
|
140
|
+
export interface PopoverMenuAria {
|
|
141
|
+
id: string;
|
|
142
|
+
role: "menu";
|
|
143
|
+
"aria-labelledby": string;
|
|
144
|
+
}
|
|
145
|
+
/** The nested menu, which owns ONLY the `menuitemradio` rows. Named by the heading when there is
|
|
146
|
+
* one (the more specific label), otherwise by the trigger. */
|
|
147
|
+
export declare function popoverMenuAria(ids: PopoverIds, titled: boolean): PopoverMenuAria;
|
|
148
|
+
/**
|
|
149
|
+
* The caret and the ✓ are pure decoration: the trigger's `aria-expanded` and the row's
|
|
150
|
+
* `aria-checked` already convey both states, so reading the glyphs would duplicate them. Lives here
|
|
151
|
+
* rather than inline in each binding so neither can quietly un-hide one.
|
|
152
|
+
*/
|
|
153
|
+
export declare const POPOVER_DECORATIVE_ARIA: {
|
|
154
|
+
readonly "aria-hidden": "true";
|
|
155
|
+
};
|
|
156
|
+
/** The rule between the menu and the caller's footer — a real separator, not a styled `<div>`. */
|
|
157
|
+
export declare const POPOVER_SEPARATOR_ARIA: {
|
|
158
|
+
readonly role: "separator";
|
|
159
|
+
};
|
|
160
|
+
export interface PopoverItemAria {
|
|
161
|
+
/** Single-select menu ⇒ radio semantics, so a screen reader announces "1 of N, checked". */
|
|
162
|
+
role: "menuitemradio";
|
|
163
|
+
"aria-checked": "true" | "false";
|
|
164
|
+
"aria-disabled": "true" | undefined;
|
|
165
|
+
}
|
|
166
|
+
export declare function popoverItemAria(item: Pick<PopoverItem, "selected" | "disabled">): PopoverItemAria;
|
|
167
|
+
/** What a key pressed on the CLOSED trigger means. Enter/Space are deliberately absent: the
|
|
168
|
+
* trigger is a real `<button>`, so the browser already synthesises a click from them — handling
|
|
169
|
+
* them here too would toggle twice. */
|
|
170
|
+
export type PopoverTriggerKeyAction = "open-first" | "open-last" | null;
|
|
171
|
+
export declare function popoverTriggerKeyAction(key: string): PopoverTriggerKeyAction;
|
|
172
|
+
/**
|
|
173
|
+
* What a key pressed INSIDE the open panel means.
|
|
174
|
+
* - `close` — Escape: close and pull focus back to the trigger (never strand it on <body>).
|
|
175
|
+
* - `dismiss` — Tab: close, but let the browser move focus onward normally.
|
|
176
|
+
* - the rest — roving focus.
|
|
177
|
+
*/
|
|
178
|
+
export type PopoverPanelKeyAction = "close" | "dismiss" | "next" | "prev" | "first" | "last" | null;
|
|
179
|
+
export declare function popoverPanelKeyAction(key: string): PopoverPanelKeyAction;
|
|
180
|
+
/** Should the binding call `preventDefault()`? Everything we act on, except `dismiss` — Tab MUST
|
|
181
|
+
* keep its native behaviour or the popover becomes a focus trap. */
|
|
182
|
+
export declare function popoverKeyHandled(action: PopoverPanelKeyAction): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Does this action apply to the element that currently has focus?
|
|
185
|
+
*
|
|
186
|
+
* Dismissal (Escape / Tab) applies from ANYWHERE inside the popover — including the caller's
|
|
187
|
+
* footer content, where Escape must still close. Roving (arrows / Home / End) applies ONLY when
|
|
188
|
+
* focus is on a menu row or on the panel fallback. That distinction is load-bearing, not tidiness:
|
|
189
|
+
* a footer control is arbitrary caller markup, so swallowing Home/End there would steal a text
|
|
190
|
+
* input's caret keys, and swallowing an arrow would yank focus out of the footer and into the menu.
|
|
191
|
+
* The binding supplies the one thing this module cannot compute — whether the focused element is a
|
|
192
|
+
* roving surface.
|
|
193
|
+
*/
|
|
194
|
+
export declare function popoverAppliesToFocus(action: PopoverPanelKeyAction, focusOnRovingSurface: boolean): boolean;
|
|
195
|
+
/** Index of the first enabled item scanning forward (`dir` 1) or backward (`dir` -1); -1 if every
|
|
196
|
+
* item is disabled (or the list is empty). */
|
|
197
|
+
export declare function edgePopoverIndex(items: readonly PopoverItem[], dir?: 1 | -1): number;
|
|
198
|
+
/** The next enabled index from `from` in `dir`, wrapping. Returns -1 when nothing is enabled and
|
|
199
|
+
* `from` itself when it is the ONLY enabled row (a wrap that lands back where it started). */
|
|
200
|
+
export declare function stepPopoverIndex(items: readonly PopoverItem[], from: number, dir: 1 | -1): number;
|
|
201
|
+
/**
|
|
202
|
+
* Where focus lands when the panel opens: the selected row if it is focusable, else the first
|
|
203
|
+
* enabled one. A selected-but-disabled row does not swallow focus.
|
|
204
|
+
*/
|
|
205
|
+
export declare function initialPopoverIndex(items: readonly PopoverItem[]): number;
|
|
206
|
+
/**
|
|
207
|
+
* The single entry point a binding's keydown handler calls: maps a panel key action + the current
|
|
208
|
+
* focused index to the next focused index. Non-navigation actions (and a null action) leave the
|
|
209
|
+
* index untouched, so the caller never has to branch on the action twice.
|
|
210
|
+
*
|
|
211
|
+
* `current < 0` means "focus is in the panel but not on a row" (an all-disabled or empty list, or
|
|
212
|
+
* a click that landed on the panel's padding). From there Down means the FIRST enabled row and Up
|
|
213
|
+
* means the LAST — not a wrap arithmetic accident off index -1.
|
|
214
|
+
*/
|
|
215
|
+
export declare function resolvePopoverIndex(action: PopoverPanelKeyAction, items: readonly PopoverItem[], current: number): number;
|
|
216
|
+
/**
|
|
217
|
+
* Is an optional slot (heading, caption, footer) worth rendering its chrome?
|
|
218
|
+
*
|
|
219
|
+
* `undefined`, `null`, `false` and `""` are all values BOTH frameworks render as nothing, and the
|
|
220
|
+
* idiomatic conditional slot is `footer={enabled && <Action />}` — which passes `false`, not
|
|
221
|
+
* `undefined`, when disabled. Testing `!== undefined` would leave an orphan separator and an empty
|
|
222
|
+
* box behind it, and an empty heading would leave the menu's `aria-labelledby` pointing at an
|
|
223
|
+
* element with no text. Shared here so both bindings apply the same rule.
|
|
224
|
+
*/
|
|
225
|
+
export declare function popoverHasSlotContent(slot: unknown): boolean;
|
|
226
|
+
export interface PopoverTriggerText {
|
|
227
|
+
/** The static prefix ("review lane:") — null when the caller supplies none. */
|
|
228
|
+
label: string | null;
|
|
229
|
+
/** The current selection's label, or the em-dash placeholder. Never invented. */
|
|
230
|
+
value: string;
|
|
231
|
+
}
|
|
232
|
+
export interface PopoverTriggerTextOptions {
|
|
233
|
+
label?: string;
|
|
234
|
+
/** Overrides {@link POPOVER_EMPTY_VALUE} for the no-selection case. */
|
|
235
|
+
placeholder?: string;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The trigger face: prefix + the CURRENTLY selected item's label. Honest by construction — with no
|
|
239
|
+
* selection (or a selection that is not in the list) it shows the placeholder rather than naming an
|
|
240
|
+
* item that is not actually chosen.
|
|
241
|
+
*/
|
|
242
|
+
export declare function popoverTriggerText(items: readonly PopoverItem[], opts?: PopoverTriggerTextOptions): PopoverTriggerText;
|