@dxos/plugin-deck 0.7.5-main.9d26e3a → 0.7.5-main.e9bb01b

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 (168) hide show
  1. package/dist/lib/browser/app-graph-builder-CI6ZFMNL.mjs +147 -0
  2. package/dist/lib/browser/app-graph-builder-CI6ZFMNL.mjs.map +7 -0
  3. package/dist/lib/browser/check-app-scheme-S3EYUPMF.mjs +33 -0
  4. package/dist/lib/browser/check-app-scheme-S3EYUPMF.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-M2L53AIH.mjs +126 -0
  6. package/dist/lib/browser/chunk-M2L53AIH.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-GVOGPULO.mjs → chunk-N7TEPFVR.mjs} +5 -4
  8. package/dist/lib/browser/chunk-N7TEPFVR.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-NYZJCVAU.mjs +22 -0
  10. package/dist/lib/browser/chunk-NYZJCVAU.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-WXNLVMK2.mjs +1119 -0
  12. package/dist/lib/browser/chunk-WXNLVMK2.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-YQ2GWTDU.mjs +17 -0
  14. package/dist/lib/browser/chunk-YQ2GWTDU.mjs.map +7 -0
  15. package/dist/lib/browser/index.mjs +90 -1775
  16. package/dist/lib/browser/index.mjs.map +4 -4
  17. package/dist/lib/browser/intent-resolver-CSXFDKTC.mjs +494 -0
  18. package/dist/lib/browser/intent-resolver-CSXFDKTC.mjs.map +7 -0
  19. package/dist/lib/browser/meta.json +1 -1
  20. package/dist/lib/browser/react-root-ECDQZYQT.mjs +46 -0
  21. package/dist/lib/browser/react-root-ECDQZYQT.mjs.map +7 -0
  22. package/dist/lib/browser/react-surface-4WIQZW2S.mjs +38 -0
  23. package/dist/lib/browser/react-surface-4WIQZW2S.mjs.map +7 -0
  24. package/dist/lib/browser/settings-WACNLCPB.mjs +28 -0
  25. package/dist/lib/browser/settings-WACNLCPB.mjs.map +7 -0
  26. package/dist/lib/browser/state-VPOYUKK6.mjs +117 -0
  27. package/dist/lib/browser/state-VPOYUKK6.mjs.map +7 -0
  28. package/dist/lib/browser/types.mjs +16 -4
  29. package/dist/lib/browser/url-handler-HLF42IHP.mjs +70 -0
  30. package/dist/lib/browser/url-handler-HLF42IHP.mjs.map +7 -0
  31. package/dist/types/src/DeckPlugin.d.ts +1 -5
  32. package/dist/types/src/DeckPlugin.d.ts.map +1 -1
  33. package/dist/types/src/capabilities/app-graph-builder.d.ts +181 -0
  34. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  35. package/dist/types/src/capabilities/capabilities.d.ts +142 -0
  36. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/check-app-scheme.d.ts +4 -0
  38. package/dist/types/src/capabilities/check-app-scheme.d.ts.map +1 -0
  39. package/dist/types/src/capabilities/index.d.ts +189 -0
  40. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  42. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/react-root.d.ts +7 -0
  44. package/dist/types/src/capabilities/react-root.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  46. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  47. package/dist/types/src/capabilities/set-active.d.ts +9 -0
  48. package/dist/types/src/capabilities/set-active.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/settings.d.ts +4 -0
  50. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  51. package/dist/types/src/capabilities/state.d.ts +76 -0
  52. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  53. package/dist/types/src/capabilities/url-handler.d.ts +4 -0
  54. package/dist/types/src/capabilities/url-handler.d.ts.map +1 -0
  55. package/dist/types/src/components/DeckLayout/ActiveNode.d.ts.map +1 -1
  56. package/dist/types/src/components/DeckLayout/Banner.d.ts +6 -0
  57. package/dist/types/src/components/DeckLayout/Banner.d.ts.map +1 -0
  58. package/dist/types/src/components/DeckLayout/ComplementarySidebar.d.ts.map +1 -1
  59. package/dist/types/src/components/DeckLayout/ContentEmpty.d.ts.map +1 -1
  60. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts +1 -4
  61. package/dist/types/src/components/DeckLayout/DeckLayout.d.ts.map +1 -1
  62. package/dist/types/src/components/DeckLayout/Fullscreen.d.ts.map +1 -1
  63. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts +3 -3
  64. package/dist/types/src/components/DeckLayout/NodePlankHeading.d.ts.map +1 -1
  65. package/dist/types/src/components/DeckLayout/Plank.d.ts +8 -6
  66. package/dist/types/src/components/DeckLayout/Plank.d.ts.map +1 -1
  67. package/dist/types/src/components/DeckLayout/PlankControls.d.ts +2 -2
  68. package/dist/types/src/components/DeckLayout/PlankControls.d.ts.map +1 -1
  69. package/dist/types/src/components/DeckLayout/PlankError.d.ts +4 -3
  70. package/dist/types/src/components/DeckLayout/PlankError.d.ts.map +1 -1
  71. package/dist/types/src/components/DeckLayout/Sidebar.d.ts +1 -5
  72. package/dist/types/src/components/DeckLayout/Sidebar.d.ts.map +1 -1
  73. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts +8 -0
  74. package/dist/types/src/components/DeckLayout/SidebarButton.d.ts.map +1 -0
  75. package/dist/types/src/components/DeckLayout/StatusBar.d.ts.map +1 -1
  76. package/dist/types/src/components/DeckLayout/Toast.d.ts +2 -2
  77. package/dist/types/src/components/DeckLayout/Toast.d.ts.map +1 -1
  78. package/dist/types/src/components/DeckLayout/Topbar.d.ts +3 -0
  79. package/dist/types/src/components/DeckLayout/Topbar.d.ts.map +1 -0
  80. package/dist/types/src/components/fragments.d.ts +4 -0
  81. package/dist/types/src/components/fragments.d.ts.map +1 -0
  82. package/dist/types/src/components/index.d.ts +0 -2
  83. package/dist/types/src/components/index.d.ts.map +1 -1
  84. package/dist/types/src/events.d.ts +4 -0
  85. package/dist/types/src/events.d.ts.map +1 -0
  86. package/dist/types/src/hooks/useMainSize.d.ts +2 -2
  87. package/dist/types/src/index.d.ts +3 -2
  88. package/dist/types/src/index.d.ts.map +1 -1
  89. package/dist/types/src/layout.d.ts +5 -19
  90. package/dist/types/src/layout.d.ts.map +1 -1
  91. package/dist/types/src/meta.d.ts +4 -4
  92. package/dist/types/src/meta.d.ts.map +1 -1
  93. package/dist/types/src/translations.d.ts +4 -2
  94. package/dist/types/src/translations.d.ts.map +1 -1
  95. package/dist/types/src/types.d.ts +117 -20
  96. package/dist/types/src/types.d.ts.map +1 -1
  97. package/dist/types/src/util/index.d.ts +3 -2
  98. package/dist/types/src/util/index.d.ts.map +1 -1
  99. package/dist/types/src/util/layoutAppliesTopbar.d.ts +2 -0
  100. package/dist/types/src/util/layoutAppliesTopbar.d.ts.map +1 -0
  101. package/dist/types/src/util/useBreakpoints.d.ts +2 -0
  102. package/dist/types/src/util/useBreakpoints.d.ts.map +1 -0
  103. package/dist/types/src/util/useHoistStatusbar.d.ts +2 -0
  104. package/dist/types/src/util/useHoistStatusbar.d.ts.map +1 -0
  105. package/dist/types/tsconfig.tsbuildinfo +1 -1
  106. package/package.json +30 -36
  107. package/src/DeckPlugin.ts +77 -0
  108. package/src/capabilities/app-graph-builder.ts +109 -0
  109. package/src/capabilities/capabilities.ts +18 -0
  110. package/src/capabilities/check-app-scheme.ts +44 -0
  111. package/src/capabilities/index.ts +16 -0
  112. package/src/capabilities/intent-resolver.ts +350 -0
  113. package/src/capabilities/react-root.tsx +48 -0
  114. package/src/capabilities/react-surface.tsx +31 -0
  115. package/src/capabilities/set-active.ts +43 -0
  116. package/src/capabilities/settings.ts +21 -0
  117. package/src/capabilities/state.ts +102 -0
  118. package/src/capabilities/url-handler.ts +63 -0
  119. package/src/components/DeckLayout/ActiveNode.tsx +2 -3
  120. package/src/components/DeckLayout/Banner.tsx +37 -0
  121. package/src/components/DeckLayout/ComplementarySidebar.tsx +128 -55
  122. package/src/components/DeckLayout/ContentEmpty.tsx +9 -4
  123. package/src/components/DeckLayout/DeckLayout.tsx +113 -76
  124. package/src/components/DeckLayout/Fullscreen.tsx +2 -3
  125. package/src/components/DeckLayout/NodePlankHeading.tsx +64 -77
  126. package/src/components/DeckLayout/Plank.tsx +34 -43
  127. package/src/components/DeckLayout/PlankControls.tsx +11 -10
  128. package/src/components/DeckLayout/PlankError.tsx +6 -5
  129. package/src/components/DeckLayout/Sidebar.tsx +19 -9
  130. package/src/components/DeckLayout/SidebarButton.tsx +68 -0
  131. package/src/components/DeckLayout/StatusBar.tsx +6 -12
  132. package/src/components/DeckLayout/Toast.tsx +2 -2
  133. package/src/components/DeckLayout/Topbar.tsx +11 -0
  134. package/src/components/LayoutSettings.tsx +8 -8
  135. package/src/components/fragments.ts +14 -0
  136. package/src/components/index.ts +0 -2
  137. package/src/events.ts +11 -0
  138. package/src/hooks/useMainSize.ts +3 -3
  139. package/src/index.ts +3 -4
  140. package/src/layout.ts +43 -212
  141. package/src/meta.ts +3 -2
  142. package/src/translations.ts +8 -6
  143. package/src/types.ts +95 -34
  144. package/src/util/index.ts +3 -2
  145. package/src/util/layoutAppliesTopbar.ts +7 -0
  146. package/src/util/useBreakpoints.ts +11 -0
  147. package/src/util/useHoistStatusbar.ts +24 -0
  148. package/dist/lib/browser/chunk-GVOGPULO.mjs.map +0 -7
  149. package/dist/lib/browser/chunk-ZC3K6C2W.mjs +0 -37
  150. package/dist/lib/browser/chunk-ZC3K6C2W.mjs.map +0 -7
  151. package/dist/lib/browser/meta.mjs +0 -9
  152. package/dist/lib/browser/meta.mjs.map +0 -7
  153. package/dist/types/src/components/DeckContext.d.ts +0 -8
  154. package/dist/types/src/components/DeckContext.d.ts.map +0 -1
  155. package/dist/types/src/components/LayoutContext.d.ts +0 -5
  156. package/dist/types/src/components/LayoutContext.d.ts.map +0 -1
  157. package/dist/types/src/layout.test.d.ts +0 -2
  158. package/dist/types/src/layout.test.d.ts.map +0 -1
  159. package/dist/types/src/util/check-app-scheme.d.ts +0 -2
  160. package/dist/types/src/util/check-app-scheme.d.ts.map +0 -1
  161. package/dist/types/src/util/layout-parts.d.ts +0 -7
  162. package/dist/types/src/util/layout-parts.d.ts.map +0 -1
  163. package/src/DeckPlugin.tsx +0 -623
  164. package/src/components/DeckContext.ts +0 -14
  165. package/src/components/LayoutContext.ts +0 -12
  166. package/src/layout.test.ts +0 -380
  167. package/src/util/check-app-scheme.ts +0 -21
  168. package/src/util/layout-parts.ts +0 -12
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Surface } from '@dxos/app-framework';
8
+ import { type ThemedClassName } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/react-ui-theme';
10
+
11
+ import { CloseSidebarButton, ToggleSidebarButton } from './SidebarButton';
12
+
13
+ export const Banner = ({ variant, classNames }: ThemedClassName<{ variant?: 'topbar' | 'sidebar' }>) => {
14
+ return (
15
+ <header
16
+ className={mx(
17
+ 'flex items-stretch relative plb-1 pis-1 pie-2',
18
+ variant === 'topbar' &&
19
+ 'fixed inset-inline-0 block-start-[env(safe-area-inset-top)] bs-[--rail-size] border-be border-separator',
20
+ classNames,
21
+ )}
22
+ >
23
+ {variant === 'sidebar' ? <CloseSidebarButton /> : <ToggleSidebarButton />}
24
+ <span className='self-center grow mis-1'>Composer</span>
25
+ {variant === 'topbar' && (
26
+ <div role='none' className='absolute inset-0 pointer-events-none'>
27
+ <div role='none' className='grid bs-full pointer-fine:p-1 max-is-md mli-auto pointer-events-auto'>
28
+ <Surface role='search-input' limit={1} />
29
+ </div>
30
+ </div>
31
+ )}
32
+ <span role='none' className='grow' />
33
+ <Surface role='header-end' limit={1} />
34
+ <Surface role='notch-start' limit={1} />
35
+ </header>
36
+ );
37
+ };
@@ -2,28 +2,28 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo } from 'react';
5
+ import React, { useCallback, useEffect, useMemo, useState, type MouseEvent } from 'react';
6
6
 
7
7
  import {
8
8
  createIntent,
9
- type LayoutCoordinate,
10
- NavigationAction,
11
- SLUG_PATH_SEPARATOR,
9
+ LayoutAction,
12
10
  Surface,
11
+ useAppGraph,
12
+ useCapability,
13
13
  useIntentDispatcher,
14
14
  } from '@dxos/app-framework';
15
- import { useGraph } from '@dxos/plugin-graph';
16
- import { Main, ScrollArea } from '@dxos/react-ui';
15
+ import { Main, useTranslation, toLocalizedString, IconButton, ScrollArea } from '@dxos/react-ui';
17
16
  import { useAttended } from '@dxos/react-ui-attention';
18
- import { railGridHorizontal, StackContext } from '@dxos/react-ui-stack';
19
- import { mx } from '@dxos/react-ui-theme';
17
+ import { Tabs } from '@dxos/react-ui-tabs';
20
18
 
21
- import { NodePlankHeading } from './NodePlankHeading';
22
19
  import { PlankContentError } from './PlankError';
23
20
  import { PlankLoading } from './PlankLoading';
21
+ import { ToggleComplementarySidebarButton } from './SidebarButton';
22
+ import { DeckCapabilities } from '../../capabilities';
24
23
  import { useNode, useNodeActionExpander } from '../../hooks';
25
- import { type Panel } from '../../types';
26
- import { useLayout } from '../LayoutContext';
24
+ import { DECK_PLUGIN } from '../../meta';
25
+ import { SLUG_PATH_SEPARATOR, type Panel } from '../../types';
26
+ import { layoutAppliesTopbar, useBreakpoints, useHoistStatusbar } from '../../util';
27
27
 
28
28
  export type ComplementarySidebarProps = {
29
29
  panels: Panel[];
@@ -31,60 +31,133 @@ export type ComplementarySidebarProps = {
31
31
  };
32
32
 
33
33
  export const ComplementarySidebar = ({ panels, current }: ComplementarySidebarProps) => {
34
- const { popoverAnchorId } = useLayout();
34
+ const layout = useCapability(DeckCapabilities.MutableDeckState);
35
35
  const attended = useAttended();
36
- const panel = (panels.find((p) => p.id === current) ?? panels[0])?.id;
37
- const id = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${panel}` : undefined;
38
- const { graph } = useGraph();
39
- const node = useNode(graph, id);
36
+ const panelIds = useMemo(() => panels.map((p) => p.id), [panels]);
37
+ const activePanelId = panelIds.find((p) => p === current) ?? panels[0].id;
38
+ const activeEntryId = attended[0] ? `${attended[0]}${SLUG_PATH_SEPARATOR}${activePanelId}` : undefined;
39
+ const { graph } = useAppGraph();
40
+ const node = useNode(graph, activeEntryId);
41
+ const { t } = useTranslation(DECK_PLUGIN);
40
42
  const { dispatchPromise: dispatch } = useIntentDispatcher();
41
43
  useNodeActionExpander(node);
44
+ const breakpoint = useBreakpoints();
45
+ const topbar = layoutAppliesTopbar(breakpoint);
46
+ const hoistStatusbar = useHoistStatusbar(breakpoint);
42
47
 
43
- const actions = useMemo(
44
- () =>
45
- panels.map(({ id, label, icon }) => ({
46
- id: `complementary-${id}`,
47
- data: () => {
48
- void dispatch(createIntent(NavigationAction.Open, { activeParts: { complementary: id } }));
49
- },
50
- properties: {
51
- label,
52
- icon,
53
- menuItemType: 'toggle',
54
- isChecked: panel === id,
55
- },
56
- })),
57
- [panel],
58
- );
48
+ const [internalValue, setInternalValue] = useState(activePanelId);
49
+
50
+ useEffect(() => {
51
+ setInternalValue(activePanelId);
52
+ }, [activePanelId]);
59
53
 
60
- // TODO(wittjosiah): Ensure that id is always defined.
61
- const coordinate: LayoutCoordinate = useMemo(() => ({ entryId: id ?? 'unknown', part: 'complementary' }), [id]);
54
+ const handleTabClick = useCallback(
55
+ (event: MouseEvent) => {
56
+ const nextValue = event.currentTarget.getAttribute('data-value') as string;
57
+ if (nextValue === activePanelId) {
58
+ layout.complementarySidebarState = layout.complementarySidebarState === 'expanded' ? 'collapsed' : 'expanded';
59
+ } else {
60
+ setInternalValue(nextValue);
61
+ layout.complementarySidebarState = 'expanded';
62
+ void dispatch(createIntent(LayoutAction.UpdateComplementary, { part: 'complementary', subject: nextValue }));
63
+ }
64
+ },
65
+ [layout, activePanelId, dispatch],
66
+ );
62
67
 
63
68
  // TODO(burdon): Scroll area should be controlled by surface.
64
69
  return (
65
- <Main.ComplementarySidebar>
66
- <StackContext.Provider value={{ size: 'contain', orientation: 'horizontal', rail: true }}>
67
- <div role='none' className={mx(railGridHorizontal, 'grid grid-cols-[100%] bs-full')}>
68
- <NodePlankHeading coordinate={coordinate} node={node} popoverAnchorId={popoverAnchorId} actions={actions} />
69
- <ScrollArea.Root>
70
- <ScrollArea.Viewport>
71
- {node && (
72
- <Surface
73
- key={id}
74
- role={`complementary--${panel}`}
75
- limit={1}
76
- data={{ id, subject: node.properties.object ?? node.properties.space, popoverAnchorId }}
77
- fallback={PlankContentError}
78
- placeholder={<PlankLoading />}
70
+ <Main.ComplementarySidebar
71
+ classNames={[
72
+ topbar && 'block-start-[calc(env(safe-area-inset-top)+var(--rail-size))]',
73
+ hoistStatusbar && 'block-end-[--statusbar-size]',
74
+ ]}
75
+ >
76
+ <Tabs.Root
77
+ orientation='vertical'
78
+ verticalVariant='stateless'
79
+ value={internalValue}
80
+ attendableId={attended[0]}
81
+ classNames='contents'
82
+ >
83
+ <div
84
+ role='none'
85
+ className='absolute z-[1] inset-block-0 inline-end-0 !is-[--r0-size] border-is border-separator grid grid-cols-1 grid-rows-[1fr_min-content] bg-baseSurface contain-layout app-drag'
86
+ >
87
+ <Tabs.Tablist classNames='grid grid-cols-1 auto-rows-[--rail-action] p-1 gap-1 !overflow-y-auto'>
88
+ {panels.map((panel) => (
89
+ <Tabs.Tab key={panel.id} value={panel.id} asChild>
90
+ <IconButton
91
+ label={toLocalizedString(panel.label, t)}
92
+ icon={panel.icon}
93
+ size={5}
94
+ iconOnly
95
+ tooltipSide='left'
96
+ data-value={panel.id}
97
+ variant={
98
+ activePanelId === panel.id
99
+ ? layout.complementarySidebarState === 'expanded'
100
+ ? 'primary'
101
+ : 'default'
102
+ : 'ghost'
103
+ }
104
+ onClick={handleTabClick}
79
105
  />
80
- )}
81
- <ScrollArea.Scrollbar orientation='vertical'>
82
- <ScrollArea.Thumb />
83
- </ScrollArea.Scrollbar>
84
- </ScrollArea.Viewport>
85
- </ScrollArea.Root>
106
+ </Tabs.Tab>
107
+ ))}
108
+ </Tabs.Tablist>
109
+ {!hoistStatusbar && (
110
+ <div role='none' className='grid grid-cols-1 auto-rows-[--rail-item] p-1 overflow-y-auto'>
111
+ <Surface role='status-bar--r0-footer' limit={1} />
112
+ </div>
113
+ )}
114
+ <div role='none' className='hidden lg:grid grid-cols-1 auto-rows-[--rail-action] p-1'>
115
+ <ToggleComplementarySidebarButton />
116
+ </div>
86
117
  </div>
87
- </StackContext.Provider>
118
+ {panels.map((panel) => (
119
+ <Tabs.Tabpanel
120
+ key={panel.id}
121
+ value={panel.id}
122
+ classNames='absolute data-[state="inactive"]:-z-[1] inset-block-0 inline-start-0 is-[calc(100%-var(--r0-size))] lg:is-[--r1-size] grid grid-cols-1 grid-rows-[var(--rail-size)_1fr_min-content]'
123
+ {...(layout.complementarySidebarState !== 'expanded' && { inert: 'true' })}
124
+ >
125
+ {panel.id === activePanelId && node && (
126
+ <>
127
+ <h2 className='flex items-center pli-2 border-separator border-be'>
128
+ {toLocalizedString(panel.label, t)}
129
+ </h2>
130
+ <ScrollArea.Root>
131
+ <ScrollArea.Viewport>
132
+ <Surface
133
+ key={activeEntryId}
134
+ role={`complementary--${activePanelId}`}
135
+ data={{
136
+ id: activeEntryId,
137
+ subject: node.properties.object ?? node.properties.space,
138
+ popoverAnchorId: layout.popoverAnchorId,
139
+ }}
140
+ fallback={PlankContentError}
141
+ placeholder={<PlankLoading />}
142
+ />
143
+ </ScrollArea.Viewport>
144
+ <ScrollArea.Scrollbar orientation='vertical'>
145
+ <ScrollArea.Thumb />
146
+ </ScrollArea.Scrollbar>
147
+ </ScrollArea.Root>
148
+ {!hoistStatusbar && (
149
+ <div
150
+ role='contentinfo'
151
+ className='flex flex-wrap justify-center items-center border-bs border-separator plb-1'
152
+ >
153
+ <Surface role='status-bar--r1-footer' limit={1} />
154
+ </div>
155
+ )}
156
+ </>
157
+ )}
158
+ </Tabs.Tabpanel>
159
+ ))}
160
+ </Tabs.Root>
88
161
  </Main.ComplementarySidebar>
89
162
  );
90
163
  };
@@ -6,16 +6,21 @@ import React from 'react';
6
6
 
7
7
  import { Surface } from '@dxos/app-framework';
8
8
 
9
+ import { ToggleSidebarButton } from './SidebarButton';
10
+ import { layoutAppliesTopbar, useBreakpoints } from '../../util';
11
+ import { fixedSidebarToggleStyles } from '../fragments';
12
+
9
13
  export const ContentEmpty = () => {
14
+ const breakpoint = useBreakpoints();
15
+ const topbar = layoutAppliesTopbar(breakpoint);
10
16
  return (
11
17
  <div
12
18
  role='none'
13
- className='min-bs-screen is-dvw sm:is-full flex items-center justify-center p-8'
19
+ className='grid place-items-center p-8 relative bg-deck'
14
20
  data-testid='layoutPlugin.firstRunMessage'
15
21
  >
16
- <div role='none' className='grid place-items-center grid-rows-[min-content_min-content]'>
17
- <Surface role='keyshortcuts' />
18
- </div>
22
+ <Surface role='keyshortcuts' />
23
+ {!topbar && <ToggleSidebarButton variant='default' classNames={fixedSidebarToggleStyles} />}
19
24
  </div>
20
25
  );
21
26
  };
@@ -2,24 +2,29 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Sidebar as MenuIcon } from '@phosphor-icons/react';
6
5
  import { untracked } from '@preact/signals-core';
7
6
  import React, { useCallback, useEffect, useMemo, useRef, type UIEvent, Fragment } from 'react';
8
7
 
9
- import { type LayoutParts, Surface, type Toast as ToastSchema, firstIdInPart, usePlugin } from '@dxos/app-framework';
10
- import { type AttentionPluginProvides } from '@dxos/plugin-attention';
8
+ import {
9
+ LayoutAction,
10
+ createIntent,
11
+ Surface,
12
+ useCapability,
13
+ useIntentDispatcher,
14
+ usePluginManager,
15
+ } from '@dxos/app-framework';
16
+ import { AttentionCapabilities } from '@dxos/plugin-attention';
11
17
  import {
12
18
  AlertDialog,
13
- Button,
14
19
  Dialog as NaturalDialog,
15
20
  Main,
16
21
  Popover,
17
22
  useOnTransition,
18
- useTranslation,
19
23
  type MainProps,
24
+ useMediaQuery,
20
25
  } from '@dxos/react-ui';
21
26
  import { Stack, StackContext, DEFAULT_HORIZONTAL_SIZE } from '@dxos/react-ui-stack';
22
- import { getSize, mainPaddingTransitions } from '@dxos/react-ui-theme';
27
+ import { mainPaddingTransitions } from '@dxos/react-ui-theme';
23
28
 
24
29
  import { ActiveNode } from './ActiveNode';
25
30
  import { ComplementarySidebar, type ComplementarySidebarProps } from './ComplementarySidebar';
@@ -27,17 +32,16 @@ import { ContentEmpty } from './ContentEmpty';
27
32
  import { Fullscreen } from './Fullscreen';
28
33
  import { Plank } from './Plank';
29
34
  import { Sidebar } from './Sidebar';
35
+ import { ToggleComplementarySidebarButton, ToggleSidebarButton } from './SidebarButton';
30
36
  import { StatusBar } from './StatusBar';
31
37
  import { Toast } from './Toast';
32
- import { DECK_PLUGIN } from '../../meta';
33
- import { type Overscroll } from '../../types';
34
- import { calculateOverscroll } from '../../util';
35
- import { useDeckContext } from '../DeckContext';
36
- import { useLayout } from '../LayoutContext';
38
+ import { Topbar } from './Topbar';
39
+ import { DeckCapabilities } from '../../capabilities';
40
+ import { getMode, type Overscroll } from '../../types';
41
+ import { calculateOverscroll, layoutAppliesTopbar, useBreakpoints, useHoistStatusbar } from '../../util';
42
+ import { fixedComplementarySidebarToggleStyles, fixedSidebarToggleStyles } from '../fragments';
37
43
 
38
44
  export type DeckLayoutProps = {
39
- layoutParts: LayoutParts;
40
- toasts: ToastSchema[];
41
45
  overscroll: Overscroll;
42
46
  showHints: boolean;
43
47
  onDismissToast: (id: string) => void;
@@ -46,12 +50,13 @@ export type DeckLayoutProps = {
46
50
  const PlankSeparator = ({ index }: { index: number }) =>
47
51
  index > 0 ? <span role='separator' className='row-span-2 bg-deck is-4' style={{ gridColumn: index * 2 }} /> : null;
48
52
 
49
- export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels, onDismissToast }: DeckLayoutProps) => {
50
- const context = useLayout();
53
+ export const DeckLayout = ({ overscroll, showHints, panels, onDismissToast }: DeckLayoutProps) => {
54
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
55
+ const context = useCapability(DeckCapabilities.MutableDeckState);
51
56
  const {
52
- layoutMode,
53
- sidebarOpen,
54
- complementarySidebarOpen,
57
+ sidebarState,
58
+ complementarySidebarState,
59
+ complementarySidebarPanel,
55
60
  dialogOpen,
56
61
  dialogContent,
57
62
  dialogBlockAlign,
@@ -59,22 +64,26 @@ export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels,
59
64
  popoverOpen,
60
65
  popoverContent,
61
66
  popoverAnchorId,
67
+ deck,
68
+ toasts,
62
69
  } = context;
63
- const { t } = useTranslation(DECK_PLUGIN);
64
- const { plankSizing } = useDeckContext();
65
- // NOTE: Not `useAttended` so that the layout component is not re-rendered when the attended list changes.
66
- const attentionPlugin = usePlugin<AttentionPluginProvides>('dxos.org/plugin/attention');
67
- const fullScreenSlug = useMemo(() => firstIdInPart(layoutParts, 'fullScreen'), [layoutParts]);
70
+ const { active, fullscreen, solo, plankSizing } = deck;
71
+ const breakpoint = useBreakpoints();
72
+ const topbar = layoutAppliesTopbar(breakpoint);
73
+ const hoistStatusbar = useHoistStatusbar(breakpoint);
74
+ const pluginManager = usePluginManager();
68
75
 
69
76
  const scrollLeftRef = useRef<number | null>();
70
77
  const deckRef = useRef<HTMLDivElement>(null);
71
78
 
72
- const isSoloModeLoaded = layoutMode === 'solo' && Boolean(layoutParts.solo?.[0]);
73
-
74
79
  // Ensure the first plank is attended when the deck is first rendered.
75
80
  useEffect(() => {
76
- const attended = untracked(() => attentionPlugin?.provides.attention.attended ?? []);
77
- const firstId = isSoloModeLoaded ? firstIdInPart(layoutParts, 'solo') : firstIdInPart(layoutParts, 'main');
81
+ // NOTE: Not `useAttended` so that the layout component is not re-rendered when the attended list changes.
82
+ const attended = untracked(() => {
83
+ const attention = pluginManager.context.requestCapability(AttentionCapabilities.Attention);
84
+ return attention.current;
85
+ });
86
+ const firstId = solo ?? active[0];
78
87
  if (attended.length === 0 && firstId) {
79
88
  // TODO(wittjosiah): Focusing the type button is a workaround.
80
89
  // If the plank is directly focused on first load the focus ring appears.
@@ -82,6 +91,27 @@ export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels,
82
91
  }
83
92
  }, []);
84
93
 
94
+ // Not using `breakpoint` to avoid firing when breakpoint changes between tablet and desktop.
95
+ // `ssr: false` to avoid using fallback values and flashing into solo mode on startup.
96
+ const [isNotMobile] = useMediaQuery('md', { ssr: false });
97
+ const shouldRevert = useRef(false);
98
+ useEffect(() => {
99
+ if (!isNotMobile && getMode(deck) === 'deck') {
100
+ // NOTE: Not `useAttended` so that the layout component is not re-rendered when the attended list changes.
101
+ const attended = untracked(() => {
102
+ const attention = pluginManager.context.requestCapability(AttentionCapabilities.Attention);
103
+ return attention.current;
104
+ });
105
+
106
+ shouldRevert.current = true;
107
+ void dispatch(
108
+ createIntent(LayoutAction.SetLayoutMode, { part: 'mode', subject: attended[0], options: { mode: 'solo' } }),
109
+ );
110
+ } else if (isNotMobile && getMode(deck) === 'solo' && shouldRevert.current) {
111
+ void dispatch(createIntent(LayoutAction.SetLayoutMode, { part: 'mode', options: { revert: true } }));
112
+ }
113
+ }, [isNotMobile, deck, dispatch]);
114
+
85
115
  /**
86
116
  * Clear scroll restoration state if the window is resized
87
117
  */
@@ -100,6 +130,7 @@ export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels,
100
130
  }
101
131
  }, []);
102
132
 
133
+ const layoutMode = getMode(deck);
103
134
  useOnTransition(layoutMode, (mode) => mode !== 'deck', 'deck', restoreScroll);
104
135
 
105
136
  /**
@@ -107,21 +138,30 @@ export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels,
107
138
  */
108
139
  const handleScroll = useCallback(
109
140
  (event: UIEvent) => {
110
- if (layoutMode === 'deck' && event.currentTarget === event.target) {
141
+ if (!solo && event.currentTarget === event.target) {
111
142
  scrollLeftRef.current = (event.target as HTMLDivElement).scrollLeft;
112
143
  }
113
144
  },
114
- [layoutMode],
145
+ [solo],
115
146
  );
116
147
 
117
- const isEmpty = (layoutParts.main?.length ?? 0) === 0 && (layoutParts.solo?.length ?? 0) === 0;
148
+ const isEmpty = !solo && active.length === 0;
118
149
 
119
150
  const padding = useMemo(() => {
120
- if (layoutMode === 'deck' && overscroll === 'centering') {
121
- return calculateOverscroll(layoutParts.main?.length ?? 0);
151
+ if (!solo && overscroll === 'centering') {
152
+ return calculateOverscroll(active.length);
122
153
  }
123
154
  return {};
124
- }, [layoutMode, overscroll, layoutParts.main]);
155
+ }, [solo, overscroll, deck]);
156
+
157
+ const mainPosition = useMemo(
158
+ () => [
159
+ 'grid !block-start-[env(safe-area-inset-top)]',
160
+ topbar && '!block-start-[calc(env(safe-area-inset-top)+var(--rail-size))]',
161
+ hoistStatusbar && 'lg:block-end-[--statusbar-size]',
162
+ ],
163
+ [topbar, hoistStatusbar],
164
+ );
125
165
 
126
166
  const Dialog = dialogType === 'alert' ? AlertDialog : NaturalDialog;
127
167
 
@@ -140,37 +180,27 @@ export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels,
140
180
  >
141
181
  <ActiveNode />
142
182
 
143
- {layoutMode === 'fullscreen' && <Fullscreen id={fullScreenSlug} />}
183
+ {fullscreen && <Fullscreen id={solo} />}
144
184
 
145
- {layoutMode !== 'fullscreen' && (
185
+ {!fullscreen && (
146
186
  <Main.Root
147
- navigationSidebarOpen={context.sidebarOpen}
148
- onNavigationSidebarOpenChange={(next) => (context.sidebarOpen = next)}
149
- complementarySidebarOpen={context.complementarySidebarOpen}
150
- onComplementarySidebarOpenChange={(next) => (context.complementarySidebarOpen = next)}
187
+ navigationSidebarState={context.sidebarState}
188
+ onNavigationSidebarStateChange={(next) => (context.sidebarState = next)}
189
+ complementarySidebarState={context.complementarySidebarState}
190
+ onComplementarySidebarStateChange={(next) => (context.complementarySidebarState = next)}
151
191
  >
152
- {/* Notch */}
153
- <Main.Notch classNames='z-[21]'>
154
- <Surface role='notch-start' />
155
- <Button onClick={() => (context.sidebarOpen = !context.sidebarOpen)} variant='ghost' classNames='p-1'>
156
- <span className='sr-only'>{t('open navigation sidebar label')}</span>
157
- <MenuIcon weight='light' className={getSize(5)} />
158
- </Button>
159
- <Surface role='notch-end' />
160
- </Main.Notch>
161
-
162
192
  {/* Left sidebar. */}
163
- <Sidebar layoutParts={layoutParts} />
193
+ <Sidebar />
164
194
 
165
195
  {/* Right sidebar. */}
166
- <ComplementarySidebar panels={panels} current={layoutParts.complementary?.[0].id} />
196
+ <ComplementarySidebar panels={panels} current={complementarySidebarPanel} />
167
197
 
168
198
  {/* Dialog overlay to dismiss dialogs. */}
169
199
  <Main.Overlay />
170
200
 
171
201
  {/* No content. */}
172
202
  {isEmpty && (
173
- <Main.Content handlesFocus>
203
+ <Main.Content bounce handlesFocus classNames={mainPosition}>
174
204
  <ContentEmpty />
175
205
  </Main.Content>
176
206
  )}
@@ -179,61 +209,68 @@ export const DeckLayout = ({ layoutParts, toasts, overscroll, showHints, panels,
179
209
  {!isEmpty && (
180
210
  <Main.Content
181
211
  bounce
182
- classNames='grid block-end-[--statusbar-size]'
212
+ classNames={mainPosition}
183
213
  handlesFocus
184
214
  style={
185
215
  {
186
- '--dx-main-sidebarWidth': sidebarOpen ? 'var(--nav-sidebar-size)' : '0px',
187
- '--dx-main-complementaryWidth': complementarySidebarOpen
188
- ? 'var(--complementary-sidebar-size)'
189
- : '0px',
190
- '--dx-main-contentFirstWidth': `${plankSizing[layoutParts.main?.[0]?.id ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
191
- '--dx-main-contentLastWidth': `${plankSizing[layoutParts.main?.[(layoutParts.main?.length ?? 1) - 1]?.id ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
216
+ '--dx-main-sidebarWidth':
217
+ sidebarState === 'expanded'
218
+ ? 'var(--nav-sidebar-size)'
219
+ : sidebarState === 'collapsed'
220
+ ? 'var(--l0-size)'
221
+ : '0',
222
+ '--dx-main-complementaryWidth':
223
+ complementarySidebarState === 'expanded'
224
+ ? 'var(--complementary-sidebar-size)'
225
+ : complementarySidebarState === 'collapsed'
226
+ ? 'var(--rail-size)'
227
+ : '0',
228
+ '--dx-main-contentFirstWidth': `${plankSizing[active[0] ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
229
+ '--dx-main-contentLastWidth': `${plankSizing[active[(active.length ?? 1) - 1] ?? 'never'] ?? DEFAULT_HORIZONTAL_SIZE}rem`,
192
230
  } as MainProps['style']
193
231
  }
194
232
  >
195
233
  <div
196
234
  role='none'
197
- className={!isSoloModeLoaded ? 'relative bg-deck overflow-hidden' : 'sr-only'}
198
- {...(isSoloModeLoaded && { inert: '' })}
235
+ className={!solo ? 'relative bg-deck overflow-hidden' : 'sr-only'}
236
+ {...(solo && { inert: '' })}
199
237
  >
238
+ {!topbar && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
239
+ {!topbar && <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />}
200
240
  <Stack
201
241
  orientation='horizontal'
202
242
  size='contain'
203
243
  classNames={['absolute inset-block-0 -inset-inline-px', mainPaddingTransitions]}
204
244
  onScroll={handleScroll}
205
- itemsCount={2 * (layoutParts.main?.length ?? 0) - 1}
245
+ itemsCount={2 * (active.length ?? 0) - 1}
206
246
  style={padding}
207
247
  ref={deckRef}
208
248
  >
209
- {layoutParts.main?.map((layoutEntry, index) => (
210
- <Fragment key={layoutEntry.id}>
249
+ {active.map((entryId, index) => (
250
+ <Fragment key={entryId}>
211
251
  <PlankSeparator index={index} />
212
- <Plank
213
- entry={layoutEntry}
214
- layoutParts={layoutParts}
215
- part='main'
216
- layoutMode={layoutMode}
217
- order={index * 2 + 1}
218
- />
252
+ <Plank id={entryId} part='deck' order={index * 2 + 1} active={active} layoutMode={layoutMode} />
219
253
  </Fragment>
220
254
  ))}
221
255
  </Stack>
222
256
  </div>
223
257
  <div
224
258
  role='none'
225
- className={isSoloModeLoaded ? 'relative bg-deck overflow-hidden' : 'sr-only'}
226
- {...(!isSoloModeLoaded && { inert: '' })}
259
+ className={solo ? 'relative bg-deck overflow-hidden' : 'sr-only'}
260
+ {...(!solo && { inert: '' })}
227
261
  >
262
+ {!topbar && <ToggleSidebarButton classNames={fixedSidebarToggleStyles} />}
263
+ {!topbar && <ToggleComplementarySidebarButton classNames={fixedComplementarySidebarToggleStyles} />}
228
264
  <StackContext.Provider value={{ size: 'contain', orientation: 'horizontal', rail: true }}>
229
- <Plank entry={layoutParts.solo?.[0]} layoutParts={layoutParts} part='solo' layoutMode={layoutMode} />
265
+ <Plank id={solo} part='solo' layoutMode={layoutMode} />
230
266
  </StackContext.Provider>
231
267
  </div>
232
268
  </Main.Content>
233
269
  )}
234
270
 
235
- {/* Footer status. */}
236
- <StatusBar showHints={showHints} />
271
+ {/* Status bar. */}
272
+ {topbar && <Topbar />}
273
+ {hoistStatusbar && <StatusBar showHints={showHints} />}
237
274
  </Main.Root>
238
275
  )}
239
276
 
@@ -4,8 +4,7 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { Surface } from '@dxos/app-framework';
8
- import { useGraph } from '@dxos/plugin-graph';
7
+ import { Surface, useAppGraph } from '@dxos/app-framework';
9
8
  import { fixedInsetFlexLayout } from '@dxos/react-ui-theme';
10
9
 
11
10
  import { Fallback } from './Fallback';
@@ -13,7 +12,7 @@ import { SURFACE_PREFIX } from './constants';
13
12
  import { useNode } from '../../hooks';
14
13
 
15
14
  export const Fullscreen = ({ id }: { id?: string }) => {
16
- const { graph } = useGraph();
15
+ const { graph } = useAppGraph();
17
16
  const fullScreenNode = useNode(graph, id);
18
17
 
19
18
  return (