@dxos/react-ui-stack 0.8.2-staging.7ac8446 → 0.8.3-main.672df60

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 (114) hide show
  1. package/dist/lib/browser/index.mjs +960 -419
  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.map +3 -3
  5. package/dist/lib/node/index.cjs +949 -408
  6. package/dist/lib/node/index.cjs.map +4 -4
  7. package/dist/lib/node/meta.json +1 -1
  8. package/dist/lib/node/testing/index.cjs.map +3 -3
  9. package/dist/lib/node-esm/index.mjs +960 -419
  10. package/dist/lib/node-esm/index.mjs.map +4 -4
  11. package/dist/lib/node-esm/meta.json +1 -1
  12. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  13. package/dist/types/src/components/{Stack.d.ts → Stack/Stack.d.ts} +4 -1
  14. package/dist/types/src/components/Stack/Stack.d.ts.map +1 -0
  15. package/dist/types/src/components/Stack/Stack.stories.d.ts +9 -0
  16. package/dist/types/src/components/Stack/Stack.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Stack/index.d.ts +2 -0
  18. package/dist/types/src/components/Stack/index.d.ts.map +1 -0
  19. package/dist/types/src/components/StackContext.d.ts +14 -10
  20. package/dist/types/src/components/StackContext.d.ts.map +1 -1
  21. package/dist/types/src/components/StackItem/MenuSignifier.d.ts.map +1 -0
  22. package/dist/types/src/components/{StackItem.d.ts → StackItem/StackItem.d.ts} +15 -6
  23. package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -0
  24. package/dist/types/src/components/StackItem/StackItem.stories.d.ts +8 -0
  25. package/dist/types/src/components/StackItem/StackItem.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -0
  27. package/dist/types/src/components/{StackItemDragHandle.d.ts → StackItem/StackItemDragHandle.d.ts} +1 -1
  28. package/dist/types/src/components/StackItem/StackItemDragHandle.d.ts.map +1 -0
  29. package/dist/types/src/components/{StackItemHeading.d.ts → StackItem/StackItemHeading.d.ts} +4 -2
  30. package/dist/types/src/components/StackItem/StackItemHeading.d.ts.map +1 -0
  31. package/dist/types/src/components/StackItem/StackItemResizeHandle.d.ts.map +1 -0
  32. package/dist/types/src/components/StackItem/StackItemSigil.d.ts.map +1 -0
  33. package/dist/types/src/components/StackItem/index.d.ts +2 -0
  34. package/dist/types/src/components/StackItem/index.d.ts.map +1 -0
  35. package/dist/types/src/components/defs.d.ts +18 -0
  36. package/dist/types/src/components/defs.d.ts.map +1 -0
  37. package/dist/types/src/components/deprecated/LayoutControls.d.ts.map +1 -0
  38. package/dist/types/src/components/index.d.ts +1 -2
  39. package/dist/types/src/components/index.d.ts.map +1 -1
  40. package/dist/types/src/exemplars/Card/Card.d.ts +58 -0
  41. package/dist/types/src/exemplars/Card/Card.d.ts.map +1 -0
  42. package/dist/types/src/exemplars/Card/Card.stories-todo.d.ts +1 -0
  43. package/dist/types/src/exemplars/Card/Card.stories-todo.d.ts.map +1 -0
  44. package/dist/types/src/exemplars/Card/CardDragPreview.d.ts +6 -0
  45. package/dist/types/src/exemplars/Card/CardDragPreview.d.ts.map +1 -0
  46. package/dist/types/src/exemplars/Card/fragments.d.ts +6 -0
  47. package/dist/types/src/exemplars/Card/fragments.d.ts.map +1 -0
  48. package/dist/types/src/exemplars/Card/index.d.ts +3 -0
  49. package/dist/types/src/exemplars/Card/index.d.ts.map +1 -0
  50. package/dist/types/src/exemplars/CardStack/CardStack.d.ts +34 -0
  51. package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +1 -0
  52. package/dist/types/src/exemplars/CardStack/CardStack.stories-todo.d.ts +1 -0
  53. package/dist/types/src/exemplars/CardStack/CardStack.stories-todo.d.ts.map +1 -0
  54. package/dist/types/src/exemplars/CardStack/CardStackDragPreview.d.ts +9 -0
  55. package/dist/types/src/exemplars/CardStack/CardStackDragPreview.d.ts.map +1 -0
  56. package/dist/types/src/exemplars/CardStack/index.d.ts +3 -0
  57. package/dist/types/src/exemplars/CardStack/index.d.ts.map +1 -0
  58. package/dist/types/src/exemplars/index.d.ts +3 -0
  59. package/dist/types/src/exemplars/index.d.ts.map +1 -0
  60. package/dist/types/src/hooks/useStackDropForElements.d.ts +4 -4
  61. package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
  62. package/dist/types/src/index.d.ts +1 -0
  63. package/dist/types/src/index.d.ts.map +1 -1
  64. package/dist/types/src/testing/stack-manager.d.ts.map +1 -1
  65. package/dist/types/src/translations.d.ts +1 -0
  66. package/dist/types/src/translations.d.ts.map +1 -1
  67. package/dist/types/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +22 -20
  69. package/src/components/{Stack.stories.tsx → Stack/Stack.stories.tsx} +29 -17
  70. package/src/components/{Stack.tsx → Stack/Stack.tsx} +55 -5
  71. package/src/components/Stack/index.ts +5 -0
  72. package/src/components/StackContext.tsx +21 -13
  73. package/src/components/StackItem/StackItem.stories.tsx +49 -0
  74. package/src/components/{StackItem.tsx → StackItem/StackItem.tsx} +90 -11
  75. package/src/components/{StackItemContent.tsx → StackItem/StackItemContent.tsx} +2 -1
  76. package/src/components/{StackItemDragHandle.tsx → StackItem/StackItemDragHandle.tsx} +2 -2
  77. package/src/components/{StackItemHeading.tsx → StackItem/StackItemHeading.tsx} +10 -6
  78. package/src/components/{StackItemResizeHandle.tsx → StackItem/StackItemResizeHandle.tsx} +1 -1
  79. package/src/components/{StackItemSigil.tsx → StackItem/StackItemSigil.tsx} +3 -15
  80. package/src/components/StackItem/index.ts +5 -0
  81. package/src/components/defs.ts +26 -0
  82. package/src/components/{LayoutControls.tsx → deprecated/LayoutControls.tsx} +3 -23
  83. package/src/components/index.ts +2 -2
  84. package/src/exemplars/Card/Card.stories-todo.tsx +135 -0
  85. package/src/exemplars/Card/Card.tsx +178 -0
  86. package/src/exemplars/Card/CardDragPreview.tsx +22 -0
  87. package/src/exemplars/Card/fragments.ts +14 -0
  88. package/src/exemplars/Card/index.ts +6 -0
  89. package/src/exemplars/CardStack/CardStack.stories-todo.tsx +80 -0
  90. package/src/exemplars/CardStack/CardStack.tsx +118 -0
  91. package/src/exemplars/CardStack/CardStackDragPreview.tsx +61 -0
  92. package/src/exemplars/CardStack/index.ts +6 -0
  93. package/src/exemplars/index.ts +6 -0
  94. package/src/hooks/useStackDropForElements.ts +7 -6
  95. package/src/index.ts +4 -0
  96. package/src/testing/stack-manager.ts +6 -6
  97. package/src/translations.ts +1 -0
  98. package/dist/types/src/components/LayoutControls.d.ts.map +0 -1
  99. package/dist/types/src/components/MenuSignifier.d.ts.map +0 -1
  100. package/dist/types/src/components/Stack.d.ts.map +0 -1
  101. package/dist/types/src/components/Stack.stories.d.ts +0 -8
  102. package/dist/types/src/components/Stack.stories.d.ts.map +0 -1
  103. package/dist/types/src/components/StackItem.d.ts.map +0 -1
  104. package/dist/types/src/components/StackItemContent.d.ts.map +0 -1
  105. package/dist/types/src/components/StackItemDragHandle.d.ts.map +0 -1
  106. package/dist/types/src/components/StackItemHeading.d.ts.map +0 -1
  107. package/dist/types/src/components/StackItemResizeHandle.d.ts.map +0 -1
  108. package/dist/types/src/components/StackItemSigil.d.ts.map +0 -1
  109. /package/dist/types/src/components/{MenuSignifier.d.ts → StackItem/MenuSignifier.d.ts} +0 -0
  110. /package/dist/types/src/components/{StackItemContent.d.ts → StackItem/StackItemContent.d.ts} +0 -0
  111. /package/dist/types/src/components/{StackItemResizeHandle.d.ts → StackItem/StackItemResizeHandle.d.ts} +0 -0
  112. /package/dist/types/src/components/{StackItemSigil.d.ts → StackItem/StackItemSigil.d.ts} +0 -0
  113. /package/dist/types/src/components/{LayoutControls.d.ts → deprecated/LayoutControls.d.ts} +0 -0
  114. /package/src/components/{MenuSignifier.tsx → StackItem/MenuSignifier.tsx} +0 -0
@@ -2,6 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import '@dxos-theme';
6
+
5
7
  import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
6
8
  import { type Meta, type StoryObj } from '@storybook/react';
7
9
  import React, { useState, useCallback } from 'react';
@@ -10,8 +12,8 @@ import { faker } from '@dxos/random';
10
12
  import { withTheme } from '@dxos/storybook-utils';
11
13
 
12
14
  import { Stack } from './Stack';
13
- import { type StackItemData } from './StackContext';
14
- import { StackItem } from './StackItem';
15
+ import { StackItem } from '../StackItem';
16
+ import { type StackItemData } from '../defs';
15
17
 
16
18
  type StoryStackItem = {
17
19
  id: string;
@@ -27,7 +29,7 @@ const KanbanBlock = ({ item }: { item: StoryStackItem }) => {
27
29
  );
28
30
  };
29
31
 
30
- const StorybookStack = () => {
32
+ const DefaultStory = () => {
31
33
  const [columns, setColumns] = useState<StoryStackItem[]>(
32
34
  faker.helpers.multiple(
33
35
  () =>
@@ -95,14 +97,24 @@ const StorybookStack = () => {
95
97
  return (
96
98
  <main className='fixed inset-0'>
97
99
  <Stack orientation='horizontal' size='contain' onRearrange={reorderItem}>
98
- {columns.map((column) => (
99
- <StackItem.Root key={column.id} item={column}>
100
+ {columns.map((column, columnIndex, columnsArray) => (
101
+ <StackItem.Root
102
+ key={column.id}
103
+ item={column}
104
+ prevSiblingId={columnIndex > 0 ? columnsArray[columnIndex - 1].id : undefined}
105
+ nextSiblingId={columnIndex < columnsArray.length - 1 ? columnsArray[columnIndex + 1].id : undefined}
106
+ >
100
107
  <StackItem.Heading>
101
108
  <StackItem.ResizeHandle />
102
109
  </StackItem.Heading>
103
110
  <Stack orientation='vertical' size='contain'>
104
- {column.items?.map((card) => (
105
- <StackItem.Root key={card.id} item={card}>
111
+ {column.items?.map((card, cardIndex, cardsArray) => (
112
+ <StackItem.Root
113
+ key={card.id}
114
+ item={card}
115
+ prevSiblingId={cardIndex > 0 ? cardsArray[cardIndex - 1].id : undefined}
116
+ nextSiblingId={cardIndex < cardsArray.length - 1 ? cardsArray[cardIndex + 1].id : undefined}
117
+ >
106
118
  <StackItem.Heading>
107
119
  <StackItem.ResizeHandle />
108
120
  </StackItem.Heading>
@@ -117,19 +129,19 @@ const StorybookStack = () => {
117
129
  );
118
130
  };
119
131
 
120
- type Story = StoryObj<typeof StorybookStack>;
121
-
122
- export const Default: Story = {
123
- args: {
124
- orientation: 'horizontal',
125
- },
126
- };
127
-
128
- const meta: Meta<typeof StorybookStack> = {
132
+ const meta: Meta<typeof DefaultStory> = {
129
133
  title: 'ui/react-ui-stack/Stack',
130
- component: StorybookStack,
134
+ component: DefaultStory,
131
135
  decorators: [withTheme],
132
136
  argTypes: { orientation: { control: 'radio', options: ['horizontal', 'vertical'] } },
133
137
  };
134
138
 
135
139
  export default meta;
140
+
141
+ type Story = StoryObj<typeof DefaultStory>;
142
+
143
+ export const Default: Story = {
144
+ args: {
145
+ orientation: 'horizontal',
146
+ },
147
+ };
@@ -4,19 +4,33 @@
4
4
 
5
5
  import { useArrowNavigationGroup } from '@fluentui/react-tabster';
6
6
  import { composeRefs } from '@radix-ui/react-compose-refs';
7
- import React, { Children, type CSSProperties, type ComponentPropsWithRef, forwardRef, useState, useMemo } from 'react';
7
+ import React, {
8
+ Children,
9
+ type CSSProperties,
10
+ type ComponentPropsWithRef,
11
+ forwardRef,
12
+ useState,
13
+ useMemo,
14
+ useCallback,
15
+ useEffect,
16
+ } from 'react';
8
17
 
9
18
  import { type ThemedClassName, ListItem } from '@dxos/react-ui';
10
19
  import { mx } from '@dxos/react-ui-theme';
11
20
 
12
- import { type StackContextValue, StackContext } from './StackContext';
13
- import { useStackDropForElements } from '../hooks';
21
+ import { useStackDropForElements } from '../../hooks';
22
+ import { StackContext } from '../StackContext';
23
+ import { type StackContextValue } from '../defs';
14
24
 
15
25
  export type Orientation = 'horizontal' | 'vertical';
16
26
  export type Size = 'intrinsic' | 'contain' | 'contain-fit-content';
17
27
 
18
28
  export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
19
- Partial<StackContextValue> & { itemsCount?: number };
29
+ Partial<StackContextValue> & {
30
+ itemsCount?: number;
31
+ getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
32
+ separatorOnScroll?: number;
33
+ };
20
34
 
21
35
  export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
22
36
  export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
@@ -40,6 +54,8 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
40
54
  size = 'intrinsic',
41
55
  onRearrange,
42
56
  itemsCount = Children.count(children),
57
+ getDropElement,
58
+ separatorOnScroll,
43
59
  ...props
44
60
  },
45
61
  forwardedRef,
@@ -58,12 +74,29 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
58
74
 
59
75
  const { dropping } = useStackDropForElements({
60
76
  id: props.id,
61
- element: stackElement,
77
+ element: getDropElement && stackElement ? getDropElement(stackElement) : stackElement,
78
+ scrollElement: stackElement,
62
79
  selfDroppable,
63
80
  orientation,
64
81
  onRearrange,
65
82
  });
66
83
 
84
+ const handleScroll = useCallback(() => {
85
+ if (stackElement && Number.isFinite(separatorOnScroll)) {
86
+ const scrollPosition = orientation === 'horizontal' ? stackElement.scrollLeft : stackElement.scrollTop;
87
+ const scrollSize = orientation === 'horizontal' ? stackElement.scrollWidth : stackElement.scrollHeight;
88
+ const clientSize = orientation === 'horizontal' ? stackElement.clientWidth : stackElement.clientHeight;
89
+ const separatorHost = stackElement.closest('[data-scroll-separator]');
90
+ if (separatorHost) {
91
+ separatorHost.setAttribute('data-scroll-separator', String(scrollPosition > separatorOnScroll!));
92
+ separatorHost.setAttribute(
93
+ 'data-scroll-separator-end',
94
+ String(scrollSize - (scrollPosition + clientSize) > separatorOnScroll!),
95
+ );
96
+ }
97
+ }
98
+ }, [stackElement, separatorOnScroll, orientation]);
99
+
67
100
  const gridClasses = useMemo(() => {
68
101
  if (!rail) {
69
102
  return orientation === 'horizontal' ? 'grid-rows-1 pli-1' : 'grid-cols-1 plb-1';
@@ -75,6 +108,22 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
75
108
  }
76
109
  }, [rail, orientation, size]);
77
110
 
111
+ useEffect(() => {
112
+ if (!(stackElement && Number.isFinite(separatorOnScroll))) {
113
+ return;
114
+ }
115
+
116
+ const observer = new MutationObserver(() => {
117
+ handleScroll();
118
+ });
119
+
120
+ observer.observe(stackElement, { childList: true, subtree: true });
121
+
122
+ return () => {
123
+ observer.disconnect();
124
+ };
125
+ }, [stackElement, handleScroll]);
126
+
78
127
  return (
79
128
  <StackContext.Provider value={{ orientation, rail, size, onRearrange }}>
80
129
  <div
@@ -93,6 +142,7 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
93
142
  aria-orientation={orientation}
94
143
  style={styles}
95
144
  ref={composedItemRef}
145
+ {...(Number.isFinite(separatorOnScroll) && { onScroll: handleScroll })}
96
146
  >
97
147
  {children}
98
148
  {selfDroppable && dropping && (
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './Stack';
@@ -2,22 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
6
5
  import { createContext, useContext } from 'react';
7
6
 
8
- import { type Size as DndSize } from '@dxos/react-ui-dnd';
9
-
10
7
  import { type Orientation, type Size } from './Stack';
11
-
12
- export type StackItemSize = DndSize;
13
-
14
- export type StackItemData = { id: string; type: 'column' | 'card' };
15
-
16
- export type StackItemRearrangeHandler<Data extends { id: string } = StackItemData> = (
17
- source: Data,
18
- target: Data,
19
- closestEdge: Edge | null,
20
- ) => void;
8
+ import { type StackItemSize, type StackItemRearrangeHandler } from './defs';
21
9
 
22
10
  export type StackContextValue = {
23
11
  orientation: Orientation;
@@ -34,16 +22,36 @@ export const StackContext = createContext<StackContextValue>({
34
22
 
35
23
  export const useStack = () => useContext(StackContext);
36
24
 
25
+ export type ItemDragState =
26
+ | {
27
+ type: 'idle';
28
+ }
29
+ | {
30
+ type: 'preview';
31
+ container: HTMLElement;
32
+ item: any;
33
+ }
34
+ | {
35
+ type: 'is-dragging';
36
+ item: any;
37
+ };
38
+
39
+ export const idle: ItemDragState = { type: 'idle' };
40
+
37
41
  export type StackItemContextValue = {
38
42
  selfDragHandleRef: (element: HTMLDivElement | null) => void;
39
43
  size: StackItemSize;
40
44
  setSize: (nextSize: StackItemSize, commit?: boolean) => void;
45
+ state: ItemDragState;
46
+ setState: (state: ItemDragState) => void;
41
47
  };
42
48
 
43
49
  export const StackItemContext = createContext<StackItemContextValue>({
44
50
  selfDragHandleRef: () => {},
45
51
  size: 'min-content',
46
52
  setSize: () => {},
53
+ state: idle,
54
+ setState: () => {},
47
55
  });
48
56
 
49
57
  export const useStackItem = () => useContext(StackItemContext);
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+ import React from 'react';
9
+
10
+ import { Icon, DropdownMenu } from '@dxos/react-ui';
11
+ import { withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { StackItem } from './StackItem';
14
+
15
+ const meta: Meta<typeof StackItem.Root> = {
16
+ title: 'ui/react-ui-stack/StackItem',
17
+ component: StackItem.Root,
18
+ render: (args) => (
19
+ <StackItem.Root role='section' {...args} classNames='w-[20rem] border border-separator'>
20
+ <StackItem.Heading>
21
+ <span className='sr-only'>Title</span>
22
+ <div role='none' className='sticky -block-start-px bg-[--sticky-bg] p-1 is-full'>
23
+ <DropdownMenu.Root>
24
+ <DropdownMenu.Trigger asChild>
25
+ <StackItem.SigilButton>
26
+ <Icon icon={'ph--dots-three--regular'} size={5} />
27
+ </StackItem.SigilButton>
28
+ </DropdownMenu.Trigger>
29
+ </DropdownMenu.Root>
30
+ </div>
31
+ </StackItem.Heading>
32
+ <StackItem.Content classNames='p-2'>Content</StackItem.Content>
33
+ </StackItem.Root>
34
+ ),
35
+ decorators: [withTheme],
36
+ parameters: {
37
+ layout: 'centered',
38
+ },
39
+ };
40
+
41
+ export default meta;
42
+
43
+ type Story = StoryObj<typeof StackItem.Root>;
44
+
45
+ export const Default: Story = {
46
+ args: {
47
+ item: { id: '1' },
48
+ },
49
+ };
@@ -5,7 +5,7 @@
5
5
  import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
6
6
  import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
7
7
  import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
8
- import { scrollJustEnoughIntoView } from '@atlaskit/pragmatic-drag-and-drop/element/scroll-just-enough-into-view';
8
+ import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
9
9
  import {
10
10
  attachClosestEdge,
11
11
  extractClosestEdge,
@@ -13,13 +13,20 @@ import {
13
13
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
14
14
  import { useFocusableGroup } from '@fluentui/react-tabster';
15
15
  import { composeRefs } from '@radix-ui/react-compose-refs';
16
- import React, { forwardRef, useLayoutEffect, useState, type ComponentPropsWithRef, useCallback } from 'react';
16
+ import React, {
17
+ forwardRef,
18
+ useLayoutEffect,
19
+ useState,
20
+ type ComponentPropsWithRef,
21
+ useCallback,
22
+ type ReactNode,
23
+ } from 'react';
24
+ import { createPortal } from 'react-dom';
17
25
 
18
26
  import { type ThemedClassName, ListItem } from '@dxos/react-ui';
19
27
  import { resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
20
28
  import { mx } from '@dxos/react-ui-theme';
21
29
 
22
- import { useStack, StackItemContext, type StackItemSize, type StackItemData } from './StackContext';
23
30
  import { StackItemContent, type StackItemContentProps } from './StackItemContent';
24
31
  import { StackItemDragHandle, type StackItemDragHandleProps } from './StackItemDragHandle';
25
32
  import {
@@ -36,15 +43,19 @@ import {
36
43
  type StackItemSigilButtonProps,
37
44
  StackItemSigilButton,
38
45
  } from './StackItemSigil';
46
+ import { useStack, StackItemContext, idle, type ItemDragState, useStackItem } from '../StackContext';
47
+ import { type StackItemSize, type StackItemData } from '../defs';
39
48
 
40
49
  // NOTE: 48rem fills the screen on a MacbookPro with the sidebars closed.
41
50
  export const DEFAULT_HORIZONTAL_SIZE = 48 satisfies StackItemSize;
42
51
  export const DEFAULT_VERTICAL_SIZE = 'min-content' satisfies StackItemSize;
43
52
  export const DEFAULT_EXTRINSIC_SIZE = DEFAULT_HORIZONTAL_SIZE satisfies StackItemSize;
44
53
 
45
- export type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
54
+ type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
46
55
  item: Omit<StackItemData, 'type'>;
47
56
  order?: number;
57
+ prevSiblingId?: string;
58
+ nextSiblingId?: string;
48
59
  size?: StackItemSize;
49
60
  onSizeChange?: (nextSize: StackItemSize) => void;
50
61
  role?: 'article' | 'section';
@@ -62,6 +73,8 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
62
73
  onSizeChange,
63
74
  role,
64
75
  order,
76
+ prevSiblingId,
77
+ nextSiblingId,
65
78
  style,
66
79
  disableRearrange,
67
80
  focusIndicatorVariant = 'over-all',
@@ -72,6 +85,8 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
72
85
  const [itemElement, itemRef] = useState<HTMLDivElement | null>(null);
73
86
  const [selfDragHandleElement, selfDragHandleRef] = useState<HTMLDivElement | null>(null);
74
87
  const [closestEdge, setEdge] = useState<Edge | null>(null);
88
+ const [sourceId, setSourceId] = useState<string | null>(null);
89
+ const [dragState, setDragState] = useState<ItemDragState>(idle);
75
90
  const { orientation, rail, onRearrange } = useStack();
76
91
  const [size = orientation === 'horizontal' ? DEFAULT_HORIZONTAL_SIZE : DEFAULT_VERTICAL_SIZE, setInternalSize] =
77
92
  useState(propsSize);
@@ -104,18 +119,28 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
104
119
  getInitialData: () => ({ id: item.id, type }),
105
120
  onGenerateDragPreview: ({ nativeSetDragImage, source, location }) => {
106
121
  document.body.setAttribute('data-drag-preview', 'true');
107
- scrollJustEnoughIntoView({ element: source.element });
108
- const { x, y } = preserveOffsetOnSource({ element: source.element, input: location.current.input })({
109
- container: (source.element.offsetParent ?? document.body) as HTMLElement,
122
+ const offsetFn = preserveOffsetOnSource({ element: source.element, input: location.current.input });
123
+ const rect = source.element.getBoundingClientRect();
124
+ setCustomNativeDragPreview({
125
+ nativeSetDragImage,
126
+ getOffset: ({ container }) => {
127
+ return offsetFn({ container });
128
+ },
129
+ render: ({ container }) => {
130
+ container.style.width = rect.width + 'px';
131
+ setDragState({ type: 'preview', container, item });
132
+ return () => {};
133
+ },
110
134
  });
111
- nativeSetDragImage?.(source.element, x, y);
112
135
  },
113
136
  onDragStart: () => {
114
137
  document.body.removeAttribute('data-drag-preview');
115
138
  itemElement?.closest('[data-drag-autoscroll]')?.setAttribute('data-drag-autoscroll', 'active');
139
+ setDragState({ type: 'is-dragging', item });
116
140
  },
117
141
  onDrop: () => {
118
142
  itemElement?.closest('[data-drag-autoscroll]')?.setAttribute('data-drag-autoscroll', 'idle');
143
+ setDragState(idle);
119
144
  },
120
145
  }),
121
146
  dropTargetForElements({
@@ -129,16 +154,22 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
129
154
  onDragEnter: ({ self, source }) => {
130
155
  if (source.data.type === self.data.type) {
131
156
  setEdge(extractClosestEdge(self.data));
157
+ setSourceId(source.data.id as string);
132
158
  }
133
159
  },
134
160
  onDrag: ({ self, source }) => {
135
161
  if (source.data.type === self.data.type) {
136
162
  setEdge(extractClosestEdge(self.data));
163
+ setSourceId(source.data.id as string);
137
164
  }
138
165
  },
139
- onDragLeave: () => setEdge(null),
166
+ onDragLeave: () => {
167
+ setEdge(null);
168
+ setSourceId(null);
169
+ },
140
170
  onDrop: ({ self, source }) => {
141
171
  setEdge(null);
172
+ setSourceId(null);
142
173
  if (source.data.type === self.data.type) {
143
174
  onRearrange(source.data as StackItemData, self.data as StackItemData, extractClosestEdge(self.data));
144
175
  }
@@ -149,8 +180,42 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
149
180
 
150
181
  const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
151
182
 
183
+ // Determine if the drop would result in any changes
184
+ const shouldShowDropIndicator = () => {
185
+ if (!closestEdge || !sourceId) {
186
+ return false;
187
+ }
188
+
189
+ // Don't show indicator when dragged item is over itself
190
+ if (sourceId === item.id) {
191
+ return false;
192
+ }
193
+
194
+ // Don't show indicator when dragged item is over the trailing edge of its previous sibling
195
+ const isTrailingEdgeOfPrevSibling =
196
+ prevSiblingId !== undefined &&
197
+ sourceId === prevSiblingId &&
198
+ ((orientation === 'horizontal' && closestEdge === 'left') ||
199
+ (orientation === 'vertical' && closestEdge === 'top'));
200
+ if (isTrailingEdgeOfPrevSibling) {
201
+ return false;
202
+ }
203
+
204
+ // Don't show indicator when dragged item is over the leading edge of its next sibling
205
+ const isLeadingEdgeOfNextSibling =
206
+ nextSiblingId !== undefined &&
207
+ sourceId === nextSiblingId &&
208
+ ((orientation === 'horizontal' && closestEdge === 'right') ||
209
+ (orientation === 'vertical' && closestEdge === 'bottom'));
210
+ if (isLeadingEdgeOfNextSibling) {
211
+ return false;
212
+ }
213
+
214
+ return true;
215
+ };
216
+
152
217
  return (
153
- <StackItemContext.Provider value={{ selfDragHandleRef, size, setSize }}>
218
+ <StackItemContext.Provider value={{ selfDragHandleRef, size, setSize, state: dragState, setState: setDragState }}>
154
219
  <Root
155
220
  {...props}
156
221
  tabIndex={0}
@@ -178,13 +243,24 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
178
243
  ref={composedItemRef}
179
244
  >
180
245
  {children}
181
- {closestEdge && <ListItem.DropIndicator lineInset={8} terminalInset={-8} edge={closestEdge} />}
246
+ {shouldShowDropIndicator() && closestEdge && (
247
+ <ListItem.DropIndicator lineInset={8} terminalInset={-8} edge={closestEdge} />
248
+ )}
182
249
  </Root>
183
250
  </StackItemContext.Provider>
184
251
  );
185
252
  },
186
253
  );
187
254
 
255
+ type StackItemDragPreviewProps = {
256
+ children: ({ item }: { item: any }) => ReactNode;
257
+ };
258
+
259
+ export const StackItemDragPreview = ({ children }: StackItemDragPreviewProps) => {
260
+ const { state } = useStackItem();
261
+ return state?.type === 'preview' ? createPortal(children({ item: state.item }), state.container) : null;
262
+ };
263
+
188
264
  export const StackItem = {
189
265
  Root: StackItemRoot,
190
266
  Content: StackItemContent,
@@ -194,9 +270,11 @@ export const StackItem = {
194
270
  DragHandle: StackItemDragHandle,
195
271
  Sigil: StackItemSigil,
196
272
  SigilButton: StackItemSigilButton,
273
+ DragPreview: StackItemDragPreview,
197
274
  };
198
275
 
199
276
  export type {
277
+ StackItemRootProps,
200
278
  StackItemContentProps,
201
279
  StackItemHeadingProps,
202
280
  StackItemHeadingLabelProps,
@@ -205,4 +283,5 @@ export type {
205
283
  StackItemSigilProps,
206
284
  StackItemSigilButtonProps,
207
285
  StackItemSigilAction,
286
+ StackItemDragPreviewProps,
208
287
  };
@@ -7,7 +7,7 @@ import React, { type ComponentPropsWithoutRef, forwardRef } from 'react';
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
8
  import { mx } from '@dxos/react-ui-theme';
9
9
 
10
- import { useStack } from './StackContext';
10
+ import { useStack } from '../StackContext';
11
11
 
12
12
  export type StackItemContentProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & {
13
13
  /**
@@ -53,6 +53,7 @@ export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps
53
53
  ...(statusbar ? ['var(--statusbar-size)'] : []),
54
54
  ].join(' '),
55
55
  }}
56
+ data-popover-collision-boundary={true}
56
57
  ref={forwardedRef}
57
58
  >
58
59
  {children}
@@ -5,9 +5,9 @@
5
5
  import { Slot } from '@radix-ui/react-slot';
6
6
  import React, { type ComponentPropsWithoutRef } from 'react';
7
7
 
8
- import { useStackItem } from './StackContext';
8
+ import { useStackItem } from '../StackContext';
9
9
 
10
- export type StackItemDragHandleProps = ComponentPropsWithoutRef<'button'> & { asChild: boolean };
10
+ export type StackItemDragHandleProps = ComponentPropsWithoutRef<'button'> & { asChild?: boolean };
11
11
 
12
12
  export const StackItemDragHandle = ({ asChild, children }: StackItemDragHandleProps) => {
13
13
  const { selfDragHandleRef } = useStackItem();
@@ -3,34 +3,38 @@
3
3
  //
4
4
 
5
5
  import { useFocusableGroup } from '@fluentui/react-tabster';
6
+ import { Slot } from '@radix-ui/react-slot';
6
7
  import React, { type ComponentPropsWithoutRef, type ComponentPropsWithRef, forwardRef } from 'react';
7
8
 
8
9
  import { type ThemedClassName } from '@dxos/react-ui';
9
10
  import { useAttention, type AttendableId, type Related } from '@dxos/react-ui-attention';
10
11
  import { mx } from '@dxos/react-ui-theme';
11
12
 
12
- import { useStack } from './StackContext';
13
+ import { useStack } from '../StackContext';
13
14
 
14
- export type StackItemHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
15
+ export type StackItemHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & { asChild?: boolean };
15
16
 
16
- export const StackItemHeading = ({ children, classNames, ...props }: StackItemHeadingProps) => {
17
+ export const StackItemHeading = ({ children, classNames, asChild, ...props }: StackItemHeadingProps) => {
17
18
  const { orientation } = useStack();
18
19
  const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
19
20
 
21
+ const Root = asChild ? Slot : 'div';
22
+
20
23
  return (
21
- <div
24
+ <Root
22
25
  role='heading'
23
26
  {...props}
24
27
  tabIndex={0}
25
28
  {...focusableGroupAttrs}
26
29
  className={mx(
27
- 'flex items-center dx-focus-ring-inset-over-all relative !border-is-0',
30
+ 'flex items-center dx-focus-ring-inset-over-all relative !border-is-0 bg-headerSurface border-transparent [[data-scroll-separator="true"]_&]:border-subduedSeparator',
28
31
  orientation === 'horizontal' ? 'bs-[--rail-size]' : 'is-[--rail-size] flex-col',
32
+ orientation === 'horizontal' ? 'border-be' : 'border-ie',
29
33
  classNames,
30
34
  )}
31
35
  >
32
36
  {children}
33
- </div>
37
+ </Root>
34
38
  );
35
39
  };
36
40
 
@@ -6,8 +6,8 @@ import React from 'react';
6
6
 
7
7
  import { ResizeHandle } from '@dxos/react-ui-dnd';
8
8
 
9
- import { useStack, useStackItem } from './StackContext';
10
9
  import { DEFAULT_EXTRINSIC_SIZE } from './StackItem';
10
+ import { useStack, useStackItem } from '../StackContext';
11
11
 
12
12
  const MIN_WIDTH = 20;
13
13
  const MIN_HEIGHT = 3;
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { Fragment, type PropsWithChildren, forwardRef, useRef, useState } from 'react';
5
+ import React, { Fragment, type PropsWithChildren, forwardRef, useState } from 'react';
6
6
 
7
7
  import { type ActionLike } from '@dxos/app-graph';
8
8
  import { keySymbols } from '@dxos/keyboard';
@@ -12,7 +12,7 @@ import { descriptionText, mx } from '@dxos/react-ui-theme';
12
12
  import { getHostPlatform } from '@dxos/util';
13
13
 
14
14
  import { MenuSignifierHorizontal } from './MenuSignifier';
15
- import { translationKey } from '../translations';
15
+ import { translationKey } from '../../translations';
16
16
 
17
17
  export type KeyBinding = {
18
18
  windows?: string;
@@ -62,7 +62,6 @@ export type StackItemSigilProps = PropsWithChildren<
62
62
  export const StackItemSigil = forwardRef<HTMLButtonElement, StackItemSigilProps>(
63
63
  ({ actions: actionGroups, onAction, triggerLabel, attendableId, icon, related, children }, forwardedRef) => {
64
64
  const { t } = useTranslation(translationKey);
65
- const suppressNextTooltip = useRef(false);
66
65
 
67
66
  const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
68
67
 
@@ -87,17 +86,7 @@ export const StackItemSigil = forwardRef<HTMLButtonElement, StackItemSigilProps>
87
86
  }
88
87
 
89
88
  return (
90
- <DropdownMenu.Root
91
- {...{
92
- open: optionsMenuOpen,
93
- onOpenChange: (nextOpen: boolean) => {
94
- if (!nextOpen) {
95
- suppressNextTooltip.current = true;
96
- }
97
- return setOptionsMenuOpen(nextOpen);
98
- },
99
- }}
100
- >
89
+ <DropdownMenu.Root open={optionsMenuOpen} onOpenChange={setOptionsMenuOpen}>
101
90
  <DropdownMenu.Trigger asChild ref={forwardedRef}>
102
91
  {button}
103
92
  </DropdownMenu.Trigger>
@@ -127,7 +116,6 @@ export const StackItemSigil = forwardRef<HTMLButtonElement, StackItemSigilProps>
127
116
  }
128
117
  event.stopPropagation();
129
118
  // TODO(thure): Why does Dialog’s modal-ness cause issues if we don’t explicitly close the menu here?
130
- suppressNextTooltip.current = true;
131
119
  setOptionsMenuOpen(false);
132
120
  onAction?.(action);
133
121
  }}
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './StackItem';