@dxos/react-ui-stack 0.6.14-staging.934c9de → 0.6.14-staging.9b873ce

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/lib/browser/index.mjs +494 -335
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +3 -6
  5. package/dist/lib/browser/testing/index.mjs.map +3 -3
  6. package/dist/lib/node/index.cjs +478 -326
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +3 -6
  10. package/dist/lib/node/testing/index.cjs.map +3 -3
  11. package/dist/lib/node-esm/index.mjs +494 -335
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +3 -6
  15. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  16. package/dist/types/src/components/LayoutControls.d.ts +19 -0
  17. package/dist/types/src/components/LayoutControls.d.ts.map +1 -0
  18. package/dist/types/src/components/MenuSignifier.d.ts +4 -0
  19. package/dist/types/src/components/MenuSignifier.d.ts.map +1 -0
  20. package/dist/types/src/components/Stack.d.ts +12 -12
  21. package/dist/types/src/components/Stack.d.ts.map +1 -1
  22. package/dist/types/src/components/Stack.stories.d.ts +6 -83
  23. package/dist/types/src/components/Stack.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/StackContext.d.ts +19 -0
  25. package/dist/types/src/components/StackContext.d.ts.map +1 -0
  26. package/dist/types/src/components/StackItem.d.ts +41 -0
  27. package/dist/types/src/components/StackItem.d.ts.map +1 -0
  28. package/dist/types/src/components/StackItemContent.d.ts +9 -0
  29. package/dist/types/src/components/StackItemContent.d.ts.map +1 -0
  30. package/dist/types/src/components/StackItemHeading.d.ts +8 -0
  31. package/dist/types/src/components/StackItemHeading.d.ts.map +1 -0
  32. package/dist/types/src/components/StackItemResizeHandle.d.ts +3 -0
  33. package/dist/types/src/components/StackItemResizeHandle.d.ts.map +1 -0
  34. package/dist/types/src/components/StackItemSigil.d.ts +31 -0
  35. package/dist/types/src/components/StackItemSigil.d.ts.map +1 -0
  36. package/dist/types/src/components/index.d.ts +2 -1
  37. package/dist/types/src/components/index.d.ts.map +1 -1
  38. package/dist/types/src/testing/EditorContent.d.ts +2 -2
  39. package/dist/types/src/testing/EditorContent.d.ts.map +1 -1
  40. package/dist/types/src/testing/stack-manager.d.ts +0 -1
  41. package/dist/types/src/testing/stack-manager.d.ts.map +1 -1
  42. package/dist/types/src/translations.d.ts +8 -8
  43. package/package.json +19 -20
  44. package/src/components/LayoutControls.tsx +131 -0
  45. package/src/components/MenuSignifier.tsx +33 -0
  46. package/src/components/Stack.stories.tsx +109 -182
  47. package/src/components/Stack.tsx +61 -156
  48. package/src/components/StackContext.tsx +38 -0
  49. package/src/components/StackItem.tsx +173 -0
  50. package/src/components/StackItemContent.tsx +49 -0
  51. package/src/components/StackItemHeading.tsx +55 -0
  52. package/src/components/StackItemResizeHandle.tsx +115 -0
  53. package/src/components/StackItemSigil.tsx +170 -0
  54. package/src/components/index.ts +3 -2
  55. package/src/playwright/smoke.spec.ts +3 -3
  56. package/src/testing/EditorContent.tsx +4 -4
  57. package/src/testing/stack-manager.ts +3 -7
  58. package/src/translations.ts +8 -8
  59. package/dist/types/src/components/CaretDownUp.d.ts +0 -4
  60. package/dist/types/src/components/CaretDownUp.d.ts.map +0 -1
  61. package/dist/types/src/components/ContentTypes.stories.d.ts +0 -96
  62. package/dist/types/src/components/ContentTypes.stories.d.ts.map +0 -1
  63. package/dist/types/src/components/Deck.stories.d.ts +0 -19
  64. package/dist/types/src/components/Deck.stories.d.ts.map +0 -1
  65. package/dist/types/src/components/Section.d.ts +0 -53
  66. package/dist/types/src/components/Section.d.ts.map +0 -1
  67. package/dist/types/src/components/Section.stories.d.ts +0 -36
  68. package/dist/types/src/components/Section.stories.d.ts.map +0 -1
  69. package/dist/types/src/components/style-fragments.d.ts +0 -2
  70. package/dist/types/src/components/style-fragments.d.ts.map +0 -1
  71. package/dist/types/src/next/Stack.d.ts +0 -9
  72. package/dist/types/src/next/Stack.d.ts.map +0 -1
  73. package/dist/types/src/next/Stack.stories.d.ts +0 -8
  74. package/dist/types/src/next/Stack.stories.d.ts.map +0 -1
  75. package/dist/types/src/next/StackItem.d.ts +0 -14
  76. package/dist/types/src/next/StackItem.d.ts.map +0 -1
  77. package/dist/types/src/next/index.d.ts +0 -2
  78. package/dist/types/src/next/index.d.ts.map +0 -1
  79. package/dist/types/src/testing/TableContent.d.ts +0 -20
  80. package/dist/types/src/testing/TableContent.d.ts.map +0 -1
  81. package/src/components/CaretDownUp.tsx +0 -31
  82. package/src/components/ContentTypes.stories.tsx +0 -104
  83. package/src/components/Deck.stories.tsx +0 -362
  84. package/src/components/Section.stories.tsx +0 -50
  85. package/src/components/Section.tsx +0 -378
  86. package/src/components/style-fragments.ts +0 -5
  87. package/src/next/Stack.stories.tsx +0 -148
  88. package/src/next/Stack.tsx +0 -30
  89. package/src/next/StackItem.tsx +0 -78
  90. package/src/next/index.ts +0 -5
  91. 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
+ );
@@ -1,6 +1,7 @@
1
1
  //
2
- // Copyright 2022 DXOS.org
2
+ // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
5
  export * from './Stack';
6
- export { SectionToolbar, type SectionToolbarProps, sectionToolbarLayout, type StackSectionItem } from './Section';
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
- test.describe('Stack', () => {
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.empty()).toBeVisible();
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 { StackSectionContent } from '../components/Section';
22
+ import { StackItem, type StackItemContentProps } from '../components';
23
23
 
24
- export const EditorContent = ({ data: { content = '' } }: { data: StackSectionContent & { content?: string } }) => {
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
- <div role='none' className='flex flex-col'>
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
- </div>
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('li');
15
+ return this.locator.locator('section');
20
16
  }
21
17
 
22
18
  order() {
23
- return this.locator.locator('li').evaluateAll((els) => els.map((el) => el.getAttribute('id')));
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('li').nth(index));
23
+ return new SectionManager(this.locator.locator('section').nth(index));
28
24
  }
29
25
  }
30
26
 
@@ -8,14 +8,14 @@ export default [
8
8
  {
9
9
  'en-US': {
10
10
  [translationKey]: {
11
- 'empty stack message': 'Drag items into the stack.',
12
- 'remove section label': 'Delete',
13
- 'navigate to section label': 'Navigate to item',
14
- 'untitled section title': 'Untitled section',
15
- 'add section before label': 'Add before',
16
- 'add section after label': 'Add after',
17
- 'expand label': 'Expand',
18
- 'collapse label': 'Collapse',
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
  },