@sigx/lynx-navigation 0.1.0 → 0.1.2

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.
Files changed (139) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +355 -0
  3. package/dist/components/Drawer.d.ts +56 -0
  4. package/dist/components/Drawer.d.ts.map +1 -0
  5. package/dist/components/Drawer.js +74 -0
  6. package/dist/components/Drawer.js.map +1 -0
  7. package/dist/components/EdgeBackHandle.js +144 -0
  8. package/dist/components/EdgeBackHandle.js.map +1 -0
  9. package/dist/components/EntryScope.d.ts +26 -0
  10. package/dist/components/EntryScope.d.ts.map +1 -0
  11. package/dist/components/EntryScope.js +33 -0
  12. package/dist/components/EntryScope.js.map +1 -0
  13. package/dist/components/Header.d.ts +7 -0
  14. package/dist/components/Header.d.ts.map +1 -0
  15. package/dist/components/Header.js +103 -0
  16. package/dist/components/Header.js.map +1 -0
  17. package/dist/components/Link.js +1 -4
  18. package/dist/components/Link.js.map +1 -1
  19. package/dist/components/NavigationRoot.d.ts +1 -1
  20. package/dist/components/NavigationRoot.d.ts.map +1 -1
  21. package/dist/components/NavigationRoot.js +29 -3
  22. package/dist/components/NavigationRoot.js.map +1 -1
  23. package/dist/components/Screen.d.ts +98 -0
  24. package/dist/components/Screen.d.ts.map +1 -0
  25. package/dist/components/Screen.js +94 -0
  26. package/dist/components/Screen.js.map +1 -0
  27. package/dist/components/ScreenContainer.d.ts.map +1 -1
  28. package/dist/components/ScreenContainer.js +77 -0
  29. package/dist/components/ScreenContainer.js.map +1 -0
  30. package/dist/components/Stack.d.ts.map +1 -1
  31. package/dist/components/Stack.js +60 -24
  32. package/dist/components/Stack.js.map +1 -1
  33. package/dist/components/TabBar.d.ts +40 -0
  34. package/dist/components/TabBar.d.ts.map +1 -0
  35. package/dist/components/TabBar.js +63 -0
  36. package/dist/components/TabBar.js.map +1 -0
  37. package/dist/components/Tabs.d.ts +101 -0
  38. package/dist/components/Tabs.d.ts.map +1 -0
  39. package/dist/components/Tabs.js +140 -0
  40. package/dist/components/Tabs.js.map +1 -0
  41. package/dist/hooks/use-focus.d.ts +46 -0
  42. package/dist/hooks/use-focus.d.ts.map +1 -0
  43. package/dist/hooks/use-focus.js +81 -0
  44. package/dist/hooks/use-focus.js.map +1 -0
  45. package/dist/hooks/use-hardware-back.js +50 -0
  46. package/dist/hooks/use-hardware-back.js.map +1 -0
  47. package/dist/hooks/use-linking-nav.d.ts +92 -0
  48. package/dist/hooks/use-linking-nav.d.ts.map +1 -0
  49. package/dist/hooks/use-linking-nav.js +109 -0
  50. package/dist/hooks/use-linking-nav.js.map +1 -0
  51. package/dist/hooks/use-nav-internal.d.ts +38 -1
  52. package/dist/hooks/use-nav-internal.d.ts.map +1 -1
  53. package/dist/hooks/use-nav-internal.js +32 -0
  54. package/dist/hooks/use-nav-internal.js.map +1 -1
  55. package/dist/hooks/use-nav-serializer.d.ts +83 -0
  56. package/dist/hooks/use-nav-serializer.d.ts.map +1 -0
  57. package/dist/hooks/use-nav-serializer.js +181 -0
  58. package/dist/hooks/use-nav-serializer.js.map +1 -0
  59. package/dist/hooks/use-nav.js.map +1 -1
  60. package/dist/hooks/use-screen-options.d.ts +3 -0
  61. package/dist/hooks/use-screen-options.d.ts.map +1 -0
  62. package/dist/hooks/use-screen-options.js +43 -0
  63. package/dist/hooks/use-screen-options.js.map +1 -0
  64. package/dist/href.d.ts +16 -1
  65. package/dist/href.d.ts.map +1 -1
  66. package/dist/href.js +50 -7
  67. package/dist/href.js.map +1 -1
  68. package/dist/index.d.ts +18 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +15 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/internal/screen-registry.d.ts +49 -0
  73. package/dist/internal/screen-registry.d.ts.map +1 -0
  74. package/dist/internal/screen-registry.js +59 -0
  75. package/dist/internal/screen-registry.js.map +1 -0
  76. package/dist/internal/screen-width.js +30 -0
  77. package/dist/internal/screen-width.js.map +1 -0
  78. package/dist/navigator/core.d.ts +20 -1
  79. package/dist/navigator/core.d.ts.map +1 -1
  80. package/dist/navigator/core.js +231 -36
  81. package/dist/navigator/core.js.map +1 -1
  82. package/dist/types.d.ts +56 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/url/build.d.ts +16 -0
  85. package/dist/url/build.d.ts.map +1 -0
  86. package/dist/url/build.js +30 -0
  87. package/dist/url/build.js.map +1 -0
  88. package/dist/url/compile.d.ts +35 -0
  89. package/dist/url/compile.d.ts.map +1 -0
  90. package/dist/url/compile.js +83 -0
  91. package/dist/url/compile.js.map +1 -0
  92. package/dist/url/format.d.ts +29 -0
  93. package/dist/url/format.d.ts.map +1 -0
  94. package/dist/url/format.js +102 -0
  95. package/dist/url/format.js.map +1 -0
  96. package/dist/url/index.d.ts +13 -0
  97. package/dist/url/index.d.ts.map +1 -0
  98. package/dist/url/index.js +13 -0
  99. package/dist/url/index.js.map +1 -0
  100. package/dist/url/parse.d.ts +21 -0
  101. package/dist/url/parse.d.ts.map +1 -0
  102. package/dist/url/parse.js +94 -0
  103. package/dist/url/parse.js.map +1 -0
  104. package/dist/url/registry.d.ts +41 -0
  105. package/dist/url/registry.d.ts.map +1 -0
  106. package/dist/url/registry.js +56 -0
  107. package/dist/url/registry.js.map +1 -0
  108. package/dist/url/validate.d.ts +24 -0
  109. package/dist/url/validate.d.ts.map +1 -0
  110. package/dist/url/validate.js +37 -0
  111. package/dist/url/validate.js.map +1 -0
  112. package/package.json +44 -15
  113. package/src/components/Drawer.tsx +119 -0
  114. package/src/components/EdgeBackHandle.tsx +1 -1
  115. package/src/components/EntryScope.tsx +38 -0
  116. package/src/components/Header.tsx +129 -0
  117. package/src/components/NavigationRoot.tsx +9 -1
  118. package/src/components/Screen.tsx +116 -0
  119. package/src/components/ScreenContainer.tsx +14 -1
  120. package/src/components/Stack.tsx +21 -2
  121. package/src/components/TabBar.tsx +104 -0
  122. package/src/components/Tabs.tsx +216 -0
  123. package/src/hooks/use-focus.ts +88 -0
  124. package/src/hooks/use-linking-nav.ts +159 -0
  125. package/src/hooks/use-nav-internal.ts +48 -1
  126. package/src/hooks/use-nav-serializer.ts +239 -0
  127. package/src/hooks/use-screen-options.ts +48 -0
  128. package/src/href.ts +68 -11
  129. package/src/index.ts +29 -0
  130. package/src/internal/screen-registry.ts +89 -0
  131. package/src/navigator/core.ts +86 -4
  132. package/src/types.ts +56 -0
  133. package/src/url/build.ts +35 -0
  134. package/src/url/compile.ts +109 -0
  135. package/src/url/format.ts +95 -0
  136. package/src/url/index.ts +18 -0
  137. package/src/url/parse.ts +102 -0
  138. package/src/url/registry.ts +69 -0
  139. package/src/url/validate.ts +67 -0
@@ -1,39 +1,75 @@
1
- import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
2
2
  import { component } from '@sigx/lynx';
3
+ import { Suspense, isLazyComponent } from '@sigx/lynx';
3
4
  import { useNav } from '../hooks/use-nav.js';
4
- import { useNavRoutes } from '../hooks/use-nav-internal.js';
5
+ import { useNavInternals, useNavRoutes } from '../hooks/use-nav-internal.js';
6
+ import { ScreenContainer } from './ScreenContainer.js';
7
+ import { EdgeBackHandle } from './EdgeBackHandle.js';
8
+ import { EntryScope } from './EntryScope.js';
5
9
  /**
6
- * Stack navigator — renders the topmost stack entry's component.
10
+ * Stack navigator — renders the topmost stack entry's component at rest, or
11
+ * the top + underneath entries during a transition.
7
12
  *
8
- * v0.1 minimal slice: renders only the top entry, with no transition and no
9
- * preserved-mount lower entries. Phase 0.2 extends this to mount the
10
- * top-N entries (so back-swipe can show the underlying screen) and runs MTS
11
- * transition drivers between them.
13
+ * **Idle**: just the top entry, full-bleed, no transform. The screen
14
+ * component mounts directly so it can use its own layout (no extra absolute
15
+ * positioning that would break percentage heights).
12
16
  *
13
- * `key={top.key}` forces unmount/remount when the active entry changes — same
14
- * pattern `@sigx/router`'s `<RouterView>` uses to prevent lazy components
15
- * from stacking (`packages/router/src/RouterView.tsx:97-98`).
17
+ * **Transitioning**: two `<ScreenContainer>` instances stacked absolutely,
18
+ * each with an MT-driven `translateX` that reads from the navigator's
19
+ * progress `SharedValue`. The host's BG thread doesn't tick per frame —
20
+ * `useAnimatedStyle` runs the interpolation entirely on MT.
21
+ *
22
+ * `key={top.key}` keeps the idle render's component instance stable across
23
+ * unrelated re-renders. During transitions, composite keys
24
+ * (`${entry.key}-${role}-${kind}`) ensure a fresh mount per role/kind pair so
25
+ * the `useAnimatedStyle` binding is set with the right input/output ranges.
16
26
  */
17
27
  export const Stack = component(() => {
18
28
  const nav = useNav();
19
29
  const routes = useNavRoutes();
30
+ const internals = useNavInternals();
20
31
  return () => {
32
+ const transition = nav.transition;
21
33
  const top = nav.current;
22
- const route = routes[top.route];
23
- if (!route) {
24
- // Defensive: navigator state should never reference an unregistered
25
- // route, but if it does we surface nothing rather than crash.
26
- return null;
34
+ if (!transition) {
35
+ const route = routes[top.route];
36
+ if (!route)
37
+ return null;
38
+ const Comp = route.component;
39
+ if (typeof Comp !== 'function')
40
+ return null;
41
+ const params = top.params;
42
+ // Wrap lazy routes that declare a `fallback` in <Suspense> so the
43
+ // chunk-load shows the user-provided spinner instead of throwing
44
+ // up to the nearest outer boundary (which may be wrong layer or
45
+ // missing entirely).
46
+ const body = isLazyComponent(Comp) && route.fallback
47
+ ? (_jsx(Suspense, { fallback: route.fallback, children: _jsx(Comp, { ...params }) }))
48
+ : _jsx(Comp, { ...params });
49
+ // When canGoBack and edge-swipe is enabled, overlay the gesture
50
+ // handle so the user can pan from the left edge to start a back
51
+ // transition. `position: absolute` doesn't disturb the screen's
52
+ // own layout — the handle only intercepts touches in the leftmost
53
+ // 20px, and only when they pan rightward past `MIN_DISTANCE`.
54
+ if (nav.canGoBack && internals.edgeSwipeEnabled) {
55
+ return (_jsxs("view", { style: {
56
+ position: 'relative',
57
+ width: '100%',
58
+ height: '100%',
59
+ }, children: [_jsx(EntryScope, { entry: top, children: body }, top.key), _jsx(EdgeBackHandle, {}, "edge-back")] }));
60
+ }
61
+ return (_jsx(EntryScope, { entry: top, children: body }, top.key));
27
62
  }
28
- const Comp = route.component;
29
- if (typeof Comp !== 'function')
30
- return null;
31
- // Phase 0.1 only handles eager component factories. Lazy
32
- // imports — `() => import(...)` need a `lazy()` wrapper from sigx;
33
- // we'll integrate that in the showcase-migration slice.
34
- const ComponentFactoryRef = Comp;
35
- const params = top.params;
36
- return _jsx(ComponentFactoryRef, { ...params }, top.key);
63
+ // Cast progress: TransitionState carries it as `unknown` to avoid
64
+ // pinning the contract to `@sigx/lynx`'s SharedValue at the type
65
+ // level; here at the runtime boundary we know it's a SharedValue<number>.
66
+ const progress = transition.progress;
67
+ return (_jsxs("view", { style: {
68
+ position: 'relative',
69
+ width: '100%',
70
+ height: '100%',
71
+ overflow: 'hidden',
72
+ }, children: [_jsx(ScreenContainer, { entry: transition.underneathEntry, routes: routes, role: "underneath", kind: transition.kind, progress: progress }, `${transition.underneathEntry.key}-underneath-${transition.kind}`), _jsx(ScreenContainer, { entry: transition.topEntry, routes: routes, role: "top", kind: transition.kind, progress: progress }, `${transition.topEntry.key}-top-${transition.kind}`)] }));
37
73
  };
38
74
  });
39
75
  //# sourceMappingURL=Stack.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Stack.js","sourceRoot":"","sources":["../../src/components/Stack.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE;IAChC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAE9B,OAAO,GAAG,EAAE;QACR,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC;QACxB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,oEAAoE;YACpE,8DAA8D;YAC9D,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC;QAC7B,IAAI,OAAO,IAAI,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC;QAC5C,yDAAyD;QACzD,qEAAqE;QACrE,wDAAwD;QACxD,MAAM,mBAAmB,GAAG,IAI3B,CAAC;QACF,MAAM,MAAM,GAAG,GAAG,CAAC,MAAiC,CAAC;QACrD,OAAO,KAAC,mBAAmB,OAAmB,MAAM,IAAnB,GAAG,CAAC,GAAG,CAAgB,CAAC;IAC7D,CAAC,CAAC;AACN,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"Stack.js","sourceRoot":"","sources":["../../src/components/Stack.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `<TabBar>` — default chrome for `<Tabs>`.
3
+ *
4
+ * Renders a row of tab buttons reading from the enclosing `useTabs()`
5
+ * navigator. Active tab is highlighted via the `active` prop on each
6
+ * default button (consumers can re-style via `renderTab`).
7
+ *
8
+ * Customization knobs:
9
+ * - `renderTab`: a function `(info, ctx) => JSX` that fully replaces the
10
+ * default button rendering for each tab. `ctx.active` tells the
11
+ * consumer whether this tab is currently focused; `ctx.onPress`
12
+ * activates the tab.
13
+ *
14
+ * Accessibility:
15
+ * - Each default button gets `accessibility-label` from
16
+ * `info.accessibilityLabel ?? info.label ?? info.name`.
17
+ * - Each default button gets `accessibility-element="true"` so screen
18
+ * readers see the whole pill, not just the inner `<text>`.
19
+ * - Each default button gets `accessibility-trait="button"` and a
20
+ * `selected` flag on the active one so VoiceOver/TalkBack announces
21
+ * focus state on tab switch.
22
+ *
23
+ * Placement: mount inside `<Tabs>` alongside the `<Tabs.Screen>`s. Order
24
+ * matters visually (place above or below the screen bodies depending on
25
+ * the layout), and `<Tabs.Screen>` bodies all stack with `display:flex` so
26
+ * the TabBar should be at a deterministic position in the JSX.
27
+ */
28
+ import { type JSXElement } from '@sigx/lynx';
29
+ import { type TabInfo } from './Tabs.js';
30
+ /** Rendering context passed to a `renderTab` consumer. */
31
+ export interface TabRenderContext {
32
+ /** True when this tab is currently active. Reactive — re-runs render on change. */
33
+ readonly active: boolean;
34
+ /** Activates this tab. Use as a `bindtap` handler on the rendered node. */
35
+ onPress(): void;
36
+ }
37
+ export declare const TabBar: import("@sigx/runtime-core").ComponentFactory<{
38
+ renderTab?: ((info: TabInfo, ctx: TabRenderContext) => JSXElement) | undefined;
39
+ }, void, {}>;
40
+ //# sourceMappingURL=TabBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabBar.d.ts","sourceRoot":"","sources":["../../src/components/TabBar.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAGH,KAAK,UAAU,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAElD,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,OAAO,IAAI,IAAI,CAAC;CACnB;AAmCD,eAAO,MAAM,MAAM;wBAhCmB,OAAO,OAAO,gBAAgB,KAAK,UAAU;YA4DjF,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
2
+ /**
3
+ * `<TabBar>` — default chrome for `<Tabs>`.
4
+ *
5
+ * Renders a row of tab buttons reading from the enclosing `useTabs()`
6
+ * navigator. Active tab is highlighted via the `active` prop on each
7
+ * default button (consumers can re-style via `renderTab`).
8
+ *
9
+ * Customization knobs:
10
+ * - `renderTab`: a function `(info, ctx) => JSX` that fully replaces the
11
+ * default button rendering for each tab. `ctx.active` tells the
12
+ * consumer whether this tab is currently focused; `ctx.onPress`
13
+ * activates the tab.
14
+ *
15
+ * Accessibility:
16
+ * - Each default button gets `accessibility-label` from
17
+ * `info.accessibilityLabel ?? info.label ?? info.name`.
18
+ * - Each default button gets `accessibility-element="true"` so screen
19
+ * readers see the whole pill, not just the inner `<text>`.
20
+ * - Each default button gets `accessibility-trait="button"` and a
21
+ * `selected` flag on the active one so VoiceOver/TalkBack announces
22
+ * focus state on tab switch.
23
+ *
24
+ * Placement: mount inside `<Tabs>` alongside the `<Tabs.Screen>`s. Order
25
+ * matters visually (place above or below the screen bodies depending on
26
+ * the layout), and `<Tabs.Screen>` bodies all stack with `display:flex` so
27
+ * the TabBar should be at a deterministic position in the JSX.
28
+ */
29
+ import { component, } from '@sigx/lynx';
30
+ import { useTabs } from './Tabs.js';
31
+ /**
32
+ * Default per-tab button. Plain `<view>` with a `<text>` inside, an
33
+ * `accessibility-*` cluster for screen readers, and a tap handler. No
34
+ * styling beyond a minimal active-state marker — consumers that want
35
+ * branded chrome pass `renderTab`.
36
+ */
37
+ const DefaultTabButton = component(({ props }) => {
38
+ return () => {
39
+ const label = props.info.label ?? props.info.name;
40
+ const a11y = props.info.accessibilityLabel ?? label;
41
+ return (_jsxs("view", { bindtap: () => props.onPress(), "accessibility-element": true, "accessibility-label": a11y, "accessibility-trait": "button", "accessibility-status": props.active ? 'selected' : undefined, style: { opacity: props.active ? 1 : 0.6 }, children: [props.info.icon ?? null, _jsx("text", { children: label })] }));
42
+ };
43
+ });
44
+ export const TabBar = component(({ props }) => {
45
+ const nav = useTabs();
46
+ return () => {
47
+ // Reading `nav.tabs` and `nav.active` here ties this render to both
48
+ // the registration list and the active signal — switching active or
49
+ // adding/removing a `<Tabs.Screen>` updates the bar reactively.
50
+ const tabs = nav.tabs;
51
+ const active = nav.active;
52
+ const renderer = props.renderTab;
53
+ return (_jsx("view", { "accessibility-element": false, children: tabs.map((info) => {
54
+ const isActive = info.name === active;
55
+ const onPress = () => nav.setActive(info.name);
56
+ if (renderer) {
57
+ return renderer(info, { active: isActive, onPress });
58
+ }
59
+ return (_jsx(DefaultTabButton, { info: info, active: isActive, onPress: onPress }));
60
+ }) }));
61
+ };
62
+ });
63
+ //# sourceMappingURL=TabBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabBar.js","sourceRoot":"","sources":["../../src/components/TabBar.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EACH,SAAS,GAGZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAgB,MAAM,WAAW,CAAC;AAalD;;;;;GAKG;AACH,MAAM,gBAAgB,GAAG,SAAS,CAIhC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;IACZ,OAAO,GAAG,EAAE;QACR,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QAClD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,kBAAkB,IAAI,KAAK,CAAC;QACpD,OAAO,CACH,gBACI,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,2BACP,IAAI,yBACN,IAAI,yBACL,QAAQ,0BACN,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EAC3D,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,aAEzC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,EACxB,yBAAO,KAAK,GAAQ,IACjB,CACV,CAAC;IACN,CAAC,CAAC;AACN,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG,SAAS,CAAc,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;IACvD,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,OAAO,GAAG,EAAE;QACR,oEAAoE;QACpE,oEAAoE;QACpE,gEAAgE;QAChE,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACtB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC;QACjC,OAAO,CACH,wCAA6B,KAAK,YAC7B,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gBACf,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC;gBACtC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC/C,IAAI,QAAQ,EAAE,CAAC;oBACX,OAAO,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;gBACzD,CAAC;gBACD,OAAO,CACH,KAAC,gBAAgB,IACb,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,OAAO,GAClB,CACL,CAAC;YACN,CAAC,CAAC,GACC,CACV,CAAC;IACN,CAAC,CAAC;AACN,CAAC,CAAC,CAAC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `<Tabs>` — Lynx tab navigator.
3
+ *
4
+ * Usage:
5
+ *
6
+ * ```tsx
7
+ * <NavigationRoot routes={routes}>
8
+ * <Tabs initialTab="feed">
9
+ * <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
10
+ * <FeedView />
11
+ * </Tabs.Screen>
12
+ * <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
13
+ * <ProfileView />
14
+ * </Tabs.Screen>
15
+ * </Tabs>
16
+ * </NavigationRoot>
17
+ * ```
18
+ *
19
+ * Scope of this slice (v0.1): pure UI primitive. Each tab's body stays
20
+ * mounted for state preservation (the inactive ones render with
21
+ * `display: 'none'`). Active tab is reactive via `useTabs()`.
22
+ *
23
+ * Out of scope (deferred to a nested-navigators slice):
24
+ * - Per-tab `<Stack>` with its own navigator state machine
25
+ * - `nav.parent` chain into the Tabs nav
26
+ * - Named navigators (`useNav('root')`)
27
+ *
28
+ * Those build on multi-navigator-state plumbing that isn't ready yet.
29
+ * For now, the inner content of a `<Tabs.Screen>` shares the same nav as
30
+ * its outer `<NavigationRoot>` — usable for shallow tab apps, but full
31
+ * nested routing comes later.
32
+ */
33
+ import { type Define, type JSXElement } from '@sigx/lynx';
34
+ /** Metadata about a registered `<Tabs.Screen>`. */
35
+ export interface TabInfo {
36
+ /** Stable tab id, used by `setActive`. */
37
+ readonly name: string;
38
+ /** Optional icon node — passed through to the default tab bar. */
39
+ readonly icon?: JSXElement;
40
+ /** Optional human-readable label. Defaults to `name`. */
41
+ readonly label?: string;
42
+ /**
43
+ * Accessibility label announced by screen readers. Falls back to
44
+ * `label`, then `name`. Surfaced as `accessibility-label` on the
45
+ * default `<TabBar>` button.
46
+ */
47
+ readonly accessibilityLabel?: string;
48
+ }
49
+ /** Reactive controller exposed by `useTabs()`. */
50
+ export interface TabsNav {
51
+ /** Currently-active tab name. Reactive — accessing inside render/effect tracks. */
52
+ readonly active: string;
53
+ /** Switch the active tab. Triggers reactive updates in any consumer. */
54
+ setActive(name: string): void;
55
+ /** Snapshot of registered tabs in registration order. Reactive. */
56
+ readonly tabs: ReadonlyArray<TabInfo>;
57
+ }
58
+ /**
59
+ * Access the enclosing Tabs navigator. Throws when called outside `<Tabs>`.
60
+ */
61
+ export declare const useTabs: import("@sigx/runtime-core").InjectableFunction<TabsNav>;
62
+ type TabsProps = Define.Prop<'initialTab', string> & Define.Slot<'default'>;
63
+ type TabsScreenProps = Define.Prop<'name', string, true> & Define.Prop<'icon', JSXElement> & Define.Prop<'label', string> & Define.Prop<'accessibilityLabel', string> & Define.Slot<'default'>;
64
+ /**
65
+ * Compound export. `Tabs` is the parent component; `Tabs.Screen` registers
66
+ * an individual tab. Matches the `Screen` / `Screen.Header` shape used
67
+ * elsewhere in this package and the daisyui `Modal` / `Modal.Header`
68
+ * convention.
69
+ */
70
+ export declare const Tabs: ((props: {
71
+ initialTab?: string | undefined;
72
+ } & {} & {
73
+ slots?: Partial<{
74
+ default: () => JSXElement | JSXElement[] | null;
75
+ }> | undefined;
76
+ } & {} & JSX.IntrinsicAttributes & import("@sigx/runtime-core").ComponentAttributeExtensions & {
77
+ ref?: import("@sigx/runtime-core").Ref<void> | undefined;
78
+ children?: any;
79
+ }) => JSXElement) & {
80
+ __setup: import("@sigx/runtime-core").SetupFn<{
81
+ initialTab?: string | undefined;
82
+ }, TabsProps, void, {
83
+ default: () => JSXElement | JSXElement[] | null;
84
+ }>;
85
+ __name?: string;
86
+ __islandId?: string;
87
+ __props: {
88
+ initialTab?: string | undefined;
89
+ };
90
+ __events: TabsProps;
91
+ __ref: void;
92
+ __slots: {
93
+ default: () => JSXElement | JSXElement[] | null;
94
+ };
95
+ } & {
96
+ Screen: import("@sigx/runtime-core").ComponentFactory<TabsScreenProps, void, {
97
+ default: () => JSXElement | JSXElement[] | null;
98
+ }>;
99
+ };
100
+ export {};
101
+ //# sourceMappingURL=Tabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../src/components/Tabs.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAQH,KAAK,MAAM,EACX,KAAK,UAAU,EAElB,MAAM,YAAY,CAAC;AAEpB,mDAAmD;AACnD,MAAM,WAAW,OAAO;IACpB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,kDAAkD;AAClD,MAAM,WAAW,OAAO;IACpB,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,eAAO,MAAM,OAAO,0DAIlB,CAAC;AAsBH,KAAK,SAAS,GACR,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,GACjC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAgE7B,KAAK,eAAe,GACd,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,GACjC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,GAC/B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAC5B,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,GACzC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAoC7B;;;;;GAKG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEf,CAAC"}
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
+ /**
3
+ * `<Tabs>` — Lynx tab navigator.
4
+ *
5
+ * Usage:
6
+ *
7
+ * ```tsx
8
+ * <NavigationRoot routes={routes}>
9
+ * <Tabs initialTab="feed">
10
+ * <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
11
+ * <FeedView />
12
+ * </Tabs.Screen>
13
+ * <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
14
+ * <ProfileView />
15
+ * </Tabs.Screen>
16
+ * </Tabs>
17
+ * </NavigationRoot>
18
+ * ```
19
+ *
20
+ * Scope of this slice (v0.1): pure UI primitive. Each tab's body stays
21
+ * mounted for state preservation (the inactive ones render with
22
+ * `display: 'none'`). Active tab is reactive via `useTabs()`.
23
+ *
24
+ * Out of scope (deferred to a nested-navigators slice):
25
+ * - Per-tab `<Stack>` with its own navigator state machine
26
+ * - `nav.parent` chain into the Tabs nav
27
+ * - Named navigators (`useNav('root')`)
28
+ *
29
+ * Those build on multi-navigator-state plumbing that isn't ready yet.
30
+ * For now, the inner content of a `<Tabs.Screen>` shares the same nav as
31
+ * its outer `<NavigationRoot>` — usable for shallow tab apps, but full
32
+ * nested routing comes later.
33
+ */
34
+ import { component, compound, defineInjectable, defineProvide, onUnmounted, signal, untrack, } from '@sigx/lynx';
35
+ /**
36
+ * Access the enclosing Tabs navigator. Throws when called outside `<Tabs>`.
37
+ */
38
+ export const useTabs = defineInjectable(() => {
39
+ throw new Error('[lynx-navigation] useTabs() called outside of a <Tabs> component.');
40
+ });
41
+ const useTabsRegistrar = defineInjectable(() => {
42
+ throw new Error('[lynx-navigation] <Tabs.Screen> rendered outside a <Tabs> component.');
43
+ });
44
+ const _Tabs = component(({ props, slots }) => {
45
+ // Tabs are stored as a deeply-reactive proxy signal so `tabs` consumers
46
+ // re-render when registration changes. `activeSignal` uses the wrapped
47
+ // `{value}` pattern so we can write a `string | null` without the
48
+ // proxy treating the inner string as an object.
49
+ const tabs = signal([]);
50
+ const activeSignal = signal({
51
+ value: props.initialTab ?? null,
52
+ });
53
+ const registrar = {
54
+ register(info) {
55
+ // Wrap in untrack so registration writes inside `<Tabs.Screen>`'s
56
+ // setup phase don't notify the same setup effect that issued them
57
+ // — sigx's setup runs in a tracked scope by default.
58
+ untrack(() => {
59
+ const idx = tabs.findIndex((t) => t.name === info.name);
60
+ if (idx === -1)
61
+ tabs.push(info);
62
+ else
63
+ tabs[idx] = info;
64
+ if (activeSignal.value === null) {
65
+ activeSignal.value = info.name;
66
+ }
67
+ });
68
+ },
69
+ unregister(name) {
70
+ untrack(() => {
71
+ const idx = tabs.findIndex((t) => t.name === name);
72
+ if (idx !== -1)
73
+ tabs.splice(idx, 1);
74
+ if (activeSignal.value === name) {
75
+ activeSignal.value = tabs[0]?.name ?? null;
76
+ }
77
+ });
78
+ },
79
+ tabs,
80
+ activeSignal,
81
+ };
82
+ const nav = {
83
+ get active() {
84
+ // Empty-tabs state is rare in practice (no <Tabs.Screen> yet) but
85
+ // possible during initial render; expose '' rather than null so
86
+ // consumers can compare strings without narrowing.
87
+ return activeSignal.value ?? '';
88
+ },
89
+ setActive(name) {
90
+ // Silently ignore unknown names rather than writing them and
91
+ // hiding every tab body. Surfacing as a no-op gives consumers a
92
+ // predictable failure mode for typos / dynamic name sources.
93
+ if (!tabs.some((t) => t.name === name))
94
+ return;
95
+ activeSignal.value = name;
96
+ },
97
+ get tabs() {
98
+ return tabs;
99
+ },
100
+ };
101
+ defineProvide(useTabs, () => nav);
102
+ defineProvide(useTabsRegistrar, () => registrar);
103
+ return () => slots.default?.();
104
+ });
105
+ const TabsScreen = component(({ props, slots }) => {
106
+ const registrar = useTabsRegistrar();
107
+ // Capture `name` once at setup. Props is reactive in sigx, but using a
108
+ // changing `name` for an already-registered screen would be ambiguous
109
+ // (rename vs re-register?) — pin it and require callers to remount on
110
+ // identity change. This matches React Navigation's contract.
111
+ const name = props.name;
112
+ registrar.register({
113
+ name,
114
+ icon: props.icon,
115
+ label: props.label,
116
+ accessibilityLabel: props.accessibilityLabel,
117
+ });
118
+ onUnmounted(() => registrar.unregister(name));
119
+ return () => {
120
+ // `display: none` keeps the body mounted so per-tab state survives
121
+ // tab switches. Read activeSignal here so re-activating triggers a
122
+ // re-render with display restored.
123
+ const active = registrar.activeSignal.value === name;
124
+ return (_jsx("view", { style: {
125
+ display: active ? 'flex' : 'none',
126
+ width: '100%',
127
+ height: '100%',
128
+ }, children: slots.default?.() }));
129
+ };
130
+ });
131
+ /**
132
+ * Compound export. `Tabs` is the parent component; `Tabs.Screen` registers
133
+ * an individual tab. Matches the `Screen` / `Screen.Header` shape used
134
+ * elsewhere in this package and the daisyui `Modal` / `Modal.Header`
135
+ * convention.
136
+ */
137
+ export const Tabs = compound(_Tabs, {
138
+ Screen: TabsScreen,
139
+ });
140
+ //# sourceMappingURL=Tabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../src/components/Tabs.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type Computed } from '@sigx/lynx';
2
+ /**
3
+ * Reactive "is this screen the focused entry?" signal.
4
+ *
5
+ * Must be called from inside a component rendered as a route by `<Stack>` (or
6
+ * any other navigator that uses `<EntryScope>`); throws otherwise. The
7
+ * returned `Computed` reads `nav.current.key` and compares it to the entry
8
+ * the calling screen was mounted for, so any nav mutation that changes the
9
+ * top entry flips the value.
10
+ *
11
+ * Note: screens stay mounted when something is pushed on top of them — they
12
+ * just lose focus. Pop the new top off and they regain focus.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const Profile = component(() => {
17
+ * const isFocused = useIsFocused();
18
+ * return () => <text>{isFocused.value ? 'visible' : 'hidden'}</text>;
19
+ * });
20
+ * ```
21
+ */
22
+ export declare function useIsFocused(): Computed<boolean>;
23
+ /**
24
+ * Run `cb` whenever this screen gains focus; run the returned cleanup when it
25
+ * loses focus or unmounts. Mirrors React Navigation's `useFocusEffect`.
26
+ *
27
+ * Lifecycle:
28
+ * - cb runs immediately if the screen is already focused at mount.
29
+ * - When the screen loses focus (something pushed on top), cleanup runs.
30
+ * - When focus returns (the cover is popped), `cb` runs again — yielding a
31
+ * fresh cleanup for the next blur.
32
+ * - On unmount, cleanup runs once if still focused.
33
+ *
34
+ * Common uses: subscribe to a data source while visible, track an analytics
35
+ * "screen view" event, start/stop a polling loop.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * useFocusEffect(() => {
40
+ * const id = setInterval(refresh, 5000);
41
+ * return () => clearInterval(id);
42
+ * });
43
+ * ```
44
+ */
45
+ export declare function useFocusEffect(cb: () => void | (() => void)): void;
46
+ //# sourceMappingURL=use-focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-focus.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,QAAQ,EAChB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAAC,OAAO,CAAC,CAOhD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CA0BlE"}
@@ -0,0 +1,81 @@
1
+ import { computed, effect, onUnmounted, untrack, } from '@sigx/lynx';
2
+ import { useNav } from './use-nav.js';
3
+ import { useCurrentEntry } from './use-nav-internal.js';
4
+ /**
5
+ * Reactive "is this screen the focused entry?" signal.
6
+ *
7
+ * Must be called from inside a component rendered as a route by `<Stack>` (or
8
+ * any other navigator that uses `<EntryScope>`); throws otherwise. The
9
+ * returned `Computed` reads `nav.current.key` and compares it to the entry
10
+ * the calling screen was mounted for, so any nav mutation that changes the
11
+ * top entry flips the value.
12
+ *
13
+ * Note: screens stay mounted when something is pushed on top of them — they
14
+ * just lose focus. Pop the new top off and they regain focus.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * const Profile = component(() => {
19
+ * const isFocused = useIsFocused();
20
+ * return () => <text>{isFocused.value ? 'visible' : 'hidden'}</text>;
21
+ * });
22
+ * ```
23
+ */
24
+ export function useIsFocused() {
25
+ const nav = useNav();
26
+ // Capture the entry's key once at setup. The entry object provided
27
+ // through `defineProvide` may carry reactive dependencies; we only care
28
+ // about the immutable key of the entry this screen was mounted for.
29
+ const myKey = useCurrentEntry().key;
30
+ return computed(() => nav.current.key === myKey);
31
+ }
32
+ /**
33
+ * Run `cb` whenever this screen gains focus; run the returned cleanup when it
34
+ * loses focus or unmounts. Mirrors React Navigation's `useFocusEffect`.
35
+ *
36
+ * Lifecycle:
37
+ * - cb runs immediately if the screen is already focused at mount.
38
+ * - When the screen loses focus (something pushed on top), cleanup runs.
39
+ * - When focus returns (the cover is popped), `cb` runs again — yielding a
40
+ * fresh cleanup for the next blur.
41
+ * - On unmount, cleanup runs once if still focused.
42
+ *
43
+ * Common uses: subscribe to a data source while visible, track an analytics
44
+ * "screen view" event, start/stop a polling loop.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * useFocusEffect(() => {
49
+ * const id = setInterval(refresh, 5000);
50
+ * return () => clearInterval(id);
51
+ * });
52
+ * ```
53
+ */
54
+ export function useFocusEffect(cb) {
55
+ const isFocused = useIsFocused();
56
+ let cleanup;
57
+ const runner = effect(() => {
58
+ const focused = isFocused.value;
59
+ // Always tear down any previous focus session before starting a new
60
+ // one (or before going dormant on blur). Wrap `cb` in `untrack` so
61
+ // signals read inside the user-provided callback can't retrigger the
62
+ // outer effect and stack subscriptions.
63
+ if (typeof cleanup === 'function') {
64
+ const fn = cleanup;
65
+ cleanup = undefined;
66
+ fn();
67
+ }
68
+ if (focused) {
69
+ cleanup = untrack(() => cb());
70
+ }
71
+ });
72
+ onUnmounted(() => {
73
+ if (typeof cleanup === 'function') {
74
+ const fn = cleanup;
75
+ cleanup = undefined;
76
+ fn();
77
+ }
78
+ runner.stop();
79
+ });
80
+ }
81
+ //# sourceMappingURL=use-focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-focus.js","sourceRoot":"","sources":["../../src/hooks/use-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,QAAQ,EACR,MAAM,EACN,WAAW,EACX,OAAO,GAEV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,wEAAwE;IACxE,oEAAoE;IACpE,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC,GAAG,CAAC;IACpC,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,EAA6B;IACxD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,IAAI,OAA4B,CAAC;IACjC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC;QAChC,oEAAoE;QACpE,mEAAmE;QACnE,qEAAqE;QACrE,wCAAwC;QACxC,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,OAAO,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;YACpB,EAAE,EAAE,CAAC;QACT,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACV,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAClC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,WAAW,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,OAAO,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;YACpB,EAAE,EAAE,CAAC;QACT,CAAC;QACD,MAAM,CAAC,IAAI,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC"}