@peers-app/peers-ui 0.25.2 → 0.25.3

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 (177) hide show
  1. package/README.md +2 -1
  2. package/dist/components/markdown-editor/mention-node.d.ts +4 -0
  3. package/dist/components/markdown-editor/mention-node.js +4 -1
  4. package/dist/components/responsive-section-nav.d.ts +34 -0
  5. package/dist/components/responsive-section-nav.js +19 -0
  6. package/dist/components/responsive-section-nav.test.d.ts +1 -0
  7. package/dist/components/responsive-section-nav.test.js +45 -0
  8. package/dist/components/router.d.ts +2 -3
  9. package/dist/components/router.js +6 -9
  10. package/dist/globals-hash.test.d.ts +1 -0
  11. package/dist/globals-hash.test.js +16 -0
  12. package/dist/globals.d.ts +19 -0
  13. package/dist/globals.js +51 -12
  14. package/dist/hooks/use-host-contract.d.ts +18 -0
  15. package/dist/hooks/use-host-contract.js +53 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/peers-services-registration.d.ts +1 -5
  19. package/dist/peers-services-registration.js +6 -16
  20. package/dist/root-layout/root-layout-host.js +7 -4
  21. package/dist/root-layout/root-layout-host.test.js +3 -6
  22. package/dist/screens/account/account-screen.d.ts +1 -0
  23. package/dist/screens/account/account-screen.js +29 -53
  24. package/dist/screens/account/account-screen.test.d.ts +1 -0
  25. package/dist/screens/account/account-screen.test.js +47 -0
  26. package/dist/screens/contacts/contact-details.js +2 -23
  27. package/dist/screens/contacts/contact-list.d.ts +24 -1
  28. package/dist/screens/contacts/contact-list.js +128 -143
  29. package/dist/screens/contacts/contact-list.test.d.ts +1 -0
  30. package/dist/screens/contacts/contact-list.test.js +48 -0
  31. package/dist/screens/contacts/index.d.ts +0 -2
  32. package/dist/screens/contacts/index.js +0 -3
  33. package/dist/screens/device-pairing/device-pairing.js +17 -16
  34. package/dist/screens/device-pairing/device-pairing.test.js +4 -9
  35. package/dist/screens/device-pairing/identity-ipc.d.ts +11 -0
  36. package/dist/screens/device-pairing/identity-ipc.js +20 -0
  37. package/dist/screens/device-pairing/use-device-pairing-state.d.ts +2 -2
  38. package/dist/screens/device-pairing/use-device-pairing-state.js +7 -25
  39. package/dist/screens/devices/devices-list.d.ts +18 -0
  40. package/dist/screens/devices/devices-list.js +173 -0
  41. package/dist/screens/devices/devices-list.test.d.ts +1 -0
  42. package/dist/screens/devices/devices-list.test.js +53 -0
  43. package/dist/screens/devices/index.d.ts +1 -0
  44. package/dist/screens/devices/index.js +5 -0
  45. package/dist/screens/groups/group-details.d.ts +1 -0
  46. package/dist/screens/groups/group-details.js +4 -63
  47. package/dist/screens/groups/group-invite-panel.js +2 -2
  48. package/dist/screens/groups/group-list.d.ts +18 -1
  49. package/dist/screens/groups/group-list.js +107 -136
  50. package/dist/screens/groups/group-list.test.d.ts +1 -0
  51. package/dist/screens/groups/group-list.test.js +38 -0
  52. package/dist/screens/groups/group-members.d.ts +1 -0
  53. package/dist/screens/groups/group-members.js +2 -1
  54. package/dist/screens/groups/index.d.ts +0 -3
  55. package/dist/screens/groups/index.js +0 -4
  56. package/dist/screens/identity/identity-routes.d.ts +107 -0
  57. package/dist/screens/identity/identity-routes.js +245 -0
  58. package/dist/screens/identity/identity-routes.test.d.ts +1 -0
  59. package/dist/screens/identity/identity-routes.test.js +51 -0
  60. package/dist/screens/identity/identity-screen.d.ts +9 -0
  61. package/dist/screens/identity/identity-screen.js +143 -0
  62. package/dist/screens/identity/identity-screen.test.d.ts +1 -0
  63. package/dist/screens/identity/identity-screen.test.js +103 -0
  64. package/dist/screens/identity/index.d.ts +2 -0
  65. package/dist/screens/identity/index.js +6 -0
  66. package/dist/screens/identity/invite-contact-panel.d.ts +4 -0
  67. package/dist/screens/identity/invite-contact-panel.js +30 -0
  68. package/dist/screens/identity/profile-invite.d.ts +7 -0
  69. package/dist/screens/identity/profile-invite.js +62 -0
  70. package/dist/screens/invites/accept-invite.js +1 -1
  71. package/dist/screens/invites/activity-view.d.ts +24 -0
  72. package/dist/screens/invites/activity-view.js +64 -0
  73. package/dist/screens/invites/activity-view.test.d.ts +1 -0
  74. package/dist/screens/invites/activity-view.test.js +108 -0
  75. package/dist/screens/invites/index.d.ts +1 -2
  76. package/dist/screens/invites/index.js +1 -3
  77. package/dist/screens/invites/invite-capture.d.ts +0 -2
  78. package/dist/screens/invites/invite-capture.js +1 -3
  79. package/dist/screens/invites/invite-classify.test.d.ts +1 -0
  80. package/dist/screens/invites/invite-classify.test.js +98 -0
  81. package/dist/screens/invites/invites-ipc.d.ts +1 -1
  82. package/dist/screens/invites/invites-ipc.js +5 -45
  83. package/dist/screens/invites/pending-invites-list.d.ts +35 -16
  84. package/dist/screens/invites/pending-invites-list.js +69 -30
  85. package/dist/screens/network-viewer/network-viewer-ipc.js +4 -59
  86. package/dist/screens/search/global-search.d.ts +1 -0
  87. package/dist/screens/search/global-search.js +2 -1
  88. package/dist/screens/settings/settings-page.js +25 -58
  89. package/dist/screens/setup-user.js +5 -5
  90. package/dist/screens/tools/tool-access-ipc.d.ts +2 -0
  91. package/dist/screens/tools/tool-access-ipc.js +8 -55
  92. package/dist/screens/welcome-modal.js +3 -2
  93. package/dist/system-apps/identity.app.d.ts +3 -0
  94. package/dist/system-apps/identity.app.js +10 -0
  95. package/dist/system-apps/index.d.ts +1 -4
  96. package/dist/system-apps/index.js +5 -17
  97. package/dist/tabs-layout/network-status-indicator.js +10 -3
  98. package/dist/tabs-layout/network-status-indicator.test.js +15 -0
  99. package/dist/tabs-layout/tabs-layout.js +3 -1
  100. package/dist/tabs-layout/tabs-state.d.ts +79 -1
  101. package/dist/tabs-layout/tabs-state.js +256 -79
  102. package/dist/tabs-layout/tabs-state.test.d.ts +1 -0
  103. package/dist/tabs-layout/tabs-state.test.js +110 -0
  104. package/package.json +3 -3
  105. package/src/components/markdown-editor/mention-node.ts +5 -1
  106. package/src/components/responsive-section-nav.test.tsx +88 -0
  107. package/src/components/responsive-section-nav.tsx +92 -0
  108. package/src/components/router.tsx +6 -9
  109. package/src/globals-hash.test.ts +19 -0
  110. package/src/globals.tsx +53 -13
  111. package/src/hooks/use-host-contract.ts +75 -0
  112. package/src/index.tsx +1 -0
  113. package/src/peers-services-registration.ts +6 -17
  114. package/src/root-layout/root-layout-host.test.tsx +4 -7
  115. package/src/root-layout/root-layout-host.tsx +9 -11
  116. package/src/screens/account/account-screen.test.tsx +61 -0
  117. package/src/screens/account/account-screen.tsx +116 -95
  118. package/src/screens/contacts/contact-details.tsx +4 -26
  119. package/src/screens/contacts/contact-list.test.tsx +59 -0
  120. package/src/screens/contacts/contact-list.tsx +246 -260
  121. package/src/screens/contacts/index.ts +0 -4
  122. package/src/screens/device-pairing/device-pairing.test.tsx +7 -9
  123. package/src/screens/device-pairing/device-pairing.tsx +19 -23
  124. package/src/screens/device-pairing/identity-ipc.ts +25 -0
  125. package/src/screens/device-pairing/use-device-pairing-state.ts +10 -33
  126. package/src/screens/devices/devices-list.test.tsx +68 -0
  127. package/src/screens/devices/devices-list.tsx +361 -0
  128. package/src/screens/devices/index.ts +1 -0
  129. package/src/screens/groups/group-details.tsx +8 -142
  130. package/src/screens/groups/group-invite-panel.tsx +2 -7
  131. package/src/screens/groups/group-list.test.tsx +44 -0
  132. package/src/screens/groups/group-list.tsx +227 -288
  133. package/src/screens/groups/group-members.tsx +3 -2
  134. package/src/screens/groups/index.ts +0 -5
  135. package/src/screens/identity/identity-routes.test.ts +70 -0
  136. package/src/screens/identity/identity-routes.ts +301 -0
  137. package/src/screens/identity/identity-screen.test.tsx +138 -0
  138. package/src/screens/identity/identity-screen.tsx +282 -0
  139. package/src/screens/identity/index.ts +2 -0
  140. package/src/screens/identity/invite-contact-panel.tsx +45 -0
  141. package/src/screens/identity/profile-invite.tsx +93 -0
  142. package/src/screens/invites/accept-invite.tsx +1 -1
  143. package/src/screens/invites/activity-view.test.tsx +127 -0
  144. package/src/screens/invites/activity-view.tsx +162 -0
  145. package/src/screens/invites/index.ts +1 -4
  146. package/src/screens/invites/invite-capture.ts +0 -3
  147. package/src/screens/invites/invite-classify.test.ts +110 -0
  148. package/src/screens/invites/invites-ipc.ts +6 -55
  149. package/src/screens/invites/pending-invites-list.tsx +85 -61
  150. package/src/screens/network-viewer/network-viewer-ipc.ts +8 -82
  151. package/src/screens/search/global-search.tsx +2 -1
  152. package/src/screens/settings/settings-page.tsx +53 -126
  153. package/src/screens/setup-user.tsx +5 -6
  154. package/src/screens/tools/tool-access-ipc.ts +13 -74
  155. package/src/screens/welcome-modal.tsx +3 -3
  156. package/src/system-apps/identity.app.ts +9 -0
  157. package/src/system-apps/index.ts +3 -12
  158. package/src/tabs-layout/network-status-indicator.test.tsx +22 -0
  159. package/src/tabs-layout/network-status-indicator.tsx +31 -0
  160. package/src/tabs-layout/tabs-layout.tsx +7 -1
  161. package/src/tabs-layout/tabs-state.test.ts +152 -0
  162. package/src/tabs-layout/tabs-state.ts +300 -83
  163. package/dist/screens/invites/invites-screen.d.ts +0 -9
  164. package/dist/screens/invites/invites-screen.js +0 -36
  165. package/dist/system-apps/account.app.d.ts +0 -2
  166. package/dist/system-apps/account.app.js +0 -8
  167. package/dist/system-apps/contacts.app.d.ts +0 -2
  168. package/dist/system-apps/contacts.app.js +0 -9
  169. package/dist/system-apps/groups.app.d.ts +0 -2
  170. package/dist/system-apps/groups.app.js +0 -9
  171. package/dist/system-apps/invites.app.d.ts +0 -2
  172. package/dist/system-apps/invites.app.js +0 -9
  173. package/src/screens/invites/invites-screen.tsx +0 -78
  174. package/src/system-apps/account.app.ts +0 -7
  175. package/src/system-apps/contacts.app.ts +0 -8
  176. package/src/system-apps/groups.app.ts +0 -8
  177. package/src/system-apps/invites.app.ts +0 -8
@@ -12,6 +12,7 @@ exports.TabsStripIndicators = TabsStripIndicators;
12
12
  const jsx_runtime_1 = require("react/jsx-runtime");
13
13
  const moment_1 = __importDefault(require("moment"));
14
14
  const react_1 = require("react");
15
+ const identity_routes_1 = require("../screens/identity/identity-routes");
15
16
  const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
16
17
  const tabs_state_1 = require("./tabs-state");
17
18
  const use_network_status_1 = require("./use-network-status");
@@ -189,8 +190,8 @@ function ConnectedDevicesList({ loadState }) {
189
190
  }
190
191
  return ((0, jsx_runtime_1.jsx)("ul", { className: "small mb-0 ps-0", style: { maxHeight: "180px", overflowY: "auto" }, children: loadState.connections.map((connection) => ((0, jsx_runtime_1.jsx)(ConnectedDeviceRow, { connection: connection }, connection.deviceId))) }));
191
192
  }
192
- function NetworkStatusDetails({ detailsId, status, unavailable, connectionsLoadState, onOpenViewer, }) {
193
- return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Connections" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectionCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Syncing contexts" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.syncingContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Sync errors" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.errorCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups connected" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectedContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups offline" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.disconnectedContextCount }), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
193
+ function NetworkStatusDetails({ detailsId, status, unavailable, connectionsLoadState, onUseInvite, onOpenIdentity, onOpenViewer, }) {
194
+ return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 mb-3", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-primary flex-fill", onClick: onUseInvite, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-qr-code me-1", "aria-hidden": "true" }), "Use Invite"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-primary flex-fill", onClick: onOpenIdentity, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-badge me-1", "aria-hidden": "true" }), "Identity"] })] }), (0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Connections" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectionCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Syncing contexts" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.syncingContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Sync errors" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.errorCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups connected" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectedContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Groups offline" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.disconnectedContextCount }), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
194
195
  }
195
196
  /** Accessible reactive network status button and details popover. */
196
197
  function NetworkStatusIndicator({ compact = false }) {
@@ -305,7 +306,13 @@ function NetworkStatusIndicator({ compact = false }) {
305
306
  fontWeight: 700,
306
307
  right: "-0.28rem",
307
308
  bottom: "-0.32rem",
308
- }, "aria-hidden": "true", children: presentation.disconnectedGroupsOverlay.count }))] }), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onOpenViewer: () => {
309
+ }, "aria-hidden": "true", children: presentation.disconnectedGroupsOverlay.count }))] }), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onUseInvite: () => {
310
+ setOpen(false);
311
+ (0, tabs_state_1.goToTabPath)(identity_routes_1.IDENTITY_ACCEPT_PATH);
312
+ }, onOpenIdentity: () => {
313
+ setOpen(false);
314
+ (0, tabs_state_1.goToTabPath)(identity_routes_1.IDENTITY_ACCOUNT_PATH);
315
+ }, onOpenViewer: () => {
309
316
  setOpen(false);
310
317
  (0, tabs_state_1.goToTabPath)("network-viewer");
311
318
  } }))] }));
@@ -7,6 +7,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const peers_sdk_1 = require("@peers-app/peers-sdk");
8
8
  const react_1 = require("@testing-library/react");
9
9
  const moment_1 = __importDefault(require("moment"));
10
+ const identity_routes_1 = require("../screens/identity/identity-routes");
10
11
  const network_status_indicator_1 = require("./network-status-indicator");
11
12
  const goToTabPath = jest.fn();
12
13
  const getConnections = jest.fn();
@@ -424,6 +425,20 @@ describe("NetworkStatusIndicator", () => {
424
425
  expect(view.getByRole("dialog")).toHaveTextContent("Couldn't load devices");
425
426
  });
426
427
  });
428
+ it("offers shortcuts to use an invite and open the Identity account", () => {
429
+ mockViewState = { availability: "ready", status: status({ state: "synced" }) };
430
+ const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
431
+ react_1.fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
432
+ expect(view.getByRole("button", { name: "Use Invite" }).querySelector(".bi-qr-code")).toBeTruthy();
433
+ expect(view.getByRole("button", { name: "Identity" }).querySelector(".bi-person-badge")).toBeTruthy();
434
+ react_1.fireEvent.click(view.getByRole("button", { name: "Use Invite" }));
435
+ expect(goToTabPath).toHaveBeenCalledWith(identity_routes_1.IDENTITY_ACCEPT_PATH);
436
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
437
+ react_1.fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
438
+ react_1.fireEvent.click(view.getByRole("button", { name: "Identity" }));
439
+ expect(goToTabPath).toHaveBeenCalledWith(identity_routes_1.IDENTITY_ACCOUNT_PATH);
440
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
441
+ });
427
442
  it("updates on rerender and opens details before navigating to Network Viewer", async () => {
428
443
  const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
429
444
  expect(view.getByRole("button", { name: /loading/ })).toBeTruthy();
@@ -44,6 +44,7 @@ const group_switcher_1 = require("../components/group-switcher");
44
44
  const router_1 = require("../components/router");
45
45
  const globals_1 = require("../globals");
46
46
  const hooks_1 = require("../hooks");
47
+ const identity_routes_1 = require("../screens/identity/identity-routes");
47
48
  const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown");
48
49
  const system_apps_1 = require("../system-apps");
49
50
  const routes_loader_1 = require("../ui-router/routes-loader");
@@ -281,7 +282,8 @@ const TabContentRenderer = react_1.default.memo(({ tab, isMobile }) => {
281
282
  // System apps use the main Router (which reads from mainContentPath)
282
283
  // Package apps use UIRouter with explicit path
283
284
  const isSystemApp = tab.packageId === "system-apps";
284
- return ((0, jsx_runtime_1.jsx)("div", { className: `h-100 overflow-auto ${isMobile ? "p-2" : "p-3"}`, children: isSystemApp ? ((0, jsx_runtime_1.jsx)(router_1.Router, { path: tab.path })) : ((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: tab.path, uiCategory: "screen", props: {} })) }));
285
+ const identityApp = isSystemApp && (0, identity_routes_1.isIdentityPath)(tab.path);
286
+ return ((0, jsx_runtime_1.jsx)("div", { className: `h-100 ${identityApp ? "overflow-hidden" : "overflow-auto"} ${identityApp ? "" : isMobile ? "p-2" : "p-3"}`, children: isSystemApp ? ((0, jsx_runtime_1.jsx)(router_1.Router, { path: tab.path })) : ((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: tab.path, uiCategory: "screen", props: {} })) }));
285
287
  }, (prevProps, nextProps) => {
286
288
  // Only re-render if the tab or isMobile changed
287
289
  return (prevProps.tab.tabId === nextProps.tab.tabId &&
@@ -1,5 +1,6 @@
1
1
  import { type IAppNav, type Observable } from "@peers-app/peers-sdk";
2
2
  import { type IPackageWithNavs } from "../ui-router/routes-loader";
3
+ /** One top-level app tab. */
3
4
  export interface TabState {
4
5
  tabId: string;
5
6
  packageId?: string;
@@ -7,24 +8,92 @@ export interface TabState {
7
8
  title: string;
8
9
  iconClassName?: string;
9
10
  }
11
+ /** The app launcher tab. It is always present and cannot be closed. */
10
12
  export declare const launcherApp: TabState;
13
+ /** Open tabs. Writes through to device storage after the initial restore. */
11
14
  export declare const activeTabs: Observable<TabState[]> & {
12
15
  loadingPromise: Promise<void>;
13
16
  };
17
+ /** Id of the tab whose path is showing. */
14
18
  export declare const activeTabId: Observable<string> & {
15
19
  loadingPromise: Promise<void>;
16
20
  };
21
+ /** App roots recently opened from the launcher, newest first. */
17
22
  export declare const recentlyUsedApps: Observable<string[]> & {
18
23
  loadingPromise: Promise<void>;
19
24
  };
25
+ /** Tab ids whose bodies have been mounted at least once. */
20
26
  export declare const initializedTabs: Set<string>;
27
+ /** Options for {@link navigateTo}. */
28
+ export interface NavigateOptions {
29
+ /**
30
+ * When the requested path is an app's root and that app is already open,
31
+ * activate the existing tab without resetting its section.
32
+ */
33
+ preserveSectionOnAppRoot?: boolean;
34
+ }
35
+ /**
36
+ * Open a path in the tab that owns its app.
37
+ *
38
+ * Legacy identity aliases are rewritten first. A route owned by the current
39
+ * app replaces that tab's path. A route owned by another open app activates
40
+ * that app's tab. Otherwise a new tab is opened.
41
+ *
42
+ * @param rawPath - Requested route, with or without a query string.
43
+ * @param options - App-root behavior. Launcher opens preserve the current section.
44
+ */
45
+ export declare function navigateTo(rawPath: string, options?: NavigateOptions): void;
46
+ /**
47
+ * Open an app or path from the launcher.
48
+ *
49
+ * Choosing an app that is already open keeps the section the user was on.
50
+ *
51
+ * @param path - App root or deep link.
52
+ */
21
53
  export declare function goToTabPath(path: string): void;
22
- export declare const handleMainPathChanged: (oldPath: string, newPath: string, setNewMainPath: (path: string) => void) => void;
54
+ /**
55
+ * Apply a {@link mainContentPath} write to the open tabs.
56
+ *
57
+ * @param _oldPath - Previous route. Retained for existing call sites.
58
+ * @param newPath - Requested route.
59
+ * @param _setNewMainPath - Direct path setter. Navigation updates the path itself.
60
+ */
61
+ export declare const handleMainPathChanged: (_oldPath: string, newPath: string, _setNewMainPath: (path: string) => void) => void;
62
+ /**
63
+ * Rewrite stored tabs onto canonical routes.
64
+ *
65
+ * Contacts, Groups, and Devices tabs from before the Identity app become one
66
+ * Identity tab. The active tab is kept when it is one of those duplicates.
67
+ *
68
+ * @param tabs - Persisted tabs.
69
+ * @param activeId - Persisted active tab id.
70
+ */
71
+ export declare function restoreNavigationState(tabs: TabState[], activeId: string): {
72
+ tabs: TabState[];
73
+ activeId: string;
74
+ };
75
+ /**
76
+ * Map a stored recent-app path onto the current app root.
77
+ *
78
+ * @param path - Persisted recent path, possibly a legacy alias.
79
+ */
80
+ export declare function migrateRecentPath(path: string): string;
23
81
  type AppInfo = {
24
82
  navItem: IAppNav;
25
83
  package: IPackageWithNavs;
26
84
  };
85
+ /**
86
+ * Resolve the app that owns a route.
87
+ *
88
+ * @param path - Route, with or without a query string.
89
+ * @returns The matching app, the launcher for an empty path, or `undefined`.
90
+ */
27
91
  export declare function determineAppFromPath(path: string): AppInfo | undefined;
92
+ /**
93
+ * Set the active tab's title.
94
+ *
95
+ * @param newTitle - Title shown on the tab strip.
96
+ */
28
97
  export declare const updateActiveTabTitle: (newTitle: string) => void;
29
98
  /**
30
99
  * Enable or disable single-tab mode.
@@ -36,8 +105,17 @@ export declare const updateActiveTabTitle: (newTitle: string) => void;
36
105
  * and disable it on unmount.
37
106
  */
38
107
  export declare const setSingleTabMode: (enabled: boolean) => void;
108
+ /**
109
+ * Open a tab, or activate an existing tab with the same package and path.
110
+ *
111
+ * @param tab - Tab contents. `tabId` is assigned here.
112
+ * @param forceNew - When true, always append a tab instead of reusing one.
113
+ */
39
114
  export declare const openNewTab: (tab: Omit<TabState, "tabId">, forceNew?: boolean) => void;
115
+ /** Close the active tab. The launcher tab stays open. */
40
116
  export declare const closeCurrentTab: () => void;
117
+ /** Activate the next open tab, wrapping around. */
41
118
  export declare const switchToNextTab: () => void;
119
+ /** Activate the previous open tab, wrapping around. */
42
120
  export declare const switchToPreviousTab: () => void;
43
121
  export {};
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.switchToPreviousTab = exports.switchToNextTab = exports.closeCurrentTab = exports.openNewTab = exports.setSingleTabMode = exports.updateActiveTabTitle = exports.handleMainPathChanged = exports.initializedTabs = exports.recentlyUsedApps = exports.activeTabId = exports.activeTabs = exports.launcherApp = void 0;
4
+ exports.navigateTo = navigateTo;
4
5
  exports.goToTabPath = goToTabPath;
6
+ exports.restoreNavigationState = restoreNavigationState;
7
+ exports.migrateRecentPath = migrateRecentPath;
5
8
  exports.determineAppFromPath = determineAppFromPath;
6
9
  const peers_sdk_1 = require("@peers-app/peers-sdk");
7
10
  const globals_1 = require("../globals");
11
+ const identity_routes_1 = require("../screens/identity/identity-routes");
8
12
  const system_apps_1 = require("../system-apps");
9
13
  const routes_loader_1 = require("../ui-router/routes-loader");
14
+ /** The app launcher tab. It is always present and cannot be closed. */
10
15
  exports.launcherApp = {
11
16
  tabId: "launcher",
12
17
  packageId: "launcher",
@@ -24,109 +29,163 @@ const _persistentActiveTabId = (0, peers_sdk_1.groupDeviceVar)("activeTabId", {
24
29
  const _persistentRecentlyUsedApps = (0, peers_sdk_1.groupUserVar)("recentlyUsedApps", {
25
30
  defaultValue: [],
26
31
  });
32
+ let finishNavigationRestore = () => { };
33
+ const navigationReady = new Promise((resolve) => {
34
+ finishNavigationRestore = resolve;
35
+ });
27
36
  // In-memory observables for UI state (loaded once, then write-through to persistent vars)
37
+ /** Open tabs. Writes through to device storage after the initial restore. */
28
38
  exports.activeTabs = (() => {
29
39
  const obs = (0, peers_sdk_1.observable)([exports.launcherApp]);
30
40
  // Write-through to persistent var on change
31
41
  obs.subscribe((value) => {
32
42
  _persistentActiveTabs(value);
33
43
  });
34
- // Load initial value once
35
- const loadingPromise = _persistentActiveTabs.loadingPromise.then(() => {
36
- obs(_persistentActiveTabs());
37
- });
38
- return Object.assign(obs, { loadingPromise });
44
+ return Object.assign(obs, { loadingPromise: navigationReady });
39
45
  })();
46
+ /** Id of the tab whose path is showing. */
40
47
  exports.activeTabId = (() => {
41
48
  const obs = (0, peers_sdk_1.observable)("launcher");
42
49
  // Write-through to persistent var on change
43
50
  obs.subscribe((value) => {
44
51
  _persistentActiveTabId(value);
45
52
  });
46
- // Load initial value once
47
- const loadingPromise = _persistentActiveTabId.loadingPromise.then(() => {
48
- obs(_persistentActiveTabId());
49
- });
50
- return Object.assign(obs, { loadingPromise });
53
+ return Object.assign(obs, { loadingPromise: navigationReady });
51
54
  })();
55
+ /** App roots recently opened from the launcher, newest first. */
52
56
  exports.recentlyUsedApps = (() => {
53
57
  const obs = (0, peers_sdk_1.observable)([]);
54
58
  // Write-through to persistent var on change
55
59
  obs.subscribe((value) => {
56
60
  _persistentRecentlyUsedApps(value);
57
61
  });
58
- // Load initial value once
59
- const loadingPromise = _persistentRecentlyUsedApps.loadingPromise.then(() => {
60
- obs(_persistentRecentlyUsedApps());
61
- });
62
- return Object.assign(obs, { loadingPromise });
62
+ return Object.assign(obs, { loadingPromise: navigationReady });
63
63
  })();
64
- exports.initializedTabs = new Set();
65
- function goToTabPath(path) {
66
- const tab = (0, exports.activeTabs)().find((t) => t.path === path);
67
- if (tab) {
68
- (0, exports.activeTabId)(tab.tabId);
64
+ void Promise.all([
65
+ _persistentActiveTabs.loadingPromise,
66
+ _persistentActiveTabId.loadingPromise,
67
+ _persistentRecentlyUsedApps.loadingPromise,
68
+ ]).then(() => {
69
+ const restored = restoreNavigationState(_persistentActiveTabs(), _persistentActiveTabId());
70
+ (0, exports.activeTabs)(restored.tabs);
71
+ if ((0, exports.activeTabId)() !== restored.activeId) {
72
+ (0, exports.activeTabId)(restored.activeId);
69
73
  }
70
- else {
71
- const appInfo = determineAppFromPath(path);
72
- if (appInfo) {
73
- (0, exports.openNewTab)({
74
- path: path,
75
- title: appInfo.navItem.displayName || appInfo.navItem.name,
76
- packageId: appInfo.package.packageId,
77
- iconClassName: appInfo.navItem.iconClassName,
78
- });
74
+ (0, exports.recentlyUsedApps)(migrateRecentPaths(_persistentRecentlyUsedApps()));
75
+ finishNavigationRestore();
76
+ });
77
+ /** Tab ids whose bodies have been mounted at least once. */
78
+ exports.initializedTabs = new Set();
79
+ /**
80
+ * Open a path in the tab that owns its app.
81
+ *
82
+ * Legacy identity aliases are rewritten first. A route owned by the current
83
+ * app replaces that tab's path. A route owned by another open app activates
84
+ * that app's tab. Otherwise a new tab is opened.
85
+ *
86
+ * @param rawPath - Requested route, with or without a query string.
87
+ * @param options - App-root behavior. Launcher opens preserve the current section.
88
+ */
89
+ function navigateTo(rawPath, options) {
90
+ const requestedPathname = (0, identity_routes_1.parseRoute)(rawPath).pathname;
91
+ if (!requestedPathname) {
92
+ const launcher = (0, exports.activeTabs)().find((tab) => tab.tabId === "launcher") ?? (0, exports.activeTabs)()[0];
93
+ if (launcher && (0, exports.activeTabId)() !== launcher.tabId) {
94
+ (0, exports.activeTabId)(launcher.tabId);
79
95
  }
80
- else {
81
- // No app matched — navigate by updating the active tab's path (e.g. deep link).
82
- // Must replace the tab object (immutable): TabContentRenderer is memoized on tab.path,
83
- // and mutating in place makes prev/next props compare equal so Router never re-renders.
84
- const currentTabId = (0, exports.activeTabId)();
85
- const currentTabs = (0, exports.activeTabs)();
86
- const idx = currentTabs.findIndex((t) => t.tabId === currentTabId);
87
- if (idx >= 0 && currentTabs[idx].path !== "") {
88
- const prev = currentTabs[idx];
89
- const newTabs = [...currentTabs];
90
- newTabs[idx] = { ...prev, path };
91
- (0, exports.activeTabs)(newTabs);
92
- if (prev.packageId !== "launcher") {
93
- (0, globals_1._mainContentPath)(path);
94
- }
95
- }
96
- else {
97
- (0, exports.openNewTab)({
98
- path: path,
99
- title: "New Tab",
100
- packageId: "system-apps",
101
- iconClassName: "bi-question-circle",
102
- });
96
+ return;
97
+ }
98
+ const next = (0, identity_routes_1.canonicalizeRoute)(rawPath);
99
+ const app = determineAppFromPath(next);
100
+ if (options?.preserveSectionOnAppRoot && app && isAppRootRequest(requestedPathname, app)) {
101
+ const existing = findTabForApp(app);
102
+ if (existing) {
103
+ if ((0, exports.activeTabId)() !== existing.tabId) {
104
+ (0, exports.activeTabId)(existing.tabId);
103
105
  }
106
+ rememberRecentApp(existing.path);
107
+ return;
104
108
  }
105
109
  }
106
- }
107
- const handleMainPathChanged = (oldPath, newPath, setNewMainPath) => {
108
- const oldPathPart = oldPath.toLowerCase().trim().split("?")[0];
109
- const newPathPart = newPath.toLowerCase().trim().split("?")[0];
110
- const currentTab = (0, exports.activeTabs)().find((t) => t.tabId === (0, exports.activeTabId)());
111
- if (oldPathPart === newPathPart || (currentTab && currentTab.path === newPath)) {
112
- // nothing to do
113
- setNewMainPath(newPath);
110
+ if (!app || app.package.packageId === "launcher") {
111
+ openUnresolved(next);
114
112
  return;
115
113
  }
116
- const appInfo = determineAppFromPath(newPath);
117
- if (appInfo && oldPath !== "") {
118
- (0, exports.openNewTab)({
119
- path: newPath,
120
- title: appInfo.navItem.displayName || appInfo.navItem.name,
121
- packageId: appInfo.package.packageId,
122
- iconClassName: appInfo.navItem.iconClassName,
123
- });
114
+ const current = (0, exports.activeTabs)().find((tab) => tab.tabId === (0, exports.activeTabId)());
115
+ const currentApp = current ? determineAppFromPath(current.path) : undefined;
116
+ if (current && current.packageId !== "launcher" && currentApp && sameApp(currentApp, app)) {
117
+ showTab(current.tabId, next);
118
+ return;
124
119
  }
125
- else {
126
- setNewMainPath(newPath);
120
+ const existing = findTabForApp(app);
121
+ if (existing) {
122
+ showTab(existing.tabId, next);
123
+ return;
127
124
  }
125
+ (0, exports.openNewTab)({
126
+ path: next,
127
+ title: app.navItem.displayName || app.navItem.name,
128
+ packageId: app.package.packageId,
129
+ iconClassName: app.navItem.iconClassName,
130
+ });
131
+ }
132
+ /**
133
+ * Open an app or path from the launcher.
134
+ *
135
+ * Choosing an app that is already open keeps the section the user was on.
136
+ *
137
+ * @param path - App root or deep link.
138
+ */
139
+ function goToTabPath(path) {
140
+ navigateTo(path, { preserveSectionOnAppRoot: true });
141
+ }
142
+ /**
143
+ * Apply a {@link mainContentPath} write to the open tabs.
144
+ *
145
+ * @param _oldPath - Previous route. Retained for existing call sites.
146
+ * @param newPath - Requested route.
147
+ * @param _setNewMainPath - Direct path setter. Navigation updates the path itself.
148
+ */
149
+ const handleMainPathChanged = (_oldPath, newPath, _setNewMainPath) => {
150
+ navigateTo(newPath);
128
151
  };
129
152
  exports.handleMainPathChanged = handleMainPathChanged;
153
+ /**
154
+ * Rewrite stored tabs onto canonical routes.
155
+ *
156
+ * Contacts, Groups, and Devices tabs from before the Identity app become one
157
+ * Identity tab. The active tab is kept when it is one of those duplicates.
158
+ *
159
+ * @param tabs - Persisted tabs.
160
+ * @param activeId - Persisted active tab id.
161
+ */
162
+ function restoreNavigationState(tabs, activeId) {
163
+ const migrated = (tabs.length > 0 ? tabs : [exports.launcherApp]).map(migrateStoredTab);
164
+ const merged = mergeIdentityTabs(migrated, activeId);
165
+ const activeExists = merged.tabs.some((tab) => tab.tabId === merged.activeId);
166
+ return {
167
+ tabs: merged.tabs,
168
+ activeId: activeExists ? merged.activeId : merged.tabs[0].tabId,
169
+ };
170
+ }
171
+ /**
172
+ * Map a stored recent-app path onto the current app root.
173
+ *
174
+ * @param path - Persisted recent path, possibly a legacy alias.
175
+ */
176
+ function migrateRecentPath(path) {
177
+ const canonical = (0, identity_routes_1.canonicalizeRoute)(path);
178
+ if ((0, identity_routes_1.isIdentityPath)(canonical)) {
179
+ return identity_routes_1.IDENTITY_APP_PATH;
180
+ }
181
+ return canonical;
182
+ }
183
+ /**
184
+ * Resolve the app that owns a route.
185
+ *
186
+ * @param path - Route, with or without a query string.
187
+ * @returns The matching app, the launcher for an empty path, or `undefined`.
188
+ */
130
189
  function determineAppFromPath(path) {
131
190
  const launcherInfo = {
132
191
  navItem: {
@@ -192,7 +251,123 @@ function determineAppFromPath(path) {
192
251
  console.warn(`No app found for path: ${path}`);
193
252
  return undefined;
194
253
  }
195
- // Utility function for detail screens to update the active tab's title
254
+ function appKey(app) {
255
+ const nav = app.navItem.navigationPath || app.navItem.name.toLowerCase();
256
+ return `${app.package.packageId}:${nav}`;
257
+ }
258
+ function sameApp(left, right) {
259
+ return appKey(left) === appKey(right);
260
+ }
261
+ function isAppRootRequest(pathname, app) {
262
+ const nav = (app.navItem.navigationPath || app.navItem.name).toLowerCase();
263
+ return pathname.toLowerCase() === nav;
264
+ }
265
+ function findTabForApp(app) {
266
+ const key = appKey(app);
267
+ return (0, exports.activeTabs)().find((tab) => {
268
+ const info = determineAppFromPath(tab.path);
269
+ return info ? appKey(info) === key : false;
270
+ });
271
+ }
272
+ /**
273
+ * Replace a tab's path immutably so the memoized tab body re-renders.
274
+ * Activating a different tab lets the tab subscription publish the path.
275
+ */
276
+ function showTab(tabId, path) {
277
+ const tabs = (0, exports.activeTabs)();
278
+ const index = tabs.findIndex((tab) => tab.tabId === tabId);
279
+ if (index < 0) {
280
+ return;
281
+ }
282
+ const current = tabs[index];
283
+ if (current.path !== path) {
284
+ const nextTabs = [...tabs];
285
+ nextTabs[index] = { ...current, path };
286
+ (0, exports.activeTabs)(nextTabs);
287
+ }
288
+ if ((0, exports.activeTabId)() !== tabId) {
289
+ (0, exports.activeTabId)(tabId);
290
+ }
291
+ else if ((0, globals_1._mainContentPath)() !== path) {
292
+ (0, globals_1._mainContentPath)(path);
293
+ }
294
+ rememberRecentApp(path);
295
+ }
296
+ function openUnresolved(path) {
297
+ const current = (0, exports.activeTabs)().find((tab) => tab.tabId === (0, exports.activeTabId)());
298
+ if (current && current.packageId === "system-apps") {
299
+ showTab(current.tabId, path);
300
+ return;
301
+ }
302
+ (0, exports.openNewTab)({
303
+ path,
304
+ title: "New Tab",
305
+ packageId: "system-apps",
306
+ iconClassName: "bi-question-circle",
307
+ });
308
+ }
309
+ function rememberRecentApp(path) {
310
+ const app = determineAppFromPath((0, identity_routes_1.canonicalizeRoute)(path));
311
+ if (!app || app.package.packageId === "launcher") {
312
+ return;
313
+ }
314
+ const recentPath = app.package.packageId === system_apps_1.systemPackage.packageId ? app.navItem.navigationPath || path : path;
315
+ if (!recentPath) {
316
+ return;
317
+ }
318
+ const recent = (0, exports.recentlyUsedApps)();
319
+ const filtered = recent.filter((entry) => entry !== recentPath);
320
+ const next = [recentPath, ...filtered].slice(0, 8);
321
+ if (next.length === recent.length && next.every((entry, index) => entry === recent[index])) {
322
+ return;
323
+ }
324
+ (0, exports.recentlyUsedApps)(next);
325
+ }
326
+ function migrateStoredTab(tab) {
327
+ const path = (0, identity_routes_1.canonicalizeRoute)(tab.path);
328
+ if (path === tab.path) {
329
+ return tab;
330
+ }
331
+ const app = determineAppFromPath(path);
332
+ const identity = app?.navItem.navigationPath === identity_routes_1.IDENTITY_APP_PATH;
333
+ return {
334
+ ...tab,
335
+ path,
336
+ packageId: app?.package.packageId ?? tab.packageId,
337
+ title: identity ? app?.navItem.displayName || app?.navItem.name || tab.title : tab.title,
338
+ iconClassName: identity ? app?.navItem.iconClassName || tab.iconClassName : tab.iconClassName,
339
+ };
340
+ }
341
+ function mergeIdentityTabs(tabs, activeId) {
342
+ const identityTabs = tabs.filter((tab) => (0, identity_routes_1.isIdentityPath)(tab.path));
343
+ if (identityTabs.length <= 1) {
344
+ return { tabs, activeId };
345
+ }
346
+ const keeper = identityTabs.find((tab) => tab.tabId === activeId) ?? identityTabs[0];
347
+ const remove = new Set(identityTabs.filter((tab) => tab.tabId !== keeper.tabId).map((tab) => tab.tabId));
348
+ return {
349
+ tabs: tabs.filter((tab) => !remove.has(tab.tabId)),
350
+ activeId: remove.has(activeId) ? keeper.tabId : activeId,
351
+ };
352
+ }
353
+ function migrateRecentPaths(paths) {
354
+ const seen = new Set();
355
+ const next = [];
356
+ for (const path of paths) {
357
+ const migrated = migrateRecentPath(path);
358
+ if (!migrated || seen.has(migrated)) {
359
+ continue;
360
+ }
361
+ seen.add(migrated);
362
+ next.push(migrated);
363
+ }
364
+ return next.slice(0, 8);
365
+ }
366
+ /**
367
+ * Set the active tab's title.
368
+ *
369
+ * @param newTitle - Title shown on the tab strip.
370
+ */
196
371
  const updateActiveTabTitle = (newTitle) => {
197
372
  // TODO Fix this to use a more reliable update mechanism
198
373
  // using setTimeout to get around a race condition for setting title on new tabs
@@ -222,7 +397,12 @@ const setSingleTabMode = (enabled) => {
222
397
  singleTabModeEnabled = enabled;
223
398
  };
224
399
  exports.setSingleTabMode = setSingleTabMode;
225
- // Global function to open content in new tab
400
+ /**
401
+ * Open a tab, or activate an existing tab with the same package and path.
402
+ *
403
+ * @param tab - Tab contents. `tabId` is assigned here.
404
+ * @param forceNew - When true, always append a tab instead of reusing one.
405
+ */
226
406
  const openNewTab = (tab, forceNew = false) => {
227
407
  const newTab = {
228
408
  ...tab,
@@ -255,15 +435,12 @@ const openNewTab = (tab, forceNew = false) => {
255
435
  (0, exports.activeTabs)([...currentTabs, newTab]);
256
436
  (0, exports.activeTabId)(newTab.tabId);
257
437
  }
258
- // Update recently used (track by path instead of packageId)
259
438
  if (tab.path && tab.packageId !== "launcher") {
260
- const recent = (0, exports.recentlyUsedApps)();
261
- const filtered = recent.filter((path) => path !== tab.path);
262
- (0, exports.recentlyUsedApps)([tab.path, ...filtered].slice(0, 8));
439
+ rememberRecentApp(tab.path);
263
440
  }
264
441
  };
265
442
  exports.openNewTab = openNewTab;
266
- // Close the current active tab (but not launcher)
443
+ /** Close the active tab. The launcher tab stays open. */
267
444
  const closeCurrentTab = () => {
268
445
  const currentTabs = (0, exports.activeTabs)();
269
446
  const _activeTabId = (0, exports.activeTabId)();
@@ -285,7 +462,7 @@ const closeCurrentTab = () => {
285
462
  }
286
463
  };
287
464
  exports.closeCurrentTab = closeCurrentTab;
288
- // Navigate to next tab
465
+ /** Activate the next open tab, wrapping around. */
289
466
  const switchToNextTab = () => {
290
467
  const currentTabs = (0, exports.activeTabs)();
291
468
  const activeTab = (0, exports.activeTabId)();
@@ -296,7 +473,7 @@ const switchToNextTab = () => {
296
473
  (0, exports.activeTabId)(currentTabs[nextIndex].tabId);
297
474
  };
298
475
  exports.switchToNextTab = switchToNextTab;
299
- // Navigate to previous tab
476
+ /** Activate the previous open tab, wrapping around. */
300
477
  const switchToPreviousTab = () => {
301
478
  const currentTabs = (0, exports.activeTabs)();
302
479
  const activeTab = (0, exports.activeTabId)();
@@ -0,0 +1 @@
1
+ export {};