@dxos/plugin-simple-layout 0.8.4-main.9735255 → 0.8.4-main.c351d160a8
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-2YSUM2L4.mjs +1153 -0
- package/dist/lib/browser/chunk-2YSUM2L4.mjs.map +7 -0
- package/dist/lib/browser/{chunk-P77G4YTR.mjs → chunk-MRR7PXSM.mjs} +5 -5
- package/dist/lib/browser/chunk-MRR7PXSM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +20 -19
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{operation-resolver-775UYAC2.mjs → operation-resolver-LV5FK6MN.mjs} +39 -47
- package/dist/lib/browser/operation-resolver-LV5FK6MN.mjs.map +7 -0
- package/dist/lib/browser/{react-root-KM55OMGJ.mjs → react-root-JR6OENXV.mjs} +5 -5
- package/dist/lib/browser/react-root-JR6OENXV.mjs.map +7 -0
- package/dist/lib/browser/{react-surface-BABGAWGY.mjs → react-surface-5JOPIP5Z.mjs} +18 -13
- package/dist/lib/browser/react-surface-5JOPIP5Z.mjs.map +7 -0
- package/dist/lib/browser/{spotlight-dismiss-VSNOPETH.mjs → spotlight-dismiss-67PHYS5B.mjs} +3 -3
- package/dist/lib/browser/spotlight-dismiss-67PHYS5B.mjs.map +7 -0
- package/dist/lib/browser/{state-OUFTC2KV.mjs → state-TXSMUWYI.mjs} +5 -4
- package/dist/lib/browser/state-TXSMUWYI.mjs.map +7 -0
- package/dist/lib/browser/url-handler-RBRONH7S.mjs +151 -0
- package/dist/lib/browser/url-handler-RBRONH7S.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-RCQE6FR2.mjs +1154 -0
- package/dist/lib/node-esm/chunk-RCQE6FR2.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-F5TEKVJG.mjs → chunk-WMNTJ2MK.mjs} +5 -5
- package/dist/lib/node-esm/chunk-WMNTJ2MK.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +20 -19
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{operation-resolver-LDNYS3DI.mjs → operation-resolver-FD3EF2LX.mjs} +39 -47
- package/dist/lib/node-esm/operation-resolver-FD3EF2LX.mjs.map +7 -0
- package/dist/lib/node-esm/{react-root-36UYFEEB.mjs → react-root-CRTCNH3F.mjs} +5 -5
- package/dist/lib/node-esm/react-root-CRTCNH3F.mjs.map +7 -0
- package/dist/lib/node-esm/{react-surface-CGHFVWU3.mjs → react-surface-NRTDSGFB.mjs} +18 -13
- package/dist/lib/node-esm/react-surface-NRTDSGFB.mjs.map +7 -0
- package/dist/lib/node-esm/{spotlight-dismiss-L5PCWIJG.mjs → spotlight-dismiss-RMLRZUVY.mjs} +3 -3
- package/dist/lib/node-esm/spotlight-dismiss-RMLRZUVY.mjs.map +7 -0
- package/dist/lib/node-esm/{state-Q2ZA26W5.mjs → state-JMX6FAG4.mjs} +5 -4
- package/dist/lib/node-esm/state-JMX6FAG4.mjs.map +7 -0
- package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs +152 -0
- package/dist/lib/node-esm/url-handler-QSMCH3JB.mjs.map +7 -0
- package/dist/types/src/SimpleLayoutPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +2 -2
- package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-root/react-root.d.ts +1 -1
- package/dist/types/src/capabilities/react-root/react-root.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface/index.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts +2 -2
- package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts +1 -1
- package/dist/types/src/capabilities/spotlight-dismiss/spotlight-dismiss.d.ts.map +1 -1
- package/dist/types/src/capabilities/state/index.d.ts +1 -1
- package/dist/types/src/capabilities/state/state.d.ts +1 -1
- package/dist/types/src/capabilities/state/state.d.ts.map +1 -1
- package/dist/types/src/capabilities/url-handler/url-handler.d.ts +5 -3
- package/dist/types/src/capabilities/url-handler/url-handler.d.ts.map +1 -1
- package/dist/types/src/components/ContentError.stories.d.ts +1 -3
- package/dist/types/src/components/ContentError.stories.d.ts.map +1 -1
- package/dist/types/src/components/ContentLoading/ContentLoading.d.ts.map +1 -0
- package/dist/types/src/components/ContentLoading/ContentLoading.stories.d.ts.map +1 -0
- package/dist/types/src/components/ContentLoading/index.d.ts +2 -0
- package/dist/types/src/components/ContentLoading/index.d.ts.map +1 -0
- package/dist/types/src/components/Home/Home.d.ts.map +1 -1
- 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/NavBranch/NavBranch.d.ts +11 -0
- package/dist/types/src/components/NavBranch/NavBranch.d.ts.map +1 -0
- package/dist/types/src/components/NavBranch/index.d.ts +2 -0
- package/dist/types/src/components/NavBranch/index.d.ts.map +1 -0
- package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
- 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 +1 -1
- package/dist/types/src/components/SimpleLayout/Drawer.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/Main.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts +10 -3
- package/dist/types/src/components/SimpleLayout/NavBar.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts +4 -4
- package/dist/types/src/components/SimpleLayout/NavBar.stories.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/SimpleLayout.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/SimpleLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/SimpleLayout/index.d.ts +3 -0
- package/dist/types/src/components/SimpleLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/hooks.d.ts +4 -2
- package/dist/types/src/components/hooks.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/actions.d.ts +19 -0
- package/dist/types/src/hooks/actions.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- 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 +5 -1
- package/dist/types/src/hooks/useCompanions.d.ts.map +1 -1
- 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 +3 -3
- package/dist/types/src/hooks/useSimpleLayoutState.d.ts.map +1 -1
- package/dist/types/src/types/capabilities.d.ts +7 -6
- package/dist/types/src/types/capabilities.d.ts.map +1 -1
- package/dist/types/src/types/events.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -30
- package/src/SimpleLayoutPlugin.ts +10 -9
- package/src/capabilities/operation-resolver/operation-resolver.ts +34 -46
- package/src/capabilities/react-root/react-root.tsx +2 -2
- package/src/capabilities/react-surface/react-surface.tsx +14 -11
- package/src/capabilities/spotlight-dismiss/spotlight-dismiss.ts +2 -2
- package/src/capabilities/state/state.tsx +4 -3
- package/src/capabilities/url-handler/url-handler.ts +98 -45
- package/src/components/ContentError.stories.tsx +8 -7
- package/src/components/{ContentLoading.stories.tsx → ContentLoading/ContentLoading.stories.tsx} +2 -2
- package/src/components/{ContentLoading.tsx → ContentLoading/ContentLoading.tsx} +1 -1
- package/src/components/ContentLoading/index.ts +5 -0
- package/src/components/Dialog/Dialog.tsx +5 -5
- package/src/components/Home/Home.tsx +44 -36
- package/src/components/MobileLayout/MobileLayout.stories.tsx +129 -0
- package/src/components/MobileLayout/MobileLayout.tsx +305 -0
- package/src/components/MobileLayout/index.ts +5 -0
- package/src/components/NavBranch/NavBranch.tsx +133 -0
- package/src/components/{Workspace → NavBranch}/index.ts +1 -1
- package/src/components/Popover/Popover.tsx +7 -7
- package/src/components/SimpleLayout/AppBar.stories.tsx +144 -0
- package/src/components/SimpleLayout/AppBar.tsx +94 -0
- package/src/components/SimpleLayout/Drawer.tsx +25 -80
- package/src/components/SimpleLayout/Main.tsx +40 -30
- package/src/components/SimpleLayout/NavBar.stories.tsx +131 -23
- package/src/components/SimpleLayout/NavBar.tsx +15 -48
- package/src/components/SimpleLayout/SimpleLayout.stories.tsx +20 -11
- package/src/components/SimpleLayout/SimpleLayout.tsx +38 -19
- package/src/components/SimpleLayout/index.ts +3 -0
- package/src/components/hooks.ts +9 -9
- package/src/components/index.ts +2 -1
- package/src/hooks/actions.ts +83 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAppBarProps.ts +115 -0
- package/src/hooks/useCompanions.ts +8 -5
- package/src/hooks/useDrawerActions.ts +100 -0
- package/src/hooks/useNavbarActions.ts +87 -0
- package/src/hooks/useSimpleLayoutState.ts +5 -5
- package/src/meta.ts +1 -1
- package/src/types/capabilities.ts +11 -7
- package/src/types/events.ts +3 -2
- package/dist/lib/browser/chunk-LR3EE3VB.mjs +0 -789
- package/dist/lib/browser/chunk-LR3EE3VB.mjs.map +0 -7
- package/dist/lib/browser/chunk-P77G4YTR.mjs.map +0 -7
- package/dist/lib/browser/operation-resolver-775UYAC2.mjs.map +0 -7
- package/dist/lib/browser/react-root-KM55OMGJ.mjs.map +0 -7
- package/dist/lib/browser/react-surface-BABGAWGY.mjs.map +0 -7
- package/dist/lib/browser/spotlight-dismiss-VSNOPETH.mjs.map +0 -7
- package/dist/lib/browser/state-OUFTC2KV.mjs.map +0 -7
- package/dist/lib/browser/url-handler-DOUFQIAC.mjs +0 -54
- package/dist/lib/browser/url-handler-DOUFQIAC.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-F5TEKVJG.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HB2B3LLG.mjs +0 -790
- package/dist/lib/node-esm/chunk-HB2B3LLG.mjs.map +0 -7
- package/dist/lib/node-esm/operation-resolver-LDNYS3DI.mjs.map +0 -7
- package/dist/lib/node-esm/react-root-36UYFEEB.mjs.map +0 -7
- package/dist/lib/node-esm/react-surface-CGHFVWU3.mjs.map +0 -7
- package/dist/lib/node-esm/spotlight-dismiss-L5PCWIJG.mjs.map +0 -7
- package/dist/lib/node-esm/state-Q2ZA26W5.mjs.map +0 -7
- package/dist/lib/node-esm/url-handler-DVAZZEUO.mjs +0 -55
- package/dist/lib/node-esm/url-handler-DVAZZEUO.mjs.map +0 -7
- package/dist/types/src/components/ContentError.d.ts +0 -5
- package/dist/types/src/components/ContentError.d.ts.map +0 -1
- package/dist/types/src/components/ContentLoading.d.ts.map +0 -1
- package/dist/types/src/components/ContentLoading.stories.d.ts.map +0 -1
- package/dist/types/src/components/SimpleLayout/Banner.d.ts +0 -8
- package/dist/types/src/components/SimpleLayout/Banner.d.ts.map +0 -1
- package/dist/types/src/components/Workspace/Workspace.d.ts +0 -9
- package/dist/types/src/components/Workspace/Workspace.d.ts.map +0 -1
- package/dist/types/src/components/Workspace/index.d.ts +0 -2
- package/dist/types/src/components/Workspace/index.d.ts.map +0 -1
- package/src/components/ContentError.tsx +0 -23
- package/src/components/SimpleLayout/Banner.tsx +0 -113
- package/src/components/Workspace/Workspace.tsx +0 -115
- /package/dist/types/src/components/{ContentLoading.d.ts → ContentLoading/ContentLoading.d.ts} +0 -0
- /package/dist/types/src/components/{ContentLoading.stories.d.ts → ContentLoading/ContentLoading.stories.d.ts} +0 -0
|
@@ -4,28 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
6
|
|
|
7
|
+
import { ErrorFallback } from '@dxos/react-ui';
|
|
7
8
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
8
9
|
|
|
9
10
|
import { translations } from '../translations';
|
|
10
11
|
|
|
11
|
-
import { ContentError } from './ContentError';
|
|
12
|
-
|
|
13
12
|
const meta = {
|
|
14
|
-
title: 'plugins/plugin-simple-layout/
|
|
15
|
-
component:
|
|
16
|
-
decorators: [withTheme],
|
|
13
|
+
title: 'plugins/plugin-simple-layout/components/ErrorFallback',
|
|
14
|
+
component: ErrorFallback,
|
|
15
|
+
decorators: [withTheme()],
|
|
17
16
|
parameters: {
|
|
18
17
|
layout: 'centered',
|
|
19
18
|
translations,
|
|
20
19
|
},
|
|
21
|
-
} satisfies Meta<typeof
|
|
20
|
+
} satisfies Meta<typeof ErrorFallback>;
|
|
22
21
|
|
|
23
22
|
export default meta;
|
|
24
23
|
|
|
25
24
|
type Story = StoryObj<typeof meta>;
|
|
26
25
|
|
|
27
26
|
export const Default: Story = {
|
|
28
|
-
args: {
|
|
27
|
+
args: {
|
|
28
|
+
error: new Error('An unexpected error occurred'),
|
|
29
|
+
},
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export const WithError: Story = {
|
package/src/components/{ContentLoading.stories.tsx → ContentLoading/ContentLoading.stories.tsx}
RENAMED
|
@@ -9,9 +9,9 @@ import { withTheme } from '@dxos/react-ui/testing';
|
|
|
9
9
|
import { ContentLoading } from './ContentLoading';
|
|
10
10
|
|
|
11
11
|
const meta = {
|
|
12
|
-
title: 'plugins/plugin-simple-layout/ContentLoading',
|
|
12
|
+
title: 'plugins/plugin-simple-layout/components/ContentLoading',
|
|
13
13
|
component: ContentLoading,
|
|
14
|
-
decorators: [withTheme],
|
|
14
|
+
decorators: [withTheme()],
|
|
15
15
|
parameters: {
|
|
16
16
|
layout: 'centered',
|
|
17
17
|
},
|
|
@@ -6,5 +6,5 @@ import React from 'react';
|
|
|
6
6
|
|
|
7
7
|
// TODO(burdon): Show skeleton: https://github.com/dxos/dxos/issues/8259
|
|
8
8
|
export const ContentLoading = () => {
|
|
9
|
-
return <div role='none' className='grid place-items-center attention-surface' />;
|
|
9
|
+
return <div role='none' className='grid place-items-center dx-attention-surface' />;
|
|
10
10
|
};
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import { Surface } from '@dxos/app-framework/
|
|
7
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
8
8
|
import { AlertDialog, Dialog as NaturalDialog } from '@dxos/react-ui';
|
|
9
|
+
import { ErrorFallback } from '@dxos/react-ui';
|
|
9
10
|
|
|
10
11
|
import { useSimpleLayoutState } from '../../hooks';
|
|
11
|
-
import { ContentError } from '../ContentError';
|
|
12
12
|
|
|
13
13
|
export const Dialog = () => {
|
|
14
14
|
const { state, updateState } = useSimpleLayoutState();
|
|
@@ -20,17 +20,17 @@ export const Dialog = () => {
|
|
|
20
20
|
<DialogRoot
|
|
21
21
|
modal={state.dialogBlockAlign !== 'end'}
|
|
22
22
|
open={state.dialogOpen}
|
|
23
|
-
onOpenChange={(nextOpen) => updateState((
|
|
23
|
+
onOpenChange={(nextOpen) => updateState((state) => ({ ...state, dialogOpen: nextOpen }))}
|
|
24
24
|
>
|
|
25
25
|
{state.dialogBlockAlign === 'end' ? (
|
|
26
|
-
<Surface role='dialog' data={state.dialogContent} limit={1} fallback={
|
|
26
|
+
<Surface.Surface role='dialog' data={state.dialogContent} limit={1} fallback={ErrorFallback} />
|
|
27
27
|
) : (
|
|
28
28
|
<DialogOverlay
|
|
29
29
|
blockAlign={state.dialogBlockAlign}
|
|
30
30
|
classNames={state.dialogOverlayClasses}
|
|
31
31
|
style={state.dialogOverlayStyle}
|
|
32
32
|
>
|
|
33
|
-
<Surface role='dialog' data={state.dialogContent} limit={1} fallback={
|
|
33
|
+
<Surface.Surface role='dialog' data={state.dialogContent} limit={1} fallback={ErrorFallback} />
|
|
34
34
|
</DialogOverlay>
|
|
35
35
|
)}
|
|
36
36
|
</DialogRoot>
|
|
@@ -4,17 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { useOperationInvoker } from '@dxos/app-framework/ui';
|
|
8
|
+
import { LayoutOperation } from '@dxos/app-toolkit';
|
|
9
|
+
import { useAppGraph } from '@dxos/app-toolkit/ui';
|
|
9
10
|
import { Node, useConnections } from '@dxos/plugin-graph';
|
|
10
|
-
import { Avatar, Icon, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
11
|
-
import { Card
|
|
11
|
+
import { Avatar, Icon, Panel, ScrollArea, Toolbar, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
12
|
+
import { Card } from '@dxos/react-ui';
|
|
13
|
+
import { Mosaic, type MosaicStackTileComponent } from '@dxos/react-ui-mosaic';
|
|
12
14
|
import { SearchList, useSearchListItem, useSearchListResults } from '@dxos/react-ui-searchlist';
|
|
13
15
|
import { mx } from '@dxos/ui-theme';
|
|
14
16
|
import { byPosition } from '@dxos/util';
|
|
15
17
|
|
|
16
18
|
import { meta } from '../../meta';
|
|
17
|
-
import {
|
|
19
|
+
import { useExpandPath } from '../hooks';
|
|
18
20
|
|
|
19
21
|
export type HomeProps = {};
|
|
20
22
|
|
|
@@ -26,7 +28,7 @@ export const Home = (_: HomeProps) => {
|
|
|
26
28
|
const userAccountItem = useItemsByDisposition('user-account')[0];
|
|
27
29
|
const pinnedItems = useItemsByDisposition('pin-end', true);
|
|
28
30
|
const workspaceItems = useItemsByDisposition('workspace');
|
|
29
|
-
|
|
31
|
+
useExpandPath(Node.RootId);
|
|
30
32
|
|
|
31
33
|
const items = useMemo(
|
|
32
34
|
() => [...(userAccountItem ? [userAccountItem] : []), ...pinnedItems, ...workspaceItems],
|
|
@@ -39,43 +41,49 @@ export const Home = (_: HomeProps) => {
|
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
return (
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
<Toolbar
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
<SearchList.Root onSearch={handleSearch}>
|
|
45
|
+
<Panel.Root>
|
|
46
|
+
<Panel.Toolbar asChild>
|
|
47
|
+
<Toolbar.Root>
|
|
48
|
+
<SearchList.Input placeholder={t('search placeholder')} />
|
|
49
|
+
</Toolbar.Root>
|
|
50
|
+
</Panel.Toolbar>
|
|
51
|
+
<Panel.Content asChild>
|
|
52
|
+
<SearchList.Content>
|
|
53
|
+
<Mosaic.Container asChild>
|
|
54
|
+
<ScrollArea.Root orientation='vertical'>
|
|
55
|
+
<ScrollArea.Viewport classNames='p-2'>
|
|
56
|
+
<Mosaic.Stack items={results} getId={(node) => node.id} Tile={WorkspaceTile} />
|
|
57
|
+
</ScrollArea.Viewport>
|
|
58
|
+
</ScrollArea.Root>
|
|
59
|
+
</Mosaic.Container>
|
|
60
|
+
</SearchList.Content>
|
|
61
|
+
</Panel.Content>
|
|
62
|
+
</Panel.Root>
|
|
63
|
+
</SearchList.Root>
|
|
56
64
|
);
|
|
57
65
|
};
|
|
58
66
|
|
|
59
|
-
const WorkspaceTile:
|
|
67
|
+
const WorkspaceTile: MosaicStackTileComponent<Node.Node> = (props) => {
|
|
68
|
+
const data = props.data;
|
|
60
69
|
const { t } = useTranslation(meta.id);
|
|
61
70
|
const { invokePromise } = useOperationInvoker();
|
|
62
71
|
const { selectedValue, registerItem, unregisterItem } = useSearchListItem();
|
|
63
|
-
const
|
|
72
|
+
const name = toLocalizedString(data.properties.label, t);
|
|
73
|
+
const isSelected = selectedValue === data.id;
|
|
74
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
|
|
76
|
+
useExpandPath(data.id);
|
|
64
77
|
|
|
65
78
|
const handleSelect = useCallback(
|
|
66
|
-
() => invokePromise(
|
|
79
|
+
() => invokePromise(LayoutOperation.SwitchWorkspace, { subject: data.id }),
|
|
67
80
|
[invokePromise, data.id],
|
|
68
81
|
);
|
|
69
82
|
|
|
70
|
-
useLoadDescendents(data.id);
|
|
71
|
-
|
|
72
|
-
const name = toLocalizedString(data.properties.label, t);
|
|
73
|
-
const isSelected = selectedValue === data.id;
|
|
74
|
-
|
|
75
83
|
// Register this workspace with the search context.
|
|
76
84
|
useEffect(() => {
|
|
77
|
-
if (
|
|
78
|
-
registerItem(data.id,
|
|
85
|
+
if (cardRef.current) {
|
|
86
|
+
registerItem(data.id, cardRef.current, handleSelect);
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
return () => unregisterItem(data.id);
|
|
@@ -83,20 +91,20 @@ const WorkspaceTile: StackTileComponent<Node.Node> = ({ data }) => {
|
|
|
83
91
|
|
|
84
92
|
// Scroll into view when selected.
|
|
85
93
|
useEffect(() => {
|
|
86
|
-
if (isSelected &&
|
|
87
|
-
|
|
94
|
+
if (isSelected && cardRef.current) {
|
|
95
|
+
cardRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
88
96
|
}
|
|
89
97
|
}, [isSelected]);
|
|
90
98
|
|
|
91
99
|
return (
|
|
92
100
|
<Card.Root
|
|
93
|
-
ref={ref}
|
|
94
101
|
role='button'
|
|
95
102
|
fullWidth
|
|
96
103
|
tabIndex={-1} // TODO(burdon): Use Mosaic.Focus.
|
|
97
104
|
data-selected={isSelected}
|
|
98
|
-
classNames={mx('dx-focus-ring', isSelected && 'bg-
|
|
105
|
+
classNames={mx('dx-focus-ring', isSelected && 'bg-hover-overlay')}
|
|
99
106
|
onClick={handleSelect}
|
|
107
|
+
ref={cardRef}
|
|
100
108
|
>
|
|
101
109
|
<Card.Toolbar density='coarse'>
|
|
102
110
|
<Avatar.Root>
|
|
@@ -105,7 +113,7 @@ const WorkspaceTile: StackTileComponent<Node.Node> = ({ data }) => {
|
|
|
105
113
|
hue={data.properties.hue}
|
|
106
114
|
hueVariant='transparent'
|
|
107
115
|
variant='square'
|
|
108
|
-
size={
|
|
116
|
+
size={8}
|
|
109
117
|
fallback={name}
|
|
110
118
|
/>
|
|
111
119
|
<Avatar.Label>{name}</Avatar.Label>
|
|
@@ -124,7 +132,7 @@ const filterItems = (node: Node.Node, disposition: string) => {
|
|
|
124
132
|
/** Returns root-level items filtered by disposition. */
|
|
125
133
|
const useItemsByDisposition = (disposition: string, sort = false) => {
|
|
126
134
|
const { graph } = useAppGraph();
|
|
127
|
-
const connections = useConnections(graph, Node.RootId);
|
|
135
|
+
const connections = useConnections(graph, Node.RootId, 'child');
|
|
128
136
|
const filtered = connections.filter((node) => filterItems(node, disposition));
|
|
129
137
|
return sort ? filtered.toSorted((a, b) => byPosition(a.properties, b.properties)) : filtered;
|
|
130
138
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { type PropsWithChildren, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { addEventListener, combine } from '@dxos/async';
|
|
9
|
+
import { Flex, Input, Panel, Splitter, type SplitterMode, Toolbar } from '@dxos/react-ui';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
|
|
12
|
+
import { MobileLayout, type MobileLayoutRootProps } from './MobileLayout';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Simulate ios keyboard.
|
|
16
|
+
*/
|
|
17
|
+
const WithKeyboard = ({ children }: PropsWithChildren) => {
|
|
18
|
+
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
return combine(
|
|
22
|
+
addEventListener(document, 'focusin', (event: FocusEvent) => {
|
|
23
|
+
const target = event.target as HTMLElement;
|
|
24
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
25
|
+
setKeyboardOpen(true);
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
addEventListener(document, 'focusout', (event: FocusEvent) => {
|
|
29
|
+
const target = event.target as HTMLElement;
|
|
30
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
31
|
+
setKeyboardOpen(false);
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const keyboardHeight = keyboardOpen ? 300 : 0;
|
|
39
|
+
document.documentElement.style.setProperty('--kb-height', `${keyboardHeight}px`);
|
|
40
|
+
document.documentElement.style.setProperty('--kb-open', keyboardOpen ? '1' : '0');
|
|
41
|
+
|
|
42
|
+
// Dispatch custom keyboard event that useIOSKeyboard listens for.
|
|
43
|
+
window.dispatchEvent(
|
|
44
|
+
new CustomEvent('keyboard', {
|
|
45
|
+
detail: {
|
|
46
|
+
type: keyboardOpen ? 'show' : 'hide',
|
|
47
|
+
height: keyboardHeight,
|
|
48
|
+
duration: 300,
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}, [keyboardOpen]);
|
|
53
|
+
|
|
54
|
+
return <div className='h-screen relative'>{children}</div>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const StoryPanel = ({ children, label }: PropsWithChildren<{ label: string }>) => {
|
|
58
|
+
return (
|
|
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
|
+
<Flex column classNames='p-1'>
|
|
69
|
+
<Input.Root>
|
|
70
|
+
<Input.TextInput />
|
|
71
|
+
</Input.Root>
|
|
72
|
+
</Flex>
|
|
73
|
+
</Panel.Content>
|
|
74
|
+
</Panel.Root>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const DefaultStory = () => {
|
|
79
|
+
const [splitterMode, setSplitterMode] = useState<SplitterMode>('upper');
|
|
80
|
+
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setSplitterMode(splitterMode === 'both' ? 'lower' : splitterMode);
|
|
84
|
+
}, [keyboardOpen]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<WithKeyboard>
|
|
88
|
+
<MobileLayout.Root onKeyboardOpenChange={setKeyboardOpen}>
|
|
89
|
+
<MobileLayout.Panel safe={{ top: true, bottom: splitterMode === 'upper' }}>
|
|
90
|
+
<Splitter.Root mode={splitterMode} ratio={0.5}>
|
|
91
|
+
<Splitter.Panel position='upper'>
|
|
92
|
+
<StoryPanel label='Main'>
|
|
93
|
+
{splitterMode === 'upper' && (
|
|
94
|
+
<Toolbar.IconButton icon='ph--plus--regular' label='Open' onClick={() => setSplitterMode('both')} />
|
|
95
|
+
)}
|
|
96
|
+
</StoryPanel>
|
|
97
|
+
</Splitter.Panel>
|
|
98
|
+
<Splitter.Panel position='lower'>
|
|
99
|
+
<StoryPanel label='Drawer'>
|
|
100
|
+
<Toolbar.IconButton
|
|
101
|
+
icon={splitterMode === 'lower' ? 'ph--arrow-down--regular' : 'ph--arrow-up--regular'}
|
|
102
|
+
label={splitterMode === 'lower' ? 'Collapse' : 'Expand'}
|
|
103
|
+
onClick={() => setSplitterMode((splitterMode) => (splitterMode === 'both' ? 'lower' : 'both'))}
|
|
104
|
+
/>
|
|
105
|
+
<Toolbar.IconButton icon='ph--x--regular' label='Close' onClick={() => setSplitterMode('upper')} />
|
|
106
|
+
</StoryPanel>
|
|
107
|
+
</Splitter.Panel>
|
|
108
|
+
</Splitter.Root>
|
|
109
|
+
</MobileLayout.Panel>
|
|
110
|
+
</MobileLayout.Root>
|
|
111
|
+
</WithKeyboard>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const meta: Meta<MobileLayoutRootProps> = {
|
|
116
|
+
title: 'plugins/plugin-simple-layout/components/MobileLayout',
|
|
117
|
+
component: MobileLayout.Root,
|
|
118
|
+
render: DefaultStory,
|
|
119
|
+
decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'relative' })],
|
|
120
|
+
parameters: {
|
|
121
|
+
layout: 'fullscreen',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default meta;
|
|
126
|
+
|
|
127
|
+
type Story = StoryObj<MobileLayoutRootProps>;
|
|
128
|
+
|
|
129
|
+
export const Default: Story = {};
|
|
@@ -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: `h-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 h-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
|
+
};
|