@rigerc/ink-ui 0.0.0-stage → 0.41.1

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.
@@ -0,0 +1,1094 @@
1
+ import * as React from 'react';
2
+ import React__default, { ReactNode } from 'react';
3
+
4
+ type BannerProps = {
5
+ title: string;
6
+ subtitle?: string;
7
+ icon?: string;
8
+ };
9
+ declare const Banner: ({ title, subtitle, icon }: BannerProps) => React__default.JSX.Element;
10
+
11
+ type BadgeVariant = "success" | "error" | "warning" | "info";
12
+ type BadgeProps = {
13
+ children: string;
14
+ variant?: BadgeVariant;
15
+ };
16
+ declare const Badge: ({ children, variant }: BadgeProps) => React__default.JSX.Element;
17
+
18
+ type PillVariant = "success" | "error" | "warning" | "info" | "accent" | "muted" | "group";
19
+ type PillTone = "solid" | "soft" | "outline" | "tonal";
20
+ type PillProps = {
21
+ children: string;
22
+ variant?: PillVariant;
23
+ /**
24
+ * Solid is an EVENT — something happened to the thing (`merged`, `NEW`,
25
+ * `GONE`). Soft is a CLASSIFICATION — what the thing is (`epic`, `bug`): a
26
+ * quiet permanent fill from `softColors`, built to sit on every row of a
27
+ * dark ground without out-shouting the row's news. Outline is the same
28
+ * classification drawn as a hue-only ring, for a ground where a fill of any
29
+ * weight is too much. Tonal is CHROME — a count, a slot label — a tint of
30
+ * the hue over the ground from `tonalColors`, inked in the hue: the
31
+ * quietest filled form, for what the frame says about itself rather than
32
+ * row data. The default stays solid so nothing already rendered moves.
33
+ */
34
+ tone?: PillTone;
35
+ /**
36
+ * Tonal only: the thing you are ON takes the stronger tint from
37
+ * `tonalStrongColors` and bolds its label. Ignored by every other tone.
38
+ */
39
+ strong?: boolean;
40
+ /**
41
+ * An explicit fill, for a state the external system INVENTED — GitHub's
42
+ * merged purple, a CI provider's result colours. The hue is that system's
43
+ * vocabulary for something that happened, and repainting it as a token would
44
+ * mislabel the event. Overrides `variant`.
45
+ *
46
+ * Never for that system's SKIN. Jira colours its issue types, but an issue
47
+ * type is a classification, and a classification takes a `variant` chosen by
48
+ * meaning, not the colour Jira happens to paint it. The test is whether the
49
+ * hue names a state the reader already knows from the source system, or
50
+ * merely decorates a category the tokens can already say.
51
+ */
52
+ color?: string;
53
+ };
54
+ /**
55
+ * A rounded label — a category the thing belongs to, or an event that has
56
+ * just happened to it.
57
+ *
58
+ * The law, in one line: the column says where it sits, soft says what it is,
59
+ * solid says something happened, tonal says where you are. A solid pill is an
60
+ * event (`merged`, `NEW`, `GONE`) and earns its loud fill by being news; a
61
+ * soft pill is a classification (`epic`, `bug`, `task`) and takes the quiet
62
+ * fill from `softColors`, because a type is a fact about the row, not
63
+ * something that happened to it — it has to be legible on every row without
64
+ * competing with the one row that has news. Outline is the classification
65
+ * with no fill at all, for a ground where even a soft block is too much.
66
+ * Tonal is chrome on the frame — a count, a slot label — the quietest filled
67
+ * form, with `strong` for the thing you are on. A screen where every pill is
68
+ * solid has no way to say which pill is the news.
69
+ *
70
+ * Reach for it when the word IS the information and you want it to read as
71
+ * one object rather than as more prose. For a reference the reader is meant
72
+ * to follow — a ticket key, a repo — leave the text dim: a fill gives a
73
+ * breadcrumb a weight it has not earned, and once everything is a pill none
74
+ * of them is.
75
+ *
76
+ * The WORD carries the meaning and the colour only reinforces it, so a pill
77
+ * survives being read in monochrome, piped, or by someone who cannot separate
78
+ * the hues.
79
+ *
80
+ * Powerline half-circles, like `ToggleSwitch` and for the same reason: they are
81
+ * the only way to get a genuinely rounded end out of a terminal cell, and a font
82
+ * without them renders blanks, which degrades to a square pill rather than
83
+ * breaking the layout. Not gated on `getIconMode` — that switch chooses between
84
+ * two glyph SETS, and this is one glyph with a graceful absence. Solid and soft take
85
+ * the filled caps, outline the thin ones from the same Powerline Extra block.
86
+ *
87
+ * `NO_COLOR` sends solid, soft and tonal to brackets: with the fill stripped,
88
+ * the caps would be drawing the outline of a pill that is not there. Outline
89
+ * keeps its caps — they ARE the outline, and there was never a fill for them
90
+ * to be missing.
91
+ */
92
+ declare const Pill: ({ children, variant, tone, strong, color, }: PillProps) => React__default.JSX.Element;
93
+ /**
94
+ * How many columns `<Pill>` occupies for `text` — the label plus its two caps,
95
+ * whichever tone draws them.
96
+ *
97
+ * Exported because a caller laying out a fixed-width row has to price the pill
98
+ * BEFORE rendering it, and measuring the rendered output is not available at
99
+ * that point. A row that budgets for the label alone overflows by exactly two
100
+ * columns, which in a frame sized to fill the terminal scrolls the whole panel
101
+ * instead of clipping.
102
+ */
103
+ declare const pillWidth: (text: string) => number;
104
+
105
+ type HeaderProps = {
106
+ children: string;
107
+ subtitle?: string;
108
+ };
109
+ declare const Header: ({ children, subtitle }: HeaderProps) => React__default.JSX.Element;
110
+
111
+ type SpinnerProps = {
112
+ label?: string;
113
+ /** Use ASCII spinner frames on limited terminals. */
114
+ ascii?: boolean;
115
+ };
116
+ declare const Spinner: ({ label, ascii }: SpinnerProps) => React__default.JSX.Element;
117
+
118
+ type ColumnAlign = "left" | "center" | "right";
119
+ type ColumnOverflow = "wrap" | "truncate";
120
+
121
+ type Column<T extends Record<string, unknown>> = {
122
+ key: keyof T & string;
123
+ header: string;
124
+ width?: number;
125
+ minWidth?: number;
126
+ align?: ColumnAlign;
127
+ overflow?: ColumnOverflow;
128
+ };
129
+ type TableProps<T extends Record<string, unknown>> = {
130
+ data: T[];
131
+ columns: Column<T>[];
132
+ gap?: number;
133
+ maxWidth?: number;
134
+ headerColor?: string;
135
+ };
136
+ declare const Table: <T extends Record<string, unknown>>({ data, columns, gap, maxWidth, headerColor, }: TableProps<T>) => React__default.JSX.Element;
137
+
138
+ type Hint = [key: string, label: string];
139
+ type FooterHintsProps = {
140
+ hints: Hint[];
141
+ /**
142
+ * Where this page sits, so the footer can end the same way on every page
143
+ * of every app: `⌫ back` on a nested page, then `? help`, then `q quit`,
144
+ * always last. A host passes only its own hints — navigation, actions,
145
+ * modes — and never these three. Omit it to draw the hints as given.
146
+ */
147
+ page?: "root" | "nested";
148
+ /** Drop `? help` from the tail for an app with no legend. */
149
+ help?: boolean;
150
+ };
151
+ /** The cells every page ends with, in the order the eye learns once. */
152
+ declare const tailHints: (page: "root" | "nested", help?: boolean) => Hint[];
153
+ declare const FooterHints: ({ hints, page, help }: FooterHintsProps) => React__default.JSX.Element;
154
+
155
+ /**
156
+ * The title row's live segment. `news` is something to act on (`● 5 new ·
157
+ * 38 moved · r apply`) and draws bold in the accent; `busy` is in flight
158
+ * (`↻ refreshing…`) and draws in the info colour; `quiet` is freshness
159
+ * (`updated 42m ago`) and draws dim.
160
+ */
161
+ type PageStatus = {
162
+ text: string;
163
+ tone: "news" | "busy" | "quiet";
164
+ };
165
+ type PageProps = {
166
+ /** The app's name, bold, in the accent. Constant across every page of the app. */
167
+ title: string;
168
+ /** One glyph before the name — the app's mark. */
169
+ icon?: string;
170
+ /** How many things the page holds; drawn dim as ` 127 items · ` with a width-stable count. */
171
+ count?: number;
172
+ /** What `count` counts. Defaults to `item`. */
173
+ noun?: string;
174
+ /** Whose things these are — `@kud` — drawn plain, never dim: scope is the fact the row exists to state. */
175
+ user?: string;
176
+ /** A further scope in the host's words, dim: `work`, or a breadcrumb like `ACME-102 · 3 of 12`. */
177
+ scope?: string;
178
+ /** Something worth acting on today — a budget running out — bold in the accent. `critical` turns it red. */
179
+ alert?: string;
180
+ critical?: boolean;
181
+ /** Freshness or news; see `PageStatus`. */
182
+ status?: PageStatus;
183
+ /** Free text after the name, dim — for a host with nothing structured to say (`loading…`). */
184
+ facts?: string;
185
+ /** The tab strip, only on pages that have one. A detail page passes none and the band is dropped, never left empty. */
186
+ tabs?: ReactNode;
187
+ /** Right-aligned, dim, above the hints: `3 of 12`, `↓ 40%`. */
188
+ counter?: string;
189
+ /** The host's own hints — navigation, actions, modes. The tail (`⌫ back · ? help · q quit`) is derived from `page`. */
190
+ hints?: Hint[];
191
+ /** Root or nested, which decides whether `⌫ back` is drawn. Defaults to root. */
192
+ page?: "root" | "nested";
193
+ /** Whether the app has a `?` legend; drops `? help` from the tail when false. */
194
+ help?: boolean;
195
+ /**
196
+ * Whether the frame draws the blank under the title (band two). A body that
197
+ * draws that band itself — a board whose search box lives there — passes
198
+ * `false`, or the two blanks together overflow the frame and one is lost.
199
+ */
200
+ gap?: boolean;
201
+ /**
202
+ * A `FilterBar`, drawn in band two — the blank under the title, which is
203
+ * where search lives — so every app puts its filter in the same place. It
204
+ * takes the blank's row rather than adding one, so the body never moves
205
+ * when a filter opens or closes.
206
+ */
207
+ filter?: ReactNode;
208
+ /**
209
+ * Size the frame to the terminal, and follow it on resize. Without it the
210
+ * frame is content-sized: as wide as its container and as tall as its body.
211
+ * An explicit `width` or `height` still wins over the window on that axis.
212
+ */
213
+ fill?: boolean;
214
+ width?: number;
215
+ height?: number;
216
+ children: ReactNode;
217
+ };
218
+ /**
219
+ * The one frame every screen of an app sits in — list, detail, loading,
220
+ * error, prompt — so the border and the title row never come and go between
221
+ * states. Six bands inside a round border: title row, the blank under it,
222
+ * tabs where the page has them, body, counter, hints. Presentational: it
223
+ * binds no keys, so a host can take the frame without surrendering its own
224
+ * keyboard handling. `Panel` stays for inner regions inside `Columns`.
225
+ *
226
+ * The title row is the cockpit's, made general: brand · count · user · scope ·
227
+ * alert · status, then a dotted rule to the edge. The count is padded in
228
+ * front rather than behind so the header does not shuffle as it changes and
229
+ * the digits stay against their noun. Only the status segment changes width
230
+ * from one refresh to the next, and the rule after it absorbs the difference.
231
+ *
232
+ * A detail page keeps the app's title row rather than taking the item's key
233
+ * as its title: the title row is how you know which app you are in and how
234
+ * stale it is, and neither changes when you open an item. The item's key
235
+ * belongs in `scope` as a breadcrumb.
236
+ */
237
+ declare const Page: ({ title, icon, count, noun, user, scope, alert, critical, status, facts, tabs, counter, hints, page, help, gap, filter, fill, width, height, children, }: PageProps) => React__default.JSX.Element;
238
+ /** Lines the frame itself spends around the body: two borders, the title row, the blank under it. */
239
+ declare const PAGE_CHROME = 4;
240
+
241
+ type KeyValueProps = {
242
+ label: string;
243
+ value: React__default.ReactNode;
244
+ labelWidth?: number;
245
+ };
246
+ declare const KeyValue: ({ label, value, labelWidth }: KeyValueProps) => React__default.JSX.Element;
247
+
248
+ type LoadingScreenProps = {
249
+ label: string;
250
+ };
251
+ declare const LoadingScreen: ({ label }: LoadingScreenProps) => React__default.JSX.Element;
252
+
253
+ type PanelProps = {
254
+ /** Optional title shown in the panel's header row. */
255
+ title?: string;
256
+ /** Accent colour for the border + title when focused. Defaults to the info token. */
257
+ color?: string;
258
+ /** When true the border brightens to `color` and the title gains a ● marker. */
259
+ focused?: boolean;
260
+ width?: number;
261
+ height?: number;
262
+ flexGrow?: number;
263
+ children: ReactNode;
264
+ };
265
+ declare const Panel: ({ title, color, focused, width, height, flexGrow, children, }: PanelProps) => React__default.JSX.Element;
266
+
267
+ type ColumnsProps = {
268
+ /** Horizontal gap between columns (character cells). Defaults to 1. */
269
+ gap?: number;
270
+ children: ReactNode;
271
+ };
272
+ declare const Columns: ({ gap, children }: ColumnsProps) => React__default.JSX.Element;
273
+
274
+ type SelectableRowProps = {
275
+ active?: boolean;
276
+ marker?: string;
277
+ children: React__default.ReactNode;
278
+ };
279
+ declare const SelectableRow: ({ active, marker, children, }: SelectableRowProps) => React__default.JSX.Element;
280
+
281
+ type SelectOption<T extends string = string> = {
282
+ label: string;
283
+ value: T;
284
+ };
285
+ type SelectProps<T extends string> = {
286
+ options: SelectOption<T>[];
287
+ onChange?: (value: T) => void;
288
+ onSubmit?: (value: T) => void;
289
+ defaultValue?: T;
290
+ isDisabled?: boolean;
291
+ visibleOptionCount?: number;
292
+ };
293
+ declare const Select: <T extends string>({ options, onChange, onSubmit, defaultValue, isDisabled, visibleOptionCount, }: SelectProps<T>) => React__default.JSX.Element;
294
+
295
+ type MultiSelectProps<T extends string> = {
296
+ options: SelectOption<T>[];
297
+ onChange?: (values: T[]) => void;
298
+ onSubmit?: (values: T[]) => void;
299
+ defaultValue?: T[];
300
+ isDisabled?: boolean;
301
+ visibleOptionCount?: number;
302
+ };
303
+ declare const MultiSelect: <T extends string>({ options, onChange, onSubmit, defaultValue, isDisabled, visibleOptionCount, }: MultiSelectProps<T>) => React__default.JSX.Element;
304
+
305
+ type TextInputProps = {
306
+ placeholder?: string;
307
+ defaultValue?: string;
308
+ onChange?: (value: string) => void;
309
+ onSubmit?: (value: string) => void;
310
+ /**
311
+ * Called on `esc`. The value is kept — cancelling blurs the field, it does
312
+ * not clear it — so the host can drop focus and let its own back key
313
+ * take over, without a second `useInput` around the box.
314
+ */
315
+ onCancel?: () => void;
316
+ isDisabled?: boolean;
317
+ };
318
+ declare const TextInput: ({ placeholder, defaultValue, onChange, onSubmit, onCancel, isDisabled, }: TextInputProps) => React__default.JSX.Element;
319
+
320
+ type EmailInputProps = {
321
+ placeholder?: string;
322
+ defaultValue?: string;
323
+ domains?: string[];
324
+ onChange?: (value: string) => void;
325
+ onSubmit?: (value: string) => void;
326
+ isDisabled?: boolean;
327
+ };
328
+ declare const EmailInput: ({ placeholder, defaultValue, domains, onChange, onSubmit, isDisabled, }: EmailInputProps) => React__default.JSX.Element;
329
+
330
+ type PasswordInputProps = {
331
+ placeholder?: string;
332
+ mask?: string;
333
+ onChange?: (value: string) => void;
334
+ onSubmit?: (value: string) => void;
335
+ isDisabled?: boolean;
336
+ };
337
+ declare const PasswordInput: ({ placeholder, mask, onChange, onSubmit, isDisabled, }: PasswordInputProps) => React__default.JSX.Element;
338
+
339
+ type ConfirmInputProps = {
340
+ defaultChoice?: "confirm" | "cancel";
341
+ onConfirm?: () => void;
342
+ onCancel?: () => void;
343
+ isDisabled?: boolean;
344
+ };
345
+ declare const ConfirmInput: ({ defaultChoice, onConfirm, onCancel, isDisabled, }: ConfirmInputProps) => React__default.JSX.Element;
346
+
347
+ type ItemProps$1 = {
348
+ children: React__default.ReactNode;
349
+ };
350
+ type UnorderedListProps = {
351
+ children: React__default.ReactNode;
352
+ };
353
+ declare const UnorderedList: (({ children }: UnorderedListProps) => React__default.JSX.Element) & {
354
+ Item: ({ children }: ItemProps$1) => React__default.JSX.Element;
355
+ };
356
+
357
+ type ItemProps = {
358
+ children: React__default.ReactNode;
359
+ _index?: number;
360
+ };
361
+ type OrderedListProps = {
362
+ children: React__default.ReactNode;
363
+ };
364
+ declare const OrderedList: (({ children }: OrderedListProps) => React__default.JSX.Element) & {
365
+ Item: ({ children, _index }: ItemProps) => React__default.JSX.Element;
366
+ };
367
+
368
+ type Span = {
369
+ text: string;
370
+ color?: string;
371
+ bold?: boolean;
372
+ dim?: boolean;
373
+ italic?: boolean;
374
+ };
375
+ type StyledLine = {
376
+ text: string;
377
+ color?: string;
378
+ bold?: boolean;
379
+ dim?: boolean;
380
+ spans?: Span[];
381
+ };
382
+ type ScrollViewProps = {
383
+ lines: StyledLine[];
384
+ showIndicator?: boolean;
385
+ initialStart?: number;
386
+ isActive?: boolean;
387
+ };
388
+ declare const ScrollView: ({ lines, showIndicator, initialStart, isActive, }: ScrollViewProps) => React__default.JSX.Element;
389
+
390
+ type TabItem<T extends string = string> = {
391
+ value: T;
392
+ label: string;
393
+ /**
394
+ * A single glyph drawn between the `marker` gutter and the label, one space
395
+ * before the label — a Nerd Font char, a geometric shape, whatever names the
396
+ * tab at a glance.
397
+ *
398
+ * It is part of the label, not a second gutter: it takes the label's own
399
+ * style (bold in the accent when active, dim when not) and the active rule
400
+ * spans icon, space and label together. The marker keeps its own cell and its
401
+ * own colour, so a pulse appearing beside an icon still moves nothing.
402
+ * Measured with `string-width` rather than counted, so a wide glyph prices
403
+ * its own columns.
404
+ *
405
+ * It is also what a narrow terminal folds to: when the strip does not fit its
406
+ * `width`, an inactive tab with an icon drops its label and renders `icon
407
+ * count`. Omit it and the tab never folds.
408
+ */
409
+ icon?: string;
410
+ /**
411
+ * How many things the tab holds, drawn as a soft chip beside the label — no
412
+ * parentheses. `null` means "not known yet" and draws a chip holding `–`,
413
+ * padded in front to the width of a two-digit count, so the bar does not
414
+ * shift when the real numbers land. Omit it for a tab with no count. With
415
+ * `NO_COLOR` the chip falls back to the parenthesised `(–)` form.
416
+ */
417
+ count?: number | null;
418
+ /**
419
+ * A marker drawn immediately before the label, in its own colour.
420
+ *
421
+ * Give every tab one of the SAME WIDTH, or none at all. A marker that appears
422
+ * on one tab alone pushes every tab after it sideways — which is the whole
423
+ * reason this is a field rather than something a caller prepends to `label`:
424
+ * a bar that shifts when news arrives is a bar you have to re-find. A blank of
425
+ * the right width is how you say "not this one".
426
+ *
427
+ * Its own `Text` because the label's colour answers "is this tab active" and a
428
+ * marker usually answers something else; folded together, the marker would
429
+ * have to borrow the answer to the wrong question.
430
+ *
431
+ * Animating one costs nothing: the cell is already reserved, so a caller
432
+ * cycling the glyph or the colour per frame moves no layout at all.
433
+ */
434
+ marker?: string;
435
+ markerColor?: string;
436
+ /**
437
+ * The true size of what `count` is a window onto, drawn inside the count's
438
+ * own chip as `count/total` — one chip, never two.
439
+ *
440
+ * A field rather than something a caller folds into `label`, for the same
441
+ * reason `marker` is one — and this one was learned the hard way. A caller
442
+ * with no channel for "there are more than these" smuggled it into the label
443
+ * as `Issues +129 (20)`, and `+` is an operator: it tells the reader to add,
444
+ * with nothing on screen to add it to. The arithmetic even works, which is
445
+ * what makes it vicious — a reader who obeys gets a real number for a question
446
+ * nobody asked, with no signal they have misread anything.
447
+ *
448
+ * A fraction is part-of-whole, the most over-learned notation there is: page 3
449
+ * of 12. Nobody computes with a slash. The distinction rides on a glyph rather
450
+ * than a hue, so it survives dimming, greyscale and colourblindness — and the
451
+ * magnitude survives with it, which is the half that matters most. `30/37`
452
+ * and `20/149` say very different things about how far a tab can be trusted,
453
+ * where a bare truncation flag says only "incomplete".
454
+ *
455
+ * With `NO_COLOR` the chip falls back to the parenthesised `(count/total)`
456
+ * form.
457
+ *
458
+ * Omit it when the count IS the whole, so one number keeps meaning "complete"
459
+ * and two always mean "you are looking at a sample".
460
+ */
461
+ total?: number;
462
+ /**
463
+ * Optional group name. A divider is drawn between tabs whose group value
464
+ * changes. Tabs with no group, or the same group as their neighbour, render
465
+ * without a divider.
466
+ */
467
+ group?: string;
468
+ };
469
+ type TabsProps<T extends string> = {
470
+ active: T;
471
+ items: TabItem<T>[];
472
+ /**
473
+ * The width the strip may spend. Defaults to the terminal's columns; when
474
+ * that is unknown — piped output, a captured stdout — the strip never folds,
475
+ * because a fold is a judgement about a visible edge.
476
+ *
477
+ * Folding is all-or-nothing and measured, never a breakpoint: when the full
478
+ * strip (every marker gutter, icon, label, chip, gap and divider) does not
479
+ * fit, each inactive tab WITH an icon drops its label and renders `icon` plus
480
+ * its chip. The active tab always keeps its full form, tabs without an icon
481
+ * never fold, and dividers stay where they were.
482
+ */
483
+ width?: number;
484
+ };
485
+ declare const Tabs: <T extends string>({ active, items, width, }: TabsProps<T>) => React__default.JSX.Element;
486
+
487
+ /** Inspired by the MIT-licensed Stepper in rigerc/nosey. */
488
+ interface StepContext {
489
+ currentStep: number;
490
+ totalSteps: number;
491
+ isFirst: boolean;
492
+ isLast: boolean;
493
+ isValidating: boolean;
494
+ /** Enabled only when the parent opts into the Stepper's Enter shortcut. */
495
+ keyboardNav: boolean;
496
+ goNext: () => Promise<void>;
497
+ goBack: () => Promise<void>;
498
+ /** Jump without canProceed, but still run onExitStep. */
499
+ goTo: (index: number) => Promise<void>;
500
+ cancel: () => void;
501
+ }
502
+ interface StepProps {
503
+ name: string;
504
+ canProceed?: boolean | (() => boolean | Promise<boolean>);
505
+ children: ReactNode | ((context: StepContext) => ReactNode);
506
+ }
507
+ /** Marker component: only the active Step's content is mounted. */
508
+ declare const Step: (_props: StepProps) => null;
509
+ interface StepperProps {
510
+ /** Step elements or fragments of Steps; key conditional/reorderable Steps. */
511
+ children: ReactNode;
512
+ onComplete: () => void;
513
+ onCancel?: () => void;
514
+ onStepChange?: (index: number) => void;
515
+ /** Returning false blocks leaving the current step. */
516
+ onExitStep?: (index: number) => void | boolean | Promise<void | boolean>;
517
+ onEnterStep?: (index: number) => void;
518
+ onError?: (cause: unknown) => void;
519
+ /** Controlled, zero-based position. */
520
+ step?: number;
521
+ initialStep?: number;
522
+ /**
523
+ * Listen to Enter to advance. Off by default: Ink broadcasts input to all
524
+ * active useInput listeners, and nested inputs may already own Enter.
525
+ * Esc/backspace/q always belong to the host's useAppKeys.
526
+ */
527
+ keyboardNav?: boolean;
528
+ /** Use ASCII symbols in the built-in progress display. */
529
+ ascii?: boolean;
530
+ showProgress?: boolean;
531
+ renderProgress?: (props: StepperProgressProps) => ReactNode;
532
+ }
533
+ interface StepperProgressProps {
534
+ steps: readonly {
535
+ id: string;
536
+ name: string;
537
+ }[];
538
+ /** steps.length means all steps have been completed. */
539
+ currentStep: number;
540
+ orientation?: "horizontal" | "vertical";
541
+ /** Use printable ASCII symbols rather than Unicode. */
542
+ ascii?: boolean;
543
+ }
544
+ declare const StepperProgress: ({ steps, currentStep, orientation, ascii, }: StepperProgressProps) => React__default.JSX.Element;
545
+ declare const useStepper: () => StepContext;
546
+ /** Hints follow the FooterHints visual language; the host still owns Back. */
547
+ declare const StepperNavigation: () => React__default.JSX.Element;
548
+ declare const Stepper: ({ children, onComplete, onCancel, onStepChange, onExitStep, onEnterStep, onError, step, initialStep, keyboardNav, ascii, showProgress, renderProgress, }: StepperProps) => React__default.JSX.Element | null;
549
+
550
+ type UseTabsOptions<T extends string> = {
551
+ initial?: T;
552
+ isActive?: boolean;
553
+ /**
554
+ * Whether ←→ move between tabs as well as Tab / Shift+Tab. On by default:
555
+ * a screen whose ←→ mean something else — a yes/no toggle, a horizontal
556
+ * scroll — turns them off here rather than fighting the hook for the keys.
557
+ */
558
+ arrows?: boolean;
559
+ };
560
+ declare const useTabs: <T extends string>(items: TabItem<T>[], { initial, isActive, arrows }?: UseTabsOptions<T>) => {
561
+ active: T | undefined;
562
+ setActive: React.Dispatch<React.SetStateAction<T | undefined>>;
563
+ };
564
+
565
+ type PaletteItem = {
566
+ id: string;
567
+ /** What is matched and, absent `label`, what is drawn. */
568
+ title: string;
569
+ /**
570
+ * What is drawn instead of `title` when a row needs more than one ink — a
571
+ * ticket key in the accent beside a destination word in `info`. Matching
572
+ * still reads `title`, so a host that draws rich rows keeps them findable.
573
+ */
574
+ label?: ReactNode;
575
+ /** Rows sharing a group are drawn under one header while the query is empty. */
576
+ group?: string;
577
+ /** Dim trailing text: a description, a path, what the row will do. */
578
+ hint?: string;
579
+ keywords?: string[];
580
+ /**
581
+ * One cell, drawn before the title. Every row in a list carries a marker of
582
+ * the same width or none — the cell is reserved for all rows the moment one
583
+ * row has one, so a marker arriving never shifts its neighbours.
584
+ */
585
+ marker?: string;
586
+ markerColor?: string;
587
+ };
588
+ /**
589
+ * A message with a tone. `muted` (the default) is what the query did not find
590
+ * or what is not configured; `error` is a failure, drawn in `colors.error`
591
+ * behind the error glyph so it reads as one without the hue. `onSubmit` makes
592
+ * Enter act while the message shows — a failure the host can retry — where a
593
+ * bare message leaves Enter a no-op.
594
+ */
595
+ type PaletteMessage = {
596
+ text: string;
597
+ tone?: "muted" | "error";
598
+ onSubmit?: () => void;
599
+ };
600
+ type CommandPaletteProps = {
601
+ /** The rows to draw, in order. The palette never filters them. */
602
+ items: PaletteItem[];
603
+ query: string;
604
+ onQueryChange: (query: string) => void;
605
+ onSelect: (id: string) => void;
606
+ /** `esc`. The value is the host's, so nothing here clears it. */
607
+ onClose?: () => void;
608
+ /**
609
+ * One line drawn where the rows would be, when there are none: what
610
+ * the query did not find, or what is not configured. Enter is a no-op while
611
+ * it shows, unless the message carries `onSubmit`. Never a row, so the
612
+ * cursor cannot land on it. A string is a muted message; pass a
613
+ * `PaletteMessage` for the error tone.
614
+ */
615
+ message?: string | PaletteMessage;
616
+ /**
617
+ * One muted line drawn where the rows would be while the palette is still
618
+ * waiting for its first query: `items` is empty and the query is too, e.g.
619
+ * "type a ticket key". Loses to `message`, and never shows beside rows —
620
+ * once the host has rows (grouped or flat) they are drawn instead.
621
+ */
622
+ emptyHint?: string;
623
+ placeholder?: string;
624
+ width?: number;
625
+ /** Visual lines the row area may spend before it scrolls. */
626
+ maxRows?: number;
627
+ hints?: Hint[];
628
+ isActive?: boolean;
629
+ };
630
+ /**
631
+ * A launcher: a query line over a list of rows, one cursor, Enter runs the row
632
+ * under it. Dumb by design — it holds no query state of its own and does no
633
+ * filtering, so the same component serves a host that narrows a fixed command
634
+ * tree (hand `fuzzyFilter` to `onQueryChange`) and a host that DERIVES rows from
635
+ * the query, where `ACC-1234` becomes two verbs. Uncontrolled in the keyboard
636
+ * sense: the query field owns typing, `↑↓` move the cursor, `⏎` selects, `esc`
637
+ * calls `onClose`. Mount it as the topmost layer and let the app's `useAppKeys`
638
+ * stand down while it is up, or `esc` will close it and go back a level too.
639
+ */
640
+ declare const CommandPalette: ({ items, query, onQueryChange, onSelect, onClose, message, emptyHint, placeholder, width, maxRows, hints, isActive, }: CommandPaletteProps) => React__default.JSX.Element;
641
+
642
+ /**
643
+ * The static-list half of a launcher, as a pure function: the items that match
644
+ * `query`, substring hits first, in the caller's order within each rank. An
645
+ * empty or whitespace query returns every item unchanged — the palette then
646
+ * draws them grouped — so a host that only ever has a fixed command tree can
647
+ * hand this straight to `onQueryChange` and never filter itself.
648
+ *
649
+ * Deliberately NOT inside `CommandPalette`: the palette takes whatever rows the
650
+ * host gives it and knows nothing about where they came from, which is what
651
+ * lets a host derive rows from the query (a ticket key becomes two verbs) rather
652
+ * than narrow a preset list. Both shapes share one component; only this helper
653
+ * is optional.
654
+ */
655
+ declare const fuzzyFilter: (items: PaletteItem[], query: string) => PaletteItem[];
656
+
657
+ type UseListCursorOptions = {
658
+ initial?: number;
659
+ wrap?: boolean;
660
+ vimKeys?: boolean;
661
+ isActive?: boolean;
662
+ };
663
+ declare const useListCursor: (length: number, { initial, wrap, vimKeys, isActive, }?: UseListCursorOptions) => {
664
+ cursor: number;
665
+ setCursor: React.Dispatch<React.SetStateAction<number>>;
666
+ moveCursor: (delta: number) => void;
667
+ };
668
+
669
+ type UseAppKeysOptions = {
670
+ /**
671
+ * The host's peel, called on `esc` and `backspace`: close the topmost layer
672
+ * it owns — a focused input, an overlay, a detail screen — and return
673
+ * `true`. Return `false` when there was nothing to pop; `atRoot` then says
674
+ * what happens. A `void` return counts as handled.
675
+ */
676
+ onBack?: () => boolean | void;
677
+ /** What `q` does. Defaults to Ink's `exit()`, which restores the terminal. */
678
+ onQuit?: () => void;
679
+ /** `esc` with nothing left to pop: nothing (the default) or quit. */
680
+ atRoot?: "ignore" | "quit";
681
+ /** False while a text field has focus — `q` is a letter there and `backspace` deletes. */
682
+ isActive?: boolean;
683
+ };
684
+ /**
685
+ * The three keys that belong to the APP, never to a screen: `q` quits from
686
+ * anywhere, `esc` and `backspace` go back exactly one level. Mounted once, at
687
+ * the root — Ink runs every active `useInput` on every key with no order and
688
+ * no propagation, so "who gets `esc`" is only ever solved by there being one
689
+ * claimant. Not a back stack: a host's layers are a priority order over its
690
+ * own booleans, and that order stays in the host as `onBack`.
691
+ *
692
+ * `esc` at the root does nothing by default. A key that sometimes quits is a
693
+ * key you flinch from; quitting has its own key.
694
+ */
695
+ declare const useAppKeys: ({ onBack, onQuit, atRoot, isActive, }?: UseAppKeysOptions) => void;
696
+
697
+ type UseFilterModeOptions = {
698
+ /** False while something above the list owns the keyboard — an overlay, a palette. */
699
+ isActive?: boolean;
700
+ /** Called after the term changes, with the new one. Typically `setCursor(0)`. */
701
+ onChange?: (term: string | null) => void;
702
+ };
703
+ /**
704
+ * A list filter with two modes, vim's split between inserting and acting.
705
+ *
706
+ * TYPING: `/` opens the field and every printable key goes into the term.
707
+ * `↵` and `esc` both leave typing and KEEP the filter (an empty term leaves
708
+ * no filter); `ctrl+u` clears the term and stays in the field.
709
+ * ↑↓ are not taken, so a host's `useListCursor` still walks the matches while
710
+ * the field is open — pass it `vimKeys: !typing` so `j`/`k` type instead.
711
+ *
712
+ * KEPT: the filter stays applied and the letter keys are the host's hotkeys
713
+ * again. `/` goes back INTO the term rather than starting over — the whole
714
+ * point of keeping it is being able to refine it. `esc` here is not this
715
+ * hook's: a kept filter is the bottom layer of the host's peel, so the host's
716
+ * `onBack` clears it through `clear()`, after every layer pushed above it.
717
+ * `esc` twice therefore clears a filter from anywhere.
718
+ *
719
+ * Owns its own `useInput`, like `useListCursor`. The host stands its other
720
+ * handlers down while `typing` is true — `useAppKeys({ isActive: !typing })`
721
+ * and an early return in its hotkey handler — because inside the field `q` is
722
+ * a letter, and Ink hands every key to every active hook.
723
+ *
724
+ * Extracted from gh-cockpit's inbox, where `/` used to clear on the way back
725
+ * in, so refining a kept query meant typing it again from scratch.
726
+ */
727
+ declare const useFilterMode: ({ isActive, onChange, }?: UseFilterModeOptions) => {
728
+ term: string | null;
729
+ typing: boolean;
730
+ caret: number;
731
+ active: boolean;
732
+ clear: () => void;
733
+ hints: Hint[];
734
+ };
735
+
736
+ type FilterBarProps = {
737
+ /** From `useFilterMode`. `null` draws nothing. */
738
+ term: string | null;
739
+ /** From `useFilterMode`. Draws the caret. */
740
+ typing: boolean;
741
+ /** From `useFilterMode`. Where the caret sits; defaults to end-of-term. */
742
+ caret?: number;
743
+ /** How many rows survive the filter. Omit to draw no count. */
744
+ matches?: number;
745
+ };
746
+ /**
747
+ * The line a `useFilterMode` filter draws: `/ term▏ 3 matches`. The caret
748
+ * is there only while typing, which is the one visible difference between a
749
+ * field that takes your keys and a filter that is merely standing — so it is
750
+ * the thing that tells you whether `c` is a letter or a command.
751
+ *
752
+ * Presentational and keyless. It sits in the `Page`'s `filter` slot, band two,
753
+ * so every app draws its filter in the same place.
754
+ */
755
+ declare const FilterBar: ({ term, typing, caret, matches }: FilterBarProps) => React__default.JSX.Element | null;
756
+
757
+ type SwitchValue = "left" | "right";
758
+ type SwitchProps = {
759
+ left: string;
760
+ right: string;
761
+ value: SwitchValue;
762
+ color?: string;
763
+ };
764
+ declare const Switch: ({ left, right, value, color, }: SwitchProps) => React__default.JSX.Element;
765
+
766
+ type ToggleProps = {
767
+ label: string;
768
+ on: boolean;
769
+ color?: string;
770
+ };
771
+ declare const Toggle: ({ label, on, color }: ToggleProps) => React__default.JSX.Element;
772
+
773
+ type ToggleSwitchProps = {
774
+ on: boolean;
775
+ label?: string;
776
+ onColor?: string;
777
+ offColor?: string;
778
+ };
779
+ declare const ToggleSwitch: ({ on, label, onColor, offColor, }: ToggleSwitchProps) => React__default.JSX.Element;
780
+
781
+ type UpdateBannerProps = {
782
+ name: string;
783
+ current: string;
784
+ latest: string;
785
+ command: string;
786
+ onConfirm: () => void;
787
+ onDecline: () => void;
788
+ focused?: boolean;
789
+ };
790
+ declare const UpdateBanner: ({ name, current, latest, command, onConfirm, onDecline, focused, }: UpdateBannerProps) => React__default.JSX.Element;
791
+
792
+ type ProgressBarProps = {
793
+ value: number;
794
+ width?: number;
795
+ color?: string;
796
+ trackColor?: string;
797
+ };
798
+ declare const ProgressBar: ({ value, width, color, trackColor, }: ProgressBarProps) => React__default.JSX.Element;
799
+
800
+ type SkeletonBarProps = {
801
+ /** Cells to draw. */
802
+ width: number;
803
+ /**
804
+ * Settle for the plain track bar. Defaults to true; still draws static when
805
+ * stdout is not a TTY or motion is opted out, whatever is passed.
806
+ */
807
+ animate?: boolean;
808
+ /**
809
+ * A frame on the shared clock, passed by `SkeletonRows` so every row ticks
810
+ * together. Omit it and the bar keeps its own clock — that is the standalone
811
+ * behaviour.
812
+ */
813
+ frame?: number;
814
+ /**
815
+ * Stagger within a shared clock, in frames. `SkeletonRows` passes each row's
816
+ * lag; a standalone bar leaves it at 0.
817
+ */
818
+ rowOffset?: number;
819
+ };
820
+ /**
821
+ * A placeholder for text that has not arrived: a run of `█` over the track
822
+ * ground, swept left to right by a soft highlight band that loops after a
823
+ * short pause.
824
+ *
825
+ * Animated on purpose — amended 2026-10-05. This used to be static, on the
826
+ * grounds that a shimmer is motion with nothing behind it and the status slot
827
+ * is where a page says it is busy. In practice a first load with nothing on
828
+ * screen reads as frozen without motion: the status slot says busy, but there
829
+ * is no body yet for it to reassure. Still draws the plain static bar when
830
+ * there is nowhere for motion to show — stdout piped, `NO_MOTION` or
831
+ * `REDUCE_MOTION` set — or when `animate` is false.
832
+ */
833
+ declare const SkeletonBar: ({ width, animate, frame, rowOffset, }: SkeletonBarProps) => React__default.JSX.Element;
834
+ type SkeletonRowsProps = {
835
+ /** How many bars to draw. Blank gap rows are extra. */
836
+ rows: number;
837
+ /**
838
+ * Each bar's length as a fraction (0–1) of the width left after `indent`,
839
+ * cycled in order: row `i` takes `widths[i % widths.length]`. Deterministic,
840
+ * so the placeholder does not reshuffle on every render.
841
+ */
842
+ widths: number[];
843
+ /** Insert one blank row after every `gapEvery` bars, never after the last. */
844
+ gapEvery?: number;
845
+ /** Leading blank cells before every bar. Defaults to 0. */
846
+ indent?: number;
847
+ /**
848
+ * The width the fractions are taken of. Omit it and the rows measure the
849
+ * space their container gives them, re-measuring when the terminal resizes.
850
+ */
851
+ width?: number;
852
+ /**
853
+ * Settle every row for the plain track bar. Passed through to each
854
+ * `SkeletonBar`; defaults to true.
855
+ */
856
+ animate?: boolean;
857
+ };
858
+ /**
859
+ * The body of a list that has nothing to show yet: `rows` skeleton bars whose
860
+ * lengths cycle through `widths`, grouped by blank rows when `gapEvery` is set.
861
+ * Only for the first load, when nothing is known — once real rows exist, keep
862
+ * them on screen and say busy in `Page`'s status slot instead.
863
+ *
864
+ * One clock for the group: a single interval here hands each bar its frame,
865
+ * staggered by row so the wave travels diagonally down-and-across rather than
866
+ * marching in lockstep.
867
+ */
868
+ declare const SkeletonRows: ({ rows, widths, gapEvery, indent, width, animate, }: SkeletonRowsProps) => React__default.JSX.Element;
869
+
870
+ type StatusVariant = "success" | "error" | "warning" | "info";
871
+
872
+ type StatusMessageProps = {
873
+ variant?: StatusVariant;
874
+ children: React__default.ReactNode;
875
+ };
876
+ declare const StatusMessage: ({ variant, children, }: StatusMessageProps) => React__default.JSX.Element;
877
+
878
+ type AlertProps = {
879
+ variant?: StatusVariant;
880
+ title?: string;
881
+ children: React__default.ReactNode;
882
+ };
883
+ declare const Alert: ({ variant, title, children }: AlertProps) => React__default.JSX.Element;
884
+
885
+ type ToastProps = {
886
+ message: string;
887
+ variant?: StatusVariant;
888
+ duration?: number;
889
+ onDone?: () => void;
890
+ };
891
+ declare const Toast: ({ message, variant, duration, onDone, }: ToastProps) => React__default.JSX.Element | null;
892
+
893
+ type IconMode = "text" | "nerd";
894
+ declare const setIconMode: (mode: IconMode) => void;
895
+ declare const getIconMode: () => IconMode;
896
+
897
+ type NotifyOptions = {
898
+ title?: string;
899
+ subtitle?: string;
900
+ sound?: boolean | string;
901
+ };
902
+ declare const notify: (message: string, options?: NotifyOptions) => void;
903
+
904
+ /**
905
+ * The raw hexes behind every token, tuned to the dark ground `#1a1b27`.
906
+ * Dark-only and truecolour assumed; on a 256-colour terminal chalk downsamples
907
+ * them (`tokens.test.ts` checks the pairs that must stay apart). Hex rather
908
+ * than ANSI names, because the terminal theme repaints a named colour.
909
+ */
910
+ declare const palette: {
911
+ readonly orange: "#FF8C00";
912
+ readonly amber: "#FFB400";
913
+ readonly yellow: "#FFCB6B";
914
+ readonly red: "#FF5364";
915
+ readonly mint: "#5FD7A7";
916
+ readonly green: "#4ADE80";
917
+ readonly coral: "#FF6B6B";
918
+ readonly blue: "#82AAFF";
919
+ readonly cyan: "#89DDFF";
920
+ readonly purple: "#C792EA";
921
+ readonly ink: "#E4E4EC";
922
+ readonly grey: "#999999";
923
+ readonly slate: "#75767E";
924
+ readonly dusk: "#5C5E73";
925
+ readonly selection: "#30364A";
926
+ readonly track: "#2B323D";
927
+ readonly trackHigh: "#6B7480";
928
+ readonly ground: "#1A1B27";
929
+ };
930
+ type Palette = (typeof palette)[keyof typeof palette];
931
+ /**
932
+ * `secondary` is the third neutral: text that is context rather than the
933
+ * answer — a row's labels beside its title — and must read as its own tier.
934
+ * Default foreground is the answer, `muted` (measured #75767E) is furniture,
935
+ * and a middle tier drawn in either of those is not "quiet", it is absent: on
936
+ * a dark ground `dimColor` inks at L* 50 and the eye files it with the age
937
+ * column. Hex rather than an ANSI name because the tier only exists as a
938
+ * measured step — `#999999` is L* 63, 5.9:1 on `#1c1c28` and 13 L* above the
939
+ * faint, which reads as a decision; `#888888` is 7 L* above it and reads as a
940
+ * rendering wobble. Tuned to the dark ground every consumer already commits
941
+ * to; a light-theme terminal would need its own value.
942
+ *
943
+ * `track` is the unlit ground of a measured strip: the empty part of a
944
+ * `ProgressBar`, and the base of a `SkeletonBar` standing in for text that
945
+ * has not arrived. Furniture, never a state — kept clearly darker than any
946
+ * fill drawn over it, so the two separate by lightness for every reader.
947
+ *
948
+ * `trackHighlight` is the crest of that ground: the lighter step a skeleton's
949
+ * shimmer band peaks at while it sweeps, with the falloff each side blended
950
+ * between the two. Still ground, never ink — nothing is ever written in it,
951
+ * which is why it can sit well above `track` without becoming a state.
952
+ */
953
+ declare const colors: {
954
+ readonly accent: "#FF8C00";
955
+ readonly secondary: "#999999";
956
+ readonly muted: "#75767E";
957
+ readonly success: "#5FD7A7";
958
+ readonly error: "#FF5364";
959
+ readonly warning: "#FFB400";
960
+ readonly info: "#89DDFF";
961
+ readonly group: "#C792EA";
962
+ readonly track: "#2B323D";
963
+ readonly trackHighlight: "#6B7480";
964
+ readonly ticket: "#FFCB6B";
965
+ readonly pr: "#FF8C00";
966
+ readonly link: "#82AAFF";
967
+ readonly added: "#4ADE80";
968
+ readonly removed: "#FF6B6B";
969
+ readonly pending: "#75767E";
970
+ readonly dim: "#5C5E73";
971
+ readonly selection: "#30364A";
972
+ };
973
+ type Color = (typeof colors)[keyof typeof colors];
974
+ /**
975
+ * The soft family: a filled block that is quiet enough to live on every row.
976
+ *
977
+ * Solid `colors` are events and shout on purpose. A permanent classifier —
978
+ * an issue type, a category — sits on a dark ground beside the row's real
979
+ * news, and a saturated fill there out-shouts it. These are the same
980
+ * semantics, desaturated and darkened so white ink reads on all seven,
981
+ * measured against `#1e1f26` for a deutan/protan reader. Hex rather than
982
+ * ANSI names because the point is a fixed, measured contrast, and a named
983
+ * colour is whatever the theme says it is.
984
+ *
985
+ * `group` is the container of the rows beneath it (an epic). It is
986
+ * deliberately not the accent hue: an accent-filled block on the same row
987
+ * as an accent-coloured key steals the key's colour.
988
+ */
989
+ declare const softColors: {
990
+ readonly accent: "#8f4611";
991
+ readonly muted: "#474a54";
992
+ readonly success: "#2e6d4c";
993
+ readonly error: "#7e2a2c";
994
+ readonly warning: "#8a6a1e";
995
+ readonly info: "#3c74a2";
996
+ readonly group: "#524389";
997
+ };
998
+ type SoftColor = (typeof softColors)[keyof typeof softColors];
999
+ /**
1000
+ * The tonal family: a tint of the hue over the ground, inked in the hue — the
1001
+ * quietest filled form, for chrome on the frame (a count, a slot label)
1002
+ * rather than row data.
1003
+ *
1004
+ * Each fill is a linear blend of the variant's solid hue over `#1A1B27` at a
1005
+ * fixed weight (16% rest, 28% strong), computed once and written in as a
1006
+ * literal — no runtime mixing. The accent and muted pairs are frozen
1007
+ * measurements rather than exact mixes: the focus pill is about 16% orange,
1008
+ * the active tab chip about 28%, and the inactive tab chip a step above the
1009
+ * ground that never dims into it. Measured like `softColors`, for the same
1010
+ * reason: a tint is a fixed contrast, and a named colour is whatever the
1011
+ * theme says it is.
1012
+ *
1013
+ * `tonalStrongColors` is the thing you are ON — the active tab's chip — a
1014
+ * stronger tint of the same hue with the same ink, plus a bold label at the
1015
+ * call site. The keys are the seven `Pill` variant names, written out rather
1016
+ * than imported: tokens never reach back into components.
1017
+ */
1018
+ declare const tonalColors: Record<"success" | "error" | "warning" | "info" | "accent" | "muted" | "group", {
1019
+ fill: string;
1020
+ ink: string;
1021
+ }>;
1022
+ declare const tonalStrongColors: Record<"success" | "error" | "warning" | "info" | "accent" | "muted" | "group", {
1023
+ fill: string;
1024
+ ink: string;
1025
+ }>;
1026
+ /**
1027
+ * Priority is ordinal, so it steps in lightness, never hue: a red chevron
1028
+ * would claim a failure. The chevron's shape carries the rank.
1029
+ */
1030
+ declare const priorityColors: {
1031
+ readonly highest: "#E4E4EC";
1032
+ readonly high: "#E4E4EC";
1033
+ readonly medium: "#999999";
1034
+ readonly low: "#75767E";
1035
+ readonly lowest: "#75767E";
1036
+ };
1037
+ type PriorityColor = (typeof priorityColors)[keyof typeof priorityColors];
1038
+ declare const spacing: {
1039
+ readonly xs: 1;
1040
+ readonly sm: 2;
1041
+ readonly md: 3;
1042
+ readonly lg: 4;
1043
+ };
1044
+ type Spacing = (typeof spacing)[keyof typeof spacing];
1045
+ /**
1046
+ * Semantic terminal glyphs, inspired by the token categories in rigerc/nosey.
1047
+ * The default glyphs match ink-ui's existing StatusMessage and Select
1048
+ * conventions. Shape carries meaning; color only reinforces it.
1049
+ */
1050
+ type Symbols = {
1051
+ /** Filled state marker used by switches, toggles and focused panels. */
1052
+ active: string;
1053
+ /** Nested square markers for lists. */
1054
+ nestedBullet: string;
1055
+ nestedBulletEmpty: string;
1056
+ /** Dotted horizontal rule for page headers. */
1057
+ rule: string;
1058
+ /** Terminal input caret. */
1059
+ caret: string;
1060
+ /** Vertical group separator. */
1061
+ verticalLine: string;
1062
+ success: string;
1063
+ error: string;
1064
+ warn: string;
1065
+ info: string;
1066
+ pointer: string;
1067
+ checkboxChecked: string;
1068
+ checkboxUnchecked: string;
1069
+ bullet: string;
1070
+ pending: string;
1071
+ ellipsis: string;
1072
+ line: string;
1073
+ barFull: string;
1074
+ barEmpty: string;
1075
+ };
1076
+ declare const symbols: Symbols;
1077
+ declare const asciiSymbols: Symbols;
1078
+ /** Border presets are style names accepted by Ink's Box borderStyle prop. */
1079
+ type BorderStyle = "round" | "single" | "bold" | "double" | "classic";
1080
+ declare const borders: {
1081
+ readonly base: "round";
1082
+ readonly focus: "bold";
1083
+ readonly ascii: "classic";
1084
+ };
1085
+ /** Motion and fallback frames are visual tokens, not animation state. */
1086
+ declare const motion: {
1087
+ readonly spinner: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
1088
+ readonly asciiSpinner: readonly ["-", "\\", "|", "/"];
1089
+ readonly intervalMs: 80;
1090
+ /** Skeleton shimmer has a faster independent rhythm. */
1091
+ readonly skeletonIntervalMs: 70;
1092
+ };
1093
+
1094
+ export { Alert, Badge, type BadgeVariant, Banner, type BorderStyle, type Color, type Column, type ColumnAlign, type ColumnOverflow, Columns, CommandPalette, ConfirmInput, EmailInput, FilterBar, FooterHints, Header, type Hint, type IconMode, KeyValue, LoadingScreen, MultiSelect, type NotifyOptions, OrderedList, PAGE_CHROME, Page, type PageStatus, type Palette, type PaletteItem, type PaletteMessage, Panel, PasswordInput, Pill, type PillTone, type PillVariant, type PriorityColor, ProgressBar, ScrollView, Select, type SelectOption, SelectableRow, SkeletonBar, SkeletonRows, type SoftColor, type Spacing, type Span, Spinner, StatusMessage, type StatusVariant, Step, type StepContext, type StepProps, Stepper, StepperNavigation, StepperProgress, type StepperProgressProps, type StepperProps, type StyledLine, Switch, type SwitchValue, type Symbols, type TabItem, Table, Tabs, TextInput, Toast, Toggle, ToggleSwitch, UnorderedList, UpdateBanner, asciiSymbols, borders, colors, fuzzyFilter, getIconMode, motion, notify, palette, pillWidth, priorityColors, setIconMode, softColors, spacing, symbols, tailHints, tonalColors, tonalStrongColors, useAppKeys, useFilterMode, useListCursor, useStepper, useTabs };