@dxos/plugin-simple-layout 0.8.4-main.69d29f4 → 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 (192) hide show
  1. package/dist/lib/browser/chunk-MDPEKLKR.mjs +1163 -0
  2. package/dist/lib/browser/chunk-MDPEKLKR.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-CLPGTNWJ.mjs → chunk-MRR7PXSM.mjs} +5 -5
  4. package/dist/lib/browser/chunk-MRR7PXSM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +27 -20
  6. package/dist/lib/browser/index.mjs.map +3 -3
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/{operation-resolver-LTB63NKP.mjs → operation-resolver-VTZ6HZ4B.mjs} +45 -19
  9. package/dist/lib/browser/operation-resolver-VTZ6HZ4B.mjs.map +7 -0
  10. package/dist/lib/browser/{react-root-6ARAPH3O.mjs → react-root-WVQYY2JA.mjs} +5 -5
  11. package/dist/lib/browser/react-root-WVQYY2JA.mjs.map +7 -0
  12. package/dist/lib/browser/{react-surface-SO7B23GS.mjs → react-surface-VLBR37ED.mjs} +18 -13
  13. package/dist/lib/browser/react-surface-VLBR37ED.mjs.map +7 -0
  14. package/dist/lib/browser/{spotlight-dismiss-VSNOPETH.mjs → spotlight-dismiss-67PHYS5B.mjs} +3 -3
  15. package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs.map +7 -0
  16. package/dist/lib/browser/{state-H4IGICBB.mjs → state-TXSMUWYI.mjs} +8 -5
  17. package/dist/lib/browser/state-TXSMUWYI.mjs.map +7 -0
  18. package/dist/lib/browser/url-handler-RBRONH7S.mjs +151 -0
  19. package/dist/lib/browser/url-handler-RBRONH7S.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-DCKASLMP.mjs +1164 -0
  21. package/dist/lib/node-esm/chunk-DCKASLMP.mjs.map +7 -0
  22. package/dist/lib/node-esm/{chunk-MUVVYBUE.mjs → chunk-WMNTJ2MK.mjs} +5 -5
  23. package/dist/lib/node-esm/chunk-WMNTJ2MK.mjs.map +7 -0
  24. package/dist/lib/node-esm/index.mjs +27 -20
  25. package/dist/lib/node-esm/index.mjs.map +3 -3
  26. package/dist/lib/node-esm/meta.json +1 -1
  27. package/dist/lib/node-esm/{operation-resolver-7O6O7T4Q.mjs → operation-resolver-R7CQ6ERU.mjs} +45 -19
  28. package/dist/lib/node-esm/operation-resolver-R7CQ6ERU.mjs.map +7 -0
  29. package/dist/lib/node-esm/{react-root-2CPA2ZUS.mjs → react-root-XBNDM7BE.mjs} +5 -5
  30. package/dist/lib/node-esm/react-root-XBNDM7BE.mjs.map +7 -0
  31. package/dist/lib/node-esm/{react-surface-FKAV56MO.mjs → react-surface-U5NHA367.mjs} +18 -13
  32. package/dist/lib/node-esm/react-surface-U5NHA367.mjs.map +7 -0
  33. package/dist/lib/node-esm/{spotlight-dismiss-L5PCWIJG.mjs → spotlight-dismiss-RMLRZUVY.mjs} +3 -3
  34. package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs.map +7 -0
  35. package/dist/lib/node-esm/{state-QIU2LMLT.mjs → state-JMX6FAG4.mjs} +8 -5
  36. package/dist/lib/node-esm/state-JMX6FAG4.mjs.map +7 -0
  37. package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs +152 -0
  38. package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs.map +7 -0
  39. package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -1
  40. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +2 -2
  41. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
  42. package/dist/types/src/capabilities/react-root/react-root.d.ts +1 -1
  43. package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -1
  44. package/dist/types/src/capabilities/react-surface/index.d.ts +1 -1
  45. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -1
  46. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +2 -2
  47. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -1
  48. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +1 -1
  49. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -1
  50. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +1 -1
  51. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -1
  52. package/dist/types/src/capabilities/state/index.d.ts +1 -1
  53. package/dist/types/src/capabilities/state/state.d.ts +1 -1
  54. package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
  55. package/dist/types/src/capabilities/url-handler/url-handler.d.ts +5 -3
  56. package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -1
  57. package/dist/types/src/components/ContentError.stories.d.ts +7 -3
  58. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/ContentLoading/ContentLoading.d.ts.map +1 -0
  60. package/dist/types/src/components/ContentLoading/ContentLoading.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/ContentLoading/index.d.ts +2 -0
  62. package/dist/types/src/components/ContentLoading/index.d.ts.map +1 -0
  63. package/dist/types/src/components/Home/Home.d.ts.map +1 -1
  64. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts +35 -0
  65. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts.map +1 -0
  66. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts +7 -0
  67. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -0
  68. package/dist/types/src/components/MobileLayout/index.d.ts +2 -0
  69. package/dist/types/src/components/MobileLayout/index.d.ts.map +1 -0
  70. package/dist/types/src/components/NavBranch/NavBranch.d.ts +11 -0
  71. package/dist/types/src/components/NavBranch/NavBranch.d.ts.map +1 -0
  72. package/dist/types/src/components/NavBranch/index.d.ts +2 -0
  73. package/dist/types/src/components/NavBranch/index.d.ts.map +1 -0
  74. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  75. package/dist/types/src/components/SimpleLayout/AppBar.d.ts +26 -0
  76. package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -0
  77. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts +47 -0
  78. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts.map +1 -0
  79. package/dist/types/src/components/SimpleLayout/Drawer.d.ts +9 -0
  80. package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -0
  81. package/dist/types/src/components/SimpleLayout/Main.d.ts +1 -1
  82. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
  83. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +13 -3
  84. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
  85. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +13 -9
  86. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -1
  87. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -1
  88. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +10 -0
  89. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/SimpleLayout/index.d.ts +3 -0
  91. package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -1
  92. package/dist/types/src/components/hooks.d.ts +4 -2
  93. package/dist/types/src/components/hooks.d.ts.map +1 -1
  94. package/dist/types/src/components/index.d.ts +2 -1
  95. package/dist/types/src/components/index.d.ts.map +1 -1
  96. package/dist/types/src/hooks/actions.d.ts +19 -0
  97. package/dist/types/src/hooks/actions.d.ts.map +1 -0
  98. package/dist/types/src/hooks/index.d.ts +5 -0
  99. package/dist/types/src/hooks/index.d.ts.map +1 -1
  100. package/dist/types/src/hooks/useAppBarProps.d.ts +7 -0
  101. package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -0
  102. package/dist/types/src/hooks/useCompanions.d.ts +12 -0
  103. package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
  104. package/dist/types/src/hooks/useDrawerActions.d.ts +13 -0
  105. package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -0
  106. package/dist/types/src/hooks/useNavbarActions.d.ts +14 -0
  107. package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -0
  108. package/dist/types/src/hooks/useSimpleLayoutState.d.ts +3 -3
  109. package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -1
  110. package/dist/types/src/translations.d.ts +6 -0
  111. package/dist/types/src/translations.d.ts.map +1 -1
  112. package/dist/types/src/types/capabilities.d.ts +8 -3
  113. package/dist/types/src/types/capabilities.d.ts.map +1 -1
  114. package/dist/types/src/types/events.d.ts.map +1 -1
  115. package/dist/types/tsconfig.tsbuildinfo +1 -1
  116. package/package.json +36 -30
  117. package/src/SimpleLayoutPlugin.ts +10 -9
  118. package/src/capabilities/operation-resolver/operation-resolver.ts +43 -19
  119. package/src/capabilities/react-root/react-root.tsx +2 -2
  120. package/src/capabilities/react-surface/react-surface.tsx +14 -11
  121. package/src/capabilities/spotlight-dismiss/spotlight-dismiss.ts +2 -2
  122. package/src/capabilities/state/state.tsx +6 -3
  123. package/src/capabilities/url-handler/url-handler.ts +98 -45
  124. package/src/components/ContentError.stories.tsx +8 -7
  125. package/src/components/{ContentLoading.stories.tsx → ContentLoading/ContentLoading.stories.tsx} +2 -2
  126. package/src/components/{ContentLoading.tsx → ContentLoading/ContentLoading.tsx} +1 -1
  127. package/src/components/ContentLoading/index.ts +5 -0
  128. package/src/components/Dialog/Dialog.tsx +5 -5
  129. package/src/components/Home/Home.tsx +43 -35
  130. package/src/components/MobileLayout/MobileLayout.stories.tsx +129 -0
  131. package/src/components/MobileLayout/MobileLayout.tsx +305 -0
  132. package/src/components/MobileLayout/index.ts +5 -0
  133. package/src/components/NavBranch/NavBranch.tsx +130 -0
  134. package/src/components/{Workspace → NavBranch}/index.ts +1 -1
  135. package/src/components/Popover/Popover.tsx +17 -7
  136. package/src/components/SimpleLayout/AppBar.stories.tsx +144 -0
  137. package/src/components/SimpleLayout/AppBar.tsx +94 -0
  138. package/src/components/SimpleLayout/Drawer.tsx +96 -0
  139. package/src/components/SimpleLayout/Main.tsx +46 -45
  140. package/src/components/SimpleLayout/NavBar.stories.tsx +131 -26
  141. package/src/components/SimpleLayout/NavBar.tsx +18 -58
  142. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +24 -11
  143. package/src/components/SimpleLayout/SimpleLayout.tsx +45 -6
  144. package/src/components/SimpleLayout/index.ts +3 -0
  145. package/src/components/hooks.ts +10 -14
  146. package/src/components/index.ts +2 -1
  147. package/src/hooks/actions.ts +83 -0
  148. package/src/hooks/index.ts +5 -0
  149. package/src/hooks/useAppBarProps.ts +115 -0
  150. package/src/hooks/useCompanions.ts +22 -0
  151. package/src/hooks/useDrawerActions.ts +100 -0
  152. package/src/hooks/useNavbarActions.ts +87 -0
  153. package/src/hooks/useSimpleLayoutState.ts +5 -5
  154. package/src/meta.ts +1 -1
  155. package/src/translations.ts +6 -0
  156. package/src/types/capabilities.ts +13 -4
  157. package/src/types/events.ts +3 -2
  158. package/dist/lib/browser/chunk-CLPGTNWJ.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-FK4M7GJV.mjs +0 -613
  160. package/dist/lib/browser/chunk-FK4M7GJV.mjs.map +0 -7
  161. package/dist/lib/browser/operation-resolver-LTB63NKP.mjs.map +0 -7
  162. package/dist/lib/browser/react-root-6ARAPH3O.mjs.map +0 -7
  163. package/dist/lib/browser/react-surface-SO7B23GS.mjs.map +0 -7
  164. package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs.map +0 -7
  165. package/dist/lib/browser/state-H4IGICBB.mjs.map +0 -7
  166. package/dist/lib/browser/url-handler-7CFGTLNG.mjs +0 -54
  167. package/dist/lib/browser/url-handler-7CFGTLNG.mjs.map +0 -7
  168. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs +0 -614
  169. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs.map +0 -7
  170. package/dist/lib/node-esm/chunk-MUVVYBUE.mjs.map +0 -7
  171. package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs.map +0 -7
  172. package/dist/lib/node-esm/react-root-2CPA2ZUS.mjs.map +0 -7
  173. package/dist/lib/node-esm/react-surface-FKAV56MO.mjs.map +0 -7
  174. package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs.map +0 -7
  175. package/dist/lib/node-esm/state-QIU2LMLT.mjs.map +0 -7
  176. package/dist/lib/node-esm/url-handler-4LYP3JM7.mjs +0 -55
  177. package/dist/lib/node-esm/url-handler-4LYP3JM7.mjs.map +0 -7
  178. package/dist/types/src/components/ContentError.d.ts +0 -5
  179. package/dist/types/src/components/ContentError.d.ts.map +0 -1
  180. package/dist/types/src/components/ContentLoading.d.ts.map +0 -1
  181. package/dist/types/src/components/ContentLoading.stories.d.ts.map +0 -1
  182. package/dist/types/src/components/SimpleLayout/Banner.d.ts +0 -8
  183. package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +0 -1
  184. package/dist/types/src/components/Workspace/Workspace.d.ts +0 -9
  185. package/dist/types/src/components/Workspace/Workspace.d.ts.map +0 -1
  186. package/dist/types/src/components/Workspace/index.d.ts +0 -2
  187. package/dist/types/src/components/Workspace/index.d.ts.map +0 -1
  188. package/src/components/ContentError.tsx +0 -23
  189. package/src/components/SimpleLayout/Banner.tsx +0 -82
  190. package/src/components/Workspace/Workspace.tsx +0 -115
  191. /package/dist/types/src/components/{ContentLoading.d.ts → ContentLoading/ContentLoading.d.ts} +0 -0
  192. /package/dist/types/src/components/{ContentLoading.stories.d.ts → ContentLoading/ContentLoading.stories.d.ts} +0 -0
@@ -0,0 +1,130 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
6
+
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 { type Node, useConnections } from '@dxos/plugin-graph';
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';
14
+ import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
15
+ import { mx } from '@dxos/ui-theme';
16
+
17
+ import { meta } from '../../meta';
18
+ import { useExpandPath } from '../hooks';
19
+
20
+ export type NavBranchProps = {
21
+ id: string;
22
+ };
23
+
24
+ /**
25
+ * Renders the children of a graph branch node as a searchable mosaic list.
26
+ * Used for any node with `role: 'branch'` or a workspace disposition, including
27
+ * spaces, collection sections, type sections, and schema nodes.
28
+ */
29
+ export const NavBranch = ({ id }: NavBranchProps) => {
30
+ const { t } = useTranslation(meta.id);
31
+ const { graph } = useAppGraph();
32
+
33
+ useExpandPath(id);
34
+
35
+ const children = useConnections(graph, id, 'child');
36
+
37
+ // TODO(wittjosiah): Move alternate-tree nodes to a non-child relation so they don't need filtering.
38
+ const visibleChildren = useMemo(
39
+ () => children.filter((node) => node.properties.disposition !== 'alternate-tree'),
40
+ [children],
41
+ );
42
+
43
+ const { results, handleSearch } = useSearchListResults({
44
+ items: visibleChildren,
45
+ extract: (child) => toLocalizedString(child.properties.label, t),
46
+ });
47
+
48
+ return (
49
+ <SearchList.Root onSearch={handleSearch}>
50
+ <Panel.Root>
51
+ <Panel.Toolbar asChild>
52
+ <Toolbar.Root>
53
+ {/* TODO(wittjosiah): Search should be pluggable. Must support searching via ECHO query inside a space. */}
54
+ <SearchList.Input placeholder={t('search placeholder')} autoFocus />
55
+ </Toolbar.Root>
56
+ </Panel.Toolbar>
57
+ <Panel.Content asChild>
58
+ <SearchList.Content>
59
+ <Mosaic.Container asChild>
60
+ <ScrollArea.Root orientation='vertical'>
61
+ <ScrollArea.Viewport classNames='p-2'>
62
+ <Mosaic.Stack items={results} getId={(child) => child.id} Tile={NavBranchTile} />
63
+ </ScrollArea.Viewport>
64
+ </ScrollArea.Root>
65
+ </Mosaic.Container>
66
+ </SearchList.Content>
67
+ </Panel.Content>
68
+ </Panel.Root>
69
+ </SearchList.Root>
70
+ );
71
+ };
72
+
73
+ const NavBranchTile: MosaicStackTileComponent<Node.Node> = (props) => {
74
+ const data = props.data;
75
+ const { t } = useTranslation(meta.id);
76
+ const { invokeSync } = useOperationInvoker();
77
+ const ref = useRef<HTMLDivElement>(null);
78
+ const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
79
+ const isSelected = selectedValue === data.id;
80
+
81
+ const name = toLocalizedString(data.properties.label, t);
82
+
83
+ const handleSelect = useCallback(
84
+ () => invokeSync(LayoutOperation.Open, { subject: [data.id] }),
85
+ [invokeSync, data.id],
86
+ );
87
+
88
+ // Register this item with the search context.
89
+ useEffect(() => {
90
+ if (ref.current) {
91
+ registerItem(data.id, ref.current, handleSelect);
92
+ }
93
+
94
+ return () => unregisterItem(data.id);
95
+ }, [data.id, handleSelect, registerItem, unregisterItem]);
96
+
97
+ // Scroll into view when selected.
98
+ useEffect(() => {
99
+ if (isSelected && ref.current) {
100
+ ref.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
101
+ }
102
+ }, [isSelected]);
103
+
104
+ return (
105
+ <Card.Root
106
+ ref={ref}
107
+ role='button'
108
+ fullWidth
109
+ tabIndex={-1} // TODO(burdon): Use Mosaic.Focus.
110
+ data-selected={isSelected}
111
+ classNames={mx('dx-focus-ring', isSelected && 'bg-hover-overlay')}
112
+ onClick={handleSelect}
113
+ >
114
+ <Card.Toolbar density='coarse'>
115
+ <Avatar.Root>
116
+ <Avatar.Content
117
+ hue={data.properties.hue}
118
+ icon={data.properties.icon}
119
+ hueVariant='transparent'
120
+ variant='square'
121
+ size={12}
122
+ fallback={name}
123
+ />
124
+ <Avatar.Label>{name}</Avatar.Label>
125
+ <Icon icon='ph--caret-right--regular' />
126
+ </Avatar.Root>
127
+ </Card.Toolbar>
128
+ </Card.Root>
129
+ );
130
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './Workspace';
5
+ export * from './NavBranch';
@@ -5,9 +5,10 @@
5
5
  import { createContext } from '@radix-ui/react-context';
6
6
  import React, { type PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { Surface } from '@dxos/app-framework/react';
8
+ import { Surface } from '@dxos/app-framework/ui';
9
+ import { useObjectNavigate } from '@dxos/app-toolkit/ui';
9
10
  import { Popover, type PopoverContentInteractOutsideEvent, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
- import { Card } from '@dxos/react-ui-mosaic';
11
+ import { Card } from '@dxos/react-ui';
11
12
 
12
13
  import { useSimpleLayoutState } from '../../hooks';
13
14
  import { meta } from '../../meta';
@@ -53,10 +54,15 @@ export const PopoverRoot = ({ children }: PropsWithChildren) => {
53
54
  );
54
55
  };
55
56
 
57
+ // Extracts the subject from popover content if it has one, otherwise returns the content as-is.
58
+ const getPopoverSubject = (content: unknown): unknown =>
59
+ content && typeof content === 'object' && 'subject' in content ? (content as { subject: unknown }).subject : content;
60
+
56
61
  export const PopoverContent = () => {
57
62
  const { t } = useTranslation(meta.id);
58
63
  const { state, updateState } = useSimpleLayoutState();
59
64
  const { setOpen } = useLayoutPopoverContext('PopoverContent');
65
+ const handleNavigate = useObjectNavigate(getPopoverSubject(state.popoverContent));
60
66
 
61
67
  const handleClose = useCallback(() => {
62
68
  setOpen(false);
@@ -100,18 +106,22 @@ export const PopoverContent = () => {
100
106
  onEscapeKeyDown={handleInteractOutside}
101
107
  >
102
108
  <Popover.Viewport>
109
+ {state.popoverKind === 'base' && <Surface.Surface role='popover' data={state.popoverContent} limit={1} />}
103
110
  {state.popoverKind === 'card' && (
104
- <Card.Root>
111
+ <Card.Root border={false} classNames='dx-card-popover'>
105
112
  <Card.Toolbar>
106
113
  {/* TODO(wittjosiah): Cleaner way to handle no drag handle in toolbar? */}
107
114
  <span />
108
- {state.popoverTitle ? <Card.Title>{toLocalizedString(state.popoverTitle, t)}</Card.Title> : <span />}
109
- <Card.Close onClick={handleClose} />
115
+ {state.popoverTitle ? (
116
+ <Card.Title onClick={handleNavigate}>{toLocalizedString(state.popoverTitle, t)}</Card.Title>
117
+ ) : (
118
+ <span />
119
+ )}
120
+ <Card.CloseIconButton onClick={handleClose} />
110
121
  </Card.Toolbar>
111
- <Surface role='card--content' data={state.popoverContent} limit={1} />
122
+ <Surface.Surface role='card--content' data={state.popoverContent} limit={1} />
112
123
  </Card.Root>
113
124
  )}
114
- {state.popoverKind === 'base' && <Surface role='popover' data={state.popoverContent} limit={1} />}
115
125
  </Popover.Viewport>
116
126
  <Popover.Arrow />
117
127
  </Popover.Content>
@@ -0,0 +1,144 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
+ import React, { useMemo } from 'react';
8
+ import { type Mock, expect, fn, screen, userEvent, within } from 'storybook/test';
9
+
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { type ActionGraphProps, createMenuAction } from '@dxos/react-ui-menu';
12
+ import { withRegistry } from '@dxos/storybook-utils';
13
+
14
+ import { translations } from '../../translations';
15
+ import { MobileLayout } from '../MobileLayout';
16
+
17
+ import { AppBar, type AppBarProps } from './AppBar';
18
+
19
+ const buildEmptyActions = (): ActionGraphProps => ({ nodes: [], edges: [] });
20
+
21
+ const buildDefaultActions = (): ActionGraphProps => {
22
+ const result: ActionGraphProps = { nodes: [], edges: [] };
23
+ const actions = [
24
+ createMenuAction('action-edit', () => console.log('Edit'), {
25
+ icon: 'ph--pencil--regular',
26
+ label: 'Edit',
27
+ }),
28
+ createMenuAction('action-share', () => console.log('Share'), {
29
+ icon: 'ph--share--regular',
30
+ label: 'Share',
31
+ }),
32
+ createMenuAction('action-delete', () => console.log('Delete'), {
33
+ icon: 'ph--trash--regular',
34
+ label: 'Delete',
35
+ }),
36
+ ];
37
+ result.nodes.push(...actions);
38
+ result.edges.push(...actions.map((a) => ({ source: 'root', target: a.id, relation: 'child' })));
39
+ return result;
40
+ };
41
+
42
+ type StoryProps = Omit<AppBarProps, 'actions'> & {
43
+ actions: ActionGraphProps;
44
+ };
45
+
46
+ const DefaultStory = ({ actions: actionsProp, ...props }: StoryProps) => {
47
+ const actions = useMemo(() => Atom.make(actionsProp).pipe(Atom.keepAlive), [actionsProp]);
48
+ return (
49
+ <MobileLayout.Root>
50
+ <AppBar {...props} actions={actions} />
51
+ </MobileLayout.Root>
52
+ );
53
+ };
54
+
55
+ const meta = {
56
+ title: 'plugins/plugin-simple-layout/components/AppBar',
57
+ render: DefaultStory,
58
+ decorators: [
59
+ withTheme(),
60
+ withLayout({
61
+ layout: 'column',
62
+ classNames: 'relative',
63
+ }),
64
+ withRegistry,
65
+ ],
66
+ parameters: {
67
+ layout: 'fullscreen',
68
+ translations,
69
+ },
70
+ } satisfies Meta<typeof DefaultStory>;
71
+
72
+ export default meta;
73
+
74
+ type Story = StoryObj<StoryProps>;
75
+
76
+ export const Default: Story = {
77
+ tags: ['test'],
78
+ args: {
79
+ actions: buildDefaultActions(),
80
+ title: 'Document Title',
81
+ showBackButton: true,
82
+ onAction: fn(),
83
+ onBack: fn(),
84
+ },
85
+ play: async ({ args, canvasElement }) => {
86
+ const canvas = within(canvasElement);
87
+
88
+ // Verify the banner renders with the correct title.
89
+ await expect(canvas.getByRole('banner')).toBeInTheDocument();
90
+ await expect(canvas.getByText('Document Title')).toBeInTheDocument();
91
+
92
+ // Test back button click.
93
+ const backButton = canvas.getByRole('button', { name: /back/i });
94
+ await expect(backButton).toBeInTheDocument();
95
+ await userEvent.click(backButton);
96
+ await expect(args.onBack).toHaveBeenCalledTimes(1);
97
+
98
+ // Test actions menu opens and action fires.
99
+ const menuTrigger = canvas.getByRole('button', { name: /actions/i });
100
+ await expect(menuTrigger).toBeInTheDocument();
101
+ await userEvent.click(menuTrigger);
102
+
103
+ // Wait for menu to open and click an action (menu items render in a portal).
104
+ const editAction = await screen.findByRole('menuitem', { name: /edit/i });
105
+ await userEvent.click(editAction);
106
+ await expect(args.onAction).toHaveBeenCalledTimes(1);
107
+ await expect((args.onAction as Mock).mock.calls[0][0]).toHaveProperty('id', 'action-edit');
108
+ },
109
+ };
110
+
111
+ export const NoBackButton: Story = {
112
+ args: {
113
+ actions: buildDefaultActions(),
114
+ title: 'Home',
115
+ showBackButton: false,
116
+ onAction: (action) => console.log('Action:', action.id),
117
+ },
118
+ };
119
+
120
+ export const LongTitle: Story = {
121
+ args: {
122
+ actions: buildDefaultActions(),
123
+ title: 'This is a very long document title that should be truncated when it exceeds the available space',
124
+ showBackButton: true,
125
+ onBack: () => console.log('Back clicked'),
126
+ onAction: (action) => console.log('Action:', action.id),
127
+ },
128
+ };
129
+
130
+ export const NoActions: Story = {
131
+ args: {
132
+ actions: buildEmptyActions(),
133
+ title: 'Empty Document',
134
+ showBackButton: true,
135
+ onBack: () => console.log('Back clicked'),
136
+ onAction: (action) => console.log('Action:', action.id),
137
+ },
138
+ };
139
+
140
+ export const Empty: Story = {
141
+ args: {
142
+ actions: buildEmptyActions(),
143
+ },
144
+ };
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Atom, useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { Fragment } from 'react';
7
+
8
+ import { IconButton, Popover, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
9
+ import { type ActionExecutor, type ActionGraphProps, Menu, useMenuActions } from '@dxos/react-ui-menu';
10
+ import { mx, osTranslations } from '@dxos/ui-theme';
11
+
12
+ import { meta } from '../../meta';
13
+ import { useMobileLayout } from '../MobileLayout';
14
+
15
+ const APP_BAR_NAME = 'SimpleLayout.AppBar';
16
+
17
+ export type AppBarProps = ThemedClassName<{
18
+ /** Title/label to display in the banner. */
19
+ title?: string;
20
+ /** Action graph atom for the dropdown menu. */
21
+ actions: Atom.Atom<ActionGraphProps>;
22
+ /** Whether to show the back button. */
23
+ showBackButton?: boolean;
24
+ /** Popover anchor ID for the dropdown trigger. */
25
+ popoverAnchorId?: string;
26
+ /** Action executor callback. */
27
+ onAction?: ActionExecutor;
28
+ /** Callback when back button is clicked. */
29
+ onBack?: () => void;
30
+ }>;
31
+
32
+ /**
33
+ * AppBar component that renders a title, optional back button, and actions dropdown.
34
+ */
35
+ export const AppBar = ({
36
+ classNames,
37
+ title,
38
+ actions,
39
+ showBackButton,
40
+ popoverAnchorId,
41
+ onAction,
42
+ onBack,
43
+ }: AppBarProps) => {
44
+ const { t } = useTranslation(meta.id);
45
+ const menu = useMenuActions(actions);
46
+ const actionsValue = useAtomValue(actions);
47
+ const hasActions = actionsValue.nodes.length > 0;
48
+ const { keyboardOpen } = useMobileLayout(APP_BAR_NAME);
49
+
50
+ // Fall back to app name if no title provided.
51
+ const displayTitle = title ?? t('current app name', { ns: osTranslations });
52
+
53
+ // Wrap the menu trigger with Popover.Anchor when the popoverAnchorId is set.
54
+ const AnchorRoot = popoverAnchorId ? Popover.Anchor : Fragment;
55
+
56
+ return (
57
+ <Toolbar.Root
58
+ role='banner'
59
+ classNames={mx(
60
+ 'grid grid-cols-[var(--dx-rail-size)_1fr_var(--dx-rail-size)] items-center',
61
+ 'dx-density-fine',
62
+ classNames,
63
+ )}
64
+ >
65
+ {keyboardOpen ? (
66
+ <IconButton variant='ghost' icon='ph--x--regular' iconOnly label={t('done label')} />
67
+ ) : showBackButton ? (
68
+ <IconButton variant='ghost' icon='ph--caret-left--regular' iconOnly label={t('back label')} onClick={onBack} />
69
+ ) : (
70
+ <div />
71
+ )}
72
+ <h1 className='text-center truncate font-thin uppercase'>{displayTitle}</h1>
73
+ {hasActions ? (
74
+ <AnchorRoot>
75
+ <Menu.Root {...menu} caller={meta.id} onAction={onAction}>
76
+ <Menu.Trigger asChild>
77
+ <IconButton
78
+ variant='ghost'
79
+ icon='ph--dots-three-vertical--regular'
80
+ iconOnly
81
+ label={t('actions menu label')}
82
+ />
83
+ </Menu.Trigger>
84
+ <Menu.Content />
85
+ </Menu.Root>
86
+ </AnchorRoot>
87
+ ) : (
88
+ <span />
89
+ )}
90
+ </Toolbar.Root>
91
+ );
92
+ };
93
+
94
+ AppBar.displayName = APP_BAR_NAME;
@@ -0,0 +1,96 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { Surface } from '@dxos/app-framework/ui';
8
+ import { getCompanionVariant } from '@dxos/app-toolkit';
9
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
10
+ import { type Node, useNode } from '@dxos/plugin-graph';
11
+ import { ErrorFallback, Panel } from '@dxos/react-ui';
12
+ import { Menu, useMenuActions } from '@dxos/react-ui-menu';
13
+
14
+ import { useCompanions, useDrawerActions, useSimpleLayoutState } from '../../hooks';
15
+ import { ContentLoading } from '../ContentLoading';
16
+
17
+ const DRAWER_NAME = 'SimpleLayout.Drawer';
18
+
19
+ /**
20
+ * Companion drawer component.
21
+ */
22
+ export const Drawer = () => {
23
+ const { graph } = useAppGraph();
24
+ const { state: layoutState } = useSimpleLayoutState();
25
+
26
+ const placeholder = useMemo(() => <ContentLoading />, []);
27
+
28
+ // Get all companions for the current active (primary) item.
29
+ const activeId = layoutState.active ?? layoutState.workspace;
30
+ const companions = useCompanions(activeId);
31
+ const { companionId, variant } = useSelectedCompanion(companions, layoutState.companionVariant);
32
+
33
+ // Get node for the selected companion.
34
+ const node = useNode(graph, companionId);
35
+ const parentNode = useNode(graph, activeId);
36
+
37
+ // Build Surface data for the companion content.
38
+ const data = useMemo(() => {
39
+ return (
40
+ node && {
41
+ attendableId: companionId,
42
+ subject: node.data,
43
+ companionTo: parentNode?.data,
44
+ properties: node.properties,
45
+ variant,
46
+ }
47
+ );
48
+ }, [companionId, node, parentNode, variant]);
49
+
50
+ // Get drawer actions (tabs + toolbar buttons).
51
+ const { actions, onAction } = useDrawerActions(DRAWER_NAME);
52
+ const menu = useMenuActions(actions);
53
+
54
+ return (
55
+ <Panel.Root>
56
+ <Panel.Toolbar>
57
+ <Menu.Root {...menu} alwaysActive onAction={onAction}>
58
+ <Menu.Toolbar density='coarse' />
59
+ </Menu.Root>
60
+ </Panel.Toolbar>
61
+ <Panel.Content asChild>
62
+ <Surface.Surface role='article' data={data} limit={1} fallback={ErrorFallback} placeholder={placeholder} />
63
+ </Panel.Content>
64
+ </Panel.Root>
65
+ );
66
+ };
67
+
68
+ Drawer.displayName = DRAWER_NAME;
69
+
70
+ /**
71
+ * Resolves which companion to show based on variant preference.
72
+ * Falls back to first available if preferred variant not available.
73
+ */
74
+ const useSelectedCompanion = (companions: Node.Node[], preferredVariant?: string) => {
75
+ const selectedCompanion = useMemo(() => {
76
+ if (companions.length === 0) {
77
+ return undefined;
78
+ }
79
+
80
+ // Try to find companion matching the preferred variant.
81
+ if (preferredVariant) {
82
+ const preferred = companions.find((c) => getCompanionVariant(c.id) === preferredVariant);
83
+ if (preferred) {
84
+ return preferred;
85
+ }
86
+ }
87
+
88
+ // Fallback to first companion.
89
+ return companions[0];
90
+ }, [companions, preferredVariant]);
91
+
92
+ const companionId = selectedCompanion?.id;
93
+ const variant = companionId ? getCompanionVariant(companionId) : undefined;
94
+
95
+ return { selectedCompanion, companionId, variant };
96
+ };
@@ -2,80 +2,81 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useMemo } from 'react';
5
+ import React, { useMemo } from 'react';
6
6
 
7
- import { Surface, useAppGraph } from '@dxos/app-framework/react';
8
- import { log } from '@dxos/log';
7
+ import { Surface } from '@dxos/app-framework/ui';
8
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
9
9
  import { useNode } from '@dxos/plugin-graph';
10
- import { Main as NaturalMain } from '@dxos/react-ui';
11
- import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
12
- import { Mosaic } from '@dxos/react-ui-mosaic';
10
+ import { ErrorFallback } from '@dxos/react-ui';
11
+ import { useAttentionAttributes } from '@dxos/react-ui-attention';
13
12
  import { mx } from '@dxos/ui-theme';
14
13
 
15
- import { useSimpleLayoutState } from '../../hooks';
16
- import { ContentError } from '../ContentError';
14
+ import { useAppBarProps, useNavbarActions, useSimpleLayoutState } from '../../hooks';
17
15
  import { ContentLoading } from '../ContentLoading';
16
+ import { useExpandPath } from '../hooks';
17
+ import { useMobileLayout } from '../MobileLayout';
18
18
 
19
- import { Banner } from './Banner';
19
+ import { AppBar } from './AppBar';
20
20
  import { NavBar } from './NavBar';
21
21
 
22
+ const MAIN_NAME = 'SimpleLayout.Main';
23
+
22
24
  /**
23
- * Main root component.
25
+ * Main content component.
24
26
  */
25
27
  export const Main = () => {
26
28
  const { state } = useSimpleLayoutState();
27
29
  const id = state.active ?? state.workspace;
28
- const showNavBar = !state.isPopover;
29
- const { graph } = useAppGraph();
30
- const node = useNode(graph, id);
30
+ const attentionAttrs = useAttentionAttributes(id);
31
+ const { keyboardOpen } = useMobileLayout(MAIN_NAME);
32
+ const { actions, onAction } = useNavbarActions();
33
+ const appBarProps = useAppBarProps();
31
34
 
32
35
  const placeholder = useMemo(() => <ContentLoading />, []);
33
36
 
37
+ const { graph } = useAppGraph();
38
+ const node = useNode(graph, id);
34
39
  const data = useMemo(() => {
35
- const { variant } = parseEntryId(id);
36
40
  return (
37
41
  node && {
38
42
  attendableId: id,
39
43
  subject: node.data,
40
44
  properties: node.properties,
41
45
  popoverAnchorId: state.popoverAnchorId,
42
- variant,
43
46
  }
44
47
  );
45
48
  }, [id, node, node?.data, node?.properties, state.popoverAnchorId]);
46
49
 
47
- const handleActiveIdChange = useCallback((nextActiveId: string | null) => {
48
- log.info('navigate', { nextActiveId });
49
- }, []);
50
+ useExpandPath(id);
51
+
52
+ // TODO(burdon): BUG: When showing ANY statusbar the size progressively shrinks when the keyboard opens/closes.
53
+ const showNavBar = !keyboardOpen && !state.isPopover && state.drawerState === 'closed';
50
54
 
51
55
  return (
52
- <Mosaic.Root>
53
- <NaturalMain.Root complementarySidebarState='closed' navigationSidebarState='closed'>
54
- <NaturalMain.Content
55
- bounce
56
- classNames={mx(
57
- 'dx-mobile-main dx-mobile-main-scroll-area--flush',
58
- 'grid bs-full overflow-hidden',
59
- showNavBar ? 'grid-rows-[min-content_1fr_min-content]' : 'grid-rows-[min-content_1fr]',
60
- )}
61
- >
62
- <Banner classNames='border-be border-separator' node={node} />
63
- <article className='contents'>
64
- <Surface key={id} role='article' data={data} limit={1} fallback={ContentError} placeholder={placeholder} />
65
- </article>
66
- {showNavBar && (
67
- <NavBar classNames='border-bs border-separator' activeId={id} onActiveIdChange={handleActiveIdChange} />
68
- )}
69
- </NaturalMain.Content>
70
- </NaturalMain.Root>
71
- </Mosaic.Root>
56
+ <div
57
+ role='none'
58
+ className={mx(
59
+ 'h-full grid overflow-hidden bg-toolbar-surface',
60
+ showNavBar
61
+ ? 'grid-rows-[var(--dx-rail-action)_1fr_var(--dx-toolbar-size)]'
62
+ : 'grid-rows-[var(--dx-rail-action)_1fr]',
63
+ )}
64
+ {...attentionAttrs}
65
+ >
66
+ <AppBar {...appBarProps} />
67
+ <article className='h-full overflow-hidden bg-base-surface'>
68
+ <Surface.Surface
69
+ key={id}
70
+ role='article'
71
+ data={data}
72
+ limit={1}
73
+ fallback={ErrorFallback}
74
+ placeholder={placeholder}
75
+ />
76
+ </article>
77
+ {showNavBar && <NavBar classNames='border-y border-subdued-separator' actions={actions} onAction={onAction} />}
78
+ </div>
72
79
  );
73
80
  };
74
81
 
75
- // TODO(wittjosiah): Factor out. Copied from deck plugin.
76
- const parseEntryId = (entryId: string) => {
77
- const [id, variant] = entryId.split(ATTENDABLE_PATH_SEPARATOR);
78
- return { id, variant };
79
- };
80
-
81
- Main.displayName = 'SimpleLayout.Main';
82
+ Main.displayName = MAIN_NAME;