@dxos/plugin-simple-layout 0.0.0 → 0.8.4-main.52d7546f51
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.
- package/dist/lib/browser/chunk-7VLT3S46.mjs +29 -0
- package/dist/lib/browser/chunk-7VLT3S46.mjs.map +7 -0
- package/dist/lib/browser/chunk-O3BQBYMW.mjs +1165 -0
- package/dist/lib/browser/chunk-O3BQBYMW.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +101 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/operation-resolver-BYRIQOQT.mjs +205 -0
- package/dist/lib/browser/operation-resolver-BYRIQOQT.mjs.map +7 -0
- package/dist/lib/browser/react-root-GPTKI5H2.mjs +21 -0
- package/dist/lib/browser/react-root-GPTKI5H2.mjs.map +7 -0
- package/dist/lib/browser/react-surface-LT5JJTPR.mjs +41 -0
- package/dist/lib/browser/react-surface-LT5JJTPR.mjs.map +7 -0
- package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs +66 -0
- package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs.map +7 -0
- package/dist/lib/browser/state-A3PGDWWZ.mjs +48 -0
- package/dist/lib/browser/state-A3PGDWWZ.mjs.map +7 -0
- package/dist/lib/browser/url-handler-HTIUY6WL.mjs +152 -0
- package/dist/lib/browser/url-handler-HTIUY6WL.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-UAWM4B2S.mjs +1166 -0
- package/dist/lib/node-esm/chunk-UAWM4B2S.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-VIDE5UMB.mjs +31 -0
- package/dist/lib/node-esm/chunk-VIDE5UMB.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +102 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/operation-resolver-BDTFNCS2.mjs +206 -0
- package/dist/lib/node-esm/operation-resolver-BDTFNCS2.mjs.map +7 -0
- package/dist/lib/node-esm/react-root-GRG2OAI2.mjs +22 -0
- package/dist/lib/node-esm/react-root-GRG2OAI2.mjs.map +7 -0
- package/dist/lib/node-esm/react-surface-TCUSDIN2.mjs +42 -0
- package/dist/lib/node-esm/react-surface-TCUSDIN2.mjs.map +7 -0
- package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs +68 -0
- package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs.map +7 -0
- package/dist/lib/node-esm/state-ZCFZTTPL.mjs +49 -0
- package/dist/lib/node-esm/state-ZCFZTTPL.mjs.map +7 -0
- package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs +153 -0
- package/dist/lib/node-esm/url-handler-WBVVKVPC.mjs.map +7 -0
- package/dist/types/src/SimpleLayoutPlugin.d.ts +7 -0
- package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +7 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
- package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-root/index.d.ts +6 -0
- package/dist/types/src/capabilities/react-root/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-root/react-root.d.ts +9 -0
- package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
- package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
- package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +3 -0
- package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +14 -0
- package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -0
- package/dist/types/src/capabilities/state/index.d.ts +13 -0
- package/dist/types/src/capabilities/state/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/state/state.d.ts +19 -0
- package/dist/types/src/capabilities/state/state.d.ts.map +1 -0
- package/dist/types/src/capabilities/url-handler/index.d.ts +3 -0
- package/dist/types/src/capabilities/url-handler/index.d.ts.map +1 -0
- package/dist/types/src/capabilities/url-handler/url-handler.d.ts +12 -0
- package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -0
- package/dist/types/src/components/ContentError.d.ts +5 -0
- package/dist/types/src/components/ContentError.d.ts.map +1 -0
- package/dist/types/src/components/ContentError.stories.d.ts +41 -0
- package/dist/types/src/components/ContentError.stories.d.ts.map +1 -0
- package/dist/types/src/components/ContentLoading.d.ts +3 -0
- package/dist/types/src/components/ContentLoading.d.ts.map +1 -0
- package/dist/types/src/components/ContentLoading.stories.d.ts +13 -0
- package/dist/types/src/components/ContentLoading.stories.d.ts.map +1 -0
- package/dist/types/src/components/Dialog/Dialog.d.ts +3 -0
- package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -0
- package/dist/types/src/components/Dialog/index.d.ts +2 -0
- package/dist/types/src/components/Dialog/index.d.ts.map +1 -0
- package/dist/types/src/components/Home/Home.d.ts +7 -0
- package/dist/types/src/components/Home/Home.d.ts.map +1 -0
- package/dist/types/src/components/Home/index.d.ts +2 -0
- package/dist/types/src/components/Home/index.d.ts.map +1 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.d.ts +35 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.d.ts.map +1 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts +7 -0
- package/dist/types/src/components/MobileLayout/MobileLayout.stories.d.ts.map +1 -0
- package/dist/types/src/components/MobileLayout/index.d.ts +2 -0
- package/dist/types/src/components/MobileLayout/index.d.ts.map +1 -0
- package/dist/types/src/components/Popover/Popover.d.ts +4 -0
- package/dist/types/src/components/Popover/Popover.d.ts.map +1 -0
- package/dist/types/src/components/Popover/index.d.ts +2 -0
- package/dist/types/src/components/Popover/index.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/AppBar.d.ts +26 -0
- package/dist/types/src/components/SimpleLayout/AppBar.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts +47 -0
- package/dist/types/src/components/SimpleLayout/AppBar.stories.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts +9 -0
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/Main.d.ts +9 -0
- package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts +18 -0
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +43 -0
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts +3 -0
- package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts +47 -0
- package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -0
- package/dist/types/src/components/SimpleLayout/index.d.ts +5 -0
- package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -0
- package/dist/types/src/components/Workspace/Workspace.d.ts +11 -0
- package/dist/types/src/components/Workspace/Workspace.d.ts.map +1 -0
- package/dist/types/src/components/Workspace/index.d.ts +2 -0
- package/dist/types/src/components/Workspace/index.d.ts.map +1 -0
- package/dist/types/src/components/hooks.d.ts +5 -0
- package/dist/types/src/components/hooks.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +7 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/hooks/actions.d.ts +20 -0
- package/dist/types/src/hooks/actions.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +7 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useAppBarProps.d.ts +7 -0
- package/dist/types/src/hooks/useAppBarProps.d.ts.map +1 -0
- package/dist/types/src/hooks/useCompanions.d.ts +12 -0
- package/dist/types/src/hooks/useCompanions.d.ts.map +1 -0
- package/dist/types/src/hooks/useDrawerActions.d.ts +13 -0
- package/dist/types/src/hooks/useDrawerActions.d.ts.map +1 -0
- package/dist/types/src/hooks/useNavbarActions.d.ts +14 -0
- package/dist/types/src/hooks/useNavbarActions.d.ts.map +1 -0
- package/dist/types/src/hooks/useSimpleLayoutState.d.ts +7 -0
- package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +3 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +26 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types/capabilities.d.ts +36 -0
- package/dist/types/src/types/capabilities.d.ts.map +1 -0
- package/dist/types/src/types/events.d.ts +6 -0
- package/dist/types/src/types/events.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +3 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +39 -29
- package/src/SimpleLayoutPlugin.ts +25 -8
- package/src/capabilities/index.ts +3 -0
- package/src/capabilities/operation-resolver/operation-resolver.ts +135 -53
- package/src/capabilities/react-root/react-root.tsx +2 -2
- package/src/capabilities/react-surface/index.ts +7 -0
- package/src/capabilities/react-surface/react-surface.tsx +41 -0
- package/src/capabilities/spotlight-dismiss/index.ts +7 -0
- package/src/{hooks/useSpotlightDismiss.ts → capabilities/spotlight-dismiss/spotlight-dismiss.ts} +31 -40
- package/src/capabilities/state/state.tsx +25 -33
- package/src/capabilities/url-handler/index.ts +7 -0
- package/src/capabilities/url-handler/url-handler.ts +157 -0
- package/src/components/ContentError.stories.tsx +1 -1
- package/src/components/ContentLoading.stories.tsx +1 -1
- package/src/components/Dialog/Dialog.tsx +14 -14
- package/src/components/Home/Home.tsx +64 -70
- package/src/components/MobileLayout/MobileLayout.stories.tsx +125 -0
- package/src/components/MobileLayout/MobileLayout.tsx +305 -0
- package/src/components/MobileLayout/index.ts +5 -0
- package/src/components/Popover/Popover.tsx +45 -27
- package/src/components/SimpleLayout/AppBar.stories.tsx +144 -0
- package/src/components/SimpleLayout/AppBar.tsx +101 -0
- package/src/components/SimpleLayout/Drawer.tsx +102 -0
- package/src/components/SimpleLayout/Main.tsx +53 -57
- package/src/components/SimpleLayout/NavBar.stories.tsx +164 -0
- package/src/components/SimpleLayout/NavBar.tsx +29 -86
- package/src/components/SimpleLayout/SimpleLayout.stories.tsx +24 -18
- package/src/components/SimpleLayout/SimpleLayout.tsx +45 -7
- package/src/components/SimpleLayout/index.ts +3 -0
- package/src/components/Workspace/Workspace.tsx +119 -0
- package/src/components/Workspace/index.ts +5 -0
- package/src/components/hooks.ts +26 -0
- package/src/components/index.ts +2 -0
- package/src/hooks/actions.ts +85 -0
- package/src/hooks/index.ts +6 -1
- package/src/hooks/useAppBarProps.ts +112 -0
- package/src/hooks/useCompanions.ts +22 -0
- package/src/hooks/useDrawerActions.ts +98 -0
- package/src/hooks/useNavbarActions.ts +86 -0
- package/src/hooks/useSimpleLayoutState.ts +30 -0
- package/src/translations.ts +6 -0
- package/src/types/capabilities.ts +20 -4
- package/src/types/events.ts +15 -0
- package/src/types/index.ts +1 -0
- package/src/components/SimpleLayout/Banner.tsx +0 -60
- package/src/components/SimpleLayout/NavBarstories.tsx +0 -59
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import React, { type PropsWithChildren, forwardRef, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { addEventListener, combine } from '@dxos/async';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
|
+
import { mx } from '@dxos/ui-theme';
|
|
12
|
+
|
|
13
|
+
// TODO(burdon): Move into @dxos/react-ui?
|
|
14
|
+
|
|
15
|
+
const MOBILE_LAYOUT_NAME = 'MobileLayout';
|
|
16
|
+
const MOBILE_LAYOUT_ROOT_NAME = 'MobileLayout.Root';
|
|
17
|
+
const MOBILE_LAYOUT_PANEL_NAME = 'MobileLayout.Panel';
|
|
18
|
+
|
|
19
|
+
//
|
|
20
|
+
// Context
|
|
21
|
+
//
|
|
22
|
+
|
|
23
|
+
type MobileLayoutContextValue = {
|
|
24
|
+
keyboardOpen: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const [MobileLayoutProvider, useMobileLayout] = createContext<MobileLayoutContextValue>(MOBILE_LAYOUT_NAME);
|
|
28
|
+
|
|
29
|
+
//
|
|
30
|
+
// Root
|
|
31
|
+
//
|
|
32
|
+
|
|
33
|
+
type MobileLayoutRootProps = ThemedClassName<
|
|
34
|
+
PropsWithChildren<{
|
|
35
|
+
transition?: number;
|
|
36
|
+
onKeyboardOpenChange?: (nextState: boolean) => void;
|
|
37
|
+
}>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mobile layout root container that handles iOS keyboard detection.
|
|
42
|
+
*/
|
|
43
|
+
// TODO(burdon): Should this be ios-only?
|
|
44
|
+
const MobileLayoutRoot = forwardRef<HTMLDivElement, MobileLayoutRootProps>(
|
|
45
|
+
({ classNames, children, transition = 250, onKeyboardOpenChange, ...props }, forwardedRef) => {
|
|
46
|
+
const { open: keyboardOpen } = useIOSKeyboard();
|
|
47
|
+
useAutoScroll();
|
|
48
|
+
useEffect(() => onKeyboardOpenChange?.(keyboardOpen), [onKeyboardOpenChange, keyboardOpen]);
|
|
49
|
+
useLockBodyScroll(keyboardOpen);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<MobileLayoutProvider keyboardOpen={keyboardOpen}>
|
|
53
|
+
<div
|
|
54
|
+
{...props}
|
|
55
|
+
role='none'
|
|
56
|
+
style={{
|
|
57
|
+
transition: `block-size ${transition}ms ease-out`,
|
|
58
|
+
blockSize: 'calc(100vh - var(--kb-height, 0px))',
|
|
59
|
+
}}
|
|
60
|
+
className={mx('absolute top-0 left-0 right-0 flex flex-col', classNames)}
|
|
61
|
+
ref={forwardedRef}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
</MobileLayoutProvider>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
MobileLayoutRoot.displayName = MOBILE_LAYOUT_ROOT_NAME;
|
|
71
|
+
|
|
72
|
+
//
|
|
73
|
+
// Panel
|
|
74
|
+
//
|
|
75
|
+
|
|
76
|
+
type MobileLayoutPanelProps = ThemedClassName<
|
|
77
|
+
PropsWithChildren<{
|
|
78
|
+
safe?: {
|
|
79
|
+
top: boolean;
|
|
80
|
+
bottom: boolean;
|
|
81
|
+
};
|
|
82
|
+
}>
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Mobile layout panel that applies safe area insets.
|
|
87
|
+
*/
|
|
88
|
+
const MobileLayoutPanel = forwardRef<HTMLDivElement, MobileLayoutPanelProps>(
|
|
89
|
+
({ classNames, children, safe, ...props }, forwardedRef) => {
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
{...props}
|
|
93
|
+
role='none'
|
|
94
|
+
style={{
|
|
95
|
+
paddingTop: safe?.top ? 'env(safe-area-inset-top)' : undefined,
|
|
96
|
+
paddingBottom: safe?.bottom ? `calc((1 - var(--kb-open, 0)) * env(safe-area-inset-bottom))` : undefined,
|
|
97
|
+
}}
|
|
98
|
+
className={mx('relative bs-full flex flex-col overflow-hidden', classNames)}
|
|
99
|
+
ref={forwardedRef}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
MobileLayoutPanel.displayName = MOBILE_LAYOUT_PANEL_NAME;
|
|
108
|
+
|
|
109
|
+
//
|
|
110
|
+
// Exports
|
|
111
|
+
//
|
|
112
|
+
|
|
113
|
+
export const MobileLayout = {
|
|
114
|
+
Root: MobileLayoutRoot,
|
|
115
|
+
Panel: MobileLayoutPanel,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export { useMobileLayout };
|
|
119
|
+
|
|
120
|
+
export type { MobileLayoutRootProps, MobileLayoutPanelProps };
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Prevent auto-scroll when input is focused.
|
|
124
|
+
*/
|
|
125
|
+
const useAutoScroll = () => {
|
|
126
|
+
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 },
|
|
157
|
+
);
|
|
158
|
+
}, []);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Prevent iOS Safari viewport scroll when enabled.
|
|
163
|
+
* Setting overflow:hidden doesn't work on iOS, so we must preventDefault on touchmove events.
|
|
164
|
+
* Only allows scrolling if the target is within a scrollable container.
|
|
165
|
+
*/
|
|
166
|
+
const useLockBodyScroll = (enabled: boolean) => {
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!enabled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isScrollable = (el: HTMLElement | null, axis: 'x' | 'y'): boolean => {
|
|
173
|
+
while (el && el !== document.body) {
|
|
174
|
+
const style = getComputedStyle(el);
|
|
175
|
+
if (axis === 'y') {
|
|
176
|
+
const overflow = style.overflowY;
|
|
177
|
+
if ((overflow === 'auto' || overflow === 'scroll') && el.scrollHeight > el.clientHeight) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
const overflow = style.overflowX;
|
|
182
|
+
if ((overflow === 'auto' || overflow === 'scroll') && el.scrollWidth > el.clientWidth) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
el = el.parentElement;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let touchStartX = 0;
|
|
194
|
+
let touchStartY = 0;
|
|
195
|
+
|
|
196
|
+
return combine(
|
|
197
|
+
// Record initial touch position.
|
|
198
|
+
addEventListener(
|
|
199
|
+
document,
|
|
200
|
+
'touchstart',
|
|
201
|
+
(event: TouchEvent) => {
|
|
202
|
+
const touch = event.touches[0];
|
|
203
|
+
touchStartX = touch.clientX;
|
|
204
|
+
touchStartY = touch.clientY;
|
|
205
|
+
},
|
|
206
|
+
{ passive: true },
|
|
207
|
+
),
|
|
208
|
+
|
|
209
|
+
// Prevent scrolling the viewport.
|
|
210
|
+
addEventListener(
|
|
211
|
+
document,
|
|
212
|
+
'touchmove',
|
|
213
|
+
(event: TouchEvent) => {
|
|
214
|
+
const touch = event.touches[0];
|
|
215
|
+
const dx = Math.abs(touch.clientX - touchStartX);
|
|
216
|
+
const dy = Math.abs(touch.clientY - touchStartY);
|
|
217
|
+
if (!isScrollable(event.target as HTMLElement, dx > dy ? 'x' : 'y')) {
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{ passive: false },
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}, [enabled]);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
//
|
|
228
|
+
// Hooks
|
|
229
|
+
//
|
|
230
|
+
|
|
231
|
+
type IOSKeyboard = {
|
|
232
|
+
open: boolean;
|
|
233
|
+
height: number;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Mobile container that handles iOS keyboard layout adjustments.
|
|
238
|
+
*
|
|
239
|
+
* Uses two strategies for keyboard detection:
|
|
240
|
+
* 1. Tauri iOS: Native keyboard plugin for reliable height/animation events.
|
|
241
|
+
* 2. Web/PWA: visualViewport API as fallback.
|
|
242
|
+
*
|
|
243
|
+
* iPhone (portrait, points)
|
|
244
|
+
* - Without predictive bar: ~291 pt
|
|
245
|
+
* - With predictive bar: ~335 pt
|
|
246
|
+
* - With accessory view: ~380–420 pt
|
|
247
|
+
*
|
|
248
|
+
* Example:
|
|
249
|
+
* - Viewport: 874 (entire screen)
|
|
250
|
+
* - SafeArea: 96 (62+34)
|
|
251
|
+
* - Main: 778
|
|
252
|
+
* - Keyboard: 318; 413 (incl. Input Accessory View)
|
|
253
|
+
*
|
|
254
|
+
* CSS Variables set on document.documentElement:
|
|
255
|
+
* --vvh: Visual viewport height (use as container height).
|
|
256
|
+
* --kb-height: Keyboard height in pixels.
|
|
257
|
+
* --kb-open: 1 when keyboard is open, 0 when closed.
|
|
258
|
+
*
|
|
259
|
+
* NOTE: By default when an input is selected on iOS the Input Accessory View is shown above the keyboard.
|
|
260
|
+
* This can be disabled by setting the `inputAccessoryView` property to `false`.
|
|
261
|
+
*
|
|
262
|
+
* On iOS (Tauri), listens for 'keyboard' CustomEvents dispatched by the native KeyboardObserver.swift.
|
|
263
|
+
* Falls back to VisualViewport API on other platforms.
|
|
264
|
+
*/
|
|
265
|
+
const useIOSKeyboard = (): IOSKeyboard => {
|
|
266
|
+
const [open, setOpen] = useState(false);
|
|
267
|
+
const [height, setHeight] = useState(0);
|
|
268
|
+
|
|
269
|
+
// Detect keybaord state.
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const viewport = window.visualViewport;
|
|
272
|
+
if (!viewport) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handler for VisualViewport resize (fallback for non-iOS).
|
|
277
|
+
const initialHeight = viewport.height ?? window.innerHeight;
|
|
278
|
+
|
|
279
|
+
const updateState = (keyboardHeight: number, keyboardOpen: boolean) => {
|
|
280
|
+
setOpen(keyboardOpen);
|
|
281
|
+
setHeight(keyboardHeight);
|
|
282
|
+
|
|
283
|
+
const vvh = initialHeight - keyboardHeight;
|
|
284
|
+
document.documentElement.style.setProperty('--vvh', `${vvh}px`);
|
|
285
|
+
document.documentElement.style.setProperty('--kb-height', `${keyboardHeight}px`);
|
|
286
|
+
document.documentElement.style.setProperty('--kb-open', keyboardOpen ? '1' : '0');
|
|
287
|
+
log.info('viewport size', { initialHeight, vvh, keyboardHeight, keyboardOpen });
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return combine(
|
|
291
|
+
// Handler for native iOS keyboard events (from KeyboardObserver.swift).
|
|
292
|
+
addEventListener(
|
|
293
|
+
window,
|
|
294
|
+
'keyboard' as any,
|
|
295
|
+
(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');
|
|
299
|
+
},
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
}, []);
|
|
303
|
+
|
|
304
|
+
return { open, height };
|
|
305
|
+
};
|
|
@@ -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
|
|
9
|
-
import { Popover, type PopoverContentInteractOutsideEvent } from '@dxos/react-ui';
|
|
8
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
9
|
+
import { Popover, type PopoverContentInteractOutsideEvent, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
10
|
+
import { Card } from '@dxos/react-ui-mosaic';
|
|
10
11
|
|
|
11
|
-
import {
|
|
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
|
|
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 (
|
|
34
|
+
if (state.popoverOpen) {
|
|
33
35
|
if (debounceRef.current) {
|
|
34
36
|
clearTimeout(debounceRef.current);
|
|
35
37
|
}
|
|
36
|
-
if (
|
|
37
|
-
virtualRef.current =
|
|
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
|
-
}, [
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
68
|
-
layout.popoverOpen = false;
|
|
69
|
-
layout.popoverAnchor = undefined;
|
|
70
|
-
layout.popoverAnchorId = undefined;
|
|
71
|
-
layout.popoverSide = undefined;
|
|
81
|
+
handleClose();
|
|
72
82
|
}
|
|
73
83
|
},
|
|
74
|
-
[
|
|
84
|
+
[handleClose],
|
|
75
85
|
);
|
|
76
86
|
|
|
77
87
|
const collisionBoundaries: HTMLElement[] = useMemo(() => {
|
|
78
|
-
const closest =
|
|
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
|
-
}, [
|
|
90
|
+
}, [state.popoverAnchor]);
|
|
84
91
|
|
|
85
92
|
return (
|
|
86
93
|
<Popover.Portal>
|
|
87
94
|
<Popover.Content
|
|
88
|
-
side={
|
|
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='
|
|
103
|
+
{state.popoverKind === 'base' && <Surface.Surface role='popover' data={state.popoverContent} limit={1} />}
|
|
104
|
+
{state.popoverKind === 'card' && (
|
|
105
|
+
<Card.Root border={false} classNames='popover-card-max-width'>
|
|
106
|
+
<Card.Toolbar>
|
|
107
|
+
{/* TODO(wittjosiah): Cleaner way to handle no drag handle in toolbar? */}
|
|
108
|
+
<span />
|
|
109
|
+
{state.popoverTitle ? <Card.Title>{toLocalizedString(state.popoverTitle, t)}</Card.Title> : <span />}
|
|
110
|
+
<Card.Close onClick={handleClose} />
|
|
111
|
+
</Card.Toolbar>
|
|
112
|
+
<Surface.Surface role='card--content' data={state.popoverContent} limit={1} />
|
|
113
|
+
</Card.Root>
|
|
114
|
+
)}
|
|
97
115
|
</Popover.Viewport>
|
|
98
116
|
<Popover.Arrow />
|
|
99
117
|
</Popover.Content>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
import { type Mock, expect, fn, screen, userEvent, within } from 'storybook/test';
|
|
9
|
+
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
import { type ActionGraphProps, createMenuAction } from '@dxos/react-ui-menu';
|
|
12
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
13
|
+
|
|
14
|
+
import { translations } from '../../translations';
|
|
15
|
+
import { MobileLayout } from '../MobileLayout';
|
|
16
|
+
|
|
17
|
+
import { AppBar, type AppBarProps } from './AppBar';
|
|
18
|
+
|
|
19
|
+
const buildEmptyActions = (): ActionGraphProps => ({ nodes: [], edges: [] });
|
|
20
|
+
|
|
21
|
+
const buildDefaultActions = (): ActionGraphProps => {
|
|
22
|
+
const result: ActionGraphProps = { nodes: [], edges: [] };
|
|
23
|
+
const actions = [
|
|
24
|
+
createMenuAction('action-edit', () => console.log('Edit'), {
|
|
25
|
+
icon: 'ph--pencil--regular',
|
|
26
|
+
label: 'Edit',
|
|
27
|
+
}),
|
|
28
|
+
createMenuAction('action-share', () => console.log('Share'), {
|
|
29
|
+
icon: 'ph--share--regular',
|
|
30
|
+
label: 'Share',
|
|
31
|
+
}),
|
|
32
|
+
createMenuAction('action-delete', () => console.log('Delete'), {
|
|
33
|
+
icon: 'ph--trash--regular',
|
|
34
|
+
label: 'Delete',
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
37
|
+
result.nodes.push(...actions);
|
|
38
|
+
result.edges.push(...actions.map((a) => ({ source: 'root', target: a.id })));
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type StoryProps = Omit<AppBarProps, 'actions'> & {
|
|
43
|
+
actions: ActionGraphProps;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DefaultStory = ({ actions: actionsProp, ...props }: StoryProps) => {
|
|
47
|
+
const actions = useMemo(() => Atom.make(actionsProp).pipe(Atom.keepAlive), [actionsProp]);
|
|
48
|
+
return (
|
|
49
|
+
<MobileLayout.Root>
|
|
50
|
+
<AppBar {...props} actions={actions} />
|
|
51
|
+
</MobileLayout.Root>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const meta = {
|
|
56
|
+
title: 'plugins/plugin-simple-layout/AppBar',
|
|
57
|
+
render: DefaultStory,
|
|
58
|
+
decorators: [
|
|
59
|
+
withTheme(),
|
|
60
|
+
withLayout({
|
|
61
|
+
layout: 'column',
|
|
62
|
+
classNames: 'relative',
|
|
63
|
+
}),
|
|
64
|
+
withRegistry,
|
|
65
|
+
],
|
|
66
|
+
parameters: {
|
|
67
|
+
layout: 'fullscreen',
|
|
68
|
+
translations,
|
|
69
|
+
},
|
|
70
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
71
|
+
|
|
72
|
+
export default meta;
|
|
73
|
+
|
|
74
|
+
type Story = StoryObj<StoryProps>;
|
|
75
|
+
|
|
76
|
+
export const Default: Story = {
|
|
77
|
+
tags: ['test'],
|
|
78
|
+
args: {
|
|
79
|
+
actions: buildDefaultActions(),
|
|
80
|
+
title: 'Document Title',
|
|
81
|
+
showBackButton: true,
|
|
82
|
+
onAction: fn(),
|
|
83
|
+
onBack: fn(),
|
|
84
|
+
},
|
|
85
|
+
play: async ({ args, canvasElement }) => {
|
|
86
|
+
const canvas = within(canvasElement);
|
|
87
|
+
|
|
88
|
+
// Verify the banner renders with the correct title.
|
|
89
|
+
await expect(canvas.getByRole('banner')).toBeInTheDocument();
|
|
90
|
+
await expect(canvas.getByText('Document Title')).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
// Test back button click.
|
|
93
|
+
const backButton = canvas.getByRole('button', { name: /back/i });
|
|
94
|
+
await expect(backButton).toBeInTheDocument();
|
|
95
|
+
await userEvent.click(backButton);
|
|
96
|
+
await expect(args.onBack).toHaveBeenCalledTimes(1);
|
|
97
|
+
|
|
98
|
+
// Test actions menu opens and action fires.
|
|
99
|
+
const menuTrigger = canvas.getByRole('button', { name: /actions/i });
|
|
100
|
+
await expect(menuTrigger).toBeInTheDocument();
|
|
101
|
+
await userEvent.click(menuTrigger);
|
|
102
|
+
|
|
103
|
+
// Wait for menu to open and click an action (menu items render in a portal).
|
|
104
|
+
const editAction = await screen.findByRole('menuitem', { name: /edit/i });
|
|
105
|
+
await userEvent.click(editAction);
|
|
106
|
+
await expect(args.onAction).toHaveBeenCalledTimes(1);
|
|
107
|
+
await expect((args.onAction as Mock).mock.calls[0][0]).toHaveProperty('id', 'action-edit');
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const NoBackButton: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
actions: buildDefaultActions(),
|
|
114
|
+
title: 'Home',
|
|
115
|
+
showBackButton: false,
|
|
116
|
+
onAction: (action) => console.log('Action:', action.id),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const LongTitle: Story = {
|
|
121
|
+
args: {
|
|
122
|
+
actions: buildDefaultActions(),
|
|
123
|
+
title: 'This is a very long document title that should be truncated when it exceeds the available space',
|
|
124
|
+
showBackButton: true,
|
|
125
|
+
onBack: () => console.log('Back clicked'),
|
|
126
|
+
onAction: (action) => console.log('Action:', action.id),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const NoActions: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
actions: buildEmptyActions(),
|
|
133
|
+
title: 'Empty Document',
|
|
134
|
+
showBackButton: true,
|
|
135
|
+
onBack: () => console.log('Back clicked'),
|
|
136
|
+
onAction: (action) => console.log('Action:', action.id),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const Empty: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
actions: buildEmptyActions(),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Atom, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { Fragment } from 'react';
|
|
7
|
+
|
|
8
|
+
import { IconButton, Popover, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import {
|
|
10
|
+
type ActionExecutor,
|
|
11
|
+
type ActionGraphProps,
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
MenuProvider,
|
|
14
|
+
useMenuActions,
|
|
15
|
+
} from '@dxos/react-ui-menu';
|
|
16
|
+
import { mx, osTranslations } from '@dxos/ui-theme';
|
|
17
|
+
|
|
18
|
+
import { meta } from '../../meta';
|
|
19
|
+
import { useMobileLayout } from '../MobileLayout';
|
|
20
|
+
|
|
21
|
+
const APP_BAR_NAME = 'SimpleLayout.AppBar';
|
|
22
|
+
|
|
23
|
+
export type AppBarProps = ThemedClassName<{
|
|
24
|
+
/** Title/label to display in the banner. */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Action graph atom for the dropdown menu. */
|
|
27
|
+
actions: Atom.Atom<ActionGraphProps>;
|
|
28
|
+
/** Whether to show the back button. */
|
|
29
|
+
showBackButton?: boolean;
|
|
30
|
+
/** Popover anchor ID for the dropdown trigger. */
|
|
31
|
+
popoverAnchorId?: string;
|
|
32
|
+
/** Action executor callback. */
|
|
33
|
+
onAction?: ActionExecutor;
|
|
34
|
+
/** Callback when back button is clicked. */
|
|
35
|
+
onBack?: () => void;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* AppBar component that renders a title, optional back button, and actions dropdown.
|
|
40
|
+
*/
|
|
41
|
+
export const AppBar = ({
|
|
42
|
+
classNames,
|
|
43
|
+
title,
|
|
44
|
+
actions,
|
|
45
|
+
showBackButton,
|
|
46
|
+
popoverAnchorId,
|
|
47
|
+
onAction,
|
|
48
|
+
onBack,
|
|
49
|
+
}: AppBarProps) => {
|
|
50
|
+
const { t } = useTranslation(meta.id);
|
|
51
|
+
const menu = useMenuActions(actions);
|
|
52
|
+
const actionsValue = useAtomValue(actions);
|
|
53
|
+
const hasActions = actionsValue.nodes.length > 0;
|
|
54
|
+
const { keyboardOpen } = useMobileLayout(APP_BAR_NAME);
|
|
55
|
+
|
|
56
|
+
// Fall back to app name if no title provided.
|
|
57
|
+
const displayTitle = title ?? t('current app name', { ns: osTranslations });
|
|
58
|
+
|
|
59
|
+
// Wrap the menu trigger with Popover.Anchor when the popoverAnchorId is set.
|
|
60
|
+
const AnchorRoot = popoverAnchorId ? Popover.Anchor : Fragment;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Toolbar.Root
|
|
64
|
+
role='banner'
|
|
65
|
+
classNames={mx(
|
|
66
|
+
'grid grid-cols-[var(--rail-size)_1fr_var(--rail-size)] bs-[var(--rail-action)] items-center',
|
|
67
|
+
'density-fine',
|
|
68
|
+
classNames,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{keyboardOpen ? (
|
|
72
|
+
<IconButton variant='ghost' icon='ph--x--regular' iconOnly label={t('done label')} />
|
|
73
|
+
) : showBackButton ? (
|
|
74
|
+
<IconButton variant='ghost' icon='ph--caret-left--regular' iconOnly label={t('back label')} onClick={onBack} />
|
|
75
|
+
) : (
|
|
76
|
+
<div />
|
|
77
|
+
)}
|
|
78
|
+
<h1 className='text-center truncate font-thin uppercase'>{displayTitle}</h1>
|
|
79
|
+
{hasActions ? (
|
|
80
|
+
<AnchorRoot>
|
|
81
|
+
<MenuProvider {...menu} onAction={onAction}>
|
|
82
|
+
<DropdownMenu.Root caller={meta.id}>
|
|
83
|
+
<DropdownMenu.Trigger asChild>
|
|
84
|
+
<IconButton
|
|
85
|
+
variant='ghost'
|
|
86
|
+
icon='ph--dots-three-vertical--regular'
|
|
87
|
+
iconOnly
|
|
88
|
+
label={t('actions menu label')}
|
|
89
|
+
/>
|
|
90
|
+
</DropdownMenu.Trigger>
|
|
91
|
+
</DropdownMenu.Root>
|
|
92
|
+
</MenuProvider>
|
|
93
|
+
</AnchorRoot>
|
|
94
|
+
) : (
|
|
95
|
+
<span />
|
|
96
|
+
)}
|
|
97
|
+
</Toolbar.Root>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
AppBar.displayName = APP_BAR_NAME;
|