@kitlangton/knobs 0.0.0-stage → 0.2.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1414 -2
  3. package/dist/dev/chunk-4km87zfx.js +155 -0
  4. package/dist/dev/chunk-5cr85vv6.js +17726 -0
  5. package/dist/dev/chunk-693jd1gj.js +1435 -0
  6. package/dist/dev/chunk-gezhc24g.js +1132 -0
  7. package/dist/dev/index.js +1660 -0
  8. package/dist/index.js +2651 -0
  9. package/dist/panel.js +26404 -0
  10. package/dist/react.js +326 -0
  11. package/dist/solid.js +336 -0
  12. package/dist/themes.js +235 -0
  13. package/dist/types/core/adapter.d.ts +21 -0
  14. package/dist/types/core/code.d.ts +3 -0
  15. package/dist/types/core/color.d.ts +27 -0
  16. package/dist/types/core/easing.d.ts +21 -0
  17. package/dist/types/core/internal.d.ts +18 -0
  18. package/dist/types/core/keys.d.ts +21 -0
  19. package/dist/types/core/mutate.d.ts +21 -0
  20. package/dist/types/core/ramp.d.ts +150 -0
  21. package/dist/types/core/random.d.ts +62 -0
  22. package/dist/types/core/readout.d.ts +73 -0
  23. package/dist/types/core/remote.d.ts +260 -0
  24. package/dist/types/core/schema.d.ts +550 -0
  25. package/dist/types/core/spring.d.ts +38 -0
  26. package/dist/types/core/storage.d.ts +8 -0
  27. package/dist/types/core/store.d.ts +536 -0
  28. package/dist/types/core/theme.d.ts +27 -0
  29. package/dist/types/core/variants.d.ts +25 -0
  30. package/dist/types/index.d.ts +14 -0
  31. package/dist/types/panel/attach.d.ts +5 -0
  32. package/dist/types/panel/choice.d.ts +56 -0
  33. package/dist/types/panel/die.d.ts +121 -0
  34. package/dist/types/panel/editors.d.ts +170 -0
  35. package/dist/types/panel/flash.d.ts +13 -0
  36. package/dist/types/panel/folds.d.ts +14 -0
  37. package/dist/types/panel/give.d.ts +73 -0
  38. package/dist/types/panel/gpu/atlas.d.ts +40 -0
  39. package/dist/types/panel/gpu/color.d.ts +128 -0
  40. package/dist/types/panel/gpu/die.wgsl.d.ts +1 -0
  41. package/dist/types/panel/gpu/renderer.d.ts +784 -0
  42. package/dist/types/panel/gpu/shader.wgsl.d.ts +2 -0
  43. package/dist/types/panel/haptics.d.ts +12 -0
  44. package/dist/types/panel/has.d.ts +13 -0
  45. package/dist/types/panel/hints.d.ts +45 -0
  46. package/dist/types/panel/icons.d.ts +30 -0
  47. package/dist/types/panel/index.d.ts +11 -0
  48. package/dist/types/panel/inspector.d.ts +50 -0
  49. package/dist/types/panel/measure.d.ts +6 -0
  50. package/dist/types/panel/menu.d.ts +38 -0
  51. package/dist/types/panel/morph.d.ts +38 -0
  52. package/dist/types/panel/motion.d.ts +77 -0
  53. package/dist/types/panel/optkeys.d.ts +7 -0
  54. package/dist/types/panel/page-hints.d.ts +82 -0
  55. package/dist/types/panel/page.d.ts +423 -0
  56. package/dist/types/panel/panel.d.ts +43 -0
  57. package/dist/types/panel/position.d.ts +5 -0
  58. package/dist/types/panel/prefs.d.ts +28 -0
  59. package/dist/types/panel/presets.d.ts +60 -0
  60. package/dist/types/panel/ramp-morph.d.ts +117 -0
  61. package/dist/types/panel/ramps.d.ts +14 -0
  62. package/dist/types/panel/readout-row.d.ts +21 -0
  63. package/dist/types/panel/reel.d.ts +43 -0
  64. package/dist/types/panel/reveal.d.ts +15 -0
  65. package/dist/types/panel/rolling-css.d.ts +2 -0
  66. package/dist/types/panel/scope.d.ts +19 -0
  67. package/dist/types/panel/search.d.ts +36 -0
  68. package/dist/types/panel/seg.d.ts +15 -0
  69. package/dist/types/panel/session.d.ts +21 -0
  70. package/dist/types/panel/slider.d.ts +92 -0
  71. package/dist/types/panel/smear.d.ts +7 -0
  72. package/dist/types/panel/sound.d.ts +137 -0
  73. package/dist/types/panel/spin.d.ts +13 -0
  74. package/dist/types/panel/styles.d.ts +11 -0
  75. package/dist/types/panel/swipe.d.ts +114 -0
  76. package/dist/types/panel/switcher.d.ts +20 -0
  77. package/dist/types/panel/tabs.d.ts +31 -0
  78. package/dist/types/panel/thumb.d.ts +36 -0
  79. package/dist/types/panel/toolbar-grid.d.ts +6 -0
  80. package/dist/types/react/Knobs.d.ts +15 -0
  81. package/dist/types/react/VariantSwitcher.d.ts +10 -0
  82. package/dist/types/react/index.d.ts +3 -0
  83. package/dist/types/react/useControls.d.ts +11 -0
  84. package/dist/types/solid/index.d.ts +50 -0
  85. package/dist/types/themes/index.d.ts +1 -0
  86. package/dist/types/ui/activity-css.d.ts +1 -0
  87. package/dist/types/ui/activity-model.d.ts +122 -0
  88. package/dist/types/ui/activity.d.ts +67 -0
  89. package/dist/types/ui/bento-css.d.ts +1 -0
  90. package/dist/types/ui/bento-model.d.ts +96 -0
  91. package/dist/types/ui/bento.d.ts +77 -0
  92. package/dist/types/ui/commands.d.ts +11 -0
  93. package/dist/types/ui/confirm.d.ts +13 -0
  94. package/dist/types/ui/follow.d.ts +17 -0
  95. package/dist/types/ui/index.d.ts +24 -0
  96. package/dist/types/ui/layer.d.ts +70 -0
  97. package/dist/types/ui/look.d.ts +62 -0
  98. package/dist/types/ui/menu.d.ts +63 -0
  99. package/dist/types/ui/overlay-css.d.ts +1 -0
  100. package/dist/types/ui/overlay-model.d.ts +87 -0
  101. package/dist/types/ui/palette.d.ts +38 -0
  102. package/dist/types/ui/picker-css.d.ts +1 -0
  103. package/dist/types/ui/picker-model.d.ts +53 -0
  104. package/dist/types/ui/picker.d.ts +67 -0
  105. package/dist/types/ui/popover.d.ts +35 -0
  106. package/dist/types/ui/react.d.ts +72 -0
  107. package/dist/types/ui/readout-css.d.ts +1 -0
  108. package/dist/types/ui/readout-model.d.ts +137 -0
  109. package/dist/types/ui/readouts.d.ts +85 -0
  110. package/dist/types/ui/rgba.d.ts +6 -0
  111. package/dist/types/ui/solid.d.ts +63 -0
  112. package/dist/types/ui/split-model.d.ts +135 -0
  113. package/dist/types/ui/split.d.ts +92 -0
  114. package/dist/types/ui/timeline-model.d.ts +83 -0
  115. package/dist/types/ui/timeline.d.ts +76 -0
  116. package/dist/types/ui/toast.d.ts +19 -0
  117. package/dist/types/ui/tooltip.d.ts +19 -0
  118. package/dist/types/ui/tree-model.d.ts +125 -0
  119. package/dist/types/ui/tree.d.ts +95 -0
  120. package/dist/ui-react.js +9230 -0
  121. package/dist/ui-solid.js +9162 -0
  122. package/dist/ui.js +11826 -0
  123. package/package.json +99 -4
@@ -0,0 +1,77 @@
1
+ import { type BentoPlan } from "./bento-model.js";
2
+ import { type UiLook } from "./look.js";
3
+ export interface BentoItem {
4
+ readonly id: string;
5
+ /** Width / height. Images without one take their natural size. */
6
+ readonly aspect?: number;
7
+ /** An image. */
8
+ readonly src?: string;
9
+ /** Responsive candidates for the image; the tile's size picks one, and a zoom a larger one. */
10
+ readonly srcset?: string;
11
+ readonly alt?: string;
12
+ /**
13
+ * Your own content (a live canvas, a prototype), drawn into `el` when the tile comes into view;
14
+ * return a cleanup for when it leaves. Zoomed, it takes the pointer.
15
+ */
16
+ readonly render?: (el: HTMLElement) => void | (() => void);
17
+ /** Shown as type on hover. */
18
+ readonly label?: string;
19
+ /** Items in the same group sit together under its name (the set then scrolls). */
20
+ readonly group?: string;
21
+ /** Asks to be the large tile when the set fits one screen. */
22
+ readonly hero?: boolean;
23
+ }
24
+ export interface BentoOptions extends UiLook {
25
+ readonly items: readonly BentoItem[];
26
+ /** Between tiles, px (2). */
27
+ readonly gap?: number;
28
+ /** Around the tiles, px (the gap). */
29
+ readonly padding?: number;
30
+ /** How far a tile may be cropped to fill a row (.08). */
31
+ readonly tolerance?: number;
32
+ /** Scrolling rows' target height, px (about a third of the view). */
33
+ readonly rowHeight?: number;
34
+ /** Whether a few items may lay out on one screen (true). */
35
+ readonly fit?: boolean;
36
+ /** The most a zooming or sliding tile blurs with its speed, px (2.5; 0 for none). */
37
+ readonly blur?: number;
38
+ readonly picks?: Iterable<string>;
39
+ /** Space toggles a pick; the picks in the items' order. */
40
+ readonly onPick?: (ids: readonly string[]) => void;
41
+ readonly onZoom?: (id: string | null) => void;
42
+ readonly onActive?: (id: string | null) => void;
43
+ /** The grid's accessible name. */
44
+ readonly label?: string;
45
+ }
46
+ export interface Bento {
47
+ readonly element: HTMLElement;
48
+ /** New items: tiles that stay glide to their places, new ones come in, gone ones soften out. */
49
+ update(items: readonly BentoItem[]): void;
50
+ /** Zooms an item to fill the view (`null` zooms back out). */
51
+ zoom(id: string | null): void;
52
+ readonly zoomed: string | null;
53
+ readonly active: string | null;
54
+ focus(id?: string): void;
55
+ readonly picks: readonly string[];
56
+ pick(ids: Iterable<string>): void;
57
+ readonly plan: BentoPlan;
58
+ /** Whether anything is moving (the frame loop runs only then). */
59
+ readonly moving: boolean;
60
+ /** Frames stepped, and the script time each took (ms), for perf checks. */
61
+ stats(): {
62
+ frames: number;
63
+ p50: number;
64
+ p95: number;
65
+ max: number;
66
+ };
67
+ dispose(): void;
68
+ }
69
+ /**
70
+ * An ideas grid: a bento of images and live content, laid out to show as much as possible, that
71
+ * zooms an item to fill the view. See `planBento` for the layout. Click or Enter zooms; Esc, a
72
+ * click or a pinch out returns to the exact grid place; ←/→ (or a swipe) step through neighbours
73
+ * while zoomed; ⌘-scroll or a pinch zooms by hand. Arrows move a wash through the grid by
74
+ * geometry, Space picks, 1–9 jump. Offscreen tiles aren't in the page, and the frame loop sleeps
75
+ * when nothing moves.
76
+ */
77
+ export declare function bento(container: HTMLElement, options: BentoOptions): Bento;
@@ -0,0 +1,11 @@
1
+ import type { Controls } from "../core/store.js";
2
+ import type { Command } from "./palette.js";
3
+ export interface CommandsFromOptions {
4
+ /** Prefix for sections ("Station" → "Station · Presets"); the store id is never shown. */
5
+ readonly title?: string;
6
+ /** Leave out fields (only actions and presets). */
7
+ readonly fields?: boolean;
8
+ /** A field picked from the palette: show it (scroll a panel to it, focus it). */
9
+ reveal?(key: string): void;
10
+ }
11
+ export declare function commandsFrom(controls: Controls<any>, options?: CommandsFromOptions): Command[];
@@ -0,0 +1,13 @@
1
+ import type { UiLook } from "./look.js";
2
+ export interface ConfirmOptions {
3
+ readonly title: string;
4
+ readonly body?: string;
5
+ /** The confirming button's label ("OK"). */
6
+ readonly confirm?: string;
7
+ readonly cancel?: string;
8
+ /** It deletes or discards: the button reads in the critical colour, and Cancel takes focus. */
9
+ readonly destructive?: boolean;
10
+ readonly look?: UiLook;
11
+ readonly doc?: Document;
12
+ }
13
+ export declare function confirm(options: ConfirmOptions): Promise<boolean>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A diff for each set of prop values, given the values the piece was made with: the props that
3
+ * differ from the last values seen (at first, those at creation).
4
+ */
5
+ export declare function propChanges<P extends object>(initial: P, same?: (a: unknown, b: unknown) => boolean): (next: P) => Partial<P>;
6
+ /** Whether a diff from `propChanges` has anything in it. */
7
+ export declare const anyChanged: (changed: object) => boolean;
8
+ /**
9
+ * A timeline's changed props as one `update()`: unset ones dropped, and so are values it already
10
+ * has (a `time` or `playing` it reported itself, markers it handed out).
11
+ */
12
+ export declare function timelineChanges<C extends Record<string, unknown>>(now: {
13
+ readonly time: number;
14
+ readonly playing: boolean;
15
+ readonly markers: unknown;
16
+ readonly ranges: unknown;
17
+ }, changed: C): Partial<C>;
@@ -0,0 +1,24 @@
1
+ export { injectUiStyles, restyle, type RestyleLook, type UiLook, type UiTheme } from "./look.js";
2
+ export { commandsFrom, type CommandsFromOptions } from "./commands.js";
3
+ export { confirm, type ConfirmOptions } from "./confirm.js";
4
+ export { overlayLook, type Anchor } from "./layer.js";
5
+ export { assignPageCodes, hints, pageHints, type HintPainter, type HintSourceOptions, type PageHints, type PageHintTarget, } from "../panel/page-hints.js";
6
+ export { fuzzy, inTriangle, marks, matchItem, place, safeTriangle, stepItem, rank, type Box, type FuzzyMatch, type Placed, type Placement, type Ranked, type Side, } from "./overlay-model.js";
7
+ export { commandPalette, type Command, type CommandPalette, type CommandPaletteOptions, } from "./palette.js";
8
+ export { contextMenu, menu, type ContextMenu, type ContextMenuOptions, type Menu, type MenuCloseReason, type MenuEntry, type MenuItem, type MenuOptions, } from "./menu.js";
9
+ export { popover, type Popover, type PopoverCloseReason, type PopoverOptions } from "./popover.js";
10
+ export { readout, type Readout } from "./readouts.js";
11
+ export { toast, type Toast, type ToastOptions } from "./toast.js";
12
+ export { tooltip, tooltips, type TooltipContent, type TooltipOptions } from "./tooltip.js";
13
+ export { split, type PaneAction, type PaneOptions, type Split, type SplitFold, type SplitOptions, type SplitPane, } from "./split.js";
14
+ export { COLLAPSE_PAST, drag as dragDivider, parseLayout, softClamp, solve as solveSplit, type PaneSpec, type SplitDirection, type SplitLayout, } from "./split-model.js";
15
+ export { timeline, type SeekSource, type Timeline, type TimelineMarker, type TimelineOptions, type TimelineRange, } from "./timeline.js";
16
+ export { adjacentMarker, advance, clampView, followView, frameTime, magnet, rulerTicks, scrubTime, timeLabel, toFrame, zoomView, type Tick, type Ticks, type TimeUnits, type TimeView, } from "./timeline-model.js";
17
+ export { tree, type Tree, type TreeOptions, type TreeRowContext, type TreeToggle } from "./tree.js";
18
+ export { applyMove, plainAccess, dropTarget, flatten, indexTree, matchLabel, navigate, select, stepMove, topmost, typeahead, validMove, virtualWindow, type DropTarget, type FlatRow, type Selection, type TreeAccess, type TreeFilter, type TreeMove, } from "./tree-model.js";
19
+ export { bento, type Bento, type BentoItem, type BentoOptions } from "./bento.js";
20
+ export { fitRect, FIT_MAX, motionBlur, neighbour as bentoNeighbour, planBento, rowTarget, windowOf as bentoWindow, type BentoDirection, type BentoLayout, type BentoMode, type BentoPlan, type BentoRect, } from "./bento-model.js";
21
+ export { activity, type Activity, type ActivityOptions, } from "./activity.js";
22
+ export { diffActivity, flattenActivity, formatDuration, indexActivity, keepSelection as keepActivitySelection, navigateActivity, railSegments, showsSelection, stackRows, type ActivityEvent, type ActivityInput, type ActivityKey, type ActivityKind, type ActivityRow, type RailNode, } from "./activity-model.js";
23
+ export { optionPicker, type OptionPicker, type OptionPickerOptions, type PickerOption, } from "./picker.js";
24
+ export { claimsDigit, digitPick, keepPick, stepPick } from "./picker-model.js";
@@ -0,0 +1,70 @@
1
+ import { type UiLook } from "./look.js";
2
+ /** The look every overlay takes unless given its own (a preset and theme, as panels take). */
3
+ export declare function overlayLook(look: UiLook): void;
4
+ export declare const lookOf: (look?: UiLook) => UiLook;
5
+ /** The document's overlay layer: fixed, above everything, passing the pointer through. */
6
+ export declare function layerRoot(doc: Document): HTMLElement;
7
+ /** A new overlay element in the layer, with the pieces' look. */
8
+ export declare function layerElement(doc: Document, cls: string, look?: UiLook): HTMLDivElement;
9
+ export interface PresenceOptions {
10
+ /** Scale it grows from (0.94). */
11
+ readonly scale?: number;
12
+ /** Blur (px) it sharpens from (6). */
13
+ readonly blur?: number;
14
+ /** Where it slides from, px (toward its anchor). */
15
+ readonly dx?: number;
16
+ readonly dy?: number;
17
+ /** Perceptual duration in, s (0.32). */
18
+ readonly duration?: number;
19
+ /** Bounce in (0.2); exits never bounce. */
20
+ readonly bounce?: number;
21
+ /** Perceptual duration out, s (0.2): exits run shorter than entrances. */
22
+ readonly exit?: number;
23
+ /** Called once it has gone (faded all the way out). */
24
+ onGone?(): void;
25
+ /** Each frame, with progress (0 → 1, may overshoot a little). */
26
+ onFrame?(p: number): void;
27
+ }
28
+ export interface Presence {
29
+ show(): void;
30
+ hide(): Promise<void>;
31
+ /** Jumps to shown (a hand-off between neighbours) or gone, with no motion. */
32
+ snap(to?: 0 | 1): void;
33
+ readonly shown: boolean;
34
+ /** Changes where it slides from (after a flip). */
35
+ from(dx: number, dy: number): void;
36
+ dispose(): void;
37
+ }
38
+ /** Brings `el` in and out on a spring: opacity, scale and blur from its transform-origin. */
39
+ export declare function presence(el: HTMLElement, o?: PresenceOptions): Presence;
40
+ export interface Dismissible {
41
+ readonly el: HTMLElement;
42
+ /** Pointer-downs inside these don't count as outside (the anchor, say). */
43
+ readonly inside?: () => readonly (Element | null | undefined)[];
44
+ /** Esc and outside clicks reach the layers under a modal one only through it. */
45
+ readonly modal?: boolean;
46
+ readonly outside?: boolean;
47
+ dismiss(reason: "escape" | "outside"): void;
48
+ }
49
+ export declare function pushLayer(doc: Document, layer: Dismissible): () => void;
50
+ export declare const focusables: (root: HTMLElement) => HTMLElement[];
51
+ /** Keeps Tab inside `root`; returns the remover. */
52
+ export declare function trapFocus(root: HTMLElement): () => void;
53
+ /** Where focus was, to return it there when the layer closes (if focus is still in the layer). */
54
+ export declare function focusReturn(doc: Document, layer: HTMLElement): () => void;
55
+ export type Anchor = HTMLElement | DOMRect | {
56
+ readonly x: number;
57
+ readonly y: number;
58
+ };
59
+ export declare const anchorBox: (a: Anchor) => {
60
+ x: number;
61
+ y: number;
62
+ w: number;
63
+ h: number;
64
+ };
65
+ export declare const viewBox: (doc: Document) => {
66
+ x: number;
67
+ y: number;
68
+ w: number;
69
+ h: number;
70
+ };
@@ -0,0 +1,62 @@
1
+ import { type TokenPreset } from "../core/theme.js";
2
+ export type UiTheme = "dark" | "light" | "auto";
3
+ /** What every piece takes to match the panels beside it. */
4
+ export interface UiLook {
5
+ /** A token preset (`labPreset`): the same tokens a panel takes. */
6
+ readonly preset?: TokenPreset;
7
+ readonly theme?: UiTheme;
8
+ }
9
+ /** Adds the pieces' stylesheet once per document. */
10
+ export declare function injectUiStyles(doc: Document): void;
11
+ /** Styles, preset and theme for a piece's root. */
12
+ export declare function applyLook(root: HTMLElement, look: UiLook): void;
13
+ /**
14
+ * `--k-frame: flat` (a preset's, or a host's on any ancestor) marks the piece `data-frame`. Read
15
+ * again once the piece is in the page.
16
+ */
17
+ export declare function syncFrame(root: HTMLElement): void;
18
+ /** A look for `restyle`: what the pieces take, and a panel's density. */
19
+ export interface RestyleLook extends UiLook {
20
+ readonly density?: "compact" | "default" | "comfortable";
21
+ }
22
+ /**
23
+ * Changes the look of what's already on screen: every knobs/ui piece and panel inside `target`
24
+ * (the whole document by default) takes `look`'s preset (none for knobs' own), theme and, for
25
+ * panels, density. Panels' tokens glide to the new values; splits and trees measure again. On a
26
+ * document, overlays opened afterwards take it too (`overlayLook`).
27
+ */
28
+ export declare function restyle(target: Document | Element, look: RestyleLook): void;
29
+ /** The look overlays take unless given their own (`overlayLook`, `restyle(document)`). */
30
+ export declare const lookDefaults: {
31
+ current: UiLook;
32
+ };
33
+ export declare const reducedMotion: (win: Window | null) => boolean;
34
+ export declare const tune: <T extends {
35
+ omega: number;
36
+ zeta: number;
37
+ }>(s: T, m: {
38
+ readonly omega: number;
39
+ readonly zeta: number;
40
+ }) => T;
41
+ /** Whether `target` is an editable field or select that takes typed keys. */
42
+ export declare const isTypingTarget: (t: EventTarget | null) => boolean;
43
+ /**
44
+ * Tells the page's GPU overlay that layout moved (a pane resizing, collapsing or sliding): it
45
+ * reads rects afresh on its next frame, so a panel inside a pane keeps up.
46
+ */
47
+ export declare function notifyLayout(doc: Document): void;
48
+ /** One rAF loop: `step(dt)` runs each frame while it returns true. */
49
+ export declare function frameLoop(win: Window, step: (dt: number) => boolean): {
50
+ wake(): void;
51
+ readonly running: boolean;
52
+ stop(): void;
53
+ };
54
+ export declare const svg: (body: string, box?: number) => string;
55
+ export declare const ICONS: {
56
+ caret: string;
57
+ chevronDown: string;
58
+ eye: string;
59
+ eyeOff: string;
60
+ lock: string;
61
+ unlock: string;
62
+ };
@@ -0,0 +1,63 @@
1
+ import type { UiLook } from "./look.js";
2
+ import { type Anchor } from "./layer.js";
3
+ import { type Placement } from "./overlay-model.js";
4
+ export interface MenuItem {
5
+ readonly id?: string;
6
+ readonly label: string;
7
+ /** A glyph, or an `<svg>` string. */
8
+ readonly icon?: string;
9
+ /** A key combination shown as chips ("Mod+D", "Backspace"); display only, it binds nothing. */
10
+ readonly shortcut?: string;
11
+ /** Dim text at the end, before any shortcut. */
12
+ readonly hint?: string;
13
+ readonly disabled?: boolean;
14
+ /** A check mark (`menuitemcheckbox`), or with `radio` a dot (`menuitemradio`). */
15
+ readonly checked?: boolean;
16
+ readonly radio?: boolean;
17
+ /** Drawn in `--k-critical`, for what can't be taken back. */
18
+ readonly destructive?: boolean;
19
+ /** A submenu (or a function read when it opens). */
20
+ readonly items?: readonly MenuEntry[] | (() => readonly MenuEntry[]);
21
+ run?(): unknown;
22
+ }
23
+ /** An item, `"-"` for a separator, or `null` / `false` for nothing (a conditional item). */
24
+ export type MenuEntry = MenuItem | "-" | null | false | undefined;
25
+ export type MenuCloseReason = "select" | "escape" | "outside" | "close";
26
+ export interface MenuOptions {
27
+ /** Where it opens beside its anchor (`bottom-start`; a point opens at its corner). */
28
+ readonly placement?: Placement;
29
+ /** Px between the anchor and the menu (4; 0 at a point). */
30
+ readonly offset?: number;
31
+ readonly label?: string;
32
+ readonly look?: UiLook;
33
+ /** The first item starts active (opened by a key rather than the pointer). */
34
+ readonly keyboard?: boolean;
35
+ onClose?(reason: MenuCloseReason): void;
36
+ /** Each item chosen, beside its own `run`. */
37
+ onSelect?(item: MenuItem): void;
38
+ }
39
+ export interface Menu {
40
+ readonly element: HTMLElement;
41
+ readonly open: boolean;
42
+ /** Closes it and any submenu (animated); resolves once they've gone. */
43
+ close(): Promise<void>;
44
+ }
45
+ /** A menu beside `anchor` (a button, a rect or a point). */
46
+ export declare function menu(anchor: Anchor, items: readonly MenuEntry[], options?: MenuOptions): Menu;
47
+ export interface ContextMenuOptions extends Omit<MenuOptions, "placement" | "offset" | "keyboard"> {
48
+ }
49
+ export interface ContextMenu {
50
+ /** Opens it at a point (or the focused element), as a right-click or the menu key would. */
51
+ open(at?: {
52
+ readonly x: number;
53
+ readonly y: number;
54
+ }): Menu | null;
55
+ readonly current: Menu | null;
56
+ dispose(): void;
57
+ }
58
+ /**
59
+ * A context menu for `target`: a right-click (or a two-finger click) opens it at the pointer, and
60
+ * the menu key or ⇧F10 at the focused element. `items` may be a function of the event, read each
61
+ * time (return `null` for no menu there, and the browser's own shows).
62
+ */
63
+ export declare function contextMenu(target: HTMLElement, items: readonly MenuEntry[] | ((event: MouseEvent | KeyboardEvent | null) => readonly MenuEntry[] | null | undefined), options?: ContextMenuOptions): ContextMenu;
@@ -0,0 +1 @@
1
+ export declare const overlayCss = "\n.knobs-layers{position:fixed;inset:0;z-index:2147480005;pointer-events:none}\n.knobs-layers>.knobs-layer,.knobs-ui.knobs-toast{position:fixed;left:0;top:0;box-sizing:border-box;pointer-events:auto;outline:none;\n font:var(--ku-font-weight) var(--ku-font-size)/1.4 var(--ku-font);color:var(--ku-text);letter-spacing:normal;text-align:left;text-transform:none;\n -webkit-font-smoothing:antialiased;will-change:transform,opacity,filter}\n/* The overlays' own elements stop a host's element rules; what you put in a popover is yours. */\n:where(.knobs-layers,.knobs-toast) :is(.knobs-kbd,.knobs-mitem,.knobs-menu-sep,.knobs-palette-input,.knobs-palette-label>mark,.knobs-confirm-title,.knobs-confirm-body,.knobs-confirm-btn,.knobs-toast-action){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;\n background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}\n.knobs-layers .knobs-layer *{box-sizing:border-box}\n/* A floating surface: frosted, a hairline rim, a soft shadow. */\n.knobs-layers>:is(.knobs-popover-layer,.knobs-palette,.knobs-confirm,.knobs-menu-layer){background:color-mix(in srgb,var(--ku-bg) 88%,transparent);\n -webkit-backdrop-filter:blur(28px) saturate(1.7);backdrop-filter:blur(28px) saturate(1.7);border-radius:var(--ku-panel-radius);\n box-shadow:0 0 0 .5px var(--ku-line-hi),inset 0 1px 0 var(--ku-rim),0 12px 40px rgba(0,0,0,.36),0 2px 8px rgba(0,0,0,.18)}\n.knobs-layers>.knobs-popover-layer{min-width:120px;max-width:min(420px,calc(100vw - 16px));padding:6px}\n.knobs-popover-layer .knobs-popover-text{padding:4px 6px;color:var(--ku-label)}\n\n/* Menus: rows of the panel's control height, a soft wash on the active one (never an outline),\n concentric corners inside the surface. */\n.knobs-layers>.knobs-menu-layer{display:flex;flex-direction:column;min-width:184px;max-width:min(340px,calc(100vw - 16px));max-height:calc(100vh - 16px);overflow:auto;padding:5px;\n scrollbar-width:thin;overscroll-behavior:contain;user-select:none;-webkit-user-select:none}\n.knobs-menu-layer .knobs-mitem{position:relative;display:flex;align-items:center;gap:8px;flex:none;height:max(22px,calc(var(--ku-row) - 6px));padding:0 8px;\n border-radius:max(0px,calc(var(--ku-panel-radius) - 5px));color:var(--ku-text);cursor:default;white-space:nowrap}\n.knobs-menu-layer .knobs-mitem[data-active],.knobs-menu-layer .knobs-mitem[data-open]{background:var(--ku-wash-hi)}\n.knobs-menu-layer .knobs-mitem[data-destructive]{color:var(--ku-critical)}\n.knobs-menu-layer .knobs-mitem[data-destructive][data-active]{background:color-mix(in srgb,var(--ku-critical) 16%,transparent)}\n.knobs-menu-layer .knobs-mitem[aria-disabled=\"true\"]{opacity:.38}\n.knobs-menu-layer .knobs-menu-lead{flex:none;display:flex;align-items:center;justify-content:center;width:14px;margin-left:-2px;color:var(--ku-label);font-size:calc(var(--ku-font-size) - 1px)}\n.knobs-menu-layer .knobs-mitem[data-destructive] .knobs-menu-lead,.knobs-menu-layer .knobs-mitem[aria-checked=\"true\"] .knobs-menu-lead{color:inherit}\n.knobs-menu-layer .knobs-menu-lead svg{width:13px;height:13px}\n.knobs-menu-layer .knobs-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}\n.knobs-menu-layer .knobs-menu-hint{flex:none;color:var(--ku-dim)}\n.knobs-menu-layer .knobs-menu-keys{display:inline-flex;gap:2px;flex:none;margin-left:12px}\n.knobs-menu-layer .knobs-mitem[data-active] .knobs-kbd{background:color-mix(in srgb,var(--ku-text) 14%,transparent);color:var(--ku-text)}\n.knobs-menu-layer .knobs-menu-caret{flex:none;display:flex;margin-left:8px;margin-right:-3px;color:var(--ku-dim)}\n.knobs-menu-layer .knobs-menu-caret svg{width:10px;height:10px}\n.knobs-menu-layer .knobs-menu-sep{flex:none;height:1px;margin:4px 8px;background:var(--ku-line)}\n\n.knobs-layers>.knobs-tooltip{display:inline-flex;align-items:center;gap:7px;max-width:300px;padding:4px 8px;pointer-events:none;white-space:nowrap;\n font-size:calc(var(--ku-font-size) - .5px);background:var(--ku-bg);color:var(--ku-text);border-radius:min(var(--ku-radius),6px);\n box-shadow:0 0 0 .5px var(--ku-line-hi),0 4px 14px rgba(0,0,0,.3)}\n.knobs-tooltip .knobs-tooltip-text{overflow:hidden;text-overflow:ellipsis}\n.knobs-layer :is(.knobs-tooltip-keys,.knobs-palette-keys){display:inline-flex;gap:2px;flex:none}\n.knobs-layer .knobs-tooltip-keys[hidden]{display:none}\n.knobs-layer .knobs-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:17px;height:17px;padding:0 4px;\n border-radius:min(var(--ku-radius),4px);background:var(--ku-surface-hi);color:var(--ku-label);font:500 calc(var(--ku-font-size) - 1.5px)/1 var(--ku-font)}\n\n.knobs-layers>.knobs-palette{left:50%;top:max(48px,15vh);translate:-50% 0;transform-origin:50% 0;display:flex;flex-direction:column;\n width:min(580px,calc(100vw - 32px));max-height:min(460px,72vh);overflow:hidden}\n.knobs-palette .knobs-palette-field{display:flex;align-items:center;gap:10px;flex:none;height:46px;padding:0 16px;border-bottom:1px solid var(--ku-line);color:var(--ku-dim)}\n.knobs-palette .knobs-palette-field svg{width:15px;height:15px;flex:none}\n.knobs-palette .knobs-palette-input{flex:1;min-width:0;height:100%;font-size:calc(var(--ku-font-size) + 3px);color:var(--ku-text);caret-color:var(--ku-accent)}\n.knobs-palette .knobs-palette-input::placeholder{color:var(--ku-dim)}\n.knobs-palette .knobs-palette-list{position:relative;flex:1;min-height:0;overflow:auto;padding:6px;scrollbar-width:thin;overscroll-behavior:contain}\n.knobs-palette .knobs-palette-hl{position:absolute;left:6px;right:6px;top:0;border-radius:max(0px,calc(var(--ku-panel-radius) - 6px));background:var(--ku-wash-hi);opacity:0;pointer-events:none;\n transition:transform var(--ku-ease-ms) var(--ku-ease),height var(--ku-ease-ms) var(--ku-ease),opacity .12s}\n.knobs-palette .knobs-palette-section{padding:10px 10px 4px;color:var(--ku-dim);font-size:calc(var(--ku-font-size) - 1px);font-weight:var(--ku-label-weight);user-select:none}\n.knobs-palette .knobs-palette-section:first-of-type{padding-top:4px}\n.knobs-palette .knobs-palette-item{position:relative;display:flex;align-items:center;gap:8px;height:max(32px,var(--ku-row));padding:0 10px;\n color:var(--ku-label);cursor:default;user-select:none}\n.knobs-palette .knobs-palette-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ku-text)}\n/* Searching, the label dims and its matched letters light up. */\n.knobs-palette[data-query] .knobs-palette-label{color:var(--ku-label)}\n.knobs-palette .knobs-palette-label mark{color:var(--ku-accent);font-weight:var(--ku-title-weight)}\n.knobs-palette[data-query] .knobs-palette-item[data-active] .knobs-palette-label{color:var(--ku-text)}\n.knobs-palette .knobs-palette-hint{flex:none;color:var(--ku-dim)}\n.knobs-palette .knobs-palette-fill{flex:1}\n.knobs-palette .knobs-palette-tag{flex:none;color:var(--ku-dim);font-size:calc(var(--ku-font-size) - 1px);white-space:nowrap}\n.knobs-palette .knobs-palette-item[aria-disabled=\"true\"]{opacity:.4}\n.knobs-palette .knobs-palette-empty{padding:18px 10px;color:var(--ku-dim);text-align:center}\n\n.knobs-layers>.knobs-scrim{inset:0;width:100%;height:100%;background:rgba(0,0,0,.3)}\n.knobs-layers>.knobs-confirm{left:50%;top:42%;translate:-50% -50%;width:min(340px,calc(100vw - 32px));padding:18px 18px 14px}\n.knobs-confirm .knobs-confirm-title{margin:0 0 6px;font-size:calc(var(--ku-font-size) + 1.5px);font-weight:var(--ku-title-weight);color:var(--ku-text);line-height:1.3}\n.knobs-confirm .knobs-confirm-body{margin:0;color:var(--ku-label);line-height:1.45}\n.knobs-confirm .knobs-confirm-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}\n.knobs-confirm .knobs-confirm-btn{height:28px;padding:0 14px;border-radius:min(var(--ku-radius),7px);background:var(--ku-surface-hi);color:var(--ku-text);\n font-weight:var(--ku-label-weight);cursor:pointer;transition:background .15s,filter .15s}\n.knobs-confirm .knobs-confirm-cancel:is(:hover,:focus-visible){background:color-mix(in srgb,var(--ku-text) 18%,transparent)}\n.knobs-confirm .knobs-confirm-ok{background:var(--ku-accent);color:var(--ku-bg)}\n.knobs-confirm .knobs-confirm-ok[data-destructive]{background:var(--ku-critical);color:#fff}\n.knobs-confirm .knobs-confirm-ok:is(:hover,:focus-visible){filter:brightness(1.14)}\n\n.knobs-ui.knobs-toast{left:50%;top:auto;bottom:24px;z-index:2147480005;translate:-50% 0;display:inline-flex;align-items:center;gap:10px;\n max-width:min(480px,80vw);padding:8px 16px;background:var(--ku-bg);color:var(--ku-text);border-radius:calc(var(--ku-radius) * 999);\n box-shadow:0 0 0 .5px var(--ku-line-hi),0 8px 28px rgba(0,0,0,.32)}\n.knobs-toast .knobs-toast-text{overflow-wrap:anywhere}\n.knobs-toast[data-tone=\"warn\"]::before,.knobs-toast[data-tone=\"error\"]::before{content:\"\";flex:none;width:7px;height:7px;border-radius:50%;background:var(--ku-warn)}\n.knobs-toast[data-tone=\"error\"]::before{background:var(--ku-critical)}\n.knobs-toast .knobs-toast-action{flex:none;color:var(--ku-accent);font-weight:var(--ku-title-weight);cursor:pointer}\n.knobs-toast .knobs-toast-action[hidden]{display:none}\n";
@@ -0,0 +1,87 @@
1
+ export interface Box {
2
+ readonly x: number;
3
+ readonly y: number;
4
+ readonly w: number;
5
+ readonly h: number;
6
+ }
7
+ export type Side = "top" | "bottom" | "left" | "right";
8
+ export type Placement = Side | `${Side}-start` | `${Side}-end`;
9
+ export interface Placed {
10
+ readonly x: number;
11
+ readonly y: number;
12
+ /** The placement after flipping. */
13
+ readonly placement: Placement;
14
+ /** The anchor's nearest point, in the layer's own px: where it grows from. */
15
+ readonly origin: {
16
+ readonly x: number;
17
+ readonly y: number;
18
+ };
19
+ }
20
+ /**
21
+ * Places a `size` layer on `placement`'s side of `anchor`, `offset` px away: flipped to the
22
+ * other side when it doesn't fit and that side has more room, then shifted along the side to stay
23
+ * `margin` px inside `view`.
24
+ */
25
+ export declare function place(o: {
26
+ anchor: Box;
27
+ size: {
28
+ readonly w: number;
29
+ readonly h: number;
30
+ };
31
+ view: Box;
32
+ placement?: Placement;
33
+ offset?: number;
34
+ margin?: number;
35
+ }): Placed;
36
+ export interface FuzzyMatch {
37
+ readonly score: number;
38
+ /** The matched characters' indices in the text, ascending. */
39
+ readonly indices: readonly number[];
40
+ }
41
+ /**
42
+ * Matches `query`'s characters in order anywhere in `text` (case-insensitive), or null. The best
43
+ * alignment wins: a match at the start, at word starts and in runs scores higher; gaps cost a
44
+ * little. Spaces in the query are ignored.
45
+ */
46
+ export declare function fuzzy(query: string, text: string): FuzzyMatch | null;
47
+ export interface Rankable {
48
+ readonly label: string;
49
+ readonly keywords?: readonly string[];
50
+ readonly section?: string;
51
+ }
52
+ export interface Ranked<T> {
53
+ readonly item: T;
54
+ readonly score: number;
55
+ /** Indices in the label (empty when only a keyword or the section matched). */
56
+ readonly indices: readonly number[];
57
+ }
58
+ /**
59
+ * Items matching `query`, best first; a label match beats a keyword's or the section's. Matches
60
+ * scattered thinly (letters strewn across words) are left out: a keyword or section must match
61
+ * at word starts or in runs, a label at least loosely.
62
+ */
63
+ export declare function rank<T extends Rankable>(items: readonly T[], query: string): Ranked<T>[];
64
+ /** `text` split into runs at `indices`: `[{ text, hit }]`, for highlighting. */
65
+ export declare function marks(text: string, indices: readonly number[]): {
66
+ text: string;
67
+ hit: boolean;
68
+ }[];
69
+ /** The next item from `from` by `by` (±1) that `ok` accepts, wrapping; `from` -1 starts outside. */
70
+ export declare function stepItem(count: number, from: number, by: 1 | -1, ok: (i: number) => boolean): number;
71
+ /**
72
+ * Type-ahead over menu labels: the first item after `from` starting with `typed` (repeating one
73
+ * letter cycles through the items that start with it; more letters refine from where it is).
74
+ */
75
+ export declare function matchItem(labels: readonly (string | null)[], from: number, typed: string): number;
76
+ type Pt = {
77
+ readonly x: number;
78
+ readonly y: number;
79
+ };
80
+ /** Whether `p` is inside the triangle `a b c` (either winding). */
81
+ export declare function inTriangle(p: Pt, a: Pt, b: Pt, c: Pt): boolean;
82
+ /**
83
+ * The safe triangle of a submenu's hover intent: from where the pointer was to the submenu's
84
+ * near edge (a little past its corners). While the pointer travels inside it, the submenu stays.
85
+ */
86
+ export declare function safeTriangle(from: Pt, sub: Box): [Pt, Pt, Pt];
87
+ export {};
@@ -0,0 +1,38 @@
1
+ import type { UiLook } from "./look.js";
2
+ export interface Command {
3
+ /** Stable across sessions (recents remember it); the label by default. */
4
+ readonly id?: string;
5
+ readonly label: string;
6
+ /** The heading it sits under with no query, and its dimmed tag in results. */
7
+ readonly section?: string;
8
+ /** A shortcut shown as key chips (the palette doesn't bind it). */
9
+ readonly shortcut?: string;
10
+ /** Other words it answers to. */
11
+ readonly keywords?: readonly string[];
12
+ /** Text after the label, dimmed (a current value, say). */
13
+ readonly hint?: string;
14
+ readonly disabled?: boolean;
15
+ /** What it does; a string or a promise of one shows as a toast. */
16
+ run?(): unknown;
17
+ }
18
+ export interface CommandPaletteOptions {
19
+ /** The commands, or a function asked each time it opens. */
20
+ readonly commands: readonly Command[] | (() => readonly Command[]);
21
+ /** Called after a command runs (or instead, for commands with no `run`). */
22
+ onRun?(command: Command): void;
23
+ /** The key that toggles it, anywhere on the page (`"Mod+K"`); `false` for none. */
24
+ readonly hotkey?: string | false;
25
+ readonly placeholder?: string;
26
+ /** Recent commands are remembered under this key in localStorage; `false` remembers nothing. */
27
+ readonly recents?: string | false;
28
+ readonly look?: UiLook;
29
+ readonly doc?: Document;
30
+ }
31
+ export interface CommandPalette {
32
+ open(query?: string): void;
33
+ close(): void;
34
+ toggle(): void;
35
+ readonly isOpen: boolean;
36
+ dispose(): void;
37
+ }
38
+ export declare function commandPalette(options: CommandPaletteOptions): CommandPalette;
@@ -0,0 +1 @@
1
+ export declare const pickerCss: string;
@@ -0,0 +1,53 @@
1
+ /** The option a digit names: 1–9 pick the first nine, as they read; past the last, none. */
2
+ export declare function digitPick(ids: readonly string[], key: string): string | undefined;
3
+ /** ↑/↓ (and Home/End) from `current`, stopping at the ends. */
4
+ export declare function stepPick(ids: readonly string[], current: string | null, key: "ArrowUp" | "ArrowDown" | "Home" | "End"): string | undefined;
5
+ /**
6
+ * The selection after the options change: the option a host asked for before it was there
7
+ * (`wanted`) once it comes, else the selection while its option stays, else the first.
8
+ */
9
+ export declare function keepPick(ids: readonly string[], selected: string | null, wanted?: string | null): string | null;
10
+ /**
11
+ * A host's `select(id)`: picked now if its option is there; if not, remembered (`wanted`) for
12
+ * the `set()` that brings it, so the order a host's updates land in can't lose it. A later
13
+ * select replaces what was wanted.
14
+ */
15
+ export declare function askPick(ids: readonly string[], id: string): {
16
+ pick: string | undefined;
17
+ wanted: string | null;
18
+ };
19
+ /** What's still wanted after the options change: nothing once its option has come. */
20
+ export declare const stillWanted: (ids: readonly string[], wanted: string | null) => string | null;
21
+ /** The number an option shows: its place, 1, 2, … 12 (only 1–9 are keys that pick). */
22
+ export declare const pickDigit: (index: number) => string;
23
+ /**
24
+ * Whether a key event goes to the picker's digits: a bare 1–9 that nothing focused claimed (a
25
+ * panel row picking its own options, a text field) and that isn't typed into a field.
26
+ */
27
+ export declare function claimsDigit(event: {
28
+ readonly key: string;
29
+ readonly defaultPrevented: boolean;
30
+ readonly metaKey: boolean;
31
+ readonly ctrlKey: boolean;
32
+ readonly altKey: boolean;
33
+ readonly shiftKey: boolean;
34
+ readonly repeat: boolean;
35
+ readonly typing?: boolean;
36
+ }, typing?: boolean): boolean;
37
+ /**
38
+ * Where the card comes to rest on the option at `index`: below the options before it with their
39
+ * bodies shut (once a change settles only the selected option's is open), as tall as its head
40
+ * and its body. The card aims here from the start, rather than chasing the option's top while
41
+ * the last option's body closes above it. With the body not yet measured (a panel mounting for
42
+ * the first time) there's no target yet, and the card holds where it is.
43
+ */
44
+ export declare function cardTarget(heads: readonly number[], index: number, body: number | undefined, gap?: number, top?: number): {
45
+ y: number;
46
+ h: number;
47
+ } | null;
48
+ /**
49
+ * A first-mounted body's height once it has settled: the same reading two frames running, or
50
+ * whatever it reads after `limit` frames (content that never settles doesn't hold the change
51
+ * up). Until then undefined: it's still laying out.
52
+ */
53
+ export declare function settleHeight(previous: number | undefined, now: number, frames: number, limit?: number): number | undefined;
@@ -0,0 +1,67 @@
1
+ import { type UiLook } from "./look.js";
2
+ export interface PickerOption {
3
+ readonly id: string;
4
+ readonly label: string;
5
+ /** Marked "Suggested". */
6
+ readonly suggested?: boolean;
7
+ /**
8
+ * A picture of it (an image URL), small between the number and the name. It loads lazily and
9
+ * fades in once loaded; while loading, or if it fails, nothing is drawn.
10
+ */
11
+ readonly thumb?: string;
12
+ }
13
+ export interface OptionPickerOptions extends UiLook {
14
+ readonly options: readonly PickerOption[];
15
+ /** The option on show, its parameters open under it; the first unless set. */
16
+ readonly selected?: string | null;
17
+ /** The option decided on: it takes a check. */
18
+ readonly chosen?: string | null;
19
+ /** Keys, digits and clicks moved the selection (a host's `select()` doesn't call it). */
20
+ readonly onSelect?: (id: string, source: "key" | "digit" | "pointer") => void;
21
+ /** Enter on an option. */
22
+ readonly onChoose?: (id: string) => void;
23
+ /**
24
+ * Mounts an option's parameters into `element` the first time it opens (a knobs panel, say);
25
+ * return a cleanup for when the option goes or the picker is destroyed. It stays mounted
26
+ * while closed, so a panel keeps its state.
27
+ */
28
+ readonly body?: (option: PickerOption, element: HTMLElement) => (() => void) | void;
29
+ /**
30
+ * Where 1–9 pick from: `"focus"` (the default) while focus is in the picker, its panels
31
+ * included, `"page"` from anywhere, or `false`. A focused row that picks its own options with
32
+ * digits, and text fields, keep them.
33
+ */
34
+ readonly digits?: "focus" | "page" | false;
35
+ /** The accessible name. */
36
+ readonly label?: string;
37
+ /** The suggested mark's words ("Suggested"). */
38
+ readonly suggestedLabel?: string;
39
+ }
40
+ export interface OptionPicker {
41
+ readonly element: HTMLElement;
42
+ /**
43
+ * New options: the selection stays on its option while that's still there, or goes to one
44
+ * `select()` asked for before it was there.
45
+ */
46
+ set(options: readonly PickerOption[]): void;
47
+ /**
48
+ * Opens this option's parameters (closing the last one's). Doesn't call `onSelect`. An id that
49
+ * isn't there yet is remembered, and picked when `set()` brings it.
50
+ */
51
+ select(id: string): void;
52
+ readonly selected: string | null;
53
+ /** Marks the chosen option (`null` for none). */
54
+ choose(id: string | null): void;
55
+ readonly chosen: string | null;
56
+ /** Focuses the selected option. */
57
+ focus(): void;
58
+ destroy(): void;
59
+ dispose(): void;
60
+ }
61
+ /**
62
+ * A column of options (1–n), each with a name, a "Suggested" mark and a check on the chosen one.
63
+ * The selected option sits on a card that glides between options, and its parameters (what
64
+ * `body` mounts, a knobs panel's rows) open under it in place while the last option's close.
65
+ * ↑/↓, Home/End and 1–9 pick; Enter chooses; Tab goes into the parameters.
66
+ */
67
+ export declare function optionPicker(container: HTMLElement, options: OptionPickerOptions): OptionPicker;
@@ -0,0 +1,35 @@
1
+ import type { UiLook } from "./look.js";
2
+ import { type Anchor } from "./layer.js";
3
+ import { type Placement } from "./overlay-model.js";
4
+ export interface PopoverOptions {
5
+ /** `bottom` by default; `top`, `left`, `right`, each with `-start` or `-end`. */
6
+ readonly placement?: Placement;
7
+ /** Px between the anchor and the layer (6). */
8
+ readonly offset?: number;
9
+ /** `dialog` by default; `menu`, `listbox`… for what the content is. */
10
+ readonly role?: string;
11
+ readonly label?: string;
12
+ /**
13
+ * Where focus goes when it opens: the first focusable element (`first`, the default), the
14
+ * layer itself (`layer`), or nowhere (`none`, focus stays where it was).
15
+ */
16
+ readonly focus?: "first" | "layer" | "none";
17
+ /** Tab stays inside (a modal popover). */
18
+ readonly trap?: boolean;
19
+ /** A click outside closes it (true). */
20
+ readonly closeOnOutside?: boolean;
21
+ readonly look?: UiLook;
22
+ /** Extra class names for the layer. */
23
+ readonly className?: string;
24
+ onClose?(reason: PopoverCloseReason): void;
25
+ }
26
+ export type PopoverCloseReason = "escape" | "outside" | "close";
27
+ export interface Popover {
28
+ readonly element: HTMLElement;
29
+ /** Closes it (animated); resolves once it's gone. */
30
+ close(): Promise<void>;
31
+ /** Places it again (after its content or anchor moved). */
32
+ reposition(): void;
33
+ readonly open: boolean;
34
+ }
35
+ export declare function popover(anchor: Anchor, content: HTMLElement | string | ((close: () => void) => HTMLElement), options?: PopoverOptions): Popover;