giggles 0.3.3 → 0.3.5

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.
@@ -3,6 +3,17 @@ import React__default from 'react';
3
3
  import { R as RegisteredKeybinding } from '../types-Dmw9TKt4.js';
4
4
  import 'ink';
5
5
 
6
+ type PaginatorStyle = 'arrows' | 'scrollbar' | 'counter';
7
+ type PaginatorProps = {
8
+ total: number;
9
+ offset: number;
10
+ visible: number;
11
+ style?: PaginatorStyle;
12
+ position?: 'above' | 'below';
13
+ };
14
+
15
+ declare function Paginator({ total, offset, visible, style, position }: PaginatorProps): react_jsx_runtime.JSX.Element | null;
16
+
6
17
  type CommandPaletteRenderProps = {
7
18
  query: string;
8
19
  filtered: RegisteredKeybinding[];
@@ -11,9 +22,11 @@ type CommandPaletteRenderProps = {
11
22
  };
12
23
  type CommandPaletteProps = {
13
24
  onClose: () => void;
25
+ maxVisible?: number;
26
+ paginatorStyle?: PaginatorStyle;
14
27
  render?: (props: CommandPaletteRenderProps) => React__default.ReactNode;
15
28
  };
16
- declare function CommandPalette({ onClose, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
29
+ declare function CommandPalette({ onClose, maxVisible, paginatorStyle, render }: CommandPaletteProps): react_jsx_runtime.JSX.Element;
17
30
 
18
31
  type TextInputRenderProps = {
19
32
  value: string;
@@ -51,9 +64,12 @@ type SelectProps<T> = {
51
64
  label?: string;
52
65
  immediate?: boolean;
53
66
  direction?: 'vertical' | 'horizontal';
67
+ maxVisible?: number;
68
+ paginatorStyle?: PaginatorStyle;
69
+ wrap?: boolean;
54
70
  render?: (props: SelectRenderProps<T>) => React__default.ReactNode;
55
71
  };
56
- declare function Select<T>({ options, value, onChange, onSubmit, onHighlight, label, immediate, direction, render }: SelectProps<T>): react_jsx_runtime.JSX.Element;
72
+ declare function Select<T>({ options, value, onChange, onSubmit, onHighlight, label, immediate, direction, maxVisible, paginatorStyle, wrap, render }: SelectProps<T>): react_jsx_runtime.JSX.Element;
57
73
 
58
74
  type MultiSelectRenderProps<T> = {
59
75
  option: SelectOption<T>;
@@ -69,8 +85,73 @@ type MultiSelectProps<T> = {
69
85
  onHighlight?: (value: T) => void;
70
86
  label?: string;
71
87
  direction?: 'vertical' | 'horizontal';
88
+ maxVisible?: number;
89
+ paginatorStyle?: PaginatorStyle;
90
+ wrap?: boolean;
72
91
  render?: (props: MultiSelectRenderProps<T>) => React__default.ReactNode;
73
92
  };
74
- declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, render }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
93
+ declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, maxVisible, paginatorStyle, wrap, render }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
94
+
95
+ type ConfirmProps = {
96
+ message: string;
97
+ defaultValue?: boolean;
98
+ onSubmit: (value: boolean) => void;
99
+ };
100
+ declare function Confirm({ message, defaultValue, onSubmit }: ConfirmProps): react_jsx_runtime.JSX.Element;
101
+
102
+ type AutocompleteRenderProps<T> = {
103
+ option: SelectOption<T>;
104
+ focused: boolean;
105
+ highlighted: boolean;
106
+ selected: boolean;
107
+ };
108
+ type AutocompleteProps<T> = {
109
+ options: SelectOption<T>[];
110
+ value: T;
111
+ onChange: (value: T) => void;
112
+ onSubmit?: (value: T) => void;
113
+ onHighlight?: (value: T) => void;
114
+ label?: string;
115
+ placeholder?: string;
116
+ filter?: (query: string, option: SelectOption<T>) => boolean;
117
+ maxVisible?: number;
118
+ paginatorStyle?: PaginatorStyle;
119
+ wrap?: boolean;
120
+ render?: (props: AutocompleteRenderProps<T>) => React__default.ReactNode;
121
+ };
122
+ declare function Autocomplete<T>({ options, value, onChange, onSubmit, onHighlight, label, placeholder, filter, maxVisible, paginatorStyle, wrap, render }: AutocompleteProps<T>): react_jsx_runtime.JSX.Element;
123
+
124
+ type VirtualListRenderProps<T> = {
125
+ item: T;
126
+ index: number;
127
+ };
128
+ type VirtualListBase<T> = {
129
+ items: T[];
130
+ maxVisible?: number;
131
+ paginatorStyle?: PaginatorStyle;
132
+ render: (props: VirtualListRenderProps<T>) => React__default.ReactNode;
133
+ };
134
+ type VirtualListProps<T> = VirtualListBase<T> & ({
135
+ highlightIndex?: number;
136
+ scrollOffset?: never;
137
+ } | {
138
+ highlightIndex?: never;
139
+ scrollOffset?: number;
140
+ });
141
+ declare function VirtualList<T>({ items, highlightIndex, scrollOffset: controlledOffset, maxVisible, paginatorStyle, render }: VirtualListProps<T>): react_jsx_runtime.JSX.Element;
142
+
143
+ type ViewportRenderProps<T> = {
144
+ item: T;
145
+ index: number;
146
+ focused: boolean;
147
+ };
148
+ type ViewportProps<T> = {
149
+ items: T[];
150
+ maxVisible: number;
151
+ showLineNumbers?: boolean;
152
+ paginatorStyle?: PaginatorStyle;
153
+ render?: (props: ViewportRenderProps<T>) => React__default.ReactNode;
154
+ };
155
+ declare function Viewport<T>({ items, maxVisible, showLineNumbers, paginatorStyle, render }: ViewportProps<T>): react_jsx_runtime.JSX.Element;
75
156
 
76
- export { CommandPalette, type CommandPaletteRenderProps, MultiSelect, type MultiSelectRenderProps, Select, type SelectOption, type SelectRenderProps, TextInput, type TextInputRenderProps };
157
+ export { Autocomplete, type AutocompleteRenderProps, CommandPalette, type CommandPaletteRenderProps, Confirm, MultiSelect, type MultiSelectRenderProps, Paginator, type PaginatorStyle, Select, type SelectOption, type SelectRenderProps, TextInput, type TextInputRenderProps, Viewport, type ViewportRenderProps, VirtualList, type VirtualListRenderProps };
package/dist/ui/index.js CHANGED
@@ -7,9 +7,104 @@ import {
7
7
  } from "../chunk-OYQZHF73.js";
8
8
 
9
9
  // src/ui/CommandPalette.tsx
10
- import { useState } from "react";
10
+ import { useState as useState2 } from "react";
11
+ import { Box as Box3, Text as Text2 } from "ink";
12
+
13
+ // src/ui/VirtualList.tsx
14
+ import React, { useState } from "react";
15
+ import { Box as Box2 } from "ink";
16
+
17
+ // src/ui/Paginator.tsx
11
18
  import { Box, Text } from "ink";
12
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ import { jsx, jsxs } from "react/jsx-runtime";
20
+ function Paginator({ total, offset, visible, style = "arrows", position }) {
21
+ if (total <= visible) return null;
22
+ const hasAbove = offset > 0;
23
+ const hasBelow = offset + visible < total;
24
+ if (style === "arrows") {
25
+ if (position === "above" && hasAbove) return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191" });
26
+ if (position === "below" && hasBelow) return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2193" });
27
+ return null;
28
+ }
29
+ if (style === "counter") {
30
+ if (position === "above") return null;
31
+ return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
32
+ offset + 1,
33
+ "\u2013",
34
+ Math.min(offset + visible, total),
35
+ " of ",
36
+ total
37
+ ] });
38
+ }
39
+ const thumbSize = Math.max(1, Math.round(visible / total * visible));
40
+ const maxThumbOffset = visible - thumbSize;
41
+ const maxScrollOffset = total - visible;
42
+ const thumbOffset = maxScrollOffset === 0 ? 0 : Math.round(offset / maxScrollOffset * maxThumbOffset);
43
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: Array.from({ length: visible }, (_, i) => {
44
+ const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
45
+ return /* @__PURE__ */ jsx(Text, { dimColor: !isThumb, children: isThumb ? "\u2588" : "\u2502" }, i);
46
+ }) });
47
+ }
48
+
49
+ // src/ui/VirtualList.tsx
50
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
51
+ function VirtualList({
52
+ items,
53
+ highlightIndex,
54
+ scrollOffset: controlledOffset,
55
+ maxVisible,
56
+ paginatorStyle = "arrows",
57
+ render
58
+ }) {
59
+ const [internalOffset, setInternalOffset] = useState(0);
60
+ if (maxVisible == null || items.length <= maxVisible) {
61
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx2(React.Fragment, { children: render({ item, index }) }, index)) });
62
+ }
63
+ const maxOffset = Math.max(0, items.length - maxVisible);
64
+ let offset;
65
+ if (controlledOffset != null) {
66
+ offset = Math.min(controlledOffset, maxOffset);
67
+ } else {
68
+ offset = Math.min(internalOffset, maxOffset);
69
+ if (highlightIndex != null && highlightIndex >= 0) {
70
+ if (highlightIndex < offset) {
71
+ offset = highlightIndex;
72
+ } else if (highlightIndex >= offset + maxVisible) {
73
+ offset = highlightIndex - maxVisible + 1;
74
+ }
75
+ }
76
+ if (offset !== internalOffset) {
77
+ setInternalOffset(offset);
78
+ }
79
+ }
80
+ const visible = items.slice(offset, offset + maxVisible);
81
+ const paginatorProps = {
82
+ total: items.length,
83
+ offset,
84
+ visible: maxVisible,
85
+ style: paginatorStyle
86
+ };
87
+ if (paginatorStyle === "scrollbar") {
88
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", gap: 1, children: [
89
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: visible.map((item, i) => {
90
+ const index = offset + i;
91
+ return /* @__PURE__ */ jsx2(React.Fragment, { children: render({ item, index }) }, index);
92
+ }) }),
93
+ /* @__PURE__ */ jsx2(Paginator, { ...paginatorProps })
94
+ ] });
95
+ }
96
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
97
+ /* @__PURE__ */ jsx2(Paginator, { ...paginatorProps, position: "above" }),
98
+ visible.map((item, i) => {
99
+ const index = offset + i;
100
+ return /* @__PURE__ */ jsx2(React.Fragment, { children: render({ item, index }) }, index);
101
+ }),
102
+ /* @__PURE__ */ jsx2(Paginator, { ...paginatorProps, position: "below" })
103
+ ] });
104
+ }
105
+
106
+ // src/ui/CommandPalette.tsx
107
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
13
108
  var EMPTY_KEY = {
14
109
  upArrow: false,
15
110
  downArrow: false,
@@ -42,10 +137,10 @@ function fuzzyMatch(name, query) {
42
137
  }
43
138
  return qi === lowerQuery.length;
44
139
  }
45
- function Inner({ onClose, render }) {
140
+ function Inner({ onClose, maxVisible, paginatorStyle, render }) {
46
141
  const focus = useFocus();
47
- const [query, setQuery] = useState("");
48
- const [selectedIndex, setSelectedIndex] = useState(0);
142
+ const [query, setQuery] = useState2("");
143
+ const [selectedIndex, setSelectedIndex] = useState2(0);
49
144
  const registry = useKeybindingRegistry();
50
145
  const filtered = registry.all.filter((cmd) => fuzzyMatch(cmd.name, query));
51
146
  const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
@@ -79,28 +174,37 @@ function Inner({ onClose, render }) {
79
174
  }
80
175
  );
81
176
  if (render) {
82
- return /* @__PURE__ */ jsx(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
177
+ return /* @__PURE__ */ jsx3(Fragment, { children: render({ query, filtered, selectedIndex: clampedIndex, onSelect }) });
83
178
  }
84
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", width: 40, children: [
85
- /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
86
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
87
- /* @__PURE__ */ jsx(Text, { children: query }),
88
- /* @__PURE__ */ jsx(Text, { inverse: true, children: " " })
179
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", width: 40, children: [
180
+ /* @__PURE__ */ jsxs3(Box3, { paddingX: 1, children: [
181
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "> " }),
182
+ /* @__PURE__ */ jsx3(Text2, { children: query }),
183
+ /* @__PURE__ */ jsx3(Text2, { inverse: true, children: " " })
89
184
  ] }),
90
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: filtered.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No commands found" }) }) : filtered.map((cmd, i) => /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [
91
- /* @__PURE__ */ jsx(Text, { inverse: i === clampedIndex, children: cmd.name }),
92
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: cmd.key })
93
- ] }, `${cmd.nodeId}-${cmd.key}`)) })
185
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: filtered.length === 0 ? /* @__PURE__ */ jsx3(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "No commands found" }) }) : /* @__PURE__ */ jsx3(
186
+ VirtualList,
187
+ {
188
+ items: filtered,
189
+ highlightIndex: clampedIndex,
190
+ maxVisible,
191
+ paginatorStyle,
192
+ render: ({ item: cmd, index }) => /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", paddingX: 1, children: [
193
+ /* @__PURE__ */ jsx3(Text2, { inverse: index === clampedIndex, children: cmd.name }),
194
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: cmd.key })
195
+ ] }, `${cmd.nodeId}-${cmd.key}`)
196
+ }
197
+ ) })
94
198
  ] });
95
199
  }
96
- function CommandPalette({ onClose, render }) {
97
- return /* @__PURE__ */ jsx(FocusTrap, { children: /* @__PURE__ */ jsx(Inner, { onClose, render }) });
200
+ function CommandPalette({ onClose, maxVisible, paginatorStyle, render }) {
201
+ return /* @__PURE__ */ jsx3(FocusTrap, { children: /* @__PURE__ */ jsx3(Inner, { onClose, maxVisible, paginatorStyle, render }) });
98
202
  }
99
203
 
100
204
  // src/ui/TextInput.tsx
101
205
  import { useReducer, useRef } from "react";
102
- import { Text as Text2 } from "ink";
103
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
206
+ import { Text as Text3 } from "ink";
207
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
104
208
  function TextInput({ label, value, onChange, onSubmit, placeholder, render }) {
105
209
  const focus = useFocus();
106
210
  const cursorRef = useRef(value.length);
@@ -157,29 +261,29 @@ function TextInput({ label, value, onChange, onSubmit, placeholder, render }) {
157
261
  const cursorChar = value[cursor] ?? " ";
158
262
  const after = value.slice(cursor + 1);
159
263
  if (render) {
160
- return /* @__PURE__ */ jsx2(Fragment2, { children: render({ value, focused: focus.focused, before, cursorChar, after }) });
264
+ return /* @__PURE__ */ jsx4(Fragment2, { children: render({ value, focused: focus.focused, before, cursorChar, after }) });
161
265
  }
162
266
  const displayValue = value.length > 0 ? value : placeholder ?? "";
163
267
  const isPlaceholder = value.length === 0;
164
268
  const prefix = label != null ? `${label} ` : "";
165
269
  if (focus.focused) {
166
- return /* @__PURE__ */ jsxs2(Text2, { children: [
270
+ return /* @__PURE__ */ jsxs4(Text3, { children: [
167
271
  prefix,
168
272
  before,
169
- /* @__PURE__ */ jsx2(Text2, { inverse: true, children: cursorChar }),
273
+ /* @__PURE__ */ jsx4(Text3, { inverse: true, children: cursorChar }),
170
274
  after
171
275
  ] });
172
276
  }
173
- return /* @__PURE__ */ jsxs2(Text2, { children: [
277
+ return /* @__PURE__ */ jsxs4(Text3, { children: [
174
278
  prefix,
175
- /* @__PURE__ */ jsx2(Text2, { dimColor: isPlaceholder, children: displayValue })
279
+ /* @__PURE__ */ jsx4(Text3, { dimColor: isPlaceholder, children: displayValue })
176
280
  ] });
177
281
  }
178
282
 
179
283
  // src/ui/Select.tsx
180
- import React3, { useState as useState2 } from "react";
181
- import { Box as Box2, Text as Text3 } from "ink";
182
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
284
+ import React4, { useEffect, useState as useState3 } from "react";
285
+ import { Box as Box4, Text as Text4 } from "ink";
286
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
183
287
  function Select({
184
288
  options,
185
289
  value,
@@ -189,6 +293,9 @@ function Select({
189
293
  label,
190
294
  immediate,
191
295
  direction = "vertical",
296
+ maxVisible,
297
+ paginatorStyle,
298
+ wrap = true,
192
299
  render
193
300
  }) {
194
301
  const seen = /* @__PURE__ */ new Set();
@@ -200,14 +307,16 @@ function Select({
200
307
  seen.add(key);
201
308
  }
202
309
  const focus = useFocus();
203
- const [highlightIndex, setHighlightIndex] = useState2(0);
310
+ const [highlightIndex, setHighlightIndex] = useState3(0);
204
311
  const safeIndex = options.length === 0 ? -1 : Math.min(highlightIndex, options.length - 1);
205
- if (safeIndex !== highlightIndex) {
206
- setHighlightIndex(Math.max(0, safeIndex));
207
- }
312
+ useEffect(() => {
313
+ if (safeIndex !== highlightIndex) {
314
+ setHighlightIndex(Math.max(0, safeIndex));
315
+ }
316
+ }, [safeIndex, highlightIndex]);
208
317
  const moveHighlight = (delta) => {
209
318
  if (options.length === 0) return;
210
- const next2 = Math.max(0, Math.min(options.length - 1, safeIndex + delta));
319
+ const next2 = wrap ? (safeIndex + delta + options.length) % options.length : Math.max(0, Math.min(options.length - 1, safeIndex + delta));
211
320
  if (next2 !== safeIndex) {
212
321
  setHighlightIndex(next2);
213
322
  onHighlight == null ? void 0 : onHighlight(options[next2].value);
@@ -232,27 +341,37 @@ function Select({
232
341
  }
233
342
  });
234
343
  const isHorizontal = direction === "horizontal";
235
- return /* @__PURE__ */ jsxs3(Box2, { flexDirection: isHorizontal ? "row" : "column", gap: isHorizontal ? 1 : 0, children: [
236
- label != null && /* @__PURE__ */ jsx3(Text3, { children: label }),
237
- options.map((option, index) => {
238
- const highlighted = index === safeIndex;
239
- const selected = option.value === value;
240
- if (render) {
241
- return /* @__PURE__ */ jsx3(React3.Fragment, { children: render({ option, focused: focus.focused, highlighted, selected }) }, String(option.value));
344
+ const renderOption = ({ item: option, index }) => {
345
+ const highlighted = index === safeIndex;
346
+ const selected = option.value === value;
347
+ if (render) {
348
+ return render({ option, focused: focus.focused, highlighted, selected });
349
+ }
350
+ return /* @__PURE__ */ jsxs5(Text4, { dimColor: !focus.focused, children: [
351
+ highlighted ? ">" : " ",
352
+ " ",
353
+ option.label
354
+ ] }, String(option.value));
355
+ };
356
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: isHorizontal ? "row" : "column", gap: isHorizontal ? 1 : 0, children: [
357
+ label != null && /* @__PURE__ */ jsx5(Text4, { children: label }),
358
+ isHorizontal ? options.map((option, index) => /* @__PURE__ */ jsx5(React4.Fragment, { children: renderOption({ item: option, index }) }, String(option.value))) : /* @__PURE__ */ jsx5(
359
+ VirtualList,
360
+ {
361
+ items: options,
362
+ highlightIndex: safeIndex,
363
+ maxVisible,
364
+ paginatorStyle,
365
+ render: renderOption
242
366
  }
243
- return /* @__PURE__ */ jsxs3(Text3, { dimColor: !focus.focused, children: [
244
- highlighted ? ">" : " ",
245
- " ",
246
- option.label
247
- ] }, String(option.value));
248
- })
367
+ )
249
368
  ] });
250
369
  }
251
370
 
252
371
  // src/ui/MultiSelect.tsx
253
- import React4, { useState as useState3 } from "react";
254
- import { Box as Box3, Text as Text4 } from "ink";
255
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
372
+ import React5, { useEffect as useEffect2, useState as useState4 } from "react";
373
+ import { Box as Box5, Text as Text5 } from "ink";
374
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
256
375
  function MultiSelect({
257
376
  options,
258
377
  value,
@@ -261,6 +380,9 @@ function MultiSelect({
261
380
  onHighlight,
262
381
  label,
263
382
  direction = "vertical",
383
+ maxVisible,
384
+ paginatorStyle,
385
+ wrap = true,
264
386
  render
265
387
  }) {
266
388
  const seen = /* @__PURE__ */ new Set();
@@ -272,14 +394,16 @@ function MultiSelect({
272
394
  seen.add(key);
273
395
  }
274
396
  const focus = useFocus();
275
- const [highlightIndex, setHighlightIndex] = useState3(0);
397
+ const [highlightIndex, setHighlightIndex] = useState4(0);
276
398
  const safeIndex = options.length === 0 ? -1 : Math.min(highlightIndex, options.length - 1);
277
- if (safeIndex !== highlightIndex) {
278
- setHighlightIndex(Math.max(0, safeIndex));
279
- }
399
+ useEffect2(() => {
400
+ if (safeIndex !== highlightIndex) {
401
+ setHighlightIndex(Math.max(0, safeIndex));
402
+ }
403
+ }, [safeIndex, highlightIndex]);
280
404
  const moveHighlight = (delta) => {
281
405
  if (options.length === 0) return;
282
- const next2 = Math.max(0, Math.min(options.length - 1, safeIndex + delta));
406
+ const next2 = wrap ? (safeIndex + delta + options.length) % options.length : Math.max(0, Math.min(options.length - 1, safeIndex + delta));
283
407
  if (next2 !== safeIndex) {
284
408
  setHighlightIndex(next2);
285
409
  onHighlight == null ? void 0 : onHighlight(options[next2].value);
@@ -300,27 +424,259 @@ function MultiSelect({
300
424
  ...onSubmit && { enter: () => onSubmit(value) }
301
425
  });
302
426
  const isHorizontal = direction === "horizontal";
303
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: isHorizontal ? "row" : "column", gap: isHorizontal ? 1 : 0, children: [
304
- label != null && /* @__PURE__ */ jsx4(Text4, { children: label }),
305
- options.map((option, index) => {
306
- const highlighted = index === safeIndex;
307
- const selected = value.includes(option.value);
308
- if (render) {
309
- return /* @__PURE__ */ jsx4(React4.Fragment, { children: render({ option, focused: focus.focused, highlighted, selected }) }, String(option.value));
427
+ const renderOption = ({ item: option, index }) => {
428
+ const highlighted = index === safeIndex;
429
+ const selected = value.includes(option.value);
430
+ if (render) {
431
+ return render({ option, focused: focus.focused, highlighted, selected });
432
+ }
433
+ return /* @__PURE__ */ jsxs6(Text5, { dimColor: !focus.focused, children: [
434
+ highlighted ? ">" : " ",
435
+ " [",
436
+ selected ? "x" : " ",
437
+ "] ",
438
+ option.label
439
+ ] }, String(option.value));
440
+ };
441
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: isHorizontal ? "row" : "column", gap: isHorizontal ? 1 : 0, children: [
442
+ label != null && /* @__PURE__ */ jsx6(Text5, { children: label }),
443
+ isHorizontal ? options.map((option, index) => /* @__PURE__ */ jsx6(React5.Fragment, { children: renderOption({ item: option, index }) }, String(option.value))) : /* @__PURE__ */ jsx6(
444
+ VirtualList,
445
+ {
446
+ items: options,
447
+ highlightIndex: safeIndex,
448
+ maxVisible,
449
+ paginatorStyle,
450
+ render: renderOption
310
451
  }
311
- return /* @__PURE__ */ jsxs4(Text4, { dimColor: !focus.focused, children: [
312
- highlighted ? ">" : " ",
313
- " [",
314
- selected ? "x" : " ",
315
- "] ",
316
- option.label
317
- ] }, String(option.value));
318
- })
452
+ )
319
453
  ] });
320
454
  }
455
+
456
+ // src/ui/Confirm.tsx
457
+ import { Text as Text6 } from "ink";
458
+ import { jsxs as jsxs7 } from "react/jsx-runtime";
459
+ function Confirm({ message, defaultValue = true, onSubmit }) {
460
+ const focus = useFocus();
461
+ useKeybindings(focus, {
462
+ y: () => onSubmit(true),
463
+ n: () => onSubmit(false),
464
+ enter: () => onSubmit(defaultValue)
465
+ });
466
+ const hint = defaultValue ? "Y/n" : "y/N";
467
+ return /* @__PURE__ */ jsxs7(Text6, { dimColor: !focus.focused, children: [
468
+ message,
469
+ " (",
470
+ hint,
471
+ ")"
472
+ ] });
473
+ }
474
+
475
+ // src/ui/Autocomplete.tsx
476
+ import { useEffect as useEffect3, useMemo, useReducer as useReducer2, useRef as useRef2, useState as useState5 } from "react";
477
+ import { Box as Box6, Text as Text7 } from "ink";
478
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
479
+ function defaultFilter(query, option) {
480
+ const caseSensitive = query !== query.toLowerCase();
481
+ if (caseSensitive) {
482
+ return option.label.includes(query);
483
+ }
484
+ return option.label.toLowerCase().includes(query);
485
+ }
486
+ function Autocomplete({
487
+ options,
488
+ value,
489
+ onChange,
490
+ onSubmit,
491
+ onHighlight,
492
+ label,
493
+ placeholder,
494
+ filter = defaultFilter,
495
+ maxVisible,
496
+ paginatorStyle,
497
+ wrap = true,
498
+ render
499
+ }) {
500
+ const seen = /* @__PURE__ */ new Set();
501
+ for (const opt of options) {
502
+ const key = String(opt.value);
503
+ if (seen.has(key)) {
504
+ throw new GigglesError("Autocomplete options must have unique values");
505
+ }
506
+ seen.add(key);
507
+ }
508
+ const focus = useFocus();
509
+ const [query, setQuery] = useState5("");
510
+ const [highlightIndex, setHighlightIndex] = useState5(0);
511
+ const cursorRef = useRef2(0);
512
+ const [, forceRender] = useReducer2((c) => c + 1, 0);
513
+ const filtered = useMemo(
514
+ () => query.length === 0 ? options : options.filter((opt) => filter(query, opt)),
515
+ [options, query, filter]
516
+ );
517
+ const safeIndex = filtered.length === 0 ? -1 : Math.min(highlightIndex, filtered.length - 1);
518
+ useEffect3(() => {
519
+ if (safeIndex >= 0 && safeIndex !== highlightIndex) {
520
+ setHighlightIndex(safeIndex);
521
+ }
522
+ }, [safeIndex, highlightIndex]);
523
+ const cursor = Math.min(cursorRef.current, query.length);
524
+ cursorRef.current = cursor;
525
+ const moveHighlight = (delta) => {
526
+ if (filtered.length === 0) return;
527
+ const next = wrap ? (safeIndex + delta + filtered.length) % filtered.length : Math.max(0, Math.min(filtered.length - 1, safeIndex + delta));
528
+ if (next !== safeIndex) {
529
+ setHighlightIndex(next);
530
+ onHighlight == null ? void 0 : onHighlight(filtered[next].value);
531
+ }
532
+ };
533
+ const updateQuery = (newQuery) => {
534
+ setQuery(newQuery);
535
+ setHighlightIndex(0);
536
+ };
537
+ useKeybindings(
538
+ focus,
539
+ {
540
+ up: () => moveHighlight(-1),
541
+ down: () => moveHighlight(1),
542
+ left: () => {
543
+ cursorRef.current = Math.max(0, cursorRef.current - 1);
544
+ forceRender();
545
+ },
546
+ right: () => {
547
+ cursorRef.current = Math.min(query.length, cursorRef.current + 1);
548
+ forceRender();
549
+ },
550
+ home: () => {
551
+ cursorRef.current = 0;
552
+ forceRender();
553
+ },
554
+ end: () => {
555
+ cursorRef.current = query.length;
556
+ forceRender();
557
+ },
558
+ backspace: () => {
559
+ const c = cursorRef.current;
560
+ if (c > 0) {
561
+ cursorRef.current = c - 1;
562
+ updateQuery(query.slice(0, c - 1) + query.slice(c));
563
+ }
564
+ },
565
+ delete: () => {
566
+ const c = cursorRef.current;
567
+ if (c < query.length) {
568
+ updateQuery(query.slice(0, c) + query.slice(c + 1));
569
+ }
570
+ },
571
+ enter: () => {
572
+ if (filtered.length === 0) return;
573
+ const selected = filtered[safeIndex].value;
574
+ onChange(selected);
575
+ onSubmit == null ? void 0 : onSubmit(selected);
576
+ }
577
+ },
578
+ {
579
+ capture: true,
580
+ passthrough: ["tab", "shift+tab", "escape"],
581
+ onKeypress: (input, key) => {
582
+ if (input.length === 1 && !key.ctrl && !key.return && !key.escape && !key.tab) {
583
+ const c = cursorRef.current;
584
+ cursorRef.current = c + 1;
585
+ updateQuery(query.slice(0, c) + input + query.slice(c));
586
+ }
587
+ }
588
+ }
589
+ );
590
+ const before = query.slice(0, cursor);
591
+ const cursorChar = query[cursor] ?? " ";
592
+ const after = query.slice(cursor + 1);
593
+ const prefix = label != null ? `${label} ` : "";
594
+ return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
595
+ focus.focused ? /* @__PURE__ */ jsxs8(Text7, { children: [
596
+ prefix,
597
+ before,
598
+ /* @__PURE__ */ jsx7(Text7, { inverse: true, children: cursorChar }),
599
+ after
600
+ ] }) : /* @__PURE__ */ jsxs8(Text7, { dimColor: true, children: [
601
+ prefix,
602
+ query.length > 0 ? query : placeholder ?? ""
603
+ ] }),
604
+ /* @__PURE__ */ jsx7(
605
+ VirtualList,
606
+ {
607
+ items: filtered,
608
+ highlightIndex: safeIndex,
609
+ maxVisible,
610
+ paginatorStyle,
611
+ render: ({ item: option, index }) => {
612
+ const highlighted = index === safeIndex;
613
+ const selected = option.value === value;
614
+ if (render) {
615
+ return render({ option, focused: focus.focused, highlighted, selected });
616
+ }
617
+ return /* @__PURE__ */ jsxs8(Text7, { dimColor: !focus.focused, children: [
618
+ highlighted ? ">" : " ",
619
+ " ",
620
+ option.label
621
+ ] });
622
+ }
623
+ }
624
+ )
625
+ ] });
626
+ }
627
+
628
+ // src/ui/Viewport.tsx
629
+ import { useState as useState6 } from "react";
630
+ import { Text as Text8 } from "ink";
631
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
632
+ function Viewport({ items, maxVisible, showLineNumbers, paginatorStyle, render }) {
633
+ const focus = useFocus();
634
+ const [scrollOffset, setScrollOffset] = useState6(0);
635
+ const maxOffset = Math.max(0, items.length - maxVisible);
636
+ const scroll = (delta) => {
637
+ setScrollOffset((prev) => Math.max(0, Math.min(maxOffset, prev + delta)));
638
+ };
639
+ useKeybindings(focus, {
640
+ j: () => scroll(1),
641
+ k: () => scroll(-1),
642
+ down: () => scroll(1),
643
+ up: () => scroll(-1),
644
+ pagedown: () => scroll(maxVisible),
645
+ pageup: () => scroll(-maxVisible),
646
+ g: () => setScrollOffset(0),
647
+ G: () => setScrollOffset(maxOffset)
648
+ });
649
+ const gutterWidth = showLineNumbers ? String(items.length).length + 1 : 0;
650
+ return /* @__PURE__ */ jsx8(
651
+ VirtualList,
652
+ {
653
+ items,
654
+ scrollOffset,
655
+ maxVisible,
656
+ paginatorStyle,
657
+ render: ({ item, index }) => {
658
+ if (render) {
659
+ return render({ item, index, focused: focus.focused });
660
+ }
661
+ return /* @__PURE__ */ jsxs9(Text8, { dimColor: !focus.focused, children: [
662
+ showLineNumbers && /* @__PURE__ */ jsxs9(Text8, { dimColor: true, children: [
663
+ String(index + 1).padStart(gutterWidth - 1),
664
+ " "
665
+ ] }),
666
+ String(item)
667
+ ] });
668
+ }
669
+ }
670
+ );
671
+ }
321
672
  export {
673
+ Autocomplete,
322
674
  CommandPalette,
675
+ Confirm,
323
676
  MultiSelect,
677
+ Paginator,
324
678
  Select,
325
- TextInput
679
+ TextInput,
680
+ Viewport,
681
+ VirtualList
326
682
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",