@nick-skriabin/glyph 0.1.32 → 0.1.34

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/dist/index.cjs CHANGED
@@ -2065,6 +2065,17 @@ function render(element, opts = {}) {
2065
2065
  return () => {
2066
2066
  focusChangeHandlers.delete(handler);
2067
2067
  };
2068
+ },
2069
+ getRegisteredElements() {
2070
+ const result = [];
2071
+ for (const id of focusOrder) {
2072
+ if (skippableIds.has(id)) continue;
2073
+ const node = focusRegistry.get(id);
2074
+ if (node) {
2075
+ result.push({ id, node });
2076
+ }
2077
+ }
2078
+ return result;
2068
2079
  }
2069
2080
  };
2070
2081
  const layoutSubscriptions = /* @__PURE__ */ new Map();
@@ -2288,13 +2299,17 @@ function render(element, opts = {}) {
2288
2299
  };
2289
2300
  return handle;
2290
2301
  }
2291
- function Box({ children, style, focusable }) {
2292
- return React15__default.default.createElement("box", { style, focusable }, children);
2293
- }
2294
- function Text({ children, style, wrap }) {
2295
- const mergedStyle = wrap ? { ...style, wrap } : style;
2296
- return React15__default.default.createElement("text", { style: mergedStyle }, children);
2297
- }
2302
+ var Box = React15.forwardRef(
2303
+ function Box2({ children, style, focusable }, ref) {
2304
+ return React15__default.default.createElement("box", { style, focusable, ref }, children);
2305
+ }
2306
+ );
2307
+ var Text = React15.forwardRef(
2308
+ function Text2({ children, style, wrap }, ref) {
2309
+ const mergedStyle = wrap ? { ...style, wrap } : style;
2310
+ return React15__default.default.createElement("text", { style: mergedStyle, ref }, children);
2311
+ }
2312
+ );
2298
2313
  function cursorToVisualLine(text, pos, width) {
2299
2314
  if (width <= 0) {
2300
2315
  return { visualLine: 0, visualCol: pos, totalVisualLines: 1, lineStartOffset: 0, lineLength: text.length };
@@ -2401,6 +2416,7 @@ function Input(props) {
2401
2416
  const [cursorPos, setCursorPos] = React15.useState(defaultValue.length);
2402
2417
  const [innerWidth, setInnerWidth] = React15.useState(0);
2403
2418
  const [isFocused, setIsFocused] = React15.useState(false);
2419
+ const [nodeReady, setNodeReady] = React15.useState(false);
2404
2420
  const inputCtx = React15.useContext(InputContext);
2405
2421
  const focusCtx = React15.useContext(FocusContext);
2406
2422
  const layoutCtx = React15.useContext(LayoutContext);
@@ -2449,7 +2465,7 @@ function Input(props) {
2449
2465
  React15.useEffect(() => {
2450
2466
  if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
2451
2467
  return focusCtx.register(focusIdRef.current, nodeRef.current);
2452
- }, [focusCtx]);
2468
+ }, [focusCtx, nodeReady]);
2453
2469
  const autoFocusedRef = React15.useRef(false);
2454
2470
  React15.useEffect(() => {
2455
2471
  if (autoFocus && !autoFocusedRef.current && focusCtx && focusIdRef.current) {
@@ -2459,7 +2475,7 @@ function Input(props) {
2459
2475
  focusCtx.requestFocus(fid);
2460
2476
  });
2461
2477
  }
2462
- }, [autoFocus, focusCtx]);
2478
+ }, [autoFocus, focusCtx, nodeReady]);
2463
2479
  React15.useEffect(() => {
2464
2480
  if (!focusCtx || !focusIdRef.current) return;
2465
2481
  const fid = focusIdRef.current;
@@ -2467,7 +2483,7 @@ function Input(props) {
2467
2483
  return focusCtx.onFocusChange((newId) => {
2468
2484
  setIsFocused(newId === fid);
2469
2485
  });
2470
- }, [focusCtx]);
2486
+ }, [focusCtx, nodeReady]);
2471
2487
  React15.useEffect(() => {
2472
2488
  if (!inputCtx || !focusIdRef.current) return;
2473
2489
  const fid = focusIdRef.current;
@@ -2677,7 +2693,7 @@ function Input(props) {
2677
2693
  return false;
2678
2694
  };
2679
2695
  return inputCtx.registerInputHandler(fid, handler);
2680
- }, [inputCtx]);
2696
+ }, [inputCtx, nodeReady]);
2681
2697
  const mergedStyle = {
2682
2698
  ...style,
2683
2699
  ...isFocused && focusedStyle ? focusedStyle : {}
@@ -2695,6 +2711,11 @@ function Input(props) {
2695
2711
  if (node) {
2696
2712
  nodeRef.current = node;
2697
2713
  focusIdRef.current = node.focusId;
2714
+ setNodeReady(true);
2715
+ } else {
2716
+ nodeRef.current = null;
2717
+ focusIdRef.current = null;
2718
+ setNodeReady(false);
2698
2719
  }
2699
2720
  }
2700
2721
  });
@@ -2810,11 +2831,12 @@ function Button({
2810
2831
  const focusIdRef = React15.useRef(null);
2811
2832
  const onPressRef = React15.useRef(onPress);
2812
2833
  onPressRef.current = onPress;
2834
+ const [nodeReady, setNodeReady] = React15.useState(false);
2813
2835
  const [isFocused, setIsFocused] = React15.useState(false);
2814
2836
  React15.useEffect(() => {
2815
2837
  if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
2816
2838
  return focusCtx.register(focusIdRef.current, nodeRef.current);
2817
- }, [focusCtx, disabled]);
2839
+ }, [focusCtx, disabled, nodeReady]);
2818
2840
  React15.useEffect(() => {
2819
2841
  if (!focusCtx || !focusIdRef.current) return;
2820
2842
  const fid = focusIdRef.current;
@@ -2822,7 +2844,7 @@ function Button({
2822
2844
  return focusCtx.onFocusChange((newId) => {
2823
2845
  setIsFocused(newId === fid);
2824
2846
  });
2825
- }, [focusCtx]);
2847
+ }, [focusCtx, nodeReady]);
2826
2848
  React15.useEffect(() => {
2827
2849
  if (!inputCtx || !focusIdRef.current || disabled) return;
2828
2850
  const fid = focusIdRef.current;
@@ -2835,7 +2857,7 @@ function Button({
2835
2857
  return false;
2836
2858
  };
2837
2859
  return inputCtx.registerInputHandler(fid, handler);
2838
- }, [inputCtx, focusCtx, disabled]);
2860
+ }, [inputCtx, focusCtx, disabled, nodeReady]);
2839
2861
  const mergedStyle = {
2840
2862
  ...style,
2841
2863
  ...isFocused && focusedStyle ? focusedStyle : {}
@@ -2849,6 +2871,11 @@ function Button({
2849
2871
  if (node) {
2850
2872
  nodeRef.current = node;
2851
2873
  focusIdRef.current = node.focusId;
2874
+ setNodeReady(true);
2875
+ } else {
2876
+ nodeRef.current = null;
2877
+ focusIdRef.current = null;
2878
+ setNodeReady(false);
2852
2879
  }
2853
2880
  }
2854
2881
  },
@@ -3124,6 +3151,7 @@ function List({
3124
3151
  const focusIdRef = React15.useRef(null);
3125
3152
  const onSelectRef = React15.useRef(onSelect);
3126
3153
  onSelectRef.current = onSelect;
3154
+ const [nodeReady, setNodeReady] = React15.useState(false);
3127
3155
  const [isFocused, setIsFocused] = React15.useState(false);
3128
3156
  const lastKeyRef = React15.useRef(null);
3129
3157
  const setIndex = React15.useCallback(
@@ -3154,7 +3182,7 @@ function List({
3154
3182
  React15.useEffect(() => {
3155
3183
  if (!focusCtx || !focusIdRef.current || !nodeRef.current || !focusable) return;
3156
3184
  return focusCtx.register(focusIdRef.current, nodeRef.current);
3157
- }, [focusCtx, focusable]);
3185
+ }, [focusCtx, focusable, nodeReady]);
3158
3186
  React15.useEffect(() => {
3159
3187
  if (!focusCtx || !focusIdRef.current) return;
3160
3188
  const fid = focusIdRef.current;
@@ -3162,7 +3190,7 @@ function List({
3162
3190
  return focusCtx.onFocusChange((newId) => {
3163
3191
  setIsFocused(newId === fid);
3164
3192
  });
3165
- }, [focusCtx]);
3193
+ }, [focusCtx, nodeReady]);
3166
3194
  const findFirstEnabled = React15.useCallback(
3167
3195
  (fromEnd) => {
3168
3196
  const start = fromEnd ? count - 1 : 0;
@@ -3212,7 +3240,7 @@ function List({
3212
3240
  return false;
3213
3241
  };
3214
3242
  return inputCtx.registerInputHandler(fid, handler);
3215
- }, [inputCtx, focusCtx, focusable, selectedIndex, setIndex, findNextEnabled, findFirstEnabled, disabledIndices]);
3243
+ }, [inputCtx, focusCtx, focusable, selectedIndex, setIndex, findNextEnabled, findFirstEnabled, disabledIndices, nodeReady]);
3216
3244
  const items = [];
3217
3245
  for (let i = 0; i < count; i++) {
3218
3246
  items.push(
@@ -3232,6 +3260,11 @@ function List({
3232
3260
  if (node) {
3233
3261
  nodeRef.current = node;
3234
3262
  focusIdRef.current = node.focusId;
3263
+ setNodeReady(true);
3264
+ } else {
3265
+ nodeRef.current = null;
3266
+ focusIdRef.current = null;
3267
+ setNodeReady(false);
3235
3268
  }
3236
3269
  }
3237
3270
  },
@@ -3513,6 +3546,7 @@ function Select({
3513
3546
  const focusIdRef = React15.useRef(null);
3514
3547
  const onChangeRef = React15.useRef(onChange);
3515
3548
  onChangeRef.current = onChange;
3549
+ const [nodeReady, setNodeReady] = React15.useState(false);
3516
3550
  const [isFocused, setIsFocused] = React15.useState(false);
3517
3551
  const [isOpen, setIsOpen] = React15.useState(false);
3518
3552
  const [highlightIndex, setHighlightIndex] = React15.useState(0);
@@ -3545,11 +3579,11 @@ function Select({
3545
3579
  React15.useEffect(() => {
3546
3580
  if (!focusCtx || !focusIdRef.current || !nodeRef.current) return;
3547
3581
  return focusCtx.register(focusIdRef.current, nodeRef.current);
3548
- }, [focusCtx]);
3582
+ }, [focusCtx, nodeReady]);
3549
3583
  React15.useEffect(() => {
3550
3584
  if (!focusCtx || !focusIdRef.current) return;
3551
3585
  focusCtx.setSkippable(focusIdRef.current, !!disabled);
3552
- }, [focusCtx, disabled]);
3586
+ }, [focusCtx, disabled, nodeReady]);
3553
3587
  React15.useEffect(() => {
3554
3588
  if (!focusCtx || !focusIdRef.current) return;
3555
3589
  const fid = focusIdRef.current;
@@ -3557,7 +3591,7 @@ function Select({
3557
3591
  return focusCtx.onFocusChange((newId) => {
3558
3592
  setIsFocused(newId === fid);
3559
3593
  });
3560
- }, [focusCtx]);
3594
+ }, [focusCtx, nodeReady]);
3561
3595
  const findNextEnabled = React15.useCallback(
3562
3596
  (from, direction) => {
3563
3597
  let next = from + direction;
@@ -3669,7 +3703,8 @@ function Select({
3669
3703
  searchable,
3670
3704
  searchText,
3671
3705
  findNextEnabled,
3672
- ensureVisible
3706
+ ensureVisible,
3707
+ nodeReady
3673
3708
  ]);
3674
3709
  const useDefaultBorder = !style?.bg && style?.border === void 0;
3675
3710
  const triggerStyle = {
@@ -3846,6 +3881,11 @@ function Select({
3846
3881
  if (node) {
3847
3882
  nodeRef.current = node;
3848
3883
  focusIdRef.current = node.focusId;
3884
+ setNodeReady(true);
3885
+ } else {
3886
+ nodeRef.current = null;
3887
+ focusIdRef.current = null;
3888
+ setNodeReady(false);
3849
3889
  }
3850
3890
  }
3851
3891
  },
@@ -3873,11 +3913,12 @@ function Checkbox({
3873
3913
  onChangeRef.current = onChange;
3874
3914
  const checkedRef = React15.useRef(checked);
3875
3915
  checkedRef.current = checked;
3916
+ const [nodeReady, setNodeReady] = React15.useState(false);
3876
3917
  const [isFocused, setIsFocused] = React15.useState(false);
3877
3918
  React15.useEffect(() => {
3878
3919
  if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
3879
3920
  return focusCtx.register(focusIdRef.current, nodeRef.current);
3880
- }, [focusCtx, disabled]);
3921
+ }, [focusCtx, disabled, nodeReady]);
3881
3922
  React15.useEffect(() => {
3882
3923
  if (!focusCtx || !focusIdRef.current) return;
3883
3924
  const fid = focusIdRef.current;
@@ -3885,7 +3926,7 @@ function Checkbox({
3885
3926
  return focusCtx.onFocusChange((newId) => {
3886
3927
  setIsFocused(newId === fid);
3887
3928
  });
3888
- }, [focusCtx]);
3929
+ }, [focusCtx, nodeReady]);
3889
3930
  React15.useEffect(() => {
3890
3931
  if (!inputCtx || !focusIdRef.current || disabled) return;
3891
3932
  const fid = focusIdRef.current;
@@ -3898,7 +3939,7 @@ function Checkbox({
3898
3939
  return false;
3899
3940
  };
3900
3941
  return inputCtx.registerInputHandler(fid, handler);
3901
- }, [inputCtx, focusCtx, disabled]);
3942
+ }, [inputCtx, focusCtx, disabled, nodeReady]);
3902
3943
  const mergedStyle = {
3903
3944
  flexDirection: "row",
3904
3945
  gap: 1,
@@ -3921,6 +3962,11 @@ function Checkbox({
3921
3962
  if (node) {
3922
3963
  nodeRef.current = node;
3923
3964
  focusIdRef.current = node.focusId;
3965
+ setNodeReady(true);
3966
+ } else {
3967
+ nodeRef.current = null;
3968
+ focusIdRef.current = null;
3969
+ setNodeReady(false);
3924
3970
  }
3925
3971
  }
3926
3972
  },
@@ -3956,6 +4002,7 @@ function Radio({
3956
4002
  const focusIdRef = React15.useRef(null);
3957
4003
  const onChangeRef = React15.useRef(onChange);
3958
4004
  onChangeRef.current = onChange;
4005
+ const [nodeReady, setNodeReady] = React15.useState(false);
3959
4006
  const [isFocused, setIsFocused] = React15.useState(false);
3960
4007
  const [highlightedIndex, setHighlightedIndex] = React15.useState(() => {
3961
4008
  const selectedIdx = items.findIndex((item) => item.value === value);
@@ -3976,7 +4023,7 @@ function Radio({
3976
4023
  React15.useEffect(() => {
3977
4024
  if (!focusCtx || !focusIdRef.current || !nodeRef.current || disabled) return;
3978
4025
  return focusCtx.register(focusIdRef.current, nodeRef.current);
3979
- }, [focusCtx, disabled]);
4026
+ }, [focusCtx, disabled, nodeReady]);
3980
4027
  React15.useEffect(() => {
3981
4028
  if (!focusCtx || !focusIdRef.current) return;
3982
4029
  const fid = focusIdRef.current;
@@ -3984,7 +4031,7 @@ function Radio({
3984
4031
  return focusCtx.onFocusChange((newId) => {
3985
4032
  setIsFocused(newId === fid);
3986
4033
  });
3987
- }, [focusCtx]);
4034
+ }, [focusCtx, nodeReady]);
3988
4035
  React15.useEffect(() => {
3989
4036
  if (!inputCtx || !focusIdRef.current || disabled) return;
3990
4037
  const fid = focusIdRef.current;
@@ -4008,7 +4055,7 @@ function Radio({
4008
4055
  return false;
4009
4056
  };
4010
4057
  return inputCtx.registerInputHandler(fid, handler);
4011
- }, [inputCtx, focusCtx, disabled, items, highlightedIndex, findNextEnabled]);
4058
+ }, [inputCtx, focusCtx, disabled, items, highlightedIndex, findNextEnabled, nodeReady]);
4012
4059
  React15.useEffect(() => {
4013
4060
  const selectedIdx = items.findIndex((item) => item.value === value);
4014
4061
  if (selectedIdx >= 0) {
@@ -4061,6 +4108,11 @@ function Radio({
4061
4108
  if (node) {
4062
4109
  nodeRef.current = node;
4063
4110
  focusIdRef.current = node.focusId;
4111
+ setNodeReady(true);
4112
+ } else {
4113
+ nodeRef.current = null;
4114
+ focusIdRef.current = null;
4115
+ setNodeReady(false);
4064
4116
  }
4065
4117
  }
4066
4118
  },
@@ -4295,6 +4347,221 @@ function DialogOverlay({ dialog, onDismiss }) {
4295
4347
  )
4296
4348
  );
4297
4349
  }
4350
+ var JumpNavContext = React15.createContext(null);
4351
+ function generateHints(count, chars) {
4352
+ const hints = [];
4353
+ const charList = chars.split("");
4354
+ if (count <= charList.length) {
4355
+ for (let i = 0; i < count; i++) {
4356
+ hints.push(charList[i]);
4357
+ }
4358
+ } else {
4359
+ for (let i = 0; i < charList.length && hints.length < count; i++) {
4360
+ for (let j = 0; j < charList.length && hints.length < count; j++) {
4361
+ hints.push(charList[i] + charList[j]);
4362
+ }
4363
+ }
4364
+ }
4365
+ return hints;
4366
+ }
4367
+ function JumpNav({
4368
+ children,
4369
+ activationKey = "ctrl+o",
4370
+ hintStyle,
4371
+ hintBg = "yellow",
4372
+ hintFg = "black",
4373
+ hintChars = "asdfghjklqwertyuiopzxcvbnm",
4374
+ enabled = true
4375
+ }) {
4376
+ const [isActive, setIsActive] = React15.useState(false);
4377
+ const [inputBuffer, setInputBuffer] = React15.useState("");
4378
+ const [hasChildJumpNav, setHasChildJumpNav] = React15.useState(false);
4379
+ const [elements, setElements] = React15.useState([]);
4380
+ const inputCtx = React15.useContext(InputContext);
4381
+ const focusCtx = React15.useContext(FocusContext);
4382
+ const layoutCtx = React15.useContext(LayoutContext);
4383
+ const parentJumpNav = React15.useContext(JumpNavContext);
4384
+ const wrapperRef = React15.useRef(null);
4385
+ React15.useEffect(() => {
4386
+ if (parentJumpNav) {
4387
+ return parentJumpNav.registerChildJumpNav();
4388
+ }
4389
+ }, [parentJumpNav]);
4390
+ const contextValue = React15.useMemo(() => ({
4391
+ isChildActive: hasChildJumpNav,
4392
+ registerChildJumpNav: () => {
4393
+ setHasChildJumpNav(true);
4394
+ return () => setHasChildJumpNav(false);
4395
+ }
4396
+ }), [hasChildJumpNav]);
4397
+ const parseKey = React15.useCallback((keyStr) => {
4398
+ const parts = keyStr.toLowerCase().split("+");
4399
+ return {
4400
+ ctrl: parts.includes("ctrl"),
4401
+ alt: parts.includes("alt"),
4402
+ shift: parts.includes("shift"),
4403
+ meta: parts.includes("meta"),
4404
+ name: parts[parts.length - 1] ?? ""
4405
+ };
4406
+ }, []);
4407
+ const activationKeyParsed = parseKey(activationKey);
4408
+ const findFocusableDescendants = React15.useCallback((node) => {
4409
+ const result = [];
4410
+ function walk(n) {
4411
+ if (n.focusId) {
4412
+ result.push({
4413
+ id: n.focusId,
4414
+ node: n,
4415
+ layout: layoutCtx?.getLayout(n) ?? n.layout
4416
+ });
4417
+ }
4418
+ for (const child of n.children) {
4419
+ walk(child);
4420
+ }
4421
+ }
4422
+ walk(node);
4423
+ return result;
4424
+ }, [layoutCtx]);
4425
+ const refreshElements = React15.useCallback(() => {
4426
+ if (!wrapperRef.current) return;
4427
+ const descendants = findFocusableDescendants(wrapperRef.current);
4428
+ descendants.sort((a, b) => {
4429
+ if (a.layout.y !== b.layout.y) {
4430
+ return a.layout.y - b.layout.y;
4431
+ }
4432
+ return a.layout.x - b.layout.x;
4433
+ });
4434
+ setElements(descendants);
4435
+ }, [findFocusableDescendants]);
4436
+ const wasActiveRef = React15.useRef(false);
4437
+ React15.useEffect(() => {
4438
+ if (isActive && !wasActiveRef.current) {
4439
+ refreshElements();
4440
+ }
4441
+ wasActiveRef.current = isActive;
4442
+ }, [isActive, refreshElements]);
4443
+ const visibleElements = elements.filter(
4444
+ (el) => el.layout.width > 0 && el.layout.height > 0
4445
+ );
4446
+ const visibleHints = generateHints(visibleElements.length, hintChars);
4447
+ const visibleHintMap = React15.useMemo(() => {
4448
+ const map = /* @__PURE__ */ new Map();
4449
+ visibleElements.forEach((el, i) => {
4450
+ if (visibleHints[i]) {
4451
+ map.set(visibleHints[i], el.id);
4452
+ }
4453
+ });
4454
+ return map;
4455
+ }, [visibleElements, visibleHints]);
4456
+ React15.useEffect(() => {
4457
+ if (!inputCtx || !enabled) return;
4458
+ const handler = (key) => {
4459
+ if (!isActive && !hasChildJumpNav && key.name === activationKeyParsed.name && !!key.ctrl === activationKeyParsed.ctrl && !!key.alt === activationKeyParsed.alt && !!key.shift === activationKeyParsed.shift && !!key.meta === activationKeyParsed.meta) {
4460
+ setIsActive(true);
4461
+ setInputBuffer("");
4462
+ return true;
4463
+ }
4464
+ if (isActive) {
4465
+ if (key.name === "escape") {
4466
+ setIsActive(false);
4467
+ setInputBuffer("");
4468
+ return true;
4469
+ }
4470
+ if (key.name === "backspace") {
4471
+ setInputBuffer("");
4472
+ return true;
4473
+ }
4474
+ if (key.sequence && key.sequence.length === 1 && /[a-z]/i.test(key.sequence)) {
4475
+ const newBuffer = inputBuffer + key.sequence.toLowerCase();
4476
+ const targetId = visibleHintMap.get(newBuffer);
4477
+ if (targetId) {
4478
+ focusCtx?.requestFocus(targetId);
4479
+ setIsActive(false);
4480
+ setInputBuffer("");
4481
+ return true;
4482
+ }
4483
+ const hasPartialMatch = [...visibleHintMap.keys()].some((h) => h.startsWith(newBuffer));
4484
+ if (hasPartialMatch) {
4485
+ setInputBuffer(newBuffer);
4486
+ return true;
4487
+ }
4488
+ setInputBuffer("");
4489
+ return true;
4490
+ }
4491
+ return true;
4492
+ }
4493
+ return false;
4494
+ };
4495
+ return inputCtx.subscribePriority(handler);
4496
+ }, [inputCtx, enabled, isActive, activationKeyParsed, inputBuffer, visibleHintMap, focusCtx, hasChildJumpNav]);
4497
+ const hintsOverlay = isActive ? React15__default.default.createElement(
4498
+ React15__default.default.Fragment,
4499
+ null,
4500
+ ...visibleElements.map((el, i) => {
4501
+ const hint = visibleHints[i];
4502
+ if (!hint) return null;
4503
+ const { x, y } = el.layout;
4504
+ const isPartialMatch = hint.startsWith(inputBuffer) && inputBuffer.length > 0;
4505
+ return React15__default.default.createElement(
4506
+ "box",
4507
+ {
4508
+ key: el.id,
4509
+ style: {
4510
+ position: "absolute",
4511
+ top: y,
4512
+ left: Math.max(0, x - hint.length - 2),
4513
+ bg: isPartialMatch ? "cyan" : hintBg,
4514
+ color: hintFg,
4515
+ paddingX: 1,
4516
+ zIndex: 99999,
4517
+ ...hintStyle
4518
+ }
4519
+ },
4520
+ React15__default.default.createElement("text", {
4521
+ style: { bold: true, color: hintFg }
4522
+ }, hint)
4523
+ );
4524
+ }),
4525
+ // Status bar at bottom
4526
+ React15__default.default.createElement(
4527
+ "box",
4528
+ {
4529
+ style: {
4530
+ position: "absolute",
4531
+ bottom: 0,
4532
+ left: 0,
4533
+ right: 0,
4534
+ bg: "blackBright",
4535
+ paddingX: 1,
4536
+ zIndex: 99999
4537
+ }
4538
+ },
4539
+ React15__default.default.createElement("text", {
4540
+ style: { color: "white" }
4541
+ }, inputBuffer ? `Jump: ${inputBuffer}_` : "Press a key to jump \u2022 ESC to cancel")
4542
+ )
4543
+ ) : null;
4544
+ const wrappedChildren = React15__default.default.createElement(
4545
+ "box",
4546
+ {
4547
+ ref: (node) => {
4548
+ wrapperRef.current = node;
4549
+ },
4550
+ style: {
4551
+ // Invisible wrapper - takes full size of parent
4552
+ flexGrow: 1,
4553
+ flexDirection: "column"
4554
+ }
4555
+ },
4556
+ children
4557
+ );
4558
+ return React15__default.default.createElement(
4559
+ JumpNavContext.Provider,
4560
+ { value: contextValue },
4561
+ wrappedChildren,
4562
+ hintsOverlay
4563
+ );
4564
+ }
4298
4565
  function useFocus(nodeRef) {
4299
4566
  const focusCtx = React15.useContext(FocusContext);
4300
4567
  const [id] = React15.useState(() => `focus-${Math.random().toString(36).slice(2, 9)}`);
@@ -4393,6 +4660,52 @@ function useApp() {
4393
4660
  }
4394
4661
  };
4395
4662
  }
4663
+ function useFocusRegistry() {
4664
+ const focusCtx = React15.useContext(FocusContext);
4665
+ const layoutCtx = React15.useContext(LayoutContext);
4666
+ const [elements, setElements] = React15.useState([]);
4667
+ const updateRef = React15.useRef(() => {
4668
+ });
4669
+ const updateElements = React15.useCallback(() => {
4670
+ if (!focusCtx) return;
4671
+ const registered = focusCtx.getRegisteredElements?.() ?? [];
4672
+ const mapped = registered.map(({ id, node }) => ({
4673
+ id,
4674
+ node,
4675
+ layout: layoutCtx?.getLayout(node) ?? node.layout,
4676
+ type: node.type
4677
+ }));
4678
+ mapped.sort((a, b) => {
4679
+ if (a.layout.y !== b.layout.y) {
4680
+ return a.layout.y - b.layout.y;
4681
+ }
4682
+ return a.layout.x - b.layout.x;
4683
+ });
4684
+ setElements(mapped);
4685
+ }, [focusCtx, layoutCtx]);
4686
+ updateRef.current = updateElements;
4687
+ React15.useEffect(() => {
4688
+ if (!focusCtx) return;
4689
+ updateElements();
4690
+ const unsubscribe = focusCtx.onFocusChange(() => {
4691
+ updateElements();
4692
+ });
4693
+ const timer = setTimeout(updateElements, 50);
4694
+ return () => {
4695
+ unsubscribe();
4696
+ clearTimeout(timer);
4697
+ };
4698
+ }, [focusCtx, layoutCtx, updateElements]);
4699
+ if (!focusCtx) return null;
4700
+ return {
4701
+ elements,
4702
+ focusedId: focusCtx.focusedId,
4703
+ requestFocus: focusCtx.requestFocus,
4704
+ focusNext: focusCtx.focusNext,
4705
+ focusPrev: focusCtx.focusPrev,
4706
+ refresh: () => updateRef.current()
4707
+ };
4708
+ }
4396
4709
 
4397
4710
  // src/utils/mask.ts
4398
4711
  function parseMask(mask) {
@@ -4500,6 +4813,7 @@ exports.Checkbox = Checkbox;
4500
4813
  exports.DialogHost = DialogHost;
4501
4814
  exports.FocusScope = FocusScope;
4502
4815
  exports.Input = Input;
4816
+ exports.JumpNav = JumpNav;
4503
4817
  exports.Keybind = Keybind;
4504
4818
  exports.List = List;
4505
4819
  exports.Menu = Menu;
@@ -4518,6 +4832,7 @@ exports.render = render;
4518
4832
  exports.useApp = useApp;
4519
4833
  exports.useDialog = useDialog;
4520
4834
  exports.useFocus = useFocus;
4835
+ exports.useFocusRegistry = useFocusRegistry;
4521
4836
  exports.useFocusable = useFocusable;
4522
4837
  exports.useInput = useInput;
4523
4838
  exports.useLayout = useLayout;