@dxos/plugin-simple-layout 0.8.4-main.52d7546f51 → 0.8.4-main.6fa680abb7

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 (119) hide show
  1. package/dist/lib/browser/{chunk-O3BQBYMW.mjs → chunk-MDPEKLKR.mjs} +181 -183
  2. package/dist/lib/browser/chunk-MDPEKLKR.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-7VLT3S46.mjs → chunk-MRR7PXSM.mjs} +3 -3
  4. package/dist/lib/browser/{chunk-7VLT3S46.mjs.map → chunk-MRR7PXSM.mjs.map} +1 -1
  5. package/dist/lib/browser/index.mjs +6 -6
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/{operation-resolver-BYRIQOQT.mjs → operation-resolver-VTZ6HZ4B.mjs} +24 -35
  8. package/dist/lib/browser/operation-resolver-VTZ6HZ4B.mjs.map +7 -0
  9. package/dist/lib/browser/{react-root-GPTKI5H2.mjs → react-root-WVQYY2JA.mjs} +3 -3
  10. package/dist/lib/browser/{react-surface-LT5JJTPR.mjs → react-surface-VLBR37ED.mjs} +11 -8
  11. package/dist/lib/browser/{react-surface-LT5JJTPR.mjs.map → react-surface-VLBR37ED.mjs.map} +3 -3
  12. package/dist/lib/browser/{state-A3PGDWWZ.mjs → state-TXSMUWYI.mjs} +2 -2
  13. package/dist/lib/browser/{url-handler-HTIUY6WL.mjs → url-handler-RBRONH7S.mjs} +18 -19
  14. package/dist/lib/browser/url-handler-RBRONH7S.mjs.map +7 -0
  15. package/dist/lib/node-esm/{chunk-UAWM4B2S.mjs → chunk-DCKASLMP.mjs} +181 -183
  16. package/dist/lib/node-esm/chunk-DCKASLMP.mjs.map +7 -0
  17. package/dist/lib/node-esm/{chunk-VIDE5UMB.mjs → chunk-WMNTJ2MK.mjs} +3 -3
  18. package/dist/lib/node-esm/{chunk-VIDE5UMB.mjs.map → chunk-WMNTJ2MK.mjs.map} +1 -1
  19. package/dist/lib/node-esm/index.mjs +6 -6
  20. package/dist/lib/node-esm/meta.json +1 -1
  21. package/dist/lib/node-esm/{operation-resolver-BDTFNCS2.mjs → operation-resolver-R7CQ6ERU.mjs} +24 -35
  22. package/dist/lib/node-esm/operation-resolver-R7CQ6ERU.mjs.map +7 -0
  23. package/dist/lib/node-esm/{react-root-GRG2OAI2.mjs → react-root-XBNDM7BE.mjs} +3 -3
  24. package/dist/lib/node-esm/{react-surface-TCUSDIN2.mjs → react-surface-U5NHA367.mjs} +11 -8
  25. package/dist/lib/node-esm/{react-surface-TCUSDIN2.mjs.map → react-surface-U5NHA367.mjs.map} +3 -3
  26. package/dist/lib/node-esm/{state-ZCFZTTPL.mjs → state-JMX6FAG4.mjs} +2 -2
  27. package/dist/lib/node-esm/{url-handler-WBVVKVPC.mjs → url-handler-QSMCH3JB.mjs} +18 -19
  28. package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs.map +7 -0
  29. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +1 -1
  30. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/react-root/react-root.d.ts +1 -1
  32. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +1 -1
  33. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -1
  34. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +1 -1
  35. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -1
  36. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +1 -1
  37. package/dist/types/src/capabilities/state/index.d.ts +1 -1
  38. package/dist/types/src/capabilities/state/state.d.ts +1 -1
  39. package/dist/types/src/capabilities/url-handler/url-handler.d.ts +3 -3
  40. package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -1
  41. package/dist/types/src/components/ContentError.stories.d.ts +1 -3
  42. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/ContentLoading/ContentLoading.d.ts.map +1 -0
  44. package/dist/types/src/components/ContentLoading/ContentLoading.stories.d.ts.map +1 -0
  45. package/dist/types/src/components/ContentLoading/index.d.ts +2 -0
  46. package/dist/types/src/components/ContentLoading/index.d.ts.map +1 -0
  47. package/dist/types/src/components/Home/Home.d.ts.map +1 -1
  48. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/NavBranch/NavBranch.d.ts +11 -0
  50. package/dist/types/src/components/NavBranch/NavBranch.d.ts.map +1 -0
  51. package/dist/types/src/components/NavBranch/index.d.ts +2 -0
  52. package/dist/types/src/components/NavBranch/index.d.ts.map +1 -0
  53. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  54. package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -1
  55. package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -1
  56. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
  57. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
  58. package/dist/types/src/components/hooks.d.ts +4 -2
  59. package/dist/types/src/components/hooks.d.ts.map +1 -1
  60. package/dist/types/src/components/index.d.ts +1 -1
  61. package/dist/types/src/hooks/actions.d.ts +3 -4
  62. package/dist/types/src/hooks/actions.d.ts.map +1 -1
  63. package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -1
  64. package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +29 -28
  68. package/src/capabilities/operation-resolver/operation-resolver.ts +19 -34
  69. package/src/capabilities/react-surface/react-surface.tsx +8 -6
  70. package/src/capabilities/url-handler/url-handler.ts +11 -35
  71. package/src/components/ContentError.stories.tsx +7 -6
  72. package/src/components/{ContentLoading.stories.tsx → ContentLoading/ContentLoading.stories.tsx} +1 -1
  73. package/src/components/{ContentLoading.tsx → ContentLoading/ContentLoading.tsx} +1 -1
  74. package/src/components/ContentLoading/index.ts +5 -0
  75. package/src/components/Dialog/Dialog.tsx +3 -3
  76. package/src/components/Home/Home.tsx +30 -24
  77. package/src/components/MobileLayout/MobileLayout.stories.tsx +23 -19
  78. package/src/components/MobileLayout/MobileLayout.tsx +2 -2
  79. package/src/components/{Workspace/Workspace.tsx → NavBranch/NavBranch.tsx} +43 -32
  80. package/src/components/{Workspace → NavBranch}/index.ts +1 -1
  81. package/src/components/Popover/Popover.tsx +14 -4
  82. package/src/components/SimpleLayout/AppBar.stories.tsx +2 -2
  83. package/src/components/SimpleLayout/AppBar.tsx +14 -21
  84. package/src/components/SimpleLayout/Drawer.tsx +15 -21
  85. package/src/components/SimpleLayout/Main.tsx +11 -10
  86. package/src/components/SimpleLayout/NavBar.stories.tsx +8 -8
  87. package/src/components/SimpleLayout/NavBar.tsx +4 -10
  88. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +2 -2
  89. package/src/components/SimpleLayout/SimpleLayout.tsx +1 -1
  90. package/src/components/hooks.ts +8 -8
  91. package/src/components/index.ts +1 -1
  92. package/src/hooks/actions.ts +15 -17
  93. package/src/hooks/useAppBarProps.ts +8 -5
  94. package/src/hooks/useCompanions.ts +1 -1
  95. package/src/hooks/useDrawerActions.ts +9 -7
  96. package/src/hooks/useNavbarActions.ts +10 -9
  97. package/src/meta.ts +1 -1
  98. package/src/types/capabilities.ts +1 -1
  99. package/dist/lib/browser/chunk-O3BQBYMW.mjs.map +0 -7
  100. package/dist/lib/browser/operation-resolver-BYRIQOQT.mjs.map +0 -7
  101. package/dist/lib/browser/url-handler-HTIUY6WL.mjs.map +0 -7
  102. package/dist/lib/node-esm/chunk-UAWM4B2S.mjs.map +0 -7
  103. package/dist/lib/node-esm/operation-resolver-BDTFNCS2.mjs.map +0 -7
  104. package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs.map +0 -7
  105. package/dist/types/src/components/ContentError.d.ts +0 -5
  106. package/dist/types/src/components/ContentError.d.ts.map +0 -1
  107. package/dist/types/src/components/ContentLoading.d.ts.map +0 -1
  108. package/dist/types/src/components/ContentLoading.stories.d.ts.map +0 -1
  109. package/dist/types/src/components/Workspace/Workspace.d.ts +0 -11
  110. package/dist/types/src/components/Workspace/Workspace.d.ts.map +0 -1
  111. package/dist/types/src/components/Workspace/index.d.ts +0 -2
  112. package/dist/types/src/components/Workspace/index.d.ts.map +0 -1
  113. package/src/components/ContentError.tsx +0 -23
  114. /package/dist/lib/browser/{react-root-GPTKI5H2.mjs.map → react-root-WVQYY2JA.mjs.map} +0 -0
  115. /package/dist/lib/browser/{state-A3PGDWWZ.mjs.map → state-TXSMUWYI.mjs.map} +0 -0
  116. /package/dist/lib/node-esm/{react-root-GRG2OAI2.mjs.map → react-root-XBNDM7BE.mjs.map} +0 -0
  117. /package/dist/lib/node-esm/{state-ZCFZTTPL.mjs.map → state-JMX6FAG4.mjs.map} +0 -0
  118. /package/dist/types/src/components/{ContentLoading.d.ts → ContentLoading/ContentLoading.d.ts} +0 -0
  119. /package/dist/types/src/components/{ContentLoading.stories.d.ts → ContentLoading/ContentLoading.stories.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-simple-layout",
3
- "version": "0.8.4-main.52d7546f51",
3
+ "version": "0.8.4-main.6fa680abb7",
4
4
  "description": "Simple layout plugin for minimal UI contexts like popover windows.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -14,9 +14,9 @@
14
14
  "type": "module",
15
15
  "exports": {
16
16
  ".": {
17
+ "source": "./src/index.ts",
17
18
  "browser": "./dist/lib/browser/index.mjs",
18
19
  "node": "./dist/lib/node-esm/index.mjs",
19
- "source": "./src/index.ts",
20
20
  "types": "./dist/types/src/index.d.ts"
21
21
  }
22
22
  },
@@ -31,19 +31,20 @@
31
31
  "@radix-ui/react-context": "1.1.1",
32
32
  "@tauri-apps/plugin-deep-link": "^2.2.0",
33
33
  "@tauri-apps/plugin-haptics": "^2.3.2",
34
- "@dxos/app-framework": "0.8.4-main.52d7546f51",
35
- "@dxos/async": "0.8.4-main.52d7546f51",
36
- "@dxos/log": "0.8.4-main.52d7546f51",
37
- "@dxos/operation": "0.8.4-main.52d7546f51",
38
- "@dxos/plugin-graph": "0.8.4-main.52d7546f51",
39
- "@dxos/react-ui-attention": "0.8.4-main.52d7546f51",
40
- "@dxos/react-ui-menu": "0.8.4-main.52d7546f51",
41
- "@dxos/react-ui-mosaic": "0.8.4-main.52d7546f51",
42
- "@dxos/react-ui-searchlist": "0.8.4-main.52d7546f51",
43
- "@dxos/schema": "0.8.4-main.52d7546f51",
44
- "@dxos/util": "0.8.4-main.52d7546f51",
45
- "@dxos/react-ui-stack": "0.8.4-main.52d7546f51",
46
- "@dxos/app-toolkit": "0.8.4-main.52d7546f51"
34
+ "@dxos/app-framework": "0.8.4-main.6fa680abb7",
35
+ "@dxos/async": "0.8.4-main.6fa680abb7",
36
+ "@dxos/app-toolkit": "0.8.4-main.6fa680abb7",
37
+ "@dxos/log": "0.8.4-main.6fa680abb7",
38
+ "@dxos/echo": "0.8.4-main.6fa680abb7",
39
+ "@dxos/operation": "0.8.4-main.6fa680abb7",
40
+ "@dxos/plugin-graph": "0.8.4-main.6fa680abb7",
41
+ "@dxos/react-ui-menu": "0.8.4-main.6fa680abb7",
42
+ "@dxos/react-ui-mosaic": "0.8.4-main.6fa680abb7",
43
+ "@dxos/react-ui-attention": "0.8.4-main.6fa680abb7",
44
+ "@dxos/react-ui-searchlist": "0.8.4-main.6fa680abb7",
45
+ "@dxos/react-ui-stack": "0.8.4-main.6fa680abb7",
46
+ "@dxos/schema": "0.8.4-main.6fa680abb7",
47
+ "@dxos/util": "0.8.4-main.6fa680abb7"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@types/react": "~19.2.7",
@@ -51,24 +52,24 @@
51
52
  "effect": "3.19.16",
52
53
  "react": "~19.2.3",
53
54
  "react-dom": "~19.2.3",
54
- "vite": "7.1.9",
55
- "@dxos/app-graph": "0.8.4-main.52d7546f51",
56
- "@dxos/plugin-preview": "0.8.4-main.52d7546f51",
57
- "@dxos/plugin-client": "0.8.4-main.52d7546f51",
58
- "@dxos/plugin-search": "0.8.4-main.52d7546f51",
59
- "@dxos/plugin-space": "0.8.4-main.52d7546f51",
60
- "@dxos/plugin-testing": "0.8.4-main.52d7546f51",
61
- "@dxos/react-ui": "0.8.4-main.52d7546f51",
62
- "@dxos/storybook-utils": "0.8.4-main.52d7546f51",
63
- "@dxos/schema": "0.8.4-main.52d7546f51",
64
- "@dxos/ui-theme": "0.8.4-main.52d7546f51"
55
+ "vite": "^7.1.11",
56
+ "@dxos/app-graph": "0.8.4-main.6fa680abb7",
57
+ "@dxos/plugin-client": "0.8.4-main.6fa680abb7",
58
+ "@dxos/plugin-preview": "0.8.4-main.6fa680abb7",
59
+ "@dxos/plugin-search": "0.8.4-main.6fa680abb7",
60
+ "@dxos/plugin-testing": "0.8.4-main.6fa680abb7",
61
+ "@dxos/plugin-space": "0.8.4-main.6fa680abb7",
62
+ "@dxos/react-ui": "0.8.4-main.6fa680abb7",
63
+ "@dxos/storybook-utils": "0.8.4-main.6fa680abb7",
64
+ "@dxos/schema": "0.8.4-main.6fa680abb7",
65
+ "@dxos/ui-theme": "0.8.4-main.6fa680abb7"
65
66
  },
66
67
  "peerDependencies": {
67
68
  "effect": "3.19.16",
68
69
  "react": "~19.2.3",
69
70
  "react-dom": "~19.2.3",
70
- "@dxos/react-ui": "0.8.4-main.52d7546f51",
71
- "@dxos/ui-theme": "0.8.4-main.52d7546f51"
71
+ "@dxos/react-ui": "0.8.4-main.6fa680abb7",
72
+ "@dxos/ui-theme": "0.8.4-main.6fa680abb7"
72
73
  },
73
74
  "publishConfig": {
74
75
  "access": "public"
@@ -5,21 +5,14 @@
5
5
  import * as Effect from 'effect/Effect';
6
6
 
7
7
  import { Capabilities, Capability } from '@dxos/app-framework';
8
- import { LayoutOperation } from '@dxos/app-toolkit';
8
+ import { getCompanionVariant, LayoutOperation, isPinnedWorkspace } from '@dxos/app-toolkit';
9
9
  import { Operation, OperationResolver } from '@dxos/operation';
10
- import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
11
10
 
12
11
  import { type SimpleLayoutState, SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
13
12
 
14
13
  /** Maximum number of items to keep in navigation history. */
15
14
  const MAX_HISTORY_LENGTH = 50;
16
15
 
17
- /** Parse entry ID to extract primary ID and variant. */
18
- const parseEntryId = (entryId: string) => {
19
- const [id, variant] = entryId.split(ATTENDABLE_PATH_SEPARATOR);
20
- return { id, variant };
21
- };
22
-
23
16
  export default Capability.makeModule(
24
17
  Effect.fnUntraced(function* () {
25
18
  const registry = yield* Capability.get(Capabilities.AtomRegistry);
@@ -51,6 +44,7 @@ export default Capability.makeModule(
51
44
  //
52
45
  // UpdateComplementary - Controls companion drawer.
53
46
  //
47
+ // TODO(wittjosiah): Not sure if we should be using this for the drawer.
54
48
  OperationResolver.make({
55
49
  operation: LayoutOperation.UpdateComplementary,
56
50
  handler: Effect.fnUntraced(function* (input) {
@@ -59,6 +53,13 @@ export default Capability.makeModule(
59
53
  ...state,
60
54
  drawerState: 'closed',
61
55
  }));
56
+ } else if (input.subject) {
57
+ const variant = getCompanionVariant(input.subject);
58
+ updateState((state) => ({
59
+ ...state,
60
+ companionVariant: variant,
61
+ drawerState: input.state === 'expanded' ? 'expanded' : 'open',
62
+ }));
62
63
  }
63
64
  }),
64
65
  }),
@@ -116,7 +117,7 @@ export default Capability.makeModule(
116
117
  ...state,
117
118
  // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
118
119
  // Ideally this should be worked into the data model in a generic way.
119
- previousWorkspace: !state.workspace.startsWith('!') ? state.workspace : state.previousWorkspace,
120
+ previousWorkspace: !isPinnedWorkspace(state.workspace) ? state.workspace : state.previousWorkspace,
120
121
  workspace: input.subject,
121
122
  active: undefined,
122
123
  // Clear history when switching workspaces.
@@ -145,32 +146,16 @@ export default Capability.makeModule(
145
146
  operation: LayoutOperation.Open,
146
147
  handler: Effect.fnUntraced(function* (input) {
147
148
  const id = input.subject[0];
148
- const { id: primaryId, variant } = parseEntryId(id);
149
- const state = getState();
150
-
151
- // Only treat as companion when opening a variant of the current workspace/active (e.g. object~comments).
152
- // IDs like settings~spaceId are alternate-tree nodes and should navigate main content, not open the drawer.
153
- // TODO(wittjosiah): Factor out the change-companion operation from deck to a common layout operation.
154
- const isCompanionOfCurrent = variant && (primaryId === state.workspace || primaryId === state.active);
155
- if (isCompanionOfCurrent) {
156
- updateState((state) => ({
149
+ updateState((state) => {
150
+ const newHistory = state.active ? [...state.history, state.active] : state.history;
151
+ const trimmedHistory =
152
+ newHistory.length > MAX_HISTORY_LENGTH ? newHistory.slice(-MAX_HISTORY_LENGTH) : newHistory;
153
+ return {
157
154
  ...state,
158
- companionVariant: variant,
159
- drawerState: state.drawerState === 'closed' || !state.drawerState ? 'open' : state.drawerState,
160
- }));
161
- } else {
162
- // Regular navigation - update active and history (use full id for alternate-tree nodes).
163
- updateState((state) => {
164
- const newHistory = state.active ? [...state.history, state.active] : state.history;
165
- const trimmedHistory =
166
- newHistory.length > MAX_HISTORY_LENGTH ? newHistory.slice(-MAX_HISTORY_LENGTH) : newHistory;
167
- return {
168
- ...state,
169
- active: id,
170
- history: trimmedHistory,
171
- };
172
- });
173
- }
155
+ active: id,
156
+ history: trimmedHistory,
157
+ };
158
+ });
174
159
  }),
175
160
  }),
176
161
 
@@ -9,7 +9,7 @@ import { Capabilities, Capability } from '@dxos/app-framework';
9
9
  import { Surface } from '@dxos/app-framework/ui';
10
10
  import { Node } from '@dxos/plugin-graph';
11
11
 
12
- import { Home, Workspace } from '../../components';
12
+ import { Home, NavBranch } from '../../components';
13
13
  import { meta } from '../../meta';
14
14
 
15
15
  type SurfaceData = {
@@ -23,18 +23,20 @@ export default Capability.makeModule(() =>
23
23
  Effect.succeed(
24
24
  Capability.contributes(Capabilities.ReactSurface, [
25
25
  Surface.create({
26
- id: `${meta.id}/home`,
26
+ id: `${meta.id}.home`,
27
27
  role: 'article',
28
28
  filter: (data): data is SurfaceData => data.attendableId === Node.RootId,
29
29
  component: () => <Home />,
30
30
  }),
31
31
  Surface.create({
32
- id: `${meta.id}/workspace-article`,
32
+ id: `${meta.id}.nav-branch`,
33
33
  role: 'article',
34
34
  position: 'fallback',
35
- filter: (data): data is SurfaceData =>
36
- ALLOWED_DISPOSITIONS.includes((data.properties as Record<string, any>)?.disposition),
37
- component: ({ data }) => <Workspace id={data.attendableId} />,
35
+ filter: (data): data is SurfaceData => {
36
+ const props = data.properties as Record<string, any>;
37
+ return ALLOWED_DISPOSITIONS.includes(props?.disposition) || props?.role === 'branch';
38
+ },
39
+ component: ({ data }) => <NavBranch id={data.attendableId} />,
38
40
  }),
39
41
  ]),
40
42
  ),
@@ -5,17 +5,16 @@
5
5
  import * as Effect from 'effect/Effect';
6
6
 
7
7
  import { Capabilities, Capability } from '@dxos/app-framework';
8
- import { LayoutOperation } from '@dxos/app-toolkit';
8
+ import { LayoutOperation, fromUrlPath, getWorkspaceFromPath, toUrlPath } from '@dxos/app-toolkit';
9
9
  import { log } from '@dxos/log';
10
- import { Node } from '@dxos/plugin-graph';
11
10
  import { isTauri } from '@dxos/util';
12
11
 
13
12
  import { type SimpleLayoutState, SimpleLayoutState as SimpleLayoutStateCapability } from '../../types';
14
13
 
15
14
  /**
16
15
  * URL handler for simple layout that syncs browser URL with layout state.
17
- * URL format: /{workspace} or /{workspace}/{active}
18
- * Root is represented as / or /root.
16
+ * URL paths map directly to qualified graph IDs with the leading `root` segment stripped.
17
+ * Root is represented as `/`.
19
18
  *
20
19
  * On mobile Tauri, also listens for deep links via the deep-link plugin.
21
20
  */
@@ -25,38 +24,33 @@ export default Capability.makeModule(
25
24
 
26
25
  /**
27
26
  * Handle navigation from a pathname.
28
- * Parses path and updates state accordingly.
27
+ * Restores the qualified graph ID and dispatches layout operations.
29
28
  */
30
29
  const handlePathNavigation = (pathname: string) => {
31
30
  log.info('[UrlHandler] Navigating to path', { pathname });
32
31
 
33
- // Parse URL segments: /{workspace}/{active}
34
- const [_, nextWorkspace, nextActive] = pathname.split('/');
32
+ const qualifiedId = fromUrlPath(pathname);
33
+ const workspace = getWorkspaceFromPath(qualifiedId);
35
34
 
36
- // Determine target workspace (empty or 'root' means Node.RootId).
37
- const targetWorkspace = !nextWorkspace || nextWorkspace === 'root' ? Node.RootId : nextWorkspace;
35
+ invokeSync(LayoutOperation.SwitchWorkspace, { subject: workspace });
38
36
 
39
- // Navigate via operations (they will update state accordingly).
40
- invokeSync(LayoutOperation.SwitchWorkspace, { subject: targetWorkspace });
41
- if (nextActive) {
42
- invokeSync(LayoutOperation.Open, { subject: [nextActive] });
37
+ const activeId = qualifiedId !== workspace ? qualifiedId : undefined;
38
+ if (activeId) {
39
+ invokeSync(LayoutOperation.Open, { subject: [activeId] });
43
40
  }
44
41
  };
45
42
 
46
43
  const onNavigation = handleNavigation(handlePathNavigation);
47
44
 
48
- // Handle initial URL and listen for browser navigation.
49
45
  yield* Effect.sync(() => onNavigation());
50
46
  window.addEventListener('popstate', onNavigation);
51
47
 
52
- // Set up deep link listener for mobile Tauri.
53
48
  let unlistenDeepLink: (() => void) | undefined;
54
49
  if (isTauri()) {
55
50
  yield* Effect.tryPromise({
56
51
  try: async () => {
57
52
  const { getCurrent, onOpenUrl } = await import('@tauri-apps/plugin-deep-link');
58
53
 
59
- // Check if app was launched via deep link (cold start).
60
54
  const launchUrls = await getCurrent();
61
55
  if (launchUrls && launchUrls.length > 0) {
62
56
  log.info('[UrlHandler] App launched with deep links', { urls: launchUrls });
@@ -65,7 +59,6 @@ export default Capability.makeModule(
65
59
  }
66
60
  }
67
61
 
68
- // Listen for deep links while app is running.
69
62
  unlistenDeepLink = await onOpenUrl((urls) => {
70
63
  log.info('[UrlHandler] Deep links received', { urls });
71
64
  for (const url of urls) {
@@ -82,18 +75,16 @@ export default Capability.makeModule(
82
75
  }).pipe(Effect.catchAll(() => Effect.void));
83
76
  }
84
77
 
85
- // Subscribe to state changes to update the URL.
86
78
  let lastWorkspace: string | undefined;
87
79
  let lastActive: string | undefined;
88
80
  const unsubscribe = yield* Capabilities.subscribeAtom(SimpleLayoutStateCapability, (state: SimpleLayoutState) => {
89
81
  const { workspace, active } = state;
90
82
 
91
- // Only update URL if relevant state changed.
92
83
  if (workspace !== lastWorkspace || active !== lastActive) {
93
84
  lastWorkspace = workspace;
94
85
  lastActive = active;
95
86
 
96
- const path = pathFromState(workspace, active);
87
+ const path = active ? toUrlPath(active) : toUrlPath(workspace);
97
88
  if (window.location.pathname !== path) {
98
89
  history.pushState(null, '', `${path}${window.location.search}`);
99
90
  }
@@ -110,27 +101,12 @@ export default Capability.makeModule(
110
101
  }),
111
102
  );
112
103
 
113
- // TODO(wittjosiah): Instead of hardcoding redirect paths, we should either:
114
- // 1. Validate that the workspace exists in the graph before navigating.
115
- // 2. Implement more structured routing with explicit route definitions.
116
104
  /**
117
105
  * Check if a path is a special redirect path that shouldn't be navigated to.
118
106
  * These paths are handled by other systems (e.g., OAuth).
119
107
  */
120
108
  const isRedirectPath = (pathname: string): boolean => pathname.startsWith('/redirect/');
121
109
 
122
- /**
123
- * Build pathname from layout state. Root workspace is / or /root/{active}.
124
- */
125
- const pathFromState = (workspace: string, active: string | undefined): string =>
126
- workspace === Node.RootId
127
- ? active
128
- ? `/${Node.RootId}/${active}`
129
- : '/'
130
- : active
131
- ? `/${workspace}/${active}`
132
- : `/${workspace}`;
133
-
134
110
  /**
135
111
  * Returns a handler for navigation events (initial load and popstate) that navigates to current pathname.
136
112
  */
@@ -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
10
  import { translations } from '../translations';
10
11
 
11
- import { ContentError } from './ContentError';
12
-
13
12
  const meta = {
14
- title: 'plugins/plugin-simple-layout/ContentError',
15
- component: ContentError,
13
+ title: 'plugins/plugin-simple-layout/components/ErrorFallback',
14
+ component: ErrorFallback,
16
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 = {
@@ -9,7 +9,7 @@ import { withTheme } from '@dxos/react-ui/testing';
9
9
  import { ContentLoading } from './ContentLoading';
10
10
 
11
11
  const meta = {
12
- title: 'plugins/plugin-simple-layout/ContentLoading',
12
+ title: 'plugins/plugin-simple-layout/components/ContentLoading',
13
13
  component: ContentLoading,
14
14
  decorators: [withTheme()],
15
15
  parameters: {
@@ -6,5 +6,5 @@ import React from 'react';
6
6
 
7
7
  // TODO(burdon): Show skeleton: https://github.com/dxos/dxos/issues/8259
8
8
  export const ContentLoading = () => {
9
- return <div role='none' className='grid place-items-center attention-surface' />;
9
+ return <div role='none' 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 './ContentLoading';
@@ -6,9 +6,9 @@ import React from 'react';
6
6
 
7
7
  import { Surface } from '@dxos/app-framework/ui';
8
8
  import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
9
+ import { ErrorFallback } from '@dxos/react-ui';
9
10
 
10
11
  import { useSimpleLayoutState } from '../../hooks';
11
- import { ContentError } from '../ContentError';
12
12
 
13
13
  export const Dialog = () => {
14
14
  const { state, updateState } = useSimpleLayoutState();
@@ -23,14 +23,14 @@ export const Dialog = () => {
23
23
  onOpenChange={(nextOpen) => updateState((state) => ({ ...state, dialogOpen: nextOpen }))}
24
24
  >
25
25
  {state.dialogBlockAlign === 'end' ? (
26
- <Surface.Surface role='dialog' data={state.dialogContent} limit={1} fallback={ContentError} />
26
+ <Surface.Surface role='dialog' data={state.dialogContent} limit={1} fallback={ErrorFallback} />
27
27
  ) : (
28
28
  <DialogOverlay
29
29
  blockAlign={state.dialogBlockAlign}
30
30
  classNames={state.dialogOverlayClasses}
31
31
  style={state.dialogOverlayStyle}
32
32
  >
33
- <Surface.Surface role='dialog' data={state.dialogContent} limit={1} fallback={ContentError} />
33
+ <Surface.Surface role='dialog' data={state.dialogContent} limit={1} fallback={ErrorFallback} />
34
34
  </DialogOverlay>
35
35
  )}
36
36
  </DialogRoot>
@@ -8,14 +8,15 @@ import { useOperationInvoker } from '@dxos/app-framework/ui';
8
8
  import { LayoutOperation } from '@dxos/app-toolkit';
9
9
  import { useAppGraph } from '@dxos/app-toolkit/ui';
10
10
  import { Node, useConnections } from '@dxos/plugin-graph';
11
- import { Avatar, Icon, Layout, ScrollArea, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
12
- import { Card, Mosaic, type StackTileComponent } from '@dxos/react-ui-mosaic';
11
+ import { Avatar, Icon, Panel, ScrollArea, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
12
+ import { Card } from '@dxos/react-ui';
13
+ import { Mosaic, type MosaicStackTileComponent } from '@dxos/react-ui-mosaic';
13
14
  import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
14
15
  import { mx } from '@dxos/ui-theme';
15
16
  import { byPosition } from '@dxos/util';
16
17
 
17
18
  import { meta } from '../../meta';
18
- import { useLoadDescendents } from '../hooks';
19
+ import { useExpandPath } from '../hooks';
19
20
 
20
21
  export type HomeProps = {};
21
22
 
@@ -27,7 +28,7 @@ export const Home = (_: HomeProps) => {
27
28
  const userAccountItem = useItemsByDisposition('user-account')[0];
28
29
  const pinnedItems = useItemsByDisposition('pin-end', true);
29
30
  const workspaceItems = useItemsByDisposition('workspace');
30
- useLoadDescendents(Node.RootId);
31
+ useExpandPath(Node.RootId);
31
32
 
32
33
  const items = useMemo(
33
34
  () => [...(userAccountItem ? [userAccountItem] : []), ...pinnedItems, ...workspaceItems],
@@ -40,26 +41,31 @@ export const Home = (_: HomeProps) => {
40
41
  });
41
42
 
42
43
  return (
43
- <Layout.Main toolbar>
44
- <SearchList.Root onSearch={handleSearch}>
45
- <Toolbar.Root>
46
- <SearchList.Input placeholder={t('search placeholder')} autoFocus />
47
- </Toolbar.Root>
48
- <SearchList.Content>
49
- <Mosaic.Container asChild>
50
- <ScrollArea.Root orientation='vertical'>
51
- <ScrollArea.Viewport classNames='p-2'>
52
- <Mosaic.Stack items={results} getId={(node) => node.id} Tile={WorkspaceTile} />
53
- </ScrollArea.Viewport>
54
- </ScrollArea.Root>
55
- </Mosaic.Container>
56
- </SearchList.Content>
57
- </SearchList.Root>
58
- </Layout.Main>
44
+ <SearchList.Root onSearch={handleSearch}>
45
+ <Panel.Root>
46
+ <Panel.Toolbar asChild>
47
+ <Toolbar.Root>
48
+ <SearchList.Input placeholder={t('search placeholder')} autoFocus />
49
+ </Toolbar.Root>
50
+ </Panel.Toolbar>
51
+ <Panel.Content asChild>
52
+ <SearchList.Content>
53
+ <Mosaic.Container asChild>
54
+ <ScrollArea.Root orientation='vertical'>
55
+ <ScrollArea.Viewport classNames='p-2'>
56
+ <Mosaic.Stack items={results} getId={(node) => node.id} Tile={WorkspaceTile} />
57
+ </ScrollArea.Viewport>
58
+ </ScrollArea.Root>
59
+ </Mosaic.Container>
60
+ </SearchList.Content>
61
+ </Panel.Content>
62
+ </Panel.Root>
63
+ </SearchList.Root>
59
64
  );
60
65
  };
61
66
 
62
- const WorkspaceTile: StackTileComponent<Node.Node> = ({ data }) => {
67
+ const WorkspaceTile: MosaicStackTileComponent<Node.Node> = (props) => {
68
+ const data = props.data;
63
69
  const { t } = useTranslation(meta.id);
64
70
  const { invokePromise } = useOperationInvoker();
65
71
  const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
@@ -67,7 +73,7 @@ const WorkspaceTile: StackTileComponent<Node.Node> = ({ data }) => {
67
73
  const isSelected = selectedValue === data.id;
68
74
  const cardRef = useRef<HTMLDivElement>(null);
69
75
 
70
- useLoadDescendents(data.id);
76
+ useExpandPath(data.id);
71
77
 
72
78
  const handleSelect = useCallback(
73
79
  () => invokePromise(LayoutOperation.SwitchWorkspace, { subject: data.id }),
@@ -96,7 +102,7 @@ const WorkspaceTile: StackTileComponent<Node.Node> = ({ data }) => {
96
102
  fullWidth
97
103
  tabIndex={-1} // TODO(burdon): Use Mosaic.Focus.
98
104
  data-selected={isSelected}
99
- classNames={mx('dx-focus-ring', isSelected && 'bg-hoverOverlay')}
105
+ classNames={mx('dx-focus-ring', isSelected && 'bg-hover-overlay')}
100
106
  onClick={handleSelect}
101
107
  ref={cardRef}
102
108
  >
@@ -126,7 +132,7 @@ const filterItems = (node: Node.Node, disposition: string) => {
126
132
  /** Returns root-level items filtered by disposition. */
127
133
  const useItemsByDisposition = (disposition: string, sort = false) => {
128
134
  const { graph } = useAppGraph();
129
- const connections = useConnections(graph, Node.RootId);
135
+ const connections = useConnections(graph, Node.RootId, 'child');
130
136
  const filtered = connections.filter((node) => filterItems(node, disposition));
131
137
  return sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
132
138
  };
@@ -6,7 +6,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { type PropsWithChildren, useEffect, useState } from 'react';
7
7
 
8
8
  import { addEventListener, combine } from '@dxos/async';
9
- import { Flex, Input, Layout, Splitter, type SplitterMode, Toolbar } from '@dxos/react-ui';
9
+ import { Flex, Input, Panel, Splitter, type SplitterMode, Toolbar } from '@dxos/react-ui';
10
10
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
12
  import { MobileLayout, type MobileLayoutRootProps } from './MobileLayout';
@@ -54,20 +54,24 @@ const WithKeyboard = ({ children }: PropsWithChildren) => {
54
54
  return <div className='h-screen relative'>{children}</div>;
55
55
  };
56
56
 
57
- const Panel = ({ children, label }: PropsWithChildren<{ label: string }>) => {
57
+ const StoryPanel = ({ children, label }: PropsWithChildren<{ label: string }>) => {
58
58
  return (
59
- <Layout.Main toolbar>
60
- <Toolbar.Root>
61
- {label}
62
- <Toolbar.Separator variant='gap' />
63
- {children}
64
- </Toolbar.Root>
65
- <Flex column classNames='p-1'>
66
- <Input.Root>
67
- <Input.TextInput />
68
- </Input.Root>
69
- </Flex>
70
- </Layout.Main>
59
+ <Panel.Root>
60
+ <Panel.Toolbar asChild>
61
+ <Toolbar.Root>
62
+ {label}
63
+ <Toolbar.Separator />
64
+ {children}
65
+ </Toolbar.Root>
66
+ </Panel.Toolbar>
67
+ <Panel.Content asChild>
68
+ <Flex column classNames='p-1'>
69
+ <Input.Root>
70
+ <Input.TextInput />
71
+ </Input.Root>
72
+ </Flex>
73
+ </Panel.Content>
74
+ </Panel.Root>
71
75
  );
72
76
  };
73
77
 
@@ -85,21 +89,21 @@ const DefaultStory = () => {
85
89
  <MobileLayout.Panel safe={{ top: true, bottom: splitterMode === 'upper' }}>
86
90
  <Splitter.Root mode={splitterMode} ratio={0.5}>
87
91
  <Splitter.Panel position='upper'>
88
- <Panel label='Main'>
92
+ <StoryPanel label='Main'>
89
93
  {splitterMode === 'upper' && (
90
94
  <Toolbar.IconButton icon='ph--plus--regular' label='Open' onClick={() => setSplitterMode('both')} />
91
95
  )}
92
- </Panel>
96
+ </StoryPanel>
93
97
  </Splitter.Panel>
94
98
  <Splitter.Panel position='lower'>
95
- <Panel label='Drawer'>
99
+ <StoryPanel label='Drawer'>
96
100
  <Toolbar.IconButton
97
101
  icon={splitterMode === 'lower' ? 'ph--arrow-down--regular' : 'ph--arrow-up--regular'}
98
102
  label={splitterMode === 'lower' ? 'Collapse' : 'Expand'}
99
103
  onClick={() => setSplitterMode((splitterMode) => (splitterMode === 'both' ? 'lower' : 'both'))}
100
104
  />
101
105
  <Toolbar.IconButton icon='ph--x--regular' label='Close' onClick={() => setSplitterMode('upper')} />
102
- </Panel>
106
+ </StoryPanel>
103
107
  </Splitter.Panel>
104
108
  </Splitter.Root>
105
109
  </MobileLayout.Panel>
@@ -109,7 +113,7 @@ const DefaultStory = () => {
109
113
  };
110
114
 
111
115
  const meta: Meta<MobileLayoutRootProps> = {
112
- title: 'plugins/plugin-simple-layout/MobileLayout',
116
+ title: 'plugins/plugin-simple-layout/components/MobileLayout',
113
117
  component: MobileLayout.Root,
114
118
  render: DefaultStory,
115
119
  decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'relative' })],
@@ -54,7 +54,7 @@ const MobileLayoutRoot = forwardRef<HTMLDivElement, MobileLayoutRootProps>(
54
54
  {...props}
55
55
  role='none'
56
56
  style={{
57
- transition: `block-size ${transition}ms ease-out`,
57
+ transition: `h-size ${transition}ms ease-out`,
58
58
  blockSize: 'calc(100vh - var(--kb-height, 0px))',
59
59
  }}
60
60
  className={mx('absolute top-0 left-0 right-0 flex flex-col', classNames)}
@@ -95,7 +95,7 @@ const MobileLayoutPanel = forwardRef<HTMLDivElement, MobileLayoutPanelProps>(
95
95
  paddingTop: safe?.top ? 'env(safe-area-inset-top)' : undefined,
96
96
  paddingBottom: safe?.bottom ? `calc((1 - var(--kb-open, 0)) * env(safe-area-inset-bottom))` : undefined,
97
97
  }}
98
- className={mx('relative bs-full flex flex-col overflow-hidden', classNames)}
98
+ className={mx('relative h-full flex flex-col overflow-hidden', classNames)}
99
99
  ref={forwardedRef}
100
100
  >
101
101
  {children}