@dxos/plugin-simple-layout 0.0.0 → 0.8.4-main.69d29f4

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 (154) hide show
  1. package/dist/lib/browser/chunk-CLPGTNWJ.mjs +29 -0
  2. package/dist/lib/browser/chunk-CLPGTNWJ.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-FK4M7GJV.mjs +613 -0
  4. package/dist/lib/browser/chunk-FK4M7GJV.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +94 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/operation-resolver-LTB63NKP.mjs +168 -0
  9. package/dist/lib/browser/operation-resolver-LTB63NKP.mjs.map +7 -0
  10. package/dist/lib/browser/react-root-6ARAPH3O.mjs +21 -0
  11. package/dist/lib/browser/react-root-6ARAPH3O.mjs.map +7 -0
  12. package/dist/lib/browser/react-surface-SO7B23GS.mjs +39 -0
  13. package/dist/lib/browser/react-surface-SO7B23GS.mjs.map +7 -0
  14. package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs +66 -0
  15. package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs.map +7 -0
  16. package/dist/lib/browser/state-H4IGICBB.mjs +45 -0
  17. package/dist/lib/browser/state-H4IGICBB.mjs.map +7 -0
  18. package/dist/lib/browser/url-handler-7CFGTLNG.mjs +54 -0
  19. package/dist/lib/browser/url-handler-7CFGTLNG.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs +614 -0
  21. package/dist/lib/node-esm/chunk-EGFZAVBD.mjs.map +7 -0
  22. package/dist/lib/node-esm/chunk-MUVVYBUE.mjs +31 -0
  23. package/dist/lib/node-esm/chunk-MUVVYBUE.mjs.map +7 -0
  24. package/dist/lib/node-esm/index.mjs +95 -0
  25. package/dist/lib/node-esm/index.mjs.map +7 -0
  26. package/dist/lib/node-esm/meta.json +1 -0
  27. package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs +169 -0
  28. package/dist/lib/node-esm/operation-resolver-7O6O7T4Q.mjs.map +7 -0
  29. package/dist/lib/node-esm/react-root-2CPA2ZUS.mjs +22 -0
  30. package/dist/lib/node-esm/react-root-2CPA2ZUS.mjs.map +7 -0
  31. package/dist/lib/node-esm/react-surface-FKAV56MO.mjs +40 -0
  32. package/dist/lib/node-esm/react-surface-FKAV56MO.mjs.map +7 -0
  33. package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs +68 -0
  34. package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs.map +7 -0
  35. package/dist/lib/node-esm/state-QIU2LMLT.mjs +46 -0
  36. package/dist/lib/node-esm/state-QIU2LMLT.mjs.map +7 -0
  37. package/dist/lib/node-esm/url-handler-4LYP3JM7.mjs +55 -0
  38. package/dist/lib/node-esm/url-handler-4LYP3JM7.mjs.map +7 -0
  39. package/dist/types/src/SimpleLayoutPlugin.d.ts +7 -0
  40. package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/index.d.ts +7 -0
  42. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  44. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  46. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/react-root/index.d.ts +6 -0
  48. package/dist/types/src/capabilities/react-root/index.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/react-root/react-root.d.ts +9 -0
  50. package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -0
  51. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  52. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  53. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  54. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  55. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +3 -0
  56. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -0
  57. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +14 -0
  58. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -0
  59. package/dist/types/src/capabilities/state/index.d.ts +13 -0
  60. package/dist/types/src/capabilities/state/index.d.ts.map +1 -0
  61. package/dist/types/src/capabilities/state/state.d.ts +19 -0
  62. package/dist/types/src/capabilities/state/state.d.ts.map +1 -0
  63. package/dist/types/src/capabilities/url-handler/index.d.ts +3 -0
  64. package/dist/types/src/capabilities/url-handler/index.d.ts.map +1 -0
  65. package/dist/types/src/capabilities/url-handler/url-handler.d.ts +10 -0
  66. package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -0
  67. package/dist/types/src/components/ContentError.d.ts +5 -0
  68. package/dist/types/src/components/ContentError.d.ts.map +1 -0
  69. package/dist/types/src/components/ContentError.stories.d.ts +35 -0
  70. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -0
  71. package/dist/types/src/components/ContentLoading.d.ts +3 -0
  72. package/dist/types/src/components/ContentLoading.d.ts.map +1 -0
  73. package/dist/types/src/components/ContentLoading.stories.d.ts +13 -0
  74. package/dist/types/src/components/ContentLoading.stories.d.ts.map +1 -0
  75. package/dist/types/src/components/Dialog/Dialog.d.ts +3 -0
  76. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -0
  77. package/dist/types/src/components/Dialog/index.d.ts +2 -0
  78. package/dist/types/src/components/Dialog/index.d.ts.map +1 -0
  79. package/dist/types/src/components/Home/Home.d.ts +7 -0
  80. package/dist/types/src/components/Home/Home.d.ts.map +1 -0
  81. package/dist/types/src/components/Home/index.d.ts +2 -0
  82. package/dist/types/src/components/Home/index.d.ts.map +1 -0
  83. package/dist/types/src/components/Popover/Popover.d.ts +4 -0
  84. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -0
  85. package/dist/types/src/components/Popover/index.d.ts +2 -0
  86. package/dist/types/src/components/Popover/index.d.ts.map +1 -0
  87. package/dist/types/src/components/SimpleLayout/Banner.d.ts +8 -0
  88. package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +1 -0
  89. package/dist/types/src/components/SimpleLayout/Main.d.ts +9 -0
  90. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -0
  91. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +8 -0
  92. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -0
  93. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +39 -0
  94. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts +3 -0
  96. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -0
  97. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +37 -0
  98. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -0
  99. package/dist/types/src/components/SimpleLayout/index.d.ts +2 -0
  100. package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -0
  101. package/dist/types/src/components/Workspace/Workspace.d.ts +9 -0
  102. package/dist/types/src/components/Workspace/Workspace.d.ts.map +1 -0
  103. package/dist/types/src/components/Workspace/index.d.ts +2 -0
  104. package/dist/types/src/components/Workspace/index.d.ts.map +1 -0
  105. package/dist/types/src/components/hooks.d.ts +5 -0
  106. package/dist/types/src/components/hooks.d.ts.map +1 -0
  107. package/dist/types/src/components/index.d.ts +6 -0
  108. package/dist/types/src/components/index.d.ts.map +1 -0
  109. package/dist/types/src/hooks/index.d.ts +2 -0
  110. package/dist/types/src/hooks/index.d.ts.map +1 -0
  111. package/dist/types/src/hooks/useSimpleLayoutState.d.ts +7 -0
  112. package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -0
  113. package/dist/types/src/index.d.ts +2 -0
  114. package/dist/types/src/index.d.ts.map +1 -0
  115. package/dist/types/src/meta.d.ts +3 -0
  116. package/dist/types/src/meta.d.ts.map +1 -0
  117. package/dist/types/src/translations.d.ts +20 -0
  118. package/dist/types/src/translations.d.ts.map +1 -0
  119. package/dist/types/src/types/capabilities.d.ts +31 -0
  120. package/dist/types/src/types/capabilities.d.ts.map +1 -0
  121. package/dist/types/src/types/events.d.ts +6 -0
  122. package/dist/types/src/types/events.d.ts.map +1 -0
  123. package/dist/types/src/types/index.d.ts +3 -0
  124. package/dist/types/src/types/index.d.ts.map +1 -0
  125. package/dist/types/tsconfig.tsbuildinfo +1 -0
  126. package/package.json +29 -24
  127. package/src/SimpleLayoutPlugin.ts +20 -4
  128. package/src/capabilities/index.ts +3 -0
  129. package/src/capabilities/operation-resolver/operation-resolver.ts +82 -39
  130. package/src/capabilities/react-surface/index.ts +7 -0
  131. package/src/capabilities/react-surface/react-surface.tsx +40 -0
  132. package/src/capabilities/spotlight-dismiss/index.ts +7 -0
  133. package/src/{hooks/useSpotlightDismiss.ts → capabilities/spotlight-dismiss/spotlight-dismiss.ts} +31 -40
  134. package/src/capabilities/state/state.tsx +21 -32
  135. package/src/capabilities/url-handler/index.ts +7 -0
  136. package/src/capabilities/url-handler/url-handler.ts +80 -0
  137. package/src/components/Dialog/Dialog.tsx +14 -14
  138. package/src/components/Home/Home.tsx +53 -61
  139. package/src/components/Popover/Popover.tsx +45 -27
  140. package/src/components/SimpleLayout/Banner.tsx +50 -28
  141. package/src/components/SimpleLayout/Main.tsx +40 -44
  142. package/src/components/SimpleLayout/NavBar.tsx +18 -41
  143. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +2 -9
  144. package/src/components/SimpleLayout/SimpleLayout.tsx +0 -1
  145. package/src/components/Workspace/Workspace.tsx +115 -0
  146. package/src/components/Workspace/index.ts +5 -0
  147. package/src/components/hooks.ts +30 -0
  148. package/src/components/index.ts +1 -0
  149. package/src/hooks/index.ts +1 -1
  150. package/src/hooks/useSimpleLayoutState.ts +30 -0
  151. package/src/types/capabilities.ts +8 -1
  152. package/src/types/events.ts +14 -0
  153. package/src/types/index.ts +1 -0
  154. /package/src/components/SimpleLayout/{NavBarstories.tsx → NavBar.stories.tsx} +0 -0
@@ -4,33 +4,33 @@
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/react';
8
8
  import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
9
9
 
10
- import { SimpleLayoutState } from '../../types';
10
+ import { useSimpleLayoutState } from '../../hooks';
11
11
  import { ContentError } from '../ContentError';
12
12
 
13
13
  export const Dialog = () => {
14
- const layout = useCapability(SimpleLayoutState);
14
+ const { state, updateState } = useSimpleLayoutState();
15
15
 
16
- const DialogRoot = layout.dialogType === 'alert' ? AlertDialog.Root : NaturalDialog.Root;
17
- const DialogOverlay = layout.dialogType === 'alert' ? AlertDialog.Overlay : NaturalDialog.Overlay;
16
+ const DialogRoot = state.dialogType === 'alert' ? AlertDialog.Root : NaturalDialog.Root;
17
+ const DialogOverlay = state.dialogType === 'alert' ? AlertDialog.Overlay : NaturalDialog.Overlay;
18
18
 
19
19
  return (
20
20
  <DialogRoot
21
- modal={layout.dialogBlockAlign !== 'end'}
22
- open={layout.dialogOpen}
23
- onOpenChange={(nextOpen) => (layout.dialogOpen = nextOpen)}
21
+ modal={state.dialogBlockAlign !== 'end'}
22
+ open={state.dialogOpen}
23
+ onOpenChange={(nextOpen) => updateState((s) => ({ ...s, dialogOpen: nextOpen }))}
24
24
  >
25
- {layout.dialogBlockAlign === 'end' ? (
26
- <Surface role='dialog' data={layout.dialogContent} limit={1} fallback={ContentError} placeholder={<div />} />
25
+ {state.dialogBlockAlign === 'end' ? (
26
+ <Surface role='dialog' data={state.dialogContent} limit={1} fallback={ContentError} />
27
27
  ) : (
28
28
  <DialogOverlay
29
- blockAlign={layout.dialogBlockAlign}
30
- classNames={layout.dialogOverlayClasses}
31
- style={layout.dialogOverlayStyle}
29
+ blockAlign={state.dialogBlockAlign}
30
+ classNames={state.dialogOverlayClasses}
31
+ style={state.dialogOverlayStyle}
32
32
  >
33
- <Surface role='dialog' data={layout.dialogContent} limit={1} fallback={ContentError} />
33
+ <Surface role='dialog' data={state.dialogContent} limit={1} fallback={ContentError} />
34
34
  </DialogOverlay>
35
35
  )}
36
36
  </DialogRoot>
@@ -6,69 +6,80 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
6
6
 
7
7
  import { Common } from '@dxos/app-framework';
8
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';
9
+ import { Node, useConnections } from '@dxos/plugin-graph';
10
+ import { Avatar, Icon, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
+ import { Card, Layout, Mosaic, type StackTileComponent } from '@dxos/react-ui-mosaic';
12
12
  import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
13
13
  import { mx } from '@dxos/ui-theme';
14
+ import { byPosition } from '@dxos/util';
14
15
 
15
16
  import { meta } from '../../meta';
17
+ import { useLoadDescendents } from '../hooks';
16
18
 
17
- type HomeProps = ThemedClassName;
19
+ export type HomeProps = {};
18
20
 
19
- export const Home = ({ classNames }: HomeProps) => {
21
+ /**
22
+ * Home screen.
23
+ */
24
+ export const Home = (_: HomeProps) => {
20
25
  const { t } = useTranslation(meta.id);
21
- const workspaces = useWorkspaces();
26
+ const userAccountItem = useItemsByDisposition('user-account')[0];
27
+ const pinnedItems = useItemsByDisposition('pin-end', true);
28
+ const workspaceItems = useItemsByDisposition('workspace');
22
29
  useLoadDescendents(Node.RootId);
23
30
 
31
+ const items = useMemo(
32
+ () => [...(userAccountItem ? [userAccountItem] : []), ...pinnedItems, ...workspaceItems],
33
+ [userAccountItem, pinnedItems, workspaceItems],
34
+ );
35
+
24
36
  const { results, handleSearch } = useSearchListResults({
25
- items: workspaces,
37
+ items,
26
38
  extract: (node) => toLocalizedString(node.properties.label, t),
27
39
  });
28
40
 
29
41
  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'>
42
+ <Layout.Main toolbar>
43
+ <SearchList.Root onSearch={handleSearch}>
44
+ <Toolbar.Root>
34
45
  <SearchList.Input placeholder={t('search placeholder')} autoFocus />
35
- </div>
46
+ </Toolbar.Root>
36
47
  <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>
48
+ <Mosaic.Container asChild>
49
+ <Mosaic.Viewport padding>
50
+ <Mosaic.Stack items={results} getId={(node) => node.id} Tile={WorkspaceTile} />
51
+ </Mosaic.Viewport>
52
+ </Mosaic.Container>
42
53
  </SearchList.Content>
43
54
  </SearchList.Root>
44
- </div>
55
+ </Layout.Main>
45
56
  );
46
57
  };
47
58
 
48
- const Workspace = ({ node }: { node: Node.Node }) => {
59
+ const WorkspaceTile: StackTileComponent<Node.Node> = ({ data }) => {
49
60
  const { t } = useTranslation(meta.id);
50
61
  const { invokePromise } = useOperationInvoker();
51
62
  const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
52
63
  const ref = useRef<HTMLDivElement>(null);
53
64
 
54
65
  const handleSelect = useCallback(
55
- () => invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: node.id }),
56
- [invokePromise, node.id],
66
+ () => invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: data.id }),
67
+ [invokePromise, data.id],
57
68
  );
58
69
 
59
- useLoadDescendents(node.id);
70
+ useLoadDescendents(data.id);
60
71
 
61
- const name = toLocalizedString(node.properties.label, t);
62
- const isSelected = selectedValue === node.id;
72
+ const name = toLocalizedString(data.properties.label, t);
73
+ const isSelected = selectedValue === data.id;
63
74
 
64
75
  // Register this workspace with the search context.
65
76
  useEffect(() => {
66
77
  if (ref.current) {
67
- registerItem(node.id, ref.current, handleSelect);
78
+ registerItem(data.id, ref.current, handleSelect);
68
79
  }
69
80
 
70
- return () => unregisterItem(node.id);
71
- }, [node.id, handleSelect, registerItem, unregisterItem]);
81
+ return () => unregisterItem(data.id);
82
+ }, [data.id, handleSelect, registerItem, unregisterItem]);
72
83
 
73
84
  // Scroll into view when selected.
74
85
  useEffect(() => {
@@ -81,17 +92,18 @@ const Workspace = ({ node }: { node: Node.Node }) => {
81
92
  <Card.Root
82
93
  ref={ref}
83
94
  role='button'
84
- tabIndex={-1}
95
+ fullWidth
96
+ tabIndex={-1} // TODO(burdon): Use Mosaic.Focus.
85
97
  data-selected={isSelected}
86
98
  classNames={mx('dx-focus-ring', isSelected && 'bg-hoverOverlay')}
87
99
  onClick={handleSelect}
88
100
  >
89
- <Card.Chrome classNames='grid grid-cols-[min-content_1fr_min-content] items-center gap-cardSpacingInline pie-cardSpacingInline'>
101
+ <Card.Toolbar density='coarse'>
90
102
  <Avatar.Root>
91
103
  <Avatar.Content
92
- hue={node.properties.hue}
93
- icon={node.properties.icon}
94
- hueVariant='surface'
104
+ icon={data.properties.icon}
105
+ hue={data.properties.hue}
106
+ hueVariant='transparent'
95
107
  variant='square'
96
108
  size={12}
97
109
  fallback={name}
@@ -99,40 +111,20 @@ const Workspace = ({ node }: { node: Node.Node }) => {
99
111
  <Avatar.Label>{name}</Avatar.Label>
100
112
  <Icon icon='ph--caret-right--regular' />
101
113
  </Avatar.Root>
102
- </Card.Chrome>
114
+ </Card.Toolbar>
103
115
  </Card.Root>
104
116
  );
105
117
  };
106
118
 
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]);
119
+ /** Filters nodes by disposition. */
120
+ const filterItems = (node: Node.Node, disposition: string) => {
121
+ return node.properties.disposition === disposition;
122
122
  };
123
123
 
124
- const useWorkspaces = () => {
124
+ /** Returns root-level items filtered by disposition. */
125
+ const useItemsByDisposition = (disposition: string, sort = false) => {
125
126
  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);
127
+ const connections = useConnections(graph, Node.RootId);
128
+ const filtered = connections.filter((node) => filterItems(node, disposition));
129
+ return sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
138
130
  };
@@ -5,10 +5,12 @@
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, useCapability } from '@dxos/app-framework/react';
9
- import { Popover, type PopoverContentInteractOutsideEvent } from '@dxos/react-ui';
8
+ import { Surface } from '@dxos/app-framework/react';
9
+ import { Popover, type PopoverContentInteractOutsideEvent, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
+ import { Card } from '@dxos/react-ui-mosaic';
10
11
 
11
- import { SimpleLayoutState } from '../../types';
12
+ import { useSimpleLayoutState } from '../../hooks';
13
+ import { meta } from '../../meta';
12
14
 
13
15
  const DEBOUNCE_DELAY = 40;
14
16
 
@@ -19,7 +21,7 @@ type LayoutPopoverContextValue = {
19
21
  const [LayoutPopoverProvider, useLayoutPopoverContext] = createContext<LayoutPopoverContextValue>('LayoutPopover');
20
22
 
21
23
  export const PopoverRoot = ({ children }: PropsWithChildren) => {
22
- const layout = useCapability(SimpleLayoutState);
24
+ const { state } = useSimpleLayoutState();
23
25
  const [open, setOpen] = useState(false);
24
26
  const virtualRef = useRef<HTMLButtonElement | null>(null);
25
27
  const [virtualIter, setVirtualIter] = useState(0);
@@ -29,22 +31,22 @@ export const PopoverRoot = ({ children }: PropsWithChildren) => {
29
31
  // the anchor further down the tree or measuring the virtual trigger's client rect.
30
32
  useEffect(() => {
31
33
  setOpen(false);
32
- if (layout.popoverOpen) {
34
+ if (state.popoverOpen) {
33
35
  if (debounceRef.current) {
34
36
  clearTimeout(debounceRef.current);
35
37
  }
36
- if (layout.popoverAnchor && virtualRef.current !== layout.popoverAnchor) {
37
- virtualRef.current = layout.popoverAnchor ?? null;
38
+ if (state.popoverAnchor && virtualRef.current !== state.popoverAnchor) {
39
+ virtualRef.current = state.popoverAnchor ?? null;
38
40
  setVirtualIter((iter) => iter + 1);
39
41
  }
40
42
  debounceRef.current = setTimeout(() => setOpen(true), DEBOUNCE_DELAY);
41
43
  }
42
- }, [layout.popoverOpen, layout.popoverAnchorId, layout.popoverAnchor, layout.popoverContent]);
44
+ }, [state.popoverOpen, state.popoverAnchorId, state.popoverAnchor, state.popoverContent]);
43
45
 
44
46
  return (
45
47
  <LayoutPopoverProvider setOpen={setOpen}>
46
48
  <Popover.Root modal={false} open={open}>
47
- {layout.popoverAnchor && <Popover.VirtualTrigger key={virtualIter} virtualRef={virtualRef} />}
49
+ {state.popoverAnchor && <Popover.VirtualTrigger key={virtualIter} virtualRef={virtualRef} />}
48
50
  {children}
49
51
  </Popover.Root>
50
52
  </LayoutPopoverProvider>
@@ -52,10 +54,22 @@ export const PopoverRoot = ({ children }: PropsWithChildren) => {
52
54
  };
53
55
 
54
56
  export const PopoverContent = () => {
55
- const layout = useCapability(SimpleLayoutState);
57
+ const { t } = useTranslation(meta.id);
58
+ const { state, updateState } = useSimpleLayoutState();
56
59
  const { setOpen } = useLayoutPopoverContext('PopoverContent');
57
60
 
58
- const handleClose = useCallback(
61
+ const handleClose = useCallback(() => {
62
+ setOpen(false);
63
+ updateState((s) => ({
64
+ ...s,
65
+ popoverOpen: false,
66
+ popoverAnchor: undefined,
67
+ popoverAnchorId: undefined,
68
+ popoverSide: undefined,
69
+ }));
70
+ }, [setOpen, updateState]);
71
+
72
+ const handleInteractOutside = useCallback(
59
73
  (event: KeyboardEvent | PopoverContentInteractOutsideEvent) => {
60
74
  if (
61
75
  // TODO(thure): CodeMirror should not focus itself when it updates.
@@ -64,36 +78,40 @@ export const PopoverContent = () => {
64
78
  ) {
65
79
  event.preventDefault();
66
80
  } else {
67
- setOpen(false);
68
- layout.popoverOpen = false;
69
- layout.popoverAnchor = undefined;
70
- layout.popoverAnchorId = undefined;
71
- layout.popoverSide = undefined;
81
+ handleClose();
72
82
  }
73
83
  },
74
- [setOpen],
84
+ [handleClose],
75
85
  );
76
86
 
77
87
  const collisionBoundaries: HTMLElement[] = useMemo(() => {
78
- const closest = layout.popoverAnchor?.closest('[data-popover-collision-boundary]') as
79
- | HTMLElement
80
- | null
81
- | undefined;
88
+ const closest = state.popoverAnchor?.closest('[data-popover-collision-boundary]') as HTMLElement | null | undefined;
82
89
  return closest ? [closest] : [];
83
- }, [layout.popoverAnchor]);
90
+ }, [state.popoverAnchor]);
84
91
 
85
92
  return (
86
93
  <Popover.Portal>
87
94
  <Popover.Content
88
- side={layout.popoverSide}
89
- onInteractOutside={handleClose}
90
- onEscapeKeyDown={handleClose}
91
- collisionBoundary={collisionBoundaries}
95
+ side={state.popoverSide}
92
96
  sticky='always'
93
97
  hideWhenDetached
98
+ collisionBoundary={collisionBoundaries}
99
+ onInteractOutside={handleInteractOutside}
100
+ onEscapeKeyDown={handleInteractOutside}
94
101
  >
95
102
  <Popover.Viewport>
96
- <Surface role='card--popover' data={layout.popoverContent} limit={1} />
103
+ {state.popoverKind === 'card' && (
104
+ <Card.Root>
105
+ <Card.Toolbar>
106
+ {/* TODO(wittjosiah): Cleaner way to handle no drag handle in toolbar? */}
107
+ <span />
108
+ {state.popoverTitle ? <Card.Title>{toLocalizedString(state.popoverTitle, t)}</Card.Title> : <span />}
109
+ <Card.Close onClick={handleClose} />
110
+ </Card.Toolbar>
111
+ <Surface role='card--content' data={state.popoverContent} limit={1} />
112
+ </Card.Root>
113
+ )}
114
+ {state.popoverKind === 'base' && <Surface role='popover' data={state.popoverContent} limit={1} />}
97
115
  </Popover.Viewport>
98
116
  <Popover.Arrow />
99
117
  </Popover.Content>
@@ -2,47 +2,69 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback } from 'react';
5
+ import React, { useCallback, useMemo } from 'react';
6
6
 
7
7
  import { Common } from '@dxos/app-framework';
8
- import { useCapability, useOperationInvoker } from '@dxos/app-framework/react';
9
- import { type Node } from '@dxos/plugin-graph';
10
- import { IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
- import { mx, osTranslations, surfaceZIndex } from '@dxos/ui-theme';
8
+ import { useAppGraph, useOperationInvoker } from '@dxos/app-framework/react';
9
+ import { Graph, Node } from '@dxos/plugin-graph';
10
+ import { IconButton, type ThemedClassName, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
+ import { mx, osTranslations } from '@dxos/ui-theme';
12
12
 
13
+ import { useSimpleLayoutState } from '../../hooks';
13
14
  import { meta } from '../../meta';
14
- import { SimpleLayoutState } from '../../types';
15
15
 
16
- export type BannerProps = {
17
- node?: Node.Node;
16
+ /**
17
+ * Check if an item is a direct child of a workspace or collection.
18
+ * Returns true if any parent node has disposition 'workspace' or 'collection'.
19
+ */
20
+ const isWorkspaceOrCollectionChild = (graph: Graph.ReadableGraph, itemId: string): boolean => {
21
+ const parents = Graph.getConnections(graph, itemId, 'inbound');
22
+ return parents.some(
23
+ (node) => node.properties.disposition === 'workspace' || node.properties.disposition === 'collection',
24
+ );
18
25
  };
19
26
 
20
- export const Banner = ({ node }: BannerProps) => {
27
+ export type BannerProps = ThemedClassName<{
28
+ node?: Node.Node;
29
+ }>;
30
+
31
+ export const Banner = ({ node, classNames }: BannerProps) => {
21
32
  const { t } = useTranslation(meta.id);
22
- const layout = useCapability(SimpleLayoutState);
33
+ const { state } = useSimpleLayoutState();
23
34
  const { invokePromise } = useOperationInvoker();
24
- const label = node ? toLocalizedString(node.properties.label, t) : t('current app name', { ns: osTranslations });
35
+ const { graph } = useAppGraph();
36
+
37
+ const label = (node && toLocalizedString(node.properties.label, t)) ?? t('current app name', { ns: osTranslations });
38
+
39
+ // Check if current active item is a top-level workspace/collection child.
40
+ const isTopLevelItem = useMemo(() => {
41
+ if (!state.active) {
42
+ return false;
43
+ }
44
+ return isWorkspaceOrCollectionChild(graph, state.active);
45
+ }, [graph, state.active]);
25
46
 
26
47
  const handleClick = useCallback(async () => {
27
- if (layout.active) {
28
- await invokePromise(Common.LayoutOperation.Close, { subject: [layout.active] });
48
+ if (state.active) {
49
+ // If history is empty and this is a top-level item, go to home.
50
+ if (state.history.length === 0 && isTopLevelItem) {
51
+ await invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: Node.RootId });
52
+ } else {
53
+ // Otherwise, close (which will pop from history or clear active).
54
+ await invokePromise(Common.LayoutOperation.Close, { subject: [state.active] });
55
+ }
29
56
  } else {
30
- await invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: 'default' });
57
+ await invokePromise(Common.LayoutOperation.SwitchWorkspace, { subject: Node.RootId });
31
58
  }
32
- }, [invokePromise, layout.active]);
59
+ }, [invokePromise, state.active, state.history.length, isTopLevelItem]);
60
+
61
+ if (!node) {
62
+ return null;
63
+ }
33
64
 
34
65
  return (
35
- // Note that the HTML5 element `header` has a default role of `banner`, hence the name of this component.
36
- // It should not be confused with the `heading` role (elements h1-6).
37
- // TODO(burdon): Fixed or not?
38
- <header
39
- className={mx(
40
- '_fixed flex items-center gap-2 pli-2 block-start-0 inset-inline-0 bs-[--dx-mobile-topbar-content-height,48px] bg-baseSurface border-be border-separator',
41
- 'grid grid-cols-[min-content_1fr_min-content]',
42
- surfaceZIndex({ level: 'menu' }),
43
- )}
44
- >
45
- {node ? (
66
+ <Toolbar.Root role='banner' classNames={mx('grid grid-cols-[var(--rail-size)_1fr_var(--rail-size)]', classNames)}>
67
+ {node.id !== Node.RootId ? (
46
68
  <IconButton
47
69
  iconOnly
48
70
  variant='ghost'
@@ -54,7 +76,7 @@ export const Banner = ({ node }: BannerProps) => {
54
76
  <div />
55
77
  )}
56
78
  <h1 className={'grow text-center truncate font-medium'}>{label}</h1>
57
- {/* TODO(burdon): Menu. */}
58
- </header>
79
+ <IconButton iconOnly variant='ghost' icon='ph--dots-three-vertical--regular' label={t('menu label')} />
80
+ </Toolbar.Root>
59
81
  );
60
82
  };
@@ -2,79 +2,73 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { Activity, useMemo } from 'react';
5
+ import React, { useCallback, useMemo } from 'react';
6
6
 
7
- import { Surface, useAppGraph, useCapability } from '@dxos/app-framework/react';
7
+ import { Surface, useAppGraph } from '@dxos/app-framework/react';
8
+ import { log } from '@dxos/log';
8
9
  import { useNode } from '@dxos/plugin-graph';
9
10
  import { Main as NaturalMain } from '@dxos/react-ui';
10
11
  import { ATTENDABLE_PATH_SEPARATOR } from '@dxos/react-ui-attention';
12
+ import { Mosaic } from '@dxos/react-ui-mosaic';
11
13
  import { mx } from '@dxos/ui-theme';
12
14
 
13
- import { SimpleLayoutState } from '../../types';
15
+ import { useSimpleLayoutState } from '../../hooks';
14
16
  import { ContentError } from '../ContentError';
15
17
  import { ContentLoading } from '../ContentLoading';
16
- import { Home } from '../Home';
17
18
 
18
19
  import { Banner } from './Banner';
19
20
  import { NavBar } from './NavBar';
20
21
 
22
+ /**
23
+ * Main root component.
24
+ */
21
25
  export const Main = () => {
22
- const layout = useCapability(SimpleLayoutState);
23
- const id = layout.active ?? layout.workspace;
26
+ const { state } = useSimpleLayoutState();
27
+ const id = state.active ?? state.workspace;
28
+ const showNavBar = !state.isPopover;
24
29
  const { graph } = useAppGraph();
25
30
  const node = useNode(graph, id);
26
31
 
27
32
  const placeholder = useMemo(() => <ContentLoading />, []);
28
33
 
29
- const { variant } = parseEntryId(id);
30
- const data = useMemo(
31
- () =>
34
+ const data = useMemo(() => {
35
+ const { variant } = parseEntryId(id);
36
+ return (
32
37
  node && {
33
38
  attendableId: id,
34
39
  subject: node.data,
35
40
  properties: node.properties,
41
+ popoverAnchorId: state.popoverAnchorId,
36
42
  variant,
37
- popoverAnchorId: layout.popoverAnchorId,
38
- },
39
- [node, node?.data, node?.properties, layout.popoverAnchorId, variant, id],
40
- );
41
-
42
- const handleActiveIdChange = (nextActiveId: string | null) => {
43
- // eslint-disable-next-line no-console
44
- console.log('[navigate]', nextActiveId);
45
- };
43
+ }
44
+ );
45
+ }, [id, node, node?.data, node?.properties, state.popoverAnchorId]);
46
46
 
47
- const showNavBar = !layout.isPopover;
47
+ const handleActiveIdChange = useCallback((nextActiveId: string | null) => {
48
+ log.info('navigate', { nextActiveId });
49
+ }, []);
48
50
 
49
51
  return (
50
- <NaturalMain.Root complementarySidebarState='closed' navigationSidebarState='closed'>
51
- <NaturalMain.Content bounce classNames='dx-mobile-main dx-mobile-main-scroll-area--flush !overflow-y-auto'>
52
- <div
53
- className={mx(
54
- 'bs-full overflow-hidden grid',
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',
55
59
  showNavBar ? 'grid-rows-[min-content_1fr_min-content]' : 'grid-rows-[min-content_1fr]',
56
60
  )}
57
61
  >
58
- <Banner node={node} />
59
- <Activity mode={id === 'default' ? 'visible' : 'hidden'}>
60
- <Home />
61
- </Activity>
62
- <Activity mode={id !== 'default' ? 'visible' : 'hidden'}>
63
- <section>
64
- <Surface
65
- key={id}
66
- role='article'
67
- data={data}
68
- limit={1}
69
- fallback={ContentError}
70
- placeholder={placeholder}
71
- />
72
- </section>
73
- </Activity>
74
- {showNavBar && <NavBar activeId={id} onActiveIdChange={handleActiveIdChange} />}
75
- </div>
76
- </NaturalMain.Content>
77
- </NaturalMain.Root>
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>
78
72
  );
79
73
  };
80
74
 
@@ -83,3 +77,5 @@ const parseEntryId = (entryId: string) => {
83
77
  const [id, variant] = entryId.split(ATTENDABLE_PATH_SEPARATOR);
84
78
  return { id, variant };
85
79
  };
80
+
81
+ Main.displayName = 'SimpleLayout.Main';