giggles 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,13 +76,14 @@ function useFocusNode(options) {
76
76
  const store = useStore();
77
77
  const contextParentId = useContext2(ScopeIdContext);
78
78
  const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
79
+ const focusKey = options == null ? void 0 : options.focusKey;
79
80
  const subscribe = useMemo(() => store.subscribe.bind(store), [store]);
80
81
  useEffect2(() => {
81
- store.registerNode(id, parentId);
82
+ store.registerNode(id, parentId, focusKey);
82
83
  return () => {
83
84
  store.unregisterNode(id);
84
85
  };
85
- }, [id, parentId, store]);
86
+ }, [id, parentId, focusKey, store]);
86
87
  const hasFocus = useSyncExternalStore2(subscribe, () => store.isFocused(id));
87
88
  return { id, hasFocus };
88
89
  }
@@ -128,13 +129,14 @@ function useFocusScope(options) {
128
129
  const store = useStore();
129
130
  const contextParentId = useContext3(ScopeIdContext);
130
131
  const parentId = ((_a = options == null ? void 0 : options.parent) == null ? void 0 : _a.id) ?? contextParentId;
132
+ const focusKey = options == null ? void 0 : options.focusKey;
131
133
  const subscribe = useMemo2(() => store.subscribe.bind(store), [store]);
132
134
  useEffect4(() => {
133
- store.registerNode(id, parentId);
135
+ store.registerNode(id, parentId, focusKey);
134
136
  return () => {
135
137
  store.unregisterNode(id);
136
138
  };
137
- }, [id, parentId, store]);
139
+ }, [id, parentId, focusKey, store]);
138
140
  const hasFocus = useSyncExternalStore3(subscribe, () => store.isFocused(id));
139
141
  const isPassive = useSyncExternalStore3(subscribe, () => store.isPassive(id));
140
142
  const next = useCallback(() => store.navigateSibling("next", true, id), [store, id]);
@@ -143,7 +145,9 @@ function useFocusScope(options) {
143
145
  const prevShallow = useCallback(() => store.navigateSibling("prev", true, id, true), [store, id]);
144
146
  const escape = useCallback(() => store.makePassive(id), [store, id]);
145
147
  const drillIn = useCallback(() => store.focusFirstChild(id), [store, id]);
146
- const resolvedBindings = typeof (options == null ? void 0 : options.keybindings) === "function" ? options.keybindings({ next, prev, nextShallow, prevShallow, escape, drillIn }) : (options == null ? void 0 : options.keybindings) ?? {};
148
+ const focusChild = useCallback((key) => store.focusChildByKey(id, key, false), [store, id]);
149
+ const focusChildShallow = useCallback((key) => store.focusChildByKey(id, key, true), [store, id]);
150
+ const resolvedBindings = typeof (options == null ? void 0 : options.keybindings) === "function" ? options.keybindings({ next, prev, nextShallow, prevShallow, escape, drillIn, focusChild, focusChildShallow }) : (options == null ? void 0 : options.keybindings) ?? {};
147
151
  store.registerKeybindings(id, keybindingRegistrationId, resolvedBindings);
148
152
  useEffect4(() => {
149
153
  return () => {
@@ -157,7 +161,19 @@ function useFocusScope(options) {
157
161
  );
158
162
  }
159
163
  }, [id, store]);
160
- return { id, hasFocus, isPassive, next, prev, nextShallow, prevShallow, escape, drillIn };
164
+ return {
165
+ id,
166
+ hasFocus,
167
+ isPassive,
168
+ next,
169
+ prev,
170
+ nextShallow,
171
+ prevShallow,
172
+ escape,
173
+ drillIn,
174
+ focusChild,
175
+ focusChildShallow
176
+ };
161
177
  }
162
178
 
163
179
  // src/core/focus/FocusScope.tsx
@@ -212,6 +228,8 @@ var FocusStore = class {
212
228
  // A keybinding may exist for a node that has not yet appeared in the node tree —
213
229
  // this is safe because dispatch only walks nodes in the active branch path.
214
230
  keybindings = /* @__PURE__ */ new Map();
231
+ // parentId → focusKey → childId
232
+ keyIndex = /* @__PURE__ */ new Map();
215
233
  // ---------------------------------------------------------------------------
216
234
  // Subscription
217
235
  // ---------------------------------------------------------------------------
@@ -231,7 +249,7 @@ var FocusStore = class {
231
249
  // ---------------------------------------------------------------------------
232
250
  // Registration
233
251
  // ---------------------------------------------------------------------------
234
- registerNode(id, parentId) {
252
+ registerNode(id, parentId, focusKey) {
235
253
  const node = { id, parentId, childrenIds: [] };
236
254
  this.nodes.set(id, node);
237
255
  this.parentMap.set(id, parentId);
@@ -251,6 +269,12 @@ var FocusStore = class {
251
269
  node.childrenIds.push(existingId);
252
270
  }
253
271
  }
272
+ if (focusKey && parentId) {
273
+ if (!this.keyIndex.has(parentId)) {
274
+ this.keyIndex.set(parentId, /* @__PURE__ */ new Map());
275
+ }
276
+ this.keyIndex.get(parentId).set(focusKey, id);
277
+ }
254
278
  if (this.nodes.size === 1) {
255
279
  this.focusNode(id);
256
280
  }
@@ -268,6 +292,21 @@ var FocusStore = class {
268
292
  this.nodes.delete(id);
269
293
  this.passiveSet.delete(id);
270
294
  this.pendingFocusFirstChild.delete(id);
295
+ if (node.parentId) {
296
+ const parentKeys = this.keyIndex.get(node.parentId);
297
+ if (parentKeys) {
298
+ for (const [key, childId] of parentKeys) {
299
+ if (childId === id) {
300
+ parentKeys.delete(key);
301
+ break;
302
+ }
303
+ }
304
+ if (parentKeys.size === 0) {
305
+ this.keyIndex.delete(node.parentId);
306
+ }
307
+ }
308
+ }
309
+ this.keyIndex.delete(id);
271
310
  if (this.focusedId === id) {
272
311
  let candidate = node.parentId;
273
312
  while (candidate !== null) {
@@ -315,6 +354,21 @@ var FocusStore = class {
315
354
  this.pendingFocusFirstChild.add(parentId);
316
355
  }
317
356
  }
357
+ focusChildByKey(parentId, key, shallow) {
358
+ var _a;
359
+ const childId = (_a = this.keyIndex.get(parentId)) == null ? void 0 : _a.get(key);
360
+ if (!childId || !this.nodes.has(childId)) return;
361
+ if (shallow) {
362
+ this.focusNode(childId);
363
+ } else {
364
+ const child = this.nodes.get(childId);
365
+ if (child.childrenIds.length > 0) {
366
+ this.focusFirstChild(childId);
367
+ } else {
368
+ this.focusNode(childId);
369
+ }
370
+ }
371
+ }
318
372
  // ---------------------------------------------------------------------------
319
373
  // Navigation
320
374
  // ---------------------------------------------------------------------------
package/dist/index.d.ts CHANGED
@@ -47,10 +47,13 @@ type FocusScopeHandle = {
47
47
  prevShallow: () => void;
48
48
  escape: () => void;
49
49
  drillIn: () => void;
50
+ focusChild: (key: string) => void;
51
+ focusChildShallow: (key: string) => void;
50
52
  };
51
- type FocusScopeHelpers = Pick<FocusScopeHandle, 'next' | 'prev' | 'nextShallow' | 'prevShallow' | 'escape' | 'drillIn'>;
53
+ type FocusScopeHelpers = Pick<FocusScopeHandle, 'next' | 'prev' | 'nextShallow' | 'prevShallow' | 'escape' | 'drillIn' | 'focusChild' | 'focusChildShallow'>;
52
54
  type FocusScopeOptions = {
53
55
  parent?: FocusScopeHandle;
56
+ focusKey?: string;
54
57
  keybindings?: Keybindings | ((helpers: FocusScopeHelpers) => Keybindings);
55
58
  };
56
59
  declare function useFocusScope(options?: FocusScopeOptions): FocusScopeHandle;
@@ -67,6 +70,7 @@ type FocusNodeHandle = {
67
70
  };
68
71
  type FocusNodeOptions = {
69
72
  parent?: FocusScopeHandle;
73
+ focusKey?: string;
70
74
  };
71
75
  declare function useFocusNode(options?: FocusNodeOptions): FocusNodeHandle;
72
76
 
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  useKeybindingRegistry,
15
15
  useKeybindings,
16
16
  useStore
17
- } from "./chunk-TWXBZE5C.js";
17
+ } from "./chunk-74PBSWEK.js";
18
18
  import {
19
19
  ThemeProvider,
20
20
  useTheme
@@ -31,8 +31,9 @@ type TextInputProps = {
31
31
  onSubmit?: (value: string) => void;
32
32
  placeholder?: string;
33
33
  render?: (props: TextInputRenderProps) => React$1.ReactNode;
34
+ focusKey?: string;
34
35
  };
35
- declare function TextInput({ label, value, onChange, onSubmit, placeholder, render }: TextInputProps): react_jsx_runtime.JSX.Element;
36
+ declare function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }: TextInputProps): react_jsx_runtime.JSX.Element;
36
37
 
37
38
  type PaginatorStyle = 'dots' | 'arrows' | 'scrollbar' | 'counter' | 'none';
38
39
  type PaginatorProps = {
@@ -70,8 +71,9 @@ type SelectProps<T> = {
70
71
  paginatorStyle?: PaginatorStyle;
71
72
  wrap?: boolean;
72
73
  render?: (props: SelectRenderProps<T>) => React$1.ReactNode;
74
+ focusKey?: string;
73
75
  };
74
- declare function Select<T>({ options, value, onChange, onSubmit, onHighlight, label, immediate, direction, gap, maxVisible, paginatorStyle, wrap, render }: SelectProps<T>): react_jsx_runtime.JSX.Element;
76
+ declare function Select<T>({ options, value, onChange, onSubmit, onHighlight, label, immediate, direction, gap, maxVisible, paginatorStyle, wrap, render, focusKey }: SelectProps<T>): react_jsx_runtime.JSX.Element;
75
77
 
76
78
  type MultiSelectRenderProps<T> = {
77
79
  option: SelectOption<T>;
@@ -92,15 +94,17 @@ type MultiSelectProps<T> = {
92
94
  paginatorStyle?: PaginatorStyle;
93
95
  wrap?: boolean;
94
96
  render?: (props: MultiSelectRenderProps<T>) => React$1.ReactNode;
97
+ focusKey?: string;
95
98
  };
96
- declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, gap, maxVisible, paginatorStyle, wrap, render }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
99
+ declare function MultiSelect<T>({ options, value, onChange, onSubmit, onHighlight, label, direction, gap, maxVisible, paginatorStyle, wrap, render, focusKey }: MultiSelectProps<T>): react_jsx_runtime.JSX.Element;
97
100
 
98
101
  type ConfirmProps = {
99
102
  message: string;
100
103
  defaultValue?: boolean;
101
104
  onSubmit: (value: boolean) => void;
105
+ focusKey?: string;
102
106
  };
103
- declare function Confirm({ message, defaultValue, onSubmit }: ConfirmProps): react_jsx_runtime.JSX.Element;
107
+ declare function Confirm({ message, defaultValue, onSubmit, focusKey }: ConfirmProps): react_jsx_runtime.JSX.Element;
104
108
 
105
109
  type AutocompleteRenderProps<T> = {
106
110
  option: SelectOption<T>;
@@ -122,8 +126,9 @@ type AutocompleteProps<T> = {
122
126
  paginatorStyle?: PaginatorStyle;
123
127
  wrap?: boolean;
124
128
  render?: (props: AutocompleteRenderProps<T>) => React$1.ReactNode;
129
+ focusKey?: string;
125
130
  };
126
- declare function Autocomplete<T>({ options, value, onChange, onSubmit, onHighlight, label, placeholder, filter, gap, maxVisible, paginatorStyle, wrap, render }: AutocompleteProps<T>): react_jsx_runtime.JSX.Element;
131
+ declare function Autocomplete<T>({ options, value, onChange, onSubmit, onHighlight, label, placeholder, filter, gap, maxVisible, paginatorStyle, wrap, render, focusKey }: AutocompleteProps<T>): react_jsx_runtime.JSX.Element;
127
132
 
128
133
  type VirtualListRenderProps<T> = {
129
134
  item: T;
@@ -160,6 +165,7 @@ declare const Viewport: React$1.ForwardRefExoticComponent<Omit<BoxProps, "flexDi
160
165
  height: number;
161
166
  keybindings?: boolean;
162
167
  footer?: React$1.ReactNode;
168
+ focusKey?: string;
163
169
  } & React$1.RefAttributes<ViewportRef>>;
164
170
 
165
171
  type ModalProps = Omit<BoxProps, 'children'> & {
package/dist/ui/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  useFocusNode,
5
5
  useKeybindingRegistry,
6
6
  useKeybindings
7
- } from "../chunk-TWXBZE5C.js";
7
+ } from "../chunk-74PBSWEK.js";
8
8
  import {
9
9
  CodeBlock
10
10
  } from "../chunk-SKSDNDQF.js";
@@ -136,8 +136,8 @@ function CommandPalette({ onClose, interactive = true, render }) {
136
136
  import { useReducer, useRef } from "react";
137
137
  import { Text as Text2 } from "ink";
138
138
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
139
- function TextInput({ label, value, onChange, onSubmit, placeholder, render }) {
140
- const focus = useFocusNode();
139
+ function TextInput({ label, value, onChange, onSubmit, placeholder, render, focusKey }) {
140
+ const focus = useFocusNode({ focusKey });
141
141
  const cursorRef = useRef(value.length);
142
142
  const [, forceRender] = useReducer((c) => c + 1, 0);
143
143
  const cursor = Math.min(cursorRef.current, value.length);
@@ -351,7 +351,8 @@ function Select({
351
351
  maxVisible,
352
352
  paginatorStyle,
353
353
  wrap = true,
354
- render
354
+ render,
355
+ focusKey
355
356
  }) {
356
357
  const seen = /* @__PURE__ */ new Set();
357
358
  for (const opt of options) {
@@ -361,7 +362,7 @@ function Select({
361
362
  }
362
363
  seen.add(key);
363
364
  }
364
- const focus = useFocusNode();
365
+ const focus = useFocusNode({ focusKey });
365
366
  const theme = useTheme();
366
367
  const [highlightIndex, setHighlightIndex] = useState3(0);
367
368
  const safeIndex = options.length === 0 ? -1 : Math.min(highlightIndex, options.length - 1);
@@ -438,7 +439,8 @@ function MultiSelect({
438
439
  maxVisible,
439
440
  paginatorStyle,
440
441
  wrap = true,
441
- render
442
+ render,
443
+ focusKey
442
444
  }) {
443
445
  const seen = /* @__PURE__ */ new Set();
444
446
  for (const opt of options) {
@@ -448,7 +450,7 @@ function MultiSelect({
448
450
  }
449
451
  seen.add(key);
450
452
  }
451
- const focus = useFocusNode();
453
+ const focus = useFocusNode({ focusKey });
452
454
  const theme = useTheme();
453
455
  const [highlightIndex, setHighlightIndex] = useState4(0);
454
456
  const [internalSelected, setInternalSelected] = useState4([]);
@@ -515,8 +517,8 @@ function MultiSelect({
515
517
  // src/ui/Confirm.tsx
516
518
  import { Text as Text6 } from "ink";
517
519
  import { jsxs as jsxs7 } from "react/jsx-runtime";
518
- function Confirm({ message, defaultValue = true, onSubmit }) {
519
- const focus = useFocusNode();
520
+ function Confirm({ message, defaultValue = true, onSubmit, focusKey }) {
521
+ const focus = useFocusNode({ focusKey });
520
522
  useKeybindings(focus, {
521
523
  y: () => onSubmit(true),
522
524
  n: () => onSubmit(false),
@@ -558,7 +560,8 @@ function Autocomplete({
558
560
  maxVisible,
559
561
  paginatorStyle,
560
562
  wrap = true,
561
- render
563
+ render,
564
+ focusKey
562
565
  }) {
563
566
  const seen = /* @__PURE__ */ new Set();
564
567
  for (const opt of options) {
@@ -568,7 +571,7 @@ function Autocomplete({
568
571
  }
569
572
  seen.add(key);
570
573
  }
571
- const focus = useFocusNode();
574
+ const focus = useFocusNode({ focusKey });
572
575
  const theme = useTheme();
573
576
  const [query, setQuery] = useState5("");
574
577
  const [highlightIndex, setHighlightIndex] = useState5(0);
@@ -718,8 +721,8 @@ function MeasurableItem({
718
721
  }, [index, onMeasure, children]);
719
722
  return /* @__PURE__ */ jsx8(Box7, { ref, flexShrink: 0, width: "100%", flexDirection: "column", children });
720
723
  }
721
- var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer, ...boxProps }, ref) {
722
- const focus = useFocusNode();
724
+ var Viewport = forwardRef(function Viewport2({ children, height, keybindings: enableKeybindings = true, footer, focusKey, ...boxProps }, ref) {
725
+ const focus = useFocusNode({ focusKey });
723
726
  const [scrollOffset, setScrollOffset] = useState6(0);
724
727
  const contentHeightRef = useRef4(0);
725
728
  const itemHeightsRef = useRef4({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "giggles",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "play": "f() { tsx --watch playground/examples/$1.tsx; }; f",
44
44
  "record": "f() { vhs playground/tapes/$1.tape; }; f",
45
45
  "dev:docs": "pnpm build && concurrently --kill-others \"pnpm build:watch\" \"pnpm --filter documentation dev\"",
46
- "lint": "prettier --write . && eslint . --fix"
46
+ "lint": "prettier --write --loglevel warn . && eslint . --fix --quiet"
47
47
  },
48
48
  "files": [
49
49
  "dist"