@dxos/plugin-simple-layout 0.0.0 → 0.8.4-main.03d5cd7b56

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 (261) hide show
  1. package/dist/lib/neutral/SimpleLayoutPlugin.mjs +52 -0
  2. package/dist/lib/neutral/SimpleLayoutPlugin.mjs.map +7 -0
  3. package/dist/lib/neutral/app-graph-builder-EYQKLRRP.mjs +21 -0
  4. package/dist/lib/neutral/app-graph-builder-EYQKLRRP.mjs.map +7 -0
  5. package/dist/lib/neutral/capabilities/index.mjs +21 -0
  6. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  7. package/dist/lib/neutral/chunk-7UDV3JDT.mjs +22 -0
  8. package/dist/lib/neutral/chunk-7UDV3JDT.mjs.map +7 -0
  9. package/dist/lib/neutral/chunk-AMTEDJHG.mjs +19 -0
  10. package/dist/lib/neutral/chunk-AMTEDJHG.mjs.map +7 -0
  11. package/dist/lib/neutral/chunk-FD2CAY4Q.mjs +26 -0
  12. package/dist/lib/neutral/chunk-FD2CAY4Q.mjs.map +7 -0
  13. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  14. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  15. package/dist/lib/neutral/chunk-XVUAQHKU.mjs +8 -0
  16. package/dist/lib/neutral/chunk-XVUAQHKU.mjs.map +7 -0
  17. package/dist/lib/neutral/close-WKMURGUB.mjs +35 -0
  18. package/dist/lib/neutral/close-WKMURGUB.mjs.map +7 -0
  19. package/dist/lib/neutral/components/index.mjs +924 -0
  20. package/dist/lib/neutral/components/index.mjs.map +7 -0
  21. package/dist/lib/neutral/hooks/index.mjs +332 -0
  22. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  23. package/dist/lib/neutral/index.mjs +14 -0
  24. package/dist/lib/neutral/index.mjs.map +7 -0
  25. package/dist/lib/neutral/meta.json +1 -0
  26. package/dist/lib/neutral/meta.mjs +8 -0
  27. package/dist/lib/neutral/meta.mjs.map +7 -0
  28. package/dist/lib/neutral/open-XI2T7D5O.mjs +49 -0
  29. package/dist/lib/neutral/open-XI2T7D5O.mjs.map +7 -0
  30. package/dist/lib/neutral/operation-handler-EAIE7KPR.mjs +13 -0
  31. package/dist/lib/neutral/operation-handler-EAIE7KPR.mjs.map +7 -0
  32. package/dist/lib/neutral/operations/index.mjs +8 -0
  33. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  34. package/dist/lib/neutral/plugin.mjs +16 -0
  35. package/dist/lib/neutral/plugin.mjs.map +7 -0
  36. package/dist/lib/neutral/react-root-VE265VX4.mjs +18 -0
  37. package/dist/lib/neutral/react-root-VE265VX4.mjs.map +7 -0
  38. package/dist/lib/neutral/react-surface-REZMYKQV.mjs +46 -0
  39. package/dist/lib/neutral/react-surface-REZMYKQV.mjs.map +7 -0
  40. package/dist/lib/neutral/revert-workspace-ST6NZUNG.mjs +22 -0
  41. package/dist/lib/neutral/revert-workspace-ST6NZUNG.mjs.map +7 -0
  42. package/dist/lib/neutral/set-6ZRLWPJS.mjs +22 -0
  43. package/dist/lib/neutral/set-6ZRLWPJS.mjs.map +7 -0
  44. package/dist/lib/neutral/set-layout-mode-L22HRCKS.mjs +13 -0
  45. package/dist/lib/neutral/set-layout-mode-L22HRCKS.mjs.map +7 -0
  46. package/dist/lib/neutral/spotlight-dismiss-EIYW5E7M.mjs +58 -0
  47. package/dist/lib/neutral/spotlight-dismiss-EIYW5E7M.mjs.map +7 -0
  48. package/dist/lib/neutral/state-7NXKBLPY.mjs +47 -0
  49. package/dist/lib/neutral/state-7NXKBLPY.mjs.map +7 -0
  50. package/dist/lib/neutral/switch-workspace-PYWPTMFO.mjs +25 -0
  51. package/dist/lib/neutral/switch-workspace-PYWPTMFO.mjs.map +7 -0
  52. package/dist/lib/neutral/translations.mjs +36 -0
  53. package/dist/lib/neutral/translations.mjs.map +7 -0
  54. package/dist/lib/neutral/types/index.mjs +10 -0
  55. package/dist/lib/neutral/types/index.mjs.map +7 -0
  56. package/dist/lib/neutral/update-complementary-HKWF5OXT.mjs +33 -0
  57. package/dist/lib/neutral/update-complementary-HKWF5OXT.mjs.map +7 -0
  58. package/dist/lib/neutral/update-dialog-P4ASXCE7.mjs +30 -0
  59. package/dist/lib/neutral/update-dialog-P4ASXCE7.mjs.map +7 -0
  60. package/dist/lib/neutral/update-popover-REAKC2GN.mjs +34 -0
  61. package/dist/lib/neutral/update-popover-REAKC2GN.mjs.map +7 -0
  62. package/dist/lib/neutral/update-sidebar-O5SQPR6Q.mjs +12 -0
  63. package/dist/lib/neutral/update-sidebar-O5SQPR6Q.mjs.map +7 -0
  64. package/dist/lib/neutral/url-handler-GZXUUAHD.mjs +129 -0
  65. package/dist/lib/neutral/url-handler-GZXUUAHD.mjs.map +7 -0
  66. package/dist/types/src/SimpleLayoutPlugin.d.ts +8 -0
  67. package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -0
  68. package/dist/types/src/capabilities/app-graph-builder.d.ts +6 -0
  69. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  70. package/dist/types/src/capabilities/index.d.ts +22 -0
  71. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  72. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  73. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  74. package/dist/types/src/capabilities/react-root.d.ts +9 -0
  75. package/dist/types/src/capabilities/react-root.d.ts.map +1 -0
  76. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  77. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  78. package/dist/types/src/capabilities/spotlight-dismiss.d.ts +14 -0
  79. package/dist/types/src/capabilities/spotlight-dismiss.d.ts.map +1 -0
  80. package/dist/types/src/capabilities/state.d.ts +19 -0
  81. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  82. package/dist/types/src/capabilities/url-handler.d.ts +12 -0
  83. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -0
  84. package/dist/types/src/components/ContentError.stories.d.ts +46 -0
  85. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -0
  86. package/dist/types/src/components/DebugOverlay/DebugOverlay.d.ts +19 -0
  87. package/dist/types/src/components/DebugOverlay/DebugOverlay.d.ts.map +1 -0
  88. package/dist/types/src/components/DebugOverlay/index.d.ts +2 -0
  89. package/dist/types/src/components/DebugOverlay/index.d.ts.map +1 -0
  90. package/dist/types/src/components/Dialog/Dialog.d.ts +3 -0
  91. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -0
  92. package/dist/types/src/components/Dialog/index.d.ts +2 -0
  93. package/dist/types/src/components/Dialog/index.d.ts.map +1 -0
  94. package/dist/types/src/components/Home/Home.d.ts +7 -0
  95. package/dist/types/src/components/Home/Home.d.ts.map +1 -0
  96. package/dist/types/src/components/Home/index.d.ts +2 -0
  97. package/dist/types/src/components/Home/index.d.ts.map +1 -0
  98. package/dist/types/src/components/Loading/Loading.d.ts +3 -0
  99. package/dist/types/src/components/Loading/Loading.d.ts.map +1 -0
  100. package/dist/types/src/components/Loading/Loading.stories.d.ts +13 -0
  101. package/dist/types/src/components/Loading/Loading.stories.d.ts.map +1 -0
  102. package/dist/types/src/components/Loading/index.d.ts +2 -0
  103. package/dist/types/src/components/Loading/index.d.ts.map +1 -0
  104. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts +35 -0
  105. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts.map +1 -0
  106. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts +7 -0
  107. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -0
  108. package/dist/types/src/components/MobileLayout/index.d.ts +2 -0
  109. package/dist/types/src/components/MobileLayout/index.d.ts.map +1 -0
  110. package/dist/types/src/components/NavBranch/NavBranch.d.ts +11 -0
  111. package/dist/types/src/components/NavBranch/NavBranch.d.ts.map +1 -0
  112. package/dist/types/src/components/NavBranch/index.d.ts +2 -0
  113. package/dist/types/src/components/NavBranch/index.d.ts.map +1 -0
  114. package/dist/types/src/components/Popover/Popover.d.ts +4 -0
  115. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -0
  116. package/dist/types/src/components/Popover/index.d.ts +2 -0
  117. package/dist/types/src/components/Popover/index.d.ts.map +1 -0
  118. package/dist/types/src/components/SimpleLayout/AppBar.d.ts +24 -0
  119. package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -0
  120. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts +54 -0
  121. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts.map +1 -0
  122. package/dist/types/src/components/SimpleLayout/Drawer.d.ts +6 -0
  123. package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -0
  124. package/dist/types/src/components/SimpleLayout/Main.d.ts +6 -0
  125. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -0
  126. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +16 -0
  127. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -0
  128. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +49 -0
  129. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -0
  130. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts +3 -0
  131. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -0
  132. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +48 -0
  133. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -0
  134. package/dist/types/src/components/SimpleLayout/index.d.ts +5 -0
  135. package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -0
  136. package/dist/types/src/components/hooks.d.ts +7 -0
  137. package/dist/types/src/components/hooks.d.ts.map +1 -0
  138. package/dist/types/src/components/index.d.ts +8 -0
  139. package/dist/types/src/components/index.d.ts.map +1 -0
  140. package/dist/types/src/hooks/actions.d.ts +19 -0
  141. package/dist/types/src/hooks/actions.d.ts.map +1 -0
  142. package/dist/types/src/hooks/index.d.ts +7 -0
  143. package/dist/types/src/hooks/index.d.ts.map +1 -0
  144. package/dist/types/src/hooks/useAppBarProps.d.ts +7 -0
  145. package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -0
  146. package/dist/types/src/hooks/useCompanions.d.ts +12 -0
  147. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
  148. package/dist/types/src/hooks/useDrawerActions.d.ts +13 -0
  149. package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -0
  150. package/dist/types/src/hooks/useNavbarActions.d.ts +14 -0
  151. package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -0
  152. package/dist/types/src/hooks/useSimpleLayoutState.d.ts +7 -0
  153. package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -0
  154. package/dist/types/src/index.d.ts +3 -0
  155. package/dist/types/src/index.d.ts.map +1 -0
  156. package/dist/types/src/meta.d.ts +3 -0
  157. package/dist/types/src/meta.d.ts.map +1 -0
  158. package/dist/types/src/operations/close.d.ts +5 -0
  159. package/dist/types/src/operations/close.d.ts.map +1 -0
  160. package/dist/types/src/operations/index.d.ts +3 -0
  161. package/dist/types/src/operations/index.d.ts.map +1 -0
  162. package/dist/types/src/operations/open.d.ts +5 -0
  163. package/dist/types/src/operations/open.d.ts.map +1 -0
  164. package/dist/types/src/operations/revert-workspace.d.ts +5 -0
  165. package/dist/types/src/operations/revert-workspace.d.ts.map +1 -0
  166. package/dist/types/src/operations/set-layout-mode.d.ts +5 -0
  167. package/dist/types/src/operations/set-layout-mode.d.ts.map +1 -0
  168. package/dist/types/src/operations/set.d.ts +5 -0
  169. package/dist/types/src/operations/set.d.ts.map +1 -0
  170. package/dist/types/src/operations/state-access.d.ts +8 -0
  171. package/dist/types/src/operations/state-access.d.ts.map +1 -0
  172. package/dist/types/src/operations/switch-workspace.d.ts +5 -0
  173. package/dist/types/src/operations/switch-workspace.d.ts.map +1 -0
  174. package/dist/types/src/operations/update-complementary.d.ts +5 -0
  175. package/dist/types/src/operations/update-complementary.d.ts.map +1 -0
  176. package/dist/types/src/operations/update-dialog.d.ts +5 -0
  177. package/dist/types/src/operations/update-dialog.d.ts.map +1 -0
  178. package/dist/types/src/operations/update-popover.d.ts +5 -0
  179. package/dist/types/src/operations/update-popover.d.ts.map +1 -0
  180. package/dist/types/src/operations/update-sidebar.d.ts +5 -0
  181. package/dist/types/src/operations/update-sidebar.d.ts.map +1 -0
  182. package/dist/types/src/plugin.d.ts +4 -0
  183. package/dist/types/src/plugin.d.ts.map +1 -0
  184. package/dist/types/src/translations.d.ts +32 -0
  185. package/dist/types/src/translations.d.ts.map +1 -0
  186. package/dist/types/src/types/SimpleLayoutCapabilities.d.ts +38 -0
  187. package/dist/types/src/types/SimpleLayoutCapabilities.d.ts.map +1 -0
  188. package/dist/types/src/types/SimpleLayoutEvents.d.ts +4 -0
  189. package/dist/types/src/types/SimpleLayoutEvents.d.ts.map +1 -0
  190. package/dist/types/src/types/index.d.ts +3 -0
  191. package/dist/types/src/types/index.d.ts.map +1 -0
  192. package/dist/types/tsconfig.tsbuildinfo +1 -0
  193. package/package.json +94 -29
  194. package/src/SimpleLayoutPlugin.ts +39 -11
  195. package/src/capabilities/app-graph-builder.ts +21 -0
  196. package/src/capabilities/index.ts +13 -3
  197. package/src/capabilities/operation-handler.ts +14 -0
  198. package/src/capabilities/{react-root/react-root.tsx → react-root.tsx} +4 -4
  199. package/src/capabilities/react-surface.tsx +50 -0
  200. package/src/{hooks/useSpotlightDismiss.ts → capabilities/spotlight-dismiss.ts} +31 -40
  201. package/src/capabilities/state.tsx +51 -0
  202. package/src/capabilities/url-handler.ts +164 -0
  203. package/src/components/ContentError.stories.tsx +9 -8
  204. package/src/components/DebugOverlay/DebugOverlay.tsx +96 -0
  205. package/src/components/DebugOverlay/index.ts +5 -0
  206. package/src/components/Dialog/Dialog.tsx +26 -15
  207. package/src/components/Home/Home.tsx +80 -80
  208. package/src/components/{ContentLoading.stories.tsx → Loading/Loading.stories.tsx} +5 -5
  209. package/src/components/{ContentLoading.tsx → Loading/Loading.tsx} +2 -2
  210. package/src/components/Loading/index.ts +5 -0
  211. package/src/components/MobileLayout/MobileLayout.stories.tsx +133 -0
  212. package/src/components/MobileLayout/MobileLayout.tsx +372 -0
  213. package/src/components/MobileLayout/index.ts +5 -0
  214. package/src/components/NavBranch/NavBranch.tsx +124 -0
  215. package/src/components/NavBranch/index.ts +5 -0
  216. package/src/components/Popover/Popover.tsx +49 -27
  217. package/src/components/SimpleLayout/AppBar.stories.tsx +144 -0
  218. package/src/components/SimpleLayout/AppBar.tsx +94 -0
  219. package/src/components/SimpleLayout/Drawer.tsx +104 -0
  220. package/src/components/SimpleLayout/Main.tsx +54 -61
  221. package/src/components/SimpleLayout/NavBar.stories.tsx +164 -0
  222. package/src/components/SimpleLayout/NavBar.tsx +19 -83
  223. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +46 -63
  224. package/src/components/SimpleLayout/SimpleLayout.tsx +45 -8
  225. package/src/components/SimpleLayout/index.ts +3 -0
  226. package/src/components/hooks.ts +26 -0
  227. package/src/components/index.ts +4 -1
  228. package/src/hooks/actions.ts +86 -0
  229. package/src/hooks/index.ts +6 -1
  230. package/src/hooks/useAppBarProps.ts +95 -0
  231. package/src/hooks/useCompanions.ts +22 -0
  232. package/src/hooks/useDrawerActions.ts +100 -0
  233. package/src/hooks/useNavbarActions.ts +87 -0
  234. package/src/hooks/useSimpleLayoutState.ts +32 -0
  235. package/src/index.ts +2 -1
  236. package/src/meta.ts +2 -1
  237. package/src/operations/close.ts +34 -0
  238. package/src/operations/index.ts +16 -0
  239. package/src/operations/open.ts +63 -0
  240. package/src/operations/revert-workspace.ts +22 -0
  241. package/src/operations/set-layout-mode.ts +12 -0
  242. package/src/operations/set.ts +23 -0
  243. package/src/operations/state-access.ts +21 -0
  244. package/src/operations/switch-workspace.ts +26 -0
  245. package/src/operations/update-complementary.ts +35 -0
  246. package/src/operations/update-dialog.ts +28 -0
  247. package/src/operations/update-popover.ts +35 -0
  248. package/src/operations/update-sidebar.ts +12 -0
  249. package/src/plugin.ts +11 -0
  250. package/src/translations.ts +21 -13
  251. package/src/types/{capabilities.ts → SimpleLayoutCapabilities.ts} +19 -15
  252. package/src/types/SimpleLayoutEvents.ts +15 -0
  253. package/src/types/index.ts +2 -1
  254. package/src/capabilities/operation-resolver/index.ts +0 -10
  255. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -135
  256. package/src/capabilities/react-root/index.ts +0 -7
  257. package/src/capabilities/state/index.ts +0 -9
  258. package/src/capabilities/state/state.tsx +0 -60
  259. package/src/components/ContentError.tsx +0 -23
  260. package/src/components/SimpleLayout/Banner.tsx +0 -60
  261. package/src/components/SimpleLayout/NavBarstories.tsx +0 -59
@@ -0,0 +1,164 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Capabilities, Capability } from '@dxos/app-framework';
8
+ import { AppCapabilities, LayoutOperation, fromUrlPath, getWorkspaceFromPath, toUrlPath } from '@dxos/app-toolkit';
9
+ import { runAndForwardErrors } from '@dxos/effect';
10
+ import { log } from '@dxos/log';
11
+ import { isTauri } from '@dxos/util';
12
+
13
+ import { SimpleLayoutCapabilities } from '#types';
14
+
15
+ /**
16
+ * URL handler for simple layout that syncs browser URL with layout state.
17
+ * URL paths map directly to qualified graph IDs with the leading `root` segment stripped.
18
+ * Root is represented as `/`.
19
+ *
20
+ * On Tauri, also listens for deep links via the deep-link plugin.
21
+ */
22
+ export default Capability.makeModule(
23
+ Effect.fnUntraced(function* () {
24
+ const { invokePromise } = yield* Capability.get(Capabilities.OperationInvoker);
25
+ const capabilities = yield* Capability.Service;
26
+
27
+ /** Dispatch all NavigationHandler contributions with a given URL. */
28
+ const dispatchNavigationHandlers = (url: URL) =>
29
+ Effect.gen(function* () {
30
+ const handlers = yield* Capability.getAll(AppCapabilities.NavigationHandler);
31
+ yield* Effect.all(
32
+ handlers.map((handler) => handler(url)),
33
+ { concurrency: 'unbounded' },
34
+ );
35
+ }).pipe(Effect.provideService(Capability.Service, capabilities), runAndForwardErrors);
36
+
37
+ /**
38
+ * Handle navigation from a URL.
39
+ * Dispatches to NavigationHandler contributions, then handles pathname routing.
40
+ */
41
+ const handlePathNavigation = (url?: URL) => {
42
+ const resolvedUrl = url ?? new URL(window.location.href);
43
+ void dispatchNavigationHandlers(resolvedUrl);
44
+
45
+ let pathname = resolvedUrl.pathname;
46
+ if (isFilePath(pathname)) {
47
+ log.info('[UrlHandler] Skipping file path (not a graph node)', { pathname });
48
+ pathname = '/';
49
+ }
50
+
51
+ const qualifiedId = fromUrlPath(pathname);
52
+ const workspace = getWorkspaceFromPath(qualifiedId);
53
+
54
+ void invokePromise(LayoutOperation.SwitchWorkspace, { subject: workspace });
55
+
56
+ const activeId = qualifiedId !== workspace ? qualifiedId : undefined;
57
+ if (activeId) {
58
+ void invokePromise(LayoutOperation.Open, { subject: [activeId] });
59
+ }
60
+ };
61
+
62
+ const onPopState = () => {
63
+ handlePathNavigation();
64
+ };
65
+
66
+ // Initial navigation.
67
+ yield* Effect.sync(() => handlePathNavigation());
68
+ window.addEventListener('popstate', onPopState);
69
+
70
+ // Tauri deep link support.
71
+ let unlistenDeepLink: (() => void) | undefined;
72
+ if (isTauri()) {
73
+ yield* Effect.tryPromise({
74
+ try: async () => {
75
+ const { getCurrent, onOpenUrl } = await import('@tauri-apps/plugin-deep-link');
76
+
77
+ const launchUrls = await getCurrent();
78
+ if (launchUrls && launchUrls.length > 0) {
79
+ log.info('[UrlHandler] App launched with deep links', { urls: launchUrls });
80
+ for (const urlString of launchUrls) {
81
+ handleDeepLink(urlString, handlePathNavigation);
82
+ }
83
+ }
84
+
85
+ unlistenDeepLink = await onOpenUrl((urls) => {
86
+ log.info('[UrlHandler] Deep links received', { urls });
87
+ for (const urlString of urls) {
88
+ handleDeepLink(urlString, handlePathNavigation);
89
+ }
90
+ });
91
+ },
92
+ catch: (error) => {
93
+ log.warn('[UrlHandler] Failed to initialize deep link listener', { error });
94
+ return error;
95
+ },
96
+ }).pipe(Effect.catchAll(() => Effect.void));
97
+ }
98
+
99
+ // Sync URL with layout state changes.
100
+ let lastWorkspace: string | undefined;
101
+ let lastActive: string | undefined;
102
+ const unsubscribe = yield* Capabilities.subscribeAtom(
103
+ SimpleLayoutCapabilities.State,
104
+ (state: SimpleLayoutCapabilities.SimpleLayoutState) => {
105
+ const { workspace, active } = state;
106
+
107
+ if (workspace !== lastWorkspace || active !== lastActive) {
108
+ lastWorkspace = workspace;
109
+ lastActive = active;
110
+
111
+ const path = active ? toUrlPath(active) : toUrlPath(workspace);
112
+ if (window.location.pathname !== path) {
113
+ history.pushState(null, '', `${path}${window.location.search}`);
114
+ }
115
+ }
116
+ },
117
+ );
118
+
119
+ return Capability.contributes(Capabilities.Null, null, () =>
120
+ Effect.sync(() => {
121
+ window.removeEventListener('popstate', onPopState);
122
+ unsubscribe();
123
+ unlistenDeepLink?.();
124
+ }),
125
+ );
126
+ }),
127
+ );
128
+
129
+ /** Check if a path is a redirect path handled elsewhere (e.g., OAuth). */
130
+ const isRedirectPath = (pathname: string): boolean => pathname.startsWith('/redirect/');
131
+
132
+ /** Paths with file extensions are not graph node paths. */
133
+ const isFilePath = (pathname: string): boolean => /\.[a-z]+$/i.test(pathname);
134
+
135
+ /** Handle a deep link URL string. Merges query params into window.location and navigates. */
136
+ const handleDeepLink = (urlString: string, navigate: (url?: URL) => void): void => {
137
+ log.info('[UrlHandler] Deep link received', { url: urlString });
138
+ try {
139
+ const deepLinkUrl = new URL(urlString);
140
+
141
+ // For custom schemes (e.g., composer://a/b/c), new URL() treats the first segment as the
142
+ // hostname. Reconstruct the full path from hostname + pathname.
143
+ const fullPath =
144
+ deepLinkUrl.protocol !== 'https:' && deepLinkUrl.protocol !== 'http:' && deepLinkUrl.hostname
145
+ ? '/' + deepLinkUrl.hostname + deepLinkUrl.pathname
146
+ : deepLinkUrl.pathname;
147
+
148
+ if (isRedirectPath(fullPath)) {
149
+ return;
150
+ }
151
+
152
+ // Merge deep link query params into the current window URL so handlers can read them.
153
+ const current = new URL(window.location.href);
154
+ if (deepLinkUrl.search) {
155
+ deepLinkUrl.searchParams.forEach((value, key) => current.searchParams.set(key, value));
156
+ }
157
+ current.pathname = fullPath;
158
+ history.replaceState(null, '', current.pathname + current.search);
159
+
160
+ navigate(current);
161
+ } catch (error) {
162
+ log.warn('[UrlHandler] Failed to parse deep link URL', { urlString, error });
163
+ }
164
+ };
@@ -4,28 +4,29 @@
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
+ import { ErrorFallback } from '@dxos/react-ui';
7
8
  import { withTheme } from '@dxos/react-ui/testing';
8
9
 
9
- import { translations } from '../translations';
10
-
11
- import { ContentError } from './ContentError';
10
+ import { translations } from '#translations';
12
11
 
13
12
  const meta = {
14
- title: 'plugins/plugin-simple-layout/ContentError',
15
- component: ContentError,
16
- decorators: [withTheme],
13
+ title: 'plugins/plugin-simple-layout/components/ErrorFallback',
14
+ component: ErrorFallback,
15
+ decorators: [withTheme()],
17
16
  parameters: {
18
17
  layout: 'centered',
19
18
  translations,
20
19
  },
21
- } satisfies Meta<typeof ContentError>;
20
+ } satisfies Meta<typeof ErrorFallback>;
22
21
 
23
22
  export default meta;
24
23
 
25
24
  type Story = StoryObj<typeof meta>;
26
25
 
27
26
  export const Default: Story = {
28
- args: {},
27
+ args: {
28
+ error: new Error('An unexpected error occurred'),
29
+ },
29
30
  };
30
31
 
31
32
  export const WithError: Story = {
@@ -0,0 +1,96 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type PropsWithChildren, useCallback, useRef } from 'react';
7
+
8
+ const DEBUG_OVERLAY_NAME = 'DebugOverlay';
9
+
10
+ //
11
+ // Context
12
+ //
13
+
14
+ type DebugOverlayContextValue = {
15
+ /** Log a timestamped message to the on-screen debug overlay. */
16
+ dbg: (msg: string) => void;
17
+ };
18
+
19
+ // Default to a no-op so hooks can call useDebugLog() safely outside of a provider.
20
+ const [DebugOverlayProvider, useDebugLog] = createContext<DebugOverlayContextValue>(DEBUG_OVERLAY_NAME, {
21
+ dbg: () => {},
22
+ });
23
+
24
+ //
25
+ // Root
26
+ //
27
+
28
+ type DebugOverlayRootProps = PropsWithChildren<{
29
+ /**
30
+ * When true (default), renders the on-screen log panel.
31
+ * Set to false to suppress the overlay while keeping the context available.
32
+ */
33
+ enabled?: boolean;
34
+ maxLines?: number;
35
+ }>;
36
+
37
+ /**
38
+ * Establishes a debug overlay context.
39
+ *
40
+ * When enabled, renders an on-screen monospaced log panel anchored just above
41
+ * the keyboard (via --kb-height CSS variable). Descendants can call
42
+ * useDebugLog() to obtain the dbg() function for logging.
43
+ *
44
+ * Intended for transient mobile debugging in the iOS Simulator where DevTools
45
+ * console output may not be accessible.
46
+ */
47
+ const DebugOverlayRoot = ({ children, enabled = true, maxLines = 10 }: DebugOverlayRootProps) => {
48
+ const overlayRef = useRef<HTMLDivElement>(null);
49
+
50
+ const dbg = useCallback((msg: string) => {
51
+ if (!overlayRef.current) {
52
+ return;
53
+ }
54
+ const line = document.createElement('pre');
55
+ line.textContent = `${(performance.now() / 1000).toFixed(2).padStart(8, ' ')} ${msg}`;
56
+ overlayRef.current.prepend(line);
57
+ while (overlayRef.current.children.length > maxLines) {
58
+ overlayRef.current.lastChild?.remove();
59
+ }
60
+ }, []);
61
+
62
+ return (
63
+ <DebugOverlayProvider dbg={dbg}>
64
+ {children}
65
+ {enabled && (
66
+ <div
67
+ ref={overlayRef}
68
+ style={{
69
+ position: 'fixed',
70
+ bottom: 'calc(var(--kb-height, 0px) + 8px)',
71
+ left: 8,
72
+ right: 8,
73
+ background: 'rgba(0,0,0,0.8)',
74
+ color: '#0f0',
75
+ fontSize: 10,
76
+ fontFamily: 'monospace',
77
+ padding: 6,
78
+ borderRadius: 4,
79
+ zIndex: 9999,
80
+ pointerEvents: 'none',
81
+ }}
82
+ />
83
+ )}
84
+ </DebugOverlayProvider>
85
+ );
86
+ };
87
+
88
+ //
89
+ // Exports
90
+ //
91
+
92
+ export const DebugOverlay = {
93
+ Root: DebugOverlayRoot,
94
+ };
95
+
96
+ export { useDebugLog };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export { DebugOverlay, useDebugLog } from './DebugOverlay';
@@ -4,33 +4,44 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { Surface, useCapability } from '@dxos/app-framework/react';
7
+ import { Surface } from '@dxos/app-framework/ui';
8
+ import { AppSurface } from '@dxos/app-toolkit/ui';
8
9
  import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
10
+ import { ErrorFallback } from '@dxos/react-ui';
9
11
 
10
- import { SimpleLayoutState } from '../../types';
11
- import { ContentError } from '../ContentError';
12
+ import { useSimpleLayoutState } from '#hooks';
12
13
 
13
14
  export const Dialog = () => {
14
- const layout = useCapability(SimpleLayoutState);
15
+ const { state, updateState } = useSimpleLayoutState();
15
16
 
16
- const DialogRoot = layout.dialogType === 'alert' ? AlertDialog.Root : NaturalDialog.Root;
17
- const DialogOverlay = layout.dialogType === 'alert' ? AlertDialog.Overlay : NaturalDialog.Overlay;
17
+ const DialogRoot = state.dialogType === 'alert' ? AlertDialog.Root : NaturalDialog.Root;
18
+ const DialogOverlay = state.dialogType === 'alert' ? AlertDialog.Overlay : NaturalDialog.Overlay;
18
19
 
19
20
  return (
20
21
  <DialogRoot
21
- modal={layout.dialogBlockAlign !== 'end'}
22
- open={layout.dialogOpen}
23
- onOpenChange={(nextOpen) => (layout.dialogOpen = nextOpen)}
22
+ modal={state.dialogBlockAlign !== 'end'}
23
+ open={state.dialogOpen}
24
+ onOpenChange={(nextOpen) => updateState((state) => ({ ...state, dialogOpen: nextOpen }))}
24
25
  >
25
- {layout.dialogBlockAlign === 'end' ? (
26
- <Surface role='dialog' data={layout.dialogContent} limit={1} fallback={ContentError} placeholder={<div />} />
26
+ {state.dialogBlockAlign === 'end' ? (
27
+ <Surface.Surface
28
+ type={AppSurface.Dialog}
29
+ data={state.dialogContent ?? undefined}
30
+ limit={1}
31
+ fallback={ErrorFallback}
32
+ />
27
33
  ) : (
28
34
  <DialogOverlay
29
- blockAlign={layout.dialogBlockAlign}
30
- classNames={layout.dialogOverlayClasses}
31
- style={layout.dialogOverlayStyle}
35
+ blockAlign={state.dialogBlockAlign}
36
+ classNames={state.dialogOverlayClasses}
37
+ style={state.dialogOverlayStyle}
32
38
  >
33
- <Surface role='dialog' data={layout.dialogContent} limit={1} fallback={ContentError} />
39
+ <Surface.Surface
40
+ type={AppSurface.Dialog}
41
+ data={state.dialogContent ?? undefined}
42
+ limit={1}
43
+ fallback={ErrorFallback}
44
+ />
34
45
  </DialogOverlay>
35
46
  )}
36
47
  </DialogRoot>
@@ -4,135 +4,135 @@
4
4
 
5
5
  import React, { useCallback, useEffect, useMemo, useRef } from 'react';
6
6
 
7
- import { Common } from '@dxos/app-framework';
8
- import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
9
- import { Graph, Node, useConnections } from '@dxos/plugin-graph';
10
- import { Avatar, Icon, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
- import { Card } from '@dxos/react-ui-mosaic';
12
- import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
7
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
+ import { LayoutOperation } from '@dxos/app-toolkit';
9
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
10
+ import { Node, useConnections } from '@dxos/plugin-graph';
11
+ import { Avatar, Icon, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
12
+ import { Card } from '@dxos/react-ui';
13
+ import { Mosaic, type MosaicStackTileComponent } from '@dxos/react-ui-mosaic';
14
+ import { SearchPanel, useSearchListItem, useSearchListResults } from '@dxos/react-ui-search';
13
15
  import { mx } from '@dxos/ui-theme';
16
+ import { byPosition, getHostPlatform, isTauri } from '@dxos/util';
14
17
 
15
- import { meta } from '../../meta';
18
+ import { meta } from '#meta';
16
19
 
17
- type HomeProps = ThemedClassName;
20
+ import { useExpandPath } from '../hooks';
18
21
 
19
- export const Home = ({ classNames }: HomeProps) => {
22
+ export type HomeProps = {};
23
+
24
+ /**
25
+ * Home screen.
26
+ */
27
+ export const Home = (_: HomeProps) => {
20
28
  const { t } = useTranslation(meta.id);
21
- const workspaces = useWorkspaces();
22
- useLoadDescendents(Node.RootId);
29
+ const userAccountItem = useItemsByDisposition('user-account')[0];
30
+ const pinnedItems = useItemsByDisposition('pin-end', true);
31
+ const workspaceItems = useItemsByDisposition('workspace');
32
+ useExpandPath(Node.RootId);
33
+
34
+ const items = useMemo(
35
+ () => [...(userAccountItem ? [userAccountItem] : []), ...pinnedItems, ...workspaceItems],
36
+ [userAccountItem, pinnedItems, workspaceItems],
37
+ );
23
38
 
24
39
  const { results, handleSearch } = useSearchListResults({
25
- items: workspaces,
40
+ items,
26
41
  extract: (node) => toLocalizedString(node.properties.label, t),
27
42
  });
28
43
 
44
+ const autoFocus = !isTauri() || getHostPlatform() !== 'ios';
45
+
29
46
  return (
30
- <div className={mx('flex flex-col pli-3', classNames)}>
31
- {/* <div className='container-max-width'>{t('workspaces heading')}</div> */}
32
- <SearchList.Root onSearch={handleSearch} classNames='container-max-width'>
33
- <div className='plb-3'>
34
- <SearchList.Input placeholder={t('search placeholder')} autoFocus />
35
- </div>
36
- <SearchList.Content>
37
- <SearchList.Viewport classNames='flex flex-col gap-1'>
38
- {results.map((node) => (
39
- <Workspace key={node.id} node={node} />
40
- ))}
41
- </SearchList.Viewport>
42
- </SearchList.Content>
43
- </SearchList.Root>
44
- </div>
47
+ <SearchPanel onSearch={handleSearch}>
48
+ <Mosaic.Container asChild>
49
+ <ScrollArea.Root centered padding thin>
50
+ <ScrollArea.Viewport>
51
+ <Mosaic.Stack
52
+ classNames='gap-1'
53
+ draggable={false}
54
+ items={results}
55
+ getId={(item) => item.id}
56
+ Tile={WorkspaceTile}
57
+ />
58
+ </ScrollArea.Viewport>
59
+ </ScrollArea.Root>
60
+ </Mosaic.Container>
61
+ </SearchPanel>
45
62
  );
46
63
  };
47
64
 
48
- const Workspace = ({ node }: { node: Node.Node }) => {
65
+ const WorkspaceTile: MosaicStackTileComponent<Node.Node> = (props) => {
66
+ const data = props.data;
49
67
  const { t } = useTranslation(meta.id);
50
68
  const { invokePromise } = useOperationInvoker();
51
69
  const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
52
- const ref = useRef<HTMLDivElement>(null);
70
+ const name = toLocalizedString(data.properties.label, t);
71
+ const isSelected = selectedValue === data.id;
72
+ const cardRef = useRef<HTMLDivElement>(null);
73
+
74
+ useExpandPath(data.id);
53
75
 
54
76
  const handleSelect = useCallback(
55
- () => invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: node.id }),
56
- [invokePromise, node.id],
77
+ () => invokePromise(LayoutOperation.SwitchWorkspace, { subject: data.id }),
78
+ [invokePromise, data.id],
57
79
  );
58
80
 
59
- useLoadDescendents(node.id);
60
-
61
- const name = toLocalizedString(node.properties.label, t);
62
- const isSelected = selectedValue === node.id;
63
-
64
81
  // Register this workspace with the search context.
65
82
  useEffect(() => {
66
- if (ref.current) {
67
- registerItem(node.id, ref.current, handleSelect);
83
+ if (cardRef.current) {
84
+ registerItem(data.id, cardRef.current, handleSelect);
68
85
  }
69
86
 
70
- return () => unregisterItem(node.id);
71
- }, [node.id, handleSelect, registerItem, unregisterItem]);
87
+ return () => unregisterItem(data.id);
88
+ }, [data.id, handleSelect, registerItem, unregisterItem]);
72
89
 
73
90
  // Scroll into view when selected.
74
91
  useEffect(() => {
75
- if (isSelected && ref.current) {
76
- ref.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
92
+ if (isSelected && cardRef.current) {
93
+ cardRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
77
94
  }
78
95
  }, [isSelected]);
79
96
 
80
97
  return (
81
98
  <Card.Root
82
- ref={ref}
83
99
  role='button'
84
- tabIndex={-1}
100
+ fullWidth
101
+ tabIndex={-1} // TODO(burdon): Use Mosaic.Focus.
85
102
  data-selected={isSelected}
86
- classNames={mx('dx-focus-ring', isSelected && 'bg-hoverOverlay')}
103
+ classNames={mx('dx-focus-ring', isSelected && 'bg-hover-overlay')}
87
104
  onClick={handleSelect}
105
+ ref={cardRef}
88
106
  >
89
- <Card.Chrome classNames='grid grid-cols-[min-content_1fr_min-content] items-center gap-cardSpacingInline pie-cardSpacingInline'>
107
+ <Card.Toolbar density='fine'>
90
108
  <Avatar.Root>
91
109
  <Avatar.Content
92
- hue={node.properties.hue}
93
- icon={node.properties.icon}
94
- hueVariant='surface'
110
+ icon={data.properties.icon}
111
+ hue={data.properties.hue}
112
+ hueVariant='transparent'
95
113
  variant='square'
96
- size={12}
114
+ size={8}
97
115
  fallback={name}
98
116
  />
99
- <Avatar.Label>{name}</Avatar.Label>
117
+ <Avatar.Label classNames='cursor-pointer'>{name}</Avatar.Label>
100
118
  <Icon icon='ph--caret-right--regular' />
101
119
  </Avatar.Root>
102
- </Card.Chrome>
120
+ </Card.Toolbar>
103
121
  </Card.Root>
104
122
  );
105
123
  };
106
124
 
107
- const useLoadDescendents = (nodeId?: string) => {
108
- const { graph } = useAppGraph();
109
-
110
- useEffect(() => {
111
- const frame = requestAnimationFrame(() => {
112
- if (nodeId) {
113
- Graph.expand(graph, nodeId, 'outbound');
114
- Graph.getConnections(graph, nodeId, 'outbound').forEach((child) => {
115
- Graph.expand(graph, child.id, 'outbound');
116
- });
117
- }
118
- });
119
-
120
- return () => cancelAnimationFrame(frame);
121
- }, [nodeId, graph]);
125
+ /** Filters nodes by disposition. */
126
+ const filterItems = (node: Node.Node, disposition: string) => {
127
+ return node.properties.disposition === disposition;
122
128
  };
123
129
 
124
- const useWorkspaces = () => {
130
+ /** Returns root-level items filtered by disposition. */
131
+ const useItemsByDisposition = (disposition: string, sort = false) => {
125
132
  const { graph } = useAppGraph();
126
-
127
- // Get root connections to find collections.
128
- const rootConnections = useConnections(graph, Node.RootId);
129
- const collections = useMemo(
130
- () => rootConnections.filter((node) => node.properties.disposition === 'collection'),
131
- [rootConnections],
132
- );
133
-
134
- // Get first collection's children as workspaces.
135
- // TODO(wittjosiah): Support multiple collections or nested workspaces if needed.
136
- const firstCollection = collections[0];
137
- return useConnections(graph, firstCollection?.id);
133
+ const connections = useConnections(graph, Node.RootId, 'child');
134
+ return useMemo(() => {
135
+ const filtered = connections.filter((node) => filterItems(node, disposition));
136
+ return sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
137
+ }, [connections, disposition, sort]);
138
138
  };
@@ -6,16 +6,16 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
7
  import { withTheme } from '@dxos/react-ui/testing';
8
8
 
9
- import { ContentLoading } from './ContentLoading';
9
+ import { Loading } from './Loading';
10
10
 
11
11
  const meta = {
12
- title: 'plugins/plugin-simple-layout/ContentLoading',
13
- component: ContentLoading,
14
- decorators: [withTheme],
12
+ title: 'plugins/plugin-simple-layout/components/Loading',
13
+ component: Loading,
14
+ decorators: [withTheme()],
15
15
  parameters: {
16
16
  layout: 'centered',
17
17
  },
18
- } satisfies Meta<typeof ContentLoading>;
18
+ } satisfies Meta<typeof Loading>;
19
19
 
20
20
  export default meta;
21
21
 
@@ -5,6 +5,6 @@
5
5
  import React from 'react';
6
6
 
7
7
  // TODO(burdon): Show skeleton: https://github.com/dxos/dxos/issues/8259
8
- export const ContentLoading = () => {
9
- return <div role='none' className='grid place-items-center attention-surface' />;
8
+ export const Loading = () => {
9
+ return <div className='grid place-items-center dx-attention-surface' />;
10
10
  };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './Loading';