@nick-skriabin/glyph 0.1.33 → 0.1.35

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
@@ -4347,54 +4347,7 @@ function DialogOverlay({ dialog, onDismiss }) {
4347
4347
  )
4348
4348
  );
4349
4349
  }
4350
- function useFocusRegistry() {
4351
- const focusCtx = React15.useContext(FocusContext);
4352
- const layoutCtx = React15.useContext(LayoutContext);
4353
- const [elements, setElements] = React15.useState([]);
4354
- const updateRef = React15.useRef(() => {
4355
- });
4356
- const updateElements = React15.useCallback(() => {
4357
- if (!focusCtx) return;
4358
- const registered = focusCtx.getRegisteredElements?.() ?? [];
4359
- const mapped = registered.map(({ id, node }) => ({
4360
- id,
4361
- node,
4362
- layout: layoutCtx?.getLayout(node) ?? node.layout,
4363
- type: node.type
4364
- }));
4365
- mapped.sort((a, b) => {
4366
- if (a.layout.y !== b.layout.y) {
4367
- return a.layout.y - b.layout.y;
4368
- }
4369
- return a.layout.x - b.layout.x;
4370
- });
4371
- setElements(mapped);
4372
- }, [focusCtx, layoutCtx]);
4373
- updateRef.current = updateElements;
4374
- React15.useEffect(() => {
4375
- if (!focusCtx) return;
4376
- updateElements();
4377
- const unsubscribe = focusCtx.onFocusChange(() => {
4378
- updateElements();
4379
- });
4380
- const timer = setTimeout(updateElements, 50);
4381
- return () => {
4382
- unsubscribe();
4383
- clearTimeout(timer);
4384
- };
4385
- }, [focusCtx, layoutCtx, updateElements]);
4386
- if (!focusCtx) return null;
4387
- return {
4388
- elements,
4389
- focusedId: focusCtx.focusedId,
4390
- requestFocus: focusCtx.requestFocus,
4391
- focusNext: focusCtx.focusNext,
4392
- focusPrev: focusCtx.focusPrev,
4393
- refresh: () => updateRef.current()
4394
- };
4395
- }
4396
-
4397
- // src/components/JumpNav.tsx
4350
+ var JumpNavContext = React15.createContext(null);
4398
4351
  function generateHints(count, chars) {
4399
4352
  const hints = [];
4400
4353
  const charList = chars.split("");
@@ -4422,8 +4375,25 @@ function JumpNav({
4422
4375
  }) {
4423
4376
  const [isActive, setIsActive] = React15.useState(false);
4424
4377
  const [inputBuffer, setInputBuffer] = React15.useState("");
4425
- const registry = useFocusRegistry();
4378
+ const [hasChildJumpNav, setHasChildJumpNav] = React15.useState(false);
4379
+ const [elements, setElements] = React15.useState([]);
4426
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]);
4427
4397
  const parseKey = React15.useCallback((keyStr) => {
4428
4398
  const parts = keyStr.toLowerCase().split("+");
4429
4399
  return {
@@ -4435,28 +4405,58 @@ function JumpNav({
4435
4405
  };
4436
4406
  }, []);
4437
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]);
4438
4436
  const wasActiveRef = React15.useRef(false);
4439
4437
  React15.useEffect(() => {
4440
- if (isActive && !wasActiveRef.current && registry?.refresh) {
4441
- registry.refresh();
4438
+ if (isActive && !wasActiveRef.current) {
4439
+ refreshElements();
4442
4440
  }
4443
4441
  wasActiveRef.current = isActive;
4444
- }, [isActive]);
4445
- const elements = registry?.elements ?? [];
4442
+ }, [isActive, refreshElements]);
4446
4443
  const visibleElements = elements.filter(
4447
4444
  (el) => el.layout.width > 0 && el.layout.height > 0
4448
4445
  );
4449
4446
  const visibleHints = generateHints(visibleElements.length, hintChars);
4450
- const visibleHintMap = /* @__PURE__ */ new Map();
4451
- visibleElements.forEach((el, i) => {
4452
- if (visibleHints[i]) {
4453
- visibleHintMap.set(visibleHints[i], el.id);
4454
- }
4455
- });
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
4456
  React15.useEffect(() => {
4457
4457
  if (!inputCtx || !enabled) return;
4458
4458
  const handler = (key) => {
4459
- if (!isActive && key.name === activationKeyParsed.name && !!key.ctrl === activationKeyParsed.ctrl && !!key.alt === activationKeyParsed.alt && !!key.shift === activationKeyParsed.shift && !!key.meta === activationKeyParsed.meta) {
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
4460
  setIsActive(true);
4461
4461
  setInputBuffer("");
4462
4462
  return true;
@@ -4475,7 +4475,7 @@ function JumpNav({
4475
4475
  const newBuffer = inputBuffer + key.sequence.toLowerCase();
4476
4476
  const targetId = visibleHintMap.get(newBuffer);
4477
4477
  if (targetId) {
4478
- registry?.requestFocus(targetId);
4478
+ focusCtx?.requestFocus(targetId);
4479
4479
  setIsActive(false);
4480
4480
  setInputBuffer("");
4481
4481
  return true;
@@ -4493,7 +4493,7 @@ function JumpNav({
4493
4493
  return false;
4494
4494
  };
4495
4495
  return inputCtx.subscribePriority(handler);
4496
- }, [inputCtx, enabled, isActive, activationKeyParsed, inputBuffer, visibleHintMap, registry]);
4496
+ }, [inputCtx, enabled, isActive, activationKeyParsed, inputBuffer, visibleHintMap, focusCtx, hasChildJumpNav]);
4497
4497
  const hintsOverlay = isActive ? React15__default.default.createElement(
4498
4498
  React15__default.default.Fragment,
4499
4499
  null,
@@ -4541,10 +4541,24 @@ function JumpNav({
4541
4541
  }, inputBuffer ? `Jump: ${inputBuffer}_` : "Press a key to jump \u2022 ESC to cancel")
4542
4542
  )
4543
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
+ );
4544
4558
  return React15__default.default.createElement(
4545
- React15__default.default.Fragment,
4546
- null,
4547
- children,
4559
+ JumpNavContext.Provider,
4560
+ { value: contextValue },
4561
+ wrappedChildren,
4548
4562
  hintsOverlay
4549
4563
  );
4550
4564
  }
@@ -4646,6 +4660,52 @@ function useApp() {
4646
4660
  }
4647
4661
  };
4648
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
+ }
4649
4709
 
4650
4710
  // src/utils/mask.ts
4651
4711
  function parseMask(mask) {