@peers-app/peers-ui 0.27.3 → 0.27.4

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 (309) hide show
  1. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -1
  2. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  3. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  4. package/dist/screens/variables/variable-details.js +2 -1
  5. package/dist/tabs-layout/network-status-indicator.js +26 -12
  6. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  7. package/dist/tabs-layout/use-network-status.js +27 -4
  8. package/package.json +9 -3
  9. package/.claude/settings.local.json +0 -34
  10. package/.github/README.md +0 -52
  11. package/.github/workflows/delete-all-artifacts.yml +0 -49
  12. package/.github/workflows/publish.yml +0 -51
  13. package/babel.config.js +0 -7
  14. package/biome.json +0 -198
  15. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  16. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  17. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-settings.test.js +0 -93
  19. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  20. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  21. package/dist/components/access-level-field.test.d.ts +0 -1
  22. package/dist/components/access-level-field.test.js +0 -28
  23. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  24. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  25. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  26. package/dist/components/responsive-section-nav.test.js +0 -45
  27. package/dist/globals-hash.test.d.ts +0 -1
  28. package/dist/globals-hash.test.js +0 -16
  29. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  30. package/dist/hooks/use-observable-state.test.js +0 -64
  31. package/dist/hooks/use-observable.test.d.ts +0 -1
  32. package/dist/hooks/use-observable.test.js +0 -191
  33. package/dist/hooks/use-rows.test.d.ts +0 -1
  34. package/dist/hooks/use-rows.test.js +0 -175
  35. package/dist/hooks/use-tool.test.d.ts +0 -1
  36. package/dist/hooks/use-tool.test.js +0 -190
  37. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  38. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  39. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  40. package/dist/root-layout/root-layout-host.test.js +0 -86
  41. package/dist/screens/account/account-screen.test.d.ts +0 -1
  42. package/dist/screens/account/account-screen.test.js +0 -51
  43. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  44. package/dist/screens/assistants/assistant-config.test.js +0 -57
  45. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  46. package/dist/screens/contacts/contact-list.test.js +0 -48
  47. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  48. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  49. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  50. package/dist/screens/devices/devices-list.test.js +0 -53
  51. package/dist/screens/groups/group-list.test.d.ts +0 -1
  52. package/dist/screens/groups/group-list.test.js +0 -38
  53. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  54. package/dist/screens/identity/identity-routes.test.js +0 -51
  55. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-screen.test.js +0 -103
  57. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  58. package/dist/screens/invites/accept-invite.test.js +0 -103
  59. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  60. package/dist/screens/invites/activity-view.test.js +0 -108
  61. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  62. package/dist/screens/invites/invite-classify.test.js +0 -115
  63. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  64. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  65. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  67. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  69. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  70. package/dist/screens/packages/package-helpers.test.js +0 -53
  71. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  72. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  73. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  74. package/dist/screens/packages/packages-ipc.test.js +0 -43
  75. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  76. package/dist/screens/settings/settings-page.test.js +0 -19
  77. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  78. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  79. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  81. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  82. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  83. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  84. package/dist/tabs-layout/tabs-state.test.js +0 -110
  85. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  86. package/dist/tabs-layout/use-network-status.test.js +0 -45
  87. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  88. package/dist/ui-router/ui-loader.test.js +0 -91
  89. package/jest.config.js +0 -25
  90. package/release-guard.js +0 -10
  91. package/src/app-updates/app-update-controls.tsx +0 -73
  92. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  93. package/src/app-updates/app-updates-consumer.ts +0 -121
  94. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  95. package/src/app-updates/app-updates-settings.tsx +0 -136
  96. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  97. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  98. package/src/app-updates/use-app-update-status.ts +0 -16
  99. package/src/command-palette/command-palette-ui.tsx +0 -564
  100. package/src/command-palette/command-palette.ts +0 -242
  101. package/src/components/access-level-field.test.tsx +0 -32
  102. package/src/components/access-level-field.tsx +0 -68
  103. package/src/components/checkbox.tsx +0 -25
  104. package/src/components/group-switcher.tsx +0 -526
  105. package/src/components/input-date.tsx +0 -29
  106. package/src/components/input-number.tsx +0 -83
  107. package/src/components/input.tsx +0 -25
  108. package/src/components/inverse-lazy-list.tsx +0 -107
  109. package/src/components/io-schema-values.tsx +0 -103
  110. package/src/components/io-schema.tsx +0 -259
  111. package/src/components/lazy-list.tsx +0 -115
  112. package/src/components/list-screen.tsx +0 -126
  113. package/src/components/loading-indicator.tsx +0 -7
  114. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  115. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  116. package/src/components/markdown-editor/editor.tsx +0 -158
  117. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  118. package/src/components/markdown-editor/mention-node.ts +0 -204
  119. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  120. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  121. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  122. package/src/components/markdown-editor/theme.ts +0 -46
  123. package/src/components/markdown-editor/toolbar.tsx +0 -269
  124. package/src/components/markdown-with-mentions.tsx +0 -198
  125. package/src/components/message-logs/message-logs.tsx +0 -523
  126. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  127. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  128. package/src/components/messages/avatar.tsx +0 -171
  129. package/src/components/messages/channel-message-list.tsx +0 -189
  130. package/src/components/messages/channel-view.tsx +0 -92
  131. package/src/components/messages/message-compose.tsx +0 -192
  132. package/src/components/messages/message-display.tsx +0 -231
  133. package/src/components/messages/thread-message-list.tsx +0 -135
  134. package/src/components/qr-scanner.tsx +0 -180
  135. package/src/components/responsive-section-nav.test.tsx +0 -88
  136. package/src/components/responsive-section-nav.tsx +0 -92
  137. package/src/components/router.tsx +0 -175
  138. package/src/components/save-button.tsx +0 -134
  139. package/src/components/sortable-list.tsx +0 -130
  140. package/src/components/tabs.tsx +0 -76
  141. package/src/components/tooltip.tsx +0 -43
  142. package/src/components/trust-level-badge.tsx +0 -44
  143. package/src/components/trust-level-dropdown.tsx +0 -136
  144. package/src/components/typeahead.tsx +0 -357
  145. package/src/globals-hash.test.ts +0 -19
  146. package/src/globals.tsx +0 -226
  147. package/src/hooks/index.ts +0 -8
  148. package/src/hooks/use-host-contract.ts +0 -75
  149. package/src/hooks/use-observable-state.test.tsx +0 -101
  150. package/src/hooks/use-observable-state.ts +0 -29
  151. package/src/hooks/use-observable.test.tsx +0 -258
  152. package/src/hooks/use-observable.ts +0 -67
  153. package/src/hooks/use-on-screen.ts +0 -24
  154. package/src/hooks/use-promise.ts +0 -34
  155. package/src/hooks/use-rows.test.tsx +0 -196
  156. package/src/hooks/use-rows.ts +0 -120
  157. package/src/hooks/use-subscription.ts +0 -24
  158. package/src/hooks/use-tool.test.tsx +0 -225
  159. package/src/hooks/use-tool.ts +0 -116
  160. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  161. package/src/host-shell/host-shell-consumer.ts +0 -53
  162. package/src/index.tsx +0 -42
  163. package/src/mention-configs.ts +0 -185
  164. package/src/operator-console/operator-console-state.ts +0 -119
  165. package/src/operator-console/operator-console.tsx +0 -233
  166. package/src/peers-services-registration.ts +0 -6
  167. package/src/root-layout/global-overlays.ts +0 -50
  168. package/src/root-layout/layout-picker.tsx +0 -47
  169. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  170. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  171. package/src/root-layout/root-layout-host.test.tsx +0 -108
  172. package/src/root-layout/root-layout-host.tsx +0 -147
  173. package/src/root-layout/root-layout-registry.ts +0 -51
  174. package/src/root-layout/root-layout-state.ts +0 -55
  175. package/src/root-layout/root-layout.ts +0 -32
  176. package/src/screens/account/account-screen.test.tsx +0 -65
  177. package/src/screens/account/account-screen.tsx +0 -942
  178. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  179. package/src/screens/assistants/assistant-config.tsx +0 -121
  180. package/src/screens/assistants/assistant-details.tsx +0 -89
  181. package/src/screens/assistants/assistant-info.tsx +0 -66
  182. package/src/screens/assistants/assistant-list.tsx +0 -145
  183. package/src/screens/assistants/assistant-tools.tsx +0 -82
  184. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  185. package/src/screens/console-logs/log-display.tsx +0 -137
  186. package/src/screens/console-logs/log-filters.tsx +0 -64
  187. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  188. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  189. package/src/screens/contacts/contact-details.tsx +0 -123
  190. package/src/screens/contacts/contact-list.test.tsx +0 -59
  191. package/src/screens/contacts/contact-list.tsx +0 -286
  192. package/src/screens/contacts/index.ts +0 -2
  193. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  194. package/src/screens/data-explorer/index.ts +0 -6
  195. package/src/screens/data-explorer/query-executor.tsx +0 -520
  196. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  197. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  198. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  199. package/src/screens/device-pairing/index.ts +0 -2
  200. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  201. package/src/screens/devices/devices-list.test.tsx +0 -68
  202. package/src/screens/devices/devices-list.tsx +0 -365
  203. package/src/screens/devices/index.ts +0 -1
  204. package/src/screens/groups/group-details.tsx +0 -160
  205. package/src/screens/groups/group-invite-panel.tsx +0 -311
  206. package/src/screens/groups/group-list.test.tsx +0 -44
  207. package/src/screens/groups/group-list.tsx +0 -317
  208. package/src/screens/groups/group-members.tsx +0 -343
  209. package/src/screens/groups/index.ts +0 -3
  210. package/src/screens/identity/identity-routes.test.ts +0 -70
  211. package/src/screens/identity/identity-routes.ts +0 -301
  212. package/src/screens/identity/identity-screen.test.tsx +0 -138
  213. package/src/screens/identity/identity-screen.tsx +0 -282
  214. package/src/screens/identity/index.ts +0 -2
  215. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  216. package/src/screens/identity/profile-invite.tsx +0 -93
  217. package/src/screens/invites/accept-invite.test.tsx +0 -115
  218. package/src/screens/invites/accept-invite.tsx +0 -318
  219. package/src/screens/invites/activity-view.test.tsx +0 -127
  220. package/src/screens/invites/activity-view.tsx +0 -162
  221. package/src/screens/invites/index.ts +0 -6
  222. package/src/screens/invites/invite-capture.ts +0 -104
  223. package/src/screens/invites/invite-classify.test.ts +0 -129
  224. package/src/screens/invites/invite-share.tsx +0 -104
  225. package/src/screens/invites/invites-ipc.ts +0 -25
  226. package/src/screens/invites/pending-invites-list.tsx +0 -256
  227. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  228. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  229. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  230. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  231. package/src/screens/network-viewer/index.ts +0 -8
  232. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  233. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  234. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  235. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  236. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  237. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  238. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  239. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  240. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  241. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  242. package/src/screens/packages/local-folders-ipc.ts +0 -28
  243. package/src/screens/packages/package-details.tsx +0 -166
  244. package/src/screens/packages/package-helpers.test.ts +0 -57
  245. package/src/screens/packages/package-helpers.ts +0 -137
  246. package/src/screens/packages/package-info.tsx +0 -354
  247. package/src/screens/packages/package-list.tsx +0 -519
  248. package/src/screens/packages/package-network-panel.tsx +0 -82
  249. package/src/screens/packages/package-new-local.tsx +0 -100
  250. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  251. package/src/screens/packages/package-runtime-status.tsx +0 -68
  252. package/src/screens/packages/package-versions.tsx +0 -298
  253. package/src/screens/packages/packages-ipc.test.ts +0 -47
  254. package/src/screens/packages/packages-ipc.ts +0 -125
  255. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  256. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  257. package/src/screens/search/global-search.tsx +0 -406
  258. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  259. package/src/screens/settings/settings-page.test.tsx +0 -24
  260. package/src/screens/settings/settings-page.tsx +0 -426
  261. package/src/screens/setup-user.tsx +0 -355
  262. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  263. package/src/screens/tools/tool-access-ipc.ts +0 -27
  264. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  265. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  266. package/src/screens/tools/tool-details.tsx +0 -52
  267. package/src/screens/tools/tool-info.tsx +0 -45
  268. package/src/screens/tools/tool-list.tsx +0 -106
  269. package/src/screens/tools/tool-schema.tsx +0 -44
  270. package/src/screens/tools/tool-test-details.tsx +0 -89
  271. package/src/screens/tools/tool-test-list.tsx +0 -73
  272. package/src/screens/variables/variable-details.tsx +0 -289
  273. package/src/screens/variables/variable-list.tsx +0 -110
  274. package/src/screens/welcome-modal.tsx +0 -294
  275. package/src/screens/workflows/workflow-details.tsx +0 -115
  276. package/src/screens/workflows/workflow-info.tsx +0 -22
  277. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  278. package/src/screens/workflows/workflow-list.tsx +0 -117
  279. package/src/setupTests.ts +0 -38
  280. package/src/system-apps/assistants.app.ts +0 -7
  281. package/src/system-apps/console-logs.app.ts +0 -7
  282. package/src/system-apps/data-explorer.app.ts +0 -8
  283. package/src/system-apps/identity.app.ts +0 -9
  284. package/src/system-apps/index.ts +0 -77
  285. package/src/system-apps/mobile-settings.app.ts +0 -7
  286. package/src/system-apps/network-viewer.app.ts +0 -8
  287. package/src/system-apps/packages.app.ts +0 -7
  288. package/src/system-apps/search.app.ts +0 -8
  289. package/src/system-apps/settings.app.ts +0 -7
  290. package/src/system-apps/threads.app.ts +0 -7
  291. package/src/system-apps/tools.app.ts +0 -7
  292. package/src/system-apps/types.app.ts +0 -7
  293. package/src/system-apps/variables.app.ts +0 -7
  294. package/src/system-apps/workflows.app.ts +0 -7
  295. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  296. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  297. package/src/tabs-layout/tabs-layout.tsx +0 -857
  298. package/src/tabs-layout/tabs-state.test.ts +0 -152
  299. package/src/tabs-layout/tabs-state.ts +0 -587
  300. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  301. package/src/tabs-layout/use-network-status.ts +0 -68
  302. package/src/ui-defaults/index.ts +0 -2
  303. package/src/ui-defaults/list-screen.tsx +0 -94
  304. package/src/ui-defaults/markdown-field.tsx +0 -87
  305. package/src/ui-router/routes-loader.ts +0 -193
  306. package/src/ui-router/ui-loader.test.tsx +0 -108
  307. package/src/ui-router/ui-loader.tsx +0 -606
  308. package/src/utils.ts +0 -95
  309. package/tsconfig.json +0 -18
@@ -1,857 +0,0 @@
1
- import {
2
- getUserContext,
3
- type IAppNav,
4
- rpcServerCalls,
5
- type Subscription,
6
- } from "@peers-app/peers-sdk";
7
- import React, { Component, useEffect, useState } from "react";
8
- import { openCommandPalette } from "../command-palette/command-palette";
9
- import { GroupSwitcher } from "../components/group-switcher";
10
- import { Router } from "../components/router";
11
- import { isDesktop } from "../globals";
12
- import { useObservable } from "../hooks";
13
- import { isIdentityPath } from "../screens/identity/identity-routes";
14
- import { colorMode } from "../screens/settings/color-mode-dropdown";
15
- import { systemPackage } from "../system-apps";
16
- import { allPackages } from "../ui-router/routes-loader";
17
- import { UIRouter } from "../ui-router/ui-loader";
18
- import { TabsStripIndicators } from "./network-status-indicator";
19
- import {
20
- activeTabId,
21
- activeTabs,
22
- goToTabPath,
23
- initializedTabs,
24
- recentlyUsedApps,
25
- type TabState,
26
- } from "./tabs-state";
27
-
28
- // Error boundary that wraps each tab's content so a crash in one tab
29
- // doesn't take down the entire app.
30
- interface TabErrorBoundaryProps {
31
- tabTitle: string;
32
- tabId: string;
33
- children: React.ReactNode;
34
- }
35
-
36
- interface TabErrorBoundaryState {
37
- hasError: boolean;
38
- error: Error | null;
39
- }
40
-
41
- /**
42
- * Wraps a single tab's content so a crash in one tab doesn't take down the whole app.
43
- * Exported so other root layouts (e.g. the full-screen layout) can render a tab body with
44
- * the same crash isolation. Reset it across screens by giving it a `key`.
45
- */
46
- export class TabErrorBoundary extends Component<TabErrorBoundaryProps, TabErrorBoundaryState> {
47
- constructor(props: TabErrorBoundaryProps) {
48
- super(props);
49
- this.state = { hasError: false, error: null };
50
- }
51
-
52
- static getDerivedStateFromError(error: Error): TabErrorBoundaryState {
53
- return { hasError: true, error };
54
- }
55
-
56
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
57
- console.error(
58
- `TabErrorBoundary caught error in tab "${this.props.tabTitle}":`,
59
- error,
60
- errorInfo,
61
- );
62
- }
63
-
64
- render() {
65
- if (this.state.hasError) {
66
- return (
67
- <div style={{ padding: "24px", textAlign: "center" }}>
68
- <h4 style={{ marginBottom: "12px" }}>Something went wrong in "{this.props.tabTitle}"</h4>
69
- <p
70
- style={{
71
- opacity: 0.7,
72
- marginBottom: "16px",
73
- fontFamily: "monospace",
74
- fontSize: "13px",
75
- }}
76
- >
77
- {this.state.error?.message || "Unknown error"}
78
- </p>
79
- <button
80
- type="button"
81
- className="btn btn-outline-primary btn-sm"
82
- onClick={() => this.setState({ hasError: false, error: null })}
83
- >
84
- Try Again
85
- </button>
86
- </div>
87
- );
88
- }
89
- return this.props.children;
90
- }
91
- }
92
-
93
- /**
94
- * The tab UI body: the tab strip plus the active tab's content. Rendered as the body of
95
- * the default `tabs` root layout inside `RootLayoutHost` (which owns bootstrapping, the
96
- * command palette, and the welcome modal). Branches on `isDesktop` for mobile vs. desktop
97
- * chrome.
98
- *
99
- * @param fillContainer When `true`, the body fills its parent (`height: 100%`) instead of
100
- * sizing itself to the viewport. Set this when embedding the tabs UI inside another
101
- * layout that owns the vertical space (e.g. the `tabs-with-console` split layout);
102
- * leave it `false`/unset for the standalone `tabs` layout, which sizes to the viewport.
103
- */
104
- export function TabsLayoutInternal({ fillContainer = false }: { fillContainer?: boolean } = {}) {
105
- const [_colorMode] = useObservable(colorMode);
106
- const [tabs] = useObservable(activeTabs);
107
- const [activeTab] = useObservable(activeTabId);
108
- const [currentlyActiveGroupId, setCurrentlyActiveGroupId] = useState("");
109
- const [switchingGroup, setSwitchingGroup] = useState(false);
110
-
111
- useEffect(() => {
112
- let sub: Subscription;
113
- getUserContext().then((userContext) => {
114
- setCurrentlyActiveGroupId(userContext.currentlyActiveGroupId() || userContext.userId);
115
- sub = userContext.currentlyActiveGroupId.subscribe(async (_groupId) => {
116
- setCurrentlyActiveGroupId(userContext.currentlyActiveGroupId() || userContext.userId);
117
- setSwitchingGroup(true);
118
- await userContext.waitForActiveGroupPersistence();
119
- await rpcServerCalls.flushDatabases();
120
- window.location.reload();
121
- });
122
- });
123
- return () => {
124
- if (sub) {
125
- sub.dispose();
126
- }
127
- };
128
- }, []);
129
-
130
- useObservable(isDesktop);
131
- const isMobile = !isDesktop();
132
-
133
- const closeTab = (tabId: string) => {
134
- const currentTabs = activeTabs();
135
- const tabToClose = currentTabs.find((t) => t.tabId === tabId);
136
-
137
- if (!tabToClose || tabToClose.tabId === "launcher") return;
138
-
139
- const closedIndex = currentTabs.findIndex((t) => t.tabId === tabId);
140
- const newTabs = currentTabs.filter((t) => t.tabId !== tabId);
141
- activeTabs(newTabs);
142
- initializedTabs.delete(tabId);
143
-
144
- if (activeTab === tabId) {
145
- if (newTabs.length === 0) {
146
- activeTabId("launcher");
147
- } else if (closedIndex < newTabs.length) {
148
- activeTabId(newTabs[closedIndex].tabId);
149
- } else {
150
- activeTabId(newTabs[newTabs.length - 1].tabId);
151
- }
152
- }
153
- };
154
-
155
- const switchTab = (tabId: string) => {
156
- activeTabId(tabId);
157
- };
158
-
159
- return (
160
- <div
161
- key={currentlyActiveGroupId}
162
- className="d-flex flex-column"
163
- style={{ height: fillContainer ? "100%" : "calc(100vh - 25px)", overflow: "hidden" }}
164
- >
165
- {switchingGroup && (
166
- <div
167
- style={{
168
- position: "fixed",
169
- inset: 0,
170
- zIndex: 9999,
171
- display: "flex",
172
- alignItems: "center",
173
- justifyContent: "center",
174
- backgroundColor: _colorMode === "light" ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.85)",
175
- }}
176
- >
177
- <div className="text-center">
178
- <div className="spinner-border text-secondary mb-3" role="status" />
179
- <div className={`small ${_colorMode === "light" ? "text-muted" : "text-secondary"}`}>
180
- Switching group...
181
- </div>
182
- </div>
183
- </div>
184
- )}
185
- {/* Tabs Header */}
186
- <div
187
- className={`border-bottom ${_colorMode === "light" ? "bg-light" : "bg-dark"}`}
188
- style={{
189
- borderBottomWidth: "1px",
190
- borderBottomColor: _colorMode === "light" ? "#dee2e6" : "#495057",
191
- }}
192
- >
193
- {isMobile ? (
194
- <MobileTabsHeader
195
- tabs={tabs}
196
- activeTab={activeTab}
197
- onSwitch={switchTab}
198
- onClose={closeTab}
199
- colorMode={_colorMode}
200
- />
201
- ) : (
202
- <div className="d-flex align-items-center px-1" style={{ height: "36px" }}>
203
- {/* Group Switcher */}
204
- <GroupSwitcher colorMode={_colorMode} isMobile={false} />
205
-
206
- {/* Search Button */}
207
- <button
208
- type="button"
209
- className="btn btn-sm me-2 d-flex align-items-center"
210
- onClick={openCommandPalette}
211
- title="Search everything (Cmd+K)"
212
- style={{
213
- padding: "4px 8px",
214
- fontSize: "12px",
215
- borderRadius: "6px",
216
- border: "none",
217
- background: "transparent",
218
- color: _colorMode === "light" ? "#6c757d" : "#adb5bd",
219
- }}
220
- onMouseEnter={(e) => {
221
- e.currentTarget.style.backgroundColor =
222
- _colorMode === "light" ? "#f8f9fa" : "#495057";
223
- e.currentTarget.style.color = _colorMode === "light" ? "#0d6efd" : "#ffffff";
224
- }}
225
- onMouseLeave={(e) => {
226
- e.currentTarget.style.backgroundColor = "transparent";
227
- e.currentTarget.style.color = _colorMode === "light" ? "#6c757d" : "#adb5bd";
228
- }}
229
- >
230
- <i className="bi-search" style={{ fontSize: "14px" }} />
231
- </button>
232
-
233
- <div
234
- className="d-flex flex-grow-1 overflow-auto"
235
- style={{
236
- scrollbarWidth: "none",
237
- msOverflowStyle: "none",
238
- }}
239
- >
240
- {tabs.map((tab) => (
241
- <TabHeader
242
- key={tab.tabId}
243
- tab={tab}
244
- isActive={activeTab === tab.tabId}
245
- onSwitch={switchTab}
246
- onClose={closeTab}
247
- colorMode={_colorMode}
248
- />
249
- ))}
250
- </div>
251
- <TabsStripIndicators />
252
- </div>
253
- )}
254
- </div>
255
-
256
- {/* Tab Content */}
257
- <div className="flex-grow-1 overflow-hidden">
258
- {tabs.map((tab) => (
259
- <div
260
- key={tab.tabId}
261
- className={`h-100 ${activeTab === tab.tabId ? "d-block" : "d-none"}`}
262
- style={{ display: activeTab === tab.tabId ? "block" : "none" }}
263
- >
264
- <TabErrorBoundary tabTitle={tab.title} tabId={tab.tabId}>
265
- <TabContent tab={tab} isMobile={isMobile} isActive={activeTab === tab.tabId} />
266
- </TabErrorBoundary>
267
- </div>
268
- ))}
269
- </div>
270
- </div>
271
- );
272
- }
273
-
274
- interface MobileTabsHeaderProps {
275
- tabs: TabState[];
276
- activeTab: string;
277
- onSwitch: (tabId: string) => void;
278
- onClose: (tabId: string) => void;
279
- colorMode: string;
280
- }
281
-
282
- function MobileTabsHeader({
283
- tabs,
284
- activeTab,
285
- onSwitch,
286
- onClose,
287
- colorMode,
288
- }: MobileTabsHeaderProps) {
289
- const [showMenuDropdown, setShowMenuDropdown] = React.useState(false);
290
- const currentTab = tabs.find((t) => t.tabId === activeTab);
291
- const nonLauncherTabs = tabs.filter((t) => t.packageId !== "launcher");
292
-
293
- return (
294
- <div
295
- className="d-flex align-items-center justify-content-between px-2 position-relative"
296
- style={{ height: "36px" }}
297
- >
298
- {/* Left Side - Group Switcher */}
299
- <div className="d-flex align-items-center gap-1">
300
- <GroupSwitcher colorMode={colorMode} isMobile={true} />
301
- </div>
302
-
303
- {/* Center - Current Tab Display (absolutely positioned to center on screen) */}
304
- <div
305
- className="d-flex align-items-center justify-content-center position-absolute"
306
- style={{
307
- left: "50%",
308
- transform: "translateX(-50%)",
309
- pointerEvents: "none",
310
- maxWidth: "calc(100% - 140px)",
311
- }}
312
- >
313
- {currentTab?.iconClassName && currentTab.packageId !== "launcher" && (
314
- <i className={`${currentTab.iconClassName} me-2`} />
315
- )}
316
- <span className="fw-medium text-truncate small">{currentTab?.title || "Apps"}</span>
317
- </div>
318
-
319
- {/* Right Side - Hamburger Menu */}
320
- <div className="d-flex align-items-center gap-1">
321
- <TabsStripIndicators compact />
322
- <div className="dropdown">
323
- <button
324
- type="button"
325
- className={`btn btn-sm ${colorMode === "light" ? "btn-outline-dark" : "btn-outline-light"}`}
326
- onClick={() => setShowMenuDropdown(!showMenuDropdown)}
327
- style={{ minWidth: "36px" }}
328
- >
329
- <i className="bi-list" />
330
- {nonLauncherTabs.length > 0 && <span className="ms-1">{nonLauncherTabs.length}</span>}
331
- </button>
332
- {showMenuDropdown && (
333
- <div
334
- className={`dropdown-menu show position-absolute ${colorMode === "light" ? "" : "dropdown-menu-dark"}`}
335
- style={{
336
- right: 0,
337
- top: "100%",
338
- zIndex: 1000,
339
- minWidth: "280px",
340
- maxHeight: "calc(100vh - 60px)",
341
- overflowY: "auto",
342
- }}
343
- >
344
- {/* Apps Option */}
345
- <div
346
- className={`dropdown-item d-flex align-items-center ${activeTab === "launcher" ? "active" : ""}`}
347
- style={{ cursor: "pointer", padding: "14px 18px", fontSize: "17px" }}
348
- onClick={() => {
349
- onSwitch("launcher");
350
- setShowMenuDropdown(false);
351
- }}
352
- >
353
- <i className="bi-grid-3x3-gap me-3" style={{ fontSize: "20px" }} />
354
- <span>Apps</span>
355
- </div>
356
-
357
- {/* Search Option */}
358
- <div
359
- className="dropdown-item d-flex align-items-center"
360
- style={{ cursor: "pointer", padding: "14px 18px", fontSize: "17px" }}
361
- onClick={() => {
362
- openCommandPalette();
363
- setShowMenuDropdown(false);
364
- }}
365
- >
366
- <i className="bi-search me-3" style={{ fontSize: "20px" }} />
367
- <span>Search</span>
368
- </div>
369
-
370
- {/* Divider if there are open tabs */}
371
- {nonLauncherTabs.length > 0 && (
372
- <div className="dropdown-divider" style={{ margin: "8px 0" }} />
373
- )}
374
-
375
- {/* Open Tabs */}
376
- {nonLauncherTabs
377
- .slice()
378
- .reverse()
379
- .map((tab) => (
380
- <div
381
- key={tab.tabId}
382
- className={`dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? "active" : ""}`}
383
- style={{ cursor: "pointer", padding: "14px 18px", fontSize: "17px" }}
384
- onClick={() => {
385
- onSwitch(tab.tabId);
386
- setShowMenuDropdown(false);
387
- }}
388
- >
389
- <div className="d-flex align-items-center">
390
- {tab.iconClassName && (
391
- <i className={`${tab.iconClassName} me-3`} style={{ fontSize: "20px" }} />
392
- )}
393
- <span>{tab.title}</span>
394
- </div>
395
- <button
396
- type="button"
397
- className="btn btn-sm p-0 ms-3"
398
- style={{ width: "32px", height: "32px", fontSize: "20px" }}
399
- onClick={(e) => {
400
- e.stopPropagation();
401
- onClose(tab.tabId);
402
- }}
403
- >
404
- <i className="bi-x" />
405
- </button>
406
- </div>
407
- ))}
408
- </div>
409
- )}
410
- </div>
411
- </div>
412
-
413
- {/* Backdrop to close dropdown */}
414
- {showMenuDropdown && (
415
- <div
416
- className="position-fixed w-100 h-100"
417
- style={{ top: 0, left: 0, zIndex: 999 }}
418
- onClick={() => setShowMenuDropdown(false)}
419
- />
420
- )}
421
- </div>
422
- );
423
- }
424
-
425
- interface TabHeaderProps {
426
- tab: TabState;
427
- isActive: boolean;
428
- onSwitch: (tabId: string) => void;
429
- onClose: (tabId: string) => void;
430
- colorMode: string;
431
- }
432
-
433
- function TabHeader({ tab, isActive, onSwitch, onClose, colorMode }: TabHeaderProps) {
434
- const activeClass = isActive
435
- ? colorMode === "light"
436
- ? "bg-white"
437
- : "bg-secondary"
438
- : colorMode === "light"
439
- ? "bg-light"
440
- : "bg-dark";
441
-
442
- return (
443
- <div
444
- className={`d-flex align-items-center px-2 py-1 user-select-none ${activeClass}`}
445
- style={{
446
- cursor: "pointer",
447
- minWidth: "100px",
448
- maxWidth: "180px",
449
- height: "34px",
450
- fontSize: "13px",
451
- borderTop: isActive ? "2px solid #0d6efd" : "2px solid transparent",
452
- borderLeft: "1px solid transparent",
453
- borderRight: `1px solid ${colorMode === "light" ? "#dee2e6" : "#495057"}`,
454
- borderBottom: isActive
455
- ? colorMode === "light"
456
- ? "1px solid white"
457
- : "1px solid #343a40"
458
- : "none",
459
- marginBottom: isActive ? "-1px" : "0",
460
- borderRadius: isActive ? "6px 6px 0 0" : "0",
461
- transition: "all 0.15s ease",
462
- zIndex: isActive ? 1 : 0,
463
- boxShadow: isActive ? "0 2px 4px rgba(0,0,0,0.1)" : "none",
464
- }}
465
- onMouseEnter={(e) => {
466
- if (!isActive) {
467
- e.currentTarget.style.backgroundColor = colorMode === "light" ? "#f8f9fa" : "#495057";
468
- }
469
- }}
470
- onMouseLeave={(e) => {
471
- if (!isActive) {
472
- e.currentTarget.style.backgroundColor = colorMode === "light" ? "#f1f3f4" : "#343a40";
473
- }
474
- }}
475
- onClick={() => onSwitch(tab.tabId)}
476
- >
477
- {tab.iconClassName && <i className={`${tab.iconClassName} me-2`} />}
478
- <span className="text-truncate" style={{ maxWidth: "120px", fontSize: "12px" }}>
479
- {tab.title}
480
- </span>
481
- {tab.tabId !== "launcher" && (
482
- <button
483
- type="button"
484
- className="btn p-0 ms-1 opacity-75 hover-opacity-100"
485
- style={{
486
- width: "14px",
487
- height: "14px",
488
- fontSize: "11px",
489
- border: "none",
490
- background: "none",
491
- borderRadius: "2px",
492
- }}
493
- onMouseEnter={(e) => {
494
- e.currentTarget.style.backgroundColor = "rgba(0,0,0,0.1)";
495
- }}
496
- onMouseLeave={(e) => {
497
- e.currentTarget.style.backgroundColor = "transparent";
498
- }}
499
- onClick={(e) => {
500
- e.stopPropagation();
501
- onClose(tab.tabId);
502
- }}
503
- >
504
- <i className="bi-x" />
505
- </button>
506
- )}
507
- </div>
508
- );
509
- }
510
-
511
- interface TabContentProps {
512
- tab: TabState;
513
- isMobile: boolean;
514
- isActive: boolean;
515
- }
516
-
517
- /**
518
- * Renders a single tab's body, lazily initializing it on first activation and keeping it
519
- * mounted thereafter (`initializedTabs`). Exported so other root layouts (e.g. full-screen)
520
- * can render the active screen through the same routing/launcher path.
521
- */
522
- export function TabContent({ tab, isMobile, isActive }: TabContentProps) {
523
- // Only render content if this tab is active OR has been previously initialized
524
- const shouldRender = isActive || initializedTabs.has(tab.tabId);
525
-
526
- // Mark tab as initialized when it becomes active for the first time
527
- React.useEffect(() => {
528
- if (isActive) {
529
- initializedTabs.add(tab.tabId);
530
- }
531
- }, [isActive, tab.tabId]);
532
-
533
- if (!shouldRender) {
534
- return null;
535
- }
536
-
537
- return <TabContentRenderer tab={tab} isMobile={isMobile} />;
538
- }
539
-
540
- // Create individual tab content components that don't re-render
541
- const TabContentRenderer = React.memo(
542
- ({ tab, isMobile }: { tab: TabState; isMobile: boolean }) => {
543
- if (tab.packageId === "launcher") {
544
- return <AppLauncherTab isMobile={isMobile} />;
545
- }
546
-
547
- // System apps use the main Router (which reads from mainContentPath)
548
- // Package apps use UIRouter with explicit path
549
- const isSystemApp = tab.packageId === "system-apps";
550
- const identityApp = isSystemApp && isIdentityPath(tab.path);
551
-
552
- return (
553
- <div
554
- className={`h-100 ${identityApp ? "overflow-hidden" : "overflow-auto"} ${
555
- identityApp ? "" : isMobile ? "p-2" : "p-3"
556
- }`}
557
- >
558
- {isSystemApp ? (
559
- <Router path={tab.path} />
560
- ) : (
561
- <UIRouter path={tab.path} uiCategory="screen" props={{}} />
562
- )}
563
- </div>
564
- );
565
- },
566
- (prevProps, nextProps) => {
567
- // Only re-render if the tab or isMobile changed
568
- return (
569
- prevProps.tab.tabId === nextProps.tab.tabId &&
570
- prevProps.tab.path === nextProps.tab.path &&
571
- prevProps.isMobile === nextProps.isMobile
572
- );
573
- },
574
- );
575
-
576
- interface AppLauncherTabProps {
577
- isMobile: boolean;
578
- }
579
-
580
- // Interface for app items (nav items)
581
- interface AppItem {
582
- packageId: string;
583
- packageName: string;
584
- navItem: IAppNav;
585
- path: string;
586
- name: string;
587
- displayName: string;
588
- iconClassName: string;
589
- }
590
-
591
- function AppLauncherTab({ isMobile }: AppLauncherTabProps) {
592
- const [packages] = useObservable(allPackages);
593
- const [recentApps] = useObservable(recentlyUsedApps);
594
- const [searchTerm, setSearchTerm] = React.useState("");
595
-
596
- // Combine regular packages with system apps package
597
- const allPackages_ = [...packages, systemPackage];
598
-
599
- // Create app items from nav items in packages
600
- const allApps: AppItem[] = allPackages_
601
- .filter((p) => !p.disabled && p.appNavs && p.appNavs.length > 0)
602
- .flatMap((pkg) => {
603
- const navs = pkg.appNavs;
604
- if (!navs) return [];
605
- return navs.map((navItem) => {
606
- // Construct path - use direct path for system apps, package-nav for others
607
- let path: string;
608
- if (pkg.packageId === "system-apps") {
609
- // System apps use direct routing (like the original left bar)
610
- path = navItem.navigationPath ?? navItem.name.replace(/\s/g, "-").toLowerCase();
611
- } else {
612
- // Regular packages use package-nav format
613
- path = `package-nav/${pkg.packageId}/${(navItem.navigationPath ?? navItem.name).replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}`;
614
- while (path.includes("//")) {
615
- path = path.replace("//", "/");
616
- }
617
- }
618
-
619
- return {
620
- packageId: pkg.packageId,
621
- packageName: pkg.name,
622
- navItem,
623
- path,
624
- name: navItem.name,
625
- displayName: navItem.displayName || navItem.name,
626
- iconClassName: navItem.iconClassName || "bi-box-seam",
627
- };
628
- });
629
- });
630
-
631
- // Filter apps based on search term
632
- const filteredApps = searchTerm.trim()
633
- ? allApps.filter(
634
- (app) =>
635
- app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
636
- app.displayName.toLowerCase().includes(searchTerm.toLowerCase()),
637
- )
638
- : allApps;
639
-
640
- // Categorize filtered apps by package ID
641
- const systemApps = filteredApps.filter((app) => app.packageId === "system-apps");
642
- const userApps = filteredApps.filter((app) => app.packageId !== "system-apps");
643
-
644
- // Get recently used apps by matching paths (only show if they match search)
645
- const recentAppItems = recentApps
646
- .map((path) => filteredApps.find((app) => app.path === path))
647
- .filter(Boolean) as AppItem[];
648
-
649
- const openApp = (appItem: AppItem) => {
650
- // Check if this is the mobile-settings app and we're in React Native
651
- const w = window as Window & {
652
- __NATIVE_THEME?: unknown;
653
- ReactNativeWebView?: { postMessage: (message: string) => void };
654
- };
655
- if (appItem.path === "mobile-settings" && typeof w.__NATIVE_THEME !== "undefined") {
656
- if (w.ReactNativeWebView) {
657
- w.ReactNativeWebView.postMessage(
658
- JSON.stringify({
659
- type: "navigate",
660
- path: "mobile-settings",
661
- }),
662
- );
663
- } else {
664
- window.location.href = "peers://mobile-settings";
665
- }
666
- return;
667
- }
668
- goToTabPath(appItem.path);
669
- };
670
-
671
- return (
672
- <div
673
- className={`container-fluid ${isMobile ? "p-2" : "p-4"}`}
674
- style={{ maxHeight: "100%", overflowY: "auto" }}
675
- >
676
- {/* Search Bar */}
677
- <div className={isMobile ? "mb-3" : "mb-4"}>
678
- <div className="position-relative">
679
- <i
680
- className="bi-search position-absolute"
681
- style={{
682
- left: "12px",
683
- top: "50%",
684
- transform: "translateY(-50%)",
685
- color: "#6c757d",
686
- zIndex: 1,
687
- }}
688
- />
689
- <input
690
- type="text"
691
- className="form-control"
692
- placeholder="Search apps..."
693
- value={searchTerm}
694
- onChange={(e) => setSearchTerm(e.target.value)}
695
- style={{
696
- paddingLeft: "40px",
697
- fontSize: isMobile ? "14px" : "15px",
698
- }}
699
- />
700
- {searchTerm && (
701
- <button
702
- type="button"
703
- className="btn btn-sm position-absolute"
704
- style={{
705
- right: "8px",
706
- top: "50%",
707
- transform: "translateY(-50%)",
708
- border: "none",
709
- background: "none",
710
- color: "#6c757d",
711
- }}
712
- onClick={() => setSearchTerm("")}
713
- >
714
- <i className="bi-x" />
715
- </button>
716
- )}
717
- </div>
718
- </div>
719
-
720
- {/* Recently Used */}
721
- {recentAppItems.length > 0 && (
722
- <AppSection
723
- title="Recently Used"
724
- iconClassName="bi-clock"
725
- apps={recentAppItems}
726
- onOpenApp={openApp}
727
- isMobile={isMobile}
728
- />
729
- )}
730
-
731
- {/* User Apps */}
732
- {userApps.length > 0 && (
733
- <AppSection
734
- title="User Apps"
735
- iconClassName="bi-person"
736
- apps={userApps}
737
- onOpenApp={openApp}
738
- isMobile={isMobile}
739
- />
740
- )}
741
-
742
- {/* System Apps */}
743
- {systemApps.length > 0 && (
744
- <AppSection
745
- title="System Apps"
746
- iconClassName="bi-gear"
747
- apps={systemApps}
748
- onOpenApp={openApp}
749
- isMobile={isMobile}
750
- />
751
- )}
752
- </div>
753
- );
754
- }
755
-
756
- interface AppSectionProps {
757
- title: string;
758
- iconClassName: string;
759
- apps: AppItem[];
760
- onOpenApp: (appItem: AppItem) => void;
761
- isMobile: boolean;
762
- }
763
-
764
- function AppSection({ title, iconClassName, apps, onOpenApp, isMobile }: AppSectionProps) {
765
- return (
766
- <div className="mb-5">
767
- <h4 className={`${isMobile ? "mb-3 h5" : "mb-3"}`}>
768
- <i className={`${iconClassName} me-2`} />
769
- {title}
770
- </h4>
771
- <div
772
- className={`d-flex flex-wrap ${isMobile ? "gap-3" : "gap-4"}`}
773
- style={{ gap: isMobile ? "16px" : "20px" }}
774
- >
775
- {apps.map((app) => (
776
- <AppCard
777
- key={`${app.packageId}-${app.path}`}
778
- appItem={app}
779
- onOpenApp={onOpenApp}
780
- isMobile={isMobile}
781
- />
782
- ))}
783
- </div>
784
- </div>
785
- );
786
- }
787
-
788
- interface AppCardProps {
789
- appItem: AppItem;
790
- onOpenApp: (appItem: AppItem) => void;
791
- isMobile: boolean;
792
- }
793
-
794
- function AppCard({ appItem, onOpenApp, isMobile }: AppCardProps) {
795
- const [_colorMode] = useObservable(colorMode);
796
-
797
- const isDark = _colorMode === "dark";
798
-
799
- return (
800
- <button
801
- type="button"
802
- className="d-flex flex-column align-items-center text-center"
803
- style={{
804
- cursor: "pointer",
805
- width: isMobile ? "80px" : "90px",
806
- transition: "all 0.15s ease",
807
- background: "none",
808
- border: "none",
809
- padding: 0,
810
- }}
811
- title={appItem.name}
812
- onMouseEnter={(e) => {
813
- e.currentTarget.style.transform = "scale(1.05)";
814
- }}
815
- onMouseLeave={(e) => {
816
- e.currentTarget.style.transform = "scale(1)";
817
- }}
818
- onClick={() => onOpenApp(appItem)}
819
- >
820
- <div
821
- className="d-flex align-items-center justify-content-center mb-2"
822
- style={{
823
- width: isMobile ? "64px" : "72px",
824
- height: isMobile ? "64px" : "72px",
825
- backgroundColor: isDark ? "#343a40" : "#f8f9fa",
826
- borderRadius: "12px",
827
- border: `1px solid ${isDark ? "#495057" : "#e9ecef"}`,
828
- boxShadow: isDark ? "0 2px 4px rgba(0,0,0,0.3)" : "0 2px 4px rgba(0,0,0,0.08)",
829
- transition: "all 0.15s ease",
830
- }}
831
- >
832
- <i
833
- className={appItem.iconClassName}
834
- style={{
835
- fontSize: isMobile ? "28px" : "32px",
836
- color: isDark ? "#0d6efd" : "#0d6efd",
837
- }}
838
- />
839
- </div>
840
-
841
- <span
842
- className={isDark ? "text-light" : "text-dark"}
843
- style={{
844
- fontSize: isMobile ? "11px" : "12px",
845
- fontWeight: "500",
846
- lineHeight: "1.2",
847
- maxWidth: "100%",
848
- overflow: "hidden",
849
- textOverflow: "ellipsis",
850
- whiteSpace: "nowrap",
851
- }}
852
- >
853
- {appItem.displayName}
854
- </span>
855
- </button>
856
- );
857
- }