@hyperfrontend/questions 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -1,68 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var node_readline = require('node:readline');
4
-
5
- /**
6
- * Safe copies of Object built-in methods.
7
- *
8
- * These references are captured at module initialization time to protect against
9
- * prototype pollution attacks. Import only what you need for tree-shaking.
10
- *
11
- * @module @hyperfrontend/immutable-api-utils/built-in-copy/object
12
- */
13
- const _Object = globalThis.Object;
14
- /**
15
- * (Safe copy) Prevents modification of existing property attributes and values,
16
- * and prevents the addition of new properties.
17
- */
18
- const freeze = _Object.freeze;
19
-
20
- /**
21
- * Safe Promise factory and bound static methods.
22
- *
23
- * @module @hyperfrontend/immutable-api-utils/built-in-copy/promise
24
- */
25
- /* eslint-disable workspace/lib-require-jsdoc-example */
26
- const _Promise = globalThis.Promise;
27
- const _Reflect = globalThis.Reflect;
28
- /**
29
- * (Safe copy) Creates a new Promise using the captured Promise constructor.
30
- * Use this instead of `new Promise()`.
31
- *
32
- * @param executor - The executor function.
33
- * @returns A new Promise instance.
34
- */
35
- const createPromise = (executor) => _Reflect.construct(_Promise, [executor]);
36
- /**
37
- * (Safe copy) Returns a Promise that resolves with the given value.
38
- */
39
- _Promise.resolve.bind(_Promise);
40
- /**
41
- * (Safe copy) Returns a Promise that rejects with the given reason.
42
- */
43
- _Promise.reject.bind(_Promise);
44
- /**
45
- * (Safe copy) Returns a Promise that resolves when all promises resolve.
46
- */
47
- _Promise.all.bind(_Promise);
48
- /**
49
- * (Safe copy) Returns a Promise that resolves/rejects with the first settled promise.
50
- */
51
- _Promise.race.bind(_Promise);
52
- /**
53
- * (Safe copy) Returns a Promise that resolves when all promises settle.
54
- */
55
- _Promise.allSettled.bind(_Promise);
56
- /**
57
- * (Safe copy) Returns a Promise that resolves with the first fulfilled promise.
58
- */
59
- _Promise.any.bind(_Promise);
60
- /**
61
- * (Safe copy) Creates a Promise along with its resolve and reject functions.
62
- * Note: Available only in ES2024+ environments.
63
- */
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- _Promise.withResolvers?.bind(_Promise);
3
+ const index_cjs_js = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.cjs.js');
4
+ const node_readline = require('node:readline');
5
+ const index_cjs_js$1 = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.cjs.js');
6
+ const index_cjs_js$2 = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.cjs.js');
66
7
 
67
8
  /**
68
9
  * Terminal I/O utilities using Node.js readline.
@@ -72,7 +13,7 @@ _Promise.withResolvers?.bind(_Promise);
72
13
  /**
73
14
  * Key codes for terminal navigation.
74
15
  */
75
- const Key = freeze({
16
+ const Key = index_cjs_js.freeze({
76
17
  Up: '\x1B[A',
77
18
  Down: '\x1B[B',
78
19
  Left: '\x1B[D',
@@ -88,7 +29,7 @@ const Key = freeze({
88
29
  /**
89
30
  * ANSI escape codes for terminal styling.
90
31
  */
91
- const Ansi = freeze({
32
+ const Ansi = index_cjs_js.freeze({
92
33
  /** Clear entire line */
93
34
  ClearLine: '\x1B[2K',
94
35
  /** Move cursor to start of line */
@@ -107,6 +48,13 @@ const Ansi = freeze({
107
48
  * @returns ANSI escape sequence string
108
49
  */
109
50
  cursorDown: (n) => `\x1B[${n}B`,
51
+ /**
52
+ * Generates ANSI escape code to move cursor left by specified columns.
53
+ *
54
+ * @param n - Number of columns to move left
55
+ * @returns ANSI escape sequence string
56
+ */
57
+ cursorLeft: (n) => `\x1B[${n}D`,
110
58
  /** Escape code to hide cursor */
111
59
  HideCursor: '\x1B[?25l',
112
60
  /** Escape code to show cursor */
@@ -129,6 +77,8 @@ const Ansi = freeze({
129
77
  Green: '\x1B[32m',
130
78
  /** Escape code for yellow foreground */
131
79
  Yellow: '\x1B[33m',
80
+ /** Escape code for red foreground */
81
+ Red: '\x1B[31m',
132
82
  /** Escape code for gray foreground */
133
83
  Gray: '\x1B[90m',
134
84
  });
@@ -160,7 +110,7 @@ function createTerminal(config = {}) {
160
110
  const write = (text) => {
161
111
  output.write(text);
162
112
  };
163
- const readKey = () => createPromise((resolve) => {
113
+ const readKey = () => index_cjs_js$1.createPromise((resolve) => {
164
114
  const wasRaw = input.isRaw;
165
115
  if (input.setRawMode) {
166
116
  input.setRawMode(true);
@@ -178,7 +128,7 @@ function createTerminal(config = {}) {
178
128
  };
179
129
  input.once('data', onData);
180
130
  });
181
- const readLine = () => createPromise((resolve) => {
131
+ const readLine = () => index_cjs_js$1.createPromise((resolve) => {
182
132
  const readline = getReadline();
183
133
  readline.once('line', (line) => {
184
134
  resolve(line);
@@ -205,7 +155,7 @@ function createTerminal(config = {}) {
205
155
  }
206
156
  write(Ansi.ShowCursor);
207
157
  };
208
- return freeze({
158
+ return index_cjs_js.freeze({
209
159
  write,
210
160
  readKey,
211
161
  readLine,
@@ -226,7 +176,7 @@ function createTerminal(config = {}) {
226
176
  /**
227
177
  * Unicode symbols for prompt rendering.
228
178
  */
229
- const Symbol = freeze({
179
+ const Symbol = index_cjs_js.freeze({
230
180
  Pointer: '❯',
231
181
  Radio: '◯',
232
182
  RadioSelected: '◉',
@@ -240,7 +190,7 @@ const Symbol = freeze({
240
190
  /**
241
191
  * Style functions for ANSI text formatting.
242
192
  */
243
- const style = freeze({
193
+ const style = index_cjs_js.freeze({
244
194
  /**
245
195
  * Applies bold styling to text.
246
196
  *
@@ -276,6 +226,13 @@ const style = freeze({
276
226
  * @returns Text wrapped in yellow ANSI codes
277
227
  */
278
228
  yellow: (text) => `${Ansi.Yellow}${text}${Ansi.Reset}`,
229
+ /**
230
+ * Applies red color to text.
231
+ *
232
+ * @param text - Text to style
233
+ * @returns Text wrapped in red ANSI codes
234
+ */
235
+ red: (text) => `${Ansi.Red}${text}${Ansi.Reset}`,
279
236
  /**
280
237
  * Applies gray color to text.
281
238
  *
@@ -337,7 +294,7 @@ function renderCancelled() {
337
294
  /**
338
295
  * Result state of a prompt interaction.
339
296
  */
340
- const PromptResult = freeze({
297
+ const PromptResult = index_cjs_js.freeze({
341
298
  /** User submitted a value */
342
299
  Submitted: 'submitted',
343
300
  /** User cancelled the prompt (Ctrl+C) */
@@ -403,22 +360,22 @@ async function confirm(config) {
403
360
  term.write(Ansi.CursorStart + Ansi.ClearLine);
404
361
  term.write(renderMessage(config.message) + renderCancelled() + '\n');
405
362
  term.close();
406
- return freeze({ result: PromptResult.Cancelled, value: undefined });
363
+ return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
407
364
  }
408
365
  if (lowerKey === 'y') {
409
366
  drawResult(true);
410
367
  term.close();
411
- return freeze({ result: PromptResult.Submitted, value: true });
368
+ return index_cjs_js.freeze({ result: PromptResult.Submitted, value: true });
412
369
  }
413
370
  if (lowerKey === 'n') {
414
371
  drawResult(false);
415
372
  term.close();
416
- return freeze({ result: PromptResult.Submitted, value: false });
373
+ return index_cjs_js.freeze({ result: PromptResult.Submitted, value: false });
417
374
  }
418
375
  if (key === Key.Enter && config.initial !== undefined) {
419
376
  drawResult(config.initial);
420
377
  term.close();
421
- return freeze({ result: PromptResult.Submitted, value: config.initial });
378
+ return index_cjs_js.freeze({ result: PromptResult.Submitted, value: config.initial });
422
379
  }
423
380
  }
424
381
  }
@@ -446,7 +403,7 @@ function arrayIncludes(arr, value) {
446
403
  * @param query - Search query string
447
404
  * @returns Array of indices matching the query
448
405
  */
449
- function filterChoices(choices, query) {
406
+ function filterChoices$1(choices, query) {
450
407
  if (!query) {
451
408
  return choices.map((_, i) => i);
452
409
  }
@@ -457,7 +414,7 @@ function filterChoices(choices, query) {
457
414
  indices.push(i);
458
415
  }
459
416
  });
460
- return freeze(indices);
417
+ return index_cjs_js.freeze(indices);
461
418
  }
462
419
  /**
463
420
  * Creates initial state for multiselect prompt.
@@ -467,7 +424,7 @@ function filterChoices(choices, query) {
467
424
  * @returns Initial multiselect state
468
425
  */
469
426
  function createInitialState$2(config) {
470
- return freeze({
427
+ return index_cjs_js.freeze({
471
428
  cursor: 0,
472
429
  selected: config.initial ? [...config.initial] : [],
473
430
  choices: config.choices,
@@ -619,7 +576,7 @@ function processKey$2(key, state, config) {
619
576
  if (newCursor < state.scrollOffset) {
620
577
  newScrollOffset = newCursor;
621
578
  }
622
- return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
579
+ return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
623
580
  }
624
581
  if (key === Key.Down) {
625
582
  let newCursor = state.cursor + 1;
@@ -632,7 +589,7 @@ function processKey$2(key, state, config) {
632
589
  if (newCursor >= state.scrollOffset + maxVisible) {
633
590
  newScrollOffset = newCursor - maxVisible + 1;
634
591
  }
635
- return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
592
+ return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
636
593
  }
637
594
  if (key === Key.Space) {
638
595
  const actualIndex = state.filteredIndices[state.cursor];
@@ -645,22 +602,22 @@ function processKey$2(key, state, config) {
645
602
  const isSelected = arrayIncludes(state.selected, actualIndex);
646
603
  if (isSelected) {
647
604
  const newSelected = state.selected.filter((i) => i !== actualIndex);
648
- return freeze({ ...state, selected: newSelected });
605
+ return index_cjs_js.freeze({ ...state, selected: newSelected });
649
606
  }
650
607
  else {
651
608
  if (config.max !== undefined && state.selected.length >= config.max) {
652
609
  return state;
653
610
  }
654
611
  const newSelected = [...state.selected, actualIndex];
655
- return freeze({ ...state, selected: newSelected });
612
+ return index_cjs_js.freeze({ ...state, selected: newSelected });
656
613
  }
657
614
  }
658
615
  if (config.searchable) {
659
616
  if (key === Key.Backspace || key === '\b') {
660
617
  if (state.searchQuery.length > 0) {
661
618
  const newQuery = state.searchQuery.slice(0, -1);
662
- const newFiltered = filterChoices(state.choices, newQuery);
663
- return freeze({
619
+ const newFiltered = filterChoices$1(state.choices, newQuery);
620
+ return index_cjs_js.freeze({
664
621
  ...state,
665
622
  searchQuery: newQuery,
666
623
  filteredIndices: newFiltered,
@@ -672,8 +629,8 @@ function processKey$2(key, state, config) {
672
629
  }
673
630
  if (key.length === 1 && key >= ' ' && key !== Key.Space) {
674
631
  const newQuery = state.searchQuery + key;
675
- const newFiltered = filterChoices(state.choices, newQuery);
676
- return freeze({
632
+ const newFiltered = filterChoices$1(state.choices, newQuery);
633
+ return index_cjs_js.freeze({
677
634
  ...state,
678
635
  searchQuery: newQuery,
679
636
  filteredIndices: newFiltered,
@@ -753,7 +710,7 @@ async function multiselect(config) {
753
710
  term.write(Ansi.HideCursor);
754
711
  const redraw = (submitted = false) => {
755
712
  if (lineCount > 0) {
756
- term.write(Ansi.cursorUp(lineCount - 1) + Ansi.CursorStart);
713
+ term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
757
714
  }
758
715
  term.write(Ansi.ClearToEnd);
759
716
  lineCount = render$2(term, config, state, submitted);
@@ -767,13 +724,13 @@ async function multiselect(config) {
767
724
  const key = await term.readKey();
768
725
  if (term.isCancelled()) {
769
726
  if (lineCount > 0) {
770
- term.write(Ansi.cursorUp(lineCount - 1) + Ansi.CursorStart);
727
+ term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
771
728
  }
772
729
  term.write(Ansi.ClearToEnd);
773
730
  term.write(renderMessage(config.message) + renderCancelled() + '\n');
774
731
  term.write(Ansi.ShowCursor);
775
732
  term.close();
776
- return freeze({ result: PromptResult.Cancelled, value: undefined });
733
+ return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
777
734
  }
778
735
  if (key === Key.Enter) {
779
736
  const validationError = validateSelection(state, config);
@@ -784,14 +741,14 @@ async function multiselect(config) {
784
741
  }
785
742
  const selectedValues = state.selected.map((i) => state.choices[i]?.value).filter((v) => v !== undefined);
786
743
  if (lineCount > 0) {
787
- term.write(Ansi.cursorUp(lineCount - 1) + Ansi.CursorStart);
744
+ term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
788
745
  }
789
746
  term.write(Ansi.ClearToEnd);
790
747
  render$2(term, config, state, true);
791
748
  term.write('\n');
792
749
  term.write(Ansi.ShowCursor);
793
750
  term.close();
794
- return freeze({ result: PromptResult.Submitted, value: freeze(selectedValues) });
751
+ return index_cjs_js.freeze({ result: PromptResult.Submitted, value: index_cjs_js.freeze(selectedValues) });
795
752
  }
796
753
  errorMessage = undefined;
797
754
  state = processKey$2(key, state, config);
@@ -799,6 +756,27 @@ async function multiselect(config) {
799
756
  }
800
757
  }
801
758
 
759
+ /**
760
+ * Filters choices based on search query.
761
+ *
762
+ * @internal
763
+ * @param choices - All available choices
764
+ * @param query - Search query string
765
+ * @returns Array of indices matching the query
766
+ */
767
+ function filterChoices(choices, query) {
768
+ if (!query) {
769
+ return choices.map((_, i) => i);
770
+ }
771
+ const lowerQuery = query.toLowerCase();
772
+ const indices = [];
773
+ choices.forEach((choice, i) => {
774
+ if (choice.label.toLowerCase().includes(lowerQuery)) {
775
+ indices.push(i);
776
+ }
777
+ });
778
+ return index_cjs_js.freeze(indices);
779
+ }
802
780
  /**
803
781
  * Creates initial state for select prompt.
804
782
  *
@@ -807,9 +785,11 @@ async function multiselect(config) {
807
785
  * @returns Initial select state
808
786
  */
809
787
  function createInitialState$1(config) {
810
- return freeze({
788
+ return index_cjs_js.freeze({
811
789
  cursor: config.initial ?? 0,
812
790
  choices: config.choices,
791
+ filteredIndices: config.choices.map((_, i) => i),
792
+ searchQuery: '',
813
793
  scrollOffset: 0,
814
794
  });
815
795
  }
@@ -819,12 +799,12 @@ function createInitialState$1(config) {
819
799
  * @internal
820
800
  * @param state - Current prompt state
821
801
  * @param maxVisible - Maximum number of visible choices
822
- * @returns Visible choices and their start index
802
+ * @returns Object containing visible indices and start index
823
803
  */
824
804
  function getVisibleChoices(state, maxVisible) {
825
- const total = state.choices.length;
805
+ const total = state.filteredIndices.length;
826
806
  if (total <= maxVisible) {
827
- return { choices: state.choices, startIndex: 0 };
807
+ return { indices: state.filteredIndices, startIndex: 0 };
828
808
  }
829
809
  let startIndex = state.scrollOffset;
830
810
  if (state.cursor < startIndex) {
@@ -834,7 +814,7 @@ function getVisibleChoices(state, maxVisible) {
834
814
  startIndex = state.cursor - maxVisible + 1;
835
815
  }
836
816
  return {
837
- choices: state.choices.slice(startIndex, startIndex + maxVisible),
817
+ indices: state.filteredIndices.slice(startIndex, startIndex + maxVisible),
838
818
  startIndex,
839
819
  };
840
820
  }
@@ -843,13 +823,11 @@ function getVisibleChoices(state, maxVisible) {
843
823
  *
844
824
  * @internal
845
825
  * @param choice - The choice to render
846
- * @param isSelected - Whether this choice is selected
847
826
  * @param isFocused - Whether cursor is on this choice
848
827
  * @returns Formatted choice string
849
828
  */
850
- function renderChoice(choice, isSelected, isFocused) {
829
+ function renderChoice(choice, isFocused) {
851
830
  const pointer = isFocused ? style.cyan(Symbol.Pointer) : ' ';
852
- const radio = isSelected ? style.green(Symbol.RadioSelected) : style.dim(Symbol.Radio);
853
831
  let label = choice.label;
854
832
  if (choice.disabled) {
855
833
  label = style.dim(label + ' (disabled)');
@@ -858,7 +836,7 @@ function renderChoice(choice, isSelected, isFocused) {
858
836
  label = style.cyan(label);
859
837
  }
860
838
  const hint = choice.hint ? style.dim(` — ${choice.hint}`) : '';
861
- return `${pointer} ${radio} ${label}${hint}`;
839
+ return `${pointer} ${label}${hint}`;
862
840
  }
863
841
  /**
864
842
  * Renders the select prompt to the terminal.
@@ -872,36 +850,53 @@ function renderChoice(choice, isSelected, isFocused) {
872
850
  */
873
851
  function render$1(term, config, state, submitted) {
874
852
  const maxVisible = config.maxVisible ?? 10;
875
- const { choices: visibleChoices, startIndex } = getVisibleChoices(state, maxVisible);
853
+ const { indices: visibleIndices, startIndex } = getVisibleChoices(state, maxVisible);
876
854
  let output = Ansi.CursorStart + Ansi.ClearLine + renderMessage(config.message);
877
855
  if (submitted) {
878
- const selectedChoice = state.choices[state.cursor];
856
+ const actualIndex = state.filteredIndices[state.cursor];
857
+ const selectedChoice = actualIndex !== undefined ? state.choices[actualIndex] : undefined;
879
858
  /* istanbul ignore next -- @preserve defensive: cursor always within bounds */
880
859
  output += renderSubmitted(selectedChoice?.label ?? '');
881
860
  term.write(output);
882
861
  return 1;
883
862
  }
884
- output += style.dim('(use arrows, enter to select)');
863
+ if (config.searchable && state.searchQuery) {
864
+ output += style.cyan(state.searchQuery) + style.dim(' (type to filter)');
865
+ }
866
+ else if (config.searchable) {
867
+ output += style.dim('(type to filter, enter to select)');
868
+ }
869
+ else {
870
+ output += style.dim('(use arrows, enter to select)');
871
+ }
885
872
  term.write(output + '\n');
886
873
  let lineCount = 1;
887
874
  const showScrollUp = startIndex > 0;
888
- const showScrollDown = startIndex + maxVisible < state.choices.length;
875
+ const showScrollDown = startIndex + maxVisible < state.filteredIndices.length;
889
876
  if (showScrollUp) {
890
877
  term.write(Ansi.ClearLine + style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`) + '\n');
891
878
  lineCount++;
892
879
  }
893
- visibleChoices.forEach((choice, i) => {
894
- const actualIndex = startIndex + i;
895
- const isFocused = actualIndex === state.cursor;
896
- const line = renderChoice(choice, isFocused, isFocused);
880
+ visibleIndices.forEach((actualIndex, i) => {
881
+ const choice = state.choices[actualIndex];
882
+ /* istanbul ignore if -- @preserve defensive: actualIndex always valid from filteredIndices */
883
+ if (!choice)
884
+ return;
885
+ const viewIndex = startIndex + i;
886
+ const isFocused = viewIndex === state.cursor;
887
+ const line = renderChoice(choice, isFocused);
897
888
  term.write(Ansi.ClearLine + line + '\n');
898
889
  lineCount++;
899
890
  });
900
891
  if (showScrollDown) {
901
- const remaining = state.choices.length - (startIndex + maxVisible);
892
+ const remaining = state.filteredIndices.length - (startIndex + maxVisible);
902
893
  term.write(Ansi.ClearLine + style.dim(` ${Symbol.Ellipsis} (${remaining} more below)`) + '\n');
903
894
  lineCount++;
904
895
  }
896
+ if (state.filteredIndices.length === 0 && config.searchable) {
897
+ term.write(Ansi.ClearLine + style.dim(' No matches found') + '\n');
898
+ lineCount++;
899
+ }
905
900
  return lineCount;
906
901
  }
907
902
  /**
@@ -910,16 +905,17 @@ function render$1(term, config, state, submitted) {
910
905
  * @internal
911
906
  * @param key - The key that was pressed
912
907
  * @param state - Current prompt state
913
- * @param maxVisible - Maximum visible choices for scroll calculation
908
+ * @param config - Prompt configuration
914
909
  * @returns Updated state after processing the key
915
910
  */
916
- function processKey$1(key, state, maxVisible) {
917
- const total = state.choices.length;
918
- if (total === 0)
911
+ function processKey$1(key, state, config) {
912
+ const maxVisible = config.maxVisible ?? 10;
913
+ const total = state.filteredIndices.length;
914
+ if (total === 0 && key !== Key.Backspace && key !== '\b')
919
915
  return state;
920
916
  if (key === Key.Up) {
921
917
  let newCursor = state.cursor - 1;
922
- while (newCursor >= 0 && state.choices[newCursor]?.disabled) {
918
+ while (newCursor >= 0 && state.choices[state.filteredIndices[newCursor] ?? -1]?.disabled) {
923
919
  newCursor--;
924
920
  }
925
921
  if (newCursor < 0)
@@ -928,11 +924,11 @@ function processKey$1(key, state, maxVisible) {
928
924
  if (newCursor < state.scrollOffset) {
929
925
  newScrollOffset = newCursor;
930
926
  }
931
- return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
927
+ return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
932
928
  }
933
929
  if (key === Key.Down) {
934
930
  let newCursor = state.cursor + 1;
935
- while (newCursor < total && state.choices[newCursor]?.disabled) {
931
+ while (newCursor < total && state.choices[state.filteredIndices[newCursor] ?? -1]?.disabled) {
936
932
  newCursor++;
937
933
  }
938
934
  if (newCursor >= total)
@@ -941,7 +937,34 @@ function processKey$1(key, state, maxVisible) {
941
937
  if (newCursor >= state.scrollOffset + maxVisible) {
942
938
  newScrollOffset = newCursor - maxVisible + 1;
943
939
  }
944
- return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
940
+ return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
941
+ }
942
+ if (config.searchable) {
943
+ if (key === Key.Backspace || key === '\b') {
944
+ if (state.searchQuery.length > 0) {
945
+ const newQuery = state.searchQuery.slice(0, -1);
946
+ const newFiltered = filterChoices(state.choices, newQuery);
947
+ return index_cjs_js.freeze({
948
+ ...state,
949
+ searchQuery: newQuery,
950
+ filteredIndices: newFiltered,
951
+ cursor: 0,
952
+ scrollOffset: 0,
953
+ });
954
+ }
955
+ return state;
956
+ }
957
+ if (key.length === 1 && key >= ' ') {
958
+ const newQuery = state.searchQuery + key;
959
+ const newFiltered = filterChoices(state.choices, newQuery);
960
+ return index_cjs_js.freeze({
961
+ ...state,
962
+ searchQuery: newQuery,
963
+ filteredIndices: newFiltered,
964
+ cursor: 0,
965
+ scrollOffset: 0,
966
+ });
967
+ }
945
968
  }
946
969
  return state;
947
970
  }
@@ -949,7 +972,7 @@ function processKey$1(key, state, maxVisible) {
949
972
  * Prompts for single selection from a list of choices.
950
973
  *
951
974
  * Pure functional prompt with arrow key navigation, scrolling support,
952
- * and optional disabled choices.
975
+ * optional disabled choices, and optional type-to-filter search.
953
976
  *
954
977
  * @param config - Select prompt configuration
955
978
  * @returns Promise resolving to selected value or cancellation
@@ -981,17 +1004,26 @@ function processKey$1(key, state, maxVisible) {
981
1004
  * initial: 1, // Start on Pro
982
1005
  * })
983
1006
  * ```
1007
+ *
1008
+ * @example With search
1009
+ * ```typescript
1010
+ * const outcome = await select({
1011
+ * message: 'Pick a project:',
1012
+ * choices: projects.map((p) => ({ label: p.name, value: p.id })),
1013
+ * searchable: true,
1014
+ * })
1015
+ * ```
984
1016
  */
985
1017
  async function select(config) {
986
1018
  const term = createTerminal({ input: config.input, output: config.output });
987
- const maxVisible = config.maxVisible ?? 10;
988
1019
  let state = createInitialState$1(config);
989
1020
  let lineCount = 0;
990
1021
  term.write(Ansi.HideCursor);
991
1022
  const redraw = (submitted = false) => {
992
1023
  if (lineCount > 0) {
993
- term.write(Ansi.cursorUp(lineCount - 1) + Ansi.CursorStart);
1024
+ term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
994
1025
  }
1026
+ term.write(Ansi.ClearToEnd);
995
1027
  lineCount = render$1(term, config, state, submitted);
996
1028
  };
997
1029
  redraw();
@@ -999,52 +1031,38 @@ async function select(config) {
999
1031
  const key = await term.readKey();
1000
1032
  if (term.isCancelled()) {
1001
1033
  if (lineCount > 0) {
1002
- term.write(Ansi.cursorUp(lineCount - 1) + Ansi.CursorStart);
1034
+ term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
1003
1035
  }
1004
1036
  term.write(Ansi.ClearToEnd);
1005
1037
  term.write(renderMessage(config.message) + renderCancelled() + '\n');
1006
1038
  term.write(Ansi.ShowCursor);
1007
1039
  term.close();
1008
- return freeze({ result: PromptResult.Cancelled, value: undefined });
1040
+ return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
1009
1041
  }
1010
1042
  if (key === Key.Enter) {
1011
- const selectedChoice = state.choices[state.cursor];
1043
+ const actualIndex = state.filteredIndices[state.cursor];
1044
+ if (actualIndex === undefined) {
1045
+ continue;
1046
+ }
1047
+ const selectedChoice = state.choices[actualIndex];
1012
1048
  if (!selectedChoice || selectedChoice.disabled) {
1013
1049
  continue;
1014
1050
  }
1015
1051
  if (lineCount > 0) {
1016
- term.write(Ansi.cursorUp(lineCount - 1) + Ansi.CursorStart);
1052
+ term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
1017
1053
  }
1018
1054
  term.write(Ansi.ClearToEnd);
1019
1055
  render$1(term, config, state, true);
1020
1056
  term.write('\n');
1021
1057
  term.write(Ansi.ShowCursor);
1022
1058
  term.close();
1023
- return freeze({ result: PromptResult.Submitted, value: selectedChoice.value });
1059
+ return index_cjs_js.freeze({ result: PromptResult.Submitted, value: selectedChoice.value });
1024
1060
  }
1025
- state = processKey$1(key, state, maxVisible);
1061
+ state = processKey$1(key, state, config);
1026
1062
  redraw();
1027
1063
  }
1028
1064
  }
1029
1065
 
1030
- /**
1031
- * Safe copies of Math built-in methods.
1032
- *
1033
- * These references are captured at module initialization time to protect against
1034
- * prototype pollution attacks. Import only what you need for tree-shaking.
1035
- *
1036
- * @module @hyperfrontend/immutable-api-utils/built-in-copy/math
1037
- */
1038
- const _Math = globalThis.Math;
1039
- /**
1040
- * (Safe copy) Returns the larger of zero or more numbers.
1041
- */
1042
- const max = _Math.max;
1043
- /**
1044
- * (Safe copy) Returns the smaller of zero or more numbers.
1045
- */
1046
- const min = _Math.min;
1047
-
1048
1066
  /**
1049
1067
  * Creates initial state for text prompt.
1050
1068
  *
@@ -1071,14 +1089,18 @@ function createInitialState(config) {
1071
1089
  */
1072
1090
  function render(term, config, state, submitted) {
1073
1091
  const displayValue = config.format ? config.format(state.value) : state.value;
1074
- let output = renderMessage(config.message);
1092
+ const messageText = config.renderMessage ? config.renderMessage(state.value) : config.message;
1093
+ let output = renderMessage(messageText);
1094
+ let trailingChars = 0;
1075
1095
  if (submitted) {
1076
1096
  output += renderSubmitted(displayValue || config.initial || '');
1077
1097
  }
1078
1098
  else {
1079
1099
  output += displayValue;
1100
+ trailingChars = displayValue.length - state.cursorPos;
1080
1101
  if (config.initial && !state.value) {
1081
1102
  output += style.dim(config.initial);
1103
+ trailingChars += config.initial.length;
1082
1104
  }
1083
1105
  }
1084
1106
  term.write(Ansi.CursorStart + Ansi.ClearLine + output);
@@ -1086,6 +1108,9 @@ function render(term, config, state, submitted) {
1086
1108
  term.write('\n' + style.yellow(` ${state.error}`));
1087
1109
  return 2;
1088
1110
  }
1111
+ if (!submitted && trailingChars > 0) {
1112
+ term.write(Ansi.cursorLeft(trailingChars));
1113
+ }
1089
1114
  return 1;
1090
1115
  }
1091
1116
  /**
@@ -1122,13 +1147,13 @@ function processKey(key, state) {
1122
1147
  if (key === Key.Left) {
1123
1148
  return {
1124
1149
  ...state,
1125
- cursorPos: max(0, state.cursorPos - 1),
1150
+ cursorPos: index_cjs_js$2.max(0, state.cursorPos - 1),
1126
1151
  };
1127
1152
  }
1128
1153
  if (key === Key.Right) {
1129
1154
  return {
1130
1155
  ...state,
1131
- cursorPos: min(state.value.length, state.cursorPos + 1),
1156
+ cursorPos: index_cjs_js$2.min(state.value.length, state.cursorPos + 1),
1132
1157
  };
1133
1158
  }
1134
1159
  return state;
@@ -1173,7 +1198,6 @@ async function text(config) {
1173
1198
  const term = createTerminal({ input: config.input, output: config.output });
1174
1199
  let state = createInitialState(config);
1175
1200
  let lineCount = 0;
1176
- term.write(Ansi.HideCursor);
1177
1201
  const redraw = (submitted = false) => {
1178
1202
  if (lineCount > 0) {
1179
1203
  term.clearLines(lineCount);
@@ -1187,7 +1211,7 @@ async function text(config) {
1187
1211
  redraw();
1188
1212
  term.write(renderCancelled() + '\n');
1189
1213
  term.close();
1190
- return freeze({ result: PromptResult.Cancelled, value: undefined });
1214
+ return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
1191
1215
  }
1192
1216
  if (key === Key.Enter) {
1193
1217
  const value = state.value || config.initial || '';
@@ -1202,7 +1226,7 @@ async function text(config) {
1202
1226
  redraw(true);
1203
1227
  term.write('\n');
1204
1228
  term.close();
1205
- return freeze({ result: PromptResult.Submitted, value });
1229
+ return index_cjs_js.freeze({ result: PromptResult.Submitted, value });
1206
1230
  }
1207
1231
  state = processKey(key, state);
1208
1232
  redraw();
@@ -1213,5 +1237,5 @@ exports.PromptResult = PromptResult;
1213
1237
  exports.confirm = confirm;
1214
1238
  exports.multiselect = multiselect;
1215
1239
  exports.select = select;
1240
+ exports.style = style;
1216
1241
  exports.text = text;
1217
- //# sourceMappingURL=index.cjs.js.map