@dxos/react-ui-stack 0.6.14-staging.e15392e → 0.7.0
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/index.mjs +494 -335
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +3 -6
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +478 -326
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +3 -6
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/node-esm/index.mjs +494 -335
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +3 -6
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/components/LayoutControls.d.ts +19 -0
- package/dist/types/src/components/LayoutControls.d.ts.map +1 -0
- package/dist/types/src/components/MenuSignifier.d.ts +4 -0
- package/dist/types/src/components/MenuSignifier.d.ts.map +1 -0
- package/dist/types/src/components/Stack.d.ts +12 -12
- package/dist/types/src/components/Stack.d.ts.map +1 -1
- package/dist/types/src/components/Stack.stories.d.ts +6 -83
- package/dist/types/src/components/Stack.stories.d.ts.map +1 -1
- package/dist/types/src/components/StackContext.d.ts +19 -0
- package/dist/types/src/components/StackContext.d.ts.map +1 -0
- package/dist/types/src/components/StackItem.d.ts +41 -0
- package/dist/types/src/components/StackItem.d.ts.map +1 -0
- package/dist/types/src/components/StackItemContent.d.ts +9 -0
- package/dist/types/src/components/StackItemContent.d.ts.map +1 -0
- package/dist/types/src/components/StackItemHeading.d.ts +8 -0
- package/dist/types/src/components/StackItemHeading.d.ts.map +1 -0
- package/dist/types/src/components/StackItemResizeHandle.d.ts +3 -0
- package/dist/types/src/components/StackItemResizeHandle.d.ts.map +1 -0
- package/dist/types/src/components/StackItemSigil.d.ts +31 -0
- package/dist/types/src/components/StackItemSigil.d.ts.map +1 -0
- 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/testing/EditorContent.d.ts +2 -2
- package/dist/types/src/testing/EditorContent.d.ts.map +1 -1
- package/dist/types/src/testing/stack-manager.d.ts +0 -1
- package/dist/types/src/testing/stack-manager.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +8 -8
- package/package.json +19 -20
- package/src/components/LayoutControls.tsx +131 -0
- package/src/components/MenuSignifier.tsx +33 -0
- package/src/components/Stack.stories.tsx +109 -182
- package/src/components/Stack.tsx +61 -156
- package/src/components/StackContext.tsx +38 -0
- package/src/components/StackItem.tsx +173 -0
- package/src/components/StackItemContent.tsx +49 -0
- package/src/components/StackItemHeading.tsx +55 -0
- package/src/components/StackItemResizeHandle.tsx +115 -0
- package/src/components/StackItemSigil.tsx +170 -0
- package/src/components/index.ts +3 -2
- package/src/playwright/smoke.spec.ts +3 -3
- package/src/testing/EditorContent.tsx +4 -4
- package/src/testing/stack-manager.ts +3 -7
- package/src/translations.ts +8 -8
- package/dist/types/src/components/CaretDownUp.d.ts +0 -4
- package/dist/types/src/components/CaretDownUp.d.ts.map +0 -1
- package/dist/types/src/components/ContentTypes.stories.d.ts +0 -96
- package/dist/types/src/components/ContentTypes.stories.d.ts.map +0 -1
- package/dist/types/src/components/Deck.stories.d.ts +0 -19
- package/dist/types/src/components/Deck.stories.d.ts.map +0 -1
- package/dist/types/src/components/Section.d.ts +0 -53
- package/dist/types/src/components/Section.d.ts.map +0 -1
- package/dist/types/src/components/Section.stories.d.ts +0 -36
- package/dist/types/src/components/Section.stories.d.ts.map +0 -1
- package/dist/types/src/components/style-fragments.d.ts +0 -2
- package/dist/types/src/components/style-fragments.d.ts.map +0 -1
- package/dist/types/src/next/Stack.d.ts +0 -9
- package/dist/types/src/next/Stack.d.ts.map +0 -1
- package/dist/types/src/next/Stack.stories.d.ts +0 -8
- package/dist/types/src/next/Stack.stories.d.ts.map +0 -1
- package/dist/types/src/next/StackItem.d.ts +0 -14
- package/dist/types/src/next/StackItem.d.ts.map +0 -1
- package/dist/types/src/next/index.d.ts +0 -2
- package/dist/types/src/next/index.d.ts.map +0 -1
- package/dist/types/src/testing/TableContent.d.ts +0 -20
- package/dist/types/src/testing/TableContent.d.ts.map +0 -1
- package/src/components/CaretDownUp.tsx +0 -31
- package/src/components/ContentTypes.stories.tsx +0 -104
- package/src/components/Deck.stories.tsx +0 -362
- package/src/components/Section.stories.tsx +0 -50
- package/src/components/Section.tsx +0 -378
- package/src/components/style-fragments.ts +0 -5
- package/src/next/Stack.stories.tsx +0 -148
- package/src/next/Stack.tsx +0 -30
- package/src/next/StackItem.tsx +0 -78
- package/src/next/index.ts +0 -5
- package/src/testing/TableContent.tsx +0 -119
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
|
6
|
+
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
7
|
+
import {
|
|
8
|
+
attachClosestEdge,
|
|
9
|
+
type Edge,
|
|
10
|
+
extractClosestEdge,
|
|
11
|
+
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
12
|
+
import { DropIndicator } from '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box';
|
|
13
|
+
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
14
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
15
|
+
import React, { forwardRef, useLayoutEffect, useState, type ComponentPropsWithRef, useCallback } from 'react';
|
|
16
|
+
|
|
17
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
18
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
19
|
+
|
|
20
|
+
import { useStack, StackItemContext } from './StackContext';
|
|
21
|
+
import { StackItemContent, type StackItemContentProps } from './StackItemContent';
|
|
22
|
+
import {
|
|
23
|
+
StackItemHeading,
|
|
24
|
+
StackItemHeadingLabel,
|
|
25
|
+
type StackItemHeadingProps,
|
|
26
|
+
type StackItemHeadingLabelProps,
|
|
27
|
+
} from './StackItemHeading';
|
|
28
|
+
import { StackItemResizeHandle } from './StackItemResizeHandle';
|
|
29
|
+
import {
|
|
30
|
+
StackItemSigil,
|
|
31
|
+
type StackItemSigilProps,
|
|
32
|
+
type StackItemSigilAction,
|
|
33
|
+
type StackItemSigilButtonProps,
|
|
34
|
+
StackItemSigilButton,
|
|
35
|
+
} from './StackItemSigil';
|
|
36
|
+
|
|
37
|
+
export type StackItemSize = number | 'min-content';
|
|
38
|
+
export const DEFAULT_HORIZONTAL_SIZE = 44 satisfies StackItemSize;
|
|
39
|
+
export const DEFAULT_VERTICAL_SIZE = 'min-content' satisfies StackItemSize;
|
|
40
|
+
export const DEFAULT_EXTRINSIC_SIZE = DEFAULT_HORIZONTAL_SIZE satisfies StackItemSize;
|
|
41
|
+
|
|
42
|
+
export type StackItemData = { id: string; type: 'column' | 'card' };
|
|
43
|
+
|
|
44
|
+
export type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
|
|
45
|
+
item: Omit<StackItemData, 'type'>;
|
|
46
|
+
order?: number;
|
|
47
|
+
onRearrange?: (source: StackItemData, target: StackItemData, closestEdge: Edge | null) => void;
|
|
48
|
+
size?: StackItemSize;
|
|
49
|
+
onSizeChange?: (nextSize: StackItemSize) => void;
|
|
50
|
+
role?: 'article' | 'section';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
|
|
54
|
+
(
|
|
55
|
+
{ item, children, classNames, onRearrange, size: propsSize, onSizeChange, role, order, style, ...props },
|
|
56
|
+
forwardedRef,
|
|
57
|
+
) => {
|
|
58
|
+
const [itemElement, itemRef] = useState<HTMLDivElement | null>(null);
|
|
59
|
+
const [selfDragHandleElement, selfDragHandleRef] = useState<HTMLDivElement | null>(null);
|
|
60
|
+
const [closestEdge, setEdge] = useState<Edge | null>(null);
|
|
61
|
+
const { orientation, rail, separators } = useStack();
|
|
62
|
+
const [size = orientation === 'horizontal' ? DEFAULT_HORIZONTAL_SIZE : DEFAULT_VERTICAL_SIZE, setInternalSize] =
|
|
63
|
+
useState(propsSize);
|
|
64
|
+
|
|
65
|
+
const Root = role ?? 'div';
|
|
66
|
+
|
|
67
|
+
const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
|
|
68
|
+
|
|
69
|
+
const setSize = useCallback(
|
|
70
|
+
(nextSize: StackItemSize, commit?: boolean) => {
|
|
71
|
+
setInternalSize(nextSize);
|
|
72
|
+
if (commit) {
|
|
73
|
+
onSizeChange?.(nextSize);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[onSizeChange],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const type = orientation === 'horizontal' ? 'column' : 'card';
|
|
80
|
+
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
if (!itemElement || !onRearrange) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
return combine(
|
|
86
|
+
draggable({
|
|
87
|
+
element: itemElement,
|
|
88
|
+
...(selfDragHandleElement && { dragHandle: selfDragHandleElement }),
|
|
89
|
+
getInitialData: () => ({ id: item.id, type }),
|
|
90
|
+
}),
|
|
91
|
+
dropTargetForElements({
|
|
92
|
+
element: itemElement,
|
|
93
|
+
getData: ({ input, element }) => {
|
|
94
|
+
return attachClosestEdge(
|
|
95
|
+
{ id: item.id, type },
|
|
96
|
+
{ input, element, allowedEdges: orientation === 'horizontal' ? ['left', 'right'] : ['top', 'bottom'] },
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
onDragEnter: ({ self, source }) => {
|
|
100
|
+
if (source.data.type === self.data.type) {
|
|
101
|
+
setEdge(extractClosestEdge(self.data));
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onDrag: ({ self, source }) => {
|
|
105
|
+
if (source.data.type === self.data.type) {
|
|
106
|
+
setEdge(extractClosestEdge(self.data));
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
onDragLeave: () => setEdge(null),
|
|
110
|
+
onDrop: ({ self, source }) => {
|
|
111
|
+
setEdge(null);
|
|
112
|
+
if (source.data.type === self.data.type) {
|
|
113
|
+
onRearrange(source.data as StackItemData, self.data as StackItemData, extractClosestEdge(self.data));
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}, [orientation, item, onRearrange, selfDragHandleElement, itemElement]);
|
|
119
|
+
|
|
120
|
+
const focusGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<StackItemContext.Provider value={{ selfDragHandleRef, size, setSize }}>
|
|
124
|
+
<Root
|
|
125
|
+
{...props}
|
|
126
|
+
tabIndex={0}
|
|
127
|
+
{...focusGroupAttrs}
|
|
128
|
+
className={mx(
|
|
129
|
+
'group/stack-item grid relative ch-focus-ring-inset-over-all',
|
|
130
|
+
size === 'min-content' && (orientation === 'horizontal' ? 'is-min' : 'bs-min'),
|
|
131
|
+
orientation === 'horizontal' ? 'grid-rows-subgrid' : 'grid-cols-subgrid',
|
|
132
|
+
rail && (orientation === 'horizontal' ? 'row-span-2' : 'col-span-2'),
|
|
133
|
+
separators && (orientation === 'horizontal' ? 'divide-separator divide-y' : 'divide-separator divide-x'),
|
|
134
|
+
classNames,
|
|
135
|
+
)}
|
|
136
|
+
data-dx-stack-item
|
|
137
|
+
style={{
|
|
138
|
+
...(size !== 'min-content' && {
|
|
139
|
+
[orientation === 'horizontal' ? 'inlineSize' : 'blockSize']: `${size}rem`,
|
|
140
|
+
}),
|
|
141
|
+
...(Number.isFinite(order) && {
|
|
142
|
+
[orientation === 'horizontal' ? 'gridColumn' : 'gridRow']: `${order}`,
|
|
143
|
+
}),
|
|
144
|
+
...style,
|
|
145
|
+
}}
|
|
146
|
+
ref={composedItemRef}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
{closestEdge && <DropIndicator edge={closestEdge} />}
|
|
150
|
+
</Root>
|
|
151
|
+
</StackItemContext.Provider>
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
export const StackItem = {
|
|
157
|
+
Root: StackItemRoot,
|
|
158
|
+
Content: StackItemContent,
|
|
159
|
+
Heading: StackItemHeading,
|
|
160
|
+
HeadingLabel: StackItemHeadingLabel,
|
|
161
|
+
ResizeHandle: StackItemResizeHandle,
|
|
162
|
+
Sigil: StackItemSigil,
|
|
163
|
+
SigilButton: StackItemSigilButton,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export type {
|
|
167
|
+
StackItemContentProps,
|
|
168
|
+
StackItemHeadingProps,
|
|
169
|
+
StackItemHeadingLabelProps,
|
|
170
|
+
StackItemSigilProps,
|
|
171
|
+
StackItemSigilButtonProps,
|
|
172
|
+
StackItemSigilAction,
|
|
173
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ComponentPropsWithoutRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
import { useStack } from './StackContext';
|
|
11
|
+
|
|
12
|
+
export type StackItemContentProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & {
|
|
13
|
+
toolbar?: boolean;
|
|
14
|
+
statusbar?: boolean;
|
|
15
|
+
contentSize?: 'cover' | 'intrinsic';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const StackItemContent = ({
|
|
19
|
+
children,
|
|
20
|
+
toolbar = true,
|
|
21
|
+
statusbar,
|
|
22
|
+
contentSize = 'cover',
|
|
23
|
+
classNames,
|
|
24
|
+
...props
|
|
25
|
+
}: StackItemContentProps) => {
|
|
26
|
+
const { size, separators } = useStack();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
role='none'
|
|
31
|
+
{...props}
|
|
32
|
+
className={mx(
|
|
33
|
+
'group grid grid-cols-[100%]',
|
|
34
|
+
size === 'contain' && 'min-bs-0 overflow-hidden',
|
|
35
|
+
separators && 'divide-separator divide-y',
|
|
36
|
+
classNames,
|
|
37
|
+
)}
|
|
38
|
+
style={{
|
|
39
|
+
gridTemplateRows: [
|
|
40
|
+
...(toolbar ? ['var(--rail-action)'] : []),
|
|
41
|
+
'1fr',
|
|
42
|
+
...(statusbar ? ['var(--statusbar-size)'] : []),
|
|
43
|
+
].join(' '),
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
6
|
+
import React, { type ComponentPropsWithoutRef, type ComponentPropsWithRef, forwardRef } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
9
|
+
import { useAttention, type AttendableId, type Related } from '@dxos/react-ui-attention';
|
|
10
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
11
|
+
|
|
12
|
+
import { useStack, useStackItem } from './StackContext';
|
|
13
|
+
|
|
14
|
+
export type StackItemHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
|
|
15
|
+
|
|
16
|
+
export const StackItemHeading = ({ children, classNames, ...props }: StackItemHeadingProps) => {
|
|
17
|
+
const { orientation } = useStack();
|
|
18
|
+
const { selfDragHandleRef } = useStackItem();
|
|
19
|
+
const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
role='heading'
|
|
23
|
+
{...props}
|
|
24
|
+
tabIndex={0}
|
|
25
|
+
{...focusableGroupAttrs}
|
|
26
|
+
className={mx(
|
|
27
|
+
'flex items-center ch-focus-ring-inset-over-all relative !border-is-0',
|
|
28
|
+
orientation === 'horizontal' ? 'bs-[--rail-size]' : 'is-[--rail-size] flex-col',
|
|
29
|
+
classNames,
|
|
30
|
+
)}
|
|
31
|
+
ref={selfDragHandleRef}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type StackItemHeadingLabelProps = ThemedClassName<ComponentPropsWithRef<'h1'>> & AttendableId & Related;
|
|
39
|
+
|
|
40
|
+
export const StackItemHeadingLabel = forwardRef<HTMLHeadingElement, StackItemHeadingLabelProps>(
|
|
41
|
+
({ attendableId, related, classNames, ...props }, forwardedRef) => {
|
|
42
|
+
const { hasAttention, isAncestor, isRelated } = useAttention(attendableId);
|
|
43
|
+
return (
|
|
44
|
+
<h1
|
|
45
|
+
{...props}
|
|
46
|
+
data-attention={((related && isRelated) || hasAttention || isAncestor).toString()}
|
|
47
|
+
className={mx(
|
|
48
|
+
'pli-1 min-is-0 is-0 grow truncate font-medium text-baseText data-[attention=true]:text-accentText',
|
|
49
|
+
classNames,
|
|
50
|
+
)}
|
|
51
|
+
ref={forwardedRef}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
|
+
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
|
|
7
|
+
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
|
8
|
+
import { type DragLocationHistory } from '@atlaskit/pragmatic-drag-and-drop/types';
|
|
9
|
+
import React, { useLayoutEffect, useRef } from 'react';
|
|
10
|
+
|
|
11
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
12
|
+
|
|
13
|
+
import { useStack, useStackItem } from './StackContext';
|
|
14
|
+
import { DEFAULT_EXTRINSIC_SIZE, type StackItemSize } from './StackItem';
|
|
15
|
+
|
|
16
|
+
const REM = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
17
|
+
|
|
18
|
+
const MIN_SIZE = 20;
|
|
19
|
+
|
|
20
|
+
const measureStackItem = (element: HTMLButtonElement): { width: number; height: number } => {
|
|
21
|
+
const stackItemElement = element.closest('[data-dx-stack-item]');
|
|
22
|
+
return stackItemElement?.getBoundingClientRect() ?? { width: DEFAULT_EXTRINSIC_SIZE, height: DEFAULT_EXTRINSIC_SIZE };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getNextSize = (startSize: number, location: DragLocationHistory, client: 'clientX' | 'clientY') => {
|
|
26
|
+
return Math.max(MIN_SIZE, startSize + (location.current.input[client] - location.initial.input[client]) / REM);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const StackItemResizeHandle = () => {
|
|
30
|
+
const { orientation } = useStack();
|
|
31
|
+
const { setSize, size } = useStackItem();
|
|
32
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
33
|
+
const dragStartSize = useRef<StackItemSize>(size);
|
|
34
|
+
const client = orientation === 'horizontal' ? 'clientX' : 'clientY';
|
|
35
|
+
|
|
36
|
+
useLayoutEffect(
|
|
37
|
+
() => {
|
|
38
|
+
if (!buttonRef.current || buttonRef.current.hasAttribute('draggable')) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// TODO(thure): This should handle StackItem state vs local state better.
|
|
42
|
+
draggable({
|
|
43
|
+
element: buttonRef.current,
|
|
44
|
+
onGenerateDragPreview: ({ nativeSetDragImage }) => {
|
|
45
|
+
// we will be moving the line to indicate a drag
|
|
46
|
+
// we can disable the native drag preview
|
|
47
|
+
disableNativeDragPreview({ nativeSetDragImage });
|
|
48
|
+
// we don't want any native drop animation for when the user
|
|
49
|
+
// does not drop on a drop target. we want the drag to finish immediately
|
|
50
|
+
preventUnhandled.start();
|
|
51
|
+
},
|
|
52
|
+
onDragStart: () => {
|
|
53
|
+
dragStartSize.current =
|
|
54
|
+
dragStartSize.current === 'min-content'
|
|
55
|
+
? measureStackItem(buttonRef.current!)[orientation === 'horizontal' ? 'width' : 'height'] / REM
|
|
56
|
+
: dragStartSize.current;
|
|
57
|
+
},
|
|
58
|
+
onDrag: ({ location }) => {
|
|
59
|
+
if (typeof dragStartSize.current !== 'number') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setSize(getNextSize(dragStartSize.current, location, client));
|
|
63
|
+
},
|
|
64
|
+
onDrop: ({ location }) => {
|
|
65
|
+
if (typeof dragStartSize.current !== 'number') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const nextSize = getNextSize(dragStartSize.current, location, client);
|
|
69
|
+
setSize(nextSize, true);
|
|
70
|
+
dragStartSize.current = nextSize;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
[
|
|
75
|
+
// Note that `size` should not be a dependency here since dragging this adjusts the size.
|
|
76
|
+
],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
ref={buttonRef}
|
|
82
|
+
className={mx(
|
|
83
|
+
orientation === 'horizontal' ? 'cursor-col-resize' : 'cursor-row-resize',
|
|
84
|
+
'group absolute is-3 bs-full inline-end-[-1px] !border-lb-0',
|
|
85
|
+
'before:transition-opacity before:duration-100 before:ease-in-out before:opacity-0 hover:before:opacity-100 focus-visible:before:opacity-100 active:before:opacity-100',
|
|
86
|
+
'before:absolute before:block before:inset-block-0 before:inline-end-0 before:is-1 before:bg-accentFocusIndicator',
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<div
|
|
90
|
+
role='none'
|
|
91
|
+
className='absolute block-start-0 inline-end-[1px] bs-[--rail-size] flex items-center group-hover:opacity-0 group-focus-visible:opacity-0 group-active:opacity-0'
|
|
92
|
+
>
|
|
93
|
+
<DragHandleSignifier />
|
|
94
|
+
</div>
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DragHandleSignifier = () => {
|
|
100
|
+
return (
|
|
101
|
+
<svg
|
|
102
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
103
|
+
viewBox='0 0 256 256'
|
|
104
|
+
fill='currentColor'
|
|
105
|
+
className='shrink-0 bs-[1em] is-[1em] text-unAccent'
|
|
106
|
+
>
|
|
107
|
+
{/* two pips: <path d='M256,120c-8.8,0-16-7.2-16-16v-56c0-8.8,7.2-16,16-16v88Z' />
|
|
108
|
+
<path d='M256,232c-8.8,0-16-7.2-16-16v-56c0-8.8,7.2-16,16-16v88Z' /> */}
|
|
109
|
+
<path d='M256,64c-8.8,0-16-7.2-16-16s7.2-16,16-16v32Z' />
|
|
110
|
+
<path d='M256,120c-8.8,0-16-7.2-16-16s7.2-16,16-16v32Z' />
|
|
111
|
+
<path d='M256,176c-8.8,0-16-7.2-16-16s7.2-16,16-16v32Z' />
|
|
112
|
+
<path d='M256,232c-8.8,0-16-7.2-16-16s7.2-16,16-16v32Z' />
|
|
113
|
+
</svg>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { Fragment, type PropsWithChildren, forwardRef, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type ActionLike } from '@dxos/app-graph';
|
|
8
|
+
import { keySymbols } from '@dxos/keyboard';
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
type ButtonProps,
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
Icon,
|
|
14
|
+
toLocalizedString,
|
|
15
|
+
Tooltip,
|
|
16
|
+
useTranslation,
|
|
17
|
+
} from '@dxos/react-ui';
|
|
18
|
+
import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
|
|
19
|
+
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
20
|
+
import { getHostPlatform } from '@dxos/util';
|
|
21
|
+
|
|
22
|
+
import { MenuSignifierHorizontal } from './MenuSignifier';
|
|
23
|
+
import { translationKey } from '../translations';
|
|
24
|
+
|
|
25
|
+
export type KeyBinding = {
|
|
26
|
+
windows?: string;
|
|
27
|
+
macos?: string;
|
|
28
|
+
ios?: string;
|
|
29
|
+
linux?: string;
|
|
30
|
+
unknown?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type StackItemSigilAction = Pick<ActionLike, 'id' | 'properties' | 'data'>;
|
|
34
|
+
|
|
35
|
+
export type StackItemSigilButtonProps = Omit<ButtonProps, 'variant'> & AttendableId & Related;
|
|
36
|
+
|
|
37
|
+
export const StackItemSigilButton = forwardRef<HTMLButtonElement, StackItemSigilButtonProps>(
|
|
38
|
+
({ attendableId, classNames, related, children, ...props }, forwardedRef) => {
|
|
39
|
+
const { hasAttention, isAncestor, isRelated } = useAttention(attendableId);
|
|
40
|
+
const variant = (related && isRelated) || hasAttention || isAncestor ? 'primary' : 'ghost';
|
|
41
|
+
return (
|
|
42
|
+
<Button
|
|
43
|
+
{...props}
|
|
44
|
+
variant={variant}
|
|
45
|
+
classNames={['m-1 shrink-0 pli-0 min-bs-0 is-[--rail-action] bs-[--rail-action] relative', classNames]}
|
|
46
|
+
ref={forwardedRef}
|
|
47
|
+
>
|
|
48
|
+
<MenuSignifierHorizontal />
|
|
49
|
+
{children}
|
|
50
|
+
</Button>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export type StackItemSigilProps = PropsWithChildren<
|
|
56
|
+
{
|
|
57
|
+
attendableId?: string;
|
|
58
|
+
triggerLabel: string;
|
|
59
|
+
actions?: StackItemSigilAction[][];
|
|
60
|
+
icon: string;
|
|
61
|
+
onAction?: (action: StackItemSigilAction) => void;
|
|
62
|
+
} & Related
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
export const StackItemSigil = forwardRef<HTMLButtonElement, StackItemSigilProps>(
|
|
66
|
+
({ actions: actionGroups, onAction, triggerLabel, attendableId, icon, related, children }, forwardedRef) => {
|
|
67
|
+
const { t } = useTranslation(translationKey);
|
|
68
|
+
const suppressNextTooltip = useRef(false);
|
|
69
|
+
|
|
70
|
+
const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
|
|
71
|
+
const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Tooltip.Root
|
|
75
|
+
open={triggerTooltipOpen}
|
|
76
|
+
onOpenChange={(nextOpen) => {
|
|
77
|
+
if (suppressNextTooltip.current) {
|
|
78
|
+
setTriggerTooltipOpen(false);
|
|
79
|
+
suppressNextTooltip.current = false;
|
|
80
|
+
} else {
|
|
81
|
+
setTriggerTooltipOpen(nextOpen);
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<DropdownMenu.Root
|
|
86
|
+
{...{
|
|
87
|
+
open: optionsMenuOpen,
|
|
88
|
+
onOpenChange: (nextOpen: boolean) => {
|
|
89
|
+
if (!nextOpen) {
|
|
90
|
+
suppressNextTooltip.current = true;
|
|
91
|
+
}
|
|
92
|
+
return setOptionsMenuOpen(nextOpen);
|
|
93
|
+
},
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Tooltip.Trigger asChild>
|
|
97
|
+
<DropdownMenu.Trigger asChild ref={forwardedRef}>
|
|
98
|
+
<StackItemSigilButton attendableId={attendableId} related={related}>
|
|
99
|
+
<span className='sr-only'>{triggerLabel}</span>
|
|
100
|
+
<Icon icon={icon} size={5} />
|
|
101
|
+
</StackItemSigilButton>
|
|
102
|
+
</DropdownMenu.Trigger>
|
|
103
|
+
</Tooltip.Trigger>
|
|
104
|
+
<DropdownMenu.Portal>
|
|
105
|
+
<DropdownMenu.Content classNames='z-[31]'>
|
|
106
|
+
<DropdownMenu.Viewport>
|
|
107
|
+
{actionGroups?.map((actions, index) => {
|
|
108
|
+
const separator = index > 0 ? <DropdownMenu.Separator /> : null;
|
|
109
|
+
return (
|
|
110
|
+
<Fragment key={index}>
|
|
111
|
+
{separator}
|
|
112
|
+
{actions.map((action) => {
|
|
113
|
+
const shortcut =
|
|
114
|
+
typeof action.properties.keyBinding === 'string'
|
|
115
|
+
? action.properties.keyBinding
|
|
116
|
+
: action.properties.keyBinding?.[getHostPlatform()];
|
|
117
|
+
|
|
118
|
+
const menuItemType = action.properties.menuItemType;
|
|
119
|
+
const Root = menuItemType === 'toggle' ? DropdownMenu.CheckboxItem : DropdownMenu.Item;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Root
|
|
123
|
+
key={action.id}
|
|
124
|
+
onClick={(event) => {
|
|
125
|
+
if (action.properties.disabled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
event.stopPropagation();
|
|
129
|
+
// TODO(thure): Why does Dialog’s modal-ness cause issues if we don’t explicitly close the menu here?
|
|
130
|
+
suppressNextTooltip.current = true;
|
|
131
|
+
setOptionsMenuOpen(false);
|
|
132
|
+
onAction?.(action);
|
|
133
|
+
}}
|
|
134
|
+
classNames='gap-2'
|
|
135
|
+
disabled={action.properties.disabled}
|
|
136
|
+
checked={menuItemType === 'toggle' ? action.properties.isChecked : undefined}
|
|
137
|
+
{...(action.properties?.testId && { 'data-testid': action.properties.testId })}
|
|
138
|
+
>
|
|
139
|
+
<Icon icon={action.properties.icon ?? 'ph--placeholder--regular'} size={4} />
|
|
140
|
+
<span className='grow truncate'>{toLocalizedString(action.properties.label ?? '', t)}</span>
|
|
141
|
+
{menuItemType === 'toggle' && (
|
|
142
|
+
<DropdownMenu.ItemIndicator asChild>
|
|
143
|
+
<Icon icon='ph--check--regular' size={4} />
|
|
144
|
+
</DropdownMenu.ItemIndicator>
|
|
145
|
+
)}
|
|
146
|
+
{shortcut && (
|
|
147
|
+
<span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>
|
|
148
|
+
)}
|
|
149
|
+
</Root>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</Fragment>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
{children}
|
|
156
|
+
</DropdownMenu.Viewport>
|
|
157
|
+
<DropdownMenu.Arrow />
|
|
158
|
+
</DropdownMenu.Content>
|
|
159
|
+
</DropdownMenu.Portal>
|
|
160
|
+
</DropdownMenu.Root>
|
|
161
|
+
<Tooltip.Portal>
|
|
162
|
+
<Tooltip.Content style={{ zIndex: 70 }} side='bottom'>
|
|
163
|
+
{triggerLabel}
|
|
164
|
+
<Tooltip.Arrow />
|
|
165
|
+
</Tooltip.Content>
|
|
166
|
+
</Tooltip.Portal>
|
|
167
|
+
</Tooltip.Root>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
);
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './Stack';
|
|
6
|
-
export
|
|
6
|
+
export * from './StackItem';
|
|
7
|
+
export * from './LayoutControls';
|
|
@@ -11,7 +11,8 @@ import { StackManager } from '../testing';
|
|
|
11
11
|
// TODO(wittjosiah): Factor out.
|
|
12
12
|
const storybookUrl = (storyId: string) => `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story`;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// TODO(wittjosiah): Update for new stack.
|
|
15
|
+
test.describe.skip('Stack', () => {
|
|
15
16
|
test('remove', async ({ browser }) => {
|
|
16
17
|
const { page } = await setupPage(browser, { url: storybookUrl('ui-react-ui-stack-stack--transfer') });
|
|
17
18
|
await page.getByTestId('stack-transfer').waitFor({ state: 'visible' });
|
|
@@ -70,13 +71,12 @@ test.describe('Stack', () => {
|
|
|
70
71
|
const stack2 = new StackManager(page.getByTestId('stack-2'));
|
|
71
72
|
|
|
72
73
|
await expect(stack1.sections()).toHaveCount(8);
|
|
73
|
-
await expect(stack2.
|
|
74
|
+
await expect(stack2.sections()).toHaveCount(0);
|
|
74
75
|
|
|
75
76
|
const sectionText = await stack1.section(0).locator.innerText();
|
|
76
77
|
await stack1.section(0).dragTo(stack2.locator.getByTestId('stack.empty'));
|
|
77
78
|
|
|
78
79
|
await expect(stack1.sections()).toHaveCount(8);
|
|
79
|
-
await expect(stack2.empty()).not.toBeVisible();
|
|
80
80
|
await expect(stack2.sections()).toHaveCount(1);
|
|
81
81
|
expect(await stack2.section(0).locator.innerText()).toEqual(sectionText);
|
|
82
82
|
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
} from '@dxos/react-ui-editor';
|
|
20
20
|
import { focusRing, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
21
21
|
|
|
22
|
-
import type
|
|
22
|
+
import { StackItem, type StackItemContentProps } from '../components';
|
|
23
23
|
|
|
24
|
-
export const EditorContent = ({ data: { content = '' } }: { data:
|
|
24
|
+
export const EditorContent = ({ data: { content = '' } }: { data: StackItemContentProps & { content?: string } }) => {
|
|
25
25
|
const { themeMode } = useThemeContext();
|
|
26
26
|
const [text] = useState(create(Expando, { content }));
|
|
27
27
|
const id = text.id;
|
|
@@ -44,7 +44,7 @@ export const EditorContent = ({ data: { content = '' } }: { data: StackSectionCo
|
|
|
44
44
|
const handleAction = useActionHandler(view);
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
|
-
<
|
|
47
|
+
<StackItem.Content contentSize='intrinsic'>
|
|
48
48
|
<div {...focusAttributes} className={mx(textBlockWidth, focusRing, 'rounded-sm order-last')} ref={parentRef} />
|
|
49
49
|
<Toolbar.Root
|
|
50
50
|
onAction={handleAction}
|
|
@@ -54,6 +54,6 @@ export const EditorContent = ({ data: { content = '' } }: { data: StackSectionCo
|
|
|
54
54
|
<Toolbar.Markdown />
|
|
55
55
|
<Toolbar.Separator />
|
|
56
56
|
</Toolbar.Root>
|
|
57
|
-
</
|
|
57
|
+
</StackItem.Content>
|
|
58
58
|
);
|
|
59
59
|
};
|
|
@@ -11,20 +11,16 @@ export class StackManager {
|
|
|
11
11
|
this._page = locator.page();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
empty() {
|
|
15
|
-
return this.locator.getByTestId('stack.empty');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
sections() {
|
|
19
|
-
return this.locator.locator('
|
|
15
|
+
return this.locator.locator('section');
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
order() {
|
|
23
|
-
return this.locator.locator('
|
|
19
|
+
return this.locator.locator('section').evaluateAll((els) => els.map((el) => el.getAttribute('id')));
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
section(index: number) {
|
|
27
|
-
return new SectionManager(this.locator.locator('
|
|
23
|
+
return new SectionManager(this.locator.locator('section').nth(index));
|
|
28
24
|
}
|
|
29
25
|
}
|
|
30
26
|
|
package/src/translations.ts
CHANGED
|
@@ -8,14 +8,14 @@ export default [
|
|
|
8
8
|
{
|
|
9
9
|
'en-US': {
|
|
10
10
|
[translationKey]: {
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
11
|
+
'resize label': 'Drag to resize',
|
|
12
|
+
'pin start label': 'Pin to the left sidebar',
|
|
13
|
+
'pin end label': 'Pin to the right sidebar',
|
|
14
|
+
'increment start label': 'Move to the left',
|
|
15
|
+
'increment end label': 'Move to the right',
|
|
16
|
+
'solo plank label': 'Toggle solo mode',
|
|
17
|
+
'close label': 'Close',
|
|
18
|
+
'minify label': 'Minify',
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
},
|