@dxos/plugin-simple-layout 0.8.4-main.2244d791bb → 0.8.4-main.422d1c7879

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 (224) hide show
  1. package/dist/lib/browser/index.mjs +35 -56
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +35 -55
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/SimpleLayoutPlugin.d.ts +1 -1
  8. package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -1
  9. package/dist/types/src/capabilities/app-graph-builder.d.ts +6 -0
  10. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  11. package/dist/types/src/capabilities/index.d.ts +21 -6
  12. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  13. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  14. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  15. package/dist/types/src/capabilities/{react-root/react-root.d.ts → react-root.d.ts} +1 -1
  16. package/dist/types/src/capabilities/react-root.d.ts.map +1 -0
  17. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  18. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  19. package/dist/types/src/capabilities/{spotlight-dismiss/spotlight-dismiss.d.ts → spotlight-dismiss.d.ts} +1 -1
  20. package/dist/types/src/capabilities/spotlight-dismiss.d.ts.map +1 -0
  21. package/dist/types/src/capabilities/{state/state.d.ts → state.d.ts} +2 -2
  22. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  23. package/dist/types/src/capabilities/url-handler.d.ts +12 -0
  24. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -0
  25. package/dist/types/src/components/ContentError.stories.d.ts +26 -21
  26. package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/DebugOverlay/DebugOverlay.d.ts +19 -0
  28. package/dist/types/src/components/DebugOverlay/DebugOverlay.d.ts.map +1 -0
  29. package/dist/types/src/components/DebugOverlay/index.d.ts +2 -0
  30. package/dist/types/src/components/DebugOverlay/index.d.ts.map +1 -0
  31. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -1
  32. package/dist/types/src/components/Home/Home.d.ts.map +1 -1
  33. package/dist/types/src/components/Loading/Loading.d.ts +3 -0
  34. package/dist/types/src/components/Loading/Loading.d.ts.map +1 -0
  35. package/dist/types/src/components/{ContentLoading.stories.d.ts → Loading/Loading.stories.d.ts} +1 -1
  36. package/dist/types/src/components/Loading/Loading.stories.d.ts.map +1 -0
  37. package/dist/types/src/components/Loading/index.d.ts +2 -0
  38. package/dist/types/src/components/Loading/index.d.ts.map +1 -0
  39. package/dist/types/src/components/MobileLayout/MobileLayout.d.ts.map +1 -1
  40. package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/NavBranch/NavBranch.d.ts +11 -0
  42. package/dist/types/src/components/NavBranch/NavBranch.d.ts.map +1 -0
  43. package/dist/types/src/components/NavBranch/index.d.ts +2 -0
  44. package/dist/types/src/components/NavBranch/index.d.ts.map +1 -0
  45. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  46. package/dist/types/src/components/SimpleLayout/AppBar.d.ts +5 -7
  47. package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -1
  48. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts +28 -21
  49. package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts.map +1 -1
  50. package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -1
  51. package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
  52. package/dist/types/src/components/SimpleLayout/NavBar.d.ts +5 -7
  53. package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
  54. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +28 -22
  55. package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -1
  57. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +26 -25
  58. package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/hooks.d.ts +4 -2
  60. package/dist/types/src/components/hooks.d.ts.map +1 -1
  61. package/dist/types/src/components/index.d.ts +3 -2
  62. package/dist/types/src/components/index.d.ts.map +1 -1
  63. package/dist/types/src/hooks/actions.d.ts +4 -5
  64. package/dist/types/src/hooks/actions.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useAppBarProps.d.ts +1 -1
  66. package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -1
  67. package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -1
  68. package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -1
  69. package/dist/types/src/hooks/useSimpleLayoutState.d.ts +1 -1
  70. package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -1
  71. package/dist/types/src/operations/close.d.ts +5 -0
  72. package/dist/types/src/operations/close.d.ts.map +1 -0
  73. package/dist/types/src/operations/index.d.ts +3 -0
  74. package/dist/types/src/operations/index.d.ts.map +1 -0
  75. package/dist/types/src/operations/open.d.ts +5 -0
  76. package/dist/types/src/operations/open.d.ts.map +1 -0
  77. package/dist/types/src/operations/revert-workspace.d.ts +5 -0
  78. package/dist/types/src/operations/revert-workspace.d.ts.map +1 -0
  79. package/dist/types/src/operations/set-layout-mode.d.ts +5 -0
  80. package/dist/types/src/operations/set-layout-mode.d.ts.map +1 -0
  81. package/dist/types/src/operations/set.d.ts +5 -0
  82. package/dist/types/src/operations/set.d.ts.map +1 -0
  83. package/dist/types/src/operations/state-access.d.ts +8 -0
  84. package/dist/types/src/operations/state-access.d.ts.map +1 -0
  85. package/dist/types/src/operations/switch-workspace.d.ts +5 -0
  86. package/dist/types/src/operations/switch-workspace.d.ts.map +1 -0
  87. package/dist/types/src/operations/update-complementary.d.ts +5 -0
  88. package/dist/types/src/operations/update-complementary.d.ts.map +1 -0
  89. package/dist/types/src/operations/update-dialog.d.ts +5 -0
  90. package/dist/types/src/operations/update-dialog.d.ts.map +1 -0
  91. package/dist/types/src/operations/update-popover.d.ts +5 -0
  92. package/dist/types/src/operations/update-popover.d.ts.map +1 -0
  93. package/dist/types/src/operations/update-sidebar.d.ts +5 -0
  94. package/dist/types/src/operations/update-sidebar.d.ts.map +1 -0
  95. package/dist/types/src/translations.d.ts +26 -19
  96. package/dist/types/src/translations.d.ts.map +1 -1
  97. package/dist/types/src/types/capabilities.d.ts +10 -2
  98. package/dist/types/src/types/capabilities.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +41 -30
  101. package/src/SimpleLayoutPlugin.ts +16 -6
  102. package/src/capabilities/app-graph-builder.ts +21 -0
  103. package/src/capabilities/index.ts +13 -6
  104. package/src/capabilities/operation-handler.ts +14 -0
  105. package/src/capabilities/{react-root/react-root.tsx → react-root.tsx} +2 -2
  106. package/src/capabilities/{react-surface/react-surface.tsx → react-surface.tsx} +16 -7
  107. package/src/capabilities/{state/state.tsx → state.tsx} +2 -2
  108. package/src/capabilities/url-handler.ts +161 -0
  109. package/src/components/ContentError.stories.tsx +7 -6
  110. package/src/components/DebugOverlay/DebugOverlay.tsx +96 -0
  111. package/src/components/DebugOverlay/index.ts +5 -0
  112. package/src/components/Dialog/Dialog.tsx +15 -4
  113. package/src/components/Home/Home.tsx +36 -31
  114. package/src/components/{ContentLoading.stories.tsx → Loading/Loading.stories.tsx} +4 -4
  115. package/src/components/{ContentLoading.tsx → Loading/Loading.tsx} +2 -2
  116. package/src/components/Loading/index.ts +5 -0
  117. package/src/components/MobileLayout/MobileLayout.stories.tsx +38 -30
  118. package/src/components/MobileLayout/MobileLayout.tsx +118 -49
  119. package/src/components/{Workspace/Workspace.tsx → NavBranch/NavBranch.tsx} +47 -39
  120. package/src/components/{Workspace → NavBranch}/index.ts +1 -1
  121. package/src/components/Popover/Popover.tsx +12 -8
  122. package/src/components/SimpleLayout/AppBar.stories.tsx +10 -11
  123. package/src/components/SimpleLayout/AppBar.tsx +56 -59
  124. package/src/components/SimpleLayout/Drawer.tsx +38 -36
  125. package/src/components/SimpleLayout/Main.tsx +22 -25
  126. package/src/components/SimpleLayout/NavBar.stories.tsx +8 -9
  127. package/src/components/SimpleLayout/NavBar.tsx +10 -17
  128. package/src/components/SimpleLayout/SimpleLayout.stories.tsx +45 -68
  129. package/src/components/SimpleLayout/SimpleLayout.tsx +33 -34
  130. package/src/components/hooks.ts +8 -8
  131. package/src/components/index.ts +3 -2
  132. package/src/hooks/actions.ts +17 -18
  133. package/src/hooks/useAppBarProps.ts +17 -14
  134. package/src/hooks/useCompanions.ts +1 -1
  135. package/src/hooks/useDrawerActions.ts +15 -13
  136. package/src/hooks/useNavbarActions.ts +13 -12
  137. package/src/hooks/useSimpleLayoutState.ts +1 -1
  138. package/src/meta.ts +1 -1
  139. package/src/operations/close.ts +34 -0
  140. package/src/operations/index.ts +16 -0
  141. package/src/operations/open.ts +63 -0
  142. package/src/operations/revert-workspace.ts +22 -0
  143. package/src/operations/set-layout-mode.ts +12 -0
  144. package/src/operations/set.ts +23 -0
  145. package/src/operations/state-access.ts +19 -0
  146. package/src/operations/switch-workspace.ts +26 -0
  147. package/src/operations/update-complementary.ts +35 -0
  148. package/src/operations/update-dialog.ts +28 -0
  149. package/src/operations/update-popover.ts +35 -0
  150. package/src/operations/update-sidebar.ts +12 -0
  151. package/src/translations.ts +21 -19
  152. package/src/types/capabilities.ts +4 -4
  153. package/dist/lib/browser/chunk-7VLT3S46.mjs +0 -29
  154. package/dist/lib/browser/chunk-7VLT3S46.mjs.map +0 -7
  155. package/dist/lib/browser/chunk-U632CHRU.mjs +0 -1167
  156. package/dist/lib/browser/chunk-U632CHRU.mjs.map +0 -7
  157. package/dist/lib/browser/operation-resolver-BYRIQOQT.mjs +0 -205
  158. package/dist/lib/browser/operation-resolver-BYRIQOQT.mjs.map +0 -7
  159. package/dist/lib/browser/react-root-ZQTWLJYR.mjs +0 -21
  160. package/dist/lib/browser/react-root-ZQTWLJYR.mjs.map +0 -7
  161. package/dist/lib/browser/react-surface-IOYDLMNR.mjs +0 -41
  162. package/dist/lib/browser/react-surface-IOYDLMNR.mjs.map +0 -7
  163. package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs +0 -66
  164. package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs.map +0 -7
  165. package/dist/lib/browser/state-A3PGDWWZ.mjs +0 -48
  166. package/dist/lib/browser/state-A3PGDWWZ.mjs.map +0 -7
  167. package/dist/lib/browser/url-handler-HTIUY6WL.mjs +0 -152
  168. package/dist/lib/browser/url-handler-HTIUY6WL.mjs.map +0 -7
  169. package/dist/lib/node-esm/chunk-UXFYLQJA.mjs +0 -1168
  170. package/dist/lib/node-esm/chunk-UXFYLQJA.mjs.map +0 -7
  171. package/dist/lib/node-esm/chunk-VIDE5UMB.mjs +0 -31
  172. package/dist/lib/node-esm/chunk-VIDE5UMB.mjs.map +0 -7
  173. package/dist/lib/node-esm/operation-resolver-BDTFNCS2.mjs +0 -206
  174. package/dist/lib/node-esm/operation-resolver-BDTFNCS2.mjs.map +0 -7
  175. package/dist/lib/node-esm/react-root-FMAUHDJI.mjs +0 -22
  176. package/dist/lib/node-esm/react-root-FMAUHDJI.mjs.map +0 -7
  177. package/dist/lib/node-esm/react-surface-ZAZRIKZQ.mjs +0 -42
  178. package/dist/lib/node-esm/react-surface-ZAZRIKZQ.mjs.map +0 -7
  179. package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs +0 -68
  180. package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs.map +0 -7
  181. package/dist/lib/node-esm/state-ZCFZTTPL.mjs +0 -49
  182. package/dist/lib/node-esm/state-ZCFZTTPL.mjs.map +0 -7
  183. package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs +0 -153
  184. package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs.map +0 -7
  185. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  186. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  187. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  188. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  189. package/dist/types/src/capabilities/react-root/index.d.ts +0 -6
  190. package/dist/types/src/capabilities/react-root/index.d.ts.map +0 -1
  191. package/dist/types/src/capabilities/react-root/react-root.d.ts.map +0 -1
  192. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  193. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  194. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +0 -5
  195. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  196. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +0 -3
  197. package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +0 -1
  198. package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +0 -1
  199. package/dist/types/src/capabilities/state/index.d.ts +0 -13
  200. package/dist/types/src/capabilities/state/index.d.ts.map +0 -1
  201. package/dist/types/src/capabilities/state/state.d.ts.map +0 -1
  202. package/dist/types/src/capabilities/url-handler/index.d.ts +0 -3
  203. package/dist/types/src/capabilities/url-handler/index.d.ts.map +0 -1
  204. package/dist/types/src/capabilities/url-handler/url-handler.d.ts +0 -12
  205. package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +0 -1
  206. package/dist/types/src/components/ContentError.d.ts +0 -5
  207. package/dist/types/src/components/ContentError.d.ts.map +0 -1
  208. package/dist/types/src/components/ContentLoading.d.ts +0 -3
  209. package/dist/types/src/components/ContentLoading.d.ts.map +0 -1
  210. package/dist/types/src/components/ContentLoading.stories.d.ts.map +0 -1
  211. package/dist/types/src/components/Workspace/Workspace.d.ts +0 -11
  212. package/dist/types/src/components/Workspace/Workspace.d.ts.map +0 -1
  213. package/dist/types/src/components/Workspace/index.d.ts +0 -2
  214. package/dist/types/src/components/Workspace/index.d.ts.map +0 -1
  215. package/src/capabilities/operation-resolver/index.ts +0 -10
  216. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -217
  217. package/src/capabilities/react-root/index.ts +0 -7
  218. package/src/capabilities/react-surface/index.ts +0 -7
  219. package/src/capabilities/spotlight-dismiss/index.ts +0 -7
  220. package/src/capabilities/state/index.ts +0 -9
  221. package/src/capabilities/url-handler/index.ts +0 -7
  222. package/src/capabilities/url-handler/url-handler.ts +0 -157
  223. package/src/components/ContentError.tsx +0 -23
  224. /package/src/capabilities/{spotlight-dismiss/spotlight-dismiss.ts → spotlight-dismiss.ts} +0 -0
@@ -8,14 +8,16 @@ 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 MosaicStackTileComponent } from '@dxos/react-ui-mosaic';
13
- import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
11
+ import { Avatar, Icon, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
12
+ import { Card } from '@dxos/react-ui';
13
+ import { Mosaic, type MosaicStackTileComponent } from '@dxos/react-ui-mosaic';
14
+ import { SearchPanel, useSearchListItem, useSearchListResults } from '@dxos/react-ui-search';
14
15
  import { mx } from '@dxos/ui-theme';
15
- import { byPosition } from '@dxos/util';
16
+ import { byPosition, getHostPlatform, isTauri } from '@dxos/util';
16
17
 
17
- import { meta } from '../../meta';
18
- import { useLoadDescendents } from '../hooks';
18
+ import { meta } from '#meta';
19
+
20
+ import { useExpandPath } from '../hooks';
19
21
 
20
22
  export type HomeProps = {};
21
23
 
@@ -27,7 +29,7 @@ export const Home = (_: HomeProps) => {
27
29
  const userAccountItem = useItemsByDisposition('user-account')[0];
28
30
  const pinnedItems = useItemsByDisposition('pin-end', true);
29
31
  const workspaceItems = useItemsByDisposition('workspace');
30
- useLoadDescendents(Node.RootId);
32
+ useExpandPath(Node.RootId);
31
33
 
32
34
  const items = useMemo(
33
35
  () => [...(userAccountItem ? [userAccountItem] : []), ...pinnedItems, ...workspaceItems],
@@ -39,23 +41,24 @@ export const Home = (_: HomeProps) => {
39
41
  extract: (node) => toLocalizedString(node.properties.label, t),
40
42
  });
41
43
 
44
+ const autoFocus = !isTauri() || getHostPlatform() !== 'ios';
45
+
42
46
  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>
47
+ <SearchPanel onSearch={handleSearch}>
48
+ <Mosaic.Container asChild>
49
+ <ScrollArea.Root centered padding thin>
50
+ <ScrollArea.Viewport>
51
+ <Mosaic.Stack
52
+ classNames='gap-1'
53
+ draggable={false}
54
+ items={results}
55
+ getId={(item) => item.id}
56
+ Tile={WorkspaceTile}
57
+ />
58
+ </ScrollArea.Viewport>
59
+ </ScrollArea.Root>
60
+ </Mosaic.Container>
61
+ </SearchPanel>
59
62
  );
60
63
  };
61
64
 
@@ -68,7 +71,7 @@ const WorkspaceTile: MosaicStackTileComponent<Node.Node> = (props) => {
68
71
  const isSelected = selectedValue === data.id;
69
72
  const cardRef = useRef<HTMLDivElement>(null);
70
73
 
71
- useLoadDescendents(data.id);
74
+ useExpandPath(data.id);
72
75
 
73
76
  const handleSelect = useCallback(
74
77
  () => invokePromise(LayoutOperation.SwitchWorkspace, { subject: data.id }),
@@ -97,21 +100,21 @@ const WorkspaceTile: MosaicStackTileComponent<Node.Node> = (props) => {
97
100
  fullWidth
98
101
  tabIndex={-1} // TODO(burdon): Use Mosaic.Focus.
99
102
  data-selected={isSelected}
100
- classNames={mx('dx-focus-ring', isSelected && 'bg-hoverOverlay')}
103
+ classNames={mx('dx-focus-ring', isSelected && 'bg-hover-overlay')}
101
104
  onClick={handleSelect}
102
105
  ref={cardRef}
103
106
  >
104
- <Card.Toolbar density='coarse'>
107
+ <Card.Toolbar density='fine'>
105
108
  <Avatar.Root>
106
109
  <Avatar.Content
107
110
  icon={data.properties.icon}
108
111
  hue={data.properties.hue}
109
112
  hueVariant='transparent'
110
113
  variant='square'
111
- size={12}
114
+ size={8}
112
115
  fallback={name}
113
116
  />
114
- <Avatar.Label>{name}</Avatar.Label>
117
+ <Avatar.Label classNames='cursor-pointer'>{name}</Avatar.Label>
115
118
  <Icon icon='ph--caret-right--regular' />
116
119
  </Avatar.Root>
117
120
  </Card.Toolbar>
@@ -127,7 +130,9 @@ const filterItems = (node: Node.Node, disposition: string) => {
127
130
  /** Returns root-level items filtered by disposition. */
128
131
  const useItemsByDisposition = (disposition: string, sort = false) => {
129
132
  const { graph } = useAppGraph();
130
- const connections = useConnections(graph, Node.RootId);
131
- const filtered = connections.filter((node) => filterItems(node, disposition));
132
- return sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
133
+ const connections = useConnections(graph, Node.RootId, 'child');
134
+ return useMemo(() => {
135
+ const filtered = connections.filter((node) => filterItems(node, disposition));
136
+ return sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
137
+ }, [connections, disposition, sort]);
133
138
  };
@@ -6,16 +6,16 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
7
  import { withTheme } from '@dxos/react-ui/testing';
8
8
 
9
- import { ContentLoading } from './ContentLoading';
9
+ import { Loading } from './Loading';
10
10
 
11
11
  const meta = {
12
- title: 'plugins/plugin-simple-layout/ContentLoading',
13
- component: ContentLoading,
12
+ title: 'plugins/plugin-simple-layout/components/Loading',
13
+ component: Loading,
14
14
  decorators: [withTheme()],
15
15
  parameters: {
16
16
  layout: 'centered',
17
17
  },
18
- } satisfies Meta<typeof ContentLoading>;
18
+ } satisfies Meta<typeof Loading>;
19
19
 
20
20
  export default meta;
21
21
 
@@ -5,6 +5,6 @@
5
5
  import React from 'react';
6
6
 
7
7
  // TODO(burdon): Show skeleton: https://github.com/dxos/dxos/issues/8259
8
- export const ContentLoading = () => {
9
- return <div role='none' className='grid place-items-center attention-surface' />;
8
+ export const Loading = () => {
9
+ return <div 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 './Loading';
@@ -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 { Column, 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,52 +54,60 @@ 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
+ <Column.Root gutter='xs' classNames='py-form-chrome'>
69
+ <Column.Center>
70
+ <Flex column>
71
+ <Input.Root>
72
+ <Input.TextInput placeholder={label} />
73
+ </Input.Root>
74
+ </Flex>
75
+ </Column.Center>
76
+ </Column.Root>
77
+ </Panel.Content>
78
+ </Panel.Root>
71
79
  );
72
80
  };
73
81
 
74
82
  const DefaultStory = () => {
75
- const [splitterMode, setSplitterMode] = useState<SplitterMode>('upper');
83
+ const [splitterMode, setSplitterMode] = useState<SplitterMode>('top');
76
84
  const [keyboardOpen, setKeyboardOpen] = useState(false);
77
85
 
78
86
  useEffect(() => {
79
- setSplitterMode(splitterMode === 'both' ? 'lower' : splitterMode);
87
+ setSplitterMode(splitterMode === 'split' ? 'bottom' : splitterMode);
80
88
  }, [keyboardOpen]);
81
89
 
82
90
  return (
83
91
  <WithKeyboard>
84
92
  <MobileLayout.Root onKeyboardOpenChange={setKeyboardOpen}>
85
- <MobileLayout.Panel safe={{ top: true, bottom: splitterMode === 'upper' }}>
93
+ <MobileLayout.Panel safe={{ top: true, bottom: splitterMode === 'top' }}>
86
94
  <Splitter.Root mode={splitterMode} ratio={0.5}>
87
- <Splitter.Panel position='upper'>
88
- <Panel label='Main'>
89
- {splitterMode === 'upper' && (
90
- <Toolbar.IconButton icon='ph--plus--regular' label='Open' onClick={() => setSplitterMode('both')} />
95
+ <Splitter.Panel position='top'>
96
+ <StoryPanel label='Main'>
97
+ {splitterMode === 'top' && (
98
+ <Toolbar.IconButton icon='ph--plus--regular' label='Open' onClick={() => setSplitterMode('split')} />
91
99
  )}
92
- </Panel>
100
+ </StoryPanel>
93
101
  </Splitter.Panel>
94
- <Splitter.Panel position='lower'>
95
- <Panel label='Drawer'>
102
+ <Splitter.Panel position='bottom'>
103
+ <StoryPanel label='Drawer'>
96
104
  <Toolbar.IconButton
97
- icon={splitterMode === 'lower' ? 'ph--arrow-down--regular' : 'ph--arrow-up--regular'}
98
- label={splitterMode === 'lower' ? 'Collapse' : 'Expand'}
99
- onClick={() => setSplitterMode((splitterMode) => (splitterMode === 'both' ? 'lower' : 'both'))}
105
+ icon={splitterMode === 'bottom' ? 'ph--arrow-down--regular' : 'ph--arrow-up--regular'}
106
+ label={splitterMode === 'bottom' ? 'Collapse' : 'Expand'}
107
+ onClick={() => setSplitterMode((splitterMode) => (splitterMode === 'split' ? 'bottom' : 'split'))}
100
108
  />
101
- <Toolbar.IconButton icon='ph--x--regular' label='Close' onClick={() => setSplitterMode('upper')} />
102
- </Panel>
109
+ <Toolbar.IconButton icon='ph--x--regular' label='Close' onClick={() => setSplitterMode('top')} />
110
+ </StoryPanel>
103
111
  </Splitter.Panel>
104
112
  </Splitter.Root>
105
113
  </MobileLayout.Panel>
@@ -109,7 +117,7 @@ const DefaultStory = () => {
109
117
  };
110
118
 
111
119
  const meta: Meta<MobileLayoutRootProps> = {
112
- title: 'plugins/plugin-simple-layout/MobileLayout',
120
+ title: 'plugins/plugin-simple-layout/components/MobileLayout',
113
121
  component: MobileLayout.Root,
114
122
  render: DefaultStory,
115
123
  decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'relative' })],
@@ -3,14 +3,14 @@
3
3
  //
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
- import React, { type PropsWithChildren, forwardRef, useEffect, useState } from 'react';
6
+ import React, { type PropsWithChildren, forwardRef, useEffect, useLayoutEffect, useState } from 'react';
7
7
 
8
8
  import { addEventListener, combine } from '@dxos/async';
9
9
  import { log } from '@dxos/log';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
11
11
  import { mx } from '@dxos/ui-theme';
12
12
 
13
- // TODO(burdon): Move into @dxos/react-ui?
13
+ import { useDebugLog } from '../DebugOverlay';
14
14
 
15
15
  const MOBILE_LAYOUT_NAME = 'MobileLayout';
16
16
  const MOBILE_LAYOUT_ROOT_NAME = 'MobileLayout.Root';
@@ -42,11 +42,15 @@ type MobileLayoutRootProps = ThemedClassName<
42
42
  */
43
43
  // TODO(burdon): Should this be ios-only?
44
44
  const MobileLayoutRoot = forwardRef<HTMLDivElement, MobileLayoutRootProps>(
45
- ({ classNames, children, transition = 250, onKeyboardOpenChange, ...props }, forwardedRef) => {
45
+ ({ classNames, children, transition = 500, onKeyboardOpenChange, ...props }, forwardedRef) => {
46
46
  const { open: keyboardOpen } = useIOSKeyboard();
47
- useAutoScroll();
48
- useEffect(() => onKeyboardOpenChange?.(keyboardOpen), [onKeyboardOpenChange, keyboardOpen]);
49
47
  useLockBodyScroll(keyboardOpen);
48
+ useAutoScroll();
49
+
50
+ // Fire synchronously after DOM mutation (before paint) so SimpleLayout's Splitter mode
51
+ // change is batched into the same paint as the keyboard open state change, preventing
52
+ // intermediate render frames from showing an un-adjusted layout.
53
+ useLayoutEffect(() => onKeyboardOpenChange?.(keyboardOpen), [keyboardOpen, onKeyboardOpenChange]);
50
54
 
51
55
  return (
52
56
  <MobileLayoutProvider keyboardOpen={keyboardOpen}>
@@ -54,10 +58,11 @@ const MobileLayoutRoot = forwardRef<HTMLDivElement, MobileLayoutRootProps>(
54
58
  {...props}
55
59
  role='none'
56
60
  style={{
57
- transition: `block-size ${transition}ms ease-out`,
58
- blockSize: 'calc(100vh - var(--kb-height, 0px))',
61
+ height: 'calc(100vh - var(--kb-height, 0px))',
62
+ transition: `height ${keyboardOpen ? 0 : transition}ms ease-out`,
63
+ // transition: `height ${animationDuration}ms ease-out`,
59
64
  }}
60
- className={mx('absolute top-0 left-0 right-0 flex flex-col', classNames)}
65
+ className={mx('fixed top-0 left-0 right-0 grid overflow-hidden', classNames)}
61
66
  ref={forwardedRef}
62
67
  >
63
68
  {children}
@@ -95,7 +100,7 @@ const MobileLayoutPanel = forwardRef<HTMLDivElement, MobileLayoutPanelProps>(
95
100
  paddingTop: safe?.top ? 'env(safe-area-inset-top)' : undefined,
96
101
  paddingBottom: safe?.bottom ? `calc((1 - var(--kb-open, 0)) * env(safe-area-inset-bottom))` : undefined,
97
102
  }}
98
- className={mx('relative bs-full flex flex-col overflow-hidden', classNames)}
103
+ className={mx(classNames)}
99
104
  ref={forwardedRef}
100
105
  >
101
106
  {children}
@@ -120,42 +125,62 @@ export { useMobileLayout };
120
125
  export type { MobileLayoutRootProps, MobileLayoutPanelProps };
121
126
 
122
127
  /**
123
- * Prevent auto-scroll when input is focused.
128
+ * Prevents iOS (WKWebView) from shifting the layout when the keyboard appears.
129
+ *
130
+ * Scroll events and window.scrollY stay at 0 in this WKWebView setup — the shift is
131
+ * caused by the browser's scroll-into-view for the focused input. We keep a window
132
+ * scroll reset as belt-and-suspenders, and also monitor container scroll events.
124
133
  */
125
134
  const useAutoScroll = () => {
135
+ // TODO(burdon): Remove debug logging.
136
+ const { dbg } = useDebugLog('useAutoScroll');
137
+
126
138
  useEffect(() => {
127
- // Prevent auto-scroll when input is focused.
128
- return addEventListener(
129
- document,
130
- 'focus',
131
- (event: FocusEvent) => {
132
- const target = event.target as HTMLElement;
133
- if (
134
- target.tagName === 'INPUT' ||
135
- target.tagName === 'TEXTAREA' ||
136
- (target.tagName === 'DIV' && target.isContentEditable)
137
- ) {
138
- // Prevent default focus behavior.
139
- event.preventDefault();
140
-
141
- // Manually focus without scroll.
142
- target.focus({ preventScroll: true });
143
-
144
- // Lock current scroll position.
145
- const scrollX = window.scrollX;
146
- const scrollY = window.scrollY;
147
- requestAnimationFrame(() => {
148
- window.scrollTo(scrollX, scrollY);
149
- });
150
-
151
- // TODO(burdon): Scroll to position in parent; this may need to be via an intent,
152
- // since it may be plugin-specific (e.g., codemirror document.)
153
- }
154
- },
155
- // Important: focus events don't bubble, so capture phase is required.
156
- { capture: true },
139
+ const resetScroll = () => {
140
+ if (window.scrollX !== 0 || window.scrollY !== 0) {
141
+ window.scrollTo(0, 0);
142
+ }
143
+ };
144
+
145
+ const detectContainerScroll = (event: Event) => {
146
+ const el = event.target as HTMLElement;
147
+ if (el === document.documentElement || el === document.body) {
148
+ return;
149
+ }
150
+
151
+ dbg(`scroll: ${el.tagName}.${Array.from(el.classList).slice(0, 2).join('.')} top=${el.scrollTop.toFixed(0)}`);
152
+ };
153
+
154
+ return combine(
155
+ addEventListener(window, 'scroll', resetScroll),
156
+ window.visualViewport ? addEventListener(window.visualViewport, 'scroll' as any, resetScroll) : () => {},
157
+
158
+ // TODO(burdon): Remove debug logging.
159
+ addEventListener(document, 'scroll', detectContainerScroll as EventListener, { capture: true } as any),
160
+
161
+ // Prevent focus-triggered scroll-into-view on inputs.
162
+ (() => {
163
+ let focusingWithPreventScroll = false;
164
+ return addEventListener(
165
+ document,
166
+ 'focus',
167
+ (event: FocusEvent) => {
168
+ if (focusingWithPreventScroll) {
169
+ return;
170
+ }
171
+
172
+ const target = event.target as HTMLElement;
173
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
174
+ focusingWithPreventScroll = true;
175
+ target.focus({ preventScroll: true });
176
+ focusingWithPreventScroll = false;
177
+ }
178
+ },
179
+ { capture: true },
180
+ );
181
+ })(),
157
182
  );
158
- }, []);
183
+ }, [dbg]);
159
184
  };
160
185
 
161
186
  /**
@@ -231,6 +256,8 @@ const useLockBodyScroll = (enabled: boolean) => {
231
256
  type IOSKeyboard = {
232
257
  open: boolean;
233
258
  height: number;
259
+ /** Native keyboard animation duration in ms, from the iOS keyboard event. */
260
+ duration: number | undefined;
234
261
  };
235
262
 
236
263
  /**
@@ -263,10 +290,13 @@ type IOSKeyboard = {
263
290
  * Falls back to VisualViewport API on other platforms.
264
291
  */
265
292
  const useIOSKeyboard = (): IOSKeyboard => {
293
+ const { dbg } = useDebugLog('useIOSKeyboard');
294
+
266
295
  const [open, setOpen] = useState(false);
267
296
  const [height, setHeight] = useState(0);
297
+ const [duration, setDuration] = useState<number | undefined>(undefined);
268
298
 
269
- // Detect keybaord state.
299
+ // Detect keyboard state.
270
300
  useEffect(() => {
271
301
  const viewport = window.visualViewport;
272
302
  if (!viewport) {
@@ -276,30 +306,69 @@ const useIOSKeyboard = (): IOSKeyboard => {
276
306
  // Handler for VisualViewport resize (fallback for non-iOS).
277
307
  const initialHeight = viewport.height ?? window.innerHeight;
278
308
 
279
- const updateState = (keyboardHeight: number, keyboardOpen: boolean) => {
309
+ const updateState = (keyboardHeight: number, keyboardOpen: boolean, animationDuration?: number) => {
280
310
  setOpen(keyboardOpen);
281
311
  setHeight(keyboardHeight);
312
+ setDuration(animationDuration);
282
313
 
283
314
  const vvh = initialHeight - keyboardHeight;
284
315
  document.documentElement.style.setProperty('--vvh', `${vvh}px`);
285
316
  document.documentElement.style.setProperty('--kb-height', `${keyboardHeight}px`);
286
317
  document.documentElement.style.setProperty('--kb-open', keyboardOpen ? '1' : '0');
287
- log.info('viewport size', { initialHeight, vvh, keyboardHeight, keyboardOpen });
318
+ log.info('viewport size', { initialHeight, vvh, keyboardHeight, keyboardOpen, animationDuration });
288
319
  };
289
320
 
321
+ let rafId: number | undefined;
322
+
290
323
  return combine(
291
324
  // Handler for native iOS keyboard events (from KeyboardObserver.swift).
292
325
  addEventListener(
293
326
  window,
294
327
  'keyboard' as any,
295
328
  (event: CustomEvent<{ type: 'show' | 'hide'; height: number; duration: number }>) => {
296
- const { type, height } = event.detail;
297
- log.info('keyboard event', { type, height });
298
- updateState(height, type === 'show');
329
+ const { type, height, duration } = event.detail;
330
+ // iOS KeyboardObserver.swift sends duration in seconds (e.g., 0.25). Convert to ms.
331
+ const durationMs = duration < 1 ? duration * 1000 : duration;
332
+
333
+ // TODO(burdon): Remove debug logging.
334
+ const vp = window.visualViewport;
335
+ dbg(
336
+ `kb:${type} h=${height} dur=${duration} scrollY=${window.scrollY} vpOffset=${vp?.offsetTop?.toFixed(0) ?? '?'}`,
337
+ );
338
+ log.info('keyboard event', { type, height, duration });
339
+
340
+ updateState(height, type === 'show', durationMs);
341
+
342
+ // RAF loop: monitor visualViewport.offsetTop and window.scrollY every frame.
343
+ // TODO(burdon): Remove debug logging.
344
+ const end = performance.now() + durationMs + 300;
345
+ let prevOffsetTop = vp?.offsetTop ?? 0;
346
+ let prevScrollY = window.scrollY;
347
+ const monitorFrame = () => {
348
+ const offsetTop = vp?.offsetTop ?? 0;
349
+ const scrollY = window.scrollY;
350
+ if (offsetTop !== prevOffsetTop || scrollY !== prevScrollY) {
351
+ dbg(`Δ vpOffset=${offsetTop.toFixed(0)} scrollY=${scrollY.toFixed(0)}`);
352
+ prevOffsetTop = offsetTop;
353
+ prevScrollY = scrollY;
354
+ }
355
+ if (scrollY !== 0) {
356
+ window.scrollTo(0, 0);
357
+ }
358
+ if (performance.now() < end) {
359
+ rafId = requestAnimationFrame(monitorFrame);
360
+ }
361
+ };
362
+ rafId = requestAnimationFrame(monitorFrame);
299
363
  },
300
364
  ),
365
+ () => {
366
+ if (rafId !== undefined) {
367
+ cancelAnimationFrame(rafId);
368
+ }
369
+ },
301
370
  );
302
- }, []);
371
+ }, [dbg]);
303
372
 
304
- return { open, height };
373
+ return { open, height, duration };
305
374
  };