giggles 0.3.10 → 0.3.12

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,56 @@
1
+ // src/terminal/hooks/useTerminalFocus.ts
2
+ import { useEffect, useRef } from "react";
3
+ function useTerminalFocus(callback) {
4
+ const callbackRef = useRef(callback);
5
+ callbackRef.current = callback;
6
+ useEffect(() => {
7
+ const handler = (data) => {
8
+ const str = data.toString();
9
+ if (str.includes("\x1B[I")) callbackRef.current(true);
10
+ if (str.includes("\x1B[O")) callbackRef.current(false);
11
+ };
12
+ process.stdin.on("data", handler);
13
+ const timer = setTimeout(() => {
14
+ process.stdout.write("\x1B[?1004h");
15
+ }, 0);
16
+ return () => {
17
+ clearTimeout(timer);
18
+ process.stdout.write("\x1B[?1004l");
19
+ process.stdin.off("data", handler);
20
+ };
21
+ }, []);
22
+ }
23
+
24
+ // src/terminal/hooks/useShellout.ts
25
+ import { execa } from "execa";
26
+ import { useCallback, useState } from "react";
27
+ import { useStdin } from "ink";
28
+ function useShellOut() {
29
+ const [, setRedrawCount] = useState(0);
30
+ const { setRawMode } = useStdin();
31
+ const run = useCallback(
32
+ async (command) => {
33
+ process.stdout.write("\x1B[?1049l");
34
+ setRawMode(false);
35
+ try {
36
+ const result = await execa(command, { stdio: "inherit", shell: true, reject: false });
37
+ return { exitCode: result.exitCode ?? 0 };
38
+ } finally {
39
+ setRawMode(true);
40
+ process.stdout.write("\x1B[?1049h");
41
+ process.stdout.write("\x1B[2J");
42
+ process.stdout.write("\x1B[H");
43
+ setRedrawCount((c) => c + 1);
44
+ }
45
+ },
46
+ [setRawMode]
47
+ );
48
+ return {
49
+ run
50
+ };
51
+ }
52
+
53
+ export {
54
+ useTerminalFocus,
55
+ useShellOut
56
+ };
@@ -0,0 +1,49 @@
1
+ // src/terminal/hooks/useTerminalSize.ts
2
+ import { useEffect, useState } from "react";
3
+ function useTerminalSize() {
4
+ const [size, setSize] = useState({
5
+ rows: process.stdout.rows,
6
+ columns: process.stdout.columns
7
+ });
8
+ useEffect(() => {
9
+ const handleResize = () => {
10
+ setSize({
11
+ rows: process.stdout.rows,
12
+ columns: process.stdout.columns
13
+ });
14
+ };
15
+ process.stdout.on("resize", handleResize);
16
+ return () => {
17
+ process.stdout.off("resize", handleResize);
18
+ };
19
+ }, []);
20
+ return size;
21
+ }
22
+
23
+ // src/terminal/components/AlternateScreen.tsx
24
+ import { useEffect as useEffect2, useState as useState2 } from "react";
25
+ import { Box } from "ink";
26
+ import { Fragment, jsx } from "react/jsx-runtime";
27
+ var _a;
28
+ var isTTY = typeof process !== "undefined" && ((_a = process.stdout) == null ? void 0 : _a.write);
29
+ function AlternateScreen({ children, fullScreen = true }) {
30
+ const [ready, setReady] = useState2(!isTTY);
31
+ const { rows, columns } = useTerminalSize();
32
+ useEffect2(() => {
33
+ if (!isTTY) return;
34
+ process.stdout.write("\x1B[?1049h");
35
+ process.stdout.write("\x1B[2J");
36
+ process.stdout.write("\x1B[H");
37
+ setReady(true);
38
+ return () => {
39
+ process.stdout.write("\x1B[?1049l");
40
+ };
41
+ }, []);
42
+ if (!ready) return null;
43
+ return fullScreen ? /* @__PURE__ */ jsx(Box, { height: rows, width: columns, children }) : /* @__PURE__ */ jsx(Fragment, { children });
44
+ }
45
+
46
+ export {
47
+ useTerminalSize,
48
+ AlternateScreen
49
+ };
@@ -3,8 +3,8 @@ import {
3
3
  } from "./chunk-EVD6YPS3.js";
4
4
 
5
5
  // src/ui/CodeBlock.tsx
6
- import Prism from "prismjs";
7
6
  import { Box, Text } from "ink";
7
+ import Prism from "prismjs";
8
8
  import { jsx } from "react/jsx-runtime";
9
9
  var defaultTokenColors = {
10
10
  keyword: "#C678DD",
@@ -22,15 +22,12 @@ var defaultTokenColors = {
22
22
  inserted: "#98C379",
23
23
  deleted: "#E06C75"
24
24
  };
25
- function CodeBlock({ children, language, showBorder = true, tokenColors }) {
25
+ function CodeBlock({ children, language, tokenColors, ...boxProps }) {
26
26
  const theme = useTheme();
27
27
  const colors = { ...defaultTokenColors, ...tokenColors };
28
28
  const grammar = language ? Prism.languages[language] : void 0;
29
29
  const content = grammar ? renderTokens(Prism.tokenize(children, grammar), colors) : /* @__PURE__ */ jsx(Text, { children });
30
- if (!showBorder) {
31
- return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: content }) });
32
- }
33
- return /* @__PURE__ */ jsx(Box, { paddingX: 1, borderStyle: "single", borderColor: theme.borderColor, children: /* @__PURE__ */ jsx(Text, { children: content }) });
30
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, borderStyle: "round", borderColor: theme.borderColor, ...boxProps, children: /* @__PURE__ */ jsx(Text, { children: content }) });
34
31
  }
35
32
  function renderTokens(tokens, colors) {
36
33
  return tokens.map((token, idx) => {
package/dist/index.d.ts CHANGED
@@ -30,9 +30,10 @@ declare function useTheme(): GigglesTheme;
30
30
 
31
31
  type GigglesProviderProps = {
32
32
  theme?: Partial<GigglesTheme>;
33
+ fullScreen?: boolean;
33
34
  children: React__default.ReactNode;
34
35
  };
35
- declare function GigglesProvider({ theme, children }: GigglesProviderProps): react_jsx_runtime.JSX.Element;
36
+ declare function GigglesProvider({ theme, fullScreen, children }: GigglesProviderProps): react_jsx_runtime.JSX.Element;
36
37
 
37
38
  declare function useKeybindings(focus: FocusHandle, bindings: Keybindings, options?: KeybindingOptions): void;
38
39
 
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- import {
2
- AlternateScreen
3
- } from "./chunk-7PDVDYFB.js";
4
1
  import {
5
2
  FocusGroup,
6
3
  FocusNodeContext,
@@ -19,11 +16,14 @@ import {
19
16
  ThemeProvider,
20
17
  useTheme
21
18
  } from "./chunk-EVD6YPS3.js";
19
+ import {
20
+ AlternateScreen
21
+ } from "./chunk-5BONVNP7.js";
22
22
 
23
23
  // src/core/GigglesProvider.tsx
24
24
  import { jsx } from "react/jsx-runtime";
25
- function GigglesProvider({ theme, children }) {
26
- return /* @__PURE__ */ jsx(AlternateScreen, { children: /* @__PURE__ */ jsx(ThemeProvider, { theme, children: /* @__PURE__ */ jsx(FocusProvider, { children: /* @__PURE__ */ jsx(InputProvider, { children: /* @__PURE__ */ jsx(InputRouter, { children }) }) }) }) });
25
+ function GigglesProvider({ theme, fullScreen, children }) {
26
+ return /* @__PURE__ */ jsx(AlternateScreen, { fullScreen, children: /* @__PURE__ */ jsx(ThemeProvider, { theme, children: /* @__PURE__ */ jsx(FocusProvider, { children: /* @__PURE__ */ jsx(InputProvider, { children: /* @__PURE__ */ jsx(InputRouter, { children }) }) }) }) });
27
27
  }
28
28
 
29
29
  // src/core/router/Router.tsx
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CodeBlock
3
- } from "../chunk-WJRBHS5J.js";
3
+ } from "../chunk-JTJH45JR.js";
4
4
  import {
5
5
  useTheme
6
6
  } from "../chunk-EVD6YPS3.js";
@@ -10,9 +10,11 @@ declare function useTerminalSize(): TerminalSize;
10
10
 
11
11
  declare function useTerminalFocus(callback: (focused: boolean) => void): void;
12
12
 
13
- declare function AlternateScreen({ children }: {
13
+ type AlternateScreenProps = {
14
14
  children: ReactNode;
15
- }): react_jsx_runtime.JSX.Element | null;
15
+ fullScreen?: boolean;
16
+ };
17
+ declare function AlternateScreen({ children, fullScreen }: AlternateScreenProps): react_jsx_runtime.JSX.Element | null;
16
18
 
17
19
  declare function useShellOut(): {
18
20
  run: (command: string) => Promise<{
@@ -1,80 +1,11 @@
1
1
  import {
2
- AlternateScreen
3
- } from "../chunk-7PDVDYFB.js";
4
-
5
- // src/terminal/hooks/useTerminalSize.ts
6
- import { useEffect, useState } from "react";
7
- function useTerminalSize() {
8
- const [size, setSize] = useState({
9
- rows: process.stdout.rows,
10
- columns: process.stdout.columns
11
- });
12
- useEffect(() => {
13
- const handleResize = () => {
14
- setSize({
15
- rows: process.stdout.rows,
16
- columns: process.stdout.columns
17
- });
18
- };
19
- process.stdout.on("resize", handleResize);
20
- return () => {
21
- process.stdout.off("resize", handleResize);
22
- };
23
- }, []);
24
- return size;
25
- }
26
-
27
- // src/terminal/hooks/useTerminalFocus.ts
28
- import { useEffect as useEffect2, useRef } from "react";
29
- function useTerminalFocus(callback) {
30
- const callbackRef = useRef(callback);
31
- callbackRef.current = callback;
32
- useEffect2(() => {
33
- const handler = (data) => {
34
- const str = data.toString();
35
- if (str.includes("\x1B[I")) callbackRef.current(true);
36
- if (str.includes("\x1B[O")) callbackRef.current(false);
37
- };
38
- process.stdin.on("data", handler);
39
- const timer = setTimeout(() => {
40
- process.stdout.write("\x1B[?1004h");
41
- }, 0);
42
- return () => {
43
- clearTimeout(timer);
44
- process.stdout.write("\x1B[?1004l");
45
- process.stdin.off("data", handler);
46
- };
47
- }, []);
48
- }
49
-
50
- // src/terminal/hooks/useShellout.ts
51
- import { execa } from "execa";
52
- import { useCallback, useState as useState2 } from "react";
53
- import { useStdin } from "ink";
54
- function useShellOut() {
55
- const [, setRedrawCount] = useState2(0);
56
- const { setRawMode } = useStdin();
57
- const run = useCallback(
58
- async (command) => {
59
- process.stdout.write("\x1B[?1049l");
60
- setRawMode(false);
61
- try {
62
- const result = await execa(command, { stdio: "inherit", shell: true, reject: false });
63
- return { exitCode: result.exitCode ?? 0 };
64
- } finally {
65
- setRawMode(true);
66
- process.stdout.write("\x1B[?1049h");
67
- process.stdout.write("\x1B[2J");
68
- process.stdout.write("\x1B[H");
69
- setRedrawCount((c) => c + 1);
70
- }
71
- },
72
- [setRawMode]
73
- );
74
- return {
75
- run
76
- };
77
- }
2
+ useShellOut,
3
+ useTerminalFocus
4
+ } from "../chunk-4LEJFY5C.js";
5
+ import {
6
+ AlternateScreen,
7
+ useTerminalSize
8
+ } from "../chunk-5BONVNP7.js";
78
9
  export {
79
10
  AlternateScreen,
80
11
  useShellOut,
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { R as RegisteredKeybinding } from '../types-Dmw9TKt4.js';
3
3
  import React__default from 'react';
4
- import 'ink';
4
+ import { BoxProps } from 'ink';
5
5
 
6
6
  type CommandPaletteRenderProps = {
7
7
  query: string;
@@ -144,29 +144,30 @@ type VirtualListProps<T> = VirtualListBase<T> & ({
144
144
  });
145
145
  declare function VirtualList<T>({ items, highlightIndex, scrollOffset: controlledOffset, gap, maxVisible, paginatorStyle, render }: VirtualListProps<T>): react_jsx_runtime.JSX.Element;
146
146
 
147
- type ViewportRenderProps<T> = {
148
- item: T;
149
- index: number;
150
- focused: boolean;
147
+ type ViewportRef = {
148
+ scrollTo: (offset: number) => void;
149
+ scrollBy: (delta: number) => void;
150
+ scrollToTop: () => void;
151
+ scrollToBottom: () => void;
152
+ scrollToItem: (index: number) => void;
153
+ getScrollOffset: () => number;
154
+ getContentHeight: () => number;
155
+ getViewportHeight: () => number;
151
156
  };
152
- type ViewportProps<T> = {
153
- items: T[];
154
- maxVisible: number;
155
- showLineNumbers?: boolean;
156
- gap?: number;
157
- paginatorStyle?: PaginatorStyle;
158
- render?: (props: ViewportRenderProps<T>) => React__default.ReactNode;
157
+ type ViewportProps = {
158
+ children?: React__default.ReactNode;
159
+ height: number;
160
+ keybindings?: boolean;
161
+ footer?: React__default.ReactNode;
159
162
  };
160
- declare function Viewport<T>({ items, maxVisible, showLineNumbers, gap, paginatorStyle, render }: ViewportProps<T>): react_jsx_runtime.JSX.Element;
163
+ declare const Viewport: React__default.ForwardRefExoticComponent<ViewportProps & React__default.RefAttributes<ViewportRef>>;
161
164
 
162
- type BorderStyle = 'single' | 'double' | 'round' | 'bold' | 'singleDouble' | 'doubleSingle' | 'classic' | 'arrow';
163
- type ModalProps = {
165
+ type ModalProps = Omit<BoxProps, 'children'> & {
164
166
  children: React__default.ReactNode;
165
167
  onClose: () => void;
166
168
  title?: string;
167
- borderStyle?: BorderStyle;
168
169
  };
169
- declare function Modal({ children, onClose, title, borderStyle }: ModalProps): react_jsx_runtime.JSX.Element;
170
+ declare function Modal({ children, onClose, title, ...boxProps }: ModalProps): react_jsx_runtime.JSX.Element;
170
171
 
171
172
  type BadgeVariant = 'round' | 'arrow' | 'plain';
172
173
  type BadgeProps = {
@@ -177,14 +178,12 @@ type BadgeProps = {
177
178
  };
178
179
  declare function Badge({ children, color, background, variant }: BadgeProps): react_jsx_runtime.JSX.Element;
179
180
 
180
- type PanelProps = {
181
+ type PanelProps = Omit<BoxProps, 'children'> & {
181
182
  children: React__default.ReactNode;
182
183
  title?: string;
183
- width?: number;
184
- borderColor?: string;
185
184
  footer?: React__default.ReactNode;
186
185
  };
187
- declare function Panel({ children, title, width, borderColor, footer }: PanelProps): react_jsx_runtime.JSX.Element;
186
+ declare function Panel({ children, title, width, borderColor, footer, ...boxProps }: PanelProps): react_jsx_runtime.JSX.Element;
188
187
 
189
188
  type TokenColors = {
190
189
  keyword: string;
@@ -202,12 +201,71 @@ type TokenColors = {
202
201
  inserted: string;
203
202
  deleted: string;
204
203
  };
205
- type CodeBlockProps = {
204
+ type CodeBlockProps = Omit<BoxProps, 'children'> & {
206
205
  children: string;
207
206
  language?: string;
208
- showBorder?: boolean;
209
207
  tokenColors?: Partial<TokenColors>;
210
208
  };
211
- declare function CodeBlock({ children, language, showBorder, tokenColors }: CodeBlockProps): react_jsx_runtime.JSX.Element;
209
+ declare function CodeBlock({ children, language, tokenColors, ...boxProps }: CodeBlockProps): react_jsx_runtime.JSX.Element;
210
+
211
+ type SpinnerDef = {
212
+ frames: string[];
213
+ interval: number;
214
+ };
215
+ declare const spinners: {
216
+ line: {
217
+ frames: string[];
218
+ interval: number;
219
+ };
220
+ dot: {
221
+ frames: string[];
222
+ interval: number;
223
+ };
224
+ miniDot: {
225
+ frames: string[];
226
+ interval: number;
227
+ };
228
+ jump: {
229
+ frames: string[];
230
+ interval: number;
231
+ };
232
+ pulse: {
233
+ frames: string[];
234
+ interval: number;
235
+ };
236
+ points: {
237
+ frames: string[];
238
+ interval: number;
239
+ };
240
+ clock: {
241
+ frames: string[];
242
+ interval: number;
243
+ };
244
+ hearts: {
245
+ frames: string[];
246
+ interval: number;
247
+ };
248
+ moon: {
249
+ frames: string[];
250
+ interval: number;
251
+ };
252
+ meter: {
253
+ frames: string[];
254
+ interval: number;
255
+ };
256
+ hamburger: {
257
+ frames: string[];
258
+ interval: number;
259
+ };
260
+ ellipsis: {
261
+ frames: string[];
262
+ interval: number;
263
+ };
264
+ };
265
+ type SpinnerProps = {
266
+ spinner?: SpinnerDef;
267
+ color?: string;
268
+ };
269
+ declare function Spinner({ spinner, color }: SpinnerProps): react_jsx_runtime.JSX.Element;
212
270
 
213
- export { Autocomplete, type AutocompleteRenderProps, Badge, type BadgeProps, type BadgeVariant, CodeBlock, CommandPalette, type CommandPaletteRenderProps, Confirm, Modal, MultiSelect, type MultiSelectRenderProps, Paginator, type PaginatorStyle, Panel, type PanelProps, Select, type SelectOption, type SelectRenderProps, TextInput, type TextInputRenderProps, type TokenColors, Viewport, type ViewportRenderProps, VirtualList, type VirtualListRenderProps };
271
+ export { Autocomplete, type AutocompleteRenderProps, Badge, type BadgeProps, type BadgeVariant, CodeBlock, CommandPalette, type CommandPaletteRenderProps, Confirm, Modal, MultiSelect, type MultiSelectRenderProps, Paginator, type PaginatorStyle, Panel, type PanelProps, Select, type SelectOption, type SelectRenderProps, Spinner, type SpinnerDef, TextInput, type TextInputRenderProps, type TokenColors, Viewport, type ViewportRef, VirtualList, type VirtualListRenderProps, spinners };
package/dist/ui/index.js CHANGED
@@ -7,10 +7,14 @@ import {
7
7
  } from "../chunk-CKA5JJ4B.js";
8
8
  import {
9
9
  CodeBlock
10
- } from "../chunk-WJRBHS5J.js";
10
+ } from "../chunk-JTJH45JR.js";
11
11
  import {
12
12
  useTheme
13
13
  } from "../chunk-EVD6YPS3.js";
14
+ import "../chunk-4LEJFY5C.js";
15
+ import {
16
+ useTerminalSize
17
+ } from "../chunk-5BONVNP7.js";
14
18
 
15
19
  // src/ui/CommandPalette.tsx
16
20
  import { useState } from "react";
@@ -689,81 +693,166 @@ function Autocomplete({
689
693
  }
690
694
 
691
695
  // src/ui/Viewport.tsx
692
- import { useState as useState6 } from "react";
693
- import { Text as Text8 } from "ink";
696
+ import {
697
+ Children,
698
+ forwardRef,
699
+ isValidElement,
700
+ useCallback,
701
+ useImperativeHandle,
702
+ useLayoutEffect,
703
+ useRef as useRef4,
704
+ useState as useState6
705
+ } from "react";
706
+ import { Box as Box7, measureElement } from "ink";
694
707
  import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
695
- function Viewport({ items, maxVisible, showLineNumbers, gap, paginatorStyle, render }) {
708
+ function MeasurableItem({
709
+ children,
710
+ index,
711
+ onMeasure
712
+ }) {
713
+ const ref = useRef4(null);
714
+ useLayoutEffect(() => {
715
+ if (ref.current) {
716
+ const { height } = measureElement(ref.current);
717
+ onMeasure(index, height);
718
+ }
719
+ }, [index, onMeasure, children]);
720
+ return /* @__PURE__ */ jsx8(Box7, { ref, flexShrink: 0, width: "100%", flexDirection: "column", children });
721
+ }
722
+ var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer }, ref) {
696
723
  const focus = useFocus();
697
724
  const [scrollOffset, setScrollOffset] = useState6(0);
698
- const maxOffset = Math.max(0, items.length - maxVisible);
699
- const scroll = (delta) => {
700
- setScrollOffset((prev) => Math.max(0, Math.min(maxOffset, prev + delta)));
701
- };
702
- useKeybindings(focus, {
703
- j: () => scroll(1),
704
- k: () => scroll(-1),
705
- down: () => scroll(1),
706
- up: () => scroll(-1),
707
- pagedown: () => scroll(maxVisible),
708
- pageup: () => scroll(-maxVisible),
709
- g: () => setScrollOffset(0),
710
- G: () => setScrollOffset(maxOffset)
725
+ const contentHeightRef = useRef4(0);
726
+ const itemHeightsRef = useRef4({});
727
+ const itemKeysRef = useRef4([]);
728
+ const scrollOffsetRef = useRef4(0);
729
+ scrollOffsetRef.current = scrollOffset;
730
+ const childKeys = [];
731
+ Children.forEach(children, (child, index) => {
732
+ if (!child) return;
733
+ const key = isValidElement(child) ? child.key : null;
734
+ childKeys[index] = key !== null ? key : index;
711
735
  });
712
- const gutterWidth = showLineNumbers ? String(items.length).length + 1 : 0;
713
- return /* @__PURE__ */ jsx8(
714
- VirtualList,
715
- {
716
- items,
717
- scrollOffset,
718
- gap,
719
- maxVisible,
720
- paginatorStyle,
721
- render: ({ item, index }) => {
722
- if (render) {
723
- return render({ item, index, focused: focus.focused });
724
- }
725
- return /* @__PURE__ */ jsxs9(Text8, { dimColor: !focus.focused, children: [
726
- showLineNumbers && /* @__PURE__ */ jsxs9(Text8, { dimColor: true, children: [
727
- String(index + 1).padStart(gutterWidth - 1),
728
- " "
729
- ] }),
730
- String(item)
731
- ] });
736
+ itemKeysRef.current = childKeys;
737
+ const getMaxOffset = useCallback(() => Math.max(0, contentHeightRef.current - height), [height]);
738
+ const clampAndSetOffset = useCallback(
739
+ (offset) => {
740
+ const clamped = Math.max(0, Math.min(offset, getMaxOffset()));
741
+ setScrollOffset(clamped);
742
+ },
743
+ [getMaxOffset]
744
+ );
745
+ const handleItemMeasure = useCallback(
746
+ (index, measuredHeight) => {
747
+ const key = itemKeysRef.current[index] ?? index;
748
+ if (itemHeightsRef.current[key] === measuredHeight) return;
749
+ itemHeightsRef.current[key] = measuredHeight;
750
+ let total = 0;
751
+ for (const k of itemKeysRef.current) {
752
+ total += itemHeightsRef.current[k] ?? 0;
753
+ }
754
+ contentHeightRef.current = total;
755
+ const maxOffset = Math.max(0, total - height);
756
+ if (scrollOffsetRef.current > maxOffset) {
757
+ setScrollOffset(maxOffset);
732
758
  }
759
+ },
760
+ [height]
761
+ );
762
+ const getItemTop = useCallback((index) => {
763
+ let top = 0;
764
+ for (let i = 0; i < index && i < itemKeysRef.current.length; i++) {
765
+ const key = itemKeysRef.current[i] ?? i;
766
+ top += itemHeightsRef.current[key] ?? 0;
733
767
  }
768
+ return top;
769
+ }, []);
770
+ useImperativeHandle(
771
+ ref,
772
+ () => ({
773
+ scrollTo: (offset) => clampAndSetOffset(offset),
774
+ scrollBy: (delta) => clampAndSetOffset(scrollOffsetRef.current + delta),
775
+ scrollToTop: () => setScrollOffset(0),
776
+ scrollToBottom: () => setScrollOffset(getMaxOffset()),
777
+ scrollToItem: (index) => {
778
+ const itemTop = getItemTop(index);
779
+ const key = itemKeysRef.current[index] ?? index;
780
+ const itemHeight = itemHeightsRef.current[key] ?? 0;
781
+ const itemBottom = itemTop + itemHeight;
782
+ const current = scrollOffsetRef.current;
783
+ if (itemTop < current) {
784
+ clampAndSetOffset(itemTop);
785
+ } else if (itemBottom > current + height) {
786
+ clampAndSetOffset(itemBottom - height);
787
+ }
788
+ },
789
+ getScrollOffset: () => scrollOffsetRef.current,
790
+ getContentHeight: () => contentHeightRef.current,
791
+ getViewportHeight: () => height
792
+ }),
793
+ [clampAndSetOffset, getMaxOffset, getItemTop, height]
734
794
  );
735
- }
795
+ useKeybindings(
796
+ focus,
797
+ enableKeybindings ? {
798
+ j: () => clampAndSetOffset(scrollOffsetRef.current + 1),
799
+ k: () => clampAndSetOffset(scrollOffsetRef.current - 1),
800
+ down: () => clampAndSetOffset(scrollOffsetRef.current + 1),
801
+ up: () => clampAndSetOffset(scrollOffsetRef.current - 1),
802
+ pagedown: () => clampAndSetOffset(scrollOffsetRef.current + height),
803
+ pageup: () => clampAndSetOffset(scrollOffsetRef.current - height),
804
+ g: () => setScrollOffset(0),
805
+ G: () => setScrollOffset(getMaxOffset())
806
+ } : {}
807
+ );
808
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
809
+ /* @__PURE__ */ jsx8(Box7, { height, overflow: "hidden", children: /* @__PURE__ */ jsx8(Box7, { marginTop: -scrollOffset, flexDirection: "column", width: "100%", children: Children.map(children, (child, index) => {
810
+ if (!child) return null;
811
+ return /* @__PURE__ */ jsx8(
812
+ MeasurableItem,
813
+ {
814
+ index,
815
+ onMeasure: handleItemMeasure,
816
+ children: child
817
+ },
818
+ isValidElement(child) ? child.key ?? index : index
819
+ );
820
+ }) }) }),
821
+ footer
822
+ ] });
823
+ });
736
824
 
737
825
  // src/ui/Modal.tsx
738
- import { Box as Box7, Text as Text9 } from "ink";
826
+ import { Box as Box8, Text as Text8 } from "ink";
739
827
  import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
740
- function ModalInner({ children, onClose, title, borderStyle = "round" }) {
828
+ function ModalInner({ children, onClose, title, ...boxProps }) {
741
829
  const focus = useFocus();
742
830
  const theme = useTheme();
743
831
  useKeybindings(focus, {
744
832
  escape: onClose
745
833
  });
746
834
  return /* @__PURE__ */ jsxs10(
747
- Box7,
835
+ Box8,
748
836
  {
749
837
  flexDirection: "column",
750
838
  alignSelf: "flex-start",
751
- borderStyle,
839
+ borderStyle: "round",
752
840
  borderColor: theme.borderColor,
753
841
  paddingX: 1,
842
+ ...boxProps,
754
843
  children: [
755
- title != null && /* @__PURE__ */ jsx9(Text9, { bold: true, children: title }),
844
+ title != null && /* @__PURE__ */ jsx9(Text8, { bold: true, children: title }),
756
845
  children
757
846
  ]
758
847
  }
759
848
  );
760
849
  }
761
- function Modal({ children, onClose, title, borderStyle }) {
762
- return /* @__PURE__ */ jsx9(FocusTrap, { children: /* @__PURE__ */ jsx9(ModalInner, { onClose, title, borderStyle, children }) });
850
+ function Modal({ children, onClose, title, ...boxProps }) {
851
+ return /* @__PURE__ */ jsx9(FocusTrap, { children: /* @__PURE__ */ jsx9(ModalInner, { onClose, title, ...boxProps, children }) });
763
852
  }
764
853
 
765
854
  // src/ui/Badge.tsx
766
- import { Text as Text10 } from "ink";
855
+ import { Text as Text9 } from "ink";
767
856
  import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
768
857
  var glyphs = {
769
858
  round: ["\uE0B6", "\uE0B4"],
@@ -776,26 +865,32 @@ function Badge({ children, color, background, variant = "round" }) {
776
865
  const fg = color ?? "#000000";
777
866
  const [left, right] = glyphs[variant];
778
867
  const label = variant === "plain" ? ` ${children} ` : children;
779
- return /* @__PURE__ */ jsxs11(Text10, { children: [
780
- left && /* @__PURE__ */ jsx10(Text10, { color: bg, children: left }),
781
- /* @__PURE__ */ jsx10(Text10, { color: fg, backgroundColor: bg, bold: true, children: label }),
782
- right && /* @__PURE__ */ jsx10(Text10, { color: bg, children: right })
868
+ return /* @__PURE__ */ jsxs11(Text9, { children: [
869
+ left && /* @__PURE__ */ jsx10(Text9, { color: bg, children: left }),
870
+ /* @__PURE__ */ jsx10(Text9, { color: fg, backgroundColor: bg, bold: true, children: label }),
871
+ right && /* @__PURE__ */ jsx10(Text9, { color: bg, children: right })
783
872
  ] });
784
873
  }
785
874
 
786
875
  // src/ui/Panel.tsx
787
876
  import { useState as useState7 } from "react";
788
- import { Box as Box8, Text as Text11, measureElement } from "ink";
877
+ import { Box as Box9, Text as Text10, measureElement as measureElement2 } from "ink";
789
878
  import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
790
- function Panel({ children, title, width, borderColor, footer }) {
879
+ function parsePercentage(value, total) {
880
+ const pct = parseFloat(value);
881
+ return isNaN(pct) ? 0 : Math.floor(pct / 100 * total);
882
+ }
883
+ function Panel({ children, title, width, borderColor, footer, ...boxProps }) {
791
884
  const theme = useTheme();
885
+ const { columns } = useTerminalSize();
792
886
  const color = borderColor ?? theme.borderColor;
793
- const [measuredWidth, setMeasuredWidth] = useState7(width ?? 0);
794
- const effectiveWidth = width ?? measuredWidth;
887
+ const initialWidth = typeof width === "number" ? width : typeof width === "string" && width.endsWith("%") ? parsePercentage(width, columns) : 0;
888
+ const [measuredWidth, setMeasuredWidth] = useState7(initialWidth);
889
+ const effectiveWidth = typeof width === "number" ? width : measuredWidth;
795
890
  const renderTopBorder = () => {
796
891
  if (effectiveWidth === 0) return null;
797
892
  if (title == null) {
798
- return /* @__PURE__ */ jsxs12(Text11, { color, children: [
893
+ return /* @__PURE__ */ jsxs12(Text10, { color, children: [
799
894
  "\u256D",
800
895
  "\u2500".repeat(Math.max(0, effectiveWidth - 2)),
801
896
  "\u256E"
@@ -805,10 +900,10 @@ function Panel({ children, title, width, borderColor, footer }) {
805
900
  const displayTitle = title.length > maxTitleWidth ? title.slice(0, maxTitleWidth - 1) + "\u2026" : title;
806
901
  const titlePart = `\u256D ${displayTitle} `;
807
902
  const totalDashes = Math.max(1, effectiveWidth - titlePart.length - 1);
808
- return /* @__PURE__ */ jsxs12(Text11, { children: [
809
- /* @__PURE__ */ jsx11(Text11, { color, children: "\u256D " }),
810
- /* @__PURE__ */ jsx11(Text11, { color, bold: true, children: displayTitle }),
811
- /* @__PURE__ */ jsxs12(Text11, { color, children: [
903
+ return /* @__PURE__ */ jsxs12(Text10, { children: [
904
+ /* @__PURE__ */ jsx11(Text10, { color, children: "\u256D " }),
905
+ /* @__PURE__ */ jsx11(Text10, { color, bold: true, children: displayTitle }),
906
+ /* @__PURE__ */ jsxs12(Text10, { color, children: [
812
907
  " ",
813
908
  "\u2500".repeat(totalDashes),
814
909
  "\u256E"
@@ -816,27 +911,59 @@ function Panel({ children, title, width, borderColor, footer }) {
816
911
  ] });
817
912
  };
818
913
  return /* @__PURE__ */ jsxs12(
819
- Box8,
914
+ Box9,
820
915
  {
821
916
  flexDirection: "column",
822
917
  width,
823
918
  flexGrow: width == null ? 1 : void 0,
824
919
  ref: (node) => {
825
920
  if (node) {
826
- const { width: w } = measureElement(node);
827
- if (w !== measuredWidth) setMeasuredWidth(w);
921
+ const { width: w } = measureElement2(node);
922
+ if (w > 0 && w !== measuredWidth) setMeasuredWidth(w);
828
923
  }
829
924
  },
925
+ ...boxProps,
830
926
  children: [
831
927
  renderTopBorder(),
832
- /* @__PURE__ */ jsx11(Box8, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderTop: false, borderColor: color, paddingX: 1, children: footer ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
833
- /* @__PURE__ */ jsx11(Box8, { flexDirection: "column", flexGrow: 1, children }),
928
+ /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderTop: false, borderColor: color, paddingX: 1, children: footer ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
929
+ /* @__PURE__ */ jsx11(Box9, { flexDirection: "column", flexGrow: 1, children }),
834
930
  footer
835
931
  ] }) : children })
836
932
  ]
837
933
  }
838
934
  );
839
935
  }
936
+
937
+ // src/ui/Spinner.tsx
938
+ import { useEffect as useEffect5, useState as useState8 } from "react";
939
+ import { Text as Text11 } from "ink";
940
+ import { jsx as jsx12 } from "react/jsx-runtime";
941
+ var spinners = {
942
+ line: { frames: ["-", "\\", "|", "/"], interval: 130 },
943
+ dot: { frames: ["\u28FE", "\u28FD", "\u28FB", "\u28BF", "\u287F", "\u28DF", "\u28EF", "\u28F7"], interval: 130 },
944
+ miniDot: { frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"], interval: 80 },
945
+ jump: { frames: ["\u2884", "\u2882", "\u2881", "\u2841", "\u2848", "\u2850", "\u2860"], interval: 100 },
946
+ pulse: { frames: ["\u2588", "\u2593", "\u2592", "\u2591"], interval: 120 },
947
+ points: { frames: ["\u2219\u2219\u2219", "\u25CF\u2219\u2219", "\u2219\u25CF\u2219", "\u2219\u2219\u25CF"], interval: 200 },
948
+ clock: { frames: ["\u{1F55B}", "\u{1F550}", "\u{1F551}", "\u{1F552}", "\u{1F553}", "\u{1F554}", "\u{1F555}", "\u{1F556}", "\u{1F557}", "\u{1F558}", "\u{1F559}", "\u{1F55A}"], interval: 100 },
949
+ hearts: { frames: ["\u2764\uFE0F", "\u{1F9E1}", "\u{1F49B}", "\u{1F49A}", "\u{1F499}", "\u{1F49C}"], interval: 120 },
950
+ moon: { frames: ["\u{1F311}", "\u{1F312}", "\u{1F313}", "\u{1F314}", "\u{1F315}", "\u{1F316}", "\u{1F317}", "\u{1F318}"], interval: 180 },
951
+ meter: { frames: ["\u25B1\u25B1\u25B1", "\u25B0\u25B1\u25B1", "\u25B0\u25B0\u25B1", "\u25B0\u25B0\u25B0", "\u25B0\u25B0\u25B1", "\u25B0\u25B1\u25B1", "\u25B1\u25B1\u25B1"], interval: 100 },
952
+ hamburger: { frames: ["\u2631", "\u2632", "\u2634"], interval: 100 },
953
+ ellipsis: { frames: [". ", ".. ", "...", " "], interval: 300 }
954
+ };
955
+ function Spinner({ spinner = spinners.line, color }) {
956
+ const theme = useTheme();
957
+ const [frame, setFrame] = useState8(0);
958
+ useEffect5(() => {
959
+ setFrame(0);
960
+ const id = setInterval(() => {
961
+ setFrame((f) => (f + 1) % spinner.frames.length);
962
+ }, spinner.interval);
963
+ return () => clearInterval(id);
964
+ }, [spinner]);
965
+ return /* @__PURE__ */ jsx12(Text11, { color: color ?? theme.accentColor, children: spinner.frames[frame] });
966
+ }
840
967
  export {
841
968
  Autocomplete,
842
969
  Badge,
@@ -848,7 +975,9 @@ export {
848
975
  Paginator,
849
976
  Panel,
850
977
  Select,
978
+ Spinner,
851
979
  TextInput,
852
980
  Viewport,
853
- VirtualList
981
+ VirtualList,
982
+ spinners
854
983
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,23 +0,0 @@
1
- // src/terminal/components/AlternateScreen.tsx
2
- import { useEffect, useState } from "react";
3
- import { Fragment, jsx } from "react/jsx-runtime";
4
- var _a;
5
- var isTTY = typeof process !== "undefined" && ((_a = process.stdout) == null ? void 0 : _a.write);
6
- function AlternateScreen({ children }) {
7
- const [ready, setReady] = useState(!isTTY);
8
- useEffect(() => {
9
- if (!isTTY) return;
10
- process.stdout.write("\x1B[?1049h");
11
- process.stdout.write("\x1B[2J");
12
- process.stdout.write("\x1B[H");
13
- setReady(true);
14
- return () => {
15
- process.stdout.write("\x1B[?1049l");
16
- };
17
- }, []);
18
- return ready ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
19
- }
20
-
21
- export {
22
- AlternateScreen
23
- };