@henryavila/blink-tui 0.1.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.
@@ -0,0 +1,2429 @@
1
+ import React from 'react';
2
+ import { BoxProps } from 'ink';
3
+
4
+ /**
5
+ * blink palettes — the raw colour layer (LAYER 1 of the theming contract).
6
+ *
7
+ * A palette is 26 raw colours. It is the **only** thing a theme changes; every
8
+ * semantic token (`--fg`, `--accent`, `--state-ok`, `--domain-*`, …) maps a
9
+ * *role* onto one of these slots, and that role→slot grammar is identical for
10
+ * every theme (see `tokens.ts`). Components never reach for a palette colour;
11
+ * they consume {@link SemanticTokens}.
12
+ *
13
+ * Hex values are a 1:1 port of the `[data-theme]` blocks in the design system's
14
+ * `colors_and_type.css`. This is the one file allowed to carry raw hex (the
15
+ * contract checker enforces that).
16
+ *
17
+ * Slots run darkest → lightest for surfaces / overlays / text, then the 14
18
+ * accent hues. Adding a theme = add one palette here and one entry in
19
+ * `theme.ts` — every component and screen inherits it for free.
20
+ */
21
+ /** A raw palette colour name — the 26 Catppuccin-shaped slots. */
22
+ type PaletteColor = 'crust' | 'mantle' | 'base' | 'surface0' | 'surface1' | 'surface2' | 'overlay0' | 'overlay1' | 'overlay2' | 'subtext0' | 'subtext1' | 'text' | 'rosewater' | 'flamingo' | 'pink' | 'mauve' | 'red' | 'maroon' | 'peach' | 'yellow' | 'green' | 'teal' | 'sky' | 'sapphire' | 'blue' | 'lavender';
23
+ /** The raw palette shape: every colour name mapped to a hex string. */
24
+ type Palette = Record<PaletteColor, string>;
25
+ /** The slot order — darkest→lightest surfaces, then text tiers, then 14 hues. */
26
+ declare const PALETTE_SLOTS: readonly PaletteColor[];
27
+ /**
28
+ * neutral — Catppuccin Mocha. blink's calm dark base palette. `contrast`
29
+ * (neutral+) and `vivid` reuse this exact palette and differ only in how the
30
+ * intent layer spends it (see `theme.ts`).
31
+ */
32
+ declare const neutral: Palette;
33
+ /** nord — frost & polar night · cool, low-chroma. */
34
+ declare const nord: Palette;
35
+ /** gruvbox — retro · warm & earthy. */
36
+ declare const gruvbox: Palette;
37
+ /**
38
+ * tokyonight — saturated indigo night, deepened: a darker canvas so the bright
39
+ * indigo text & accents pop harder. blink's default theme.
40
+ */
41
+ declare const tokyonight: Palette;
42
+ /** latte — Catppuccin Latte · the light theme (same roles, inverted luminance). */
43
+ declare const latte: Palette;
44
+ /** Every named base palette, keyed by id — used by the theme registry. */
45
+ declare const palettes: {
46
+ readonly neutral: Palette;
47
+ readonly nord: Palette;
48
+ readonly gruvbox: Palette;
49
+ readonly tokyonight: Palette;
50
+ readonly latte: Palette;
51
+ };
52
+ /**
53
+ * Catppuccin Mocha — kept as a named export for back-compat. Identical to
54
+ * {@link neutral}; it is the escape hatch for the rare one-off hue.
55
+ */
56
+ declare const catppuccinMocha: Palette;
57
+
58
+ /**
59
+ * Semantic tokens — the layer components actually consume (LAYER 2 of the
60
+ * theming contract).
61
+ *
62
+ * A 1:1 port of the intent `--*` custom properties in the design system's
63
+ * `colors_and_type.css`. Each token maps a *role* ("focus", "an error", "a
64
+ * postgres row") onto a palette slot. This grammar is **identical for every
65
+ * theme** — only the palette underneath it changes — which is what lets a
66
+ * component recolour for free when the surface theme flips. A component never
67
+ * sees a palette name and has no colour prop.
68
+ *
69
+ * In a terminal there is no CSS background; `bg*` tokens are applied via Ink's
70
+ * `backgroundColor` prop (used sparingly — selection fills, inverse hotkeys)
71
+ * and `fg*` tokens via `color`.
72
+ */
73
+ interface SemanticTokens {
74
+ bg: string;
75
+ bgElevated: string;
76
+ bgSunken: string;
77
+ bgPanel: string;
78
+ bgSelected: string;
79
+ bgFocused: string;
80
+ bgInverse: string;
81
+ fg: string;
82
+ fgMuted: string;
83
+ fgDim: string;
84
+ fgFaint: string;
85
+ fgDisabled: string;
86
+ fgInverse: string;
87
+ border: string;
88
+ borderFocus: string;
89
+ borderStrong: string;
90
+ accent: string;
91
+ accentAlt: string;
92
+ link: string;
93
+ highlight: string;
94
+ stateOk: string;
95
+ stateErr: string;
96
+ stateWarn: string;
97
+ statePending: string;
98
+ stateDrift: string;
99
+ stateInfo: string;
100
+ domainBlue: string;
101
+ domainAzure: string;
102
+ domainCyan: string;
103
+ domainGreen: string;
104
+ domainRed: string;
105
+ domainAmber: string;
106
+ domainYellow: string;
107
+ domainViolet: string;
108
+ domainNeutral: string;
109
+ }
110
+ /**
111
+ * Build the default intent mapping for a palette. This is LAYER 2 of the
112
+ * contract — the role→slot grammar that never moves between themes. A theme is
113
+ * just `buildTokens(itsPalette)`, optionally with a few intent overrides layered
114
+ * on top (see `theme.ts`).
115
+ */
116
+ declare function buildTokens(p: Palette): SemanticTokens;
117
+ /** Catppuccin Mocha (neutral) semantic tokens — kept as a named export. */
118
+ declare const mochaTokens: SemanticTokens;
119
+
120
+ /** Dark or light surface — drives a theme picker's grouping, nothing else. */
121
+ type ThemeMode = 'dark' | 'light';
122
+ /**
123
+ * A blink theme bundles a raw {@link Palette} with its {@link SemanticTokens}
124
+ * mapping plus picker metadata. The colour scheme is a property of the terminal
125
+ * *surface*, exactly like a real terminal emulator: a component emits a semantic
126
+ * role and the surface decides the pixels. Switching themes re-renders every
127
+ * component from the new tokens — a component cannot diverge because it owns no
128
+ * colour. The surface is owned in exactly one place (the `ThemeProvider`).
129
+ */
130
+ interface Theme {
131
+ /** Stable id, e.g. `"tokyonight"`. */
132
+ id: string;
133
+ /** Human label for a picker, e.g. `"tokyo night"`. */
134
+ label: string;
135
+ /** Dark or light. */
136
+ mode: ThemeMode;
137
+ /** One-line description for a picker. */
138
+ blurb: string;
139
+ /** Raw palette (escape hatch for one-off hues). */
140
+ palette: Palette;
141
+ /** Role-mapped tokens — what components consume. */
142
+ tokens: SemanticTokens;
143
+ /** @deprecated alias of {@link id}, kept for back-compat. */
144
+ name: string;
145
+ }
146
+ /** Just the picker-facing fields of a {@link Theme}. */
147
+ type ThemeMeta = Pick<Theme, 'id' | 'label' | 'mode' | 'blurb'>;
148
+ /** neutral — Catppuccin Mocha · the calm dark default palette. */
149
+ declare const mocha: Theme;
150
+ /** blink's default theme — `tokyonight`. Used when no theme is selected. */
151
+ declare const defaultTheme: Theme;
152
+ /** Resolve a theme by id (falls back to {@link defaultTheme} for an unknown id). */
153
+ declare function getTheme(id: string): Theme;
154
+ /** True if a theme id is registered. */
155
+ declare function hasTheme(id: string): boolean;
156
+ /** Every registered theme, in picker order. */
157
+ declare function allThemes(): Theme[];
158
+ /** Picker-facing metadata for every theme, in order. */
159
+ declare function listThemes(): ThemeMeta[];
160
+ /** A runtime theme definition — mirrors the design system's `registerTheme()`. */
161
+ interface ThemeDefinition {
162
+ /** Stable id (required). */
163
+ id: string;
164
+ /** Picker label. Defaults to `id`. */
165
+ label?: string;
166
+ /** Dark or light. Defaults to `'dark'`. */
167
+ mode?: ThemeMode;
168
+ /** One-line picker description. */
169
+ blurb?: string;
170
+ /** Base theme id to inherit the palette from. Defaults to `'neutral'`. */
171
+ extends?: string;
172
+ /** Palette slots to override — only list what you change; the rest inherit. */
173
+ palette?: Partial<Palette>;
174
+ /** Intent-token overrides (the "vivid"/"neutral+" trick). */
175
+ intent?: Partial<SemanticTokens>;
176
+ }
177
+ /**
178
+ * Register (or replace) a theme at runtime, from an app's own code — no
179
+ * framework edits, mirroring `registerGlyphs`. Only list the slots you want to
180
+ * change; everything else inherits from `extends` (default `neutral`), so a
181
+ * handful of accent overrides is already a complete theme:
182
+ *
183
+ * ```ts
184
+ * registerTheme({
185
+ * id: 'dracula', label: 'dracula', blurb: 'classic purple dark',
186
+ * extends: 'neutral',
187
+ * // hex strings for just the slots you change — the rest inherit from `extends`
188
+ * palette: { base, surface1, text, lavender, red, green },
189
+ * });
190
+ * // then drive it through the ThemeProvider / useThemeControls().setTheme('dracula')
191
+ * ```
192
+ *
193
+ * Returns the assembled {@link Theme}. Unlike the web engine there is no global
194
+ * "current theme" to `select` — the active theme is owned by the surface (the
195
+ * `ThemeProvider`); switch with `useThemeControls().setTheme(id)`.
196
+ */
197
+ declare function registerTheme(def: ThemeDefinition): Theme;
198
+
199
+ /**
200
+ * The three rendering modes every blink glyph supports.
201
+ *
202
+ * - `nerd` — full Nerd Font vocabulary, incl. private-use domain logos.
203
+ * - `unicode` — safe Unicode glyphs (✓ ✗ ◯ …); domain glyphs degrade to text.
204
+ * - `ascii` — `[x] [!] [ ]` etc. for CI, `TERM=dumb`, and fontless terminals.
205
+ *
206
+ * An app never breaks: if the font is missing, the worst case is text-shaped
207
+ * fallbacks, never tofu boxes (□). Resolved once at startup by
208
+ * `detectIconSet()` and carried in the theme context.
209
+ */
210
+ type IconSet = 'nerd' | 'unicode' | 'ascii';
211
+ /**
212
+ * A glyph's colour, expressed as **intent** — a semantic token key, never a raw
213
+ * hue. A domain glyph paints through a hue family (`'domainBlue'`,
214
+ * `'domainGreen'`, …) so it keeps its relative identity *and* recolours with the
215
+ * active theme. State / nav contract glyphs leave this unset — their colour
216
+ * comes from the state-intent map, not the glyph entry.
217
+ */
218
+ type GlyphColor = keyof SemanticTokens;
219
+ /** A single glyph's three rendering variants, plus an optional owned colour. */
220
+ interface GlyphVariants {
221
+ nerd: string;
222
+ unicode: string;
223
+ ascii: string;
224
+ /**
225
+ * The colour this glyph renders in, owned at registration (the "intent, not
226
+ * style" rule for domain glyphs) and given as a {@link SemanticTokens} key.
227
+ * Components resolve it through the active theme via `glyphColor(name)` and
228
+ * fall back to a muted token when absent.
229
+ */
230
+ color?: GlyphColor;
231
+ }
232
+
233
+ /**
234
+ * What every blink app reads from context: the active {@link Theme}, the
235
+ * resolved {@link IconSet}, and the controls to switch theme. The theme is the
236
+ * one mutable piece of the surface — exactly like a terminal emulator's scheme.
237
+ * The icon set is decided once at boot.
238
+ */
239
+ interface BlinkContextValue {
240
+ theme: Theme;
241
+ iconSet: IconSet;
242
+ /** Switch the active theme by id or by a {@link Theme} object. */
243
+ setTheme: (theme: string | Theme) => void;
244
+ /** Every registered theme's picker metadata, in order. */
245
+ themes: ThemeMeta[];
246
+ }
247
+ interface ThemeProviderProps {
248
+ /**
249
+ * The initial theme — a {@link Theme} object or a registered theme id. When
250
+ * omitted, blink starts on its default (`tokyonight`). Components read the
251
+ * *active* theme, which a picker can switch via `useThemeControls()`.
252
+ */
253
+ theme?: Theme | string;
254
+ /** @deprecated alias of {@link theme} when passing an id. */
255
+ initialTheme?: string;
256
+ /**
257
+ * Resolved icon set. Pass the result of `detectIconSet()` (awaited at boot).
258
+ * Defaults to `'unicode'` — the safe mode that renders everywhere.
259
+ */
260
+ iconSet?: IconSet;
261
+ children: React.ReactNode;
262
+ }
263
+ /**
264
+ * Wrap a blink app once, at the root, above `<App/>` — the surface that owns the
265
+ * colour scheme:
266
+ *
267
+ * ```tsx
268
+ * const iconSet = await detectIconSet();
269
+ * render(
270
+ * <ThemeProvider iconSet={iconSet} theme="tokyonight">
271
+ * <App />
272
+ * </ThemeProvider>
273
+ * );
274
+ * ```
275
+ *
276
+ * The provider holds the active theme in state; a theme picker calls
277
+ * `useThemeControls().setTheme(id)` and the whole tree recolours from the new
278
+ * tokens. No component sets, reads, or branches on the theme — consistency is
279
+ * structural, not a matter of discipline.
280
+ */
281
+ declare function ThemeProvider({ theme, initialTheme, iconSet, children, }: ThemeProviderProps): React.ReactElement;
282
+ /** Full context: `{ theme, iconSet, setTheme, themes }`. */
283
+ declare function useBlink$1(): BlinkContextValue;
284
+ /** The active theme. */
285
+ declare function useTheme(): Theme;
286
+ /** Just the semantic tokens — the common case in components. */
287
+ declare function useTokens(): SemanticTokens;
288
+ /** The resolved icon set (`'nerd' | 'unicode' | 'ascii'`). */
289
+ declare function useIconSet(): IconSet;
290
+ /** The theme switch + list — what a theme picker drives. */
291
+ interface ThemeControls {
292
+ /** The active theme. */
293
+ theme: Theme;
294
+ /** The active theme's id. */
295
+ themeId: string;
296
+ /** Switch theme by id or {@link Theme}. */
297
+ setTheme: (theme: string | Theme) => void;
298
+ /** Every registered theme's metadata, in picker order. */
299
+ themes: ThemeMeta[];
300
+ }
301
+ /**
302
+ * The theme controls for a picker — the TUI analogue of the design system's
303
+ * `BlinkTheme.setTheme / getTheme / THEMES`. The picker is the *only* place that
304
+ * should switch the theme; ordinary components just read {@link useTokens}.
305
+ */
306
+ declare function useThemeControls(): ThemeControls;
307
+
308
+ /** Tier 1 · COMMON_DOMAINS — the dev-tool domains most TUIs reuse. */
309
+ declare const COMMON_DOMAINS: {
310
+ database: {
311
+ nerd: string;
312
+ unicode: string;
313
+ ascii: string;
314
+ color: "domainNeutral";
315
+ };
316
+ mysql: {
317
+ nerd: string;
318
+ unicode: string;
319
+ ascii: string;
320
+ color: "domainAzure";
321
+ };
322
+ postgresql: {
323
+ nerd: string;
324
+ unicode: string;
325
+ ascii: string;
326
+ color: "domainBlue";
327
+ };
328
+ redis: {
329
+ nerd: string;
330
+ unicode: string;
331
+ ascii: string;
332
+ color: "domainRed";
333
+ };
334
+ docker: {
335
+ nerd: string;
336
+ unicode: string;
337
+ ascii: string;
338
+ color: "domainCyan";
339
+ };
340
+ github: {
341
+ nerd: string;
342
+ unicode: string;
343
+ ascii: string;
344
+ color: "domainNeutral";
345
+ };
346
+ git: {
347
+ nerd: string;
348
+ unicode: string;
349
+ ascii: string;
350
+ color: "domainAmber";
351
+ };
352
+ ssh: {
353
+ nerd: string;
354
+ unicode: string;
355
+ ascii: string;
356
+ color: "domainYellow";
357
+ };
358
+ nodejs: {
359
+ nerd: string;
360
+ unicode: string;
361
+ ascii: string;
362
+ color: "domainGreen";
363
+ };
364
+ php: {
365
+ nerd: string;
366
+ unicode: string;
367
+ ascii: string;
368
+ color: "domainViolet";
369
+ };
370
+ python: {
371
+ nerd: string;
372
+ unicode: string;
373
+ ascii: string;
374
+ color: "domainYellow";
375
+ };
376
+ vim: {
377
+ nerd: string;
378
+ unicode: string;
379
+ ascii: string;
380
+ color: "domainGreen";
381
+ };
382
+ apple: {
383
+ nerd: string;
384
+ unicode: string;
385
+ ascii: string;
386
+ color: "domainNeutral";
387
+ };
388
+ linux: {
389
+ nerd: string;
390
+ unicode: string;
391
+ ascii: string;
392
+ color: "domainYellow";
393
+ };
394
+ ubuntu: {
395
+ nerd: string;
396
+ unicode: string;
397
+ ascii: string;
398
+ color: "domainAmber";
399
+ };
400
+ font: {
401
+ nerd: string;
402
+ unicode: string;
403
+ ascii: string;
404
+ color: "domainNeutral";
405
+ };
406
+ ai: {
407
+ nerd: string;
408
+ unicode: string;
409
+ ascii: string;
410
+ color: "accent";
411
+ };
412
+ bolt: {
413
+ nerd: string;
414
+ unicode: string;
415
+ ascii: string;
416
+ color: "domainAmber";
417
+ };
418
+ };
419
+ /** A domain name from the {@link COMMON_DOMAINS} (Tier 1) pack. */
420
+ type CommonDomainName = keyof typeof COMMON_DOMAINS;
421
+ /** languages — programming languages / runtimes. */
422
+ declare const LANGUAGES: {
423
+ javascript: {
424
+ nerd: string;
425
+ unicode: string;
426
+ ascii: string;
427
+ color: "domainYellow";
428
+ };
429
+ typescript: {
430
+ nerd: string;
431
+ unicode: string;
432
+ ascii: string;
433
+ color: "domainBlue";
434
+ };
435
+ python: {
436
+ nerd: string;
437
+ unicode: string;
438
+ ascii: string;
439
+ color: "domainYellow";
440
+ };
441
+ php: {
442
+ nerd: string;
443
+ unicode: string;
444
+ ascii: string;
445
+ color: "domainViolet";
446
+ };
447
+ ruby: {
448
+ nerd: string;
449
+ unicode: string;
450
+ ascii: string;
451
+ color: "domainRed";
452
+ };
453
+ rust: {
454
+ nerd: string;
455
+ unicode: string;
456
+ ascii: string;
457
+ color: "domainAmber";
458
+ };
459
+ go: {
460
+ nerd: string;
461
+ unicode: string;
462
+ ascii: string;
463
+ color: "domainCyan";
464
+ };
465
+ java: {
466
+ nerd: string;
467
+ unicode: string;
468
+ ascii: string;
469
+ color: "domainAmber";
470
+ };
471
+ nodejs: {
472
+ nerd: string;
473
+ unicode: string;
474
+ ascii: string;
475
+ color: "domainGreen";
476
+ };
477
+ cpp: {
478
+ nerd: string;
479
+ unicode: string;
480
+ ascii: string;
481
+ color: "domainBlue";
482
+ };
483
+ c: {
484
+ nerd: string;
485
+ unicode: string;
486
+ ascii: string;
487
+ color: "domainBlue";
488
+ };
489
+ csharp: {
490
+ nerd: string;
491
+ unicode: string;
492
+ ascii: string;
493
+ color: "domainViolet";
494
+ };
495
+ html: {
496
+ nerd: string;
497
+ unicode: string;
498
+ ascii: string;
499
+ color: "domainAmber";
500
+ };
501
+ css: {
502
+ nerd: string;
503
+ unicode: string;
504
+ ascii: string;
505
+ color: "domainBlue";
506
+ };
507
+ react: {
508
+ nerd: string;
509
+ unicode: string;
510
+ ascii: string;
511
+ color: "domainCyan";
512
+ };
513
+ vue: {
514
+ nerd: string;
515
+ unicode: string;
516
+ ascii: string;
517
+ color: "domainGreen";
518
+ };
519
+ };
520
+ /** databases — engines & stores. */
521
+ declare const DATABASES: {
522
+ database: {
523
+ nerd: string;
524
+ unicode: string;
525
+ ascii: string;
526
+ color: "domainNeutral";
527
+ };
528
+ postgresql: {
529
+ nerd: string;
530
+ unicode: string;
531
+ ascii: string;
532
+ color: "domainBlue";
533
+ };
534
+ mysql: {
535
+ nerd: string;
536
+ unicode: string;
537
+ ascii: string;
538
+ color: "domainAzure";
539
+ };
540
+ mariadb: {
541
+ nerd: string;
542
+ unicode: string;
543
+ ascii: string;
544
+ color: "domainAzure";
545
+ };
546
+ redis: {
547
+ nerd: string;
548
+ unicode: string;
549
+ ascii: string;
550
+ color: "domainRed";
551
+ };
552
+ mongodb: {
553
+ nerd: string;
554
+ unicode: string;
555
+ ascii: string;
556
+ color: "domainGreen";
557
+ };
558
+ sqlite: {
559
+ nerd: string;
560
+ unicode: string;
561
+ ascii: string;
562
+ color: "domainBlue";
563
+ };
564
+ };
565
+ /** cloud — cloud / devops / infra. */
566
+ declare const CLOUD: {
567
+ aws: {
568
+ nerd: string;
569
+ unicode: string;
570
+ ascii: string;
571
+ color: "domainAmber";
572
+ };
573
+ cloud: {
574
+ nerd: string;
575
+ unicode: string;
576
+ ascii: string;
577
+ color: "domainCyan";
578
+ };
579
+ server: {
580
+ nerd: string;
581
+ unicode: string;
582
+ ascii: string;
583
+ color: "domainNeutral";
584
+ };
585
+ docker: {
586
+ nerd: string;
587
+ unicode: string;
588
+ ascii: string;
589
+ color: "domainCyan";
590
+ };
591
+ kubernetes: {
592
+ nerd: string;
593
+ unicode: string;
594
+ ascii: string;
595
+ color: "domainBlue";
596
+ };
597
+ nginx: {
598
+ nerd: string;
599
+ unicode: string;
600
+ ascii: string;
601
+ color: "domainGreen";
602
+ };
603
+ };
604
+ /** editors — editors / IDEs. */
605
+ declare const EDITORS: {
606
+ vim: {
607
+ nerd: string;
608
+ unicode: string;
609
+ ascii: string;
610
+ color: "domainGreen";
611
+ };
612
+ neovim: {
613
+ nerd: string;
614
+ unicode: string;
615
+ ascii: string;
616
+ color: "domainGreen";
617
+ };
618
+ vscode: {
619
+ nerd: string;
620
+ unicode: string;
621
+ ascii: string;
622
+ color: "domainBlue";
623
+ };
624
+ sublime: {
625
+ nerd: string;
626
+ unicode: string;
627
+ ascii: string;
628
+ color: "domainAmber";
629
+ };
630
+ emacs: {
631
+ nerd: string;
632
+ unicode: string;
633
+ ascii: string;
634
+ color: "domainViolet";
635
+ };
636
+ };
637
+ /** os — operating systems / distros. */
638
+ declare const OS: {
639
+ apple: {
640
+ nerd: string;
641
+ unicode: string;
642
+ ascii: string;
643
+ color: "domainNeutral";
644
+ };
645
+ linux: {
646
+ nerd: string;
647
+ unicode: string;
648
+ ascii: string;
649
+ color: "domainYellow";
650
+ };
651
+ ubuntu: {
652
+ nerd: string;
653
+ unicode: string;
654
+ ascii: string;
655
+ color: "domainAmber";
656
+ };
657
+ debian: {
658
+ nerd: string;
659
+ unicode: string;
660
+ ascii: string;
661
+ color: "domainRed";
662
+ };
663
+ arch: {
664
+ nerd: string;
665
+ unicode: string;
666
+ ascii: string;
667
+ color: "domainBlue";
668
+ };
669
+ fedora: {
670
+ nerd: string;
671
+ unicode: string;
672
+ ascii: string;
673
+ color: "domainBlue";
674
+ };
675
+ windows: {
676
+ nerd: string;
677
+ unicode: string;
678
+ ascii: string;
679
+ color: "domainCyan";
680
+ };
681
+ android: {
682
+ nerd: string;
683
+ unicode: string;
684
+ ascii: string;
685
+ color: "domainGreen";
686
+ };
687
+ };
688
+ /** companies — brands / vendors. */
689
+ declare const COMPANIES: {
690
+ github: {
691
+ nerd: string;
692
+ unicode: string;
693
+ ascii: string;
694
+ color: "domainNeutral";
695
+ };
696
+ gitlab: {
697
+ nerd: string;
698
+ unicode: string;
699
+ ascii: string;
700
+ color: "domainAmber";
701
+ };
702
+ bitbucket: {
703
+ nerd: string;
704
+ unicode: string;
705
+ ascii: string;
706
+ color: "domainBlue";
707
+ };
708
+ google: {
709
+ nerd: string;
710
+ unicode: string;
711
+ ascii: string;
712
+ color: "domainBlue";
713
+ };
714
+ microsoft: {
715
+ nerd: string;
716
+ unicode: string;
717
+ ascii: string;
718
+ color: "domainCyan";
719
+ };
720
+ apple: {
721
+ nerd: string;
722
+ unicode: string;
723
+ ascii: string;
724
+ color: "domainNeutral";
725
+ };
726
+ slack: {
727
+ nerd: string;
728
+ unicode: string;
729
+ ascii: string;
730
+ color: "domainViolet";
731
+ };
732
+ npm: {
733
+ nerd: string;
734
+ unicode: string;
735
+ ascii: string;
736
+ color: "domainRed";
737
+ };
738
+ git: {
739
+ nerd: string;
740
+ unicode: string;
741
+ ascii: string;
742
+ color: "domainAmber";
743
+ };
744
+ claude: {
745
+ nerd: string;
746
+ unicode: string;
747
+ ascii: string;
748
+ color: "accent";
749
+ };
750
+ };
751
+ /** frameworks — web / app frameworks. */
752
+ declare const FRAMEWORKS: {
753
+ react: {
754
+ nerd: string;
755
+ unicode: string;
756
+ ascii: string;
757
+ color: "domainCyan";
758
+ };
759
+ vue: {
760
+ nerd: string;
761
+ unicode: string;
762
+ ascii: string;
763
+ color: "domainGreen";
764
+ };
765
+ angular: {
766
+ nerd: string;
767
+ unicode: string;
768
+ ascii: string;
769
+ color: "domainRed";
770
+ };
771
+ svelte: {
772
+ nerd: string;
773
+ unicode: string;
774
+ ascii: string;
775
+ color: "domainAmber";
776
+ };
777
+ laravel: {
778
+ nerd: string;
779
+ unicode: string;
780
+ ascii: string;
781
+ color: "domainRed";
782
+ };
783
+ django: {
784
+ nerd: string;
785
+ unicode: string;
786
+ ascii: string;
787
+ color: "domainGreen";
788
+ };
789
+ rails: {
790
+ nerd: string;
791
+ unicode: string;
792
+ ascii: string;
793
+ color: "domainRed";
794
+ };
795
+ dotnet: {
796
+ nerd: string;
797
+ unicode: string;
798
+ ascii: string;
799
+ color: "domainViolet";
800
+ };
801
+ bootstrap: {
802
+ nerd: string;
803
+ unicode: string;
804
+ ascii: string;
805
+ color: "domainViolet";
806
+ };
807
+ jquery: {
808
+ nerd: string;
809
+ unicode: string;
810
+ ascii: string;
811
+ color: "domainBlue";
812
+ };
813
+ };
814
+ /** files — files & formats. */
815
+ declare const FILES: {
816
+ file: {
817
+ nerd: string;
818
+ unicode: string;
819
+ ascii: string;
820
+ color: "domainNeutral";
821
+ };
822
+ folder: {
823
+ nerd: string;
824
+ unicode: string;
825
+ ascii: string;
826
+ color: "domainBlue";
827
+ };
828
+ folder_open: {
829
+ nerd: string;
830
+ unicode: string;
831
+ ascii: string;
832
+ color: "domainBlue";
833
+ };
834
+ json: {
835
+ nerd: string;
836
+ unicode: string;
837
+ ascii: string;
838
+ color: "domainYellow";
839
+ };
840
+ yaml: {
841
+ nerd: string;
842
+ unicode: string;
843
+ ascii: string;
844
+ color: "domainAmber";
845
+ };
846
+ markdown: {
847
+ nerd: string;
848
+ unicode: string;
849
+ ascii: string;
850
+ color: "domainNeutral";
851
+ };
852
+ pdf: {
853
+ nerd: string;
854
+ unicode: string;
855
+ ascii: string;
856
+ color: "domainRed";
857
+ };
858
+ image: {
859
+ nerd: string;
860
+ unicode: string;
861
+ ascii: string;
862
+ color: "domainGreen";
863
+ };
864
+ archive: {
865
+ nerd: string;
866
+ unicode: string;
867
+ ascii: string;
868
+ color: "domainAmber";
869
+ };
870
+ lock: {
871
+ nerd: string;
872
+ unicode: string;
873
+ ascii: string;
874
+ color: "domainYellow";
875
+ };
876
+ };
877
+ /** social — social & messaging brands. */
878
+ declare const SOCIAL: {
879
+ slack: {
880
+ nerd: string;
881
+ unicode: string;
882
+ ascii: string;
883
+ color: "domainViolet";
884
+ };
885
+ discord: {
886
+ nerd: string;
887
+ unicode: string;
888
+ ascii: string;
889
+ color: "domainBlue";
890
+ };
891
+ telegram: {
892
+ nerd: string;
893
+ unicode: string;
894
+ ascii: string;
895
+ color: "domainCyan";
896
+ };
897
+ twitter: {
898
+ nerd: string;
899
+ unicode: string;
900
+ ascii: string;
901
+ color: "domainCyan";
902
+ };
903
+ youtube: {
904
+ nerd: string;
905
+ unicode: string;
906
+ ascii: string;
907
+ color: "domainRed";
908
+ };
909
+ linkedin: {
910
+ nerd: string;
911
+ unicode: string;
912
+ ascii: string;
913
+ color: "domainBlue";
914
+ };
915
+ reddit: {
916
+ nerd: string;
917
+ unicode: string;
918
+ ascii: string;
919
+ color: "domainAmber";
920
+ };
921
+ mastodon: {
922
+ nerd: string;
923
+ unicode: string;
924
+ ascii: string;
925
+ color: "domainViolet";
926
+ };
927
+ };
928
+ /** actions — generic UI action glyphs (not in the Tier 0 contract). */
929
+ declare const ACTIONS: {
930
+ search: {
931
+ nerd: string;
932
+ unicode: string;
933
+ ascii: string;
934
+ color: "domainNeutral";
935
+ };
936
+ filter: {
937
+ nerd: string;
938
+ unicode: string;
939
+ ascii: string;
940
+ color: "domainNeutral";
941
+ };
942
+ settings: {
943
+ nerd: string;
944
+ unicode: string;
945
+ ascii: string;
946
+ color: "domainNeutral";
947
+ };
948
+ trash: {
949
+ nerd: string;
950
+ unicode: string;
951
+ ascii: string;
952
+ color: "stateErr";
953
+ };
954
+ download: {
955
+ nerd: string;
956
+ unicode: string;
957
+ ascii: string;
958
+ color: "domainBlue";
959
+ };
960
+ upload: {
961
+ nerd: string;
962
+ unicode: string;
963
+ ascii: string;
964
+ color: "domainBlue";
965
+ };
966
+ refresh: {
967
+ nerd: string;
968
+ unicode: string;
969
+ ascii: string;
970
+ color: "stateInfo";
971
+ };
972
+ edit: {
973
+ nerd: string;
974
+ unicode: string;
975
+ ascii: string;
976
+ color: "domainNeutral";
977
+ };
978
+ bell: {
979
+ nerd: string;
980
+ unicode: string;
981
+ ascii: string;
982
+ color: "domainYellow";
983
+ };
984
+ link: {
985
+ nerd: string;
986
+ unicode: string;
987
+ ascii: string;
988
+ color: "link";
989
+ };
990
+ };
991
+ /**
992
+ * system — general-purpose system / OS / UI domain glyphs.
993
+ *
994
+ * The recurring "thing" glyphs that aren't a brand (→ {@link COMPANIES}), an
995
+ * action verb (→ {@link ACTIONS}), or a file format (→ {@link FILES}): a
996
+ * terminal, a globe, a home, a key, an envelope. Every `nerd` codepoint is a
997
+ * classic Font Awesome value; each carries a width-1 `unicode` fallback
998
+ * (verified via `string-width`) so it never tofus on a non–Nerd-Font terminal.
999
+ */
1000
+ declare const SYSTEM: {
1001
+ terminal: {
1002
+ nerd: string;
1003
+ unicode: string;
1004
+ ascii: string;
1005
+ color: "domainGreen";
1006
+ };
1007
+ code: {
1008
+ nerd: string;
1009
+ unicode: string;
1010
+ ascii: string;
1011
+ color: "domainBlue";
1012
+ };
1013
+ globe: {
1014
+ nerd: string;
1015
+ unicode: string;
1016
+ ascii: string;
1017
+ color: "domainCyan";
1018
+ };
1019
+ home: {
1020
+ nerd: string;
1021
+ unicode: string;
1022
+ ascii: string;
1023
+ color: "domainNeutral";
1024
+ };
1025
+ key: {
1026
+ nerd: string;
1027
+ unicode: string;
1028
+ ascii: string;
1029
+ color: "domainYellow";
1030
+ };
1031
+ mail: {
1032
+ nerd: string;
1033
+ unicode: string;
1034
+ ascii: string;
1035
+ color: "domainBlue";
1036
+ };
1037
+ phone: {
1038
+ nerd: string;
1039
+ unicode: string;
1040
+ ascii: string;
1041
+ color: "domainGreen";
1042
+ };
1043
+ package: {
1044
+ nerd: string;
1045
+ unicode: string;
1046
+ ascii: string;
1047
+ color: "domainAmber";
1048
+ };
1049
+ sync: {
1050
+ nerd: string;
1051
+ unicode: string;
1052
+ ascii: string;
1053
+ color: "domainCyan";
1054
+ };
1055
+ text: {
1056
+ nerd: string;
1057
+ unicode: string;
1058
+ ascii: string;
1059
+ color: "domainNeutral";
1060
+ };
1061
+ tools: {
1062
+ nerd: string;
1063
+ unicode: string;
1064
+ ascii: string;
1065
+ color: "domainNeutral";
1066
+ };
1067
+ };
1068
+ /** packages — package managers. */
1069
+ declare const PACKAGES: {
1070
+ npm: {
1071
+ nerd: string;
1072
+ unicode: string;
1073
+ ascii: string;
1074
+ color: "domainRed";
1075
+ };
1076
+ yarn: {
1077
+ nerd: string;
1078
+ unicode: string;
1079
+ ascii: string;
1080
+ color: "domainBlue";
1081
+ };
1082
+ cargo: {
1083
+ nerd: string;
1084
+ unicode: string;
1085
+ ascii: string;
1086
+ color: "domainAmber";
1087
+ };
1088
+ pip: {
1089
+ nerd: string;
1090
+ unicode: string;
1091
+ ascii: string;
1092
+ color: "domainYellow";
1093
+ };
1094
+ composer: {
1095
+ nerd: string;
1096
+ unicode: string;
1097
+ ascii: string;
1098
+ color: "domainYellow";
1099
+ };
1100
+ homebrew: {
1101
+ nerd: string;
1102
+ unicode: string;
1103
+ ascii: string;
1104
+ color: "domainYellow";
1105
+ };
1106
+ apt: {
1107
+ nerd: string;
1108
+ unicode: string;
1109
+ ascii: string;
1110
+ color: "domainRed";
1111
+ };
1112
+ };
1113
+ /**
1114
+ * devinfra — recurring local dev-infrastructure tools. These show up often
1115
+ * enough as raw `nf()` registrations in apps to earn CURATED entries (the bar
1116
+ * for Tier 2). `laravel` is promoted from {@link FRAMEWORKS} and `valet` aliases
1117
+ * its glyph; `code-server` aliases the vscode family from {@link EDITORS}. The
1118
+ * genuine newcomers — tailscale / syncthing / mosh — carry safe `{ unicode,
1119
+ * ascii }` fallbacks so they never tofu without a Nerd Font; their `nerd`
1120
+ * codepoint is left empty (a curator must verify it) and the glyph degrades to
1121
+ * its unicode form until then.
1122
+ *
1123
+ * PRIME DIRECTIVE still holds: this pack is OPT-IN content, registered by no app
1124
+ * automatically. A one-app-only glyph stays in that app — these are here because
1125
+ * they recur, not to make every app carry them.
1126
+ *
1127
+ * Two single-cell divergences from the design system, for the same reason as
1128
+ * {@link COMMON_DOMAINS} `bolt` (see also `glyphs/glyphs.ts`): the DS unicode
1129
+ * fallbacks `⚡` (ngrok) and `✉` (mailpit) are emoji-presentation and
1130
+ * double-wide — they break the one-glyph-one-cell grid in unicode mode. blink
1131
+ * substitutes the width-1 `↯` and `⊠`; `string-width` is the arbiter.
1132
+ */
1133
+ declare const DEVINFRA: {
1134
+ laravel: {
1135
+ nerd: string;
1136
+ unicode: string;
1137
+ ascii: string;
1138
+ color: "domainRed";
1139
+ };
1140
+ valet: {
1141
+ nerd: string;
1142
+ unicode: string;
1143
+ ascii: string;
1144
+ color: "domainRed";
1145
+ };
1146
+ 'code-server': {
1147
+ nerd: string;
1148
+ unicode: string;
1149
+ ascii: string;
1150
+ color: "domainBlue";
1151
+ };
1152
+ ngrok: {
1153
+ nerd: string;
1154
+ unicode: string;
1155
+ ascii: string;
1156
+ color: "domainAmber";
1157
+ };
1158
+ mailpit: {
1159
+ nerd: string;
1160
+ unicode: string;
1161
+ ascii: string;
1162
+ color: "domainAmber";
1163
+ };
1164
+ tailscale: {
1165
+ nerd: string;
1166
+ unicode: string;
1167
+ ascii: string;
1168
+ color: "domainCyan";
1169
+ };
1170
+ syncthing: {
1171
+ nerd: string;
1172
+ unicode: string;
1173
+ ascii: string;
1174
+ color: "domainViolet";
1175
+ };
1176
+ mosh: {
1177
+ nerd: string;
1178
+ unicode: string;
1179
+ ascii: string;
1180
+ color: "domainAmber";
1181
+ };
1182
+ };
1183
+ /** Tier-2 pack directory — used by pickers / docs to label & iterate packs. */
1184
+ declare const GLYPH_PACKS: Record<string, Record<string, GlyphVariants>>;
1185
+
1186
+ /**
1187
+ * Tier 3 — the raw Nerd Font index (the escape hatch).
1188
+ *
1189
+ * A flat map of canonical Nerd Font names → codepoint (hex string), following
1190
+ * the upstream `glyphnames.json` convention (source-prefixed: `fa-`, `dev-`,
1191
+ * `seti-`, `pl-`, …). It carries **no curation**: no unicode/ascii fallback, no
1192
+ * semantic colour. `nf(name)` returns the Nerd Font char only; on a non–Nerd-Font
1193
+ * terminal it has nothing to degrade to. Use it as a deliberate escape hatch —
1194
+ *
1195
+ * ```ts
1196
+ * nf('fa-rocket'); // → the glyph, or '' if unknown
1197
+ * registerGlyphs({ deploy: { nf: 'fa-rocket' } }); // muted, ascii derived
1198
+ * ```
1199
+ *
1200
+ * — and for anything an app shows often, promote it to a curated Tier 1/2 entry
1201
+ * (give it a colour + fallbacks) instead of leaning on the raw index.
1202
+ *
1203
+ * Shipped here is a **verified seed** (~130 of the most-used glyphs). The full
1204
+ * production index (~10k) is generated from upstream `glyphnames.json`; merge it
1205
+ * at boot with {@link registerNerdIndex} so the bundle stays lean (the seed
1206
+ * always wins — nothing curated is clobbered).
1207
+ */
1208
+ /** name → hex codepoint. Mutable so a generated full index can be merged in. */
1209
+ declare const NERD_INDEX: Record<string, string>;
1210
+ /** Source prefixes in the seed → human label (for a glyph picker's filter). */
1211
+ declare const NERD_INDEX_SOURCES: Record<string, string>;
1212
+ /**
1213
+ * Merge a generated full index into {@link NERD_INDEX}. The seed always wins, so
1214
+ * curated entries are never clobbered; keys starting with `_` (e.g. `_meta`) are
1215
+ * skipped. This is the Node-side analogue of the web engine's lazy fetch — the
1216
+ * app loads the generated `glyph-index.json` (~10k) and hands it over once at boot.
1217
+ */
1218
+ declare function registerNerdIndex(full: Record<string, string>): void;
1219
+ /**
1220
+ * Resolve a raw codepoint to a char — by index name (`'dev-laravel'`) or by a
1221
+ * bare hex codepoint (`'e73f'`). Returns `''` for an unknown name (never throws,
1222
+ * never tofu-by-surprise on lookup).
1223
+ */
1224
+ declare function nfChar(nameOrHex: string | null | undefined): string;
1225
+ /** `nf('fa-rocket')` → the Nerd Font char (nerd-only, no fallback), or `''`. */
1226
+ declare function nf(name: string): string;
1227
+ /** True if a raw index name is known. */
1228
+ declare function nfHas(name: string): boolean;
1229
+
1230
+ /**
1231
+ * The blink glyph ENGINE — the Tier 0 contract (state · nav · box · spinner ·
1232
+ * blocks) plus the extensible registry that powers Tiers 1–3.
1233
+ *
1234
+ * ─ Fonts vs. registry ─ The *drawing* of every Nerd Font glyph already lives in
1235
+ * the user's terminal **font** (CaskaydiaMono ships ~10k icons in the Private
1236
+ * Use Area); printing a codepoint is enough. What blink owns is the **registry**:
1237
+ * the map from a semantic NAME to a glyph plus its curated `{ unicode, ascii }`
1238
+ * fallbacks and its semantic `color` (a {@link SemanticTokens} key). Those three
1239
+ * are curation — they can't be auto-derived for 10k icons — which is why blink
1240
+ * *degrades* a glyph (nerd → unicode → ascii) rather than rendering tofu.
1241
+ *
1242
+ * ─ Contract vs. content ─ Tier 0 (this file) is the contract: it appears in
1243
+ * every blink app and never changes, so it's the only thing seeded into the
1244
+ * registry at import. Domain glyphs (Tiers 1–3) are CONTENT: the app opts in via
1245
+ * {@link registerGlyphs} (`packs.ts`) or the raw index (`nerdIndex.ts`, `nf()`).
1246
+ * blink core ships **zero** domain glyphs registered.
1247
+ */
1248
+ declare const stateGlyphs: {
1249
+ check: {
1250
+ nerd: string;
1251
+ unicode: string;
1252
+ ascii: string;
1253
+ };
1254
+ cross: {
1255
+ nerd: string;
1256
+ unicode: string;
1257
+ ascii: string;
1258
+ };
1259
+ circle: {
1260
+ nerd: string;
1261
+ unicode: string;
1262
+ ascii: string;
1263
+ };
1264
+ half: {
1265
+ nerd: string;
1266
+ unicode: string;
1267
+ ascii: string;
1268
+ };
1269
+ checkboxOn: {
1270
+ nerd: string;
1271
+ unicode: string;
1272
+ ascii: string;
1273
+ };
1274
+ checkboxOff: {
1275
+ nerd: string;
1276
+ unicode: string;
1277
+ ascii: string;
1278
+ };
1279
+ checkboxLock: {
1280
+ nerd: string;
1281
+ unicode: string;
1282
+ ascii: string;
1283
+ };
1284
+ warn: {
1285
+ nerd: string;
1286
+ unicode: string;
1287
+ ascii: string;
1288
+ };
1289
+ rerun: {
1290
+ nerd: string;
1291
+ unicode: string;
1292
+ ascii: string;
1293
+ };
1294
+ };
1295
+ /** Navigation glyphs — focus carets, expand chevrons, relation arrows. */
1296
+ declare const navGlyphs: {
1297
+ focus: {
1298
+ nerd: string;
1299
+ unicode: string;
1300
+ ascii: string;
1301
+ };
1302
+ collapsed: {
1303
+ nerd: string;
1304
+ unicode: string;
1305
+ ascii: string;
1306
+ };
1307
+ expanded: {
1308
+ nerd: string;
1309
+ unicode: string;
1310
+ ascii: string;
1311
+ };
1312
+ depends: {
1313
+ nerd: string;
1314
+ unicode: string;
1315
+ ascii: string;
1316
+ };
1317
+ flow: {
1318
+ nerd: string;
1319
+ unicode: string;
1320
+ ascii: string;
1321
+ };
1322
+ back: {
1323
+ nerd: string;
1324
+ unicode: string;
1325
+ ascii: string;
1326
+ };
1327
+ moreAbove: {
1328
+ nerd: string;
1329
+ unicode: string;
1330
+ ascii: string;
1331
+ };
1332
+ moreBelow: {
1333
+ nerd: string;
1334
+ unicode: string;
1335
+ ascii: string;
1336
+ };
1337
+ };
1338
+ /** Built-in glyph names — the contract set, before any app registrations. */
1339
+ type BuiltinGlyphName = keyof typeof stateGlyphs | keyof typeof navGlyphs;
1340
+ /** A raw glyph with no curated colour renders muted — neutral, never semantic. */
1341
+ declare const DEFAULT_GLYPH_COLOR: GlyphColor;
1342
+ /** A registered glyph with no curated unicode fallback degrades to this mark. */
1343
+ declare const DEFAULT_UNICODE = "\u25C6";
1344
+ /** `deriveAscii('postgresql')` → `'[po]'` — used when an entry omits an ascii fallback. */
1345
+ declare function deriveAscii(name: string): string;
1346
+ /**
1347
+ * What {@link registerGlyphs} accepts per entry — three shapes, all coexisting:
1348
+ * - verbose : `{ nerd:'', unicode:'◆', ascii:'[la]', color:'domainRed' }`
1349
+ * - easy : `{ nf:'dev-laravel', color:'domainRed' }` ← codepoint from the index
1350
+ * - raw cp : `{ cp:'e73f', color:'domainRed' }` ← codepoint from hex
1351
+ * - string : `''` ← nerd only, all defaults
1352
+ * Anything omitted is filled in: `unicode → ◆`, `ascii → derived`, `color → muted`.
1353
+ */
1354
+ type GlyphInput = string | (Partial<GlyphVariants> & {
1355
+ nf?: string;
1356
+ cp?: string;
1357
+ });
1358
+ /**
1359
+ * Register app-domain glyphs (or override built-ins). Call once at startup,
1360
+ * before the first render. Accepts one or more maps — later wins — so an app can
1361
+ * take a pack then override the few entries it cares about. Each entry's `color`
1362
+ * is owned here, at REGISTRATION, never per render site, so a row says "this is a
1363
+ * postgres row", not "paint this blue":
1364
+ *
1365
+ * ```ts
1366
+ * registerGlyphs(COMMON_DOMAINS); // Tier 1 pack
1367
+ * registerGlyphs(LANGUAGES, DATABASES); // many Tier 2 packs
1368
+ * registerGlyphs({ deploy: { nf: 'fa-rocket' } }); // easy form, from the index
1369
+ * registerGlyphs({ database: { color: 'domainCyan' } }); // override one
1370
+ * ```
1371
+ */
1372
+ declare function registerGlyphs(...maps: Array<Record<string, GlyphInput> | undefined>): void;
1373
+ /** Single-entry convenience for {@link registerGlyphs}. */
1374
+ declare function registerGlyph(name: string, entry: GlyphInput): void;
1375
+ /** True if a glyph name is registered. */
1376
+ declare function hasGlyph(name: string): boolean;
1377
+ /** Every registered glyph name, sorted — for pickers & tests. */
1378
+ declare function registeredNames(): string[];
1379
+ /**
1380
+ * Resolve a glyph name to a string for the given icon set, with the
1381
+ * `nerd → unicode → ascii` fallback. Unknown names return the name itself (so a
1382
+ * typo or an unregistered domain renders visibly as text rather than tofu).
1383
+ * Prefer the {@link useGlyph} hook inside components — it reads the icon set from
1384
+ * context.
1385
+ */
1386
+ declare function glyph(name: string, set: IconSet): string;
1387
+ /**
1388
+ * The colour a registered glyph renders in — a {@link SemanticTokens} key, owned
1389
+ * at registration. Resolve it through the active theme: `tokens[glyphColor(name)
1390
+ * ?? 'fgMuted']`. Returns `undefined` for an unregistered name or a contract
1391
+ * glyph (whose colour comes from the state-intent map, not the entry).
1392
+ */
1393
+ declare function glyphColor(name: string): GlyphColor | undefined;
1394
+ /** A resolved intent: a glyph NAME (resolve via {@link glyph}) + a colour token key. */
1395
+ interface StateIntent {
1396
+ /** Registry glyph name — resolve with `useGlyph()` / `glyph(name, set)`. */
1397
+ glyph: string;
1398
+ /** Semantic token key for the colour — read `tokens[token]`. */
1399
+ token: keyof SemanticTokens;
1400
+ }
1401
+ /** Row / detail status intents → glyph + colour. The house mapping, central. */
1402
+ declare const stateIntents: {
1403
+ installed: {
1404
+ glyph: string;
1405
+ token: "stateOk";
1406
+ };
1407
+ ok: {
1408
+ glyph: string;
1409
+ token: "stateOk";
1410
+ };
1411
+ done: {
1412
+ glyph: string;
1413
+ token: "stateOk";
1414
+ };
1415
+ missing: {
1416
+ glyph: string;
1417
+ token: "stateErr";
1418
+ };
1419
+ error: {
1420
+ glyph: string;
1421
+ token: "stateErr";
1422
+ };
1423
+ err: {
1424
+ glyph: string;
1425
+ token: "stateErr";
1426
+ };
1427
+ failed: {
1428
+ glyph: string;
1429
+ token: "stateErr";
1430
+ };
1431
+ drift: {
1432
+ glyph: string;
1433
+ token: "stateDrift";
1434
+ };
1435
+ partial: {
1436
+ glyph: string;
1437
+ token: "stateDrift";
1438
+ };
1439
+ warn: {
1440
+ glyph: string;
1441
+ token: "stateWarn";
1442
+ };
1443
+ idempotent: {
1444
+ glyph: string;
1445
+ token: "stateInfo";
1446
+ };
1447
+ pending: {
1448
+ glyph: string;
1449
+ token: "statePending";
1450
+ };
1451
+ info: {
1452
+ glyph: string;
1453
+ token: "stateInfo";
1454
+ };
1455
+ };
1456
+ /** A semantic state name the framework knows how to draw. */
1457
+ type StateName = keyof typeof stateIntents;
1458
+ /** Resolve a state intent name to its glyph + colour token, or `null` if unknown. */
1459
+ declare function stateGlyph(name: string): StateIntent | null;
1460
+ /** Selection intents → checkbox glyph + colour. `locked` is selected + non-toggle. */
1461
+ declare const selectionIntents: {
1462
+ selected: {
1463
+ glyph: string;
1464
+ token: "accent";
1465
+ };
1466
+ unselected: {
1467
+ glyph: string;
1468
+ token: "fgDim";
1469
+ };
1470
+ locked: {
1471
+ glyph: string;
1472
+ token: "fgMuted";
1473
+ };
1474
+ };
1475
+ /** A selection intent name. */
1476
+ type SelectionName = keyof typeof selectionIntents;
1477
+ /** A complete set of box-drawing characters for one border style. */
1478
+ interface BoxChars {
1479
+ tl: string;
1480
+ tr: string;
1481
+ bl: string;
1482
+ br: string;
1483
+ h: string;
1484
+ v: string;
1485
+ }
1486
+ /**
1487
+ * blink's border styles. The house style is **single-line, rounded** corners;
1488
+ * `single` (square) is a legacy opt-out. There is **no double-line border** — it
1489
+ * reads dated, and focus / modals are signalled by colour, not a heavier line.
1490
+ * `ascii` is the fontless fallback (`+ - |`).
1491
+ */
1492
+ declare const boxStyles: {
1493
+ single: {
1494
+ tl: string;
1495
+ tr: string;
1496
+ bl: string;
1497
+ br: string;
1498
+ h: string;
1499
+ v: string;
1500
+ };
1501
+ rounded: {
1502
+ tl: string;
1503
+ tr: string;
1504
+ bl: string;
1505
+ br: string;
1506
+ h: string;
1507
+ v: string;
1508
+ };
1509
+ ascii: {
1510
+ tl: string;
1511
+ tr: string;
1512
+ bl: string;
1513
+ br: string;
1514
+ h: string;
1515
+ v: string;
1516
+ };
1517
+ };
1518
+ type BoxStyleName = 'single' | 'rounded';
1519
+ /**
1520
+ * Pick the right box characters for a style + icon set. In `ascii` mode every
1521
+ * style collapses to the ASCII set (`+ - |`); rounded corners aren't ASCII-safe.
1522
+ */
1523
+ declare function boxChars(style: BoxStyleName, set: IconSet): BoxChars;
1524
+ /** Spinner frames per icon set. Braille for nerd/unicode, classic for ascii. */
1525
+ declare const spinnerFrames: {
1526
+ readonly braille: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
1527
+ readonly ascii: readonly ["|", "/", "-", "\\"];
1528
+ };
1529
+ /** Frames for the given icon set. */
1530
+ declare function spinnerFor(set: IconSet): readonly string[];
1531
+ /** Block-shade ramp — the only "gradient" the contract allows. */
1532
+ declare const blocks: {
1533
+ readonly full: "█";
1534
+ readonly dark: "▓";
1535
+ readonly medium: "▒";
1536
+ readonly light: "░";
1537
+ readonly cursor: "▎";
1538
+ };
1539
+ /**
1540
+ * Horizontal eighth-block ramp — the sub-cell material for a determinate
1541
+ * {@link ProgressBar}. Indexed by eighths filled (`0` = empty, `8` = a full
1542
+ * cell `█`). Left-to-right partials, the same family as {@link blocks}.
1543
+ */
1544
+ declare const blocksH: readonly [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
1545
+
1546
+ /**
1547
+ * Returns a resolver bound to the icon set in context, so components don't have
1548
+ * to thread `iconSet` everywhere:
1549
+ *
1550
+ * ```tsx
1551
+ * const g = useGlyph();
1552
+ * <Text color={tokens.stateOk}>{g('check')}</Text>
1553
+ * ```
1554
+ */
1555
+ declare function useGlyph(): (name: string) => string;
1556
+
1557
+ interface DetectOptions {
1558
+ /**
1559
+ * Path to a JSON file holding `{ "iconSet": "nerd" | "unicode" | "ascii" }`.
1560
+ * Defaults to `~/.config/blink/preferences.json`. This is the per-user
1561
+ * answer saved by a first-run probe (apps own the probe UI).
1562
+ */
1563
+ configPath?: string;
1564
+ /**
1565
+ * Absolute paths to "a Nerd Font is installed" marker files. If any exists,
1566
+ * detection resolves to `nerd`. Apps pass their own markers (e.g. a font
1567
+ * bundle's install receipt) — blink ships none, staying agnostic.
1568
+ */
1569
+ markerFiles?: string[];
1570
+ /** Override `process.env` (testing). */
1571
+ env?: NodeJS.ProcessEnv;
1572
+ }
1573
+ /**
1574
+ * Resolve which {@link IconSet} to render with, once, at app startup.
1575
+ *
1576
+ * The cascade (first match wins):
1577
+ * 1. hard env override — `BLINK_ICON_SET`, or `BLINK_NERD_FONT=1|0`, `BLINK_ASCII=1`
1578
+ * 2. saved per-user preference — `configPath`
1579
+ * 3. app-state marker — a font bundle's install receipt (`markerFiles`)
1580
+ * 4. terminal hints — `WT_SESSION`, `ITERM_PROFILE`, `TERM_PROGRAM`, …
1581
+ * 5. CI / dumb terminals → `ascii`
1582
+ * 6. safe default → `unicode`
1583
+ *
1584
+ * Never throws and never blocks: a blink app always gets a usable icon set.
1585
+ */
1586
+ declare function detectIconSet(opts?: DetectOptions): Promise<IconSet>;
1587
+
1588
+ /**
1589
+ * Terminal cell width of a string, used to pin {@link List} columns and to drop
1590
+ * {@link Footer} chips that don't fit — so the character grid stays aligned (the
1591
+ * contract's "strict character-cell grid").
1592
+ *
1593
+ * Delegates to the same `string-width` Ink uses for its own layout, so the
1594
+ * widths computed here always agree with how Ink sizes its boxes — including
1595
+ * the wide glyphs in the palette (`⚠`, `☑` measure two cells) and the
1596
+ * multi-char `ascii` fallbacks (`[x]` measures three). Hand-rolling this drifts
1597
+ * from Ink and makes fixed-width cells truncate or misalign.
1598
+ *
1599
+ * Caveat: a terminal configured with "ambiguous = wide" (common in CJK locales)
1600
+ * may render some glyphs wider than string-width reports; that's a terminal
1601
+ * setting no layout-time measurement can see.
1602
+ */
1603
+ declare function cellWidth(str: string): number;
1604
+
1605
+ interface Dimensions {
1606
+ columns: number;
1607
+ rows: number;
1608
+ }
1609
+ /**
1610
+ * The live terminal size in character cells, updating on `SIGWINCH` (resize).
1611
+ *
1612
+ * blink's design target is 100×30; the mobile-mosh fallback is 60×20. Layouts
1613
+ * read this to switch between the full multi-pane view and a stacked one.
1614
+ * Falls back to 80×24 when the stream isn't a TTY (pipes, CI).
1615
+ *
1616
+ * Tip: size your root box to `rows - 1`, not `rows`. An output exactly as tall
1617
+ * as the terminal makes Ink redraw with a full-screen clear every render
1618
+ * (visible flicker on each keypress); one spare line keeps it on the smooth
1619
+ * incremental path.
1620
+ */
1621
+ declare function useStdoutDimensions(): Dimensions;
1622
+
1623
+ /**
1624
+ * The cursor blink — blink's one sanctioned text animation.
1625
+ *
1626
+ * Returns a boolean that toggles at `hz` (default 1 Hz) with `step-end` timing:
1627
+ * fully on for half the period, fully off for the other half. No fade, by
1628
+ * contract. Pass `active=false` to hold it visible (e.g. an unfocused input).
1629
+ *
1630
+ * ```tsx
1631
+ * const on = useBlink();
1632
+ * <Text>{value}{on ? '▎' : ' '}</Text>
1633
+ * ```
1634
+ */
1635
+ declare function useBlink(active?: boolean, hz?: number): boolean;
1636
+
1637
+ interface SpinnerOptions {
1638
+ /** Advance only while true. Defaults to true. */
1639
+ active?: boolean;
1640
+ /** Frame interval in ms. Defaults to 80 (the contract's spinner cadence). */
1641
+ intervalMs?: number;
1642
+ }
1643
+ /**
1644
+ * A monotonically-increasing frame counter for spinners, advancing every
1645
+ * `intervalMs` (default 80 ms) while `active`. The caller picks the glyph:
1646
+ *
1647
+ * ```tsx
1648
+ * const frame = useSpinnerFrame({ active: syncing });
1649
+ * const frames = spinnerFor(iconSet);
1650
+ * <Text color={tokens.stateInfo}>{frames[frame % frames.length]}</Text>
1651
+ * ```
1652
+ *
1653
+ * Returns a counter rather than a glyph so it's icon-set agnostic and trivially
1654
+ * testable. Resets to 0 whenever it goes inactive.
1655
+ */
1656
+ declare function useSpinnerFrame(opts?: SpinnerOptions): number;
1657
+
1658
+ interface UseListWindowOptions {
1659
+ /** Total number of rows. */
1660
+ rowCount: number;
1661
+ /** Index of the focused row; `null` / `< 0` = no focus-follow (window holds). */
1662
+ focusedIndex: number | null;
1663
+ /** Max rows the viewport renders, INCLUDING any marker rows. */
1664
+ height: number;
1665
+ /**
1666
+ * Context rows kept between the caret and the edge before the window scrolls.
1667
+ * Default `0` → the caret may reach the last content row, and moving past it
1668
+ * scrolls the window by one (vim `scrolloff=0`).
1669
+ */
1670
+ scrolloff?: number;
1671
+ /**
1672
+ * Reserve a chrome row for the `▴`/`▾` "N more" marker on an overflowing side.
1673
+ * Default `true`. When `false`, the window still scrolls but clips silently.
1674
+ */
1675
+ overflowMarkers?: boolean;
1676
+ }
1677
+ interface ListWindow {
1678
+ /** First visible CONTENT row index (inclusive). Slice with `rows.slice(start, end)`. */
1679
+ start: number;
1680
+ /** One past the last visible content row. */
1681
+ end: number;
1682
+ /** Rows hidden above / below the window (0 = none). Drive the markers off these. */
1683
+ aboveCount: number;
1684
+ belowCount: number;
1685
+ }
1686
+ /**
1687
+ * Pure windowing math: given the previous window start, where does the window
1688
+ * sit now so it (a) fits `height`, (b) contains `focusedIndex`, and (c) reserves
1689
+ * a chrome row for each overflow marker that shows? Exported for unit tests and
1690
+ * for callers windowing something other than {@link useListWindow}.
1691
+ *
1692
+ * Markers are **separate chrome rows**, not overlays: `content + markers` always
1693
+ * equals `height`, so the window never exceeds its container, and `aboveCount` /
1694
+ * `belowCount` are the exact hidden-row counts (no off-by-one against a marker).
1695
+ * The caret is always inside the content band, so the focused row is never
1696
+ * hidden behind a "N more".
1697
+ *
1698
+ * Marker reservation and the window position are mutually dependent (a marker
1699
+ * costs a content row, which can move the window, which can flip a marker), so
1700
+ * the start is settled over a few passes, then the markers are finalised from
1701
+ * the settled start so the returned values are always self-consistent.
1702
+ *
1703
+ * One consequence: the *single* step where a marker first appears shifts the
1704
+ * window by two rows (the new marker claims a content row, so the band shrinks
1705
+ * by one as focus moves by one). Every other step is a smooth 1/row scroll —
1706
+ * the steady-state behaviour. The alternative (markers overlaying edge rows)
1707
+ * keeps 1/row but muddies the hidden-row counts; clean counts win here.
1708
+ */
1709
+ declare function computeWindow(prevStart: number, rowCount: number, focusedIndex: number | null, height: number, scrolloff?: number, overflowMarkers?: boolean): ListWindow;
1710
+ /**
1711
+ * Headless windowing for a fixed-height viewport over a flat row array — the
1712
+ * engine behind a windowed {@link List}, and reusable on its own for any
1713
+ * keyboard-paged list.
1714
+ *
1715
+ * It owns the window **offset** across renders (view-state, like a scroll
1716
+ * position — *not* the focus/selection state the contract reserves for the app)
1717
+ * and re-derives the window each render to keep `focusedIndex` visible. The hook
1718
+ * never sees a keystroke: the app moves `focusedIndex`, the window follows. With
1719
+ * `scrolloff = 0` the caret travels to the edge of the viewport and only then
1720
+ * does the window scroll.
1721
+ *
1722
+ * ```tsx
1723
+ * const win = useListWindow({ rowCount: rows.length, focusedIndex, height: 20 });
1724
+ * const visible = rows.slice(win.start, win.end);
1725
+ * ```
1726
+ */
1727
+ declare function useListWindow(opts: UseListWindowOptions): ListWindow;
1728
+
1729
+ interface UseListNavigationOptions {
1730
+ /** Ordered row ids. */
1731
+ ids: string[];
1732
+ /** Controlled focus. Omit to let the hook own it (seeded to the first id). */
1733
+ focusedId?: string | null;
1734
+ onFocusChange?: (id: string) => void;
1735
+ /** Wrap first↔last instead of clamping at the ends. Default false. */
1736
+ wrap?: boolean;
1737
+ }
1738
+ interface ListNavigation {
1739
+ focusedId: string | null;
1740
+ /** Index of `focusedId` in `ids` (`-1` when none) — feed to {@link useListWindow}. */
1741
+ focusedIndex: number;
1742
+ focusNext: () => void;
1743
+ focusPrev: () => void;
1744
+ focusFirst: () => void;
1745
+ focusLast: () => void;
1746
+ focusTo: (id: string) => void;
1747
+ }
1748
+ /**
1749
+ * Headless focus movement for a list — clamp/wrap at the ends, next/prev/first/
1750
+ * last/seek. Exposes **intent methods**, never a baked-in key handler: the app
1751
+ * owns `useInput` and maps its own keys (`j/k`, arrows, …) to these, so blink
1752
+ * never embeds one app's keymap. Separate from {@link useListSelection} because
1753
+ * focus movement is generic to any list, including read-only menus that never
1754
+ * select.
1755
+ *
1756
+ * ```tsx
1757
+ * const nav = useListNavigation({ ids });
1758
+ * useInput((_, key) => {
1759
+ * if (key.downArrow) nav.focusNext();
1760
+ * if (key.upArrow) nav.focusPrev();
1761
+ * });
1762
+ * <List rows={rows} focusedId={nav.focusedId} height={20} />
1763
+ * ```
1764
+ */
1765
+ declare function useListNavigation(opts: UseListNavigationOptions): ListNavigation;
1766
+
1767
+ type SelectionMode = 'single' | 'multi';
1768
+ interface UseListSelectionOptions {
1769
+ /** The selectable row ids (used for bounds only; selection is by id). */
1770
+ ids: string[];
1771
+ /** `'single'` keeps exactly one selected; `'multi'` toggles a subset. */
1772
+ mode: SelectionMode;
1773
+ /** multi/single: minimum that must stay selected — deselecting below is blocked. */
1774
+ min?: number;
1775
+ /** multi: maximum selectable — selecting above is blocked. */
1776
+ max?: number;
1777
+ /** Initially selected ids. */
1778
+ initial?: Iterable<string>;
1779
+ onChange?: (selected: Set<string>) => void;
1780
+ }
1781
+ interface ListSelection {
1782
+ /** Feed straight to `<List selectedIds={...} />`. */
1783
+ selectedIds: Set<string>;
1784
+ isSelected: (id: string) => boolean;
1785
+ /** Toggle `id`. Returns `false` when blocked by `min`/`max` (and sets `blocked`). */
1786
+ toggle: (id: string) => boolean;
1787
+ /** Single-select: make `id` the only selected row. */
1788
+ selectOnly: (id: string) => void;
1789
+ /** Clear all (blocked when `min` would be violated). */
1790
+ clear: () => void;
1791
+ /** True when the last operation was blocked by `min`/`max` — render as `Input.error`. */
1792
+ blocked: boolean;
1793
+ }
1794
+ /**
1795
+ * Headless selection logic for a list — the toggle, the single-select
1796
+ * invariant, and the min/max guards that every consumer otherwise hand-rolls.
1797
+ * It owns the selected `Set`; the **app still owns the keys** and renders the
1798
+ * existing presentational {@link List}. Pair with {@link useListNavigation} for
1799
+ * the cursor; this hook is only about *what is selected*.
1800
+ *
1801
+ * ```tsx
1802
+ * const sel = useListSelection({ ids, mode: 'multi', min: 1 });
1803
+ * useInput((input) => { if (input === ' ' && focusedId) sel.toggle(focusedId); });
1804
+ * <List rows={rows} selectedIds={sel.selectedIds} focusedId={focusedId} />
1805
+ * ```
1806
+ */
1807
+ declare function useListSelection(opts: UseListSelectionOptions): ListSelection;
1808
+
1809
+ /**
1810
+ * Pane emphasis, by PURPOSE — the consumer declares what a pane *means*, blink
1811
+ * draws it. There is exactly one border shape in the house style (single-line,
1812
+ * rounded), so tone never changes the geometry, only the colour:
1813
+ *
1814
+ * - `resting` — a pane at rest (muted border).
1815
+ * - `focus` — the focused pane (border + title recoloured lavender).
1816
+ * - `error` — an error / destructive pane (border + title red).
1817
+ */
1818
+ type PaneTone = 'resting' | 'focus' | 'error';
1819
+ /**
1820
+ * Legacy border treatments. Kept only so older call sites keep working while
1821
+ * they migrate to {@link PaneTone}. `'double'` is gone from the house style and
1822
+ * now renders as the rounded default; `'square'` is the one legacy shape opt-out.
1823
+ * @deprecated pass `tone` instead.
1824
+ */
1825
+ type PaneVariant = 'default' | 'rounded' | 'square' | 'double' | 'error';
1826
+ interface PaneProps {
1827
+ /** Title shown inside the top border: `╭─ title ──────╮`. Keep ≤ 18 chars. */
1828
+ title?: string;
1829
+ /**
1830
+ * Semantic emphasis. The framework owns the border colour, the title colour,
1831
+ * and the (always rounded) shape. Default `'resting'`.
1832
+ */
1833
+ tone?: PaneTone;
1834
+ /**
1835
+ * @deprecated use `tone="focus"`. A focused pane gets the lavender border.
1836
+ * Kept as a back-compat alias; `tone` wins when both are set.
1837
+ */
1838
+ focused?: boolean;
1839
+ /**
1840
+ * @deprecated use `tone`. `'error'` ⇒ `tone="error"`, `'square'` ⇒ the legacy
1841
+ * square shape; every other value falls through to the rounded house style.
1842
+ */
1843
+ variant?: PaneVariant;
1844
+ /** Flex grow factor (default 1 — panes fill available space). */
1845
+ flexGrow?: number;
1846
+ /** Flex basis (e.g. `'56%'`). */
1847
+ flexBasis?: BoxProps['flexBasis'];
1848
+ /** Fixed width in cells. */
1849
+ width?: BoxProps['width'];
1850
+ /** Fixed height in cells. */
1851
+ height?: BoxProps['height'];
1852
+ /** Minimum height in cells. */
1853
+ minHeight?: BoxProps['minHeight'];
1854
+ children?: React.ReactNode;
1855
+ }
1856
+ /**
1857
+ * A box-drawn rectangle with an optional title in the top border — the
1858
+ * analogue of a "card" in blink (panes don't lift, shadow, or change shape).
1859
+ *
1860
+ * Borders are real box-drawing glyphs. Ink's native border renders the
1861
+ * full-height left/right sides and the bottom edge; the top edge is drawn by
1862
+ * hand so the title can sit *inside* it. The two halves share a width, so they
1863
+ * join into one continuous frame. Focus and elevation are signalled by **border
1864
+ * colour** alone — the shape is identical across tones, so the layout never
1865
+ * shifts when a pane gains or loses focus.
1866
+ */
1867
+ declare function Pane({ title, tone, focused, variant, flexGrow, flexBasis, width, height, minHeight, children, }: PaneProps): React.ReactElement;
1868
+
1869
+ /**
1870
+ * One row's worth of data — declared as **intent**, never style. A row says what
1871
+ * it MEANS (`state="installed"`, `selected`, `domain="postgresql"`) and the
1872
+ * framework resolves the glyph and its colour from the house tokens. The
1873
+ * consumer never passes a raw glyph or a raw colour. (The focus caret is the one
1874
+ * exception — it is chrome, not data.)
1875
+ */
1876
+ interface ListRowData {
1877
+ /** Stable identity, used for focus/selection lookups and React keys. */
1878
+ id: string;
1879
+ /** The row's primary text, in `fg` (or `fgDim` when `muted`). */
1880
+ label: string;
1881
+ /**
1882
+ * Semantic status name → the framework draws its glyph + colour:
1883
+ * `installed | ok | done | missing | error | drift | partial | idempotent |
1884
+ * pending | warn | info`. See `stateGlyph()`.
1885
+ */
1886
+ state?: string;
1887
+ /** Selection intent → `☑ / ☐`. Presence of the field (even `false`) opts the row into the checkbox column. */
1888
+ selected?: boolean;
1889
+ /** Required, non-toggle (implies selected) → `▣`. */
1890
+ locked?: boolean;
1891
+ /** A **registered** domain glyph name → the glyph + its colour, owned at registration. */
1892
+ domain?: string;
1893
+ /** Right-aligned consequence / aside text (content), in `fgDim`. */
1894
+ meta?: string;
1895
+ /** De-emphasise the whole row (e.g. a disabled / required label). */
1896
+ muted?: boolean;
1897
+ }
1898
+ interface ListRowProps {
1899
+ row: ListRowData;
1900
+ /** Carries the `►` caret + `bgFocused` fill. Wins over `selected`. */
1901
+ focused?: boolean;
1902
+ /** Draws the `bgSelected` fill (only when not also focused). */
1903
+ selected?: boolean;
1904
+ /**
1905
+ * Which intent columns this row reserves, and their fixed cell widths, so the
1906
+ * glyph columns line up down the whole list. `List` computes these from the
1907
+ * widest cell in each column; a standalone `ListRow` derives them from itself.
1908
+ */
1909
+ showCheckbox?: boolean;
1910
+ showState?: boolean;
1911
+ showDomain?: boolean;
1912
+ caretWidth?: number;
1913
+ checkboxWidth?: number;
1914
+ stateWidth?: number;
1915
+ domainWidth?: number;
1916
+ }
1917
+ interface ListProps {
1918
+ rows: ListRowData[];
1919
+ /** Id of the row that holds the nav caret. */
1920
+ focusedId?: string | null;
1921
+ /** Ids drawn with the selection fill. */
1922
+ selectedIds?: Set<string>;
1923
+ /**
1924
+ * Max rows to render, including any overflow-marker rows. Omit to render every
1925
+ * row. When set and `rows` exceeds it, List shows a window that always contains
1926
+ * `focusedId` and scrolls as focus nears an edge — keyboard-paged, never
1927
+ * mouse-scrolled (see the contract).
1928
+ */
1929
+ height?: number;
1930
+ /** Context rows kept before the window scrolls. Default 0 (scroll at the edge). */
1931
+ scrolloff?: number;
1932
+ /** Draw `▴ N more` / `▾ N more` on overflowing sides. Default true. */
1933
+ overflowMarkers?: boolean;
1934
+ }
1935
+ /**
1936
+ * A single list row — a caret column, then the optional intent columns
1937
+ * (checkbox · state · domain), the label, then the meta pushed right. The
1938
+ * focused row fills with `bgFocused`; a selected-but-unfocused row fills with
1939
+ * `bgSelected`. No hover, by contract.
1940
+ *
1941
+ * Each intent column is a **fixed-width cell** (truncating, never wrapping), so
1942
+ * labels stay column-aligned no matter how wide a row's glyph (or its multi-char
1943
+ * `ascii`/`unicode` fallback) renders. The glyph and its colour are resolved
1944
+ * here from the row's intent — the consumer hands over meaning, not pixels.
1945
+ */
1946
+ declare function ListRow({ row, focused, selected, showCheckbox, showState, showDomain, caretWidth, checkboxWidth, stateWidth, domainWidth, }: ListRowProps): React.ReactElement;
1947
+ /**
1948
+ * A vertical stack of {@link ListRow}s — blink's plain list. Exactly one row may
1949
+ * be focused (`focusedId`); any number may be selection-filled (`selectedIds`).
1950
+ * Rows are pure **intent** ({@link ListRowData}); the list resolves the glyphs.
1951
+ *
1952
+ * List sizes the caret / checkbox / state / domain columns to the widest cell
1953
+ * across all rows and passes those widths down, so every row shares one grid —
1954
+ * the fix for ragged columns when glyphs fall back to variable-width text.
1955
+ */
1956
+ declare function List({ rows, focusedId, selectedIds, height, scrolloff, overflowMarkers, }: ListProps): React.ReactElement;
1957
+
1958
+ interface HeaderProps {
1959
+ /**
1960
+ * Leading accent mark. Defaults to the blink cursor block `▎`. Pass a node
1961
+ * (e.g. a blinking `<Cursor />`) for the live mark, or a string for a static
1962
+ * one; `null` drops the mark entirely.
1963
+ */
1964
+ mark?: React.ReactNode;
1965
+ /** Screen title, in `fg`. */
1966
+ title: string;
1967
+ /** Optional aside, rendered `· subtitle` in `fgMuted`. */
1968
+ subtitle?: string;
1969
+ /** Optional node flush-right — status, counts, a delta summary. */
1970
+ right?: React.ReactNode;
1971
+ }
1972
+ /**
1973
+ * The one-row status bar that tops a blink screen: a lavender mark, a title
1974
+ * (with an optional `· subtitle`), and a right-aligned status slot. It recurs on
1975
+ * every wizard / app screen — the brand chrome — so it is a primitive, not
1976
+ * per-app code. One line, never wraps; the subtitle truncates first.
1977
+ */
1978
+ declare function Header({ mark, title, subtitle, right, }: HeaderProps): React.ReactElement;
1979
+
1980
+ /** One term/value row. Carries intent (`state`, `muted`) — never a raw glyph or colour. */
1981
+ interface DescriptionItem {
1982
+ /** The label in the gutter (`fgDim`). Omit for a full-width value line. */
1983
+ term?: string;
1984
+ /** The value text. */
1985
+ value: string;
1986
+ /**
1987
+ * A semantic status name → the framework draws its glyph + colour before the
1988
+ * value (`installed`, `missing`, `drift`, `pending`, …). See `stateGlyph()`.
1989
+ */
1990
+ state?: string;
1991
+ /** De-emphasise the value (e.g. a description line) → rendered in `fgMuted`. */
1992
+ muted?: boolean;
1993
+ }
1994
+ interface DescriptionListProps {
1995
+ /** The rows. */
1996
+ items?: DescriptionItem[];
1997
+ /** Term-column width in cells. Default 10. */
1998
+ gutter?: number;
1999
+ }
2000
+ /**
2001
+ * A key/value block aligned to a character gutter — the generic shape behind any
2002
+ * detail pane or summary screen. The complement to {@link List}: List is a
2003
+ * vertical menu of peers, DescriptionList is the attributes of one thing.
2004
+ *
2005
+ * INTENT, NOT STYLE: a row may carry a semantic `state` (→ framework glyph +
2006
+ * colour) and a `muted` flag (→ de-emphasised value); it never takes a raw glyph
2007
+ * or a raw colour. Term-less rows render as a full-width value line.
2008
+ */
2009
+ declare function DescriptionList({ items, gutter, }: DescriptionListProps): React.ReactElement;
2010
+
2011
+ interface LogViewProps {
2012
+ /** The full line buffer. LogView renders only the tail that fits `height`. */
2013
+ lines: string[];
2014
+ /** Viewport height in rows, including an overflow-marker row when one shows. */
2015
+ height: number;
2016
+ /** Follow the newest line (default). `false` freezes the window as lines append. */
2017
+ follow?: boolean;
2018
+ /** Wrap long lines to the viewport width (default) vs truncate them. */
2019
+ wrap?: boolean;
2020
+ /** Viewport width in cells. Omit to measure the rendered box (Ink `measureElement`). */
2021
+ width?: number;
2022
+ }
2023
+ /**
2024
+ * A bottom-anchored, height-bounded viewport over an ever-growing line stream —
2025
+ * subprocess output, a build log, a chat transcript. The newest lines stay
2026
+ * pinned to the bottom; older lines fall off the top with a dim `▴ N more`
2027
+ * marker. The mirror of a windowed {@link List}: that one follows a *focused*
2028
+ * row, this one follows the *tail*.
2029
+ *
2030
+ * Unlike Ink's `<Static>` (append-only scrollback that grows past the screen),
2031
+ * LogView is a fixed region. The app owns the subprocess and feeds `lines`;
2032
+ * LogView only renders the tail — the window advancing on new data is content
2033
+ * re-render, not motion, under the one-animation contract.
2034
+ *
2035
+ * When `wrap` is on, a logical line spans `ceil(width / cellWidth)` visual rows,
2036
+ * so the tail is counted in **visual rows**, not array entries — which needs a
2037
+ * width. Pass `width` (it's known inside a sized `Pane`); otherwise LogView
2038
+ * measures its own box, approximating for the first frame.
2039
+ */
2040
+ declare function LogView({ lines, height, follow, wrap, width, }: LogViewProps): React.ReactElement;
2041
+
2042
+ /** One hotkey: a key chip plus its terse description. */
2043
+ interface HotkeyDef {
2044
+ /** The key, lowercased: `'tab'`, `'enter'`, `'q'`, `'/'`, `'?'`. */
2045
+ k: string;
2046
+ /** Terse lowercase label for what the key does (`'switch pane'`). */
2047
+ desc: string;
2048
+ }
2049
+ interface FooterProps {
2050
+ /** Hotkeys, laid left-to-right with a 3-cell gap between them. */
2051
+ keys?: HotkeyDef[];
2052
+ /** Optional status node, flush-right in faint text (e.g. `'6 of 8'`). */
2053
+ right?: React.ReactNode;
2054
+ /**
2055
+ * Cells of breathing room above the bar. House default is `1` so the footer
2056
+ * never butts up against the content above it; pass `0` to pin it flush.
2057
+ */
2058
+ marginTop?: number;
2059
+ }
2060
+ /**
2061
+ * The always-visible hotkey bar pinned to the bottom row. A single sunken-fill
2062
+ * line: hotkeys flush-left (3-cell gaps), an optional status node flush-right
2063
+ * in faint text. Never wraps — the bar is one cell tall by contract.
2064
+ *
2065
+ * When the chips + status don't fit the terminal width, whole chips are dropped
2066
+ * from the right rather than clipped mid-word (a chip reading `he` or a `q` with
2067
+ * no label is worse than one fewer key). Apps should order `keys` by importance.
2068
+ *
2069
+ * Ink's `<Box>` has no fill (only `<Text>` takes `backgroundColor`), and a
2070
+ * terminal cell can't layer — every cell is one glyph with one fg + one bg. So
2071
+ * the solid sunken bar of the design system is built by making the *gaps and
2072
+ * padding themselves* background-carrying spaces inside a single `<Text>`, with
2073
+ * the inverse key chips nested as `<Text>` that override the bg. That paints the
2074
+ * whole row edge-to-edge rather than only the cells under the glyphs.
2075
+ *
2076
+ * A measurable (string) `right` is flush-right with an exact space fill. An
2077
+ * unmeasurable React-node `right` falls back to the flex layout below (its
2078
+ * middle gap stays unfilled), since the fill width can't be computed.
2079
+ */
2080
+ declare function Footer({ keys, right, marginTop }: FooterProps): React.ReactElement;
2081
+
2082
+ interface CursorProps {
2083
+ /** Blink while true (default), hold solid while false. */
2084
+ active?: boolean;
2085
+ /** Block colour. Defaults to `tokens.fg`. */
2086
+ color?: string;
2087
+ }
2088
+ /**
2089
+ * The blink caret — a `▎` block that toggles at 1 Hz with step-end timing,
2090
+ * blink's one sanctioned text animation. The "off" frame is a single space, so
2091
+ * the caret occupies a stable cell and never nudges the line around it.
2092
+ */
2093
+ declare function Cursor({ active, color }: CursorProps): React.ReactElement;
2094
+ interface InputProps {
2095
+ /** Title shown inside the top border. Keep ≤ 18 chars. */
2096
+ title?: string;
2097
+ /** Current field value. The app owns key handling; this is presentational. */
2098
+ value?: string;
2099
+ /** Hint shown in `fgDisabled` while the value is empty. */
2100
+ placeholder?: string;
2101
+ /** Focused fields round their border and trail a live cursor. */
2102
+ focused?: boolean;
2103
+ /** Error message — promotes the border to red and shows a line below. */
2104
+ error?: string;
2105
+ }
2106
+ /**
2107
+ * A single-line text field: a {@link Pane} wrapping one row of value (or
2108
+ * placeholder), with a {@link Cursor} trailing while focused. Value-driven and
2109
+ * presentational — the app feeds `value` and owns the keys.
2110
+ *
2111
+ * INTENT, NOT STYLE: the consumer never picks a border treatment; the field
2112
+ * derives its pane `tone` from its own state — `error` → red, `focused` →
2113
+ * lavender, otherwise resting. An error also prints a `✗ message` line below.
2114
+ */
2115
+ declare function Input({ title, value, placeholder, focused, error, }: InputProps): React.ReactElement;
2116
+
2117
+ /** One footer action: a key chip plus its label. */
2118
+ interface DialogAction {
2119
+ /** The hotkey shown in the chip, e.g. `'y'`. */
2120
+ key: string;
2121
+ /** What the key does, e.g. `'delete'`. */
2122
+ label: string;
2123
+ /** The default/confirming action — renders its chip in inverse-accent. */
2124
+ primary?: boolean;
2125
+ }
2126
+ /** Dialog emphasis, by purpose. `'default'` is a focused (lavender) modal. */
2127
+ type DialogTone = 'default' | 'error';
2128
+ interface DialogProps {
2129
+ /** Shown inside the top border. Keep ≤ 18 chars (it nests in the frame). */
2130
+ title: string;
2131
+ /**
2132
+ * Semantic emphasis: `'default'` is a focused (lavender) modal, `'error'`
2133
+ * recolours it red. The consumer never picks the shape — it is always the
2134
+ * rounded house pane.
2135
+ */
2136
+ tone?: DialogTone;
2137
+ /** Body rows, one `<Text>` line each — the convenience for plain-text bodies. */
2138
+ lines?: string[];
2139
+ /** Rich body (a `List`, glyph rows, a small form). Wins over `lines` when both given. */
2140
+ children?: React.ReactNode;
2141
+ /** Footer actions, laid out left-to-right with a 2-cell gap. */
2142
+ actions?: DialogAction[];
2143
+ /** Fixed dialog width in cells. */
2144
+ width?: number;
2145
+ }
2146
+ /**
2147
+ * A centred modal — the analogue of a "confirm" overlay in blink. There is no
2148
+ * backdrop, blur, or fade; Ink has no absolute positioning or z-index, so the
2149
+ * app renders `<Dialog/>` as a *full-screen replacement layer* instead of a
2150
+ * floating panel — it simply replaces focus.
2151
+ *
2152
+ * The frame is a fixed-width rounded {@link Pane}: lavender (focus tone) by
2153
+ * default, red for `tone="error"` — elevation is colour, never a heavier line.
2154
+ * The body is a blank line, the supplied `lines`, another blank line, then the
2155
+ * actions row. The primary action's key chip renders in inverse-accent video;
2156
+ * the rest sit muted.
2157
+ */
2158
+ declare function Dialog({ title, tone, lines, children, actions, width, }: DialogProps): React.ReactElement;
2159
+
2160
+ /** Notice severity — the only thing the consumer expresses. */
2161
+ type BannerTone = 'info' | 'success' | 'warn';
2162
+ interface BannerProps {
2163
+ /** Rich body. Wins over `text` when both are given. */
2164
+ children?: React.ReactNode;
2165
+ /** Plain-text body, the common case. */
2166
+ text?: string;
2167
+ /** Intent — the framework picks the leading glyph + its colour. Default `'info'`. */
2168
+ tone?: BannerTone;
2169
+ }
2170
+ /**
2171
+ * A one-line, non-blocking notice in the content flow — "auto-selected X",
2172
+ * "saved", "3 items skipped". The middle ground between the blocking
2173
+ * {@link Dialog} and the persistent `Footer`: it acknowledges a side effect
2174
+ * without stealing focus.
2175
+ *
2176
+ * INTENT, NOT STYLE: the consumer picks a `tone`; the framework owns the leading
2177
+ * glyph and the colour. Per the contract, semantic colour lives on the glyph —
2178
+ * the message text stays calm (`fgMuted`). Purely presentational: no timer, no
2179
+ * auto-dismiss (the app mounts/unmounts it, keeping the one-animation contract
2180
+ * clean).
2181
+ */
2182
+ declare function Banner({ children, text, tone }: BannerProps): React.ReactElement;
2183
+
2184
+ interface SpinnerProps {
2185
+ /** Advance the spinner. When false, the first frame is shown statically. Defaults to true. */
2186
+ active?: boolean;
2187
+ /** Override the glyph colour. Defaults to the info state token. */
2188
+ color?: string;
2189
+ /** Frame interval in ms. Defaults to 80 (the contract's spinner cadence). */
2190
+ intervalMs?: number;
2191
+ }
2192
+ /**
2193
+ * A one-cell animated spinner — braille for nerd/unicode, classic `| / - \` for
2194
+ * ascii. Frames advance every `intervalMs` while `active`; when inactive it
2195
+ * rests on frame 0 with no timer running.
2196
+ *
2197
+ * The frame counter comes from {@link useSpinnerFrame} and the glyph table from
2198
+ * {@link spinnerFor}, so the component stays icon-set agnostic — context decides
2199
+ * which alphabet renders.
2200
+ */
2201
+ declare function Spinner({ active, color, intervalMs }: SpinnerProps): React.ReactElement;
2202
+
2203
+ interface ProgressBarProps {
2204
+ /** Progress in `0..1` (clamped). */
2205
+ value: number;
2206
+ /** Bar length in cells. */
2207
+ width: number;
2208
+ /** Filled colour. Defaults to `tokens.accent`. */
2209
+ color?: string;
2210
+ /** Track (empty rail) colour. Defaults to `tokens.border` (surface1), matching the design system's `bar-rest`. */
2211
+ trackColor?: string;
2212
+ /** Append a ` NN%` readout in `fgDim` after the bar. Defaults to `true`, matching the design system. */
2213
+ showPercent?: boolean;
2214
+ }
2215
+ /**
2216
+ * A determinate progress bar built from the horizontal eighth-block ramp
2217
+ * ({@link blocksH}) — the complement to the indeterminate {@link Spinner}. The
2218
+ * fractional cell renders to one of eight partials for sub-cell precision.
2219
+ *
2220
+ * The empty remainder is a visible `░` rail in the track colour (the design
2221
+ * system's `[████░░░░]` treatment), not blank space — so the bar always reads
2222
+ * as a bar, full or not.
2223
+ *
2224
+ * The `ascii` icon set has no partials, so it degrades to whole `#` cells (the
2225
+ * fractional eighth is dropped) over a blank track, like every other glyph that
2226
+ * falls back.
2227
+ */
2228
+ declare function ProgressBar({ value, width, color, trackColor, showPercent }: ProgressBarProps): React.ReactElement;
2229
+
2230
+ /**
2231
+ * The states a {@link ProgressList} line can be in — the execution vocabulary of
2232
+ * any apply / migrate / sync / build. Each maps to **intent**, never style: the
2233
+ * framework owns the glyph (or the live spinner) and its semantic colour.
2234
+ *
2235
+ * - `pending` — queued, not started (`◯`, dim).
2236
+ * - `running` — executing now → the one sanctioned animation, a {@link Spinner}.
2237
+ * - `ok` / `done` — finished cleanly (`✓`, green).
2238
+ * - `failed` / `error` — finished badly (`✗`, red).
2239
+ * - `waiting` — blocked on a *manual* action and may stay so indefinitely
2240
+ * (`◐`, warn). It reads apart from `running` so a step paused on, say, a device
2241
+ * pairing is legible. (The pause prompt itself is an app `Dialog`, not a
2242
+ * primitive.)
2243
+ * - `skipped` — deliberately not run (`◌`, disabled).
2244
+ */
2245
+ type ProgressState = 'pending' | 'running' | 'ok' | 'done' | 'failed' | 'error' | 'waiting' | 'skipped';
2246
+ /** One step / task in a {@link ProgressList}. Intent only — no glyph, no colour. */
2247
+ interface ProgressItem {
2248
+ /** Stable identity, used for the active-line lookup and React keys. */
2249
+ id: string;
2250
+ /** The step's primary text. */
2251
+ label: string;
2252
+ /** A **registered** domain glyph name → its glyph + owned colour (optional column). */
2253
+ domain?: string;
2254
+ /** Execution status → the framework draws the glyph / spinner + colour. */
2255
+ state: ProgressState;
2256
+ /** Right-aligned aside (elapsed time, hint, count …), in `fgDim`. */
2257
+ meta?: string;
2258
+ }
2259
+ interface ProgressListProps {
2260
+ /** The steps, in execution order. */
2261
+ items: ProgressItem[];
2262
+ /** Id of the active (executing) line — fills with `bgFocused`, kept in view. */
2263
+ activeId?: string | null;
2264
+ /**
2265
+ * Max lines to render, including any overflow-marker rows. Omit to render every
2266
+ * line. When set and `items` exceeds it, the window always contains `activeId`
2267
+ * and follows it as it advances — keyboard-paged, like {@link List}.
2268
+ */
2269
+ height?: number;
2270
+ /** Draw `▴ N more` / `▾ N more` on overflowing sides. Default true. */
2271
+ overflowMarkers?: boolean;
2272
+ /**
2273
+ * Advance the running line's spinner. Defaults to true; pass `false` for a
2274
+ * static frame (snapshots / non-interactive renders), matching every other
2275
+ * blink motion.
2276
+ */
2277
+ animate?: boolean;
2278
+ }
2279
+ /**
2280
+ * A list of steps that transition through {@link ProgressState}s, with a live
2281
+ * {@link Spinner} on the running line — the universal apply / migrate / sync /
2282
+ * build view. The complement to {@link ProgressBar}: the bar is the aggregate
2283
+ * (compose one above this), this is the per-line detail.
2284
+ *
2285
+ * INTENT, NOT STYLE: a line carries a `state` (and an optional `domain` name);
2286
+ * the framework owns the glyph (or the spinner), its colour, the label tier, the
2287
+ * active-line fill, and the `▴/▾` overflow chrome. The consumer never passes a
2288
+ * glyph or a colour. The list windows to keep the active line in view, exactly
2289
+ * like {@link List}, since a queue is usually taller than its pane.
2290
+ */
2291
+ declare function ProgressList({ items, activeId, height, overflowMarkers, animate, }: ProgressListProps): React.ReactElement;
2292
+
2293
+ /**
2294
+ * Form — a vertical set of LABELLED fields, each a control of a known `kind`,
2295
+ * navigated by keyboard and validated by declared constraints. The universal
2296
+ * "options / config screen" primitive (git config, tokens, versions, flags).
2297
+ * Without it every app re-derives checkbox / radio / toggle from the raw
2298
+ * SELECTION glyphs — exactly the divergence blink exists to avoid.
2299
+ *
2300
+ * INTENT, NOT STYLE: a field declares its `kind`; blink owns the glyph, the
2301
+ * colour, the focus fill, the required marker, and the error line. The consumer
2302
+ * never passes a glyph or a colour. `text` / `secret` reuse {@link Input} (the
2303
+ * `▎` cursor, placeholder, error, focus border); Form is the layer above it.
2304
+ */
2305
+ /** The control a field renders — the ONLY look prop. */
2306
+ type FieldKind = 'text' | 'secret' | 'toggle' | 'select' | 'multiselect';
2307
+ /** A choice for a `select` / `multiselect` — a bare id, or an id with a label. */
2308
+ type ChoiceInput = string | {
2309
+ id: string;
2310
+ label?: string;
2311
+ };
2312
+ /** A resolved choice (id + display label). */
2313
+ interface ResolvedChoice {
2314
+ id: string;
2315
+ label: string;
2316
+ }
2317
+ /** One field in a {@link Form}. */
2318
+ interface FieldSpec {
2319
+ /** Stable key into the values object. */
2320
+ name: string;
2321
+ /** Which control to draw — the only look prop. */
2322
+ kind: FieldKind;
2323
+ /** The label above the control. */
2324
+ label: string;
2325
+ /** Required → a warn `*` marker and an empty-value error. */
2326
+ required?: boolean;
2327
+ /** Placeholder for `text` / `secret`, shown while empty. */
2328
+ placeholder?: string;
2329
+ /** Choices for `select` / `multiselect`. */
2330
+ choices?: ChoiceInput[];
2331
+ /** Derive choices from another field's current value (its selected ids). */
2332
+ optionsFrom?: string;
2333
+ /** `multiselect` lower bound — Form refuses to deselect below it. */
2334
+ min?: number;
2335
+ /** `multiselect` upper bound — Form refuses to select above it. */
2336
+ max?: number;
2337
+ }
2338
+ /** A single field's value: a string (text/secret/select), a set of ids (multiselect), or a flag (toggle). */
2339
+ type FieldValue = string | string[] | boolean | undefined;
2340
+ /** The form's value map, keyed by field name. */
2341
+ type FormValues = Record<string, FieldValue>;
2342
+ /** Validation result — `ok` plus a per-field message map. */
2343
+ interface FormValidation {
2344
+ ok: boolean;
2345
+ errors: Record<string, string>;
2346
+ }
2347
+ /** A keyboard focus stop. text/secret/toggle = one per field; select/multiselect = one per choice. */
2348
+ interface FocusStop {
2349
+ /** `name` for single-stop fields, `name::choiceId` for choice fields. */
2350
+ id: string;
2351
+ name: string;
2352
+ kind: FieldKind;
2353
+ choiceId: string | null;
2354
+ }
2355
+ /**
2356
+ * `optionsFrom` resolves the choices of a select/multiselect from another
2357
+ * field's current value (its selected ids); otherwise normalises the field's
2358
+ * own `choices` to `{ id, label }`.
2359
+ */
2360
+ declare function resolveChoices(field: FieldSpec, values?: FormValues): ResolvedChoice[];
2361
+ /**
2362
+ * Ordered focus stops. text/secret/toggle yield one stop (`id = name`);
2363
+ * select/multiselect yield one stop PER choice (`id = name::choiceId`), so nav
2364
+ * moves linearly choice → choice → next field.
2365
+ */
2366
+ declare function buildStops(fields: FieldSpec[], values?: FormValues): FocusStop[];
2367
+ /**
2368
+ * Validate `required` (any kind) and `multiselect` `min`. Pure — used by
2369
+ * {@link useFormNavigation}'s `commit()` and renderable directly into a {@link Form}.
2370
+ */
2371
+ declare function validateForm(fields: FieldSpec[], values?: FormValues): FormValidation;
2372
+ /** What {@link useFormNavigation} returns — the intents the app wires keys to. */
2373
+ interface FormNavigation {
2374
+ /** Current focus-stop id (`name` or `name::choiceId`), or null when empty. */
2375
+ focusId: string | null;
2376
+ /** The current focus stop. */
2377
+ focusStop: FocusStop | null;
2378
+ /** All stops, in order. */
2379
+ stops: FocusStop[];
2380
+ /** Move to the next / previous control (linear). */
2381
+ next: () => void;
2382
+ prev: () => void;
2383
+ /** Jump focus to a field by name. */
2384
+ focusField: (name: string) => void;
2385
+ /** `␣` on the focused control — toggles a flag / selects a choice (honouring min/max). */
2386
+ toggle: () => void;
2387
+ /** Edit a `text` / `secret` field's value. */
2388
+ setText: (name: string, str: string) => void;
2389
+ /** Validate the current values. */
2390
+ commit: () => FormValidation;
2391
+ }
2392
+ /**
2393
+ * Headless navigation for a {@link Form} — the app owns the keys and calls these
2394
+ * intents; NO Form component reads a key (the rest of blink works the same way).
2395
+ * `onChange(nextValues)` receives every value edit.
2396
+ *
2397
+ * ```tsx
2398
+ * const nav = useFormNavigation({ fields, values, onChange: setValues });
2399
+ * useInput((input, key) => {
2400
+ * if (key.downArrow) nav.next();
2401
+ * else if (key.upArrow) nav.prev();
2402
+ * else if (input === ' ') nav.toggle();
2403
+ * });
2404
+ * return <Form fields={fields} values={values} focusId={nav.focusId} errors={errors} />;
2405
+ * ```
2406
+ */
2407
+ declare function useFormNavigation({ fields, values, onChange, }: {
2408
+ fields: FieldSpec[];
2409
+ values?: FormValues;
2410
+ onChange?: (next: FormValues) => void;
2411
+ }): FormNavigation;
2412
+ interface FormProps {
2413
+ /** The fields, top to bottom. */
2414
+ fields: FieldSpec[];
2415
+ /** Current values, keyed by field name. */
2416
+ values?: FormValues;
2417
+ /** The focused stop id (from {@link useFormNavigation}'s `focusId`). */
2418
+ focusId?: string | null;
2419
+ /** Per-field error messages (e.g. from {@link validateForm}). */
2420
+ errors?: Record<string, string>;
2421
+ }
2422
+ /**
2423
+ * Render-only: `fields` + `values` + `focusId` (+ `errors`) in; glyphs, colours,
2424
+ * the focus fill, required markers, and error lines out. Reads no keys — pair it
2425
+ * with {@link useFormNavigation} and the app's `useInput`.
2426
+ */
2427
+ declare function Form({ fields, values, focusId, errors }: FormProps): React.ReactElement;
2428
+
2429
+ export { ACTIONS, Banner, type BannerProps, type BannerTone, type BlinkContextValue, type BoxChars, type BoxStyleName, type BuiltinGlyphName, CLOUD, COMMON_DOMAINS, COMPANIES, type ChoiceInput, type CommonDomainName, Cursor, type CursorProps, DATABASES, DEFAULT_GLYPH_COLOR, DEFAULT_UNICODE, DEVINFRA, type DescriptionItem, DescriptionList, type DescriptionListProps, type DetectOptions, Dialog, type DialogAction, type DialogProps, type DialogTone, type Dimensions, EDITORS, FILES, FRAMEWORKS, type FieldKind, type FieldSpec, type FieldValue, type FocusStop, Footer, type FooterProps, Form, type FormNavigation, type FormProps, type FormValidation, type FormValues, GLYPH_PACKS, type GlyphColor, type GlyphInput, type GlyphVariants, Header, type HeaderProps, type HotkeyDef, type IconSet, Input, type InputProps, LANGUAGES, List, type ListNavigation, type ListProps, ListRow, type ListRowData, type ListRowProps, type ListSelection, type ListWindow, LogView, type LogViewProps, NERD_INDEX, NERD_INDEX_SOURCES, OS, PACKAGES, PALETTE_SLOTS, type Palette, type PaletteColor, Pane, type PaneProps, type PaneTone, type PaneVariant, ProgressBar, type ProgressBarProps, type ProgressItem, ProgressList, type ProgressListProps, type ProgressState, type ResolvedChoice, SOCIAL, SYSTEM, type SelectionMode, type SelectionName, type SemanticTokens, Spinner, type SpinnerOptions, type SpinnerProps, type StateIntent, type StateName, type Theme, type ThemeControls, type ThemeDefinition, type ThemeMeta, type ThemeMode, ThemeProvider, type ThemeProviderProps, type UseListNavigationOptions, type UseListSelectionOptions, type UseListWindowOptions, allThemes, blocks, blocksH, boxChars, boxStyles, buildStops, buildTokens, catppuccinMocha, cellWidth, computeWindow, defaultTheme, deriveAscii, detectIconSet, getTheme, glyph, glyphColor, gruvbox, hasGlyph, hasTheme, latte, listThemes, mocha, mochaTokens, navGlyphs, neutral, nf, nfChar, nfHas, nord, palettes, registerGlyph, registerGlyphs, registerNerdIndex, registerTheme, registeredNames, resolveChoices, selectionIntents, spinnerFor, spinnerFrames, stateGlyph, stateGlyphs, stateIntents, tokyonight, useBlink, useBlink$1 as useBlinkContext, useFormNavigation, useGlyph, useIconSet, useListNavigation, useListSelection, useListWindow, useSpinnerFrame, useStdoutDimensions, useTheme, useThemeControls, useTokens, validateForm };